diff --git a/calm-calatheas/.devcontainer/devcontainer.json b/calm-calatheas/.devcontainer/devcontainer.json new file mode 100644 index 00000000..66071997 --- /dev/null +++ b/calm-calatheas/.devcontainer/devcontainer.json @@ -0,0 +1,93 @@ +{ + "customizations": { + "vscode": { + "extensions": [ + "bierner.markdown-mermaid", + "charliermarsh.ruff", + "DavidAnson.vscode-markdownlint", + "esbenp.prettier-vscode", + "GitHub.vscode-pull-request-github", + "tamasfe.even-better-toml", + "-ms-python.autopep8" + ], + "settings": { + "[json]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "[markdown]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "[python]": { + "editor.defaultFormatter": "charliermarsh.ruff" + }, + "[toml]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "[yaml]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "editor.formatOnSave": true, + "editor.formatOnSaveMode": "file", + "files.exclude": { + "**/.pytest_cache": true, + "**/.ruff_cache": true, + "**/__pycache__": true, + ".venv": true, + "node_modules": true, + "site": true + }, + "files.insertFinalNewline": true, + "files.watcherExclude": { + "**/.pytest_cache": true, + "**/.ruff_cache": true, + "**/__pycache__": true, + "**/dist": true, + ".git/objects/**": true, + ".git/subtree-cache/**": true, + ".venv": true, + "node_modules": true, + "site": true + }, + "python.analysis.typeCheckingMode": "standard", + "python.defaultInterpreterPath": "${containerWorkspaceFolder}/.venv/bin/python", + "python.testing.pytestEnabled": true, + "python.testing.unittestEnabled": false + } + } + }, + "features": { + "ghcr.io/devcontainers-extra/features/apt-packages": { + "packages": ["gnupg2", "graphviz"] + }, + "ghcr.io/devcontainers/features/docker-in-docker:2": {}, + "ghcr.io/devcontainers/features/node:1": { + "version": "lts" + }, + "ghcr.io/devcontainers/features/python:1": { + "toolsToInstall": ["uv"], + "version": "3.13" + } + }, + "forwardPorts": [8000, 9000], + "image": "mcr.microsoft.com/devcontainers/base:noble", + "name": "Calm Calatheas 🪴", + "onCreateCommand": { + "npm": "bash .devcontainer/npm.sh", + "uv": "bash .devcontainer/uv.sh" + }, + "portsAttributes": { + "8000": { + "label": "Development Server", + "onAutoForward": "notify" + }, + "9000": { + "label": "Documentation Server", + "onAutoForward": "notify" + } + }, + "postCreateCommand": { + "playwright": "bash .devcontainer/playwright.sh", + "pre-commit": "bash .devcontainer/pre-commit.sh" + }, + "runArgs": ["--gpus", "all"] +} diff --git a/calm-calatheas/.devcontainer/npm.sh b/calm-calatheas/.devcontainer/npm.sh new file mode 100755 index 00000000..7ffc1b66 --- /dev/null +++ b/calm-calatheas/.devcontainer/npm.sh @@ -0,0 +1,4 @@ +#!/bin/bash + +# Install or update dependencies +npm install diff --git a/calm-calatheas/.devcontainer/playwright.sh b/calm-calatheas/.devcontainer/playwright.sh new file mode 100755 index 00000000..0aa5ad7a --- /dev/null +++ b/calm-calatheas/.devcontainer/playwright.sh @@ -0,0 +1,4 @@ +#!/bin/bash + +# Install playwright dependencies +uv run playwright install --with-deps diff --git a/calm-calatheas/.devcontainer/pre-commit.sh b/calm-calatheas/.devcontainer/pre-commit.sh new file mode 100755 index 00000000..cc98e504 --- /dev/null +++ b/calm-calatheas/.devcontainer/pre-commit.sh @@ -0,0 +1,7 @@ +#!/bin/bash + +# Mark the current directory as safe for Git operations +git config --global --add safe.directory $PWD + +# Install pre-commit hooks using uv +uv run pre-commit install diff --git a/calm-calatheas/.devcontainer/uv.sh b/calm-calatheas/.devcontainer/uv.sh new file mode 100755 index 00000000..e24759bd --- /dev/null +++ b/calm-calatheas/.devcontainer/uv.sh @@ -0,0 +1,5 @@ +#!/bin/bash + +# Install Python dependencies using uv +uv venv --allow-existing +uv sync diff --git a/calm-calatheas/.envrc b/calm-calatheas/.envrc new file mode 100644 index 00000000..894571bf --- /dev/null +++ b/calm-calatheas/.envrc @@ -0,0 +1,3 @@ +source_url "https://raw.githubusercontent.com/cachix/devenv/82c0147677e510b247d8b9165c54f73d32dfd899/direnvrc" "sha256-7u4iDd1nZpxL4tCzmPG0dQgC5V+/44Ba+tHkPob1v2k=" + +use devenv diff --git a/calm-calatheas/.gitattributes b/calm-calatheas/.gitattributes new file mode 100644 index 00000000..6313b56c --- /dev/null +++ b/calm-calatheas/.gitattributes @@ -0,0 +1 @@ +* text=auto eol=lf diff --git a/calm-calatheas/.github/workflows/lint.yaml b/calm-calatheas/.github/workflows/lint.yaml new file mode 100644 index 00000000..71d6e3ef --- /dev/null +++ b/calm-calatheas/.github/workflows/lint.yaml @@ -0,0 +1,57 @@ +name: Lint + +on: + push: + branches: + - main + pull_request: + +concurrency: lint-${{ github.sha }} + +jobs: + lint: + runs-on: ubuntu-latest + + env: + PYTHON_VERSION: "3.13" + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Python ${{ env.PYTHON_VERSION }} + uses: actions/setup-python@v5 + with: + python-version: ${{ env.PYTHON_VERSION }} + + - name: Install uv + run: python -m pip install uv + + - name: Cache the virtualenv + uses: actions/cache@v4 + with: + path: ./.venv + key: ${{ runner.os }}-venv-${{ hashFiles('**/uv.lock') }} + + - name: Install Python dependencies + run: uv venv --allow-existing && uv sync + + - name: Update GITHUB_PATH + run: echo "$(uv python find)" >> $GITHUB_PATH + + - name: Setup Node.js and dependencies + uses: actions/setup-node@v4 + with: + node-version: 20 + cache: npm + registry-url: "https://npm.pkg.github.com" + + - name: Install Node.js dependencies + run: npm install + shell: bash + + - name: Run pre-commit hooks + uses: pre-commit/action@v3.0.1 + + - name: Build the documentation + run: uv run task build-docs diff --git a/calm-calatheas/.github/workflows/publish-docs.yaml b/calm-calatheas/.github/workflows/publish-docs.yaml new file mode 100644 index 00000000..6701fa28 --- /dev/null +++ b/calm-calatheas/.github/workflows/publish-docs.yaml @@ -0,0 +1,50 @@ +name: Publish Documentation + +concurrency: + group: "docs" + +on: + push: + branches: + - main + +jobs: + publish: + runs-on: ubuntu-latest + + permissions: + contents: write + pages: write + + env: + PYTHON_VERSION: "3.13" + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - run: git config --global user.email "calm-calatheas@github.com" + - run: git config --global user.name "Calm Calatheas" + + - name: Set up Python ${{ env.PYTHON_VERSION }} + uses: actions/setup-python@v5 + with: + python-version: ${{ env.PYTHON_VERSION }} + + - name: Install uv + run: python -m pip install uv + + - name: Cache the virtualenv + uses: actions/cache@v4 + with: + path: ./.venv + key: ${{ runner.os }}-venv-${{ hashFiles('**/uv.lock') }} + + - name: Install Python dependencies + run: uv venv --allow-existing && uv sync + + - name: Publish documentation + run: uv run mkdocs gh-deploy + shell: bash diff --git a/calm-calatheas/.gitignore b/calm-calatheas/.gitignore new file mode 100644 index 00000000..4d8efb00 --- /dev/null +++ b/calm-calatheas/.gitignore @@ -0,0 +1,321 @@ +### Python ### +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + +### Python Patch ### +# Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration +poetry.toml + +# ruff +.ruff_cache/ + +# LSP config files +pyrightconfig.json + +# Playwright +test-results/ + +### Node ### +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) +web_modules/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional stylelint cache +.stylelintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# Next.js build output +.next +out + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and not Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# vuepress v2.x temp and cache directory +.temp + +# Docusaurus cache and generated files +.docusaurus + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test + +# yarn v2 +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* + +### Node Patch ### +# Serverless Webpack directories +.webpack/ + +# Optional stylelint cache + +# SvelteKit build / generate output +.svelte-kit + +# Devenv +.devenv* +devenv.local.nix + +# Direnv +.direnv diff --git a/calm-calatheas/.markdownlint.json b/calm-calatheas/.markdownlint.json new file mode 100644 index 00000000..76de1448 --- /dev/null +++ b/calm-calatheas/.markdownlint.json @@ -0,0 +1,16 @@ +{ + "MD013": { + "code_blocks": false, + "line_length": 119, + "tables": false + }, + "MD024": { + "siblings_only": true + }, + "MD030": false, + "MD033": { + "allowed_elements": ["div", "img", "figcaption", "figure", "source", "video"] + }, + "MD046": false, + "default": true +} diff --git a/calm-calatheas/.pre-commit-config.yaml b/calm-calatheas/.pre-commit-config.yaml new file mode 100644 index 00000000..518d9e4c --- /dev/null +++ b/calm-calatheas/.pre-commit-config.yaml @@ -0,0 +1,47 @@ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v5.0.0 + hooks: + - id: check-merge-conflict + name: Check for merge conflicts + - id: check-json + name: Check for JSON syntax errors + - id: check-toml + name: Check for TOML syntax errors + - id: check-yaml + name: Check for YAML syntax errors + args: [--unsafe] + - id: end-of-file-fixer + name: Ensure files end with a newline + - id: trailing-whitespace + name: Trim trailing whitespace + args: [--markdown-linebreak-ext=md] + + - repo: https://github.com/pre-commit/mirrors-prettier + rev: v3.1.0 + hooks: + - id: prettier + name: Format code with Prettier + additional_dependencies: + - prettier@3.6.2 + - prettier-plugin-sort-json@4.1.1 + - prettier-plugin-toml@2.0.6 + + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.12.3 + hooks: + - id: ruff + name: Check for Python linting errors + args: [--fix] + - id: ruff-format + name: Format Python code + + - repo: https://github.com/RobertCraigie/pyright-python + rev: v1.1.403 + hooks: + - id: pyright + name: Check for Python type errors + entry: uv run pyright + language: system + "types_or": [python, pyi] + require_serial: true diff --git a/calm-calatheas/.prettierignore b/calm-calatheas/.prettierignore new file mode 100644 index 00000000..8cc9258d --- /dev/null +++ b/calm-calatheas/.prettierignore @@ -0,0 +1,3 @@ +# Add files here to ignore them from prettier formatting +/dist +/coverage diff --git a/calm-calatheas/.prettierrc b/calm-calatheas/.prettierrc new file mode 100644 index 00000000..a17940f3 --- /dev/null +++ b/calm-calatheas/.prettierrc @@ -0,0 +1,11 @@ +{ + "jsonRecursiveSort": true, + "plugins": ["prettier-plugin-sort-json", "prettier-plugin-toml"], + "printWidth": 119, + "proseWrap": "preserve", + "reorderKeys": true, + "semi": true, + "singleQuote": false, + "tabWidth": 4, + "useTabs": false +} diff --git a/calm-calatheas/Dockerfile b/calm-calatheas/Dockerfile new file mode 100644 index 00000000..94d6fb3f --- /dev/null +++ b/calm-calatheas/Dockerfile @@ -0,0 +1,56 @@ +# Use the official Python image as a base image for building the application +FROM python:3.13 AS build-image + +# Set the working directory +WORKDIR /build + +# Copy the wheel file into the container. +COPY --chown=root:root --chmod=0755 ./dist/*.whl . + +# Install package dependencies, then clean up. Use a cache mount to speed up the process. +RUN --mount=type=cache,mode=0755,target=/root/.cache/pip \ + pip install --target=./ ./*.whl && \ + rm ./*.whl + +# Use a slim version of the base Python image to reduce the final image size +FROM python:3.13-slim + +# Add image metadata +LABEL org.opencontainers.image.authors="Calm Calatheas" +LABEL org.opencontainers.image.description="This is the app built by the Calm Calatheas team for the Python Discord Code Jam 2025" +LABEL org.opencontainers.image.licenses=MIT +LABEL org.opencontainers.image.source=https://github.com/cj12-calm-calatheas/code-jam-12 +LABEL org.opencontainers.image.title="Calm Calatheas App" + +# Add a non-root user and group +RUN addgroup calm-calatheas && \ + adduser --ingroup calm-calatheas calm-calatheas + +# Install required system dependencies +RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ + --mount=type=cache,target=/var/lib/apt,sharing=locked \ + apt-get update && \ + apt-get install --no-install-recommends --no-install-suggests -y curl + +# Set the working directory +WORKDIR /application + +# Copy in the static assets for the app +COPY --chown=root:root --chmod=0755 ./app ./app + +# Copy in the built dependencies +COPY --chown=root:root --chmod=0755 --from=build-image /build ./ + +# Switch to the non-root user +USER calm-calatheas + +# Set default values for the environment variables +ENV HOST=0.0.0.0 +ENV PORT=8000 + +# Configure a healthcheck to ensure the application is running +HEALTHCHECK --interval=30s --timeout=5s --start-period=120s --retries=6 \ + CMD ["sh", "-c", "curl --fail \"http://localhost:${PORT}/healthcheck\" || exit 1"] + +# Start the application +ENTRYPOINT [ "python", "-m", "calm_calatheas" ] diff --git a/calm-calatheas/LICENSE.txt b/calm-calatheas/LICENSE.txt new file mode 100644 index 00000000..a3c2eed9 --- /dev/null +++ b/calm-calatheas/LICENSE.txt @@ -0,0 +1,7 @@ +Copyright 2025 Calm Calatheas 🪴 + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/calm-calatheas/README.md b/calm-calatheas/README.md new file mode 100644 index 00000000..05822f50 --- /dev/null +++ b/calm-calatheas/README.md @@ -0,0 +1,37 @@ +# Pokedexter + +_Who's that Pokémon?_ + +Ever wondered if your cat could be a Pokémon? Curious about what’s hiding in your cupboard? **Pokedexter** is an AI-powered +Pokédex that helps you discover Pokémon wherever you are. Just snap a photo, and Pokedexter will identify the Pokémon—maybe +even ones you never expected! + +Open Pokedexter on your phone and start discovering Pokémon all around you! + +## Documentation + +For detailed documentation, please refer to our [GitHub Pages site](https://cj12-calm-calatheas.github.io/code-jam-12/). + +## Quick Start + +Get started quickly with the following resources: + +- [Introduction](https://cj12-calm-calatheas.github.io/code-jam-12/) +- [User Guide](https://cj12-calm-calatheas.github.io/code-jam-12/user-guide/) +- [Setup Guide](https://cj12-calm-calatheas.github.io/code-jam-12/setup-guide/) +- [Contributor Guide](https://cj12-calm-calatheas.github.io/code-jam-12/contributor-guide/) +- [Design Documentation](https://cj12-calm-calatheas.github.io/code-jam-12/design/) +- [Code Reference](https://cj12-calm-calatheas.github.io/code-jam-12/code/) + +## About the Team + +This project has been built by the Calm Calatheas team for the [Python Discord Code Jam 2025](https://pythondiscord.com/events/code-jams/12/). +Please feel free to reach out if you have any questions, or need a hand with anything! + +| | Name | Contributions | +| --------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------- | ------------------------------------------------------------------ | +| [TFBlunt](https://github.com/thijsfranck) | [TFBlunt](https://github.com/thijsfranck) | Team Lead, Frontend, Description Generation, Docs | +| [leoluy](https://github.com/leolhuile) | [leoluy](https://github.com/leolhuile) | Description Generation, Model Selection, Frontend Mockup, Ideation | +| [zike01](https://github.com/Zike01) | [Zike01](https://github.com/Zike01) | Ideation | +| [Flonc](https://github.com/FloncDev) | [Flonc](https://github.com/FloncDev) | Initial Frontend Prototype | +| [esmaycat](https://github.com/esmaycat) | [esmaycat](https://github.com/esmaycat) | Object Detection, Transformers.js integration, Favourites Feature | diff --git a/calm-calatheas/app/assets/logo-128x128.png b/calm-calatheas/app/assets/logo-128x128.png new file mode 100644 index 00000000..51375e09 Binary files /dev/null and b/calm-calatheas/app/assets/logo-128x128.png differ diff --git a/calm-calatheas/app/assets/logo-16x16.png b/calm-calatheas/app/assets/logo-16x16.png new file mode 100644 index 00000000..c156031c Binary files /dev/null and b/calm-calatheas/app/assets/logo-16x16.png differ diff --git a/calm-calatheas/app/assets/logo-48x48.png b/calm-calatheas/app/assets/logo-48x48.png new file mode 100644 index 00000000..287c4ca3 Binary files /dev/null and b/calm-calatheas/app/assets/logo-48x48.png differ diff --git a/calm-calatheas/app/assets/pokemongb.ttf b/calm-calatheas/app/assets/pokemongb.ttf new file mode 100644 index 00000000..b5025f06 Binary files /dev/null and b/calm-calatheas/app/assets/pokemongb.ttf differ diff --git a/calm-calatheas/app/favicon.ico b/calm-calatheas/app/favicon.ico new file mode 100644 index 00000000..c156031c Binary files /dev/null and b/calm-calatheas/app/favicon.ico differ diff --git a/calm-calatheas/app/frontend/__init__.py b/calm-calatheas/app/frontend/__init__.py new file mode 100644 index 00000000..3fd20621 --- /dev/null +++ b/calm-calatheas/app/frontend/__init__.py @@ -0,0 +1,3 @@ +from .app import App + +__all__ = ["App"] diff --git a/calm-calatheas/app/frontend/app.py b/calm-calatheas/app/frontend/app.py new file mode 100644 index 00000000..c1aa0204 --- /dev/null +++ b/calm-calatheas/app/frontend/app.py @@ -0,0 +1,95 @@ +from typing import override + +import reactivex.operators as op +from js import Event, document +from pyodide.ffi.wrappers import add_event_listener + +from frontend.base import Component +from frontend.components import Footer, Header, LoadingCaptionModel, Pokemon +from frontend.services import pokemon + +TEMPLATE = """ +
+
+
+
+
+
+
+

Welcome to your Pokedex!

+

Take a picture or upload an image to discover the Pokemon inside.

+
+
+
+

Your Pokemon

+
+
+ +
+
+
+
+
+
+ +
+""" + + +class App(Component): + """The main application class.""" + + @override + def build(self) -> str: + return TEMPLATE + + @override + def pre_destroy(self) -> None: + self._footer.destroy() + self._header.destroy() + self._loading_caption_model.destroy() + + @override + def on_render(self) -> None: + self._footer = Footer(document.getElementById("app-footer")) + self._footer.render() + + self._header = Header(document.getElementById("app-header")) + self._header.render() + + self._notifications = document.getElementById("notifications") + self._loading_caption_model = LoadingCaptionModel(self._notifications) + + self._pokemon = Pokemon(document.getElementById("pokemon")) + self._pokemon.render() + + self._pokemon_refresh = document.getElementById("pokemon-refresh") + self._pokemon_refresh_icon = document.getElementById("pokemon-refresh-icon") + add_event_listener(self._pokemon_refresh, "click", self._on_pokemon_refresh) + + # Update the UI whenever the loading state changes + pokemon.is_refreshing.pipe( + op.take_until(self.destroyed), + ).subscribe(lambda is_refreshing: self._handle_pokemon_is_refreshing(is_refreshing=is_refreshing)) + + def _on_pokemon_refresh(self, event: Event) -> None: + """Refresh the list of Pokemon.""" + if event.currentTarget.hasAttribute("disabled"): # type: ignore[currentTarget is available] + return + + pokemon.refresh() + + def _handle_pokemon_is_refreshing(self, *, is_refreshing: bool) -> None: + """Spin the refresh icon while the Pokemon list is being refreshed.""" + if is_refreshing: + self._pokemon_refresh.setAttribute("disabled", "") + self._pokemon_refresh_icon.classList.add("fa-spin") + else: + self._pokemon_refresh.removeAttribute("disabled") + self._pokemon_refresh_icon.classList.remove("fa-spin") diff --git a/calm-calatheas/app/frontend/base/__init__.py b/calm-calatheas/app/frontend/base/__init__.py new file mode 100644 index 00000000..18b1cb04 --- /dev/null +++ b/calm-calatheas/app/frontend/base/__init__.py @@ -0,0 +1,4 @@ +from .component import Component +from .service import Service + +__all__ = ["Component", "Service"] diff --git a/calm-calatheas/app/frontend/base/component.py b/calm-calatheas/app/frontend/base/component.py new file mode 100644 index 00000000..33a7b96f --- /dev/null +++ b/calm-calatheas/app/frontend/base/component.py @@ -0,0 +1,65 @@ +from abc import ABC, abstractmethod +from typing import cast +from uuid import uuid4 + +from js import DOMParser +from pyodide.ffi import JsDomElement +from reactivex import Subject + + +class Component(ABC): + """A base class for all components.""" + + parser = DOMParser.new() # type: ignore[] + + def __init__(self, root: JsDomElement) -> None: + self.destroyed = Subject[None]() + self.element: JsDomElement | None = None + self.guid = uuid4() + self.root = root + + @abstractmethod + def build(self) -> str: + """Build the component's template and output it as an HTML string.""" + + def destroy(self) -> None: + """Destroy the component and clean up resources.""" + self.destroyed.on_next(None) + self.destroyed.dispose() + self.remove() + self.on_destroy() + + def on_destroy(self) -> None: + """Hook to perform actions after the component is destroyed.""" + return + + def on_render(self) -> None: + """Hook to perform actions after rendering the component.""" + return + + def pre_destroy(self) -> None: + """Hook to perform actions before the component is destroyed.""" + return + + def pre_render(self) -> None: + """Hook to perform actions before rendering the component.""" + return + + def remove(self) -> None: + """Remove the component's element from the DOM.""" + if self.element: + self.element.remove() # type: ignore[remove method is available] + + def render(self) -> None: + """Create a new DOM element for the component and append it to the root element.""" + self.pre_render() + + # Render the given template as an HTML document + template = self.build() + document = self.parser.parseFromString(template, "text/html") + + # Take the first child of the body and append it to the root element + self.element = cast("JsDomElement", document.body.firstChild) + self.root.appendChild(self.element) + + self.on_render() diff --git a/calm-calatheas/app/frontend/base/service.py b/calm-calatheas/app/frontend/base/service.py new file mode 100644 index 00000000..b234b3f3 --- /dev/null +++ b/calm-calatheas/app/frontend/base/service.py @@ -0,0 +1,18 @@ +from reactivex import Subject + + +class Service: + """Base class for all services.""" + + def __init__(self) -> None: + self.destroyed = Subject[None]() + + def destroy(self) -> None: + """Destroy the service.""" + self.destroyed.on_next(None) + self.destroyed.dispose() + self.on_destroy() + + def on_destroy(self) -> None: + """Hook to perform actions after the service is destroyed.""" + return diff --git a/calm-calatheas/app/frontend/components/__init__.py b/calm-calatheas/app/frontend/components/__init__.py new file mode 100644 index 00000000..13275351 --- /dev/null +++ b/calm-calatheas/app/frontend/components/__init__.py @@ -0,0 +1,9 @@ +from .description import Description +from .description_dropdown import DescriptionDropdown +from .footer import Footer +from .header import Header +from .loading_caption_model import LoadingCaptionModel +from .pokemon import Pokemon +from .theme import Theme + +__all__ = ["Description", "DescriptionDropdown", "Footer", "Header", "LoadingCaptionModel", "Pokemon", "Theme"] diff --git a/calm-calatheas/app/frontend/components/camera.py b/calm-calatheas/app/frontend/components/camera.py new file mode 100644 index 00000000..ae7e1a5e --- /dev/null +++ b/calm-calatheas/app/frontend/components/camera.py @@ -0,0 +1,135 @@ +from typing import TYPE_CHECKING, Optional, cast, override + +import reactivex.operators as op +from js import Blob, Event, MediaStream, document +from pyodide.ffi import JsDomElement, create_once_callable +from pyodide.ffi.wrappers import add_event_listener + +from frontend.base import Component +from frontend.services import Camera as CameraService +from frontend.services import reader + +if TYPE_CHECKING: + from js import JsVideoElement + + +TEMPLATE = """ + +""" + + +class Camera(Component): + """Component for displaying the camera feed.""" + + def __init__(self, root: JsDomElement) -> None: + super().__init__(root) + self._camera = CameraService() + + @override + def build(self) -> str: + return TEMPLATE + + @override + def on_destroy(self) -> None: + self._camera.destroy() + + @override + def on_render(self) -> None: + self._camera_capture = document.getElementById("camera-capture") + self._camera_container = document.getElementById("camera-container") + self._camera_close = document.getElementById("camera-close") + self._camera_stream = cast("JsVideoElement", document.getElementById("camera-stream")) + self._camera_switch = document.getElementById("camera-switch") + + add_event_listener(self._camera_capture, "click", self._handle_capture) + add_event_listener(self._camera_close, "click", self._handle_close) + add_event_listener(self._camera_switch, "click", self._handle_toggle_facing_mode) + + # Update the UI whenever the media stream is being acquired + self._camera.is_acquiring_media_stream.pipe( + op.take_until(self.destroyed), + ).subscribe(lambda status: self._handle_is_acquiring_media_stream(status=status)) + + # Update the UI whenever the media stream is available + self._camera.media_stream.pipe( + op.take_until(self.destroyed), + ).subscribe(self._handle_media_stream) + + self._camera.acquire_media_stream() + + def _handle_capture(self, event: Event) -> None: + """Capture a snapshot from the camera stream.""" + if event.currentTarget.hasAttribute("disabled"): # type: ignore[currentTarget is available] + return + + canvas = document.createElement("canvas") + canvas.width = self._camera_stream.videoWidth + canvas.height = self._camera_stream.videoHeight + + context = canvas.getContext("2d") + + context.drawImage( + self._camera_stream, + 0, + 0, + self._camera_stream.videoWidth, + self._camera_stream.videoHeight, + ) + + canvas.toBlob(create_once_callable(self._handle_capture_success), "image/png") + + def _handle_capture_success(self, blob: Blob) -> None: + """Send the captured image to the reader.""" + reader.read(blob) + self.destroy() + + def _handle_close(self, _: Event) -> None: + """Close the camera modal.""" + self.destroy() + + def _handle_is_acquiring_media_stream(self, *, status: bool) -> None: + """Set the spinner on the capture button.""" + if status: + self._camera_capture.classList.add("is-loading") + else: + self._camera_capture.classList.remove("is-loading") + + def _handle_media_stream(self, stream: Optional[MediaStream]) -> None: + """ + Set the camera stream source. + + If no source is given, disable the controls and show a loading indicator. + """ + self._camera_stream.srcObject = stream + + if not stream: + self._camera_capture.setAttribute("disabled", "") + self._camera_switch.setAttribute("disabled", "") + self._camera_container.classList.add("is-skeleton") + else: + self._camera_capture.removeAttribute("disabled") + self._camera_switch.removeAttribute("disabled") + self._camera_container.classList.remove("is-skeleton") + + def _handle_toggle_facing_mode(self, event: Event) -> None: + """Switch the preferred facing mode between user and environment.""" + if event.currentTarget.hasAttribute("disabled"): # type: ignore[currentTarget is available] + return + + self._camera.toggle_facing_mode() diff --git a/calm-calatheas/app/frontend/components/description.py b/calm-calatheas/app/frontend/components/description.py new file mode 100644 index 00000000..bcae56d0 --- /dev/null +++ b/calm-calatheas/app/frontend/components/description.py @@ -0,0 +1,173 @@ +from typing import override + +from js import document +from pyodide.ffi import JsDomElement + +from frontend.base import Component +from frontend.components.description_dropdown import DescriptionDropdown +from frontend.models import PokemonRecord + +LOADING_TEMPLATE = """ +
+
+
+

+
+
+
+

Name

+

Category

+
+ Type +
+
+
+
+ +
+
+
+
+
+
+ Ability +
+
+
+
+ Habitat +
+
+
+
+ Height +
+
+
+
+ Weight +
+
+
+
+""" + +TYPE_TEMPLATE = """ +{type_name} +""" + +TEMPLATE = """ +
+
+
+

+ {name} +

+
+
+
+

+ {name} + + + +

+

The {category} Pokemon

+
{types}
+
+
+
+ +
+
+
+

{flavor_text}

+
+
+
+
+ Ability + {ability} +
+
+
+
+ Habitat + {habitat} +
+
+
+
+ Height + {height} m +
+
+
+
+ Weight + {weight} kg +
+
+
+
+""" + + +class Description(Component): + """Test component to demonstrate the descriptions service.""" + + def __init__(self, root: JsDomElement, description: PokemonRecord | None) -> None: + super().__init__(root) + self._description = description + + @override + def build(self) -> str: + if not self._description: + return LOADING_TEMPLATE + + types = "\n".join( + TYPE_TEMPLATE.format(type_class=type_, type_name=type_.capitalize()) for type_ in self._description.types + ) + + return TEMPLATE.format( + guid=self.guid, + favourite_icon_class="" if self._description.favourite else "is-hidden", + image_url=self._description.img_url, + name=self._description.name, + category=self._description.category.capitalize(), + types=types, + flavor_text=self._description.flavor_text, + ability=self._description.ability.capitalize(), + habitat=self._description.habitat.capitalize(), + height=self._description.height, + weight=self._description.weight, + ) + + @override + def on_render(self) -> None: + if not self._description: + return + + self._description_dropdown = DescriptionDropdown( + document.getElementById(f"dropdown-{self.guid}"), + self._description, + ) + + self._description_dropdown.render() + + @override + def pre_destroy(self) -> None: + self._description_dropdown.destroy() diff --git a/calm-calatheas/app/frontend/components/description_dropdown.py b/calm-calatheas/app/frontend/components/description_dropdown.py new file mode 100644 index 00000000..2b5e199a --- /dev/null +++ b/calm-calatheas/app/frontend/components/description_dropdown.py @@ -0,0 +1,61 @@ +from typing import override +from uuid import uuid4 + +from js import Event, document +from pyodide.ffi import JsDomElement +from pyodide.ffi.wrappers import add_event_listener + +from frontend.base import Component +from frontend.models import PokemonRecord +from frontend.services import pokemon + +TEMPLATE = """ + +""" + + +class DescriptionDropdown(Component): + """Dropdown for Pokemon descriptions.""" + + def __init__(self, root: JsDomElement, description: PokemonRecord) -> None: + super().__init__(root) + self._description = description + self._favourite_guid = uuid4() + self._delete_guid = uuid4() + + @override + def build(self) -> str: + return TEMPLATE.format( + favourite_guid=self._favourite_guid, + favourite_text="Unfavourite" if self._description.favourite else "Favourite", + delete_guid=self._delete_guid, + ) + + @override + def on_render(self) -> None: + self._delete_button = document.getElementById(f"delete-{self._delete_guid}") + self._favourite_button = document.getElementById(f"favourite-{self._favourite_guid}") + + add_event_listener(self._delete_button, "click", self._on_delete_button_click) + add_event_listener(self._favourite_button, "click", self._on_favourite_button_click) + + def _on_delete_button_click(self, _: Event) -> None: + pokemon.delete(self._description.name) + + def _on_favourite_button_click(self, _: Event) -> None: + self._description.favourite = not self._description.favourite + pokemon.put(self._description) diff --git a/calm-calatheas/app/frontend/components/footer.py b/calm-calatheas/app/frontend/components/footer.py new file mode 100644 index 00000000..a4cf42ae --- /dev/null +++ b/calm-calatheas/app/frontend/components/footer.py @@ -0,0 +1,112 @@ +from typing import TYPE_CHECKING, cast, override + +import reactivex.operators as op +from js import Event, document +from pyodide.ffi import JsDomElement +from pyodide.ffi.wrappers import add_event_listener +from reactivex import combine_latest + +from frontend.base import Component +from frontend.services import caption, pokemon, reader + +from .camera import Camera + +if TYPE_CHECKING: + from js import JsButtonElement, JsFileInputElement + +TEMPLATE = """ + +""" + + +class Footer(Component): + """Footer for the application.""" + + def __init__(self, root: JsDomElement) -> None: + super().__init__(root) + self._overlay: Camera | None = None + + @override + def build(self) -> str: + return TEMPLATE + + @override + def pre_destroy(self) -> None: + if self._overlay: + self._overlay.destroy() + + @override + def on_render(self) -> None: + self._camera_button = cast("JsButtonElement", document.getElementById("camera-button")) + self._file_input = cast("JsFileInputElement", document.getElementById("file-input")) + self._upload_button = cast("JsButtonElement", document.getElementById("upload-button")) + + add_event_listener(self._camera_button, "click", self._on_camera_button_click) + add_event_listener(self._file_input, "change", self._on_file_input_change) + add_event_listener(self._upload_button, "click", self._on_upload_button_click) + + # Disable the controls while the model is loading or generating + combine_latest(caption.is_loading_model, pokemon.is_generating).pipe( + op.map(lambda is_loading: any(is_loading)), + op.take_until(self.destroyed), + ).subscribe(lambda is_loading: self._handle_is_loading(is_loading=is_loading)) + + def _handle_is_loading(self, *, is_loading: bool) -> None: + """Handle the loading state of the footer.""" + if is_loading: + self._camera_button.setAttribute("disabled", "") + self._file_input.setAttribute("disabled", "") + self._upload_button.setAttribute("disabled", "") + else: + self._camera_button.removeAttribute("disabled") + self._file_input.removeAttribute("disabled") + self._upload_button.removeAttribute("disabled") + + def _on_camera_button_click(self, event: Event) -> None: + """Open the camera modal.""" + if event.currentTarget.hasAttribute("disabled"): # type: ignore[currentTarget is available] + return + + self._overlay = Camera(self.root) + self._overlay.render() + + def _on_file_input_change(self, event: Event) -> None: + """Send the selected file to the reader.""" + if event.target.hasAttribute("disabled"): + return + + files = self._file_input.files + + if files.length: + reader.read(files.item(0)) + + def _on_upload_button_click(self, event: Event) -> None: + """Trigger the hidden file input.""" + if event.currentTarget.hasAttribute("disabled"): # type: ignore[currentTarget is available] + return + + self._file_input.click() diff --git a/calm-calatheas/app/frontend/components/header.py b/calm-calatheas/app/frontend/components/header.py new file mode 100644 index 00000000..acefa3d1 --- /dev/null +++ b/calm-calatheas/app/frontend/components/header.py @@ -0,0 +1,78 @@ +from typing import TYPE_CHECKING, cast, override + +from js import Event, document +from pyodide.ffi import JsDomElement +from pyodide.ffi.wrappers import add_event_listener + +from frontend.base import Component + +from .theme import Theme + +if TYPE_CHECKING: + from js import JsAnchorElement + +TEMPLATE = """ + +""" + + +class Header(Component): + """The main header for the application.""" + + def __init__(self, root: JsDomElement) -> None: + super().__init__(root) + self._expanded = False + + @override + def build(self) -> str: + return TEMPLATE + + @override + def pre_destroy(self) -> None: + self._theme_selector.destroy() + + @override + def on_render(self) -> None: + self._theme_selector = Theme(document.getElementById("navbar-end")) + self._theme_selector.render() + + self._main_navigation = document.getElementById("main-navigation") + self._navbar_burger = cast("JsAnchorElement", document.getElementById("navbar-burger")) + + add_event_listener(self._navbar_burger, "click", self._toggle_navbar) + + @property + def expanded(self) -> bool: + """Whether or not the navbar menu is expanded.""" + return self._expanded + + @expanded.setter + def expanded(self, value: bool) -> None: + self._expanded = value + + if value: + self._main_navigation.classList.add("is-active") + self._navbar_burger.classList.add("is-active") + else: + self._main_navigation.classList.remove("is-active") + self._navbar_burger.classList.remove("is-active") + + def _toggle_navbar(self, _: Event) -> None: + """Toggle the navbar menu.""" + self.expanded = not self.expanded diff --git a/calm-calatheas/app/frontend/components/loading_caption_model.py b/calm-calatheas/app/frontend/components/loading_caption_model.py new file mode 100644 index 00000000..c361edd4 --- /dev/null +++ b/calm-calatheas/app/frontend/components/loading_caption_model.py @@ -0,0 +1,40 @@ +from typing import override + +import reactivex.operators as op +from pyodide.ffi import JsDomElement + +from frontend.base import Component +from frontend.services import caption + +TEMPLATE = """ +
+

Loading the model for generating captions

+ +
+""" + + +class LoadingCaptionModel(Component): + """A component that shows a loading indicator while the caption model is being loaded.""" + + def __init__(self, root: JsDomElement) -> None: + super().__init__(root) + + # Update the UI whenever the loading state changes + caption.is_loading_model.pipe( + op.distinct_until_changed(), + op.take_until(self.destroyed), + ).subscribe( + lambda is_loading: self._handle_is_loading_update(is_loading=is_loading), + ) + + @override + def build(self) -> str: + return TEMPLATE + + def _handle_is_loading_update(self, *, is_loading: bool) -> None: + """Show the notification while the model is loading.""" + if is_loading: + self.render() + else: + self.remove() diff --git a/calm-calatheas/app/frontend/components/pokemon.py b/calm-calatheas/app/frontend/components/pokemon.py new file mode 100644 index 00000000..7ea309f4 --- /dev/null +++ b/calm-calatheas/app/frontend/components/pokemon.py @@ -0,0 +1,85 @@ +from typing import override + +import reactivex.operators as op +from js import document +from pyodide.ffi import JsDomElement +from reactivex import combine_latest + +from frontend.base import Component +from frontend.components import Description +from frontend.models import PokemonRecord +from frontend.services import pokemon + +EMPTY_PLACEHOLDER_TEMPLATE = """ +

Nothing to show here yet!

+""" + +TEMPLATE = """ +
+

Nothing to show here yet!

+
+""" + + +class Pokemon(Component): + """The list of Pokemon.""" + + def __init__(self, root: JsDomElement) -> None: + super().__init__(root) + self._current_pokemon = [] + + @override + def build(self) -> str: + return TEMPLATE + + @override + def on_render(self) -> None: + self._pokemon_grid = document.getElementById("pokemon-grid") + + # Update the UI whenever the list of pokemon or the loading state changes + combine_latest(pokemon.pokemon, pokemon.is_generating).pipe( + op.take_until(self.destroyed), + ).subscribe(lambda params: self._render_pokemon(params[0], is_generating=params[1])) + + def _render_pokemon(self, pokemon: list[PokemonRecord], *, is_generating: bool) -> None: + """Render the given list of Pokemon.""" + for component in self._current_pokemon: + component.destroy() + + while cell := self._pokemon_grid.firstChild: + self._pokemon_grid.removeChild(cell) + + self._current_pokemon = [] + + if not (pokemon or is_generating): + self._pokemon_grid.innerHTML = EMPTY_PLACEHOLDER_TEMPLATE # type: ignore[innerHTML is available] + return + + for item in pokemon: + cell = document.createElement("div") + cell.classList.add("cell") + + description = Description(cell, item) + + self._pokemon_grid.appendChild(cell) + self._current_pokemon.append(description) + + description.render() + + if is_generating: + self._render_generating_placeholder() + + def _render_generating_placeholder(self) -> None: + """Render a placeholder in the Pokemon grid while generating.""" + if placeholder := document.getElementById("pokemon-empty-placeholder"): + placeholder.remove() # type: ignore[remove is available] + + cell = document.createElement("div") + cell.classList.add("cell") + + # Create a description component with no data to show loading state + description = Description(cell, None) + description.render() + + self._pokemon_grid.prepend(cell) # type: ignore[prepend is available] + self._current_pokemon.append(description) diff --git a/calm-calatheas/app/frontend/components/theme.py b/calm-calatheas/app/frontend/components/theme.py new file mode 100644 index 00000000..a78b96cd --- /dev/null +++ b/calm-calatheas/app/frontend/components/theme.py @@ -0,0 +1,84 @@ +from typing import TYPE_CHECKING, cast, override + +import reactivex.operators as op +from js import Event, document +from pyodide.ffi.wrappers import add_event_listener + +from frontend.base import Component +from frontend.services import Theme_, theme + +if TYPE_CHECKING: + from js import JsAnchorElement + +TEMPLATE = """ + +""" + + +class Theme(Component): + """A component for selecting the theme.""" + + @override + def build(self) -> str: + return TEMPLATE + + @override + def on_destroy(self) -> None: + self._current_theme_listener.dispose() + + @override + def on_render(self) -> None: + self._select_theme_light = cast("JsAnchorElement", document.getElementById("select-theme-light")) + self._select_theme_dark = cast("JsAnchorElement", document.getElementById("select-theme-dark")) + self._select_theme_auto = cast("JsAnchorElement", document.getElementById("select-theme-auto")) + + add_event_listener(self._select_theme_light, "click", self._set_theme_light) + add_event_listener(self._select_theme_dark, "click", self._set_theme_dark) + add_event_listener(self._select_theme_auto, "click", self._set_theme_auto) + + # Update the UI whenever the current theme changes + self._current_theme_listener = theme.current.pipe( + op.take_until(self.destroyed), + ).subscribe(lambda theme: self._update_current_theme(theme)) + + def _set_theme_light(self, _: Event) -> None: + """Set the theme to light.""" + theme.current.on_next("light") + + def _set_theme_dark(self, _: Event) -> None: + """Set the theme to dark.""" + theme.current.on_next("dark") + + def _set_theme_auto(self, _: Event) -> None: + """Set the theme to auto.""" + theme.current.on_next(None) + + def _update_current_theme(self, theme: Theme_) -> None: + """Set the active state on the appropriate theme selector.""" + if theme == "light": + self._select_theme_light.classList.add("is-active") + self._select_theme_dark.classList.remove("is-active") + self._select_theme_auto.classList.remove("is-active") + elif theme == "dark": + self._select_theme_dark.classList.add("is-active") + self._select_theme_light.classList.remove("is-active") + self._select_theme_auto.classList.remove("is-active") + else: + self._select_theme_auto.classList.add("is-active") + self._select_theme_light.classList.remove("is-active") + self._select_theme_dark.classList.remove("is-active") diff --git a/calm-calatheas/app/frontend/models/__init__.py b/calm-calatheas/app/frontend/models/__init__.py new file mode 100644 index 00000000..bab02b48 --- /dev/null +++ b/calm-calatheas/app/frontend/models/__init__.py @@ -0,0 +1,3 @@ +from .pokemon_description import PokemonDescription, PokemonRecord, PokemonType + +__all__ = ["PokemonDescription", "PokemonRecord", "PokemonType"] diff --git a/calm-calatheas/app/frontend/models/pokemon_description.py b/calm-calatheas/app/frontend/models/pokemon_description.py new file mode 100644 index 00000000..13554d42 --- /dev/null +++ b/calm-calatheas/app/frontend/models/pokemon_description.py @@ -0,0 +1,48 @@ +from datetime import UTC, datetime +from enum import StrEnum, auto + +from pydantic import BaseModel, Field + + +class PokemonType(StrEnum): + """An enumeration of Pokemon types.""" + + BUG = auto() + DARK = auto() + DRAGON = auto() + ELECTRIC = auto() + FAIRY = auto() + FIGHTING = auto() + FIRE = auto() + FLYING = auto() + GHOST = auto() + GRASS = auto() + GROUND = auto() + ICE = auto() + NORMAL = auto() + POISON = auto() + PSYCHIC = auto() + ROCK = auto() + STEEL = auto() + WATER = auto() + + +class PokemonDescription(BaseModel): + """A description of a Pokemon, as generated by the API.""" + + ability: str = Field() + category: str = Field() + flavor_text: str = Field() + habitat: str = Field() + height: float = Field() + name: str = Field() + types: set[PokemonType] = Field() + weight: float = Field() + + +class PokemonRecord(PokemonDescription): + """A description of a Pokemon with an image and timestamp, as stored in the database.""" + + img_url: str = Field() + favourite: bool = Field(default=False) + timestamp: datetime = Field(default_factory=lambda: datetime.now(UTC)) diff --git a/calm-calatheas/app/frontend/services/__init__.py b/calm-calatheas/app/frontend/services/__init__.py new file mode 100644 index 00000000..4410555a --- /dev/null +++ b/calm-calatheas/app/frontend/services/__init__.py @@ -0,0 +1,24 @@ +from .camera import Camera +from .caption import Caption, caption +from .database import Database, database +from .description import Description, description +from .pokemon import Pokemon, pokemon +from .reader import Reader, reader +from .theme import Theme, Theme_, theme + +__all__ = [ + "Camera", + "Caption", + "Database", + "Description", + "Pokemon", + "Reader", + "Theme", + "Theme_", + "caption", + "database", + "description", + "pokemon", + "reader", + "theme", +] diff --git a/calm-calatheas/app/frontend/services/camera.py b/calm-calatheas/app/frontend/services/camera.py new file mode 100644 index 00000000..723deca0 --- /dev/null +++ b/calm-calatheas/app/frontend/services/camera.py @@ -0,0 +1,96 @@ +from typing import Literal, Optional, cast, override + +from js import MediaStream, console, localStorage, navigator +from pyodide.webloop import PyodideFuture +from reactivex import Observable, Subject, empty +from reactivex import operators as op +from reactivex.subject import BehaviorSubject + +from frontend.base import Service + +FACING_MODES = {"user", "environment"} +LOCAL_STORAGE_KEY = "preferred_facing_mode" + +type FacingMode = Literal["user", "environment"] + + +class Camera(Service): + """A service for accessing the user's camera.""" + + def __init__(self) -> None: + super().__init__() + + self.media_stream = BehaviorSubject[Optional[MediaStream]](value=None) + self.is_acquiring_media_stream = BehaviorSubject[bool](value=False) + + self._acquire = Subject[None]() + + # Whenever acquisition is triggered, attempt to acquire the media stream + self._acquire.pipe( + op.do_action(lambda _: self.is_acquiring_media_stream.on_next(value=True)), + op.flat_map_latest( + lambda _: self._acquire_media_stream().finally_( + lambda: self.is_acquiring_media_stream.on_next(value=False), + ), + ), + op.catch(lambda err, _: self._handle_acquisition_error(err)), + op.take_until(self.destroyed), + ).subscribe(self.media_stream) + + def dispose_media_stream(self) -> None: + """Stop all tracks in the media stream and notify subscribers.""" + if not (camera_stream := self.media_stream.value): + return + + for track in camera_stream.getTracks(): + track.stop() + + self.media_stream.on_next(None) + + @override + def on_destroy(self) -> None: + self.dispose_media_stream() + self._acquire.dispose() + self.media_stream.dispose() + + def acquire_media_stream(self) -> None: + """Trigger the process of acquiring the media stream.""" + if self.media_stream.value: + return + + self._acquire.on_next(None) + + def toggle_facing_mode(self) -> None: + """Switch the preferred facing mode between user and environment.""" + self._preferred_facing_mode = "environment" if self._preferred_facing_mode == "user" else "user" + + self.dispose_media_stream() + self.acquire_media_stream() + + def _acquire_media_stream(self) -> PyodideFuture[MediaStream]: + """ + Get the user's media stream. + + Requests video access from the user. If access is not granted, a DOMException will be raised. + """ + constraints = {"video": {"facingMode": self._preferred_facing_mode}} + return navigator.mediaDevices.getUserMedia(constraints) + + def _handle_acquisition_error(self, err: Exception) -> Observable: + """Handle errors that occur while acquiring the media stream.""" + console.error("Error acquiring media stream:", err) + return empty() + + @property + def _preferred_facing_mode(self) -> FacingMode: + """ + Return the preferred facing mode for the camera. + + Save the user preference in local storage to ensure it persists across sessions. + """ + mode = localStorage.getItem(LOCAL_STORAGE_KEY) + return cast("FacingMode", mode) if mode in FACING_MODES else "user" + + @_preferred_facing_mode.setter + def _preferred_facing_mode(self, value: FacingMode) -> None: + localStorage.setItem(LOCAL_STORAGE_KEY, value) diff --git a/calm-calatheas/app/frontend/services/caption.py b/calm-calatheas/app/frontend/services/caption.py new file mode 100644 index 00000000..f7fd9ac5 --- /dev/null +++ b/calm-calatheas/app/frontend/services/caption.py @@ -0,0 +1,89 @@ +from asyncio import Future, create_task +from collections.abc import Callable +from typing import TYPE_CHECKING, override + +from js import console, window +from reactivex import Observable, combine_latest, empty, from_future, of +from reactivex import operators as op +from reactivex.subject import BehaviorSubject, ReplaySubject + +from frontend.base import Service + +from .reader import reader + +if TYPE_CHECKING: + from transformers_js import ModelOutput + +type Model = Callable[[str], Future[ModelOutput]] + +MODEL_NAME = "Xenova/vit-gpt2-image-captioning" + + +class Caption(Service): + """Service to generate captions for images.""" + + def __init__(self) -> None: + super().__init__() + + self.captions = ReplaySubject[str]() + self.model = ReplaySubject[Model]() + + self.is_generating_caption = BehaviorSubject[bool](value=False) + self.is_loading_model = BehaviorSubject[bool](value=False) + + # Load the captioning model on startup + of(MODEL_NAME).pipe( + op.do_action(lambda _: self.is_loading_model.on_next(value=True)), + op.flat_map_latest( + lambda model_name: from_future(create_task(self._load_model(model_name))).pipe( + op.finally_action( + lambda: self.is_loading_model.on_next(value=False), + ), + ), + ), + op.catch(lambda err, _: self._handle_load_model_error(err)), + op.take_until(self.destroyed), + ).subscribe(self.model) + + # Generate captions whenever an image is available and the model is loaded + combine_latest(reader.object_urls, self.model).pipe( + op.do_action(lambda _: self.is_generating_caption.on_next(value=True)), + op.flat_map_latest( + lambda params: from_future(create_task(self._caption(*params))).pipe( + op.finally_action( + lambda: self.is_generating_caption.on_next(value=False), + ), + ), + ), + op.catch(lambda err, _: self._handle_caption_error(err)), + op.take_until(self.destroyed), + ).subscribe(self.captions) + + @override + def on_destroy(self) -> None: + self.captions.dispose() + self.model.dispose() + self.is_generating_caption.dispose() + self.is_loading_model.dispose() + + async def _caption(self, url: str, model: Model) -> str: + """Generate a caption for the image at the given URL.""" + output = await model(url) + return output.at(0).generated_text + + def _handle_caption_error(self, err: Exception) -> Observable: + """Handle errors that occur while generating captions.""" + console.error("Failed to generate caption:", err) + return empty() + + def _handle_load_model_error(self, err: Exception) -> Observable: + """Handle errors that occur while loading the model.""" + console.error("Failed to load model:", err) + return empty() + + async def _load_model(self, model_name: str) -> Model: + """Load the given model.""" + return await window.pipeline("image-to-text", model_name, {"dtype": "q8", "device": "wasm"}) + + +caption = Caption() diff --git a/calm-calatheas/app/frontend/services/database.py b/calm-calatheas/app/frontend/services/database.py new file mode 100644 index 00000000..d2fcf1f3 --- /dev/null +++ b/calm-calatheas/app/frontend/services/database.py @@ -0,0 +1,192 @@ +import asyncio +from typing import TYPE_CHECKING, override + +from js import JSON, Event, console, indexedDB +from pyodide.ffi.wrappers import add_event_listener + +from frontend.base import Service +from frontend.models import PokemonRecord + +if TYPE_CHECKING: + from js import IDBDatabase + +_COLLECTION_NAME = "pokemon" +_DB_NAME = "calm_calatheas" +_DB_VERSION = 1 +_READY = asyncio.Event() + + +class DatabaseNotInitializedError(Exception): + """Error raised when the database is not initialized.""" + + def __init__(self) -> None: + super().__init__("Database is not initialized.") + + +class Database(Service): + """Service for interacting with IndexedDB.""" + + def __init__(self) -> None: + super().__init__() + + self._db: IDBDatabase | None = None + + open_ = indexedDB.open(_DB_NAME, _DB_VERSION) + + add_event_listener(open_, "success", self._handle_open_success) + add_event_listener(open_, "upgradeneeded", self._handle_open_upgrade_needed) + + @override + def on_destroy(self) -> None: + if self._db: + self._db.close() + + async def delete(self, name: str) -> None: + """Delete a Pokemon.""" + await _READY.wait() + + if not self._db: + raise DatabaseNotInitializedError + + future = asyncio.Future[None]() + + transaction = self._db.transaction(_COLLECTION_NAME, "readwrite") + store = transaction.objectStore(_COLLECTION_NAME) + + query = store.delete(name) + + def on_complete(_: Event) -> None: + transaction.close() + + def on_error(_: Event) -> None: + future.set_exception(Exception(f"Failed to delete pokemon {name}")) + + def on_success(_: Event) -> None: + future.set_result(None) + + add_event_listener(query, "complete", on_complete) + add_event_listener(query, "error", on_error) + add_event_listener(query, "success", on_success) + + return await future + + async def find_all(self) -> list[PokemonRecord]: + """Find all Pokemon.""" + await _READY.wait() + + if not self._db: + raise DatabaseNotInitializedError + + future = asyncio.Future[list[PokemonRecord]]() + + transaction = self._db.transaction(_COLLECTION_NAME, "readonly") + store = transaction.objectStore(_COLLECTION_NAME) + + query = store.getAll() + + def on_complete(_: Event) -> None: + transaction.close() + + def on_error(_: Event) -> None: + future.set_result([]) + + def on_success(event: Event) -> None: + # Some serialization and deserialization magic to ensure the data is accepted by Pydantic + result = [ + PokemonRecord.model_validate_json(JSON.stringify(item)) + for item in event.target.result # type: ignore[result is available] + ] + future.set_result(result) + + add_event_listener(query, "complete", on_complete) + add_event_listener(query, "error", on_error) + add_event_listener(query, "success", on_success) + + return await future + + async def find_one(self, name: str) -> PokemonRecord | None: + """Find a single Pokemon.""" + await _READY.wait() + + if not self._db: + raise DatabaseNotInitializedError + + future = asyncio.Future[PokemonRecord | None]() + + transaction = self._db.transaction(_COLLECTION_NAME, "readonly") + store = transaction.objectStore(_COLLECTION_NAME) + + query = store.get(name) + + def on_complete(_: Event) -> None: + transaction.close() + + def on_error(_: Event) -> None: + future.set_result(None) + + def on_success(event: Event) -> None: + # Some serialization and deserialization magic to ensure the data is accepted by Pydantic + result = PokemonRecord.model_validate_json(JSON.stringify(event.target.result)) # type: ignore[result is available] + future.set_result(result) + + add_event_listener(query, "complete", on_complete) + add_event_listener(query, "error", on_error) + add_event_listener(query, "success", on_success) + + return await future + + async def put(self, description: PokemonRecord) -> None: + """Store a Pokemon.""" + await _READY.wait() + + if not self._db: + raise DatabaseNotInitializedError + + future = asyncio.Future[None]() + + transaction = self._db.transaction(_COLLECTION_NAME, "readwrite") + store = transaction.objectStore(_COLLECTION_NAME) + + # Some serialization and deserialization magic to ensure the data is accepted by IndexedDB + query = store.put(JSON.parse(description.model_dump_json())) + + def on_complete(_: Event) -> None: + transaction.close() + + def on_error(_: Event) -> None: + future.set_exception(Exception(f"Failed to store pokemon {description.name}")) + + def on_success(_: Event) -> None: + future.set_result(None) + + add_event_listener(query, "complete", on_complete) + add_event_listener(query, "error", on_error) + add_event_listener(query, "success", on_success) + + return await future + + def _handle_open_success(self, event: Event) -> None: + """Handle the successful opening of the database.""" + self._db = event.target.result # type: ignore[result is available] + console.log("Opened IndexedDB.") + + _READY.set() + + def _handle_open_upgrade_needed(self, event: Event) -> None: + """Handle the upgrade needed event.""" + self._db = event.target.result # type: ignore[result is available] + + if not self._db: + raise DatabaseNotInitializedError + + self._db.createObjectStore(_COLLECTION_NAME, {"keyPath": "name"}) + + add_event_listener(event.target.transaction, "complete", self._handle_upgrade_transaction_complete) # type: ignore[transaction is available] + + def _handle_upgrade_transaction_complete(self, _: Event) -> None: + """Handle the completion of the upgrade transaction.""" + console.log("Initialized IndexedDB.") + _READY.set() + + +database = Database() diff --git a/calm-calatheas/app/frontend/services/description.py b/calm-calatheas/app/frontend/services/description.py new file mode 100644 index 00000000..4a2185de --- /dev/null +++ b/calm-calatheas/app/frontend/services/description.py @@ -0,0 +1,58 @@ +from asyncio import create_task + +from js import console +from pyodide.http import pyfetch +from reactivex import Observable, empty, from_future +from reactivex import operators as op +from reactivex.subject import BehaviorSubject, ReplaySubject + +from frontend.base import Service +from frontend.models import PokemonDescription + +from .caption import caption + + +class Description(Service): + """Service to generate descriptions from captions.""" + + def __init__(self) -> None: + super().__init__() + + self.is_generating_description = BehaviorSubject[bool](value=False) + self.descriptions = ReplaySubject[PokemonDescription]() + + # Generate descriptions whenever a new caption is available + caption.captions.pipe( + op.do_action(lambda _: self.is_generating_description.on_next(value=True)), + op.flat_map_latest( + lambda caption: from_future(create_task(self._describe(caption))).pipe( + op.finally_action( + lambda: self.is_generating_description.on_next(value=False), + ), + ), + ), + op.catch(lambda err, _: self._handle_description_error(err)), + op.take_until(self.destroyed), + ).subscribe(self.descriptions) + + async def _describe(self, caption: str) -> PokemonDescription: + """Generate a description from the given caption.""" + console.log("Generating description for caption:", caption) + + response = await pyfetch(f"/describe?prompt={caption}") + response.raise_for_status() + + data = await response.json() + description = PokemonDescription.model_validate(data) + + console.log("Generated description:", description.model_dump_json()) + + return description + + def _handle_description_error(self, err: Exception) -> Observable: + """Handle errors that occur while generating descriptions.""" + console.error("Failed to generate description:", err) + return empty() + + +description = Description() diff --git a/calm-calatheas/app/frontend/services/pokemon.py b/calm-calatheas/app/frontend/services/pokemon.py new file mode 100644 index 00000000..7a8561b1 --- /dev/null +++ b/calm-calatheas/app/frontend/services/pokemon.py @@ -0,0 +1,121 @@ +from asyncio import create_task +from typing import override + +from js import console +from reactivex import Observable, combine_latest, empty, from_future +from reactivex import operators as op +from reactivex.subject import BehaviorSubject, Subject + +from frontend.base import Service +from frontend.models import PokemonRecord + +from .caption import caption +from .database import database +from .description import description +from .reader import reader + + +class Pokemon(Service): + """Service that maintains a list of the user's current Pokemon.""" + + def __init__(self) -> None: + super().__init__() + + self.is_generating = BehaviorSubject[bool](value=False) + self.is_refreshing = BehaviorSubject[bool](value=False) + self.pokemon = BehaviorSubject[list[PokemonRecord]](value=[]) + + self._delete = Subject[str]() + self._put = Subject[PokemonRecord]() + self._refresh = Subject[None]() + + # Combine the loading states from all relevant sources + combine_latest( + caption.is_generating_caption, + description.is_generating_description, + reader.is_reading, + ).pipe( + op.map(lambda is_loading: any(is_loading)), + op.distinct_until_changed(), + op.take_until(self.destroyed), + ).subscribe(self.is_generating) + + # Whenever a new description is available, get the corresponding image url and create an new database record + description.descriptions.pipe( + op.with_latest_from(reader.object_urls), + op.map(lambda params: PokemonRecord(**params[0].model_dump(), img_url=params[1])), + op.take_until(self.destroyed), + ).subscribe(lambda pokemon: self.put(pokemon)) + + # On put, update the database with the given record + self._put.pipe( + op.flat_map_latest(lambda pokemon: from_future(create_task(database.put(pokemon)))), + op.catch(lambda err, _: self._handle_update_error(err)), + op.take_until(self.destroyed), + ).subscribe(lambda _: self.refresh()) + + # On delete, remove the Pokemon from the list and trigger a refresh + self._delete.pipe( + op.flat_map_latest(lambda name: from_future(create_task(database.delete(name)))), + op.catch(lambda err, _: self._handle_delete_error(err)), + op.take_until(self.destroyed), + ).subscribe(lambda _: self.refresh()) + + # On refresh, retrieve the current list of Pokemon from the database. Sort the list by timestamp + self._refresh.pipe( + op.do_action(lambda _: self.is_refreshing.on_next(value=True)), + op.flat_map_latest( + lambda _: from_future(create_task(database.find_all())).pipe( + op.finally_action(lambda: self.is_refreshing.on_next(value=False)), + ), + ), + op.catch(lambda err, _: self._handle_refresh_error(err)), + op.map(lambda pokemon: sorted(pokemon, key=lambda p: p.timestamp, reverse=True)), + op.take_until(self.destroyed), + ).subscribe(self.pokemon) + + # Trigger a refresh on startup + self.refresh() + + @override + def on_destroy(self) -> None: + self.is_generating.dispose() + self.is_refreshing.dispose() + self.pokemon.dispose() + self._delete.dispose() + self._put.dispose() + self._refresh.dispose() + + def delete(self, name: str) -> None: + """Delete the pokemon with the given name.""" + self._delete.on_next(name) + + def put(self, pokemon: PokemonRecord) -> None: + """Update the database with the given pokemon.""" + self._put.on_next(pokemon) + + def refresh(self) -> None: + """Trigger a refresh of the list.""" + self._refresh.on_next(None) + + def _handle_delete_error(self, err: Exception) -> Observable: + """Handle errors that occur while deleting a Pokemon.""" + console.error("Failed to delete pokemon:", err) + return empty() + + def _handle_favourite_error(self, err: Exception) -> Observable: + console.error("Failed to favourite pokemon", err) + return empty() + + def _handle_refresh_error(self, err: Exception) -> Observable: + """Handle errors that occur while refreshing the list of Pokemon.""" + console.error("Failed to refresh list of pokemon:", err) + return empty() + + def _handle_update_error(self, err: Exception) -> Observable: + """Handle errors that occur while updating a Pokemon.""" + console.error("Failed to update pokemon:", err) + return empty() + + +pokemon = Pokemon() diff --git a/calm-calatheas/app/frontend/services/reader.py b/calm-calatheas/app/frontend/services/reader.py new file mode 100644 index 00000000..58f33a02 --- /dev/null +++ b/calm-calatheas/app/frontend/services/reader.py @@ -0,0 +1,67 @@ +from asyncio import Future +from typing import Union, override + +from js import Blob, File, FileReader, console +from pyodide.ffi.wrappers import add_event_listener +from reactivex import Observable, empty, from_future +from reactivex import operators as op +from reactivex.subject import BehaviorSubject, ReplaySubject, Subject + +from frontend.base import Service + +type Readable = Union[Blob, File] + + +class Reader(Service): + """Service for reading files and generating object URLs.""" + + def __init__(self) -> None: + super().__init__() + + self.is_reading = BehaviorSubject[bool](value=False) + self.object_urls = ReplaySubject[str]() + + self._read = Subject[Readable]() + + # On read, generate an object URL for the object + self._read.pipe( + op.do_action(lambda _: self.is_reading.on_next(value=True)), + op.flat_map_latest( + lambda file_: from_future(self._generate_object_url(file_)).pipe( + op.finally_action(lambda: self.is_reading.on_next(value=False)), + ), + ), + op.catch(lambda err, _: self._handle_reader_error(err)), + op.take_until(self.destroyed), + ).subscribe(self.object_urls) + + @override + def on_destroy(self) -> None: + self._read.dispose() + self.is_reading.dispose() + self.object_urls.dispose() + + def read(self, object_: Readable) -> None: + """Upload an object and trigger further processing.""" + self._read.on_next(object_) + + def _handle_reader_error(self, err: Exception) -> Observable: + """Handle errors that occur while reading objects.""" + console.error("Error reading object:", err) + return empty() + + def _generate_object_url(self, object_: Readable) -> Future[str]: + """Read an object and return its object URL.""" + result = Future() + + reader = FileReader.new() + + add_event_listener(reader, "load", lambda _: result.set_result(reader.result)) # type: ignore[FileReader also supported] + add_event_listener(reader, "error", lambda e: result.set_exception(e)) # type: ignore[FileReader also supported] + + reader.readAsDataURL(object_) + + return result + + +reader = Reader() diff --git a/calm-calatheas/app/frontend/services/theme.py b/calm-calatheas/app/frontend/services/theme.py new file mode 100644 index 00000000..decdbe23 --- /dev/null +++ b/calm-calatheas/app/frontend/services/theme.py @@ -0,0 +1,61 @@ +from typing import Literal, cast, override + +import reactivex.operators as op +from js import document, localStorage +from reactivex.subject import BehaviorSubject + +from frontend.base import Service + +type Theme_ = Literal["light", "dark"] | None + +ATTRIBUTE_NAME = "data-theme" + + +def _update_document_theme(theme: Theme_) -> None: + """Set the theme of the document.""" + if theme: + document.documentElement.setAttribute(ATTRIBUTE_NAME, theme) # type: ignore[setAttribute not defined] + else: + document.documentElement.removeAttribute(ATTRIBUTE_NAME) # type: ignore[removeAttribute not defined] + + +LOCAL_STORAGE_KEY = "theme" + + +def _update_local_storage(theme: Theme_) -> None: + """Set the theme in local storage.""" + if theme: + localStorage.setItem(LOCAL_STORAGE_KEY, theme) + else: + localStorage.removeItem(LOCAL_STORAGE_KEY) + + +class Theme(Service): + """Service to manage the theme of the application.""" + + def __init__(self) -> None: + super().__init__() + + self.current = BehaviorSubject[Theme_]( + cast("Theme_", theme) + if (theme := localStorage.getItem(LOCAL_STORAGE_KEY)) and theme in {"light", "dark"} + else None, + ) + + # Update the document theme whenever the current theme changes + self.current.pipe( + op.take_until(self.destroyed), + ).subscribe(_update_document_theme) + + # Update the local storage whenever the current theme changes + self.current.pipe( + op.take_until(self.destroyed), + ).subscribe(_update_local_storage) + + @override + def on_destroy(self) -> None: + """Clean up the theme service.""" + self.current.dispose() + + +theme = Theme() diff --git a/calm-calatheas/app/index.html b/calm-calatheas/app/index.html new file mode 100644 index 00000000..e3b859ed --- /dev/null +++ b/calm-calatheas/app/index.html @@ -0,0 +1,31 @@ + + + + + + Calm Calatheas + + + + + + + + + + + diff --git a/calm-calatheas/app/main.py b/calm-calatheas/app/main.py new file mode 100644 index 00000000..a5a35686 --- /dev/null +++ b/calm-calatheas/app/main.py @@ -0,0 +1,12 @@ +from frontend import App +from js import document + + +def bootstrap() -> None: + """Bootstrap the application to the DOM.""" + app = App(document.body) + app.render() + + +if __name__ == "__main__": + bootstrap() diff --git a/calm-calatheas/app/manifest.json b/calm-calatheas/app/manifest.json new file mode 100644 index 00000000..36de9e18 --- /dev/null +++ b/calm-calatheas/app/manifest.json @@ -0,0 +1,25 @@ +{ + "background_color": "white", + "display": "standalone", + "icons": [ + { + "sizes": "16x16", + "src": "/assets/logo-16x16.png", + "type": "image/png" + }, + { + "sizes": "48x48", + "src": "/assets/logo-48x48.png", + "type": "image/png" + }, + { + "sizes": "128x128", + "src": "/assets/logo-128x128.png", + "type": "image/png" + } + ], + "name": "Pokedexter", + "short_name": "Pokedexter", + "start_url": ".", + "theme_color": "black" +} diff --git a/calm-calatheas/app/pyscript.toml b/calm-calatheas/app/pyscript.toml new file mode 100644 index 00000000..b31310a8 --- /dev/null +++ b/calm-calatheas/app/pyscript.toml @@ -0,0 +1,34 @@ +docked = false +terminal = false + +packages = ['pydantic', 'reactivex'] + +[[fetch]] +files = [ + '__init__.py', + 'app.py', + 'base/__init__.py', + 'base/component.py', + 'base/service.py', + 'components/__init__.py', + 'components/camera.py', + 'components/footer.py', + 'components/header.py', + 'components/loading_caption_model.py', + 'components/pokemon.py', + 'components/theme.py', + 'components/description.py', + 'components/description_dropdown.py', + 'models/__init__.py', + 'models/pokemon_description.py', + 'services/__init__.py', + 'services/camera.py', + 'services/caption.py', + 'services/database.py', + 'services/description.py', + 'services/pokemon.py', + 'services/reader.py', + 'services/theme.py', +] +from = 'frontend' +to_folder = 'frontend' diff --git a/calm-calatheas/app/styles/theme.css b/calm-calatheas/app/styles/theme.css new file mode 100644 index 00000000..85dc9856 --- /dev/null +++ b/calm-calatheas/app/styles/theme.css @@ -0,0 +1,165 @@ +@import "https://cdn.jsdelivr.net/npm/bulma@1.0.4/css/bulma.min.css"; + +:root { + --bulma-primary-h: 359deg; + --bulma-primary-s: 91%; + --bulma-primary-l: 56%; + --bulma-success-h: 120deg; + --bulma-success-s: 50%; + --bulma-success-l: 40%; + --bulma-warning-h: 52deg; + --bulma-warning-l: 50%; + --bulma-danger-h: 0deg; + --bulma-danger-l: 50%; + + --type-bug: #94a034; + --type-dark: #4d3f3e; + --type-dragon: #4c60a9; + --type-electric: #f2c340; + --type-fairy: #ba7fb5; + --type-fighting: #f08833; + --type-fire: #d43a30; + --type-flying: #8eb8e3; + --type-ghost: #6b426e; + --type-grass: #5d9d3c; + --type-ground: #895229; + --type-ice: #78ccf0; + --type-normal: #a2a2a2; + --type-poison: #6d4b97; + --type-psychic: #dc4d79; + --type-rock: #ada984; + --type-steel: #74a2b9; + --type-water: #4c79bc; +} + +@font-face { + font-family: "pokemongb"; + src: url("/assets/pokemongb.ttf"); +} + +body { + font-family: "pokemongb"; +} + +#app-container { + max-height: 100vh; +} + +#app-body { + align-items: unset; + display: block; + flex-shrink: unset; + overflow-y: auto; +} + +.modal { + animation: fadeIn 0.2s; +} + +.notification { + animation: fadeIn 0.2s; +} + +.pokemon-description { + animation: fadeIn 0.2s; +} + +@keyframes fadeIn { + 0% { + opacity: 0; + } + + 100% { + opacity: 1; + } +} + +.tag.type-bug { + background-color: var(--type-bug); + color: hsl(0, 0%, 100%); +} + +.tag.type-dark { + background-color: var(--type-dark); + color: hsl(0, 0%, 100%); +} + +.tag.type-dragon { + background-color: var(--type-dragon); + color: hsl(0, 0%, 100%); +} + +.tag.type-electric { + background-color: var(--type-electric); + color: hsl(0, 0%, 4%); +} + +.tag.type-fairy { + background-color: var(--type-fairy); + color: hsl(0, 0%, 4%); +} + +.tag.type-fighting { + background-color: var(--type-fighting); + color: hsl(0, 0%, 4%); +} + +.tag.type-fire { + background-color: var(--type-fire); + color: hsl(0, 0%, 100%); +} + +.tag.type-flying { + background-color: var(--type-flying); + color: hsl(0, 0%, 4%); +} + +.tag.type-ghost { + background-color: var(--type-ghost); + color: hsl(0, 0%, 100%); +} + +.tag.type-grass { + background-color: var(--type-grass); + color: hsl(0, 0%, 100%); +} + +.tag.type-ground { + background-color: var(--type-ground); + color: hsl(0, 0%, 100%); +} + +.tag.type-ice { + background-color: var(--type-ice); + color: hsl(0, 0%, 4%); +} + +.tag.type-normal { + background-color: var(--type-normal); + color: hsl(0, 0%, 4%); +} + +.tag.type-poison { + background-color: var(--type-poison); + color: hsl(0, 0%, 100%); +} + +.tag.type-psychic { + background-color: var(--type-psychic); + color: hsl(0, 0%, 100%); +} + +.tag.type-rock { + background-color: var(--type-rock); + color: hsl(0, 0%, 4%); +} + +.tag.type-steel { + background-color: var(--type-steel); + color: hsl(0, 0%, 4%); +} + +.tag.type-water { + background-color: var(--type-water); + color: hsl(0, 0%, 100%); +} diff --git a/calm-calatheas/calm_calatheas/__init__.py b/calm-calatheas/calm_calatheas/__init__.py new file mode 100644 index 00000000..cd0ecd76 --- /dev/null +++ b/calm-calatheas/calm_calatheas/__init__.py @@ -0,0 +1,4 @@ +from .app import app +from .settings import settings + +__all__ = ["app", "settings"] diff --git a/calm-calatheas/calm_calatheas/__main__.py b/calm-calatheas/calm_calatheas/__main__.py new file mode 100644 index 00000000..7be6879b --- /dev/null +++ b/calm-calatheas/calm_calatheas/__main__.py @@ -0,0 +1,5 @@ +from uvicorn import run + +from calm_calatheas import app, settings + +run(app=app, host=settings.host, port=settings.port) diff --git a/calm-calatheas/calm_calatheas/app.py b/calm-calatheas/calm_calatheas/app.py new file mode 100644 index 00000000..6f25da76 --- /dev/null +++ b/calm-calatheas/calm_calatheas/app.py @@ -0,0 +1,34 @@ +from starlette.applications import Starlette +from starlette.requests import Request +from starlette.responses import Response +from starlette.routing import Mount, Route +from starlette.staticfiles import StaticFiles + +from .model import generate_description +from .settings import settings + + +async def describe(request: Request) -> Response: + """Handle GET requests to the /describe endpoint.""" + user_prompt = request.query_params.get("prompt", "") + + if not user_prompt: + return Response("Missing prompt", status_code=400) + + description = generate_description(user_prompt) + + return Response(description.model_dump_json(), media_type="application/json") + + +async def healthcheck(_: Request) -> Response: + """Handle GET requests to the /healthcheck endpoint.""" + return Response("OK", media_type="text/plain") + + +routes = [ + Route("/describe", endpoint=describe, methods=["GET"]), + Route("/healthcheck", endpoint=healthcheck, methods=["GET"]), + Mount("/", app=StaticFiles(directory=settings.static_files_path, html=True), name="static"), +] + +app = Starlette(routes=routes) diff --git a/calm-calatheas/calm_calatheas/logger.py b/calm-calatheas/calm_calatheas/logger.py new file mode 100644 index 00000000..454366f8 --- /dev/null +++ b/calm-calatheas/calm_calatheas/logger.py @@ -0,0 +1,6 @@ +import logging + +from .settings import settings + +logging.basicConfig(level=settings.log_level) +LOGGER = logging.getLogger("calm_calatheas") diff --git a/calm-calatheas/calm_calatheas/model.py b/calm-calatheas/calm_calatheas/model.py new file mode 100644 index 00000000..63eab33f --- /dev/null +++ b/calm-calatheas/calm_calatheas/model.py @@ -0,0 +1,194 @@ +import json +from enum import StrEnum, auto +from functools import lru_cache +from typing import override + +from pydantic import BaseModel, Field, ValidationError +from transformers import AutoModelForCausalLM, AutoTokenizer + +from .logger import LOGGER + + +class PokemonType(StrEnum): + """An enumeration of Pokemon types.""" + + BUG = auto() + DARK = auto() + DRAGON = auto() + ELECTRIC = auto() + FAIRY = auto() + FIGHTING = auto() + FIRE = auto() + FLYING = auto() + GHOST = auto() + GRASS = auto() + GROUND = auto() + ICE = auto() + NORMAL = auto() + POISON = auto() + PSYCHIC = auto() + ROCK = auto() + STEEL = auto() + WATER = auto() + + @classmethod + @override + def _missing_(cls, value: object) -> "PokemonType | None": + """ + The model will sometimes generate types that don't match the enum exactly. + + Try normalizing the input by converting it to lowercase. + """ + if isinstance(value, str): + value = value.lower() + + for member in cls: + if member.lower() == value: + return member + + return None + + +class PokemonDescription(BaseModel): + """A description of a Pokemon.""" + + ability: str = Field( + description="The primary ability of the Pokemon, which can affect its performance in battles.", + ) + + category: str = Field( + description="The category of the Pokemon, phrased as a noun.", + ) + + flavor_text: str = Field( + description="Flavor text to add characterization or lore to the Pokemon in question.", + max_length=255, + ) + + habitat: str = Field( + description="The natural habitat where the Pokemon can typically be found, phrased as a noun.", + max_length=15, + ) + + height: float = Field( + description="The height of the Pokemon in meters.", + ) + + name: str = Field( + description="The creative name for the Pokemon. Avoid using real names or actual Pokemon names.", + ) + + types: set[PokemonType] = Field( + description="The type(s) of the Pokemon.", + max_length=2, + min_length=1, + ) + + weight: float = Field( + description="The weight of the Pokemon in kilograms.", + ) + + +MODEL_NAME = "Qwen/Qwen3-1.7B" + +TOKENIZER = AutoTokenizer.from_pretrained(MODEL_NAME) +MODEL = AutoModelForCausalLM.from_pretrained( + MODEL_NAME, + torch_dtype="auto", + device_map="auto", +) + +DESCRIPTION_PROMPT = f""" +You are a helpful Pokemon professor. +The user is a Pokemon trainer seeking information. +The user will prompt you with a caption for a picture of a Pokemon. +Answer using the following schema: {json.dumps(PokemonDescription.model_json_schema())} +""" + +REPAIR_PROMPT = f""" +You are a helpful Pokemon professor. +The input is a Pokemon description and a validation error. +The description needs to be repaired based on the error. +Leave fields not mentioned in the error unchanged. +Answer using the following schema: {json.dumps(PokemonDescription.model_json_schema())} +""" + + +@lru_cache +def generate_description(user_prompt: str) -> PokemonDescription: + """Generate a Pokemon description based on the user's prompt.""" + LOGGER.debug('Generating a description based on user prompt: "%s"', user_prompt) + + messages = [ + { + "role": "system", + "content": DESCRIPTION_PROMPT, + }, + { + "role": "user", + "content": user_prompt, + }, + ] + + thinking_content, content = _prompt(messages) + + LOGGER.debug(thinking_content) + + try: + result = PokemonDescription.model_validate_json(content) + except ValidationError as e: + result = _repair(content, e) + + return result + + +def _repair(content: str, validation_error: ValidationError) -> PokemonDescription: + """Attempt to repair the given content based on the given validation error.""" + LOGGER.debug("Repairing content based on validation error: %s", validation_error) + LOGGER.debug("Original content: %s", content) + + messages = [ + { + "role": "system", + "content": REPAIR_PROMPT, + }, + { + "role": "user", + "content": f"Description: {content}\n\nError: {validation_error}", + }, + ] + + thinking_content, content = _prompt(messages) + + LOGGER.debug(thinking_content) + + return PokemonDescription.model_validate_json(content) + + +def _prompt(messages: list[dict[str, str]]) -> tuple[str, str]: + """Prompt the model with the given messages and return the generated text.""" + text = TOKENIZER.apply_chat_template( + messages, + tokenize=False, + add_generation_prompt=True, + enable_thinking=True, + ) + + model_inputs = TOKENIZER([text], return_tensors="pt").to(MODEL.device) + + # The magic numbers below taken from the model documentation, see https://huggingface.co/Qwen/Qwen3-1.7B#quickstart + generated_ids = MODEL.generate(**model_inputs, max_new_tokens=32768) + output_ids = generated_ids[0][len(model_inputs.input_ids[0]) :].tolist() + + try: + index = len(output_ids) - output_ids[::-1].index(151668) + except ValueError: + index = 0 + + thinking_content = TOKENIZER.decode( + output_ids[:index], + skip_special_tokens=True, + ).strip("\n") + content = TOKENIZER.decode(output_ids[index:], skip_special_tokens=True).strip("\n") + + return thinking_content, content diff --git a/calm-calatheas/calm_calatheas/settings.py b/calm-calatheas/calm_calatheas/settings.py new file mode 100644 index 00000000..7082121c --- /dev/null +++ b/calm-calatheas/calm_calatheas/settings.py @@ -0,0 +1,31 @@ +from pydantic import Field +from pydantic_settings import BaseSettings, SettingsConfigDict + + +class Settings(BaseSettings): + """Settings for the application.""" + + host: str = Field( + default="localhost", + description="Host to bind the server to", + ) + + log_level: str = Field( + default="DEBUG", + description="Logging level for the application", + ) + + port: int = Field( + default=8000, + description="Port to bind the server to", + ) + + static_files_path: str = Field( + default="app", + description="Path to the static files directory", + ) + + model_config = SettingsConfigDict(extra="ignore") + + +settings = Settings() diff --git a/calm-calatheas/devenv.lock b/calm-calatheas/devenv.lock new file mode 100644 index 00000000..2250f543 --- /dev/null +++ b/calm-calatheas/devenv.lock @@ -0,0 +1,103 @@ +{ + "nodes": { + "devenv": { + "locked": { + "dir": "src/modules", + "lastModified": 1754418859, + "owner": "cachix", + "repo": "devenv", + "rev": "e13cd53579f6a0f441ac09230178dccb3008dd36", + "type": "github" + }, + "original": { + "dir": "src/modules", + "owner": "cachix", + "repo": "devenv", + "type": "github" + } + }, + "flake-compat": { + "flake": false, + "locked": { + "lastModified": 1747046372, + "owner": "edolstra", + "repo": "flake-compat", + "rev": "9100a0f413b0c601e0533d1d94ffd501ce2e7885", + "type": "github" + }, + "original": { + "owner": "edolstra", + "repo": "flake-compat", + "type": "github" + } + }, + "git-hooks": { + "inputs": { + "flake-compat": "flake-compat", + "gitignore": "gitignore", + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1754416808, + "owner": "cachix", + "repo": "git-hooks.nix", + "rev": "9c52372878df6911f9afc1e2a1391f55e4dfc864", + "type": "github" + }, + "original": { + "owner": "cachix", + "repo": "git-hooks.nix", + "type": "github" + } + }, + "gitignore": { + "inputs": { + "nixpkgs": [ + "git-hooks", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1709087332, + "owner": "hercules-ci", + "repo": "gitignore.nix", + "rev": "637db329424fd7e46cf4185293b9cc8c88c95394", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "gitignore.nix", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1753719760, + "owner": "cachix", + "repo": "devenv-nixpkgs", + "rev": "0f871fffdc0e5852ec25af99ea5f09ca7be9b632", + "type": "github" + }, + "original": { + "owner": "cachix", + "ref": "rolling", + "repo": "devenv-nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "devenv": "devenv", + "git-hooks": "git-hooks", + "nixpkgs": "nixpkgs", + "pre-commit-hooks": [ + "git-hooks" + ] + } + } + }, + "root": "root", + "version": 7 +} diff --git a/calm-calatheas/devenv.nix b/calm-calatheas/devenv.nix new file mode 100644 index 00000000..2c50bc3e --- /dev/null +++ b/calm-calatheas/devenv.nix @@ -0,0 +1,18 @@ +{ pkgs, ... }: +{ + packages = [ + pkgs.pre-commit + ]; + + languages.python = { + enable = true; + uv = { + enable = true; + sync.enable = true; + }; + }; + + languages.javascript = { + enable = true; + }; +} diff --git a/calm-calatheas/docs/assets/code/backend.svg b/calm-calatheas/docs/assets/code/backend.svg new file mode 100644 index 00000000..8cfedad5 --- /dev/null +++ b/calm-calatheas/docs/assets/code/backend.svg @@ -0,0 +1,103 @@ + + + + + + +G + + + +calm_calatheas + +calm_calatheas + + + +calm_calatheas___main__ + +calm_calatheas. +__main__ + + + +calm_calatheas_app + +calm_calatheas. +app + + + +calm_calatheas_app->calm_calatheas + + + + + +calm_calatheas_app->calm_calatheas___main__ + + + + + +calm_calatheas_logger + +calm_calatheas. +logger + + + +calm_calatheas_model + +calm_calatheas. +model + + + +calm_calatheas_logger->calm_calatheas_model + + + + + +calm_calatheas_model->calm_calatheas_app + + + + + +calm_calatheas_settings + +calm_calatheas. +settings + + + +calm_calatheas_settings->calm_calatheas + + + + + +calm_calatheas_settings->calm_calatheas___main__ + + + + + +calm_calatheas_settings->calm_calatheas_app + + + + + + +calm_calatheas_settings->calm_calatheas_logger + + + + + diff --git a/calm-calatheas/docs/assets/code/frontend.svg b/calm-calatheas/docs/assets/code/frontend.svg new file mode 100644 index 00000000..84d1c3d0 --- /dev/null +++ b/calm-calatheas/docs/assets/code/frontend.svg @@ -0,0 +1,471 @@ + + + + + + +G + + + +frontend + +frontend + + + +frontend_app + +frontend.app + + + +frontend_app->frontend + + + + + +frontend_components + +frontend. +components + + + +frontend_components->frontend_app + + + + + +frontend_components_camera + +frontend. +components. +camera + + + +frontend_components_footer + +frontend. +components. +footer + + + +frontend_components_camera->frontend_components_footer + + + + + +frontend_components_description + +frontend. +components. +description + + + +frontend_components_description->frontend_components + + + + + +frontend_components_description_dropdown + +frontend. +components. +description_dropdown + + + +frontend_components_description_dropdown->frontend_components + + + + + + +frontend_components_description_dropdown->frontend_components_description + + + + + +frontend_components_footer->frontend_components + + + + + +frontend_components_header + +frontend. +components. +header + + + +frontend_components_header->frontend_components + + + + + +frontend_components_loading_caption_model + +frontend. +components. +loading_caption_model + + + +frontend_components_loading_caption_model->frontend_components + + + + + +frontend_components_pokemon + +frontend. +components. +pokemon + + + +frontend_components_pokemon->frontend_components + + + + +frontend_components_theme + +frontend. +components. +theme + + + +frontend_components_theme->frontend_components + + + + +frontend_components_theme->frontend_components_header + + + + + +frontend_models + +frontend. +models + + + +frontend_models->frontend_components_description + + + + + + + + + +frontend_models->frontend_components_description_dropdown + + + + + + +frontend_models->frontend_components_pokemon + + + + + + +frontend_services_database + +frontend. +services. +database + + + +frontend_models->frontend_services_database + + + + + +frontend_services_description + +frontend. +services. +description + + + +frontend_models->frontend_services_description + + + + + +frontend_services_pokemon + +frontend. +services. +pokemon + + + +frontend_models->frontend_services_pokemon + + + + + +frontend_models_pokemon_description + +frontend. +models. +pokemon_description + + + +frontend_models_pokemon_description->frontend_models + + + + + +frontend_services + +frontend. +services + + + +frontend_services->frontend_app + + + + + +frontend_services->frontend_components_camera + + + + + + +frontend_services->frontend_components_description_dropdown + + + + + +frontend_services->frontend_components_footer + + + + +frontend_services->frontend_components_loading_caption_model + + + + + + +frontend_services->frontend_components_pokemon + + + + + + +frontend_services->frontend_components_theme + + + + + +frontend_services_camera + +frontend. +services. +camera + + + +frontend_services_camera->frontend_services + + + + + +frontend_services_caption + +frontend. +services. +caption + + + +frontend_services_caption->frontend_components_footer + + + + + + +frontend_services_caption->frontend_components_loading_caption_model + + + + + + +frontend_services_caption->frontend_services + + + + + + +frontend_services_caption->frontend_services_description + + + + + +frontend_services_caption->frontend_services_pokemon + + + + + +frontend_services_database->frontend_services + + + + + + +frontend_services_database->frontend_services_pokemon + + + + + +frontend_services_description->frontend_services + + + + +frontend_services_description->frontend_services_pokemon + + + + + +frontend_services_pokemon->frontend_app + + + + + +frontend_services_pokemon->frontend_components_description_dropdown + + + + + +frontend_services_pokemon->frontend_components_footer + + + + + + +frontend_services_pokemon->frontend_components_pokemon + + + + +frontend_services_pokemon->frontend_services + + + + + +frontend_services_reader + +frontend. +services. +reader + + + +frontend_services_reader->frontend_components_camera + + + + + +frontend_services_reader->frontend_components_footer + + + + +frontend_services_reader->frontend_services + + + + + +frontend_services_reader->frontend_services_caption + + + + + +frontend_services_reader->frontend_services_pokemon + + + + + +frontend_services_theme + +frontend. +services. +theme + + + +frontend_services_theme->frontend_components_theme + + + + + +frontend_services_theme->frontend_services + + + + + diff --git a/calm-calatheas/docs/assets/design/mvp.png b/calm-calatheas/docs/assets/design/mvp.png new file mode 100644 index 00000000..9dd87a7f Binary files /dev/null and b/calm-calatheas/docs/assets/design/mvp.png differ diff --git a/calm-calatheas/docs/assets/design/overview.drawio b/calm-calatheas/docs/assets/design/overview.drawio new file mode 100644 index 00000000..fda9d9bc --- /dev/null +++ b/calm-calatheas/docs/assets/design/overview.drawio @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/calm-calatheas/docs/assets/logo-128x128.png b/calm-calatheas/docs/assets/logo-128x128.png new file mode 100644 index 00000000..51375e09 Binary files /dev/null and b/calm-calatheas/docs/assets/logo-128x128.png differ diff --git a/calm-calatheas/docs/assets/logo-16x16.png b/calm-calatheas/docs/assets/logo-16x16.png new file mode 100644 index 00000000..c156031c Binary files /dev/null and b/calm-calatheas/docs/assets/logo-16x16.png differ diff --git a/calm-calatheas/docs/assets/pokemon/aquapuff.png b/calm-calatheas/docs/assets/pokemon/aquapuff.png new file mode 100644 index 00000000..31d74331 Binary files /dev/null and b/calm-calatheas/docs/assets/pokemon/aquapuff.png differ diff --git a/calm-calatheas/docs/assets/pokemon/chronoclock.png b/calm-calatheas/docs/assets/pokemon/chronoclock.png new file mode 100644 index 00000000..5644f15b Binary files /dev/null and b/calm-calatheas/docs/assets/pokemon/chronoclock.png differ diff --git a/calm-calatheas/docs/assets/pokemon/lumina.png b/calm-calatheas/docs/assets/pokemon/lumina.png new file mode 100644 index 00000000..e17b2e7c Binary files /dev/null and b/calm-calatheas/docs/assets/pokemon/lumina.png differ diff --git a/calm-calatheas/docs/assets/pokemon/shadowclaw.png b/calm-calatheas/docs/assets/pokemon/shadowclaw.png new file mode 100644 index 00000000..07c333f6 Binary files /dev/null and b/calm-calatheas/docs/assets/pokemon/shadowclaw.png differ diff --git a/calm-calatheas/docs/assets/user-guide/camera-active.png b/calm-calatheas/docs/assets/user-guide/camera-active.png new file mode 100644 index 00000000..a4e898bd Binary files /dev/null and b/calm-calatheas/docs/assets/user-guide/camera-active.png differ diff --git a/calm-calatheas/docs/assets/user-guide/camera-loading.gif b/calm-calatheas/docs/assets/user-guide/camera-loading.gif new file mode 100644 index 00000000..8711918e Binary files /dev/null and b/calm-calatheas/docs/assets/user-guide/camera-loading.gif differ diff --git a/calm-calatheas/docs/assets/user-guide/collection.png b/calm-calatheas/docs/assets/user-guide/collection.png new file mode 100644 index 00000000..3533c707 Binary files /dev/null and b/calm-calatheas/docs/assets/user-guide/collection.png differ diff --git a/calm-calatheas/docs/assets/user-guide/dark.png b/calm-calatheas/docs/assets/user-guide/dark.png new file mode 100644 index 00000000..7bfcceb9 Binary files /dev/null and b/calm-calatheas/docs/assets/user-guide/dark.png differ diff --git a/calm-calatheas/docs/assets/user-guide/details.png b/calm-calatheas/docs/assets/user-guide/details.png new file mode 100644 index 00000000..ed2132fb Binary files /dev/null and b/calm-calatheas/docs/assets/user-guide/details.png differ diff --git a/calm-calatheas/docs/assets/user-guide/favourite.png b/calm-calatheas/docs/assets/user-guide/favourite.png new file mode 100644 index 00000000..2068fadc Binary files /dev/null and b/calm-calatheas/docs/assets/user-guide/favourite.png differ diff --git a/calm-calatheas/docs/assets/user-guide/home.png b/calm-calatheas/docs/assets/user-guide/home.png new file mode 100644 index 00000000..5a2f4fde Binary files /dev/null and b/calm-calatheas/docs/assets/user-guide/home.png differ diff --git a/calm-calatheas/docs/assets/user-guide/light.png b/calm-calatheas/docs/assets/user-guide/light.png new file mode 100644 index 00000000..c86e9a88 Binary files /dev/null and b/calm-calatheas/docs/assets/user-guide/light.png differ diff --git a/calm-calatheas/docs/assets/user-guide/processing.gif b/calm-calatheas/docs/assets/user-guide/processing.gif new file mode 100644 index 00000000..6588bcb0 Binary files /dev/null and b/calm-calatheas/docs/assets/user-guide/processing.gif differ diff --git a/calm-calatheas/docs/assets/user-guide/results.png b/calm-calatheas/docs/assets/user-guide/results.png new file mode 100644 index 00000000..465c0967 Binary files /dev/null and b/calm-calatheas/docs/assets/user-guide/results.png differ diff --git a/calm-calatheas/docs/code/backend.md b/calm-calatheas/docs/code/backend.md new file mode 100644 index 00000000..dcb29b29 --- /dev/null +++ b/calm-calatheas/docs/code/backend.md @@ -0,0 +1,16 @@ +# Backend + +Below is a high-level overview of the backend architecture: + +[![Backend Architecture Overview](../assets/code/backend.svg)](../assets/code/backend.svg) + +This diagram was generated automatically using [`pydeps`](https://pypi.org/project/pydeps/). The detailed documentation +below was generated with [`mkdocstrings`](https://mkdocstrings.github.io/). + +::: calm_calatheas.app + +::: calm_calatheas.logger + +::: calm_calatheas.model + +::: calm_calatheas.settings diff --git a/calm-calatheas/docs/code/frontend.md b/calm-calatheas/docs/code/frontend.md new file mode 100644 index 00000000..e1c10931 --- /dev/null +++ b/calm-calatheas/docs/code/frontend.md @@ -0,0 +1,23 @@ +# Frontend + +Below is a high-level overview of the frontend architecture: + +[![Frontend Architecture Overview](../assets/code/frontend.svg)](../assets/code/frontend.svg) + +This diagram was generated automatically using [`pydeps`](https://pypi.org/project/pydeps/). + +!!! NOTE "Diagram completeness" + + To keep the diagram clear, relationships between components, services, and their base classes are not shown. + +The following sections provide detailed documentation, generated automatically with [`mkdocstrings`](https://mkdocstrings.github.io/). + +::: app.frontend + +::: app.frontend.base + +::: app.frontend.components + +::: app.frontend.models + +::: app.frontend.services diff --git a/calm-calatheas/docs/code/index.md b/calm-calatheas/docs/code/index.md new file mode 100644 index 00000000..15379c91 --- /dev/null +++ b/calm-calatheas/docs/code/index.md @@ -0,0 +1,7 @@ +# Code + +This section provides automatically generated documentation for the codebase, including an overview of the project's modules, +classes, and functions. + +It is intended for developers who want to understand or contribute to the project, as well as for code jam judges reviewing +the implementation. diff --git a/calm-calatheas/docs/contributor-guide/development-environment.md b/calm-calatheas/docs/contributor-guide/development-environment.md new file mode 100644 index 00000000..abd732b3 --- /dev/null +++ b/calm-calatheas/docs/contributor-guide/development-environment.md @@ -0,0 +1,147 @@ +# Development Environment + +Follow the steps below to set up your development environment. + +## Configure your SSH Key + +Follow the steps below to configure your SSH key for accessing the repository: + +1. [Generate an SSH key](https://docs.github.com/en/github/authenticating-to-github/connecting-to-github-with-ssh/generating-a-new-ssh-key-and-adding-it-to-the-ssh-agent). +2. [Add the SSH key to your GitHub account](https://docs.github.com/en/github/authenticating-to-github/connecting-to-github-with-ssh/adding-a-new-ssh-key-to-your-github-account). + +## Clone the Repository + +To clone the repository, run the following command: + +```bash +git clone git@github.com:cj12-calm-calatheas/code-jam-12.git +``` + +This will clone the repository to your local machine using SSH. + +## Environment Setup + +To get started with the project, you can either install the [devcontainer](https://containers.dev) or follow the manual +setup instructions below. + +### Using the Devcontainer + +This project includes a [devcontainer](https://containers.dev) to automatically set up your development +environment, including the all tools and dependencies required for local development. + +??? NOTE "Prerequisites" + + Please ensure you have the following prerequisites installed: + + - [Docker](https://www.docker.com) must be installed on your system to use the devcontainer. + + - The [Remote Development Extension Pack](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.vscode-remote-extensionpack) for Visual Studio Code must be installed to work with devcontainers. + +??? TIP "Use WSL on Windows" + + If you are using Windows, we **strongly** recommend cloning the repository into the [WSL](https://learn.microsoft.com/en-us/windows/wsl/) + filesystem instead of the Windows filesystem. This significantly improves I/O performance when running the devcontainer. + +#### Configure your SSH Agent + +The devcontainer will attempt to pick up your SSH key from your `ssh-agent` when it starts. Follow the +guide on [sharing git credentials with the devcontainer](https://code.visualstudio.com/remote/advancedcontainers/sharing-git-credentials) +to ensure your SSH key is available inside the container. + +#### Open the Repository + +To get started, navigate to the folder where you cloned the repository and run: + +```bash +code . +``` + +This will open the current directory in Visual Studio Code. + +#### Build the Environment + +Once Visual Studio Code is open, you will see a notification at the bottom right corner of the window asking if +you want to open the project in a devcontainer. Select `Reopen in Container`. + +Your development environment will now be set up automatically. + +??? QUESTION "What if I don't see the notification?" + + You can manually open the devcontainer by pressing `F1` to open the command pallette. Type + `>Dev Containers: Reopen in Container` and press `Enter` to select the command. + +??? EXAMPLE "Detailed Setup Guides" + + For more details, refer to the setup guide for your IDE: + + - [Visual Studio Code](https://code.visualstudio.com/docs/devcontainers/tutorial) + - [PyCharm](https://www.jetbrains.com/help/pycharm/connect-to-devcontainer.html) + +### Manual Setup + +Alternatively, you can set up the development environment manually by following the steps below. + +??? NOTE "Prerequisites" + + Please ensure you have the following prerequisites installed: + + - [Python 3.13](https://www.python.org/downloads/) must be installed on your system. + - [Node.js](https://nodejs.org) must be installed on your system for linting non-Python files. + + You can check your Python version with: + + ```bash + python --version + ``` + +#### Open the Repository + +Start by opening the repository in your terminal or command prompt. + +```bash +cd path/to/your/repository +``` + +#### Set up your Python Environment + +This project uses [uv](https://docs.astral.sh/uv/) for dependency management. If you don't have `uv` installed, you can +install it using pip: + +```bash +python -m pip install uv +``` + +To install the dependencies, run: + +```bash +uv venv --allow-existing && uv sync +``` + +This sets up a virtual environment and installs all required packages. + +#### Install Node.js Dependencies + +For linting non-Python files, we also require some Node.js dependencies. To install them, run: + +```bash +npm install +``` + +#### Set up Pre-commit Hooks + +To ensure code quality, this project uses pre-commit hooks. Install them by running: + +```bash +uv run pre-commit install +``` + +This will set up the pre-commit hooks to run automatically on each commit. + +#### Install Playwright + +This project uses [Playwright](https://playwright.dev/python/) to simulate user interactions for testing. To install the +required dependencies, run the following command: + +```bash +uv run playwright install --with-deps +``` diff --git a/calm-calatheas/docs/contributor-guide/documentation.md b/calm-calatheas/docs/contributor-guide/documentation.md new file mode 100644 index 00000000..7da0a531 --- /dev/null +++ b/calm-calatheas/docs/contributor-guide/documentation.md @@ -0,0 +1,67 @@ +# Documentation + +This page provides guidelines for contributing to the documentation. + +## Tools + +The documentation is built using [MkDocs](https://www.mkdocs.org/), a static site generator that converts Markdown +files into a website. + +Markdown is a lightweight markup language with plain-text formatting syntax. Refer to the [Markdown Guide](https://www.markdownguide.org) +for more information on how to use Markdown. + +This project uses the [Material for MkDocs](https://squidfunk.github.io/mkdocs-material/) theme to generate the +documentation. Please review the theme documentation for guidance on how to use its various features. + +## Running the Documentation + +!!! NOTE "Prerequisites" + + Ensure you have [set up your development environment](./development-environment.md) before running the documentation. + +To view the documentation locally, you can use the following command: + +```bash +uv run mkdocs serve +``` + +Open your browser and navigate to [`http://localhost:8000`](http://localhost:8000) to view the documentation. +The changes you make to the documentation will be automatically reflected in the browser. + +## Adding a New Page + +To add a new page to the documentation, create a new Markdown file in the `docs` directory. + +Next, update the `nav` section in the `mkdocs.yaml` file to include the new page. The `nav` section defines the +structure of the documentation and the order in which the pages are displayed in the navigation bar. + +Please ensure that the folder structure in the `docs` directory matches the structure defined in the `nav` section. + +## Linting + +This project is configured to use [markdownlint](https://github.com/DavidAnson/markdownlint) to ensure consistent +Markdown styling and formatting across the documentation. The linter is automatically run when you commit changes +to the repository. + +You can configure the linter rules in the `.markdownlint.json` file. Refer to the [markdownlint rules](https://github.com/DavidAnson/markdownlint?tab=readme-ov-file#rules--aliases) +for more information on the available rules. + +!!! TIP "Use a Markdown Linter Extension" + + We recommend installing a Markdown linter extension in your editor to help identify and fix issues as you write. + The devcontainer is pre-configured with the [`markdownlint`](https://marketplace.visualstudio.com/items?itemName=DavidAnson.vscode-markdownlint) + extension for Visual Studio Code. + +## Formatting + +The documentation is formatted using [Prettier](https://prettier.io/), an opinionated code formatter that ensures +consistent style across the project. Prettier is automatically run when you save a Markdown file in the editor. + +You can configure the formatting rules in the `.prettierrc.json` file. Refer to the [Prettier options](https://prettier.io/docs/en/options.html) +for more information on the available options. + +## Publishing the Documentation + +The documentation is published automatically when changes are merged into the `main` branch. A GitHub Action workflow +is triggered to build the documentation and push it to the `gh-pages` branch. The published documentation is hosted +on GitHub Pages. diff --git a/calm-calatheas/docs/contributor-guide/index.md b/calm-calatheas/docs/contributor-guide/index.md new file mode 100644 index 00000000..4623788d --- /dev/null +++ b/calm-calatheas/docs/contributor-guide/index.md @@ -0,0 +1,6 @@ +# Contributor Guide + +This guide provides information on how to contribute to the project, including setting up your development environment, +using version control, and contributing to the documentation. + +It is intended for project members as well as the code jam judges. diff --git a/calm-calatheas/docs/contributor-guide/version-control.md b/calm-calatheas/docs/contributor-guide/version-control.md new file mode 100644 index 00000000..246c0e87 --- /dev/null +++ b/calm-calatheas/docs/contributor-guide/version-control.md @@ -0,0 +1,165 @@ +# Version Control + +Follow the steps below when contributing to the project. These steps ensure that all changes are properly tracked and reviewed. + +## Create a New Branch + +Always create a new branch for your changes. This makes it easier to handle multiple contributions simultaneously. + +??? QUESTION "Why should I create a new branch?" + + Creating a new branch allows you to work on your changes without affecting the `main` branch. This makes it + easier to collaborate with others and keep the codebase clean. + +First, pull the latest changes from the `main` branch: + +```bash +git pull main +``` + +Next, create a new branch with the following command: + +```bash +git checkout -b "" +``` + +Replace `` with a short, descriptive name for your branch. For example, `add-uptime-command`. + +## Commit your Changes + +On your local branch, you can make changes to the code such as adding new features, fixing bugs, or updating documentation. +Once you have made your changes, you can commit them to your branch. + +```bash +git add . +git commit -m "feat: add uptime command" +``` + +Make sure to write a clear and concise commit message that describes the changes you have made. + +??? QUESTION "How often should I commit my changes?" + + It's a good practice to commit your changes often. This allows you to track your progress and revert changes if needed. + +### Automated Checks + +The project includes pre-commit hooks to ensure your code meets the quality standards. These hooks run automatically +before each commit. + +??? QUESTION "What if the pre-commit hooks fail?" + + If the pre-commit hooks fail, you will need to address the issues before committing your changes. Follow the + instructions provided by the pre-commit hooks to identify and fix the issues. + +??? QUESTION "How do I run the pre-commit hooks manually?" + + Pre-commit hooks can also be run manually using the following command: + + ```bash + uv run pre-commit + ``` + +The pre-commit hooks are intended to help us keep the codebase maintainable. If there are rules that you believe +are too strict, please discuss them with the team. + +## Create a Pull Request + +Once you have completed your changes, it's time to create a pull request. A pull request allows your changes to +be reviewed and merged into the `main` branch. + +Before creating a pull request, ensure your branch is up to date with the latest changes from the `main` branch: + +```bash +git pull main +``` + +Next, push your changes to the repository: + +```bash +git push +``` + +Finally, [create a pull request on GitHub](https://github.com/cj12-calm-calatheas/code-jam-12/compare). Select +your branch as the source and the `main` branch as the base. + +Give your pull request a descriptive title that summarizes the changes you have made. In the pull request description, +provide a brief overview of the changes and any relevant information for reviewers. + +??? EXAMPLE "Pull Request Description" + + Here's an example of a good pull request description: + + ```plaintext + # feat: add uptime command + + This pull request adds a new uptime command to display the bot's uptime. + + ## Changes + + - Added a new command to display the bot's uptime + - Updated the help command to include information about the new command + + ## Notes + + - The new command is implemented in a separate file for better organization + - The command has been tested locally and works as expected + ``` + +### Automated Checks + +The same pre-commit hooks that run locally will also run automatically on the pull request. The workflow also +runs the tests to ensure everything is working correctly, and checks the docs for any broken links. + +??? QUESTION "What if the checks fail on the pull request?" + + If the checks fail on the pull request, you will need to address the issues in your branch and push + the changes. The checks will run again automatically. + + Please address any issues identified by the checks before requesting a review. + +## Ask for a Review + +All pull requests should be reviewed by at least one other team member before merging. The reviewer will provide +feedback and suggestions for improvement. + +Once the reviewer approves the pull request, you can merge it into the `main` branch. + +??? QUESTION "How do I request a review?" + + Request a review from a team member by [assigning them as a reviewer](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/requesting-a-pull-request-review) + to your pull request. + +### Giving Feedback + +When providing feedback on a pull request, be constructive and specific. Point out areas for improvement and suggest +possible solutions. If you have any questions or concerns, don't hesitate to ask the author for clarification. + +A code review should focus on the following aspects: + +- Correctness and functionality +- Code quality and readability +- Adherence to the project guidelines + +??? EXAMPLE "Good Code Review Feedback" + + Here are some examples of good code review feedback: + + ```plaintext + - Great work on the new command! The implementation looks good overall. + - I noticed a small typo in the docstring. Could you update it to fix the typo? + - The logic in the new command is a bit complex. Consider breaking it down into smaller functions for clarity. + - The tests cover most of the functionality, but we are missing a test case for edge case X. Could you add a test for that? + ``` + +Always be respectful and considerate when giving feedback. Remember that the goal is to improve the code and help +the author grow as a developer. + +!!! SUCCESS "Be Positive" + + Don't forget to acknowledge the positive aspects of the contribution as well! + +## Merge the Pull Request + +Once the pull request has been approved and all checks have passed, you can merge it into the `main` branch. +To merge the pull request, click the "Merge" button on the pull request page. After merging, your branch will be automatically +deleted. diff --git a/calm-calatheas/docs/design/backend.md b/calm-calatheas/docs/design/backend.md new file mode 100644 index 00000000..c0c808c1 --- /dev/null +++ b/calm-calatheas/docs/design/backend.md @@ -0,0 +1,63 @@ +# Backend + +The Pokedexter backend is primarily responsible for serving the machine learning model for generating descriptions and serving +the static files for the frontend application. + +## Web Server + +The web server is built with [Starlette](https://www.starlette.io/), a lightweight [ASGI](https://en.wikipedia.org/wiki/Asynchronous_Server_Gateway_Interface) +framework for Python web applications. Starlette offers essential features for handling HTTP requests, routing, and middleware, +making it a straightforward choice for our backend. While we considered [FastAPI](https://fastapi.tiangolo.com/), we ultimately +selected Starlette for its simplicity and minimalism. + +The server serves static frontend files and exposes two main endpoints: + +- **Description Generation:** An endpoint that uses the machine learning model to generate Pokémon descriptions. +- **Healthcheck:** An endpoint for monitoring the server’s status. Used by the Docker container to ensure the service + is running. + +This setup keeps the backend focused and efficient, aligning with our design goals. + +## Description Generation + +The backend hosts a machine learning model that generates Pokémon descriptions. This model is accessed through the Description +Generation endpoint, allowing the frontend to request descriptions based on captions created in the browser. + +We use the [`Qwen/Qwen3-1.7B`](https://huggingface.co/Qwen/Qwen3-1.7B) model, a general-purpose text generator. After +testing various prompts and settings, we found that this model produces high-quality Pokémon descriptions from image captions. +However, the model is quite large. While it can run on a laptop or desktop for limited use, it does not scale well to many +users and user experience will degrade under heavy load. + +For best results, we recommend running the model on a machine that has a [GPU with CUDA support](https://en.wikipedia.org/wiki/CUDA#GPUs_supported) +(the oldest version we tested was CUDA 6.5 on an NVIDIA GeForce GTX 1080ti) and 16GB of RAM. In our experience, generating +a description typically takes less than a minute. + +We would have preferred to use a more lightweight model that could run directly in the browser. However, the lightweight +models we tested did not generate high-quality Pokémon descriptions. One possible solution would be to fine-tune or train +a smaller model specifically for this task, but this would have required more time and a dataset of high-quality Pokémon +descriptions, which were beyond our resources for the code jam. + +## Reverse Proxy + +We recommend deploying Pokedexter behind a [reverse proxy](https://en.wikipedia.org/wiki/Reverse_proxy) acting as a +[TLS termination proxy](https://en.wikipedia.org/wiki/TLS_termination_proxy). Both the camera and PWA features require +a [secure browser context](https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts), which is only available +when the app is served over HTTPS. + +!!! DANGER "Secure browser context required" + + The camera and PWA features will not work outside of a secure browser context! + +A reverse proxy is not included in our stack, as most users will already have their own solution or use a managed reverse +proxy provided by their cloud platform. The deployment guide includes instructions for setting up a TLS termination proxy +with [Caddy](https://caddyserver.com/). + +If you do not have a valid TLS certificate, the app can only be used on `localhost`, since browsers treat it as a secure +context. + +## Docker + +Pokedexter can be easily deployed using [Docker](https://www.docker.com/). We provide a `Dockerfile` that sets up the necessary +environment and dependencies for running the application. The Docker image includes the web server, the machine learning +model, and all static files needed for the frontend. We do not publish the Docker image to a public registry, so users +will need to build it locally. diff --git a/calm-calatheas/docs/design/design-goals.md b/calm-calatheas/docs/design/design-goals.md new file mode 100644 index 00000000..468e906e --- /dev/null +++ b/calm-calatheas/docs/design/design-goals.md @@ -0,0 +1,13 @@ +# Design Goals + +Our primary design goals for the code jam project were: + +- **Browser-first:** The application should run entirely in the browser, using technologies like PyScript and Pyodide and + leveraging the browser APIs where possible. +- **Python-centric:** The project should be developed mainly in Python, minimizing reliance on other languages. +- **Modern web architecture:** The app’s structure should follow typical web application patterns, allowing us to assess + Python’s suitability for building modern web apps. +- **Mobile-friendly:** The application should provide a seamless experience on mobile devices, similar to an authentic Pokédex. + +As the project progressed, we aimed to add **offline capability** as a key feature. However, this goal was only partially +achieved, as some components ultimately did not run in the browser. diff --git a/calm-calatheas/docs/design/index.md b/calm-calatheas/docs/design/index.md new file mode 100644 index 00000000..5642df1f --- /dev/null +++ b/calm-calatheas/docs/design/index.md @@ -0,0 +1,4 @@ +# Design + +This section describes the design of the Pokedexter application. It is intended for developers, the code jam judges, and +anyone interested in understanding the architecture, components, and design decisions behind the system. diff --git a/calm-calatheas/docs/design/system-overview.md b/calm-calatheas/docs/design/system-overview.md new file mode 100644 index 00000000..a730c9cb --- /dev/null +++ b/calm-calatheas/docs/design/system-overview.md @@ -0,0 +1,12 @@ +# System Overview + +The diagram below illustrates the architecture of the Pokedexter system: + +![System Overview](../assets/design/overview.drawio) + +Most of the system runs in the browser, using technologies such as PyScript and Pyodide, and makes extensive use of browser +APIs. Features like object recognition and the database, which are often implemented as backend services, are handled in-browser +where possible. + +The backend is intentionally minimal to align with our design goals and the code jam theme. It mainly serves static frontend +assets and processes tasks that cannot be handled in the browser due to resource constraints. diff --git a/calm-calatheas/docs/design/web-app.md b/calm-calatheas/docs/design/web-app.md new file mode 100644 index 00000000..b60f3c17 --- /dev/null +++ b/calm-calatheas/docs/design/web-app.md @@ -0,0 +1,166 @@ +# Web App + +The web app is the core of the system, responsible for the user experience, interface, and interactions. Built with [PyScript](https://pyscript.net/) +and [Pyodide](https://pyodide.org/en/stable/), it runs Python code directly in the browser. The app includes a presentation +layer and modules for camera access, image processing, and database management. + +## Presentation Layer + +The presentation layer manages the user interface and interactions. It is structured as a [Single Page Application (SPA)](https://en.wikipedia.org/wiki/Single-page_application) +and follows a [Model-View-Presenter (MVP)](https://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93presenter) architecture, +which separates UI components from business logic and state management. This design keeps the application architecture +similar to modern web frameworks like React or Angular, aligning with our key design goals. + +[![Model-View-Presenter Pattern](../assets/design/mvp.png)](../assets/design/mvp.png) + +### Components + +Views are organized as individual components, each responsible for a specific part of the user interface. They are all grouped +in the [`components`][app.frontend.components] module and inherit from a base [`Component`][app.frontend.base.component.Component] +class that implements rendering logic and lifecycle methods. + +The lifecycle of a component includes _initialization_, _rendering_, and _destruction_. This lifecycle is typically managed +by a parent component that coordinates these phases. + +```mermaid +graph TB + subgraph Component Lifecycle + direction LR + A[Initialization] --> B[Rendering] + B --> C[Destruction] + end +``` + +**Initialization**: The component is created but not yet displayed. This phase sets up the component’s internal state +and prepares it for rendering. + +**Rendering**: The component generates its HTML and inserts it into the DOM at its designated root element. Most components +also attach event listeners at this stage to handle user interactions and state changes. + +**Destruction**: The component is removed from the DOM and any resources it used are released. This includes detaching +event listeners and cleaning up internal state. + +Lifecycle hooks enable components to run custom logic before and after each phase of their lifecycle. These hooks provide +flexibility for setup, teardown, and responding to changes during initialization, rendering, and destruction. + +Components define their HTML structure using text-based templates, which are processed by a simple, custom-built templating +engine. This engine is intentionally minimal and does not support advanced features such as conditionals or loops, unlike +the templating systems in frameworks like React or Angular. Looking ahead, the upcoming [template strings](https://peps.python.org/pep-0750) +feature in Python 3.14 is expected to improve Python’s native HTML templating capabilities, as supporting this use case +is specifically mentioned in the PEP. + +### Services + +Services are responsible for providing business logic and state management for the application. They encapsulate the core +functionality and can be reused across different components. Most services exist globally and can be accessed by any component +that needs them, although they can also be owned by specific components if needed. + +All services are organized into the [`services`][app.frontend.services] module and inherit from the [`Service`][app.frontend.base.service.Service] +base class, which provides lifecycle management for all services. + +A service’s lifecycle consists of two main phases: _initialization_ and _destruction_. Global services are managed centrally, +while services owned by specific components are managed by those components. + +```mermaid +graph TB + subgraph Service Lifecycle + direction LR + A[Initialization] --> B[Destruction] + end +``` + +**Initialization**: The service is instantiated and its internal state is set up. This may include fetching initial data +or configuring dependencies. + +**Destruction**: The service is cleaned up when it is no longer needed. This involves releasing resources such as event +listeners, database sessions, or network connections. + +Lifecycle hooks allow services to execute custom logic before and after each phase, providing flexibility for setup, teardown, +and responding to changes during initialization and destruction. + +### State Management + +State is managed by services, which implement the observer pattern. This allows components to subscribe to state changes +and react as needed, decoupling state management from the presentation layer and ensuring a clean separation of concerns. + +Unlike frameworks such as React or Angular, which provide built-in concepts like signals and effects, PyScript does not +offer these features natively. Instead, we use the [reactivex](https://github.com/ReactiveX/RxPY) library to manage asynchronous +data streams and events, enabling reactive programming patterns within the application. + +## Camera + +The camera service manages access to the user's camera and handles image capture and permissions. + +Integrating camera functionality directly into the application ensures a seamless experience for users as they explore +and search for Pokémon, without needing to switch to a separate camera app. This approach also accommodates devices that +may not have a dedicated camera application. + +For users without a camera, or for those who wish to analyze existing images, the application also provides an image upload +feature. This allows users to select and process images from their device, ensuring accessibility and flexibility for all +users. + +## Object Recognition + +The object recognition service identifies and classifies objects within images, then generates captions describing them. +These captions are passed to the description generation model, which creates Pokémon-themed descriptions based on the recognized +objects. + +The object recognition service uses the [`Xenova/vit-gpt2-image-captioning`](https://huggingface.co/Xenova/vit-gpt2-image-captioning) +model, an image-to-text machine learning model. This lightweight model runs well even on devices with limited computing +power, such as smartphones. While it provides reasonable results for image captioning tasks, our experimentation shows +that its accuracy may vary depending on the input. + +We originally planned to use the [`transformers`](https://pypi.org/project/transformers/) library for Python to run the +model. Unfortunately, some of its dependencies are incompatible with Pyodide because they cannot be compiled to WebAssembly. + +We considered two options: moving the object recognition service to the backend to keep it in Python, or running the model +in the browser using [`transformers.js`](https://huggingface.co/docs/transformers.js/en/index) and accessing it from Python +via Pyodide. To keep with the spirit of the code jam, we chose to run the model entirely in the browser using `transformers.js`. + +While we would have preferred to leverage Python’s rich machine learning ecosystem for this feature, current limitations +prevent us from doing so. Enabling advanced machine learning capabilities directly in the browser would make Python a much +stronger choice for web development and help it move beyond experimental or hobby projects. + +There is a noticeable slowdown during model initialization and occasionally when processing images. To address this, we +tried running the model in a separate [web worker](https://docs.pyscript.net/2025.8.1/user-guide/workers/) to offload +processing from the main thread. However, we encountered errors loading the model in the worker and were unable to resolve +them within our timeframe. With more time, we believe this issue could be solved, but we chose to focus on other features. + +## Database + +The database is responsible for storing each user's Pokémon collection. + +We use [IndexedDB](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API) as our database solution because it +is a widely supported web standard that offers a robust, asynchronous API for managing large amounts of structured data +directly in the browser. + +Storing each user's collection in a client-side database means the data is kept locally in their browser. This approach +has some limitations: collections cannot be shared across devices or with other users, and all data will be lost if the +user clears their browser storage. We accept these trade-offs because using a browser-based database fits the code jam +theme and our design goals. For a future version, we would consider synchronizing the database with a remote server to +enable cross-device access and backups. + +## Progressive Web App + +Pokedexter is a [Progressive Web App (PWA)](https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps), meaning +it can be installed on a user's device and run like a native app. When installed, it operates in standalone mode, providing +easier access and a familiar user experience. + +While some PWAs offer offline capabilities, Pokedexter currently relies on the backend for description generation and requires +an internet connection. A key improvement could be to explore or train a model that can run entirely on-device, enabling +a fully offline experience. This would enhance the fantasy of carrying a real Pokédex with you outdoors or in nature. + +## Browser Support + +Pokedexter is tested and works reliably on all major modern browsers, including Chromium-based browsers (such as Chrome +and Edge), WebKit-based browsers (Safari), and Firefox. To ensure consistent behavior across environments, we use [Playwright](https://playwright.dev/python/) +together with [Pytest](https://pytest.org/) and [Testcontainers](https://testcontainers.com/) for automated cross-browser +testing in a production-like environment. + +You can find the test scenarios in the `tests` folder. + +## CSS Framework + +We chose [Bulma](https://bulma.io/) as our CSS framework because it is a modern, responsive framework that makes it easy +to create visually appealing and responsive layouts. The choice for a CSS framework rather than writing custom styles from +scratch saved us some time to focus on the functionality of the app. diff --git a/calm-calatheas/docs/index.md b/calm-calatheas/docs/index.md new file mode 100644 index 00000000..14fce7cd --- /dev/null +++ b/calm-calatheas/docs/index.md @@ -0,0 +1,63 @@ +# Pokedexter + +_Who's that Pokémon?_ + +Ever wondered if your cat could be a Pokémon? Curious about what’s hiding in your cupboard? **Pokedexter** is an AI-powered +Pokédex that helps you discover Pokémon wherever you are. Just snap a photo, and Pokedexter will identify the Pokémon—maybe +even ones you never expected! + +Open Pokedexter on your phone and start discovering Pokémon all around you! + +
+ + + Shadowclaw + + + + AquaPuff + + + + ChronoClock + + + + Lumina + + +
+ +## Features + +- Identify Pokémon anywhere using AI image recognition and description generation. +- Build your personal Pokémon collection—your progress is saved locally. +- Use Pokedexter on any device that supports a modern web browser. +- Designed for mobile use with a responsive layout. +- Installable as a Progressive Web App (PWA). +- Powered by a technology stack that’s 99% Python. + +## Fit with the theme + +The theme for this year's code jam is to write a **browser-based** Python application that is **the wrong tool for the job**. + +Pokedexter is an intentionally out of place tool for Pokémon identification. It neither identifies real-world objects nor +actual Pokémon. Instead, it invites users to view their everyday surroundings through a playful, imaginative Pokémon perspective. + +Most modern web apps are built with JavaScript frameworks like React or Angular. With Pokedexter, we set out to see how +closely we could replicate a typical web app experience using Python instead of JavaScript. Our goal was to apply common +web development architecture and design patterns, while also taking advantage of Python’s strengths in data processing +and machine learning. + +## About the team + +This project has been built by the Calm Calatheas team for the [Python Discord Code Jam 2025](https://pythondiscord.com/events/code-jams/12/). +Please feel free to reach out if you have any questions, or need a hand with anything! + +| | Name | Contributions | +| --------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------- | ------------------------------------------------------------------ | +| [TFBlunt](https://github.com/thijsfranck) | [TFBlunt](https://github.com/thijsfranck) | Team Lead, Frontend, Description Generation, Docs | +| [leoluy](https://github.com/leolhuile) | [leoluy](https://github.com/leolhuile) | Description Generation, Model Selection, Frontend Mockup, Ideation | +| [zike01](https://github.com/Zike01) | [Zike01](https://github.com/Zike01) | Ideation | +| [Flonc](https://github.com/FloncDev) | [Flonc](https://github.com/FloncDev) | Initial Frontend Prototype | +| [esmaycat](https://github.com/esmaycat) | [esmaycat](https://github.com/esmaycat) | Object Detection, Transformers.js integration, Favourites Feature | diff --git a/calm-calatheas/docs/setup-guide.md b/calm-calatheas/docs/setup-guide.md new file mode 100644 index 00000000..5fb64095 --- /dev/null +++ b/calm-calatheas/docs/setup-guide.md @@ -0,0 +1,163 @@ +# Setup Guide + +This guide provides a step-by-step approach on how to run Pokedexter. + +## Set up the development environment + +Follow the instructions in the [development environment setup guide](./contributor-guide/development-environment.md) to +set up your local environment. + +## Run the app locally + +The easiest way to run Pokedexter locally is to use the included [`taskipy`](https://pypi.org/project/taskipy/) configuration. +Run the following command: + +```bash +uv run task serve +``` + +This runs a development server that you can use to access the app from your local machine. This is great for trying out +the app yourself on the device where you are running the server. + +Keep reading if you'd like to deploy Pokedexter for production use, or if you'd like to access the app from another device +like a mobile phone or tablet. + +## Local HTTPS for Mobile Testing + +You may just want to test the application on a mobile device without setting up a full reverse proxy. Here's how to create a simple, self-signed HTTPS server for local testing. + +First, you'll need to create your own SSL/TLS certificate. This certificate will be used to encrypt the connection between your computer and the mobile device. To generate it, run the following command in the project's root directory: + +```bash +openssl req -x509 -keyout key.pem -out cert.pem -nodes +``` + +The command will prompt you for some information. When asked for the "Common Name", enter your computer's local IP address. For all other prompts, press Enter to accept the default values. This process will generate two files in your project's root directory: `key.pem` (your private key) and `cert.pem` (your self-signed certificate). + +You can now start the server with the following command: + +```bash +uv run uvicorn calm_calatheas.app:app \ + --host "0.0.0.0" \ + --port 4443 \ + --ssl-keyfile key.pem \ + --ssl-certfile cert.pem +``` + +Because the certificate is self-signed (i.e. not issued by a trusted authority), your browser will likely display a "certificate not trusted" warning. This is expected. You can safely bypass this warning to continue to your application. + +## Build the Docker image + +The easiest way to deploy Pokedexter is to use [Docker](https://www.docker.com/). To deploy Pokedexter, you must first +build the Docker image. + +!!! INFO "Prerequisite" + + Make sure you have [Docker](https://www.docker.com/) installed before proceeding. + +The project has a `taskipy` configuration that makes it easy to build the Docker image. Run the following command: + +```bash +uv run task build-docker +``` + +This first builds a `.whl` file for the project, and then uses that file to build the Docker image based on the included +`Dockerfile`. The docker image will be called `calm-calatheas:latest`. + +## Set the environment variables + +Pokedexter can be configured using environment variables. The following configuration options are available: + +| Environment Variable | Description | Default | +| -------------------- | --------------------------------------- | --------- | +| `HOST` | The address to bind the server to. | `0.0.0.0` | +| `LOG_LEVEL` | The logging level for the application. | `DEBUG` | +| `PORT` | The port to run the server on. | `8000` | +| `STATIC_FILES_PATH` | The path to the static files directory. | `app` | + +!!! NOTE "All settings are optional" + + You can run the app using the default settings without specifying any environment variables. + +See the [`Settings`][calm_calatheas.settings.Settings] documentation for more information. + +## Run the Docker container + +Once the image is built, you can deploy the app to an environment of your choice. + +!!! INFO "Minimum system specs" + + For a minimal deployment, we recommend **2 CPU cores** and **8GB of RAM**. We also recommend a GPU with at least + **4GB of VRAM** and **CUDA 6.5** support or higher. + +**If you are deploying Pokedexter to the cloud**, refer to your cloud provider's documentation on how to deploy a Docker +container. + +**If you are hosting Pokedexter yourself**, you can run the Docker container with the following command: + +```bash +docker run -p 8000:8000 calm-calatheas:latest +``` + +This runs the container and maps the default port `8000` to the host machine, allowing you to access the app at `http://localhost:8000`. + +!!! DANGER "Secure browser context required" + + Both the camera and PWA features require a [secure browser context](https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts), which is only available when the app is served over HTTPS or on `localhost`. + + Keep reading if your deployment will be accessed outside of `localhost`. + +## Set up a reverse proxy + +We recommend deploying Pokedexter behind a [reverse proxy](https://en.wikipedia.org/wiki/Reverse_proxy) acting as a +[TLS termination proxy](https://en.wikipedia.org/wiki/TLS_termination_proxy). + +!!! INFO "Prerequisite" + + Make sure you have a registered [domain name](https://en.wikipedia.org/wiki/Domain_name) and that you have access + to the [DNS](https://en.wikipedia.org/wiki/Domain_Name_System) settings for that domain. + +**If you are deploying Pokedexter to the cloud**, we recommend that you use your cloud provider's gateway solution to set +up HTTPS for the app. + +**If you are hosting Pokedexter yourself**, here's a sample `docker-compose.yaml` file using [Caddy](https://caddyserver.com/): + +```yaml +name: pokedexter + +services: + reverse-proxy: + image: caddy:latest + command: caddy reverse-proxy --from :8000 --to app:8000 + depends_on: + app: + condition: service_healthy + ports: + - 8000:8000 + + app: + image: calm-calatheas:latest + ports: + - 8000 +``` + +This configuration sets up Caddy as a reverse proxy for your app, allowing you to access it securely over HTTPS. Caddy +will automatically obtain and renew SSL certificates for your domain using [Let's Encrypt](https://letsencrypt.org/). + +## Set up DNS + +Finally, set up a DNS record for your domain that points to the server where the reverse proxy is running: + +```plaintext +Type: A +Host: +Value: +TTL: 3600 +``` + +Replace `` with the public IP address of the machine running your reverse proxy. This will direct +traffic for `` to your server. + +!!! SUCCESS "Deployment complete" + + You can now access the app from any device at `https://:8000`! diff --git a/calm-calatheas/docs/stylesheets/table.css b/calm-calatheas/docs/stylesheets/table.css new file mode 100644 index 00000000..b7354a34 --- /dev/null +++ b/calm-calatheas/docs/stylesheets/table.css @@ -0,0 +1,11 @@ +/* + * Make tables full width by default + */ + +.md-typeset__table { + min-width: 100%; +} + +.md-typeset table:not([class]) { + display: table; +} diff --git a/calm-calatheas/docs/user-guide.md b/calm-calatheas/docs/user-guide.md new file mode 100644 index 00000000..91f3e734 --- /dev/null +++ b/calm-calatheas/docs/user-guide.md @@ -0,0 +1,151 @@ +# User Guide + +Welcome to the user guide for Pokedexter! This guide will help you get started building your Pokémon collection. + +Open Pokedexter in your favorite web browser to get started! + +!!! TIP "Use Pokedexter on your phone" + + Open Pokedexter on your phone to identify Pokémon wherever you are! + +## Home Page + +The home page is the main screen of the app. Let's go through its step by step: + +
+ + + Home + + +
+ +At the top of the page is a welcome message and some instructions on how to get started. Below that is your Pokémon collection, +which is empty for now. + +The app is telling us to take a picture or upload an image to discover the Pokémon inside. Let's do that now! + +## Taking a Picture + +To start taking a picture, click the camera icon at the bottom of the screen. This will pop up the camera interface, allowing +you to take a picture of the object you would like to identify. + +
+ + + Camera Loading + + + + Camera Active + + +
+ +While loading, you can see the camera interface preparing to take a picture. You may at this point be prompted to allow +camera access if you haven't already done so. + +!!! INFO "Grant Camera Access" + + You will not be able to use the camera feature until you grant access. If you deny access by mistake, you can refresh the page to + be prompted again, or enable it again in your device settings. + +Once ready, the camera image will appear on the screen. You can now take a picture by clicking the **capture** button. +This will process the image and attempt to identify the Pokémon within it. + +!!! TIP "Switching Cameras" + + If your device has more than one camera (such as front and back cameras), you can switch between them by clicking the **switch camera** + button in the camera interface, located next to the **capture** button. + +## Uploading an Image + +If you prefer to upload an image instead of using the camera, you can do so by clicking the **upload** button at the bottom +of the screen. This will open a file dialog, allowing you to select an image file from your device. + +!!! TIP "Access your Camera App" + + If you prefer to use your phone's camera app, it will also be available as an option in the upload dialog. + +Once you have selected an image, the app will process it and attempt to identify the Pokémon within it. + +## Analyzing the Image + +Now, the app will analyze the image and try to detect any Pokémon present. This may take a few moments, so please be patient. +While you wait, you can try to guess which Pokémon it might come up with! + +Once the image has been processed, the app will display the results, showing the identified Pokémon along with their details. + +
+ + + Processing + + + + Results + + +
+ +## Building your Collection + +Congratulations on capturing your first Pokémon! You can now view it in your collection and continue to add more Pokémon +as you discover them. + +!!! INFO "Saving Your Collection" + + Your collection is automatically saved on your device so you don't lose your progress. + +Here's what it looks like when there's a few more Pokémon in your collection: + +
+ + + Collection + + +
+ +## Managing your Collection + +Once you've started building your collection, you may want to show your love for a specific Pokémon. You can mark it as +a favorite by clicking the **favourite** button in the Pokémon details view. A heart icon will appear next to the Pokémon's +name! + +If you ever change your mind, you can also unfavourite a Pokémon by clicking the **unfavourite** button. If you'd like +to remove a Pokémon from your collection entirely, you can do so by clicking the **delete** button at the bottom of the +details view. + +
+ + + Details + + + + Favourite + + +
+ +## Switching the Theme + +By default, the app will use your system's theme preference (light or dark). However, you can change the theme manually +by clicking the **theme** buttons in the app settings. + +You can switch back to your system default by selecting **auto**. + +
+ + + Light Theme + + + + Dark Theme + + +
+ +Happy exploring! diff --git a/calm-calatheas/mkdocs.yaml b/calm-calatheas/mkdocs.yaml new file mode 100644 index 00000000..32b5e423 --- /dev/null +++ b/calm-calatheas/mkdocs.yaml @@ -0,0 +1,116 @@ +copyright: © 2025 Calm Calatheas 🪴 +repo_name: cj12-calm-calatheas/code-jam-12 +repo_url: https://github.com/cj12-calm-calatheas/code-jam-12 +site_name: Pokedexter +site_url: https://cj12-calm-calatheas.github.io/code-jam-12/ +dev_addr: 127.0.0.1:9000 + +theme: + favicon: assets/logo-16x16.png + logo: assets/logo-128x128.png + name: material + search: true + + features: + - content.code.copy + - navigation.indexes + - navigation.instant + - navigation.instant.progress + - navigation.path + - navigation.sections + - navigation.tabs + - search.highlight + + icon: + repo: fontawesome/brands/github + + palette: + - media: "(prefers-color-scheme)" + toggle: + icon: material/brightness-auto + name: Switch to light mode + - media: "(prefers-color-scheme: light)" + primary: white + scheme: default + toggle: + icon: material/brightness-7 + name: Switch to dark mode + - media: "(prefers-color-scheme: dark)" + primary: black + scheme: slate + toggle: + icon: material/brightness-4 + name: Switch to system preference + +markdown_extensions: + - admonition + - attr_list + - codehilite + - md_in_html + - pymdownx.details + - pymdownx.snippets + - pymdownx.superfences: + custom_fences: + - name: mermaid + class: mermaid + format: !!python/name:pymdownx.superfences.fence_code_format + - pymdownx.emoji: + emoji_index: !!python/name:material.extensions.emoji.twemoji + emoji_generator: !!python/name:material.extensions.emoji.to_svg + - pymdownx.snippets + - pymdownx.tabbed: + alternate_style: true + - pymdownx.tasklist: + clickable_checkbox: true + custom_checkbox: true + - toc: + permalink: true + +nav: + - User Guide: user-guide.md + - Setup Guide: setup-guide.md + - Contributor Guide: + - contributor-guide/index.md + - Development Environment: contributor-guide/development-environment.md + - Version Control: contributor-guide/version-control.md + - Documentation: contributor-guide/documentation.md + - Design: + - design/index.md + - Design Goals: design/design-goals.md + - System Overview: design/system-overview.md + - Web App: design/web-app.md + - Backend: design/backend.md + - Code: + - code/index.md + - Frontend: code/frontend.md + - Backend: code/backend.md + +plugins: + - autorefs + - drawio + - mkdocstrings: + handlers: + python: + options: + show_root_heading: true + show_object_full_path: false + show_symbol_type_heading: true + show_symbol_type_toc: true + show_signature: true + separate_signature: true + show_signature_annotations: true + signature_crossrefs: true + show_source: false + show_if_no_docstring: true + show_docstring_examples: true + - search + - tags + +extra: + generator: false + social: + - icon: fontawesome/brands/github + link: https://github.com/cj12-calm-calatheas/code-jam-12 + +extra_css: + - stylesheets/table.css diff --git a/calm-calatheas/package-lock.json b/calm-calatheas/package-lock.json new file mode 100644 index 00000000..dce2767f --- /dev/null +++ b/calm-calatheas/package-lock.json @@ -0,0 +1,454 @@ +{ + "name": "calm-calatheas", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "calm-calatheas", + "devDependencies": { + "nodemon": "3.1.10", + "prettier": "3.6.2", + "prettier-plugin-sort-json": "4.1.1", + "prettier-plugin-toml": "2.0.6" + } + }, + "node_modules/@taplo/core": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@taplo/core/-/core-0.2.0.tgz", + "integrity": "sha512-r8bl54Zj1In3QLkiW/ex694bVzpPJ9EhwqT9xkcUVODnVUGirdB1JTsmiIv0o1uwqZiwhi8xNnTOQBRQCpizrQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@taplo/lib": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@taplo/lib/-/lib-0.5.0.tgz", + "integrity": "sha512-+xIqpQXJco3T+VGaTTwmhxLa51qpkQxCjRwezjFZgr+l21ExlywJFcDfTrNmL6lG6tqb0h8GyJKO3UPGPtSCWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@taplo/core": "^0.2.0" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/ignore-by-default": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", + "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", + "dev": true, + "license": "ISC" + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nodemon": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.10.tgz", + "integrity": "sha512-WDjw3pJ0/0jMFmyNDp3gvY2YizjLmmOUQo6DEBY+JgdvW/yQ9mEeSw6H5ythl5Ny2ytb7f9C2nIbjSxMNzbJXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^3.5.2", + "debug": "^4", + "ignore-by-default": "^1.0.1", + "minimatch": "^3.1.2", + "pstree.remy": "^1.1.8", + "semver": "^7.5.3", + "simple-update-notifier": "^2.0.0", + "supports-color": "^5.5.0", + "touch": "^3.1.0", + "undefsafe": "^2.0.5" + }, + "bin": { + "nodemon": "bin/nodemon.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nodemon" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/prettier": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz", + "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prettier-plugin-sort-json": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/prettier-plugin-sort-json/-/prettier-plugin-sort-json-4.1.1.tgz", + "integrity": "sha512-uJ49wCzwJ/foKKV4tIPxqi4jFFvwUzw4oACMRG2dcmDhBKrxBv0L2wSKkAqHCmxKCvj0xcCZS4jO2kSJO/tRJw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "prettier": "^3.0.0" + } + }, + "node_modules/prettier-plugin-toml": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/prettier-plugin-toml/-/prettier-plugin-toml-2.0.6.tgz", + "integrity": "sha512-12N/wBuHa9jd/KVy9pRP20NMKxQfQLMseQCt66lIbLaPLItvGUcSIryE1eZZMJ7loSws6Ig3M2Elc2EreNh76w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@taplo/lib": "^0.5.0" + }, + "engines": { + "node": ">=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/unts" + }, + "peerDependencies": { + "prettier": "^3.0.3" + } + }, + "node_modules/pstree.remy": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", + "dev": true, + "license": "MIT" + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/simple-update-notifier": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", + "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/touch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz", + "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==", + "dev": true, + "license": "ISC", + "bin": { + "nodetouch": "bin/nodetouch.js" + } + }, + "node_modules/undefsafe": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", + "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", + "dev": true, + "license": "MIT" + } + } +} diff --git a/calm-calatheas/package.json b/calm-calatheas/package.json new file mode 100644 index 00000000..973ab7f8 --- /dev/null +++ b/calm-calatheas/package.json @@ -0,0 +1,10 @@ +{ + "devDependencies": { + "nodemon": "3.1.10", + "prettier": "3.6.2", + "prettier-plugin-sort-json": "4.1.1", + "prettier-plugin-toml": "2.0.6" + }, + "name": "calm-calatheas", + "private": true +} diff --git a/calm-calatheas/pyproject.toml b/calm-calatheas/pyproject.toml new file mode 100644 index 00000000..2bda1094 --- /dev/null +++ b/calm-calatheas/pyproject.toml @@ -0,0 +1,89 @@ +[project] +authors = [{ name = "esmecat" }, { name = "floncdev" }, { name = "leoluy" }, { name = "tfblunt" }, { name = "zike01" }] +description = "This is the project of the Calm Calatheas team for the Python Discord Code Jam 2025" +name = "calm-calatheas" +readme = "README.md" +requires-python = ">=3.13" +version = "0.0.0" + +dependencies = [ + "accelerate~=1.10", + "kernels~=0.9", + "pydantic~=2.11", + "pydantic-settings~=2.10", + "starlette~=0.47", + "transformers~=4.55", + "torch~=2.6", + "uvicorn~=0.35.0", +] + +[dependency-groups] +dev = [ + "bitsandbytes~=0.47", + "mkdocs~=1.6", + "mkdocs-drawio~=1.11", + "mkdocs-material~=9.6", + "mkdocstrings~=0.29", + "mkdocstrings-python~=1.16", + "playwright~=1.54", + "pre-commit~=4.2", + "pydeps~=3.0", + "pyodide-py~=0.28", + "pyright==1.1.403", + "pyscript~=0.3", + "pytest~=8.4", + "pytest-asyncio~=1.0", + "pytest-playwright~=0.7", + "pytest-sugar~=1.0", + "python-dotenv~=1.1", + "reactivex~=4.0", + "ruff~=0.12", + "taskipy~=1.14", + "testcontainers~=4.12", +] + +[tool.pyright] +exclude = [".venv"] +pythonVersion = "3.13" +reportMissingModuleSource = "none" # Pyodide "js" module is dynamic. +reportUnnecessaryTypeIgnoreComment = "error" +typeCheckingMode = "standard" +venv = ".venv" +venvPath = "." + +[tool.pytest.ini_options] +addopts = "--browser chromium --browser firefox --browser webkit --tracing retain-on-failure" +asyncio_default_fixture_loop_scope = "session" +asyncio_mode = "auto" +required_plugins = ["pytest-asyncio"] +testpaths = ["tests"] + +[tool.ruff] +line-length = 119 +target-version = "py313" + +[tool.ruff.lint] +ignore = ["D100", "D104", "D105", "D212", "D107", "FIX002", "LOG015", "PT001", "UP007", "UP045", "S311"] +select = ["ALL"] + +[tool.ruff.lint.per-file-ignores] +"**/conftest.py" = ["INP001"] +"**/test__*.py" = ["INP001", "S101", "PLR2004"] +"app/calm_calatheas/services/image_captioner.py" = ["RUF006"] +"app/frontend/components/description_dropdown.py" = ["SLF001"] +"app/main.py" = ["INP001"] +"typings/js.pyi" = ["ALL"] + +[tool.ruff.lint.pydocstyle] +convention = "google" + +[tool.setuptools] +packages = ["calm_calatheas"] + +[tool.taskipy.tasks] +build = "uv build" +build-docker = "task build && docker build . -t calm-calatheas:latest" +build-docs = "mkdocs build --strict" +docs = "mkdocs serve" +serve = "python -m calm_calatheas --log_level DEBUG" +test = "pytest" diff --git a/calm-calatheas/tests/assets/elephant.jpg b/calm-calatheas/tests/assets/elephant.jpg new file mode 100644 index 00000000..63321aeb Binary files /dev/null and b/calm-calatheas/tests/assets/elephant.jpg differ diff --git a/calm-calatheas/tests/conftest.py b/calm-calatheas/tests/conftest.py new file mode 100644 index 00000000..47a803f1 --- /dev/null +++ b/calm-calatheas/tests/conftest.py @@ -0,0 +1,49 @@ +from collections.abc import Generator +from pathlib import Path + +import pytest +from playwright.sync_api import Page, expect +from testcontainers.compose import DockerCompose + +DESCRIPTION_GENERATED_TIMEOUT_MS = 600000 +CAPTION_MODEL_LOADED_TIMEOUT_MS = 30000 +PYSCRIPT_READY_TIMEOUT_MS = 30000 + + +@pytest.fixture(scope="session") +def compose() -> Generator[DockerCompose]: + """Return a Docker Compose instance.""" + with DockerCompose(context=Path(__file__).parent.absolute(), build=True) as compose: + yield compose + + +@pytest.fixture(scope="session") +def base_url(compose: DockerCompose) -> str: + """Return the base URL for the application.""" + port = compose.get_service_port("app", 8000) + return f"http://localhost:{port}" + + +@pytest.fixture() +def app(base_url: str, page: Page) -> Page: + """Navigate to the home page, wait for PyScript load and return the page instance.""" + page.goto(base_url) + + page.wait_for_event( + event="console", + predicate=lambda event: "PyScript Ready" in event.text, + timeout=PYSCRIPT_READY_TIMEOUT_MS, + ) + + return page + + +@pytest.fixture() +def model_loaded(app: Page) -> Page: + """Wait for the caption model to be loaded.""" + notification = app.get_by_text("Loading the model for generating captions") + + expect(notification).to_be_visible() + expect(notification).not_to_be_visible(timeout=CAPTION_MODEL_LOADED_TIMEOUT_MS) + + return app diff --git a/calm-calatheas/tests/docker-compose.yaml b/calm-calatheas/tests/docker-compose.yaml new file mode 100644 index 00000000..653046d6 --- /dev/null +++ b/calm-calatheas/tests/docker-compose.yaml @@ -0,0 +1,7 @@ +name: calm-calatheas-test + +services: + app: + image: calm-calatheas:latest + ports: + - 8000 diff --git a/calm-calatheas/tests/test__calm_calatheas.py b/calm-calatheas/tests/test__calm_calatheas.py new file mode 100644 index 00000000..492fe9da --- /dev/null +++ b/calm-calatheas/tests/test__calm_calatheas.py @@ -0,0 +1,218 @@ +import re +from pathlib import Path + +import pytest +from conftest import DESCRIPTION_GENERATED_TIMEOUT_MS, PYSCRIPT_READY_TIMEOUT_MS +from playwright.sync_api import Page, expect + + +def test__main_page_has_welcome_message(app: Page) -> None: + """ + Test that the main page has a welcome message. + + Asserts: + - The welcome message is visible on the page. + """ + expect(app.get_by_text(re.compile("Welcome to your Pokedex!"))).to_be_visible() + + +def test__default_theme_is_system_preferred(app: Page) -> None: + """ + Test that the default theme is the system preferred theme. + + Asserts: + - The system preferred theme is applied by default. + """ + expect(app.locator("html")).not_to_have_attribute("data-theme", re.compile("dark|light")) + + +def test__switch_to_dark_theme(app: Page) -> None: + """ + Test that switching to the dark theme works. + + Asserts: + - The dark theme is applied when the user selects it. + """ + theme_selector = app.locator(".navbar-item", has_text="Theme") + expect(theme_selector).to_be_visible() + theme_selector.hover() + + dark_mode_selector = theme_selector.locator(".navbar-item", has_text="Dark") + expect(dark_mode_selector).to_be_visible() + dark_mode_selector.click() + + expect(app.locator("html")).to_have_attribute("data-theme", "dark") + + +def test__switch_to_light_theme(app: Page) -> None: + """ + Test that switching to the light theme works. + + Asserts: + - The light theme is applied when the user selects it. + """ + theme_selector = app.locator(".navbar-item", has_text="Theme") + expect(theme_selector).to_be_visible() + theme_selector.hover() + + light_mode_selector = theme_selector.locator(".navbar-item", has_text="Light") + expect(light_mode_selector).to_be_visible() + light_mode_selector.click() + + expect(app.locator("html")).to_have_attribute("data-theme", "light") + + +def test__switch_to_system_theme(app: Page) -> None: + """ + Test that switching to the system theme works. + + Asserts: + - The system theme is applied when the user selects it. + """ + theme_selector = app.locator(".navbar-item", has_text="Theme") + expect(theme_selector).to_be_visible() + theme_selector.hover() + + # First switch to light theme to ensure a theme is applied + dark_mode_selector = theme_selector.locator(".navbar-item", has_text="Dark") + expect(dark_mode_selector).to_be_visible() + dark_mode_selector.click() + + expect(app.locator("html")).to_have_attribute("data-theme", "dark") + + # Next switch (back) to the system theme + auto_mode_selector = theme_selector.locator(".navbar-item", has_text="Auto") + expect(auto_mode_selector).to_be_visible() + auto_mode_selector.click() + + expect(app.locator("html")).not_to_have_attribute("data-theme", re.compile("dark|light")) + + +def test__theme_is_restored_after_refresh(app: Page) -> None: + """ + Test that the selected theme is restored after a page refresh. + + Asserts: + - The theme remains consistent after refreshing the page. + """ + # Switch to dark theme + theme_selector = app.locator(".navbar-item", has_text="Theme") + expect(theme_selector).to_be_visible() + theme_selector.hover() + + dark_mode_selector = theme_selector.locator(".navbar-item", has_text="Dark") + expect(dark_mode_selector).to_be_visible() + dark_mode_selector.click() + + expect(app.locator("html")).to_have_attribute("data-theme", "dark") + + # Refresh the page + app.reload() + + app.wait_for_event( + event="console", + predicate=lambda event: "PyScript Ready" in event.text, + timeout=PYSCRIPT_READY_TIMEOUT_MS, + ) + + # Check that the dark theme is still applied + expect(app.locator("html")).to_have_attribute("data-theme", "dark") + + +@pytest.mark.parametrize( + "path", + [ + Path(__file__).parent / "assets/elephant.jpg", + ], +) +def test__description_is_generated_after_uploading_an_image(model_loaded: Page, path: Path) -> None: + """ + Test that a description is generated after uploading an image through the file input. + + Asserts: + - The description is generated and displayed after the image is uploaded. + - A placeholder is shown while the description is being generated. + """ + model_loaded.locator("input[type='file']").set_input_files(path) + + # Expect a placeholder to appear while the description is being generated + placeholder = model_loaded.locator(".pokemon-description", has=model_loaded.locator(".is-skeleton")) + expect(placeholder).to_be_visible() + + # Wait for the description to be generated + description = model_loaded.locator(".pokemon-description", has_not=model_loaded.locator(".is-skeleton")) + expect(description).to_be_visible(timeout=DESCRIPTION_GENERATED_TIMEOUT_MS) + + +@pytest.mark.parametrize( + "path", + [ + Path(__file__).parent / "assets/elephant.jpg", + ], +) +def test__description_is_still_available_after_refresh(model_loaded: Page, path: Path) -> None: + """ + Test that the generated description is still available after refreshing the page. + + Asserts: + - The description is still visible after a page refresh. + """ + model_loaded.locator("input[type='file']").set_input_files(path) + + # Expect a placeholder to appear while the description is being generated + placeholder = model_loaded.locator(".pokemon-description", has=model_loaded.locator(".is-skeleton")) + expect(placeholder).to_be_visible() + + # Wait for the description to be generated + description = model_loaded.locator(".pokemon-description", has_not=model_loaded.locator(".is-skeleton")) + expect(description).to_be_visible(timeout=DESCRIPTION_GENERATED_TIMEOUT_MS) + + # Refresh the page + model_loaded.reload() + + model_loaded.wait_for_event( + event="console", + predicate=lambda event: "PyScript Ready" in event.text, + timeout=PYSCRIPT_READY_TIMEOUT_MS, + ) + + # Check that the description is still visible + after_refresh = model_loaded.locator(".pokemon-description", has_not=model_loaded.locator(".is-skeleton")) + expect(after_refresh).to_be_visible() + + +@pytest.mark.parametrize( + "path", + [ + Path(__file__).parent / "assets/elephant.jpg", + ], +) +def test__description_can_be_deleted(model_loaded: Page, path: Path) -> None: + """ + Test that the generated description can be deleted. + + Asserts: + - The description is removed from the DOM after deletion. + """ + model_loaded.locator("input[type='file']").set_input_files(path) + + # Expect a placeholder to appear while the description is being generated + placeholder = model_loaded.locator(".pokemon-description", has=model_loaded.locator(".is-skeleton")) + expect(placeholder).to_be_visible() + + # Wait for the description to be generated + description = model_loaded.locator(".pokemon-description", has_not=model_loaded.locator(".is-skeleton")) + expect(description).to_be_visible(timeout=DESCRIPTION_GENERATED_TIMEOUT_MS) + + # Open the context menu + context_menu = description.locator(".dropdown") + expect(context_menu).to_be_visible() + context_menu.hover() + + # Delete the description + delete_button = context_menu.locator("button", has_text="Delete") + expect(delete_button).to_be_visible() + delete_button.click() + + # Check that the description is removed + expect(description).not_to_be_visible() diff --git a/calm-calatheas/typings/js.pyi b/calm-calatheas/typings/js.pyi new file mode 100644 index 00000000..e7c68ba8 --- /dev/null +++ b/calm-calatheas/typings/js.pyi @@ -0,0 +1,323 @@ +# ruff: noqa +# Pyodide already has a js.pyi but it is not complete for us, +# and is not even able to be used. + +# Use https://developer.mozilla.org/en-US/docs/Web/API as reference. + +from collections.abc import Callable, Iterable +from typing import Any, Coroutine, Generic, Literal, Sequence, TypeAlias, TypeVar, overload, Self + +from _pyodide._core_docs import _JsProxyMetaClass +from pyodide.ffi import JsArray, JsDomElement as OldJSDomElement, JsException, JsFetchResponse, JsProxy, JsTypedArray +from pyodide.webloop import PyodideFuture + +class JsDomElement(OldJSDomElement): + classList: DOMTokenList + innerText: str + + @property + def children(self) -> Sequence[JsDomElement]: ... + def getAttribute(self, name: str) -> str: ... + def setAttribute(self, name: str, value: str) -> None: ... + def closest(self, selectors: str) -> JsDomElement: ... + def hasAttribute(self, attrName: str) -> bool: ... + def removeAttribute(self, attrName: str) -> None: ... + def getBoundingClientRect(self) -> DOMRect: ... + # These are on Node, which an Element is. + # As far as this project is concerned, they are just elements. + firstChild: JsDomElement | None + nextSibling: JsDomElement | None + def removeChild(self, child: JsDomElement) -> None: ... + def insertBefore(self, newChild: JsDomElement, refChild: JsDomElement) -> None: ... + def contains(self, otherNode: JsDomElement) -> bool: ... + +class JsButtonElement(JsDomElement): + def click(self) -> None: ... + +class JsVideoElement(JsDomElement): + autoplay: bool + controls: bool + src: str + srcObject: MediaStream | None + videoHeight: int + videoWidth: int + def play(self) -> PyodideFuture[None]: ... + +class JsImgElement(JsDomElement): + src: str + width: int + height: int + naturalWidth: int + naturalHeight: int + +class JsInputElement(JsDomElement): + value: str + def click(self) -> None: ... + +class JsFileInputElement(JsInputElement): + files: FileList + +class JsCanvasElement(JsDomElement): + width: int + height: int + @overload + def getContext(self, contextId: Literal["2d"]) -> CanvasRenderingContext2D: ... + @overload + def getContext(self, contextId: str) -> JsProxy: ... + def getContext(self, contextId: str) -> JsProxy: ... + def toBlob(self, callback: Callable[[Blob], None], mimeType: str) -> None: ... + +class JsAnchorElement(JsDomElement): + href: str + target: str + download: str + def click(self) -> None: ... + +class DOMRect(JsProxy): + top: float + left: float + bottom: float + right: float + width: float + height: float + +class CanvasRenderingContext2D(JsProxy): + canvas: JsCanvasElement + @overload + def drawImage(self, image: JsImgElement | JsVideoElement, dx: int, dy: int) -> None: ... + @overload + def drawImage(self, image: JsImgElement | JsVideoElement, dx: int, dy: int, dWidth: int, dHeight: int) -> None: ... + def drawImage( + self, image: JsImgElement | JsVideoElement, dx: int, dy: int, dWidth: int = ..., dHeight: int = ... + ) -> None: ... + def translate(self, x: int | float, y: int | float) -> None: ... + def rotate(self, angle: int | float) -> None: ... + +class DOMTokenList(JsProxy): + def add(self, token: str) -> None: ... + def remove(self, *tokens: str) -> None: ... + def contains(self, token: str) -> bool: ... + +class CSSStyleDeclaration(JsProxy): + def removeProperty(self, name: str) -> None: ... + def setProperty(self, name: str, value: str) -> None: ... + def __setattr__(self, name: str, value: str) -> None: ... + +ElementT = TypeVar("ElementT", bound=JsDomElement) + +class LoadEvent(JsProxy, Generic[ElementT]): + target: ElementT + +class Event(JsProxy): + target: JsDomElement + def preventDefault(self) -> None: ... + +class UIEvent(Event): ... + +class MouseEvent(Event): + relatedTarget: JsDomElement + pageX: float + pageY: float + +class DragEvent(MouseEvent): + dataTransfer: DataTransfer + +class DataTransfer(JsProxy): + dropEffect: str + def setData(self, format: str, data: str) -> None: ... + def setDragImage(self, image: JsDomElement, x: int, y: int) -> None: ... + def getData(self, format: str) -> str: ... + +class FileList: + length: int + def item(self, index: int) -> File: ... + +class File(Blob): + name: str + +class FileReader(JsProxy): + result: str + onload: Callable[[Event], Any] + onerror: Callable[[Event], Any] + @classmethod + def new(cls) -> Self: ... + def readAsDataURL(self, file: File) -> None: ... + +def eval(code: str) -> Any: ... + +# in browser the cancellation token is an int, in node it's a special opaque +# object. +_CancellationToken: TypeAlias = int | JsProxy + +def setTimeout(cb: Callable[[], Any], timeout: int | float) -> _CancellationToken: ... +def clearTimeout(id: _CancellationToken) -> None: ... +def setInterval(cb: Callable[[], Any], interval: int | float) -> _CancellationToken: ... +def clearInterval(id: _CancellationToken) -> None: ... +def fetch( + url: str, + options: JsProxy | None = None, +) -> PyodideFuture[JsFetchResponse]: ... + +indexedDB: Any = ... +type IDBDatabase = Any +localStorage: LocalStorage = ... +sessionStorage: SessionStorage = ... +console: Console = ... +self: Any = ... +window: Any = ... + +class DOMParser(JsProxy): + def parseFromString(self, string: str, type: str) -> document: ... + +class LocalStorage: + def getItem(self, key: str) -> str | None: ... + def setItem(self, key: str, value: str) -> None: ... + def removeItem(self, key: str) -> None: ... + def clear(self) -> None: ... + +class SessionStorage: + def getItem(self, key: str) -> str | None: ... + def setItem(self, key: str, value: str) -> None: ... + def removeItem(self, key: str) -> None: ... + def clear(self) -> None: ... + +class Console: + def log(self, *values: object) -> None: ... + def error(self, *values: object) -> None: ... + +# Shenanigans to convince skeptical type system to behave correctly: +# +# These classes we are declaring are actually JavaScript objects, so the class +# objects themselves need to be instances of JsProxy. So their type needs to +# subclass JsProxy. We do this with a custom metaclass. + +class _JsMeta(_JsProxyMetaClass, JsProxy): ... +class _JsObject(metaclass=_JsMeta): ... + +class XMLHttpRequest(_JsObject): + response: str + + @staticmethod + def new() -> XMLHttpRequest: ... + def open(self, method: str, url: str, sync: bool) -> None: ... + def send(self, body: JsProxy | None = None) -> None: ... + +class Object(_JsObject): + @staticmethod + def fromEntries(it: Iterable[JsArray[Any]]) -> JsProxy: ... + +class Array(_JsObject): + @staticmethod + def new() -> JsArray[Any]: ... + +class ImageData(_JsObject): + @staticmethod + def new(width: int, height: int, settings: JsProxy | None = None) -> ImageData: ... + + width: int + height: int + +class _TypedArray(_JsObject): + @staticmethod + def new( + a: int | Iterable[int | float] | JsProxy | None, + byteOffset: int = 0, + length: int = 0, + ) -> JsTypedArray: ... + +class Uint8Array(_TypedArray): + BYTES_PER_ELEMENT = 1 + +class Float64Array(_TypedArray): + BYTES_PER_ELEMENT = 8 + +class JSON(_JsObject): + @staticmethod + def stringify(a: JsProxy) -> str: ... + @staticmethod + def parse(a: str) -> JsProxy: ... + +class document(_JsObject): + body: JsDomElement + children: list[JsDomElement] + @staticmethod + def getElementById(id: str) -> JsDomElement: ... + @overload + @staticmethod + def createElement(tagName: Literal["a"]) -> JsAnchorElement: ... + @overload + @staticmethod + def createElement(tagName: Literal["img"]) -> JsImgElement: ... + @overload + @staticmethod + def createElement(tagName: Literal["canvas"]) -> JsCanvasElement: ... + @overload + @staticmethod + def createElement(tagName: str) -> JsDomElement: ... + @staticmethod + def createElement(tagName: str) -> JsDomElement: ... + @staticmethod + def appendChild(child: JsDomElement) -> None: ... + +class navigator(_JsObject): + mediaDevices: MediaDevices + +class MediaDevices(_JsObject): + def enumerateDevices(self) -> PyodideFuture[JsArray[MediaDeviceInfo]]: ... + def getUserMedia(self, constraints: dict[str, Any]) -> PyodideFuture[MediaStream]: ... + +class MediaDeviceInfo(_JsObject): + @property + def deviceId(self) -> str: ... + @property + def kind(self) -> str: ... + @property + def label(self) -> str: ... + +class MediaStream(JsProxy): + @property + def active(self) -> bool: ... + @property + def id(self) -> str: ... + def addTrack(self, track: MediaStreamTrack) -> None: ... + def clone(self) -> MediaStream: ... + def getAudioTracks(self) -> JsArray[MediaStreamTrack]: ... + def getTrackById(self, id: str) -> MediaStreamTrack | None: ... + def getTracks(self) -> JsArray[MediaStreamTrack]: ... + def getVideoTracks(self) -> JsArray[MediaStreamTrack]: ... + def removeTrack(self, track: MediaStreamTrack) -> None: ... + +class MediaStreamTrack(_JsObject): + @property + def id(self) -> str: ... + @property + def kind(self) -> str: ... + @property + def label(self) -> str: ... + @property + def muted(self) -> bool: ... + @property + def readyState(self) -> str: ... + def stop(self) -> None: ... + +class ImageCapture(JsProxy): + def takePhoto(self) -> PyodideFuture[Blob]: ... + +class URL(_JsObject): + @staticmethod + def createObjectURL(blob: Blob) -> str: ... + @staticmethod + def revokeObjectURL(url: str) -> None: ... + +class DOMException(JsException): ... + +type Blob = Any + +# Transformers.js + +class _Pipeline(JsProxy): + def __call__( + self, action: str, model: str, options: dict + ) -> Coroutine[None, None, Callable[[Any], Coroutine[None, None, Any]]]: ... + +pipeline: _Pipeline = ... diff --git a/calm-calatheas/typings/pyscript.pyi b/calm-calatheas/typings/pyscript.pyi new file mode 100644 index 00000000..66689652 --- /dev/null +++ b/calm-calatheas/typings/pyscript.pyi @@ -0,0 +1,12 @@ +from collections.abc import Awaitable +from typing import Any + +sync: Any = ... +window: Any = ... +workers: dict[str, Awaitable[PyWorker]] = ... + +class PyWorker: + sync: Any = ... + def __init__(self, file: str) -> None: ... + @property + async def ready(self) -> bool: ... diff --git a/calm-calatheas/typings/transformers_js.pyi b/calm-calatheas/typings/transformers_js.pyi new file mode 100644 index 00000000..d9db66fb --- /dev/null +++ b/calm-calatheas/typings/transformers_js.pyi @@ -0,0 +1,5 @@ +class ModelOutputItem: + generated_text: str + +class ModelOutput: + def at(self, index: int) -> ModelOutputItem: ... diff --git a/calm-calatheas/uv.lock b/calm-calatheas/uv.lock new file mode 100644 index 00000000..36cce27b --- /dev/null +++ b/calm-calatheas/uv.lock @@ -0,0 +1,1623 @@ +requires-python = ">=3.13" +revision = 3 +version = 1 + +[[package]] +dependencies = [ + { name = "huggingface-hub" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "psutil" }, + { name = "pyyaml" }, + { name = "safetensors" }, + { name = "torch" }, +] +name = "accelerate" +sdist = { url = "https://files.pythonhosted.org/packages/f7/66/be171836d86dc5b8698b3a9bf4b9eb10cb53369729939f88bf650167588b/accelerate-1.10.0.tar.gz", hash = "sha256:8270568fda9036b5cccdc09703fef47872abccd56eb5f6d53b54ea5fb7581496", size = 392261, upload-time = "2025-08-07T10:54:51.664Z" } +source = { registry = "https://pypi.org/simple" } +version = "1.10.0" +wheels = [ + { url = "https://files.pythonhosted.org/packages/30/dd/0107f0aa179869ee9f47ef5a2686abd5e022fdc82af901d535e52fe91ce1/accelerate-1.10.0-py3-none-any.whl", hash = "sha256:260a72b560e100e839b517a331ec85ed495b3889d12886e79d1913071993c5a3", size = 374718, upload-time = "2025-08-07T10:54:49.988Z" }, +] + +[[package]] +name = "annotated-types" +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +source = { registry = "https://pypi.org/simple" } +version = "0.7.0" +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, +] + +[[package]] +dependencies = [{ name = "idna" }, { name = "sniffio" }] +name = "anyio" +sdist = { url = "https://files.pythonhosted.org/packages/f1/b4/636b3b65173d3ce9a38ef5f0522789614e590dab6a8d505340a4efe4c567/anyio-4.10.0.tar.gz", hash = "sha256:3f3fae35c96039744587aa5b8371e7e8e603c0702999535961dd336026973ba6", size = 213252, upload-time = "2025-08-04T08:54:26.451Z" } +source = { registry = "https://pypi.org/simple" } +version = "4.10.0" +wheels = [ + { url = "https://files.pythonhosted.org/packages/6f/12/e5e0282d673bb9746bacfb6e2dba8719989d3660cdb2ea79aee9a9651afb/anyio-4.10.0-py3-none-any.whl", hash = "sha256:60e474ac86736bbfd6f210f7a61218939c318f43f9972497381f1c5e930ed3d1", size = 107213, upload-time = "2025-08-04T08:54:24.882Z" }, +] + +[[package]] +name = "babel" +sdist = { url = "https://files.pythonhosted.org/packages/7d/6b/d52e42361e1aa00709585ecc30b3f9684b3ab62530771402248b1b1d6240/babel-2.17.0.tar.gz", hash = "sha256:0c54cffb19f690cdcc52a3b50bcbf71e07a808d1c80d549f2459b9d2cf0afb9d", size = 9951852, upload-time = "2025-02-01T15:17:41.026Z" } +source = { registry = "https://pypi.org/simple" } +version = "2.17.0" +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/b8/3fe70c75fe32afc4bb507f75563d39bc5642255d1d94f1f23604725780bf/babel-2.17.0-py3-none-any.whl", hash = "sha256:4d0b53093fdfb4b21c92b5213dba5a1b23885afa8383709427046b21c366e5f2", size = 10182537, upload-time = "2025-02-01T15:17:37.39Z" }, +] + +[[package]] +name = "backrefs" +sdist = { url = "https://files.pythonhosted.org/packages/eb/a7/312f673df6a79003279e1f55619abbe7daebbb87c17c976ddc0345c04c7b/backrefs-5.9.tar.gz", hash = "sha256:808548cb708d66b82ee231f962cb36faaf4f2baab032f2fbb783e9c2fdddaa59", size = 5765857, upload-time = "2025-06-22T19:34:13.97Z" } +source = { registry = "https://pypi.org/simple" } +version = "5.9" +wheels = [ + { url = "https://files.pythonhosted.org/packages/19/4d/798dc1f30468134906575156c089c492cf79b5a5fd373f07fe26c4d046bf/backrefs-5.9-py310-none-any.whl", hash = "sha256:db8e8ba0e9de81fcd635f440deab5ae5f2591b54ac1ebe0550a2ca063488cd9f", size = 380267, upload-time = "2025-06-22T19:34:05.252Z" }, + { url = "https://files.pythonhosted.org/packages/55/07/f0b3375bf0d06014e9787797e6b7cc02b38ac9ff9726ccfe834d94e9991e/backrefs-5.9-py311-none-any.whl", hash = "sha256:6907635edebbe9b2dc3de3a2befff44d74f30a4562adbb8b36f21252ea19c5cf", size = 392072, upload-time = "2025-06-22T19:34:06.743Z" }, + { url = "https://files.pythonhosted.org/packages/9d/12/4f345407259dd60a0997107758ba3f221cf89a9b5a0f8ed5b961aef97253/backrefs-5.9-py312-none-any.whl", hash = "sha256:7fdf9771f63e6028d7fee7e0c497c81abda597ea45d6b8f89e8ad76994f5befa", size = 397947, upload-time = "2025-06-22T19:34:08.172Z" }, + { url = "https://files.pythonhosted.org/packages/10/bf/fa31834dc27a7f05e5290eae47c82690edc3a7b37d58f7fb35a1bdbf355b/backrefs-5.9-py313-none-any.whl", hash = "sha256:cc37b19fa219e93ff825ed1fed8879e47b4d89aa7a1884860e2db64ccd7c676b", size = 399843, upload-time = "2025-06-22T19:34:09.68Z" }, + { url = "https://files.pythonhosted.org/packages/fc/24/b29af34b2c9c41645a9f4ff117bae860291780d73880f449e0b5d948c070/backrefs-5.9-py314-none-any.whl", hash = "sha256:df5e169836cc8acb5e440ebae9aad4bf9d15e226d3bad049cf3f6a5c20cc8dc9", size = 411762, upload-time = "2025-06-22T19:34:11.037Z" }, + { url = "https://files.pythonhosted.org/packages/41/ff/392bff89415399a979be4a65357a41d92729ae8580a66073d8ec8d810f98/backrefs-5.9-py39-none-any.whl", hash = "sha256:f48ee18f6252b8f5777a22a00a09a85de0ca931658f1dd96d4406a34f3748c60", size = 380265, upload-time = "2025-06-22T19:34:12.405Z" }, +] + +[[package]] +dependencies = [{ name = "soupsieve" }, { name = "typing-extensions" }] +name = "beautifulsoup4" +sdist = { url = "https://files.pythonhosted.org/packages/d8/e4/0c4c39e18fd76d6a628d4dd8da40543d136ce2d1752bd6eeeab0791f4d6b/beautifulsoup4-4.13.4.tar.gz", hash = "sha256:dbb3c4e1ceae6aefebdaf2423247260cd062430a410e38c66f2baa50a8437195", size = 621067, upload-time = "2025-04-15T17:05:13.836Z" } +source = { registry = "https://pypi.org/simple" } +version = "4.13.4" +wheels = [ + { url = "https://files.pythonhosted.org/packages/50/cd/30110dc0ffcf3b131156077b90e9f60ed75711223f306da4db08eff8403b/beautifulsoup4-4.13.4-py3-none-any.whl", hash = "sha256:9bbbb14bfde9d79f38b8cd5f8c7c85f4b8f2523190ebed90e950a8dea4cb1c4b", size = 187285, upload-time = "2025-04-15T17:05:12.221Z" }, +] + +[[package]] +dependencies = [{ name = "numpy" }, { name = "torch" }] +name = "bitsandbytes" +source = { registry = "https://pypi.org/simple" } +version = "0.47.0" +wheels = [ + { url = "https://files.pythonhosted.org/packages/aa/eb/477d6b5602f469c7305fd43eec71d890c39909f615c1d7138f6e7d226eff/bitsandbytes-0.47.0-py3-none-manylinux_2_24_aarch64.whl", hash = "sha256:2f805b76891a596025e9e13318b675d08481b9ee650d65e5d2f9d844084c6521", size = 30004641, upload-time = "2025-08-11T18:51:20.524Z" }, + { url = "https://files.pythonhosted.org/packages/9c/40/91f1a5a694f434bc13cba160045fdc4e867032e627b001bf411048fefd9c/bitsandbytes-0.47.0-py3-none-manylinux_2_24_x86_64.whl", hash = "sha256:68f3fffd494a47ed1fd7593bfc5dd2ac69b68260599b71b4c4b3a32f90f3b184", size = 61284639, upload-time = "2025-08-11T18:51:23.581Z" }, + { url = "https://files.pythonhosted.org/packages/18/a9/e07a227f1cd6562844cea2f05ee576b0991a9a91f45965c06034178ba0f6/bitsandbytes-0.47.0-py3-none-win_amd64.whl", hash = "sha256:4880a6d42ca9628b5a571c8cc3093dc3f5f52511e5a9e47d52d569807975531a", size = 60725121, upload-time = "2025-08-11T18:51:27.543Z" }, +] + +[[package]] +dependencies = [ + { name = "accelerate" }, + { name = "kernels" }, + { name = "pydantic" }, + { name = "pydantic-settings" }, + { name = "starlette" }, + { name = "torch" }, + { name = "transformers" }, + { name = "uvicorn" }, +] +name = "calm-calatheas" +source = { virtual = "." } +version = "0.0.0" + +[package.dev-dependencies] +dev = [ + { name = "bitsandbytes" }, + { name = "mkdocs" }, + { name = "mkdocs-drawio" }, + { name = "mkdocs-material" }, + { name = "mkdocstrings" }, + { name = "mkdocstrings-python" }, + { name = "playwright" }, + { name = "pre-commit" }, + { name = "pydeps" }, + { name = "pyodide-py" }, + { name = "pyright" }, + { name = "pyscript" }, + { name = "pytest" }, + { name = "pytest-asyncio" }, + { name = "pytest-playwright" }, + { name = "pytest-sugar" }, + { name = "python-dotenv" }, + { name = "reactivex" }, + { name = "ruff" }, + { name = "taskipy" }, + { name = "testcontainers" }, +] + +[package.metadata] +requires-dist = [ + { name = "accelerate", specifier = "~=1.10" }, + { name = "kernels", specifier = "~=0.9" }, + { name = "pydantic", specifier = "~=2.11" }, + { name = "pydantic-settings", specifier = "~=2.10" }, + { name = "starlette", specifier = "~=0.47" }, + { name = "torch", specifier = "~=2.6" }, + { name = "transformers", specifier = "~=4.55" }, + { name = "uvicorn", specifier = "~=0.35.0" }, +] + +[package.metadata.requires-dev] +dev = [ + { name = "bitsandbytes", specifier = "~=0.47" }, + { name = "mkdocs", specifier = "~=1.6" }, + { name = "mkdocs-drawio", specifier = "~=1.11" }, + { name = "mkdocs-material", specifier = "~=9.6" }, + { name = "mkdocstrings", specifier = "~=0.29" }, + { name = "mkdocstrings-python", specifier = "~=1.16" }, + { name = "playwright", specifier = "~=1.54" }, + { name = "pre-commit", specifier = "~=4.2" }, + { name = "pydeps", specifier = "~=3.0" }, + { name = "pyodide-py", specifier = "~=0.28" }, + { name = "pyright", specifier = "==1.1.403" }, + { name = "pyscript", specifier = "~=0.3" }, + { name = "pytest", specifier = "~=8.4" }, + { name = "pytest-asyncio", specifier = "~=1.0" }, + { name = "pytest-playwright", specifier = "~=0.7" }, + { name = "pytest-sugar", specifier = "~=1.0" }, + { name = "python-dotenv", specifier = "~=1.1" }, + { name = "reactivex", specifier = "~=4.0" }, + { name = "ruff", specifier = "~=0.12" }, + { name = "taskipy", specifier = "~=1.14" }, + { name = "testcontainers", specifier = "~=4.12" }, +] + +[[package]] +name = "certifi" +sdist = { url = "https://files.pythonhosted.org/packages/dc/67/960ebe6bf230a96cda2e0abcf73af550ec4f090005363542f0765df162e0/certifi-2025.8.3.tar.gz", hash = "sha256:e564105f78ded564e3ae7c923924435e1daa7463faeab5bb932bc53ffae63407", size = 162386, upload-time = "2025-08-03T03:07:47.08Z" } +source = { registry = "https://pypi.org/simple" } +version = "2025.8.3" +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/48/1549795ba7742c948d2ad169c1c8cdbae65bc450d6cd753d124b17c8cd32/certifi-2025.8.3-py3-none-any.whl", hash = "sha256:f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5", size = 161216, upload-time = "2025-08-03T03:07:45.777Z" }, +] + +[[package]] +name = "cfgv" +sdist = { url = "https://files.pythonhosted.org/packages/11/74/539e56497d9bd1d484fd863dd69cbbfa653cd2aa27abfe35653494d85e94/cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560", size = 7114, upload-time = "2023-08-12T20:38:17.776Z" } +source = { registry = "https://pypi.org/simple" } +version = "3.4.0" +wheels = [ + { url = "https://files.pythonhosted.org/packages/c5/55/51844dd50c4fc7a33b653bfaba4c2456f06955289ca770a5dbd5fd267374/cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9", size = 7249, upload-time = "2023-08-12T20:38:16.269Z" }, +] + +[[package]] +name = "charset-normalizer" +sdist = { url = "https://files.pythonhosted.org/packages/83/2d/5fd176ceb9b2fc619e63405525573493ca23441330fcdaee6bef9460e924/charset_normalizer-3.4.3.tar.gz", hash = "sha256:6fce4b8500244f6fcb71465d4a4930d132ba9ab8e71a7859e6a5d59851068d14", size = 122371, upload-time = "2025-08-09T07:57:28.46Z" } +source = { registry = "https://pypi.org/simple" } +version = "3.4.3" +wheels = [ + { url = "https://files.pythonhosted.org/packages/65/ca/2135ac97709b400c7654b4b764daf5c5567c2da45a30cdd20f9eefe2d658/charset_normalizer-3.4.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:14c2a87c65b351109f6abfc424cab3927b3bdece6f706e4d12faaf3d52ee5efe", size = 205326, upload-time = "2025-08-09T07:56:24.721Z" }, + { url = "https://files.pythonhosted.org/packages/71/11/98a04c3c97dd34e49c7d247083af03645ca3730809a5509443f3c37f7c99/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:41d1fc408ff5fdfb910200ec0e74abc40387bccb3252f3f27c0676731df2b2c8", size = 146008, upload-time = "2025-08-09T07:56:26.004Z" }, + { url = "https://files.pythonhosted.org/packages/60/f5/4659a4cb3c4ec146bec80c32d8bb16033752574c20b1252ee842a95d1a1e/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1bb60174149316da1c35fa5233681f7c0f9f514509b8e399ab70fea5f17e45c9", size = 159196, upload-time = "2025-08-09T07:56:27.25Z" }, + { url = "https://files.pythonhosted.org/packages/86/9e/f552f7a00611f168b9a5865a1414179b2c6de8235a4fa40189f6f79a1753/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:30d006f98569de3459c2fc1f2acde170b7b2bd265dc1943e87e1a4efe1b67c31", size = 156819, upload-time = "2025-08-09T07:56:28.515Z" }, + { url = "https://files.pythonhosted.org/packages/7e/95/42aa2156235cbc8fa61208aded06ef46111c4d3f0de233107b3f38631803/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:416175faf02e4b0810f1f38bcb54682878a4af94059a1cd63b8747244420801f", size = 151350, upload-time = "2025-08-09T07:56:29.716Z" }, + { url = "https://files.pythonhosted.org/packages/c2/a9/3865b02c56f300a6f94fc631ef54f0a8a29da74fb45a773dfd3dcd380af7/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6aab0f181c486f973bc7262a97f5aca3ee7e1437011ef0c2ec04b5a11d16c927", size = 148644, upload-time = "2025-08-09T07:56:30.984Z" }, + { url = "https://files.pythonhosted.org/packages/77/d9/cbcf1a2a5c7d7856f11e7ac2d782aec12bdfea60d104e60e0aa1c97849dc/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:fdabf8315679312cfa71302f9bd509ded4f2f263fb5b765cf1433b39106c3cc9", size = 160468, upload-time = "2025-08-09T07:56:32.252Z" }, + { url = "https://files.pythonhosted.org/packages/f6/42/6f45efee8697b89fda4d50580f292b8f7f9306cb2971d4b53f8914e4d890/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:bd28b817ea8c70215401f657edef3a8aa83c29d447fb0b622c35403780ba11d5", size = 158187, upload-time = "2025-08-09T07:56:33.481Z" }, + { url = "https://files.pythonhosted.org/packages/70/99/f1c3bdcfaa9c45b3ce96f70b14f070411366fa19549c1d4832c935d8e2c3/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:18343b2d246dc6761a249ba1fb13f9ee9a2bcd95decc767319506056ea4ad4dc", size = 152699, upload-time = "2025-08-09T07:56:34.739Z" }, + { url = "https://files.pythonhosted.org/packages/a3/ad/b0081f2f99a4b194bcbb1934ef3b12aa4d9702ced80a37026b7607c72e58/charset_normalizer-3.4.3-cp313-cp313-win32.whl", hash = "sha256:6fb70de56f1859a3f71261cbe41005f56a7842cc348d3aeb26237560bfa5e0ce", size = 99580, upload-time = "2025-08-09T07:56:35.981Z" }, + { url = "https://files.pythonhosted.org/packages/9a/8f/ae790790c7b64f925e5c953b924aaa42a243fb778fed9e41f147b2a5715a/charset_normalizer-3.4.3-cp313-cp313-win_amd64.whl", hash = "sha256:cf1ebb7d78e1ad8ec2a8c4732c7be2e736f6e5123a4146c5b89c9d1f585f8cef", size = 107366, upload-time = "2025-08-09T07:56:37.339Z" }, + { url = "https://files.pythonhosted.org/packages/8e/91/b5a06ad970ddc7a0e513112d40113e834638f4ca1120eb727a249fb2715e/charset_normalizer-3.4.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3cd35b7e8aedeb9e34c41385fda4f73ba609e561faedfae0a9e75e44ac558a15", size = 204342, upload-time = "2025-08-09T07:56:38.687Z" }, + { url = "https://files.pythonhosted.org/packages/ce/ec/1edc30a377f0a02689342f214455c3f6c2fbedd896a1d2f856c002fc3062/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b89bc04de1d83006373429975f8ef9e7932534b8cc9ca582e4db7d20d91816db", size = 145995, upload-time = "2025-08-09T07:56:40.048Z" }, + { url = "https://files.pythonhosted.org/packages/17/e5/5e67ab85e6d22b04641acb5399c8684f4d37caf7558a53859f0283a650e9/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2001a39612b241dae17b4687898843f254f8748b796a2e16f1051a17078d991d", size = 158640, upload-time = "2025-08-09T07:56:41.311Z" }, + { url = "https://files.pythonhosted.org/packages/f1/e5/38421987f6c697ee3722981289d554957c4be652f963d71c5e46a262e135/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8dcfc373f888e4fb39a7bc57e93e3b845e7f462dacc008d9749568b1c4ece096", size = 156636, upload-time = "2025-08-09T07:56:43.195Z" }, + { url = "https://files.pythonhosted.org/packages/a0/e4/5a075de8daa3ec0745a9a3b54467e0c2967daaaf2cec04c845f73493e9a1/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18b97b8404387b96cdbd30ad660f6407799126d26a39ca65729162fd810a99aa", size = 150939, upload-time = "2025-08-09T07:56:44.819Z" }, + { url = "https://files.pythonhosted.org/packages/02/f7/3611b32318b30974131db62b4043f335861d4d9b49adc6d57c1149cc49d4/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ccf600859c183d70eb47e05a44cd80a4ce77394d1ac0f79dbd2dd90a69a3a049", size = 148580, upload-time = "2025-08-09T07:56:46.684Z" }, + { url = "https://files.pythonhosted.org/packages/7e/61/19b36f4bd67f2793ab6a99b979b4e4f3d8fc754cbdffb805335df4337126/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:53cd68b185d98dde4ad8990e56a58dea83a4162161b1ea9272e5c9182ce415e0", size = 159870, upload-time = "2025-08-09T07:56:47.941Z" }, + { url = "https://files.pythonhosted.org/packages/06/57/84722eefdd338c04cf3030ada66889298eaedf3e7a30a624201e0cbe424a/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:30a96e1e1f865f78b030d65241c1ee850cdf422d869e9028e2fc1d5e4db73b92", size = 157797, upload-time = "2025-08-09T07:56:49.756Z" }, + { url = "https://files.pythonhosted.org/packages/72/2a/aff5dd112b2f14bcc3462c312dce5445806bfc8ab3a7328555da95330e4b/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d716a916938e03231e86e43782ca7878fb602a125a91e7acb8b5112e2e96ac16", size = 152224, upload-time = "2025-08-09T07:56:51.369Z" }, + { url = "https://files.pythonhosted.org/packages/b7/8c/9839225320046ed279c6e839d51f028342eb77c91c89b8ef2549f951f3ec/charset_normalizer-3.4.3-cp314-cp314-win32.whl", hash = "sha256:c6dbd0ccdda3a2ba7c2ecd9d77b37f3b5831687d8dc1b6ca5f56a4880cc7b7ce", size = 100086, upload-time = "2025-08-09T07:56:52.722Z" }, + { url = "https://files.pythonhosted.org/packages/ee/7a/36fbcf646e41f710ce0a563c1c9a343c6edf9be80786edeb15b6f62e17db/charset_normalizer-3.4.3-cp314-cp314-win_amd64.whl", hash = "sha256:73dc19b562516fc9bcf6e5d6e596df0b4eb98d87e4f79f3ae71840e6ed21361c", size = 107400, upload-time = "2025-08-09T07:56:55.172Z" }, + { url = "https://files.pythonhosted.org/packages/8a/1f/f041989e93b001bc4e44bb1669ccdcf54d3f00e628229a85b08d330615c5/charset_normalizer-3.4.3-py3-none-any.whl", hash = "sha256:ce571ab16d890d23b5c278547ba694193a45011ff86a9162a71307ed9f86759a", size = 53175, upload-time = "2025-08-09T07:57:26.864Z" }, +] + +[[package]] +dependencies = [{ name = "colorama", marker = "sys_platform == 'win32'" }] +name = "click" +sdist = { url = "https://files.pythonhosted.org/packages/60/6c/8ca2efa64cf75a977a0d7fac081354553ebe483345c734fb6b6515d96bbc/click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202", size = 286342, upload-time = "2025-05-20T23:19:49.832Z" } +source = { registry = "https://pypi.org/simple" } +version = "8.2.1" +wheels = [ + { url = "https://files.pythonhosted.org/packages/85/32/10bb5764d90a8eee674e9dc6f4db6a0ab47c8c4d0d83c27f7c39ac415a4d/click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b", size = 102215, upload-time = "2025-05-20T23:19:47.796Z" }, +] + +[[package]] +name = "colorama" +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" } +source = { registry = "https://pypi.org/simple" } +version = "0.4.6" +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 = "distlib" +sdist = { url = "https://files.pythonhosted.org/packages/96/8e/709914eb2b5749865801041647dc7f4e6d00b549cfe88b65ca192995f07c/distlib-0.4.0.tar.gz", hash = "sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d", size = 614605, upload-time = "2025-07-17T16:52:00.465Z" } +source = { registry = "https://pypi.org/simple" } +version = "0.4.0" +wheels = [ + { url = "https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16", size = 469047, upload-time = "2025-07-17T16:51:58.613Z" }, +] + +[[package]] +dependencies = [{ name = "pywin32", marker = "sys_platform == 'win32'" }, { name = "requests" }, { name = "urllib3" }] +name = "docker" +sdist = { url = "https://files.pythonhosted.org/packages/91/9b/4a2ea29aeba62471211598dac5d96825bb49348fa07e906ea930394a83ce/docker-7.1.0.tar.gz", hash = "sha256:ad8c70e6e3f8926cb8a92619b832b4ea5299e2831c14284663184e200546fa6c", size = 117834, upload-time = "2024-05-23T11:13:57.216Z" } +source = { registry = "https://pypi.org/simple" } +version = "7.1.0" +wheels = [ + { url = "https://files.pythonhosted.org/packages/e3/26/57c6fb270950d476074c087527a558ccb6f4436657314bfb6cdf484114c4/docker-7.1.0-py3-none-any.whl", hash = "sha256:c96b93b7f0a746f9e77d325bcfb87422a3d8bd4f03136ae8a85b37f1898d5fc0", size = 147774, upload-time = "2024-05-23T11:13:55.01Z" }, +] + +[[package]] +name = "filelock" +sdist = { url = "https://files.pythonhosted.org/packages/40/bb/0ab3e58d22305b6f5440629d20683af28959bf793d98d11950e305c1c326/filelock-3.19.1.tar.gz", hash = "sha256:66eda1888b0171c998b35be2bcc0f6d75c388a7ce20c3f3f37aa8e96c2dddf58", size = 17687, upload-time = "2025-08-14T16:56:03.016Z" } +source = { registry = "https://pypi.org/simple" } +version = "3.19.1" +wheels = [ + { url = "https://files.pythonhosted.org/packages/42/14/42b2651a2f46b022ccd948bca9f2d5af0fd8929c4eec235b8d6d844fbe67/filelock-3.19.1-py3-none-any.whl", hash = "sha256:d38e30481def20772f5baf097c122c3babc4fcdb7e14e57049eb9d88c6dc017d", size = 15988, upload-time = "2025-08-14T16:56:01.633Z" }, +] + +[[package]] +name = "fsspec" +sdist = { url = "https://files.pythonhosted.org/packages/8b/02/0835e6ab9cfc03916fe3f78c0956cfcdb6ff2669ffa6651065d5ebf7fc98/fsspec-2025.7.0.tar.gz", hash = "sha256:786120687ffa54b8283d942929540d8bc5ccfa820deb555a2b5d0ed2b737bf58", size = 304432, upload-time = "2025-07-15T16:05:21.19Z" } +source = { registry = "https://pypi.org/simple" } +version = "2025.7.0" +wheels = [ + { url = "https://files.pythonhosted.org/packages/2f/e0/014d5d9d7a4564cf1c40b5039bc882db69fd881111e03ab3657ac0b218e2/fsspec-2025.7.0-py3-none-any.whl", hash = "sha256:8b012e39f63c7d5f10474de957f3ab793b47b45ae7d39f2fb735f8bbe25c0e21", size = 199597, upload-time = "2025-07-15T16:05:19.529Z" }, +] + +[[package]] +dependencies = [{ name = "python-dateutil" }] +name = "ghp-import" +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" } +source = { registry = "https://pypi.org/simple" } +version = "2.1.0" +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 = "greenlet" +sdist = { url = "https://files.pythonhosted.org/packages/03/b8/704d753a5a45507a7aab61f18db9509302ed3d0a27ac7e0359ec2905b1a6/greenlet-3.2.4.tar.gz", hash = "sha256:0dca0d95ff849f9a364385f36ab49f50065d76964944638be9691e1832e9f86d", size = 188260, upload-time = "2025-08-07T13:24:33.51Z" } +source = { registry = "https://pypi.org/simple" } +version = "3.2.4" +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/e8/58c7f85958bda41dafea50497cbd59738c5c43dbbea5ee83d651234398f4/greenlet-3.2.4-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:1a921e542453fe531144e91e1feedf12e07351b1cf6c9e8a3325ea600a715a31", size = 272814, upload-time = "2025-08-07T13:15:50.011Z" }, + { url = "https://files.pythonhosted.org/packages/62/dd/b9f59862e9e257a16e4e610480cfffd29e3fae018a68c2332090b53aac3d/greenlet-3.2.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cd3c8e693bff0fff6ba55f140bf390fa92c994083f838fece0f63be121334945", size = 641073, upload-time = "2025-08-07T13:42:57.23Z" }, + { url = "https://files.pythonhosted.org/packages/f7/0b/bc13f787394920b23073ca3b6c4a7a21396301ed75a655bcb47196b50e6e/greenlet-3.2.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:710638eb93b1fa52823aa91bf75326f9ecdfd5e0466f00789246a5280f4ba0fc", size = 655191, upload-time = "2025-08-07T13:45:29.752Z" }, + { url = "https://files.pythonhosted.org/packages/f2/d6/6adde57d1345a8d0f14d31e4ab9c23cfe8e2cd39c3baf7674b4b0338d266/greenlet-3.2.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:c5111ccdc9c88f423426df3fd1811bfc40ed66264d35aa373420a34377efc98a", size = 649516, upload-time = "2025-08-07T13:53:16.314Z" }, + { url = "https://files.pythonhosted.org/packages/7f/3b/3a3328a788d4a473889a2d403199932be55b1b0060f4ddd96ee7cdfcad10/greenlet-3.2.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d76383238584e9711e20ebe14db6c88ddcedc1829a9ad31a584389463b5aa504", size = 652169, upload-time = "2025-08-07T13:18:32.861Z" }, + { url = "https://files.pythonhosted.org/packages/ee/43/3cecdc0349359e1a527cbf2e3e28e5f8f06d3343aaf82ca13437a9aa290f/greenlet-3.2.4-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:23768528f2911bcd7e475210822ffb5254ed10d71f4028387e5a99b4c6699671", size = 610497, upload-time = "2025-08-07T13:18:31.636Z" }, + { url = "https://files.pythonhosted.org/packages/b8/19/06b6cf5d604e2c382a6f31cafafd6f33d5dea706f4db7bdab184bad2b21d/greenlet-3.2.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:00fadb3fedccc447f517ee0d3fd8fe49eae949e1cd0f6a611818f4f6fb7dc83b", size = 1121662, upload-time = "2025-08-07T13:42:41.117Z" }, + { url = "https://files.pythonhosted.org/packages/a2/15/0d5e4e1a66fab130d98168fe984c509249c833c1a3c16806b90f253ce7b9/greenlet-3.2.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:d25c5091190f2dc0eaa3f950252122edbbadbb682aa7b1ef2f8af0f8c0afefae", size = 1149210, upload-time = "2025-08-07T13:18:24.072Z" }, + { url = "https://files.pythonhosted.org/packages/0b/55/2321e43595e6801e105fcfdee02b34c0f996eb71e6ddffca6b10b7e1d771/greenlet-3.2.4-cp313-cp313-win_amd64.whl", hash = "sha256:554b03b6e73aaabec3745364d6239e9e012d64c68ccd0b8430c64ccc14939a8b", size = 299685, upload-time = "2025-08-07T13:24:38.824Z" }, + { url = "https://files.pythonhosted.org/packages/22/5c/85273fd7cc388285632b0498dbbab97596e04b154933dfe0f3e68156c68c/greenlet-3.2.4-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:49a30d5fda2507ae77be16479bdb62a660fa51b1eb4928b524975b3bde77b3c0", size = 273586, upload-time = "2025-08-07T13:16:08.004Z" }, + { url = "https://files.pythonhosted.org/packages/d1/75/10aeeaa3da9332c2e761e4c50d4c3556c21113ee3f0afa2cf5769946f7a3/greenlet-3.2.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:299fd615cd8fc86267b47597123e3f43ad79c9d8a22bebdce535e53550763e2f", size = 686346, upload-time = "2025-08-07T13:42:59.944Z" }, + { url = "https://files.pythonhosted.org/packages/c0/aa/687d6b12ffb505a4447567d1f3abea23bd20e73a5bed63871178e0831b7a/greenlet-3.2.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:c17b6b34111ea72fc5a4e4beec9711d2226285f0386ea83477cbb97c30a3f3a5", size = 699218, upload-time = "2025-08-07T13:45:30.969Z" }, + { url = "https://files.pythonhosted.org/packages/dc/8b/29aae55436521f1d6f8ff4e12fb676f3400de7fcf27fccd1d4d17fd8fecd/greenlet-3.2.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b4a1870c51720687af7fa3e7cda6d08d801dae660f75a76f3845b642b4da6ee1", size = 694659, upload-time = "2025-08-07T13:53:17.759Z" }, + { url = "https://files.pythonhosted.org/packages/92/2e/ea25914b1ebfde93b6fc4ff46d6864564fba59024e928bdc7de475affc25/greenlet-3.2.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:061dc4cf2c34852b052a8620d40f36324554bc192be474b9e9770e8c042fd735", size = 695355, upload-time = "2025-08-07T13:18:34.517Z" }, + { url = "https://files.pythonhosted.org/packages/72/60/fc56c62046ec17f6b0d3060564562c64c862948c9d4bc8aa807cf5bd74f4/greenlet-3.2.4-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:44358b9bf66c8576a9f57a590d5f5d6e72fa4228b763d0e43fee6d3b06d3a337", size = 657512, upload-time = "2025-08-07T13:18:33.969Z" }, + { url = "https://files.pythonhosted.org/packages/e3/a5/6ddab2b4c112be95601c13428db1d8b6608a8b6039816f2ba09c346c08fc/greenlet-3.2.4-cp314-cp314-win_amd64.whl", hash = "sha256:e37ab26028f12dbb0ff65f29a8d3d44a765c61e729647bf2ddfbbed621726f01", size = 303425, upload-time = "2025-08-07T13:32:27.59Z" }, +] + +[[package]] +dependencies = [{ name = "colorama" }] +name = "griffe" +sdist = { url = "https://files.pythonhosted.org/packages/81/ca/29f36e00c74844ae50d139cf5a8b1751887b2f4d5023af65d460268ad7aa/griffe-1.12.1.tar.gz", hash = "sha256:29f5a6114c0aeda7d9c86a570f736883f8a2c5b38b57323d56b3d1c000565567", size = 411863, upload-time = "2025-08-14T21:08:15.38Z" } +source = { registry = "https://pypi.org/simple" } +version = "1.12.1" +wheels = [ + { url = "https://files.pythonhosted.org/packages/13/f2/4fab6c3e5bcaf38a44cc8a974d2752eaad4c129e45d6533d926a30edd133/griffe-1.12.1-py3-none-any.whl", hash = "sha256:2d7c12334de00089c31905424a00abcfd931b45b8b516967f224133903d302cc", size = 138940, upload-time = "2025-08-14T21:08:13.382Z" }, +] + +[[package]] +name = "h11" +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +source = { registry = "https://pypi.org/simple" } +version = "0.16.0" +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "hf-xet" +sdist = { url = "https://files.pythonhosted.org/packages/b2/0a/a0f56735940fde6dd627602fec9ab3bad23f66a272397560abd65aba416e/hf_xet-1.1.7.tar.gz", hash = "sha256:20cec8db4561338824a3b5f8c19774055b04a8df7fff0cb1ff2cb1a0c1607b80", size = 477719, upload-time = "2025-08-06T00:30:55.741Z" } +source = { registry = "https://pypi.org/simple" } +version = "1.1.7" +wheels = [ + { url = "https://files.pythonhosted.org/packages/b1/7c/8d7803995caf14e7d19a392a486a040f923e2cfeff824e9b800b92072f76/hf_xet-1.1.7-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:60dae4b44d520819e54e216a2505685248ec0adbdb2dd4848b17aa85a0375cde", size = 2761743, upload-time = "2025-08-06T00:30:50.634Z" }, + { url = "https://files.pythonhosted.org/packages/51/a3/fa5897099454aa287022a34a30e68dbff0e617760f774f8bd1db17f06bd4/hf_xet-1.1.7-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:b109f4c11e01c057fc82004c9e51e6cdfe2cb230637644ade40c599739067b2e", size = 2624331, upload-time = "2025-08-06T00:30:49.212Z" }, + { url = "https://files.pythonhosted.org/packages/86/50/2446a132267e60b8a48b2e5835d6e24fd988000d0f5b9b15ebd6d64ef769/hf_xet-1.1.7-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6efaaf1a5a9fc3a501d3e71e88a6bfebc69ee3a716d0e713a931c8b8d920038f", size = 3183844, upload-time = "2025-08-06T00:30:47.582Z" }, + { url = "https://files.pythonhosted.org/packages/20/8f/ccc670616bb9beee867c6bb7139f7eab2b1370fe426503c25f5cbb27b148/hf_xet-1.1.7-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:751571540f9c1fbad9afcf222a5fb96daf2384bf821317b8bfb0c59d86078513", size = 3074209, upload-time = "2025-08-06T00:30:45.509Z" }, + { url = "https://files.pythonhosted.org/packages/21/0a/4c30e1eb77205565b854f5e4a82cf1f056214e4dc87f2918ebf83d47ae14/hf_xet-1.1.7-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:18b61bbae92d56ae731b92087c44efcac216071182c603fc535f8e29ec4b09b8", size = 3239602, upload-time = "2025-08-06T00:30:52.41Z" }, + { url = "https://files.pythonhosted.org/packages/f5/1e/fc7e9baf14152662ef0b35fa52a6e889f770a7ed14ac239de3c829ecb47e/hf_xet-1.1.7-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:713f2bff61b252f8523739969f247aa354ad8e6d869b8281e174e2ea1bb8d604", size = 3348184, upload-time = "2025-08-06T00:30:54.105Z" }, + { url = "https://files.pythonhosted.org/packages/a3/73/e354eae84ceff117ec3560141224724794828927fcc013c5b449bf0b8745/hf_xet-1.1.7-cp37-abi3-win_amd64.whl", hash = "sha256:2e356da7d284479ae0f1dea3cf5a2f74fdf925d6dca84ac4341930d892c7cb34", size = 2820008, upload-time = "2025-08-06T00:30:57.056Z" }, +] + +[[package]] +dependencies = [ + { name = "filelock" }, + { name = "fsspec" }, + { name = "hf-xet", marker = "platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'arm64' or platform_machine == 'x86_64'" }, + { name = "packaging" }, + { name = "pyyaml" }, + { name = "requests" }, + { name = "tqdm" }, + { name = "typing-extensions" }, +] +name = "huggingface-hub" +sdist = { url = "https://files.pythonhosted.org/packages/45/c9/bdbe19339f76d12985bc03572f330a01a93c04dffecaaea3061bdd7fb892/huggingface_hub-0.34.4.tar.gz", hash = "sha256:a4228daa6fb001be3f4f4bdaf9a0db00e1739235702848df00885c9b5742c85c", size = 459768, upload-time = "2025-08-08T09:14:52.365Z" } +source = { registry = "https://pypi.org/simple" } +version = "0.34.4" +wheels = [ + { url = "https://files.pythonhosted.org/packages/39/7b/bb06b061991107cd8783f300adff3e7b7f284e330fd82f507f2a1417b11d/huggingface_hub-0.34.4-py3-none-any.whl", hash = "sha256:9b365d781739c93ff90c359844221beef048403f1bc1f1c123c191257c3c890a", size = 561452, upload-time = "2025-08-08T09:14:50.159Z" }, +] + +[[package]] +name = "identify" +sdist = { url = "https://files.pythonhosted.org/packages/82/ca/ffbabe3635bb839aa36b3a893c91a9b0d368cb4d8073e03a12896970af82/identify-2.6.13.tar.gz", hash = "sha256:da8d6c828e773620e13bfa86ea601c5a5310ba4bcd65edf378198b56a1f9fb32", size = 99243, upload-time = "2025-08-09T19:35:00.6Z" } +source = { registry = "https://pypi.org/simple" } +version = "2.6.13" +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/ce/461b60a3ee109518c055953729bf9ed089a04db895d47e95444071dcdef2/identify-2.6.13-py2.py3-none-any.whl", hash = "sha256:60381139b3ae39447482ecc406944190f690d4a2997f2584062089848361b33b", size = 99153, upload-time = "2025-08-09T19:34:59.1Z" }, +] + +[[package]] +name = "idna" +sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload-time = "2024-09-15T18:07:39.745Z" } +source = { registry = "https://pypi.org/simple" } +version = "3.10" +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" }, +] + +[[package]] +name = "iniconfig" +sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" } +source = { registry = "https://pypi.org/simple" } +version = "2.1.0" +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" }, +] + +[[package]] +dependencies = [{ name = "markupsafe" }] +name = "jinja2" +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" } +source = { registry = "https://pypi.org/simple" } +version = "3.1.6" +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]] +dependencies = [{ name = "huggingface-hub" }, { name = "packaging" }, { name = "pyyaml" }] +name = "kernels" +sdist = { url = "https://files.pythonhosted.org/packages/26/4e/626c2155efa978bdec45e57bf0cfd0a76682942964fdc166cab2306d56ed/kernels-0.9.0.tar.gz", hash = "sha256:42a77d824d71f76084f7dd52dcb6d8823e243088117dc5d66006779b10f43bfb", size = 42859, upload-time = "2025-08-01T14:46:29.647Z" } +source = { registry = "https://pypi.org/simple" } +version = "0.9.0" +wheels = [ + { url = "https://files.pythonhosted.org/packages/b0/60/3f9b0fba0b78bcc9ba4fa0cd3c5132402838bdb42dc69bf81fb89980d978/kernels-0.9.0-py3-none-any.whl", hash = "sha256:81705176b3488d8eb04fa2aff1a4be8dc10eeb6a4338a510e36a6b3aeeb75969", size = 37421, upload-time = "2025-08-01T14:46:28.478Z" }, +] + +[[package]] +name = "lxml" +sdist = { url = "https://files.pythonhosted.org/packages/c5/ed/60eb6fa2923602fba988d9ca7c5cdbd7cf25faa795162ed538b527a35411/lxml-6.0.0.tar.gz", hash = "sha256:032e65120339d44cdc3efc326c9f660f5f7205f3a535c1fdbf898b29ea01fb72", size = 4096938, upload-time = "2025-06-26T16:28:19.373Z" } +source = { registry = "https://pypi.org/simple" } +version = "6.0.0" +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/21/6e7c060822a3c954ff085e5e1b94b4a25757c06529eac91e550f3f5cd8b8/lxml-6.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6da7cd4f405fd7db56e51e96bff0865b9853ae70df0e6720624049da76bde2da", size = 8414372, upload-time = "2025-06-26T16:26:39.079Z" }, + { url = "https://files.pythonhosted.org/packages/a4/f6/051b1607a459db670fc3a244fa4f06f101a8adf86cda263d1a56b3a4f9d5/lxml-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b34339898bb556a2351a1830f88f751679f343eabf9cf05841c95b165152c9e7", size = 4593940, upload-time = "2025-06-26T16:26:41.891Z" }, + { url = "https://files.pythonhosted.org/packages/8e/74/dd595d92a40bda3c687d70d4487b2c7eff93fd63b568acd64fedd2ba00fe/lxml-6.0.0-cp313-cp313-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:51a5e4c61a4541bd1cd3ba74766d0c9b6c12d6a1a4964ef60026832aac8e79b3", size = 5214329, upload-time = "2025-06-26T16:26:44.669Z" }, + { url = "https://files.pythonhosted.org/packages/52/46/3572761efc1bd45fcafb44a63b3b0feeb5b3f0066886821e94b0254f9253/lxml-6.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d18a25b19ca7307045581b18b3ec9ead2b1db5ccd8719c291f0cd0a5cec6cb81", size = 4947559, upload-time = "2025-06-28T18:47:31.091Z" }, + { url = "https://files.pythonhosted.org/packages/94/8a/5e40de920e67c4f2eef9151097deb9b52d86c95762d8ee238134aff2125d/lxml-6.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d4f0c66df4386b75d2ab1e20a489f30dc7fd9a06a896d64980541506086be1f1", size = 5102143, upload-time = "2025-06-28T18:47:33.612Z" }, + { url = "https://files.pythonhosted.org/packages/7c/4b/20555bdd75d57945bdabfbc45fdb1a36a1a0ff9eae4653e951b2b79c9209/lxml-6.0.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9f4b481b6cc3a897adb4279216695150bbe7a44c03daba3c894f49d2037e0a24", size = 5021931, upload-time = "2025-06-26T16:26:47.503Z" }, + { url = "https://files.pythonhosted.org/packages/b6/6e/cf03b412f3763d4ca23b25e70c96a74cfece64cec3addf1c4ec639586b13/lxml-6.0.0-cp313-cp313-manylinux_2_27_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8a78d6c9168f5bcb20971bf3329c2b83078611fbe1f807baadc64afc70523b3a", size = 5645469, upload-time = "2025-07-03T19:19:13.32Z" }, + { url = "https://files.pythonhosted.org/packages/d4/dd/39c8507c16db6031f8c1ddf70ed95dbb0a6d466a40002a3522c128aba472/lxml-6.0.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2ae06fbab4f1bb7db4f7c8ca9897dc8db4447d1a2b9bee78474ad403437bcc29", size = 5247467, upload-time = "2025-06-26T16:26:49.998Z" }, + { url = "https://files.pythonhosted.org/packages/4d/56/732d49def0631ad633844cfb2664563c830173a98d5efd9b172e89a4800d/lxml-6.0.0-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:1fa377b827ca2023244a06554c6e7dc6828a10aaf74ca41965c5d8a4925aebb4", size = 4720601, upload-time = "2025-06-26T16:26:52.564Z" }, + { url = "https://files.pythonhosted.org/packages/8f/7f/6b956fab95fa73462bca25d1ea7fc8274ddf68fb8e60b78d56c03b65278e/lxml-6.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1676b56d48048a62ef77a250428d1f31f610763636e0784ba67a9740823988ca", size = 5060227, upload-time = "2025-06-26T16:26:55.054Z" }, + { url = "https://files.pythonhosted.org/packages/97/06/e851ac2924447e8b15a294855caf3d543424364a143c001014d22c8ca94c/lxml-6.0.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:0e32698462aacc5c1cf6bdfebc9c781821b7e74c79f13e5ffc8bfe27c42b1abf", size = 4790637, upload-time = "2025-06-26T16:26:57.384Z" }, + { url = "https://files.pythonhosted.org/packages/06/d4/fd216f3cd6625022c25b336c7570d11f4a43adbaf0a56106d3d496f727a7/lxml-6.0.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4d6036c3a296707357efb375cfc24bb64cd955b9ec731abf11ebb1e40063949f", size = 5662049, upload-time = "2025-07-03T19:19:16.409Z" }, + { url = "https://files.pythonhosted.org/packages/52/03/0e764ce00b95e008d76b99d432f1807f3574fb2945b496a17807a1645dbd/lxml-6.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7488a43033c958637b1a08cddc9188eb06d3ad36582cebc7d4815980b47e27ef", size = 5272430, upload-time = "2025-06-26T16:27:00.031Z" }, + { url = "https://files.pythonhosted.org/packages/5f/01/d48cc141bc47bc1644d20fe97bbd5e8afb30415ec94f146f2f76d0d9d098/lxml-6.0.0-cp313-cp313-win32.whl", hash = "sha256:5fcd7d3b1d8ecb91445bd71b9c88bdbeae528fefee4f379895becfc72298d181", size = 3612896, upload-time = "2025-06-26T16:27:04.251Z" }, + { url = "https://files.pythonhosted.org/packages/f4/87/6456b9541d186ee7d4cb53bf1b9a0d7f3b1068532676940fdd594ac90865/lxml-6.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:2f34687222b78fff795feeb799a7d44eca2477c3d9d3a46ce17d51a4f383e32e", size = 4013132, upload-time = "2025-06-26T16:27:06.415Z" }, + { url = "https://files.pythonhosted.org/packages/b7/42/85b3aa8f06ca0d24962f8100f001828e1f1f1a38c954c16e71154ed7d53a/lxml-6.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:21db1ec5525780fd07251636eb5f7acb84003e9382c72c18c542a87c416ade03", size = 3672642, upload-time = "2025-06-26T16:27:09.888Z" }, +] + +[[package]] +name = "markdown" +sdist = { url = "https://files.pythonhosted.org/packages/d7/c2/4ab49206c17f75cb08d6311171f2d65798988db4360c4d1485bd0eedd67c/markdown-3.8.2.tar.gz", hash = "sha256:247b9a70dd12e27f67431ce62523e675b866d254f900c4fe75ce3dda62237c45", size = 362071, upload-time = "2025-06-19T17:12:44.483Z" } +source = { registry = "https://pypi.org/simple" } +version = "3.8.2" +wheels = [ + { url = "https://files.pythonhosted.org/packages/96/2b/34cc11786bc00d0f04d0f5fdc3a2b1ae0b6239eef72d3d345805f9ad92a1/markdown-3.8.2-py3-none-any.whl", hash = "sha256:5c83764dbd4e00bdd94d85a19b8d55ccca20fe35b2e678a1422b380324dd5f24", size = 106827, upload-time = "2025-06-19T17:12:42.994Z" }, +] + +[[package]] +dependencies = [{ name = "mdurl" }] +name = "markdown-it-py" +sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" } +source = { registry = "https://pypi.org/simple" } +version = "4.0.0" +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" }, +] + +[[package]] +name = "markupsafe" +sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537, upload-time = "2024-10-18T15:21:54.129Z" } +source = { registry = "https://pypi.org/simple" } +version = "3.0.2" +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/0e/67eb10a7ecc77a0c2bbe2b0235765b98d164d81600746914bebada795e97/MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd", size = 14274, upload-time = "2024-10-18T15:21:24.577Z" }, + { url = "https://files.pythonhosted.org/packages/2b/6d/9409f3684d3335375d04e5f05744dfe7e9f120062c9857df4ab490a1031a/MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430", size = 12352, upload-time = "2024-10-18T15:21:25.382Z" }, + { url = "https://files.pythonhosted.org/packages/d2/f5/6eadfcd3885ea85fe2a7c128315cc1bb7241e1987443d78c8fe712d03091/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094", size = 24122, upload-time = "2024-10-18T15:21:26.199Z" }, + { url = "https://files.pythonhosted.org/packages/0c/91/96cf928db8236f1bfab6ce15ad070dfdd02ed88261c2afafd4b43575e9e9/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396", size = 23085, upload-time = "2024-10-18T15:21:27.029Z" }, + { url = "https://files.pythonhosted.org/packages/c2/cf/c9d56af24d56ea04daae7ac0940232d31d5a8354f2b457c6d856b2057d69/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79", size = 22978, upload-time = "2024-10-18T15:21:27.846Z" }, + { url = "https://files.pythonhosted.org/packages/2a/9f/8619835cd6a711d6272d62abb78c033bda638fdc54c4e7f4272cf1c0962b/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a", size = 24208, upload-time = "2024-10-18T15:21:28.744Z" }, + { url = "https://files.pythonhosted.org/packages/f9/bf/176950a1792b2cd2102b8ffeb5133e1ed984547b75db47c25a67d3359f77/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca", size = 23357, upload-time = "2024-10-18T15:21:29.545Z" }, + { url = "https://files.pythonhosted.org/packages/ce/4f/9a02c1d335caabe5c4efb90e1b6e8ee944aa245c1aaaab8e8a618987d816/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c", size = 23344, upload-time = "2024-10-18T15:21:30.366Z" }, + { url = "https://files.pythonhosted.org/packages/ee/55/c271b57db36f748f0e04a759ace9f8f759ccf22b4960c270c78a394f58be/MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1", size = 15101, upload-time = "2024-10-18T15:21:31.207Z" }, + { url = "https://files.pythonhosted.org/packages/29/88/07df22d2dd4df40aba9f3e402e6dc1b8ee86297dddbad4872bd5e7b0094f/MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f", size = 15603, upload-time = "2024-10-18T15:21:32.032Z" }, + { url = "https://files.pythonhosted.org/packages/62/6a/8b89d24db2d32d433dffcd6a8779159da109842434f1dd2f6e71f32f738c/MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c", size = 14510, upload-time = "2024-10-18T15:21:33.625Z" }, + { url = "https://files.pythonhosted.org/packages/7a/06/a10f955f70a2e5a9bf78d11a161029d278eeacbd35ef806c3fd17b13060d/MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb", size = 12486, upload-time = "2024-10-18T15:21:34.611Z" }, + { url = "https://files.pythonhosted.org/packages/34/cf/65d4a571869a1a9078198ca28f39fba5fbb910f952f9dbc5220afff9f5e6/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c", size = 25480, upload-time = "2024-10-18T15:21:35.398Z" }, + { url = "https://files.pythonhosted.org/packages/0c/e3/90e9651924c430b885468b56b3d597cabf6d72be4b24a0acd1fa0e12af67/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d", size = 23914, upload-time = "2024-10-18T15:21:36.231Z" }, + { url = "https://files.pythonhosted.org/packages/66/8c/6c7cf61f95d63bb866db39085150df1f2a5bd3335298f14a66b48e92659c/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe", size = 23796, upload-time = "2024-10-18T15:21:37.073Z" }, + { url = "https://files.pythonhosted.org/packages/bb/35/cbe9238ec3f47ac9a7c8b3df7a808e7cb50fe149dc7039f5f454b3fba218/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5", size = 25473, upload-time = "2024-10-18T15:21:37.932Z" }, + { url = "https://files.pythonhosted.org/packages/e6/32/7621a4382488aa283cc05e8984a9c219abad3bca087be9ec77e89939ded9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a", size = 24114, upload-time = "2024-10-18T15:21:39.799Z" }, + { url = "https://files.pythonhosted.org/packages/0d/80/0985960e4b89922cb5a0bac0ed39c5b96cbc1a536a99f30e8c220a996ed9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9", size = 24098, upload-time = "2024-10-18T15:21:40.813Z" }, + { url = "https://files.pythonhosted.org/packages/82/78/fedb03c7d5380df2427038ec8d973587e90561b2d90cd472ce9254cf348b/MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", size = 15208, upload-time = "2024-10-18T15:21:41.814Z" }, + { url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739, upload-time = "2024-10-18T15:21:42.784Z" }, +] + +[[package]] +name = "mdurl" +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +source = { registry = "https://pypi.org/simple" } +version = "0.1.2" +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, +] + +[[package]] +name = "mergedeep" +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" } +source = { registry = "https://pypi.org/simple" } +version = "1.3.4" +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]] +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" }, +] +name = "mkdocs" +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" } +source = { registry = "https://pypi.org/simple" } +version = "1.6.1" +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]] +dependencies = [{ name = "markdown" }, { name = "markupsafe" }, { name = "mkdocs" }] +name = "mkdocs-autorefs" +sdist = { url = "https://files.pythonhosted.org/packages/47/0c/c9826f35b99c67fa3a7cddfa094c1a6c43fafde558c309c6e4403e5b37dc/mkdocs_autorefs-1.4.2.tar.gz", hash = "sha256:e2ebe1abd2b67d597ed19378c0fff84d73d1dbce411fce7a7cc6f161888b6749", size = 54961, upload-time = "2025-05-20T13:09:09.886Z" } +source = { registry = "https://pypi.org/simple" } +version = "1.4.2" +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/dc/fc063b78f4b769d1956319351704e23ebeba1e9e1d6a41b4b602325fd7e4/mkdocs_autorefs-1.4.2-py3-none-any.whl", hash = "sha256:83d6d777b66ec3c372a1aad4ae0cf77c243ba5bcda5bf0c6b8a2c5e7a3d89f13", size = 24969, upload-time = "2025-05-20T13:09:08.237Z" }, +] + +[[package]] +dependencies = [ + { name = "beautifulsoup4" }, + { name = "jinja2" }, + { name = "lxml" }, + { name = "mkdocs" }, + { name = "requests" }, +] +name = "mkdocs-drawio" +sdist = { url = "https://files.pythonhosted.org/packages/6c/bc/0ea7ba44e4f4f065554e35b3e32a4d82d0c408bbe8f1d67ea36baa4d47cb/mkdocs_drawio-1.11.2.tar.gz", hash = "sha256:73f23f7aac3147807908afa800463ab415a714d11df1cd12424500f375c9ebfa", size = 5767, upload-time = "2025-05-23T09:33:38.154Z" } +source = { registry = "https://pypi.org/simple" } +version = "1.11.2" +wheels = [ + { url = "https://files.pythonhosted.org/packages/68/0c/17a9c98ec2b71ab523f0dce728b81ee0d567857da1b4d5908ab1de188c09/mkdocs_drawio-1.11.2-py3-none-any.whl", hash = "sha256:754f224dd467a90d76d170c41d544322daa1ba1f3bd322a33f98b9884d96ec71", size = 7169, upload-time = "2025-05-23T09:33:36.476Z" }, +] + +[[package]] +dependencies = [{ name = "mergedeep" }, { name = "platformdirs" }, { name = "pyyaml" }] +name = "mkdocs-get-deps" +sdist = { url = "https://files.pythonhosted.org/packages/98/f5/ed29cd50067784976f25ed0ed6fcd3c2ce9eb90650aa3b2796ddf7b6870b/mkdocs_get_deps-0.2.0.tar.gz", hash = "sha256:162b3d129c7fad9b19abfdcb9c1458a651628e4b1dea628ac68790fb3061c60c", size = 10239, upload-time = "2023-11-20T17:51:09.981Z" } +source = { registry = "https://pypi.org/simple" } +version = "0.2.0" +wheels = [ + { url = "https://files.pythonhosted.org/packages/9f/d4/029f984e8d3f3b6b726bd33cafc473b75e9e44c0f7e80a5b29abc466bdea/mkdocs_get_deps-0.2.0-py3-none-any.whl", hash = "sha256:2bf11d0b133e77a0dd036abeeb06dec8775e46efa526dc70667d8863eefc6134", size = 9521, upload-time = "2023-11-20T17:51:08.587Z" }, +] + +[[package]] +dependencies = [ + { name = "babel" }, + { name = "backrefs" }, + { name = "click" }, + { name = "colorama" }, + { name = "jinja2" }, + { name = "markdown" }, + { name = "mkdocs" }, + { name = "mkdocs-material-extensions" }, + { name = "paginate" }, + { name = "pygments" }, + { name = "pymdown-extensions" }, + { name = "requests" }, +] +name = "mkdocs-material" +sdist = { url = "https://files.pythonhosted.org/packages/47/02/51115cdda743e1551c5c13bdfaaf8c46b959acc57ba914d8ec479dd2fe1f/mkdocs_material-9.6.17.tar.gz", hash = "sha256:48ae7aec72a3f9f501a70be3fbd329c96ff5f5a385b67a1563e5ed5ce064affe", size = 4032898, upload-time = "2025-08-15T16:09:21.412Z" } +source = { registry = "https://pypi.org/simple" } +version = "9.6.17" +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/7c/0f0d44c92c8f3068930da495b752244bd59fd87b5b0f9571fa2d2a93aee7/mkdocs_material-9.6.17-py3-none-any.whl", hash = "sha256:221dd8b37a63f52e580bcab4a7e0290e4a6f59bd66190be9c3d40767e05f9417", size = 9229230, upload-time = "2025-08-15T16:09:18.301Z" }, +] + +[[package]] +name = "mkdocs-material-extensions" +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" } +source = { registry = "https://pypi.org/simple" } +version = "1.3.1" +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]] +dependencies = [ + { name = "jinja2" }, + { name = "markdown" }, + { name = "markupsafe" }, + { name = "mkdocs" }, + { name = "mkdocs-autorefs" }, + { name = "pymdown-extensions" }, +] +name = "mkdocstrings" +sdist = { url = "https://files.pythonhosted.org/packages/e2/0a/7e4776217d4802009c8238c75c5345e23014a4706a8414a62c0498858183/mkdocstrings-0.30.0.tar.gz", hash = "sha256:5d8019b9c31ddacd780b6784ffcdd6f21c408f34c0bd1103b5351d609d5b4444", size = 106597, upload-time = "2025-07-22T23:48:45.998Z" } +source = { registry = "https://pypi.org/simple" } +version = "0.30.0" +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/b4/3c5eac68f31e124a55d255d318c7445840fa1be55e013f507556d6481913/mkdocstrings-0.30.0-py3-none-any.whl", hash = "sha256:ae9e4a0d8c1789697ac776f2e034e2ddd71054ae1cf2c2bb1433ccfd07c226f2", size = 36579, upload-time = "2025-07-22T23:48:44.152Z" }, +] + +[[package]] +dependencies = [{ name = "griffe" }, { name = "mkdocs-autorefs" }, { name = "mkdocstrings" }] +name = "mkdocstrings-python" +sdist = { url = "https://files.pythonhosted.org/packages/39/7c/6dfd8ad59c0eebae167168528ed6cad00116f58ef2327686149f7b25d175/mkdocstrings_python-1.17.0.tar.gz", hash = "sha256:c6295962b60542a9c7468a3b515ce8524616ca9f8c1a38c790db4286340ba501", size = 200408, upload-time = "2025-08-14T21:18:14.568Z" } +source = { registry = "https://pypi.org/simple" } +version = "1.17.0" +wheels = [ + { url = "https://files.pythonhosted.org/packages/bd/ac/b1fcc937f4ecd372f3e857162dea67c45c1e2eedbac80447be516e3372bb/mkdocstrings_python-1.17.0-py3-none-any.whl", hash = "sha256:49903fa355dfecc5ad0b891e78ff5d25d30ffd00846952801bbe8331e123d4b0", size = 124778, upload-time = "2025-08-14T21:18:12.821Z" }, +] + +[[package]] +name = "mpmath" +sdist = { url = "https://files.pythonhosted.org/packages/e0/47/dd32fa426cc72114383ac549964eecb20ecfd886d1e5ccf5340b55b02f57/mpmath-1.3.0.tar.gz", hash = "sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f", size = 508106, upload-time = "2023-03-07T16:47:11.061Z" } +source = { registry = "https://pypi.org/simple" } +version = "1.3.0" +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c", size = 536198, upload-time = "2023-03-07T16:47:09.197Z" }, +] + +[[package]] +name = "mslex" +sdist = { url = "https://files.pythonhosted.org/packages/e0/97/7022667073c99a0fe028f2e34b9bf76b49a611afd21b02527fbfd92d4cd5/mslex-1.3.0.tar.gz", hash = "sha256:641c887d1d3db610eee2af37a8e5abda3f70b3006cdfd2d0d29dc0d1ae28a85d", size = 11583, upload-time = "2024-10-16T13:16:18.523Z" } +source = { registry = "https://pypi.org/simple" } +version = "1.3.0" +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/f2/66bd65ca0139675a0d7b18f0bada6e12b51a984e41a76dbe44761bf1b3ee/mslex-1.3.0-py3-none-any.whl", hash = "sha256:c7074b347201b3466fc077c5692fbce9b5f62a63a51f537a53fbbd02eff2eea4", size = 7820, upload-time = "2024-10-16T13:16:17.566Z" }, +] + +[[package]] +name = "networkx" +sdist = { url = "https://files.pythonhosted.org/packages/6c/4f/ccdb8ad3a38e583f214547fd2f7ff1fc160c43a75af88e6aec213404b96a/networkx-3.5.tar.gz", hash = "sha256:d4c6f9cf81f52d69230866796b82afbccdec3db7ae4fbd1b65ea750feed50037", size = 2471065, upload-time = "2025-05-29T11:35:07.804Z" } +source = { registry = "https://pypi.org/simple" } +version = "3.5" +wheels = [ + { url = "https://files.pythonhosted.org/packages/eb/8d/776adee7bbf76365fdd7f2552710282c79a4ead5d2a46408c9043a2b70ba/networkx-3.5-py3-none-any.whl", hash = "sha256:0030d386a9a06dee3565298b4a734b68589749a544acbb6c412dc9e2489ec6ec", size = 2034406, upload-time = "2025-05-29T11:35:04.961Z" }, +] + +[[package]] +name = "nodeenv" +sdist = { url = "https://files.pythonhosted.org/packages/43/16/fc88b08840de0e0a72a2f9d8c6bae36be573e475a6326ae854bcc549fc45/nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f", size = 47437, upload-time = "2024-06-04T18:44:11.171Z" } +source = { registry = "https://pypi.org/simple" } +version = "1.9.1" +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314, upload-time = "2024-06-04T18:44:08.352Z" }, +] + +[[package]] +name = "numpy" +sdist = { url = "https://files.pythonhosted.org/packages/37/7d/3fec4199c5ffb892bed55cff901e4f39a58c81df9c44c280499e92cad264/numpy-2.3.2.tar.gz", hash = "sha256:e0486a11ec30cdecb53f184d496d1c6a20786c81e55e41640270130056f8ee48", size = 20489306, upload-time = "2025-07-24T21:32:07.553Z" } +source = { registry = "https://pypi.org/simple" } +version = "2.3.2" +wheels = [ + { url = "https://files.pythonhosted.org/packages/1c/c0/c6bb172c916b00700ed3bf71cb56175fd1f7dbecebf8353545d0b5519f6c/numpy-2.3.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c8d9727f5316a256425892b043736d63e89ed15bbfe6556c5ff4d9d4448ff3b3", size = 20949074, upload-time = "2025-07-24T20:43:07.813Z" }, + { url = "https://files.pythonhosted.org/packages/20/4e/c116466d22acaf4573e58421c956c6076dc526e24a6be0903219775d862e/numpy-2.3.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:efc81393f25f14d11c9d161e46e6ee348637c0a1e8a54bf9dedc472a3fae993b", size = 14177311, upload-time = "2025-07-24T20:43:29.335Z" }, + { url = "https://files.pythonhosted.org/packages/78/45/d4698c182895af189c463fc91d70805d455a227261d950e4e0f1310c2550/numpy-2.3.2-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:dd937f088a2df683cbb79dda9a772b62a3e5a8a7e76690612c2737f38c6ef1b6", size = 5106022, upload-time = "2025-07-24T20:43:37.999Z" }, + { url = "https://files.pythonhosted.org/packages/9f/76/3e6880fef4420179309dba72a8c11f6166c431cf6dee54c577af8906f914/numpy-2.3.2-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:11e58218c0c46c80509186e460d79fbdc9ca1eb8d8aee39d8f2dc768eb781089", size = 6640135, upload-time = "2025-07-24T20:43:49.28Z" }, + { url = "https://files.pythonhosted.org/packages/34/fa/87ff7f25b3c4ce9085a62554460b7db686fef1e0207e8977795c7b7d7ba1/numpy-2.3.2-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5ad4ebcb683a1f99f4f392cc522ee20a18b2bb12a2c1c42c3d48d5a1adc9d3d2", size = 14278147, upload-time = "2025-07-24T20:44:10.328Z" }, + { url = "https://files.pythonhosted.org/packages/1d/0f/571b2c7a3833ae419fe69ff7b479a78d313581785203cc70a8db90121b9a/numpy-2.3.2-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:938065908d1d869c7d75d8ec45f735a034771c6ea07088867f713d1cd3bbbe4f", size = 16635989, upload-time = "2025-07-24T20:44:34.88Z" }, + { url = "https://files.pythonhosted.org/packages/24/5a/84ae8dca9c9a4c592fe11340b36a86ffa9fd3e40513198daf8a97839345c/numpy-2.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:66459dccc65d8ec98cc7df61307b64bf9e08101f9598755d42d8ae65d9a7a6ee", size = 16053052, upload-time = "2025-07-24T20:44:58.872Z" }, + { url = "https://files.pythonhosted.org/packages/57/7c/e5725d99a9133b9813fcf148d3f858df98511686e853169dbaf63aec6097/numpy-2.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a7af9ed2aa9ec5950daf05bb11abc4076a108bd3c7db9aa7251d5f107079b6a6", size = 18577955, upload-time = "2025-07-24T20:45:26.714Z" }, + { url = "https://files.pythonhosted.org/packages/ae/11/7c546fcf42145f29b71e4d6f429e96d8d68e5a7ba1830b2e68d7418f0bbd/numpy-2.3.2-cp313-cp313-win32.whl", hash = "sha256:906a30249315f9c8e17b085cc5f87d3f369b35fedd0051d4a84686967bdbbd0b", size = 6311843, upload-time = "2025-07-24T20:49:24.444Z" }, + { url = "https://files.pythonhosted.org/packages/aa/6f/a428fd1cb7ed39b4280d057720fed5121b0d7754fd2a9768640160f5517b/numpy-2.3.2-cp313-cp313-win_amd64.whl", hash = "sha256:c63d95dc9d67b676e9108fe0d2182987ccb0f11933c1e8959f42fa0da8d4fa56", size = 12782876, upload-time = "2025-07-24T20:49:43.227Z" }, + { url = "https://files.pythonhosted.org/packages/65/85/4ea455c9040a12595fb6c43f2c217257c7b52dd0ba332c6a6c1d28b289fe/numpy-2.3.2-cp313-cp313-win_arm64.whl", hash = "sha256:b05a89f2fb84d21235f93de47129dd4f11c16f64c87c33f5e284e6a3a54e43f2", size = 10192786, upload-time = "2025-07-24T20:49:59.443Z" }, + { url = "https://files.pythonhosted.org/packages/80/23/8278f40282d10c3f258ec3ff1b103d4994bcad78b0cba9208317f6bb73da/numpy-2.3.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:4e6ecfeddfa83b02318f4d84acf15fbdbf9ded18e46989a15a8b6995dfbf85ab", size = 21047395, upload-time = "2025-07-24T20:45:58.821Z" }, + { url = "https://files.pythonhosted.org/packages/1f/2d/624f2ce4a5df52628b4ccd16a4f9437b37c35f4f8a50d00e962aae6efd7a/numpy-2.3.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:508b0eada3eded10a3b55725b40806a4b855961040180028f52580c4729916a2", size = 14300374, upload-time = "2025-07-24T20:46:20.207Z" }, + { url = "https://files.pythonhosted.org/packages/f6/62/ff1e512cdbb829b80a6bd08318a58698867bca0ca2499d101b4af063ee97/numpy-2.3.2-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:754d6755d9a7588bdc6ac47dc4ee97867271b17cee39cb87aef079574366db0a", size = 5228864, upload-time = "2025-07-24T20:46:30.58Z" }, + { url = "https://files.pythonhosted.org/packages/7d/8e/74bc18078fff03192d4032cfa99d5a5ca937807136d6f5790ce07ca53515/numpy-2.3.2-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:a9f66e7d2b2d7712410d3bc5684149040ef5f19856f20277cd17ea83e5006286", size = 6737533, upload-time = "2025-07-24T20:46:46.111Z" }, + { url = "https://files.pythonhosted.org/packages/19/ea/0731efe2c9073ccca5698ef6a8c3667c4cf4eea53fcdcd0b50140aba03bc/numpy-2.3.2-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:de6ea4e5a65d5a90c7d286ddff2b87f3f4ad61faa3db8dabe936b34c2275b6f8", size = 14352007, upload-time = "2025-07-24T20:47:07.1Z" }, + { url = "https://files.pythonhosted.org/packages/cf/90/36be0865f16dfed20f4bc7f75235b963d5939707d4b591f086777412ff7b/numpy-2.3.2-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a3ef07ec8cbc8fc9e369c8dcd52019510c12da4de81367d8b20bc692aa07573a", size = 16701914, upload-time = "2025-07-24T20:47:32.459Z" }, + { url = "https://files.pythonhosted.org/packages/94/30/06cd055e24cb6c38e5989a9e747042b4e723535758e6153f11afea88c01b/numpy-2.3.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:27c9f90e7481275c7800dc9c24b7cc40ace3fdb970ae4d21eaff983a32f70c91", size = 16132708, upload-time = "2025-07-24T20:47:58.129Z" }, + { url = "https://files.pythonhosted.org/packages/9a/14/ecede608ea73e58267fd7cb78f42341b3b37ba576e778a1a06baffbe585c/numpy-2.3.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:07b62978075b67eee4065b166d000d457c82a1efe726cce608b9db9dd66a73a5", size = 18651678, upload-time = "2025-07-24T20:48:25.402Z" }, + { url = "https://files.pythonhosted.org/packages/40/f3/2fe6066b8d07c3685509bc24d56386534c008b462a488b7f503ba82b8923/numpy-2.3.2-cp313-cp313t-win32.whl", hash = "sha256:c771cfac34a4f2c0de8e8c97312d07d64fd8f8ed45bc9f5726a7e947270152b5", size = 6441832, upload-time = "2025-07-24T20:48:37.181Z" }, + { url = "https://files.pythonhosted.org/packages/0b/ba/0937d66d05204d8f28630c9c60bc3eda68824abde4cf756c4d6aad03b0c6/numpy-2.3.2-cp313-cp313t-win_amd64.whl", hash = "sha256:72dbebb2dcc8305c431b2836bcc66af967df91be793d63a24e3d9b741374c450", size = 12927049, upload-time = "2025-07-24T20:48:56.24Z" }, + { url = "https://files.pythonhosted.org/packages/e9/ed/13542dd59c104d5e654dfa2ac282c199ba64846a74c2c4bcdbc3a0f75df1/numpy-2.3.2-cp313-cp313t-win_arm64.whl", hash = "sha256:72c6df2267e926a6d5286b0a6d556ebe49eae261062059317837fda12ddf0c1a", size = 10262935, upload-time = "2025-07-24T20:49:13.136Z" }, + { url = "https://files.pythonhosted.org/packages/c9/7c/7659048aaf498f7611b783e000c7268fcc4dcf0ce21cd10aad7b2e8f9591/numpy-2.3.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:448a66d052d0cf14ce9865d159bfc403282c9bc7bb2a31b03cc18b651eca8b1a", size = 20950906, upload-time = "2025-07-24T20:50:30.346Z" }, + { url = "https://files.pythonhosted.org/packages/80/db/984bea9d4ddf7112a04cfdfb22b1050af5757864cfffe8e09e44b7f11a10/numpy-2.3.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:546aaf78e81b4081b2eba1d105c3b34064783027a06b3ab20b6eba21fb64132b", size = 14185607, upload-time = "2025-07-24T20:50:51.923Z" }, + { url = "https://files.pythonhosted.org/packages/e4/76/b3d6f414f4eca568f469ac112a3b510938d892bc5a6c190cb883af080b77/numpy-2.3.2-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:87c930d52f45df092f7578889711a0768094debf73cfcde105e2d66954358125", size = 5114110, upload-time = "2025-07-24T20:51:01.041Z" }, + { url = "https://files.pythonhosted.org/packages/9e/d2/6f5e6826abd6bca52392ed88fe44a4b52aacb60567ac3bc86c67834c3a56/numpy-2.3.2-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:8dc082ea901a62edb8f59713c6a7e28a85daddcb67454c839de57656478f5b19", size = 6642050, upload-time = "2025-07-24T20:51:11.64Z" }, + { url = "https://files.pythonhosted.org/packages/c4/43/f12b2ade99199e39c73ad182f103f9d9791f48d885c600c8e05927865baf/numpy-2.3.2-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:af58de8745f7fa9ca1c0c7c943616c6fe28e75d0c81f5c295810e3c83b5be92f", size = 14296292, upload-time = "2025-07-24T20:51:33.488Z" }, + { url = "https://files.pythonhosted.org/packages/5d/f9/77c07d94bf110a916b17210fac38680ed8734c236bfed9982fd8524a7b47/numpy-2.3.2-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed5527c4cf10f16c6d0b6bee1f89958bccb0ad2522c8cadc2efd318bcd545f5", size = 16638913, upload-time = "2025-07-24T20:51:58.517Z" }, + { url = "https://files.pythonhosted.org/packages/9b/d1/9d9f2c8ea399cc05cfff8a7437453bd4e7d894373a93cdc46361bbb49a7d/numpy-2.3.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:095737ed986e00393ec18ec0b21b47c22889ae4b0cd2d5e88342e08b01141f58", size = 16071180, upload-time = "2025-07-24T20:52:22.827Z" }, + { url = "https://files.pythonhosted.org/packages/4c/41/82e2c68aff2a0c9bf315e47d61951099fed65d8cb2c8d9dc388cb87e947e/numpy-2.3.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b5e40e80299607f597e1a8a247ff8d71d79c5b52baa11cc1cce30aa92d2da6e0", size = 18576809, upload-time = "2025-07-24T20:52:51.015Z" }, + { url = "https://files.pythonhosted.org/packages/14/14/4b4fd3efb0837ed252d0f583c5c35a75121038a8c4e065f2c259be06d2d8/numpy-2.3.2-cp314-cp314-win32.whl", hash = "sha256:7d6e390423cc1f76e1b8108c9b6889d20a7a1f59d9a60cac4a050fa734d6c1e2", size = 6366410, upload-time = "2025-07-24T20:56:44.949Z" }, + { url = "https://files.pythonhosted.org/packages/11/9e/b4c24a6b8467b61aced5c8dc7dcfce23621baa2e17f661edb2444a418040/numpy-2.3.2-cp314-cp314-win_amd64.whl", hash = "sha256:b9d0878b21e3918d76d2209c924ebb272340da1fb51abc00f986c258cd5e957b", size = 12918821, upload-time = "2025-07-24T20:57:06.479Z" }, + { url = "https://files.pythonhosted.org/packages/0e/0f/0dc44007c70b1007c1cef86b06986a3812dd7106d8f946c09cfa75782556/numpy-2.3.2-cp314-cp314-win_arm64.whl", hash = "sha256:2738534837c6a1d0c39340a190177d7d66fdf432894f469728da901f8f6dc910", size = 10477303, upload-time = "2025-07-24T20:57:22.879Z" }, + { url = "https://files.pythonhosted.org/packages/8b/3e/075752b79140b78ddfc9c0a1634d234cfdbc6f9bbbfa6b7504e445ad7d19/numpy-2.3.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:4d002ecf7c9b53240be3bb69d80f86ddbd34078bae04d87be81c1f58466f264e", size = 21047524, upload-time = "2025-07-24T20:53:22.086Z" }, + { url = "https://files.pythonhosted.org/packages/fe/6d/60e8247564a72426570d0e0ea1151b95ce5bd2f1597bb878a18d32aec855/numpy-2.3.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:293b2192c6bcce487dbc6326de5853787f870aeb6c43f8f9c6496db5b1781e45", size = 14300519, upload-time = "2025-07-24T20:53:44.053Z" }, + { url = "https://files.pythonhosted.org/packages/4d/73/d8326c442cd428d47a067070c3ac6cc3b651a6e53613a1668342a12d4479/numpy-2.3.2-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:0a4f2021a6da53a0d580d6ef5db29947025ae8b35b3250141805ea9a32bbe86b", size = 5228972, upload-time = "2025-07-24T20:53:53.81Z" }, + { url = "https://files.pythonhosted.org/packages/34/2e/e71b2d6dad075271e7079db776196829019b90ce3ece5c69639e4f6fdc44/numpy-2.3.2-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:9c144440db4bf3bb6372d2c3e49834cc0ff7bb4c24975ab33e01199e645416f2", size = 6737439, upload-time = "2025-07-24T20:54:04.742Z" }, + { url = "https://files.pythonhosted.org/packages/15/b0/d004bcd56c2c5e0500ffc65385eb6d569ffd3363cb5e593ae742749b2daa/numpy-2.3.2-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f92d6c2a8535dc4fe4419562294ff957f83a16ebdec66df0805e473ffaad8bd0", size = 14352479, upload-time = "2025-07-24T20:54:25.819Z" }, + { url = "https://files.pythonhosted.org/packages/11/e3/285142fcff8721e0c99b51686426165059874c150ea9ab898e12a492e291/numpy-2.3.2-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cefc2219baa48e468e3db7e706305fcd0c095534a192a08f31e98d83a7d45fb0", size = 16702805, upload-time = "2025-07-24T20:54:50.814Z" }, + { url = "https://files.pythonhosted.org/packages/33/c3/33b56b0e47e604af2c7cd065edca892d180f5899599b76830652875249a3/numpy-2.3.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:76c3e9501ceb50b2ff3824c3589d5d1ab4ac857b0ee3f8f49629d0de55ecf7c2", size = 16133830, upload-time = "2025-07-24T20:55:17.306Z" }, + { url = "https://files.pythonhosted.org/packages/6e/ae/7b1476a1f4d6a48bc669b8deb09939c56dd2a439db1ab03017844374fb67/numpy-2.3.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:122bf5ed9a0221b3419672493878ba4967121514b1d7d4656a7580cd11dddcbf", size = 18652665, upload-time = "2025-07-24T20:55:46.665Z" }, + { url = "https://files.pythonhosted.org/packages/14/ba/5b5c9978c4bb161034148ade2de9db44ec316fab89ce8c400db0e0c81f86/numpy-2.3.2-cp314-cp314t-win32.whl", hash = "sha256:6f1ae3dcb840edccc45af496f312528c15b1f79ac318169d094e85e4bb35fdf1", size = 6514777, upload-time = "2025-07-24T20:55:57.66Z" }, + { url = "https://files.pythonhosted.org/packages/eb/46/3dbaf0ae7c17cdc46b9f662c56da2054887b8d9e737c1476f335c83d33db/numpy-2.3.2-cp314-cp314t-win_amd64.whl", hash = "sha256:087ffc25890d89a43536f75c5fe8770922008758e8eeeef61733957041ed2f9b", size = 13111856, upload-time = "2025-07-24T20:56:17.318Z" }, + { url = "https://files.pythonhosted.org/packages/c1/9e/1652778bce745a67b5fe05adde60ed362d38eb17d919a540e813d30f6874/numpy-2.3.2-cp314-cp314t-win_arm64.whl", hash = "sha256:092aeb3449833ea9c0bf0089d70c29ae480685dd2377ec9cdbbb620257f84631", size = 10544226, upload-time = "2025-07-24T20:56:34.509Z" }, +] + +[[package]] +name = "nvidia-cublas-cu12" +source = { registry = "https://pypi.org/simple" } +version = "12.4.5.8" +wheels = [ + { url = "https://files.pythonhosted.org/packages/ae/71/1c91302526c45ab494c23f61c7a84aa568b8c1f9d196efa5993957faf906/nvidia_cublas_cu12-12.4.5.8-py3-none-manylinux2014_x86_64.whl", hash = "sha256:2fc8da60df463fdefa81e323eef2e36489e1c94335b5358bcb38360adf75ac9b", size = 363438805, upload-time = "2024-04-03T20:57:06.025Z" }, +] + +[[package]] +name = "nvidia-cuda-cupti-cu12" +source = { registry = "https://pypi.org/simple" } +version = "12.4.127" +wheels = [ + { url = "https://files.pythonhosted.org/packages/67/42/f4f60238e8194a3106d06a058d494b18e006c10bb2b915655bd9f6ea4cb1/nvidia_cuda_cupti_cu12-12.4.127-py3-none-manylinux2014_x86_64.whl", hash = "sha256:9dec60f5ac126f7bb551c055072b69d85392b13311fcc1bcda2202d172df30fb", size = 13813957, upload-time = "2024-04-03T20:55:01.564Z" }, +] + +[[package]] +name = "nvidia-cuda-nvrtc-cu12" +source = { registry = "https://pypi.org/simple" } +version = "12.4.127" +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/14/91ae57cd4db3f9ef7aa99f4019cfa8d54cb4caa7e00975df6467e9725a9f/nvidia_cuda_nvrtc_cu12-12.4.127-py3-none-manylinux2014_x86_64.whl", hash = "sha256:a178759ebb095827bd30ef56598ec182b85547f1508941a3d560eb7ea1fbf338", size = 24640306, upload-time = "2024-04-03T20:56:01.463Z" }, +] + +[[package]] +name = "nvidia-cuda-runtime-cu12" +source = { registry = "https://pypi.org/simple" } +version = "12.4.127" +wheels = [ + { url = "https://files.pythonhosted.org/packages/ea/27/1795d86fe88ef397885f2e580ac37628ed058a92ed2c39dc8eac3adf0619/nvidia_cuda_runtime_cu12-12.4.127-py3-none-manylinux2014_x86_64.whl", hash = "sha256:64403288fa2136ee8e467cdc9c9427e0434110899d07c779f25b5c068934faa5", size = 883737, upload-time = "2024-04-03T20:54:51.355Z" }, +] + +[[package]] +dependencies = [{ name = "nvidia-cublas-cu12" }] +name = "nvidia-cudnn-cu12" +source = { registry = "https://pypi.org/simple" } +version = "9.1.0.70" +wheels = [ + { url = "https://files.pythonhosted.org/packages/9f/fd/713452cd72343f682b1c7b9321e23829f00b842ceaedcda96e742ea0b0b3/nvidia_cudnn_cu12-9.1.0.70-py3-none-manylinux2014_x86_64.whl", hash = "sha256:165764f44ef8c61fcdfdfdbe769d687e06374059fbb388b6c89ecb0e28793a6f", size = 664752741, upload-time = "2024-04-22T15:24:15.253Z" }, +] + +[[package]] +dependencies = [{ name = "nvidia-nvjitlink-cu12" }] +name = "nvidia-cufft-cu12" +source = { registry = "https://pypi.org/simple" } +version = "11.2.1.3" +wheels = [ + { url = "https://files.pythonhosted.org/packages/27/94/3266821f65b92b3138631e9c8e7fe1fb513804ac934485a8d05776e1dd43/nvidia_cufft_cu12-11.2.1.3-py3-none-manylinux2014_x86_64.whl", hash = "sha256:f083fc24912aa410be21fa16d157fed2055dab1cc4b6934a0e03cba69eb242b9", size = 211459117, upload-time = "2024-04-03T20:57:40.402Z" }, +] + +[[package]] +name = "nvidia-curand-cu12" +source = { registry = "https://pypi.org/simple" } +version = "10.3.5.147" +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/6d/44ad094874c6f1b9c654f8ed939590bdc408349f137f9b98a3a23ccec411/nvidia_curand_cu12-10.3.5.147-py3-none-manylinux2014_x86_64.whl", hash = "sha256:a88f583d4e0bb643c49743469964103aa59f7f708d862c3ddb0fc07f851e3b8b", size = 56305206, upload-time = "2024-04-03T20:58:08.722Z" }, +] + +[[package]] +dependencies = [{ name = "nvidia-cublas-cu12" }, { name = "nvidia-cusparse-cu12" }, { name = "nvidia-nvjitlink-cu12" }] +name = "nvidia-cusolver-cu12" +source = { registry = "https://pypi.org/simple" } +version = "11.6.1.9" +wheels = [ + { url = "https://files.pythonhosted.org/packages/3a/e1/5b9089a4b2a4790dfdea8b3a006052cfecff58139d5a4e34cb1a51df8d6f/nvidia_cusolver_cu12-11.6.1.9-py3-none-manylinux2014_x86_64.whl", hash = "sha256:19e33fa442bcfd085b3086c4ebf7e8debc07cfe01e11513cc6d332fd918ac260", size = 127936057, upload-time = "2024-04-03T20:58:28.735Z" }, +] + +[[package]] +dependencies = [{ name = "nvidia-nvjitlink-cu12" }] +name = "nvidia-cusparse-cu12" +source = { registry = "https://pypi.org/simple" } +version = "12.3.1.170" +wheels = [ + { url = "https://files.pythonhosted.org/packages/db/f7/97a9ea26ed4bbbfc2d470994b8b4f338ef663be97b8f677519ac195e113d/nvidia_cusparse_cu12-12.3.1.170-py3-none-manylinux2014_x86_64.whl", hash = "sha256:ea4f11a2904e2a8dc4b1833cc1b5181cde564edd0d5cd33e3c168eff2d1863f1", size = 207454763, upload-time = "2024-04-03T20:58:59.995Z" }, +] + +[[package]] +name = "nvidia-cusparselt-cu12" +source = { registry = "https://pypi.org/simple" } +version = "0.6.2" +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/a8/bcbb63b53a4b1234feeafb65544ee55495e1bb37ec31b999b963cbccfd1d/nvidia_cusparselt_cu12-0.6.2-py3-none-manylinux2014_x86_64.whl", hash = "sha256:df2c24502fd76ebafe7457dbc4716b2fec071aabaed4fb7691a201cde03704d9", size = 150057751, upload-time = "2024-07-23T02:35:53.074Z" }, +] + +[[package]] +name = "nvidia-nccl-cu12" +source = { registry = "https://pypi.org/simple" } +version = "2.21.5" +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/99/12cd266d6233f47d00daf3a72739872bdc10267d0383508b0b9c84a18bb6/nvidia_nccl_cu12-2.21.5-py3-none-manylinux2014_x86_64.whl", hash = "sha256:8579076d30a8c24988834445f8d633c697d42397e92ffc3f63fa26766d25e0a0", size = 188654414, upload-time = "2024-04-03T15:32:57.427Z" }, +] + +[[package]] +name = "nvidia-nvjitlink-cu12" +source = { registry = "https://pypi.org/simple" } +version = "12.4.127" +wheels = [ + { url = "https://files.pythonhosted.org/packages/ff/ff/847841bacfbefc97a00036e0fce5a0f086b640756dc38caea5e1bb002655/nvidia_nvjitlink_cu12-12.4.127-py3-none-manylinux2014_x86_64.whl", hash = "sha256:06b3b9b25bf3f8af351d664978ca26a16d2c5127dbd53c0497e28d1fb9611d57", size = 21066810, upload-time = "2024-04-03T20:59:46.957Z" }, +] + +[[package]] +name = "nvidia-nvtx-cu12" +source = { registry = "https://pypi.org/simple" } +version = "12.4.127" +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/20/199b8713428322a2f22b722c62b8cc278cc53dffa9705d744484b5035ee9/nvidia_nvtx_cu12-12.4.127-py3-none-manylinux2014_x86_64.whl", hash = "sha256:781e950d9b9f60d8241ccea575b32f5105a5baf4c2351cab5256a24869f12a1a", size = 99144, upload-time = "2024-04-03T20:56:12.406Z" }, +] + +[[package]] +name = "packaging" +sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } +source = { registry = "https://pypi.org/simple" } +version = "25.0" +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, +] + +[[package]] +name = "paginate" +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" } +source = { registry = "https://pypi.org/simple" } +version = "0.5.7" +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" +sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043, upload-time = "2023-12-10T22:30:45Z" } +source = { registry = "https://pypi.org/simple" } +version = "0.12.1" +wheels = [ + { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191, upload-time = "2023-12-10T22:30:43.14Z" }, +] + +[[package]] +name = "platformdirs" +sdist = { url = "https://files.pythonhosted.org/packages/f5/52/0763d1d976d5c262df53ddda8d8d4719eedf9594d046f117c25a27261a19/platformdirs-4.2.2.tar.gz", hash = "sha256:38b7b51f512eed9e84a22788b4bce1de17c0adb134d6becb09836e37d8654cd3", size = 20916, upload-time = "2024-05-15T03:18:23.372Z" } +source = { registry = "https://pypi.org/simple" } +version = "4.2.2" +wheels = [ + { url = "https://files.pythonhosted.org/packages/68/13/2aa1f0e1364feb2c9ef45302f387ac0bd81484e9c9a4c5688a322fbdfd08/platformdirs-4.2.2-py3-none-any.whl", hash = "sha256:2d7a1657e36a80ea911db832a8a6ece5ee53d8de21edd5cc5879af6530b1bfee", size = 18146, upload-time = "2024-05-15T03:18:21.209Z" }, +] + +[[package]] +dependencies = [{ name = "greenlet" }, { name = "pyee" }] +name = "playwright" +source = { registry = "https://pypi.org/simple" } +version = "1.54.0" +wheels = [ + { url = "https://files.pythonhosted.org/packages/f3/09/33d5bfe393a582d8dac72165a9e88b274143c9df411b65ece1cc13f42988/playwright-1.54.0-py3-none-macosx_10_13_x86_64.whl", hash = "sha256:bf3b845af744370f1bd2286c2a9536f474cc8a88dc995b72ea9a5be714c9a77d", size = 40439034, upload-time = "2025-07-22T13:58:04.816Z" }, + { url = "https://files.pythonhosted.org/packages/e1/7b/51882dc584f7aa59f446f2bb34e33c0e5f015de4e31949e5b7c2c10e54f0/playwright-1.54.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:780928b3ca2077aea90414b37e54edd0c4bbb57d1aafc42f7aa0b3fd2c2fac02", size = 38702308, upload-time = "2025-07-22T13:58:08.211Z" }, + { url = "https://files.pythonhosted.org/packages/73/a1/7aa8ae175b240c0ec8849fcf000e078f3c693f9aa2ffd992da6550ea0dff/playwright-1.54.0-py3-none-macosx_11_0_universal2.whl", hash = "sha256:81d0b6f28843b27f288cfe438af0a12a4851de57998009a519ea84cee6fbbfb9", size = 40439037, upload-time = "2025-07-22T13:58:11.37Z" }, + { url = "https://files.pythonhosted.org/packages/34/a9/45084fd23b6206f954198296ce39b0acf50debfdf3ec83a593e4d73c9c8a/playwright-1.54.0-py3-none-manylinux1_x86_64.whl", hash = "sha256:09919f45cc74c64afb5432646d7fef0d19fff50990c862cb8d9b0577093f40cc", size = 45920135, upload-time = "2025-07-22T13:58:14.494Z" }, + { url = "https://files.pythonhosted.org/packages/02/d4/6a692f4c6db223adc50a6e53af405b45308db39270957a6afebddaa80ea2/playwright-1.54.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:13ae206c55737e8e3eae51fb385d61c0312eeef31535643bb6232741b41b6fdc", size = 45302695, upload-time = "2025-07-22T13:58:18.901Z" }, + { url = "https://files.pythonhosted.org/packages/72/7a/4ee60a1c3714321db187bebbc40d52cea5b41a856925156325058b5fca5a/playwright-1.54.0-py3-none-win32.whl", hash = "sha256:0b108622ffb6906e28566f3f31721cd57dda637d7e41c430287804ac01911f56", size = 35469309, upload-time = "2025-07-22T13:58:21.917Z" }, + { url = "https://files.pythonhosted.org/packages/aa/77/8f8fae05a242ef639de963d7ae70a69d0da61d6d72f1207b8bbf74ffd3e7/playwright-1.54.0-py3-none-win_amd64.whl", hash = "sha256:9e5aee9ae5ab1fdd44cd64153313a2045b136fcbcfb2541cc0a3d909132671a2", size = 35469311, upload-time = "2025-07-22T13:58:24.707Z" }, + { url = "https://files.pythonhosted.org/packages/33/ff/99a6f4292a90504f2927d34032a4baf6adb498dc3f7cf0f3e0e22899e310/playwright-1.54.0-py3-none-win_arm64.whl", hash = "sha256:a975815971f7b8dca505c441a4c56de1aeb56a211290f8cc214eeef5524e8d75", size = 31239119, upload-time = "2025-07-22T13:58:27.56Z" }, +] + +[[package]] +name = "pluggy" +sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955, upload-time = "2024-04-20T21:34:42.531Z" } +source = { registry = "https://pypi.org/simple" } +version = "1.5.0" +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556, upload-time = "2024-04-20T21:34:40.434Z" }, +] + +[[package]] +dependencies = [ + { name = "cfgv" }, + { name = "identify" }, + { name = "nodeenv" }, + { name = "pyyaml" }, + { name = "virtualenv" }, +] +name = "pre-commit" +sdist = { url = "https://files.pythonhosted.org/packages/ff/29/7cf5bbc236333876e4b41f56e06857a87937ce4bf91e117a6991a2dbb02a/pre_commit-4.3.0.tar.gz", hash = "sha256:499fe450cc9d42e9d58e606262795ecb64dd05438943c62b66f6a8673da30b16", size = 193792, upload-time = "2025-08-09T18:56:14.651Z" } +source = { registry = "https://pypi.org/simple" } +version = "4.3.0" +wheels = [ + { url = "https://files.pythonhosted.org/packages/5b/a5/987a405322d78a73b66e39e4a90e4ef156fd7141bf71df987e50717c321b/pre_commit-4.3.0-py2.py3-none-any.whl", hash = "sha256:2b0747ad7e6e967169136edffee14c16e148a778a54e4f967921aa1ebf2308d8", size = 220965, upload-time = "2025-08-09T18:56:13.192Z" }, +] + +[[package]] +name = "psutil" +sdist = { url = "https://files.pythonhosted.org/packages/1f/5a/07871137bb752428aa4b659f910b399ba6f291156bdea939be3e96cae7cb/psutil-6.1.1.tar.gz", hash = "sha256:cf8496728c18f2d0b45198f06895be52f36611711746b7f30c464b422b50e2f5", size = 508502, upload-time = "2024-12-19T18:21:20.568Z" } +source = { registry = "https://pypi.org/simple" } +version = "6.1.1" +wheels = [ + { url = "https://files.pythonhosted.org/packages/61/99/ca79d302be46f7bdd8321089762dd4476ee725fce16fc2b2e1dbba8cac17/psutil-6.1.1-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:fc0ed7fe2231a444fc219b9c42d0376e0a9a1a72f16c5cfa0f68d19f1a0663e8", size = 247511, upload-time = "2024-12-19T18:21:45.163Z" }, + { url = "https://files.pythonhosted.org/packages/0b/6b/73dbde0dd38f3782905d4587049b9be64d76671042fdcaf60e2430c6796d/psutil-6.1.1-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:0bdd4eab935276290ad3cb718e9809412895ca6b5b334f5a9111ee6d9aff9377", size = 248985, upload-time = "2024-12-19T18:21:49.254Z" }, + { url = "https://files.pythonhosted.org/packages/17/38/c319d31a1d3f88c5b79c68b3116c129e5133f1822157dd6da34043e32ed6/psutil-6.1.1-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b6e06c20c05fe95a3d7302d74e7097756d4ba1247975ad6905441ae1b5b66003", size = 284488, upload-time = "2024-12-19T18:21:51.638Z" }, + { url = "https://files.pythonhosted.org/packages/9c/39/0f88a830a1c8a3aba27fededc642da37613c57cbff143412e3536f89784f/psutil-6.1.1-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:97f7cb9921fbec4904f522d972f0c0e1f4fabbdd4e0287813b21215074a0f160", size = 287477, upload-time = "2024-12-19T18:21:55.306Z" }, + { url = "https://files.pythonhosted.org/packages/47/da/99f4345d4ddf2845cb5b5bd0d93d554e84542d116934fde07a0c50bd4e9f/psutil-6.1.1-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:33431e84fee02bc84ea36d9e2c4a6d395d479c9dd9bba2376c1f6ee8f3a4e0b3", size = 289017, upload-time = "2024-12-19T18:21:57.875Z" }, + { url = "https://files.pythonhosted.org/packages/38/53/bd755c2896f4461fd4f36fa6a6dcb66a88a9e4b9fd4e5b66a77cf9d4a584/psutil-6.1.1-cp37-abi3-win32.whl", hash = "sha256:eaa912e0b11848c4d9279a93d7e2783df352b082f40111e078388701fd479e53", size = 250602, upload-time = "2024-12-19T18:22:08.808Z" }, + { url = "https://files.pythonhosted.org/packages/7b/d7/7831438e6c3ebbfa6e01a927127a6cb42ad3ab844247f3c5b96bea25d73d/psutil-6.1.1-cp37-abi3-win_amd64.whl", hash = "sha256:f35cfccb065fff93529d2afb4a2e89e363fe63ca1e4a5da22b603a85833c2649", size = 254444, upload-time = "2024-12-19T18:22:11.335Z" }, +] + +[[package]] +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +name = "pydantic" +sdist = { url = "https://files.pythonhosted.org/packages/00/dd/4325abf92c39ba8623b5af936ddb36ffcfe0beae70405d456ab1fb2f5b8c/pydantic-2.11.7.tar.gz", hash = "sha256:d989c3c6cb79469287b1569f7447a17848c998458d49ebe294e975b9baf0f0db", size = 788350, upload-time = "2025-06-14T08:33:17.137Z" } +source = { registry = "https://pypi.org/simple" } +version = "2.11.7" +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/c0/ec2b1c8712ca690e5d61979dee872603e92b8a32f94cc1b72d53beab008a/pydantic-2.11.7-py3-none-any.whl", hash = "sha256:dde5df002701f6de26248661f6835bbe296a47bf73990135c7d07ce741b9623b", size = 444782, upload-time = "2025-06-14T08:33:14.905Z" }, +] + +[[package]] +dependencies = [{ name = "typing-extensions" }] +name = "pydantic-core" +sdist = { url = "https://files.pythonhosted.org/packages/ad/88/5f2260bdfae97aabf98f1778d43f69574390ad787afb646292a638c923d4/pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc", size = 435195, upload-time = "2025-04-23T18:33:52.104Z" } +source = { registry = "https://pypi.org/simple" } +version = "2.33.2" +wheels = [ + { url = "https://files.pythonhosted.org/packages/46/8c/99040727b41f56616573a28771b1bfa08a3d3fe74d3d513f01251f79f172/pydantic_core-2.33.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1082dd3e2d7109ad8b7da48e1d4710c8d06c253cbc4a27c1cff4fbcaa97a9e3f", size = 2015688, upload-time = "2025-04-23T18:31:53.175Z" }, + { url = "https://files.pythonhosted.org/packages/3a/cc/5999d1eb705a6cefc31f0b4a90e9f7fc400539b1a1030529700cc1b51838/pydantic_core-2.33.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f517ca031dfc037a9c07e748cefd8d96235088b83b4f4ba8939105d20fa1dcd6", size = 1844808, upload-time = "2025-04-23T18:31:54.79Z" }, + { url = "https://files.pythonhosted.org/packages/6f/5e/a0a7b8885c98889a18b6e376f344da1ef323d270b44edf8174d6bce4d622/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a9f2c9dd19656823cb8250b0724ee9c60a82f3cdf68a080979d13092a3b0fef", size = 1885580, upload-time = "2025-04-23T18:31:57.393Z" }, + { url = "https://files.pythonhosted.org/packages/3b/2a/953581f343c7d11a304581156618c3f592435523dd9d79865903272c256a/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2b0a451c263b01acebe51895bfb0e1cc842a5c666efe06cdf13846c7418caa9a", size = 1973859, upload-time = "2025-04-23T18:31:59.065Z" }, + { url = "https://files.pythonhosted.org/packages/e6/55/f1a813904771c03a3f97f676c62cca0c0a4138654107c1b61f19c644868b/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ea40a64d23faa25e62a70ad163571c0b342b8bf66d5fa612ac0dec4f069d916", size = 2120810, upload-time = "2025-04-23T18:32:00.78Z" }, + { url = "https://files.pythonhosted.org/packages/aa/c3/053389835a996e18853ba107a63caae0b9deb4a276c6b472931ea9ae6e48/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fb2d542b4d66f9470e8065c5469ec676978d625a8b7a363f07d9a501a9cb36a", size = 2676498, upload-time = "2025-04-23T18:32:02.418Z" }, + { url = "https://files.pythonhosted.org/packages/eb/3c/f4abd740877a35abade05e437245b192f9d0ffb48bbbbd708df33d3cda37/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdac5d6ffa1b5a83bca06ffe7583f5576555e6c8b3a91fbd25ea7780f825f7d", size = 2000611, upload-time = "2025-04-23T18:32:04.152Z" }, + { url = "https://files.pythonhosted.org/packages/59/a7/63ef2fed1837d1121a894d0ce88439fe3e3b3e48c7543b2a4479eb99c2bd/pydantic_core-2.33.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04a1a413977ab517154eebb2d326da71638271477d6ad87a769102f7c2488c56", size = 2107924, upload-time = "2025-04-23T18:32:06.129Z" }, + { url = "https://files.pythonhosted.org/packages/04/8f/2551964ef045669801675f1cfc3b0d74147f4901c3ffa42be2ddb1f0efc4/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c8e7af2f4e0194c22b5b37205bfb293d166a7344a5b0d0eaccebc376546d77d5", size = 2063196, upload-time = "2025-04-23T18:32:08.178Z" }, + { url = "https://files.pythonhosted.org/packages/26/bd/d9602777e77fc6dbb0c7db9ad356e9a985825547dce5ad1d30ee04903918/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:5c92edd15cd58b3c2d34873597a1e20f13094f59cf88068adb18947df5455b4e", size = 2236389, upload-time = "2025-04-23T18:32:10.242Z" }, + { url = "https://files.pythonhosted.org/packages/42/db/0e950daa7e2230423ab342ae918a794964b053bec24ba8af013fc7c94846/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:65132b7b4a1c0beded5e057324b7e16e10910c106d43675d9bd87d4f38dde162", size = 2239223, upload-time = "2025-04-23T18:32:12.382Z" }, + { url = "https://files.pythonhosted.org/packages/58/4d/4f937099c545a8a17eb52cb67fe0447fd9a373b348ccfa9a87f141eeb00f/pydantic_core-2.33.2-cp313-cp313-win32.whl", hash = "sha256:52fb90784e0a242bb96ec53f42196a17278855b0f31ac7c3cc6f5c1ec4811849", size = 1900473, upload-time = "2025-04-23T18:32:14.034Z" }, + { url = "https://files.pythonhosted.org/packages/a0/75/4a0a9bac998d78d889def5e4ef2b065acba8cae8c93696906c3a91f310ca/pydantic_core-2.33.2-cp313-cp313-win_amd64.whl", hash = "sha256:c083a3bdd5a93dfe480f1125926afcdbf2917ae714bdb80b36d34318b2bec5d9", size = 1955269, upload-time = "2025-04-23T18:32:15.783Z" }, + { url = "https://files.pythonhosted.org/packages/f9/86/1beda0576969592f1497b4ce8e7bc8cbdf614c352426271b1b10d5f0aa64/pydantic_core-2.33.2-cp313-cp313-win_arm64.whl", hash = "sha256:e80b087132752f6b3d714f041ccf74403799d3b23a72722ea2e6ba2e892555b9", size = 1893921, upload-time = "2025-04-23T18:32:18.473Z" }, + { url = "https://files.pythonhosted.org/packages/a4/7d/e09391c2eebeab681df2b74bfe6c43422fffede8dc74187b2b0bf6fd7571/pydantic_core-2.33.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61c18fba8e5e9db3ab908620af374db0ac1baa69f0f32df4f61ae23f15e586ac", size = 1806162, upload-time = "2025-04-23T18:32:20.188Z" }, + { url = "https://files.pythonhosted.org/packages/f1/3d/847b6b1fed9f8ed3bb95a9ad04fbd0b212e832d4f0f50ff4d9ee5a9f15cf/pydantic_core-2.33.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95237e53bb015f67b63c91af7518a62a8660376a6a0db19b89acc77a4d6199f5", size = 1981560, upload-time = "2025-04-23T18:32:22.354Z" }, + { url = "https://files.pythonhosted.org/packages/6f/9a/e73262f6c6656262b5fdd723ad90f518f579b7bc8622e43a942eec53c938/pydantic_core-2.33.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9", size = 1935777, upload-time = "2025-04-23T18:32:25.088Z" }, +] + +[[package]] +dependencies = [{ name = "pydantic" }, { name = "python-dotenv" }, { name = "typing-inspection" }] +name = "pydantic-settings" +sdist = { url = "https://files.pythonhosted.org/packages/68/85/1ea668bbab3c50071ca613c6ab30047fb36ab0da1b92fa8f17bbc38fd36c/pydantic_settings-2.10.1.tar.gz", hash = "sha256:06f0062169818d0f5524420a360d632d5857b83cffd4d42fe29597807a1614ee", size = 172583, upload-time = "2025-06-24T13:26:46.841Z" } +source = { registry = "https://pypi.org/simple" } +version = "2.10.1" +wheels = [ + { url = "https://files.pythonhosted.org/packages/58/f0/427018098906416f580e3cf1366d3b1abfb408a0652e9f31600c24a1903c/pydantic_settings-2.10.1-py3-none-any.whl", hash = "sha256:a60952460b99cf661dc25c29c0ef171721f98bfcb52ef8d9ea4c943d7c8cc796", size = 45235, upload-time = "2025-06-24T13:26:45.485Z" }, +] + +[[package]] +dependencies = [{ name = "stdlib-list" }] +name = "pydeps" +sdist = { url = "https://files.pythonhosted.org/packages/5a/03/ce4baba41362297576f84f2d1906af25e43b46cc368afda4ac8bfe4bfd81/pydeps-3.0.1.tar.gz", hash = "sha256:a57415a8fae2ff6840a199b7dfcfecb90c37e4b9b54b58a111808a3440bc03bc", size = 53070, upload-time = "2025-02-04T11:50:10.167Z" } +source = { registry = "https://pypi.org/simple" } +version = "3.0.1" +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/ea/663366200286a95fa6ac0ea3a67510cc5799983b102bddc845d9370bf1c8/pydeps-3.0.1-py3-none-any.whl", hash = "sha256:7c86ee63c9ee6ddd088c840364981c5aa214a994d323bb7fa4724fca30829bee", size = 47596, upload-time = "2025-02-04T11:50:07.717Z" }, +] + +[[package]] +dependencies = [{ name = "typing-extensions" }] +name = "pyee" +sdist = { url = "https://files.pythonhosted.org/packages/95/03/1fd98d5841cd7964a27d729ccf2199602fe05eb7a405c1462eb7277945ed/pyee-13.0.0.tar.gz", hash = "sha256:b391e3c5a434d1f5118a25615001dbc8f669cf410ab67d04c4d4e07c55481c37", size = 31250, upload-time = "2025-03-17T18:53:15.955Z" } +source = { registry = "https://pypi.org/simple" } +version = "13.0.0" +wheels = [ + { url = "https://files.pythonhosted.org/packages/9b/4d/b9add7c84060d4c1906abe9a7e5359f2a60f7a9a4f67268b2766673427d8/pyee-13.0.0-py3-none-any.whl", hash = "sha256:48195a3cddb3b1515ce0695ed76036b5ccc2ef3a9f963ff9f77aec0139845498", size = 15730, upload-time = "2025-03-17T18:53:14.532Z" }, +] + +[[package]] +name = "pygments" +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +source = { registry = "https://pypi.org/simple" } +version = "2.19.2" +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, +] + +[[package]] +dependencies = [{ name = "markdown" }, { name = "pyyaml" }] +name = "pymdown-extensions" +sdist = { url = "https://files.pythonhosted.org/packages/55/b3/6d2b3f149bc5413b0a29761c2c5832d8ce904a1d7f621e86616d96f505cc/pymdown_extensions-10.16.1.tar.gz", hash = "sha256:aace82bcccba3efc03e25d584e6a22d27a8e17caa3f4dd9f207e49b787aa9a91", size = 853277, upload-time = "2025-07-28T16:19:34.167Z" } +source = { registry = "https://pypi.org/simple" } +version = "10.16.1" +wheels = [ + { url = "https://files.pythonhosted.org/packages/e4/06/43084e6cbd4b3bc0e80f6be743b2e79fbc6eed8de9ad8c629939fa55d972/pymdown_extensions-10.16.1-py3-none-any.whl", hash = "sha256:d6ba157a6c03146a7fb122b2b9a121300056384eafeec9c9f9e584adfdb2a32d", size = 266178, upload-time = "2025-07-28T16:19:31.401Z" }, +] + +[[package]] +name = "pyodide-py" +sdist = { url = "https://files.pythonhosted.org/packages/85/3d/bdb623dc6b1165c906b958b25f81ea2b0ba2f06ded5e491d272f9a54c35d/pyodide_py-0.28.1.tar.gz", hash = "sha256:11464c6e0e3063e7c0bfc2802f53c74f75fb0834190b86bb78769f84c635a18e", size = 52569, upload-time = "2025-08-04T21:31:55.431Z" } +source = { registry = "https://pypi.org/simple" } +version = "0.28.1" +wheels = [ + { url = "https://files.pythonhosted.org/packages/13/f9/a2533ac6831763c36dd1f4cb6ba9eebb8f41b1880be9e89e3b5994c6c1f8/pyodide_py-0.28.1-py3-none-any.whl", hash = "sha256:e158e58da4cee77d1dc825bdeddd689335a41b8a79b4fa4483a0f3c50e0454e7", size = 58478, upload-time = "2025-08-04T21:31:54.07Z" }, +] + +[[package]] +dependencies = [{ name = "nodeenv" }, { name = "typing-extensions" }] +name = "pyright" +sdist = { url = "https://files.pythonhosted.org/packages/fe/f6/35f885264ff08c960b23d1542038d8da86971c5d8c955cfab195a4f672d7/pyright-1.1.403.tar.gz", hash = "sha256:3ab69b9f41c67fb5bbb4d7a36243256f0d549ed3608678d381d5f51863921104", size = 3913526, upload-time = "2025-07-09T07:15:52.882Z" } +source = { registry = "https://pypi.org/simple" } +version = "1.1.403" +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/b6/b04e5c2f41a5ccad74a1a4759da41adb20b4bc9d59a5e08d29ba60084d07/pyright-1.1.403-py3-none-any.whl", hash = "sha256:c0eeca5aa76cbef3fcc271259bbd785753c7ad7bcac99a9162b4c4c7daed23b3", size = 5684504, upload-time = "2025-07-09T07:15:50.958Z" }, +] + +[[package]] +dependencies = [ + { name = "jinja2" }, + { name = "platformdirs" }, + { name = "pluggy" }, + { name = "requests" }, + { name = "rich" }, + { name = "toml" }, + { name = "typer" }, +] +name = "pyscript" +sdist = { url = "https://files.pythonhosted.org/packages/9d/4d/ee8606f71049fe29666e5541f0dd1a4f861dd3a6824334fac3c5aec3e8d9/pyscript-0.3.3.tar.gz", hash = "sha256:11fc64a3f187d8645c601ae6a80e3f0142e0dd9e0c5d3244b0ec508ca0d373f9", size = 20791, upload-time = "2024-09-09T13:02:34.234Z" } +source = { registry = "https://pypi.org/simple" } +version = "0.3.3" +wheels = [ + { url = "https://files.pythonhosted.org/packages/f2/d8/1881edf3b8653cf2f3b8005704126c738c151b6f8168a5806ea61f1efb5f/pyscript-0.3.3-py3-none-any.whl", hash = "sha256:320383f38e9eec6515dbe0c184d4ad9d9c58e2c98fb82ec09e8d8b2e93c9e62f", size = 15556, upload-time = "2024-09-09T13:02:32.756Z" }, +] + +[[package]] +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, +] +name = "pytest" +sdist = { url = "https://files.pythonhosted.org/packages/08/ba/45911d754e8eba3d5a841a5ce61a65a685ff1798421ac054f85aa8747dfb/pytest-8.4.1.tar.gz", hash = "sha256:7c67fd69174877359ed9371ec3af8a3d2b04741818c51e5e99cc1742251fa93c", size = 1517714, upload-time = "2025-06-18T05:48:06.109Z" } +source = { registry = "https://pypi.org/simple" } +version = "8.4.1" +wheels = [ + { url = "https://files.pythonhosted.org/packages/29/16/c8a903f4c4dffe7a12843191437d7cd8e32751d5de349d45d3fe69544e87/pytest-8.4.1-py3-none-any.whl", hash = "sha256:539c70ba6fcead8e78eebbf1115e8b589e7565830d7d006a8723f19ac8a0afb7", size = 365474, upload-time = "2025-06-18T05:48:03.955Z" }, +] + +[[package]] +dependencies = [{ name = "pytest" }] +name = "pytest-asyncio" +sdist = { url = "https://files.pythonhosted.org/packages/4e/51/f8794af39eeb870e87a8c8068642fc07bce0c854d6865d7dd0f2a9d338c2/pytest_asyncio-1.1.0.tar.gz", hash = "sha256:796aa822981e01b68c12e4827b8697108f7205020f24b5793b3c41555dab68ea", size = 46652, upload-time = "2025-07-16T04:29:26.393Z" } +source = { registry = "https://pypi.org/simple" } +version = "1.1.0" +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/9d/bf86eddabf8c6c9cb1ea9a869d6873b46f105a5d292d3a6f7071f5b07935/pytest_asyncio-1.1.0-py3-none-any.whl", hash = "sha256:5fe2d69607b0bd75c656d1211f969cadba035030156745ee09e7d71740e58ecf", size = 15157, upload-time = "2025-07-16T04:29:24.929Z" }, +] + +[[package]] +dependencies = [{ name = "pytest" }, { name = "requests" }] +name = "pytest-base-url" +sdist = { url = "https://files.pythonhosted.org/packages/ae/1a/b64ac368de6b993135cb70ca4e5d958a5c268094a3a2a4cac6f0021b6c4f/pytest_base_url-2.1.0.tar.gz", hash = "sha256:02748589a54f9e63fcbe62301d6b0496da0d10231b753e950c63e03aee745d45", size = 6702, upload-time = "2024-01-31T22:43:00.81Z" } +source = { registry = "https://pypi.org/simple" } +version = "2.1.0" +wheels = [ + { url = "https://files.pythonhosted.org/packages/98/1c/b00940ab9eb8ede7897443b771987f2f4a76f06be02f1b3f01eb7567e24a/pytest_base_url-2.1.0-py3-none-any.whl", hash = "sha256:3ad15611778764d451927b2a53240c1a7a591b521ea44cebfe45849d2d2812e6", size = 5302, upload-time = "2024-01-31T22:42:58.897Z" }, +] + +[[package]] +dependencies = [ + { name = "playwright" }, + { name = "pytest" }, + { name = "pytest-base-url" }, + { name = "python-slugify" }, +] +name = "pytest-playwright" +sdist = { url = "https://files.pythonhosted.org/packages/e3/47/38e292ad92134a00ea05e6fc4fc44577baaa38b0922ab7ea56312b7a6663/pytest_playwright-0.7.0.tar.gz", hash = "sha256:b3f2ea514bbead96d26376fac182f68dcd6571e7cb41680a89ff1673c05d60b6", size = 16666, upload-time = "2025-01-31T11:06:05.453Z" } +source = { registry = "https://pypi.org/simple" } +version = "0.7.0" +wheels = [ + { url = "https://files.pythonhosted.org/packages/d8/96/5f8a4545d783674f3de33f0ebc4db16cc76ce77a4c404d284f43f09125e3/pytest_playwright-0.7.0-py3-none-any.whl", hash = "sha256:2516d0871fa606634bfe32afbcc0342d68da2dbff97fe3459849e9c428486da2", size = 16618, upload-time = "2025-01-31T11:06:08.075Z" }, +] + +[[package]] +dependencies = [{ name = "packaging" }, { name = "pytest" }, { name = "termcolor" }] +name = "pytest-sugar" +sdist = { url = "https://files.pythonhosted.org/packages/f5/ac/5754f5edd6d508bc6493bc37d74b928f102a5fff82d9a80347e180998f08/pytest-sugar-1.0.0.tar.gz", hash = "sha256:6422e83258f5b0c04ce7c632176c7732cab5fdb909cb39cca5c9139f81276c0a", size = 14992, upload-time = "2024-02-01T18:30:36.735Z" } +source = { registry = "https://pypi.org/simple" } +version = "1.0.0" +wheels = [ + { url = "https://files.pythonhosted.org/packages/92/fb/889f1b69da2f13691de09a111c16c4766a433382d44aa0ecf221deded44a/pytest_sugar-1.0.0-py3-none-any.whl", hash = "sha256:70ebcd8fc5795dc457ff8b69d266a4e2e8a74ae0c3edc749381c64b5246c8dfd", size = 10171, upload-time = "2024-02-01T18:30:29.395Z" }, +] + +[[package]] +dependencies = [{ name = "six" }] +name = "python-dateutil" +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" } +source = { registry = "https://pypi.org/simple" } +version = "2.9.0.post0" +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 = "python-dotenv" +sdist = { url = "https://files.pythonhosted.org/packages/f6/b0/4bc07ccd3572a2f9df7e6782f52b0c6c90dcbb803ac4a167702d7d0dfe1e/python_dotenv-1.1.1.tar.gz", hash = "sha256:a8a6399716257f45be6a007360200409fce5cda2661e3dec71d23dc15f6189ab", size = 41978, upload-time = "2025-06-24T04:21:07.341Z" } +source = { registry = "https://pypi.org/simple" } +version = "1.1.1" +wheels = [ + { url = "https://files.pythonhosted.org/packages/5f/ed/539768cf28c661b5b068d66d96a2f155c4971a5d55684a514c1a0e0dec2f/python_dotenv-1.1.1-py3-none-any.whl", hash = "sha256:31f23644fe2602f88ff55e1f5c79ba497e01224ee7737937930c448e4d0e24dc", size = 20556, upload-time = "2025-06-24T04:21:06.073Z" }, +] + +[[package]] +dependencies = [{ name = "text-unidecode" }] +name = "python-slugify" +sdist = { url = "https://files.pythonhosted.org/packages/87/c7/5e1547c44e31da50a460df93af11a535ace568ef89d7a811069ead340c4a/python-slugify-8.0.4.tar.gz", hash = "sha256:59202371d1d05b54a9e7720c5e038f928f45daaffe41dd10822f3907b937c856", size = 10921, upload-time = "2024-02-08T18:32:45.488Z" } +source = { registry = "https://pypi.org/simple" } +version = "8.0.4" +wheels = [ + { url = "https://files.pythonhosted.org/packages/a4/62/02da182e544a51a5c3ccf4b03ab79df279f9c60c5e82d5e8bec7ca26ac11/python_slugify-8.0.4-py2.py3-none-any.whl", hash = "sha256:276540b79961052b66b7d116620b36518847f52d5fd9e3a70164fc8c50faa6b8", size = 10051, upload-time = "2024-02-08T18:32:43.911Z" }, +] + +[[package]] +name = "pywin32" +source = { registry = "https://pypi.org/simple" } +version = "311" +wheels = [ + { url = "https://files.pythonhosted.org/packages/a5/be/3fd5de0979fcb3994bfee0d65ed8ca9506a8a1260651b86174f6a86f52b3/pywin32-311-cp313-cp313-win32.whl", hash = "sha256:f95ba5a847cba10dd8c4d8fefa9f2a6cf283b8b88ed6178fa8a6c1ab16054d0d", size = 8705700, upload-time = "2025-07-14T20:13:26.471Z" }, + { url = "https://files.pythonhosted.org/packages/e3/28/e0a1909523c6890208295a29e05c2adb2126364e289826c0a8bc7297bd5c/pywin32-311-cp313-cp313-win_amd64.whl", hash = "sha256:718a38f7e5b058e76aee1c56ddd06908116d35147e133427e59a3983f703a20d", size = 9494700, upload-time = "2025-07-14T20:13:28.243Z" }, + { url = "https://files.pythonhosted.org/packages/04/bf/90339ac0f55726dce7d794e6d79a18a91265bdf3aa70b6b9ca52f35e022a/pywin32-311-cp313-cp313-win_arm64.whl", hash = "sha256:7b4075d959648406202d92a2310cb990fea19b535c7f4a78d3f5e10b926eeb8a", size = 8709318, upload-time = "2025-07-14T20:13:30.348Z" }, + { url = "https://files.pythonhosted.org/packages/c9/31/097f2e132c4f16d99a22bfb777e0fd88bd8e1c634304e102f313af69ace5/pywin32-311-cp314-cp314-win32.whl", hash = "sha256:b7a2c10b93f8986666d0c803ee19b5990885872a7de910fc460f9b0c2fbf92ee", size = 8840714, upload-time = "2025-07-14T20:13:32.449Z" }, + { url = "https://files.pythonhosted.org/packages/90/4b/07c77d8ba0e01349358082713400435347df8426208171ce297da32c313d/pywin32-311-cp314-cp314-win_amd64.whl", hash = "sha256:3aca44c046bd2ed8c90de9cb8427f581c479e594e99b5c0bb19b29c10fd6cb87", size = 9656800, upload-time = "2025-07-14T20:13:34.312Z" }, + { url = "https://files.pythonhosted.org/packages/c0/d2/21af5c535501a7233e734b8af901574572da66fcc254cb35d0609c9080dd/pywin32-311-cp314-cp314-win_arm64.whl", hash = "sha256:a508e2d9025764a8270f93111a970e1d0fbfc33f4153b388bb649b7eec4f9b42", size = 8932540, upload-time = "2025-07-14T20:13:36.379Z" }, +] + +[[package]] +name = "pyyaml" +sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631, upload-time = "2024-08-06T20:33:50.674Z" } +source = { registry = "https://pypi.org/simple" } +version = "6.0.2" +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309, upload-time = "2024-08-06T20:32:43.4Z" }, + { url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679, upload-time = "2024-08-06T20:32:44.801Z" }, + { url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428, upload-time = "2024-08-06T20:32:46.432Z" }, + { url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361, upload-time = "2024-08-06T20:32:51.188Z" }, + { url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523, upload-time = "2024-08-06T20:32:53.019Z" }, + { url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660, upload-time = "2024-08-06T20:32:54.708Z" }, + { url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597, upload-time = "2024-08-06T20:32:56.985Z" }, + { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527, upload-time = "2024-08-06T20:33:03.001Z" }, + { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446, upload-time = "2024-08-06T20:33:04.33Z" }, +] + +[[package]] +dependencies = [{ name = "pyyaml" }] +name = "pyyaml-env-tag" +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" } +source = { registry = "https://pypi.org/simple" } +version = "1.1" +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]] +dependencies = [{ name = "typing-extensions" }] +name = "reactivex" +sdist = { url = "https://files.pythonhosted.org/packages/ef/63/f776322df4d7b456446eff78c4e64f14c3c26d57d46b4e06c18807d5d99c/reactivex-4.0.4.tar.gz", hash = "sha256:e912e6591022ab9176df8348a653fe8c8fa7a301f26f9931c9d8c78a650e04e8", size = 119177, upload-time = "2022-07-16T07:11:53.689Z" } +source = { registry = "https://pypi.org/simple" } +version = "4.0.4" +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/3f/2ed8c1b8fe3fc2ed816ba40554ef703aad8c51700e2606c139fcf9b7f791/reactivex-4.0.4-py3-none-any.whl", hash = "sha256:0004796c420bd9e68aad8e65627d85a8e13f293de76656165dffbcb3a0e3fb6a", size = 217791, upload-time = "2022-07-16T07:11:52.061Z" }, +] + +[[package]] +name = "regex" +sdist = { url = "https://files.pythonhosted.org/packages/0b/de/e13fa6dc61d78b30ba47481f99933a3b49a57779d625c392d8036770a60d/regex-2025.7.34.tar.gz", hash = "sha256:9ead9765217afd04a86822dfcd4ed2747dfe426e887da413b15ff0ac2457e21a", size = 400714, upload-time = "2025-07-31T00:21:16.262Z" } +source = { registry = "https://pypi.org/simple" } +version = "2025.7.34" +wheels = [ + { url = "https://files.pythonhosted.org/packages/15/16/b709b2119975035169a25aa8e4940ca177b1a2e25e14f8d996d09130368e/regex-2025.7.34-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:c3c9740a77aeef3f5e3aaab92403946a8d34437db930a0280e7e81ddcada61f5", size = 485334, upload-time = "2025-07-31T00:19:56.58Z" }, + { url = "https://files.pythonhosted.org/packages/94/a6/c09136046be0595f0331bc58a0e5f89c2d324cf734e0b0ec53cf4b12a636/regex-2025.7.34-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:69ed3bc611540f2ea70a4080f853741ec698be556b1df404599f8724690edbcd", size = 289942, upload-time = "2025-07-31T00:19:57.943Z" }, + { url = "https://files.pythonhosted.org/packages/36/91/08fc0fd0f40bdfb0e0df4134ee37cfb16e66a1044ac56d36911fd01c69d2/regex-2025.7.34-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d03c6f9dcd562c56527c42b8530aad93193e0b3254a588be1f2ed378cdfdea1b", size = 285991, upload-time = "2025-07-31T00:19:59.837Z" }, + { url = "https://files.pythonhosted.org/packages/be/2f/99dc8f6f756606f0c214d14c7b6c17270b6bbe26d5c1f05cde9dbb1c551f/regex-2025.7.34-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6164b1d99dee1dfad33f301f174d8139d4368a9fb50bf0a3603b2eaf579963ad", size = 797415, upload-time = "2025-07-31T00:20:01.668Z" }, + { url = "https://files.pythonhosted.org/packages/62/cf/2fcdca1110495458ba4e95c52ce73b361cf1cafd8a53b5c31542cde9a15b/regex-2025.7.34-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1e4f4f62599b8142362f164ce776f19d79bdd21273e86920a7b604a4275b4f59", size = 862487, upload-time = "2025-07-31T00:20:03.142Z" }, + { url = "https://files.pythonhosted.org/packages/90/38/899105dd27fed394e3fae45607c1983e138273ec167e47882fc401f112b9/regex-2025.7.34-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:72a26dcc6a59c057b292f39d41465d8233a10fd69121fa24f8f43ec6294e5415", size = 910717, upload-time = "2025-07-31T00:20:04.727Z" }, + { url = "https://files.pythonhosted.org/packages/ee/f6/4716198dbd0bcc9c45625ac4c81a435d1c4d8ad662e8576dac06bab35b17/regex-2025.7.34-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d5273fddf7a3e602695c92716c420c377599ed3c853ea669c1fe26218867002f", size = 801943, upload-time = "2025-07-31T00:20:07.1Z" }, + { url = "https://files.pythonhosted.org/packages/40/5d/cff8896d27e4e3dd11dd72ac78797c7987eb50fe4debc2c0f2f1682eb06d/regex-2025.7.34-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c1844be23cd40135b3a5a4dd298e1e0c0cb36757364dd6cdc6025770363e06c1", size = 786664, upload-time = "2025-07-31T00:20:08.818Z" }, + { url = "https://files.pythonhosted.org/packages/10/29/758bf83cf7b4c34f07ac3423ea03cee3eb3176941641e4ccc05620f6c0b8/regex-2025.7.34-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:dde35e2afbbe2272f8abee3b9fe6772d9b5a07d82607b5788e8508974059925c", size = 856457, upload-time = "2025-07-31T00:20:10.328Z" }, + { url = "https://files.pythonhosted.org/packages/d7/30/c19d212b619963c5b460bfed0ea69a092c6a43cba52a973d46c27b3e2975/regex-2025.7.34-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:f3f6e8e7af516a7549412ce57613e859c3be27d55341a894aacaa11703a4c31a", size = 849008, upload-time = "2025-07-31T00:20:11.823Z" }, + { url = "https://files.pythonhosted.org/packages/9e/b8/3c35da3b12c87e3cc00010ef6c3a4ae787cff0bc381aa3d251def219969a/regex-2025.7.34-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:469142fb94a869beb25b5f18ea87646d21def10fbacb0bcb749224f3509476f0", size = 788101, upload-time = "2025-07-31T00:20:13.729Z" }, + { url = "https://files.pythonhosted.org/packages/47/80/2f46677c0b3c2b723b2c358d19f9346e714113865da0f5f736ca1a883bde/regex-2025.7.34-cp313-cp313-win32.whl", hash = "sha256:da7507d083ee33ccea1310447410c27ca11fb9ef18c95899ca57ff60a7e4d8f1", size = 264401, upload-time = "2025-07-31T00:20:15.233Z" }, + { url = "https://files.pythonhosted.org/packages/be/fa/917d64dd074682606a003cba33585c28138c77d848ef72fc77cbb1183849/regex-2025.7.34-cp313-cp313-win_amd64.whl", hash = "sha256:9d644de5520441e5f7e2db63aec2748948cc39ed4d7a87fd5db578ea4043d997", size = 275368, upload-time = "2025-07-31T00:20:16.711Z" }, + { url = "https://files.pythonhosted.org/packages/65/cd/f94383666704170a2154a5df7b16be28f0c27a266bffcd843e58bc84120f/regex-2025.7.34-cp313-cp313-win_arm64.whl", hash = "sha256:7bf1c5503a9f2cbd2f52d7e260acb3131b07b6273c470abb78568174fe6bde3f", size = 268482, upload-time = "2025-07-31T00:20:18.189Z" }, + { url = "https://files.pythonhosted.org/packages/ac/23/6376f3a23cf2f3c00514b1cdd8c990afb4dfbac3cb4a68b633c6b7e2e307/regex-2025.7.34-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:8283afe7042d8270cecf27cca558873168e771183d4d593e3c5fe5f12402212a", size = 485385, upload-time = "2025-07-31T00:20:19.692Z" }, + { url = "https://files.pythonhosted.org/packages/73/5b/6d4d3a0b4d312adbfd6d5694c8dddcf1396708976dd87e4d00af439d962b/regex-2025.7.34-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:6c053f9647e3421dd2f5dff8172eb7b4eec129df9d1d2f7133a4386319b47435", size = 289788, upload-time = "2025-07-31T00:20:21.941Z" }, + { url = "https://files.pythonhosted.org/packages/92/71/5862ac9913746e5054d01cb9fb8125b3d0802c0706ef547cae1e7f4428fa/regex-2025.7.34-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a16dd56bbcb7d10e62861c3cd000290ddff28ea142ffb5eb3470f183628011ac", size = 286136, upload-time = "2025-07-31T00:20:26.146Z" }, + { url = "https://files.pythonhosted.org/packages/27/df/5b505dc447eb71278eba10d5ec940769ca89c1af70f0468bfbcb98035dc2/regex-2025.7.34-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:69c593ff5a24c0d5c1112b0df9b09eae42b33c014bdca7022d6523b210b69f72", size = 797753, upload-time = "2025-07-31T00:20:27.919Z" }, + { url = "https://files.pythonhosted.org/packages/86/38/3e3dc953d13998fa047e9a2414b556201dbd7147034fbac129392363253b/regex-2025.7.34-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:98d0ce170fcde1a03b5df19c5650db22ab58af375aaa6ff07978a85c9f250f0e", size = 863263, upload-time = "2025-07-31T00:20:29.803Z" }, + { url = "https://files.pythonhosted.org/packages/68/e5/3ff66b29dde12f5b874dda2d9dec7245c2051f2528d8c2a797901497f140/regex-2025.7.34-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d72765a4bff8c43711d5b0f5b452991a9947853dfa471972169b3cc0ba1d0751", size = 910103, upload-time = "2025-07-31T00:20:31.313Z" }, + { url = "https://files.pythonhosted.org/packages/9e/fe/14176f2182125977fba3711adea73f472a11f3f9288c1317c59cd16ad5e6/regex-2025.7.34-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4494f8fd95a77eb434039ad8460e64d57baa0434f1395b7da44015bef650d0e4", size = 801709, upload-time = "2025-07-31T00:20:33.323Z" }, + { url = "https://files.pythonhosted.org/packages/5a/0d/80d4e66ed24f1ba876a9e8e31b709f9fd22d5c266bf5f3ab3c1afe683d7d/regex-2025.7.34-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4f42b522259c66e918a0121a12429b2abcf696c6f967fa37bdc7b72e61469f98", size = 786726, upload-time = "2025-07-31T00:20:35.252Z" }, + { url = "https://files.pythonhosted.org/packages/12/75/c3ebb30e04a56c046f5c85179dc173818551037daae2c0c940c7b19152cb/regex-2025.7.34-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:aaef1f056d96a0a5d53ad47d019d5b4c66fe4be2da87016e0d43b7242599ffc7", size = 857306, upload-time = "2025-07-31T00:20:37.12Z" }, + { url = "https://files.pythonhosted.org/packages/b1/b2/a4dc5d8b14f90924f27f0ac4c4c4f5e195b723be98adecc884f6716614b6/regex-2025.7.34-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:656433e5b7dccc9bc0da6312da8eb897b81f5e560321ec413500e5367fcd5d47", size = 848494, upload-time = "2025-07-31T00:20:38.818Z" }, + { url = "https://files.pythonhosted.org/packages/0d/21/9ac6e07a4c5e8646a90b56b61f7e9dac11ae0747c857f91d3d2bc7c241d9/regex-2025.7.34-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e91eb2c62c39705e17b4d42d4b86c4e86c884c0d15d9c5a47d0835f8387add8e", size = 787850, upload-time = "2025-07-31T00:20:40.478Z" }, + { url = "https://files.pythonhosted.org/packages/be/6c/d51204e28e7bc54f9a03bb799b04730d7e54ff2718862b8d4e09e7110a6a/regex-2025.7.34-cp314-cp314-win32.whl", hash = "sha256:f978ddfb6216028c8f1d6b0f7ef779949498b64117fc35a939022f67f810bdcb", size = 269730, upload-time = "2025-07-31T00:20:42.253Z" }, + { url = "https://files.pythonhosted.org/packages/74/52/a7e92d02fa1fdef59d113098cb9f02c5d03289a0e9f9e5d4d6acccd10677/regex-2025.7.34-cp314-cp314-win_amd64.whl", hash = "sha256:4b7dc33b9b48fb37ead12ffc7bdb846ac72f99a80373c4da48f64b373a7abeae", size = 278640, upload-time = "2025-07-31T00:20:44.42Z" }, + { url = "https://files.pythonhosted.org/packages/d1/78/a815529b559b1771080faa90c3ab401730661f99d495ab0071649f139ebd/regex-2025.7.34-cp314-cp314-win_arm64.whl", hash = "sha256:4b8c4d39f451e64809912c82392933d80fe2e4a87eeef8859fcc5380d0173c64", size = 271757, upload-time = "2025-07-31T00:20:46.355Z" }, +] + +[[package]] +dependencies = [{ name = "certifi" }, { name = "charset-normalizer" }, { name = "idna" }, { name = "urllib3" }] +name = "requests" +sdist = { url = "https://files.pythonhosted.org/packages/9d/be/10918a2eac4ae9f02f6cfe6414b7a155ccd8f7f9d4380d62fd5b955065c3/requests-2.31.0.tar.gz", hash = "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1", size = 110794, upload-time = "2023-05-22T15:12:44.175Z" } +source = { registry = "https://pypi.org/simple" } +version = "2.31.0" +wheels = [ + { url = "https://files.pythonhosted.org/packages/70/8e/0e2d847013cb52cd35b38c009bb167a1a26b2ce6cd6965bf26b47bc0bf44/requests-2.31.0-py3-none-any.whl", hash = "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f", size = 62574, upload-time = "2023-05-22T15:12:42.313Z" }, +] + +[[package]] +dependencies = [{ name = "markdown-it-py" }, { name = "pygments" }] +name = "rich" +sdist = { url = "https://files.pythonhosted.org/packages/b3/01/c954e134dc440ab5f96952fe52b4fdc64225530320a910473c1fe270d9aa/rich-13.7.1.tar.gz", hash = "sha256:9be308cb1fe2f1f57d67ce99e95af38a1e2bc71ad9813b0e247cf7ffbcc3a432", size = 221248, upload-time = "2024-02-28T14:51:19.472Z" } +source = { registry = "https://pypi.org/simple" } +version = "13.7.1" +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/67/a37f6214d0e9fe57f6ae54b2956d550ca8365857f42a1ce0392bb21d9410/rich-13.7.1-py3-none-any.whl", hash = "sha256:4edbae314f59eb482f54e9e30bf00d33350aaa94f4bfcd4e9e3110e64d0d7222", size = 240681, upload-time = "2024-02-28T14:51:14.353Z" }, +] + +[[package]] +name = "ruff" +sdist = { url = "https://files.pythonhosted.org/packages/4a/45/2e403fa7007816b5fbb324cb4f8ed3c7402a927a0a0cb2b6279879a8bfdc/ruff-0.12.9.tar.gz", hash = "sha256:fbd94b2e3c623f659962934e52c2bea6fc6da11f667a427a368adaf3af2c866a", size = 5254702, upload-time = "2025-08-14T16:08:55.2Z" } +source = { registry = "https://pypi.org/simple" } +version = "0.12.9" +wheels = [ + { url = "https://files.pythonhosted.org/packages/ad/20/53bf098537adb7b6a97d98fcdebf6e916fcd11b2e21d15f8c171507909cc/ruff-0.12.9-py3-none-linux_armv6l.whl", hash = "sha256:fcebc6c79fcae3f220d05585229463621f5dbf24d79fdc4936d9302e177cfa3e", size = 11759705, upload-time = "2025-08-14T16:08:12.968Z" }, + { url = "https://files.pythonhosted.org/packages/20/4d/c764ee423002aac1ec66b9d541285dd29d2c0640a8086c87de59ebbe80d5/ruff-0.12.9-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:aed9d15f8c5755c0e74467731a007fcad41f19bcce41cd75f768bbd687f8535f", size = 12527042, upload-time = "2025-08-14T16:08:16.54Z" }, + { url = "https://files.pythonhosted.org/packages/8b/45/cfcdf6d3eb5fc78a5b419e7e616d6ccba0013dc5b180522920af2897e1be/ruff-0.12.9-py3-none-macosx_11_0_arm64.whl", hash = "sha256:5b15ea354c6ff0d7423814ba6d44be2807644d0c05e9ed60caca87e963e93f70", size = 11724457, upload-time = "2025-08-14T16:08:18.686Z" }, + { url = "https://files.pythonhosted.org/packages/72/e6/44615c754b55662200c48bebb02196dbb14111b6e266ab071b7e7297b4ec/ruff-0.12.9-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d596c2d0393c2502eaabfef723bd74ca35348a8dac4267d18a94910087807c53", size = 11949446, upload-time = "2025-08-14T16:08:21.059Z" }, + { url = "https://files.pythonhosted.org/packages/fd/d1/9b7d46625d617c7df520d40d5ac6cdcdf20cbccb88fad4b5ecd476a6bb8d/ruff-0.12.9-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1b15599931a1a7a03c388b9c5df1bfa62be7ede6eb7ef753b272381f39c3d0ff", size = 11566350, upload-time = "2025-08-14T16:08:23.433Z" }, + { url = "https://files.pythonhosted.org/packages/59/20/b73132f66f2856bc29d2d263c6ca457f8476b0bbbe064dac3ac3337a270f/ruff-0.12.9-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3d02faa2977fb6f3f32ddb7828e212b7dd499c59eb896ae6c03ea5c303575756", size = 13270430, upload-time = "2025-08-14T16:08:25.837Z" }, + { url = "https://files.pythonhosted.org/packages/a2/21/eaf3806f0a3d4c6be0a69d435646fba775b65f3f2097d54898b0fd4bb12e/ruff-0.12.9-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:17d5b6b0b3a25259b69ebcba87908496e6830e03acfb929ef9fd4c58675fa2ea", size = 14264717, upload-time = "2025-08-14T16:08:27.907Z" }, + { url = "https://files.pythonhosted.org/packages/d2/82/1d0c53bd37dcb582b2c521d352fbf4876b1e28bc0d8894344198f6c9950d/ruff-0.12.9-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:72db7521860e246adbb43f6ef464dd2a532ef2ef1f5dd0d470455b8d9f1773e0", size = 13684331, upload-time = "2025-08-14T16:08:30.352Z" }, + { url = "https://files.pythonhosted.org/packages/3b/2f/1c5cf6d8f656306d42a686f1e207f71d7cebdcbe7b2aa18e4e8a0cb74da3/ruff-0.12.9-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a03242c1522b4e0885af63320ad754d53983c9599157ee33e77d748363c561ce", size = 12739151, upload-time = "2025-08-14T16:08:32.55Z" }, + { url = "https://files.pythonhosted.org/packages/47/09/25033198bff89b24d734e6479e39b1968e4c992e82262d61cdccaf11afb9/ruff-0.12.9-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fc83e4e9751e6c13b5046d7162f205d0a7bac5840183c5beebf824b08a27340", size = 12954992, upload-time = "2025-08-14T16:08:34.816Z" }, + { url = "https://files.pythonhosted.org/packages/52/8e/d0dbf2f9dca66c2d7131feefc386523404014968cd6d22f057763935ab32/ruff-0.12.9-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:881465ed56ba4dd26a691954650de6ad389a2d1fdb130fe51ff18a25639fe4bb", size = 12899569, upload-time = "2025-08-14T16:08:36.852Z" }, + { url = "https://files.pythonhosted.org/packages/a0/bd/b614d7c08515b1428ed4d3f1d4e3d687deffb2479703b90237682586fa66/ruff-0.12.9-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:43f07a3ccfc62cdb4d3a3348bf0588358a66da756aa113e071b8ca8c3b9826af", size = 11751983, upload-time = "2025-08-14T16:08:39.314Z" }, + { url = "https://files.pythonhosted.org/packages/58/d6/383e9f818a2441b1a0ed898d7875f11273f10882f997388b2b51cb2ae8b5/ruff-0.12.9-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:07adb221c54b6bba24387911e5734357f042e5669fa5718920ee728aba3cbadc", size = 11538635, upload-time = "2025-08-14T16:08:41.297Z" }, + { url = "https://files.pythonhosted.org/packages/20/9c/56f869d314edaa9fc1f491706d1d8a47747b9d714130368fbd69ce9024e9/ruff-0.12.9-py3-none-musllinux_1_2_i686.whl", hash = "sha256:f5cd34fabfdea3933ab85d72359f118035882a01bff15bd1d2b15261d85d5f66", size = 12534346, upload-time = "2025-08-14T16:08:43.39Z" }, + { url = "https://files.pythonhosted.org/packages/bd/4b/d8b95c6795a6c93b439bc913ee7a94fda42bb30a79285d47b80074003ee7/ruff-0.12.9-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:f6be1d2ca0686c54564da8e7ee9e25f93bdd6868263805f8c0b8fc6a449db6d7", size = 13017021, upload-time = "2025-08-14T16:08:45.889Z" }, + { url = "https://files.pythonhosted.org/packages/c7/c1/5f9a839a697ce1acd7af44836f7c2181cdae5accd17a5cb85fcbd694075e/ruff-0.12.9-py3-none-win32.whl", hash = "sha256:cc7a37bd2509974379d0115cc5608a1a4a6c4bff1b452ea69db83c8855d53f93", size = 11734785, upload-time = "2025-08-14T16:08:48.062Z" }, + { url = "https://files.pythonhosted.org/packages/fa/66/cdddc2d1d9a9f677520b7cfc490d234336f523d4b429c1298de359a3be08/ruff-0.12.9-py3-none-win_amd64.whl", hash = "sha256:6fb15b1977309741d7d098c8a3cb7a30bc112760a00fb6efb7abc85f00ba5908", size = 12840654, upload-time = "2025-08-14T16:08:50.158Z" }, + { url = "https://files.pythonhosted.org/packages/ac/fd/669816bc6b5b93b9586f3c1d87cd6bc05028470b3ecfebb5938252c47a35/ruff-0.12.9-py3-none-win_arm64.whl", hash = "sha256:63c8c819739d86b96d500cce885956a1a48ab056bbcbc61b747ad494b2485089", size = 11949623, upload-time = "2025-08-14T16:08:52.233Z" }, +] + +[[package]] +name = "safetensors" +sdist = { url = "https://files.pythonhosted.org/packages/ac/cc/738f3011628920e027a11754d9cae9abec1aed00f7ae860abbf843755233/safetensors-0.6.2.tar.gz", hash = "sha256:43ff2aa0e6fa2dc3ea5524ac7ad93a9839256b8703761e76e2d0b2a3fa4f15d9", size = 197968, upload-time = "2025-08-08T13:13:58.654Z" } +source = { registry = "https://pypi.org/simple" } +version = "0.6.2" +wheels = [ + { url = "https://files.pythonhosted.org/packages/4d/b1/3f5fd73c039fc87dba3ff8b5d528bfc5a32b597fea8e7a6a4800343a17c7/safetensors-0.6.2-cp38-abi3-macosx_10_12_x86_64.whl", hash = "sha256:9c85ede8ec58f120bad982ec47746981e210492a6db876882aa021446af8ffba", size = 454797, upload-time = "2025-08-08T13:13:52.066Z" }, + { url = "https://files.pythonhosted.org/packages/8c/c9/bb114c158540ee17907ec470d01980957fdaf87b4aa07914c24eba87b9c6/safetensors-0.6.2-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:d6675cf4b39c98dbd7d940598028f3742e0375a6b4d4277e76beb0c35f4b843b", size = 432206, upload-time = "2025-08-08T13:13:50.931Z" }, + { url = "https://files.pythonhosted.org/packages/d3/8e/f70c34e47df3110e8e0bb268d90db8d4be8958a54ab0336c9be4fe86dac8/safetensors-0.6.2-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d2d2b3ce1e2509c68932ca03ab8f20570920cd9754b05063d4368ee52833ecd", size = 473261, upload-time = "2025-08-08T13:13:41.259Z" }, + { url = "https://files.pythonhosted.org/packages/2a/f5/be9c6a7c7ef773e1996dc214e73485286df1836dbd063e8085ee1976f9cb/safetensors-0.6.2-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:93de35a18f46b0f5a6a1f9e26d91b442094f2df02e9fd7acf224cfec4238821a", size = 485117, upload-time = "2025-08-08T13:13:43.506Z" }, + { url = "https://files.pythonhosted.org/packages/c9/55/23f2d0a2c96ed8665bf17a30ab4ce5270413f4d74b6d87dd663258b9af31/safetensors-0.6.2-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:89a89b505f335640f9120fac65ddeb83e40f1fd081cb8ed88b505bdccec8d0a1", size = 616154, upload-time = "2025-08-08T13:13:45.096Z" }, + { url = "https://files.pythonhosted.org/packages/98/c6/affb0bd9ce02aa46e7acddbe087912a04d953d7a4d74b708c91b5806ef3f/safetensors-0.6.2-cp38-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fc4d0d0b937e04bdf2ae6f70cd3ad51328635fe0e6214aa1fc811f3b576b3bda", size = 520713, upload-time = "2025-08-08T13:13:46.25Z" }, + { url = "https://files.pythonhosted.org/packages/fe/5d/5a514d7b88e310c8b146e2404e0dc161282e78634d9358975fd56dfd14be/safetensors-0.6.2-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8045db2c872db8f4cbe3faa0495932d89c38c899c603f21e9b6486951a5ecb8f", size = 485835, upload-time = "2025-08-08T13:13:49.373Z" }, + { url = "https://files.pythonhosted.org/packages/7a/7b/4fc3b2ba62c352b2071bea9cfbad330fadda70579f617506ae1a2f129cab/safetensors-0.6.2-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:81e67e8bab9878bb568cffbc5f5e655adb38d2418351dc0859ccac158f753e19", size = 521503, upload-time = "2025-08-08T13:13:47.651Z" }, + { url = "https://files.pythonhosted.org/packages/5a/50/0057e11fe1f3cead9254315a6c106a16dd4b1a19cd247f7cc6414f6b7866/safetensors-0.6.2-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:b0e4d029ab0a0e0e4fdf142b194514695b1d7d3735503ba700cf36d0fc7136ce", size = 652256, upload-time = "2025-08-08T13:13:53.167Z" }, + { url = "https://files.pythonhosted.org/packages/e9/29/473f789e4ac242593ac1656fbece6e1ecd860bb289e635e963667807afe3/safetensors-0.6.2-cp38-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:fa48268185c52bfe8771e46325a1e21d317207bcabcb72e65c6e28e9ffeb29c7", size = 747281, upload-time = "2025-08-08T13:13:54.656Z" }, + { url = "https://files.pythonhosted.org/packages/68/52/f7324aad7f2df99e05525c84d352dc217e0fa637a4f603e9f2eedfbe2c67/safetensors-0.6.2-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:d83c20c12c2d2f465997c51b7ecb00e407e5f94d7dec3ea0cc11d86f60d3fde5", size = 692286, upload-time = "2025-08-08T13:13:55.884Z" }, + { url = "https://files.pythonhosted.org/packages/ad/fe/cad1d9762868c7c5dc70c8620074df28ebb1a8e4c17d4c0cb031889c457e/safetensors-0.6.2-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:d944cea65fad0ead848b6ec2c37cc0b197194bec228f8020054742190e9312ac", size = 655957, upload-time = "2025-08-08T13:13:57.029Z" }, + { url = "https://files.pythonhosted.org/packages/59/a7/e2158e17bbe57d104f0abbd95dff60dda916cf277c9f9663b4bf9bad8b6e/safetensors-0.6.2-cp38-abi3-win32.whl", hash = "sha256:cab75ca7c064d3911411461151cb69380c9225798a20e712b102edda2542ddb1", size = 308926, upload-time = "2025-08-08T13:14:01.095Z" }, + { url = "https://files.pythonhosted.org/packages/2c/c3/c0be1135726618dc1e28d181b8c442403d8dbb9e273fd791de2d4384bcdd/safetensors-0.6.2-cp38-abi3-win_amd64.whl", hash = "sha256:c7b214870df923cbc1593c3faee16bec59ea462758699bd3fee399d00aac072c", size = 320192, upload-time = "2025-08-08T13:13:59.467Z" }, +] + +[[package]] +name = "setuptools" +sdist = { url = "https://files.pythonhosted.org/packages/18/5d/3bf57dcd21979b887f014ea83c24ae194cfcd12b9e0fda66b957c69d1fca/setuptools-80.9.0.tar.gz", hash = "sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c", size = 1319958, upload-time = "2025-05-27T00:56:51.443Z" } +source = { registry = "https://pypi.org/simple" } +version = "80.9.0" +wheels = [ + { url = "https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl", hash = "sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922", size = 1201486, upload-time = "2025-05-27T00:56:49.664Z" }, +] + +[[package]] +name = "six" +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" } +source = { registry = "https://pypi.org/simple" } +version = "1.17.0" +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 = "sniffio" +sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" } +source = { registry = "https://pypi.org/simple" } +version = "1.3.1" +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, +] + +[[package]] +name = "soupsieve" +sdist = { url = "https://files.pythonhosted.org/packages/3f/f4/4a80cd6ef364b2e8b65b15816a843c0980f7a5a2b4dc701fc574952aa19f/soupsieve-2.7.tar.gz", hash = "sha256:ad282f9b6926286d2ead4750552c8a6142bc4c783fd66b0293547c8fe6ae126a", size = 103418, upload-time = "2025-04-20T18:50:08.518Z" } +source = { registry = "https://pypi.org/simple" } +version = "2.7" +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/9c/0e6afc12c269578be5c0c1c9f4b49a8d32770a080260c333ac04cc1c832d/soupsieve-2.7-py3-none-any.whl", hash = "sha256:6e60cc5c1ffaf1cebcc12e8188320b72071e922c2e897f737cadce79ad5d30c4", size = 36677, upload-time = "2025-04-20T18:50:07.196Z" }, +] + +[[package]] +dependencies = [{ name = "anyio" }] +name = "starlette" +sdist = { url = "https://files.pythonhosted.org/packages/04/57/d062573f391d062710d4088fa1369428c38d51460ab6fedff920efef932e/starlette-0.47.2.tar.gz", hash = "sha256:6ae9aa5db235e4846decc1e7b79c4f346adf41e9777aebeb49dfd09bbd7023d8", size = 2583948, upload-time = "2025-07-20T17:31:58.522Z" } +source = { registry = "https://pypi.org/simple" } +version = "0.47.2" +wheels = [ + { url = "https://files.pythonhosted.org/packages/f7/1f/b876b1f83aef204198a42dc101613fefccb32258e5428b5f9259677864b4/starlette-0.47.2-py3-none-any.whl", hash = "sha256:c5847e96134e5c5371ee9fac6fdf1a67336d5815e09eb2a01fdb57a351ef915b", size = 72984, upload-time = "2025-07-20T17:31:56.738Z" }, +] + +[[package]] +name = "stdlib-list" +sdist = { url = "https://files.pythonhosted.org/packages/5d/09/8d5c564931ae23bef17420a6c72618463a59222ca4291a7dd88de8a0d490/stdlib_list-0.11.1.tar.gz", hash = "sha256:95ebd1d73da9333bba03ccc097f5bac05e3aa03e6822a0c0290f87e1047f1857", size = 60442, upload-time = "2025-02-18T15:39:38.769Z" } +source = { registry = "https://pypi.org/simple" } +version = "0.11.1" +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/c7/4102536de33c19d090ed2b04e90e7452e2e3dc653cf3323208034eaaca27/stdlib_list-0.11.1-py3-none-any.whl", hash = "sha256:9029ea5e3dfde8cd4294cfd4d1797be56a67fc4693c606181730148c3fd1da29", size = 83620, upload-time = "2025-02-18T15:39:37.02Z" }, +] + +[[package]] +dependencies = [{ name = "mpmath" }] +name = "sympy" +sdist = { url = "https://files.pythonhosted.org/packages/ca/99/5a5b6f19ff9f083671ddf7b9632028436167cd3d33e11015754e41b249a4/sympy-1.13.1.tar.gz", hash = "sha256:9cebf7e04ff162015ce31c9c6c9144daa34a93bd082f54fd8f12deca4f47515f", size = 7533040, upload-time = "2024-07-19T09:26:51.238Z" } +source = { registry = "https://pypi.org/simple" } +version = "1.13.1" +wheels = [ + { url = "https://files.pythonhosted.org/packages/b2/fe/81695a1aa331a842b582453b605175f419fe8540355886031328089d840a/sympy-1.13.1-py3-none-any.whl", hash = "sha256:db36cdc64bf61b9b24578b6f7bab1ecdd2452cf008f34faa33776680c26d66f8", size = 6189177, upload-time = "2024-07-19T09:26:48.863Z" }, +] + +[[package]] +dependencies = [ + { name = "colorama" }, + { name = "mslex", marker = "sys_platform == 'win32'" }, + { name = "psutil" }, + { name = "tomli", marker = "python_full_version < '4'" }, +] +name = "taskipy" +sdist = { url = "https://files.pythonhosted.org/packages/c7/44/572261df3db9c6c3332f8618fafeb07a578fd18b06673c73f000f3586749/taskipy-1.14.1.tar.gz", hash = "sha256:410fbcf89692dfd4b9f39c2b49e1750b0a7b81affd0e2d7ea8c35f9d6a4774ed", size = 14475, upload-time = "2024-11-26T16:37:46.155Z" } +source = { registry = "https://pypi.org/simple" } +version = "1.14.1" +wheels = [ + { url = "https://files.pythonhosted.org/packages/55/97/4e4cfb1391c81e926bebe3d68d5231b5dbc3bb41c6ba48349e68a881462d/taskipy-1.14.1-py3-none-any.whl", hash = "sha256:6e361520f29a0fd2159848e953599f9c75b1d0b047461e4965069caeb94908f1", size = 13052, upload-time = "2024-11-26T16:37:44.546Z" }, +] + +[[package]] +name = "termcolor" +sdist = { url = "https://files.pythonhosted.org/packages/ca/6c/3d75c196ac07ac8749600b60b03f4f6094d54e132c4d94ebac6ee0e0add0/termcolor-3.1.0.tar.gz", hash = "sha256:6a6dd7fbee581909eeec6a756cff1d7f7c376063b14e4a298dc4980309e55970", size = 14324, upload-time = "2025-04-30T11:37:53.791Z" } +source = { registry = "https://pypi.org/simple" } +version = "3.1.0" +wheels = [ + { url = "https://files.pythonhosted.org/packages/4f/bd/de8d508070629b6d84a30d01d57e4a65c69aa7f5abe7560b8fad3b50ea59/termcolor-3.1.0-py3-none-any.whl", hash = "sha256:591dd26b5c2ce03b9e43f391264626557873ce1d379019786f99b0c2bee140aa", size = 7684, upload-time = "2025-04-30T11:37:52.382Z" }, +] + +[[package]] +dependencies = [ + { name = "docker" }, + { name = "python-dotenv" }, + { name = "typing-extensions" }, + { name = "urllib3" }, + { name = "wrapt" }, +] +name = "testcontainers" +sdist = { url = "https://files.pythonhosted.org/packages/d3/62/01d9f648e9b943175e0dcddf749cf31c769665d8ba08df1e989427163f33/testcontainers-4.12.0.tar.gz", hash = "sha256:13ee89cae995e643f225665aad8b200b25c4f219944a6f9c0b03249ec3f31b8d", size = 66631, upload-time = "2025-07-21T20:32:26.37Z" } +source = { registry = "https://pypi.org/simple" } +version = "4.12.0" +wheels = [ + { url = "https://files.pythonhosted.org/packages/b2/e8/9e2c392e5d671afda47b917597cac8fde6a452f5776c4c9ceb93fbd2889f/testcontainers-4.12.0-py3-none-any.whl", hash = "sha256:26caef57e642d5e8c5fcc593881cf7df3ab0f0dc9170fad22765b184e226ab15", size = 111791, upload-time = "2025-07-21T20:32:25.038Z" }, +] + +[[package]] +name = "text-unidecode" +sdist = { url = "https://files.pythonhosted.org/packages/ab/e2/e9a00f0ccb71718418230718b3d900e71a5d16e701a3dae079a21e9cd8f8/text-unidecode-1.3.tar.gz", hash = "sha256:bad6603bb14d279193107714b288be206cac565dfa49aa5b105294dd5c4aab93", size = 76885, upload-time = "2019-08-30T21:36:45.405Z" } +source = { registry = "https://pypi.org/simple" } +version = "1.3" +wheels = [ + { url = "https://files.pythonhosted.org/packages/a6/a5/c0b6468d3824fe3fde30dbb5e1f687b291608f9473681bbf7dabbf5a87d7/text_unidecode-1.3-py2.py3-none-any.whl", hash = "sha256:1311f10e8b895935241623731c2ba64f4c455287888b18189350b67134a822e8", size = 78154, upload-time = "2019-08-30T21:37:03.543Z" }, +] + +[[package]] +dependencies = [{ name = "huggingface-hub" }] +name = "tokenizers" +sdist = { url = "https://files.pythonhosted.org/packages/c2/2f/402986d0823f8d7ca139d969af2917fefaa9b947d1fb32f6168c509f2492/tokenizers-0.21.4.tar.gz", hash = "sha256:fa23f85fbc9a02ec5c6978da172cdcbac23498c3ca9f3645c5c68740ac007880", size = 351253, upload-time = "2025-07-28T15:48:54.325Z" } +source = { registry = "https://pypi.org/simple" } +version = "0.21.4" +wheels = [ + { url = "https://files.pythonhosted.org/packages/98/c6/fdb6f72bf6454f52eb4a2510be7fb0f614e541a2554d6210e370d85efff4/tokenizers-0.21.4-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:2ccc10a7c3bcefe0f242867dc914fc1226ee44321eb618cfe3019b5df3400133", size = 2863987, upload-time = "2025-07-28T15:48:44.877Z" }, + { url = "https://files.pythonhosted.org/packages/8d/a6/28975479e35ddc751dc1ddc97b9b69bf7fcf074db31548aab37f8116674c/tokenizers-0.21.4-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:5e2f601a8e0cd5be5cc7506b20a79112370b9b3e9cb5f13f68ab11acd6ca7d60", size = 2732457, upload-time = "2025-07-28T15:48:43.265Z" }, + { url = "https://files.pythonhosted.org/packages/aa/8f/24f39d7b5c726b7b0be95dca04f344df278a3fe3a4deb15a975d194cbb32/tokenizers-0.21.4-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:39b376f5a1aee67b4d29032ee85511bbd1b99007ec735f7f35c8a2eb104eade5", size = 3012624, upload-time = "2025-07-28T13:22:43.895Z" }, + { url = "https://files.pythonhosted.org/packages/58/47/26358925717687a58cb74d7a508de96649544fad5778f0cd9827398dc499/tokenizers-0.21.4-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2107ad649e2cda4488d41dfd031469e9da3fcbfd6183e74e4958fa729ffbf9c6", size = 2939681, upload-time = "2025-07-28T13:22:47.499Z" }, + { url = "https://files.pythonhosted.org/packages/99/6f/cc300fea5db2ab5ddc2c8aea5757a27b89c84469899710c3aeddc1d39801/tokenizers-0.21.4-cp39-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c73012da95afafdf235ba80047699df4384fdc481527448a078ffd00e45a7d9", size = 3247445, upload-time = "2025-07-28T15:48:39.711Z" }, + { url = "https://files.pythonhosted.org/packages/be/bf/98cb4b9c3c4afd8be89cfa6423704337dc20b73eb4180397a6e0d456c334/tokenizers-0.21.4-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f23186c40395fc390d27f519679a58023f368a0aad234af145e0f39ad1212732", size = 3428014, upload-time = "2025-07-28T13:22:49.569Z" }, + { url = "https://files.pythonhosted.org/packages/75/c7/96c1cc780e6ca7f01a57c13235dd05b7bc1c0f3588512ebe9d1331b5f5ae/tokenizers-0.21.4-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cc88bb34e23a54cc42713d6d98af5f1bf79c07653d24fe984d2d695ba2c922a2", size = 3193197, upload-time = "2025-07-28T13:22:51.471Z" }, + { url = "https://files.pythonhosted.org/packages/f2/90/273b6c7ec78af547694eddeea9e05de771278bd20476525ab930cecaf7d8/tokenizers-0.21.4-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51b7eabb104f46c1c50b486520555715457ae833d5aee9ff6ae853d1130506ff", size = 3115426, upload-time = "2025-07-28T15:48:41.439Z" }, + { url = "https://files.pythonhosted.org/packages/91/43/c640d5a07e95f1cf9d2c92501f20a25f179ac53a4f71e1489a3dcfcc67ee/tokenizers-0.21.4-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:714b05b2e1af1288bd1bc56ce496c4cebb64a20d158ee802887757791191e6e2", size = 9089127, upload-time = "2025-07-28T15:48:46.472Z" }, + { url = "https://files.pythonhosted.org/packages/44/a1/dd23edd6271d4dca788e5200a807b49ec3e6987815cd9d0a07ad9c96c7c2/tokenizers-0.21.4-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:1340ff877ceedfa937544b7d79f5b7becf33a4cfb58f89b3b49927004ef66f78", size = 9055243, upload-time = "2025-07-28T15:48:48.539Z" }, + { url = "https://files.pythonhosted.org/packages/21/2b/b410d6e9021c4b7ddb57248304dc817c4d4970b73b6ee343674914701197/tokenizers-0.21.4-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:3c1f4317576e465ac9ef0d165b247825a2a4078bcd01cba6b54b867bdf9fdd8b", size = 9298237, upload-time = "2025-07-28T15:48:50.443Z" }, + { url = "https://files.pythonhosted.org/packages/b7/0a/42348c995c67e2e6e5c89ffb9cfd68507cbaeb84ff39c49ee6e0a6dd0fd2/tokenizers-0.21.4-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:c212aa4e45ec0bb5274b16b6f31dd3f1c41944025c2358faaa5782c754e84c24", size = 9461980, upload-time = "2025-07-28T15:48:52.325Z" }, + { url = "https://files.pythonhosted.org/packages/3d/d3/dacccd834404cd71b5c334882f3ba40331ad2120e69ded32cf5fda9a7436/tokenizers-0.21.4-cp39-abi3-win32.whl", hash = "sha256:6c42a930bc5f4c47f4ea775c91de47d27910881902b0f20e4990ebe045a415d0", size = 2329871, upload-time = "2025-07-28T15:48:56.841Z" }, + { url = "https://files.pythonhosted.org/packages/41/f2/fd673d979185f5dcbac4be7d09461cbb99751554ffb6718d0013af8604cb/tokenizers-0.21.4-cp39-abi3-win_amd64.whl", hash = "sha256:475d807a5c3eb72c59ad9b5fcdb254f6e17f53dfcbb9903233b0dfa9c943b597", size = 2507568, upload-time = "2025-07-28T15:48:55.456Z" }, +] + +[[package]] +name = "toml" +sdist = { url = "https://files.pythonhosted.org/packages/be/ba/1f744cdc819428fc6b5084ec34d9b30660f6f9daaf70eead706e3203ec3c/toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f", size = 22253, upload-time = "2020-11-01T01:40:22.204Z" } +source = { registry = "https://pypi.org/simple" } +version = "0.10.2" +wheels = [ + { url = "https://files.pythonhosted.org/packages/44/6f/7120676b6d73228c96e17f1f794d8ab046fc910d781c8d151120c3f1569e/toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", size = 16588, upload-time = "2020-11-01T01:40:20.672Z" }, +] + +[[package]] +name = "tomli" +sdist = { url = "https://files.pythonhosted.org/packages/18/87/302344fed471e44a87289cf4967697d07e532f2421fdaf868a303cbae4ff/tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", size = 17175, upload-time = "2024-11-27T22:38:36.873Z" } +source = { registry = "https://pypi.org/simple" } +version = "2.2.1" +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/90/2ee5f2e0362cb8a0b6499dc44f4d7d48f8fff06d28ba46e6f1eaa61a1388/tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7", size = 132708, upload-time = "2024-11-27T22:38:21.659Z" }, + { url = "https://files.pythonhosted.org/packages/c0/ec/46b4108816de6b385141f082ba99e315501ccd0a2ea23db4a100dd3990ea/tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c", size = 123582, upload-time = "2024-11-27T22:38:22.693Z" }, + { url = "https://files.pythonhosted.org/packages/a0/bd/b470466d0137b37b68d24556c38a0cc819e8febe392d5b199dcd7f578365/tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13", size = 232543, upload-time = "2024-11-27T22:38:24.367Z" }, + { url = "https://files.pythonhosted.org/packages/d9/e5/82e80ff3b751373f7cead2815bcbe2d51c895b3c990686741a8e56ec42ab/tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281", size = 241691, upload-time = "2024-11-27T22:38:26.081Z" }, + { url = "https://files.pythonhosted.org/packages/05/7e/2a110bc2713557d6a1bfb06af23dd01e7dde52b6ee7dadc589868f9abfac/tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272", size = 251170, upload-time = "2024-11-27T22:38:27.921Z" }, + { url = "https://files.pythonhosted.org/packages/64/7b/22d713946efe00e0adbcdfd6d1aa119ae03fd0b60ebed51ebb3fa9f5a2e5/tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140", size = 236530, upload-time = "2024-11-27T22:38:29.591Z" }, + { url = "https://files.pythonhosted.org/packages/38/31/3a76f67da4b0cf37b742ca76beaf819dca0ebef26d78fc794a576e08accf/tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2", size = 258666, upload-time = "2024-11-27T22:38:30.639Z" }, + { url = "https://files.pythonhosted.org/packages/07/10/5af1293da642aded87e8a988753945d0cf7e00a9452d3911dd3bb354c9e2/tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744", size = 243954, upload-time = "2024-11-27T22:38:31.702Z" }, + { url = "https://files.pythonhosted.org/packages/5b/b9/1ed31d167be802da0fc95020d04cd27b7d7065cc6fbefdd2f9186f60d7bd/tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec", size = 98724, upload-time = "2024-11-27T22:38:32.837Z" }, + { url = "https://files.pythonhosted.org/packages/c7/32/b0963458706accd9afcfeb867c0f9175a741bf7b19cd424230714d722198/tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69", size = 109383, upload-time = "2024-11-27T22:38:34.455Z" }, + { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257, upload-time = "2024-11-27T22:38:35.385Z" }, +] + +[[package]] +dependencies = [ + { name = "filelock" }, + { name = "fsspec" }, + { name = "jinja2" }, + { name = "networkx" }, + { name = "nvidia-cublas-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cuda-cupti-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cuda-nvrtc-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cuda-runtime-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cudnn-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cufft-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-curand-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cusolver-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cusparse-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cusparselt-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-nccl-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-nvjitlink-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-nvtx-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "setuptools" }, + { name = "sympy" }, + { name = "triton", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "typing-extensions" }, +] +name = "torch" +source = { registry = "https://pypi.org/simple" } +version = "2.6.0" +wheels = [ + { url = "https://files.pythonhosted.org/packages/24/85/ead1349fc30fe5a32cadd947c91bda4a62fbfd7f8c34ee61f6398d38fb48/torch-2.6.0-cp313-cp313-manylinux1_x86_64.whl", hash = "sha256:4874a73507a300a5d089ceaff616a569e7bb7c613c56f37f63ec3ffac65259cf", size = 766626191, upload-time = "2025-01-29T16:17:26.26Z" }, + { url = "https://files.pythonhosted.org/packages/dd/b0/26f06f9428b250d856f6d512413e9e800b78625f63801cbba13957432036/torch-2.6.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:a0d5e1b9874c1a6c25556840ab8920569a7a4137afa8a63a32cee0bc7d89bd4b", size = 95611439, upload-time = "2025-01-29T16:21:21.061Z" }, + { url = "https://files.pythonhosted.org/packages/c2/9c/fc5224e9770c83faed3a087112d73147cd7c7bfb7557dcf9ad87e1dda163/torch-2.6.0-cp313-cp313-win_amd64.whl", hash = "sha256:510c73251bee9ba02ae1cb6c9d4ee0907b3ce6020e62784e2d7598e0cfa4d6cc", size = 204126475, upload-time = "2025-01-29T16:21:55.394Z" }, + { url = "https://files.pythonhosted.org/packages/88/8b/d60c0491ab63634763be1537ad488694d316ddc4a20eaadd639cedc53971/torch-2.6.0-cp313-none-macosx_11_0_arm64.whl", hash = "sha256:ff96f4038f8af9f7ec4231710ed4549da1bdebad95923953a25045dcf6fd87e2", size = 66536783, upload-time = "2025-01-29T16:22:08.559Z" }, +] + +[[package]] +dependencies = [{ name = "colorama", marker = "sys_platform == 'win32'" }] +name = "tqdm" +sdist = { url = "https://files.pythonhosted.org/packages/a8/4b/29b4ef32e036bb34e4ab51796dd745cdba7ed47ad142a9f4a1eb8e0c744d/tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2", size = 169737, upload-time = "2024-11-24T20:12:22.481Z" } +source = { registry = "https://pypi.org/simple" } +version = "4.67.1" +wheels = [ + { url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2", size = 78540, upload-time = "2024-11-24T20:12:19.698Z" }, +] + +[[package]] +dependencies = [ + { name = "filelock" }, + { name = "huggingface-hub" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "pyyaml" }, + { name = "regex" }, + { name = "requests" }, + { name = "safetensors" }, + { name = "tokenizers" }, + { name = "tqdm" }, +] +name = "transformers" +sdist = { url = "https://files.pythonhosted.org/packages/70/a5/d8b8a1f3a051daeb5f11253bb69fc241f193d1c0566e299210ed9220ff4e/transformers-4.55.2.tar.gz", hash = "sha256:a45ec60c03474fd67adbce5c434685051b7608b3f4f167c25aa6aeb1cad16d4f", size = 9571466, upload-time = "2025-08-13T18:25:43.767Z" } +source = { registry = "https://pypi.org/simple" } +version = "4.55.2" +wheels = [ + { url = "https://files.pythonhosted.org/packages/db/5a/022ac010bedfb5119734cf9d743cf1d830cb4c604f53bb1552216f4344dc/transformers-4.55.2-py3-none-any.whl", hash = "sha256:097e3c2e2c0c9681db3da9d748d8f9d6a724c644514673d0030e8c5a1109f1f1", size = 11269748, upload-time = "2025-08-13T18:25:40.394Z" }, +] + +[[package]] +name = "triton" +source = { registry = "https://pypi.org/simple" } +version = "3.2.0" +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/30/37a3384d1e2e9320331baca41e835e90a3767303642c7a80d4510152cbcf/triton-3.2.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e5dfa23ba84541d7c0a531dfce76d8bcd19159d50a4a8b14ad01e91734a5c1b0", size = 253154278, upload-time = "2025-01-22T19:13:54.221Z" }, +] + +[[package]] +dependencies = [{ name = "click" }, { name = "typing-extensions" }] +name = "typer" +sdist = { url = "https://files.pythonhosted.org/packages/5b/49/39f10d0f75886439ab3dac889f14f8ad511982a754e382c9b6ca895b29e9/typer-0.9.0.tar.gz", hash = "sha256:50922fd79aea2f4751a8e0408ff10d2662bd0c8bbfa84755a699f3bada2978b2", size = 273985, upload-time = "2023-05-02T05:20:57.63Z" } +source = { registry = "https://pypi.org/simple" } +version = "0.9.0" +wheels = [ + { url = "https://files.pythonhosted.org/packages/bf/0e/c68adf10adda05f28a6ed7b9f4cd7b8e07f641b44af88ba72d9c89e4de7a/typer-0.9.0-py3-none-any.whl", hash = "sha256:5d96d986a21493606a358cae4461bd8cdf83cbf33a5aa950ae629ca3b51467ee", size = 45861, upload-time = "2023-05-02T05:20:55.675Z" }, +] + +[[package]] +name = "typing-extensions" +sdist = { url = "https://files.pythonhosted.org/packages/98/5a/da40306b885cc8c09109dc2e1abd358d5684b1425678151cdaed4731c822/typing_extensions-4.14.1.tar.gz", hash = "sha256:38b39f4aeeab64884ce9f74c94263ef78f3c22467c8724005483154c26648d36", size = 107673, upload-time = "2025-07-04T13:28:34.16Z" } +source = { registry = "https://pypi.org/simple" } +version = "4.14.1" +wheels = [ + { url = "https://files.pythonhosted.org/packages/b5/00/d631e67a838026495268c2f6884f3711a15a9a2a96cd244fdaea53b823fb/typing_extensions-4.14.1-py3-none-any.whl", hash = "sha256:d1e1e3b58374dc93031d6eda2420a48ea44a36c2b4766a4fdeb3710755731d76", size = 43906, upload-time = "2025-07-04T13:28:32.743Z" }, +] + +[[package]] +dependencies = [{ name = "typing-extensions" }] +name = "typing-inspection" +sdist = { url = "https://files.pythonhosted.org/packages/f8/b1/0c11f5058406b3af7609f121aaa6b609744687f1d158b3c3a5bf4cc94238/typing_inspection-0.4.1.tar.gz", hash = "sha256:6ae134cc0203c33377d43188d4064e9b357dba58cff3185f22924610e70a9d28", size = 75726, upload-time = "2025-05-21T18:55:23.885Z" } +source = { registry = "https://pypi.org/simple" } +version = "0.4.1" +wheels = [ + { url = "https://files.pythonhosted.org/packages/17/69/cd203477f944c353c31bade965f880aa1061fd6bf05ded0726ca845b6ff7/typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51", size = 14552, upload-time = "2025-05-21T18:55:22.152Z" }, +] + +[[package]] +name = "urllib3" +sdist = { url = "https://files.pythonhosted.org/packages/15/22/9ee70a2574a4f4599c47dd506532914ce044817c7752a79b6a51286319bc/urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", size = 393185, upload-time = "2025-06-18T14:07:41.644Z" } +source = { registry = "https://pypi.org/simple" } +version = "2.5.0" +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795, upload-time = "2025-06-18T14:07:40.39Z" }, +] + +[[package]] +dependencies = [{ name = "click" }, { name = "h11" }] +name = "uvicorn" +sdist = { url = "https://files.pythonhosted.org/packages/5e/42/e0e305207bb88c6b8d3061399c6a961ffe5fbb7e2aa63c9234df7259e9cd/uvicorn-0.35.0.tar.gz", hash = "sha256:bc662f087f7cf2ce11a1d7fd70b90c9f98ef2e2831556dd078d131b96cc94a01", size = 78473, upload-time = "2025-06-28T16:15:46.058Z" } +source = { registry = "https://pypi.org/simple" } +version = "0.35.0" +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/e2/dc81b1bd1dcfe91735810265e9d26bc8ec5da45b4c0f6237e286819194c3/uvicorn-0.35.0-py3-none-any.whl", hash = "sha256:197535216b25ff9b785e29a0b79199f55222193d47f820816e7da751e9bc8d4a", size = 66406, upload-time = "2025-06-28T16:15:44.816Z" }, +] + +[[package]] +dependencies = [{ name = "distlib" }, { name = "filelock" }, { name = "platformdirs" }] +name = "virtualenv" +sdist = { url = "https://files.pythonhosted.org/packages/1c/14/37fcdba2808a6c615681cd216fecae00413c9dab44fb2e57805ecf3eaee3/virtualenv-20.34.0.tar.gz", hash = "sha256:44815b2c9dee7ed86e387b842a84f20b93f7f417f95886ca1996a72a4138eb1a", size = 6003808, upload-time = "2025-08-13T14:24:07.464Z" } +source = { registry = "https://pypi.org/simple" } +version = "20.34.0" +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/06/04c8e804f813cf972e3262f3f8584c232de64f0cde9f703b46cf53a45090/virtualenv-20.34.0-py3-none-any.whl", hash = "sha256:341f5afa7eee943e4984a9207c025feedd768baff6753cd660c857ceb3e36026", size = 5983279, upload-time = "2025-08-13T14:24:05.111Z" }, +] + +[[package]] +name = "watchdog" +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" } +source = { registry = "https://pypi.org/simple" } +version = "6.0.0" +wheels = [ + { 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/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" }, +] + +[[package]] +name = "wrapt" +sdist = { url = "https://files.pythonhosted.org/packages/95/8f/aeb76c5b46e273670962298c23e7ddde79916cb74db802131d49a85e4b7d/wrapt-1.17.3.tar.gz", hash = "sha256:f66eb08feaa410fe4eebd17f2a2c8e2e46d3476e9f8c783daa8e09e0faa666d0", size = 55547, upload-time = "2025-08-12T05:53:21.714Z" } +source = { registry = "https://pypi.org/simple" } +version = "1.17.3" +wheels = [ + { url = "https://files.pythonhosted.org/packages/fc/f6/759ece88472157acb55fc195e5b116e06730f1b651b5b314c66291729193/wrapt-1.17.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a47681378a0439215912ef542c45a783484d4dd82bac412b71e59cf9c0e1cea0", size = 54003, upload-time = "2025-08-12T05:51:48.627Z" }, + { url = "https://files.pythonhosted.org/packages/4f/a9/49940b9dc6d47027dc850c116d79b4155f15c08547d04db0f07121499347/wrapt-1.17.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:54a30837587c6ee3cd1a4d1c2ec5d24e77984d44e2f34547e2323ddb4e22eb77", size = 39025, upload-time = "2025-08-12T05:51:37.156Z" }, + { url = "https://files.pythonhosted.org/packages/45/35/6a08de0f2c96dcdd7fe464d7420ddb9a7655a6561150e5fc4da9356aeaab/wrapt-1.17.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:16ecf15d6af39246fe33e507105d67e4b81d8f8d2c6598ff7e3ca1b8a37213f7", size = 39108, upload-time = "2025-08-12T05:51:58.425Z" }, + { url = "https://files.pythonhosted.org/packages/0c/37/6faf15cfa41bf1f3dba80cd3f5ccc6622dfccb660ab26ed79f0178c7497f/wrapt-1.17.3-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6fd1ad24dc235e4ab88cda009e19bf347aabb975e44fd5c2fb22a3f6e4141277", size = 88072, upload-time = "2025-08-12T05:52:37.53Z" }, + { url = "https://files.pythonhosted.org/packages/78/f2/efe19ada4a38e4e15b6dff39c3e3f3f73f5decf901f66e6f72fe79623a06/wrapt-1.17.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ed61b7c2d49cee3c027372df5809a59d60cf1b6c2f81ee980a091f3afed6a2d", size = 88214, upload-time = "2025-08-12T05:52:15.886Z" }, + { url = "https://files.pythonhosted.org/packages/40/90/ca86701e9de1622b16e09689fc24b76f69b06bb0150990f6f4e8b0eeb576/wrapt-1.17.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:423ed5420ad5f5529db9ce89eac09c8a2f97da18eb1c870237e84c5a5c2d60aa", size = 87105, upload-time = "2025-08-12T05:52:17.914Z" }, + { url = "https://files.pythonhosted.org/packages/fd/e0/d10bd257c9a3e15cbf5523025252cc14d77468e8ed644aafb2d6f54cb95d/wrapt-1.17.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e01375f275f010fcbf7f643b4279896d04e571889b8a5b3f848423d91bf07050", size = 87766, upload-time = "2025-08-12T05:52:39.243Z" }, + { url = "https://files.pythonhosted.org/packages/e8/cf/7d848740203c7b4b27eb55dbfede11aca974a51c3d894f6cc4b865f42f58/wrapt-1.17.3-cp313-cp313-win32.whl", hash = "sha256:53e5e39ff71b3fc484df8a522c933ea2b7cdd0d5d15ae82e5b23fde87d44cbd8", size = 36711, upload-time = "2025-08-12T05:53:10.074Z" }, + { url = "https://files.pythonhosted.org/packages/57/54/35a84d0a4d23ea675994104e667ceff49227ce473ba6a59ba2c84f250b74/wrapt-1.17.3-cp313-cp313-win_amd64.whl", hash = "sha256:1f0b2f40cf341ee8cc1a97d51ff50dddb9fcc73241b9143ec74b30fc4f44f6cb", size = 38885, upload-time = "2025-08-12T05:53:08.695Z" }, + { url = "https://files.pythonhosted.org/packages/01/77/66e54407c59d7b02a3c4e0af3783168fff8e5d61def52cda8728439d86bc/wrapt-1.17.3-cp313-cp313-win_arm64.whl", hash = "sha256:7425ac3c54430f5fc5e7b6f41d41e704db073309acfc09305816bc6a0b26bb16", size = 36896, upload-time = "2025-08-12T05:52:55.34Z" }, + { url = "https://files.pythonhosted.org/packages/02/a2/cd864b2a14f20d14f4c496fab97802001560f9f41554eef6df201cd7f76c/wrapt-1.17.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cf30f6e3c077c8e6a9a7809c94551203c8843e74ba0c960f4a98cd80d4665d39", size = 54132, upload-time = "2025-08-12T05:51:49.864Z" }, + { url = "https://files.pythonhosted.org/packages/d5/46/d011725b0c89e853dc44cceb738a307cde5d240d023d6d40a82d1b4e1182/wrapt-1.17.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e228514a06843cae89621384cfe3a80418f3c04aadf8a3b14e46a7be704e4235", size = 39091, upload-time = "2025-08-12T05:51:38.935Z" }, + { url = "https://files.pythonhosted.org/packages/2e/9e/3ad852d77c35aae7ddebdbc3b6d35ec8013af7d7dddad0ad911f3d891dae/wrapt-1.17.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:5ea5eb3c0c071862997d6f3e02af1d055f381b1d25b286b9d6644b79db77657c", size = 39172, upload-time = "2025-08-12T05:51:59.365Z" }, + { url = "https://files.pythonhosted.org/packages/c3/f7/c983d2762bcce2326c317c26a6a1e7016f7eb039c27cdf5c4e30f4160f31/wrapt-1.17.3-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:281262213373b6d5e4bb4353bc36d1ba4084e6d6b5d242863721ef2bf2c2930b", size = 87163, upload-time = "2025-08-12T05:52:40.965Z" }, + { url = "https://files.pythonhosted.org/packages/e4/0f/f673f75d489c7f22d17fe0193e84b41540d962f75fce579cf6873167c29b/wrapt-1.17.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dc4a8d2b25efb6681ecacad42fca8859f88092d8732b170de6a5dddd80a1c8fa", size = 87963, upload-time = "2025-08-12T05:52:20.326Z" }, + { url = "https://files.pythonhosted.org/packages/df/61/515ad6caca68995da2fac7a6af97faab8f78ebe3bf4f761e1b77efbc47b5/wrapt-1.17.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:373342dd05b1d07d752cecbec0c41817231f29f3a89aa8b8843f7b95992ed0c7", size = 86945, upload-time = "2025-08-12T05:52:21.581Z" }, + { url = "https://files.pythonhosted.org/packages/d3/bd/4e70162ce398462a467bc09e768bee112f1412e563620adc353de9055d33/wrapt-1.17.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d40770d7c0fd5cbed9d84b2c3f2e156431a12c9a37dc6284060fb4bec0b7ffd4", size = 86857, upload-time = "2025-08-12T05:52:43.043Z" }, + { url = "https://files.pythonhosted.org/packages/2b/b8/da8560695e9284810b8d3df8a19396a6e40e7518059584a1a394a2b35e0a/wrapt-1.17.3-cp314-cp314-win32.whl", hash = "sha256:fbd3c8319de8e1dc79d346929cd71d523622da527cca14e0c1d257e31c2b8b10", size = 37178, upload-time = "2025-08-12T05:53:12.605Z" }, + { url = "https://files.pythonhosted.org/packages/db/c8/b71eeb192c440d67a5a0449aaee2310a1a1e8eca41676046f99ed2487e9f/wrapt-1.17.3-cp314-cp314-win_amd64.whl", hash = "sha256:e1a4120ae5705f673727d3253de3ed0e016f7cd78dc463db1b31e2463e1f3cf6", size = 39310, upload-time = "2025-08-12T05:53:11.106Z" }, + { url = "https://files.pythonhosted.org/packages/45/20/2cda20fd4865fa40f86f6c46ed37a2a8356a7a2fde0773269311f2af56c7/wrapt-1.17.3-cp314-cp314-win_arm64.whl", hash = "sha256:507553480670cab08a800b9463bdb881b2edeed77dc677b0a5915e6106e91a58", size = 37266, upload-time = "2025-08-12T05:52:56.531Z" }, + { url = "https://files.pythonhosted.org/packages/77/ed/dd5cf21aec36c80443c6f900449260b80e2a65cf963668eaef3b9accce36/wrapt-1.17.3-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:ed7c635ae45cfbc1a7371f708727bf74690daedc49b4dba310590ca0bd28aa8a", size = 56544, upload-time = "2025-08-12T05:51:51.109Z" }, + { url = "https://files.pythonhosted.org/packages/8d/96/450c651cc753877ad100c7949ab4d2e2ecc4d97157e00fa8f45df682456a/wrapt-1.17.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:249f88ed15503f6492a71f01442abddd73856a0032ae860de6d75ca62eed8067", size = 40283, upload-time = "2025-08-12T05:51:39.912Z" }, + { url = "https://files.pythonhosted.org/packages/d1/86/2fcad95994d9b572db57632acb6f900695a648c3e063f2cd344b3f5c5a37/wrapt-1.17.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a03a38adec8066d5a37bea22f2ba6bbf39fcdefbe2d91419ab864c3fb515454", size = 40366, upload-time = "2025-08-12T05:52:00.693Z" }, + { url = "https://files.pythonhosted.org/packages/64/0e/f4472f2fdde2d4617975144311f8800ef73677a159be7fe61fa50997d6c0/wrapt-1.17.3-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5d4478d72eb61c36e5b446e375bbc49ed002430d17cdec3cecb36993398e1a9e", size = 108571, upload-time = "2025-08-12T05:52:44.521Z" }, + { url = "https://files.pythonhosted.org/packages/cc/01/9b85a99996b0a97c8a17484684f206cbb6ba73c1ce6890ac668bcf3838fb/wrapt-1.17.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:223db574bb38637e8230eb14b185565023ab624474df94d2af18f1cdb625216f", size = 113094, upload-time = "2025-08-12T05:52:22.618Z" }, + { url = "https://files.pythonhosted.org/packages/25/02/78926c1efddcc7b3aa0bc3d6b33a822f7d898059f7cd9ace8c8318e559ef/wrapt-1.17.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e405adefb53a435f01efa7ccdec012c016b5a1d3f35459990afc39b6be4d5056", size = 110659, upload-time = "2025-08-12T05:52:24.057Z" }, + { url = "https://files.pythonhosted.org/packages/dc/ee/c414501ad518ac3e6fe184753632fe5e5ecacdcf0effc23f31c1e4f7bfcf/wrapt-1.17.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:88547535b787a6c9ce4086917b6e1d291aa8ed914fdd3a838b3539dc95c12804", size = 106946, upload-time = "2025-08-12T05:52:45.976Z" }, + { url = "https://files.pythonhosted.org/packages/be/44/a1bd64b723d13bb151d6cc91b986146a1952385e0392a78567e12149c7b4/wrapt-1.17.3-cp314-cp314t-win32.whl", hash = "sha256:41b1d2bc74c2cac6f9074df52b2efbef2b30bdfe5f40cb78f8ca22963bc62977", size = 38717, upload-time = "2025-08-12T05:53:15.214Z" }, + { url = "https://files.pythonhosted.org/packages/79/d9/7cfd5a312760ac4dd8bf0184a6ee9e43c33e47f3dadc303032ce012b8fa3/wrapt-1.17.3-cp314-cp314t-win_amd64.whl", hash = "sha256:73d496de46cd2cdbdbcce4ae4bcdb4afb6a11234a1df9c085249d55166b95116", size = 41334, upload-time = "2025-08-12T05:53:14.178Z" }, + { url = "https://files.pythonhosted.org/packages/46/78/10ad9781128ed2f99dbc474f43283b13fea8ba58723e98844367531c18e9/wrapt-1.17.3-cp314-cp314t-win_arm64.whl", hash = "sha256:f38e60678850c42461d4202739f9bf1e3a737c7ad283638251e79cc49effb6b6", size = 38471, upload-time = "2025-08-12T05:52:57.784Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f6/a933bd70f98e9cf3e08167fc5cd7aaaca49147e48411c0bd5ae701bb2194/wrapt-1.17.3-py3-none-any.whl", hash = "sha256:7171ae35d2c33d326ac19dd8facb1e82e5fd04ef8c6c0e394d7af55a55051c22", size = 23591, upload-time = "2025-08-12T05:53:20.674Z" }, +]