async def test_sentry_extras_provider_procedure(self, wampclient: WAMPClient, context: Context, monkeypatch): class DummyReporter(ExceptionReporter): def report_exception(self, ctx: Context, exception: BaseException, message: str, extra: Dict[str, Any]) -> None: errors.append((exception, message, extra)) def handler(ctx): raise Exception('foo') errors = [] context.add_resource(DummyReporter(), types=[ExceptionReporter]) context.add_resource(WAMPExtrasProvider(), types=[ExtrasProvider]) await wampclient.register(handler, 'dummyprocedure') monkeypatch.setattr('asphalt.wamp.extras_providers.SENTRY_CLASS_NAME', qualified_name(DummyReporter)) with pytest.raises(ApplicationError): await wampclient.call('dummyprocedure') assert len(errors) == 1 exc, message, extra = errors[0] assert type(exc) is Exception assert str(exc) == 'foo' assert message == "Error running handler for procedure 'dummyprocedure'" assert extra == {'extra': {'procedure': 'dummyprocedure'}, 'user_context': {'auth_role': 'authorized_users', 'id': 'device1', 'session_id': wampclient.session_id} }
async def test_report_applicationerror(self, wampclient: WAMPClient, context: Context, custom_exception): class DummyReporter(ExceptionReporter): def report_exception(self, ctx: Context, exception: BaseException, message: str, extra: Dict[str, Any]) -> None: errors.append((exception, message, extra)) class CustomError(Exception): pass def handler(ctx): if custom_exception: raise CustomError else: raise ApplicationError('dummy.error') errors = [] context.add_resource(DummyReporter(), types=[ExceptionReporter]) wampclient.map_exception(CustomError, 'dummy.error') await wampclient.register(handler, 'dummyprocedure') with pytest.raises(CustomError): await wampclient.call('dummyprocedure') assert not errors
async def test_sentry_extras_provider_subscriber(self, wampclient: WAMPClient, context: Context, monkeypatch): class DummyReporter(ExceptionReporter): def report_exception(self, ctx: Context, exception: BaseException, message: str, extra: Dict[str, Any]) -> None: errors.append((exception, message, extra)) def handler(ctx): ctx.loop.call_soon(event.set) raise Exception('foo') event = asyncio.Event() errors = [] context.add_resource(DummyReporter(), types=[ExceptionReporter]) context.add_resource(WAMPExtrasProvider(), types=[ExtrasProvider]) await wampclient.subscribe(handler, 'dummytopic') monkeypatch.setattr('asphalt.wamp.extras_providers.SENTRY_CLASS_NAME', qualified_name(DummyReporter)) await wampclient.publish('dummytopic', options=dict(acknowledge=True, exclude_me=False)) await event.wait() assert len(errors) == 1 exc, message, extra = errors[0] assert type(exc) is Exception assert str(exc) == 'foo' assert message == "Error running subscription handler for topic 'dummytopic'" assert extra == {'extra': {'topic': 'dummytopic'}, 'user_context': {'auth_role': 'authorized_users', 'id': 'device1', 'session_id': wampclient.session_id} }
def test_get_resources(self, context): context.add_resource(9, 'foo') context.add_resource_factory(lambda ctx: len(ctx.context_chain), int, 'bar') context.require_resource(int, 'bar') subctx = Context(context) subctx.add_resource(4, 'foo') assert subctx.get_resources(int) == {1, 4}
async def start(self, ctx: Context): for resource_name, context_attr, client_args in self.clients: client = AsyncIOMotorClient(**client_args) ctx.finished.connect( partial(self.shutdown_client, client=client, resource_name=resource_name)) ctx.publish_resource(client, resource_name, context_attr, types=[AsyncIOMotorClient]) logger.info('Configured MongoDB client (%s / ctx.%s; host=%r)', resource_name, context_attr, client_args.get('host', 'localhost'))
async def start(self, ctx: Context): for resource_name, context_attr, serializer in self.serializers: types = [Serializer, type(serializer)] if isinstance(serializer, CustomizableSerializer): types.append(CustomizableSerializer) ctx.add_resource(serializer, resource_name, context_attr, types=types) logger.info('Configured serializer (%s / ctx.%s; type=%s)', resource_name, context_attr, serializer.mimetype)
async def start(self, ctx: Context): proxymaker = partial(TemplateRendererProxy, renderer=self.renderer) types = [TemplateRenderer, type(self.renderer)] ctx.add_resource_factory(proxymaker, types, self.resource_name) logger.info( "Configured template renderer (%s; class=%s)", self.resource_name, qualified_name(self.renderer), )
async def start(self, ctx: Context): for resource_name, context_attr, client in self.clients: ctx.add_resource(client, resource_name, context_attr) logger.info('Configured Memcached client (%s / ctx.%s)', resource_name, context_attr) await yield_() for resource_name, context_attr, client in self.clients: client.close() logger.info('Memcached client (%s) shut down', resource_name)
async def test_add_person(self, dbsession, root_component): # Simulate adding a row to the "people" table in the application async with Context() as root_ctx: await root_component.start(root_ctx) async with Context() as ctx: session = ctx.require_resource(Session) session.add(Person(name="Another person")) # The testing code should see both rows now assert dbsession.scalar(func.count(Person.id)) == 2
async def test_delete_person(self, dbsession, root_component): # Simulate removing the test person in the application async with Context() as root_ctx: await root_component.start(root_ctx) async with Context() as ctx: session = ctx.require_resource(Session) session.execute(delete(Person)) # The testing code should not see any rows now assert dbsession.scalar(func.count(Person.id)) == 0
async def start(self, ctx: Context): if isinstance(self.tls_context, str): self.tls_context = await ctx.request_resource( SSLContext, self.tls_context) self._smtp = SMTP(hostname=self.host, port=self.port, tls_context=self.tls_context, loop=ctx.loop, timeout=self.timeout) ctx.add_teardown_callback(self._smtp.close)
async def test_request_resource_parent_add(self, context, event_loop): """ Test that adding a resource to the parent context will satisfy a resource request in a child context. """ child_context = Context(context) task = event_loop.create_task(child_context.request_resource(int)) event_loop.call_soon(context.add_resource, 6) resource = await task assert resource == 6
def create_session(self, ctx: Context, factory: sessionmaker) -> Session: async def teardown_session(exception: Optional[BaseException]) -> None: try: if exception is None and session.is_active: await call_in_executor(session.commit, executor=self.commit_executor) finally: del session.info['ctx'] session.close() session = factory(info={'ctx': ctx}) ctx.add_teardown_callback(teardown_session, pass_exception=True) return session
async def start(self, ctx: Context): for resource_name, context_attr, mailer in self.mailers: await mailer.start(ctx) ctx.add_resource(mailer, resource_name, context_attr, [Mailer, type(mailer)]) logger.info('Configured mailer (%s / ctx.%s; class=%s)', resource_name, context_attr, mailer.__class__.__name__) await yield_() for resource_name, context_attr, mailer in self.mailers: logger.info('Mailer (%s) stopped', resource_name)
async def start(self, ctx: Context): for resource_name, context_attr, client in self.clients: await client.start(ctx) ctx.add_resource(client, resource_name, context_attr) logger.info( 'Configured InfluxDB client (%s / ctx.%s; base_urls=%r)', resource_name, context_attr, client.base_urls) await yield_() for resource_name, context_attr, client in self.clients: logger.info('InfluxDB client (%s) shut down', resource_name)
async def test_generate_value(self, thread, context_attr): container = ResourceContainer(lambda ctx: 'foo', (str,), 'default', context_attr, True) context = Context() if thread: value = await context.call_in_executor(container.generate_value, context) else: value = container.generate_value(context) assert value == 'foo' assert context.get_resource(str) == 'foo' if context_attr: assert getattr(context, context_attr) == 'foo'
async def start(self, ctx: Context): for resource_name, context_attr, server in self.servers: await server.start(ctx) ctx.add_resource(server, resource_name, context_attr, BaseWebServer) logger.info('Started %s (%s / ctx.%s)', server.__class__.__name__, resource_name, context_attr) await yield_() for resource_name, context_attr, server in self.servers: await server.shutdown() logger.info('Shut down %s (%s)', server.__class__.__name__, resource_name)
async def start(self, ctx: Context) -> None: """Resolve Asphalt resource references.""" if self._session is None: def close_session(): self._session.close() self._session = ClientSession() ctx.add_teardown_callback(close_session) elif isinstance(self._session, str): self._session = await ctx.request_resource(ClientSession, self._session)
async def start(self, ctx: Context): for resource_name, store in self.stores: await store.start(ctx) ctx.add_resource(store, resource_name) logger.info('Configured feed state store (%s; class=%s)', resource_name, qualified_name(store)) for resource_name, context_attr, config in self.feeds: feed = await create_feed(ctx, **config) ctx.add_resource(feed, resource_name, context_attr, types=[type(feed), FeedReader]) logger.info('Configured feed (%s / ctx.%s; url=%s)', resource_name, context_attr, feed.url)
async def start(self, ctx: Context): for (resource_name, context_attr, launch_jvm, gateway_params, callback_server_params, classpath, javaopts) in self.gateways: if launch_jvm: gateway_params.port = launch_gateway(classpath=classpath, javaopts=javaopts) gateway = JavaGateway(gateway_parameters=gateway_params, callback_server_parameters=callback_server_params) ctx.finished.connect(partial(self.shutdown_gateway, gateway=gateway, resource_name=resource_name, shutdown_jvm=launch_jvm)) ctx.publish_resource(gateway, resource_name, context_attr) logger.info('Configured Py4J gateway (%s / ctx.%s; address=%s, port=%d)', resource_name, context_attr, gateway_params.address, gateway_params.port)
async def start(self, ctx: Context): # Autobahn uses txaio to bridge the API gap between asyncio and Twisted so we need to set # it up for asyncio here txaio.use_asyncio() txaio.config.loop = get_event_loop() for resource_name, context_attr, client in self.clients: await client.start(ctx) ctx.publish_resource(client, resource_name, context_attr) logger.info( 'Configured WAMP client (%s / ctx.%s; host=%s; port=%d; realm=%s)', resource_name, context_attr, client.host, client.port, client.realm)
async def start(self, ctx: Context): if isinstance(self.commit_executor, str): self.commit_executor = await ctx.request_resource(Executor, self.commit_executor) elif self.commit_executor is None: self.commit_executor = ThreadPoolExecutor(self.commit_executor_workers) ctx.add_teardown_callback(self.commit_executor.shutdown) for resource_name, context_attr, bind, factory, ready_callback in self.session_factories: if ready_callback: retval = ready_callback(bind, factory) if isawaitable(retval): await retval engine = bind if isinstance(bind, Engine) else bind.engine ctx.add_resource(engine, resource_name) ctx.add_resource(factory, resource_name) ctx.add_resource_factory(partial(self.create_session, factory=factory), [Session], resource_name, context_attr) logger.info('Configured SQLAlchemy session maker (%s / ctx.%s; dialect=%s)', resource_name, context_attr, bind.dialect.name) await yield_() for resource_name, context_attr, bind, factory, ready_callback in self.session_factories: if isinstance(bind, Engine): bind.dispose() logger.info('SQLAlchemy session maker (%s / ctx.%s) shut down', resource_name, context_attr)
def create_async_session(self, ctx: Context) -> AsyncSession: async def teardown_session(exception: BaseException | None) -> None: try: if session.in_transaction(): if exception is None: await session.commit() else: await session.rollback() finally: await session.close() session: AsyncSession = self.sessionmaker() ctx.add_teardown_callback(teardown_session, pass_exception=True) return session
async def start(self, ip: str = "127.0.0.1", port: int = 4444, *, component=None, base_context: Context = None): """ Runs the Kyoukai component asynchronously. This will bypass Asphalt's default runner, and allow you to run your app easily inside something else, for example. :param ip: The IP of the built-in server. :param port: The port of the built-in server. :param component: The component to start the app with. This should be an instance of \ :class:`~.KyoukaiComponent`. :param base_context: The base context that the HTTPRequestContext should be started with. """ if not base_context: base_context = Context() if not component: from kyoukai.asphalt import KyoukaiComponent self.component = KyoukaiComponent(self, ip, port) else: self.component = component # Start the app. await self.component.start(base_context)
async def start(self, ctx: Context): # Need either msgpack or CBOR to serialize binary messages (such as raw file chunks) self.add_component('wamp', serializer=CBORSerializer()) await super().start(ctx) ctx.base_path = Path(sys.argv[1]) await ctx.wamp.register(send_file, 'send_file')
async def test_multiple_connections(caplog): """Test that a multiple connection configuration works as intended.""" async with Context() as context: await MemcachedComponent( clients={ 'mc1': { 'host': '1.2.3.4', 'port': 11212 }, 'mc2': { 'host': '127.0.0.2' } }).start(context) assert isinstance(context.mc1, Client) assert isinstance(context.mc2, Client) records = [ record for record in caplog.records if record.name == 'asphalt.memcached.component' ] records.sort(key=lambda r: r.message) assert len(records) == 4 assert records[0].message == 'Configured Memcached client (mc1 / ctx.mc1)' assert records[1].message == 'Configured Memcached client (mc2 / ctx.mc2)' assert records[2].message == 'Memcached client (mc1) shut down' assert records[3].message == 'Memcached client (mc2) shut down'
async def test_multiple_clients(caplog): """Test that a multiple connection configuration works as intended.""" async with Context() as context: await MongoDBComponent( clients={ 'db1': { 'host': 'localhost:27018' }, 'db2': { 'host': '/tmp/mongodb.sock', 'ssl': True } }).start(context) assert isinstance(context.db1, AsyncIOMotorClient) assert isinstance(context.db2, AsyncIOMotorClient) records = [ record for record in caplog.records if record.name == 'asphalt.mongodb.component' ] records.sort(key=lambda r: r.message) assert len(records) == 4 assert records[0].message == ("Configured MongoDB client (db1 / ctx.db1; " "hosts={'localhost:27018'})") assert records[1].message == ("Configured MongoDB client (db2 / ctx.db2; " "hosts={'/tmp/mongodb.sock'})") assert records[2].message == 'MongoDB client (db1) shut down' assert records[3].message == 'MongoDB client (db2) shut down'
async def start(self, ctx: Context): for resource_name, context_attr, config in self.clients: # Resolve resource references if isinstance(config["ssl"], str): config["ssl"] = await ctx.request_resource(SSLContext, config["ssl"]) redis = await create_reconnecting_redis(**config) ctx.finished.connect(partial(self.shutdown_client, redis=redis, resource_name=resource_name)) ctx.publish_resource(redis, resource_name, context_attr) logger.info( "Configured Redis client (%s / ctx.%s; address=%s, db=%d)", resource_name, context_attr, config["address"], config["db"], )
async def test_multiple_clients(caplog): """Test that a multiple client configuration works as expected.""" async with Context() as context: await InfluxDBComponent( clients={ 'db1': { 'base_urls': 'http://localhost:9999' }, 'db2': { 'base_urls': 'https://remotehost.example.org:443/influx' } }).start(context) assert isinstance(context.db1, InfluxDBClient) assert isinstance(context.db2, InfluxDBClient) records = [ record for record in caplog.records if record.name == 'asphalt.influxdb.component' ] records.sort(key=lambda r: r.message) assert len(records) == 4 assert records[0].message == ("Configured InfluxDB client (db1 / ctx.db1; " "base_urls=['http://localhost:9999'])") assert records[1].message == ( "Configured InfluxDB client (db2 / ctx.db2; " "base_urls=['https://remotehost.example.org:443/influx'])") assert records[2].message == 'InfluxDB client (db1) shut down' assert records[3].message == 'InfluxDB client (db2) shut down'
def test_getattr_parent(self, context): """ Test that accessing a nonexistent attribute on a context retrieves the value from parent. """ child_context = Context(context) context.a = 2 assert child_context.a == 2
async def start(self, ctx: Context) -> None: if isinstance(self.store, str): self.store = await ctx.request_resource(FeedStateStore, self.store) if isinstance(self.session, str): self.session = await ctx.request_resource(ClientSession, self.session) elif self.session is None: self.session = ClientSession() ctx.add_teardown_callback(self.session.close) if self.store is not None: state = await self.store.load_state(self.state_id) self.__setstate__(state) if self.interval: loop_task = ctx.loop.create_task(self.loop_update()) ctx.add_teardown_callback(loop_task.cancel)
async def test_missing_yield(self): @context_teardown async def start(ctx: Context): pass with pytest.raises(RuntimeError) as exc_info: await start(Context()) exc_info.match(' did not do "await yield_\(\)"$')
def create_session(self, ctx: Context) -> Session: async def teardown_session(exception: BaseException | None) -> None: try: if session.in_transaction(): context = copy_context() if exception is None: await get_running_loop().run_in_executor( self.commit_executor, context.run, session.commit) else: await get_running_loop().run_in_executor( self.commit_executor, context.run, session.rollback) finally: session.close() session = self.sessionmaker() ctx.add_teardown_callback(teardown_session, pass_exception=True) return session
async def start(self, ctx: Context): # Autobahn uses txaio to bridge the API gap between asyncio and Twisted so we need to set # it up for asyncio here txaio.use_asyncio() txaio.config.loop = ctx.loop ctx.add_resource(WAMPExtrasProvider(), 'wamp', types=[ExtrasProvider]) for resource_name, context_attr, client in self.clients: await client.start(ctx) ctx.add_resource(client, resource_name, context_attr) logger.info('Configured WAMP client (%s / ctx.%s; host=%s; port=%d; realm=%s)', resource_name, context_attr, client.host, client.port, client.realm) await yield_() for resource_name, context_attr, client in self.clients: await client.stop() logger.info('Shut down WAMP client (%s)', resource_name)
async def test_exception(self): @context_teardown async def start(ctx): raise Exception('dummy error') context = Context() with pytest.raises(Exception) as exc_info: await start(context) exc_info.match('dummy error')
async def test_rollback(self, dbsession, root_component): # Simulate a rollback happening in a subcontext async with Context() as root_ctx: await root_component.start(root_ctx) async with Context() as ctx: session = ctx.require_resource(Session) try: # No value for a non-nullable column => IntegrityError! session.add(Person()) session.flush() except IntegrityError: # Without the session listener, this row would now be inserted # outside a SAVEPOINT, breaking test isolation session.rollback() session.add(Person(name="Works now!")) session.flush() # The context is gone, but the extra Person should still be around assert dbsession.scalar(func.count(Person.id)) == 2
async def test_add_resource_factory_no_inherit(self, context): """ Test that a subcontext gets its own version of a factory-generated resource even if a parent context has one already. """ context.add_resource_factory(id, int, context_attr='foo') subcontext = Context(context) assert context.foo == id(context) assert subcontext.foo == id(subcontext)
async def run(self, ctx: Context): async with ctx.threadpool(): num_rows = 0 with self.csv_path.open() as csvfile: reader = csv.reader(csvfile, delimiter='|') for name, city, phone, email in reader: num_rows += 1 ctx.sql.execute( people.insert().values(name=name, city=city, phone=phone, email=email)) logger.info('Imported %d rows of data', num_rows)
async def start(self, ctx: Context) -> AsyncGenerator[None, Exception | None]: if self.ready_callback: retval = self.ready_callback(self.bind, self.sessionmaker) if isawaitable(retval): await retval ctx.add_resource(self.engine, self.resource_name) ctx.add_resource(self.sessionmaker, self.resource_name) if isinstance(self.engine, AsyncEngine): ctx.add_resource_factory( self.create_async_session, [AsyncSession], self.resource_name, ) else: self.commit_executor = ThreadPoolExecutor( self.commit_executor_workers) ctx.add_teardown_callback(self.commit_executor.shutdown) ctx.add_resource_factory( self.create_session, [Session], self.resource_name, ) logger.info( "Configured SQLAlchemy resources (%s; dialect=%s, driver=%s)", self.resource_name, self.bind.dialect.name, self.bind.dialect.driver, ) yield if isinstance(self.bind, Engine): self.bind.dispose() elif isinstance(self.bind, AsyncEngine): await self.bind.dispose() logger.info("SQLAlchemy resources (%s) shut down", self.resource_name)
def __init__(self, app: Kyoukai, base_context: Context = None): """ :param app: The current application to run. :param base_context: The base context which is used as the parent to the \ HTTPRequestContext created. """ self.app = app self._base_context = base_context if not self._base_context: # Create an empty context as the default base context. self._base_context = Context()
def get_protocol(self, ctx: Context, serv_info: tuple): """ Gets the protocol to use for this webserver. """ if not hasattr(self, "_cached_mod"): mod = importlib.import_module(self.backend) self._cached_mod = mod server = getattr(self._cached_mod, self._cached_mod.PROTOCOL_CLASS) proto = server(self, ctx, *serv_info) ctx.protocol = proto return proto
async def start(self, ctx: Context): for resource_name, context_attr, bind in self.connectables: ctx.publish_resource(bind, resource_name, context_attr, types=Engine) logger.info('Configured SQLAlchemy engine (%s / ctx.%s; dialect=%s)', resource_name, context_attr, bind.dialect.name) if self.sessionmaker: if isinstance(self.commit_executor, str): self.commit_executor = await ctx.request_resource(Executor, self.commit_executor) ctx.publish_resource(self.sessionmaker) ctx.publish_lazy_resource(self.create_session, Session, context_attr=self.session_context_attr) logger.info('Configured SQLAlchemy session (default / ctx.%s)', self.session_context_attr)
def context(event_loop): ctx = Context() yield ctx event_loop.run_until_complete(ctx.close())