write docs

This commit is contained in:
David Lord 2025-01-13 13:55:21 -08:00
parent 1a6daf27f5
commit 2332be07e5
No known key found for this signature in database
GPG key ID: 43368A7AA8CC5926
21 changed files with 983 additions and 17 deletions

10
.readthedocs.yaml Normal file
View file

@ -0,0 +1,10 @@
version: 2
build:
os: ubuntu-24.04
tools:
python: '3.13'
commands:
- asdf plugin add uv
- asdf install uv latest
- asdf global uv latest
- uv run --group docs sphinx-build -W -b dirhtml docs $READTHEDOCS_OUTPUT/html

3
CHANGES.md Normal file
View file

@ -0,0 +1,3 @@
## Version 0.1.0
Unreleased

View file

@ -1,4 +1,77 @@
# modify-repos # modify-repos
Clone, modify, and create pull requests across multiple repositories at Clone, modify, and create pull requests across multiple repositories at once.
once.
> [!WARNING]
> This is under development, and how it's used may change at any time.
Documentation: <https://modify-repos.readthedocs.io/>
## Example Use
Use [uv] to create a script that depends on this library.
```
$ uv init --script mod.py
$ uv add --script mod.py modify-repos
```
Subclass `modify_repos.GitHubScript` to define the repositories to change and
what changes to make. This uses the [gh] GitHub CLI, which must already be
installed and logged in.
```python
from modify_repos import GitHubScript, GitHubRepo
class MyScript(GitHubScript):
# title used in commit and PR
title = "..."
# description used in commit and PR
body = "..."
# branch to merge into, defaults to main
target = "main"
# branch to create and PR
branch = "my-changes"
# one or more users/orgs to clone repos from
orgs = ["username"]
def select_for_clone(self, repo: GitHubRepo) -> bool:
# filter to only clone some of the available repos
return repo.name in {"a", "b", "d"}
def modify(self, repo: GitHubRepo) -> None:
# make any changes, such as add/remove files, here
...
if __name__ == "__main__":
MyScript().run()
```
Call `uv run mod.py`, and it will clone and modify all the selected repos. PRs
will not be created unless you use `MyScript(submit=True)` instead, so you can
develop and preview your changes first.
[uv]: https://docs.astral.sh/uv/
[gh]: https://cli.github.com/
## Develop
This project uses [uv] to manage the development environment and [tox] to define
different tests and management scripts.
```
# set up env
$ uv sync
$ uv run pre-commit install --install-hooks
# run tests and checks
$ uv run tox p
# develop docs with auto build and serve
$ uv run tox r -e docs-auto
# update dependencies
$ uv run tox r -m update
```
[tox]: https://tox.wiki/

1
docs/_static/theme.css vendored Normal file
View file

@ -0,0 +1 @@
@import url('https://fonts.googleapis.com/css2?family=Atkinson+Hyperlegible:ital,wght@0,400;0,700;1,400;1,700&family=Source+Code+Pro:ital,wght@0,400;0,700;1,400;1,700&display=swap');

4
docs/changes.md Normal file
View file

@ -0,0 +1,4 @@
# Changes
```{include} ../CHANGES.md
```

47
docs/conf.py Normal file
View file

@ -0,0 +1,47 @@
project = "modify-repos"
default_role = "code"
extensions = [
"myst_parser",
"sphinx.ext.autodoc",
"sphinx.ext.extlinks",
"sphinx.ext.intersphinx",
]
autodoc_member_order = "bysource"
autodoc_typehints = "description"
autodoc_preserve_defaults = True
myst_enable_extensions = [
"fieldlist",
]
myst_heading_anchors = 2
extlinks = {
"issue": ("https://github.com/davidism/modify-repos/issues/%s", "#%s"),
"pr": ("https://github.com/davidism/modify-repos/pull/%s", "#%s"),
}
intersphinx_mapping = {
"python": ("https://docs.python.org/3/", None),
}
html_theme = "furo"
html_static_path = ["_static"]
html_css_files = ["theme.css"]
html_copy_source = False
html_theme_options = {
"source_repository": "https://github.com/davidism/modify-repos/",
"source_branch": "main",
"source_directory": "docs/",
"light_css_variables": {
"font-stack": "'Atkinson Hyperlegible', sans-serif",
"font-stack--monospace": "'Source Code Pro', monospace",
},
}
pygments_style = "default"
pygments_style_dark = "github-dark"
html_show_copyright = False
html_use_index = False
html_domain_indices = False

29
docs/index.md Normal file
View file

@ -0,0 +1,29 @@
# modify-repos
A framework for writing scripts that clone, modify, and create pull requests
across multiple repositories at once. Various utilities such as a Jinja template
environment and text manipulation are provided to make common script tasks
easier.
```{warning}
This is under development, and how it's used may change at any time.
```
Currently, only a GitHub provider is implemented. The library is designed to be
extended to define other sources and repository types.
Create a Python file, subclass {class}`.GitHubScript`, define a few attributes
and its {meth}`~.GitHubScript.modify` method, then call its
{meth}`~.GitHubScript.run` method. See {doc}`script` for a full example.
```{toctree}
:hidden:
script
providers/github
utils
providers/git
providers/base
changes
license
```

5
docs/license.md Normal file
View file

@ -0,0 +1,5 @@
# MIT License
```{literalinclude} ../LICENSE.txt
:language: text
```

11
docs/providers/base.md Normal file
View file

@ -0,0 +1,11 @@
# Base Provider Classes
These base classes can be subclassed to create a new provider.
```{eval-rst}
.. autoclass:: modify_repos.script.base.Script
:members:
.. autoclass:: modify_repos.repo.base.Repo
:members:
```

10
docs/providers/git.md Normal file
View file

@ -0,0 +1,10 @@
# Base Git Provider
This is an incomplete provider. Currently, it acts as a base for
{class}`.GitHubRepo`, and does not support cloning arbitrary Git URLs.
```{eval-rst}
.. autoclass:: modify_repos.repo.git.GitRepo
:show-inheritance:
:members:
```

18
docs/providers/github.md Normal file
View file

@ -0,0 +1,18 @@
# GitHub Provider
This provider supports cloning repos from GitHub and creating PRs. It uses the
[`gh` GitHub CLI], which must be installed and logged in already.
[gh]: https://cli.github.com/
```{eval-rst}
.. autoclass:: modify_repos.GitHubScript
:show-inheritance:
:members:
:inherited-members:
.. autoclass:: modify_repos.GitHubRepo
:show-inheritance:
:members:
:inherited-members:
```

46
docs/script.md Normal file
View file

@ -0,0 +1,46 @@
# Writing a Script
Use [uv] to create a script that depends on this library.
```
$ uv init --script mod.py
$ uv add --script mod.py modify-repos
```
Subclass {class}`.GitHubScript` to define the repositories to change and what
changes to make. This uses the [gh] GitHub CLI, which must already be installed
and logged in.
```python
from modify_repos import GitHubScript, GitHubRepo
class MyScript(GitHubScript):
# title used in commit and PR
title = "..."
# description used in commit and PR
body = "..."
# branch to merge into, defaults to main
target = "main"
# branch to create and PR
branch = "my-changes"
# one or more users/orgs to clone repos from
orgs = ["username"]
def select_for_clone(self, repo: GitHubRepo) -> bool:
# filter to only clone some of the available repos
return repo.name in {"a", "b", "d"}
def modify(self, repo: GitHubRepo) -> None:
# make any changes, such as add/remove files, here
...
if __name__ == "__main__":
MyScript().run()
```
Call `uv run mod.py`, and it will clone and modify all the selected repos. PRs
will not be created unless you use `MyScript(submit=True)` instead, so you can
develop and preview your changes first.
[uv]: https://docs.astral.sh/uv/
[gh]: https://cli.github.com/

13
docs/utils.md Normal file
View file

@ -0,0 +1,13 @@
# Utilities
```{eval-rst}
.. currentmodule:: modify_repos.utils
.. autofunction:: run_cmd
.. autofunction:: wrap_text
.. autofunction:: read_text
.. autofunction:: write_text
```

View file

@ -6,7 +6,7 @@ readme = "README.md"
authors = [{ name = "David Lord" }] authors = [{ name = "David Lord" }]
license = "MIT" license = "MIT"
license-files = ["LICENSE.txt"] license-files = ["LICENSE.txt"]
requires-python = "~=3.13" requires-python = "~=3.13.0"
dependencies = [ dependencies = [
"click>=8.1.8", "click>=8.1.8",
"jinja2>=3.1.5", "jinja2>=3.1.5",
@ -34,6 +34,15 @@ typing = [
"mypy>=1.14.1", "mypy>=1.14.1",
"pyright>=1.1.391", "pyright>=1.1.391",
] ]
docs = [
"furo>=2024.8.6",
"myst-parser>=4.0.0",
"sphinx>=8.1.3",
"sphinx-autodoc2>=0.5.0",
]
docs-auto = [
"sphinx-autobuild>=2024.10.3",
]
[tool.pytest.ini_options] [tool.pytest.ini_options]
testpaths = ["tests"] testpaths = ["tests"]
@ -86,7 +95,7 @@ tag-only = [
] ]
[tool.tox] [tool.tox]
env_list = ["style", "typing"] env_list = ["style", "typing", "docs"]
[tool.tox.env_run_base] [tool.tox.env_run_base]
runner = "uv-venv-lock-runner" runner = "uv-venv-lock-runner"
@ -108,6 +117,14 @@ commands = [
["pyright", "--verifytypes", "modify_repos", "--ignoreexternal"], ["pyright", "--verifytypes", "modify_repos", "--ignoreexternal"],
] ]
[tool.tox.env.docs]
dependency_groups = ["docs"]
commands = [["sphinx-build", "-E", "-W", "-b", "dirhtml", "docs", "docs/_build/dirhtml"]]
[tool.tox.env.docs-auto]
dependency_groups = ["docs", "docs-auto"]
commands = [["sphinx-autobuild", "-W", "-b", "dirhtml", "--watch", "src", "docs", "docs/_build/dirhtml"]]
[tool.tox.env.update-pre_commit] [tool.tox.env.update-pre_commit]
labels = ["update"] labels = ["update"]
dependency_groups = ["pre-commit"] dependency_groups = ["pre-commit"]

View file

@ -12,52 +12,108 @@ if t.TYPE_CHECKING:
class Repo: class Repo:
"""Defines how to manipulate a repository. :meth:`.Script.list_repos` will
return instances of a subclass of this that are set up to work with a
specific type of repo, and most of the methods will be called as part of
running the script. You'd subclass this class if you wanted to define such a
class for a new type of repo.
:param script: The script being run to modify this repo.
:param remote_id: A value that identifies where this repo was cloned from.
"""
remote_id: str remote_id: str
def __init__(self, script: Script[t.Any], remote_id: str) -> None: def __init__(self, script: Script[t.Any], remote_id: str) -> None:
self.script = script self.script = script
"""The script being run to modify this repo."""
self.remote_id = remote_id self.remote_id = remote_id
"""A value that identifies where this repo was cloned from. The format
depends on how the script finds repos.
"""
@cached_property @cached_property
def local_dir(self) -> Path: def local_dir(self) -> Path:
"""The path where this repo is cloned."""
return self.script.clones_dir / self.remote_id return self.script.clones_dir / self.remote_id
def clone(self) -> None: def clone(self) -> None:
"""Clone the repository unconditionally. This is called by
:meth:`clone_if_needed`.
"""
raise NotImplementedError raise NotImplementedError
def clone_if_needed(self) -> None: def clone_if_needed(self) -> None:
"""Clone the repository if the local directory doesn't exist. Calls
:meth:`clone`. Called by :meth:`run`.
"""
if not self.local_dir.exists(): if not self.local_dir.exists():
self.clone() self.clone()
def reset_target(self) -> None: def reset_target(self) -> None:
"""Reset the base branch that will be branched off of and merged into.
This should ensure the repository is clean and up to date, discarding
any changes from previous unsuccessful runs. Called by :meth:`run`.
"""
raise NotImplementedError raise NotImplementedError
def reset_branch(self) -> None: def reset_branch(self) -> None:
"""Create or reset the work branch. This should ensure the branch is
freshly created from the target branch. Called by :meth:`run`.
"""
raise NotImplementedError raise NotImplementedError
def needs_commit(self) -> bool: def needs_commit(self) -> bool:
"""Check if there are uncommitted changes. Called by
:meth:`auto_commit_if_needed`.
"""
raise NotImplementedError raise NotImplementedError
def commit(self) -> None: def auto_commit(self) -> None:
"""Create a commit unconditionally. This is called by
:meth:`auto_commit_if_needed` to create the automatic commit when there
are uncommitted changes, and should add those changes to the commit. It
should not be called to create other intermediate commits. It should use
:attr:`.Script.message` as the commit message.
"""
raise NotImplementedError raise NotImplementedError
def commit_if_needed(self) -> None: def auto_commit_if_needed(self) -> None:
"""Create a commit if there are uncommitted changes. Calls
:meth:`needs_commit` and :meth:`auto_commit_if_needed`. Called by
:meth:`run`.
"""
if self.needs_commit(): if self.needs_commit():
self.commit() self.auto_commit()
def needs_submit(self) -> bool: def needs_submit(self) -> bool:
"""Check if there are commits that have not been pushed upstream. Called
by :meth:`submit_if_needed`."""
raise NotImplementedError raise NotImplementedError
def submit(self) -> None: def submit(self) -> None:
"""Submit the changes upstream. What this means depends on the
implementation; whether it merges, creates a PR, or something else.
"""
raise NotImplementedError raise NotImplementedError
def submit_if_needed(self) -> None: def submit_if_needed(self) -> None:
"""Submit the changes if there are any changes. Is disabled by default
by :attr:`.Script.enable_submit`, to prevent accidental submission of a
script in development. Calls :meth:`needs_submit` and :meth:`submit`.
Called by :meth:`run`.
"""
if self.script.enable_submit and self.needs_submit(): if self.script.enable_submit and self.needs_submit():
self.submit() self.submit()
else: else:
click.secho("skipping submit", fg="yellow") click.secho("skipping submit", fg="yellow")
def run(self) -> None: def run(self) -> None:
"""Run the full workflow for this repo: clone, reset, modify, commit,
submit. Calls many of the other methods defined in this class. Called
by :meth:`.Script.run`.
"""
click.secho(self.remote_id, fg="green") click.secho(self.remote_id, fg="green")
self.clone_if_needed() self.clone_if_needed()
@ -70,5 +126,5 @@ class Repo:
self.reset_branch() self.reset_branch()
self.script.modify(self) self.script.modify(self)
self.commit_if_needed() self.auto_commit_if_needed()
self.submit_if_needed() self.submit_if_needed()

View file

@ -9,10 +9,24 @@ from .base import Repo
class GitRepo(Repo): class GitRepo(Repo):
"""Subclass this to define how to manipulate Git repositories.
:param script: The script being run to modify this repo.
"""
_git_exe = which("git") _git_exe = which("git")
add_untracked: bool = False add_untracked: bool = False
"""Whether to consider untracked files when checking if there are changes
and adding files in the auto :meth:`commit`. By default this is false to
avoid accidentally adding generated files, but this means you need to
remember to call :meth:`git_add` for any completely new files.
"""
def git_cmd(self, *args: str | Path) -> CompletedProcess[str]: def git_cmd(self, *args: str | Path) -> CompletedProcess[str]:
"""Call and pass args to the `git` command.
:param args: Command line arguments to the `git` command.
"""
if self._git_exe is None: if self._git_exe is None:
raise RuntimeError("Git is not installed.") raise RuntimeError("Git is not installed.")
@ -34,7 +48,7 @@ class GitRepo(Repo):
return bool(self.git_cmd(*args).stdout) return bool(self.git_cmd(*args).stdout)
def commit(self) -> None: def auto_commit(self) -> None:
if self.add_untracked: if self.add_untracked:
self.add_files(all=True) self.add_files(all=True)
else: else:
@ -53,6 +67,12 @@ class GitRepo(Repo):
def add_files( def add_files(
self, *items: str | Path, update: bool = False, all: bool = False self, *items: str | Path, update: bool = False, all: bool = False
) -> None: ) -> None:
"""Call `git add`.
:param items: Files to add or update. Can be empty.
:param update: Update all tracked files.
:param all: Add all files, including untracked, excluding ignored.
"""
if all: if all:
self.git_cmd("add", "--all") self.git_cmd("add", "--all")
@ -62,6 +82,11 @@ class GitRepo(Repo):
self.git_cmd("add", *items) self.git_cmd("add", *items)
def rm_files(self, *items: str | Path) -> None: def rm_files(self, *items: str | Path) -> None:
"""Call `git rm` for any given files that exist. Missing files are
skipped so that the command doesn't return an error.
:param items: Files to delete if they exist.
"""
to_remove = [item for item in items if Path(item).exists()] to_remove = [item for item in items if Path(item).exists()]
if to_remove: if to_remove:

View file

@ -14,8 +14,20 @@ if t.TYPE_CHECKING:
class GitHubRepo(GitRepo): class GitHubRepo(GitRepo):
"""Subclass this to define how to manipulate Git repositories. This extends
the plain :class:`GitRepo` to clone and create PRs using the GitHub CLI.
:param script: The script being run to modify this repo.
:param org: The GitHub user/org that owns the repo.
:param name: The GitHub repo name.
"""
_gh_exe: str | None = which("gh") _gh_exe: str | None = which("gh")
direct_submit: bool = False direct_submit: bool = False
"""Whether to merge and push directly to the target branch, rather than
creating a PR. This is disabled by default as a PR will give more
opportunity to review any mistakes with the automated changes.
"""
def __init__(self, script: Script[t.Any], org: str, name: str) -> None: def __init__(self, script: Script[t.Any], org: str, name: str) -> None:
self.org = org self.org = org
@ -23,6 +35,10 @@ class GitHubRepo(GitRepo):
super().__init__(script=script, remote_id=self.full_name) super().__init__(script=script, remote_id=self.full_name)
def gh_cmd(self, *args: str | Path) -> CompletedProcess[str]: def gh_cmd(self, *args: str | Path) -> CompletedProcess[str]:
"""Call and pass args to the `gh` command.
:param args: Command line arguments to the `gh` command.
"""
if self._gh_exe is None: if self._gh_exe is None:
raise RuntimeError("GitHub CLI is not installed.") raise RuntimeError("GitHub CLI is not installed.")
@ -30,6 +46,7 @@ class GitHubRepo(GitRepo):
@cached_property @cached_property
def full_name(self) -> str: def full_name(self) -> str:
"""The `org/name` identifier for the repo."""
return f"{self.org}/{self.name}" return f"{self.org}/{self.name}"
def clone(self) -> None: def clone(self) -> None:

View file

@ -13,25 +13,60 @@ from ..utils import wrap_text
class Script[RepoType: Repo]: class Script[RepoType: Repo]:
"""Defines how to select repositories and modify them. Typically, you'll
want to subclass a more specific class that is already set up to work with a
remote host. You'd subclass this class if you wanted to define such a class
for a new host.
:param submit: Whether to submit the changes. This is disabled by default,
to give you a chance to develop the changes first.
"""
target: str = "main" target: str = "main"
"""The name of the target branch to branch off of and merge into."""
branch: str branch: str
"""The name of the work branch to create."""
title: str title: str
"""A short title describing the change. Used as the first line of the
automatic commit, as well as the title of the PR. By convention, this should
be at most 50 characters.
"""
body: str body: str
"""Additional description about the change. Used in the commit message
after the title, separated by an empty line. Also used as the body of the
PR. This will be re-wrapped to 72 characters to match convention.
"""
def __init__(self, *, submit: bool = False) -> None: def __init__(self, *, submit: bool = False) -> None:
""" """
source_file = inspect.getsourcefile(self.__class__) source_file = inspect.getsourcefile(self.__class__)
if source_file is None: if source_file is None:
raise RuntimeError("Could not determine script root.") raise RuntimeError("Could not determine script root.")
self.root_dir: Path = Path(source_file).parent self.root_dir: Path = Path(source_file).parent
"""The directory containing the running script. Used to reference
resource files and templates.
"""
self.jinja_env: jinja2.Environment = jinja2.Environment( self.jinja_env: jinja2.Environment = jinja2.Environment(
loader=jinja2.FileSystemLoader(self.root_dir), loader=jinja2.FileSystemLoader(self.root_dir),
trim_blocks=True, trim_blocks=True,
lstrip_blocks=True, lstrip_blocks=True,
keep_trailing_newline=True, keep_trailing_newline=True,
) )
"""A Jinja environment configured to use :attr:`root_dir` as a template
folder. See :meth:`render_template`.
"""
self.clones_dir: Path = platformdirs.user_cache_path("modify-repos") / "clones" self.clones_dir: Path = platformdirs.user_cache_path("modify-repos") / "clones"
"""Directory where repos are cloned to. Uses the appropriate user cache
dir for the platform.
"""
self.clones_dir.mkdir(parents=True, exist_ok=True) self.clones_dir.mkdir(parents=True, exist_ok=True)
if not (ignore := self.clones_dir / ".gitignore").exists(): if not (ignore := self.clones_dir / ".gitignore").exists():
@ -39,36 +74,93 @@ class Script[RepoType: Repo]:
self.body = wrap_text(self.body, width=72) self.body = wrap_text(self.body, width=72)
self.enable_submit = submit self.enable_submit = submit
"""Whether to submit the changes. This is disabled by default, to give
you a chance to develop the changes first. It is set from the `submit`
param.
"""
def render_template(self, name: str, /, **kwargs: t.Any) -> str: def render_template(self, name: str, /, **kwargs: t.Any) -> str:
"""Render the named template file with context. Uses :attr:`jinja_env`,
which finds templates next to the script file.
:param name: Template name to load.
:param kwargs: Context to pass to the render call.
"""
return self.jinja_env.get_template(name).render(**kwargs) return self.jinja_env.get_template(name).render(**kwargs)
def list_all_repos(self) -> list[RepoType]: def list_all_repos(self) -> list[RepoType]:
"""Get the list of all repos that may be cloned. Override this to
define how to generate this list. Called by :meth:`list_repos`."""
raise NotImplementedError raise NotImplementedError
def list_repos(self) -> list[RepoType]: def list_repos(self) -> list[RepoType]:
"""Get the filtered list of repos that will be cloned. Override
:meth:`list_all_repos` and :meth:`select_for_clone` to control what is
returned here. Called by :meth:`run`."""
return [r for r in self.list_all_repos() if self.select_for_clone(r)] return [r for r in self.list_all_repos() if self.select_for_clone(r)]
@cached_property @cached_property
def full_target(self) -> str: def full_target(self) -> str:
"""The upstream target branch, which is :attr:`target` prefixed by
`origin/`.
"""
return f"origin/{self.target}" return f"origin/{self.target}"
@cached_property @cached_property
def commit_message(self) -> str: def commit_message(self) -> str:
"""The message to use for the automatic commit in :meth:`commit`.
Defaults to :attr:`title` and :attr:`body` separated by a blank line.
"""
return f"{self.title}\n\n{self.body}" return f"{self.title}\n\n{self.body}"
def select_for_clone(self, repo: Repo) -> bool: def select_for_clone(self, repo: Repo) -> bool:
"""Select what repos are returned by :meth:`list_repos`. Each repo from
:meth:`list_all_repos` is passed, and will be used if this method returns
true for it.
For example, override this to return true if the repo name matches a set
of names.
:param repo: The repo to filter.
"""
return True return True
def select_for_modify(self, repo: Repo) -> bool: def select_for_modify(self, repo: Repo) -> bool:
"""Select whether :meth:`modify` will be called on the repo. Called by
:meth:`run` while the current directory is the cloned repo dir.
For example, override this to return false if the repo does not contain
a file to be removed, or already contains a file to be added.
:param repo: The repo to filter.
"""
return True return True
def modify(self, repo: Repo) -> None: def modify(self, repo: Repo) -> None:
"""Perform modifications to the repo. Called by :meth:`run` while the
current directory is the cloned repo dir.
If this leaves uncommitted changes, :meth:`.Repo.commit_if_needed` will
detect that and commit automatically. You can also add and commit
manually to skip that behavior.
:param repo: The repo to modify.
"""
raise NotImplementedError raise NotImplementedError
def run(self) -> None: def run(self) -> None:
"""Call :meth:`.Repo.run` for each selected repo."""
for repo in self.list_repos(): for repo in self.list_repos():
repo.run() repo.run()
def read_text(self, path: str | PathLike[str], strip: bool = True) -> str: def read_text(self, path: str | PathLike[str], strip: bool = True) -> str:
"""Read a text file, where `path` is relative to the script's directory
:attr:`root_dir`. The file will be read as UTF-8.
:param path: Path to file to read. Relative paths are relative to
:attr:`root_dir`.
:param strip: Strip leading and trailing empty spaces and lines. Enabled
by default. The text is often formatted into an existing file, so
stripping spaces makes working with it more predictable.
"""
return read_text(self.root_dir / path, strip=strip) return read_text(self.root_dir / path, strip=strip)

View file

@ -6,6 +6,17 @@ from .base import Script
class GitHubScript(Script[GitHubRepo]): class GitHubScript(Script[GitHubRepo]):
"""Subclass this to define how to select and modify GitHub repositories.
Uses the GitHub CLI, which must already be installed and logged in.
:param submit: Whether to submit the changes. This is disabled by default,
to give you a chance to develop the changes first.
:param orgs: The list of users/orgs to clone repositories from.
"""
orgs: list[str]
"""The list of GitHub users/orgs to clone repositories from."""
def __init__(self, *, submit: bool = True, orgs: list[str] | None = None) -> None: def __init__(self, *, submit: bool = True, orgs: list[str] | None = None) -> None:
super().__init__(submit=submit) super().__init__(submit=submit)

View file

@ -1,5 +1,6 @@
from __future__ import annotations from __future__ import annotations
import os
import shlex import shlex
import subprocess import subprocess
import textwrap import textwrap
@ -14,9 +15,18 @@ import click
def run_cmd( def run_cmd(
*args: str | PathLike[str], **kwargs: t.Any *args: str | PathLike[str], **kwargs: t.Any
) -> subprocess.CompletedProcess[str]: ) -> subprocess.CompletedProcess[str]:
echo_cmd(*args) """Wrapper around :meth:`subprocess.run`. Args are passed positionally
rather than as a list. Stdout and stderr are combined, and use text mode.
The initial command is echoed, and if the return code is not 0, the output
and code are echoed.
:param args: Command line arguments.
:param kwargs: Other arguments passed to `subprocess.run`.
"""
s_args = [os.fspath(v) if isinstance(v, PathLike) else v for v in args]
click.echo(f"$ {shlex.join(s_args)}")
result: subprocess.CompletedProcess[str] = subprocess.run( result: subprocess.CompletedProcess[str] = subprocess.run(
args, s_args,
text=True, text=True,
stdout=subprocess.PIPE, stdout=subprocess.PIPE,
stderr=subprocess.STDOUT, stderr=subprocess.STDOUT,
@ -30,12 +40,15 @@ def run_cmd(
return result return result
def echo_cmd(*args: str | PathLike[str]) -> None:
click.echo(f"$ {shlex.join(str(v) for v in args)}")
def wrap_text(text: str, width: int = 80) -> str: def wrap_text(text: str, width: int = 80) -> str:
"""Wrap a multi-line, multi-paragraph string.""" """Wrap a multi-line, multi-paragraph string. The text is dedented and empty
spaces and lines are stripped, to support triple-quoted strings. Paragraphs
are separated by a blank line `\\\\n\\\\n`. Tabs are converted to 4 spaces,
and very long words are not wrapped.
:param text: The text to process.
:param width: The number of characters to wrap at.
"""
return "\n\n".join( return "\n\n".join(
textwrap.fill(p, width=width, tabsize=4, break_long_words=False) textwrap.fill(p, width=width, tabsize=4, break_long_words=False)
for p in cleandoc(text).split("\n\n") for p in cleandoc(text).split("\n\n")
@ -43,6 +56,13 @@ def wrap_text(text: str, width: int = 80) -> str:
def read_text(path: str | PathLike[str], strip: bool = True) -> str: def read_text(path: str | PathLike[str], strip: bool = True) -> str:
"""Read a file as UTF-8 text.
:param path: The path to the file to read.
:param strip: Whether to strip empty spaces and lines. Enabled by default.
This makes the text more predictable to work with when inserting it into
another text.
"""
text = Path(path).read_text("utf8") text = Path(path).read_text("utf8")
if strip: if strip:
@ -52,6 +72,13 @@ def read_text(path: str | PathLike[str], strip: bool = True) -> str:
def write_text(path: str | PathLike[str], text: str, end_nl: bool = True) -> None: def write_text(path: str | PathLike[str], text: str, end_nl: bool = True) -> None:
"""Write a file as UTF-8 text.
:param path: The path to the file to write.
:param text: The text to write.
:param end_nl: Whether to ensure the text ends with exactly one newline
`\\\\n`. This keeps file endings consistent.
"""
if end_nl: if end_nl:
text = f"{text.rstrip('\n')}\n" text = f"{text.rstrip('\n')}\n"

453
uv.lock generated
View file

@ -1,5 +1,57 @@
version = 1 version = 1
requires-python = ">=3.13, <4" requires-python = ">=3.13.0, <3.14"
[[package]]
name = "alabaster"
version = "1.0.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/a6/f8/d9c74d0daf3f742840fd818d69cfae176fa332022fd44e3469487d5a9420/alabaster-1.0.0.tar.gz", hash = "sha256:c00dca57bca26fa62a6d7d0a9fcce65f3e026e9bfe33e9c538fd3fbb2144fd9e", size = 24210 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/7e/b3/6b4067be973ae96ba0d615946e314c5ae35f9f993eca561b356540bb0c2b/alabaster-1.0.0-py3-none-any.whl", hash = "sha256:fc6786402dc3fcb2de3cabd5fe455a2db534b371124f1f21de8731783dec828b", size = 13929 },
]
[[package]]
name = "anyio"
version = "4.8.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "idna" },
{ name = "sniffio" },
]
sdist = { url = "https://files.pythonhosted.org/packages/a3/73/199a98fc2dae33535d6b8e8e6ec01f8c1d76c9adb096c6b7d64823038cde/anyio-4.8.0.tar.gz", hash = "sha256:1d9fe889df5212298c0c0723fa20479d1b94883a2df44bd3897aa91083316f7a", size = 181126 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/46/eb/e7f063ad1fec6b3178a3cd82d1a3c4de82cccf283fc42746168188e1cdd5/anyio-4.8.0-py3-none-any.whl", hash = "sha256:b5011f270ab5eb0abf13385f851315585cc37ef330dd88e27ec3d34d651fd47a", size = 96041 },
]
[[package]]
name = "astroid"
version = "3.3.8"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/80/c5/5c83c48bbf547f3dd8b587529db7cf5a265a3368b33e85e76af8ff6061d3/astroid-3.3.8.tar.gz", hash = "sha256:a88c7994f914a4ea8572fac479459f4955eeccc877be3f2d959a33273b0cf40b", size = 398196 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/07/28/0bc8a17d6cd4cc3c79ae41b7105a2b9a327c110e5ddd37a8a27b29a5c8a2/astroid-3.3.8-py3-none-any.whl", hash = "sha256:187ccc0c248bfbba564826c26f070494f7bc964fd286b6d9fff4420e55de828c", size = 275153 },
]
[[package]]
name = "babel"
version = "2.16.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/2a/74/f1bc80f23eeba13393b7222b11d95ca3af2c1e28edca18af487137eefed9/babel-2.16.0.tar.gz", hash = "sha256:d1f3554ca26605fe173f3de0c65f750f5a42f924499bf134de6423582298e316", size = 9348104 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ed/20/bc79bc575ba2e2a7f70e8a1155618bb1301eaa5132a8271373a6903f73f8/babel-2.16.0-py3-none-any.whl", hash = "sha256:368b5b98b37c06b7daf6696391c3240c938b37767d4584413e8438c5c435fa8b", size = 9587599 },
]
[[package]]
name = "beautifulsoup4"
version = "4.12.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "soupsieve" },
]
sdist = { url = "https://files.pythonhosted.org/packages/b3/ca/824b1195773ce6166d388573fc106ce56d4a805bd7427b624e063596ec58/beautifulsoup4-4.12.3.tar.gz", hash = "sha256:74e3d1928edc070d21748185c46e3fb33490f22f52a3addee9aee0f4f7781051", size = 581181 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b1/fe/e8c672695b37eecc5cbf43e1d0638d88d66ba3a44c4d321c796f4e59167f/beautifulsoup4-4.12.3-py3-none-any.whl", hash = "sha256:b80878c9f40111313e55da8ba20bdba06d8fa3969fc68304167741bbf9e082ed", size = 147925 },
]
[[package]] [[package]]
name = "cachetools" name = "cachetools"
@ -10,6 +62,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/a4/07/14f8ad37f2d12a5ce41206c21820d8cb6561b728e51fad4530dff0552a67/cachetools-5.5.0-py3-none-any.whl", hash = "sha256:02134e8439cdc2ffb62023ce1debca2944c3f289d66bb17ead3ab3dede74b292", size = 9524 }, { url = "https://files.pythonhosted.org/packages/a4/07/14f8ad37f2d12a5ce41206c21820d8cb6561b728e51fad4530dff0552a67/cachetools-5.5.0-py3-none-any.whl", hash = "sha256:02134e8439cdc2ffb62023ce1debca2944c3f289d66bb17ead3ab3dede74b292", size = 9524 },
] ]
[[package]]
name = "certifi"
version = "2024.12.14"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/0f/bd/1d41ee578ce09523c81a15426705dd20969f5abf006d1afe8aeff0dd776a/certifi-2024.12.14.tar.gz", hash = "sha256:b650d30f370c2b724812bee08008be0c4163b163ddaec3f2546c1caf65f191db", size = 166010 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a5/32/8f6669fc4798494966bf446c8c4a162e0b5d893dff088afddf76414f70e1/certifi-2024.12.14-py3-none-any.whl", hash = "sha256:1275f7a45be9464efc1173084eaa30f866fe2e47d389406136d332ed4967ec56", size = 164927 },
]
[[package]] [[package]]
name = "cfgv" name = "cfgv"
version = "3.4.0" version = "3.4.0"
@ -28,6 +89,28 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/38/6f/f5fbc992a329ee4e0f288c1fe0e2ad9485ed064cac731ed2fe47dcc38cbf/chardet-5.2.0-py3-none-any.whl", hash = "sha256:e1cf59446890a00105fe7b7912492ea04b6e6f06d4b742b2c788469e34c82970", size = 199385 }, { url = "https://files.pythonhosted.org/packages/38/6f/f5fbc992a329ee4e0f288c1fe0e2ad9485ed064cac731ed2fe47dcc38cbf/chardet-5.2.0-py3-none-any.whl", hash = "sha256:e1cf59446890a00105fe7b7912492ea04b6e6f06d4b742b2c788469e34c82970", size = 199385 },
] ]
[[package]]
name = "charset-normalizer"
version = "3.4.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/16/b0/572805e227f01586461c80e0fd25d65a2115599cc9dad142fee4b747c357/charset_normalizer-3.4.1.tar.gz", hash = "sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3", size = 123188 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/38/94/ce8e6f63d18049672c76d07d119304e1e2d7c6098f0841b51c666e9f44a0/charset_normalizer-3.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:aabfa34badd18f1da5ec1bc2715cadc8dca465868a4e73a0173466b688f29dda", size = 195698 },
{ url = "https://files.pythonhosted.org/packages/24/2e/dfdd9770664aae179a96561cc6952ff08f9a8cd09a908f259a9dfa063568/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22e14b5d70560b8dd51ec22863f370d1e595ac3d024cb8ad7d308b4cd95f8313", size = 140162 },
{ url = "https://files.pythonhosted.org/packages/24/4e/f646b9093cff8fc86f2d60af2de4dc17c759de9d554f130b140ea4738ca6/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8436c508b408b82d87dc5f62496973a1805cd46727c34440b0d29d8a2f50a6c9", size = 150263 },
{ url = "https://files.pythonhosted.org/packages/5e/67/2937f8d548c3ef6e2f9aab0f6e21001056f692d43282b165e7c56023e6dd/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d074908e1aecee37a7635990b2c6d504cd4766c7bc9fc86d63f9c09af3fa11b", size = 142966 },
{ url = "https://files.pythonhosted.org/packages/52/ed/b7f4f07de100bdb95c1756d3a4d17b90c1a3c53715c1a476f8738058e0fa/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:955f8851919303c92343d2f66165294848d57e9bba6cf6e3625485a70a038d11", size = 144992 },
{ url = "https://files.pythonhosted.org/packages/96/2c/d49710a6dbcd3776265f4c923bb73ebe83933dfbaa841c5da850fe0fd20b/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:44ecbf16649486d4aebafeaa7ec4c9fed8b88101f4dd612dcaf65d5e815f837f", size = 147162 },
{ url = "https://files.pythonhosted.org/packages/b4/41/35ff1f9a6bd380303dea55e44c4933b4cc3c4850988927d4082ada230273/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0924e81d3d5e70f8126529951dac65c1010cdf117bb75eb02dd12339b57749dd", size = 140972 },
{ url = "https://files.pythonhosted.org/packages/fb/43/c6a0b685fe6910d08ba971f62cd9c3e862a85770395ba5d9cad4fede33ab/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2967f74ad52c3b98de4c3b32e1a44e32975e008a9cd2a8cc8966d6a5218c5cb2", size = 149095 },
{ url = "https://files.pythonhosted.org/packages/4c/ff/a9a504662452e2d2878512115638966e75633519ec11f25fca3d2049a94a/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c75cb2a3e389853835e84a2d8fb2b81a10645b503eca9bcb98df6b5a43eb8886", size = 152668 },
{ url = "https://files.pythonhosted.org/packages/6c/71/189996b6d9a4b932564701628af5cee6716733e9165af1d5e1b285c530ed/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:09b26ae6b1abf0d27570633b2b078a2a20419c99d66fb2823173d73f188ce601", size = 150073 },
{ url = "https://files.pythonhosted.org/packages/e4/93/946a86ce20790e11312c87c75ba68d5f6ad2208cfb52b2d6a2c32840d922/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa88b843d6e211393a37219e6a1c1df99d35e8fd90446f1118f4216e307e48cd", size = 145732 },
{ url = "https://files.pythonhosted.org/packages/cd/e5/131d2fb1b0dddafc37be4f3a2fa79aa4c037368be9423061dccadfd90091/charset_normalizer-3.4.1-cp313-cp313-win32.whl", hash = "sha256:eb8178fe3dba6450a3e024e95ac49ed3400e506fd4e9e5c32d30adda88cbd407", size = 95391 },
{ url = "https://files.pythonhosted.org/packages/27/f2/4f9a69cc7712b9b5ad8fdb87039fd89abba997ad5cbe690d1835d40405b0/charset_normalizer-3.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:b1ac5992a838106edb89654e0aebfc24f5848ae2547d22c2c3f66454daa11971", size = 102702 },
{ url = "https://files.pythonhosted.org/packages/0e/f6/65ecc6878a89bb1c23a086ea335ad4bf21a588990c3f535a227b9eea9108/charset_normalizer-3.4.1-py3-none-any.whl", hash = "sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85", size = 49767 },
]
[[package]] [[package]]
name = "click" name = "click"
version = "8.1.8" version = "8.1.8"
@ -58,6 +141,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/91/a1/cf2472db20f7ce4a6be1253a81cfdf85ad9c7885ffbed7047fb72c24cf87/distlib-0.3.9-py2.py3-none-any.whl", hash = "sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87", size = 468973 }, { url = "https://files.pythonhosted.org/packages/91/a1/cf2472db20f7ce4a6be1253a81cfdf85ad9c7885ffbed7047fb72c24cf87/distlib-0.3.9-py2.py3-none-any.whl", hash = "sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87", size = 468973 },
] ]
[[package]]
name = "docutils"
version = "0.21.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/ae/ed/aefcc8cd0ba62a0560c3c18c33925362d46c6075480bfa4df87b28e169a9/docutils-0.21.2.tar.gz", hash = "sha256:3a6b18732edf182daa3cd12775bbb338cf5691468f91eeeb109deff6ebfa986f", size = 2204444 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/8f/d7/9322c609343d929e75e7e5e6255e614fcc67572cfd083959cdef3b7aad79/docutils-0.21.2-py3-none-any.whl", hash = "sha256:dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2", size = 587408 },
]
[[package]] [[package]]
name = "filelock" name = "filelock"
version = "3.16.1" version = "3.16.1"
@ -67,6 +159,30 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/b9/f8/feced7779d755758a52d1f6635d990b8d98dc0a29fa568bbe0625f18fdf3/filelock-3.16.1-py3-none-any.whl", hash = "sha256:2082e5703d51fbf98ea75855d9d5527e33d8ff23099bec374a134febee6946b0", size = 16163 }, { url = "https://files.pythonhosted.org/packages/b9/f8/feced7779d755758a52d1f6635d990b8d98dc0a29fa568bbe0625f18fdf3/filelock-3.16.1-py3-none-any.whl", hash = "sha256:2082e5703d51fbf98ea75855d9d5527e33d8ff23099bec374a134febee6946b0", size = 16163 },
] ]
[[package]]
name = "furo"
version = "2024.8.6"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "beautifulsoup4" },
{ name = "pygments" },
{ name = "sphinx" },
{ name = "sphinx-basic-ng" },
]
sdist = { url = "https://files.pythonhosted.org/packages/a0/e2/d351d69a9a9e4badb4a5be062c2d0e87bd9e6c23b5e57337fef14bef34c8/furo-2024.8.6.tar.gz", hash = "sha256:b63e4cee8abfc3136d3bc03a3d45a76a850bada4d6374d24c1716b0e01394a01", size = 1661506 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/27/48/e791a7ed487dbb9729ef32bb5d1af16693d8925f4366befef54119b2e576/furo-2024.8.6-py3-none-any.whl", hash = "sha256:6cd97c58b47813d3619e63e9081169880fbe331f0ca883c871ff1f3f11814f5c", size = 341333 },
]
[[package]]
name = "h11"
version = "0.14.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f5/38/3af3d3633a34a3316095b39c8e8fb4853a28a536e55d347bd8d8e9a14b03/h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d", size = 100418 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/95/04/ff642e65ad6b90db43e668d70ffb6736436c7ce41fcc549f4e9472234127/h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761", size = 58259 },
]
[[package]] [[package]]
name = "identify" name = "identify"
version = "2.6.5" version = "2.6.5"
@ -76,6 +192,24 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/ec/fa/dce098f4cdf7621aa8f7b4f919ce545891f489482f0bfa5102f3eca8608b/identify-2.6.5-py2.py3-none-any.whl", hash = "sha256:14181a47091eb75b337af4c23078c9d09225cd4c48929f521f3bf16b09d02566", size = 99078 }, { url = "https://files.pythonhosted.org/packages/ec/fa/dce098f4cdf7621aa8f7b4f919ce545891f489482f0bfa5102f3eca8608b/identify-2.6.5-py2.py3-none-any.whl", hash = "sha256:14181a47091eb75b337af4c23078c9d09225cd4c48929f521f3bf16b09d02566", size = 99078 },
] ]
[[package]]
name = "idna"
version = "3.10"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 },
]
[[package]]
name = "imagesize"
version = "1.4.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/a7/84/62473fb57d61e31fef6e36d64a179c8781605429fd927b5dd608c997be31/imagesize-1.4.1.tar.gz", hash = "sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a", size = 1280026 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ff/62/85c4c919272577931d407be5ba5d71c20f0b616d31a0befe0ae45bb79abd/imagesize-1.4.1-py2.py3-none-any.whl", hash = "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b", size = 8769 },
]
[[package]] [[package]]
name = "iniconfig" name = "iniconfig"
version = "2.0.0" version = "2.0.0"
@ -97,6 +231,18 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/bd/0f/2ba5fbcd631e3e88689309dbe978c5769e883e4b84ebfe7da30b43275c5a/jinja2-3.1.5-py3-none-any.whl", hash = "sha256:aba0f4dc9ed8013c424088f68a5c226f7d6097ed89b246d7749c2ec4175c6adb", size = 134596 }, { url = "https://files.pythonhosted.org/packages/bd/0f/2ba5fbcd631e3e88689309dbe978c5769e883e4b84ebfe7da30b43275c5a/jinja2-3.1.5-py3-none-any.whl", hash = "sha256:aba0f4dc9ed8013c424088f68a5c226f7d6097ed89b246d7749c2ec4175c6adb", size = 134596 },
] ]
[[package]]
name = "markdown-it-py"
version = "3.0.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "mdurl" },
]
sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528 },
]
[[package]] [[package]]
name = "markupsafe" name = "markupsafe"
version = "3.0.2" version = "3.0.2"
@ -125,6 +271,27 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739 }, { url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739 },
] ]
[[package]]
name = "mdit-py-plugins"
version = "0.4.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "markdown-it-py" },
]
sdist = { url = "https://files.pythonhosted.org/packages/19/03/a2ecab526543b152300717cf232bb4bb8605b6edb946c845016fa9c9c9fd/mdit_py_plugins-0.4.2.tar.gz", hash = "sha256:5f2cd1fdb606ddf152d37ec30e46101a60512bc0e5fa1a7002c36647b09e26b5", size = 43542 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a7/f7/7782a043553ee469c1ff49cfa1cdace2d6bf99a1f333cf38676b3ddf30da/mdit_py_plugins-0.4.2-py3-none-any.whl", hash = "sha256:0c673c3f889399a33b95e88d2f0d111b4447bdfea7f237dab2d488f459835636", size = 55316 },
]
[[package]]
name = "mdurl"
version = "0.1.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979 },
]
[[package]] [[package]]
name = "modify-repos" name = "modify-repos"
version = "0.1.0" version = "0.1.0"
@ -145,6 +312,15 @@ dev = [
{ name = "tox" }, { name = "tox" },
{ name = "tox-uv" }, { name = "tox-uv" },
] ]
docs = [
{ name = "furo" },
{ name = "myst-parser" },
{ name = "sphinx" },
{ name = "sphinx-autodoc2" },
]
docs-auto = [
{ name = "sphinx-autobuild" },
]
pre-commit = [ pre-commit = [
{ name = "pre-commit" }, { name = "pre-commit" },
] ]
@ -170,6 +346,13 @@ dev = [
{ name = "tox", specifier = ">=4.23.2" }, { name = "tox", specifier = ">=4.23.2" },
{ name = "tox-uv", specifier = ">=1.17.0" }, { name = "tox-uv", specifier = ">=1.17.0" },
] ]
docs = [
{ name = "furo", specifier = ">=2024.8.6" },
{ name = "myst-parser", specifier = ">=4.0.0" },
{ name = "sphinx", specifier = ">=8.1.3" },
{ name = "sphinx-autodoc2", specifier = ">=0.5.0" },
]
docs-auto = [{ name = "sphinx-autobuild", specifier = ">=2024.10.3" }]
pre-commit = [{ name = "pre-commit", specifier = ">=4.0.1" }] pre-commit = [{ name = "pre-commit", specifier = ">=4.0.1" }]
typing = [ typing = [
{ name = "mypy", specifier = ">=1.14.1" }, { name = "mypy", specifier = ">=1.14.1" },
@ -204,6 +387,23 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/2a/e2/5d3f6ada4297caebe1a2add3b126fe800c96f56dbe5d1988a2cbe0b267aa/mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d", size = 4695 }, { url = "https://files.pythonhosted.org/packages/2a/e2/5d3f6ada4297caebe1a2add3b126fe800c96f56dbe5d1988a2cbe0b267aa/mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d", size = 4695 },
] ]
[[package]]
name = "myst-parser"
version = "4.0.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "docutils" },
{ name = "jinja2" },
{ name = "markdown-it-py" },
{ name = "mdit-py-plugins" },
{ name = "pyyaml" },
{ name = "sphinx" },
]
sdist = { url = "https://files.pythonhosted.org/packages/85/55/6d1741a1780e5e65038b74bce6689da15f620261c490c3511eb4c12bac4b/myst_parser-4.0.0.tar.gz", hash = "sha256:851c9dfb44e36e56d15d05e72f02b80da21a9e0d07cba96baf5e2d476bb91531", size = 93858 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ca/b4/b036f8fdb667587bb37df29dc6644681dd78b7a2a6321a34684b79412b28/myst_parser-4.0.0-py3-none-any.whl", hash = "sha256:b9317997552424448c6096c2558872fdb6f81d3ecb3a40ce84a7518798f3f28d", size = 84563 },
]
[[package]] [[package]]
name = "nodeenv" name = "nodeenv"
version = "1.9.1" version = "1.9.1"
@ -256,6 +456,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/16/8f/496e10d51edd6671ebe0432e33ff800aa86775d2d147ce7d43389324a525/pre_commit-4.0.1-py2.py3-none-any.whl", hash = "sha256:efde913840816312445dc98787724647c65473daefe420785f885e8ed9a06878", size = 218713 }, { url = "https://files.pythonhosted.org/packages/16/8f/496e10d51edd6671ebe0432e33ff800aa86775d2d147ce7d43389324a525/pre_commit-4.0.1-py2.py3-none-any.whl", hash = "sha256:efde913840816312445dc98787724647c65473daefe420785f885e8ed9a06878", size = 218713 },
] ]
[[package]]
name = "pygments"
version = "2.19.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/7c/2d/c3338d48ea6cc0feb8446d8e6937e1408088a72a39937982cc6111d17f84/pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f", size = 4968581 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293 },
]
[[package]] [[package]]
name = "pyproject-api" name = "pyproject-api"
version = "1.8.0" version = "1.8.0"
@ -313,6 +522,21 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446 }, { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446 },
] ]
[[package]]
name = "requests"
version = "2.32.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "certifi" },
{ name = "charset-normalizer" },
{ name = "idna" },
{ name = "urllib3" },
]
sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928 },
]
[[package]] [[package]]
name = "ruff" name = "ruff"
version = "0.9.0" version = "0.9.0"
@ -338,6 +562,168 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/73/0e/c00f66731e514be3299801b1d9d54efae0abfe8f00a5c14155f2ab9e2920/ruff-0.9.0-py3-none-win_arm64.whl", hash = "sha256:7b1148771c6ca88f820d761350a053a5794bc58e0867739ea93eb5e41ad978cd", size = 9147729 }, { url = "https://files.pythonhosted.org/packages/73/0e/c00f66731e514be3299801b1d9d54efae0abfe8f00a5c14155f2ab9e2920/ruff-0.9.0-py3-none-win_arm64.whl", hash = "sha256:7b1148771c6ca88f820d761350a053a5794bc58e0867739ea93eb5e41ad978cd", size = 9147729 },
] ]
[[package]]
name = "sniffio"
version = "1.3.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 },
]
[[package]]
name = "snowballstemmer"
version = "2.2.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/44/7b/af302bebf22c749c56c9c3e8ae13190b5b5db37a33d9068652e8f73b7089/snowballstemmer-2.2.0.tar.gz", hash = "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1", size = 86699 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ed/dc/c02e01294f7265e63a7315fe086dd1df7dacb9f840a804da846b96d01b96/snowballstemmer-2.2.0-py2.py3-none-any.whl", hash = "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a", size = 93002 },
]
[[package]]
name = "soupsieve"
version = "2.6"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d7/ce/fbaeed4f9fb8b2daa961f90591662df6a86c1abf25c548329a86920aedfb/soupsieve-2.6.tar.gz", hash = "sha256:e2e68417777af359ec65daac1057404a3c8a5455bb8abc36f1a9866ab1a51abb", size = 101569 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d1/c2/fe97d779f3ef3b15f05c94a2f1e3d21732574ed441687474db9d342a7315/soupsieve-2.6-py3-none-any.whl", hash = "sha256:e72c4ff06e4fb6e4b5a9f0f55fe6e81514581fca1515028625d0f299c602ccc9", size = 36186 },
]
[[package]]
name = "sphinx"
version = "8.1.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "alabaster" },
{ name = "babel" },
{ name = "colorama", marker = "sys_platform == 'win32'" },
{ name = "docutils" },
{ name = "imagesize" },
{ name = "jinja2" },
{ name = "packaging" },
{ name = "pygments" },
{ name = "requests" },
{ name = "snowballstemmer" },
{ name = "sphinxcontrib-applehelp" },
{ name = "sphinxcontrib-devhelp" },
{ name = "sphinxcontrib-htmlhelp" },
{ name = "sphinxcontrib-jsmath" },
{ name = "sphinxcontrib-qthelp" },
{ name = "sphinxcontrib-serializinghtml" },
]
sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/be0b61178fe2cdcb67e2a92fc9ebb488e3c51c4f74a36a7824c0adf23425/sphinx-8.1.3.tar.gz", hash = "sha256:43c1911eecb0d3e161ad78611bc905d1ad0e523e4ddc202a58a821773dc4c927", size = 8184611 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/26/60/1ddff83a56d33aaf6f10ec8ce84b4c007d9368b21008876fceda7e7381ef/sphinx-8.1.3-py3-none-any.whl", hash = "sha256:09719015511837b76bf6e03e42eb7595ac8c2e41eeb9c29c5b755c6b677992a2", size = 3487125 },
]
[[package]]
name = "sphinx-autobuild"
version = "2024.10.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama" },
{ name = "sphinx" },
{ name = "starlette" },
{ name = "uvicorn" },
{ name = "watchfiles" },
{ name = "websockets" },
]
sdist = { url = "https://files.pythonhosted.org/packages/a5/2c/155e1de2c1ba96a72e5dba152c509a8b41e047ee5c2def9e9f0d812f8be7/sphinx_autobuild-2024.10.3.tar.gz", hash = "sha256:248150f8f333e825107b6d4b86113ab28fa51750e5f9ae63b59dc339be951fb1", size = 14023 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/18/c0/eba125db38c84d3c74717008fd3cb5000b68cd7e2cbafd1349c6a38c3d3b/sphinx_autobuild-2024.10.3-py3-none-any.whl", hash = "sha256:158e16c36f9d633e613c9aaf81c19b0fc458ca78b112533b20dafcda430d60fa", size = 11908 },
]
[[package]]
name = "sphinx-autodoc2"
version = "0.5.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "astroid" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/17/5f/5350046d1aa1a56b063ae08b9ad871025335c9d55fe2372896ea48711da9/sphinx_autodoc2-0.5.0.tar.gz", hash = "sha256:7d76044aa81d6af74447080182b6868c7eb066874edc835e8ddf810735b6565a", size = 115077 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/19/e6/48d47961bbdae755ba9c17dfc65d89356312c67668dcb36c87cfadfa1964/sphinx_autodoc2-0.5.0-py3-none-any.whl", hash = "sha256:e867013b1512f9d6d7e6f6799f8b537d6884462acd118ef361f3f619a60b5c9e", size = 43385 },
]
[[package]]
name = "sphinx-basic-ng"
version = "1.0.0b2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "sphinx" },
]
sdist = { url = "https://files.pythonhosted.org/packages/98/0b/a866924ded68efec7a1759587a4e478aec7559d8165fac8b2ad1c0e774d6/sphinx_basic_ng-1.0.0b2.tar.gz", hash = "sha256:9ec55a47c90c8c002b5960c57492ec3021f5193cb26cebc2dc4ea226848651c9", size = 20736 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/3c/dd/018ce05c532a22007ac58d4f45232514cd9d6dd0ee1dc374e309db830983/sphinx_basic_ng-1.0.0b2-py3-none-any.whl", hash = "sha256:eb09aedbabfb650607e9b4b68c9d240b90b1e1be221d6ad71d61c52e29f7932b", size = 22496 },
]
[[package]]
name = "sphinxcontrib-applehelp"
version = "2.0.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/ba/6e/b837e84a1a704953c62ef8776d45c3e8d759876b4a84fe14eba2859106fe/sphinxcontrib_applehelp-2.0.0.tar.gz", hash = "sha256:2f29ef331735ce958efa4734873f084941970894c6090408b079c61b2e1c06d1", size = 20053 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/5d/85/9ebeae2f76e9e77b952f4b274c27238156eae7979c5421fba91a28f4970d/sphinxcontrib_applehelp-2.0.0-py3-none-any.whl", hash = "sha256:4cd3f0ec4ac5dd9c17ec65e9ab272c9b867ea77425228e68ecf08d6b28ddbdb5", size = 119300 },
]
[[package]]
name = "sphinxcontrib-devhelp"
version = "2.0.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f6/d2/5beee64d3e4e747f316bae86b55943f51e82bb86ecd325883ef65741e7da/sphinxcontrib_devhelp-2.0.0.tar.gz", hash = "sha256:411f5d96d445d1d73bb5d52133377b4248ec79db5c793ce7dbe59e074b4dd1ad", size = 12967 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/35/7a/987e583882f985fe4d7323774889ec58049171828b58c2217e7f79cdf44e/sphinxcontrib_devhelp-2.0.0-py3-none-any.whl", hash = "sha256:aefb8b83854e4b0998877524d1029fd3e6879210422ee3780459e28a1f03a8a2", size = 82530 },
]
[[package]]
name = "sphinxcontrib-htmlhelp"
version = "2.1.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/43/93/983afd9aa001e5201eab16b5a444ed5b9b0a7a010541e0ddfbbfd0b2470c/sphinxcontrib_htmlhelp-2.1.0.tar.gz", hash = "sha256:c9e2916ace8aad64cc13a0d233ee22317f2b9025b9cf3295249fa985cc7082e9", size = 22617 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/0a/7b/18a8c0bcec9182c05a0b3ec2a776bba4ead82750a55ff798e8d406dae604/sphinxcontrib_htmlhelp-2.1.0-py3-none-any.whl", hash = "sha256:166759820b47002d22914d64a075ce08f4c46818e17cfc9470a9786b759b19f8", size = 98705 },
]
[[package]]
name = "sphinxcontrib-jsmath"
version = "1.0.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/b2/e8/9ed3830aeed71f17c026a07a5097edcf44b692850ef215b161b8ad875729/sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8", size = 5787 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c2/42/4c8646762ee83602e3fb3fbe774c2fac12f317deb0b5dbeeedd2d3ba4b77/sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178", size = 5071 },
]
[[package]]
name = "sphinxcontrib-qthelp"
version = "2.0.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/68/bc/9104308fc285eb3e0b31b67688235db556cd5b0ef31d96f30e45f2e51cae/sphinxcontrib_qthelp-2.0.0.tar.gz", hash = "sha256:4fe7d0ac8fc171045be623aba3e2a8f613f8682731f9153bb2e40ece16b9bbab", size = 17165 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/27/83/859ecdd180cacc13b1f7e857abf8582a64552ea7a061057a6c716e790fce/sphinxcontrib_qthelp-2.0.0-py3-none-any.whl", hash = "sha256:b18a828cdba941ccd6ee8445dbe72ffa3ef8cbe7505d8cd1fa0d42d3f2d5f3eb", size = 88743 },
]
[[package]]
name = "sphinxcontrib-serializinghtml"
version = "2.0.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/3b/44/6716b257b0aa6bfd51a1b31665d1c205fb12cb5ad56de752dfa15657de2f/sphinxcontrib_serializinghtml-2.0.0.tar.gz", hash = "sha256:e9d912827f872c029017a53f0ef2180b327c3f7fd23c87229f7a8e8b70031d4d", size = 16080 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/52/a7/d2782e4e3f77c8450f727ba74a8f12756d5ba823d81b941f1b04da9d033a/sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl", hash = "sha256:6e2cb0eef194e10c27ec0023bfeb25badbbb5868244cf5bc5bdc04e4464bf331", size = 92072 },
]
[[package]]
name = "starlette"
version = "0.45.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
]
sdist = { url = "https://files.pythonhosted.org/packages/90/4f/e1c9f4ec3dae67a94c9285ed275355d5f7cf0f3a5c34538c8ae5412af550/starlette-0.45.2.tar.gz", hash = "sha256:bba1831d15ae5212b22feab2f218bab6ed3cd0fc2dc1d4442443bb1ee52260e0", size = 2574026 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/aa/ab/fe4f57c83620b39dfc9e7687ebad59129ff05170b99422105019d9a65eec/starlette-0.45.2-py3-none-any.whl", hash = "sha256:4daec3356fb0cb1e723a5235e5beaf375d2259af27532958e2d79df549dad9da", size = 71505 },
]
[[package]] [[package]]
name = "tox" name = "tox"
version = "4.23.2" version = "4.23.2"
@ -381,6 +767,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 }, { url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 },
] ]
[[package]]
name = "urllib3"
version = "2.3.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/aa/63/e53da845320b757bf29ef6a9062f5c669fe997973f966045cb019c3f4b66/urllib3-2.3.0.tar.gz", hash = "sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d", size = 307268 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c8/19/4ec628951a74043532ca2cf5d97b7b14863931476d117c471e8e2b1eb39f/urllib3-2.3.0-py3-none-any.whl", hash = "sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df", size = 128369 },
]
[[package]] [[package]]
name = "uv" name = "uv"
version = "0.5.16" version = "0.5.16"
@ -405,6 +800,19 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/c6/83/72b33a32ef91e04c463d70fc4131b101386c60182de59df1b8f59b108a3d/uv-0.5.16-py3-none-win_amd64.whl", hash = "sha256:c8310f40b8834812ddfb195fd62aafaed44b2993c71de8fa75ca0960c739d04e", size = 16238620 }, { url = "https://files.pythonhosted.org/packages/c6/83/72b33a32ef91e04c463d70fc4131b101386c60182de59df1b8f59b108a3d/uv-0.5.16-py3-none-win_amd64.whl", hash = "sha256:c8310f40b8834812ddfb195fd62aafaed44b2993c71de8fa75ca0960c739d04e", size = 16238620 },
] ]
[[package]]
name = "uvicorn"
version = "0.34.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "click" },
{ name = "h11" },
]
sdist = { url = "https://files.pythonhosted.org/packages/4b/4d/938bd85e5bf2edeec766267a5015ad969730bb91e31b44021dfe8b22df6c/uvicorn-0.34.0.tar.gz", hash = "sha256:404051050cd7e905de2c9a7e61790943440b3416f49cb409f965d9dcd0fa73e9", size = 76568 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/61/14/33a3a1352cfa71812a3a21e8c9bfb83f60b0011f5e36f2b1399d51928209/uvicorn-0.34.0-py3-none-any.whl", hash = "sha256:023dc038422502fa28a09c7a30bf2b6991512da7dcdb8fd35fe57cfc154126f4", size = 62315 },
]
[[package]] [[package]]
name = "virtualenv" name = "virtualenv"
version = "20.28.1" version = "20.28.1"
@ -418,3 +826,46 @@ sdist = { url = "https://files.pythonhosted.org/packages/50/39/689abee4adc85aad2
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/51/8f/dfb257ca6b4e27cb990f1631142361e4712badab8e3ca8dc134d96111515/virtualenv-20.28.1-py3-none-any.whl", hash = "sha256:412773c85d4dab0409b83ec36f7a6499e72eaf08c80e81e9576bca61831c71cb", size = 4276719 }, { url = "https://files.pythonhosted.org/packages/51/8f/dfb257ca6b4e27cb990f1631142361e4712badab8e3ca8dc134d96111515/virtualenv-20.28.1-py3-none-any.whl", hash = "sha256:412773c85d4dab0409b83ec36f7a6499e72eaf08c80e81e9576bca61831c71cb", size = 4276719 },
] ]
[[package]]
name = "watchfiles"
version = "1.0.4"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
]
sdist = { url = "https://files.pythonhosted.org/packages/f5/26/c705fc77d0a9ecdb9b66f1e2976d95b81df3cae518967431e7dbf9b5e219/watchfiles-1.0.4.tar.gz", hash = "sha256:6ba473efd11062d73e4f00c2b730255f9c1bdd73cd5f9fe5b5da8dbd4a717205", size = 94625 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/08/98/f03efabec64b5b1fa58c0daab25c68ef815b0f320e54adcacd0d6847c339/watchfiles-1.0.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:8012bd820c380c3d3db8435e8cf7592260257b378b649154a7948a663b5f84e9", size = 390954 },
{ url = "https://files.pythonhosted.org/packages/16/09/4dd49ba0a32a45813debe5fb3897955541351ee8142f586303b271a02b40/watchfiles-1.0.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:aa216f87594f951c17511efe5912808dfcc4befa464ab17c98d387830ce07b60", size = 381133 },
{ url = "https://files.pythonhosted.org/packages/76/59/5aa6fc93553cd8d8ee75c6247763d77c02631aed21551a97d94998bf1dae/watchfiles-1.0.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:62c9953cf85529c05b24705639ffa390f78c26449e15ec34d5339e8108c7c407", size = 449516 },
{ url = "https://files.pythonhosted.org/packages/4c/aa/df4b6fe14b6317290b91335b23c96b488d365d65549587434817e06895ea/watchfiles-1.0.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7cf684aa9bba4cd95ecb62c822a56de54e3ae0598c1a7f2065d51e24637a3c5d", size = 454820 },
{ url = "https://files.pythonhosted.org/packages/5e/71/185f8672f1094ce48af33252c73e39b48be93b761273872d9312087245f6/watchfiles-1.0.4-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f44a39aee3cbb9b825285ff979ab887a25c5d336e5ec3574f1506a4671556a8d", size = 481550 },
{ url = "https://files.pythonhosted.org/packages/85/d7/50ebba2c426ef1a5cb17f02158222911a2e005d401caf5d911bfca58f4c4/watchfiles-1.0.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a38320582736922be8c865d46520c043bff350956dfc9fbaee3b2df4e1740a4b", size = 518647 },
{ url = "https://files.pythonhosted.org/packages/f0/7a/4c009342e393c545d68987e8010b937f72f47937731225b2b29b7231428f/watchfiles-1.0.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:39f4914548b818540ef21fd22447a63e7be6e24b43a70f7642d21f1e73371590", size = 497547 },
{ url = "https://files.pythonhosted.org/packages/0f/7c/1cf50b35412d5c72d63b2bf9a4fffee2e1549a245924960dd087eb6a6de4/watchfiles-1.0.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f12969a3765909cf5dc1e50b2436eb2c0e676a3c75773ab8cc3aa6175c16e902", size = 452179 },
{ url = "https://files.pythonhosted.org/packages/d6/a9/3db1410e1c1413735a9a472380e4f431ad9a9e81711cda2aaf02b7f62693/watchfiles-1.0.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:0986902677a1a5e6212d0c49b319aad9cc48da4bd967f86a11bde96ad9676ca1", size = 614125 },
{ url = "https://files.pythonhosted.org/packages/f2/e1/0025d365cf6248c4d1ee4c3d2e3d373bdd3f6aff78ba4298f97b4fad2740/watchfiles-1.0.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:308ac265c56f936636e3b0e3f59e059a40003c655228c131e1ad439957592303", size = 611911 },
{ url = "https://files.pythonhosted.org/packages/55/55/035838277d8c98fc8c917ac9beeb0cd6c59d675dc2421df5f9fcf44a0070/watchfiles-1.0.4-cp313-cp313-win32.whl", hash = "sha256:aee397456a29b492c20fda2d8961e1ffb266223625346ace14e4b6d861ba9c80", size = 271152 },
{ url = "https://files.pythonhosted.org/packages/f0/e5/96b8e55271685ddbadc50ce8bc53aa2dff278fb7ac4c2e473df890def2dc/watchfiles-1.0.4-cp313-cp313-win_amd64.whl", hash = "sha256:d6097538b0ae5c1b88c3b55afa245a66793a8fec7ada6755322e465fb1a0e8cc", size = 285216 },
]
[[package]]
name = "websockets"
version = "14.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f4/1b/380b883ce05bb5f45a905b61790319a28958a9ab1e4b6b95ff5464b60ca1/websockets-14.1.tar.gz", hash = "sha256:398b10c77d471c0aab20a845e7a60076b6390bfdaac7a6d2edb0d2c59d75e8d8", size = 162840 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/34/77/812b3ba5110ed8726eddf9257ab55ce9e85d97d4aa016805fdbecc5e5d48/websockets-14.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:3630b670d5057cd9e08b9c4dab6493670e8e762a24c2c94ef312783870736ab9", size = 161966 },
{ url = "https://files.pythonhosted.org/packages/8d/24/4fcb7aa6986ae7d9f6d083d9d53d580af1483c5ec24bdec0978307a0f6ac/websockets-14.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:36ebd71db3b89e1f7b1a5deaa341a654852c3518ea7a8ddfdf69cc66acc2db1b", size = 159625 },
{ url = "https://files.pythonhosted.org/packages/f8/47/2a0a3a2fc4965ff5b9ce9324d63220156bd8bedf7f90824ab92a822e65fd/websockets-14.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5b918d288958dc3fa1c5a0b9aa3256cb2b2b84c54407f4813c45d52267600cd3", size = 159857 },
{ url = "https://files.pythonhosted.org/packages/dd/c8/d7b425011a15e35e17757e4df75b25e1d0df64c0c315a44550454eaf88fc/websockets-14.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:00fe5da3f037041da1ee0cf8e308374e236883f9842c7c465aa65098b1c9af59", size = 169635 },
{ url = "https://files.pythonhosted.org/packages/93/39/6e3b5cffa11036c40bd2f13aba2e8e691ab2e01595532c46437b56575678/websockets-14.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8149a0f5a72ca36720981418eeffeb5c2729ea55fa179091c81a0910a114a5d2", size = 168578 },
{ url = "https://files.pythonhosted.org/packages/cf/03/8faa5c9576299b2adf34dcccf278fc6bbbcda8a3efcc4d817369026be421/websockets-14.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:77569d19a13015e840b81550922056acabc25e3f52782625bc6843cfa034e1da", size = 169018 },
{ url = "https://files.pythonhosted.org/packages/8c/05/ea1fec05cc3a60defcdf0bb9f760c3c6bd2dd2710eff7ac7f891864a22ba/websockets-14.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cf5201a04550136ef870aa60ad3d29d2a59e452a7f96b94193bee6d73b8ad9a9", size = 169383 },
{ url = "https://files.pythonhosted.org/packages/21/1d/eac1d9ed787f80754e51228e78855f879ede1172c8b6185aca8cef494911/websockets-14.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:88cf9163ef674b5be5736a584c999e98daf3aabac6e536e43286eb74c126b9c7", size = 168773 },
{ url = "https://files.pythonhosted.org/packages/0e/1b/e808685530185915299740d82b3a4af3f2b44e56ccf4389397c7a5d95d39/websockets-14.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:836bef7ae338a072e9d1863502026f01b14027250a4545672673057997d5c05a", size = 168757 },
{ url = "https://files.pythonhosted.org/packages/b6/19/6ab716d02a3b068fbbeb6face8a7423156e12c446975312f1c7c0f4badab/websockets-14.1-cp313-cp313-win32.whl", hash = "sha256:0d4290d559d68288da9f444089fd82490c8d2744309113fc26e2da6e48b65da6", size = 162834 },
{ url = "https://files.pythonhosted.org/packages/6c/fd/ab6b7676ba712f2fc89d1347a4b5bdc6aa130de10404071f2b2606450209/websockets-14.1-cp313-cp313-win_amd64.whl", hash = "sha256:8621a07991add373c3c5c2cf89e1d277e49dc82ed72c75e3afc74bd0acc446f0", size = 163277 },
{ url = "https://files.pythonhosted.org/packages/b0/0b/c7e5d11020242984d9d37990310520ed663b942333b83a033c2f20191113/websockets-14.1-py3-none-any.whl", hash = "sha256:4d4fc827a20abe6d544a119896f6b78ee13fe81cbfef416f3f2ddf09a03f0e2e", size = 156277 },
]