<?xml version="1.0" encoding="utf-8"?>
<feed xml:lang="en-us" xmlns="http://www.w3.org/2005/Atom"><title>Simon Willison's Weblog: cookiecutter</title><link href="http://simonwillison.net/" rel="alternate"/><link href="http://simonwillison.net/tags/cookiecutter.atom" rel="self"/><id>http://simonwillison.net/</id><updated>2024-10-24T05:56:21+00:00</updated><author><name>Simon Willison</name></author><entry><title>TIL: Using uv to develop Python command-line applications</title><link href="https://simonwillison.net/2024/Oct/24/uv-cli/#atom-tag" rel="alternate"/><published>2024-10-24T05:56:21+00:00</published><updated>2024-10-24T05:56:21+00:00</updated><id>https://simonwillison.net/2024/Oct/24/uv-cli/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://til.simonwillison.net/python/uv-cli-apps"&gt;TIL: Using uv to develop Python command-line applications&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
I've been increasingly using &lt;a href="https://docs.astral.sh/uv/"&gt;uv&lt;/a&gt; to try out new software (via &lt;code&gt;uvx&lt;/code&gt;) and experiment with new ideas, but I hadn't quite figured out the right way to use it for developing my own projects.&lt;/p&gt;
&lt;p&gt;It turns out I was missing a few things - in particular the fact that there's no need to use &lt;code&gt;uv pip&lt;/code&gt; at all when working with a local development environment, you can get by entirely on &lt;code&gt;uv run&lt;/code&gt; (and maybe &lt;code&gt;uv sync --extra test&lt;/code&gt; to install test dependencies) with no direct invocations of &lt;code&gt;uv pip&lt;/code&gt; at all.&lt;/p&gt;
&lt;p&gt;I bounced &lt;a href="https://gist.github.com/simonw/975dfa41e9b03bca2513a986d9aa3dcf"&gt;a few questions&lt;/a&gt; off Charlie Marsh and filled in the missing gaps - this TIL shows my new uv-powered process for hacking on Python CLI apps built using Click and my &lt;a href="https://github.com/simonw/click-app"&gt;simonw/click-app&lt;/a&gt; cookecutter template.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/cli"&gt;cli&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/packaging"&gt;packaging&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/pip"&gt;pip&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/python"&gt;python&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/til"&gt;til&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/cookiecutter"&gt;cookiecutter&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/uv"&gt;uv&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/astral"&gt;astral&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/charlie-marsh"&gt;charlie-marsh&lt;/a&gt;&lt;/p&gt;



</summary><category term="cli"/><category term="packaging"/><category term="pip"/><category term="python"/><category term="til"/><category term="cookiecutter"/><category term="uv"/><category term="astral"/><category term="charlie-marsh"/></entry><entry><title>simonw/docs cookiecutter template</title><link href="https://simonwillison.net/2024/Sep/23/docs-cookiecutter/#atom-tag" rel="alternate"/><published>2024-09-23T21:45:15+00:00</published><updated>2024-09-23T21:45:15+00:00</updated><id>https://simonwillison.net/2024/Sep/23/docs-cookiecutter/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/simonw/docs"&gt;simonw/docs cookiecutter template&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Over the last few years I’ve settled on the combination of &lt;a href="https://www.sphinx-doc.org/"&gt;Sphinx&lt;/a&gt;, the &lt;a href="https://github.com/pradyunsg/furo"&gt;Furo&lt;/a&gt; theme and the &lt;a href="https://myst-parser.readthedocs.io/en/latest/"&gt;myst-parser&lt;/a&gt; extension (enabling Markdown in place of reStructuredText) as my documentation toolkit of choice, maintained in GitHub and hosted using &lt;a href="https://about.readthedocs.com/"&gt;ReadTheDocs&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;My &lt;a href="https://llm.datasette.io/"&gt;LLM&lt;/a&gt; and &lt;a href="https://shot-scraper.datasette.io/"&gt;shot-scraper&lt;/a&gt; projects are two examples of that stack in action.&lt;/p&gt;
&lt;p&gt;Today I wanted to spin up a new documentation site so I finally took the time to construct a &lt;a href="https://cookiecutter.readthedocs.io/"&gt;cookiecutter&lt;/a&gt; template for my preferred configuration. You can use it like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pipx install cookiecutter
cookiecutter gh:simonw/docs
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Or with &lt;a href="https://docs.astral.sh/uv/"&gt;uv&lt;/a&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;uv tool run cookiecutter gh:simonw/docs
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Answer a few questions:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[1/3] project (): shot-scraper
[2/3] author (): Simon Willison
[3/3] docs_directory (docs):
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And it creates a &lt;code&gt;docs/&lt;/code&gt; directory ready for you to start editing docs:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cd docs
pip install -r requirements.txt
make livehtml
&lt;/code&gt;&lt;/pre&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/documentation"&gt;documentation&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/markdown"&gt;markdown&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/cookiecutter"&gt;cookiecutter&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/sphinx-docs"&gt;sphinx-docs&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/read-the-docs"&gt;read-the-docs&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/uv"&gt;uv&lt;/a&gt;&lt;/p&gt;



</summary><category term="documentation"/><category term="projects"/><category term="python"/><category term="markdown"/><category term="cookiecutter"/><category term="sphinx-docs"/><category term="read-the-docs"/><category term="uv"/></entry><entry><title>Upgrading my cookiecutter templates to use python -m pytest</title><link href="https://simonwillison.net/2024/Aug/17/python-m-pytest/#atom-tag" rel="alternate"/><published>2024-08-17T05:12:47+00:00</published><updated>2024-08-17T05:12:47+00:00</updated><id>https://simonwillison.net/2024/Aug/17/python-m-pytest/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/simonw/python-lib/issues/9"&gt;Upgrading my cookiecutter templates to use python -m pytest&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Every now and then I get caught out by weird test failures when I run &lt;code&gt;pytest&lt;/code&gt; and it turns out I'm running the wrong installation of that tool, so my tests fail because that &lt;code&gt;pytest&lt;/code&gt; is executing in a different virtual environment from the one needed by the tests.&lt;/p&gt;
&lt;p&gt;The fix for this is easy: run &lt;code&gt;python -m pytest&lt;/code&gt; instead, which guarantees that you will run &lt;code&gt;pytest&lt;/code&gt; in the same environment as your currently active Python.&lt;/p&gt;
&lt;p&gt;Yesterday I went through and updated every one of my &lt;code&gt;cookiecutter&lt;/code&gt; templates (&lt;a href="https://github.com/simonw/python-lib"&gt;python-lib&lt;/a&gt;, &lt;a href="https://github.com/simonw/click-app"&gt;click-app&lt;/a&gt;, &lt;a href="https://github.com/simonw/datasette-plugin"&gt;datasette-plugin&lt;/a&gt;, &lt;a href="https://github.com/simonw/sqlite-utils-plugin"&gt;sqlite-utils-plugin&lt;/a&gt;, &lt;a href="https://github.com/simonw/llm-plugin"&gt;llm-plugin&lt;/a&gt;) to use this pattern in their READMEs and generated repositories instead, to help spread that better recipe a little bit further.


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



</summary><category term="projects"/><category term="python"/><category term="pytest"/><category term="cookiecutter"/></entry><entry><title>Publish Python packages to PyPI with a python-lib cookiecutter template and GitHub Actions</title><link href="https://simonwillison.net/2024/Jan/16/python-lib-pypi/#atom-tag" rel="alternate"/><published>2024-01-16T21:59:56+00:00</published><updated>2024-01-16T21:59:56+00:00</updated><id>https://simonwillison.net/2024/Jan/16/python-lib-pypi/#atom-tag</id><summary type="html">
    &lt;p&gt;I use &lt;a href="https://github.com/cookiecutter/cookiecutter"&gt;cookiecutter&lt;/a&gt; to start almost all of my Python projects. It helps me quickly generate a skeleton of a project with my preferred directory structure and configured tools.&lt;/p&gt;
&lt;p&gt;I made some major upgrades to my &lt;a href="https://github.com/simonw/python-lib"&gt;python-lib&lt;/a&gt; cookiecutter template today. Here's what it can now do to help you get started with a new Python library:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Create a &lt;code&gt;pyproject.toml&lt;/code&gt; file configured for use with &lt;code&gt;setuptools&lt;/code&gt;. In my opinion this is the pattern with the current lowest learning curve - I wrote about that &lt;a href="https://til.simonwillison.net/python/pyproject"&gt;in detail in this TIL&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Add a skeleton &lt;code&gt;README&lt;/code&gt; and an Apache 2.0 &lt;code&gt;LICENSE&lt;/code&gt; file.&lt;/li&gt;
&lt;li&gt;Create &lt;code&gt;your_package/__init__.py&lt;/code&gt; for your code to go in.&lt;/li&gt;
&lt;li&gt;Create &lt;code&gt;tests/test_your_package.py&lt;/code&gt; with a skeleton test.&lt;/li&gt;
&lt;li&gt;Include &lt;code&gt;pytest&lt;/code&gt; as a test dependency.&lt;/li&gt;
&lt;li&gt;Configure GitHub Actions with two workflows in &lt;code&gt;.github/workflows&lt;/code&gt; - one for running the tests against Python 3.8 through 3.12, and one for publishing releases of your package to PyPI.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The changes I made today are that I switched from &lt;code&gt;setup.py&lt;/code&gt; to &lt;code&gt;pyproject.toml&lt;/code&gt;, and I made a big improvement to how the publishing workflow authenticates with PyPI.&lt;/p&gt;
&lt;h4 id="pypi-trusted-publishing"&gt;Publishing to PyPI with Trusted Publishing&lt;/h4&gt;
&lt;p&gt;My previous version of this template required you to jump through &lt;a href="https://github.com/simonw/python-lib/blob/c28bd8cf822455fd464c253daf4ef4b430758588/README.md#publishing-your-library-as-a-package-to-pypi"&gt;quite a few hoops&lt;/a&gt; to get PyPI publishing to work. You needed to create a PyPI token that could publish a new package, then paste that token into a GitHub Actions secret, then publish the package, and then disable that token and create a new one dedicated to just updating this package in the future.&lt;/p&gt;
&lt;p&gt;The new version is much simpler, thanks to PyPI's relatively new &lt;a href="https://docs.pypi.org/trusted-publishers/"&gt;Trusted Publishers&lt;/a&gt; mechanism.&lt;/p&gt;
&lt;p&gt;To publish a new package, you need to sign into PyPI and &lt;a href="https://pypi.org/manage/account/publishing/"&gt;create a new "pending publisher"&lt;/a&gt;. Effectively you tell PyPI "My GitHub repository &lt;code&gt;myname/name-of-repo&lt;/code&gt; should be allowed to publish packages with the name &lt;code&gt;name-of-package&lt;/code&gt;".&lt;/p&gt;
&lt;p&gt;Here's that form for my brand new &lt;a href="https://github.com/datasette/datasette-test"&gt;datasette-test&lt;/a&gt; library, the first library I published using this updated template:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://static.simonwillison.net/static/2024/datasette-test.png" alt="Screenshot of the create pending publisher form on PyPI. PyPI Project Name is set to datasette-test. Owner is set to datasette. Repository name is datasette-test. Workflow name is publish.yml. Environment name is release." style="max-width: 100%;" /&gt;&lt;/p&gt;
&lt;p&gt;Then create a release on GitHub, with a name that matches the version number from your &lt;code&gt;pyproject.toml&lt;/code&gt;. Everything else should Just Work.&lt;/p&gt;
&lt;p&gt;I wrote &lt;a href="https://til.simonwillison.net/pypi/pypi-releases-from-github"&gt;more about Trusted Publishing in this TIL&lt;/a&gt;.&lt;/p&gt;
&lt;h4 id="github-repository-template"&gt;Creating a package using a GitHub repository template&lt;/h4&gt;
&lt;p&gt;The &lt;a href="https://github.com/simonw/python-lib/issues/6"&gt;most time consuming part&lt;/a&gt; of this project was getting my GitHub repository template to work properly.&lt;/p&gt;
&lt;p&gt;There are two ways to use my cookiecutter template. You can use the cookiecutter command-line tool like this:&lt;/p&gt;
&lt;div class="highlight highlight-source-shell"&gt;&lt;pre&gt;pipx install cookiecutter
cookiecutter gh:simonw/python-lib
&lt;span class="pl-c"&gt;&lt;span class="pl-c"&gt;#&lt;/span&gt; Answer a few questions here&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;But a more fun and convenient option is to use my GitHub repository template, &lt;a href="https://github.com/simonw/python-lib-template-repository"&gt;simonw/python-lib-template-repository&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;This lets you &lt;a href="https://github.com/new?template_name=python-lib-template-repository&amp;amp;template_owner=simonw"&gt;fill in a form&lt;/a&gt; on GitHub to create a new repository which will then execute the cookiecutter template for you and update itself with the result.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://static.simonwillison.net/static/2024/template-repo-create.jpg" alt="Create a new repository form. I'm using the python-lib-template-repository template, and it asks for my repository name (my-new-python-library) and description." style="max-width: 100%;" /&gt;&lt;/p&gt;
&lt;p&gt;You can see an example of a repository created using this template at &lt;a href="https://github.com/datasette/datasette-test/tree/8d5f8262dc3a88f3c6d97f0cef3b55264cabc695"&gt;datasette/datasette-test&lt;/a&gt;.&lt;/p&gt;
&lt;h4 id="adding-it-all-together"&gt;Adding it all together&lt;/h4&gt;
&lt;p&gt;There are quite a lot of moving parts under the scenes here, but the end result is that anyone can now create a Python library with test coverage, GitHub CI and release automation by filling in a couple of forms and clicking some buttons.&lt;/p&gt;
&lt;p&gt;For more details on how this all works, and how it's evolved over time:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://simonwillison.net/2020/Jun/20/cookiecutter-plugins/"&gt;A cookiecutter template for writing Datasette plugins&lt;/a&gt; from June 2020 describes my first experiments with cookiecutter&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://simonwillison.net/2021/Aug/28/dynamic-github-repository-templates/"&gt;Dynamic content for GitHub repository templates using cookiecutter and GitHub Actions&lt;/a&gt; from August 2021 describes my earliest attempts at using GitHub repository templates for this&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://simonwillison.net/2021/Nov/4/publish-open-source-python-library/"&gt;How to build, test and publish an open source Python library&lt;/a&gt; is a ten minute talk I gave at PyGotham in November 2021. It describes &lt;code&gt;setup.py&lt;/code&gt; in detail, which is no longer my preferred approach.&lt;/li&gt;
&lt;/ul&gt;
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/github"&gt;github&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/projects"&gt;projects&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/pypi"&gt;pypi&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/python"&gt;python&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/github-actions"&gt;github-actions&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/cookiecutter"&gt;cookiecutter&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="github"/><category term="projects"/><category term="pypi"/><category term="python"/><category term="github-actions"/><category term="cookiecutter"/></entry><entry><title>Cookiecutter Data Science</title><link href="https://simonwillison.net/2021/Nov/18/cookiecutter-data-science/#atom-tag" rel="alternate"/><published>2021-11-18T15:21:59+00:00</published><updated>2021-11-18T15:21:59+00:00</updated><id>https://simonwillison.net/2021/Nov/18/cookiecutter-data-science/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://drivendata.github.io/cookiecutter-data-science/"&gt;Cookiecutter Data Science&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Some really solid thinking in this documentation for the DrivenData cookiecutter template. They emphasize designing data science projects for repeatability, such that just the src/ and data/ folders can be used to recreate all of the other analysis from scratch. I like the suggestion to give each project a dedicated S3 bucket for keeping immutable copies of the original raw data that might be too large for GitHub.

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="https://twitter.com/dynamicwebpaige/status/1461272760542396421"&gt;Paige Bailey&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/data-science"&gt;data-science&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/cookiecutter"&gt;cookiecutter&lt;/a&gt;&lt;/p&gt;



</summary><category term="data-science"/><category term="cookiecutter"/></entry><entry><title>Dynamic content for GitHub repository templates using cookiecutter and GitHub Actions</title><link href="https://simonwillison.net/2021/Aug/28/dynamic-github-repository-templates/#atom-tag" rel="alternate"/><published>2021-08-28T21:29:12+00:00</published><updated>2021-08-28T21:29:12+00:00</updated><id>https://simonwillison.net/2021/Aug/28/dynamic-github-repository-templates/#atom-tag</id><summary type="html">
    &lt;p&gt;GitHub repository templates were &lt;a href="https://github.blog/2019-06-06-generate-new-repositories-with-repository-templates/"&gt;introduced a couple of years ago&lt;/a&gt; to provide a mechanism for creating a brand new GitHub repository starting with an initial set of files.&lt;/p&gt;
&lt;p&gt;They have one big limitation: the repositories that they create share the exact same contents as the template repository. They're basically a replacement for duplicating an existing folder and using that as the starting point for a new project.&lt;/p&gt;
&lt;p&gt;I'm a big fan of the Python &lt;a href="https://cookiecutter.readthedocs.io/"&gt;cookiecutter&lt;/a&gt; tool, which provides a way to dynamically create new folder structures from user-provided variables using Jinja templates to generate content.&lt;/p&gt;
&lt;p&gt;This morning, inspired by &lt;a href="https://github.com/rochacbruno/python-project-template"&gt;this repo&lt;/a&gt; by Bruno Rocha, I finally figured out a neat pattern for combining cookiecutter with repository templates to compensate for that missing dynamic content ability.&lt;/p&gt;
&lt;p&gt;The result: &lt;a href="https://github.com/simonw/datasette-plugin-template-repository"&gt;datasette-plugin-template-repository&lt;/a&gt; for creating new Datasette plugins with a single click, &lt;a href="https://github.com/simonw/python-lib-template-repository"&gt;python-lib-template-repository&lt;/a&gt; for creating new Python libraries and &lt;a href="https://github.com/simonw/click-app-template-repository"&gt;click-app-template-repository&lt;/a&gt; for creating Click CLI tools.&lt;/p&gt;
&lt;h4&gt;Cookiecutter&lt;/h4&gt;
&lt;p&gt;I maintain three cookiecutter templates at the moment:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/simonw/datasette-plugin"&gt;simonw/datasette-plugin&lt;/a&gt;, for creating new &lt;a href="https://docs.datasette.io/en/stable/writing_plugins.html"&gt;Datasette plugins&lt;/a&gt;. I've used that one for dozens of plugins myself.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/simonw/click-app"&gt;simonw/click-app&lt;/a&gt;, which generates a skeleton for a new &lt;a href="https://click.palletsprojects.com/"&gt;Click-based&lt;/a&gt; command-line tool. Many of my &lt;a href="https://datasette.io/tools?q=to-sqlite"&gt;x-to-sqlite&lt;/a&gt; tools were built using this.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/simonw/python-lib"&gt;simonw/python-lib&lt;/a&gt;, for generating general-purpose Python libraries.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Having installed cookiecutter (&lt;code&gt;pip install cookiecutter&lt;/code&gt;) each of these can be used like so:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;% cookiecutter gh:simonw/datasette-plugin
plugin_name []: visualize counties
description []: Datasette plugin for visualizing counties
hyphenated [visualize-counties]: 
underscored [visualize_counties]: 
github_username []: simonw
author_name []: Simon Willison
include_static_directory []: y
include_templates_directory []: 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Cookiecutter prompts for some variables defined in a &lt;code&gt;cookiecutter.json&lt;/code&gt; file, then generates the project by evaluating the templates.&lt;/p&gt;
&lt;p&gt;The challenge was: how can I run this automatically when a new repository is created from a GitHub repository template? And where can I get those variables from?&lt;/p&gt;
&lt;h4&gt;Bruno's trick: a self-rewriting repository&lt;/h4&gt;
&lt;p&gt;Bruno has a brilliant trick for getting this to run, exhibited by &lt;a href="https://github.com/rochacbruno/python-project-template/blob/cec9c32ba007d633c4b9d26aad67889d1a6a5e9a/.github/workflows/rename_project.yml"&gt;this workflow YAML&lt;/a&gt;. His workflow starts like this:&lt;/p&gt;
&lt;div class="highlight highlight-source-yaml"&gt;&lt;pre&gt;&lt;span class="pl-ent"&gt;name&lt;/span&gt;: &lt;span class="pl-s"&gt;Rename the project from template&lt;/span&gt;

&lt;span class="pl-ent"&gt;on&lt;/span&gt;: &lt;span class="pl-s"&gt;[push]&lt;/span&gt;

&lt;span class="pl-ent"&gt;jobs&lt;/span&gt;:
  &lt;span class="pl-ent"&gt;rename-project&lt;/span&gt;:
    &lt;span class="pl-ent"&gt;if&lt;/span&gt;: &lt;span class="pl-s"&gt;${{ github.repository != 'rochacbruno/python-project-template' }}&lt;/span&gt;
    &lt;span class="pl-ent"&gt;runs-on&lt;/span&gt;: &lt;span class="pl-s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="pl-ent"&gt;steps&lt;/span&gt;:
       &lt;span class="pl-c"&gt;&lt;span class="pl-c"&gt;#&lt;/span&gt; ...&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;This means that his workflow &lt;em&gt;only&lt;/em&gt; runs on copies of the original repository - the workflow is disabled in the template repository itself by that &lt;code&gt;if:&lt;/code&gt; condition.&lt;/p&gt;
&lt;p&gt;Then at the end of the workflow he does this:&lt;/p&gt;
&lt;div class="highlight highlight-source-yaml"&gt;&lt;pre&gt;      - &lt;span class="pl-ent"&gt;uses&lt;/span&gt;: &lt;span class="pl-s"&gt;stefanzweifel/git-auto-commit-action@v4&lt;/span&gt;
        &lt;span class="pl-ent"&gt;with&lt;/span&gt;:
          &lt;span class="pl-ent"&gt;commit_message&lt;/span&gt;: &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;Ready to clone and code&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;
          &lt;span class="pl-ent"&gt;push_options&lt;/span&gt;: &lt;span class="pl-s"&gt;--force&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;This does a force push to replace the contents of the repository with whatever was generated by the rest of the workflow script!&lt;/p&gt;
&lt;p&gt;This trick was exactly what I needed to get cookiecutter to work with repository templates.&lt;/p&gt;
&lt;h4&gt;Gathering variables using the GitHub GraphQL API&lt;/h4&gt;
&lt;p&gt;All three of my existing cookiecutter templates require the following variables:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A name to use for the generated folder&lt;/li&gt;
&lt;li&gt;A one-line description to use in the README and in &lt;code&gt;setup.py&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;The GitHub username of the owner of the package&lt;/li&gt;
&lt;li&gt;The display name of the owner&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I need values for all of these before I can run cookiecutter.&lt;/p&gt;
&lt;p&gt;It turns out they are all available from the GitHub GraphQL API, which can be called from the initial workflow copied from the repository template!&lt;/p&gt;
&lt;p&gt;Here's the GitHub Actions step that does that:&lt;/p&gt;
&lt;div class="highlight highlight-source-yaml"&gt;&lt;pre&gt;- &lt;span class="pl-ent"&gt;uses&lt;/span&gt;: &lt;span class="pl-s"&gt;actions/github-script@v4&lt;/span&gt;
  &lt;span class="pl-ent"&gt;id&lt;/span&gt;: &lt;span class="pl-s"&gt;fetch-repo-and-user-details&lt;/span&gt;
  &lt;span class="pl-ent"&gt;with&lt;/span&gt;:
    &lt;span class="pl-ent"&gt;script&lt;/span&gt;: &lt;span class="pl-s"&gt;|&lt;/span&gt;
&lt;span class="pl-s"&gt;      const query = `query($owner:String!, $name:String!) {&lt;/span&gt;
&lt;span class="pl-s"&gt;        repository(owner:$owner, name:$name) {&lt;/span&gt;
&lt;span class="pl-s"&gt;          name&lt;/span&gt;
&lt;span class="pl-s"&gt;          description&lt;/span&gt;
&lt;span class="pl-s"&gt;          owner {&lt;/span&gt;
&lt;span class="pl-s"&gt;            login&lt;/span&gt;
&lt;span class="pl-s"&gt;            ... on User {&lt;/span&gt;
&lt;span class="pl-s"&gt;              name&lt;/span&gt;
&lt;span class="pl-s"&gt;            }&lt;/span&gt;
&lt;span class="pl-s"&gt;            ... on Organization {&lt;/span&gt;
&lt;span class="pl-s"&gt;              name&lt;/span&gt;
&lt;span class="pl-s"&gt;            }&lt;/span&gt;
&lt;span class="pl-s"&gt;          }&lt;/span&gt;
&lt;span class="pl-s"&gt;        }&lt;/span&gt;
&lt;span class="pl-s"&gt;      }`;&lt;/span&gt;
&lt;span class="pl-s"&gt;      const variables = {&lt;/span&gt;
&lt;span class="pl-s"&gt;        owner: context.repo.owner,&lt;/span&gt;
&lt;span class="pl-s"&gt;        name: context.repo.repo&lt;/span&gt;
&lt;span class="pl-s"&gt;      }&lt;/span&gt;
&lt;span class="pl-s"&gt;      const result = await github.graphql(query, variables)&lt;/span&gt;
&lt;span class="pl-s"&gt;      console.log(result)&lt;/span&gt;
&lt;span class="pl-s"&gt;      return result&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Here I'm using the &lt;a href="https://github.com/actions/github-script"&gt;actions/github-script&lt;/a&gt; action, which provides a pre-configured, authenticated instance of GitHub's &lt;a href="https://octokit.github.io/rest.js"&gt;octokit/rest.js&lt;/a&gt; JavaScript library. You can then provide custom JavaScript that will be executed by the action.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;await github.graphql(query, variables)&lt;/code&gt; can then execute a GitHub GraphQL query. The query I'm using here gives me back the current repository's &lt;code&gt;name&lt;/code&gt; and &lt;code&gt;description&lt;/code&gt; and the &lt;code&gt;login&lt;/code&gt; and display name of the owner of that repository.&lt;/p&gt;
&lt;p&gt;GitHub repositories can be owned by either a user or an organization - the &lt;code&gt;... on User&lt;/code&gt; / &lt;code&gt;... on Organization&lt;/code&gt; syntax provides the same result here for both types of nested object.&lt;/p&gt;
&lt;p&gt;The output of this GraphQL query looks something like 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;repository&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;name&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-verify&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;description&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;Verify that files can be opened by Datasette&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;owner&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;login&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;simonw&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;name&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;Simon Willison&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;
    }
  }
}&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;I assigned an &lt;code&gt;id&lt;/code&gt; of &lt;code&gt;fetch-repo-and-user-details&lt;/code&gt; to that step of the workflow, so that the &lt;code&gt;return&lt;/code&gt; value from the script could be accessed as JSON in the next step.&lt;/p&gt;
&lt;h4&gt;Passing those variables to cookiecutter&lt;/h4&gt;
&lt;p&gt;Cookiecutter defaults to asking for variables interactively, but it also supports passing in those variables as command-line parameters.&lt;/p&gt;
&lt;p&gt;Here's part of my next workflow steps that executes cookiecutter using the variables collected by the GraphQL query:&lt;/p&gt;
&lt;div class="highlight highlight-source-yaml"&gt;&lt;pre&gt;- &lt;span class="pl-ent"&gt;name&lt;/span&gt;: &lt;span class="pl-s"&gt;Rebuild contents using cookiecutter&lt;/span&gt;
  &lt;span class="pl-ent"&gt;env&lt;/span&gt;:
    &lt;span class="pl-ent"&gt;INFO&lt;/span&gt;: &lt;span class="pl-s"&gt;${{ steps.fetch-repo-and-user-details.outputs.result }}&lt;/span&gt;
  &lt;span class="pl-ent"&gt;run&lt;/span&gt;: &lt;span class="pl-s"&gt;|&lt;/span&gt;
&lt;span class="pl-s"&gt;    export REPO_NAME=$(echo $INFO | jq -r '.repository.name')&lt;/span&gt;
&lt;span class="pl-s"&gt;    # Run cookiecutter&lt;/span&gt;
&lt;span class="pl-s"&gt;    cookiecutter gh:simonw/python-lib --no-input \&lt;/span&gt;
&lt;span class="pl-s"&gt;      lib_name=$REPO_NAME \&lt;/span&gt;
&lt;span class="pl-s"&gt;      description="$(echo $INFO | jq -r .repository.description)" \&lt;/span&gt;
&lt;span class="pl-s"&gt;      github_username="$(echo $INFO | jq -r .repository.owner.login)" \&lt;/span&gt;
&lt;span class="pl-s"&gt;      author_name="$(echo $INFO | jq -r .repository.owner.name)"&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The &lt;code&gt;env: INFO:&lt;/code&gt; block exposes an environment variable called &lt;code&gt;INFO&lt;/code&gt; to the step, populated with the output of the previous &lt;code&gt;fetch-repo-and-user-details&lt;/code&gt; step - a string of JSON.&lt;/p&gt;
&lt;p&gt;Then within the body of the step I use &lt;a href="https://stedolan.github.io/jq/"&gt;jq&lt;/a&gt; to extract out the details that I need - first the repository name:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;export REPO_NAME=$(echo $INFO | jq -r '.repository.name')
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then I pass the other details directly to cookiecutter as arguments:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cookiecutter gh:simonw/python-lib --no-input \
  lib_name=$REPO_NAME \
  description="$(echo $INFO | jq -r .repository.description)" \
  github_username="$(echo $INFO | jq -r .repository.owner.login)" \
  author_name="$(echo $INFO | jq -r .repository.owner.name)"
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;jq -r&lt;/code&gt; ensures that the raw text value is returned by &lt;code&gt;jq&lt;/code&gt;, as opposed to the JSON string value which would be wrapped in double quotes.&lt;/p&gt;
&lt;h4&gt;Cleaning up at the end&lt;/h4&gt;
&lt;p&gt;Running cookiecutter in this way creates a folder within the root of the repository that duplicates the repository name, something like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;datasette-verify/datasette-verify
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I actually want the contents of that folder to live in the root, so the next step I run is:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mv $REPO_NAME/* .
mv $REPO_NAME/.gitignore .
mv $REPO_NAME/.github .
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here's &lt;a href="https://github.com/simonw/python-lib-template-repository/blob/45869a9156aa3c2ab95bf1b985151d1d51719c1f/.github/workflows/cookiecutter.yml"&gt;my completed workflow&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;This &lt;em&gt;almost&lt;/em&gt; worked - but when I tried to run it for the first time I got this error:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;![remote rejected] (refusing to allow an integration to create or update .github/workflows/publish.yml)&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;It turns out the credentials provided to GitHub Actions are forbidden from making modifications to their own workflow files!&lt;/p&gt;
&lt;p&gt;I can understand why that limitation is in place, but it's frustrating here. For the moment, my workaround is to do this just before pushing the final content back to the repository:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mv .github/workflows .github/rename-this-to-workflows
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I leave it up to the user to rename that folder back again when they want to enable the workflows that have been generated for them.&lt;/p&gt;
&lt;h4&gt;Give these a go&lt;/h4&gt;
&lt;p&gt;I've set up three templates using this pattern now:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/simonw/datasette-plugin-template-repository"&gt;datasette-plugin-template-repository&lt;/a&gt; for creating new Datasette plugins - &lt;a href="https://github.com/simonw/datasette-plugin-template-repository/generate"&gt;use this template&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/simonw/python-lib-template-repository"&gt;python-lib-template-repository&lt;/a&gt; for creating new Python libraries - &lt;a href="https://github.com/simonw/python-lib-template-repository/generate"&gt;use this template&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/simonw/click-app-template-repository"&gt;click-app-template-repository&lt;/a&gt; for creating new Python Click CLI tools - &lt;a href="https://github.com/simonw/click-app-template-repository/generate"&gt;use this template&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Each of these works the same way: enter a repository name and description, click "Create repository from template" and watch as GitHub copies the new repository and then, a few seconds later, runs the workflow to execute the cookiecutter template to replace the contents with the final result.&lt;/p&gt;
&lt;p&gt;You can see examples of repositories that I created using these templates here:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/simonw/datasette-plugin-template-repository-demo"&gt;https://github.com/simonw/datasette-plugin-template-repository-demo&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/simonw/python-lib-template-repository-demo"&gt;https://github.com/simonw/python-lib-template-repository-demo&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/simonw/click-app-template-repository-demo"&gt;https://github.com/simonw/click-app-template-repository-demo&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/github-actions"&gt;github-actions&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/cookiecutter"&gt;cookiecutter&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="github-actions"/><category term="cookiecutter"/></entry><entry><title>pypi-rename</title><link href="https://simonwillison.net/2020/Jul/25/pypi-rename/#atom-tag" rel="alternate"/><published>2020-07-25T23:07:02+00:00</published><updated>2020-07-25T23:07:02+00:00</updated><id>https://simonwillison.net/2020/Jul/25/pypi-rename/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/simonw/pypi-rename"&gt;pypi-rename&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
I wanted to rename a PyPI package (renaming datasette-insert-api to datasette-insert as it’s about to grow some non-API features). PyPI recommend uploading a final release under the old name which points to (and depends on) the new name. I’ve built a cookiecutter template to codify that pattern.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/projects"&gt;projects&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/pypi"&gt;pypi&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/cookiecutter"&gt;cookiecutter&lt;/a&gt;&lt;/p&gt;



</summary><category term="projects"/><category term="pypi"/><category term="cookiecutter"/></entry><entry><title>Weeknotes: cookiecutter templates, better plugin documentation, sqlite-generate</title><link href="https://simonwillison.net/2020/Jun/26/weeknotes-plugins-sqlite-generate/#atom-tag" rel="alternate"/><published>2020-06-26T01:39:50+00:00</published><updated>2020-06-26T01:39:50+00:00</updated><id>https://simonwillison.net/2020/Jun/26/weeknotes-plugins-sqlite-generate/#atom-tag</id><summary type="html">
    &lt;p&gt;I spent this week spreading myself between a bunch of smaller projects, and finally getting familiar with &lt;a href="https://cookiecutter.readthedocs.io/"&gt;cookiecutter&lt;/a&gt;. I wrote about &lt;a href="https://simonwillison.net/2020/Jun/20/cookiecutter-plugins/"&gt;my datasette-plugin cookiecutter template&lt;/a&gt; earlier in the week; here's what else I've been working on.&lt;/p&gt;

&lt;h4 id="sqlite-generate"&gt;sqlite-generate&lt;/h4&gt;

&lt;p&gt;Datasette is supposed to work against any SQLite database you throw at it, no matter how weird the schema or how unwieldy the database shape or size.&lt;/p&gt;

&lt;p&gt;I built a new tool called &lt;a href="https://github.com/simonw/sqlite-generate"&gt;sqlite-generate&lt;/a&gt; this week to help me create databases of different shapes. It's a Python command-line tool which uses &lt;a href="https://faker.readthedocs.io/"&gt;Faker&lt;/a&gt; to populate a new database with random data. You run it something like this:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;sqlite-generate demo.db \
    --tables=20 \
    --rows=100,500 \
    --columns=5,20 \
    --fks=0,3 \
    --pks=0,2 \
    --fts&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;This command creates a database containing 20 tables, each with between 100 and 500 rows and 5-20 columns. Each table will also have between 0 and 3 foreign key columns to other tables, and will feature between 0 and 2 primary key columns. SQLite full-text search will be configured against all of the text columns in the table.&lt;/p&gt;

&lt;p&gt;I always try to include a live demo with any of my projects, and &lt;code&gt;sqlite-generate&lt;/code&gt; is no exception. &lt;a href="https://github.com/simonw/sqlite-generate/blob/main/.github/workflows/demo.yml"&gt;This GitHub Action&lt;/a&gt; runs on every push to main and deploys a demo to &lt;a href="https://sqlite-generate-demo.datasette.io/"&gt;https://sqlite-generate-demo.datasette.io/&lt;/a&gt; showing the latest version of the code in action.&lt;/p&gt;

&lt;p&gt;The demo runs my &lt;a href="https://github.com/simonw/datasette-search-all"&gt;datasette-search-all&lt;/a&gt; plugin in order to more easily demonstrate full-text search across all of the text columns in the generated tables. Try searching for &lt;a href="https://sqlite-generate-demo.datasette.io/-/search?q=newspaper"&gt;newspaper&lt;/a&gt;.&lt;/p&gt;

&lt;h4 id="click-app"&gt;click-app cookiecutter template&lt;/h4&gt;

&lt;p&gt;I write quite a lot of &lt;a href="https://click.palletsprojects.com/"&gt;Click&lt;/a&gt; powered command-line tools like this one, so inspired by &lt;a href="https://github.com/simonw/datasette-plugin"&gt;datasette-plugin&lt;/a&gt; I created a new &lt;a href="https://github.com/simonw/click-app"&gt;click-app&lt;/a&gt; cookiecutter template that bakes in my own preferences about how to set up a new Click project (complete with GitHub Actions). &lt;code&gt;sqlite-generate&lt;/code&gt; is the first tool I've built using that template.&lt;/p&gt;

&lt;h4 id="improved-plugin-docs"&gt;Improved Datasette plugin documentation&lt;/h4&gt;

&lt;p&gt;I've split Datasette's plugin documentation into five separate pages, and added a new page to the documentation about patterns for testing plugins.&lt;/p&gt;

&lt;p&gt;The five pages are:&lt;/p&gt;

&lt;ul&gt;&lt;li&gt;&lt;a href="https://datasette.readthedocs.io/en/latest/plugins.html"&gt;Plugins&lt;/a&gt; describing how to install and configure plugins&lt;/li&gt;&lt;li&gt;&lt;a href="https://datasette.readthedocs.io/en/latest/writing_plugins.html"&gt;Writing plugins&lt;/a&gt; showing how to write one-off plugins, how to use the &lt;code&gt;datasette-plugin&lt;/code&gt; cookiecutter template and how to package templates for release to PyPI&lt;/li&gt;&lt;li&gt;&lt;a href="https://datasette.readthedocs.io/en/latest/plugin_hooks.html"&gt;Plugin hooks&lt;/a&gt; documenting all of the available plugin hooks&lt;/li&gt;&lt;li&gt;&lt;a href="https://datasette.readthedocs.io/en/latest/testing_plugins.html"&gt;Testing plugins&lt;/a&gt; describing my preferred patterns for writing tests for them (using &lt;a href="https://docs.pytest.org/"&gt;pytest&lt;/a&gt; and &lt;a href="https://www.python-httpx.org/"&gt;HTTPX&lt;/a&gt;)&lt;/li&gt;&lt;li&gt;&lt;a href="https://datasette.readthedocs.io/en/latest/internals.html"&gt;Internals for plugins&lt;/a&gt; describing the APIs Datasette makes available for use within plugin hook implementations&lt;/li&gt;&lt;/ul&gt;

&lt;p&gt;There's also a &lt;a href="https://datasette.readthedocs.io/en/latest/ecosystem.html#datasette-plugins"&gt;list of available plugins&lt;/a&gt; on the Datasette Ecosystem page of the documentation, though I plan to move those to a separate plugin directory in the future.&lt;/p&gt;

&lt;h4 id="datasette-block-robots"&gt;datasette-block-robots&lt;/h4&gt;

&lt;p&gt;The &lt;a href="https://github.com/simonw/datasette-plugin"&gt;datasette-plugin&lt;/a&gt; template practically eliminates the friction involved in starting a new plugin.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;sqlite-generate&lt;/code&gt; generates random names for people. I don't particularly want people who search for their own names stumbling across the live demo and being weirded out by their name featured there, so I decided to block it from search engine crawlers using &lt;code&gt;robots.txt&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;I wrote a tiny plugin to do this: &lt;a href="https://github.com/simonw/datasette-block-robots"&gt;datasette-block-robots&lt;/a&gt;, which uses the new &lt;a href="https://datasette.readthedocs.io/en/latest/plugin_hooks.html#register-routes"&gt;register_routes() plugin hook&lt;/a&gt; to add a &lt;code&gt;/robots.txt&lt;/code&gt; page.&lt;/p&gt;

&lt;p&gt;It's also a neat example of the &lt;a href="https://github.com/simonw/datasette-block-robots/blob/main/datasette_block_robots/__init__.py"&gt;simplest possible plugin&lt;/a&gt; to use that feature - along with the &lt;a href="https://github.com/simonw/datasette-block-robots/blob/main/tests/test_block_robots.py"&gt;simplest possible unit test&lt;/a&gt; for exercising such a page.&lt;/p&gt;

&lt;h4 id="datasette-saved-queries"&gt;datasette-saved-queries&lt;/h4&gt;

&lt;p&gt;Another new plugin, this time with a bit more substance to it. &lt;a href="https://github.com/simonw/datasette-saved-queries"&gt;datasette-saved-queries&lt;/a&gt; exercises the new &lt;a href="https://datasette.readthedocs.io/en/latest/plugin_hooks.html#canned-queries-datasette-database-actor"&gt;canned_queries()&lt;/a&gt; hook I &lt;a href="https://simonwillison.net/2020/Jun/19/datasette-alphas/"&gt;described last week&lt;/a&gt;. It uses the new &lt;a href="https://datasette.readthedocs.io/en/latest/plugin_hooks.html#startup-datasette"&gt;startup()&lt;/a&gt; hook to create tables on startup (if they are missing), then lets users insert records into those tables to save their own queries. Queries saved in this way are then returned as canned queries for that particular database.&lt;/p&gt;

&lt;h4 id="main-not-master"&gt;main, not master&lt;/h4&gt;

&lt;p&gt;&lt;code&gt;main&lt;/code&gt; is a better name for the main GitHub branch than &lt;code&gt;master&lt;/code&gt;, which has unpleasant connotations (it apparently derives from master/slave in BitKeeper). My &lt;code&gt;datasette-plugin&lt;/code&gt; and &lt;code&gt;click-app&lt;/code&gt; cookiecutter templates both include instructions for renaming &lt;code&gt;master&lt;/code&gt; to &lt;code&gt;main&lt;/code&gt; in their READMEs - it's as easy as running &lt;code&gt;git branch -m master main&lt;/code&gt; before running your first push to GitHub.&lt;/p&gt;

&lt;p&gt;I'm working towards &lt;a href="https://github.com/simonw/datasette/issues/849"&gt;making the switch&lt;/a&gt; for Datasette itself.&lt;/p&gt;
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/git"&gt;git&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/robots-txt"&gt;robots-txt&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/sqlite"&gt;sqlite&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/cookiecutter"&gt;cookiecutter&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="git"/><category term="plugins"/><category term="projects"/><category term="robots-txt"/><category term="sqlite"/><category term="datasette"/><category term="weeknotes"/><category term="cookiecutter"/></entry><entry><title>click-app</title><link href="https://simonwillison.net/2020/Jun/23/click-app/#atom-tag" rel="alternate"/><published>2020-06-23T02:21:08+00:00</published><updated>2020-06-23T02:21:08+00:00</updated><id>https://simonwillison.net/2020/Jun/23/click-app/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/simonw/click-app"&gt;click-app&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
While working on sqlite-generate today I built a cookiecutter template for building the skeleton for Click command-line utilities. It’s based on datasette-plugin so it automatically sets up GitHub Actions for running tests and deploying packages to PyPI.


    &lt;p&gt;Tags: &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/cookiecutter"&gt;cookiecutter&lt;/a&gt;&lt;/p&gt;



</summary><category term="projects"/><category term="python"/><category term="cookiecutter"/></entry><entry><title>A cookiecutter template for writing Datasette plugins</title><link href="https://simonwillison.net/2020/Jun/20/cookiecutter-plugins/#atom-tag" rel="alternate"/><published>2020-06-20T16:15:42+00:00</published><updated>2020-06-20T16:15:42+00:00</updated><id>https://simonwillison.net/2020/Jun/20/cookiecutter-plugins/#atom-tag</id><summary type="html">
    &lt;p&gt;Datasette’s &lt;a href="https://datasette.readthedocs.io/en/stable/plugins.html"&gt;plugin system&lt;/a&gt; is one of the most interesting parts of the entire project. As I explained to Matt Asay in &lt;a href="https://thenewstack.io/datasette-a-developer-a-shower-and-a-data-inspired-moment/"&gt;this interview&lt;/a&gt;, the great thing about plugins is that Datasette can gain new functionality overnight without me even having to review a pull request. I just need to get more people to write them!&lt;/p&gt;
&lt;p&gt;&lt;a href="https://github.com/simonw/datasette-plugin"&gt;datasette-plugin&lt;/a&gt; is my most recent effort to help make that as easy as possible. It’s a &lt;a href="https://cookiecutter.readthedocs.io/"&gt;cookiecutter&lt;/a&gt; template that sets up the outline of a new plugin, combining various best patterns I’ve discovered over the past two years of writing my own plugins.&lt;/p&gt;
&lt;p&gt;Once you’ve &lt;a href="https://cookiecutter.readthedocs.io/en/1.7.2/installation.html"&gt;installed cookiecutter&lt;/a&gt; you can start building a new plugin by running:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cookiecutter gh:simonw/datasette-plugin
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Cookiecutter will run a quick interactive session asking for a few details. It will then use those details to generate a new directory structure ready for you to start work on the plugin.&lt;/p&gt;
&lt;p&gt;The &lt;a href="https://github.com/simonw/datasette-plugin/blob/main/README.md"&gt;datasette-plugin README&lt;/a&gt; describes the next steps. A couple of things are worth exploring in more detail.&lt;/p&gt;
&lt;h4 id="Writing_tests_for_plugins_14"&gt;Writing tests for plugins&lt;/h4&gt;
&lt;p&gt;I’m a big believer in automated testing: every single one of my plugins includes tests, and those test are run against every commit and must pass before new packages are shipped to &lt;a href="https://pypi.org"&gt;PyPI&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;In my experience the hardest part of writing tests is getting them started: setting up an initial test harness and ensuring that new tests can be easily written.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;datasette-plugin&lt;/code&gt; adds &lt;a href="https://docs.pytest.org/"&gt;pytest&lt;/a&gt; as a testing dependency and creates a &lt;code&gt;tests/&lt;/code&gt; folder with an initial, passing unit test in it.&lt;/p&gt;
&lt;p&gt;The test confirms that the new plugin has been correctly installed, by running a request through a configured Datasette instance and hitting the &lt;a href="https://datasette.readthedocs.io/en/stable/introspection.html#plugins"&gt;/-/plugins.json introspection endpoint&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;In doing so, it demonstrates how to run tests that interact with Datasette’s HTTP API. This is a very productive way to write tests.&lt;/p&gt;
&lt;p&gt;The example test uses the &lt;a href="https://www.python-httpx.org"&gt;HTTPX&lt;/a&gt; Python library. HTTPX offers a requests-style API but with a couple of crucial improvements. Firstly, it’s been built with asyncio support as a top-level concern. Secondly, it &lt;a href="https://www.python-httpx.org/async/#calling-into-python-web-apps"&gt;understands the ASGI protocol&lt;/a&gt; and can be run directly against an ASGI Python interface without needing to spin up an actual HTTP server. Since Datasette &lt;a href="https://simonwillison.net/2019/Jun/23/datasette-asgi/"&gt;speaks ASGI&lt;/a&gt; this makes it the ideal tool for testing Datasette plugins.&lt;/p&gt;
&lt;p&gt;Here’s that first test that gets created by the cookiecutter template:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from datasette.app import Datasette
import pytest
import httpx

@pytest.mark.asyncio
async def test_plugin_is_installed():
    app = Datasette([], memory=True).app()
    async with httpx.AsyncClient(app=app) as client:
        response = await client.get(
            &amp;quot;http://localhost/-/plugins.json&amp;quot;
        )
        assert 200 == response.status_code
        installed_plugins = {
            p[&amp;quot;name&amp;quot;] for p in response.json()
        }
        assert &amp;quot;datasette-plugin-template-demo&amp;quot; in installed_plugins
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;My hope is that including a passing test that demonstrates how to execute test requests will make it much easier for plugin authors to start building out their own custom test suite.&lt;/p&gt;
&lt;h4 id="Continuous_integration_with_GitHub_Actions_49"&gt;Continuous integration with GitHub Actions&lt;/h4&gt;
&lt;p&gt;My favourite thing about &lt;a href="https://simonwillison.net/tags/githubactions/"&gt;GitHub Actions&lt;/a&gt; is that they’re enabled on every GitHub repository for free, without any extra configuration necessary.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;datasette-plugin&lt;/code&gt; template takes advantage of this. Not only does every new project get a passing test - it also gets a GitHub Action - in &lt;code&gt;.github/workflows/test.yml&lt;/code&gt; - that executes the tests on every commit.&lt;/p&gt;
&lt;p&gt;It even &lt;a href="https://github.com/simonw/datasette-plugin/blob/8e4d5231bc276f19ccf630b18f075222e5afecb3/datasette-%7B%7Bcookiecutter.hyphenated%7D%7D/.github/workflows/test.yml#L8-L10"&gt;runs the test suite in parallel&lt;/a&gt; against Python 3.6, 3.7 and 3.8 - the versions currently supported by Datasette itself.&lt;/p&gt;
&lt;p&gt;A second action in &lt;code&gt;.github/workflows/publish.yml&lt;/code&gt; bakes in my opinions on the best way to manage plugin releases: it builds and ships a new package to PyPI every time a new tag (and corresponding GitHub release) is added to the repository.&lt;/p&gt;
&lt;p&gt;For this to work you’ll need to create a &lt;a href="https://pypi.org/help/#apitoken"&gt;PyPI API token&lt;/a&gt; and add it to your plugin’s GitHub repository as a &lt;code&gt;PYPI_TOKEN&lt;/code&gt; secret. This is &lt;a href="https://github.com/simonw/datasette-plugin/blob/main/README.md#publishing-your-plugin-as-a-package-to-pypi"&gt;explained in the README&lt;/a&gt;.&lt;/p&gt;
&lt;h4 id="Deploying_a_live_demo_of_the_template_with_GitHub_Actions_61"&gt;Deploying a live demo of the template with GitHub Actions&lt;/h4&gt;
&lt;p&gt;Whenever possible, I like to ship my projects with live demos. The Datasette repository &lt;a href="https://github.com/simonw/datasette/blob/master/.github/workflows/deploy-latest.yml"&gt;publishes a demo&lt;/a&gt; of the latest commit to &lt;a href="https://latest.datasette.io/"&gt;https://latest.datasette.io/&lt;/a&gt; on every commit. I try to do the same for my plugins, where it makes sense to do so.&lt;/p&gt;
&lt;p&gt;What could a live demo of a cookiecutter template look like?&lt;/p&gt;
&lt;p&gt;Ideally it would show a complete, generated project. I love GitHub’s code browsing interface, so a separate repository containing that generated project would be ideal.&lt;/p&gt;
&lt;p&gt;So that’s what &lt;a href="https://github.com/simonw/datasette-plugin-template-demo"&gt;https://github.com/simonw/datasette-plugin-template-demo&lt;/a&gt; is: it’s a repository showing the most recent output of the latest version of the cookiecutter template that lives in &lt;a href="https://github.com/simonw/datasette-plugin"&gt;https://github.com/simonw/datasette-plugin&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;It’s powered by &lt;a href="https://github.com/simonw/datasette-plugin/blob/main/.github/workflows/push.yml"&gt;this GitHub Action&lt;/a&gt;, which runs on every push to the &lt;code&gt;datasette-plugin&lt;/code&gt; repo, installs cookiecutter, uses cookiecutter against some &lt;a href="https://github.com/simonw/datasette-plugin/blob/main/input-for-demo.txt"&gt;fixed inputs&lt;/a&gt; to re-generate the project and then pushes the results up to &lt;code&gt;datasette-plugin-template-demo&lt;/code&gt; as a new commit.&lt;/p&gt;
&lt;p&gt;As a fun final touch, it uses the GitHub commit comments API to add a comment to the commit to &lt;code&gt;datasette-plugin&lt;/code&gt; linking to the “browse” view on the resulting code in the &lt;code&gt;datasette-plugin-template-demo&lt;/code&gt; repository. Here’s &lt;a href="https://github.com/simonw/datasette-plugin/commit/8e4d5231bc276f19ccf630b18f075222e5afecb3"&gt;one of those commit comments&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Figuring out how to build this took quite a bit of work. &lt;a href="https://github.com/simonw/datasette-plugin/issues/4"&gt;Issue #4&lt;/a&gt; has a blow-by-blow rundown of how I got it working.&lt;/p&gt;
&lt;p&gt;I couldn’t resist tweeting about it:&lt;/p&gt;
&lt;blockquote class="twitter-tweet"&gt;&lt;p lang="en" dir="ltr"&gt;Writing a GitHub Action for a repo that generates content for a second repo and pushes that content to the second repo and then posts a comment to the commit on the first repo that links to the newly committed code in the second repo&lt;/p&gt;- Simon Willison (@simonw) &lt;a href="https://twitter.com/simonw/status/1274118461896024064?ref_src=twsrc%5Etfw"&gt;June 19, 2020&lt;/a&gt;&lt;/blockquote&gt;
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/github"&gt;github&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/pypi"&gt;pypi&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/datasette"&gt;datasette&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/pytest"&gt;pytest&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/github-actions"&gt;github-actions&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/cookiecutter"&gt;cookiecutter&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="github"/><category term="plugins"/><category term="projects"/><category term="pypi"/><category term="datasette"/><category term="pytest"/><category term="github-actions"/><category term="cookiecutter"/></entry></feed>