diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..5c3aadb --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,27 @@ +name: Build site + +on: + pull_request: + workflow_dispatch: + +permissions: + contents: read + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + + - name: Install uv + uses: astral-sh/setup-uv@v10 + with: + enable-cache: true + + - run: uv sync --frozen + + - name: Sync the manuals from the tool repositories + run: uv run scripts/sync_upstream_docs.py + + - name: Build + run: uv run mkdocs build --strict diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000..1ba5116 --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,52 @@ +name: Deploy site + +on: + push: + branches: [master] + # Pick up manual page changes in the tool repositories once a day. + schedule: + - cron: "17 4 * * *" + workflow_dispatch: + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: pages + cancel-in-progress: false + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + + - name: Install uv + uses: astral-sh/setup-uv@v10 + with: + enable-cache: true + + - name: Install dependencies + run: uv sync --frozen + + - name: Sync the manuals from the tool repositories + run: uv run scripts/sync_upstream_docs.py + + - name: Build + run: uv run mkdocs build --strict + + - uses: actions/upload-pages-artifact@v5 + with: + path: site + + deploy: + needs: build + runs-on: ubuntu-latest + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - id: deployment + uses: actions/deploy-pages@v5 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1943907 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +site/ +.venv/ +__pycache__/ +.cache/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..29c5630 --- /dev/null +++ b/README.md @@ -0,0 +1,46 @@ +# git-ftp.github.io + +The documentation site for both git-ftp implementations, +[git-ftp-py](https://github.com/git-ftp/git-ftp-py) (Python) and +[git-ftp](https://github.com/git-ftp/git-ftp) (Bash), published at +. + +Built with [Material for MkDocs](https://squidfunk.github.io/mkdocs-material/) +and deployed by GitHub Pages on every push to `master`. + +## Working on it + +```sh +uv sync +uv run mkdocs serve # http://127.0.0.1:8000 +uv run mkdocs build --strict +``` + +## Where the content lives + +Everything under `docs/` is written by hand, except the three pages in +`docs/reference/`. Those are generated from the manual pages in the tool +repositories: + +| Page | Source | +|---|---| +| `reference/manual-python.md` | `git-ftp-py:docs/git-ftp.1.md` | +| `reference/manual-bash.md` | `git-ftp:man/git-ftp.1.md` | +| `reference/compatibility.md` | `git-ftp-py:COMPATIBILITY.md` | + +```sh +uv run scripts/sync_upstream_docs.py # fetch from GitHub +uv run scripts/sync_upstream_docs.py --local # from ../git-ftp, ../git-ftp-py +uv run scripts/sync_upstream_docs.py --check # fail if they are outdated +``` + +The deploy workflow runs the sync before every build, and once a day on a +schedule, so a manual page fixed upstream appears here without a commit in this +repository. Fix those pages upstream, not here. + +## Deployment + +`.github/workflows/deploy.yml` builds the site and publishes it with +`actions/deploy-pages`. This needs **Settings → Pages → Source: GitHub +Actions** in the repository settings; no `gh-pages` branch is involved. +`.github/workflows/build.yml` builds pull requests without deploying. diff --git a/docs/about/contributing.md b/docs/about/contributing.md new file mode 100644 index 0000000..33f0e30 --- /dev/null +++ b/docs/about/contributing.md @@ -0,0 +1,96 @@ +--- +title: Contributing +--- + +# Contributing + +Both implementations are maintained on GitHub, and both take patches. + +
+ +- :fontawesome-brands-python: **[git-ftp-py][py]** + + --- + + The Python implementation. + + [Issues][py-issues] · [Pull requests][py-prs] · [Changelog][py-changelog] + +- :material-bash: **[git-ftp][sh]** + + --- + + The original Bash implementation. + + [Issues][sh-issues] · [Pull requests][sh-prs] · [Changelog][sh-changelog] + +
+ +## Reporting a bug + +Include the command you ran, the output of `git ftp push -vv` with credentials +removed, the version (`git ftp --version`) and the server software if you know +it. FTP servers differ wildly in what they accept, so naming yours often solves +the riddle straight away. + +## Working on the Python implementation + +```sh +git clone https://github.com/git-ftp/git-ftp-py.git +cd git-ftp-py +uv sync --all-groups +make lint typecheck test # ruff, mypy, pytest +make test-docker # against pure-ftpd containers, Linux only +``` + +The test suite starts real FTP, FTPS and SFTP servers in-process, so most +changes can be covered by a test without any server of your own. The manual +page source is `docs/git-ftp.1.md`. + +Anything that changes behaviour compared to the Bash original belongs in +`COMPATIBILITY.md` — that file is the contract between the two implementations +and is published [here](../reference/compatibility.md). + +## Working on the Bash implementation + +```sh +git clone https://github.com/git-ftp/git-ftp.git +cd git-ftp +make test +``` + +The core functionality is unit tested with +[shunit2](https://github.com/kward/shunit2); the tests are in `tests/`. The +manual page source is `man/git-ftp.1.md`. + +Add yourself to the [AUTHORS](https://github.com/git-ftp/git-ftp/blob/master/AUTHORS) +file with your first patch. + +## This website + +The site is built with [MkDocs](https://www.mkdocs.org/) and +[Material for MkDocs](https://squidfunk.github.io/mkdocs-material/) from the +[git-ftp.github.io](https://github.com/git-ftp/git-ftp.github.io) repository +and published by GitHub Pages on every push. + +```sh +git clone https://github.com/git-ftp/git-ftp.github.io.git +cd git-ftp.github.io +uv sync +uv run mkdocs serve +``` + +The three reference pages are generated from the tool repositories by +`scripts/sync_upstream_docs.py` and refreshed at build time — fix a manual page +upstream, not here. Everything else under `docs/` is written by hand; there is +an :material-pencil: link at the top of every page that takes you straight to +its source. + +[py]: https://github.com/git-ftp/git-ftp-py +[py-issues]: https://github.com/git-ftp/git-ftp-py/issues +[py-prs]: https://github.com/git-ftp/git-ftp-py/pulls +[py-changelog]: https://github.com/git-ftp/git-ftp-py/blob/main/CHANGELOG.md +[sh]: https://github.com/git-ftp/git-ftp +[sh-issues]: https://github.com/git-ftp/git-ftp/issues +[sh-prs]: https://github.com/git-ftp/git-ftp/pulls +[sh-changelog]: https://github.com/git-ftp/git-ftp/blob/master/CHANGELOG.md diff --git a/docs/about/license.md b/docs/about/license.md new file mode 100644 index 0000000..09fab81 --- /dev/null +++ b/docs/about/license.md @@ -0,0 +1,31 @@ +--- +title: License +--- + +# License + +Both implementations of git-ftp are free software, licensed under the +[GNU General Public License, Version 3.0](https://www.gnu.org/licenses/gpl-3.0-standalone.html) +or later. + +- [LICENSE — git-ftp (Python)](https://github.com/git-ftp/git-ftp-py/blob/main/LICENSE) +- [LICENSE — git-ftp (Bash)](https://github.com/git-ftp/git-ftp/blob/master/LICENSE) + +This means you may use, study, share and modify git-ftp, and that anything you +distribute based on it carries the same freedoms. + +## Authors + +git-ftp was started by René Moser and has since been maintained with +[Maikel Linke](https://github.com/mlinke-ai) and a long list of contributors on +GitHub. The Python port is written by René Moser and the git-ftp contributors. + +The [AUTHORS](https://github.com/git-ftp/git-ftp/blob/master/AUTHORS) file has +an (incomplete) list of everyone who helped. Add yourself when you send a +patch. + +## This documentation + +The contents of this site are part of the +[git-ftp.github.io](https://github.com/git-ftp/git-ftp.github.io) repository +and are covered by the same license. diff --git a/docs/getting-started/first-deployment.md b/docs/getting-started/first-deployment.md new file mode 100644 index 0000000..6f25fe7 --- /dev/null +++ b/docs/getting-started/first-deployment.md @@ -0,0 +1,102 @@ +--- +title: First deployment +--- + +# First deployment + +## 1. Tell git-ftp where the server is + +Run this once inside your repository: + +```sh +git config git-ftp.url "ftp://example.com/public_html" +git config git-ftp.user "alice" +git config git-ftp.password "s3cret" +``` + +The settings land in `.git/config`, which is not part of the repository, so the +password is not committed. If you would rather not store it at all, see +[credentials](../guide/protocols.md#credentials) for `-P`, `--password-command`, +`~/.netrc`, the macOS keychain and environment variables. + +## 2. Upload everything once + +If the server is empty, or its contents should be replaced by what is in Git: + +```sh +git ftp init +``` + +This uploads every tracked file that is not ignored and writes the commit id +into `.git-ftp.log` on the server. + +If the files are **already** on the server and match your working tree, do not +upload them again — just record the commit: + +```sh +git ftp catchup +``` + +!!! tip "Look before you leap" + + `--dry-run` prints what would be transferred and touches nothing: + + ```sh + git ftp init --dry-run + ``` + +## 3. Deploy your changes + +From now on, every deployment is one command: + +```sh +echo "new content" >> index.txt +git commit index.txt -m "Add new content" +git ftp push +``` + +```text +1 file to sync: +[1 of 1] Buffered for upload 'index.txt'. +Uploading ... +Last deployment changed from 1f2a3b4 to ded01b2. +``` + +git-ftp reads the commit recorded on the server, diffs it against `HEAD` and +transfers exactly the files that were added, changed or deleted. Only when +every upload succeeded does it update the log, so an interrupted deployment +never claims a commit it did not finish. + +## 4. Check what is deployed + +```sh +git ftp show # git show of the deployed commit +git ftp log # git log starting at the deployed commit +``` + +## Going back, and other branches + +Because the state on the server is just a commit, moving around in history is +ordinary Git work: + +```sh +# deploy the state of three commits ago +git checkout HEAD~3 +git ftp push + +# deploy another branch without checking it out +git ftp push -b staging + +# upload the difference between develop and master +git checkout develop +git ftp push --commit master +``` + +## Where to go next + +- [Configuration](../guide/configuration.md) — every setting, and where to put it. +- [Scopes](../guide/scopes.md) — staging and production in one repository. +- [Selecting files](../guide/selecting-files.md) — `.git-ftp-ignore`, + `.git-ftp-include`, `--syncroot`. +- [Continuous deployment](../guide/ci-cd.md) — deploy from GitHub Actions, + GitLab CI or Bitbucket Pipelines. diff --git a/docs/getting-started/implementations.md b/docs/getting-started/implementations.md new file mode 100644 index 0000000..5ca2c39 --- /dev/null +++ b/docs/getting-started/implementations.md @@ -0,0 +1,76 @@ +--- +title: Which implementation +--- + +# Which implementation + +git-ftp exists twice: as the original Bash script and as a native Python port. +Both deploy the same way and can take over from each other at any time. + +## Short answer + +Install the **Python** implementation unless you have a reason not to: + +```sh +pip install git-ftp +``` + +It transfers files in parallel, needs no `lftp` for downloading, verifies SFTP +host keys, keeps passwords out of the process list and fixes a long list of +[upstream bugs](../reference/compatibility.md#upstream-bugs-that-are-fixed). + +Use the **Bash** implementation when Python is inconvenient — a minimal +container, a shared host where only a shell script may be dropped into `bin`, +or a system whose distribution already packages `git-ftp` and where that is the +easiest thing to audit. + +## Side by side + +| | git-ftp (Python) | git-ftp (Bash) | +|---|---|---| +| Source | [git-ftp/git-ftp-py][py] | [git-ftp/git-ftp][sh] | +| Install | `pip install git-ftp` | distribution package or one script | +| Runtime | Python 3.10+, `git` | POSIX shell, `curl`, `git` | +| FTP, FTPS, FTPES | libcurl through pycurl | curl | +| SFTP | paramiko, host keys verified | curl, host key not checked | +| Transfers | parallel, `--jobs` (default 4) | one file after another | +| `download`, `pull`, `snapshot` | built in | needs `lftp` installed | +| Passwords | passed to libcurl in memory | may appear in the process list | +| Encrypted SSH keys | `--key-passphrase`, prompt, `ssh-agent` | not supported | +| Progress | spinner with a done/total count on a terminal | plain lines | +| `--worktree` | yes — edits during upload cannot leak in | no | +| Shell completion | bash, zsh, fish | no | + +## What they share + +The port's rule is that everything on the wire and in configuration stays +identical: + +- the deployed-commit file `.git-ftp.log`, at the same place with the same + content; +- the `git-ftp.*` configuration keys, the scope keys and `.git-ftp-config`, + with the same precedence; +- `.git-ftp-ignore` and `.git-ftp-include`, with the same glob and + `target:source` semantics; +- the `pre-ftp-push` and `post-ftp-push` hooks, with the same arguments and + stdin format; +- the exit codes and the progress messages. + +So you can deploy with the Bash script today and with the Python program +tomorrow, from another machine, without touching the server. + +The full list of what is identical, what was fixed and what deliberately +differs is on the [compatibility page](../reference/compatibility.md). + +## Switching + +There is nothing to migrate. Install the other implementation and run +`git ftp push` — it reads the same configuration and the same remote log. If +you want to be sure first: + +```sh +git ftp push --dry-run +``` + +[py]: https://github.com/git-ftp/git-ftp-py +[sh]: https://github.com/git-ftp/git-ftp diff --git a/docs/getting-started/index.md b/docs/getting-started/index.md new file mode 100644 index 0000000..3a2be4e --- /dev/null +++ b/docs/getting-started/index.md @@ -0,0 +1,41 @@ +--- +title: Getting started +--- + +# Getting started + +Three short pages get you from nothing to a deployed site: + +
+ +- :material-download: **[Install](install.md)** + + --- + + Pick an implementation and get `git ftp` onto your `PATH`. + +- :material-rocket-launch-outline: **[First deployment](first-deployment.md)** + + --- + + Configure a server, upload everything once, then push only what changed. + +- :material-scale-balance: **[Which implementation](implementations.md)** + + --- + + What the Python port and the Bash original do differently. + +
+ +Everything after that is in the [guide](../guide/configuration.md), and every +option is listed in the [manuals](../reference/manual-python.md). + +## What you need + +- A Git repository whose files you want on a server. +- An account on that server, reachable over FTP, FTPS, FTPES or SFTP. +- Write permission in the directory you deploy into — git-ftp keeps its log + file (`.git-ftp.log`) there. + +Nothing is installed on the server, and the server needs no Git. diff --git a/docs/getting-started/install.md b/docs/getting-started/install.md new file mode 100644 index 0000000..7192751 --- /dev/null +++ b/docs/getting-started/install.md @@ -0,0 +1,179 @@ +--- +title: Install +--- + +# Install + +=== "Python (recommended)" + + The Python implementation is published on + [PyPI](https://pypi.org/project/git-ftp/) and needs Python 3.10 or newer. + + ```sh + pip install git-ftp + ``` + + Prefer an isolated installation, so the tool does not share a virtual + environment with anything else: + + ```sh + pipx install git-ftp # or + uv tool install git-ftp + ``` + + Check it: + + ```sh + git ftp --version + ``` + + Git runs the `git-ftp` program as `git ftp` as soon as it is on your + `PATH`. libcurl comes bundled with the `pycurl` wheel and SFTP is spoken by + `paramiko`, so the only other thing you need is `git` itself. + + Upgrade with `pip install --upgrade git-ftp` (`pipx upgrade git-ftp`, + `uv tool upgrade git-ftp`). + +=== "Bash" + + The original is a single shell script that needs `git` and `curl`. + + **Debian, Ubuntu and friends** + + ```sh + sudo apt-get install git-ftp + ``` + + The distribution package can lag behind. For the newest release maintained + by the project, add the PPA: + + ```sh + sudo -s + add-apt-repository ppa:git-ftp/ppa + + # On Debian, point the PPA at the precise series + source /etc/*-release + if [ "$ID" = "debian" ]; then + dist="$(echo /etc/apt/sources.list.d/git-ftp-ppa-*.list | sed 's/^.*ppa-\(.*\)\.list$/\1/')" + sed -i.backup "s/$dist/precise/g" /etc/apt/sources.list.d/git-ftp-ppa-*.list + fi + + apt-get update + apt-get install git-ftp + ``` + + **macOS** + + ```sh + brew install git git-ftp + ``` + + **Arch Linux** + + Unofficial packages are in the + [AUR](https://aur.archlinux.org/packages?K=git-ftp). + + **From source, any Unix** + + ```sh + git clone https://github.com/git-ftp/git-ftp.git + cd git-ftp + + # check out the newest release tag + git checkout "$(git tag | grep '^[0-9]*\.[0-9]*\.[0-9]*$' | tail -1)" + sudo make install + ``` + + To update, `git fetch`, check out the new tag and run `sudo make install` + again. + + **The script alone** + + ```sh + curl https://raw.githubusercontent.com/git-ftp/git-ftp/master/git-ftp > /bin/git-ftp + chmod 755 /bin/git-ftp + ``` + + This installs the current development state. Replace `master` with a + version tag to pin a release. Uninstall by deleting the file. + +## Windows + +=== "Python" + + Install Python from [python.org](https://www.python.org/downloads/) or the + Microsoft Store, then: + + ```powershell + pip install git-ftp + git ftp --version + ``` + + All four protocols work out of the box; there is no separate curl to + install. + + + ## Shell completion + + The Python implementation ships completions: + + ```sh + eval "$(_GIT_FTP_COMPLETE=bash_source git-ftp)" # zsh: zsh_source, fish: fish_source + ``` + + Add the line to your shell's startup file to keep it. + +=== "Bash" + + Install [Git for Windows](https://gitforwindows.org/), open Git Bash as + administrator and run: + + ```bash + curl https://raw.githubusercontent.com/git-ftp/git-ftp/master/git-ftp > /bin/git-ftp + chmod 755 /bin/git-ftp + ``` + + `/bin` here is an alias, by default `C:\Program Files\Git\usr\bin`. + + For SFTP you need a [curl build](https://curl.se/windows/) with SFTP + support: install it and remove `bin/curl.exe` from the Git for Windows + directory so the new one is found. + + Cygwin works too — install its `curl` package and use the same two + commands. + + ??? warning "Git for Windows and Cygwin side by side" + + If both are installed you may see an error about a path starting with + `/cygdrive/`: + + creating `/cygdrive/c/TEMP/git-ftp-m7GH/delete_tmp': No such file or directory + + git-ftp then mixes commands from both installations, which disagree on + path prefixes. Edit `\etc\fstab` and change + + none /cygdrive/ cygdrive binary,posix=0,user 0 0 + + to + + none / cygdrive binary,posix=0,user 0 0 + + Close all consoles and try again. + + ## SFTP on macOS with the Bash version + + The curl that ships with macOS has no SFTP support and reports + `Protocol sftp not supported or disabled in libcurl`. Either use the + [Python implementation](implementations.md), which speaks SFTP through paramiko, or + build curl yourself: + + ```sh + # in the unpacked curl source from https://curl.se/download.html + brew install openssl libssh2 + ./configure -q --with-libssh2 --with-ssl=/usr/local/opt/openssl + make + make install + ``` + + `curl --version` should then list `sftp` among the protocols. If the system + curl still wins, put `/usr/local/bin` first in your `PATH`. diff --git a/docs/guide/ci-cd.md b/docs/guide/ci-cd.md new file mode 100644 index 0000000..2be1fe2 --- /dev/null +++ b/docs/guide/ci-cd.md @@ -0,0 +1,121 @@ +--- +title: Continuous deployment +--- + +# Continuous deployment + +git-ftp needs nothing but Git and a network connection, which makes it a +comfortable deploy step in any CI system. + +!!! warning "Fetch the whole history" + + Most CI systems clone with `--depth 1`. git-ftp has to diff against the + commit recorded on the server, and that commit is usually not in a shallow + clone — the deployment then uploads everything, or fails. Configure a full + clone, as shown below. + +Keep the credentials in the CI system's secret store and pass them through the +environment; never commit them. + +## GitHub Actions + +```yaml title=".github/workflows/deploy.yml" +name: Deploy + +on: + push: + branches: [main] + +concurrency: deploy # one deployment at a time + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + with: + fetch-depth: 0 # git-ftp needs the history + + - uses: actions/setup-python@v7 + with: + python-version: "3.x" + + - run: pip install git-ftp + + - name: Deploy + env: + GIT_FTP_URL: ftp://example.com/public_html + GIT_FTP_USER: ${{ secrets.FTP_USER }} + GIT_FTP_PASSWORD: ${{ secrets.FTP_PASSWORD }} + run: git ftp push --auto-init +``` + +`--auto-init` uploads everything the first time, so the very first run works +without a manual `git ftp init`. + +There is also a ready-made action built on the Bash implementation: +[FTP Deploy](https://github.com/marketplace/actions/ftp-deploy). + +## GitLab CI + +```yaml title=".gitlab-ci.yml" +deploy: + stage: deploy + image: python:3-slim + variables: + GIT_DEPTH: 0 # git-ftp needs the history + only: + - main + before_script: + - apt-get update && apt-get install -y --no-install-recommends git + - pip install git-ftp + script: + - git ftp push --auto-init +``` + +Set `GIT_FTP_URL`, `GIT_FTP_USER` and `GIT_FTP_PASSWORD` as masked, protected +CI/CD variables in the project settings. + +## Bitbucket Pipelines + +```yaml title="bitbucket-pipelines.yml" +image: python:3-slim + +clone: + depth: full # git-ftp needs the history + +pipelines: + branches: + main: + - step: + name: Deploy + deployment: production + script: + - apt-get update && apt-get install -y --no-install-recommends git + - pip install git-ftp + - git ftp push --auto-init +``` + +There is a [video walkthrough](https://www.youtube.com/watch?v=8HZhHtZebdw) of +the same setup with the Bash implementation. + +## Deploying per environment + +If scopes are named after branches, one step covers all of them: + +```sh +git ftp push -s # scope = current branch name +``` + +See [scopes](scopes.md). + +## Things worth adding + +- **Serialise deployments.** Two jobs deploying at once corrupt each other's + state. Use the CI system's concurrency control, and `--lock` as a second net. +- **Build first.** Compile assets in an earlier step and list the results in + [`.git-ftp-include`](selecting-files.md#uploading-untracked-files). +- **Look before pushing.** A `git ftp push --dry-run` step on pull requests + shows what a merge would deploy. +- **Fail loudly.** git-ftp's [exit codes](../reference/manual-python.md#exit-codes) + distinguish a login failure (4) from a locked remote (7) and a hook veto (9). diff --git a/docs/guide/configuration.md b/docs/guide/configuration.md new file mode 100644 index 0000000..b14909a --- /dev/null +++ b/docs/guide/configuration.md @@ -0,0 +1,143 @@ +--- +title: Configuration +--- + +# Configuration + +Every option can be stored, so that a deployment is just `git ftp push`. + +## Where settings come from + +git-ftp reads a setting from the first source that has it: + +1. the command line; +2. the environment — `GIT_FTP_URL`, `GIT_FTP_USER`, `GIT_FTP_PASSWORD` + (Python implementation only); +3. `.git-ftp-config` in the repository root, if present; +4. the Git configuration, scope keys (`git-ftp..`) before plain + keys (`git-ftp.`). + +`.git/config` is local to your clone, so credentials put there are not +committed. `.git-ftp-config` *is* part of the repository — keep passwords out +of it. + +## Setting defaults + +```sh +git config git-ftp.url "ftp://example.com/public_html" +git config git-ftp.user "alice" +git config git-ftp.password "s3cret" +``` + +After that: + +```sh +git ftp push +``` + +## The keys + +| Key | Meaning | +|---|---| +| `git-ftp.url` | Server URL, for example `ftp://example.com/public_html` | +| `git-ftp.user` | Login name | +| `git-ftp.password` | Password | +| `git-ftp.password-command` | Command whose first output line is the password :material-language-python: | +| `git-ftp.keychain` | macOS keychain entry, `account@host` | +| `git-ftp.syncroot` | Deploy only this directory, as if it were the repository root | +| `git-ftp.remote-root` | Remote directory to deploy into, replacing the URL's path | +| `git-ftp.deployedsha1file` | Name of the log file on the server (default `.git-ftp.log`) | +| `git-ftp.branch` | Branch to deploy | +| `git-ftp.insecure` | Do not verify TLS certificates or SFTP host keys | +| `git-ftp.cacert` | CA certificate bundle for FTPS/FTPES | +| `git-ftp.key` / `git-ftp.pubkey` | SFTP private and public key file | +| `git-ftp.key-passphrase` | Passphrase of an encrypted SFTP key :material-language-python: | +| `git-ftp.disable-epsv` | Use PASV instead of EPSV | +| `git-ftp.proxy` | Proxy URL | +| `git-ftp.jobs` | Number of parallel connections, default 4 :material-language-python: | +| `git-ftp.worktree` | Upload from a throwaway worktree :material-language-python: | +| `git-ftp.no-commit` | `pull` merges without committing | + +:material-language-python: — Python implementation only. + +Booleans are read the way `git config` reads them: `true`, `yes`, `on`, `1` +against `false`, `no`, `off`, `0`. + +## A config file in the repository + +Instead of `git config`, the same keys can live in `.git-ftp-config` in the +repository root, which takes precedence over the Git configuration: + +```ini +[git-ftp] + url = ftp://example.com/public_html + user = alice + syncroot = public + jobs = 8 +``` + +This is the place for settings that belong to the project rather than to your +machine. Credentials belong in `.git/config`, in the environment or in a +password manager — see [credentials](protocols.md#credentials). + +## Environment variables + +The Python implementation reads `GIT_FTP_URL`, `GIT_FTP_USER` and +`GIT_FTP_PASSWORD` after the command line and before the Git configuration, +which is what you want in CI: + +```sh +GIT_FTP_URL="ftp://example.com/htdocs" \ +GIT_FTP_USER="$FTP_USER" \ +GIT_FTP_PASSWORD="$FTP_PASSWORD" \ +git ftp push +``` + +Nothing is written to a file and no password appears on a command line. + +## The log file on the server + +git-ftp stores the deployed commit id in `.git-ftp.log` in the deployment +directory. Some hardened FTP servers refuse to write names starting with a dot; +give the file another name then: + +```sh +git config git-ftp.deployedsha1file gitftp.log +``` + +Both implementations use the same file, so changing the name affects any +machine deploying this repository. + +## Consistent uploads while editing + +git-ftp reads the files it uploads from the working tree. If you edit them +while a long deployment runs, the server can end up with contents that do not +match the commit. + +The Python implementation can read them from a throwaway Git worktree checked +out at the commit being deployed instead: + +```sh +git ftp push --worktree +git config git-ftp.worktree true # or always +``` + +The worktree shares the object store, so only a working copy is written to +disk, and it is removed when the deployment finishes. Untracked files added +through `.git-ftp-include` are not part of the commit and are still read from +the live working tree. + +## Parallel transfers + +The Python implementation uses four connections at once. Change that per run or +per repository: + +```sh +git ftp push --jobs 8 +git config git-ftp.jobs 1 # sequential, like the Bash version +``` + +Uploads run first, then deletes, and the log is written last, only when every +upload succeeded. The first failed upload stops the deployment with exit +code 4 and leaves the log untouched; a failed delete is a warning. Ctrl-C stops +promptly and exits with 130. diff --git a/docs/guide/download.md b/docs/guide/download.md new file mode 100644 index 0000000..72dbb83 --- /dev/null +++ b/docs/guide/download.md @@ -0,0 +1,78 @@ +--- +title: Downloading from the server +--- + +# Downloading from the server + +Sometimes the server is ahead: someone edited a file over FTP, or a CMS wrote +something. Three actions bring that back into your repository. + +!!! warning "These actions change your working tree" + + They delete local files that are not on the server, except ignored files, + `.git` and git-ftp's own files. Commit or stash your work first, and make + sure `.git-ftp-ignore` covers what must stay. + + In the Bash implementation these actions are experimental and need + [`lftp`](https://lftp.yar.ru/) installed. The Python implementation does it + itself. + +## download + +```sh +git ftp download +``` + +Mirrors the remote directory into the working tree and stops there, so you can +inspect the result with `git diff`. It refuses to run while the working tree +has untracked files. + +## pull + +`git ftp download` compares against whatever you have checked out, which is the +wrong baseline if you have local commits that were never deployed. `pull` uses +the deployed commit instead: + +```sh +git ftp pull +``` + +It does the equivalent of: + +```sh +git checkout +git ftp download +git add --all +git commit -m '[git-ftp] remotely untracked modifications' +git ftp catchup +git checkout +git merge +``` + +To look at the merge before it is committed: + +```sh +git ftp pull --no-commit +# inspect, then +git commit +# or +git merge --abort +``` + +After an abort the downloaded changes live on in an unreferenced commit until +Git's garbage collector runs. The commit id is printed, so you can tag it or +branch from it. + +`--changed-only` limits the transfer to files that also changed locally. + +## snapshot + +To turn a server that has never seen git-ftp into a repository: + +```sh +git ftp snapshot ftp://example.com/public_html projects/example +``` + +Downloads the remote directory into a new repository, commits it and records +that commit on the server, so the next `git ftp push` behaves as if you had run +`init`. diff --git a/docs/guide/hooks-and-locking.md b/docs/guide/hooks-and-locking.md new file mode 100644 index 0000000..13f7758 --- /dev/null +++ b/docs/guide/hooks-and-locking.md @@ -0,0 +1,91 @@ +--- +title: Hooks and locking +--- + +# Hooks and locking + +## Hooks + +git-ftp runs two client-side hooks during `init` and `push`, from the +repository's hooks directory (`.git/hooks`, or `core.hooksPath` if set). + +`pre-ftp-push` +: Runs after the list of changed files has been built, just before the first + upload. A non-zero exit aborts the deployment with exit code 9. Skipped + with `--no-verify`. + +`post-ftp-push` +: Runs after the transfer finished. Its exit status is ignored unless + `--enable-post-errors` is given. The Python implementation can skip it with + `--no-post-hooks`. + +Both receive four arguments: + +| | | +|---|---| +| `$1` | the scope name, or the host if no scope is used | +| `$2` | the URL being deployed to | +| `$3` | the local commit being uploaded | +| `$4` | the commit currently recorded on the server | + +`pre-ftp-push` additionally gets the change list on standard input: `A ` or +`D ` followed by the path, entries separated by NUL bytes. `A` means the file +is scheduled for upload, `D` for deletion. This list is not the same as +`git diff` — `.git-ftp-include` and `.git-ftp-ignore` have already been applied +to it, and it contains the syncroot if one is set. + +### An example + +`.git/hooks/pre-ftp-push`, executable, refusing to deploy a file that still +contains `TODO`: + +```bash +#!/bin/bash +# +# $1 -- scope name or host $3 -- local commit +# $2 -- URL $4 -- commit on the server +# +# stdin: NUL separated " " entries + +while read -r -d '' status file +do + if [ "$status" = "A" ]; then + if grep -q 'TODO' "$file"; then + echo "TODO found in $file, not uploading." + exit 1 + fi + fi +done + +exit 0 +``` + +Typical uses: building assets, clearing a cache through an HTTP call, posting a +deployment notice to chat, or refusing to deploy anything but a release tag. + +!!! note "Experimental in the Bash implementation" + + The interface may still change there. The Python implementation runs the + same hooks with the same arguments. + +## Locking + +Two deployments running at once would fight over the same files. `--lock` +prevents that: + +```sh +git ftp push --lock +``` + +Before the transfer, git-ftp writes `git-ftp.lck` on the server containing the +commit id and the user. While it exists, a deployment of a *different* commit +refuses to run and exits with code 7. A deployment of the same commit is +treated as your own and proceeds. The file is removed when the deployment +finishes. + +If a deployment was interrupted and left the lock behind: + +```sh +git ftp unlock # Python implementation +git ftp push --force # ignore the lock and deploy anyway +``` diff --git a/docs/guide/protocols.md b/docs/guide/protocols.md new file mode 100644 index 0000000..b0836b4 --- /dev/null +++ b/docs/guide/protocols.md @@ -0,0 +1,168 @@ +--- +title: Protocols and credentials +--- + +# Protocols and credentials + +## The URL + +```text +protocol://host.domain.tld:port/path +``` + +```sh +git ftp push ftp://host.example.com:2121/mypath +``` + +| URL | Transport | Encryption | +|---|---|---| +| `ftp://host/path` | curl / libcurl | none — the password crosses the network in clear | +| `ftpes://host/path` | curl / libcurl | explicit TLS (`AUTH TLS`) | +| `ftps://host/path` | curl / libcurl | implicit TLS, usually port 990 | +| `sftp://host/path` | curl (Bash) / paramiko (Python) | SSH | + +`ftp://` is the default when no protocol is given, and `host:port/path` without +a scheme is understood as FTP. + +Credentials may be part of the URL — `ftp://alice:s3cret@example.com/htdocs` — +which is how `add-scope` stores them. The Python implementation never prints +them back or passes them on a command line. + +!!! tip "Prefer an encrypted protocol" + + Plain FTP sends the password and every file unencrypted. If the server + offers FTPES or SFTP, use it. + +## SFTP paths + +Relative and absolute paths are not the same thing over SSH: + +```sh +sftp://example.com/public_html # relative to the login directory +sftp://example.com/~/public_html # relative to the home directory +sftp://example.com//var/www # absolute +``` + +## SFTP keys and host keys + +```sh +git ftp push -u alice --key ~/.ssh/id_ed25519 sftp://example.com/~/public_html +``` + +The public key is guessed as `.pub`; `--pubkey` sets it explicitly. The +Python implementation also takes `--key-passphrase` for an encrypted key, asks +for the passphrase interactively, and uses a running `ssh-agent` +(`SSH_AUTH_SOCK`) if there is one. + +It verifies the server's host key against `~/.ssh/known_hosts` and +`/etc/ssh/ssh_known_hosts`. Add an unknown server first: + +```sh +ssh-keyscan -p 22 example.com >> ~/.ssh/known_hosts +``` + +The Bash implementation accepts any host key silently. + +## TLS certificates + +Certificates are verified. For a private CA: + +```sh +git ftp push --cacert /etc/ssl/certs/ca.pem +``` + +`--insecure` turns verification off — for TLS certificates and, in the Python +implementation, for SFTP host keys as well. It leaves you open to a +man-in-the-middle, but it is still better than plain FTP. + +## Passive, active, EPSV + +FTP data connections default to EPSV. Depending on the network that can fail: + +```sh +git ftp push --disable-epsv # use PASV +git ftp push --active # the original active mode +``` + +Neither applies to SFTP. A proxy is passed through with `-x`: + +```sh +git ftp push -x http://proxy.example.com:3128 +``` + +## Credentials + +There are more ways to hand over a password than putting it into the config. + +### Ask interactively + +```sh +git ftp push -u alice -P ftp://example.com/htdocs +``` + +### From a password manager + +The Python implementation runs a command and uses the first line of its output: + +```sh +git config git-ftp.password-command "pass show example.com/ftp" +``` + +### From the environment + +```sh +GIT_FTP_USER=alice GIT_FTP_PASSWORD="$SECRET" git ftp push +``` + +### From `~/.netrc` + +Both implementations fall back to `~/.netrc` when no user is given: + +```text +machine ftp.example.com +login alice +password SECRET +``` + +```sh +git ftp init ftp.example.com +``` + +### From the macOS keychain + +```sh +git ftp init --keychain account@host ftpes://host +git config git-ftp.keychain alice@example.com +``` + +Without a value, the account and host are guessed from the user and the URL. +Add an entry like this — the entry has to be an *internet* password, not the +generic one the Keychain Access app creates: + +```sh +security add-internet-password -a alice -r "ftp " -s example.com -w secr3t +``` + +`-r` is the protocol and must be exactly four characters: `"ftp "` for FTP and +for SSH with password authentication, `ftps` for FTPS and FTPES. The keychain +cannot unlock a password-protected SSH key. + +### Passwords with special characters + +Quote them, in single quotes: + +```sh +git ftp push --passwd '#my$fancy!secret' +git config git-ftp.password '#my$fancy!secret' +``` + +A password starting with a dash defeats quoting in the Bash implementation +([by design](https://github.com/git-ftp/git-ftp/issues/468)); use `git config`, +`-P` or `~/.netrc` instead. The Python implementation accepts it as +`-p=-secret`. + +!!! danger "Passwords on the command line" + + Anything in `argv` is visible to other users through `ps`. The Bash + implementation also hands the password to curl that way. Prefer `-P`, + `--password-command`, the environment or `~/.netrc` on shared machines. diff --git a/docs/guide/scopes.md b/docs/guide/scopes.md new file mode 100644 index 0000000..7f93466 --- /dev/null +++ b/docs/guide/scopes.md @@ -0,0 +1,71 @@ +--- +title: Scopes +--- + +# Scopes + +A scope is a named set of settings in the same repository — one for staging, +one for production, one for a customer's server. + +## Creating a scope + +The `add-scope` action takes everything from a URL: + +```sh +git ftp add-scope production ftp://alice:s3cret@live.example.com/htdocs +git ftp add-scope staging ftp://alice:s3cret@staging.example.com/htdocs +``` + +Or set the keys yourself: + +```sh +git config git-ftp.production.url "ftp://live.example.com/htdocs" +git config git-ftp.production.user "manager" +git config git-ftp.production.password "n0tThatSimp3l" +``` + +## Deploying to a scope + +```sh +git ftp push -s production +git ftp init -s staging +``` + +Keys the scope does not define fall back to the plain `git-ftp.*` keys, so +shared settings are written once: + +```sh +git config git-ftp.user alice # used by every scope +git config git-ftp.staging.url ftp://staging.example.com/htdocs +``` + +An explicitly empty scope value masks the plain value: + +```sh +git config git-ftp.staging.syncroot "" # no syncroot for staging +``` + +## The branch as the scope + +If a scope is named after a branch, `-s` without a value uses the current +branch's name: + +```sh +git checkout production +git ftp push -s # pushes to the "production" scope +``` + +That makes a deploy step in CI identical for every environment. + +## Removing a scope + +```sh +git ftp remove-scope staging +``` + +!!! warning "Scopes hold credentials" + + `add-scope` writes the password from the URL into `.git/config`. That file + is not committed, but it is readable by anyone with access to your + checkout. On shared machines and in CI, prefer + [other ways of passing a password](protocols.md#credentials). diff --git a/docs/guide/selecting-files.md b/docs/guide/selecting-files.md new file mode 100644 index 0000000..c0fb4ee --- /dev/null +++ b/docs/guide/selecting-files.md @@ -0,0 +1,131 @@ +--- +title: Selecting files +--- + +# Selecting files + +By default git-ftp uploads every file Git tracks. Three mechanisms change that: +`.git-ftp-ignore` removes files, `.git-ftp-include` adds untracked ones and +`--syncroot` deploys a subdirectory. + +## Ignoring files + +Put shell glob patterns into `.git-ftp-ignore` in the repository root, one per +line. Matching files are never uploaded and never deleted on the server. + +```text +config/* +*.txt +foobar.txt +.gitignore +*/.gitkeep +.git-ftp-ignore +.git-ftp-include +.gitlab-ci.yml +``` + +Two details differ from `.gitignore`: + +- `*` also matches `/`, so `config/*` covers everything below `config/`; +- a pattern has to match the **whole** path, not a part of it. To catch a file + in any directory, write `*/.gitkeep`, not `.gitkeep`. + +Paths are the Git paths, including the syncroot if one is set. + +!!! note "Since version 1.1.0" + + Older Bash versions interpreted the patterns as regular expressions. + Everything current treats them as globs. + +## Uploading untracked files + +Build output is often not in Git but still has to reach the server. +`.git-ftp-include` lists those files. + +**Always upload a file** — prefix it with `!`: + +```text +!VERSION.txt +``` + +**Upload a file when a tracked file changed** — `target:source`: + +```text +css/style.css:scss/style.scss +``` + +Whenever `scss/style.scss` changed since the deployed commit, `css/style.css` +is uploaded. List several sources for the same target if more than one can +trigger it: + +```text +css/style.css:scss/style.scss +css/style.css:scss/mixins.scss +``` + +**Upload a whole directory** — a target ending in `/`: + +```text +vendor/:composer.lock +``` + +Everything below `vendor/` is uploaded whenever `composer.lock` changed. Note +that this uploads all of it, including files already on the server, and never +deletes anything from that directory. + +If the target is missing locally, a change to its source deletes it on the +server. + +Paths are relative to the Git working directory. With `--syncroot`, the +**source** (right of the colon) is relative to the syncroot: + +```text +# syncroot "html": upload html/style.css when html/style.scss changed +html/style.css:style.scss +``` + +Prefix the source with `/` to point outside the syncroot, relative to the +repository root: + +```text +# syncroot "dist": upload dist/style.css when src/style.scss changed +dist/style.css:/src/style.scss +``` + +Include rules are applied first, ignore patterns afterwards — an ignored file +stays ignored even if an include rule names it. + +## Deploying a subdirectory + +If only one directory belongs on the server, and it should become the server's +root: + +```sh +git ftp push --syncroot public +git config git-ftp.syncroot public # or once and for all +``` + +`public/index.html` then lands as `index.html` on the server. + +`--remote-root` is the counterpart on the other side: it sets the directory to +deploy **into**, replacing the path of the URL. + +```sh +git ftp push --remote-root htdocs +``` + +## Checking what will happen + +`--dry-run` prints the plan and transfers nothing: + +```sh +git ftp push --dry-run +``` + +Other useful selections: + +```sh +git ftp push -a # upload everything, not only the changes +git ftp push -c 1f2a3b4 # treat this commit as the deployed one +git ftp push -b staging # deploy another branch +``` diff --git a/docs/guide/troubleshooting.md b/docs/guide/troubleshooting.md new file mode 100644 index 0000000..6327a48 --- /dev/null +++ b/docs/guide/troubleshooting.md @@ -0,0 +1,144 @@ +--- +title: Troubleshooting +--- + +# Troubleshooting + +Start by asking git-ftp what it is doing: + +```sh +git ftp push -v # what happens, step by step +git ftp push -vv # everything, including the protocol trace +git ftp push --dry-run +``` + +## It wants me to run `git ftp init` + +```text +Could not get last commit. Network down? Wrong URL? Use 'git ftp init' for the initial push. +``` + +git-ftp could not read `.git-ftp.log` from the server. Either it really is not +there — then `git ftp init` (or `git ftp catchup`, if the files are already +uploaded) is right — or the server could not be reached at all. The Python +implementation distinguishes the two and tells you which one it is; with the +Bash implementation, check the URL, the credentials and the path first. + +If the server forbids files starting with a dot, give the log another name: + +```sh +git config git-ftp.deployedsha1file gitftp.log +``` + +## The connection hangs or times out + +Usually the FTP data connection, not the login. Try, in this order: + +```sh +git ftp push --disable-epsv # PASV instead of EPSV +git ftp push --active # active mode; needs an FTP-aware firewall +``` + +SFTP has none of these problems, if the server offers it. + +## Certificate or host key errors + +For FTPS and FTPES with a private or self-signed certificate: + +```sh +git ftp push --cacert /path/to/ca.pem +``` + +For SFTP with the Python implementation, an unknown host key is refused. Add +the server once: + +```sh +ssh-keyscan -p 22 example.com >> ~/.ssh/known_hosts +``` + +`--insecure` skips both checks. It is a last resort, not a fix. + +## `Protocol sftp not supported or disabled in libcurl` + +The curl in use has no SFTP support — the usual case on macOS with the Bash +implementation. Use the Python implementation, which speaks SFTP through +paramiko, or [build curl with libssh2](../getting-started/install.md#sftp-on-macos-with-the-bash-version). + +## Everything gets uploaded every time + +The commit recorded on the server is not in your clone, so git-ftp cannot +compute a difference. In CI this is almost always a shallow clone — see +[continuous deployment](ci-cd.md). Locally it happens after a force-push or a +rebase that dropped the deployed commit; `git ftp catchup` on the right commit +sets the state straight. + +## Files appear with mangled names + +Names containing `%`, `?`, `[`, `]`, `"` or a backslash were mangled by the +Bash implementation on the way into curl. The Python implementation transfers +them intact. + +## The password is not accepted + +- Quote it in single quotes: `--passwd '#my$fancy!secret'`. +- A password starting with a dash cannot be passed with `--passwd` in the Bash + implementation; use `git config`, `-P` or `~/.netrc`. +- `-P` asks interactively and avoids all quoting questions. +- Check that no scope is overriding the password: `git config --get-regexp '^git-ftp\.'`. + +## A deployment was interrupted and now everything is locked + +```text +Fatal: Repository is locked by +``` + +```sh +git ftp unlock # Python implementation +git ftp push --force # ignore the lock +``` + +The lock file is `git-ftp.lck` in the deployment directory; deleting it by hand +works too. + +## The uploaded files do not match the commit + +Something edited the working tree while the upload ran. Deploy from a +consistent snapshot instead: + +```sh +git ftp push --worktree # Python implementation +``` + +## Large files are uploaded as text stubs + +With Git LFS, git-ftp uploads what Git tracks, which is the pointer file. Fetch +the real contents first: + +```sh +git lfs pull +git ftp push +``` + +## Exit codes + +| Code | Meaning | +|---|---| +| 0 | success | +| 1 | unexpected error | +| 2 | wrong usage | +| 3 | missing argument | +| 4 | error while uploading (also: remote unreachable, login failed) | +| 5 | error while downloading (also: `push` before `init`) | +| 6 | unknown protocol | +| 7 | remote locked | +| 8 | Git error — not a repository, dirty working tree, bad branch | +| 9 | hook failed | +| 10 | local filesystem error | +| 130 | interrupted | + +## Still stuck? + +Open an issue with the output of `git ftp push -vv`, with credentials removed: + +- [git-ftp (Python) issues](https://github.com/git-ftp/git-ftp-py/issues) +- [git-ftp (Bash) issues](https://github.com/git-ftp/git-ftp/issues) diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..33e1049 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,141 @@ +--- +title: Upload to FTP servers the Git way +--- + +# Git-ftp + +**Upload to FTP servers the Git way.** + +If you use Git and you need to get your files onto an FTP server, Git-ftp saves +you time and bandwidth by uploading only the files that changed since the last +upload. + +It records the deployed commit in a log file on the server and uses Git to work +out which local files differ from it. Nothing has to be installed on the server, +and you can just as well deploy another branch or go back in history to upload +an older version. + +
+ +- :material-download: **Install** + + --- + + `pip install git-ftp`, a package from your distribution, or a single shell + script. + + [:octicons-arrow-right-24: Installation](getting-started/install.md) + +- :material-rocket-launch-outline: **First deployment** + + --- + + From an empty server to `git ftp push` in five commands. + + [:octicons-arrow-right-24: Get started](getting-started/first-deployment.md) + +- :material-cog-outline: **Configuration** + + --- + + Store the URL, the user and the password once, then deploy with a single + command. + + [:octicons-arrow-right-24: Configuration](guide/configuration.md) + +- :material-book-open-variant: **Manual** + + --- + + Every action, option and configuration key of both implementations. + + [:octicons-arrow-right-24: Reference](reference/manual-python.md) + +
+ +## In a nutshell + +=== "Python" + + ```sh + # Install + pip install git-ftp + + # Setup + git config git-ftp.url "ftp://ftp.example.net:21/public_html" + git config git-ftp.user "ftp-user" + git config git-ftp.password "secr3t" + + # Upload all files + git ftp init + + # Or, if the files are already on the server + git ftp catchup + + # Work and deploy + echo "new content" >> index.txt + git commit index.txt -m "Add new content" + git ftp push + # 1 file to sync: + # [1 of 1] Buffered for upload 'index.txt'. + # Uploading ... + # Last deployment changed from 1f2a3b4 to ded01b27e5c785fb251150805308d3d0f8117387. + ``` + +=== "Bash" + + ```sh + # Setup + git config git-ftp.url "ftp://ftp.example.net:21/public_html" + git config git-ftp.user "ftp-user" + git config git-ftp.password "secr3t" + + # Upload all files + git ftp init + + # Or, if the files are already on the server + git ftp catchup + + # Work and deploy + echo "new content" >> index.txt + git commit index.txt -m "Add new content" + git ftp push + # 1 file to sync: + # [1 of 1] Buffered for upload 'index.txt'. + # Uploading ... + # Last deployment changed to ded01b27e5c785fb251150805308d3d0f8117387. + ``` + +If something goes wrong, add `-v` or `-vv` to see what happens on the wire. + +## Two implementations, one deployment + +There are two git-ftp programs, and this site documents both. + +| | [git-ftp][py] (Python) | [git-ftp][sh] (Bash) | +|---|---|---| +| Install | `pip install git-ftp` | package manager, or one shell script | +| Needs | Python 3.10+, `git` | `curl`, `git`, a POSIX shell | +| Protocols | FTP, FTPS, FTPES, SFTP | FTP, FTPS, FTPES, SFTP | +| Transfers | parallel (`--jobs`, default 4) | sequential | +| `download` / `pull` / `snapshot` | built in | needs `lftp` | +| Status | actively developed | the classic, in most distributions | + +They read the same configuration, write the same `.git-ftp.log` and honour the +same `.git-ftp-ignore` and `.git-ftp-include` files, so a deployment made with +one can be continued with the other. + +[:octicons-arrow-right-24: Which one should I use?](getting-started/implementations.md) + +## Limitations + +- **git-ftp is not a centralised deployment tool.** While a commit is being + uploaded, the files belonging to it must stay untouched — no commits, no + checkouts, no edits — or the uploaded contents will not match the commit. + The Python implementation can take that worry away with + [`--worktree`](guide/configuration.md#consistent-uploads-while-editing). +- **Windows and macOS are lightly tested**, especially for the Bash version. + Bug reports and fixes for those platforms are very welcome. + +[py]: https://github.com/git-ftp/git-ftp-py +[sh]: https://github.com/git-ftp/git-ftp diff --git a/docs/reference/compatibility.md b/docs/reference/compatibility.md new file mode 100644 index 0000000..a173bfc --- /dev/null +++ b/docs/reference/compatibility.md @@ -0,0 +1,139 @@ +--- +title: Compatibility between the implementations +--- + +# Compatibility between the implementations + +What the Python port keeps identical to the Bash original, which of its bugs it fixes, and where it deliberately behaves differently. + +!!! info "Synced from upstream" + + This page is generated from [its source](https://github.com/git-ftp/git-ftp-py/blob/main/COMPATIBILITY.md) + in the tool's repository and is refreshed every time the site + is built. Edit it there, not here. + +## Compatibility with git-ftp 1.6.0 + +This Python git-ftp is a port of the Bash [git-ftp](https://github.com/git-ftp/git-ftp), +version 1.6.0. The rule of the port is: **keep everything on the wire and in +configuration identical, fix the bugs, and write down every deviation here.** + +A remote deployed with the Bash git-ftp can be picked up by this port and vice +versa. The deployed-commit file (`.git-ftp.log`), its content, the config keys, +the `.git-ftp-ignore` and `.git-ftp-include` formats, the hook names and +arguments, the exit codes and the default progress messages are unchanged. + +### Unchanged + +| Area | Status | +|---|---| +| Actions `init`, `push`, `catchup`, `show`, `log`, `download`, `pull`, `snapshot`, `add-scope`, `remove-scope`, `help`, `version` | Same names, same semantics | +| Options | Every 1.6.0 option is accepted with the same spelling | +| Config keys `git-ftp.*` and `git-ftp..*`, `.git-ftp-config` | Same keys, same precedence, an explicit empty value still overrides | +| `.git-ftp.log` (or `git-ftp.deployedsha1file`) | Same location and content (commit id plus newline) | +| `.git-ftp-ignore` | Same glob semantics: `*` crosses `/`, patterns match the whole Git path including the syncroot | +| `.git-ftp-include` | Same `!target` and `target:source` forms, same directory/missing-target rules, applied before ignore | +| Hooks `pre-ftp-push`, `post-ftp-push` | Same arguments and stdin format | +| Exit codes | 2 usage, 3 missing argument, 4 upload, 5 download, 6 unknown protocol, 7 locked, 8 git, 9 hook, 10 filesystem | +| Progress output | `N file(s) to sync:`, `[i of N] Buffered for upload '…'.`, `Uploading ...`, `Last deployment changed from … to ….`, etc. | +| `~/.netrc` fallback when no user is given | Same, now also for sftp | +| Remote lock semantics | Same file (`git-ftp.lck`), checked and written only with `--lock`, same "a lock for my own commit is mine" rule | +| Empty remote directories | Kept, as upstream since 1.1.0 (issue #168) | + +### Upstream bugs that are fixed + +These are behaviours of the Bash script that are clearly unintended. Each is +fixed rather than reproduced. + +- `--no-verify` and `--enable-post-errors` swallowed the argument after them. +- `--key=FILE`, `--pubkey=FILE` and `--branch=NAME` were not parsed. +- `--silent` also suppressed fatal error messages, and `-v` sent them to stdout. + Fatal errors now always go to stderr, at every verbosity. +- `--remote-root` was prepended to the URL's path although the manual says it + replaces it. It replaces it. +- `--lock` toggled: giving it twice turned locking off. It is a plain flag. +- The lock file's two lines were joined by a literal `\n`. A real newline is + written; both forms are read. +- The lock file was named after the script's own file name. The name is fixed. +- `ftp://user@host/path` took `user` for the host. Userinfo in a URL is parsed + as credentials. +- `host:2121/path` without a scheme was rejected as an unknown protocol even + though the built-in help shows that form. It is accepted as ftp. +- `add-scope` gave up on a URL whose password contained a colon or an at-sign. +- File names containing `%`, `?`, `[`, `]`, `"` or a backslash were mangled on + the way into curl. They are transferred intact. So is a file named `-`. +- `git-ftp.no-commit false` enabled the option. Booleans are parsed as Git + parses them (`true/yes/on/1` and `false/no/off/0`). +- A push whose remote log could not be read for *any* reason said + "use 'git ftp init'". That message is now reserved for a genuinely missing + log; a wrong password or an unreachable host is reported as what it is. +- The delete phase ran a directory listing of the remote root just to have a + transfer to attach `-Q` commands to. Deletes are plain `DELE` commands. +- Files whose type changed (a file becoming a symlink or vice versa, `T` in + `git diff`) were never uploaded. They are. +- Running from a subdirectory of the repository resolved `--syncroot` and the + submodule list against the wrong directory. +- `catchup` with more than one submodule only handled the first one, and + `cd`-ed deeper with every iteration. +- Passwords were visible on the curl command line (`ps`). Credentials are + passed to libcurl in memory and never appear in URLs, logs or argv. +- Temporary files were left behind on Ctrl-C. There are no temporary files. + +### New + +- **Parallel transfers**: `--jobs N` / `git-ftp.jobs` (default 4, `1` is + sequential). Uploads run first, then deletes, and the commit log is written + last, only when every upload succeeded. The first failed upload stops the + deploy (exit 4); failed deletes are warnings, as upstream. +- Native `download`, `pull` and `snapshot`: no `lftp` needed. Listings use + `MLSD` with a `LIST` fallback; downloads are parallel too. +- Submodules are deployed in-process (no recursive `git-ftp` invocation). +- Environment variables `GIT_FTP_URL`, `GIT_FTP_USER`, `GIT_FTP_PASSWORD`, + read after the command line and before Git configuration. +- `--password-command` / `git-ftp.password-command`: a shell command whose + first output line is the password. +- `unlock` action to remove a stale lock left by an interrupted deploy. +- `--key-passphrase` / `git-ftp.key-passphrase`, and an interactive prompt, + for encrypted SFTP keys. Upstream cannot use an encrypted key at all. +- SFTP authentication through a running `ssh-agent` (`SSH_AUTH_SOCK`). +- `--password` as an alias of `--passwd`; `--no-post-hooks`. +- Ctrl-C stops the transfers promptly and exits with 130. +- `version -v` prints the libcurl and paramiko versions in use. +- An interactive terminal shows a progress spinner with a `done/total` count + while files upload, delete or download; it renders on stderr and is silent + when output is not a terminal or under `-n`. +- `--worktree` / `git-ftp.worktree`: `init` and `push` read the upload from a + temporary Git worktree checked out at the deployed commit, so edits to the + working tree during the upload cannot leak in. Untracked files added by + `.git-ftp-include` are read from the live working tree, since a worktree of the + commit cannot contain them. + +### Behaviour that differs on purpose + +- **SFTP host keys are verified** against `~/.ssh/known_hosts` and + `/etc/ssh/ssh_known_hosts`. Upstream accepted any host key silently. An + unknown host is refused with a pointer to `ssh-keyscan`; `--insecure` turns + verification off, as it does for TLS. +- **SFTP is spoken by paramiko**, not libcurl. FTP, FTPS and FTPES use libcurl + through pycurl. +- **`git` is required** at run time (the port shells out to it, as upstream). +- A valueless boolean key such as a bare `insecure` line in `.git-ftp-config` + reads as **true**, exactly as `git config` reads it. +- Diagnostics printed with `-v` go to stderr with a timestamp; progress lines + stay on stdout. Warnings (`WARNING: …`) go to stderr at every verbosity. +- `ftps://` and `ftpes://` require TLS 1.2 or newer and encrypt the data + connection as well as the control connection. +- `-u`, `-s` and `-k` without an argument work as upstream (local user, current + branch, default keychain item). An action name after a bare flag (`-u push`) + is recognised as the action. +- The `Insecure is 'N'.` and `Disable EPSV is '1'.` diagnostics are printed + once per repository (twice with one submodule), as upstream. +- `download --changed-only` uses the deployed commit to pick the files + (upstream ignored the flag for `download`). +- The macOS keychain is read through the `security` command on macOS and + ignored elsewhere, as upstream. Keychain entries cannot unlock SSH keys. + +### Not implemented + +- `download`, `pull` and `snapshot` never print lftp's transfer log; they print + a one-line summary instead (`Downloaded N file(s), deleted M local file(s).`). diff --git a/docs/reference/manual-bash.md b/docs/reference/manual-bash.md new file mode 100644 index 0000000..c6c9b94 --- /dev/null +++ b/docs/reference/manual-bash.md @@ -0,0 +1,692 @@ +--- +title: Manual — git-ftp (Bash) +--- + +# Manual — git-ftp (Bash) + +The complete manual page of the original Bash implementation, the one `git ftp help` and `man git-ftp` show. + +!!! info "Synced from upstream" + + This page is generated from [its source](https://github.com/git-ftp/git-ftp/blob/master/man/git-ftp.1.md) + in the tool's repository and is refreshed every time the site + is built. Edit it there, not here. + +## NAME + +Git-ftp - Git powered FTP client written as shell script. + +## SYNOPSIS + +*git-ftp* <action> [<options>] [<url>] + +## DESCRIPTION + +Git-ftp is an FTP client using [Git] to determine which local files to upload or +which files to delete on the remote host. + +It saves the deployed state by uploading the SHA1 hash in the `.git-ftp.log` +file. There is no need for Git to be installed on the remote host. + +Even if you play with different branches, git-ftp knows which files are +different and handles only those files. That saves time and bandwidth. + +## ACTIONS + +`init` +: Uploads all git-tracked non-ignored files to the remote server and + creates the `.git-ftp.log` file containing the SHA1 of the latest + commit. + +`catchup` +: Creates or updates the `.git-ftp.log` file on the remote host. + It assumes that you uploaded all other files already. + You might have done that with another program. + +`push` +: Uploads files that have changed and + deletes files that have been deleted since the last upload. + If you are using GIT LFS, this uploads LFS link files, + not large files (stored on LFS server). + To upload the LFS tracked files, run `git lfs pull` + before `git ftp push`: LFS link files will be replaced with + large files so they can be uploaded. + +`download` (EXPERIMENTAL) +: Downloads changes from the remote host into your working tree. + This feature needs lftp to be installed and does not use any power of + Git. + WARNING: It can delete local untracked files that are not listed in + your `.git-ftp-ignore` file. + +`pull` (EXPERIMENTAL) +: Downloads changes from the remote host into a separate commit + and merges that into your current branch. + If you just want to download the files without a merge, consider `download`. + This feature needs lftp to be installed. + +`snapshot` (EXPERIMENTAL) +: Downloads files into a new Git repository. Takes an additional + argument as local destination directory. Example: + `git-ftp snapshot ftp://example.com/public_html projects/example` + This feature needs lftp to be installed. + +`show` +: Downloads last uploaded SHA1 from log and hooks `git show`. + +`log` +: Downloads last uploaded SHA1 from log and hooks `git log`. + +`add-scope ` +: Creates a new scope (e.g. dev, production, testing, foobar). + This is a wrapper action over git-config. + See **SCOPES** section for more information. + +`remove-scope ` +: Remove a scope. + +`help` +: Shows a help screen. + +## OPTIONS + +`-u [username]`, `--user [username]` +: FTP login name. If no argument is given, local user will be taken. + +`-p [password]`, `--passwd [password]` +: FTP password. See `-P` for interactive password prompt. ([note](#passwords)) + +`-P`, `--ask-passwd` +: Ask for FTP password interactively. + +`-k [[account]@[host]]`, `--keychain [[account]@[host]]` +: FTP password from KeyChain (macOS only). + +`-a`, `--all` +: Uploads all files of current Git checkout. + +`-c`, `--commit` +: Sets SHA1 hash of last deployed commit by option. + +`-A`, `--active` +: Uses FTP active mode. This works only if you have either no firewall + and a direct connection to the server or an FTP aware firewall. If you + don't know what it means, you probably won't need it. + +`-b [branch]`, `--branch [branch]` +: Push a specific branch + +`-s [scope]`, `--scope [scope]` +: Using a scope (e.g. dev, production, testing, foobar). See **SCOPE** + and **DEFAULTS** section for more information. + +`-l`, `--lock` +: Enable remote locking. + +`-D`, `--dry-run` +: Does not upload or delete anything, but tries to get the `.git-ftp.log` + file from remote host. + +`-f`, `--force` +: Does not ask any questions, it just does. + +`-n`, `--silent` +: Be silent. + +`-h`, `--help` +: Prints some usage information. + +`-v`, `--verbose` +: Be verbose. + +`-vv` +: Be as verbose as possible. Useful for debug information. + +`--remote-root` +: Specifies the remote root directory to deploy to. + The remote path in the URL is ignored. + +`--syncroot` +: Specifies a local directory to sync from as if it were the git project + root path. + +`--key` +: SSH private key file name for SFTP. + +`--pubkey` +: SSH public key file name. Used with --key option. + +`--insecure` +: Don't verify server's certificate. + +`--cacert ` +: Use as CA certificate store. + Useful when a server has a self-signed certificate. + +`--disable-epsv` +: Tell curl to disable the use of the EPSV command when doing passive FTP + transfers. + Curl will normally always first attempt to use EPSV before PASV, + but with this option, it will not try using EPSV. + +`--no-commit` +: Stop while merging downloaded changes during the pull action. + A commit is made anyway, but the merge is interrupted. + If you just want to download the files you could also consider the action + `download`. + +`--changed-only` +: During the ftp mirror operation during a pull command, consider only + the files changed since the deployed commit. + +`--no-verify` +: Bypass the pre-ftp-push hook. See **HOOKS** section. + +`--enable-post-errors` +: Fails if post-ftp-push raises an error. + +`--auto-init` +: Automatically run init action when running push action + +`--version` +: Prints version. + +`-x [protocol://]host[:port]`, `--proxy [protocol://]host[:port]` +: Use the specified proxy. This option is passed to curl. + See the curl manual for more information. + +## URL + +The scheme of an URL is what you would expect + + protocol://host.domain.tld:port/path + +Below a full featured URL to *host.example.com* on port *2121* to path *mypath* +using protocol *ftp*: + + ftp://host.example.com:2121/mypath + +But, there is not just FTP. Supported protocols are: + +`ftp://...` +: FTP (default if no protocol is set) + +`sftp://...` +: SFTP + +`ftps://...` +: FTPS + +`ftpes://...` +: FTP over explicit SSL (FTPES) protocol + +## EXAMPLES + +### FIRST UPLOADS + +Upload your files to an FTP server the first time: + + $ git ftp init -u "john" -P "ftp://example.com/public_html" + +It will authenticate with the username `john` and ask for the +password. By default, it tries to transfer data in EPSV mode. +Depending on the network and server configuration, that may fail. +You can try to add the `--disable-epsv` option to use the IPv4 passive FTP +connection (PASV). In rare circumstances, you can use `--active` for the +original FTP transfer mode. These options do not apply to SFTP. + +You are less likely to face connection problems with SFTP. +But be aware of the different +handling of relative and absolute paths. If the directory `public_html` is in +the home directory on the server, then upload like this: + + $ git ftp init -u "john" --key "$HOME/.ssh/id_rsa" "sftp://example.com/~/public_html" + +Otherwise it will use an absolute path, for example: + + $ git ftp init -u "john" --key "$HOME/.ssh/id_rsa" "sftp://example.com/var/www" + +On some systems Git-ftp fails to verify the server's fingerprint. +You can then use the `--insecure` option to skip the verification. +That will leave you vulnerable to man-in-the-middle attacks, but is still more +secure than plain FTP. + +Git-ftp guesses the path of the public key file corresponding to your private +key file. If you just have a private key, for example a .pem file, you need +Git-ftp version 1.3.4 and Curl version 7.39.0 or newer. +If you have an older version of Git-ftp or Curl, you can +create the public key with the ssh-keygen command: + + $ ssh-keygen -y -f key.pem > key.pem.pub + +### RESET THE UPLOADED STATE + +Many people already uploaded their files to the server. +If you want to mark the uploaded version as the same as your local branch: + + $ git ftp catchup + +This example omits options like `--user`, `--password` and `url`. +See DEFAULTS below to learn how to store your configuration so that you don't +need to repeat it. + +After you stored the commit id of the uploaded commit via `init` or +`catchup`, you can then upload any new commits: + + $ git ftp push + +If you discovered a bug in the last uploaded version and you want to go back +by three commits: + + $ git checkout HEAD~3 + $ git ftp push + +Or maybe some files got changed on the server and you want to upload all +changes between branch `master` and branch `develop`: + + $ git checkout develop # This is the version which is uploaded. + $ git ftp push --commit master # Upload changes compared to master. + +## DEFAULTS + +Don't repeat yourself. Setting config defaults for git-ftp in .git/config + + $ git config git-ftp.<(url|user|password|syncroot|cacert|keychain|...)> + +Everyone likes examples: + + $ git config git-ftp.user john + $ git config git-ftp.url ftp.example.com + $ git config git-ftp.password secr3t + $ git config git-ftp.syncroot path/dir + $ git config git-ftp.cacert caCertStore + $ git config git-ftp.deployedsha1file mySHA1File + $ git config git-ftp.insecure 1 + $ git config git-ftp.key ~/.ssh/id_rsa + $ git config git-ftp.keychain user@example.com + $ git config git-ftp.remote-root htdocs + $ git config git-ftp.disable-epsv 1 + $ git config git-ftp.no-commit 1 + +After setting those defaults, push to *john@ftp.example.com* is as simple as + + $ git ftp push + +If you run into issues with setting up your password please check this [note](#passwords). + +## SCOPES + +Need different config defaults per each system or environment? Use the so +called scope feature. + +Useful if you use multi environment development. Like a development, testing +and a production environment. + + $ git config git-ftp..<(url|user|password|syncroot|cacert)> + +So in the case below you would set a testing scope and a production scope. + +Here we set the params for the scope "testing" + + $ git config git-ftp.testing.url ftp.testing.com:8080/foobar-path + $ git config git-ftp.testing.password simp3l + +Here we set the params for the scope "production" + + $ git config git-ftp.production.user manager + $ git config git-ftp.production.url live.example.com + $ git config git-ftp.production.password n0tThatSimp3l + +Pushing to scope *testing* alias *john@ftp.testing.com:8080/foobar-path* +using password *simp3l* + + $ git ftp push -s testing + +*Note:* The **SCOPE** feature can be mixed with the **DEFAULTS** feature. +Because we didn't set the user for this scope, +git-ftp uses *john* as user as set before in **DEFAULTS**. + +Pushing to scope *production* alias *manager@live.example.com* using +password *n0tThatSimp3l* + + $ git ftp push -s production + +*Hint:* If your scope name is identical with your branch name. You can skip the +scope argument, e.g. if your current branch is "production": + + $ git ftp push -s + +You can also create scopes using the add-scope action. +All settings can be defined in the URL. +Here we create the *production* scope using add-scope + + $ git ftp add-scope production ftp://manager:n0tThatSimp3l@live.example.com/foobar-path + +Deleting scopes is easy using the `remove-scope` action. + + $ git ftp remove-scope production + +## IGNORING FILES TO BE SYNCED + +Add patterns to `.git-ftp-ignore` and all matching file names will be ignored. +The patterns are interpreted as shell glob patterns since version 1.1.0. +Before version 1.1.0, patterns were interpreted as regular expressions. +Here are some glob pattern examples: + +Ignoring everything in a directory named `config`: + + config/* + +Ignoring all files having extension `.txt`: + + *.txt + +Ignoring a single file called `foobar.txt`: + + foobar.txt + +Ignoring Git related files: + + .gitignore + */.gitignore # ignore files in sub directories + */.gitkeep + .git-ftp-ignore + .git-ftp-include + .gitlab-ci.yml + +## SYNCING UNTRACKED FILES + +The `.git-ftp-include` file specifies intentionally untracked files that +Git-ftp should upload. +If you have a file that should always be uploaded, add a line beginning with ! +followed by the file's name. +For example, if you have a file called VERSION.txt then add the following line: + + !VERSION.txt + +If you have a file that should be uploaded whenever a tracked file changes, add +a line beginning with the untracked file's name followed by a colon and the +tracked file's name. +For example, if you have a CSS file compiled from an SCSS file then add the +following line: + + css/style.css:scss/style.scss + +If you have multiple source files, you can add multiple lines for each of them. +Whenever one of the tracked files changes, the upload of the paired untracked +file will be triggered. + + css/style.css:scss/style.scss + css/style.css:scss/mixins.scss + +If a local untracked file is deleted, any change of a paired tracked file will +trigger the deletion of the remote file on the server. + +All paths are usually relative to the Git working directory. +When using the `--syncroot` option, paths of tracked files +(right side of the colon) are relative to the set syncroot. +Example: + + # upload "html/style.css" triggered by html/style.scss + # with syncroot "html" + html/style.css:style.scss + +If your *source* file is outside the syncroot, +prefix it with a / and define a path relative +to the Git working directory. For example: + + # upload "dist/style.css" with syncroot "dist" + dist/style.css:/src/style.scss + +It is also possible to upload whole directories. +For example, if you use a package manager like composer, you can upload all +vendor packages when the file composer.lock changes: + + vendor/:composer.lock + +But keep in mind that this will upload all files in the vendor folder, even +those that are on the server already. +And it will not delete files from that directory if local files are deleted. + +## DOWNLOADING FILES (EXPERIMENTAL) + +**WARNING:** It can delete local untracked files that are not listed in your +`.git-ftp-ignore` file. + +You can use git-ftp to download from the remote host into your repository. +You will need to install the lftp command line tool for that. + + git ftp download + +It uses lftp's mirror command to download all files that are different on the +remote host. You can inspect the changes with git-diff. +But if you have some local commits that have not been uploaded to the remote +host, you may not compare to the right version. +You need to compare the downloaded files to the commit that was uploaded last. +This magic is done automatically by + + git ftp pull + +It does the following steps for you: + + git checkout + git ftp download + git add --all + git commit -m '[git-ftp] remotely untracked modifications' + git ftp catchup + git checkout + git merge + +If you want to inspect the downloaded changes before merging them into your +current branch, add the option `--no-commit`. +It will stop during the merge at the end of the pull action. +You can inspect the merge result first and can then decide to continue or +abort. + + git ftp pull --no-commit + # inspect the result and commit them + git commit + # or abort the merge + git merge --abort + +If you abort the merge, the downloaded changes will stay in an unreferenced +commit until the Git garbage collector is run. +The commit id will be printed so that you can tag it or create a new branch. + +## HOOKS (EXPERIMENTAL) + +**This feature is experimental. The interface may change.** + +Git-ftp supports client-side hook scripts during the init and the push action. + +`pre-ftp-push` is called just before the upload to the server starts, but after +the changeset of files was generated. It can be bypassed with the --no-verify +option. + +The hook is called with four parameters. +The first is the used scope or the host name if no scope is used. +The second parameter is the destination URL. +The third is the local commit id which is going to be uploaded and +the fourth is the remote commit id on the server which is going to be updated. + +The standard input is a list of all filenames to sync. Each file is preceeded +by A or D followed by a space. A means that this file is scheduled for upload, +D means it's scheduled for deletion. All entries are separated by the NUL byte. +This list is different to git diff, because +it has been changed by the rules of the `.git-ftp-include` file and the +`.git-ftp-ignore` file. + +Exiting with non-zero status from this script causes +Git-ftp to abort and exit with status 9. + +An example script is: + +```bash +##!/bin/bash +## +## An example hook script to verify what is about to be uploaded. +## +## Called by "git ftp push" after it has checked the remote status, but before +## anything has been pushed. If this script exits with a non-zero status nothing +## will be pushed. +## +## This hook is called with the following parameters: +## +## $1 -- Scope name if set or host name of the remote +## $2 -- URL to which the upload is being done +## $3 -- Local commit id which is being uploaded +## $4 -- Remote commit id which is on the server +## +## Information about the files which are being uploaded or deleted is supplied +## as NUL separated entries to the standard input in the form: +## +## +## +## The status is either A for upload or D for delete. The path contains the +## path to the local file. It contains the syncroot if set. +## +## This sample shows how to prevent upload of files containing the word TODO. + +remote="$1" +url="$2" +local_sha="$3" +remote_sha="$4" + +while read -r -d '' status file +do + if [ "$status" = "A" ] + then + if grep 'TODO' "$file"; then + echo "TODO found in file $file, not uploading." + exit 1 + fi + fi +done + +exit 0 +``` + +`post-ftp-push` is called after the transfer has been finished. The standard +input is empty, but the parameters are the same as given to the `pre-ftp-push` +hook. This hook is **not** bypassed by the --no-verify option. +It is meant primarily for notification and its exit status does not have any +effect. + +## PASSWORDS + +If your password contains special characters you have to take it with care. In most cases it is a good idea to quote passwords with single quotes: + + --passwd '#my$fancy!secret' + +Mostly `--ask-passwd` works even if `--passwd` does not work. So maybe you can give this a try. + +If your password starts with a hyphen/dash (`-`) even quoting might fail. +This is [by design](https://github.com/git-ftp/git-ftp/issues/468) and will not be fixed. +In this case you can use one of the other options to set your password: the defaults feature using `git config`, `--ask-passwd` or `~/.netrc`. + +Quoting also works if a [default](#defaults) is set with `git config`: + + $ git config git-ftp.password '#my$fancy!secret' + +### NETRC + +In the backend, Git-ftp uses curl. +This means `~/.netrc` could be used beside the other options of Git-ftp +to authenticate. + + $ editor ~/.netrc + machine ftp.example.com + login john + password SECRET + +With git-ftp the credentials stored in this file are used if no username is set. +For example, if you set up your .netrc file like this you can just call + + git ftp init ftp.example.com + +Of course this can be combined with the [defaults feature](#defaults) to set config defaults for other options as well. + +### Keychain on macOS + +On macOS you can use the built in keychain to store and get your passwords. + +You can use this feature by using the option `--keychain` in your command: + + $ git ftp init --keychain account@host ftpes://host + +You can omit the value for this option. Then git-ftp will guess the account and hostname from user and url. + +Or you can set a config for this, so you don’t need to repeat yourself (see [defaults](#defaults) for details): + + $ git config git-ftp.keychain account@host + +You can omit the hostname here. If there is no `@` in the config value git-ftp will guess the hostname from url. + +If you run a command using the keychain feature, the system might ask you if git-ftp is allowed to access the keychain entry. +If the keychain is locked you have to enter the keychain password (not the value of the entry), sometimes twice. + +If your password is not in your keychain yet it is recommended adding it using the following command: + + $ security add-internet-password -a account -r "ftp " -s host -w secr3t + +The options are: +- `-a`: user account +- `-r`: protocol; has to be exactly 4 characters long, so if you use `FTP` it should be `"ftp "`, for `FTPS` and `FTPES` use `ftps` + and for SSH with password auth you can use `"ftp "` as well. +- `-s`: your host name; includes subdomains but no paths +- `-w`: password + +You can omit the option `-r` and everything will work fine, but the _Keychain Access_ Utility will not show the server in the field “Where:”. +This is only shown if `-r` and `-s` are set both. \ +If you create a keychain entry with the _Keychain Access_ Utility it creates a generic password and not an internet password. +Therefore, unfortunately, this will not work. + +Please not that the keychain entry can not be used for password protected private keys in SSH. + +## EXIT CODES + +There are a bunch of different error codes and their corresponding error +messages that may appear during bad conditions. +At the time of this writing, the exit codes are: + +`1` +: Unknown error + +`2` +: Wrong Usage + +`3` +: Missing arguments + +`4` +: Error while uploading + +`5` +: Error while downloading + +`6` +: Unknown protocol + +`7` +: Remote locked + +`8` +: Not a Git project + +`9` +: The `pre-ftp-push` hook failed + +`10` +: A local file operation like `cd` or `mkdir` failed + +## KNOWN ISSUES & BUGS + +The upstream BTS can be found at . + +[Git]: http://git-scm.org + +## AUTHORS + +Git-ftp was started by Rene Moser and is currently maintained by Maikel Linke. +Numerous contributions have come from GitHub users. +See the AUTHORS file for an incomplete list of contributors. diff --git a/docs/reference/manual-python.md b/docs/reference/manual-python.md new file mode 100644 index 0000000..6b46137 --- /dev/null +++ b/docs/reference/manual-python.md @@ -0,0 +1,374 @@ +--- +title: Manual — git-ftp (Python) +--- + +# Manual — git-ftp (Python) + +The complete manual page of the Python implementation, the one `git ftp help` and `man git-ftp` show. + +!!! info "Synced from upstream" + + This page is generated from [its source](https://github.com/git-ftp/git-ftp-py/blob/main/docs/git-ftp.1.md) + in the tool's repository and is refreshed every time the site + is built. Edit it there, not here. + +## NAME + +git-ftp - Git powered FTP, FTPS, FTPES and SFTP client + +## SYNOPSIS + +*git-ftp* <action> [<options>] [<url>] + +## DESCRIPTION + +This manual page documents *git-ftp*, a Python program that uploads the +files of a Git repository to a server over FTP, FTPS, FTPES or SFTP. Only the +files that changed since the last deployment are transferred. The deployed +commit is recorded in a file on the server, *.git-ftp.log* by default, so no +software has to be installed on the server. + +Git runs the program as *git ftp* when *git-ftp* is on the PATH. + +## ACTIONS + +*init* +: Uploads all tracked files that are not ignored and records the commit in + *.git-ftp.log* on the server. Fails when the server already has a log. + +*catchup* +: Only creates or updates *.git-ftp.log* on the server. Use it when the + server already holds the current files. + +*push* +: Uploads the files added or changed and deletes the files removed since the + deployed commit, then updates *.git-ftp.log*. With *--auto-init* a missing + log behaves like *init*. + +*download* +: Mirrors the remote directory into the working tree (files missing on the + server are removed locally; ignored files are left alone). Refuses to run + while the working tree has untracked files. + +*pull* +: Checks out the deployed commit, downloads the remote changes into a + commit, records that commit on the server and merges it into the current + branch. + +*snapshot* [<directory>] +: Downloads a remote directory that is not yet managed by git-ftp into a new + repository, commits it and records the commit on the server. + +*show* +: Runs *git show* on the deployed commit. + +*log* +: Runs *git log* on the deployed commit. + +*add-scope* <scope> <url> +: Stores the URL (and the credentials contained in it) under a scope name in + the repository's Git configuration. + +*remove-scope* <scope> +: Removes a scope from the Git configuration. + +*unlock* +: Removes a stale remote lock left by an interrupted deploy. + +*help* +: Prints the built-in help. + +*version* +: Prints the version; with *-v* also the libcurl and paramiko versions. + +## OPTIONS + +*-u [<username>]*, *--user [<username>]* +: FTP login name. Without a value the local user name is used. + +*-p <password>*, *--passwd <password>*, *--password <password>* +: FTP password. A value starting with a dash is accepted (*-p=-secret*). + +*-P*, *--ask-passwd* +: Ask for the password interactively. + +*--password-command <command>* +: Run a shell command and use the first line of its output as the password. + +*-k [[<account>]@[<host>]]*, *--keychain [[<account>]@[<host>]]* +: Read the password from the macOS keychain (ignored on other systems). + +*-a*, *--all* +: Upload all files instead of only the changed ones. + +*-c <commit>*, *--commit <commit>* +: Treat the given commit as the deployed one instead of reading the log. + +*-A*, *--active* +: Use FTP active mode. + +*-b [<branch>]*, *--branch [<branch>]* +: Deploy the given branch and switch back afterwards. + +*-s [<scope>]*, *--scope [<scope>]* +: Use the configuration of the given scope. Without a value the current + branch name is the scope. + +*-l*, *--lock* +: Write a lock file on the server for the duration of the deploy. + +*-D*, *--dry-run* +: Print what would be transferred without transferring anything. + +*-f*, *--force* +: Skip the lock check and the question about an unknown deployed commit. + +*-n*, *--silent* +: Print nothing but fatal errors. + +*-v*, *--verbose* +: Print diagnostics on stderr. *-vv* also prints the protocol trace. + +*-j <n>*, *--jobs <n>* +: Number of parallel connections (default 4). *1* transfers sequentially. + +*--remote-root <directory>* +: Remote directory to deploy into, replacing the path of the URL. + +*--syncroot <directory>* +: Deploy only this directory; it becomes the remote root. + +*--key <file>* +: SFTP private key. + +*--pubkey <file>* +: SFTP public key (defaults to *<key>.pub*). + +*--key-passphrase <text>* +: Passphrase of an encrypted SFTP private key. + +*--insecure* +: Do not verify TLS certificates or SFTP host keys. + +*--cacert <file>* +: CA certificate bundle used to verify FTPS/FTPES servers. + +*--disable-epsv* +: Use PASV instead of EPSV. + +*-x <url>*, *--proxy <url>* +: Proxy URL (also read from *git-ftp.proxy* and *http.proxy*). + +*--no-commit* +: *pull*: merge without committing. + +*--changed-only* +: *download*, *pull*: only transfer files that changed locally as well. + +*--no-verify* +: Skip the *pre-ftp-push* hook. + +*--no-post-hooks* +: Skip the *post-ftp-push* hook. + +*--enable-post-errors* +: Fail when the *post-ftp-push* hook fails. + +*--auto-init* +: *push*: behave like *init* when the server has no log yet. + +*--worktree* +: Read the files to upload from a temporary Git worktree checked out at the + commit being deployed, so edits to the working tree during the upload are + ignored. + +*--version* +: Print the version and exit. + +*-h*, *--help* +: Print the help and exit. + +## URL + +The URL has the form *protocol://host.domain.tld:port/path*. Supported +protocols are *ftp://* (the default when none is given), *ftpes://* (explicit +TLS), *ftps://* (implicit TLS) and *sftp://*. *host:port/path* without a +protocol is accepted as FTP. Credentials may be given as +*ftp://user:password@host/path*; the password never appears in any output. + +For SFTP, *sftp://host/dir* is relative to the login directory, +*sftp://host/~/dir* to the home directory and *sftp://host//dir* is absolute. + +## DEFAULTS + +Every option can be stored in the Git configuration: + + git config git-ftp.url ftp://example.com/public_html + git config git-ftp.user alice + git config git-ftp.password s3cret + git config git-ftp.password-command "pass show example.com/ftp" + git config git-ftp.syncroot public_html + git config git-ftp.remote-root htdocs + git config git-ftp.cacert /etc/ssl/certs/ca.pem + git config git-ftp.insecure true + git config git-ftp.disable-epsv true + git config git-ftp.proxy http://proxy:3128 + git config git-ftp.key ~/.ssh/id_ed25519 + git config git-ftp.pubkey ~/.ssh/id_ed25519.pub + git config git-ftp.key-passphrase ... + git config git-ftp.keychain alice@example.com + git config git-ftp.branch main + git config git-ftp.no-commit true + git config git-ftp.deployedsha1file .git-ftp.log + git config git-ftp.jobs 8 + git config git-ftp.worktree true + +The same keys may be placed in a *.git-ftp-config* file in the repository, +which takes precedence over the Git configuration. + +## ENVIRONMENT + +*GIT_FTP_URL*, *GIT_FTP_USER*, *GIT_FTP_PASSWORD* +: Read after the command line and before the Git configuration. + +*NETRC* +: Alternative *~/.netrc* file. The netrc file is consulted only when neither + a user nor a password was given by other means. + +*SSH_AUTH_SOCK* +: A running ssh-agent is used for SFTP authentication. + +## SCOPES + +Scopes hold a separate set of settings, for example for staging and +production: + + git config git-ftp.production.url ftp://live.example.com/htdocs + git config git-ftp.production.password s3cret + git ftp push -s production + +Unset scope keys fall back to the plain keys. An explicitly empty scope value +masks the plain value. *git ftp push -s* uses the current branch name as the +scope name. *add-scope* and *remove-scope* manage scopes from the command line. + +## IGNORING FILES + +*.git-ftp-ignore* in the repository root lists shell glob patterns, one per +line, of Git paths that are never uploaded (and never deleted). As in the +original, *\** also matches */* and the pattern has to match the whole path: + + config/* + *.txt + foobar.txt + .gitignore + */.gitkeep + .git-ftp-ignore + .git-ftp-include + +## SYNCING UNTRACKED FILES + +*.git-ftp-include* uploads files that are not tracked by Git: + + !VERSION.txt + css/style.css:scss/style.scss + css/style.css:scss/mixins.scss + vendor/:composer.lock + dist/style.css:/src/style.scss + +A line starting with *!* always uploads the file. *target:source* uploads +*target* whenever the tracked *source* changed since the deployed commit. With +*--syncroot* the source is relative to the syncroot unless it starts with */*. +A directory target uploads everything below it. A target that no longer exists +locally is deleted on the server. Ignore patterns still apply afterwards. + +## PARALLEL TRANSFERS + +Files are transferred over several connections at once (*--jobs*, default 4). +Uploads happen first, then deletes, then the log is written. The first failed +upload aborts the deploy with exit code 4 and the log is left untouched; a +failed delete is only a warning. Ctrl-C stops the transfers and exits with 130. + +## WORKTREE + +With *--worktree* (or *git config git-ftp.worktree true*) *init* and *push* check +the deployed commit out into a throwaway Git worktree and read the uploaded file +contents from there. Editing the working tree while a long upload is running then +cannot change what is deployed. The worktree shares the object store, so only a +working copy is written to disk; it is removed when the deploy finishes. Untracked +files added through *.git-ftp-include* are not part of the commit and are read from +the live working tree as before. + +## LOCKING + +With *--lock* a file *git-ftp.lck* containing the commit id and the user is +written on the server before the transfer and removed afterwards. A deploy of +a different commit refuses to run while the lock exists (exit code 7); *--force* +ignores the lock and *unlock* removes it. + +## DOWNLOADING + +*download*, *pull* and *snapshot* list the remote directory (MLSD, falling back +to LIST) and download files that are missing locally or differ in size or +modification time. Files missing on the server are deleted locally unless they +are ignored, part of *.git* or one of git-ftp's own files. + +## HOOKS + +*pre-ftp-push* and *post-ftp-push* in the repository's hooks directory +(*core.hooksPath* is honoured) are run with four arguments: the scope name or +host, the display URL, the local commit and the deployed commit (the previous +one for the post hook). The pre hook receives the NUL separated change list +(*A path* / *D path*) on stdin; a non-zero exit aborts the deploy with exit code +9. The post hook's exit status is ignored unless *--enable-post-errors* is +given. + +## SFTP HOST KEYS + +Host keys are verified against *~/.ssh/known_hosts* and +*/etc/ssh/ssh_known_hosts*. Add a server with + + ssh-keyscan -p 22 host >> ~/.ssh/known_hosts + +or pass *--insecure* to skip the verification. + +## EXIT CODES + +0 +: success + +1 +: unexpected error + +2 +: wrong usage + +3 +: missing argument + +4 +: error while uploading (also: remote not reachable, login failed) + +5 +: error while downloading (also: *push* before *init*) + +6 +: unknown protocol + +7 +: remote locked + +8 +: not a Git project, dirty working tree or invalid branch + +9 +: hook failed + +10 +: local filesystem error + +130 +: interrupted + +## SEE ALSO + +git(1), curl(1). Upstream: diff --git a/docs/stylesheets/extra.css b/docs/stylesheets/extra.css new file mode 100644 index 0000000..a5e2973 --- /dev/null +++ b/docs/stylesheets/extra.css @@ -0,0 +1,29 @@ +/* A slightly denser code font size, since most pages are command lines. */ +.md-typeset pre > code { + font-size: 0.72rem; +} + +/* Cards on the landing page keep an even height in a row. */ +.md-typeset .grid.cards > ul > li { + display: flex; + flex-direction: column; +} + +.md-typeset .grid.cards > ul > li > hr { + margin-block: 0.6rem; +} + +/* Definition lists carry the manual pages; make the term stand out. */ +.md-typeset dl dt { + font-weight: 600; +} + +.md-typeset dl dd { + margin-bottom: 0.8em; +} + +/* The tables comparing the two implementations read better left aligned + and with room to breathe. */ +.md-typeset table:not([class]) th { + white-space: nowrap; +} diff --git a/images/bg_hr.png b/images/bg_hr.png deleted file mode 100644 index 7973bd6..0000000 Binary files a/images/bg_hr.png and /dev/null differ diff --git a/images/bkg.png b/images/bkg.png deleted file mode 100644 index fcebb5b..0000000 Binary files a/images/bkg.png and /dev/null differ diff --git a/images/blacktocat.png b/images/blacktocat.png deleted file mode 100644 index 273d571..0000000 Binary files a/images/blacktocat.png and /dev/null differ diff --git a/images/body-bg.jpg b/images/body-bg.jpg deleted file mode 100644 index 719fb88..0000000 Binary files a/images/body-bg.jpg and /dev/null differ diff --git a/images/download-button.png b/images/download-button.png deleted file mode 100644 index c5ffb3a..0000000 Binary files a/images/download-button.png and /dev/null differ diff --git a/images/github-button.png b/images/github-button.png deleted file mode 100644 index cd41580..0000000 Binary files a/images/github-button.png and /dev/null differ diff --git a/images/header-bg.jpg b/images/header-bg.jpg deleted file mode 100644 index d16497a..0000000 Binary files a/images/header-bg.jpg and /dev/null differ diff --git a/images/highlight-bg.jpg b/images/highlight-bg.jpg deleted file mode 100644 index 355e089..0000000 Binary files a/images/highlight-bg.jpg and /dev/null differ diff --git a/images/icon_download.png b/images/icon_download.png deleted file mode 100644 index a2a287f..0000000 Binary files a/images/icon_download.png and /dev/null differ diff --git a/images/sidebar-bg.jpg b/images/sidebar-bg.jpg deleted file mode 100644 index 536ead9..0000000 Binary files a/images/sidebar-bg.jpg and /dev/null differ diff --git a/images/sprite_download.png b/images/sprite_download.png deleted file mode 100644 index f2babd5..0000000 Binary files a/images/sprite_download.png and /dev/null differ diff --git a/index.html b/index.html deleted file mode 100644 index 35bcf7a..0000000 --- a/index.html +++ /dev/null @@ -1,117 +0,0 @@ - - - - - - - - - - - - - - - Git-ftp by git-ftp - - - -
-
-

Git-ftp

-

Upload to FTP servers the Git way

- View project on GitHub -
-
- -
-
-
-

If you use Git and you need to upload your files to an FTP server, -Git-ftp can save you some time and bandwidth by uploading only those files that -changed since the last upload.

- -

It keeps track of the uploaded files by storing the commit id -in a log file on the server. It uses Git to determine which local -files have changed.

- -

You can easily deploy another branch or go back in the Git history to upload -an older version.

- -
# Setup
-git config git-ftp.url ftp.example.net
-git config git-ftp.user ftp-user
-git config git-ftp.password secr3t
-
-# Upload all files
-git ftp init
-
-# Or if the files are already there
-git ftp catchup
-
-# Work and deploy
-echo "new content" >> index.txt
-git commit index.txt -m "Add new content"
-git ftp push
-# 1 file to sync:
-# [1 of 1] Buffered for upload 'index.txt'.
-# Uploading ...
-# Last deployment changed to ded01b27e5c785fb251150805308d3d0f8117387.
- -

-Further Reading

- - - -

-Limitations

- -
    -
  • Windows and OS X: I am very limited in testing on Windows and OS X. Thanks -for helping me out fixing bugs on these platforms.
  • -
  • git-ftp as deployment tool: git-ftp was not designed as centralized -deployment tool. While running git-ftp, you have to take care, no one pushes or -touches this repo (e.g. no commits, no checkouts, no file modifications)!
  • -
- -

-Contributions

- -

Don't hesitate to improve this tool. -Don't forget to add yourself to the AUTHORS file. -Core functionality is unit tested using shunit2. -You can find the tests in tests/.

- -

-Copyright

- -

This application is licensed under GNU General Public License, Version 3.0

-
- - -
-
- - - - diff --git a/javascripts/main.js b/javascripts/main.js deleted file mode 100644 index d8135d3..0000000 --- a/javascripts/main.js +++ /dev/null @@ -1 +0,0 @@ -console.log('This would be the main JS file.'); diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 0000000..0dcd26f --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,115 @@ +site_name: Git-ftp +site_description: Upload to FTP servers the Git way — deploy a Git repository over FTP, FTPS, FTPES or SFTP, uploading only the files that changed. +site_url: https://git-ftp.github.io/ +site_author: René Moser and the git-ftp contributors +copyright: Git-ftp is licensed under the GNU General Public License, Version 3.0 + +repo_url: https://github.com/git-ftp/git-ftp.github.io +repo_name: git-ftp.github.io +edit_uri: edit/master/docs/ + +theme: + name: material + language: en + icon: + repo: fontawesome/brands/github + logo: material/cloud-upload-outline + palette: + - media: "(prefers-color-scheme)" + toggle: + icon: material/brightness-auto + name: Follow system theme + - media: "(prefers-color-scheme: light)" + scheme: default + primary: indigo + accent: indigo + toggle: + icon: material/brightness-7 + name: Switch to dark mode + - media: "(prefers-color-scheme: dark)" + scheme: slate + primary: indigo + accent: indigo + toggle: + icon: material/brightness-4 + name: Switch to light mode + features: + - navigation.tabs + - navigation.sections + - navigation.top + - navigation.instant + - navigation.tracking + - navigation.indexes + - toc.follow + - search.suggest + - search.highlight + - content.code.copy + - content.code.annotate + - content.action.edit + - content.tabs.link + +extra_css: + - stylesheets/extra.css + +extra: + social: + - icon: fontawesome/brands/github + link: https://github.com/git-ftp + name: git-ftp on GitHub + - icon: fontawesome/brands/python + link: https://pypi.org/project/git-ftp/ + name: git-ftp on PyPI + +markdown_extensions: + - abbr + - admonition + - attr_list + - def_list + - footnotes + - md_in_html + - tables + - toc: + permalink: true + - pymdownx.details + - pymdownx.highlight: + anchor_linenums: true + line_spans: __span + pygments_lang_class: true + - pymdownx.inlinehilite + - pymdownx.keys + - pymdownx.snippets + - pymdownx.superfences + - pymdownx.tabbed: + alternate_style: true + - pymdownx.tasklist: + custom_checkbox: true + - pymdownx.emoji: + emoji_index: !!python/name:material.extensions.emoji.twemoji + emoji_generator: !!python/name:material.extensions.emoji.to_svg + +plugins: + - search + +nav: + - Home: index.md + - Getting started: + - getting-started/index.md + - Install: getting-started/install.md + - First deployment: getting-started/first-deployment.md + - Which implementation: getting-started/implementations.md + - Guide: + - Configuration: guide/configuration.md + - Scopes: guide/scopes.md + - Selecting files: guide/selecting-files.md + - Protocols and credentials: guide/protocols.md + - Hooks and locking: guide/hooks-and-locking.md + - Downloading from the server: guide/download.md + - Continuous deployment: guide/ci-cd.md + - Troubleshooting: guide/troubleshooting.md + - Reference: + - Manual (Python): reference/manual-python.md + - Manual (Bash): reference/manual-bash.md + - Compatibility: reference/compatibility.md + - About: + - Contributing: about/contributing.md + - License: about/license.md diff --git a/params.json b/params.json deleted file mode 100644 index 2200319..0000000 --- a/params.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "name": "Git-ftp", - "tagline": "Upload to FTP servers the Git way", - "body": "If you use Git and you need to upload your files to an FTP server,\r\nGit-ftp can save you some time and bandwidth by uploading only those files that\r\nchanged since the last upload.\r\n\r\nIt keeps track of the uploaded files by storing the commit id\r\nin a log file on the server. It uses Git to determine which local\r\nfiles have changed.\r\n\r\nYou can easily deploy another branch or go back in the Git history to upload\r\nan older version.\r\n\r\n```sh\r\n# Setup\r\ngit config git-ftp.url ftp.example.net\r\ngit config git-ftp.user ftp-user\r\ngit config git-ftp.password secr3t\r\n\r\n# Upload all files\r\ngit ftp init\r\n\r\n# Or if the files are already there\r\ngit ftp catchup\r\n\r\n# Work and deploy\r\necho \"new content\" >> index.txt\r\ngit commit index.txt -m \"Add new content\"\r\ngit ftp push\r\n# 1 file to sync:\r\n# [1 of 1] Buffered for upload 'index.txt'.\r\n# Uploading ...\r\n# Last deployment changed to ded01b27e5c785fb251150805308d3d0f8117387.\r\n```\r\n\r\nFurther Reading\r\n---------------\r\n\r\n* Read the [manual](man/git-ftp.1.md) for more options, features and examples.\r\n* See the [installtion instructions](INSTALL.md) for your system.\r\n* Check [git-ftp issues on GitHub] for open issues.\r\n* Follow this project on twitter [@gitftp].\r\n\r\nLimitations\r\n-----------\r\n\r\n* Windows and OS X: I am very limited in testing on Windows and OS X. Thanks\r\nfor helping me out fixing bugs on these platforms.\r\n* git-ftp as deployment tool: git-ftp was not designed as centralized\r\ndeployment tool. While running git-ftp, you have to take care, no one pushes or\r\ntouches this repo (e.g. no commits, no checkouts, no file modifications)!\r\n\r\nContributions\r\n-------------\r\n\r\nDon't hesitate to improve this tool.\r\nDon't forget to add yourself to the [AUTHORS](AUTHORS) file.\r\nCore functionality is unit tested using shunit2.\r\nYou can find the tests in `tests/`.\r\n\r\nCopyright\r\n---------\r\n\r\nThis application is licensed under [GNU General Public License, Version 3.0]\r\n\r\n[git-ftp issues on GitHub]: http://github.com/git-ftp/git-ftp/issues\r\n[GNU General Public License, Version 3.0]:\r\n http://www.gnu.org/licenses/gpl-3.0-standalone.html\r\n[@gitftp]: https://twitter.com/gitftp\r\n", - "google": "", - "note": "Don't delete this file! It's used internally to help with page regeneration." -} \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..1e30ce2 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,11 @@ +[project] +name = "git-ftp-docs" +version = "0" +description = "Sources of the git-ftp documentation site at https://git-ftp.github.io" +requires-python = ">=3.10" +dependencies = [ + "mkdocs-material>=9.5,<10", +] + +[tool.uv] +package = false diff --git a/scripts/sync_upstream_docs.py b/scripts/sync_upstream_docs.py new file mode 100755 index 0000000..d768f43 --- /dev/null +++ b/scripts/sync_upstream_docs.py @@ -0,0 +1,219 @@ +#!/usr/bin/env python3 +"""Pull the manual pages of both git-ftp implementations into docs/reference/. + +The manuals live in the tool repositories, written as pandoc man page source. +This script fetches them (or copies them from a local checkout with --local) +and converts them to pages MkDocs can render: the pandoc title block goes, the +headings are demoted by one level so the page title is the only H1, tab indented +definition lists become space indented ones, and relative links are turned into +links to the upstream repository. + + ./scripts/sync_upstream_docs.py # fetch from GitHub + ./scripts/sync_upstream_docs.py --local # from ../git-ftp and ../git-ftp-py +""" + +from __future__ import annotations + +import argparse +import re +import sys +from dataclasses import dataclass, field +from pathlib import Path +from urllib.request import urlopen + +ROOT = Path(__file__).resolve().parent.parent +REFERENCE = ROOT / "docs" / "reference" + +PY_REPO = "https://github.com/git-ftp/git-ftp-py" +SH_REPO = "https://github.com/git-ftp/git-ftp" +PY_RAW = "https://raw.githubusercontent.com/git-ftp/git-ftp-py/main" +SH_RAW = "https://raw.githubusercontent.com/git-ftp/git-ftp/master" + + +@dataclass +class Source: + """One upstream document and how to turn it into a page of this site.""" + + target: str + raw_url: str + local_path: Path + blob_url: str + title: str + nav_title: str + intro: str + link_base: str + drop: list[str] = field(default_factory=list) + links: dict[str, str] = field(default_factory=dict) + + +SOURCES = [ + Source( + target="manual-python.md", + raw_url=f"{PY_RAW}/docs/git-ftp.1.md", + local_path=Path("../git-ftp-py/docs/git-ftp.1.md"), + blob_url=f"{PY_REPO}/blob/main/docs/git-ftp.1.md", + title="Manual — git-ftp (Python)", + nav_title="Manual (Python)", + intro=( + "The complete manual page of the Python implementation, the one " + "`git ftp help` and `man git-ftp` show." + ), + link_base=f"{PY_REPO}/blob/main", + links={ + "COMPATIBILITY.md": "compatibility.md", + "CHANGELOG.md": f"{PY_REPO}/blob/main/CHANGELOG.md", + }, + ), + Source( + target="manual-bash.md", + raw_url=f"{SH_RAW}/man/git-ftp.1.md", + local_path=Path("../git-ftp/man/git-ftp.1.md"), + blob_url=f"{SH_REPO}/blob/master/man/git-ftp.1.md", + title="Manual — git-ftp (Bash)", + nav_title="Manual (Bash)", + intro=( + "The complete manual page of the original Bash implementation, " + "the one `git ftp help` and `man git-ftp` show." + ), + link_base=f"{SH_REPO}/blob/master", + drop=[ + r"^This is the manual for version", + r"^Please consider the \[changelog\]", + r"^the _Branch > Tags_ select above", + ], + links={ + "../CHANGELOG.md": f"{SH_REPO}/blob/master/CHANGELOG.md", + "../INSTALL.md": "../getting-started/install.md", + }, + ), + Source( + target="compatibility.md", + raw_url=f"{PY_RAW}/COMPATIBILITY.md", + local_path=Path("../git-ftp-py/COMPATIBILITY.md"), + blob_url=f"{PY_REPO}/blob/main/COMPATIBILITY.md", + title="Compatibility between the implementations", + nav_title="Compatibility", + intro=( + "What the Python port keeps identical to the Bash original, which " + "of its bugs it fixes, and where it deliberately behaves " + "differently." + ), + link_base=f"{PY_REPO}/blob/main", + ), +] + + +def read(source: Source, local: bool) -> str: + if local: + path = (ROOT / source.local_path).resolve() + if not path.is_file(): + sys.exit(f"missing local checkout: {path}") + return path.read_text(encoding="utf-8") + with urlopen(source.raw_url, timeout=30) as response: # noqa: S310 - fixed https URLs + return response.read().decode("utf-8") + + +def strip_title_block(text: str) -> str: + """Remove the leading pandoc title block ("% title" lines).""" + lines = text.splitlines() + while lines and (lines[0].startswith("%") or not lines[0].strip()): + lines.pop(0) + return "\n".join(lines) + + +def expand_tabs(line: str) -> str: + """Turn leading tabs into four spaces each, so code blocks and definition + list bodies keep their meaning in Python-Markdown.""" + stripped = line.lstrip("\t") + return " " * (len(line) - len(stripped)) + stripped + + +def convert(source: Source, text: str) -> str: + body = strip_title_block(text) + + out: list[str] = [] + for raw_line in body.splitlines(): + if any(re.search(pattern, raw_line) for pattern in source.drop): + continue + line = expand_tabs(raw_line) + # ":definition" has become ": definition"; a definition list + # marker wants the colon plus a space, with the body on column four. + line = re.sub(r"^:[ \t]+", ": ", line) + # pandoc man source escapes angle brackets and backticks; on the web + # they should be an entity and a code span. + line = line.replace("\\<", "<").replace("\\>", ">") + line = line.replace("\\`", "`") + # Demote headings so that the page title stays the only level one. + if line.startswith("#"): + line = "#" + line + out.append(line) + body = "\n".join(out).strip() + + for old, new in source.links.items(): + body = body.replace(f"]({old})", f"]({new})") + # Anything still pointing at a repository file goes to the repository. + body = re.sub( + r"\]\((?!https?:|#|\.\./|[a-z-]+\.md)([A-Za-z0-9_./-]+\.(?:md|sh|1))\)", + rf"]({source.link_base}/\1)", + body, + ) + + header = "\n".join( + [ + "---", + f"title: {source.title}", + "---", + "", + f"# {source.title}", + "", + source.intro, + "", + "!!! info \"Synced from upstream\"", + "", + f" This page is generated from [its source]({source.blob_url})", + " in the tool's repository and is refreshed every time the site", + " is built. Edit it there, not here.", + "", + ] + ) + return f"{header}\n{body}\n" + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument( + "--local", + action="store_true", + help="read from sibling checkouts instead of GitHub", + ) + parser.add_argument( + "--check", + action="store_true", + help="fail instead of writing when a page would change", + ) + args = parser.parse_args() + + REFERENCE.mkdir(parents=True, exist_ok=True) + changed = False + for source in SOURCES: + page = convert(source, read(source, args.local)) + target = REFERENCE / source.target + current = target.read_text(encoding="utf-8") if target.is_file() else None + if current == page: + print(f"unchanged docs/reference/{source.target}") + continue + changed = True + if args.check: + print(f"OUTDATED docs/reference/{source.target}") + continue + target.write_text(page, encoding="utf-8") + print(f"written docs/reference/{source.target}") + + if args.check and changed: + print("\nRun ./scripts/sync_upstream_docs.py to update.", file=sys.stderr) + return 1 + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/stylesheets/github-light.css b/stylesheets/github-light.css deleted file mode 100644 index 872a6f4..0000000 --- a/stylesheets/github-light.css +++ /dev/null @@ -1,116 +0,0 @@ -/* - Copyright 2014 GitHub Inc. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - -*/ - -.pl-c /* comment */ { - color: #969896; -} - -.pl-c1 /* constant, markup.raw, meta.diff.header, meta.module-reference, meta.property-name, support, support.constant, support.variable, variable.other.constant */, -.pl-s .pl-v /* string variable */ { - color: #0086b3; -} - -.pl-e /* entity */, -.pl-en /* entity.name */ { - color: #795da3; -} - -.pl-s .pl-s1 /* string source */, -.pl-smi /* storage.modifier.import, storage.modifier.package, storage.type.java, variable.other, variable.parameter.function */ { - color: #333; -} - -.pl-ent /* entity.name.tag */ { - color: #63a35c; -} - -.pl-k /* keyword, storage, storage.type */ { - color: #a71d5d; -} - -.pl-pds /* punctuation.definition.string, string.regexp.character-class */, -.pl-s /* string */, -.pl-s .pl-pse .pl-s1 /* string punctuation.section.embedded source */, -.pl-sr /* string.regexp */, -.pl-sr .pl-cce /* string.regexp constant.character.escape */, -.pl-sr .pl-sra /* string.regexp string.regexp.arbitrary-repitition */, -.pl-sr .pl-sre /* string.regexp source.ruby.embedded */ { - color: #183691; -} - -.pl-v /* variable */ { - color: #ed6a43; -} - -.pl-id /* invalid.deprecated */ { - color: #b52a1d; -} - -.pl-ii /* invalid.illegal */ { - background-color: #b52a1d; - color: #f8f8f8; -} - -.pl-sr .pl-cce /* string.regexp constant.character.escape */ { - color: #63a35c; - font-weight: bold; -} - -.pl-ml /* markup.list */ { - color: #693a17; -} - -.pl-mh /* markup.heading */, -.pl-mh .pl-en /* markup.heading entity.name */, -.pl-ms /* meta.separator */ { - color: #1d3e81; - font-weight: bold; -} - -.pl-mq /* markup.quote */ { - color: #008080; -} - -.pl-mi /* markup.italic */ { - color: #333; - font-style: italic; -} - -.pl-mb /* markup.bold */ { - color: #333; - font-weight: bold; -} - -.pl-md /* markup.deleted, meta.diff.header.from-file */ { - background-color: #ffecec; - color: #bd2c00; -} - -.pl-mi1 /* markup.inserted, meta.diff.header.to-file */ { - background-color: #eaffea; - color: #55a532; -} - -.pl-mdr /* meta.diff.range */ { - color: #795da3; - font-weight: bold; -} - -.pl-mo /* meta.output */ { - color: #1d3e81; -} - diff --git a/stylesheets/print.css b/stylesheets/print.css deleted file mode 100644 index 7da6db0..0000000 --- a/stylesheets/print.css +++ /dev/null @@ -1,228 +0,0 @@ -html, body, div, span, applet, object, iframe, -h1, h2, h3, h4, h5, h6, p, blockquote, pre, -a, abbr, acronym, address, big, cite, code, -del, dfn, em, img, ins, kbd, q, s, samp, -small, strike, strong, sub, sup, tt, var, -b, u, i, center, -dl, dt, dd, ol, ul, li, -fieldset, form, label, legend, -table, caption, tbody, tfoot, thead, tr, th, td, -article, aside, canvas, details, embed, -figure, figcaption, footer, header, hgroup, -menu, nav, output, ruby, section, summary, -time, mark, audio, video { - padding: 0; - margin: 0; - font: inherit; - font-size: 100%; - vertical-align: baseline; - border: 0; -} -/* HTML5 display-role reset for older browsers */ -article, aside, details, figcaption, figure, -footer, header, hgroup, menu, nav, section { - display: block; -} -body { - line-height: 1; -} -ol, ul { - list-style: none; -} -blockquote, q { - quotes: none; -} -blockquote:before, blockquote:after, -q:before, q:after { - content: ''; - content: none; -} -table { - border-spacing: 0; - border-collapse: collapse; -} -body { - font-family: 'Helvetica Neue', Helvetica, Arial, serif; - font-size: 13px; - line-height: 1.5; - color: #000; -} - -a { - font-weight: bold; - color: #d5000d; -} - -header { - padding-top: 35px; - padding-bottom: 10px; -} - -header h1 { - font-size: 48px; - font-weight: bold; - line-height: 1.2; - color: #303030; - letter-spacing: -1px; -} - -header h2 { - font-size: 24px; - font-weight: normal; - line-height: 1.3; - color: #aaa; - letter-spacing: -1px; -} -#downloads { - display: none; -} -#main_content { - padding-top: 20px; -} - -code, pre { - margin-bottom: 30px; - font-family: Monaco, "Bitstream Vera Sans Mono", "Lucida Console", Terminal; - font-size: 12px; - color: #222; -} - -code { - padding: 0 3px; -} - -pre { - padding: 20px; - overflow: auto; - border: solid 1px #ddd; -} -pre code { - padding: 0; -} - -ul, ol, dl { - margin-bottom: 20px; -} - - -/* COMMON STYLES */ - -table { - width: 100%; - border: 1px solid #ebebeb; -} - -th { - font-weight: 500; -} - -td { - font-weight: 300; - text-align: center; - border: 1px solid #ebebeb; -} - -form { - padding: 20px; - background: #f2f2f2; - -} - - -/* GENERAL ELEMENT TYPE STYLES */ - -h1 { - font-size: 2.8em; -} - -h2 { - margin-bottom: 8px; - font-size: 22px; - font-weight: bold; - color: #303030; -} - -h3 { - margin-bottom: 8px; - font-size: 18px; - font-weight: bold; - color: #d5000d; -} - -h4 { - font-size: 16px; - font-weight: bold; - color: #303030; -} - -h5 { - font-size: 1em; - color: #303030; -} - -h6 { - font-size: .8em; - color: #303030; -} - -p { - margin-bottom: 20px; - font-weight: 300; -} - -a { - text-decoration: none; -} - -p a { - font-weight: 400; -} - -blockquote { - padding: 0 0 0 30px; - margin-bottom: 20px; - font-size: 1.6em; - border-left: 10px solid #e9e9e9; -} - -ul li { - padding-left: 20px; - list-style-position: inside; - list-style: disc; -} - -ol li { - padding-left: 3px; - list-style-position: inside; - list-style: decimal; -} - -dl dd { - font-style: italic; - font-weight: 100; -} - -footer { - padding-top: 20px; - padding-bottom: 30px; - margin-top: 40px; - font-size: 13px; - color: #aaa; -} - -footer a { - color: #666; -} - -/* MISC */ -.clearfix:after { - display: block; - height: 0; - clear: both; - visibility: hidden; - content: '.'; -} - -.clearfix {display: inline-block;} -* html .clearfix {height: 1%;} -.clearfix {display: block;} diff --git a/stylesheets/pygment_trac.css b/stylesheets/pygment_trac.css deleted file mode 100644 index c6a6452..0000000 --- a/stylesheets/pygment_trac.css +++ /dev/null @@ -1,69 +0,0 @@ -.highlight { background: #ffffff; } -.highlight .c { color: #999988; font-style: italic } /* Comment */ -.highlight .err { color: #a61717; background-color: #e3d2d2 } /* Error */ -.highlight .k { font-weight: bold } /* Keyword */ -.highlight .o { font-weight: bold } /* Operator */ -.highlight .cm { color: #999988; font-style: italic } /* Comment.Multiline */ -.highlight .cp { color: #999999; font-weight: bold } /* Comment.Preproc */ -.highlight .c1 { color: #999988; font-style: italic } /* Comment.Single */ -.highlight .cs { color: #999999; font-weight: bold; font-style: italic } /* Comment.Special */ -.highlight .gd { color: #000000; background-color: #ffdddd } /* Generic.Deleted */ -.highlight .gd .x { color: #000000; background-color: #ffaaaa } /* Generic.Deleted.Specific */ -.highlight .ge { font-style: italic } /* Generic.Emph */ -.highlight .gr { color: #aa0000 } /* Generic.Error */ -.highlight .gh { color: #999999 } /* Generic.Heading */ -.highlight .gi { color: #000000; background-color: #ddffdd } /* Generic.Inserted */ -.highlight .gi .x { color: #000000; background-color: #aaffaa } /* Generic.Inserted.Specific */ -.highlight .go { color: #888888 } /* Generic.Output */ -.highlight .gp { color: #555555 } /* Generic.Prompt */ -.highlight .gs { font-weight: bold } /* Generic.Strong */ -.highlight .gu { color: #800080; font-weight: bold; } /* Generic.Subheading */ -.highlight .gt { color: #aa0000 } /* Generic.Traceback */ -.highlight .kc { font-weight: bold } /* Keyword.Constant */ -.highlight .kd { font-weight: bold } /* Keyword.Declaration */ -.highlight .kn { font-weight: bold } /* Keyword.Namespace */ -.highlight .kp { font-weight: bold } /* Keyword.Pseudo */ -.highlight .kr { font-weight: bold } /* Keyword.Reserved */ -.highlight .kt { color: #445588; font-weight: bold } /* Keyword.Type */ -.highlight .m { color: #009999 } /* Literal.Number */ -.highlight .s { color: #d14 } /* Literal.String */ -.highlight .na { color: #008080 } /* Name.Attribute */ -.highlight .nb { color: #0086B3 } /* Name.Builtin */ -.highlight .nc { color: #445588; font-weight: bold } /* Name.Class */ -.highlight .no { color: #008080 } /* Name.Constant */ -.highlight .ni { color: #800080 } /* Name.Entity */ -.highlight .ne { color: #990000; font-weight: bold } /* Name.Exception */ -.highlight .nf { color: #990000; font-weight: bold } /* Name.Function */ -.highlight .nn { color: #555555 } /* Name.Namespace */ -.highlight .nt { color: #000080 } /* Name.Tag */ -.highlight .nv { color: #008080 } /* Name.Variable */ -.highlight .ow { font-weight: bold } /* Operator.Word */ -.highlight .w { color: #bbbbbb } /* Text.Whitespace */ -.highlight .mf { color: #009999 } /* Literal.Number.Float */ -.highlight .mh { color: #009999 } /* Literal.Number.Hex */ -.highlight .mi { color: #009999 } /* Literal.Number.Integer */ -.highlight .mo { color: #009999 } /* Literal.Number.Oct */ -.highlight .sb { color: #d14 } /* Literal.String.Backtick */ -.highlight .sc { color: #d14 } /* Literal.String.Char */ -.highlight .sd { color: #d14 } /* Literal.String.Doc */ -.highlight .s2 { color: #d14 } /* Literal.String.Double */ -.highlight .se { color: #d14 } /* Literal.String.Escape */ -.highlight .sh { color: #d14 } /* Literal.String.Heredoc */ -.highlight .si { color: #d14 } /* Literal.String.Interpol */ -.highlight .sx { color: #d14 } /* Literal.String.Other */ -.highlight .sr { color: #009926 } /* Literal.String.Regex */ -.highlight .s1 { color: #d14 } /* Literal.String.Single */ -.highlight .ss { color: #990073 } /* Literal.String.Symbol */ -.highlight .bp { color: #999999 } /* Name.Builtin.Pseudo */ -.highlight .vc { color: #008080 } /* Name.Variable.Class */ -.highlight .vg { color: #008080 } /* Name.Variable.Global */ -.highlight .vi { color: #008080 } /* Name.Variable.Instance */ -.highlight .il { color: #009999 } /* Literal.Number.Integer.Long */ - -.type-csharp .highlight .k { color: #0000FF } -.type-csharp .highlight .kt { color: #0000FF } -.type-csharp .highlight .nf { color: #000000; font-weight: normal } -.type-csharp .highlight .nc { color: #2B91AF } -.type-csharp .highlight .nn { color: #000000 } -.type-csharp .highlight .s { color: #A31515 } -.type-csharp .highlight .sc { color: #A31515 } diff --git a/stylesheets/stylesheet.css b/stylesheets/stylesheet.css deleted file mode 100644 index 543c951..0000000 --- a/stylesheets/stylesheet.css +++ /dev/null @@ -1,881 +0,0 @@ -/*! normalize.css v3.0.2 | MIT License | git.io/normalize */ - -/** - * 1. Set default font family to sans-serif. - * 2. Prevent iOS text size adjust after orientation change, without disabling - * user zoom. - */ - -html { - font-family: sans-serif; /* 1 */ - -webkit-text-size-adjust: 100%; /* 2 */ - -ms-text-size-adjust: 100%; /* 2 */ -} - -/** - * Remove default margin. - */ - -body { - margin: 0; -} - -/* HTML5 display definitions - ========================================================================== */ - -/** - * Correct `block` display not defined for any HTML5 element in IE 8/9. - * Correct `block` display not defined for `details` or `summary` in IE 10/11 - * and Firefox. - * Correct `block` display not defined for `main` in IE 11. - */ - -article, -aside, -details, -figcaption, -figure, -footer, -header, -hgroup, -main, -menu, -nav, -section, -summary { - display: block; -} - -/** - * 1. Correct `inline-block` display not defined in IE 8/9. - * 2. Normalize vertical alignment of `progress` in Chrome, Firefox, and Opera. - */ - -audio, -canvas, -progress, -video { - display: inline-block; /* 1 */ - vertical-align: baseline; /* 2 */ -} - -/** - * Prevent modern browsers from displaying `audio` without controls. - * Remove excess height in iOS 5 devices. - */ - -audio:not([controls]) { - display: none; - height: 0; -} - -/** - * Address `[hidden]` styling not present in IE 8/9/10. - * Hide the `template` element in IE 8/9/11, Safari, and Firefox < 22. - */ - -[hidden], -template { - display: none; -} - -/* Links - ========================================================================== */ - -/** - * Remove the gray background color from active links in IE 10. - */ - -a { - background-color: transparent; -} - -/** - * Improve readability when focused and also mouse hovered in all browsers. - */ - -a:active, -a:hover { - outline: 0; -} - -/* Text-level semantics - ========================================================================== */ - -/** - * Address styling not present in IE 8/9/10/11, Safari, and Chrome. - */ - -abbr[title] { - border-bottom: 1px dotted; -} - -/** - * Address style set to `bolder` in Firefox 4+, Safari, and Chrome. - */ - -b, -strong { - font-weight: bold; -} - -/** - * Address styling not present in Safari and Chrome. - */ - -dfn { - font-style: italic; -} - -/** - * Address variable `h1` font-size and margin within `section` and `article` - * contexts in Firefox 4+, Safari, and Chrome. - */ - -h1 { - margin: 0.67em 0; - font-size: 2em; -} - -/** - * Address styling not present in IE 8/9. - */ - -mark { - color: #000; - background: #ff0; -} - -/** - * Address inconsistent and variable font size in all browsers. - */ - -small { - font-size: 80%; -} - -/** - * Prevent `sub` and `sup` affecting `line-height` in all browsers. - */ - -sub, -sup { - position: relative; - font-size: 75%; - line-height: 0; - vertical-align: baseline; -} - -sup { - top: -0.5em; -} - -sub { - bottom: -0.25em; -} - -/* Embedded content - ========================================================================== */ - -/** - * Remove border when inside `a` element in IE 8/9/10. - */ - -img { - border: 0; -} - -/** - * Correct overflow not hidden in IE 9/10/11. - */ - -svg:not(:root) { - overflow: hidden; -} - -/* Grouping content - ========================================================================== */ - -/** - * Address margin not present in IE 8/9 and Safari. - */ - -figure { - margin: 1em 40px; -} - -/** - * Address differences between Firefox and other browsers. - */ - -hr { - height: 0; - -moz-box-sizing: content-box; - box-sizing: content-box; -} - -/** - * Contain overflow in all browsers. - */ - -pre { - overflow: auto; -} - -/** - * Address odd `em`-unit font size rendering in all browsers. - */ - -code, -kbd, -pre, -samp { - font-family: monospace, monospace; - font-size: 1em; -} - -/* Forms - ========================================================================== */ - -/** - * Known limitation: by default, Chrome and Safari on OS X allow very limited - * styling of `select`, unless a `border` property is set. - */ - -/** - * 1. Correct color not being inherited. - * Known issue: affects color of disabled elements. - * 2. Correct font properties not being inherited. - * 3. Address margins set differently in Firefox 4+, Safari, and Chrome. - */ - -button, -input, -optgroup, -select, -textarea { - margin: 0; /* 3 */ - font: inherit; /* 2 */ - color: inherit; /* 1 */ -} - -/** - * Address `overflow` set to `hidden` in IE 8/9/10/11. - */ - -button { - overflow: visible; -} - -/** - * Address inconsistent `text-transform` inheritance for `button` and `select`. - * All other form control elements do not inherit `text-transform` values. - * Correct `button` style inheritance in Firefox, IE 8/9/10/11, and Opera. - * Correct `select` style inheritance in Firefox. - */ - -button, -select { - text-transform: none; -} - -/** - * 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio` - * and `video` controls. - * 2. Correct inability to style clickable `input` types in iOS. - * 3. Improve usability and consistency of cursor style between image-type - * `input` and others. - */ - -button, -html input[type="button"], /* 1 */ -input[type="reset"], -input[type="submit"] { - -webkit-appearance: button; /* 2 */ - cursor: pointer; /* 3 */ -} - -/** - * Re-set default cursor for disabled elements. - */ - -button[disabled], -html input[disabled] { - cursor: default; -} - -/** - * Remove inner padding and border in Firefox 4+. - */ - -button::-moz-focus-inner, -input::-moz-focus-inner { - padding: 0; - border: 0; -} - -/** - * Address Firefox 4+ setting `line-height` on `input` using `!important` in - * the UA stylesheet. - */ - -input { - line-height: normal; -} - -/** - * It's recommended that you don't attempt to style these elements. - * Firefox's implementation doesn't respect box-sizing, padding, or width. - * - * 1. Address box sizing set to `content-box` in IE 8/9/10. - * 2. Remove excess padding in IE 8/9/10. - */ - -input[type="checkbox"], -input[type="radio"] { - box-sizing: border-box; /* 1 */ - padding: 0; /* 2 */ -} - -/** - * Fix the cursor style for Chrome's increment/decrement buttons. For certain - * `font-size` values of the `input`, it causes the cursor style of the - * decrement button to change from `default` to `text`. - */ - -input[type="number"]::-webkit-inner-spin-button, -input[type="number"]::-webkit-outer-spin-button { - height: auto; -} - -/** - * 1. Address `appearance` set to `searchfield` in Safari and Chrome. - * 2. Address `box-sizing` set to `border-box` in Safari and Chrome - * (include `-moz` to future-proof). - */ - -input[type="search"] { - -webkit-box-sizing: content-box; /* 2 */ - -moz-box-sizing: content-box; - box-sizing: content-box; - -webkit-appearance: textfield; /* 1 */ -} - -/** - * Remove inner padding and search cancel button in Safari and Chrome on OS X. - * Safari (but not Chrome) clips the cancel button when the search input has - * padding (and `textfield` appearance). - */ - -input[type="search"]::-webkit-search-cancel-button, -input[type="search"]::-webkit-search-decoration { - -webkit-appearance: none; -} - -/** - * Define consistent border, margin, and padding. - */ - -fieldset { - padding: 0.35em 0.625em 0.75em; - margin: 0 2px; - border: 1px solid #c0c0c0; -} - -/** - * 1. Correct `color` not being inherited in IE 8/9/10/11. - * 2. Remove padding so people aren't caught out if they zero out fieldsets. - */ - -legend { - padding: 0; /* 2 */ - border: 0; /* 1 */ -} - -/** - * Remove default vertical scrollbar in IE 8/9/10/11. - */ - -textarea { - overflow: auto; -} - -/** - * Don't inherit the `font-weight` (applied by a rule above). - * NOTE: the default cannot safely be changed in Chrome and Safari on OS X. - */ - -optgroup { - font-weight: bold; -} - -/* Tables - ========================================================================== */ - -/** - * Remove most spacing between table cells. - */ - -table { - border-spacing: 0; - border-collapse: collapse; -} - -td, -th { - padding: 0; -} - -/* LAYOUT STYLES */ -body { - font-family: 'Helvetica Neue', Helvetica, Arial, serif; - font-size: 15px; - font-weight: 400; - line-height: 1.5; - color: #666; - background: #fafafa url(../images/body-bg.jpg) 0 0 repeat; -} - -p { - margin-top: 0; -} - -a { - color: #2879d0; -} -a:hover { - color: #2268b2; -} - -header { - padding-top: 40px; - padding-bottom: 40px; - font-family: 'Architects Daughter', 'Helvetica Neue', Helvetica, Arial, serif; - background: #2e7bcf url(../images/header-bg.jpg) 0 0 repeat-x; - border-bottom: solid 1px #275da1; -} - -header h1 { - width: 540px; - margin-top: 0; - margin-bottom: 0.2em; - font-size: 72px; - font-weight: normal; - line-height: 1; - color: #fff; - letter-spacing: -1px; -} - -header h2 { - width: 540px; - margin-top: 0; - margin-bottom: 0; - font-size: 26px; - font-weight: normal; - line-height: 1.3; - color: #9ddcff; - letter-spacing: 0; -} - -.inner { - position: relative; - width: 940px; - margin: 0 auto; -} - -#content-wrapper { - padding-top: 30px; - border-top: solid 1px #fff; -} - -#main-content { - float: left; - width: 690px; -} - -#main-content img { - max-width: 100%; -} - -aside#sidebar { - float: right; - width: 200px; - min-height: 504px; - padding-left: 20px; - font-size: 12px; - line-height: 1.3; - background: transparent url(../images/sidebar-bg.jpg) 0 0 no-repeat; -} - -aside#sidebar p.repo-owner, -aside#sidebar p.repo-owner a { - font-weight: bold; -} - -#downloads { - margin-bottom: 40px; -} - -a.button { - width: 134px; - height: 58px; - padding-top: 22px; - padding-left: 68px; - font-family: 'Architects Daughter', 'Helvetica Neue', Helvetica, Arial, serif; - font-size: 23px; - line-height: 1.2; - color: #fff; -} -a.button small { - display: block; - font-size: 11px; -} -header a.button { - position: absolute; - top: 0; - right: 0; - background: transparent url(../images/github-button.png) 0 0 no-repeat; -} -aside a.button { - display: block; - width: 138px; - padding-left: 64px; - margin-bottom: 20px; - font-size: 21px; - background: transparent url(../images/download-button.png) 0 0 no-repeat; -} - -code, pre { - margin-bottom: 30px; - font-family: Monaco, "Bitstream Vera Sans Mono", "Lucida Console", Terminal, monospace; - font-size: 13px; - color: #222; -} - -code { - padding: 0 3px; - background-color: #f2f8fc; - border: solid 1px #dbe7f3; -} - -pre { - padding: 20px; - overflow: auto; - text-shadow: none; - background: #fff; - border: solid 1px #f2f2f2; -} -pre code { - padding: 0; - color: #2879d0; - background-color: #fff; - border: none; -} - -ul, ol, dl { - margin-bottom: 20px; -} - - -/* COMMON STYLES */ - -hr { - height: 0; - margin-top: 1em; - margin-bottom: 1em; - border: 0; - border-top: solid 1px #ddd; -} - -table { - width: 100%; - border: 1px solid #ebebeb; -} - -th { - font-weight: 500; -} - -td { - font-weight: 300; - text-align: center; - border: 1px solid #ebebeb; -} - -form { - padding: 20px; - background: #f2f2f2; - -} - - -/* GENERAL ELEMENT TYPE STYLES */ - -#main-content h1 { - margin-top: 0; - margin-bottom: 0; - font-family: 'Architects Daughter', 'Helvetica Neue', Helvetica, Arial, serif; - font-size: 2.8em; - font-weight: normal; - color: #474747; - text-indent: 6px; - letter-spacing: -1px; -} - -#main-content h1:before { - padding-right: 0.3em; - margin-left: -0.9em; - color: #9ddcff; - content: "/"; -} - -#main-content h2 { - margin-bottom: 8px; - font-family: 'Architects Daughter', 'Helvetica Neue', Helvetica, Arial, serif; - font-size: 22px; - font-weight: bold; - color: #474747; - text-indent: 4px; -} -#main-content h2:before { - padding-right: 0.3em; - margin-left: -1.5em; - content: "//"; - color: #9ddcff; -} - -#main-content h3 { - margin-top: 24px; - margin-bottom: 8px; - font-family: 'Architects Daughter', 'Helvetica Neue', Helvetica, Arial, serif; - font-size: 18px; - font-weight: bold; - color: #474747; - text-indent: 3px; -} - -#main-content h3:before { - padding-right: 0.3em; - margin-left: -2em; - content: "///"; - color: #9ddcff; -} - -#main-content h4 { - margin-bottom: 8px; - font-family: 'Architects Daughter', 'Helvetica Neue', Helvetica, Arial, serif; - font-size: 15px; - font-weight: bold; - color: #474747; - text-indent: 3px; -} - -h4:before { - padding-right: 0.3em; - margin-left: -2.8em; - content: "////"; - color: #9ddcff; -} - -#main-content h5 { - margin-bottom: 8px; - font-family: 'Architects Daughter', 'Helvetica Neue', Helvetica, Arial, serif; - font-size: 14px; - color: #474747; - text-indent: 3px; -} -h5:before { - padding-right: 0.3em; - margin-left: -3.2em; - content: "/////"; - color: #9ddcff; -} - -#main-content h6 { - margin-bottom: 8px; - font-family: 'Architects Daughter', 'Helvetica Neue', Helvetica, Arial, serif; - font-size: .8em; - color: #474747; - text-indent: 3px; -} -h6:before { - padding-right: 0.3em; - margin-left: -3.7em; - content: "//////"; - color: #9ddcff; -} - -p { - margin-bottom: 20px; -} - -a { - text-decoration: none; -} - -p a { - font-weight: 400; -} - -blockquote { - padding: 0 0 0 30px; - margin-bottom: 20px; - font-size: 1.6em; - border-left: 10px solid #e9e9e9; -} - -ul { - list-style-position: inside; - list-style: disc; - padding-left: 20px; -} - -ol { - list-style-position: inside; - list-style: decimal; - padding-left: 3px; -} - -dl dd { - font-style: italic; - font-weight: 100; -} - -footer { - padding-top: 20px; - padding-bottom: 30px; - margin-top: 40px; - font-size: 13px; - color: #aaa; - background: transparent url('../images/hr.png') 0 0 no-repeat; -} - -footer a { - color: #666; -} -footer a:hover { - color: #444; -} - -/* MISC */ -.clearfix:after { - display: block; - height: 0; - clear: both; - visibility: hidden; - content: '.'; -} - -.clearfix {display: inline-block;} -* html .clearfix {height: 1%;} -.clearfix {display: block;} - -/* #Media Queries -================================================== */ - -/* Smaller than standard 960 (devices and browsers) */ -@media only screen and (max-width: 959px) { } - -/* Tablet Portrait size to standard 960 (devices and browsers) */ -@media only screen and (min-width: 768px) and (max-width: 959px) { - .inner { - width: 740px; - } - header h1, header h2 { - width: 340px; - } - header h1 { - font-size: 60px; - } - header h2 { - font-size: 30px; - } - #main-content { - width: 490px; - } - #main-content h1:before, - #main-content h2:before, - #main-content h3:before, - #main-content h4:before, - #main-content h5:before, - #main-content h6:before { - padding-right: 0; - margin-left: 0; - content: none; - } -} - -/* All Mobile Sizes (devices and browser) */ -@media only screen and (max-width: 767px) { - .inner { - width: 93%; - } - header { - padding: 20px 0; - } - header .inner { - position: relative; - } - header h1, header h2 { - width: 100%; - } - header h1 { - font-size: 48px; - } - header h2 { - font-size: 24px; - } - header a.button { - position: relative; - display: inline-block; - width: auto; - height: auto; - padding: 5px 10px; - margin-top: 15px; - font-size: 13px; - line-height: 1; - color: #2879d0; - text-align: center; - background-color: #9ddcff; - background-image: none; - border-radius: 5px; - -moz-border-radius: 5px; - -webkit-border-radius: 5px; - } - header a.button small { - display: inline; - font-size: 13px; - } - #main-content, - aside#sidebar { - float: none; - width: 100% ! important; - } - aside#sidebar { - min-height: 0; - padding: 20px 0; - margin-top: 20px; - background-image: none; - border-top: solid 1px #ddd; - } - aside#sidebar a.button { - display: none; - } - #main-content h1:before, - #main-content h2:before, - #main-content h3:before, - #main-content h4:before, - #main-content h5:before, - #main-content h6:before { - padding-right: 0; - margin-left: 0; - content: none; - } -} - -/* Mobile Landscape Size to Tablet Portrait (devices and browsers) */ -@media only screen and (min-width: 480px) and (max-width: 767px) { } - -/* Mobile Portrait Size to Mobile Landscape Size (devices and browsers) */ -@media only screen and (max-width: 479px) { } - diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..3b41afd --- /dev/null +++ b/uv.lock @@ -0,0 +1,642 @@ +version = 1 +revision = 3 +requires-python = ">=3.10" + +[[package]] +name = "babel" +version = "2.18.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/b2/51899539b6ceeeb420d40ed3cd4b7a40519404f9baf3d4ac99dc413a834b/babel-2.18.0.tar.gz", hash = "sha256:b80b99a14bd085fcacfa15c9165f651fbb3406e66cc603abf11c5750937c992d", size = 9959554, upload-time = "2026-02-01T12:30:56.078Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/f5/21d2de20e8b8b0408f0681956ca2c69f1320a3848ac50e6e7f39c6159675/babel-2.18.0-py3-none-any.whl", hash = "sha256:e2b422b277c2b9a9630c1d7903c2a00d0830c409c59ac8cae9081c92f1aeba35", size = 10196845, upload-time = "2026-02-01T12:30:53.445Z" }, +] + +[[package]] +name = "backrefs" +version = "8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ec/56/4744bcd0c82184e80c52b0ac4076c261a8ffa1f1b343ff2f6e89ce0e1cef/backrefs-8.0.tar.gz", hash = "sha256:b556cd7d36c3a3a2f256b89590b176b8eddfb73bcfaee3a3ddd84ea66d21ce50", size = 7013081, upload-time = "2026-07-26T19:54:24.638Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e3/fd/9bf53b6a6f6f519ffaac765df2f2a25e5c2fc6d32cfd2b2747099e72c911/backrefs-8.0-py310-none-any.whl", hash = "sha256:4a627b817fd2dce43b79ab48da63613340509381cd8ce0897078a0bce79a2ab8", size = 380377, upload-time = "2026-07-26T19:54:17.457Z" }, + { url = "https://files.pythonhosted.org/packages/e1/29/4bd7ae72a2634da00379c2b3bcc5439e7c94620235c6afea8af15229a973/backrefs-8.0-py311-none-any.whl", hash = "sha256:f0c35cf0102ba6b6070c12a492be3c1c1d3f5839529784b9a9565d6d04569a01", size = 392169, upload-time = "2026-07-26T19:54:18.782Z" }, + { url = "https://files.pythonhosted.org/packages/29/13/232505664e8e2a0c7a2eb0c505cfade9d715538f89a5d62bc4c272968f62/backrefs-8.0-py312-none-any.whl", hash = "sha256:87f0fae8c5f207fe9f4b2887efc71d42f4900ac78faa1af08d675ef303692dc5", size = 398084, upload-time = "2026-07-26T19:54:19.954Z" }, + { url = "https://files.pythonhosted.org/packages/8a/69/47a3dc20abc4fa5486655fde681bd55e63211b46c886d8c02223d6468431/backrefs-8.0-py313-none-any.whl", hash = "sha256:601ce68ca12385dbda06ce264406b4c4210cf5b79fd0fd627592365c92f29a88", size = 400040, upload-time = "2026-07-26T19:54:21.194Z" }, + { url = "https://files.pythonhosted.org/packages/1c/cf/e5f9b68a5b0e939a2fb933a66c20180d0c9241bf8927f7a47fa48c1675e9/backrefs-8.0-py314-none-any.whl", hash = "sha256:9ec96efa080938be92323e8e730e57718c9c88eb15ad70bbef4e1766df591408", size = 411903, upload-time = "2026-07-26T19:54:23.221Z" }, +] + +[[package]] +name = "certifi" +version = "2026.7.22" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a3/c2/24167ea9858356b47a87a50d39908bfdb72ceeefe0041586e704e5376b3a/certifi-2026.7.22.tar.gz", hash = "sha256:741e2c3b351ddf169a738da9f2c048608ff7f2c5cc02f1ebc6b118bb090d5d55", size = 138112, upload-time = "2026-07-22T03:35:12.644Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/a7/71ac2cff56fec219ed242bb11b8efb69fcc4bec75db06fb7bfe35de520e6/certifi-2026.7.22-py3-none-any.whl", hash = "sha256:62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775", size = 136983, upload-time = "2026-07-22T03:35:11.276Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e5/3f/143b048436775b0f76ac3eec145c019e8173ccc2885c8f20319b996d5e83/charset_normalizer-3.5.1.tar.gz", hash = "sha256:6117b84ea48435e5356dc737f5121485c30920ba43375fa7b434fd753df0eac3", size = 171764, upload-time = "2026-08-15T08:20:44.807Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/aa/554e2614f38fc34c58ff1d0911ae8535ad2516440d5482d76fe59f1088b0/charset_normalizer-3.5.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d1ee1e296209fdce05b81b663250eefa02213a2da7b41bf26f7829b8ba3545aa", size = 369072, upload-time = "2026-08-15T08:16:22.964Z" }, + { url = "https://files.pythonhosted.org/packages/03/6d/439231dfc3ccfa6f8c06477b7da2219cbd41a2de3d49084df8ec7b5100f2/charset_normalizer-3.5.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e9fbdce1e47394b09bc9f26ab117dfc8d6491977a11d86f592bb42c779db2fda", size = 251142, upload-time = "2026-08-15T08:16:24.81Z" }, + { url = "https://files.pythonhosted.org/packages/55/53/7d819bd23a00ef45039146fa2cce1daa2f0771e758c5653ee1f6edac91ed/charset_normalizer-3.5.1-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:00668ebb0609751758682eb0b5857e7c35b9f00e84dfdef062e103244ec94d45", size = 240714, upload-time = "2026-08-15T08:16:26.392Z" }, + { url = "https://files.pythonhosted.org/packages/b2/2c/45847198c16f4b38090cc7423b2b6a9008e438704d8ab413211832498d31/charset_normalizer-3.5.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ba2f37ee79e6338845261a3c5b1784e5d1acdff2c0785b284f1b633033d136ab", size = 279637, upload-time = "2026-08-15T08:16:27.961Z" }, + { url = "https://files.pythonhosted.org/packages/69/2b/d8be3523ddf9f0b0f3e56d1359034aa10653a4d11564c697f802b4775766/charset_normalizer-3.5.1-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ce854f5f478050ade5a238731c4ca985a7d3b3cb53ff600a9b5c3b689b5f0a7a", size = 276543, upload-time = "2026-08-15T08:16:29.399Z" }, + { url = "https://files.pythonhosted.org/packages/32/cd/4f564b8f132de25db594efc706897069f016790cea63a5669c9df2675f64/charset_normalizer-3.5.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:96eefc178f8636b9c760c5829345307fd81cfae9ab1e80997dbddeb0f54ee9a3", size = 261644, upload-time = "2026-08-15T08:16:30.722Z" }, + { url = "https://files.pythonhosted.org/packages/f5/e3/38b975422534a608f98c360e79c2f07c763d66dd4272300d45fb1fee54b0/charset_normalizer-3.5.1-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:366ec70f5547c640d3ce1985722490f23faf4eb5216a7eeba78277490e78dacb", size = 259609, upload-time = "2026-08-15T08:16:32.248Z" }, + { url = "https://files.pythonhosted.org/packages/87/bd/fbc24d825c66f1c74f6ccdea3742c3d8354a4888e86d1315a197fee69061/charset_normalizer-3.5.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:950f23cb393f85543777b0433f082cddd25b51ab398eac7971146495679efe5f", size = 252457, upload-time = "2026-08-15T08:16:33.849Z" }, + { url = "https://files.pythonhosted.org/packages/b9/2d/918d0e98a0e679469ed05bb2d90c2088b4d315bb612969d8499f76fb5210/charset_normalizer-3.5.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:c1dcc36dcb96abc02236e182d17e0f71430152a6c2c7447421da2d2dc144edea", size = 242240, upload-time = "2026-08-15T08:16:35.396Z" }, + { url = "https://files.pythonhosted.org/packages/20/c8/c36f6e0b2dfec351bd38cbc05362697e58bcd073d7dbd95154290c9714ce/charset_normalizer-3.5.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:07ffd07412fc5d5e84cd8952acf9ff7e4ed7a708e69d1bada19d8ba91711353f", size = 280308, upload-time = "2026-08-15T08:16:36.825Z" }, + { url = "https://files.pythonhosted.org/packages/ca/7b/311b3e02e8c4092400c449c850a760d8c45d900983c83a70cc07208c551d/charset_normalizer-3.5.1-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:f5542f9b941279d82d41eb0aa9f98eba36fe4df5c7086c651df7944935b37182", size = 258679, upload-time = "2026-08-15T08:16:38.22Z" }, + { url = "https://files.pythonhosted.org/packages/b9/90/082cc45599c392f28c036a497f49e0634041a785fc3849c80ccf396d096f/charset_normalizer-3.5.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:a545775cfe815855ea32d7c27731d79da358ef2055b4a25830231b1622dd18aa", size = 277221, upload-time = "2026-08-15T08:16:39.62Z" }, + { url = "https://files.pythonhosted.org/packages/58/ad/b9aecf38d805cbcf84fa94f14c5d972a16561e20296a11dc799a5dcf3763/charset_normalizer-3.5.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:494b70049a4d69aec6e8137c13af4cf8db8c9f9820a1392ac293b0dd2987a818", size = 263799, upload-time = "2026-08-15T08:16:40.885Z" }, + { url = "https://files.pythonhosted.org/packages/b7/23/b38a20598d5a825f85d9d7636860e56ff0db1479f86497a6e485aa9326f7/charset_normalizer-3.5.1-cp310-cp310-win32.whl", hash = "sha256:94fbf1c0c6cc0d3d5e50f9a9313a8cdca90dd696d34b381cd1704f8c9e939f20", size = 182037, upload-time = "2026-08-15T08:16:42.198Z" }, + { url = "https://files.pythonhosted.org/packages/d2/21/83fffb77864408b8bf0fe1ca603926401d6f8775a8e150b39aacc9958f8a/charset_normalizer-3.5.1-cp310-cp310-win_amd64.whl", hash = "sha256:be47f99644b208bff7766314013f9acf57b056b04191d570d68ad14022cf5b1d", size = 206030, upload-time = "2026-08-15T08:16:43.787Z" }, + { url = "https://files.pythonhosted.org/packages/86/2e/b93135b5034b1157fb29554b0d06d4844ce62282f0e0a14036f93d7ee2e7/charset_normalizer-3.5.1-cp310-cp310-win_arm64.whl", hash = "sha256:a6d095662e73e74f0a49988e0593373e243e3a52e27bfeea0a859e88acf4a0f5", size = 185092, upload-time = "2026-08-15T08:16:45.177Z" }, + { url = "https://files.pythonhosted.org/packages/6a/b6/034f6802e9c3f6418966cfabb7db8c9252cc2429c5098f41cc43af804149/charset_normalizer-3.5.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:eda059b6bc8bc0812d626fd91a7ce01bf583df0a61296eff390fd94141a34e30", size = 363585, upload-time = "2026-08-15T08:16:46.646Z" }, + { url = "https://files.pythonhosted.org/packages/d5/fa/6a7e2a7c4b5451912b8c417732df79574354443592a88d616de03da66ae5/charset_normalizer-3.5.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa2bb0b37202dca27175591f761108b5d34096ade1191ffe4808bdf6b1571488", size = 251189, upload-time = "2026-08-15T08:16:48.287Z" }, + { url = "https://files.pythonhosted.org/packages/a4/c8/ab42b07cfd82e919f427fcfaa7c41abae8242833ad1aad66d42bae40b669/charset_normalizer-3.5.1-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0b2b1b3fa5670c127b246df1d0c059defd41f689a868a3b9d79df9b1cac42d22", size = 239724, upload-time = "2026-08-15T08:16:49.67Z" }, + { url = "https://files.pythonhosted.org/packages/e7/80/b9348b5d3041209f98b4cdad7655766369233f1d533f4f4f7558e9717bec/charset_normalizer-3.5.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6e5e4d73d588ca5ed09df1b7dcd1b203d1df3c542e3f50d126c947d432b10731", size = 280078, upload-time = "2026-08-15T08:16:51.228Z" }, + { url = "https://files.pythonhosted.org/packages/82/38/083a24028304bc85bb9e376fed801178423dcbb67495f73b6ea0624e1894/charset_normalizer-3.5.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b54e7e13267d49ffbfe68e25b3cbd774dab38fa37238f71265e91b36146eb21c", size = 276650, upload-time = "2026-08-15T08:16:52.625Z" }, + { url = "https://files.pythonhosted.org/packages/0d/35/731ac04aa0a097fc1c97f0994c375bdb230c6c96619db794208fe664e9ce/charset_normalizer-3.5.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c7b742bf31c88566b4bb6335a7f393bb322e580b6bb98df7bd0c25e6e3519ce8", size = 262325, upload-time = "2026-08-15T08:16:54.085Z" }, + { url = "https://files.pythonhosted.org/packages/f5/28/c2028e7021fb89c6e56868ed0e387b8e9aa811abdd2ab3208d6578d2c930/charset_normalizer-3.5.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6ba32c4d2abf1d2fe7cf27d280f4cca5664233b0f885549c7761719eb977f486", size = 261140, upload-time = "2026-08-15T08:16:55.604Z" }, + { url = "https://files.pythonhosted.org/packages/28/f0/0c0ceec6d98b7daa62e361e418135d59685811d79ba11529aad5cdf15e84/charset_normalizer-3.5.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0722590aabf9dc6a6c0343d523c05458fa2b5047dbe6302fd526bb570600753f", size = 252791, upload-time = "2026-08-15T08:16:57.103Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3e/48f4cd187b1c33189d86039e9cbe4f92c05454175504b44ff81806d4d1bf/charset_normalizer-3.5.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:aa1099b956fb795e686d073568f6dc002a0bb89765ea6d5b055dd7d9bf1b116c", size = 240730, upload-time = "2026-08-15T08:16:58.418Z" }, + { url = "https://files.pythonhosted.org/packages/42/85/f9e22af69af67c54cce42be9455d9c81294f918b4ccc454db01f66efcac2/charset_normalizer-3.5.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:bd6c173f04743d483881bffa1478d5a4624475b8cd1d2194956a75548e191c18", size = 280791, upload-time = "2026-08-15T08:16:59.918Z" }, + { url = "https://files.pythonhosted.org/packages/fd/4c/9044135f42127630b6fa742feb51256353f6ab87a78f2fdd1de3de955a7f/charset_normalizer-3.5.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:f298e218441525d3794428b4c8b8fb8662c6d3ea79925d4807ee6b9a96a3bca5", size = 259598, upload-time = "2026-08-15T08:17:01.421Z" }, + { url = "https://files.pythonhosted.org/packages/ba/ed/1dd7cfebb4e75812934c49ca3b79757d11948053f7937ab7070c151f3c55/charset_normalizer-3.5.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:6e2912d4babbc65196ac13c2f53468dc57fb8b9c25ef913e8c59ddf7c6dc0e1b", size = 278217, upload-time = "2026-08-15T08:17:02.782Z" }, + { url = "https://files.pythonhosted.org/packages/bf/eb/239c84503cc9e3ba6eb34686a24bc66e84f3924efdd7e38e751a19f6bc10/charset_normalizer-3.5.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3d27167433c0d5f18dc850f07d0b3816221984fecdc405d6c157a6f0b8f8e9e6", size = 263417, upload-time = "2026-08-15T08:17:04.216Z" }, + { url = "https://files.pythonhosted.org/packages/37/ab/4e4510e1e288478e2c8333131d1c1382382ba8cd2165053c79e39d1da961/charset_normalizer-3.5.1-cp311-cp311-win32.whl", hash = "sha256:ac00177c4831ffa650f8609e4bdddd5fe09c03b1c0c47acece7e6ea20421598b", size = 181774, upload-time = "2026-08-15T08:17:05.58Z" }, + { url = "https://files.pythonhosted.org/packages/e3/57/32f0ccea59e8612057c61d6fd22ef2cb63cca93c9fe594094919696ac170/charset_normalizer-3.5.1-cp311-cp311-win_amd64.whl", hash = "sha256:f9b1e28d0e8dbfa858abdba91d6b547beaf2df1a59bec6da6faae7b96a4991a9", size = 206653, upload-time = "2026-08-15T08:17:07.075Z" }, + { url = "https://files.pythonhosted.org/packages/17/d4/b65c433fc521e58b5f54293982a5e51c05cb5f2dd3f1c7a6acb65b75324e/charset_normalizer-3.5.1-cp311-cp311-win_arm64.whl", hash = "sha256:ae31a1a1db2ee6cc2942fccaf695c934bc7f3db9f2133a3fef1f367cf1a4ab10", size = 185630, upload-time = "2026-08-15T08:17:08.502Z" }, + { url = "https://files.pythonhosted.org/packages/30/27/78873dc8b6a56357517b74b6bb9568b80450e7bb4f6ef7e3fa9d22aa0bd7/charset_normalizer-3.5.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:5b6d1386bf0096d26d3a863dc0a487a5b4eb9aa93cf5ba69683d29dde6b9d60f", size = 344456, upload-time = "2026-08-15T08:17:10.072Z" }, + { url = "https://files.pythonhosted.org/packages/9a/4c/be49ada26b1f0232d57aa89bbebf997a5cc2332a5616b6eca26ff680044d/charset_normalizer-3.5.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4582c27e8c889d64811987b5967fbd3ae0c823fe1fd933b543d55ac20bb475fa", size = 238530, upload-time = "2026-08-15T08:17:11.563Z" }, + { url = "https://files.pythonhosted.org/packages/76/84/6f1290fa07ae6978d3960caa3eb1b8019bf9284ab7c2297b00c099ef4250/charset_normalizer-3.5.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:1d1c7a53a6c2103925cdd6d7229f8c567379f211c869793df679f2e9f738c369", size = 230200, upload-time = "2026-08-15T08:17:12.919Z" }, + { url = "https://files.pythonhosted.org/packages/e7/a0/47b18adeed31c8f16ba9700f32c1b18594cfa09f47eb672a488c273c22bf/charset_normalizer-3.5.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e6621fb2a4988d6e53eedc455e5903e2679f3967b8acb3d639f1b63c14a2e893", size = 262222, upload-time = "2026-08-15T08:17:14.571Z" }, + { url = "https://files.pythonhosted.org/packages/38/fe/341861ac118dae06f3ec0eb487488af52128f2ef2faf0b11003944d22259/charset_normalizer-3.5.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7c0c10730342b0c9b35dd1d619beb8214e520bd96a1f870f452680b238aab3e0", size = 258951, upload-time = "2026-08-15T08:17:16.158Z" }, + { url = "https://files.pythonhosted.org/packages/6f/89/bb5108dc6c3651dca963f2b0a3ba19bbcb370c94e1b6d3e0e844a58e6dca/charset_normalizer-3.5.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b9af956078716df40d985fb0dfeb2c2120c5ca92ba4ff4b388acfd01cdc14d08", size = 248801, upload-time = "2026-08-15T08:17:17.683Z" }, + { url = "https://files.pythonhosted.org/packages/b1/ba/ef83ae3aca816393decfa3530976f38a79812d707b80b580ac33b83f9877/charset_normalizer-3.5.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f9f8405c2c758532c74fed975dbee57be1f31a6e865c031870c79a6ed3212ada", size = 244070, upload-time = "2026-08-15T08:17:19.191Z" }, + { url = "https://files.pythonhosted.org/packages/f6/0b/c5292a2462d69b7378ea89793bbb5b2b6fcf6f7dd6d1667f9619094ad553/charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:96fef3e886d6a9874b14f27fc193fbdc69d5d8035783d86aa4e1cea594e695f9", size = 240110, upload-time = "2026-08-15T08:17:20.547Z" }, + { url = "https://files.pythonhosted.org/packages/46/22/111e5be3b740d5c2a5bfcedb3d237b6591e5c2e82ae9d6ffcb121fe0909c/charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5d8531a6569d025f68e2321e7638fb7978f23db58e5f69f56913837aae03816e", size = 232836, upload-time = "2026-08-15T08:17:21.895Z" }, + { url = "https://files.pythonhosted.org/packages/f9/d2/d2aad6fe0dbb44b194bf3becb60f5a0ac48446ade999a47fe7bb41eb09a7/charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:aae2ee51122d3ae968a3837d97dc24a0aeebb0dea23694422cd172bd30017cd6", size = 262712, upload-time = "2026-08-15T08:17:23.727Z" }, + { url = "https://files.pythonhosted.org/packages/35/5a/337e4663a5eae6de99db940ee8066d4145caafb61327db62deda15313cce/charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:7235dc28fc6dd9d832ac7c7bce95367dedb85929f17368a0c2bee1e080b9acbf", size = 242977, upload-time = "2026-08-15T08:17:25.157Z" }, + { url = "https://files.pythonhosted.org/packages/ca/85/f82f8a92e31c7519410e2e1afdc630f28ec47490ce2c09a11c1a43cbb459/charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:4abdc5f9ad448c1ecbfae2974b820535d6bc6e7eef63babbab3d81cf46968c71", size = 260207, upload-time = "2026-08-15T08:17:26.602Z" }, + { url = "https://files.pythonhosted.org/packages/b7/52/643d11ffd60e9ac2fd1fb87e167a19285b9eefeff4a40e63c87cbfbeab36/charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ba501e667c17d8411f98e67a022d9604ef179aff0e459b7e292c796837c13573", size = 250562, upload-time = "2026-08-15T08:17:27.971Z" }, + { url = "https://files.pythonhosted.org/packages/62/16/46556278c2168d12df9da7fede5dc6fc70e60301b26a82bbeec238c9cfe3/charset_normalizer-3.5.1-cp312-cp312-win32.whl", hash = "sha256:cfa1c0cc3a8f9f53f1243a5a99ac36fd003880199383b37672e86ddda9cb07e2", size = 178507, upload-time = "2026-08-15T08:17:29.277Z" }, + { url = "https://files.pythonhosted.org/packages/9d/7a/4c6c298171e6b3e745633180ff59350fc0ca0db1ffd28df1e369e0579f71/charset_normalizer-3.5.1-cp312-cp312-win_amd64.whl", hash = "sha256:3617ac3cfd8b9888f145ad89dd6e692285834b0201c6074a5eeaad3fd4d668c2", size = 200551, upload-time = "2026-08-15T08:17:30.668Z" }, + { url = "https://files.pythonhosted.org/packages/cd/d7/eb95a042f0dd22e304b0b6472b154f3546a1a039a9ee89ccb2a7f61591fc/charset_normalizer-3.5.1-cp312-cp312-win_arm64.whl", hash = "sha256:88e85ab89cb822c1e635f51d6d32e488f94e002e70e2f492bdb8b945543f345a", size = 180700, upload-time = "2026-08-15T08:17:32.028Z" }, + { url = "https://files.pythonhosted.org/packages/bc/61/2cb6ad133dbbb449fa2d37ccae973232f4827e799af258d15e589a3d1e9e/charset_normalizer-3.5.1-cp313-cp313-android_24_arm64_v8a.whl", hash = "sha256:4f298bdadb8f0b9e5672877f647d1be9373ef5320c9e2f049795e26cad28b6a9", size = 211584, upload-time = "2026-08-15T08:17:33.597Z" }, + { url = "https://files.pythonhosted.org/packages/18/57/a305c968be1ca13f3dd1b32f445877e97addf55d80b65c7cb35fac82b777/charset_normalizer-3.5.1-cp313-cp313-android_24_x86_64.whl", hash = "sha256:88ca277405c2d3b71c4e1c2ee0e7966e807bcba86a69d11e19ba199d18ae4491", size = 223359, upload-time = "2026-08-15T08:17:35.022Z" }, + { url = "https://files.pythonhosted.org/packages/09/0a/d3646670292ce8d8f8cc11ac067d44885e697a5591f57a9221128da5e7b3/charset_normalizer-3.5.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:9362dd90aa7dab48c0054a21187791ccf05473f7dba5d92b8033ae62164675e7", size = 194464, upload-time = "2026-08-15T08:17:36.452Z" }, + { url = "https://files.pythonhosted.org/packages/de/93/d51ec556e01042fed6f993ea859311bc7917b466684182fbbceb6ca24762/charset_normalizer-3.5.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:977cdbd483a9cff38179bea4fd754289a6f2195c7abd414aba85410b3e66cc5e", size = 197676, upload-time = "2026-08-15T08:17:37.819Z" }, + { url = "https://files.pythonhosted.org/packages/a4/a0/562247944386f7d4ef94467e84876600cc1e0f1b93239aaa9213d2bc3cbd/charset_normalizer-3.5.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e90251c0c7bdd54a100a0dce3c07b7e637278c93af29dbf78ebb89a58c4bac7d", size = 340473, upload-time = "2026-08-15T08:17:39.303Z" }, + { url = "https://files.pythonhosted.org/packages/31/e7/1d994be1b93d41e9502b8b0460eaa88a1dd8df335df415db87d6c3e91ab2/charset_normalizer-3.5.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:94d78ecec2605a8d0398b0f365d5f12a63248438516f5dac536a5eff7337df4a", size = 240156, upload-time = "2026-08-15T08:17:40.66Z" }, + { url = "https://files.pythonhosted.org/packages/09/53/27923ce5cc6cbccb832037b27dca98882d9c53e9b69e866bbbef4aae7fc8/charset_normalizer-3.5.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d59b75732e9b6f27388e10c14b0259cc5f2e48c78627d185e6a177b58ad3cffe", size = 228246, upload-time = "2026-08-15T08:17:42.003Z" }, + { url = "https://files.pythonhosted.org/packages/ce/48/5a97e84d63af1d55c07439cb80e56d99a8efb4295700eb4e18c0d1615d2c/charset_normalizer-3.5.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0d929fc574b4d6fd9e7c0f5c2ede8716a41911923aa7fa5fce38e0818aa4a1ac", size = 263660, upload-time = "2026-08-15T08:17:43.627Z" }, + { url = "https://files.pythonhosted.org/packages/7a/c2/071575791dcc88316c0a9a65ce38897a82e4cfe4a325f0f7fe1b1ac47bcf/charset_normalizer-3.5.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:394fea06235c8543390050ed5f529187074b029fb027213f6c46ac11ab5d950e", size = 260354, upload-time = "2026-08-15T08:17:45.094Z" }, + { url = "https://files.pythonhosted.org/packages/fb/af/63240b0c0248c075c2535a1f1bd992821d8251b9f173abc13329661d09e4/charset_normalizer-3.5.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:62b55f6722735a6c472f88361cde6640608773d9443cebdbb51abf436a1fcdd3", size = 250638, upload-time = "2026-08-15T08:17:46.496Z" }, + { url = "https://files.pythonhosted.org/packages/4d/66/70dfad64f15be09c15ccfee81330a7e515895dbe296dd23114e9a231268a/charset_normalizer-3.5.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fa48b1b63d639f9483e0633e092f5851e2348c352f1f9bb6c8182f87884ef876", size = 244583, upload-time = "2026-08-15T08:17:47.963Z" }, + { url = "https://files.pythonhosted.org/packages/c0/24/ef36367d38b9ddd4bccbf72888c342e8de1f5ae506fa0b2dcf970e2732a1/charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c71fb0d56c920c269cd3e2e3fe7c610e3f1fdb21a6ce60efa6430ff63676cea6", size = 242038, upload-time = "2026-08-15T08:17:49.481Z" }, + { url = "https://files.pythonhosted.org/packages/db/ab/55e683ba0fff2e43adafc10daa3001eac90fdaa419a97227d5a7067eedde/charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:485a0d363cafefcd2538a73c7c838daa2035f09b2c9f9b5e3133f80c6aeb84c2", size = 233677, upload-time = "2026-08-15T08:17:50.845Z" }, + { url = "https://files.pythonhosted.org/packages/bd/67/0f40eaf8d1b6e7cf15e82382a2965efaca787fc1c2794b7021d37aaf5036/charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5c0ea61a470e070686aa30892fed79e297d2c8d0ab46b8bcdf027d38c51da591", size = 264491, upload-time = "2026-08-15T08:17:52.61Z" }, + { url = "https://files.pythonhosted.org/packages/5c/64/12b4c2a11ee8df4fcc518c78b0d93e3a92bd3d5253d1617ce74ff0e8c7ef/charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:90b7481fb62fbe172c558bc6fd1c4c98d82004a54a7551f20e11ac9bf0b8708c", size = 245196, upload-time = "2026-08-15T08:17:54.023Z" }, + { url = "https://files.pythonhosted.org/packages/37/2e/651d910af6d0fba325eee1cda37ec5443462ed25360e666c144166eb6091/charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:35fe081843b35aad20ffeccec3eeffbe637b15d14f3fb22cc1b59cd8ec17e93c", size = 261660, upload-time = "2026-08-15T08:17:55.491Z" }, + { url = "https://files.pythonhosted.org/packages/90/c6/b09e05e6db7f64338e0dc067c79577b1138da86c1e38369096851d96be88/charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fd0350afdc3aabd5576f60ea109228bd5538139713c7b094c5cd27c73a98bc6f", size = 252618, upload-time = "2026-08-15T08:17:57.025Z" }, + { url = "https://files.pythonhosted.org/packages/76/4e/362d4f9fdcdf5556fb2aa3ce7d4a58ebce03ed1ff03aa1d9aca8d02f13f3/charset_normalizer-3.5.1-cp313-cp313-pyemscripten_2025_0_wasm32.whl", hash = "sha256:9d9a0dc7cbe9bec24c3f767c9122c41fe5a1bc43f47cd099d00d393e09769de4", size = 140362, upload-time = "2026-08-15T08:17:58.425Z" }, + { url = "https://files.pythonhosted.org/packages/b4/d4/703be739b26acce318bd29eb3b25b7209e1b1f527f9eae3d1f1f01fdde2b/charset_normalizer-3.5.1-cp313-cp313-win32.whl", hash = "sha256:d63600d620ad0064c3a748b950ac5ea38a80190e5498532efefa4b7b3f1da1f3", size = 177755, upload-time = "2026-08-15T08:18:00.037Z" }, + { url = "https://files.pythonhosted.org/packages/8a/33/56d97ade41c8db611e727168c52ae46c9224c362ec28d4b65d7e9869e8da/charset_normalizer-3.5.1-cp313-cp313-win_amd64.whl", hash = "sha256:aea996a6aba25260827c9ea511d1addfde2da9eb686ac961838509086188b7e6", size = 199295, upload-time = "2026-08-15T08:18:01.506Z" }, + { url = "https://files.pythonhosted.org/packages/5b/75/5b20dd1e6573a01a08158fe104104fa2c8abf941745596954185726cd46c/charset_normalizer-3.5.1-cp313-cp313-win_arm64.whl", hash = "sha256:fd0a274c0e5f9a21565cd9d3dd749b61f96b7aa1e20a93aa1ba4029518f2e5c0", size = 179856, upload-time = "2026-08-15T08:18:02.929Z" }, + { url = "https://files.pythonhosted.org/packages/29/cd/2b812ce5e888f1ce69a5350281e58aab07ae64a958ecae8912f30865718e/charset_normalizer-3.5.1-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:774d157f112367ff4abd29019f38f023c24e00e56edc7829c20e358a5a913ad8", size = 212318, upload-time = "2026-08-15T08:18:04.403Z" }, + { url = "https://files.pythonhosted.org/packages/9e/4a/a6ee107430768a5334e6d63f31f148a04a1a491ef161a1ac9415a73f2fa8/charset_normalizer-3.5.1-cp314-cp314-android_24_x86_64.whl", hash = "sha256:26422d45fd13551cf564c58932f7d72b4f58b93b0fcf18c35ba6be12b46bb102", size = 224897, upload-time = "2026-08-15T08:18:05.997Z" }, + { url = "https://files.pythonhosted.org/packages/c3/d9/35ae3f64f29d0179c35c3baefe575904df2913dde519129c7f75995a2b1d/charset_normalizer-3.5.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:09a7bba9f739468c8e78c36a75c33768e53cb1959fc638f510454c14683f00d5", size = 194848, upload-time = "2026-08-15T08:18:07.397Z" }, + { url = "https://files.pythonhosted.org/packages/74/76/f2fc7380f056cc273a53af37f50d08ad54b2c59f61078f31432edcf1c2bd/charset_normalizer-3.5.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:4c9548dc78002099910abaebc0a72ac58b7d30931869e0351c09b507dff4ece3", size = 198163, upload-time = "2026-08-15T08:18:08.989Z" }, + { url = "https://files.pythonhosted.org/packages/e9/40/095ce62fa078483cccc1fa2b36e6bc9580b85422a20ee9f925341c50e44f/charset_normalizer-3.5.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:c428c6c31eb5f4277d7f8eccaf767fbd548ddd5ce3c8b4f4cbbfab3d96b5904c", size = 341823, upload-time = "2026-08-15T08:18:10.458Z" }, + { url = "https://files.pythonhosted.org/packages/f1/5a/0e58b1c04a1596e0256f407274a92d5fb2ee21324409d1fab1da48a65b5b/charset_normalizer-3.5.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2f06b7eae9dbe77fe1d644ca244dad508de8d302870a43f3c559b521270938a0", size = 242458, upload-time = "2026-08-15T08:18:11.989Z" }, + { url = "https://files.pythonhosted.org/packages/22/95/b4618ce912e6db0b1aae89ba788e38e8a7eba0f3025cc66e8c0699f977b2/charset_normalizer-3.5.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6b7430cf5728e68f6c462254009a6ef4086e1bea43cf2f57aa9c55fb4f50ff96", size = 226717, upload-time = "2026-08-15T08:18:13.401Z" }, + { url = "https://files.pythonhosted.org/packages/8a/76/c681192bbda3d55356db5dadd64381d5202b37c6b598fcda5282e88b5d3d/charset_normalizer-3.5.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ab743e9bc90c1f73552ec33e10e3331315acd2c397b36065b591b0181de533cc", size = 266111, upload-time = "2026-08-15T08:18:14.961Z" }, + { url = "https://files.pythonhosted.org/packages/88/be/55127bfca72c0cff6c022488d140d7c5b04c771e3b72e9bdb4836d54979d/charset_normalizer-3.5.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f6f7deae3feb4edfa2efaf7c574fe88cbf055038a6abdb40188e4fff66d5699f", size = 263128, upload-time = "2026-08-15T08:18:16.515Z" }, + { url = "https://files.pythonhosted.org/packages/e0/91/39c3af510b0aa32bbda03374259200f28430febfd1bf5e511fe765282ce5/charset_normalizer-3.5.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15f024313246a4ed976c60f440bb8d257815513a681d212ff74fd46f7d715a90", size = 251240, upload-time = "2026-08-15T08:18:18.127Z" }, + { url = "https://files.pythonhosted.org/packages/1c/a5/cbe418bbc6ecdfc3e05a0116002897c4b403a5e838d697e64c78e9f0190d/charset_normalizer-3.5.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:823f82903d189af463d7df250ef1f7f696f3cee08cc8d91deb565e8d425f6506", size = 245282, upload-time = "2026-08-15T08:18:19.625Z" }, + { url = "https://files.pythonhosted.org/packages/cc/a4/689bb42e8e7cd492f3cb64907c6bc00ad247ec9a3628cd3f8eed126e8ae1/charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:01e93745f7f219b703b60ba7afead36cfc4242782be5af484673fc500df12da5", size = 244597, upload-time = "2026-08-15T08:18:21.121Z" }, + { url = "https://files.pythonhosted.org/packages/c1/ce/9962938e179cf9f699d3f1e7b3114b5d7642dee6a893745229f9dd04f274/charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:329fc3ccb63ad22d867d84c2adea759a64079a37ba4a343433b02c7a2816871e", size = 231376, upload-time = "2026-08-15T08:18:22.57Z" }, + { url = "https://files.pythonhosted.org/packages/85/54/46000450ada53bd9eac5429a2c8c54cd2d9b39c0c255f229aea9af0948a5/charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:bb57753e36e4855b8ca375069482250a6246372331a3e4f3407eaebb007443f5", size = 266715, upload-time = "2026-08-15T08:18:24.235Z" }, + { url = "https://files.pythonhosted.org/packages/3d/bb/618749d70f792b44252a777bf89bfb86823b9bbc1ea13fe8ce759b07f38a/charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:fce8cbd4997efeb450bd298b54f755dcdff18d496f7a5ddbb4867c6d7c88fdc3", size = 245848, upload-time = "2026-08-15T08:18:25.726Z" }, + { url = "https://files.pythonhosted.org/packages/7e/3f/ffb64458527c7668031d5eb095d978de561958dc9f5b53f8e488a533e603/charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:6c9cdde8becb25a7fde49924511aa2644d6f8081cc8df8e9452724303348d8e3", size = 264521, upload-time = "2026-08-15T08:18:27.193Z" }, + { url = "https://files.pythonhosted.org/packages/4f/ab/74a55fd803916a35ac461daf002708191aac19b546b80dc8cabfedc63d98/charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9ac4444d8d4fd4c4bd08bf451ed3167aa9e7ec6cdb41b648794f1d1103652e36", size = 253054, upload-time = "2026-08-15T08:18:28.568Z" }, + { url = "https://files.pythonhosted.org/packages/a0/2a/6a9034b7d3c60b17499afb482df5878bf9fa20b50cc3887d5ef017a833db/charset_normalizer-3.5.1-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:f03ac127268b43ef4fe9e6ab6794a6794b49485a0cc0c1db79876d2f33f75bc7", size = 140580, upload-time = "2026-08-15T08:18:30.214Z" }, + { url = "https://files.pythonhosted.org/packages/f3/46/1d362e1a00d035d66b9869e1281eee115907f7e390a16a07824ab5737360/charset_normalizer-3.5.1-cp314-cp314-win32.whl", hash = "sha256:1f5883d77fd409a261abb5dc8ccbe335720d798b1de4abb3b1d47ccbbc76b53b", size = 180325, upload-time = "2026-08-15T08:18:31.877Z" }, + { url = "https://files.pythonhosted.org/packages/7a/7c/4938c329b6a9d446f6a59aa2092ff7118f274209b5ed0e26893d1d30a63c/charset_normalizer-3.5.1-cp314-cp314-win_amd64.whl", hash = "sha256:c658c50ac0c98cd755a2dd50b7977d3bca7df401dcc47fbdfa87db53ef7d4e8b", size = 204175, upload-time = "2026-08-15T08:18:33.466Z" }, + { url = "https://files.pythonhosted.org/packages/ac/33/eeb384dbd8dec570661354592f4f2e1b2fcc92585624d146a000caf53841/charset_normalizer-3.5.1-cp314-cp314-win_arm64.whl", hash = "sha256:4bea7f8ebe90bbd7f0e4a2de42ca6924ba23e3e76418c408ff82f1d46fabd687", size = 184123, upload-time = "2026-08-15T08:18:34.913Z" }, + { url = "https://files.pythonhosted.org/packages/1c/6c/c73fa9d5a85f6ab05395de61c5f6984e0a9ff40bb5ff888d46dff02526c6/charset_normalizer-3.5.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:fbc597639158fd7c14d55e808718848319540f51b0e6746e3eefa59723a4a348", size = 381682, upload-time = "2026-08-15T08:18:36.349Z" }, + { url = "https://files.pythonhosted.org/packages/30/c7/63565f860921457feba93bae6c86fb7746deb4cffeed2f375cb845318146/charset_normalizer-3.5.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e71c909f353863b2b89c83de2ebed71ea6d0df8a6ef65a128193c5e650766bef", size = 240826, upload-time = "2026-08-15T08:18:37.887Z" }, + { url = "https://files.pythonhosted.org/packages/06/ae/7ae8807410dfa33f8e6f1715740adeaafa8a816cc4cb33508f54b1f7c896/charset_normalizer-3.5.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7ac76cf9afd34929d76eb7fcb63be476a4853d8a96f0dcf2d0db68a0cbdf9885", size = 227861, upload-time = "2026-08-15T08:18:39.315Z" }, + { url = "https://files.pythonhosted.org/packages/e9/a3/887c1642f0da26000b0e0652d91071113c0e72cea33952e225cf589f49a9/charset_normalizer-3.5.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a3a370082ce34d0612f421e15fe011c53bb1feff21a26d06ad4fb244dab5a375", size = 260758, upload-time = "2026-08-15T08:18:40.88Z" }, + { url = "https://files.pythonhosted.org/packages/3e/11/e6f5b9a3d0e55b0ef7505cd3765cdd48f22db89994c947b316f52f801fd8/charset_normalizer-3.5.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:256dd4d85d9e4dc595e2bc983c980e73f62ddeb3165c58b4c3dfe78c5c8548c1", size = 259950, upload-time = "2026-08-15T08:18:42.351Z" }, + { url = "https://files.pythonhosted.org/packages/1b/ee/e4e10a94d51cd1ee638aa7e00b65399e6b2a4e8376ab6d2eac9f95586671/charset_normalizer-3.5.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:58d4aa13a59c969dbfdf9e6a9560e242cbfd9e8a8f50c2747714df1a423adf65", size = 249329, upload-time = "2026-08-15T08:18:43.914Z" }, + { url = "https://files.pythonhosted.org/packages/c4/25/d5f4198819e6059735a84e8d0bfb72dc33976da67b97adcd3fb5a5e07ec6/charset_normalizer-3.5.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0c6dfb5ca6723eeed15aa8e564a014d69fcb8812f94eef11fe3631e0508199f5", size = 243137, upload-time = "2026-08-15T08:18:45.368Z" }, + { url = "https://files.pythonhosted.org/packages/a5/e9/e925ca7569cf9fb9701fd82503fee73eea5268fdb856bdd64947092d3daa/charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c010f5581d9c612804cc59fcf7b524b707fbcb72828551237ab545bb5c7034af", size = 242820, upload-time = "2026-08-15T08:18:46.842Z" }, + { url = "https://files.pythonhosted.org/packages/34/17/672c251a888ed2aebcdd2fe830ad0104e25ff83c43f5c4f9c15e9fc6853c/charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:52ec005752a56ae79547a05c0139ca2501a0c866390b6115008456b9f0e7cde1", size = 230504, upload-time = "2026-08-15T08:18:48.353Z" }, + { url = "https://files.pythonhosted.org/packages/3f/fc/f6a85abebd42ce4da2f1db0aa56cc6a0df1995e318b3875d14401b8381d1/charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:2bced4061f000f7187254a02ad3433ae17eaf991747ceea2f478422590a5bba9", size = 263087, upload-time = "2026-08-15T08:18:49.859Z" }, + { url = "https://files.pythonhosted.org/packages/98/66/7c42677e739ba66746b297e2046918d793078094dc239e1e72768cffccc6/charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:9eea3ab2597a5e65fe65296e2d6a84570845a6b55532d90333d740d48bbc850a", size = 243269, upload-time = "2026-08-15T08:18:51.601Z" }, + { url = "https://files.pythonhosted.org/packages/de/d8/a50b79237f417af10f8c2a501ce8d1ca87829a22e69117891ca4ba20a69e/charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:496846868fea80e479324862fa877f02411f2fd0f83b79ccee2607aa68b2a032", size = 258766, upload-time = "2026-08-15T08:18:53.23Z" }, + { url = "https://files.pythonhosted.org/packages/2e/1d/0fc91aeaeb3c83b748f532399ce67cf84604b48297405d740000f7a9e786/charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:85d5855daafc240cc045c026d7a15fd198a09b0fc8ff6f5ecbb5297b509cb11e", size = 250814, upload-time = "2026-08-15T08:18:54.768Z" }, + { url = "https://files.pythonhosted.org/packages/ae/10/3d8c777cf9024615295aa1b808324ad5b4a77855869c00824bad74ffaf8a/charset_normalizer-3.5.1-cp314-cp314t-win32.whl", hash = "sha256:58d3e12c88e0950bca850ae1f7c256055c097639c2edb9eb123af9807d8b15e4", size = 191074, upload-time = "2026-08-15T08:18:56.305Z" }, + { url = "https://files.pythonhosted.org/packages/4d/81/ae557d3c44d1a1d688696d60563413a0866a91b7ebc50f20df838be3d8c8/charset_normalizer-3.5.1-cp314-cp314t-win_amd64.whl", hash = "sha256:acaf604462bf330b0d07e7a07c1d6e4adac79e5fb13e9c5140590542cafacc00", size = 216476, upload-time = "2026-08-15T08:18:57.889Z" }, + { url = "https://files.pythonhosted.org/packages/27/e9/61c01fb8b804692569c036b3fc50495814502dcf13a60649c6055390b02c/charset_normalizer-3.5.1-cp314-cp314t-win_arm64.whl", hash = "sha256:fdb8a068947befafba9952162645dc2fecaeb400e64584829ed5e9b2fbe21a7f", size = 194115, upload-time = "2026-08-15T08:18:59.418Z" }, + { url = "https://files.pythonhosted.org/packages/4a/4e/8544831ef59d8f27ce92c80871380fdacc8076a8a56ed62f82e54f991333/charset_normalizer-3.5.1-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:9085f87b0e38a2b92b8923059b4e8789fe40d9279712d15dcc670048d77079af", size = 342048, upload-time = "2026-08-15T08:19:01.054Z" }, + { url = "https://files.pythonhosted.org/packages/7f/a6/e3b46852424246065355644f4fb6dbccc0239a42a2eee27ecfc8957f0bcd/charset_normalizer-3.5.1-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2679de311c7946dde5d3b6f44941844133ff5c7cb86099c0061ab1e8901c20a8", size = 242997, upload-time = "2026-08-15T08:19:02.492Z" }, + { url = "https://files.pythonhosted.org/packages/03/3b/0cc9a26777334ab2f2e3089b948bbf4e4fe72ea70b897715ef6415043ec8/charset_normalizer-3.5.1-cp315-cp315-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:baf3775a2635e5a11fbd5e4e64ee69c7e86875d224a5c72aca4c141064589a90", size = 237014, upload-time = "2026-08-15T08:19:03.943Z" }, + { url = "https://files.pythonhosted.org/packages/8c/c2/027335f0aa337a2a2e121bac1ad88c4f02ba6053ea0926802784f3db11af/charset_normalizer-3.5.1-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ac8c94b6539074e0f40899301273ac8402b9b3e01c7b7ba269ff30340aaaf20", size = 266174, upload-time = "2026-08-15T08:19:05.598Z" }, + { url = "https://files.pythonhosted.org/packages/86/d3/e367787febe4e74769dec0f406f2c3c8d1b955fce5aee1fd0f94e8367a45/charset_normalizer-3.5.1-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8fe532b3c966d1fb794e0698e4589d0444017ae77fc0b31edea13c0e35bcc449", size = 263361, upload-time = "2026-08-15T08:19:07.251Z" }, + { url = "https://files.pythonhosted.org/packages/af/3d/391b193eb9f3e84b02f9314088c386debdc0debee843535aaea2e2c6715d/charset_normalizer-3.5.1-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5c84bec0ab5ae0c64bfe73a7d2adcb5ce73b467523fc27fd6a28ab2aa6cbe35a", size = 252143, upload-time = "2026-08-15T08:19:08.816Z" }, + { url = "https://files.pythonhosted.org/packages/2e/57/de221f1745a90d418199761967e2776bfe2c275a1194220985e8c1d37833/charset_normalizer-3.5.1-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:854066be00447fa8de2ccbbe893e2ffc4b123ef16d897af794c1e18bd4a714b0", size = 252086, upload-time = "2026-08-15T08:19:10.255Z" }, + { url = "https://files.pythonhosted.org/packages/c8/e3/d119f86a01f9331e8186175f24873b1d74a7ee9e2e4b4d68f9947dae5afd/charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:21b82d8082f6f5e7f456ef0bd16323d08de1266efbfeb476e64b2a91d1471a4e", size = 245231, upload-time = "2026-08-15T08:19:11.807Z" }, + { url = "https://files.pythonhosted.org/packages/26/de/d8e48c135ae480879539cdb179c8d3b50c7879497d75dd899b5763b69cee/charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_armv7l.whl", hash = "sha256:838648accb3a7fd9803fd45c87bce8509648eb0c11bc34e216141300977244f2", size = 241546, upload-time = "2026-08-15T08:19:13.416Z" }, + { url = "https://files.pythonhosted.org/packages/67/c4/217755fd1abc50d326c252922cd642002758095a81ff45010337b8b3ef65/charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:195ce897c6153c0700078142cf8efe3e6454ca4cf4357499e4078dfd83396626", size = 267033, upload-time = "2026-08-15T08:19:14.981Z" }, + { url = "https://files.pythonhosted.org/packages/b8/d7/34d8e404e358d2adcc5a228c2134643af00104c8fb0bf525f3688d756f05/charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:978eab16f55b4ab2c2a745be9a0a840bf8f09a7f227d9c76eb30214d078865a5", size = 252045, upload-time = "2026-08-15T08:19:16.618Z" }, + { url = "https://files.pythonhosted.org/packages/5e/fa/40414471acf0aa0692ca77305aa00e434fcd8288f0941c93c30e9a5f8f2f/charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_s390x.whl", hash = "sha256:cc0329df4caaceb950d2f580b5ac716a377f7059624a0bafaeaf8a218c6ed774", size = 264866, upload-time = "2026-08-15T08:19:18.101Z" }, + { url = "https://files.pythonhosted.org/packages/32/90/fcc850bae791abd2e0c041847f13e270aa08692a79f3e00de6d2dce1cb50/charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:687c9ca3035544b113bea2055e180af96fb63c0c476e22a9180f51925186e7b7", size = 253932, upload-time = "2026-08-15T08:19:19.734Z" }, + { url = "https://files.pythonhosted.org/packages/af/af/53afe99068b3c10b4cbae592a52ef72a7c92c0188440e83ee3a078fd8f75/charset_normalizer-3.5.1-cp315-cp315-win32.whl", hash = "sha256:706bfd38730a5ac7a365793269a00f4e988178cec121391f4248d84ad8c972e9", size = 180320, upload-time = "2026-08-15T08:19:21.37Z" }, + { url = "https://files.pythonhosted.org/packages/c9/bc/f46a132041b29e4a8779ed712d3df1bf112e94ca8de58b66d7ec2c0cf8b9/charset_normalizer-3.5.1-cp315-cp315-win_amd64.whl", hash = "sha256:92caef967d287a407085d61176fce4012b1dd62daed4eb6d5ceb26d3d2538712", size = 204174, upload-time = "2026-08-15T08:19:23.088Z" }, + { url = "https://files.pythonhosted.org/packages/a1/5d/9ed554480eda8e447b673648628fdc29574d23dbad01fe11837adedd1cae/charset_normalizer-3.5.1-cp315-cp315-win_arm64.whl", hash = "sha256:5fc45d653ea8c9a20479167e11d4a0f8cb2fa3470737ab6f9c827532313187b7", size = 184126, upload-time = "2026-08-15T08:19:24.471Z" }, + { url = "https://files.pythonhosted.org/packages/3b/32/9b8929bf384061ee1fe5d9c27c6f9776d3d824039ad4e14c88ec00c7808e/charset_normalizer-3.5.1-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:59171c6e45bf07d0d5cab3b0bf81d945035530f6873398b3b531c31184d46663", size = 381441, upload-time = "2026-08-15T08:19:26.038Z" }, + { url = "https://files.pythonhosted.org/packages/96/10/e9aa7923d3ddac652c99a1c5f7be494e737e151566a44abe018daf757f2c/charset_normalizer-3.5.1-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9dbdd9205662134957cf0c324f639bdc5031c0ca056e2369e238db75187c0f11", size = 241742, upload-time = "2026-08-15T08:19:27.532Z" }, + { url = "https://files.pythonhosted.org/packages/28/53/a2d249ebddf47b889a100c0bdcb61a2f9dbb8bc24ef325cc062e4f476877/charset_normalizer-3.5.1-cp315-cp315t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e4b018dc5a0eee4676e38fe84a47a427816c590b93b55d9025274ec4d6ffc2dc", size = 235298, upload-time = "2026-08-15T08:19:29.274Z" }, + { url = "https://files.pythonhosted.org/packages/7d/07/469f78af590f7d5cd48e20d8dbfa3d66deeff9ba37768c04d886b5afd45c/charset_normalizer-3.5.1-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ced3fdd71aaa83ce593746c2edb42b7a59cb4c19c8b5c407781c72e493aae55a", size = 262500, upload-time = "2026-08-15T08:19:30.955Z" }, + { url = "https://files.pythonhosted.org/packages/55/66/3bb56a47f7dcba014055b1a1d33c6f08bbe9c1e74dba154cfa25f90ae885/charset_normalizer-3.5.1-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:19a3dd5aa73cef1c99687c4fc57db016a9c17104ae1185da88ba566a5d3bebe4", size = 258888, upload-time = "2026-08-15T08:19:32.458Z" }, + { url = "https://files.pythonhosted.org/packages/ff/c1/2adc2800903fb013210349313b710a5376856578d9e33e6b9a1d8b36714a/charset_normalizer-3.5.1-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cc5d36d96478aa9c60654bd932525bf32964c62a7281eafdf16d85003a8d6004", size = 250243, upload-time = "2026-08-15T08:19:33.94Z" }, + { url = "https://files.pythonhosted.org/packages/95/b5/a18d0dd1157ab655cc2cb14a545f4a4784bbad70ab3502412e36097502d9/charset_normalizer-3.5.1-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:04368edf83514385ffc3e1cfd4546e595f4f1272dd23ba437a93a9cc3741d47b", size = 249871, upload-time = "2026-08-15T08:19:35.413Z" }, + { url = "https://files.pythonhosted.org/packages/ad/c3/525f508cd1e58d0450ac55ed40ac75bc3a97482c59def5278456a5fbf03c/charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:9b5db6052055d34d41230fb78d7c439c23dc536a9896f6cb039e8dd92cfc1263", size = 243580, upload-time = "2026-08-15T08:19:36.886Z" }, + { url = "https://files.pythonhosted.org/packages/7c/c1/49a91fe7e97c8140094ca5c64161ab623a70d9f636bf834eace14048acb5/charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_armv7l.whl", hash = "sha256:252d099029bcbea642f2a06c4ed5046bdf8b5a8150b64afa5e027e88b106e5ee", size = 239807, upload-time = "2026-08-15T08:19:38.392Z" }, + { url = "https://files.pythonhosted.org/packages/d3/58/56a48c296601274c4689b864a8e2dfb209b81dfcb39472753ce95eea662b/charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:6199d5606e2bbf2b096cf64d03f8b6790c91081d5ac866b8e7bb6422738cc60c", size = 264083, upload-time = "2026-08-15T08:19:39.856Z" }, + { url = "https://files.pythonhosted.org/packages/10/4c/dc48409274a1817ff349711d26c62aa0c597df865d4d69ef79160c859193/charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:77efcff2b23071c349402ac1066667a3d011f62398d81408c9b88ad991747c9e", size = 250317, upload-time = "2026-08-15T08:19:41.53Z" }, + { url = "https://files.pythonhosted.org/packages/81/58/d325912115caec62d6bdd77bbab5e0b7da5d234a9f20affdffcbcb530d0b/charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_s390x.whl", hash = "sha256:a5cbd90ecf0fc62e64726917ad083b73001f0563657a87ec3c0b504e277dc90d", size = 258173, upload-time = "2026-08-15T08:19:43.07Z" }, + { url = "https://files.pythonhosted.org/packages/34/f7/b13b1ccae2c8ec63980d13be1890eb73f8aeabbfce02a24aabc0908788f5/charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:4d26f14f041e83dd8edfd61f4cd4fa7285d31798b5bf1f28e70c367ba6c41d61", size = 251960, upload-time = "2026-08-15T08:19:44.587Z" }, + { url = "https://files.pythonhosted.org/packages/1e/25/ed3f9919c5aef8cc818be1f972f565f7610d7b2076b8ebb98839516ffc3c/charset_normalizer-3.5.1-cp315-cp315t-win32.whl", hash = "sha256:ac13b004224fb341e1e25a1ed5e19d32f57cdb2a403e01f003b46f051a550f6f", size = 191186, upload-time = "2026-08-15T08:19:46.293Z" }, + { url = "https://files.pythonhosted.org/packages/69/d5/43c2b3e9d8267092b913eb8b0603f0f71993c395632886bd37a7223f96cf/charset_normalizer-3.5.1-cp315-cp315t-win_amd64.whl", hash = "sha256:35aea775dc2bd5f54cd84a1cd2696cc3207c479cb9cf0bd346f0d343e4300ddb", size = 215947, upload-time = "2026-08-15T08:19:47.853Z" }, + { url = "https://files.pythonhosted.org/packages/a8/76/9aad3e9c8865e5e0efa9a7f6f81c37a67635a985145ecd44528a81e088ee/charset_normalizer-3.5.1-cp315-cp315t-win_arm64.whl", hash = "sha256:fb78f6e7fcd8ad785d28cd577168bc1aaee827b25bb8755638f694794ea98f0a", size = 193909, upload-time = "2026-08-15T08:19:49.383Z" }, + { url = "https://files.pythonhosted.org/packages/5b/97/fb4e82231aba271ffd775a1b4993b0defc4e3059f286ae41d9433409fe85/charset_normalizer-3.5.1-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:41876ee62a3dddf48ff1121ad8f0798032aa03f2fd35f21f34a4cab14f18d8d2", size = 331467, upload-time = "2026-08-15T08:19:50.959Z" }, + { url = "https://files.pythonhosted.org/packages/9f/2f/fe3f187327aac18e2d54e9d2b08e15d27bf9b642d9e51c219f130fc34d1a/charset_normalizer-3.5.1-cp37-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a6dac12ff6b846103483683f60c5f8fee205121adc58ffd87e90a90a3af69e99", size = 253057, upload-time = "2026-08-15T08:19:52.654Z" }, + { url = "https://files.pythonhosted.org/packages/d7/c7/9e48cee5c161fe24da823b61bf381921d77cb994a0a4de148e95018c1984/charset_normalizer-3.5.1-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cee5dd7c6fb5dd52a0fe2a740f9bc6e3593f5f8b1788bde49de02086f30182b2", size = 240930, upload-time = "2026-08-15T08:19:54.163Z" }, + { url = "https://files.pythonhosted.org/packages/49/e0/716601f3cc69be7b198951150c75ead1ece33c3c8036ff6ffa46029659a0/charset_normalizer-3.5.1-cp37-abi3-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:343fb4f2821043bd87095f7b08a1a181febc8e36ac64212143bbfd0a0e1bc235", size = 230822, upload-time = "2026-08-15T08:19:55.807Z" }, + { url = "https://files.pythonhosted.org/packages/d3/05/71bfc5caa0abcc45aea1f6a4d50ac68e59605ddc7666fe8494f4cd229665/charset_normalizer-3.5.1-cp37-abi3-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ae4a097991662cd4fff0ddc74e0fe7874f82e00042fa0ea00855645ed0c79598", size = 260037, upload-time = "2026-08-15T08:19:57.312Z" }, + { url = "https://files.pythonhosted.org/packages/c3/92/de7e32ed05341e7a9c4c877c318418197b7f2d66a3b68d561bf2ac57ca3e/charset_normalizer-3.5.1-cp37-abi3-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4b599739b93b2cbeded49645ae3c8d1405c29ddfbceac1545c87a3f9580a9e96", size = 255097, upload-time = "2026-08-15T08:19:59.056Z" }, + { url = "https://files.pythonhosted.org/packages/f5/7b/ade0a122600319dfa0b1000ab0f9731c94a817904cf3c5de408c73a4ede7/charset_normalizer-3.5.1-cp37-abi3-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b39b69b347e5e47a3b5b8cfc005c68c1ba347474e3960236c4944a8ecd174962", size = 250166, upload-time = "2026-08-15T08:20:00.612Z" }, + { url = "https://files.pythonhosted.org/packages/75/9c/019fbb9f4834491a160951349b1a3714439376f66e5f7cf18b4f18f0c7aa/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:a2028475ba855475b8b4d3cfeb4994269c967aea8b9892dfba907f4263a863a3", size = 241821, upload-time = "2026-08-15T08:20:02.321Z" }, + { url = "https://files.pythonhosted.org/packages/2b/b8/11d4840bfc99330cc7fbcc2681ee5a044553a6e77655508d8f9b2bff7b34/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:36047af20e17097c3bb9476c2b7655f2f7aa51322c0ba58c07695bedf755a950", size = 232529, upload-time = "2026-08-15T08:20:04.008Z" }, + { url = "https://files.pythonhosted.org/packages/18/96/2b3a21492d9f65171ac75d872f5018260013d00bfa0ff70ec9f179148cbd/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:4c4fb141a727957c93edfe5c32a26ceb6b5f6461d67146e2d39f51e16170bea8", size = 260348, upload-time = "2026-08-15T08:20:05.877Z" }, + { url = "https://files.pythonhosted.org/packages/d6/aa/a69a2028e8bd052476c245460ab19d7de595de084dd968f2d75cd50c3e25/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:2f293479cce755c75f1697e87c409b7ae4c555c7dfecb6e988ad13abba943031", size = 247234, upload-time = "2026-08-15T08:20:07.487Z" }, + { url = "https://files.pythonhosted.org/packages/35/8a/3d130aeabcaf3d2466af76b7b141c08d9e89c9016ab4b7cdd0f7dc2d1c62/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_s390x.whl", hash = "sha256:3588e376b3ea2eea84976f67273d679f229e24c66dce7b82ae45aef04ff6e072", size = 256917, upload-time = "2026-08-15T08:20:09.142Z" }, + { url = "https://files.pythonhosted.org/packages/80/c2/a7379b840292d0c1ab9fbd17d1f3967aa81794dc95bc74be8999d7fedcf7/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:e199fb99720074809a7720f1c0b4d919eea8b87e88713e0f8f602f7bef543d9d", size = 254846, upload-time = "2026-08-15T08:20:10.727Z" }, + { url = "https://files.pythonhosted.org/packages/01/65/d43b714731bb2f40d4053dfa00ecfc1c5a301f8e3316c5db3a09af59fe94/charset_normalizer-3.5.1-cp37-abi3-win32.whl", hash = "sha256:dd732602a7009217f658d5863d12d79d373a4de0eebc111094bcdd3bb8e0a6cc", size = 174216, upload-time = "2026-08-15T08:20:12.334Z" }, + { url = "https://files.pythonhosted.org/packages/35/4f/b911ed898b26a09789eba9c9200c999aff6c61b4bafaf4838e56d1a1e1a3/charset_normalizer-3.5.1-cp37-abi3-win_amd64.whl", hash = "sha256:70055ff39b97c99e7ae40ea3e393fb62aa2e44dbd9b29f8d14f42fb0025c3959", size = 199764, upload-time = "2026-08-15T08:20:13.908Z" }, + { url = "https://files.pythonhosted.org/packages/f0/a7/920baf467bfd9bf689f3b318340f37aee4572a71f162bd8db51da55ba4fa/charset_normalizer-3.5.1-cp37-abi3-win_arm64.whl", hash = "sha256:87e4f41d375c0b9be2fb5251aee4b8a689169e134535aed81bf085c3b647451e", size = 287318, upload-time = "2026-08-15T08:20:15.551Z" }, + { url = "https://files.pythonhosted.org/packages/cc/61/d01fc49b8dea277640b55a9e15960dbca9fdc8c9fde18e572d39c59f4019/charset_normalizer-3.5.1-py3-none-any.whl", hash = "sha256:6df0ec430f9a831772c23ca5a224cba36517a58a84bb32c32bb59a9fa67c47f6", size = 68658, upload-time = "2026-08-15T08:20:43.306Z" }, +] + +[[package]] +name = "click" +version = "8.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/0e/7fa0ef50764b67090eca4114772a2abf8b6148198475e54c660b97caeee6/click-8.5.0.tar.gz", hash = "sha256:ba0d2089de75ea0310e2dde03160e6ca10009947fb95a182f9b54021bb272e34", size = 382235, upload-time = "2026-08-26T13:33:14.56Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/58/50/6c0d534c5f134586a8e1ba4e330569e32f057e33372ae556463212fb4cd3/click-8.5.0-py3-none-any.whl", hash = "sha256:255bc9599cf7748b4b1a446ccc735421bd08a2ae529a8b88597d3de5664ee360", size = 125251, upload-time = "2026-08-26T13:33:12.928Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "ghp-import" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "python-dateutil" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d9/29/d40217cbe2f6b1359e00c6c307bb3fc876ba74068cbab3dde77f03ca0dc4/ghp-import-2.1.0.tar.gz", hash = "sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343", size = 10943, upload-time = "2022-05-02T15:47:16.11Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f7/ec/67fbef5d497f86283db54c22eec6f6140243aae73265799baaaa19cd17fb/ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619", size = 11034, upload-time = "2022-05-02T15:47:14.552Z" }, +] + +[[package]] +name = "git-ftp-docs" +version = "0" +source = { virtual = "." } +dependencies = [ + { name = "mkdocs-material" }, +] + +[package.metadata] +requires-dist = [{ name = "mkdocs-material", specifier = ">=9.5,<10" }] + +[[package]] +name = "idna" +version = "3.20" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f5/08/8eea9d4b8302028f3abb2c0813953f7aec26d33b7a8960ed760e65ff29fa/idna-3.20.tar.gz", hash = "sha256:a7db850025b95ded1eae8a46181a1a6c56c92c96f0e2b005d9ff8dc0210cab44", size = 216463, upload-time = "2026-09-17T14:11:04.752Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/58/a2/bb081bab032533a855d44de1d56f8e8426114ff1ba5d1f07a438a0a654f8/idna-3.20-py3-none-any.whl", hash = "sha256:ab7ae7122974553370f0bdb919e1a960b2cd1bc1ef0276416d896db81c14582c", size = 69583, upload-time = "2026-09-17T14:11:03.168Z" }, +] + +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, +] + +[[package]] +name = "markdown" +version = "3.10.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/29/6f/da4c6aea59b3001f2e8c0ec7497475aadaf3b021c10cab5b2858f0f32b26/markdown-3.10.3.tar.gz", hash = "sha256:3589362618f743188b4d955b874402bc814f4f83f544dc207719f4baa7d9c45f", size = 372596, upload-time = "2026-07-30T19:05:29.005Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/69/4a5af2bc115a9a33fefe51709749de8262be3f9ba063d1753a837cdbc49c/markdown-3.10.3-py3-none-any.whl", hash = "sha256:fa6c92a00a4a3c98b22728c64a935ae1928250ae65058a6ded814d2cc29a4cea", size = 110757, upload-time = "2026-07-30T19:05:27.883Z" }, +] + +[[package]] +name = "markupsafe" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e8/4b/3541d44f3937ba468b75da9eebcae497dcf67adb65caa16760b0a6807ebb/markupsafe-3.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559", size = 11631, upload-time = "2025-09-27T18:36:05.558Z" }, + { url = "https://files.pythonhosted.org/packages/98/1b/fbd8eed11021cabd9226c37342fa6ca4e8a98d8188a8d9b66740494960e4/markupsafe-3.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419", size = 12057, upload-time = "2025-09-27T18:36:07.165Z" }, + { url = "https://files.pythonhosted.org/packages/40/01/e560d658dc0bb8ab762670ece35281dec7b6c1b33f5fbc09ebb57a185519/markupsafe-3.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695", size = 22050, upload-time = "2025-09-27T18:36:08.005Z" }, + { url = "https://files.pythonhosted.org/packages/af/cd/ce6e848bbf2c32314c9b237839119c5a564a59725b53157c856e90937b7a/markupsafe-3.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591", size = 20681, upload-time = "2025-09-27T18:36:08.881Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2a/b5c12c809f1c3045c4d580b035a743d12fcde53cf685dbc44660826308da/markupsafe-3.0.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c", size = 20705, upload-time = "2025-09-27T18:36:10.131Z" }, + { url = "https://files.pythonhosted.org/packages/cf/e3/9427a68c82728d0a88c50f890d0fc072a1484de2f3ac1ad0bfc1a7214fd5/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f", size = 21524, upload-time = "2025-09-27T18:36:11.324Z" }, + { url = "https://files.pythonhosted.org/packages/bc/36/23578f29e9e582a4d0278e009b38081dbe363c5e7165113fad546918a232/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6", size = 20282, upload-time = "2025-09-27T18:36:12.573Z" }, + { url = "https://files.pythonhosted.org/packages/56/21/dca11354e756ebd03e036bd8ad58d6d7168c80ce1fe5e75218e4945cbab7/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1", size = 20745, upload-time = "2025-09-27T18:36:13.504Z" }, + { url = "https://files.pythonhosted.org/packages/87/99/faba9369a7ad6e4d10b6a5fbf71fa2a188fe4a593b15f0963b73859a1bbd/markupsafe-3.0.3-cp310-cp310-win32.whl", hash = "sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa", size = 14571, upload-time = "2025-09-27T18:36:14.779Z" }, + { url = "https://files.pythonhosted.org/packages/d6/25/55dc3ab959917602c96985cb1253efaa4ff42f71194bddeb61eb7278b8be/markupsafe-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8", size = 15056, upload-time = "2025-09-27T18:36:16.125Z" }, + { url = "https://files.pythonhosted.org/packages/d0/9e/0a02226640c255d1da0b8d12e24ac2aa6734da68bff14c05dd53b94a0fc3/markupsafe-3.0.3-cp310-cp310-win_arm64.whl", hash = "sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1", size = 13932, upload-time = "2025-09-27T18:36:17.311Z" }, + { url = "https://files.pythonhosted.org/packages/08/db/fefacb2136439fc8dd20e797950e749aa1f4997ed584c62cfb8ef7c2be0e/markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad", size = 11631, upload-time = "2025-09-27T18:36:18.185Z" }, + { url = "https://files.pythonhosted.org/packages/e1/2e/5898933336b61975ce9dc04decbc0a7f2fee78c30353c5efba7f2d6ff27a/markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a", size = 12058, upload-time = "2025-09-27T18:36:19.444Z" }, + { url = "https://files.pythonhosted.org/packages/1d/09/adf2df3699d87d1d8184038df46a9c80d78c0148492323f4693df54e17bb/markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50", size = 24287, upload-time = "2025-09-27T18:36:20.768Z" }, + { url = "https://files.pythonhosted.org/packages/30/ac/0273f6fcb5f42e314c6d8cd99effae6a5354604d461b8d392b5ec9530a54/markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf", size = 22940, upload-time = "2025-09-27T18:36:22.249Z" }, + { url = "https://files.pythonhosted.org/packages/19/ae/31c1be199ef767124c042c6c3e904da327a2f7f0cd63a0337e1eca2967a8/markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f", size = 21887, upload-time = "2025-09-27T18:36:23.535Z" }, + { url = "https://files.pythonhosted.org/packages/b2/76/7edcab99d5349a4532a459e1fe64f0b0467a3365056ae550d3bcf3f79e1e/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a", size = 23692, upload-time = "2025-09-27T18:36:24.823Z" }, + { url = "https://files.pythonhosted.org/packages/a4/28/6e74cdd26d7514849143d69f0bf2399f929c37dc2b31e6829fd2045b2765/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115", size = 21471, upload-time = "2025-09-27T18:36:25.95Z" }, + { url = "https://files.pythonhosted.org/packages/62/7e/a145f36a5c2945673e590850a6f8014318d5577ed7e5920a4b3448e0865d/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a", size = 22923, upload-time = "2025-09-27T18:36:27.109Z" }, + { url = "https://files.pythonhosted.org/packages/0f/62/d9c46a7f5c9adbeeeda52f5b8d802e1094e9717705a645efc71b0913a0a8/markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19", size = 14572, upload-time = "2025-09-27T18:36:28.045Z" }, + { url = "https://files.pythonhosted.org/packages/83/8a/4414c03d3f891739326e1783338e48fb49781cc915b2e0ee052aa490d586/markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01", size = 15077, upload-time = "2025-09-27T18:36:29.025Z" }, + { url = "https://files.pythonhosted.org/packages/35/73/893072b42e6862f319b5207adc9ae06070f095b358655f077f69a35601f0/markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c", size = 13876, upload-time = "2025-09-27T18:36:29.954Z" }, + { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" }, + { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" }, + { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" }, + { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" }, + { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" }, + { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" }, + { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" }, + { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" }, + { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" }, + { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" }, + { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" }, + { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" }, + { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" }, + { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" }, + { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" }, + { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" }, + { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" }, + { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" }, + { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" }, + { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" }, + { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" }, + { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" }, + { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" }, + { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" }, + { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" }, + { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" }, + { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" }, + { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" }, + { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" }, + { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, + { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, + { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, + { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, + { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, + { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, + { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, + { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, + { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, + { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, + { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, + { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, + { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, + { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, + { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, + { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, +] + +[[package]] +name = "mergedeep" +version = "1.3.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3a/41/580bb4006e3ed0361b8151a01d324fb03f420815446c7def45d02f74c270/mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8", size = 4661, upload-time = "2021-02-05T18:55:30.623Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/19/04f9b178c2d8a15b076c8b5140708fa6ffc5601fb6f1e975537072df5b2a/mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307", size = 6354, upload-time = "2021-02-05T18:55:29.583Z" }, +] + +[[package]] +name = "mkdocs" +version = "1.6.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "ghp-import" }, + { name = "jinja2" }, + { name = "markdown" }, + { name = "markupsafe" }, + { name = "mergedeep" }, + { name = "mkdocs-get-deps" }, + { name = "packaging" }, + { name = "pathspec" }, + { name = "pyyaml" }, + { name = "pyyaml-env-tag" }, + { name = "watchdog" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bc/c6/bbd4f061bd16b378247f12953ffcb04786a618ce5e904b8c5a01a0309061/mkdocs-1.6.1.tar.gz", hash = "sha256:7b432f01d928c084353ab39c57282f29f92136665bdd6abf7c1ec8d822ef86f2", size = 3889159, upload-time = "2024-08-30T12:24:06.899Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/5b/dbc6a8cddc9cfa9c4971d59fb12bb8d42e161b7e7f8cc89e49137c5b279c/mkdocs-1.6.1-py3-none-any.whl", hash = "sha256:db91759624d1647f3f34aa0c3f327dd2601beae39a366d6e064c03468d35c20e", size = 3864451, upload-time = "2024-08-30T12:24:05.054Z" }, +] + +[[package]] +name = "mkdocs-get-deps" +version = "0.2.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mergedeep" }, + { name = "platformdirs" }, + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ce/25/b3cccb187655b9393572bde9b09261d267c3bf2f2cdabe347673be5976a6/mkdocs_get_deps-0.2.2.tar.gz", hash = "sha256:8ee8d5f316cdbbb2834bc1df6e69c08fe769a83e040060de26d3c19fad3599a1", size = 11047, upload-time = "2026-03-10T02:46:33.632Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/29/744136411e785c4b0b744d5413e56555265939ab3a104c6a4b719dad33fd/mkdocs_get_deps-0.2.2-py3-none-any.whl", hash = "sha256:e7878cbeac04860b8b5e0ca31d3abad3df9411a75a32cde82f8e44b6c16ff650", size = 9555, upload-time = "2026-03-10T02:46:32.256Z" }, +] + +[[package]] +name = "mkdocs-material" +version = "9.7.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "babel" }, + { name = "backrefs" }, + { name = "colorama" }, + { name = "jinja2" }, + { name = "markdown" }, + { name = "mkdocs" }, + { name = "mkdocs-material-extensions" }, + { name = "paginate" }, + { name = "pygments" }, + { name = "pymdown-extensions" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f1/cd/c05d3a530ba7934f144fb45f7203cd236adc25c7bdcc34673d202f4b0278/mkdocs_material-9.7.7.tar.gz", hash = "sha256:c0649c065b1b0512d60aad8c10f947f8e455284475239b364b610f2deb4d0855", size = 4097923, upload-time = "2026-07-17T16:21:33.156Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ad/21/17c1bc9e6f47c972ad66fb2ac2568f99f90f1207eeb6fc3b34d094dba7b5/mkdocs_material-9.7.7-py3-none-any.whl", hash = "sha256:8ea9bb1737a5b524a5f9dcf2e1b4ebda8274ae3008aa7845720a97083bef708f", size = 9305438, upload-time = "2026-07-17T16:21:30.017Z" }, +] + +[[package]] +name = "mkdocs-material-extensions" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/79/9b/9b4c96d6593b2a541e1cb8b34899a6d021d208bb357042823d4d2cabdbe7/mkdocs_material_extensions-1.3.1.tar.gz", hash = "sha256:10c9511cea88f568257f960358a467d12b970e1f7b2c0e5fb2bb48cab1928443", size = 11847, upload-time = "2023-11-22T19:09:45.208Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5b/54/662a4743aa81d9582ee9339d4ffa3c8fd40a4965e033d77b9da9774d3960/mkdocs_material_extensions-1.3.1-py3-none-any.whl", hash = "sha256:adff8b62700b25cb77b53358dad940f3ef973dd6db797907c49e3c2ef3ab4e31", size = 8728, upload-time = "2023-11-22T19:09:43.465Z" }, +] + +[[package]] +name = "packaging" +version = "26.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/fa/3944b40b07da9ce895c0e6303a5ab7d53da063554f534556b134a54d6093/packaging-26.3.tar.gz", hash = "sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79", size = 313412, upload-time = "2026-08-04T18:15:28.737Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/63/34/ba1c580383c9eada3711951fef0795c80b829a078d72188184bcab9dd527/packaging-26.3-py3-none-any.whl", hash = "sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c", size = 129956, upload-time = "2026-08-04T18:15:27.159Z" }, +] + +[[package]] +name = "paginate" +version = "0.5.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ec/46/68dde5b6bc00c1296ec6466ab27dddede6aec9af1b99090e1107091b3b84/paginate-0.5.7.tar.gz", hash = "sha256:22bd083ab41e1a8b4f3690544afb2c60c25e5c9a63a30fa2f483f6c60c8e5945", size = 19252, upload-time = "2024-08-25T14:17:24.139Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/90/96/04b8e52da071d28f5e21a805b19cb9390aa17a47462ac87f5e2696b9566d/paginate-0.5.7-py2.py3-none-any.whl", hash = "sha256:b885e2af73abcf01d9559fd5216b57ef722f8c42affbb63942377668e35c7591", size = 13746, upload-time = "2024-08-25T14:17:22.55Z" }, +] + +[[package]] +name = "pathspec" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/82/42f767fc1c1143d6fd36efb827202a2d997a375e160a71eb2888a925aac1/pathspec-1.1.1.tar.gz", hash = "sha256:17db5ecd524104a120e173814c90367a96a98d07c45b2e10c2f3919fff91bf5a", size = 135180, upload-time = "2026-04-27T01:46:08.907Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl", hash = "sha256:a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189", size = 57328, upload-time = "2026-04-27T01:46:07.06Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.11.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f8/13/f870dd0b42690138e4e37a76b5138e5690ed4365a77071bb092d59037da0/platformdirs-4.11.11.tar.gz", hash = "sha256:b0befe8a90759e4a9a8b9820d434ae226a6549063210b596da0038a7a05aede4", size = 39949, upload-time = "2026-09-19T01:18:47.259Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/39/be/3ae887e84c38c4dd1c549cd5825adc93f48069a94fe1cb8ac52a9da16f15/platformdirs-4.11.11-py3-none-any.whl", hash = "sha256:972ea6b2b387155a536226a0750e46923d731d459a387c4a255c583ed2f64547", size = 24992, upload-time = "2026-09-19T01:18:45.825Z" }, +] + +[[package]] +name = "pygments" +version = "2.21.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/49/2e/ced460408999b33da6b31b0021b0f37d329e202d4169aeb164493778f25b/pygments-2.21.0.tar.gz", hash = "sha256:610ca751c9bc2492b38eb9a38a7fbc93edbbb2d7182edaf34e66ae493dee5c8c", size = 5005329, upload-time = "2026-08-17T08:02:48.824Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/46/17f022dd3e953bf20a04a028a21ec746d942f8d2af30fa0f124fa0e6a684/pygments-2.21.0-py3-none-any.whl", hash = "sha256:2363c69b61c4a97c838da3b130dcd6468f4848992b21a82f2a63ec34377137d9", size = 1250147, upload-time = "2026-08-17T08:02:44.912Z" }, +] + +[[package]] +name = "pymdown-extensions" +version = "12.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown" }, + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/7e/53144b512818c75d459523e3a91d9bf7b65cd6a1dfb964fe690adc8b3670/pymdown_extensions-12.0.1.tar.gz", hash = "sha256:88fe2c97fb9153b9f2cc1da59f1d7c7cb74971110daf33cccb2a5514e0f7e970", size = 866904, upload-time = "2026-09-15T23:20:33.187Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8c/30/c866b3ca8ad5cca79a3ee1a55c12d61d3e72381e5eadbfdaceb6006ebc84/pymdown_extensions-12.0.1-py3-none-any.whl", hash = "sha256:15d629e8035892b93d788a8424ccea754d4aa3cec247a64cbfe7b15089c9ad7f", size = 276957, upload-time = "2026-09-15T23:20:31.551Z" }, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/a0/39350dd17dd6d6c6507025c0e53aef67a9293a6d37d3511f23ea510d5800/pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b", size = 184227, upload-time = "2025-09-25T21:31:46.04Z" }, + { url = "https://files.pythonhosted.org/packages/05/14/52d505b5c59ce73244f59c7a50ecf47093ce4765f116cdb98286a71eeca2/pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956", size = 174019, upload-time = "2025-09-25T21:31:47.706Z" }, + { url = "https://files.pythonhosted.org/packages/43/f7/0e6a5ae5599c838c696adb4e6330a59f463265bfa1e116cfd1fbb0abaaae/pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8", size = 740646, upload-time = "2025-09-25T21:31:49.21Z" }, + { url = "https://files.pythonhosted.org/packages/2f/3a/61b9db1d28f00f8fd0ae760459a5c4bf1b941baf714e207b6eb0657d2578/pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198", size = 840793, upload-time = "2025-09-25T21:31:50.735Z" }, + { url = "https://files.pythonhosted.org/packages/7a/1e/7acc4f0e74c4b3d9531e24739e0ab832a5edf40e64fbae1a9c01941cabd7/pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b", size = 770293, upload-time = "2025-09-25T21:31:51.828Z" }, + { url = "https://files.pythonhosted.org/packages/8b/ef/abd085f06853af0cd59fa5f913d61a8eab65d7639ff2a658d18a25d6a89d/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0", size = 732872, upload-time = "2025-09-25T21:31:53.282Z" }, + { url = "https://files.pythonhosted.org/packages/1f/15/2bc9c8faf6450a8b3c9fc5448ed869c599c0a74ba2669772b1f3a0040180/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69", size = 758828, upload-time = "2025-09-25T21:31:54.807Z" }, + { url = "https://files.pythonhosted.org/packages/a3/00/531e92e88c00f4333ce359e50c19b8d1de9fe8d581b1534e35ccfbc5f393/pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e", size = 142415, upload-time = "2025-09-25T21:31:55.885Z" }, + { url = "https://files.pythonhosted.org/packages/2a/fa/926c003379b19fca39dd4634818b00dec6c62d87faf628d1394e137354d4/pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c", size = 158561, upload-time = "2025-09-25T21:31:57.406Z" }, + { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" }, + { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" }, + { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" }, + { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" }, + { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" }, + { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" }, + { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" }, + { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" }, + { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" }, + { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, + { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, + { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, + { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, + { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, + { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, + { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, + { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, + { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, + { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, + { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, + { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, + { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, + { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, + { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, + { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, + { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, + { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, + { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, + { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, + { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, +] + +[[package]] +name = "pyyaml-env-tag" +version = "1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/2e/79c822141bfd05a853236b504869ebc6b70159afc570e1d5a20641782eaa/pyyaml_env_tag-1.1.tar.gz", hash = "sha256:2eb38b75a2d21ee0475d6d97ec19c63287a7e140231e4214969d0eac923cd7ff", size = 5737, upload-time = "2025-05-13T15:24:01.64Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/11/432f32f8097b03e3cd5fe57e88efb685d964e2e5178a48ed61e841f7fdce/pyyaml_env_tag-1.1-py3-none-any.whl", hash = "sha256:17109e1a528561e32f026364712fee1264bc2ea6715120891174ed1b980d2e04", size = 4722, upload-time = "2025-05-13T15:23:59.629Z" }, +] + +[[package]] +name = "requests" +version = "2.34.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ac/c3/e2a2b89f2d3e2179abd6d00ebd70bff6273f37fb3e0cc209f48b39d00cbf/requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed", size = 142856, upload-time = "2026-05-14T19:25:27.735Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0", size = 73075, upload-time = "2026-05-14T19:25:26.443Z" }, +] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + +[[package]] +name = "urllib3" +version = "2.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e3/05/b17359e1cefb4f909b5e40b1b90a496d987258916dbbf88e842c729f510e/urllib3-2.8.0.tar.gz", hash = "sha256:63bf2ead4c879426ebf22ef2a781eeb4aa3b4ae798a0435506f8687fd5bb9b63", size = 458972, upload-time = "2026-09-15T19:29:36.253Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/92/9d/c4e665119135114480843e7ab388fa94d8480650450e6f8e26b70d323a4c/urllib3-2.8.0-py3-none-any.whl", hash = "sha256:0cf3cae568d36aa9576b28dfb35f11328f1cb974ca7647d9475ebb86c75ac6e3", size = 135717, upload-time = "2026-09-15T19:29:34.577Z" }, +] + +[[package]] +name = "watchdog" +version = "6.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/db/7d/7f3d619e951c88ed75c6037b246ddcf2d322812ee8ea189be89511721d54/watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282", size = 131220, upload-time = "2024-11-01T14:07:13.037Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/56/90994d789c61df619bfc5ce2ecdabd5eeff564e1eb47512bd01b5e019569/watchdog-6.0.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d1cdb490583ebd691c012b3d6dae011000fe42edb7a82ece80965b42abd61f26", size = 96390, upload-time = "2024-11-01T14:06:24.793Z" }, + { url = "https://files.pythonhosted.org/packages/55/46/9a67ee697342ddf3c6daa97e3a587a56d6c4052f881ed926a849fcf7371c/watchdog-6.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bc64ab3bdb6a04d69d4023b29422170b74681784ffb9463ed4870cf2f3e66112", size = 88389, upload-time = "2024-11-01T14:06:27.112Z" }, + { url = "https://files.pythonhosted.org/packages/44/65/91b0985747c52064d8701e1075eb96f8c40a79df889e59a399453adfb882/watchdog-6.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c897ac1b55c5a1461e16dae288d22bb2e412ba9807df8397a635d88f671d36c3", size = 89020, upload-time = "2024-11-01T14:06:29.876Z" }, + { url = "https://files.pythonhosted.org/packages/e0/24/d9be5cd6642a6aa68352ded4b4b10fb0d7889cb7f45814fb92cecd35f101/watchdog-6.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6eb11feb5a0d452ee41f824e271ca311a09e250441c262ca2fd7ebcf2461a06c", size = 96393, upload-time = "2024-11-01T14:06:31.756Z" }, + { url = "https://files.pythonhosted.org/packages/63/7a/6013b0d8dbc56adca7fdd4f0beed381c59f6752341b12fa0886fa7afc78b/watchdog-6.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ef810fbf7b781a5a593894e4f439773830bdecb885e6880d957d5b9382a960d2", size = 88392, upload-time = "2024-11-01T14:06:32.99Z" }, + { url = "https://files.pythonhosted.org/packages/d1/40/b75381494851556de56281e053700e46bff5b37bf4c7267e858640af5a7f/watchdog-6.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:afd0fe1b2270917c5e23c2a65ce50c2a4abb63daafb0d419fde368e272a76b7c", size = 89019, upload-time = "2024-11-01T14:06:34.963Z" }, + { url = "https://files.pythonhosted.org/packages/39/ea/3930d07dafc9e286ed356a679aa02d777c06e9bfd1164fa7c19c288a5483/watchdog-6.0.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bdd4e6f14b8b18c334febb9c4425a878a2ac20efd1e0b231978e7b150f92a948", size = 96471, upload-time = "2024-11-01T14:06:37.745Z" }, + { url = "https://files.pythonhosted.org/packages/12/87/48361531f70b1f87928b045df868a9fd4e253d9ae087fa4cf3f7113be363/watchdog-6.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c7c15dda13c4eb00d6fb6fc508b3c0ed88b9d5d374056b239c4ad1611125c860", size = 88449, upload-time = "2024-11-01T14:06:39.748Z" }, + { url = "https://files.pythonhosted.org/packages/5b/7e/8f322f5e600812e6f9a31b75d242631068ca8f4ef0582dd3ae6e72daecc8/watchdog-6.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6f10cb2d5902447c7d0da897e2c6768bca89174d0c6e1e30abec5421af97a5b0", size = 89054, upload-time = "2024-11-01T14:06:41.009Z" }, + { url = "https://files.pythonhosted.org/packages/68/98/b0345cabdce2041a01293ba483333582891a3bd5769b08eceb0d406056ef/watchdog-6.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:490ab2ef84f11129844c23fb14ecf30ef3d8a6abafd3754a6f75ca1e6654136c", size = 96480, upload-time = "2024-11-01T14:06:42.952Z" }, + { url = "https://files.pythonhosted.org/packages/85/83/cdf13902c626b28eedef7ec4f10745c52aad8a8fe7eb04ed7b1f111ca20e/watchdog-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:76aae96b00ae814b181bb25b1b98076d5fc84e8a53cd8885a318b42b6d3a5134", size = 88451, upload-time = "2024-11-01T14:06:45.084Z" }, + { url = "https://files.pythonhosted.org/packages/fe/c4/225c87bae08c8b9ec99030cd48ae9c4eca050a59bf5c2255853e18c87b50/watchdog-6.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a175f755fc2279e0b7312c0035d52e27211a5bc39719dd529625b1930917345b", size = 89057, upload-time = "2024-11-01T14:06:47.324Z" }, + { url = "https://files.pythonhosted.org/packages/30/ad/d17b5d42e28a8b91f8ed01cb949da092827afb9995d4559fd448d0472763/watchdog-6.0.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:c7ac31a19f4545dd92fc25d200694098f42c9a8e391bc00bdd362c5736dbf881", size = 87902, upload-time = "2024-11-01T14:06:53.119Z" }, + { url = "https://files.pythonhosted.org/packages/5c/ca/c3649991d140ff6ab67bfc85ab42b165ead119c9e12211e08089d763ece5/watchdog-6.0.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:9513f27a1a582d9808cf21a07dae516f0fab1cf2d7683a742c498b93eedabb11", size = 88380, upload-time = "2024-11-01T14:06:55.19Z" }, + { url = "https://files.pythonhosted.org/packages/a9/c7/ca4bf3e518cb57a686b2feb4f55a1892fd9a3dd13f470fca14e00f80ea36/watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13", size = 79079, upload-time = "2024-11-01T14:06:59.472Z" }, + { url = "https://files.pythonhosted.org/packages/5c/51/d46dc9332f9a647593c947b4b88e2381c8dfc0942d15b8edc0310fa4abb1/watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379", size = 79078, upload-time = "2024-11-01T14:07:01.431Z" }, + { url = "https://files.pythonhosted.org/packages/d4/57/04edbf5e169cd318d5f07b4766fee38e825d64b6913ca157ca32d1a42267/watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e", size = 79076, upload-time = "2024-11-01T14:07:02.568Z" }, + { url = "https://files.pythonhosted.org/packages/ab/cc/da8422b300e13cb187d2203f20b9253e91058aaf7db65b74142013478e66/watchdog-6.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f", size = 79077, upload-time = "2024-11-01T14:07:03.893Z" }, + { url = "https://files.pythonhosted.org/packages/2c/3b/b8964e04ae1a025c44ba8e4291f86e97fac443bca31de8bd98d3263d2fcf/watchdog-6.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26", size = 79078, upload-time = "2024-11-01T14:07:05.189Z" }, + { url = "https://files.pythonhosted.org/packages/62/ae/a696eb424bedff7407801c257d4b1afda455fe40821a2be430e173660e81/watchdog-6.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c", size = 79077, upload-time = "2024-11-01T14:07:06.376Z" }, + { url = "https://files.pythonhosted.org/packages/b5/e8/dbf020b4d98251a9860752a094d09a65e1b436ad181faf929983f697048f/watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2", size = 79078, upload-time = "2024-11-01T14:07:07.547Z" }, + { url = "https://files.pythonhosted.org/packages/07/f6/d0e5b343768e8bcb4cda79f0f2f55051bf26177ecd5651f84c07567461cf/watchdog-6.0.0-py3-none-win32.whl", hash = "sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a", size = 79065, upload-time = "2024-11-01T14:07:09.525Z" }, + { url = "https://files.pythonhosted.org/packages/db/d9/c495884c6e548fce18a8f40568ff120bc3a4b7b99813081c8ac0c936fa64/watchdog-6.0.0-py3-none-win_amd64.whl", hash = "sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680", size = 79070, upload-time = "2024-11-01T14:07:10.686Z" }, + { url = "https://files.pythonhosted.org/packages/33/e8/e40370e6d74ddba47f002a32919d91310d6074130fe4e17dabcafc15cbf1/watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f", size = 79067, upload-time = "2024-11-01T14:07:11.845Z" }, +]