class Detector: changed = Signal(WebPageChangeEvent) def __init__(self, url, delay): self.url = url self.delay = delay async def run(self): with aiohttp.ClientSession() as session: last_modified, old_lines = None, None while True: logger.debug('Fetching contents of %s', self.url) headers = { 'if-modified-since': last_modified } if last_modified else {} async with session.get(self.url, headers=headers) as resp: logger.debug('Response status: %d', resp.status) if resp.status == 200: last_modified = resp.headers['date'] new_lines = (await resp.text()).split('\n') if old_lines is not None and old_lines != new_lines: self.changed.dispatch(old_lines, new_lines) old_lines = new_lines await asyncio.sleep(self.delay)
def test_class_attribute_access(self): """ Test that accessing the descriptor on the class level returns the same signal instance. """ signal = Signal(DummyEvent) class EventSource: dummysignal = signal assert EventSource.dummysignal is signal
class FeedReader(metaclass=ABCMeta): """ Interface for feed readers. :var entry_discovered: a signal dispatched when a resource has been published in this context :vartype entry_discovered: Signal[EntryEvent] :var metadata_changed: a signal dispatched when the feed metadata has been changed :vartype metadata_changed: Signal[MetadataEvent] :ivar str url: the feed URL """ entry_discovered = Signal(EntryEvent) metadata_changed = Signal(MetadataEvent) url = None # type: str @abstractmethod def start(self, ctx: Context) -> Awaitable[None]: """ Initialize the feed. This should do the following: * Claim any required resources (including the state store if defined) * Load the state from the store (if a store was defined) * Conditionally start a timer task that calls :meth:`~.update` on the configured intervals """ @abstractmethod def __getstate__(self) -> Dict[str, Any]: """ Return persistable state of the feed. The returned structure must be JSON compatible. """ @abstractmethod def __setstate__(self, state: Dict[str, Any]) -> None: """Apply previously saved state to this feed.""" @property @abstractmethod def metadata(self) -> FeedMetadata: """Return the feed's metadata.""" @abstractmethod def update(self) -> Awaitable[None]: """Read the feed from the source and dispatch any events necessary.""" @classmethod def can_parse(cls, document: str, content_type: str) -> Optional[str]: """ Determine if this reader class is suitable for parsing the given document as a feed. This method is only used for autodetection of feed type by :func:`~asphalt.feedreader.component.create_feed` (ie. when the feed parser has not been specified). Autodetection is skipped when the feed parser has been explicitly given. :param document: document loaded from the feed URL :param content_type: MIME type of the loaded document :return: the reason why this class cannot parse the given document, or ``None`` if it can parse it """ return 'Autodetection not implemented for this class'
class WAMPClient: """ A WAMP client. :ivar Signal realm_joined: a signal (:class:`~asphalt.wamp.events.SessionJoinEvent`) dispatched when the client has joined the realm and has registered any procedures and subscribers on the router :ivar Signal realm_left: a signal (:class:`~asphalt.wamp.events.SessionLeaveEvent`) dispatched when the client has left the realm :ivar str realm: the WAMP realm :ivar str url: the WAMP URL :ivar WAMPRegistry register: the root registry object """ realm_joined = Signal(SessionJoinEvent) realm_left = Signal(SessionLeaveEvent) def __init__(self, host: str = 'localhost', port: int = 8080, path: str = '/', realm: str = 'default', *, reconnect_delay: int = 5, max_reconnection_attempts: int = None, registry: Union[WAMPRegistry, str] = None, ssl: Union[bool, str, SSLContext] = False, serializer: Union[Serializer, str] = None, auth_method: str = 'anonymous', auth_id: str = None, auth_secret: str = None): """ The following parameters are also available as instance attributes: :param host: host address of the WAMP router :param port: port to connect to :param path: HTTP path on the router :param realm: the WAMP realm to join the application session to (defaults to the resource name if not specified) :param reconnect_delay: delay between connection attempts (in seconds) :param max_reconnection_attempts: maximum number of connection attempts before giving up :param registry: a WAMP registry or a string reference to one (defaults to creating a new instance if omitted) :param ssl: one of the following: * ``False`` to disable SSL * ``True`` to enable SSL using the default context * an :class:`ssl.SSLContext` instance * a ``module:varname`` reference to an :class:`~ssl.SSLContext` instance * name of an :class:`ssl.SSLContext` resource :param serializer: a serializer instance or the name of a :class:`asphalt.serialization.api.Serializer` resource (defaults to creating a new :class:`~asphalt.serialization.json.JSONSerializer` if omitted) :param auth_method: authentication method to use (valid values are currently ``anonymous``, ``wampcra`` and ``ticket``) :param auth_id: authentication ID (username) :param auth_secret: secret to use for authentication (ticket or password) """ assert check_argument_types() self.host = host self.port = port self.path = path self.reconnect_delay = reconnect_delay self.max_reconnection_attempts = max_reconnection_attempts self.realm = realm self.registry = resolve_reference(registry) or WAMPRegistry() self.ssl = resolve_reference(ssl) self.serializer = resolve_reference(serializer) or JSONSerializer() self.auth_method = auth_method self.auth_id = auth_id self.auth_secret = auth_secret self._context = None self._session = None # type: AsphaltSession self._session_details = None # type: SessionDetails self._connect_task = None # type: Task async def _register(self, procedure: Procedure): async def wrapper(*args, _call_details: CallDetails, **kwargs): async with CallContext(self._context, self._session_details, _call_details) as ctx: retval = procedure.handler(ctx, *args, **kwargs) if isawaitable(retval): retval = await retval return retval options = RegisterOptions(details_arg='_call_details', **procedure.options) return await self._session.register(wrapper, procedure.name, options) async def _subscribe(self, subscriber: Subscriber): async def wrapper(*args, _event_details: EventDetails, **kwargs): async with EventContext(self._context, self._session_details, _event_details) as ctx: retval = subscriber.handler(ctx, *args, **kwargs) if isawaitable(retval): await retval options = SubscribeOptions(details_arg='_event_details', **subscriber.options) await self._session.subscribe(wrapper, subscriber.topic, options) async def start(self, ctx: Context): self._context = ctx if isinstance(self.ssl, str): self.ssl = await ctx.request_resource(SSLContext, self.ssl) if isinstance(self.registry, str): self.registry = await ctx.request_resource(WAMPRegistry, self.registry) if isinstance(self.serializer, str): self.serializer = await ctx.request_resource( Serializer, self.serializer) async def map_exception(self, exc_class: type, error: str) -> None: """ Map a Python exception to a WAMP error. :param exc_class: an exception class :param error: the WAMP error code """ self.registry.map_exception(exc_class, error) if self._session is None: await self.connect() # this will automatically map the exception else: self._session.define(exc_class, error) async def register_procedure(self, handler: Callable, name: str, **options) -> None: """ Add a procedure handler to the registry and attempts to register it on the router. :param handler: callable that handles calls for the given endpoint :param name: name of the endpoint to register (e.g. ``x.y.z``) :param options: extra keyword arguments to pass to :meth:`~.registry.WAMPRegistry.add_procedure` """ assert check_argument_types() procedure = self.registry.add_procedure(handler, name, **options) if self._session is None: await self.connect( ) # this will automatically register the procedure else: await self._register(procedure) async def subscribe(self, handler: Callable, topic: str, **options) -> None: """ Add a WAMP event subscriber to the registry and attempts to register it on the router. :param handler: the callable that is called when a message arrives :param topic: topic to subscribe to :param options: extra keyword arguments to pass to :meth:`~.registry.WAMPRegistry.add_subscriber` """ assert check_argument_types() subscriber = self.registry.add_subscriber(handler, topic, **options) if self._session is None: await self.connect( ) # this will automatically register the subscriber else: await self._subscribe(subscriber) async def publish(self, topic: str, *args, acknowledge: bool = False, exclude_me: bool = None, exclude: Iterable[int] = None, eligible_sessions: Iterable[int] = None, **kwargs) -> Optional[int]: """ Publish an event on the given topic. :param topic: the topic to publish on :param args: positional arguments to pass to subscribers :param acknowledge: ``True`` to wait until the router has acknowledged the event :param exclude_me: ``False`` to have the router also send the event back to the sender if it has any matching subscriptions :param exclude: iterable of WAMP session IDs to exclude from receiving this event :param eligible_sessions: list of WAMP session IDs eligible to receive this event :param kwargs: keyword arguments to pass to subscribers :return: publication ID (with ``acknowledge=True``) """ assert check_argument_types() if self._session is None: await self.connect() kwargs['options'] = PublishOptions( acknowledge=acknowledge, exclude_me=exclude_me, exclude=list(exclude) if exclude else None, eligible=list(eligible_sessions) if eligible_sessions else None) retval = self._session.publish(topic, *args, **kwargs) if acknowledge: publication = await retval return publication.id async def call(self, endpoint: str, *args, on_progress: Callable[..., None] = None, timeout: int = None, **kwargs): """ Call an RPC function. :param endpoint: name of the endpoint to call :param args: positional arguments to call the endpoint with :param on_progress: a callable that will receive progress reports from the endpoint :param timeout: timeout (in seconds) to wait for the completion of the call :param kwargs: keyword arguments to call the endpoint with :return: the return value of the call :raises TimeoutError: if the call times out """ assert check_argument_types() if self._session is None: await self.connect() options = CallOptions(on_progress=on_progress, timeout=timeout) return await self._session.call(endpoint, *args, options=options, **kwargs) def connect(self) -> Awaitable[None]: """ Connect to the WAMP router and join the designated realm. When the realm is successfully joined, exceptions, procedures and event subscriptions from the registry are automatically registered with the router. The connection process is restarted if connection, joining the realm or registering the exceptions/procedures/subscriptions fails. If ``max_connection_attempts`` is set, it will limit the number of attempts. If this limit is reached, the awaitable gets the last exception set to it. Otherwise, the process is repeated indefinitely until it succeeds. If the realm has already been joined, the awaitable completes instantly. """ def _leave_callback(session: AsphaltSession, leave_details: CloseDetails): self._session = self._session_details = self._connect_task = None self.realm_left.dispatch(leave_details) if leave_details.reason == CloseDetails.REASON_TRANSPORT_LOST: logger.debug('Connection lost; reconnecting') self.connect() async def do_connect() -> None: proto = 'wss' if self.ssl else 'ws' url = '{proto}://{self.host}:{self.port}{self.path}'.format( proto=proto, self=self) logger.debug('Connecting to %s:%d (ssl=%s)', self.host, self.port, bool(self.ssl)) serializers = [wrap_serializer(self.serializer)] loop = txaio.config.loop = get_event_loop() transport = None attempts = 0 while self._session is None: attempts += 1 try: join_future = Future() session_factory = partial(AsphaltSession, self.realm, self.auth_method, self.auth_id, self.auth_secret, join_future) transport_factory = WampWebSocketClientFactory( session_factory, url=url, serializers=serializers, loop=loop) transport, protocol = await loop.create_connection( transport_factory, self.host, self.port, ssl=self.ssl) # Connection established; wait for the session to join the realm logger.debug('Connected; attempting to join realm %s', self.realm) self._session_details, self._session = await wait_for( join_future, timeout=5, loop=loop) # Register exception mappings with the session logger.debug( 'Realm joined; registering exceptions, subscriptions and procedures' ) for error, exc_type in self.registry.exceptions.items(): self._session.define(exc_type, error) # Register procedures and subscribers with the session tasks = [ loop.create_task(self._subscribe(subscriber)) for subscriber in self.registry.subscriptions ] tasks += [ loop.create_task(self._register(procedure)) for procedure in self.registry.procedures.values() ] if tasks: done, not_done = await wait( tasks, loop=loop, timeout=10, return_when=FIRST_EXCEPTION) for task in done: if task.exception(): raise task.exception() except CancelledError: raise except Exception as e: if transport: transport.close() self._session = self._session_details = transport = None if (self.max_reconnection_attempts is not None and attempts > self.max_reconnection_attempts): raise logger.info( 'Connection failed (attempt %d): %s(%s); reconnecting in %d ' 'seconds', attempts, e.__class__.__name__, e, self.reconnect_delay) await sleep(self.reconnect_delay) self._session.on('leave', _leave_callback) # Notify listeners that we've joined the realm self.realm_joined.dispatch(self._session_details) logger.debug('Registration complete') # Start a new connection attempt only if not connected and there is no attempt in progress if not self._connect_task: self._connect_task = get_event_loop().create_task(do_connect()) return self._connect_task async def disconnect(self) -> None: """ Leave the WAMP realm and disconnect from the server. If a connection attempt is in progress, it is cancelled. Does nothing if not connected to a server. """ if self._session: await self._session.leave() elif self._connect_task and not self._connect_task.done(): self._connect_task.cancel() self._connect_task = None @property def session_id(self) -> Optional[int]: """ Return the current WAMP session ID. :return: the session ID or ``None`` if not in a session. """ return self._session_details.session if self._session_details else None
class SignalOwner: dummy = Signal(Event)
class DummySource: event_a = Signal(DummyEvent) event_b = Signal(DummyEvent)