async def test_locmem_cache_uses_net_loc_for_separaing_namespaces(): cache = Cache("locmem://") other_cache = Cache("locmem://other") await cache.connect() await other_cache.connect() await cache.set("test", "Ok!") assert await other_cache.get("test") is None assert await cache.get("test") == "Ok!"
async def test_streaming_response() -> None: """Streaming responses should not be cached.""" cache = Cache("locmem://null") async def body() -> typing.AsyncIterator[str]: yield "Hello, " yield "world!" async def app(scope: Scope, receive: Receive, send: Send) -> None: response = StreamingResponse(body()) await response(scope, receive, send) spy = CacheSpy(app) app = CacheMiddleware(spy, cache=cache) client = httpx.AsyncClient(app=app, base_url="http://testserver") async with cache, client: assert spy.misses == 0 r = await client.get("/") assert r.status_code == 200 assert r.text == "Hello, world!" assert spy.misses == 1 r = await client.get("/") assert r.status_code == 200 assert r.text == "Hello, world!" assert spy.misses == 2
async def test_use_cached_head_response_on_get() -> None: """ Making a HEAD request should use the cached response for future GET requests. """ cache = Cache("locmem://null") spy = CacheSpy(PlainTextResponse("Hello, world!")) app = CacheMiddleware(spy, cache=cache) client = httpx.AsyncClient(app=app, base_url="http://testserver") async with cache, client: assert spy.misses == 0 r = await client.head("/") assert not r.text assert r.status_code == 200 assert "Expires" in r.headers assert "Cache-Control" in r.headers assert spy.misses == 1 r1 = await client.get("/") assert r1.text == "Hello, world!" assert r1.status_code == 200 assert "Expires" in r.headers assert "Cache-Control" in r.headers assert spy.misses == 1
async def test_not_http() -> None: async def app(scope: Scope, receive: Receive, send: Send) -> None: assert scope["type"] == "lifespan" cache = Cache("locmem://null") app = CacheMiddleware(app, cache=cache) await app({"type": "lifespan"}, mock_receive, mock_send)
async def test_decorator_raw_asgi() -> None: cache = Cache("locmem://null", ttl=2 * 60) @cached(cache) async def app(scope: Scope, receive: Receive, send: Send) -> None: response = PlainTextResponse("Hello, world!") await response(scope, receive, send) spy = app.app = CacheSpy(app.app) client = httpx.AsyncClient(app=app, base_url="http://testserver") async with cache, client: assert spy.misses == 0 r = await client.get("/") assert r.status_code == 200 assert r.text == "Hello, world!" assert "Expires" in r.headers assert "Cache-Control" in r.headers assert spy.misses == 1 r = await client.get("/") assert r.status_code == 200 assert r.text == "Hello, world!" assert "Expires" in r.headers assert "Cache-Control" in r.headers assert spy.misses == 1
async def test_cookies_in_response_and_cookieless_request() -> None: """ Responses that set cookies shouldn't be cached if the request doesn't have cookies. """ cache = Cache("locmem://null") async def app(scope: Scope, receive: Receive, send: Send) -> None: response = PlainTextResponse("Hello, world!") response.set_cookie("session_id", "1234") await response(scope, receive, send) spy = CacheSpy(app) app = CacheMiddleware(spy, cache=cache) client = httpx.AsyncClient(app=app, base_url="http://testserver") async with cache, client: r = await client.get("/") assert r.status_code == 200 assert r.text == "Hello, world!" assert spy.misses == 1 r = await client.get("/") assert r.status_code == 200 assert r.text == "Hello, world!" assert spy.misses == 2
async def test_cache_response() -> None: cache = Cache("locmem://null", ttl=2 * 60) spy = CacheSpy(PlainTextResponse("Hello, world!")) app = CacheMiddleware(spy, cache=cache) client = httpx.AsyncClient(app=app, base_url="http://testserver") async with cache, client: assert spy.misses == 0 r = await client.get("/") assert r.status_code == 200 assert r.text == "Hello, world!" assert spy.misses == 1 assert "Expires" in r.headers expires_fmt = "%a, %d %b %Y %H:%M:%S GMT" expires = dt.datetime.strptime(r.headers["Expires"], expires_fmt) delta: dt.timedelta = expires - dt.datetime.utcnow() assert delta.total_seconds() == pytest.approx(120, rel=1e-2) assert "Cache-Control" in r.headers assert r.headers["Cache-Control"] == "max-age=120" r1 = await client.get("/") assert spy.misses == 1 assert ComparableHTTPXResponse(r1) == r r2 = await client.get("/") assert spy.misses == 1 assert ComparableHTTPXResponse(r2) == r
async def cache(): obj = Cache("redis://localhost:6379/1") await obj.connect() await obj.clear() yield obj await obj.clear() await obj.disconnect()
async def test_decorate_starlette_view() -> None: cache = Cache("locmem://null", ttl=2 * 60) with pytest.raises(ValueError): @cached(cache) async def home(request: Request) -> Response: ... # pragma: no cover
async def test_cache_can_be_used_as_context_manager(): async with Cache("locmem://") as cache: await cache.set("test", "Ok!") assert await cache.get("test") == "Ok!" assert await cache.get_or_set('test2', _testing_coroutine('arg', test1='kwarg')) == "Ok!" assert await cache(_testing_coroutine('arg', test1='kwarg')) == 'Ok!'
async def test_vary() -> None: """ Sending different values for request headers registered as varying should result in different cache entries. """ cache = Cache("locmem://null") async def gzippable_app(scope: Scope, receive: Receive, send: Send) -> None: headers = Headers(scope=scope) if "gzip" in headers.getlist("accept-encoding"): body = gzip.compress(b"Hello, world!") response = PlainTextResponse( content=body, headers={ "Content-Encoding": "gzip", "Content-Length": str(len(body)) }, ) else: response = PlainTextResponse("Hello, world!") response.headers["Vary"] = "Accept-Encoding" await response(scope, receive, send) spy = CacheSpy(gzippable_app) app = CacheMiddleware(spy, cache=cache) client = httpx.AsyncClient(app=app, base_url="http://testserver") async with cache, client: r = await client.get("/", headers={"accept-encoding": "gzip"}) assert spy.misses == 1 assert r.status_code == 200 assert r.text == "Hello, world!" assert r.headers["content-encoding"] == "gzip" assert "Expires" in r.headers assert "Cache-Control" in r.headers # Different "Accept-Encoding" header => the cached result # for "Accept-Encoding: gzip" should not be used. r1 = await client.get("/", headers={"accept-encoding": "identity"}) assert spy.misses == 2 assert r1.status_code == 200 assert r1.text == "Hello, world!" assert "Expires" in r.headers assert "Cache-Control" in r.headers # This "Accept-Encoding" header has already been seen => we should # get a cached response. r2 = await client.get("/", headers={"accept-encoding": "gzip"}) assert spy.misses == 2 assert r.status_code == 200 assert r.text == "Hello, world!" assert r2.headers["Content-Encoding"] == "gzip" assert "Expires" in r.headers assert "Cache-Control" in r.headers
async def test_duplicate_caching() -> None: cache = Cache("locmem://default") special_cache = Cache("locmem://special") class DuplicateCache(HTTPEndpoint): pass app = Starlette( routes=[ Route("/duplicate_cache", CacheMiddleware(DuplicateCache, cache=special_cache)) ], middleware=[Middleware(CacheMiddleware, cache=cache)], ) client = httpx.AsyncClient(app=app, base_url="http://testserver") async with cache, special_cache, client: with pytest.raises(DuplicateCaching): await client.get("/duplicate_cache")
async def test_logs_trace(capsys: typing.Any) -> None: cache = Cache("locmem://null", ttl=2 * 60) app = CacheMiddleware(PlainTextResponse("Hello, world!"), cache=cache) client = httpx.AsyncClient(app=app, base_url="http://testserver") async with cache, client: with override_log_level("trace"): await client.get("/") stderr = capsys.readouterr().err assert "cache_lookup MISS" in stderr assert "get_from_cache request.url='http://testserver/" in stderr
async def test_cache_not_connected() -> None: cache = Cache("locmem://null", ttl=2 * 60) app = CacheMiddleware(PlainTextResponse("Hello, world!"), cache=cache) client = httpx.AsyncClient(app=app, base_url="http://testserver") async with client: with pytest.raises(CacheNotConnected) as ctx: await client.get("/") exc = ctx.value assert exc.cache is cache assert str(cache.url) in str(exc)
async def test_decorator_starlette_endpoint() -> None: cache = Cache("locmem://null", ttl=2 * 60) @cached(cache) class CachedHome(HTTPEndpoint): async def get(self, request: Request) -> Response: return PlainTextResponse("Hello, world!") class UncachedUsers(HTTPEndpoint): async def get(self, request: Request) -> Response: return PlainTextResponse("Hello, users!") assert isinstance(CachedHome, CacheMiddleware) spy = CachedHome.app = CacheSpy(CachedHome.app) users_spy = CacheSpy(UncachedUsers) app = Starlette(routes=[Route("/", CachedHome), Route("/users", users_spy)]) client = httpx.AsyncClient(app=app, base_url="http://testserver") async with cache, client: assert spy.misses == 0 r = await client.get("/") assert r.status_code == 200 assert r.text == "Hello, world!" assert "Expires" in r.headers assert "Cache-Control" in r.headers assert spy.misses == 1 r = await client.get("/") assert r.status_code == 200 assert r.text == "Hello, world!" assert "Expires" in r.headers assert "Cache-Control" in r.headers assert spy.misses == 1 assert users_spy.misses == 0 r = await client.get("/users") assert r.status_code == 200 assert r.text == "Hello, users!" assert "Expires" not in r.headers assert "Cache-Control" not in r.headers assert users_spy.misses == 1 r = await client.get("/users") assert r.status_code == 200 assert r.text == "Hello, users!" assert "Expires" not in r.headers assert "Cache-Control" not in r.headers assert users_spy.misses == 2
async def test_logs_debug(capsys: typing.Any) -> None: cache = Cache("locmem://null", ttl=2 * 60) app = CacheMiddleware(PlainTextResponse("Hello, world!"), cache=cache) client = httpx.AsyncClient(app=app, base_url="http://testserver") async with cache, client: with override_log_level("debug"): await client.get("/") await client.get("/") stderr = capsys.readouterr().err miss_line, store_line, hit_line, *_ = stderr.split("\n") assert "cache_lookup MISS" in miss_line assert "store_in_cache max_age=120" in store_line assert "cache_lookup HIT" in hit_line assert "get_from_cache request.url='http://testserver/" not in stderr
async def test_not_200_ok(status_code: int) -> None: """Responses that don't have status code 200 should not be cached.""" cache = Cache("locmem://null") spy = CacheSpy(PlainTextResponse("Hello, world!", status_code=status_code)) app = CacheMiddleware(spy, cache=cache) client = httpx.AsyncClient(app=app, base_url="http://testserver") async with cache, client: r = await client.get("/") assert r.status_code == status_code assert r.text == "Hello, world!" assert "Expires" not in r.headers assert "Cache-Control" not in r.headers assert spy.misses == 1 r1 = await client.get("/") assert ComparableHTTPXResponse(r1) == r assert spy.misses == 2
async def test_non_cachable_request() -> None: cache = Cache("locmem://null") spy = CacheSpy(PlainTextResponse("Hello, world!")) app = CacheMiddleware(spy, cache=cache) client = httpx.AsyncClient(app=app, base_url="http://testserver") async with cache, client: assert spy.misses == 0 r = await client.post("/") assert r.status_code == 200 assert r.text == "Hello, world!" assert "Expires" not in r.headers assert "Cache-Control" not in r.headers assert spy.misses == 1 r1 = await client.post("/") assert ComparableHTTPXResponse(r1) == r assert spy.misses == 2
def setup(project_settings: str = None, database: bool = False) -> GraphQL: """Load Turbulette applications and return the GraphQL route.""" project_settings_module = (get_project_settings_by_env() if not project_settings else import_module(project_settings)) # The database connection has to be initialized before the LazySettings object to be setup # so we have to connect to the database before the registry to be setup if database: get_gino_instance() registry = Registry(project_settings_module=project_settings_module) conf.registry.__setup__(registry) schema = registry.setup() # At this point, settings are now available through `settings` from `turbulette.conf` module settings = conf.settings # Now that the database connection is established, we can use `settings` cache.__setup__(Cache(settings.CACHE)) extensions: List[Type[Extension]] = [PolicyExtension] for ext in settings.ARIADNE_EXTENSIONS: module_class = ext.rsplit(".", 1) extensions.append( getattr( import_module(module_class[0]), module_class[1], )) graphql_route = GraphQL( schema, debug=settings.DEBUG, extensions=extensions, error_formatter=error_formatter, ) return graphql_route
def test_cache_cant_be_initialized_with_base_backend(): with pytest.raises(AssertionError): Cache("base://null")
async def test_backend_errors_if_more_than_one_connection_is_opened(): cache = Cache("redis://localhost/1") await cache.connect() with pytest.raises(AssertionError): await cache.connect() await cache.disconnect()
async def cache(): obj = Cache("locmem://0") await obj.connect() return obj
async def test_backend_errors_if_nonexistant_connection_is_closed(): cache = Cache("redis://localhost/1") with pytest.raises(AssertionError): await cache.disconnect()
def test_dummy_cache_can_be_initialized_without_net_loc(): Cache("dummy://")
def test_dummy_cache_can_be_initialized_with_net_loc(): Cache("dummy://primary")
async def test_dummy_cache_can_be_connected_and_disconnected(): cache = Cache("dummy://", version=20) await cache.connect() await cache.disconnect()
def test_dummy_cache_can_be_initialized_with_version(): Cache("dummy://", version=20)
def test_dummy_cache_can_be_initialized_with_ttl(): Cache("dummy://", ttl=600)
def test_dummy_cache_can_be_initialized_with_key_prefix(): Cache("dummy://", key_prefix="test")
from caches import Cache cache = Cache("locmem://default", ttl=2 * 60) special_cache = Cache("locmem://special", ttl=60)