<?xml version="1.0" encoding="utf-8"?>
<feed xml:lang="en-us" xmlns="http://www.w3.org/2005/Atom"><title>Simon Willison's Weblog: async</title><link href="http://simonwillison.net/" rel="alternate"/><link href="http://simonwillison.net/tags/async.atom" rel="self"/><id>http://simonwillison.net/</id><updated>2026-06-11T06:28:09+00:00</updated><author><name>Simon Willison</name></author><entry><title>asyncinject 0.7</title><link href="https://simonwillison.net/2026/Jun/11/asyncinject/#atom-tag" rel="alternate"/><published>2026-06-11T06:28:09+00:00</published><updated>2026-06-11T06:28:09+00:00</updated><id>https://simonwillison.net/2026/Jun/11/asyncinject/#atom-tag</id><summary type="html">
    
        &lt;p&gt;&lt;strong&gt;Release:&lt;/strong&gt; &lt;a href="https://github.com/simonw/asyncinject/releases/tag/0.7"&gt;asyncinject 0.7&lt;/a&gt;&lt;/p&gt;
        &lt;p&gt;I built this utility library to support an &lt;code&gt;asyncio&lt;/code&gt; dependency injection pattern a few years ago. I was using it with Datasette and Claude Fable 5 spotted some bugs in the dependency which it then fixed for me. It's a very proactive model!&lt;/p&gt;
    
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/async"&gt;async&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/projects"&gt;projects&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/python"&gt;python&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/claude-mythos"&gt;claude-mythos&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="async"/><category term="projects"/><category term="python"/><category term="claude-mythos"/></entry><entry><title>llm-all-models-async 0.1</title><link href="https://simonwillison.net/2026/Mar/31/llm-all-models-async/#atom-tag" rel="alternate"/><published>2026-03-31T20:52:02+00:00</published><updated>2026-03-31T20:52:02+00:00</updated><id>https://simonwillison.net/2026/Mar/31/llm-all-models-async/#atom-tag</id><summary type="html">
    
        &lt;p&gt;&lt;strong&gt;Release:&lt;/strong&gt; &lt;a href="https://github.com/simonw/llm-all-models-async/releases/tag/0.1"&gt;llm-all-models-async 0.1&lt;/a&gt;&lt;/p&gt;
        &lt;p&gt;LLM plugins can define new models in both &lt;a href="https://llm.datasette.io/en/stable/plugins/tutorial-model-plugin.html"&gt;sync&lt;/a&gt; and &lt;a href="https://llm.datasette.io/en/stable/plugins/advanced-model-plugins.html#async-models"&gt;async&lt;/a&gt; varieties. The async variants are most common for API-backed models - sync variants tend to be things that run the model directly within the plugin.&lt;/p&gt;
&lt;p&gt;My &lt;a href="https://simonwillison.net/2026/Mar/30/mr-chatterbox/#running-it-locally-with-llm"&gt;llm-mrchatterbox&lt;/a&gt; plugin is sync only. I wanted to try it out with various Datasette LLM features (specifically &lt;a href="https://github.com/datasette/datasette-enrichments-llm"&gt;datasette-enrichments-llm&lt;/a&gt;) but Datasette can only use async models.&lt;/p&gt;
&lt;p&gt;So... I had Claude spin up this plugin that turns sync models into async models using a thread pool. This ended up needing an extra plugin hook mechanism in LLM itself, which I shipped just now in &lt;a href="https://llm.datasette.io/en/stable/changelog.html#v0-30"&gt;LLM 0.30&lt;/a&gt;.&lt;/p&gt;
    
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/async"&gt;async&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/python"&gt;python&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/llm"&gt;llm&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="async"/><category term="python"/><category term="llm"/></entry><entry><title>Quoting Kumar Aditya</title><link href="https://simonwillison.net/2025/Sep/11/kumar-aditya/#atom-tag" rel="alternate"/><published>2025-09-11T03:07:16+00:00</published><updated>2025-09-11T03:07:16+00:00</updated><id>https://simonwillison.net/2025/Sep/11/kumar-aditya/#atom-tag</id><summary type="html">
    &lt;blockquote cite="https://labs.quansight.org/blog/scaling-asyncio-on-free-threaded-python"&gt;&lt;p&gt;In Python 3.14, I have implemented several changes to fix thread safety of &lt;code&gt;asyncio&lt;/code&gt; and enable it to scale effectively on the free-threaded build of CPython. It is now implemented using lock-free data structures and per-thread state, allowing for highly efficient task management and execution across multiple threads. In the general case of multiple event loops running in parallel, there is no lock contention and performance scales linearly with the number of threads. [...]&lt;/p&gt;
&lt;p&gt;For a deeper dive into the implementation, check out the &lt;a href="https://github.com/python/cpython/blob/main/InternalDocs/asyncio.md#python-314-implementation"&gt;internal docs for asyncio&lt;/a&gt;.&lt;/p&gt;&lt;/blockquote&gt;
&lt;p class="cite"&gt;&amp;mdash; &lt;a href="https://labs.quansight.org/blog/scaling-asyncio-on-free-threaded-python"&gt;Kumar Aditya&lt;/a&gt;, Scaling asyncio on Free-Threaded Python&lt;/p&gt;

    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/async"&gt;async&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/gil"&gt;gil&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/python"&gt;python&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/scaling"&gt;scaling&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/threads"&gt;threads&lt;/a&gt;&lt;/p&gt;



</summary><category term="async"/><category term="gil"/><category term="python"/><category term="scaling"/><category term="threads"/></entry><entry><title>Textual v4.0.0: The Streaming Release</title><link href="https://simonwillison.net/2025/Jul/22/textual-v4/#atom-tag" rel="alternate"/><published>2025-07-22T00:32:53+00:00</published><updated>2025-07-22T00:32:53+00:00</updated><id>https://simonwillison.net/2025/Jul/22/textual-v4/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/Textualize/textual/releases/tag/v4.0.0"&gt;Textual v4.0.0: The Streaming Release&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Will McGugan may &lt;a href="https://textual.textualize.io/blog/2025/05/07/the-future-of-textualize/"&gt;no longer be running&lt;/a&gt; a commercial company around Textual, but that hasn't stopped his progress on the open source project.&lt;/p&gt;
&lt;p&gt;He recently released v4 of his Python framework for building TUI command-line apps, and the signature feature is &lt;a href="https://github.com/Textualize/textual/pull/5950"&gt;streaming Markdown support&lt;/a&gt; - super relevant in our current age of LLMs, most of which default to outputting a stream of Markdown via their APIs.&lt;/p&gt;
&lt;p&gt;I took an example &lt;a href="https://github.com/Textualize/textual/blob/03b94706399f110ff93fa396d4afbc79c3738638/tests/snapshot_tests/test_snapshots.py#L4378-L4400"&gt;from one of his tests&lt;/a&gt;, spliced in my &lt;a href="https://llm.datasette.io/en/stable/python-api.html#async-models"&gt;async LLM Python library&lt;/a&gt; and &lt;a href="https://chatgpt.com/share/687c3a6a-4e1c-8006-83a2-706b4bf04067"&gt;got some help from o3&lt;/a&gt; to turn it into &lt;a href="https://github.com/simonw/tools/blob/916b16cd7dfd3c23315d0a4ed02172878feafa45/python/streaming_textual_markdown.py"&gt;a streaming script&lt;/a&gt; for talking to models, which can be run like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;uv run http://tools.simonwillison.net/python/streaming_textual_markdown.py \
'Markdown headers and tables comparing pelicans and wolves' \
-m gpt-4.1-mini
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img alt="Running that prompt streams a Markdown table to my console." src="https://static.simonwillison.net/static/2025/epic-table.gif" /&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/async"&gt;async&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/python"&gt;python&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/markdown"&gt;markdown&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ai"&gt;ai&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/will-mcgugan"&gt;will-mcgugan&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/generative-ai"&gt;generative-ai&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/llms"&gt;llms&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/textual"&gt;textual&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/llm"&gt;llm&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/uv"&gt;uv&lt;/a&gt;&lt;/p&gt;



</summary><category term="async"/><category term="python"/><category term="markdown"/><category term="ai"/><category term="will-mcgugan"/><category term="generative-ai"/><category term="llms"/><category term="textual"/><category term="llm"/><category term="uv"/></entry><entry><title>files-to-prompt 0.5</title><link href="https://simonwillison.net/2025/Feb/14/files-to-prompt/#atom-tag" rel="alternate"/><published>2025-02-14T04:14:21+00:00</published><updated>2025-02-14T04:14:21+00:00</updated><id>https://simonwillison.net/2025/Feb/14/files-to-prompt/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/simonw/files-to-prompt/releases/tag/0.5"&gt;files-to-prompt 0.5&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
My &lt;code&gt;files-to-prompt&lt;/code&gt; tool (&lt;a href="https://simonwillison.net/2024/Apr/8/files-to-prompt/"&gt;originally built using Claude 3 Opus back in April&lt;/a&gt;) had been accumulating a bunch of issues and PRs - I finally got around to spending some time with it and pushed a fresh release:&lt;/p&gt;
&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;New &lt;code&gt;-n/--line-numbers&lt;/code&gt; flag for including line numbers in the output. Thanks, &lt;a href="https://github.com/danclaytondev"&gt;Dan Clayton&lt;/a&gt;. &lt;a href="https://github.com/simonw/files-to-prompt/pull/38"&gt;#38&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Fix for utf-8 handling on Windows. Thanks, &lt;a href="https://github.com/david-jarman"&gt;David Jarman&lt;/a&gt;. &lt;a href="https://github.com/simonw/files-to-prompt/pull/36"&gt;#36&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--ignore&lt;/code&gt; patterns are now matched against directory names as well as file names, unless you pass the new &lt;code&gt;--ignore-files-only&lt;/code&gt; flag. Thanks, &lt;a href="https://github.com/nmpowell"&gt;Nick Powell&lt;/a&gt;. &lt;a href="https://github.com/simonw/files-to-prompt/pull/30"&gt;#30&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;I use this tool myself on an almost daily basis - it's fantastic for quickly answering questions about code. Recently I've been plugging it into Gemini 2.0 with its 2 million token context length, running recipes like this one:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git clone https://github.com/bytecodealliance/componentize-py
cd componentize-py
files-to-prompt . -c | llm -m gemini-2.0-pro-exp-02-05 \
  -s 'How does this work? Does it include a python compiler or AST trick of some sort?'
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I ran that question against the &lt;a href="https://github.com/bytecodealliance/componentize-py"&gt;bytecodealliance/componentize-py&lt;/a&gt; repo - which provides a tool for turning Python code into compiled WASM - and got &lt;a href="https://gist.github.com/simonw/a9d72e7f903417fb49e1d7a531ee8f97"&gt;this really useful answer&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Here's another example. I decided to have o3-mini review how Datasette handles concurrent SQLite connections from async Python code - so I ran this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git clone https://github.com/simonw/datasette
cd datasette/datasette
files-to-prompt database.py utils/__init__.py -c | \
  llm -m o3-mini -o reasoning_effort high \
  -s 'Output in markdown a detailed analysis of how this code handles the challenge of running SQLite queries from a Python asyncio application. Explain how it works in the first section, then explore the pros and cons of this design. In a final section propose alternative mechanisms that might work better.'
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here's &lt;a href="https://gist.github.com/simonw/76c8c433f4a65cf01a5c9121453683ab"&gt;the result&lt;/a&gt;. It did an extremely good job of explaining how my code works - despite being fed just the Python and none of the other documentation. Then it made some solid recommendations for potential alternatives.&lt;/p&gt;
&lt;p&gt;I added a couple of follow-up questions (using &lt;code&gt;llm -c&lt;/code&gt;) which resulted in &lt;a href="https://gist.github.com/simonw/76c8c433f4a65cf01a5c9121453683ab?permalink_comment_id=5438685#gistcomment-5438685"&gt;a full working prototype&lt;/a&gt; of an alternative threadpool mechanism, plus &lt;a href="https://gist.github.com/simonw/76c8c433f4a65cf01a5c9121453683ab?permalink_comment_id=5438691#gistcomment-5438691"&gt;some benchmarks&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;One final example: I decided to see if there were any undocumented features in &lt;a href="https://litestream.io/"&gt;Litestream&lt;/a&gt;, so I checked out the repo and ran a prompt against just the &lt;code&gt;.go&lt;/code&gt; files in that project:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git clone https://github.com/benbjohnson/litestream
cd litestream
files-to-prompt . -e go -c | llm -m o3-mini \
  -s 'Write extensive user documentation for this project in markdown'
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Once again, o3-mini provided a &lt;a href="https://gist.github.com/simonw/cbf339032f99fee72af5fd5455bc7235"&gt;really impressively detailed&lt;/a&gt; set of unofficial documentation derived purely from reading the source.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/async"&gt;async&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/projects"&gt;projects&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/python"&gt;python&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/sqlite"&gt;sqlite&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ai"&gt;ai&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/datasette"&gt;datasette&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/webassembly"&gt;webassembly&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/litestream"&gt;litestream&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/generative-ai"&gt;generative-ai&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/llms"&gt;llms&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ai-assisted-programming"&gt;ai-assisted-programming&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/llm"&gt;llm&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/gemini"&gt;gemini&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/llm-reasoning"&gt;llm-reasoning&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/files-to-prompt"&gt;files-to-prompt&lt;/a&gt;&lt;/p&gt;



</summary><category term="async"/><category term="projects"/><category term="python"/><category term="sqlite"/><category term="ai"/><category term="datasette"/><category term="webassembly"/><category term="litestream"/><category term="generative-ai"/><category term="llms"/><category term="ai-assisted-programming"/><category term="llm"/><category term="gemini"/><category term="llm-reasoning"/><category term="files-to-prompt"/></entry><entry><title>In search of a faster SQLite</title><link href="https://simonwillison.net/2024/Dec/15/in-search-of-a-faster-sqlite/#atom-tag" rel="alternate"/><published>2024-12-15T18:09:17+00:00</published><updated>2024-12-15T18:09:17+00:00</updated><id>https://simonwillison.net/2024/Dec/15/in-search-of-a-faster-sqlite/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://avi.im/blag/2024/faster-sqlite/"&gt;In search of a faster SQLite&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Turso developer Avinash Sajjanshetty (&lt;a href="https://simonwillison.net/2021/Jul/19/one-billion-rows/"&gt;previously&lt;/a&gt;) shares notes on the April 2024 paper &lt;a href="https://penberg.org/papers/penberg-edgesys24.pdf"&gt;Serverless Runtime / Database Co-Design With Asynchronous I/O&lt;/a&gt; by Turso founder and CTO Pekka Enberg, Jon Crowcroft, Sasu Tarkoma and Ashwin Rao.&lt;/p&gt;
&lt;p&gt;The theme of the paper is rearchitecting SQLite for asynchronous I/O, and Avinash describes it as "the foundational paper behind &lt;a href="https://github.com/tursodatabase/limbo"&gt;Limbo&lt;/a&gt;, the SQLite rewrite in Rust."&lt;/p&gt;
&lt;p&gt;From the paper abstract:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;We propose rearchitecting SQLite to provide asynchronous byte-code instructions for I/O to avoid blocking in the library and de-coupling the query and storage engines to facilitate database and
serverless runtime co-design. Our preliminary evaluation shows
up to a 100x reduction in tail latency, suggesting that our approach
is conducive to runtime/database co-design for low latency.&lt;/p&gt;
&lt;/blockquote&gt;

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="https://lobste.rs/s/bwovro/search_faster_sqlite"&gt;lobste.rs&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/async"&gt;async&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/sqlite"&gt;sqlite&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/rust"&gt;rust&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/limbo"&gt;limbo&lt;/a&gt;&lt;/p&gt;



</summary><category term="async"/><category term="sqlite"/><category term="rust"/><category term="limbo"/></entry><entry><title>googleapis/python-genai</title><link href="https://simonwillison.net/2024/Dec/12/python-genai/#atom-tag" rel="alternate"/><published>2024-12-12T16:21:46+00:00</published><updated>2024-12-12T16:21:46+00:00</updated><id>https://simonwillison.net/2024/Dec/12/python-genai/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/googleapis/python-genai"&gt;googleapis/python-genai&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Google released this brand new Python library for accessing their generative AI models yesterday, offering an alternative to their existing &lt;a href="https://github.com/google-gemini/generative-ai-python"&gt;generative-ai-python&lt;/a&gt; library.&lt;/p&gt;
&lt;p&gt;The API design looks very solid to me, and it includes both sync and async implementations. Here's an async streaming response:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;async for response in client.aio.models.generate_content_stream(
    model='gemini-2.0-flash-exp',
    contents='Tell me a story in 300 words.'
):
    print(response.text)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It also includes Pydantic-based output schema support and some nice syntactic sugar for defining tools using Python functions.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/async"&gt;async&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/google"&gt;google&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/python"&gt;python&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ai"&gt;ai&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/generative-ai"&gt;generative-ai&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/llms"&gt;llms&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/gemini"&gt;gemini&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/llm-tool-use"&gt;llm-tool-use&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/pydantic"&gt;pydantic&lt;/a&gt;&lt;/p&gt;



</summary><category term="async"/><category term="google"/><category term="python"/><category term="ai"/><category term="generative-ai"/><category term="llms"/><category term="gemini"/><category term="llm-tool-use"/><category term="pydantic"/></entry><entry><title>llm-gemini 0.4</title><link href="https://simonwillison.net/2024/Nov/18/llm-gemini-04/#atom-tag" rel="alternate"/><published>2024-11-18T07:37:17+00:00</published><updated>2024-11-18T07:37:17+00:00</updated><id>https://simonwillison.net/2024/Nov/18/llm-gemini-04/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/simonw/llm-gemini/releases/tag/0.4"&gt;llm-gemini 0.4&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
New release of my &lt;a href="https://github.com/simonw/llm-gemini"&gt;llm-gemini&lt;/a&gt; plugin, adding support for asynchronous models (see &lt;a href="https://simonwillison.net/2024/Nov/17/llm-018/"&gt;LLM 0.18&lt;/a&gt;), plus the new &lt;code&gt;gemini-exp-1114&lt;/code&gt; model (currently at the top of the &lt;a href="https://lmarena.ai/"&gt;Chatbot Arena&lt;/a&gt;) and a &lt;code&gt;-o json_object 1&lt;/code&gt; option to force JSON output.&lt;/p&gt;
&lt;p&gt;I also released &lt;a href="https://github.com/simonw/llm-claude-3/releases/tag/0.9"&gt;llm-claude-3 0.9&lt;/a&gt; which adds asynchronous support for the Claude family of models.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/async"&gt;async&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/google"&gt;google&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/plugins"&gt;plugins&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/projects"&gt;projects&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/python"&gt;python&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ai"&gt;ai&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/generative-ai"&gt;generative-ai&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/llms"&gt;llms&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/llm"&gt;llm&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/anthropic"&gt;anthropic&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/claude"&gt;claude&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/gemini"&gt;gemini&lt;/a&gt;&lt;/p&gt;



</summary><category term="async"/><category term="google"/><category term="plugins"/><category term="projects"/><category term="python"/><category term="ai"/><category term="generative-ai"/><category term="llms"/><category term="llm"/><category term="anthropic"/><category term="claude"/><category term="gemini"/></entry><entry><title>LLM 0.18</title><link href="https://simonwillison.net/2024/Nov/17/llm-018/#atom-tag" rel="alternate"/><published>2024-11-17T20:40:27+00:00</published><updated>2024-11-17T20:40:27+00:00</updated><id>https://simonwillison.net/2024/Nov/17/llm-018/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://llm.datasette.io/en/stable/changelog.html#v0-18"&gt;LLM 0.18&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
New release of LLM. The big new feature is &lt;a href="https://llm.datasette.io/en/stable/python-api.html#python-api-async"&gt;asynchronous model support&lt;/a&gt; - you can now use supported models in async Python code like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import llm

model = llm.get_async_model("gpt-4o")
async for chunk in model.prompt(
    "Five surprising names for a pet pelican"
):
    print(chunk, end="", flush=True)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Also new in this release: support for sending audio attachments to OpenAI's &lt;code&gt;gpt-4o-audio-preview&lt;/code&gt; model.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/async"&gt;async&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/projects"&gt;projects&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/python"&gt;python&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ai"&gt;ai&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/generative-ai"&gt;generative-ai&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/llms"&gt;llms&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/llm"&gt;llm&lt;/a&gt;&lt;/p&gt;



</summary><category term="async"/><category term="projects"/><category term="python"/><category term="ai"/><category term="generative-ai"/><category term="llms"/><category term="llm"/></entry><entry><title>Free Threaded Python With Asyncio</title><link href="https://simonwillison.net/2024/Oct/9/free-threaded-python-with-asyncio/#atom-tag" rel="alternate"/><published>2024-10-09T20:38:19+00:00</published><updated>2024-10-09T20:38:19+00:00</updated><id>https://simonwillison.net/2024/Oct/9/free-threaded-python-with-asyncio/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://blog.changs.co.uk/free-threaded-python-with-asyncio.html"&gt;Free Threaded Python With Asyncio&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Jamie Chang expanded &lt;a href="https://til.simonwillison.net/python/trying-free-threaded-python"&gt;my free-threaded Python experiment&lt;/a&gt; from a few months ago to explore the interaction between Python's &lt;code&gt;asyncio&lt;/code&gt; and the new GIL-free build of Python 3.13.&lt;/p&gt;
&lt;p&gt;The results look really promising. Jamie says:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Generally when it comes to Asyncio, the discussion around it is always about the performance or lack there of. Whilst peroformance is certain important, the ability to reason about concurrency is the biggest benefit. [...]&lt;/p&gt;
&lt;p&gt;Depending on your familiarity with AsyncIO, it might actually be the simplest way to start a thread.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;This code for running a Python function in a thread really is very pleasant to look at:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;result = await asyncio.to_thread(some_function, *args, **kwargs)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Jamie also demonstrates &lt;a href="https://docs.python.org/3/library/asyncio-task.html#task-groups"&gt;asyncio.TaskGroup&lt;/a&gt;, which makes it easy to execute a whole bunch of threads and wait for them all to finish:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;async with TaskGroup() as tg:
    for _ in range(args.tasks):
        tg.create_task(to_thread(cpu_bound_task, args.size))
&lt;/code&gt;&lt;/pre&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/async"&gt;async&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/gil"&gt;gil&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/python"&gt;python&lt;/a&gt;&lt;/p&gt;



</summary><category term="async"/><category term="gil"/><category term="python"/></entry><entry><title>aiolimiter</title><link href="https://simonwillison.net/2024/Feb/20/aiolimiter/#atom-tag" rel="alternate"/><published>2024-02-20T01:15:27+00:00</published><updated>2024-02-20T01:15:27+00:00</updated><id>https://simonwillison.net/2024/Feb/20/aiolimiter/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://aiolimiter.readthedocs.io/"&gt;aiolimiter&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
I found myself wanting an asyncio rate limiter for Python today—so I could send POSTs to an API endpoint no more than once every 10 seconds. This library worked out really well—it has a very neat design and lets you set up rate limits for things like “no more than 50 items every 10 seconds”, implemented using the leaky bucket algorithm.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/async"&gt;async&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/python"&gt;python&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/rate-limiting"&gt;rate-limiting&lt;/a&gt;&lt;/p&gt;



</summary><category term="async"/><category term="python"/><category term="rate-limiting"/></entry><entry><title>Writing a chat application in Django 4.2 using async StreamingHttpResponse, Server-Sent Events and PostgreSQL LISTEN/NOTIFY</title><link href="https://simonwillison.net/2023/May/19/chat-application-in-django/#atom-tag" rel="alternate"/><published>2023-05-19T15:42:03+00:00</published><updated>2023-05-19T15:42:03+00:00</updated><id>https://simonwillison.net/2023/May/19/chat-application-in-django/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://valberg.dk/django-sse-postgresql-listen-notify.html"&gt;Writing a chat application in Django 4.2 using async StreamingHttpResponse, Server-Sent Events and PostgreSQL LISTEN/NOTIFY&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Excellent tutorial by Víðir Valberg Guðmundsson on implementing chat with server-sent events using the newly async-capable StreamingHttpResponse from Django 4.2x.&lt;/p&gt;

&lt;p&gt;He uses PostgreSQL’a LISTEN/NOTIFY mechanism which can be used asynchronously in psycopg3—at the cost of a separate connection per user of the chat.&lt;/p&gt;

&lt;p&gt;The article also covers how to use the Last-Event-ID header to implement reconnections in server-sent events, transmitting any events that may have been missed during the time that the connection was dropped.

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="https://lobste.rs/s/qyler8/writing_chat_application_django_4_2_using"&gt;lobste.rs&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/async"&gt;async&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/django"&gt;django&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/postgresql"&gt;postgresql&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/asgi"&gt;asgi&lt;/a&gt;&lt;/p&gt;



</summary><category term="async"/><category term="django"/><category term="postgresql"/><category term="asgi"/></entry><entry><title>Django 4.2 released</title><link href="https://simonwillison.net/2023/Apr/3/django-42/#atom-tag" rel="alternate"/><published>2023-04-03T14:14:39+00:00</published><updated>2023-04-03T14:14:39+00:00</updated><id>https://simonwillison.net/2023/Apr/3/django-42/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://www.djangoproject.com/weblog/2023/apr/03/django-42-released/"&gt;Django 4.2 released&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
“This version has been designated as a long-term support (LTS) release, which means that security and data loss fixes will be applied for at least the next three years.” Some neat new async features, including improvements to async streaming responses.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/async"&gt;async&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/django"&gt;django&lt;/a&gt;&lt;/p&gt;



</summary><category term="async"/><category term="django"/></entry><entry><title>Why Rust’s postfix await syntax is good</title><link href="https://simonwillison.net/2022/May/15/why-rusts-postfix-await-syntax-is-good/#atom-tag" rel="alternate"/><published>2022-05-15T14:27:53+00:00</published><updated>2022-05-15T14:27:53+00:00</updated><id>https://simonwillison.net/2022/May/15/why-rusts-postfix-await-syntax-is-good/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://blog.ceejbot.com/posts/postfix-await/"&gt;Why Rust’s postfix await syntax is good&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
C J Silverio explains postfix await in Rust—where you can write a line like this, with the ? causing any errors to be caught and turned into an error return from your function:&lt;/p&gt;

&lt;p&gt;let count = fetch_all_animals().await?.filter_for_hedgehogs().len();

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="https://twitter.com/ceejbot/status/1525260424731455489"&gt;@ceejbot&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/async"&gt;async&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/rust"&gt;rust&lt;/a&gt;&lt;/p&gt;



</summary><category term="async"/><category term="rust"/></entry><entry><title>Weeknotes: Parallel SQL queries for Datasette, plus some middleware tricks</title><link href="https://simonwillison.net/2022/Apr/27/parallel-queries/#atom-tag" rel="alternate"/><published>2022-04-27T19:01:30+00:00</published><updated>2022-04-27T19:01:30+00:00</updated><id>https://simonwillison.net/2022/Apr/27/parallel-queries/#atom-tag</id><summary type="html">
    &lt;p&gt;A promising new performance optimization for Datasette, plus new &lt;code&gt;datasette-gzip&lt;/code&gt; and &lt;code&gt;datasette-total-page-time&lt;/code&gt; plugins.&lt;/p&gt;
&lt;h4&gt;Parallel SQL queries in Datasette&lt;/h4&gt;
&lt;p&gt;From the start of the project, Datasette has been built on top of Python's &lt;code&gt;asyncio&lt;/code&gt; capabilities - mainly to benefit things like &lt;a href="https://docs.datasette.io/en/stable/csv_export.html#streaming-all-records"&gt;streaming enormous CSV files&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;This week I started experimenting with a new way to take advantage of them, by exploring the potential to run multiple SQL queries in parallel.&lt;/p&gt;
&lt;p&gt;Consider &lt;a href="https://latest-with-plugins.datasette.io/github/commits?_facet=repo&amp;amp;_facet=committer"&gt;this Datasette table&lt;/a&gt; page:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://static.simonwillison.net/static/2022/sql-parallel-commits.jpg" alt="Screenshot of the commits table, showing a count and suggested facets and activated facets and some table data" style="max-width:100%;" /&gt;&lt;/p&gt;
&lt;p&gt;That page has to execute quite a few SQL queries:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A &lt;code&gt;select count(*) ...&lt;/code&gt; to populate the 3,283 rows heading at the top&lt;/li&gt;
&lt;li&gt;Queries against each column to decide what the "suggested facets" should be (&lt;a href="https://docs.datasette.io/en/stable/facets.html#suggested-facets"&gt;details here&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;For each of the selected facets (in this case &lt;code&gt;repos&lt;/code&gt; and &lt;code&gt;committer&lt;/code&gt;) a &lt;code&gt;select name, count(*) from ... group by name order by count(*) desc&lt;/code&gt; query&lt;/li&gt;
&lt;li&gt;The actual &lt;code&gt;select * from ... limit 101&lt;/code&gt; query used to display the actual table&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;It ends up executing more than 30 queries! Which may seem like a lot, but &lt;a href="https://www.sqlite.org/np1queryprob.html"&gt;Many Small Queries Are Efficient In SQLite&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;One thing that's interesting about the above list of queries though is that they don't actually have any dependencies on each other. There's no reason not to run all of them in parallel - later queries don't depend on the results from earlier queries.&lt;/p&gt;
&lt;p&gt;I've been exploring a fancy way of executing parallel code using pytest-style dependency injection in my &lt;a href="https://github.com/simonw/asyncinject"&gt;asyncinject&lt;/a&gt; library. But I decided to do a quick prototype to see what this would look like using &lt;a href="https://docs.python.org/3/library/asyncio-task.html#asyncio.gather"&gt;asyncio.gather()&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;It turns out that simpler approach worked surprisingly well!&lt;/p&gt;
&lt;p&gt;You can follow my research &lt;a href="https://github.com/simonw/datasette/issues/1723"&gt;in this issue&lt;/a&gt;, but the short version is that as-of a few days ago the Datasette &lt;code&gt;main&lt;/code&gt; branch runs many of the above queries in parallel.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://latest-with-plugins.datasette.io/github/commits?_facet=repo&amp;amp;_facet=committer&amp;amp;_trace=1"&gt;This trace&lt;/a&gt; (using the &lt;a href="https://datasette.io/plugins/datasette-pretty-traces"&gt;datasette-pretty-traces&lt;/a&gt; plugin) illustrates my initial results:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://static.simonwillison.net/static/2022/sql-parallel-trace.jpg" alt="Screenshot of a trace - many SQL queries have overlapping lines" style="max-width:100%;" /&gt;&lt;/p&gt;
&lt;p&gt;As you can see, the grey lines for many of those SQL queries are now overlapping.&lt;/p&gt;
&lt;p&gt;You can add the undocumented &lt;code&gt;?_noparallel=1&lt;/code&gt; query string parameter to disable parallel execution to &lt;a href="https://latest-with-plugins.datasette.io/github/commits?_facet=repo&amp;amp;_facet=committer&amp;amp;_trace=1&amp;amp;_noparallel=1"&gt;compare the difference&lt;/a&gt;:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://static.simonwillison.net/static/2022/sql-parallel-trace-noparallel.jpg" alt="Same trace again, but this time each query ends before the next one begins" style="max-width:100%;" /&gt;&lt;/p&gt;
&lt;p&gt;One thing that gives me pause: for this particular Datasette deployment (on the cheapest available Cloud Run instance) the overall performance difference between the two is very small.&lt;/p&gt;
&lt;p&gt;I need to dig into this deeper: on my laptop I feel like I'm seeing slightly better results, but definitely not conclusively. It may be that multiple cores are not being used effectively here.&lt;/p&gt;
&lt;p&gt;Datasette runs SQL queries in a pool of threads. You might expect Python's infamous GIL (Global Interpreter Lock) to prevent these from executing across multiple cores - but I checked, and &lt;a href="https://github.com/python/cpython/blob/f348154c8f8a9c254503306c59d6779d4d09b3a9/Modules/_sqlite/cursor.c#L749-L759"&gt;the GIL is released&lt;/a&gt; in Python's C code the moment control transfers to SQLite. And since SQLite can happily run multiple threads, my hunch is that this means parallel queries should be able to take advantage of multiple cores. Theoretically at least!&lt;/p&gt;
&lt;p&gt;I haven't yet figured out how to prove this though, and I'm not currently convinced that parallel queries are providing any overall benefit at all. If you have any ideas I'd love to hear them - I have a &lt;a href="https://github.com/simonw/datasette/issues/1727"&gt;research issue&lt;/a&gt; open, comments welcome!&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Update 28th April 2022:&lt;/strong&gt; Research continues, but it looks like there's little performance benefit from this. Current leading theory is that this is because of the GIL - while the SQLite C code releases the GIL, much of the activity involved in things like assembling &lt;code&gt;Row&lt;/code&gt; objects returned by a query still uses Python - so parallel queries still end up mostly blocked on a single core. Follow &lt;a href="https://github.com/simonw/datasette/issues/1727"&gt;the issue&lt;/a&gt; for more details. I started &lt;a href="https://sqlite.org/forum/forumpost/4a4b00e6d6bcf63e"&gt;a discussion on the SQLite Forum&lt;/a&gt; which has some interesting clues in it as well.&lt;/p&gt;
&lt;p&gt;Further update: It's definitely the GIL. I know because I tried running it against Sam Gross's &lt;a href="https://github.com/colesbury/nogil"&gt;nogil Python fork&lt;/a&gt; and the parallel version soundly beat the non-parallel version! Details &lt;a href="https://github.com/simonw/datasette/issues/1727#issuecomment-1112889800"&gt;in this comment&lt;/a&gt;.&lt;/p&gt;
&lt;h4 id="datasette-gzip"&gt;datasette-gzip&lt;/h4&gt;
&lt;p&gt;I've been putting off investigating gzip support for Datasette for a long time, because it's easy to add as a separate layer. If you run Datasette behind Cloudflare or an Apache or Nginx proxy configuring gzip can happen there, with very little effort and fantastic performance.&lt;/p&gt;
&lt;p&gt;Then I noticed that my Global Power Plants demo returned an HTML table page that weighed in at 420KB... but gzipped was just 16.61KB. Turns out HTML tables have a ton of repeated markup and compress REALLY well!&lt;/p&gt;
&lt;p&gt;More importantly: Google Cloud Run doesn't gzip for you. So all of my Datasette instances that were running on Cloud Run without also using Cloudflare were really suffering.&lt;/p&gt;
&lt;p&gt;So this morning I released &lt;a href="https://datasette.io/plugins/datasette-gzip"&gt;datasette-gzip&lt;/a&gt;, a plugin that gzips content if the browser sends an &lt;code&gt;Accept-Encoding: gzip&lt;/code&gt; header.&lt;/p&gt;
&lt;p&gt;The plugin is an incredibly thin wrapper around the thorougly proven-in-production &lt;a href="https://www.starlette.io/middleware/#gzipmiddleware"&gt;GZipMiddleware&lt;/a&gt;. So thin that this is &lt;a href="https://github.com/simonw/datasette-gzip/blob/0.1/datasette_gzip/__init__.py"&gt;the full implementation&lt;/a&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;span class="pl-k"&gt;from&lt;/span&gt; &lt;span class="pl-s1"&gt;datasette&lt;/span&gt; &lt;span class="pl-k"&gt;import&lt;/span&gt; &lt;span class="pl-s1"&gt;hookimpl&lt;/span&gt;
&lt;span class="pl-k"&gt;from&lt;/span&gt; &lt;span class="pl-s1"&gt;starlette&lt;/span&gt;.&lt;span class="pl-s1"&gt;middleware&lt;/span&gt;.&lt;span class="pl-s1"&gt;gzip&lt;/span&gt; &lt;span class="pl-k"&gt;import&lt;/span&gt; &lt;span class="pl-v"&gt;GZipMiddleware&lt;/span&gt;

&lt;span class="pl-en"&gt;@&lt;span class="pl-en"&gt;hookimpl&lt;/span&gt;(&lt;span class="pl-s1"&gt;trylast&lt;/span&gt;&lt;span class="pl-c1"&gt;=&lt;/span&gt;&lt;span class="pl-c1"&gt;True&lt;/span&gt;)&lt;/span&gt;
&lt;span class="pl-k"&gt;def&lt;/span&gt; &lt;span class="pl-en"&gt;asgi_wrapper&lt;/span&gt;(&lt;span class="pl-s1"&gt;datasette&lt;/span&gt;):
    &lt;span class="pl-k"&gt;return&lt;/span&gt; &lt;span class="pl-v"&gt;GZipMiddleware&lt;/span&gt;&lt;/pre&gt;
&lt;p&gt;This kind of thing is exactly why I &lt;a href="https://simonwillison.net/2019/Jun/23/datasette-asgi/"&gt;ported Datasette to ASGI&lt;/a&gt; back in 2019 - and why I continue to think that the burgeoning ASGI ecosystem is the most under-rated piece of today's Python web development environment.&lt;/p&gt;
&lt;p&gt;The &lt;a href="https://github.com/simonw/datasette-gzip/blob/0.1/tests/test_gzip.py"&gt;plugin's tests&lt;/a&gt; are a lot more interesting.&lt;/p&gt;
&lt;p&gt;That &lt;code&gt;@hookimpl(trylast=True)&lt;/code&gt; line is there to ensure that this plugin runs last, after ever other plugin has executed.&lt;/p&gt;
&lt;p&gt;This is necessary because there are existing ASGI plugins for Datasette (such as the new &lt;a href="https://datasette.io/plugins/datasette-total-page-time"&gt;datasette-total-page-time&lt;/a&gt;) which modify the generated request.&lt;/p&gt;
&lt;p&gt;If the gzip plugin runs before they do, they'll get back a blob of gzipped data rather than the HTML that they were expecting. This is likely to break them.&lt;/p&gt;
&lt;p&gt;I wanted to prove to myself that &lt;code&gt;trylast=True&lt;/code&gt; would prevent these errors - so I ended up writing &lt;a href="https://github.com/simonw/datasette-gzip/blob/0.1/tests/test_gzip.py#L83"&gt;a test&lt;/a&gt; that demonstrated that the plugin registered with &lt;code&gt;trylast=True&lt;/code&gt; was compatible with a transforming content plugin (in the test it just converts everything to uppercase) whereas &lt;code&gt;tryfirst=True&lt;/code&gt; would instead result in an error.&lt;/p&gt;
&lt;p&gt;Thankfully I have an older TIL on &lt;a href="https://til.simonwillison.net/pytest/registering-plugins-in-tests"&gt;Registering temporary pluggy plugins inside tests&lt;/a&gt; that I could lean on to help figure out how to do this.&lt;/p&gt;
&lt;p&gt;The plugin is now running on my &lt;a href="https://latest-with-plugins.datasette.io/global-power-plants/global-power-plants"&gt;latest-with-plugins demo instance&lt;/a&gt;. Since that instance loads dozens of different plugins it ends up serving a bunch of extra JavaScript and CSS, all of which benefits from gzip:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://static.simonwillison.net/static/2022/datasette-gzip-results.png" alt="Screenshot of the Firefox inspector pane showing 419KB of HTML reduced to 16.61KB and 922KB of JavaScript reduced to 187.47KB. Total of 3.08MB page weight but only 546KB transferred." style="max-width:100%;" /&gt;&lt;/p&gt;
&lt;h4&gt;datasette-total-page-time&lt;/h4&gt;
&lt;p&gt;To help understand the performance improvements introduced by parallel SQL queries I decided I wanted the Datasette footer to be able to show how long it took for the entire page to load.&lt;/p&gt;
&lt;p&gt;This is a tricky thing to do: how do you measure the total time for a page and then include it on that page if the page itself hasn't finished loading when you render that template?&lt;/p&gt;
&lt;p&gt;I came up with a pretty devious middleware trick to solve this, released as the &lt;a href="https://datasette.io/plugins/datasette-total-page-time"&gt;datasette-total-page-time&lt;/a&gt; plugin.&lt;/p&gt;
&lt;p&gt;The trick is to start a timer when the page load begins, and then end that timer at the very last possible moment as the page is being served back to the user.&lt;/p&gt;
&lt;p&gt;Then, inject the following HTML directly after the closing &lt;code&gt;&amp;lt;/html&amp;gt;&lt;/code&gt; tag (which works fine, even though it's technically invalid):&lt;/p&gt;
&lt;div class="highlight highlight-text-html-basic"&gt;&lt;pre&gt;&lt;span class="pl-kos"&gt;&amp;lt;&lt;/span&gt;&lt;span class="pl-ent"&gt;script&lt;/span&gt;&lt;span class="pl-kos"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="pl-k"&gt;let&lt;/span&gt; &lt;span class="pl-s1"&gt;footer&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-smi"&gt;document&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;querySelector&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s"&gt;"footer"&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
&lt;span class="pl-k"&gt;if&lt;/span&gt; &lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;footer&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt; &lt;span class="pl-kos"&gt;{&lt;/span&gt;
    &lt;span class="pl-k"&gt;let&lt;/span&gt; &lt;span class="pl-s1"&gt;ms&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-c1"&gt;37.224&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
    &lt;span class="pl-k"&gt;let&lt;/span&gt; &lt;span class="pl-s1"&gt;s&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s"&gt;` &amp;amp;middot; Page took &lt;span class="pl-s1"&gt;&lt;span class="pl-kos"&gt;${&lt;/span&gt;&lt;span class="pl-s1"&gt;ms&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;toFixed&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-c1"&gt;3&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;}&lt;/span&gt;&lt;/span&gt;ms`&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
    &lt;span class="pl-s1"&gt;footer&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;innerHTML&lt;/span&gt; &lt;span class="pl-c1"&gt;+=&lt;/span&gt; &lt;span class="pl-s1"&gt;s&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
&lt;span class="pl-kos"&gt;}&lt;/span&gt;
&lt;span class="pl-kos"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="pl-ent"&gt;script&lt;/span&gt;&lt;span class="pl-kos"&gt;&amp;gt;&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;This adds the timing information to the page's &lt;code&gt;&amp;lt;footer&amp;gt;&lt;/code&gt; element, if one exists.&lt;/p&gt;
&lt;p&gt;You can see this running on &lt;a href="https://latest-with-plugins.datasette.io/github/commits?_trace=1"&gt;this latest-with-plugins page&lt;/a&gt;.&lt;/p&gt;
&lt;h4&gt;Releases this week&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/simonw/datasette-gzip"&gt;datasette-gzip&lt;/a&gt;&lt;/strong&gt;: &lt;a href="https://github.com/simonw/datasette-gzip/releases/tag/0.1"&gt;0.1&lt;/a&gt; - 2022-04-27
&lt;br /&gt;Add gzip compression to Datasette&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/simonw/datasette-total-page-time"&gt;datasette-total-page-time&lt;/a&gt;&lt;/strong&gt;: &lt;a href="https://github.com/simonw/datasette-total-page-time/releases/tag/0.1"&gt;0.1&lt;/a&gt; - 2022-04-26
&lt;br /&gt;Add a note to the Datasette footer measuring the total page load time&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/simonw/asyncinject"&gt;asyncinject&lt;/a&gt;&lt;/strong&gt;: &lt;a href="https://github.com/simonw/asyncinject/releases/tag/0.5"&gt;0.5&lt;/a&gt; - (&lt;a href="https://github.com/simonw/asyncinject/releases"&gt;7 releases total&lt;/a&gt;) - 2022-04-22
&lt;br /&gt;Run async workflows using pytest-fixtures-style dependency injection&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/simonw/django-sql-dashboard"&gt;django-sql-dashboard&lt;/a&gt;&lt;/strong&gt;: &lt;a href="https://github.com/simonw/django-sql-dashboard/releases/tag/1.1"&gt;1.1&lt;/a&gt; - (&lt;a href="https://github.com/simonw/django-sql-dashboard/releases"&gt;35 releases total&lt;/a&gt;) - 2022-04-20
&lt;br /&gt;Django app for building dashboards using raw SQL queries&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/simonw/shot-scraper"&gt;shot-scraper&lt;/a&gt;&lt;/strong&gt;: &lt;a href="https://github.com/simonw/shot-scraper/releases/tag/0.13"&gt;0.13&lt;/a&gt; - (&lt;a href="https://github.com/simonw/shot-scraper/releases"&gt;14 releases total&lt;/a&gt;) - 2022-04-18
&lt;br /&gt;Tools for taking automated screenshots of websites&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;TIL this week&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://til.simonwillison.net/sphinx/blacken-docs"&gt;Format code examples in documentation with blacken-docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://til.simonwillison.net/macos/open-files-with-opensnoop"&gt;Seeing files opened by a process using opensnoop&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://til.simonwillison.net/macos/atuin"&gt;Atuin for zsh shell history in SQLite&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/async"&gt;async&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/gzip"&gt;gzip&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/python"&gt;python&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/datasette"&gt;datasette&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/weeknotes"&gt;weeknotes&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="async"/><category term="gzip"/><category term="python"/><category term="datasette"/><category term="weeknotes"/></entry><entry><title>PHP 8.1 release notes</title><link href="https://simonwillison.net/2021/Nov/25/php-81-release-notes/#atom-tag" rel="alternate"/><published>2021-11-25T19:53:30+00:00</published><updated>2021-11-25T19:53:30+00:00</updated><id>https://simonwillison.net/2021/Nov/25/php-81-release-notes/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://www.php.net/releases/8.1/en.php"&gt;PHP 8.1 release notes&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
PHP is gaining “Fibers” for lightweight cooperative concurrency—very similar to Python asyncio. Interestingly you don’t need to use separate syntax like “await fn()” to call them—calls to non-blocking functions are visually indistinguishable from calls to blocking functions. Considering how much additional library complexity has emerged in Python world from having these two different colours of functions it’s noteworthy that PHP has chosen to go in a different direction here.

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="https://news.ycombinator.com/item?id=29343773"&gt;Hacker News&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/async"&gt;async&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/php"&gt;php&lt;/a&gt;&lt;/p&gt;



</summary><category term="async"/><category term="php"/></entry><entry><title>Finding and reporting an asyncio bug in Python 3.10</title><link href="https://simonwillison.net/2021/Oct/9/finding-and-reporting-a-bug/#atom-tag" rel="alternate"/><published>2021-10-09T04:46:30+00:00</published><updated>2021-10-09T04:46:30+00:00</updated><id>https://simonwillison.net/2021/Oct/9/finding-and-reporting-a-bug/#atom-tag</id><summary type="html">
    &lt;p&gt;I found a bug in Python 3.10 today! Some notes on how I found it and my process for handling it once I figured out what was going on.&lt;/p&gt;
&lt;h4&gt;Testing Datasette against Python 3.10&lt;/h4&gt;
&lt;p&gt;I finally got around to attempting to upgrade Datasette to the just-released Python 3.10 this morning. I started by &lt;a href="https://github.com/simonw/datasette/pull/1481/commits/18a490734fc724b52cfb5a20601af3b00748af8d"&gt;adding &lt;code&gt;"3.10"&lt;/code&gt;&lt;/a&gt; to the matrix of Python versions that Datasette is tested against by GitHub Actions:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;strategy:
  matrix:
    python-version: ["3.6", "3.7", "3.8", "3.9", "3.10"]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That line previously looked like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;python-version: [3.6, 3.7, 3.8, 3.9]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I switched to using quoted strings here based on &lt;a href="https://twitter.com/webology/status/1445394072492023811"&gt;a tip from Jeff Triplett&lt;/a&gt;, who pointed out that &lt;code&gt;3.10&lt;/code&gt; is actually a floating point number in YAML syntax which will be treated as referring to Python 3.1 instead!&lt;/p&gt;
&lt;p&gt;This one-line change in a pull request was all it took to run the tests against the new version of Python... and they failed, spectacularly.&lt;/p&gt;
&lt;p&gt;Annoyingly the root error wasn't displayed directly in the tests, because it was triggered and then caught by Datasette's own error handling and turned into a 500 HTTP error. So, I needed to run Python 3.10 locally and it the debugger.&lt;/p&gt;
&lt;h4&gt;Running Python 3.10 locally&lt;/h4&gt;
&lt;p&gt;I occasionally use &lt;a href="https://github.com/pyenv/pyenv"&gt;pyenv&lt;/a&gt; to manage multiple Python versions on my machine. I have this installed through Homebrew.&lt;/p&gt;
&lt;p&gt;I had to run &lt;code&gt;brew upgrade pyenv&lt;/code&gt; first,  because my installed version didn't know about Python 3.10. Then I ran this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pyenv install 3.10.0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Which gave me a Python 3.10 installation on my Mac in &lt;code&gt;~/.pyenv/versions/3.10.0/bin/python&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Finally, I created a new virtual environment using this version of Python.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;~/.pyenv/versions/3.10.0/bin/python \
  -m venv /tmp/py310
# Now activate that virtual environment:
source /tmp/py310/bin/activate
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I put this in my &lt;code&gt;/tmp&lt;/code&gt; directory so I don't have to remember to clean it up later.&lt;/p&gt;
&lt;p&gt;Having done this, I installed my Datasette test dependencies into the new environment and used &lt;code&gt;pytest&lt;/code&gt; to run my tests:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;% cd ~/Dropbox/Development/datasette
% pip install -e '.[test]'
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Running &lt;code&gt;pytest -x --pdb&lt;/code&gt; stopped at the first failing test and dropped me into a debugger where I could finally access the full traceback, which looked something like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  File "datasette/datasette/views/base.py", line 122, in dispatch_request
    await self.ds.refresh_schemas()
  File "datasette/datasette/app.py", line 344, in refresh_schemas
    await self._refresh_schemas()
  File "datasette/datasette/app.py", line 349, in _refresh_schemas
    await init_internal_db(internal_db)
  File "datasette/datasette/utils/internal_db.py", line 5, in init_internal_db
    await db.execute_write(
  File "datasette/datasette/database.py", line 102, in execute_write
    return await self.execute_write_fn(_inner, block=block)
  File "datasette/datasette/database.py", line 113, in execute_write_fn
    reply_queue = janus.Queue()
  File "janus/__init__.py", line 39, in __init__
    self._async_not_empty = asyncio.Condition(self._async_mutex)
  File "lib/python3.10/asyncio/locks.py", line 234, in __init__
    raise ValueError("loop argument must agree with lock")
ValueError: loop argument must agree with lock
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;So, something is going long with &lt;code&gt;asyncio.Condition()&lt;/code&gt;, which is being called by &lt;code&gt;janus.Queue()&lt;/code&gt;.&lt;/p&gt;
&lt;h4&gt;Some background on Janus&lt;/h4&gt;
&lt;p&gt;The &lt;a href="https://github.com/aio-libs/janus"&gt;Janus&lt;/a&gt; library referenced here describes itself as a "Thread-safe asyncio-aware queue for Python".&lt;/p&gt;
&lt;p&gt;I use it in Datasette for the &lt;a href="https://github.com/simonw/datasette/issues/682"&gt;write queue&lt;/a&gt; - a mechanism where writes can be safely made to a SQLite database by first queueing them up in memory.&lt;/p&gt;
&lt;p&gt;Janus provides a mechanism that lets asyncio event tasks send and receive messages to Python threads, and vice-versa, via a queue class that is modelled on Python's own standard library queues. It's really neat.&lt;/p&gt;
&lt;h4&gt;Tracking the investigation in issue comments&lt;/h4&gt;
&lt;p&gt;Any time I'm investigating a bug I make sure that there's an associated GitHub issue or pull request, to give me somewhere to build up detailed notes of my investigation.&lt;/p&gt;
&lt;p&gt;I used my &lt;a href="https://github.com/simonw/datasette/pull/1481"&gt;PR #1481&lt;/a&gt; for this. Now that I had an error message - "loop argument must agree with lock" - I did some initial Google searches and found to my surprise that it had very few existing mentions - my first clue that this might be related to the new Python 3.10 release itself.&lt;/p&gt;
&lt;p&gt;I tracked down the relevant source code in both Python and the Janus library based on the traceback and linked to them from the issue comments. I pasted in copies of the relevant code too, since GitHub only magically embeds code from the same repository, not code from other repositories. You can see &lt;a href="https://github.com/simonw/datasette/pull/1481#issuecomment-939078872"&gt;those comments here&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;I always like to include copies of the code most likely to be the cause of a bug in an issue comment, to save myself from having to dig around for it again later on and to act as a historical record once the bug has been fixed.&lt;/p&gt;
&lt;p&gt;I also started an issue in the Janus repo, with the title &lt;a href="https://github.com/aio-libs/janus/issues/358"&gt;Error with Python 3.10: "ValueError: loop argument must agree with lock"&lt;/a&gt;. I linked this to my own issue and posted relevant research in both places.&lt;/p&gt;
&lt;p&gt;The two lines of code in Janus that caused the bug where these ones (note my added comment):&lt;/p&gt;
&lt;pre&gt;&lt;span class="pl-s1"&gt;self&lt;/span&gt;.&lt;span class="pl-s1"&gt;_async_mutex&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s1"&gt;asyncio&lt;/span&gt;.&lt;span class="pl-v"&gt;Lock&lt;/span&gt;()
&lt;span class="pl-c"&gt;# "loop argument must agree with lock" exception is raised here:&lt;/span&gt;
&lt;span class="pl-s1"&gt;self&lt;/span&gt;.&lt;span class="pl-s1"&gt;_async_not_empty&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s1"&gt;asyncio&lt;/span&gt;.&lt;span class="pl-v"&gt;Condition&lt;/span&gt;(&lt;span class="pl-s1"&gt;self&lt;/span&gt;.&lt;span class="pl-s1"&gt;_async_mutex&lt;/span&gt;)&lt;/pre&gt;
&lt;p&gt;This is where I began to suspect a Python bug. The above code can be simplified to this:&lt;/p&gt;
&lt;pre&gt;&lt;span class="pl-s1"&gt;asyncio&lt;/span&gt;.&lt;span class="pl-v"&gt;Condition&lt;/span&gt;(&lt;span class="pl-s1"&gt;asyncio&lt;/span&gt;.&lt;span class="pl-v"&gt;Lock&lt;/span&gt;())&lt;/pre&gt;
&lt;p&gt;This is &lt;a href="https://docs.python.org/3/library/asyncio-sync.html#asyncio.Condition"&gt;a documented way&lt;/a&gt; of using conditions in Python: you can instantiate conditions with an optional lock, which allows that lock to be shared by more than one condition.&lt;/p&gt;
&lt;p&gt;So I tried that in the Python 3.10 interpreter... and it didn't throw the error:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;% ~/.pyenv/versions/3.10.0/bin/python
Python 3.10.0 (default, Oct  7 2021, 13:45:58) [Clang 12.0.0 (clang-1200.0.32.29)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
&amp;gt;&amp;gt;&amp;gt; import asyncio
&amp;gt;&amp;gt;&amp;gt; print(asyncio.Condition(asyncio.Lock()))
&amp;lt;asyncio.locks.Condition object at 0x1062521d0 [unlocked]&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;From browsing the source code I could see that there was something going on here involving the event loop - so on a hunch I tried running that code in an event loop instead, like so:&lt;/p&gt;
&lt;div class="highlight highlight-text-python-console"&gt;&lt;pre&gt;&amp;gt;&amp;gt;&amp;gt; &lt;span class="pl-k"&gt;async&lt;/span&gt; &lt;span class="pl-k"&gt;def&lt;/span&gt; &lt;span class="pl-en"&gt;example&lt;/span&gt;():
...     &lt;span class="pl-c1"&gt;print&lt;/span&gt;(asyncio.Condition(asyncio.Lock()))
... 
&amp;gt;&amp;gt;&amp;gt; asyncio.run(example())
Traceback (most recent call last):
  ...
  File "&amp;lt;stdin&amp;gt;", line 2, in example
  File "/Users/simon/.pyenv/versions/3.10.0/lib/python3.10/asyncio/locks.py", line 234, in __init__
    raise ValueError("loop argument must agree with lock")
ValueError: loop argument must agree with lock&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;There's the exception!&lt;/p&gt;
&lt;p&gt;So it looked like this might be a genuine Python 3.10 bug. After all, I knew that this code worked in prior versions of Python (since Janus and Datasette worked fine there). In fact I could confirm it like so:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;~ % python3.9
Python 3.9.7 (default, Sep  3 2021, 12:45:31) 
[Clang 12.0.0 (clang-1200.0.32.29)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
&amp;gt;&amp;gt;&amp;gt; import asyncio
&amp;gt;&amp;gt;&amp;gt; async def example():
...     print(asyncio.Condition(asyncio.Lock()))
... 
&amp;gt;&amp;gt;&amp;gt; asyncio.run(example())
&amp;lt;asyncio.locks.Condition object at 0x1049aab50 [unlocked]&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Reporting the bug to the Python issue tracker&lt;/h4&gt;
&lt;p&gt;Python bugs are tracked on &lt;a href="https://bugs.python.org/"&gt;bugs.python.org&lt;/a&gt; - thankfully they support sign in with GitHub (and Google and even OpenID) so I signed in with my GitHub account to report the bug.&lt;/p&gt;
&lt;p&gt;I filed &lt;a href="https://bugs.python.org/issue45416"&gt;Issue 45416&lt;/a&gt; entitled:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;"loop argument must agree with lock" instantiating asyncio.Condition&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;And included a brief description with my minimal Python script that demonstrated the bug.&lt;/p&gt;
&lt;p&gt;Then I added a comment pointing back to one of the related GitHub issues where I had been figuring this out, and another comment that linked to &lt;a href="https://github.com/python/cpython/blob/a1092f62492a3fcd6195bea94eccf8d5a300acb1/Lib/test/test_asyncio/test_locks.py#L722-L727"&gt;the most relevant test&lt;/a&gt; in Python's own test suite, which I found using GitHub search for the term &lt;code&gt;Condition&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Any time I file an issue against a project - my own or someone else's - I always like to do a bit of extra research and link to both the likely code at fault and the likely test.&lt;/p&gt;
&lt;p&gt;As a maintainer, this saves me a bunch of time - if the issue links straight to the code and tests I don't have to spend any time digging around looking for them.&lt;/p&gt;
&lt;p&gt;My hope is that this saves time for the people I am reporting the bug to as well, which increases the chance that they'll dig into it and help me find a fix!&lt;/p&gt;
&lt;p&gt;Then I tweeted about it. I have a bunch of followers with relevant experience, so I thought it was time to get a few more eyes on the problem.&lt;/p&gt;
&lt;blockquote class="twitter-tweet"&gt;&lt;p lang="en" dir="ltr"&gt;I think I /may/ have found a bug in Python 3.10, which is exciting!&lt;br /&gt;&lt;br /&gt;asyncio.Condition(asyncio.Lock()) raises a ValueError error if executed while an event loop is running&lt;br /&gt;&lt;br /&gt;Means Datasette doesn't work on 3.10 (yet)&lt;a href="https://t.co/ofd9MmzuJS"&gt;https://t.co/ofd9MmzuJS&lt;/a&gt; &lt;a href="https://t.co/ICQz8lf39M"&gt;pic.twitter.com/ICQz8lf39M&lt;/a&gt;&lt;/p&gt;- Simon Willison (@simonw) &lt;a href="https://twitter.com/simonw/status/1446576515940904962?ref_src=twsrc%5Etfw"&gt;October 8, 2021&lt;/a&gt;&lt;/blockquote&gt;

&lt;p&gt;I included a screenshot of my steps-to-reproduce script to save anyone who might be casually interested from having to click through to the issue itself.&lt;/p&gt;
&lt;h4&gt;Łukasz Langa to the rescue&lt;/h4&gt;
&lt;p&gt;Łukasz is the "CPython Developer in Residence", which means he is paid by the Python Software Foundation to work on Python full-time.&lt;/p&gt;
&lt;p&gt;Less than an hour after I filed the issue he &lt;a href="https://bugs.python.org/msg403506"&gt;posted a comment&lt;/a&gt; that both confirmed the bug and provided two short-term workarounds for it!&lt;/p&gt;
&lt;p&gt;I really liked his first workaround, which looks like this:&lt;/p&gt;
&lt;div class="highlight highlight-text-python-console"&gt;&lt;pre&gt;&amp;gt;&amp;gt;&amp;gt; l &lt;span class="pl-k"&gt;=&lt;/span&gt; asyncio.Lock()
&amp;gt;&amp;gt;&amp;gt; &lt;span class="pl-c1"&gt;getattr&lt;/span&gt;(l, &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;'&lt;/span&gt;_get_loop&lt;span class="pl-pds"&gt;'&lt;/span&gt;&lt;/span&gt;, &lt;span class="pl-k"&gt;lambda&lt;/span&gt;: &lt;span class="pl-c1"&gt;None&lt;/span&gt;)()
&amp;lt;_UnixSelectorEventLoop running=True closed=False debug=False&amp;gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;It works by calling the &lt;code&gt;lock._get_loop()&lt;/code&gt; method, but only if it exists - so even though that's an undocumented internal method this should be safe even when run against future Python versions that might remove the method.&lt;/p&gt;
&lt;p&gt;You can see &lt;a href="https://github.com/python/cpython/blob/v3.10.0/Lib/asyncio/mixins.py#L22-L31"&gt;that method here&lt;/a&gt; - it safely populates the &lt;code&gt;self._loop&lt;/code&gt; property, which helps work around the bug.&lt;/p&gt;
&lt;h4&gt;Submitting a PR to Janus&lt;/h4&gt;
&lt;p&gt;The best place to apply this workaround is to Janus - so I submitted a PR there which adds the workaround and updates their CI configuration to test against Python 3.10: &lt;a href="https://github.com/aio-libs/janus/pull/359"&gt;Janus PR #359&lt;/a&gt;. The GitHub Actions tests for that PR are now passing against Python 3.10.&lt;/p&gt;
&lt;p&gt;Their README &lt;a href="https://github.com/aio-libs/janus#communication-channels"&gt;links to a gitter chat&lt;/a&gt; so I dropped a link to my PR in there too. Hopefully that can be merged soon!&lt;/p&gt;
&lt;h4&gt;What I should have done&lt;/h4&gt;
&lt;p&gt;I'm feeling pretty good about this situation - the bug is set to be fixed, we have a great workaround thanks to Łukasz and I'm optimistic that the upstream library from Datasette will land the fix soon.&lt;/p&gt;
&lt;p&gt;If only I'd done this a few weeks earlier!&lt;/p&gt;
&lt;p&gt;The &lt;a href="https://www.python.org/dev/peps/pep-0619/#schedule"&gt;Python 3.10 release schedule&lt;/a&gt; shipped the first alpha a year ago, in October 2020. It totalled 7 alphas, 4 betas and 2 release candidates before the final 3.10 release on Monday 4th October 2021.&lt;/p&gt;
&lt;p&gt;If I'd taken the time to get Datasette's test suite running against Python 3.10 for any of those pre-releases, I could have helped spot this bug before it landed in the final release!&lt;/p&gt;
&lt;p&gt;So lesson learned: Python has alphas and betas and RCs for a reason. For future Pythons I'm going to pay attention to testing against these for my projects before the final release lands.&lt;/p&gt;
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/async"&gt;async&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/debugging"&gt;debugging&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/python"&gt;python&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/weeknotes"&gt;weeknotes&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/github-actions"&gt;github-actions&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="async"/><category term="debugging"/><category term="python"/><category term="weeknotes"/><category term="github-actions"/></entry><entry><title>Introducing Partytown 🎉: Run Third-Party Scripts From a Web Worker</title><link href="https://simonwillison.net/2021/Sep/23/partytown/#atom-tag" rel="alternate"/><published>2021-09-23T18:29:14+00:00</published><updated>2021-09-23T18:29:14+00:00</updated><id>https://simonwillison.net/2021/Sep/23/partytown/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://dev.to/adamdbradley/introducing-partytown-run-third-party-scripts-from-a-web-worker-2cnp"&gt;Introducing Partytown 🎉: Run Third-Party Scripts From a Web Worker&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
This is just spectacularly clever. Partytown is a 6KB JavaScript library that helps you move gnarly poorly performing third-party scripts out of your main page and into a web worker, so they won’t destroy your page performance. The really clever bit is in how it provides sandboxed access to the page DOM: it uses a devious trick where a proxy object provides getters and setters which then make blocking API calls to a separate service worker, using the mostly-forgotten xhr.open(..., false) parameter that turns off the async default for an XMLHttpRequest call.

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="https://twitter.com/Rich_Harris/status/1441101927798820869"&gt;@Rich_Harris&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/async"&gt;async&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/javascript"&gt;javascript&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/webworkers"&gt;webworkers&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/service-workers"&gt;service-workers&lt;/a&gt;&lt;/p&gt;



</summary><category term="async"/><category term="javascript"/><category term="webworkers"/><category term="service-workers"/></entry><entry><title>Quoting Using async and await in Flask 2.0</title><link href="https://simonwillison.net/2021/May/12/using-async-and-await/#atom-tag" rel="alternate"/><published>2021-05-12T17:59:51+00:00</published><updated>2021-05-12T17:59:51+00:00</updated><id>https://simonwillison.net/2021/May/12/using-async-and-await/#atom-tag</id><summary type="html">
    &lt;blockquote cite="https://flask.palletsprojects.com/en/2.0.x/async-await/"&gt;&lt;p&gt;Async functions require an event loop to run. Flask, as a WSGI application, uses one worker to handle one request/response cycle. When a request comes in to an async view, Flask will start an event loop in a thread, run the view function there, then return the result.&lt;/p&gt;
&lt;p&gt;Each request still ties up one worker, even for async views. The upside is that you can run async code within a view, for example to make multiple concurrent database queries, HTTP requests to an external API, etc. However, the number of requests your application can handle at one time will remain the same.&lt;/p&gt;&lt;/blockquote&gt;
&lt;p class="cite"&gt;&amp;mdash; &lt;a href="https://flask.palletsprojects.com/en/2.0.x/async-await/"&gt;Using async and await in Flask 2.0&lt;/a&gt;&lt;/p&gt;

    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/async"&gt;async&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/flask"&gt;flask&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/python"&gt;python&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/wsgi"&gt;wsgi&lt;/a&gt;&lt;/p&gt;



</summary><category term="async"/><category term="flask"/><category term="python"/><category term="wsgi"/></entry><entry><title>New Major Versions Released! Flask 2.0, Werkzeug 2.0, Jinja 3.0, Click 8.0, ItsDangerous 2.0, and MarkupSafe 2.0</title><link href="https://simonwillison.net/2021/May/12/pallets/#atom-tag" rel="alternate"/><published>2021-05-12T17:37:14+00:00</published><updated>2021-05-12T17:37:14+00:00</updated><id>https://simonwillison.net/2021/May/12/pallets/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://palletsprojects.com/blog/flask-2-0-released/"&gt;New Major Versions Released! Flask 2.0, Werkzeug 2.0, Jinja 3.0, Click 8.0, ItsDangerous 2.0, and MarkupSafe 2.0&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Huge set of releases from the Pallets team. Python 3.6+ required and comprehensive type annotations. Flask now supports async views, Jinja async templates (used extensively by Datasette) “no longer requires patching”, Click has a bunch of new code around shell tab completion, ItsDangerous supports key rotation and so much more.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/async"&gt;async&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/flask"&gt;flask&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/jinja"&gt;jinja&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/python"&gt;python&lt;/a&gt;&lt;/p&gt;



</summary><category term="async"/><category term="flask"/><category term="jinja"/><category term="python"/></entry><entry><title>unasync</title><link href="https://simonwillison.net/2021/Feb/27/unasync/#atom-tag" rel="alternate"/><published>2021-02-27T22:20:08+00:00</published><updated>2021-02-27T22:20:08+00:00</updated><id>https://simonwillison.net/2021/Feb/27/unasync/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/python-trio/unasync"&gt;unasync&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Today I started wondering out loud if one could write code that takes an asyncio Python library and transforms it into the synchronous equivalent by using some regular expressions to strip out the “await ...” keywords and suchlike. Turns out that can indeed work, and Ratan Kulshreshtha built it! unasync uses the standard library tokenize module to run some transformations against an async library and spit out the sync version automatically. I’m now considering using this for sqlite-utils.

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="https://twitter.com/simonw/status/1365784007909142528"&gt;@simonw&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/async"&gt;async&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/python"&gt;python&lt;/a&gt;&lt;/p&gt;



</summary><category term="async"/><category term="python"/></entry><entry><title>datasette-ripgrep: deploy a regular expression search engine for your source code</title><link href="https://simonwillison.net/2020/Nov/28/datasette-ripgrep/#atom-tag" rel="alternate"/><published>2020-11-28T06:51:06+00:00</published><updated>2020-11-28T06:51:06+00:00</updated><id>https://simonwillison.net/2020/Nov/28/datasette-ripgrep/#atom-tag</id><summary type="html">
    &lt;p&gt;This week I built &lt;a href="https://github.com/simonw/datasette-ripgrep"&gt;datasette-ripgrep&lt;/a&gt; - a web application  for running regular expression searches against source code, built on top of the amazing &lt;a href="https://github.com/BurntSushi/ripgrep"&gt;ripgrep&lt;/a&gt; command-line tool.&lt;/p&gt;
&lt;h4&gt;datasette-ripgrep demo&lt;/h4&gt;
&lt;p&gt;I've deployed a demo version of the application here:&lt;/p&gt;
&lt;p&gt;&lt;a href="https://ripgrep.datasette.io/-/ripgrep?pattern=pytest"&gt;ripgrep.datasette.io/-/ripgrep?pattern=pytest&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;The demo runs searches against the source code of every one of my GitHub repositories that start with &lt;code&gt;datasette&lt;/code&gt; - &lt;a href="https://github-to-sqlite.dogsheep.net/github/repos?name__startswith=datasette&amp;amp;owner__exact=9599"&gt;61 repos&lt;/a&gt; right now - so it should include all of my Datasette plugins plus the core Datasette repository itself.&lt;/p&gt;
&lt;p&gt;Since it's running on top of &lt;code&gt;ripgrep&lt;/code&gt;, it supports regular expressions. This is absurdly useful. Some examples:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Every usage of the &lt;code&gt;.plugin_config(&lt;/code&gt; method: &lt;a href="https://ripgrep.datasette.io/-/ripgrep?pattern=%5C.plugin_config%5C%28"&gt;plugin_config\(&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Everywhere I use &lt;code&gt;async with httpx.AsyncClient&lt;/code&gt; (usually in tests): &lt;a href="https://ripgrep.datasette.io/-/ripgrep?pattern=async+with.*AsyncClient"&gt;async with.*AsyncClient&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;All places where I use a Jinja &lt;code&gt;|&lt;/code&gt; filter inside a variable: &lt;a href="https://ripgrep.datasette.io/-/ripgrep?pattern=%5C%7B%5C%7B.*%5C%7C.*%5C%7D%5C%7D"&gt;\{\{.*\|.*\}\}&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I usually run ripgrep as &lt;code&gt;rg&lt;/code&gt; on the command-line, or use it within Visual Studio Code (&lt;a href="https://twitter.com/simonw/status/1331381448171929600"&gt;fun fact&lt;/a&gt;: the reason VS Code's "Find in Files" is so good is it's running ripgrep under the hood).&lt;/p&gt;
&lt;p&gt;So why have it as a web application? Because this means I can link to it, bookmark it and use it on my phone.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://static.simonwillison.net/static/2020/datasette-ripgrep.png" alt="A screenshot of datasette-ripgrep in action" style="max-width: 100%" /&gt;&lt;/p&gt;
&lt;h4&gt;Why build this?&lt;/h4&gt;
&lt;p&gt;There are plenty of great existing code search tools out there already: I've heard great things about &lt;a href="https://github.com/livegrep/livegrep"&gt;livegrep&lt;/a&gt;, and a quick Google search shows a bunch of other options.&lt;/p&gt;
&lt;p&gt;Aside from being a fun project, &lt;code&gt;datasette-ripgrep&lt;/code&gt; has one key advantage: it gets to benefit from Datasette's publishing mechanism, which means it's really easy to deploy.&lt;/p&gt;
&lt;p&gt;That &lt;a href="https://ripgrep.datasette.io/"&gt;ripgrep.datasette.io&lt;/a&gt; demo is deployed by checking out the source code to be searched into a &lt;code&gt;all&lt;/code&gt; directory and then using the following command:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;datasette publish cloudrun \
    --metadata metadata.json \
    --static all:all \
    --install=datasette-ripgrep \
    --service datasette-ripgrep \
    --apt-get-install ripgrep
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;all&lt;/code&gt; is a folder containing the source code to be searched. &lt;code&gt;metadata.json&lt;/code&gt; contains this:&lt;/p&gt;
&lt;div class="highlight highlight-source-json"&gt;&lt;pre&gt;{
    &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;plugins&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;: {
        &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;datasette-ripgrep&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;: {
            &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;path&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;: &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;/app/all&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;,
            &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;time_limit&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;: &lt;span class="pl-c1"&gt;3.0&lt;/span&gt;
        }
    }
}&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;That's all there is to it! The result is a deployed code search engine, running on Google Cloud Run.&lt;/p&gt;
&lt;p&gt;(If you want to try this yourself you'll need to be using the just-released Datasette 0.52.)&lt;/p&gt;
&lt;p&gt;The &lt;a href="https://github.com/simonw/datasette-ripgrep/blob/main/.github/workflows/deploy_demo.yml"&gt;GitHub Action workflow&lt;/a&gt; that deploys the demo also uses my &lt;a href="https://github.com/dogsheep/github-to-sqlite"&gt;github-to-sqlite&lt;/a&gt; tool to fetch my repos and then shallow-clones the ones that begin with &lt;code&gt;datasette&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;If you have &lt;a href="https://docs.datasette.io/en/stable/publish.html#publishing-to-google-cloud-run"&gt;your own Google Cloud Run credentials&lt;/a&gt;, you can run your own copy of that workflow against your own repositories.&lt;/p&gt;
&lt;h4&gt;A different kind of Datasette plugin&lt;/h4&gt;
&lt;p&gt;Datasette is a tool for publishing SQLite databases, so most Datasette plugins integrate with SQLite in some way.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;datasette-ripgrep&lt;/code&gt; is different: it makes no use of SQLite at all, but instead takes advantage of Datasette's URL routing, &lt;code&gt;datasette publish&lt;/code&gt; deployments and permissions system.&lt;/p&gt;
&lt;p&gt;The plugin implementation is currently &lt;a href="https://github.com/simonw/datasette-ripgrep/blob/07b9ced2935b0b6080c1c42fcaf6ab9e8003d186/datasette_ripgrep/__init__.py"&gt;134 lines of code&lt;/a&gt;, excluding tests and templates.&lt;/p&gt;
&lt;p&gt;While the plugin doesn't use SQLite, it does share a common philosophy with Datasette: the plugin bundles the source code that it is going to search as part of the deployed application, in a similar way to how Datasette usually bundles one or more SQLite database files.&lt;/p&gt;
&lt;p&gt;As such, it's extremely inexpensive to run and can be deployed to serverless hosting. If you need to scale it, you can run more copies.&lt;/p&gt;
&lt;p&gt;This does mean that the application needs to be re-deployed to pick up changes to the searchable code. I'll probably set my demo to do this on a daily basis.&lt;/p&gt;
&lt;h4&gt;Controlling processes from asyncio&lt;/h4&gt;
&lt;p&gt;The trickiest part of the implementation was figuring out how to use Python's &lt;code&gt;asyncio.create_subprocess_exec()&lt;/code&gt; method to safely run the &lt;code&gt;rg&lt;/code&gt; process in response to incoming requests.&lt;/p&gt;
&lt;p&gt;I don't want expensive searches to tie up the server, so I implemented two limits here. The first is a time limit: by default, searches have a second to run after which the &lt;code&gt;rg&lt;/code&gt; process will be terminated and only results recieved so far will be returned. This is achieved using the &lt;a href="https://docs.python.org/3/library/asyncio-task.html#asyncio.wait_for"&gt;asyncio.wait_for()&lt;/a&gt; function.&lt;/p&gt;
&lt;p&gt;I also implemented a limit on the number of matching lines that can be returned, defaulting to 2,000. Any more than that and the process is terminated early.&lt;/p&gt;
&lt;p&gt;Both of these limits can be customized using plugin settings (documented in &lt;a href="https://github.com/simonw/datasette-ripgrep/blob/main/README.md"&gt;the README&lt;/a&gt;). You can see how they are implemented in the &lt;a href="https://github.com/simonw/datasette-ripgrep/blob/0.2/datasette_ripgrep/__init__.py#L9-L55"&gt;async def run_ripgrep(pattern, path, time_limit=1.0, max_lines=2000)&lt;/a&gt; function.&lt;/p&gt;
&lt;h4&gt;Highlighted linkable line numbers&lt;/h4&gt;
&lt;p&gt;The other fun implementation detail is the way the source code listings are displayed. I'm using CSS to display the line numbers in a way that makes them visible without them breaking copy-and-paste (inspired by &lt;a href="https://www.sylvaindurand.org/using-css-to-add-line-numbering/"&gt;this article by Sylvain Durand&lt;/a&gt;).&lt;/p&gt;
&lt;div class="highlight highlight-source-css"&gt;&lt;pre&gt;&lt;span class="pl-ent"&gt;code&lt;/span&gt;:&lt;span class="pl-c1"&gt;before&lt;/span&gt; {
    &lt;span class="pl-c1"&gt;content&lt;/span&gt;: &lt;span class="pl-en"&gt;attr&lt;/span&gt;(data-line);
    &lt;span class="pl-c1"&gt;display&lt;/span&gt;: inline-block;
    &lt;span class="pl-c1"&gt;width&lt;/span&gt;: &lt;span class="pl-c1"&gt;3.5&lt;span class="pl-smi"&gt;ch&lt;/span&gt;&lt;/span&gt;;
    &lt;span class="pl-c1"&gt;-webkit-user-select&lt;/span&gt;: none;
    &lt;span class="pl-c1"&gt;color&lt;/span&gt;: &lt;span class="pl-pds"&gt;&lt;span class="pl-kos"&gt;#&lt;/span&gt;666&lt;/span&gt;;
}&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The HTML looks like this:&lt;/p&gt;
&lt;div class="highlight highlight-text-html-basic"&gt;&lt;pre&gt;&lt;span class="pl-kos"&gt;&amp;lt;&lt;/span&gt;&lt;span class="pl-ent"&gt;pre&lt;/span&gt;&lt;span class="pl-kos"&gt;&amp;gt;&lt;/span&gt;&lt;span class="pl-kos"&gt;&amp;lt;&lt;/span&gt;&lt;span class="pl-ent"&gt;code&lt;/span&gt; &lt;span class="pl-c1"&gt;id&lt;/span&gt;="&lt;span class="pl-s"&gt;L1&lt;/span&gt;" &lt;span class="pl-c1"&gt;data-line&lt;/span&gt;="&lt;span class="pl-s"&gt;1&lt;/span&gt;"&lt;span class="pl-kos"&gt;&amp;gt;&lt;/span&gt;from setuptools import setup&lt;span class="pl-kos"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="pl-ent"&gt;code&lt;/span&gt;&lt;span class="pl-kos"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="pl-kos"&gt;&amp;lt;&lt;/span&gt;&lt;span class="pl-ent"&gt;code&lt;/span&gt; &lt;span class="pl-c1"&gt;id&lt;/span&gt;="&lt;span class="pl-s"&gt;L2&lt;/span&gt;" &lt;span class="pl-c1"&gt;data-line&lt;/span&gt;="&lt;span class="pl-s"&gt;2&lt;/span&gt;"&lt;span class="pl-kos"&gt;&amp;gt;&lt;/span&gt;import os&lt;span class="pl-kos"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="pl-ent"&gt;code&lt;/span&gt;&lt;span class="pl-kos"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="pl-kos"&gt;&amp;lt;&lt;/span&gt;&lt;span class="pl-ent"&gt;code&lt;/span&gt; &lt;span class="pl-c1"&gt;id&lt;/span&gt;="&lt;span class="pl-s"&gt;L3&lt;/span&gt;" &lt;span class="pl-c1"&gt;data-line&lt;/span&gt;="&lt;span class="pl-s"&gt;3&lt;/span&gt;"&lt;span class="pl-kos"&gt;&amp;gt;&lt;/span&gt;&amp;amp;nbsp;&lt;span class="pl-kos"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="pl-ent"&gt;code&lt;/span&gt;&lt;span class="pl-kos"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="pl-kos"&gt;&amp;lt;&lt;/span&gt;&lt;span class="pl-ent"&gt;code&lt;/span&gt; &lt;span class="pl-c1"&gt;id&lt;/span&gt;="&lt;span class="pl-s"&gt;L4&lt;/span&gt;" &lt;span class="pl-c1"&gt;data-line&lt;/span&gt;="&lt;span class="pl-s"&gt;4&lt;/span&gt;"&lt;span class="pl-kos"&gt;&amp;gt;&lt;/span&gt;VERSION = &amp;amp;#34;0.1&amp;amp;#34;&lt;span class="pl-kos"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="pl-ent"&gt;code&lt;/span&gt;&lt;span class="pl-kos"&gt;&amp;gt;&lt;/span&gt;
...&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;I wanted to imitate GitHub's handling of line links, where adding &lt;code&gt;#L23&lt;/code&gt; to the URL both jumps to that line and causes the line to be highlighted. Here's &lt;a href="https://ripgrep.datasette.io/-/ripgrep/view/datasette-allow-permissions-debug/setup.py#L23"&gt;a demo of that&lt;/a&gt; - I use the following JavaScript to update the contents of a &lt;code&gt;&amp;lt;style id="highlightStyle"&amp;gt;&amp;lt;/style&amp;gt;&lt;/code&gt; element in the document head any time the URL fragment changes:&lt;/p&gt;
&lt;div class="highlight highlight-source-js"&gt;&lt;pre&gt;&lt;span class="pl-c1"&gt;&amp;lt;&lt;/span&gt;&lt;span class="pl-ent"&gt;script&lt;/span&gt;&lt;span class="pl-c1"&gt;&amp;gt;&lt;/span&gt;
var highlightStyle = document.getElementById('highlightStyle');
function highlightLineFromFragment() &lt;span class="pl-kos"&gt;{&lt;/span&gt;
    &lt;span class="pl-en"&gt;if&lt;/span&gt; &lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-pds"&gt;/&lt;span class="pl-cce"&gt;^&lt;/span&gt;#L&lt;span class="pl-cce"&gt;\d&lt;/span&gt;&lt;span class="pl-c1"&gt;+&lt;/span&gt;&lt;span class="pl-cce"&gt;$&lt;/span&gt;/&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;exec&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;location&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;hash&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;&lt;/span&gt; &lt;span class="pl-kos"&gt;{&lt;/span&gt;
        &lt;span class="pl-s1"&gt;highlightStyle&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;innerText&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s"&gt;`&lt;span class="pl-s1"&gt;&lt;span class="pl-kos"&gt;${&lt;/span&gt;&lt;span class="pl-s1"&gt;location&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;hash&lt;/span&gt;&lt;span class="pl-kos"&gt;}&lt;/span&gt;&lt;/span&gt; { background-color: yellow; }`&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
    &lt;span class="pl-kos"&gt;}&lt;/span&gt;
&lt;span class="pl-kos"&gt;}&lt;/span&gt;
&lt;span class="pl-en"&gt;highlightLineFromFragment&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
&lt;span class="pl-smi"&gt;window&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;addEventListener&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s"&gt;"hashchange"&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt; &lt;span class="pl-s1"&gt;highlightLineFromFragment&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
&lt;span class="pl-c1"&gt;&amp;lt;&lt;/span&gt;/&lt;span class="pl-ent"&gt;script&lt;/span&gt;&lt;span class="pl-c1"&gt;&amp;gt;&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;It's the simplest way I could think of to achieve this effect.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Update 28th November 2020&lt;/strong&gt;: Louis Lévêque on Twitter suggested using the CSS &lt;a href="https://developer.mozilla.org/en-US/docs/Web/CSS/:target"&gt;:target selector&lt;/a&gt; instead, which is indeed MUCH simpler - I deleted the above JavaScript and replaced it with this CSS:&lt;/p&gt;
&lt;div class="highlight highlight-source-css"&gt;&lt;pre&gt;:&lt;span class="pl-c1"&gt;target&lt;/span&gt; {
    &lt;span class="pl-c1"&gt;background-color&lt;/span&gt;: &lt;span class="pl-pds"&gt;&lt;span class="pl-kos"&gt;#&lt;/span&gt;FFFF99&lt;/span&gt;;
}&lt;/pre&gt;&lt;/div&gt;
&lt;h4&gt;Next steps for this project&lt;/h4&gt;
&lt;p&gt;I'm pleased to have got &lt;a href=""&gt;datasette-ripgrep&lt;/a&gt; to a workable state, and I'm looking forward to using it to answer questions about the growing Datasette ecosystem. I don't know how much more time I'll invest in this - if it proves useful then I may well expand it.&lt;/p&gt;
&lt;p&gt;I do think there's something really interesting about being able to spin up this kind of code search engine on demand using &lt;code&gt;datasette publish&lt;/code&gt;. It feels like a very useful trick to have access to.&lt;/p&gt;
&lt;h4&gt;Better URLs for my TILs&lt;/h4&gt;
&lt;p&gt;My other project this week was an upgrade to &lt;a href="https://til.simonwillison.net/"&gt;til.simonwillison.net&lt;/a&gt;: I finally spent the time to &lt;a href="https://github.com/simonw/til/issues/34"&gt;design nicer URLs&lt;/a&gt; for the site.&lt;/p&gt;
&lt;p&gt;Before:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;til.simonwillison.net/til/til/javascript_manipulating-query-params.md&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;After:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;til.simonwillison.net/javascript/manipulating-query-params&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;The implementation for this takes advantage of a feature I sneaked into Datasette 0.49: &lt;a href="https://simonwillison.net/2020/Sep/15/datasette-0-49#path-parameters-custom-page-templates"&gt;Path parameters for custom page templates&lt;/a&gt;. I can create a template file called &lt;code&gt;pages/{topic}/{slug}.html&lt;/code&gt; and Datasette use that template to handle 404 errors that match that pattern.&lt;/p&gt;
&lt;p&gt;Here's &lt;a href="https://github.com/simonw/til/blob/main/templates/pages/%7Btopic%7D/%7Bslug%7D.html"&gt;the new pages/{topic}/{slug}.html&lt;/a&gt; template for my TIL site. It uses the &lt;code&gt;sql()&lt;/code&gt; template function from the &lt;a href="https://github.com/simonw/datasette-template-sql"&gt;datasette-template-sql&lt;/a&gt; plugin to retrieve and render the matching TIL, or raises a 404 if no TIL can be found.&lt;/p&gt;
&lt;p&gt;I also needed to setup redirects from the old pages to the new ones. I wrote a &lt;a href="https://til.simonwillison.net/til/til/datasette_redirects-for-datasette.md"&gt;TIL on edirects for Datasette&lt;/a&gt; explaining how I did that.&lt;/p&gt;
&lt;h4&gt;TIL this week&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://til.simonwillison.net/til/til/datasette_redirects-for-datasette.md"&gt;Redirects for Datasette&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;Releases this week&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/simonw/datasette-ripgrep/releases/tag/0.2"&gt;datasette-ripgrep 0.2&lt;/a&gt; - 2020-11-27&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/simonw/datasette-ripgrep/releases/tag/0.1"&gt;datasette-ripgrep 0.1&lt;/a&gt; - 2020-11-26&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/simonw/datasette-atom/releases/tag/0.8.1"&gt;datasette-atom 0.8.1&lt;/a&gt; - 2020-11-25&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/simonw/datasette-ripgrep/releases/tag/0.1a1"&gt;datasette-ripgrep 0.1a1&lt;/a&gt; - 2020-11-25&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/simonw/datasette-ripgrep/releases/tag/0.1a0"&gt;datasette-ripgrep 0.1a0&lt;/a&gt; - 2020-11-25&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/simonw/datasette-graphql/releases/tag/1.2.1"&gt;datasette-graphql 1.2.1&lt;/a&gt; - 2020-11-24&lt;/li&gt;
&lt;/ul&gt;
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/async"&gt;async&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/css"&gt;css&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/projects"&gt;projects&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/python"&gt;python&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/regular-expressions"&gt;regular-expressions&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/datasette"&gt;datasette&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/weeknotes"&gt;weeknotes&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/cloudrun"&gt;cloudrun&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ripgrep"&gt;ripgrep&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/baked-data"&gt;baked-data&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="async"/><category term="css"/><category term="projects"/><category term="python"/><category term="regular-expressions"/><category term="datasette"/><category term="weeknotes"/><category term="cloudrun"/><category term="ripgrep"/><category term="baked-data"/></entry><entry><title>Quoting Carlton Gibson</title><link href="https://simonwillison.net/2020/Sep/27/carlton-gibson/#atom-tag" rel="alternate"/><published>2020-09-27T15:09:26+00:00</published><updated>2020-09-27T15:09:26+00:00</updated><id>https://simonwillison.net/2020/Sep/27/carlton-gibson/#atom-tag</id><summary type="html">
    &lt;blockquote cite="https://noumenal.es/posts/weeknotes-wk-39/rX/"&gt;&lt;p&gt;Inevitably we got round to talking about async.&lt;/p&gt;
&lt;p&gt;As much of an unneeded complication as it is for so many day-to-day use-cases, it’s important for Python because, if and when you do need the high throughput handling of these io-bound use-cases, you don’t want to have to switch language.&lt;/p&gt;
&lt;p&gt;The same for Django: most of what you’re doing has no need of async but you don’t want to have to change web framework just because you need a sprinkling of non-blocking IO.&lt;/p&gt;&lt;/blockquote&gt;
&lt;p class="cite"&gt;&amp;mdash; &lt;a href="https://noumenal.es/posts/weeknotes-wk-39/rX/"&gt;Carlton Gibson&lt;/a&gt;&lt;/p&gt;

    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/async"&gt;async&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/django"&gt;django&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/python"&gt;python&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/carlton-gibson"&gt;carlton-gibson&lt;/a&gt;&lt;/p&gt;



</summary><category term="async"/><category term="django"/><category term="python"/><category term="carlton-gibson"/></entry><entry><title>The "await me maybe" pattern for Python asyncio</title><link href="https://simonwillison.net/2020/Sep/2/await-me-maybe/#atom-tag" rel="alternate"/><published>2020-09-02T23:09:41+00:00</published><updated>2020-09-02T23:09:41+00:00</updated><id>https://simonwillison.net/2020/Sep/2/await-me-maybe/#atom-tag</id><summary type="html">
    &lt;p&gt;I've identified a pattern for handling potentially-asynchronous callback functions in Python which I'm calling the "await me maybe" pattern. It works by letting you return a value, a callable function that returns a value OR an awaitable function that returns that value.&lt;/p&gt;
&lt;h4 id="await-me-maybe-background"&gt;Background&lt;/h4&gt;
&lt;p&gt;&lt;a href="https://docs.datasette.io/"&gt;Datasette&lt;/a&gt; has been built on top of Python 3 &lt;a href="https://docs.python.org/3/library/asyncio.html"&gt;asyncio&lt;/a&gt; from the very start - initially using &lt;a href="https://sanic.readthedocs.io/"&gt;Sanic&lt;/a&gt;, and as-of &lt;a href="https://docs.datasette.io/en/stable/changelog.html#v0-29"&gt;Datasette 0.29&lt;/a&gt; using a custom mini-framework on top of ASGI 3, usually running under Uvicorn.&lt;/p&gt;
&lt;p&gt;Datasette also has a plugin system, built on top of &lt;a href="https://pypi.org/project/pluggy/"&gt;Pluggy&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Pluggy is a beautifully designed mechanism for plugins. It works based on decorated functions, which are called at various points by Datasette itself.&lt;/p&gt;
&lt;p&gt;A simple plugin that injects a new JavaScript file into a page coud look like this:&lt;/p&gt;

&lt;pre&gt;&lt;span class="pl-k"&gt;from&lt;/span&gt; &lt;span class="pl-s1"&gt;datasette&lt;/span&gt; &lt;span class="pl-k"&gt;import&lt;/span&gt; &lt;span class="pl-s1"&gt;hookimpl&lt;/span&gt;

&lt;span class="pl-en"&gt;@&lt;span class="pl-s1"&gt;hookimpl&lt;/span&gt;&lt;/span&gt;
&lt;span class="pl-k"&gt;def&lt;/span&gt; &lt;span class="pl-en"&gt;extra_js_urls&lt;/span&gt;():
    &lt;span class="pl-k"&gt;return&lt;/span&gt; [
        &lt;span class="pl-s"&gt;"https://code.jquery.com/jquery-3.5.1.min.js"&lt;/span&gt;
    ]&lt;/pre&gt;
&lt;p&gt;Datasette can then gather together all of the extra JavaScript URLs that should be injected into a page by running this code:&lt;/p&gt;

&lt;pre&gt;&lt;span class="pl-s1"&gt;urls&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; []
&lt;span class="pl-k"&gt;for&lt;/span&gt; &lt;span class="pl-s1"&gt;url&lt;/span&gt; &lt;span class="pl-c1"&gt;in&lt;/span&gt; &lt;span class="pl-s1"&gt;pm&lt;/span&gt;.&lt;span class="pl-s1"&gt;hook&lt;/span&gt;.&lt;span class="pl-en"&gt;extra_js_urls&lt;/span&gt;(
    &lt;span class="pl-s1"&gt;template&lt;/span&gt;&lt;span class="pl-c1"&gt;=&lt;/span&gt;&lt;span class="pl-s1"&gt;template&lt;/span&gt;.&lt;span class="pl-s1"&gt;name&lt;/span&gt;,
    &lt;span class="pl-s1"&gt;datasette&lt;/span&gt;&lt;span class="pl-c1"&gt;=&lt;/span&gt;&lt;span class="pl-s1"&gt;datasette&lt;/span&gt;,
):
    &lt;span class="pl-s1"&gt;urls&lt;/span&gt;.&lt;span class="pl-en"&gt;extend&lt;/span&gt;(&lt;span class="pl-s1"&gt;url&lt;/span&gt;)&lt;/pre&gt;
&lt;p&gt;What's up with the &lt;code&gt;template=&lt;/code&gt; and &lt;code&gt;datasette=&lt;/code&gt; parameters that are passed here?&lt;/p&gt;
&lt;p&gt;Pluggy implements a form of dependency injection, where plugin hook functions can optionally list additional parameters that they would like to have access to.&lt;/p&gt;
&lt;p&gt;The above simple example didn't need any extra information. But imagine a plugin that only wants to inject jQuery on the &lt;code&gt;table.html&lt;/code&gt; template page:&lt;/p&gt;

&lt;pre&gt;&lt;span class="pl-en"&gt;@&lt;span class="pl-s1"&gt;hookimpl&lt;/span&gt;&lt;/span&gt;
&lt;span class="pl-k"&gt;def&lt;/span&gt; &lt;span class="pl-en"&gt;extra_js_urls&lt;/span&gt;(&lt;span class="pl-s1"&gt;template&lt;/span&gt;):
    &lt;span class="pl-k"&gt;if&lt;/span&gt; &lt;span class="pl-s1"&gt;template&lt;/span&gt; &lt;span class="pl-c1"&gt;==&lt;/span&gt; &lt;span class="pl-s"&gt;"table.html"&lt;/span&gt;:
        &lt;span class="pl-k"&gt;return&lt;/span&gt; [
            &lt;span class="pl-s"&gt;"https://code.jquery.com/jquery-3.5.1.min.js"&lt;/span&gt;
        ]&lt;/pre&gt;
&lt;p&gt;Datasette actually provides several more optional argument for these plugin functions - see &lt;a href="https://docs.datasette.io/en/stable/plugin_hooks.html#extra-template-vars-template-database-table-columns-view-name-request-datasette"&gt;the plugin hooks documentation&lt;/a&gt; for full details.&lt;/p&gt;
&lt;h4&gt;What if we need to await something?&lt;/h4&gt;
&lt;p&gt;The &lt;a href="https://docs.datasette.io/en/stable/internals.html#datasette-class"&gt;datasette object&lt;/a&gt; that can be passed to plugin hooks is special: it provides an object that can be used for the following:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Executing SQL against databases connected to Datasette&lt;/li&gt;
&lt;li&gt;Looking up Datasette metadata and configuration settings, including plugin configuration&lt;/li&gt;
&lt;li&gt;Rendering templates using the template environment configured by Datasette&lt;/li&gt;
&lt;li&gt;Performing checks against the Datasette &lt;a href="https://docs.datasette.io/en/stable/authentication.html#permissions"&gt;permissions system&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Here's the problem: many of those methods on Datasette are awaitable - &lt;code&gt;await datasette.render_template(...)&lt;/code&gt; for example. But Pluggy is built around regular non-awaitable Python functions.&lt;/p&gt;
&lt;p&gt;If my &lt;code&gt;def extra_js_urls()&lt;/code&gt; plugin function needs to execute a SQL query to decide what JavaScript to include, it won't be able to - because you can't use &lt;code&gt;await&lt;/code&gt; inside a regular Python function.&lt;/p&gt;
&lt;p&gt;That's where the "await me maybe" pattern comes in.&lt;/p&gt;
&lt;p&gt;The basic idea is that a function can return a value, OR a function-that-returns-a-value, OR an awaitable-function-that-returns-a-value.&lt;/p&gt;
&lt;p&gt;If we want our &lt;code&gt;extra_js_urls(datasette)&lt;/code&gt; hook to execute a SQL query in order to decide what URLs to return, it can look like this:&lt;/p&gt;

&lt;pre&gt;&lt;span class="pl-en"&gt;@&lt;span class="pl-s1"&gt;hookimpl&lt;/span&gt;&lt;/span&gt;
&lt;span class="pl-k"&gt;def&lt;/span&gt; &lt;span class="pl-en"&gt;extra_js_urls&lt;/span&gt;(&lt;span class="pl-s1"&gt;datasette&lt;/span&gt;):
    &lt;span class="pl-k"&gt;async&lt;/span&gt; &lt;span class="pl-k"&gt;def&lt;/span&gt; &lt;span class="pl-en"&gt;inner&lt;/span&gt;():
        &lt;span class="pl-s1"&gt;db&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s1"&gt;datasette&lt;/span&gt;.&lt;span class="pl-en"&gt;get_database&lt;/span&gt;()
        &lt;span class="pl-s1"&gt;results&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-k"&gt;await&lt;/span&gt; &lt;span class="pl-s1"&gt;db&lt;/span&gt;.&lt;span class="pl-en"&gt;execute&lt;/span&gt;(&lt;span class="pl-s"&gt;"select url from js_files"&lt;/span&gt;)
        &lt;span class="pl-k"&gt;return&lt;/span&gt; [&lt;span class="pl-s1"&gt;r&lt;/span&gt;[&lt;span class="pl-c1"&gt;0&lt;/span&gt;] &lt;span class="pl-k"&gt;for&lt;/span&gt; &lt;span class="pl-s1"&gt;r&lt;/span&gt; &lt;span class="pl-c1"&gt;in&lt;/span&gt; &lt;span class="pl-s1"&gt;results&lt;/span&gt;]

    &lt;span class="pl-k"&gt;return&lt;/span&gt; &lt;span class="pl-s1"&gt;inner&lt;/span&gt;&lt;/pre&gt;
&lt;p&gt;Note that Python lets you define an &lt;code&gt;async def inner()&lt;/code&gt; function inside the body of a regular function, which is what we're doing here.&lt;/p&gt;
&lt;p&gt;The code that calls the plugin hook in Datasette can then look like this:&lt;/p&gt;

&lt;pre&gt;&lt;span class="pl-s1"&gt;urls&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; []
&lt;span class="pl-k"&gt;for&lt;/span&gt; &lt;span class="pl-s1"&gt;url&lt;/span&gt; &lt;span class="pl-c1"&gt;in&lt;/span&gt; &lt;span class="pl-s1"&gt;pm&lt;/span&gt;.&lt;span class="pl-s1"&gt;hook&lt;/span&gt;.&lt;span class="pl-en"&gt;extra_js_urls&lt;/span&gt;(
    &lt;span class="pl-s1"&gt;template&lt;/span&gt;&lt;span class="pl-c1"&gt;=&lt;/span&gt;&lt;span class="pl-s1"&gt;template&lt;/span&gt;.&lt;span class="pl-s1"&gt;name&lt;/span&gt;,
    &lt;span class="pl-s1"&gt;datasette&lt;/span&gt;&lt;span class="pl-c1"&gt;=&lt;/span&gt;&lt;span class="pl-s1"&gt;datasette&lt;/span&gt;,
):
    &lt;span class="pl-k"&gt;if&lt;/span&gt; &lt;span class="pl-en"&gt;callable&lt;/span&gt;(&lt;span class="pl-s1"&gt;url&lt;/span&gt;):
        &lt;span class="pl-s1"&gt;url&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-en"&gt;url&lt;/span&gt;()
    &lt;span class="pl-k"&gt;if&lt;/span&gt; &lt;span class="pl-s1"&gt;asyncio&lt;/span&gt;.&lt;span class="pl-en"&gt;iscoroutine&lt;/span&gt;(&lt;span class="pl-s1"&gt;url&lt;/span&gt;):
        &lt;span class="pl-s1"&gt;url&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-k"&gt;await&lt;/span&gt; &lt;span class="pl-s1"&gt;url&lt;/span&gt;
    &lt;span class="pl-s1"&gt;urls&lt;/span&gt;.&lt;span class="pl-en"&gt;append&lt;/span&gt;(&lt;span class="pl-s1"&gt;url&lt;/span&gt;)&lt;/pre&gt;
&lt;p&gt;I use this pattern in a bunch of different places in Datasette, so today I refactored that into &lt;a href="https://github.com/simonw/datasette/blob/26b2922f177caa4e147aaee28be0cff37a457802/datasette/utils/__init__.py#L55-L60"&gt;a utility function&lt;/a&gt;:&lt;/p&gt;

&lt;pre&gt;&lt;span class="pl-k"&gt;import&lt;/span&gt; &lt;span class="pl-s1"&gt;asyncio&lt;/span&gt;

&lt;span class="pl-k"&gt;async&lt;/span&gt; &lt;span class="pl-k"&gt;def&lt;/span&gt; &lt;span class="pl-en"&gt;await_me_maybe&lt;/span&gt;(&lt;span class="pl-s1"&gt;value&lt;/span&gt;):
    &lt;span class="pl-k"&gt;if&lt;/span&gt; &lt;span class="pl-en"&gt;callable&lt;/span&gt;(&lt;span class="pl-s1"&gt;value&lt;/span&gt;):
        &lt;span class="pl-s1"&gt;value&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-en"&gt;value&lt;/span&gt;()
    &lt;span class="pl-k"&gt;if&lt;/span&gt; &lt;span class="pl-s1"&gt;asyncio&lt;/span&gt;.&lt;span class="pl-en"&gt;iscoroutine&lt;/span&gt;(&lt;span class="pl-s1"&gt;value&lt;/span&gt;):
        &lt;span class="pl-s1"&gt;value&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-k"&gt;await&lt;/span&gt; &lt;span class="pl-s1"&gt;value&lt;/span&gt;
    &lt;span class="pl-k"&gt;return&lt;/span&gt; &lt;span class="pl-s1"&gt;value&lt;/span&gt;&lt;/pre&gt;
&lt;p&gt;&lt;a href="https://github.com/simonw/datasette/commit/26b2922f177caa4e147aaee28be0cff37a457802"&gt;This commit&lt;/a&gt; includes a bunch of examples where this function is called, for example &lt;a href="https://github.com/simonw/datasette/blob/26b2922f177caa4e147aaee28be0cff37a457802/datasette/app.py#L702-L714"&gt;this code&lt;/a&gt; which gathers extra body scripts to be included at the bottom of the page:&lt;/p&gt;

&lt;pre&gt;&lt;span class="pl-s1"&gt;body_scripts&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; []
&lt;span class="pl-k"&gt;for&lt;/span&gt; &lt;span class="pl-s1"&gt;extra_script&lt;/span&gt; &lt;span class="pl-c1"&gt;in&lt;/span&gt; &lt;span class="pl-s1"&gt;pm&lt;/span&gt;.&lt;span class="pl-s1"&gt;hook&lt;/span&gt;.&lt;span class="pl-en"&gt;extra_body_script&lt;/span&gt;(
    &lt;span class="pl-s1"&gt;template&lt;/span&gt;&lt;span class="pl-c1"&gt;=&lt;/span&gt;&lt;span class="pl-s1"&gt;template&lt;/span&gt;.&lt;span class="pl-s1"&gt;name&lt;/span&gt;,
    &lt;span class="pl-s1"&gt;database&lt;/span&gt;&lt;span class="pl-c1"&gt;=&lt;/span&gt;&lt;span class="pl-s1"&gt;context&lt;/span&gt;.&lt;span class="pl-en"&gt;get&lt;/span&gt;(&lt;span class="pl-s"&gt;"database"&lt;/span&gt;),
    &lt;span class="pl-s1"&gt;table&lt;/span&gt;&lt;span class="pl-c1"&gt;=&lt;/span&gt;&lt;span class="pl-s1"&gt;context&lt;/span&gt;.&lt;span class="pl-en"&gt;get&lt;/span&gt;(&lt;span class="pl-s"&gt;"table"&lt;/span&gt;),
    &lt;span class="pl-s1"&gt;columns&lt;/span&gt;&lt;span class="pl-c1"&gt;=&lt;/span&gt;&lt;span class="pl-s1"&gt;context&lt;/span&gt;.&lt;span class="pl-en"&gt;get&lt;/span&gt;(&lt;span class="pl-s"&gt;"columns"&lt;/span&gt;),
    &lt;span class="pl-s1"&gt;view_name&lt;/span&gt;&lt;span class="pl-c1"&gt;=&lt;/span&gt;&lt;span class="pl-s1"&gt;view_name&lt;/span&gt;,
    &lt;span class="pl-s1"&gt;request&lt;/span&gt;&lt;span class="pl-c1"&gt;=&lt;/span&gt;&lt;span class="pl-s1"&gt;request&lt;/span&gt;,
    &lt;span class="pl-s1"&gt;datasette&lt;/span&gt;&lt;span class="pl-c1"&gt;=&lt;/span&gt;&lt;span class="pl-s1"&gt;self&lt;/span&gt;,
):
    &lt;span class="pl-s1"&gt;extra_script&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-k"&gt;await&lt;/span&gt; &lt;span class="pl-en"&gt;await_me_maybe&lt;/span&gt;(&lt;span class="pl-s1"&gt;extra_script&lt;/span&gt;)
    &lt;span class="pl-s1"&gt;body_scripts&lt;/span&gt;.&lt;span class="pl-en"&gt;append&lt;/span&gt;(&lt;span class="pl-v"&gt;Markup&lt;/span&gt;(&lt;span class="pl-s1"&gt;extra_script&lt;/span&gt;))&lt;/pre&gt;
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/async"&gt;async&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/patterns"&gt;patterns&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/plugins"&gt;plugins&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/python"&gt;python&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/datasette"&gt;datasette&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="async"/><category term="patterns"/><category term="plugins"/><category term="python"/><category term="datasette"/></entry><entry><title>Waiting in asyncio</title><link href="https://simonwillison.net/2020/May/26/waiting-asyncio/#atom-tag" rel="alternate"/><published>2020-05-26T15:28:10+00:00</published><updated>2020-05-26T15:28:10+00:00</updated><id>https://simonwillison.net/2020/May/26/waiting-asyncio/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://hynek.me/articles/waiting-in-asyncio/"&gt;Waiting in asyncio&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Handy cheatsheet explaining the differences between asyncio.gather(), asyncio.wait_for(), asyncio.as_completed() and asyncio.wait() by Hynek Schlawack.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/async"&gt;async&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/python"&gt;python&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/hynek-schlawack"&gt;hynek-schlawack&lt;/a&gt;&lt;/p&gt;



</summary><category term="async"/><category term="python"/><category term="hynek-schlawack"/></entry><entry><title>Django: Added support for asynchronous views and middleware</title><link href="https://simonwillison.net/2020/Mar/19/async-views-and-middleware/#atom-tag" rel="alternate"/><published>2020-03-19T03:43:40+00:00</published><updated>2020-03-19T03:43:40+00:00</updated><id>https://simonwillison.net/2020/Mar/19/async-views-and-middleware/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/django/django/commit/fc0fa72ff4cdbf5861a366e31cb8bbacd44da22d"&gt;Django: Added support for asynchronous views and middleware&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
An enormously consequential feature just landed in Django, and is set to ship as part of Django 3.1 in August. Asynchronous views will allow Django applications to define views using “async def myview(request)”—taking full advantage of Python’s growing asyncio ecosystem and providing enormous performance improvements for Django sites that do things like hitting APIs over HTTP. Andrew has been puzzling over this for ages and it’s really exciting to see it land in a form that should be usable in a stable Django release in just a few months.

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="https://twitter.com/andrewgodwin/status/1240357729174052866"&gt;@andrewgodwin&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/andrew-godwin"&gt;andrew-godwin&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/async"&gt;async&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/django"&gt;django&lt;/a&gt;&lt;/p&gt;



</summary><category term="andrew-godwin"/><category term="async"/><category term="django"/></entry><entry><title>Async Support - HTTPX</title><link href="https://simonwillison.net/2020/Jan/10/httpx/#atom-tag" rel="alternate"/><published>2020-01-10T04:49:59+00:00</published><updated>2020-01-10T04:49:59+00:00</updated><id>https://simonwillison.net/2020/Jan/10/httpx/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://www.python-httpx.org/async/"&gt;Async Support - HTTPX&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
HTTPX is the new async-friendly HTTP library for Python spearheaded by Tom Christie. It works in both async and non-async mode with an API very similar to requests. The async support is particularly interesting - it's a really clean API, and now that Jupyter supports top-level await you can run &lt;code&gt;(await httpx.AsyncClient().get(url)).text&lt;/code&gt; directly in a cell and get back the response. Most excitingly the library lets you pass an ASGI app directly to the client and then perform requests against it - ideal for unit tests.

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="https://twitter.com/_tomchristie/status/1215240517962870784"&gt;@_tomchristie&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/async"&gt;async&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/http"&gt;http&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/python"&gt;python&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/asgi"&gt;asgi&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/kim-christie"&gt;kim-christie&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/httpx"&gt;httpx&lt;/a&gt;&lt;/p&gt;



</summary><category term="async"/><category term="http"/><category term="python"/><category term="asgi"/><category term="kim-christie"/><category term="httpx"/></entry><entry><title>Quoting Andrew Godwin</title><link href="https://simonwillison.net/2019/May/10/andrew-godwin/#atom-tag" rel="alternate"/><published>2019-05-10T02:00:03+00:00</published><updated>2019-05-10T02:00:03+00:00</updated><id>https://simonwillison.net/2019/May/10/andrew-godwin/#atom-tag</id><summary type="html">
    &lt;blockquote cite="https://github.com/andrewgodwin/deps/blob/async/draft/0009-async.rst"&gt;&lt;p&gt;... the overall conclusion I reach is that we have so much to gain from making Django async-capable that it is worth the large amount of work it will take. I also believe, crucially, that we can undertake this change in an iterative, community-driven way that does not rely solely on one or two long-time contributors burning themselves out.&lt;/p&gt;&lt;/blockquote&gt;
&lt;p class="cite"&gt;&amp;mdash; &lt;a href="https://github.com/andrewgodwin/deps/blob/async/draft/0009-async.rst"&gt;Andrew Godwin&lt;/a&gt;&lt;/p&gt;

    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/andrew-godwin"&gt;andrew-godwin&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/async"&gt;async&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/django"&gt;django&lt;/a&gt;&lt;/p&gt;



</summary><category term="andrew-godwin"/><category term="async"/><category term="django"/></entry><entry><title>Quoting Tom Christie</title><link href="https://simonwillison.net/2018/Oct/8/tom-christie/#atom-tag" rel="alternate"/><published>2018-10-08T14:43:16+00:00</published><updated>2018-10-08T14:43:16+00:00</updated><id>https://simonwillison.net/2018/Oct/8/tom-christie/#atom-tag</id><summary type="html">
    &lt;blockquote cite="https://www.encode.io/articles/hello-asgi/"&gt;&lt;p&gt;The ASGI specification provides an opportunity for Python to hit a productivity/performance sweet-spot for a wide range of use-cases, from writing high-volume proxy servers through to bringing large-scale web applications to market at speed.&lt;/p&gt;&lt;/blockquote&gt;
&lt;p class="cite"&gt;&amp;mdash; &lt;a href="https://www.encode.io/articles/hello-asgi/"&gt;Tom Christie&lt;/a&gt;&lt;/p&gt;

    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/async"&gt;async&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/python"&gt;python&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/asgi"&gt;asgi&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/kim-christie"&gt;kim-christie&lt;/a&gt;&lt;/p&gt;



</summary><category term="async"/><category term="python"/><category term="asgi"/><category term="kim-christie"/></entry><entry><title>Changelog 2018-06-12 / Observable</title><link href="https://simonwillison.net/2018/Jun/13/observable-changelog/#atom-tag" rel="alternate"/><published>2018-06-13T15:50:01+00:00</published><updated>2018-06-13T15:50:01+00:00</updated><id>https://simonwillison.net/2018/Jun/13/observable-changelog/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://beta.observablehq.com/@mbostock/changelog-2018-06-12"&gt;Changelog 2018-06-12 / Observable&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
The ability to download an Observable notebook as a stand-alone ES module and run it anywhere using their open source runtime is fascinating, but it’s also worth reading the changelog for some of the new clever tricks they are pulling using await—“await visibility();” in a notebook cell will cause execution to pause until the cell scrolls into view for example.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/async"&gt;async&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/javascript"&gt;javascript&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/observable"&gt;observable&lt;/a&gt;&lt;/p&gt;



</summary><category term="async"/><category term="javascript"/><category term="observable"/></entry></feed>