TIL search: asgi
I got my [asgi-csrf](https://github.com/simonw/asgi-csrf) Python package up to 100% code coverage. Here's [the pull request](https://github.com/simonw/asgi-csrf/issues/13).
I started by installing and using the [pytest-cov](https://pypi.org/project/pytest-cov/) pytest plugin.
```
pip install pytest-cov
pytest --cov=asgi_csrf
```
This shows the current...
Uvicorn silently ignores exceptions that occur during startup against the ASGI lifespan protocol - see [starlette/issues/486](https://github.com/encode/starlette/issues/486).
You can disable this feature using the `lifespan="on"` parameter to `uvicorn.run()` - which Datasette now does as-of [16f592247a2a0e140ada487e9972645406dcae69](https://github.com/simonw/datasette/commit/16f592247a2a0e140ada487e9972645406dcae69)
This exposed a bug in `datasette-debug-asgi`: it...
[Datasette](https://datasette.io/) is implemented as an ASGI application.
Django can be [run and deployed using ASGI](https://docs.djangoproject.com/en/4.1/howto/deployment/asgi/).
At the [DjangoCon Sprints](https://2022.djangocon.us/schedule/#Day-Sprints1) this morning I figured out how to run Datasette inside a Django application, talking to the same SQLite databases as Django...
...It turns out you can subvert that model entirely and route all of your traffic through a single function - great for serving up Python WSGI or ASGI apps that handle traffic routing themselves.
the trick is to use the `"routes"` key in a `vercel.json` file like this:
```json
{
"version": 2,
"builds": [
{
"src": "json_head.py",
"use": "@vercel/python"
}
],
"routes...
...I figured out a pattern for doing that using `pytest-httpx` to intercept outbound HTTP requests and send them through a Datasette ASGI application instead.
Here's a simplified example of this pattern, with inline comments explaining how it works.
Dependencies are:
```bash
pip install pytest pytest-httpx httpx datasette
```
I saved this as `test_demo.py` and ran it...
...None for invalid_option in invalid_options})
kwargs["return_instance"] = True
ds = cli.serve.callback(**kwargs)
# ds is now a configured Datasette instance
asgi = StandaloneApplication(
app=ds.app(),
options={
"bind": "{}:{}".format(host, port),
"workers": workers,
},
)
asgi.run()
@hookimpl
def register_commands(cli):
# Get a reference to the existing "datasette serve" command
serve_command = cli.commands["serve"]
# Create a new list...
...I configured the redirects using a one-off Datasette plugin called `redirects.py` which I dropped into the `plugins/` directory for the Datasette instance:
```python
from datasette import hookimpl
from datasette.utils.asgi import Response
@hookimpl
def register_routes():
return (
(r"^/til/til/(?P<topic>[^_]+)_(?P<slug>[^\.]+)\.md$", lambda request: Response.redirect(
"/{topic}/{slug}".format(**request.url_vars), status=301...
...haversine","simonw/sqlite-transform","simonw/datasette-csvs","simonw/datasette-render-markdown","simonw/datasette-template-sql","simonw/asgi-log-to-sqlite","simonw/datasette-configure-asgi","simonw/datasette-upload-csvs","simonw/datasette-auth-existing-cookies","simonw/datasette-sentry","simonw/geojson-to-sqlite","simonw/datasette-debug-asgi","simonw/shapefile-to-sqlite","simonw/datasette-mask-columns","simonw/datasette-ics","simonw/datasette-configure-fts","simonw...
...service \
--region=$region --platform=managed \
--update-labels service=$service
echo
done
```
It runs the equivalent of this for each service:
```
gcloud run services update asgi-log-demo --region=us-central1 --platform=managed --update-labels service=asgi-log-demo
```
I saved that as a `runme.sh` script, run `chmod 755 runme.sh` and then `./runme.sh` to run it.
The output...
...datasetteproject/datasette
command:
- sh
- -c
args:
- |-
# Install some plugins
pip install \
datasette-debug-asgi \
datasette-cluster-map \
datasette-psutil
# Download a DB (using Python because curl/wget are not available)
python -c 'import urllib.request; urllib.request.urlretrieve("https://global-power-plants.datasettes.com/global-power-plants.db", "/home/global-power-plants.db")'
# Start Datasette, on 0.0.0.0...
...This recipe works! The result is a `lib/` folder full of Amazon Linux Python packages, ready to be zipped up and deployed.
## Running an ASGI application
I want to deploy [Datasette](https://datasette.io/).
Datasette is an [ASGI application](https://simonwillison.net/2019/Jun/23/datasette-asgi/).
But... AWS Lambda functions have their own weird interface to HTTP - the `event...
...rfc3986, mypy-extensions, iniconfig, zipp, typing-extensions, typed-ast, tomli, soupsieve, sniffio, six, PyYAML, pyparsing, pycparser, py, platformdirs, pathspec, mergedeep, MarkupSafe, itsdangerous, idna, hupper, h11, execnet, cogapp, certifi, attrs, aiofiles, python-multipart, packaging, Jinja2, janus, importlib-metadata, cffi, beautifulsoup4, asgiref, anyio, pluggy, pint, httpcore, cryptography, click, asgi-csrf, uvicorn, trustme, pytest, httpx, click-default-group-wheel, black, pytest-timeout, pytest...
...versions.json | jq
```
Shows:
```json
{
"python": {
"version": "3.10.10",
"full": "3.10.10 (main, Mar 21 2023, 13:41:05) [Clang 14.0.6 ]"
},
"datasette": {
"version": "1.0a4"
},
"asgi": "3.0",
"uvicorn": "0.23.2",
"sqlite": {
"version": "3.42.0",
"fts_versions": [],
"extensions": {
"json1": null
},
"compile_options": [
"ATOMIC_INTRINSICS=1",
"COMPILER=clang-14.0.3",
"DEFAULT_AUTOVACUUM",
"DEFAULT_CACHE...
...Uvicorn running on http://0.0.0.0:8001 (Press CTRL+C to quit)
And now http://0.0.0.0:8001/-/versions reports the following:
```json
{
"python": {
"version": "3.8.2",
"full": "3.8.2 (default, Apr 27 2020, 15:53:34) \n[GCC 9.3.0]"
},
"datasette": {
"version": "0.44"
},
"asgi": "3.0",
"uvicorn": "0.11.5",
"sqlite...