diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 0000000..acbd83f --- /dev/null +++ b/.readthedocs.yaml @@ -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 diff --git a/CHANGES.md b/CHANGES.md new file mode 100644 index 0000000..81dd74c --- /dev/null +++ b/CHANGES.md @@ -0,0 +1,3 @@ +## Version 0.1.0 + +Unreleased diff --git a/README.md b/README.md index e2a2891..721dd2d 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,77 @@ # modify-repos -Clone, modify, and create pull requests across multiple repositories at -once. +Clone, modify, and create pull requests across multiple repositories at once. + +> [!WARNING] +> This is under development, and how it's used may change at any time. + +Documentation: + +## 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/ diff --git a/docs/_static/theme.css b/docs/_static/theme.css new file mode 100644 index 0000000..35e705c --- /dev/null +++ b/docs/_static/theme.css @@ -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'); diff --git a/docs/changes.md b/docs/changes.md new file mode 100644 index 0000000..5f522c2 --- /dev/null +++ b/docs/changes.md @@ -0,0 +1,4 @@ +# Changes + +```{include} ../CHANGES.md +``` diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 0000000..13629b4 --- /dev/null +++ b/docs/conf.py @@ -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 diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..0bfa501 --- /dev/null +++ b/docs/index.md @@ -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 +``` diff --git a/docs/license.md b/docs/license.md new file mode 100644 index 0000000..0f433a0 --- /dev/null +++ b/docs/license.md @@ -0,0 +1,5 @@ +# MIT License + +```{literalinclude} ../LICENSE.txt +:language: text +``` diff --git a/docs/providers/base.md b/docs/providers/base.md new file mode 100644 index 0000000..48c7c21 --- /dev/null +++ b/docs/providers/base.md @@ -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: +``` diff --git a/docs/providers/git.md b/docs/providers/git.md new file mode 100644 index 0000000..5d39580 --- /dev/null +++ b/docs/providers/git.md @@ -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: +``` diff --git a/docs/providers/github.md b/docs/providers/github.md new file mode 100644 index 0000000..e083a7a --- /dev/null +++ b/docs/providers/github.md @@ -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: +``` diff --git a/docs/script.md b/docs/script.md new file mode 100644 index 0000000..46e57d3 --- /dev/null +++ b/docs/script.md @@ -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/ diff --git a/docs/utils.md b/docs/utils.md new file mode 100644 index 0000000..36d8c08 --- /dev/null +++ b/docs/utils.md @@ -0,0 +1,13 @@ +# Utilities + +```{eval-rst} +.. currentmodule:: modify_repos.utils + +.. autofunction:: run_cmd + +.. autofunction:: wrap_text + +.. autofunction:: read_text + +.. autofunction:: write_text +``` diff --git a/pyproject.toml b/pyproject.toml index 742328b..d77474a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ readme = "README.md" authors = [{ name = "David Lord" }] license = "MIT" license-files = ["LICENSE.txt"] -requires-python = "~=3.13" +requires-python = "~=3.13.0" dependencies = [ "click>=8.1.8", "jinja2>=3.1.5", @@ -34,6 +34,15 @@ typing = [ "mypy>=1.14.1", "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] testpaths = ["tests"] @@ -86,7 +95,7 @@ tag-only = [ ] [tool.tox] -env_list = ["style", "typing"] +env_list = ["style", "typing", "docs"] [tool.tox.env_run_base] runner = "uv-venv-lock-runner" @@ -108,6 +117,14 @@ commands = [ ["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] labels = ["update"] dependency_groups = ["pre-commit"] diff --git a/src/modify_repos/repo/base.py b/src/modify_repos/repo/base.py index fec8e0f..6179495 100644 --- a/src/modify_repos/repo/base.py +++ b/src/modify_repos/repo/base.py @@ -12,52 +12,108 @@ if t.TYPE_CHECKING: 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 def __init__(self, script: Script[t.Any], remote_id: str) -> None: self.script = script + """The script being run to modify this repo.""" + 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 def local_dir(self) -> Path: + """The path where this repo is cloned.""" return self.script.clones_dir / self.remote_id def clone(self) -> None: + """Clone the repository unconditionally. This is called by + :meth:`clone_if_needed`. + """ raise NotImplementedError 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(): self.clone() 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 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 def needs_commit(self) -> bool: + """Check if there are uncommitted changes. Called by + :meth:`auto_commit_if_needed`. + """ 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 - 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(): - self.commit() + self.auto_commit() def needs_submit(self) -> bool: + """Check if there are commits that have not been pushed upstream. Called + by :meth:`submit_if_needed`.""" raise NotImplementedError 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 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(): self.submit() else: click.secho("skipping submit", fg="yellow") 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") self.clone_if_needed() @@ -70,5 +126,5 @@ class Repo: self.reset_branch() self.script.modify(self) - self.commit_if_needed() + self.auto_commit_if_needed() self.submit_if_needed() diff --git a/src/modify_repos/repo/git.py b/src/modify_repos/repo/git.py index 1f1e8a5..f3b3d81 100644 --- a/src/modify_repos/repo/git.py +++ b/src/modify_repos/repo/git.py @@ -9,10 +9,24 @@ from .base import 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") 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]: + """Call and pass args to the `git` command. + + :param args: Command line arguments to the `git` command. + """ if self._git_exe is None: raise RuntimeError("Git is not installed.") @@ -34,7 +48,7 @@ class GitRepo(Repo): return bool(self.git_cmd(*args).stdout) - def commit(self) -> None: + def auto_commit(self) -> None: if self.add_untracked: self.add_files(all=True) else: @@ -53,6 +67,12 @@ class GitRepo(Repo): def add_files( self, *items: str | Path, update: bool = False, all: bool = False ) -> 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: self.git_cmd("add", "--all") @@ -62,6 +82,11 @@ class GitRepo(Repo): self.git_cmd("add", *items) 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()] if to_remove: diff --git a/src/modify_repos/repo/github.py b/src/modify_repos/repo/github.py index 82eee6e..be0493e 100644 --- a/src/modify_repos/repo/github.py +++ b/src/modify_repos/repo/github.py @@ -14,8 +14,20 @@ if t.TYPE_CHECKING: 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") 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: self.org = org @@ -23,6 +35,10 @@ class GitHubRepo(GitRepo): super().__init__(script=script, remote_id=self.full_name) 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: raise RuntimeError("GitHub CLI is not installed.") @@ -30,6 +46,7 @@ class GitHubRepo(GitRepo): @cached_property def full_name(self) -> str: + """The `org/name` identifier for the repo.""" return f"{self.org}/{self.name}" def clone(self) -> None: diff --git a/src/modify_repos/script/base.py b/src/modify_repos/script/base.py index 4d04c27..48dc386 100644 --- a/src/modify_repos/script/base.py +++ b/src/modify_repos/script/base.py @@ -13,25 +13,60 @@ from ..utils import wrap_text 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" + """The name of the target branch to branch off of and merge into.""" + branch: str + """The name of the work branch to create.""" + 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 + """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: + """ """ source_file = inspect.getsourcefile(self.__class__) if source_file is None: raise RuntimeError("Could not determine script root.") 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( loader=jinja2.FileSystemLoader(self.root_dir), trim_blocks=True, lstrip_blocks=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" + """Directory where repos are cloned to. Uses the appropriate user cache + dir for the platform. + """ + self.clones_dir.mkdir(parents=True, exist_ok=True) 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.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: + """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) 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 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)] @cached_property def full_target(self) -> str: + """The upstream target branch, which is :attr:`target` prefixed by + `origin/`. + """ return f"origin/{self.target}" @cached_property 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}" 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 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 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 def run(self) -> None: + """Call :meth:`.Repo.run` for each selected repo.""" for repo in self.list_repos(): repo.run() 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) diff --git a/src/modify_repos/script/github.py b/src/modify_repos/script/github.py index c7f99d6..258df06 100644 --- a/src/modify_repos/script/github.py +++ b/src/modify_repos/script/github.py @@ -6,6 +6,17 @@ from .base import Script 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: super().__init__(submit=submit) diff --git a/src/modify_repos/utils.py b/src/modify_repos/utils.py index 7494364..7e718be 100644 --- a/src/modify_repos/utils.py +++ b/src/modify_repos/utils.py @@ -1,5 +1,6 @@ from __future__ import annotations +import os import shlex import subprocess import textwrap @@ -14,9 +15,18 @@ import click def run_cmd( *args: str | PathLike[str], **kwargs: t.Any ) -> 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( - args, + s_args, text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, @@ -30,12 +40,15 @@ def run_cmd( 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: - """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( textwrap.fill(p, width=width, tabsize=4, break_long_words=False) 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: + """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") 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: + """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: text = f"{text.rstrip('\n')}\n" diff --git a/uv.lock b/uv.lock index 92b1557..2c46368 100644 --- a/uv.lock +++ b/uv.lock @@ -1,5 +1,57 @@ 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]] 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 }, ] +[[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]] name = "cfgv" 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 }, ] +[[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]] name = "click" 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 }, ] +[[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]] name = "filelock" 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 }, ] +[[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]] name = "identify" 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 }, ] +[[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]] name = "iniconfig" 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 }, ] +[[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]] name = "markupsafe" 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 }, ] +[[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]] name = "modify-repos" version = "0.1.0" @@ -145,6 +312,15 @@ dev = [ { name = "tox" }, { name = "tox-uv" }, ] +docs = [ + { name = "furo" }, + { name = "myst-parser" }, + { name = "sphinx" }, + { name = "sphinx-autodoc2" }, +] +docs-auto = [ + { name = "sphinx-autobuild" }, +] pre-commit = [ { name = "pre-commit" }, ] @@ -170,6 +346,13 @@ dev = [ { name = "tox", specifier = ">=4.23.2" }, { 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" }] typing = [ { 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 }, ] +[[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]] name = "nodeenv" 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 }, ] +[[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]] name = "pyproject-api" 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 }, ] +[[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]] name = "ruff" 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 }, ] +[[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]] name = "tox" 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 }, ] +[[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]] name = "uv" 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 }, ] +[[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]] name = "virtualenv" version = "20.28.1" @@ -418,3 +826,46 @@ sdist = { url = "https://files.pythonhosted.org/packages/50/39/689abee4adc85aad2 wheels = [ { 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 }, +]