Skip to main content

Caching for ASGI applications, inspired by Django's cache framework

Project description

asgi-caches

Build Status Coverage Package version

asgi-caches provides middleware and utilities for adding caching to ASGI applications. It is powered by async-caches, and inspired by Django's cache framework.

Note: this project is in an "alpha" status. Several features still need to be implemented.

Features

  • Compatible with any ASGI application (e.g. Starlette, FastAPI, Quart, etc.).
  • Support for application-wide or per-endpoint caching.
  • Ability to fine-tune the cache behavior (TTL, cache control) down to the endpoint level.
  • Clean and explicit API enabled by a loose coupling with async-caches.
  • Fully type annotated.
  • 100% test coverage.

Installation

pip install asgi-caches

Usage

We'll use this sample Starlette application equipped with an in-memory cache as a supporting example:

from caches import Cache
from starlette.applications import Starlette

app = Starlette()
cache = Cache("locmem://null", key_prefix="my-app", ttl=2 * 60)
app.add_event_handler("startup", cache.connect)
app.add_event_handler("shutdown", cache.disconnect)

Application-wide caching

To cache all endpoints, wrap the application around CacheMiddleware:

from asgi_caches.middleware import CacheMiddleware

app.add_middleware(CacheMiddleware, cache=cache)

This middleware applies the Cache-Control and Expires headers based on the cache ttl (see also Time to live). These headers tell the browser how and for how long it should cache responses.

If you have multiple middleware, read Order of middleware to know at which point in the stack CacheMiddleware should be applied.

Per-endpoint caching (TODO)

You can specify the cache policy on a given endpoint using the @cached decoraotr:

from starlette.endpoints import HTTPEndpoint
from asgi_caches.decorators import cached

@app.route("/users/{user_id:int}")
@cached(cache)
class UserDetail(HTTPEndpoint):
    async def get(self, request):
        ...

Note that since the @cached decorator actually works on any ASGI application, the snippet above uses a Starlette endpoint instead of a function-based view. (As a consequence, applying @cached to methods of an endpoint class is not supported. This should not be a problem, because caching is only ever applied to GET and HEAD operations.)

Disabling caching (TODO)

To disable caching altogether on a given endpoint, use the @never_cache decorator:

from datetime import datetime
from asgi_caches.decorators import never_cache

@app.route("/datetime")
@never_cache
class DateTime(HTTPEndpoint):
    async def get(self, request):
        return JSONResponse({"time": datetime.now().utcformat()})

Time to live

Time to live (TTL) refers to how long (in seconds) a response can stay in the cache before it expires.

Components in asgi-caches will use whichever TTL is set on the Cache instance by default:

# Cache for 2 minutes by default.
cache = Cache("locmem://null", ttl=2 * 60)

(See also Default time to live in the async-caches documentation.)

(TODO) You can override the TTL on a per-view basis using the ttl parameter, e.g.:

import math
from starlette.responses import JSONResponse
from asgi_caches.decorators import cached

@app.route("/pi")
@cached(cache, ttl=None)  # Cache forever
class Pi(HTTPEndpoint):
    async def get(self, request):
        return JSONResponse({"value": math.pi})

Cache control (TODO)

You can use the @cache_control() decorator to add cache control directives to responses. This decorator will set the appropriate headers automatically (e.g. Cache-Control).

One typical use case is cache privacy. If your view returns sensitive information to clients (e.g. a bank account number), you will probably want to mark its cache as private. This is how to do it:

from asgi_caches.decorators import cache_control

@app.route("/accounts/{account_id}")
@cache_control(private=True)
class BankAccountDetail(HTTPEndpoint):
    async def get(self, request):
        ...

Alternatively, you can explicitly mark a cache as public with public=True.

(Note that the public and private directives are mutually exclusive. The decorator ensures that one is removed if the other is set, and vice versa.)

Besides, @cache_control() accepts any valid Cache-Control directives. For example, max-age controls the amount of time clients should cache the response:

from asgi_caches.decorators import cache_control

@app.route("/weather_reports/today")
@cache_control(max_age=3600)
class DailyWeatherReport(HTTPEndpoint):
    async def get(self, request):
        ...

Other example directives:

  • no_transform=True
  • must_revalidate=True
  • stale_while_revalidate=num_seconds

See RFC7234 (Caching) for more information, and the HTTP Cache Directive Registry for the list of valid cache directives (note not all apply to responses).

Order of middleware

The cache middleware uses the Vary header present in responses to know by which request header it should vary the cache. For example, if a response contains Vary: Accept-Encoding, a request containing Accept-Encoding: gzip won't result in using the same cache entry than a request containing Accept-Encoding: identity.

As a result of this mechanism, there are some rules relative to which point in the middleware stack cache middleware should be applied:

  • CacheMiddleware should be applied after middleware that modifies the Vary header. For example:
from starlette.middleware.gzip import GZipMiddleware

app.add_middleware(GZipMiddleware)  # Adds 'Accept-Encoding'
app.add_middleware(CacheMiddleware, cache=cache)
  • Similarly, it should be applied before middleware that may add something to the varying headers of the request. (As a contrived example, if you had a middleware that added gzip to Accept-Encoding to later decompress the resulting response body, then you'd need to place this middleware before CacheMiddleware.)

Credits

Due credit goes to the Django developers and maintainers, as a lot of the API and implementation was directly inspired by the Django cache framework.

License

MIT

Changelog

All notable changes to this project will be documented in this file.

The format is based on Keep a Changelog.

0.1.0 - 2019-11-12

Added

  • Add CacheMiddleware. (Pull #8)
  • Prevent caching of responses that have cookies when the request has none. (Pull #9)
  • Prevent caching of responses if the cache TTL is zero. (Pull #10)

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

asgi-caches-0.1.0.tar.gz (12.0 kB view details)

Uploaded Source

Built Distribution

asgi_caches-0.1.0-py3-none-any.whl (9.9 kB view details)

Uploaded Python 3

File details

Details for the file asgi-caches-0.1.0.tar.gz.

File metadata

  • Download URL: asgi-caches-0.1.0.tar.gz
  • Upload date:
  • Size: 12.0 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/2.0.0 pkginfo/1.5.0.1 requests/2.22.0 setuptools/41.2.0 requests-toolbelt/0.9.1 tqdm/4.38.0 CPython/3.8.0

File hashes

Hashes for asgi-caches-0.1.0.tar.gz
Algorithm Hash digest
SHA256 f1bee4c3cdf593c2b5ae2072f6056eb529bcf1b22b5713eedeaa580a3a6eb5e3
MD5 9c6c1e3ff2ebfbb62da09b4307163d44
BLAKE2b-256 d33e9178748f0b5a51efa959efb96b32112e6f546469517a7955138bb073bbd0

See more details on using hashes here.

File details

Details for the file asgi_caches-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: asgi_caches-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 9.9 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/2.0.0 pkginfo/1.5.0.1 requests/2.22.0 setuptools/41.2.0 requests-toolbelt/0.9.1 tqdm/4.38.0 CPython/3.8.0

File hashes

Hashes for asgi_caches-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 5c3d767547c0006b1da2d2954ea0c8b67df12ee4ab3033dc122a2db3b0a6a027
MD5 25e6ba97be58a068ee0d2548e987ccf2
BLAKE2b-256 bf8f90bede3d04cfe115ffbe553d99b915d52df5c97f8e1477ec28b22ccc844e

See more details on using hashes here.

Supported by

AWS AWS Cloud computing and Security Sponsor Datadog Datadog Monitoring Fastly Fastly CDN Google Google Download Analytics Microsoft Microsoft PSF Sponsor Pingdom Pingdom Monitoring Sentry Sentry Error logging StatusPage StatusPage Status page