<?xml version="1.0" encoding="utf-8"?>
<feed xml:lang="en-us" xmlns="http://www.w3.org/2005/Atom"><title>Simon Willison's Weblog: httpx</title><link href="http://simonwillison.net/" rel="alternate"/><link href="http://simonwillison.net/tags/httpx.atom" rel="self"/><id>http://simonwillison.net/</id><updated>2025-05-29T03:33:17+00:00</updated><author><name>Simon Willison</name></author><entry><title>llm-mistral 0.14</title><link href="https://simonwillison.net/2025/May/29/llm-mistral-014/#atom-tag" rel="alternate"/><published>2025-05-29T03:33:17+00:00</published><updated>2025-05-29T03:33:17+00:00</updated><id>https://simonwillison.net/2025/May/29/llm-mistral-014/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/simonw/llm-mistral/releases/tag/0.14"&gt;llm-mistral 0.14&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
I &lt;a href="https://github.com/simonw/llm-mistral/issues/31"&gt;added tool-support&lt;/a&gt; to my plugin for accessing the Mistral API from LLM today, plus support for Mistral's new &lt;a href="https://simonwillison.net/2025/May/28/codestral-embed/"&gt;Codestral Embed&lt;/a&gt; embedding model.&lt;/p&gt;
&lt;p&gt;An interesting challenge here is that I'm not using an official client library for &lt;code&gt;llm-mistral&lt;/code&gt; - I rolled my own client on top of their streaming HTTP API using Florimond Manca's &lt;a href="https://github.com/florimondmanca/httpx-sse"&gt;httpx-sse&lt;/a&gt; library. It's a very pleasant way to interact with streaming APIs - here's &lt;a href="https://github.com/simonw/llm-mistral/blob/098a4eaf624a3a723f91381915f93b4783d498bc/llm_mistral.py#L456-L502"&gt;my code that does most of the work&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The problem I faced is that Mistral's API &lt;a href="https://docs.mistral.ai/capabilities/function_calling/"&gt;documentation for function calling&lt;/a&gt; has examples in Python and TypeScript but doesn't include &lt;code&gt;curl&lt;/code&gt; or direct documentation of their HTTP endpoints!&lt;/p&gt;
&lt;p&gt;I needed documentation at the HTTP level. Could I maybe extract that directly from Mistral's official Python library?&lt;/p&gt;
&lt;p&gt;It turns out &lt;a href="https://github.com/simonw/llm-mistral/issues/31#issuecomment-2917121330"&gt;I could&lt;/a&gt;. I started by cloning the repo:&lt;/p&gt;
&lt;div class="highlight highlight-source-shell"&gt;&lt;pre&gt;git clone https://github.com/mistralai/client-python
&lt;span class="pl-c1"&gt;cd&lt;/span&gt; client-python/src/mistralai
files-to-prompt &lt;span class="pl-c1"&gt;.&lt;/span&gt; &lt;span class="pl-k"&gt;|&lt;/span&gt; ttok&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;My &lt;a href="https://github.com/simonw/ttok"&gt;ttok&lt;/a&gt; tool gave me a token count of 212,410 (counted using OpenAI's tokenizer, but that's normally a close enough estimate) - Mistral's models tap out at 128,000 so I switched to Gemini 2.5 Flash which can easily handle that many.&lt;/p&gt;
&lt;p&gt;I ran this:&lt;/p&gt;
&lt;div class="highlight highlight-source-shell"&gt;&lt;pre&gt;files-to-prompt -c &lt;span class="pl-c1"&gt;.&lt;/span&gt; &lt;span class="pl-k"&gt;&amp;gt;&lt;/span&gt; /tmp/mistral.txt

llm -f /tmp/mistral.txt \
  -m gemini-2.5-flash-preview-05-20 \
  -s &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;'&lt;/span&gt;Generate comprehensive HTTP API documentation showing
how function calling works, include example curl commands for each step&lt;span class="pl-pds"&gt;'&lt;/span&gt;&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The results were pretty spectacular! Gemini 2.5 Flash produced a &lt;a href="https://gist.github.com/simonw/03f2049cd9af6dc072e1ee33461f3437#response"&gt;detailed description&lt;/a&gt; of the exact set of HTTP APIs I needed to interact with, and the JSON formats I should pass to them.&lt;/p&gt;
&lt;p&gt;There are a bunch of steps needed to get tools working in a new model, as described in &lt;a href="https://llm.datasette.io/en/stable/plugins/advanced-model-plugins.html#supporting-tools"&gt;the LLM plugin authors documentation&lt;/a&gt;. I started working through them by hand... and then got lazy and decided to see if I could get a model to do the work for me.&lt;/p&gt;
&lt;p&gt;This time I tried the new Claude Opus 4. I fed it three files: my existing, incomplete &lt;code&gt;llm_mistral.py&lt;/code&gt;, a full copy of &lt;a href="https://github.com/simonw/llm-gemini/blob/6177aa2a0676bf004b374a8863914585aa93ca52/llm_gemini.py"&gt;llm_gemini.py&lt;/a&gt; with its working tools implementation and a copy of the API docs Gemini had written for me earlier. I prompted:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;I need to update this Mistral code to add tool support. I've included examples of that code for Gemini, and a detailed README explaining the Mistral format.&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Claude churned away and wrote me code that was &lt;em&gt;most&lt;/em&gt; of what I needed. I tested it in a bunch of different scenarios, pasted problems back into Claude to see what would happen, and eventually took over and finished the rest of the code myself. Here's &lt;a href="https://claude.ai/share/7c609a61-4b32-45ca-bdca-31bf4ef25d2d"&gt;the full transcript&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;I'm a little sad I didn't use Mistral to write the code to support Mistral, but I'm pleased to add yet another model family to the list that's supported for tool usage in LLM.


    &lt;p&gt;Tags: &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/httpx"&gt;httpx&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/claude"&gt;claude&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/mistral"&gt;mistral&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/claude-4"&gt;claude-4&lt;/a&gt;&lt;/p&gt;



</summary><category term="plugins"/><category term="projects"/><category term="python"/><category term="ai"/><category term="httpx"/><category term="generative-ai"/><category term="llms"/><category term="ai-assisted-programming"/><category term="llm"/><category term="claude"/><category term="mistral"/><category term="gemini"/><category term="llm-tool-use"/><category term="claude-4"/></entry><entry><title>Re-assessing the automatic charset decoding policy in HTTPX</title><link href="https://simonwillison.net/2021/Aug/13/charset/#atom-tag" rel="alternate"/><published>2021-08-13T22:07:54+00:00</published><updated>2021-08-13T22:07:54+00:00</updated><id>https://simonwillison.net/2021/Aug/13/charset/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://www.encode.io/reports/august-2021#weeknotes-friday-13th-august-2021"&gt;Re-assessing the automatic charset decoding policy in HTTPX&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Tom Christie ran an analysis of the top 1,000 most accessed websites (according to an older extract from Google’s Ad Planner service) and found that a full 5% of them both omitted a charset parameter and failed to decode as UTF-8. As a result, HTTPX will be depending on the charset-normalizer Python library to handle those cases.

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


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/unicode"&gt;unicode&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="unicode"/><category term="kim-christie"/><category term="httpx"/></entry><entry><title>Datasette 0.50: The annotated release notes</title><link href="https://simonwillison.net/2020/Oct/9/datasette-0-50/#atom-tag" rel="alternate"/><published>2020-10-09T20:23:11+00:00</published><updated>2020-10-09T20:23:11+00:00</updated><id>https://simonwillison.net/2020/Oct/9/datasette-0-50/#atom-tag</id><summary type="html">
    &lt;p&gt;I released &lt;a href="https://docs.datasette.io/en/stable/changelog.html#v0-50"&gt;Datasette 0.50&lt;/a&gt; this morning, with a new user-facing column actions menu feature and a way for plugins to make internal HTTP requests to consume the JSON API of their parent Datasette instance.&lt;/p&gt;
&lt;h4 id="column-actions"&gt;The column actions menu&lt;/h4&gt;
&lt;blockquote cite="https://docs.datasette.io/en/stable/changelog.html#v0-50"&gt;&lt;p&gt;The key new feature in this release is the &lt;strong&gt;column actions&lt;/strong&gt; menu on the table page (&lt;a href="https://github.com/simonw/datasette/issues/891"&gt;#891&lt;/a&gt;). This can be used to sort a column in ascending or descending order, facet data by that column or filter the table to just rows that have a value for that column.&lt;/p&gt;&lt;/blockquote&gt;
&lt;p&gt;The table page is the most important page within Datasette: it's where users interact with database tables.&lt;/p&gt;
&lt;p&gt;Prior to 0.50 users could sort those tables by clicking on the column header. If they wanted to sort in descending order they had to click it, wait for the table to reload and then click it a second time.&lt;/p&gt;
&lt;p&gt;In 0.50 I've introduced a new UI element which I'm calling the &lt;em&gt;column actions menu&lt;/em&gt;. Here's an animation showing it in action on the &lt;a href="https://latest.datasette.io/fixtures/facetable"&gt;facetable&lt;/a&gt; demo table:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://static.simonwillison.net/static/2020/column-actions.gif" alt="Animated demo of the columns action menu, showing it used to sort a column and select two other columns for faceting" style="max-width:100%;" /&gt;&lt;/p&gt;
&lt;p&gt;Right now the menu can be used to sort ascending, sort descending or add the column to the current set of select facets. If a column has any blank values on the current page a menu option to "Show not-blank rows" appears too - you can try that out on the &lt;a href="https://latest.datasette.io/fixtures/sortable"&gt;sortable&lt;/a&gt; table.&lt;/p&gt;
&lt;p&gt;I plan to extend this with more options in the future. I'd also like to make it a documented plugin extension point, so plugins can add their own column-specific actions. I need to figure out a JavaScript equivalent of the Python pluggy plugins mechanism first though, see &lt;a href="https://github.com/simonw/datasette/issues/983"&gt;issue 983&lt;/a&gt;.&lt;/p&gt;
&lt;h4 id="datasette-client"&gt;datasette.client&lt;/h4&gt;
&lt;blockquote cite="https://docs.datasette.io/en/stable/changelog.html#v0-50"&gt;&lt;p&gt;Plugin authors can use the new &lt;a class="reference internal" href="https://docs.datasette.io/en/stable/internals.html#internals-datasette-client"&gt;&lt;span class="std std-ref"&gt;datasette.client&lt;/span&gt;&lt;/a&gt; object to make internal HTTP requests from their plugins, allowing them to make use of Datasette's JSON API. (&lt;a href="https://github.com/simonw/datasette/issues/943"&gt;#943&lt;/a&gt;)&lt;/p&gt;&lt;/blockquote&gt;
&lt;p&gt;In building the &lt;a href="https://github.com/simonw/datasette-graphql"&gt;datasette-graphql&lt;/a&gt; plugin I ran into an interesting requirement. I wanted to provide efficient &lt;a href="https://simonwillison.net/2018/Oct/4/datasette-ideas/#Keyset_pagination"&gt;keyset pagination&lt;/a&gt; within the GraphQL schema, which is actually quite a complex things to implement.&lt;/p&gt;
&lt;p&gt;Datasette already has a robust implementation of keyset pagination, but it's tangled up in the implementation of &lt;a href="https://github.com/simonw/datasette/blob/0.50/datasette/views/table.py#L742-L770"&gt;the internal TableView class&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;It's not available as a documented, stable Python API... but it IS available via the Datasette JSON API.&lt;/p&gt;
&lt;p&gt;Wouldn't it be great if Datasette plugins could make direct calls to the same externally facing, documented HTTP JSON API that Datasette itself exposes to end users?&lt;/p&gt;
&lt;p&gt;That's what the new &lt;code&gt;datasette.client&lt;/code&gt; object does. It's a thin wrapper around &lt;a href="https://www.python-httpx.org/async/"&gt;HTTPX AsyncClient&lt;/a&gt; (the excellent new Python HTTP library which takes the Requests API and makes it fully asyncio compliant) which dispatches requests internally to Datasette's ASGI application, without any of the network overhead of an external HTTP request.&lt;/p&gt;
&lt;p&gt;One of my goals for Datasette 1.0 is to bring the externally facing JSON API to full, documented, stable status.&lt;/p&gt;
&lt;p&gt;The idea of Python plugins being able to efficiently use that same API feels really elegant to me. I'm looking forward to taking advantage of this in my own plugins.&lt;/p&gt;

&lt;h4 id="deploying-datasette"&gt;Deploying Datasette documentation&lt;/h4&gt;
&lt;blockquote cite="https://docs.datasette.io/en/stable/changelog.html#v0-50"&gt;&lt;p&gt;New &lt;a href="https://docs.datasette.io/en/stable/deploying.html#deploying"&gt;Deploying Datasette&lt;/a&gt; documentation with guides for deploying Datasette on a Linux server &lt;a href="https://docs.datasette.io/en/stable/deploying.html#deploying-systemd"&gt;using systemd&lt;/a&gt; or to hosting providers &lt;a href="https://docs.datasette.io/en/stable/deploying.html#deploying-buildpacks"&gt;that support buildpacks&lt;/a&gt;. (&lt;a href="https://github.com/simonw/datasette/issues/514"&gt;#514&lt;/a&gt;, &lt;a href="https://github.com/simonw/datasette/issues/997"&gt;#997&lt;/a&gt;)&lt;/p&gt;&lt;/blockquote&gt;

&lt;p&gt;The buildpack documenation was inspired by &lt;a href="https://til.simonwillison.net/til/til/digitalocean_datasette-on-digitalocean-app-platform.md"&gt;my experiments&lt;/a&gt; with the new &lt;a href="https://www.digitalocean.com/docs/app-platform/"&gt;DigitialOcean App Platform&lt;/a&gt; this week. App Platform is a Heroku-style PaaS hosting platform that implements the &lt;a href="https://buildpacks.io/"&gt;Cloud Native Buildpacks&lt;/a&gt; standard which emerged based on Heroku's architecture a few years ago.&lt;/p&gt;

&lt;p&gt;I hadn't realized quite how easy it is to run a custom Python application (such as Datasette) using buildpacks - it's literally just a GitHub repository with two single-line files in it, &lt;samp&gt;requirements.txt&lt;/samp&gt; and &lt;samp&gt;Procfile&lt;/samp&gt; - the buildpacks mechanism detects the &lt;samp&gt;requirements.txt&lt;/samp&gt; and configures a Python environment automatically.&lt;/p&gt;

&lt;p&gt;I deployed my new &lt;a href="https://github.com/simonw/buildpack-datasette-demo"&gt;simonw/buildpack-datasette-demo&lt;/a&gt; repo on DigitalOcean, Heroku and &lt;a href="https://scalingo.com/"&gt;Scalingo&lt;/a&gt; to try this out. It worked on all three providers with no changes - and all three offer continuous deployment against GitHub where any changes to that repository automatically trigger a deployment (optionally guarded by a CI test suite).&lt;/p&gt;

&lt;p&gt;Since I was creating a deployment documenatation page I decided to finally address &lt;a href="https://github.com/simonw/datasette/issues/514"&gt;issue 514&lt;/a&gt; and document how I've used &lt;samp&gt;systemd&lt;/samp&gt; to deploy Datasette on some of my own projects. I'm very keen to hear from people who try out this recipe so I can continue to improve it over time.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;This is part of a series: see also the annotated release notes for Datasette &lt;a href="https://simonwillison.net/2020/Jun/12/annotated-release-notes/"&gt;0.44&lt;/a&gt;, &lt;a href="https://simonwillison.net/2020/Jul/1/datasette-045/"&gt;0.45&lt;/a&gt; and &lt;a href="https://simonwillison.net/2020/Sep/15/datasette-0-49/"&gt;0.49&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/projects"&gt;projects&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/releasenotes"&gt;releasenotes&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/datasette"&gt;datasette&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/annotated-release-notes"&gt;annotated-release-notes&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/httpx"&gt;httpx&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="projects"/><category term="releasenotes"/><category term="datasette"/><category term="annotated-release-notes"/><category term="httpx"/></entry><entry><title>Weeknotes: datasette-auth-existing-cookies and datasette-sentry</title><link href="https://simonwillison.net/2020/Jan/29/weeknotes-datasette-cookies-sentry/#atom-tag" rel="alternate"/><published>2020-01-29T05:58:13+00:00</published><updated>2020-01-29T05:58:13+00:00</updated><id>https://simonwillison.net/2020/Jan/29/weeknotes-datasette-cookies-sentry/#atom-tag</id><summary type="html">
    &lt;p&gt;Work on &lt;a href="https://simonwillison.net/tags/datasettecloud/"&gt;Datasette Cloud&lt;/a&gt; continues - I'm tantalizingly close to having a MVP I can start to invite people to try out.&lt;/p&gt;

&lt;p&gt;I'm trying to get as much work as possible done for it using Datasette Plugins. This week I've released two new plugins to assist in the effort.&lt;/p&gt;

&lt;h3&gt;datasette-auth-existing-cookies&lt;/h3&gt;

&lt;p&gt;My first attempt at adding authentication to Datasette was &lt;a href="https://simonwillison.net/2019/Jul/14/sso-asgi/"&gt;datasette-auth-github&lt;/a&gt;, which takes advantages of GitHub's best-in-class OAuth flow to implement sign-in with GitHub and builds a relatively powerful permission system on top of GitHub users, organizations and teams.&lt;/p&gt;

&lt;p&gt;For Datasette Cloud I need to go a step further: I'm definitely going to have regular username/password accounts, and I'll probably implement sign-in-with-Google as well.&lt;/p&gt;

&lt;p&gt;I don't particularly want to implement username/password accounts from scratch. Django (and &lt;a href="https://github.com/ubernostrum/django-registration"&gt;django-registration&lt;/a&gt;) provide robust and very well tested solution for this. How about I use that?&lt;/p&gt;

&lt;p&gt;Datasette Cloud teams will each get their own Datasette instance running on a subdomain. If I implement authentication as a Django app running on &lt;code&gt;example.com&lt;/code&gt; I can set that as the cookie domain - then Datasette instances running on &lt;code&gt;teamname.example.com&lt;/code&gt; will be able to see the resulting authentication cookie.&lt;/p&gt;

&lt;p&gt;Given a Django authentication cookie (which may just be a &lt;code&gt;sessionid&lt;/code&gt;) how can I tell if it corresponds to a logged in user? That's where my new &lt;a href="https://github.com/simonw/datasette-auth-existing-cookies/"&gt;datasette-auth-existing-cookies&lt;/a&gt; plugin comes in.&lt;/p&gt;

&lt;p&gt;The plugin lets you configure Datasette to read in a specified list of cookies and then forward them on as part of an API request to an underlying application. That application then returns JSON showing if the user is signed in or not. The plugin then sets a short-lived signed cookie that persists that information.&lt;/p&gt;

&lt;p&gt;Here's what the configuration looks like:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;{
    "plugins": {
        "datasette-auth-existing-cookies": {
            "api_url": "https://www.example.com/user-from-cookies",
            "auth_redirect_url": "https://www.example.com/login",
            "original_cookies": ["sessionid"]
        }
    }
}&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Any hits to &lt;code&gt;teamname.example.com&lt;/code&gt; will be checked for a &lt;code&gt;sessionid&lt;/code&gt; cookie. That cookie is forwarded on to &lt;code&gt;https://www.example.com/user-from-cookies&lt;/code&gt; to see if it's valid.&lt;/p&gt;

&lt;p&gt;If the cookie is missing or invalid, the user will be redirected to the following URL:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;https://www.example.com/login?next=https://teamname.example.com/&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;The plugin has a few other options: you can request that the &lt;code&gt;?next=&lt;/code&gt; parameter is itself signed to help protect against &lt;a href="https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html"&gt;unvalidated redirects&lt;/a&gt; for example. But it's a pretty simple piece of code that hopefully means I won't have to spend much more time thinking about login and registration.&lt;/p&gt;

&lt;h3 id="httpx-testing"&gt;httpx for testing ASGI apps&lt;/h3&gt;

&lt;p&gt;All of my Datasette plugins ship with unit tests - mainly so that I can implement continuous deployment from them, where new tagged releases are automatically shipped to PyPI provided the tests pass.&lt;/p&gt;

&lt;p&gt;For ASGI plugins, this means writing unit tests against the ASGI spec. I've mainly been doing this using the &lt;code&gt;ApplicationCommunicator&lt;/code&gt; class from &lt;a href="https://github.com/django/asgiref"&gt;asgiref&lt;/a&gt;, which provides powerful low-level hooks for interacting with an ASGI application. The tests end up being pretty verbose though!&lt;/p&gt;

&lt;p&gt;Here's &lt;a href="https://github.com/simonw/datasette-auth-existing-cookies/blob/be84a5f85b6f827cfb3f34a1795a6b37e46e38f3/test_datasette_auth_existing_cookies.py#L31-L63"&gt;the ApplicationCommunicator test&lt;/a&gt; I first wrote for &lt;code&gt;datasette-auth-existing-cookies&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;I've been exploring Tom Christie's &lt;a href="https://www.python-httpx.org/"&gt;httpx library&lt;/a&gt; for asynchronous HTTP calls in Python recently, and I spotted an interesting capability buried deep in the documentation: you can &lt;a href="https://www.python-httpx.org/async/#calling-into-python-web-apps"&gt;pass it an ASGI app&lt;/a&gt; and make requests directly against the app, without round-tripping through HTTP!&lt;/p&gt;

&lt;p&gt;This looked ideal for unit testing, so I had a go at rewriting my tests using it. &lt;a href="https://github.com/simonw/datasette-auth-existing-cookies/blob/a9f4de0ec1c61956c05caf434e2e95e6952d5474/test_datasette_auth_existing_cookies.py#L33-L46"&gt;The result&lt;/a&gt; was delightful:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;auth_app = ExistingCookiesAuthTest(
    hello_world_app,
    ...
)
async with httpx.AsyncClient(app=auth_app) as client:
    response = await client.get(
        "https://demo.example.com/", allow_redirects=False
    )
    assert 302 == response.status_code
    location = response.headers["location"]
    assert "https://www.example.com/login" == location&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;This is a much nicer way of writing tests for ASGI applications and middleware. I'm going to be using this for all of my projects going forward.&lt;/p&gt;

&lt;h3&gt;datasette-sentry&lt;/h3&gt;

&lt;p&gt;In starting to deploy Datasette Cloud I quickly ran into the need to start collecting and analyzing errors thrown in production.&lt;/p&gt;

&lt;p&gt;I've been enjoing using &lt;a href="https://sentry.io/"&gt;Sentry&lt;/a&gt; for this for several years now, and I was pleased to see that the official Sentry SDK &lt;a href="https://docs.sentry.io/platforms/python/asgi/"&gt;grew support for ASGI&lt;/a&gt; last July.&lt;/p&gt;

&lt;p&gt;Wrapping it up as a Datasette plugin took less than half an hour: &lt;a href="https://github.com/simonw/datasette-sentry"&gt;datasette-sentry&lt;/a&gt;. It's configured like this:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;{
    "plugins": {
        "datasette-sentry": {
            "dsn": {
                "$env": "SENTRY_DSN"
            }
        }
    }
}&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The DSN configuring Sentry will then be read from the &lt;code&gt;SENTRY_DSN&lt;/code&gt; environment variable.&lt;/p&gt;
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/cookies"&gt;cookies&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/django"&gt;django&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/projects"&gt;projects&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/sentry"&gt;sentry&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/datasette-cloud"&gt;datasette-cloud&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/httpx"&gt;httpx&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="cookies"/><category term="django"/><category term="projects"/><category term="sentry"/><category term="datasette"/><category term="weeknotes"/><category term="datasette-cloud"/><category term="httpx"/></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></feed>