diff --git a/.env.example b/.env.example new file mode 100644 index 0000000000000000000000000000000000000000..81d87071f881581802518f248a16698d8e2b8c5e --- /dev/null +++ b/.env.example @@ -0,0 +1,188 @@ +# ============================================================================= +# FICHIER DE CONFIGURATION ENVIRONNEMENT - PROJECT5 +# ============================================================================= +# Copiez ce fichier vers .env et modifiez les valeurs selon vos besoins +# ATTENTION: Ne commitez jamais le fichier .env (il contient des secrets) + +# ============================================================================= +# CONFIGURATION DE L'APPLICATION +# ============================================================================= +APP_NAME=Project5 API +VERSION=0.1.0 +ENVIRONMENT=development +DEBUG=true + +# Host et port pour le serveur +HOST=0.0.0.0 +PORT=8000 + +# ============================================================================= +# SÉCURITÉ - CHANGEZ CES VALEURS EN PRODUCTION ! +# ============================================================================= +# Clé secrète pour JWT et sessions (génération: openssl rand -hex 32) +SECRET_KEY=votre-cle-secrete-super-longue-et-complexe-changez-moi-en-production-123456789abcdef + +# Algorithme de hachage pour JWT +ALGORITHM=HS256 + +# Durée de validité des tokens (en minutes) +ACCESS_TOKEN_EXPIRE_MINUTES=30 + +# Clé pour le refresh token (optionnel) +REFRESH_TOKEN_EXPIRE_DAYS=7 + +# ============================================================================= +# BASE DE DONNÉES - POSTGRESQL +# ============================================================================= +# URL complète de la base de données PostgreSQL +DATABASE_URL=postgresql://project5_user:project5_password@localhost:5432/project5_db + +# Paramètres séparés (utilisés par Docker Compose) +POSTGRES_HOST=localhost +POSTGRES_PORT=5432 +POSTGRES_DB=project5_db +POSTGRES_USER=project5_user +POSTGRES_PASSWORD=project5_password + +# Configuration SQLAlchemy +SQLALCHEMY_ECHO=false +SQLALCHEMY_POOL_SIZE=5 +SQLALCHEMY_MAX_OVERFLOW=10 +SQLALCHEMY_POOL_PRE_PING=true +SQLALCHEMY_POOL_RECYCLE=300 + +# ============================================================================= +# BASE DE DONNÉES DE TEST - SQLITE +# ============================================================================= +# Utilisée pour les tests unitaires (plus rapide que PostgreSQL) +TEST_DATABASE_URL=sqlite:///./test_project5.db + +# ============================================================================= +# LOGGING +# ============================================================================= +LOG_LEVEL=INFO +LOG_FORMAT=%(asctime)s - %(name)s - %(levelname)s - %(message)s +LOG_FILE=logs/project5.log +LOG_MAX_SIZE=10MB +LOG_BACKUP_COUNT=5 + +# ============================================================================= +# CORS - Configuration des domaines autorisés +# ============================================================================= +# Domaines autorisés pour les requêtes CORS (séparés par des virgules) +ALLOWED_ORIGINS=http://localhost:3000,http://localhost:8080,http://127.0.0.1:3000 +ALLOWED_METHODS=GET,POST,PUT,DELETE,PATCH,OPTIONS +ALLOWED_HEADERS=* + +# ============================================================================= +# REDIS - Cache et sessions (optionnel) +# ============================================================================= +# REDIS_URL=redis://localhost:6379/0 +# REDIS_HOST=localhost +# REDIS_PORT=6379 +# REDIS_DB=0 +# REDIS_PASSWORD= +# REDIS_EXPIRE_TIME=3600 + +# ============================================================================= +# EMAIL - Configuration SMTP (optionnel) +# ============================================================================= +# SMTP_HOST=smtp.gmail.com +# SMTP_PORT=587 +# SMTP_USER=votre.email@gmail.com +# SMTP_PASSWORD=votre-mot-de-passe-app +# SMTP_TLS=true +# SMTP_FROM_EMAIL=noreply@project5.com +# SMTP_FROM_NAME=Project5 API + +# ============================================================================= +# STOCKAGE DE FICHIERS (optionnel) +# ============================================================================= +# Répertoire local pour les uploads +# UPLOAD_DIRECTORY=uploads +# MAX_FILE_SIZE=10MB +# ALLOWED_FILE_EXTENSIONS=jpg,jpeg,png,gif,pdf,doc,docx + +# Configuration AWS S3 (optionnel) +# AWS_ACCESS_KEY_ID=votre-access-key +# AWS_SECRET_ACCESS_KEY=votre-secret-key +# AWS_BUCKET_NAME=votre-bucket +# AWS_REGION=eu-west-1 + +# ============================================================================= +# MONITORING ET OBSERVABILITÉ (optionnel) +# ============================================================================= +# Sentry pour le monitoring d'erreurs +# SENTRY_DSN=https://votre-sentry-dsn@sentry.io/project-id + +# Prometheus metrics +# ENABLE_METRICS=false +# METRICS_PORT=9090 + +# ============================================================================= +# RATE LIMITING (optionnel) +# ============================================================================= +# RATE_LIMIT_REQUESTS=100 +# RATE_LIMIT_WINDOW=3600 +# RATE_LIMIT_STORAGE=memory + +# ============================================================================= +# CONFIGURATION DOCKER COMPOSE +# ============================================================================= +# Variables utilisées par docker-compose.yml +COMPOSE_PROJECT_NAME=project5 +COMPOSE_FILE=docker-compose.yml + +# Ports exposés +API_PORT=8000 +DB_PORT=5432 + +# ============================================================================= +# DÉVELOPPEMENT LOCAL +# ============================================================================= +# Rechargement automatique en développement +AUTO_RELOAD=true + +# Affichage détaillé des requêtes SQL +SQL_DEBUG=false + +# Mode de développement pour FastAPI +FASTAPI_DEBUG=true +FASTAPI_RELOAD=true + +# ============================================================================= +# TESTS +# ============================================================================= +# Configuration spécifique aux tests +TESTING=false +TEST_LOG_LEVEL=WARNING + +# Base de données de test séparée +TEST_POSTGRES_DB=project5_test_db +TEST_POSTGRES_USER=project5_test_user +TEST_POSTGRES_PASSWORD=project5_test_password + +# ============================================================================= +# PRODUCTION - Variables critiques à définir +# ============================================================================= +# En production, assurez-vous de définir: +# - SECRET_KEY (généré de manière sécurisée) +# - DATABASE_URL (avec les vrais credentials) +# - ALLOWED_ORIGINS (domaines de production) +# - ENVIRONMENT=production +# - DEBUG=false +# - LOG_LEVEL=WARNING ou ERROR + +# ============================================================================= +# NOTES D'UTILISATION +# ============================================================================= +# 1. Copiez ce fichier: cp .env.example .env +# 2. Modifiez les valeurs dans .env selon vos besoins +# 3. Le fichier .env ne doit JAMAIS être commité dans Git +# 4. Utilisez des valeurs différentes pour dev/test/production +# 5. Générez des clés secrètes fortes pour la production +# +# Génération de SECRET_KEY sécurisée: +# python -c "import secrets; print(secrets.token_urlsafe(32))" +# ou +# openssl rand -base64 32 \ No newline at end of file diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000000000000000000000000000000000000..21ff4ec6c503a1f2da31326fffb9310e36f7884b --- /dev/null +++ b/.flake8 @@ -0,0 +1,18 @@ +[flake8] +max-line-length = 88 +extend-ignore = + E203, + W503, + E402, + E501 + +exclude = + .git, + __pycache__, + .venv, + venv, + .jupyter, + .ipynb_checkpoints + +per-file-ignores = + *.ipynb:E402,E501 \ No newline at end of file diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000000000000000000000000000000000000..5cf5de2e49abb6f986903bc40aafb6cba910648d --- /dev/null +++ b/.gitattributes @@ -0,0 +1,37 @@ +# Git LFS configuration for large files (e.g., models, datasets) +*.7z filter=lfs diff=lfs merge=lfs -text +*.arrow filter=lfs diff=lfs merge=lfs -text +*.bin filter=lfs diff=lfs merge=lfs -text +*.bz2 filter=lfs diff=lfs merge=lfs -text +*.ckpt filter=lfs diff=lfs merge=lfs -text +*.ftz filter=lfs diff=lfs merge=lfs -text +*.gz filter=lfs diff=lfs merge=lfs -text +*.h5 filter=lfs diff=lfs merge=lfs -text +*.joblib filter=lfs diff=lfs merge=lfs -text +*.lfs.* filter=lfs diff=lfs merge=lfs -text +*.mlmodel filter=lfs diff=lfs merge=lfs -text +*.model filter=lfs diff=lfs merge=lfs -text +*.msgpack filter=lfs diff=lfs merge=lfs -text +*.npy filter=lfs diff=lfs merge=lfs -text +*.npz filter=lfs diff=lfs merge=lfs -text +*.onnx filter=lfs diff=lfs merge=lfs -text +*.ot filter=lfs diff=lfs merge=lfs -text +*.parquet filter=lfs diff=lfs merge=lfs -text +*.pb filter=lfs diff=lfs merge=lfs -text +*.pickle filter=lfs diff=lfs merge=lfs -text +*.pkl filter=lfs diff=lfs merge=lfs -text +*.pt filter=lfs diff=lfs merge=lfs -text +*.pth filter=lfs diff=lfs merge=lfs -text +*.rar filter=lfs diff=lfs merge=lfs -text +*.safetensors filter=lfs diff=lfs merge=lfs -text +saved_model/**/* filter=lfs diff=lfs merge=lfs -text +*.tar.* filter=lfs diff=lfs merge=lfs -text +*.tar filter=lfs diff=lfs merge=lfs -text +*.tflite filter=lfs diff=lfs merge=lfs -text +*.tgz filter=lfs diff=lfs merge=lfs -text +*.wasm filter=lfs diff=lfs merge=lfs -text +*.xz filter=lfs diff=lfs merge=lfs -text +*.zip filter=lfs diff=lfs merge=lfs -text +*.zst filter=lfs diff=lfs merge=lfs -text +*tfevents* filter=lfs diff=lfs merge=lfs -text +model/model.pkl filter=lfs diff=lfs merge=lfs -text diff --git a/.github/workflows/cicd.yml b/.github/workflows/cicd.yml new file mode 100644 index 0000000000000000000000000000000000000000..9a58af99663a5a9f41ceceeb7d23f3fcf32b2a21 --- /dev/null +++ b/.github/workflows/cicd.yml @@ -0,0 +1,197 @@ +name: Project5 CI/CD + +on: + push: + branches: [ main, develop ] + pull_request: + branches: [ main, develop ] + workflow_dispatch: + inputs: + environment: + description: 'Environnement' + type: choice + options: ['dev', 'production'] +jobs: + test: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.11"] + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + + - name: Install Poetry + uses: snok/install-poetry@v1 + with: + version: latest + virtualenvs-create: true + virtualenvs-in-project: true + installer-parallel: true + + - name: Load cached venv + id: cached-poetry-dependencies + uses: actions/cache@v3 + with: + path: .venv + key: venv-${{ runner.os }}-${{ matrix.python-version }}-${{ hashFiles('**/poetry.lock') }} + + - name: Install dependencies + if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true' + run: poetry install --no-interaction --no-root + + - name: Install project + run: poetry install --no-interaction + + - name: Run tests with pytest + run: | + DATABASE_URL="sqlite:///:memory:" poetry run pytest tests/ --cov=src/project5 --cov-report=xml --cov-report=html --cov-report=term-missing --cov-fail-under=80 -v + + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v3 + with: + token: ${{ secrets.CODECOV_TOKEN }} + file: ./coverage.xml + flags: unittests + name: codecov-umbrella + + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: "3.11" + + - name: Install Poetry + uses: snok/install-poetry@v1 + with: + version: latest + virtualenvs-create: true + virtualenvs-in-project: true + + - name: Install dependencies + run: poetry install --no-interaction + + - name: Run Black + run: poetry run black --check ./src + + - name: Run isort + run: poetry run isort --check-only . + + - name: Run flake8 + run: poetry run flake8 ./src + + # - name: Run mypy + # run: poetry run mypy ./src + + security: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: "3.11" + + - name: Install Poetry + uses: snok/install-poetry@v1 + + - name: Install dependencies + run: poetry install --no-interaction + + - name: Run Bandit security linter + run: poetry run bandit -r ./src -f json + + - name: Run Safety check + run: poetry run safety check --output json + + deploy: + runs-on: ubuntu-latest + needs: [test, lint, security] + if: github.ref == 'refs/heads/main' && github.event.inputs.environment == 'production' + environment: production # Nécessite approbation dans Settings > Environments + + steps: + - name: Setup Git LFS + run: | + git lfs install + + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + lfs: true + + - name: Pull LFS files + run: git lfs pull + + - name: Push to Hugging Face Space + env: + HF_TOKEN: ${{ secrets.HF_TOKEN }} + HF_SPACE_NAME: ${{ secrets.HF_SPACE_NAME }} + run: | + echo "Configuration Git pour HF" + git config --global user.email "action@github.com" + git config --global user.name "GitHub Action" + + echo "Ajouter remote HF si pas déjà présent" + if ! git remote | grep -q huggingface; then + git remote add huggingface https://huggingface.co/spaces/$HF_SPACE_NAME + fi + + echo "Créer une branche orpheline pour éviter l'historique" + git checkout --orphan deploy-hf + + echo "Configuration Git LFS pour HuggingFace" + git lfs track "*.pkl" + + echo "Copier le Dockerfile pour HF (renommer)" + cp Dockerfile_app Dockerfile + git rm -f README.md + echo "Créer README.md pour HF Spaces si absent" + if [ ! -f README.md ]; then + cat > README.md << EOF + --- + title: Building Energy Prediction API + emoji: 🏢 + colorFrom: blue + colorTo: green + sdk: docker + app_port: 7860 + pinned: false + license: mit + --- + + # Building Energy Prediction API + + API FastAPI pour la prédiction de consommation énergétique des bâtiments. + + ## Fonctionnalités + - 🏢 Gestion des quartiers, bâtiments et propriétés + - 🤖 Prédictions ML avec RandomForest + - 📊 API REST complète avec documentation Swagger + + EOF + fi + + echo "Echo suppression des données non nécessaires en production" + git rm -f *.pdf + git rm -rf docs + git rm -rf sql + git rm -rf tests + echo "Ajouter tous les fichiers nécessaires (y compris LFS)" + git add -A + git commit -m "Deploy to HuggingFace Spaces from main branch" + + echo "Push vers HF avec authentification (LFS supporté)" + git push https://oauth2:$HF_TOKEN@huggingface.co/spaces/$HF_SPACE_NAME deploy-hf:main --force + + \ No newline at end of file diff --git a/.github/workflows/doc.yml b/.github/workflows/doc.yml new file mode 100644 index 0000000000000000000000000000000000000000..10eeb519e8a296f1b9b4daaaec657fa007cee379 --- /dev/null +++ b/.github/workflows/doc.yml @@ -0,0 +1,53 @@ +name: Build and Deploy Documentation + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + build-and-deploy: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.11' + + - name: Install Poetry + uses: snok/install-poetry@v1 + with: + version: latest + virtualenvs-create: true + virtualenvs-in-project: true + + - name: Load cached venv + id: cached-poetry-dependencies + uses: actions/cache@v3 + with: + path: .venv + key: venv-${{ runner.os }}-${{ steps.setup-python.outputs.python-version }}-${{ hashFiles('**/poetry.lock') }} + + - name: Install dependencies + if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true' + run: poetry install --no-interaction --no-root + + - name: Build Sphinx documentation + run: | + cd docs && make sphinx-html + + - name: Deploy to GitHub Pages + uses: peaceiris/actions-gh-pages@v3 + if: github.ref == 'refs/heads/main' + with: + personal_token: ${{ secrets.DOCS_DEPLOY_TOKEN }} + external_repository: ${{ secrets.DOCS_REPOSITORY }} + publish_dir: ./docs/build/html + publish_branch: gh-pages + user_name: 'github-actions[bot]' + user_email: 'github-actions[bot]@users.noreply.github.com' + commit_message: 'Deploy documentation from ${{ github.repository }}@${{ github.sha }}' \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..2ebcceac1d3abb93c258805f84eee68a9b91dc0e --- /dev/null +++ b/.gitignore @@ -0,0 +1,685 @@ +# Created by https://www.toptal.com/developers/gitignore/api/pycharm,python,git +# Edit at https://www.toptal.com/developers/gitignore?templates=pycharm,python,git + +### Git ### +# Created by git for backups. To disable backups in Git: +# $ git config --global mergetool.keepBackup false +*.orig + +# Created by git when using merge tools for conflicts +*.BACKUP.* +*.BASE.* +*.LOCAL.* +*.REMOTE.* +*_BACKUP_*.txt +*_BASE_*.txt +*_LOCAL_*.txt +*_REMOTE_*.txt + +### PyCharm ### +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# AWS User-specific +.idea/**/aws.xml + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/artifacts +# .idea/compiler.xml +# .idea/jarRepositories.xml +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# SonarLint plugin +.idea/sonarlint/ + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + +### PyCharm Patch ### +# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 + +# *.iml +# modules.xml +# .idea/misc.xml +# *.ipr + +# Sonarlint plugin +# https://plugins.jetbrains.com/plugin/7973-sonarlint +.idea/**/sonarlint/ + +# SonarQube Plugin +# https://plugins.jetbrains.com/plugin/7238-sonarqube-community-plugin +.idea/**/sonarIssues.xml + +# Markdown Navigator plugin +# https://plugins.jetbrains.com/plugin/7896-markdown-navigator-enhanced +.idea/**/markdown-navigator.xml +.idea/**/markdown-navigator-enh.xml +.idea/**/markdown-navigator/ + +# Cache file creation bug +# See https://youtrack.jetbrains.com/issue/JBR-2257 +.idea/$CACHE_FILE$ + +# CodeStream plugin +# https://plugins.jetbrains.com/plugin/12206-codestream +.idea/codestream.xml + +# Azure Toolkit for IntelliJ plugin +# https://plugins.jetbrains.com/plugin/8053-azure-toolkit-for-intellij +.idea/**/azureSettings.xml + +### 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 + +# End of https://www.toptal.com/developers/gitignore/api/pycharm,python,git +.env + +# Created by https://www.toptal.com/developers/gitignore/api/visualstudiocode +# Edit at https://www.toptal.com/developers/gitignore?templates=visualstudiocode + +### VisualStudioCode ### +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +!.vscode/*.code-snippets + +# Local History for Visual Studio Code +.history/ + +# Built Visual Studio Code Extensions +*.vsix + +### VisualStudioCode Patch ### +# Ignore all local history of files +.history +.ionide + +# End of https://www.toptal.com/developers/gitignore/api/visualstudiocode +# Created by https://www.toptal.com/developers/gitignore/api/macos +# Edit at https://www.toptal.com/developers/gitignore?templates=macos + +### macOS ### +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +### macOS Patch ### +# iCloud generated files +*.icloud + +# End of https://www.toptal.com/developers/gitignore/api/macos +# Created by https://www.toptal.com/developers/gitignore/api/latex +# Edit at https://www.toptal.com/developers/gitignore?templates=latex + +### LaTeX ### +## Core latex/pdflatex auxiliary files: +*.aux +*.lof +*.log +*.lot +*.fls +*.out +*.toc +*.fmt +*.fot +*.cb +*.cb2 +.*.lb + +## Intermediate documents: +*.dvi +*.xdv +*-converted-to.* +# these rules might exclude image files for figures etc. +# *.ps +# *.eps +# *.pdf + +## Generated if empty string is given at "Please type another file name for output:" +.pdf + +## Bibliography auxiliary files (bibtex/biblatex/biber): +*.bbl +*.bcf +*.blg +*-blx.aux +*-blx.bib +*.run.xml + +## Build tool auxiliary files: +*.fdb_latexmk +*.synctex +*.synctex(busy) +*.synctex.gz +*.synctex.gz(busy) +*.pdfsync + +## Build tool directories for auxiliary files +# latexrun +latex.out/ + +## Auxiliary and intermediate files from other packages: +# algorithms +*.alg +*.loa + +# achemso +acs-*.bib + +# amsthm +*.thm + +# beamer +*.nav +*.pre +*.snm +*.vrb + +# changes +*.soc + +# comment +*.cut + +# cprotect +*.cpt + +# elsarticle (documentclass of Elsevier journals) +*.spl + +# endnotes +*.ent + +# fixme +*.lox + +# feynmf/feynmp +*.mf +*.mp +*.t[1-9] +*.t[1-9][0-9] +*.tfm + +#(r)(e)ledmac/(r)(e)ledpar +*.end +*.?end +*.[1-9] +*.[1-9][0-9] +*.[1-9][0-9][0-9] +*.[1-9]R +*.[1-9][0-9]R +*.[1-9][0-9][0-9]R +*.eledsec[1-9] +*.eledsec[1-9]R +*.eledsec[1-9][0-9] +*.eledsec[1-9][0-9]R +*.eledsec[1-9][0-9][0-9] +*.eledsec[1-9][0-9][0-9]R + +# glossaries +*.acn +*.acr +*.glg +*.glo +*.gls +*.glsdefs +*.lzo +*.lzs +*.slg +*.slo +*.sls + +# uncomment this for glossaries-extra (will ignore makeindex's style files!) +# *.ist + +# gnuplot +*.gnuplot +*.table + +# gnuplottex +*-gnuplottex-* + +# gregoriotex +*.gaux +*.glog +*.gtex + +# htlatex +*.4ct +*.4tc +*.idv +*.lg +*.trc +*.xref + +# hyperref +*.brf + +# knitr +*-concordance.tex +# TODO Uncomment the next line if you use knitr and want to ignore its generated tikz files +# *.tikz +*-tikzDictionary + +# listings +*.lol + +# luatexja-ruby +*.ltjruby + +# makeidx +*.idx +*.ilg +*.ind + +# minitoc +*.maf +*.mlf +*.mlt +*.mtc[0-9]* +*.slf[0-9]* +*.slt[0-9]* +*.stc[0-9]* + +# minted +_minted* +*.pyg + +# morewrites +*.mw + +# newpax +*.newpax + +# nomencl +*.nlg +*.nlo +*.nls + +# pax +*.pax + +# pdfpcnotes +*.pdfpc + +# sagetex +*.sagetex.sage +*.sagetex.py +*.sagetex.scmd + +# scrwfile +*.wrt + +# svg +svg-inkscape/ + +# sympy +*.sout +*.sympy +sympy-plots-for-*.tex/ + +# pdfcomment +*.upa +*.upb + +# pythontex +*.pytxcode +pythontex-files-*/ + +# tcolorbox +*.listing + +# thmtools +*.loe + +# TikZ & PGF +*.dpth +*.md5 +*.auxlock + +# titletoc +*.ptc + +# todonotes +*.tdo + +# vhistory +*.hst +*.ver + +# easy-todo +*.lod + +# xcolor +*.xcp + +# xmpincl +*.xmpi + +# xindy +*.xdy + +# xypic precompiled matrices and outlines +*.xyc +*.xyd + +# endfloat +*.ttt +*.fff + +# Latexian +TSWLatexianTemp* + +## Editors: +# WinEdt +*.bak +*.sav + +# Texpad +.texpadtmp + +# LyX +*.lyx~ + +# Kile +*.backup + +# gummi +.*.swp + +# KBibTeX +*~[0-9]* + +# TeXnicCenter +*.tps + +# auto folder when using emacs and auctex +./auto/* +*.el + +# expex forward references with \gathertags +*-tags.tex + +# standalone packages +*.sta + +# Makeindex log files +*.lpz + +# xwatermark package +*.xwm + +# REVTeX puts footnotes in the bibliography by default, unless the nofootinbib +# option is specified. Footnotes are the stored in a file with suffix Notes.bib. +# Uncomment the next line to have this generated file ignored. +#*Notes.bib + +### LaTeX Patch ### +# LIPIcs / OASIcs +*.vtc + +# glossaries +*.glstex + +# End of https://www.toptal.com/developers/gitignore/api/latex +.gradio +.env +sql/data/psql_data +app.db +model/backup \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000000000000000000000000000000000000..291568e52a893655997bd20adff4cd0c4e04417d --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,13 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Python: Current File", + "type": "debugpy", + "request": "launch", + "program": "${file}", + "console": "integratedTerminal", + "python": "${workspaceFolder}/.venv/bin/python" + } + ] +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000000000000000000000000000000000000..23c8f39a4fca2dd10888e18ef873eef8b60085f8 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,48 @@ +{ + "python.defaultInterpreterPath": "./venv/bin/python", + "python.terminal.activateEnvironment": true, + // Linting avec Poetry + "python.linting.enabled": true, + "python.linting.flake8Enabled": true, + "python.linting.lintOnSave": true, + "python.linting.flake8Path": "./venv/bin/flake8", + // Configuration spécifique pour Jupyter + "jupyter.linting.enabled": true, + // Formatting (si vous utilisez Black) + "python.formatting.provider": "black", + "python.formatting.blackPath": "./venv/bin/black", + "editor.formatOnSave": true, + // Désactiver autres linters + "python.linting.pylintEnabled": false, + "python.linting.pycodestyleEnabled": false, + "makefile.configureOnOpen": false, + "python.linting.flake8Args": [ + "--max-line-length=88", + "--extend-ignore=E203,W503,E402" + ], + "editor.rulers": [ + 88 + ], + // === ACTIONS SUR SAUVEGARDE NOTEBOOKS === + "notebook.codeActionsOnSave": { + "source.organizeImports": true, // Organise les imports + "source.fixAll": false, // Désactiver les fix auto (optionnel) + }, + // === ACTIONS SUR SAUVEGARDE FICHIERS PYTHON === + "[python]": { + "editor.codeActionsOnSave": { + "source.organizeImports": "explicit", + "source.fixAll.flake8": "explicit" + }, + "editor.defaultFormatter": "ms-python.black-formatter", + "editor.formatOnSave": true + }, + // === CONFIGURATION ISORT (pour organiser imports) === + "python.sortImports.args": [ + "--profile=black", // Compatible avec Black + "--line-length=88" + ], + "black-formatter.args": [ + "--line-length=88" + ] +} \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000000000000000000000000000000000000..69e2beb459c84b3fba9444ede57253f03cba806a --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,24 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "poetry install", + "type": "shell", + "command": "poetry", + "args": [ + "install" + ], + "group": "build" + }, + { + "label": "poetry run tests", + "type": "shell", + "command": "poetry", + "args": [ + "run", + "pytest" + ], + "group": "test" + } + ] +} \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..d91f3ef3d627d513c8df248e62f86ab8f522702d --- /dev/null +++ b/Dockerfile @@ -0,0 +1,47 @@ +FROM python:3.11-slim + +# Installer Poetry et les dépendances système pour SQLite +RUN apt-get update && apt-get install -y \ + sqlite3 \ + libsqlite3-dev \ + && rm -rf /var/lib/apt/lists/* \ + && pip install poetry + +# Configuration Poetry +ENV POETRY_NO_INTERACTION=1 \ + POETRY_VENV_IN_PROJECT=1 \ + POETRY_CACHE_DIR=/tmp/poetry_cache \ + VIRTUAL_ENV=/code/.venv \ + PATH="/code/.venv/bin:$PATH" + +WORKDIR /code + +# Copier les fichiers de dépendances +COPY pyproject.toml poetry.lock ./ + +# Copier le code source d'abord +COPY . . + +# Configurer Poetry pour créer le venv dans le projet + +RUN poetry config virtualenvs.in-project true && \ + poetry install --only main --no-interaction --no-ansi + +# Installer toutes les dépendances et le projet +#RUN poetry install --only=main && rm -rf $POETRY_CACHE_DIR + +# Configuration pour SQLite et HuggingFace Spaces +ENV DATABASE_URL="sqlite:///:memory:" \ + ENVIRONMENT="production" \ + PORT=7860 + +ENV ML_MODEL_PATH=../model/model.pkl + +# Exposer le port requis par HF Spaces +EXPOSE 7860 + +# Rendre le script d'initialisation exécutable +RUN chmod +x init_db.py + +# Commande de démarrage avec initialisation de la base +CMD ["sh", "-c", " poetry env activate && cd src && poetry run uvicorn project5.main:app --host 0.0.0.0 --port 7860"] diff --git a/Dockerfile_app b/Dockerfile_app new file mode 100644 index 0000000000000000000000000000000000000000..d91f3ef3d627d513c8df248e62f86ab8f522702d --- /dev/null +++ b/Dockerfile_app @@ -0,0 +1,47 @@ +FROM python:3.11-slim + +# Installer Poetry et les dépendances système pour SQLite +RUN apt-get update && apt-get install -y \ + sqlite3 \ + libsqlite3-dev \ + && rm -rf /var/lib/apt/lists/* \ + && pip install poetry + +# Configuration Poetry +ENV POETRY_NO_INTERACTION=1 \ + POETRY_VENV_IN_PROJECT=1 \ + POETRY_CACHE_DIR=/tmp/poetry_cache \ + VIRTUAL_ENV=/code/.venv \ + PATH="/code/.venv/bin:$PATH" + +WORKDIR /code + +# Copier les fichiers de dépendances +COPY pyproject.toml poetry.lock ./ + +# Copier le code source d'abord +COPY . . + +# Configurer Poetry pour créer le venv dans le projet + +RUN poetry config virtualenvs.in-project true && \ + poetry install --only main --no-interaction --no-ansi + +# Installer toutes les dépendances et le projet +#RUN poetry install --only=main && rm -rf $POETRY_CACHE_DIR + +# Configuration pour SQLite et HuggingFace Spaces +ENV DATABASE_URL="sqlite:///:memory:" \ + ENVIRONMENT="production" \ + PORT=7860 + +ENV ML_MODEL_PATH=../model/model.pkl + +# Exposer le port requis par HF Spaces +EXPOSE 7860 + +# Rendre le script d'initialisation exécutable +RUN chmod +x init_db.py + +# Commande de démarrage avec initialisation de la base +CMD ["sh", "-c", " poetry env activate && cd src && poetry run uvicorn project5.main:app --host 0.0.0.0 --port 7860"] diff --git a/Makefile b/Makefile new file mode 100644 index 0000000000000000000000000000000000000000..89983baaa7d9f53eabed9546b3e2d2e2ba08367b --- /dev/null +++ b/Makefile @@ -0,0 +1,73 @@ +# Makefile pour FastAPI avec Poetry et Docker +.PHONY: help build up down restart logs shell db-shell test lint format clean install migrate seed backup restore + +# Variables +DOCKER_COMPOSE = docker-compose +SERVICE_API = api +SERVICE_DB = db +PYTHON_FILES = src/project5/ tests/ docs/ +PROJECT_NAME = project5 + +# Format des images par default +FORMAT=png + +# Couleurs pour l'affichage +GREEN = \033[0;32m +YELLOW = \033[0;33m +RED = \033[0;31m +NC = \033[0m + +# Help - Affiche l'aide +help: ## Affiche cette aide + @echo "$(GREEN)Makefile pour $(PROJECT_NAME) - FastAPI avec Poetry$(NC)" + @echo "" + @echo "$(YELLOW)Developpement LOCAL (recommande):$(NC)" + @grep -E '^[a-zA-Z_-]*-local:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf " $(YELLOW)%-25s$(NC) %s\n", $$1, $$2}' + +# Developpement local (sans Docker) +dev-local: ## Lance l'API en local avec Poetry + @echo "$(GREEN)Demarrage local avec Poetry...$(NC)" + poetry run uvicorn src.$(PROJECT_NAME).main:app --host 0.0.0.0 --port 8000 --reload + +start-local: ## Alias pour dev-local + $(MAKE) dev-local + +install-local: ## Installe les dependances avec Poetry + @echo "$(GREEN)Installation des dependances avec Poetry...$(NC)" + poetry install + +update-local: ## Met a jour les dependances + @echo "$(GREEN)Mise a jour des dependances...$(NC)" + poetry update + + +# Qualite de code (local) +lint-local: ## Verifie le code avec flake8 et mypy en local + @echo "$(GREEN)Verification du code en local...$(NC)" + poetry run flake8 $(PYTHON_FILES) + poetry run mypy $(PYTHON_FILES) --ignore-missing-imports + +format-local: ## Formate le code avec black et isort en local + @echo "$(GREEN)Formatage du code en local...$(NC)" + poetry run black $(PYTHON_FILES) + poetry run isort $(PYTHON_FILES) + +format-check-local: ## Verifie le formatage sans modifier en local + @echo "$(GREEN)Verification du formatage en local...$(NC)" + poetry run black --check $(PYTHON_FILES) + poetry run isort --check-only $(PYTHON_FILES) + +check-local: format-check-local lint-local ## Verifie le formatage et la qualite du code en local + +# Tests local +test-local: ## Lance les tests en local avec Poetry + @echo "$(GREEN)Execution des tests en local...$(NC)" + poetry run pytest -v + +test-cov-local: ## Lance les tests avec couverture en local + @echo "$(GREEN)Tests avec couverture en local...$(NC)" + poetry run pytest --cov=src/$(PROJECT_NAME) --cov-report=html --cov-report=term-missing + + +# Commande par defaut +.DEFAULT_GOAL := help \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000000000000000000000000000000000000..659b9d29306061552a1b83fa7cf1095e55a4c182 --- /dev/null +++ b/README.md @@ -0,0 +1,20 @@ +--- +title: Building Energy Prediction API +emoji: 🏢 +colorFrom: blue +colorTo: green +sdk: docker +app_port: 7860 +pinned: false +license: mit +--- + +# Building Energy Prediction API + +API FastAPI pour la prédiction de consommation énergétique des bâtiments. + +## Fonctionnalités +- 🏢 Gestion des quartiers, bâtiments et propriétés +- 🤖 Prédictions ML avec RandomForest +- 📊 API REST complète avec documentation Swagger + diff --git a/alembic.ini b/alembic.ini new file mode 100644 index 0000000000000000000000000000000000000000..1e8c258279ae3869032c69fdde1b9a5b2e1cda4b --- /dev/null +++ b/alembic.ini @@ -0,0 +1,116 @@ +# A generic, single database configuration. + +[alembic] +# path to migration scripts +script_location = alembic + +# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s +# Uncomment the line below if you want the files to be prepended with date and time +# see https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file +# for all available tokens +# file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s + +# sys.path path, will be prepended to sys.path if present. +# defaults to the current working directory. +prepend_sys_path = . + +# timezone to use when rendering the date within the migration file +# as well as the filename. +# If specified, requires the python-dateutil library that can be +# installed by adding `alembic[tz]` to the pip requirements +# string value is passed to dateutil.tz.gettz() +# leave blank for localtime +# timezone = + +# max length of characters to apply to the +# "slug" field +# truncate_slug_length = 40 + +# set to 'true' to run the environment during +# the 'revision' command, regardless of autogenerate +# revision_environment = false + +# set to 'true' to allow .pyc and .pyo files without +# a source .py file to be detected as revisions in the +# versions/ directory +# sourceless = false + +# version location specification; This defaults +# to alembic/versions. When using multiple version +# directories, initial revisions must be specified with --version-path. +# The path separator used here should be the separator specified by "version_path_separator" below. +# version_locations = %(here)s/bar:%(here)s/bat:alembic/versions + +# version path separator; As mentioned above, this is the character used to split +# version_locations. The default within new alembic.ini files is "os", which uses os.pathsep. +# If this key is omitted entirely, it falls back to the legacy behavior of splitting on spaces and/or commas. +# Valid values for version_path_separator are: +# +# version_path_separator = : +# version_path_separator = ; +# version_path_separator = space +version_path_separator = os # Use os.pathsep. Default configuration used for new projects. + +# set to 'true' to search source files recursively +# in each "version_locations" directory +# new in Alembic version 1.10 +# recursive_version_locations = false + +# the output encoding used when revision files +# are written from script.py.mako +# output_encoding = utf-8 + +sqlalchemy.url = driver://user:pass@localhost/dbname + + +[post_write_hooks] +# post_write_hooks defines scripts or Python functions that are run +# on newly generated revision scripts. See the documentation for further +# detail and examples + +# format using "black" - use the console_scripts runner, against the "black" entrypoint +# hooks = black +# black.type = console_scripts +# black.entrypoint = black +# black.options = -l 79 REVISION_SCRIPT_FILENAME + +# lint with attempts to fix using "ruff" - use the exec runner, execute a binary +# hooks = ruff +# ruff.type = exec +# ruff.executable = %(here)s/.venv/bin/ruff +# ruff.options = --fix REVISION_SCRIPT_FILENAME + +# Logging configuration +[loggers] +keys = root,sqlalchemy,alembic + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARN +handlers = console +qualname = + +[logger_sqlalchemy] +level = WARN +handlers = +qualname = sqlalchemy.engine + +[logger_alembic] +level = INFO +handlers = +qualname = alembic + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(levelname)-5.5s [%(name)s] %(message)s +datefmt = %H:%M:%S diff --git a/alembic/README b/alembic/README new file mode 100644 index 0000000000000000000000000000000000000000..98e4f9c44effe479ed38c66ba922e7bcc672916f --- /dev/null +++ b/alembic/README @@ -0,0 +1 @@ +Generic single-database configuration. \ No newline at end of file diff --git a/alembic/env.py b/alembic/env.py new file mode 100644 index 0000000000000000000000000000000000000000..7a9dc679e6ebb6fbfee895c22b93829f84747962 --- /dev/null +++ b/alembic/env.py @@ -0,0 +1,75 @@ +from logging.config import fileConfig + +from sqlalchemy import engine_from_config, pool + +from alembic import context + +# this is the Alembic Config object, which provides +# access to the values within the .ini file in use. +config = context.config + +# Interpret the config file for Python logging. +# This line sets up loggers basically. +if config.config_file_name is not None: + fileConfig(config.config_file_name) + +# add your model's MetaData object here +# for 'autogenerate' support +# from myapp import mymodel +# target_metadata = mymodel.Base.metadata +target_metadata = None + +# other values from the config, defined by the needs of env.py, +# can be acquired: +# my_important_option = config.get_main_option("my_important_option") +# ... etc. + + +def run_migrations_offline() -> None: + """Run migrations in 'offline' mode. + + This configures the context with just a URL + and not an Engine, though an Engine is acceptable + here as well. By skipping the Engine creation + we don't even need a DBAPI to be available. + + Calls to context.execute() here emit the given string to the + script output. + + """ + url = config.get_main_option("sqlalchemy.url") + context.configure( + url=url, + target_metadata=target_metadata, + literal_binds=True, + dialect_opts={"paramstyle": "named"}, + ) + + with context.begin_transaction(): + context.run_migrations() + + +def run_migrations_online() -> None: + """Run migrations in 'online' mode. + + In this scenario we need to create an Engine + and associate a connection with the context. + + """ + connectable = engine_from_config( + config.get_section(config.config_ini_section, {}), + prefix="sqlalchemy.", + poolclass=pool.NullPool, + ) + + with connectable.connect() as connection: + context.configure(connection=connection, target_metadata=target_metadata) + + with context.begin_transaction(): + context.run_migrations() + + +if context.is_offline_mode(): + run_migrations_offline() +else: + run_migrations_online() diff --git a/alembic/script.py.mako b/alembic/script.py.mako new file mode 100644 index 0000000000000000000000000000000000000000..fbc4b07dcef98b20c6f96b642097f35e8433258e --- /dev/null +++ b/alembic/script.py.mako @@ -0,0 +1,26 @@ +"""${message} + +Revision ID: ${up_revision} +Revises: ${down_revision | comma,n} +Create Date: ${create_date} + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa +${imports if imports else ""} + +# revision identifiers, used by Alembic. +revision: str = ${repr(up_revision)} +down_revision: Union[str, None] = ${repr(down_revision)} +branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)} +depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)} + + +def upgrade() -> None: + ${upgrades if upgrades else "pass"} + + +def downgrade() -> None: + ${downgrades if downgrades else "pass"} diff --git a/init_db.py b/init_db.py new file mode 100644 index 0000000000000000000000000000000000000000..5ae46374eb45de88413817c3addc52f6ce78f258 --- /dev/null +++ b/init_db.py @@ -0,0 +1,746 @@ +#!/usr/bin/env python3 +"""Script d'initialisation de la base de données SQLite pour HuggingFace Spaces.""" + +import os +import sqlite3 +import sys + +# Ajouter le répertoire src au path pour les imports +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "src")) + +# Connexion globale en mémoire partagée +_db_connection = None + + +def get_db_connection(): + """Retourne la connexion SQLite partagée en mémoire.""" + global _db_connection + if _db_connection is None: + _db_connection = sqlite3.connect(":memory:", check_same_thread=False) + return _db_connection + + +def init_sqlite_data(): + """Initialise la base SQLite avec les données essentielles en utilisant SQLite direct.""" + try: + conn = get_db_connection() + cursor = conn.cursor() + + # Vérifier les tables existantes + cursor.execute("SELECT name FROM sqlite_master WHERE type='table';") + tables = cursor.fetchall() + print(f"📋 Tables existantes: {[table[0] for table in tables]}") + + print("📊 Insertion des données de référence...") + + # Données Neighborhoods + neighborhoods = [ + (1, "UNKNOWN", -1, "2025-09-09 09:56:21", "2025-09-09 09:56:21"), + (2, "BALLARD", 0, "2025-09-09 09:56:21", "2025-09-09 09:56:21"), + (3, "CENTRAL", 1, "2025-09-09 09:56:21", "2025-09-09 09:56:21"), + (4, "DELRIDGE", 2, "2025-09-09 09:56:21", "2025-09-09 09:56:21"), + (5, "DOWNTOWN", 3, "2025-09-09 09:56:21", "2025-09-09 09:56:21"), + (6, "EAST", 4, "2025-09-09 09:56:21", "2025-09-09 09:56:21"), + (7, "GREATER DUWAMISH", 5, "2025-09-09 09:56:21", "2025-09-09 09:56:21"), + (8, "LAKE UNION", 6, "2025-09-09 09:56:21", "2025-09-09 09:56:21"), + ( + 9, + "MAGNOLIA / QUEEN ANNE", + 7, + "2025-09-09 09:56:21", + "2025-09-09 09:56:21", + ), + (10, "NORTH", 8, "2025-09-09 09:56:21", "2025-09-09 09:56:21"), + (11, "NORTHEAST", 9, "2025-09-09 09:56:21", "2025-09-09 09:56:21"), + (12, "NORTHWEST", 10, "2025-09-09 09:56:21", "2025-09-09 09:56:21"), + (13, "SOUTHEAST", 11, "2025-09-09 09:56:21", "2025-09-09 09:56:21"), + (14, "SOUTHWEST", 12, "2025-09-09 09:56:21", "2025-09-09 09:56:21"), + (15, "WEST", 13, "2025-09-09 09:56:21", "2025-09-09 09:56:21"), + ] + + cursor.executemany( + "INSERT OR REPLACE INTO neighborhood (id, neighborhood_name, model_id, created_at, updated_at) VALUES (?, ?, ?, ?, ?)", + neighborhoods, + ) + + # Données Building Types + building_types = [ + ( + 1, + -1, + "UNKNOWN", + "Type de bâtiment inconnu ou non spécifié", + "2025-09-09 09:56:21", + "2025-09-09 09:56:21", + ), + ( + 2, + 0, + "CAMPUS", + "Campus building complex", + "2025-09-09 09:56:21", + "2025-09-09 09:56:21", + ), + ( + 3, + 1, + "NONRESIDENTIAL", + "Non-residential building", + "2025-09-09 09:56:21", + "2025-09-09 09:56:21", + ), + ( + 4, + 2, + "NONRESIDENTIAL COS", + "Non-residential COS type", + "2025-09-09 09:56:21", + "2025-09-09 09:56:21", + ), + ( + 5, + 3, + "NONRESIDENTIAL WA", + "Non-residential WA type", + "2025-09-09 09:56:21", + "2025-09-09 09:56:21", + ), + ( + 6, + 4, + "SPS-DISTRICT K-12", + "Seattle Public Schools District K-12", + "2025-09-09 09:56:21", + "2025-09-09 09:56:21", + ), + ( + 7, + 5, + "Multifamily MR (5-9)", + "Multifamily Mid-Rise 5-9 units", + "2025-09-09 09:56:21", + "2025-09-09 09:56:21", + ), + ( + 8, + 6, + "Multifamily HR (10+)", + "Multifamily High-Rise 10+ units", + "2025-09-09 09:56:21", + "2025-09-09 09:56:21", + ), + ( + 9, + 7, + "Multifamily LR (2-4)", + "Multifamily Low-Rise 2-4 units", + "2025-09-09 09:56:21", + "2025-09-09 09:56:21", + ), + ] + + cursor.executemany( + "INSERT OR REPLACE INTO building_type (id, model_id, building_type_name, description, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)", + building_types, + ) + + # Données Categories + categories = [ + ( + 1, + "UNKNOWN", + "UNKNOWN", + "Catégorie inconnue ou non spécifiée", + "2025-09-09 09:56:21", + "2025-09-09 09:56:21", + ), + ( + 2, + "CAMPUS", + "Campus", + "Complexes de campus et installations multiples", + "2025-09-09 09:56:21", + "2025-09-09 09:56:21", + ), + ( + 3, + "EDUCATION", + "Éducation", + "Établissements denseignement et de formation", + "2025-09-09 09:56:21", + "2025-09-09 09:56:21", + ), + ( + 4, + "ENTERTAINMENT", + "Divertissement", + "Théâtres, cinémas et espaces de divertissement", + "2025-09-09 09:56:21", + "2025-09-09 09:56:21", + ), + ( + 5, + "FINANCIAL", + "Services financiers", + "Banques, bureaux financiers et services monétaires", + "2025-09-09 09:56:21", + "2025-09-09 09:56:21", + ), + ( + 6, + "HEALTHCARE", + "Santé", + "Hôpitaux, cliniques et établissements de soins médicaux", + "2025-09-09 09:56:21", + "2025-09-09 09:56:21", + ), + ( + 7, + "INDUSTRIAL", + "Industrie", + "Usines et installations industrielles", + "2025-09-09 09:56:21", + "2025-09-09 09:56:21", + ), + ( + 8, + "LODGING", + "Hébergement", + "Hôtels et logements temporaires", + "2025-09-09 09:56:21", + "2025-09-09 09:56:21", + ), + ( + 9, + "MIXED", + "Usage mixte", + "Propriétés à usage multiple", + "2025-09-09 09:56:21", + "2025-09-09 09:56:21", + ), + ( + 10, + "NONE", + "Aucun", + "Aucune utilisation spécifique", + "2025-09-09 09:56:21", + "2025-09-09 09:56:21", + ), + ( + 11, + "OFFICE", + "Bureaux", + "Espaces de bureaux et administratifs", + "2025-09-09 09:56:21", + "2025-09-09 09:56:21", + ), + ( + 12, + "PARKING", + "Stationnement", + "Structures et espaces de stationnement", + "2025-09-09 09:56:21", + "2025-09-09 09:56:21", + ), + ( + 13, + "PUBLIC", + "Services publics", + "Services gouvernementaux et publics", + "2025-09-09 09:56:21", + "2025-09-09 09:56:21", + ), + ( + 14, + "RECREATION", + "Loisirs", + "Installations sportives et récréatives", + "2025-09-09 09:56:21", + "2025-09-09 09:56:21", + ), + ( + 15, + "RELIGIOUS", + "Religieux", + "Églises et lieux de culte", + "2025-09-09 09:56:21", + "2025-09-09 09:56:21", + ), + ( + 16, + "RESIDENTIAL", + "Résidentiel", + "Logements et habitations", + "2025-09-09 09:56:21", + "2025-09-09 09:56:21", + ), + ( + 17, + "RESTAURANT", + "Restauration", + "Restaurants et services alimentaires", + "2025-09-09 09:56:21", + "2025-09-09 09:56:21", + ), + ( + 18, + "RETAIL", + "Commerce de détail", + "Magasins et commerces", + "2025-09-09 09:56:21", + "2025-09-09 09:56:21", + ), + ( + 19, + "SOCIAL", + "Social", + "Clubs et espaces sociaux", + "2025-09-09 09:56:21", + "2025-09-09 09:56:21", + ), + ( + 20, + "STORE", + "Magasins", + "Commerces et magasins divers", + "2025-09-09 09:56:21", + "2025-09-09 09:56:21", + ), + ] + + cursor.executemany( + "INSERT OR REPLACE INTO categories (id, category_code, category_name, description, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)", + categories, + ) + + # Données Properties (sélection des principales) + properties = [ + (1, -1, "UNKNOWN", 1, "2025-09-09 09:56:21", "2025-09-09 09:56:21"), + (2, 0, "ADULT EDUCATION", 3, "2025-09-09 09:56:21", "2025-09-09 09:56:21"), + ( + 3, + 1, + "AUTOMOBILE DEALERSHIP", + 20, + "2025-09-09 09:56:21", + "2025-09-09 09:56:21", + ), + (4, 2, "BANK BRANCH", 5, "2025-09-09 09:56:21", "2025-09-09 09:56:21"), + (5, 3, "BAR/NIGHTCLUB", 19, "2025-09-09 09:56:21", "2025-09-09 09:56:21"), + ( + 6, + 4, + "COLLEGE/UNIVERSITY", + 3, + "2025-09-09 09:56:21", + "2025-09-09 09:56:21", + ), + ( + 7, + 5, + "CONVENIENCE STORE WITHOUT GAS STATION", + 20, + "2025-09-09 09:56:21", + "2025-09-09 09:56:21", + ), + (8, 6, "COURTHOUSE", 15, "2025-09-09 09:56:21", "2025-09-09 09:56:21"), + (9, 7, "DATA CENTER", 11, "2025-09-09 09:56:21", "2025-09-09 09:56:21"), + ( + 10, + 8, + "DISTRIBUTION CENTER", + 7, + "2025-09-09 09:56:21", + "2025-09-09 09:56:21", + ), + ( + 11, + 9, + "FINANCIAL OFFICE", + 5, + "2025-09-09 09:56:21", + "2025-09-09 09:56:21", + ), + (12, 10, "FOOD SALES", 17, "2025-09-09 09:56:21", "2025-09-09 09:56:21"), + ( + 13, + 11, + "HOSPITAL (GENERAL MEDICAL & SURGICAL)", + 6, + "2025-09-09 09:56:21", + "2025-09-09 09:56:21", + ), + (14, 12, "HOTEL", 8, "2025-09-09 09:56:21", "2025-09-09 09:56:21"), + (15, 13, "K-12 SCHOOL", 3, "2025-09-09 09:56:21", "2025-09-09 09:56:21"), + (16, 14, "LIBRARY", 3, "2025-09-09 09:56:21", "2025-09-09 09:56:21"), + (17, 15, "MEDICAL OFFICE", 6, "2025-09-09 09:56:21", "2025-09-09 09:56:21"), + ( + 18, + 16, + "MULTIFAMILY HOUSING", + 16, + "2025-09-09 09:56:21", + "2025-09-09 09:56:21", + ), + ( + 19, + 17, + "MUNICIPAL WASTEWATER TREATMENT PLANT", + 7, + "2025-09-09 09:56:21", + "2025-09-09 09:56:21", + ), + (20, 18, "OFFICE", 11, "2025-09-09 09:56:21", "2025-09-09 09:56:21"), + (21, 19, "OTHER", 1, "2025-09-09 09:56:21", "2025-09-09 09:56:21"), + (22, 20, "PARKING", 12, "2025-09-09 09:56:21", "2025-09-09 09:56:21"), + (23, 21, "RESTAURANT", 17, "2025-09-09 09:56:21", "2025-09-09 09:56:21"), + (24, 22, "RETAIL STORE", 18, "2025-09-09 09:56:21", "2025-09-09 09:56:21"), + ( + 25, + 23, + "SELF-STORAGE FACILITY", + 7, + "2025-09-09 09:56:21", + "2025-09-09 09:56:21", + ), + ( + 26, + 24, + "SENIOR LIVING COMMUNITY", + 8, + "2025-09-09 09:56:21", + "2025-09-09 09:56:21", + ), + ( + 27, + 25, + "SUPERMARKET/GROCERY STORE", + 17, + "2025-09-09 09:56:21", + "2025-09-09 09:56:21", + ), + ( + 28, + 26, + "WAREHOUSE (UNREFRIGERATED)", + 7, + "2025-09-09 09:56:21", + "2025-09-09 09:56:21", + ), + ( + 29, + 27, + "WORSHIP FACILITY", + 15, + "2025-09-09 09:56:21", + "2025-09-09 09:56:21", + ), + ] + + cursor.executemany( + "INSERT OR REPLACE INTO property (id, model_id, property_name, category_id, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)", + properties, + ) + + # Données Building Models (exemples pour tester l'API) + building_models = [ + ( + 1, + "SEATTLE001", + "123 Main Street", + "Seattle", + "WA", + "98101", + "TAX001", + "DISTRICT1", + 47.6062, + -122.3321, + 1995, + 1, + 15, + 50000.0, + 5000.0, + 2000.0, + 500.0, + 0, # multiusage + 0, # steam + 1, # electricity + 1, # natural_gas + 2, # neighborhood_id (BALLARD) + 3, # building_type_id (NONRESIDENTIAL) + 20, # largest_property_use_type_id (OFFICE) + 20, # primary_property_type_id (OFFICE) + 1, # second_largest_property_use_type_id (UNKNOWN) + 1, # third_largest_property_use_type_id (UNKNOWN) + "2025-09-09 09:56:21", + "2025-09-09 09:56:21", + ), + ( + 2, + "SEATTLE002", + "456 Pine Avenue", + "Seattle", + "WA", + "98102", + "TAX002", + "DISTRICT2", + 47.6205, + -122.3493, + 2010, + 1, + 8, + 25000.0, + 2000.0, + 0.0, + 0.0, + 0, # multiusage + 0, # steam + 1, # electricity + 1, # natural_gas + 5, # neighborhood_id (DOWNTOWN) + 7, # building_type_id (Multifamily MR 5-9) + 18, # largest_property_use_type_id (MULTIFAMILY HOUSING) + 18, # primary_property_type_id (MULTIFAMILY HOUSING) + 1, # second_largest_property_use_type_id (UNKNOWN) + 1, # third_largest_property_use_type_id (UNKNOWN) + "2025-09-09 09:56:21", + "2025-09-09 09:56:21", + ), + ( + 3, + "SEATTLE003", + "789 University Way", + "Seattle", + "WA", + "98105", + "TAX003", + "DISTRICT3", + 47.6587, + -122.3128, + 1980, + 1, + 3, + 15000.0, + 1000.0, + 500.0, + 200.0, + 1, # multiusage + 0, # steam + 1, # electricity + 1, # natural_gas + 11, # neighborhood_id (NORTHEAST) + 6, # building_type_id (SPS-DISTRICT K-12) + 15, # largest_property_use_type_id (K-12 SCHOOL) + 15, # primary_property_type_id (K-12 SCHOOL) + 16, # second_largest_property_use_type_id (LIBRARY) + 1, # third_largest_property_use_type_id (UNKNOWN) + "2025-09-09 09:56:21", + "2025-09-09 09:56:21", + ), + ] + + cursor.executemany( + """INSERT OR REPLACE INTO building_models ( + id, ose_building_id, address, city, state, zip_code, + tax_parcel_identification_number, council_district_code, + latitude, longitude, year_built, number_of_buildings, number_of_floors, + property_gfa_total, property_gfa_parking, + second_largest_property_use_type_gfa, third_largest_property_use_type_gfa, + multiusage, steam, electricity, natural_gas, + neighborhood_id, building_type_id, + largest_property_use_type_id, primary_property_type_id, + second_largest_property_use_type_id, third_largest_property_use_type_id, + created_at, updated_at + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""", + building_models, + ) + + # Données Building Energy Predictions (exemples de prédictions) + energy_predictions = [ + ( + 1, + 1, # building_id (correspond à SEATTLE001) + 45678.5, # site_energy_use_wn_kbtu + 1, # predicted (True) + "2025-09-09 10:30:00", + ), + ( + 2, + 2, # building_id (correspond à SEATTLE002) + 23456.7, # site_energy_use_wn_kbtu + 1, # predicted (True) + "2025-09-09 11:15:00", + ), + ( + 3, + 3, # building_id (correspond à SEATTLE003) + 12345.3, # site_energy_use_wn_kbtu + 1, # predicted (True) + "2025-09-09 12:00:00", + ), + ] + + cursor.executemany( + """INSERT OR REPLACE INTO building_energy_predictions ( + id, building_id, site_energy_use_wn_kbtu, predicted, updated_at + ) VALUES (?, ?, ?, ?, ?)""", + energy_predictions, + ) + + conn.commit() + # Ne pas fermer la connexion - elle reste en mémoire + + print(f"✅ {len(neighborhoods)} quartiers insérés") + print(f"✅ {len(building_types)} types de bâtiments insérés") + print(f"✅ {len(categories)} catégories insérées") + print(f"✅ {len(properties)} propriétés insérées") + print(f"✅ {len(building_models)} bâtiments d'exemple insérés") + print(f"✅ {len(energy_predictions)} prédictions d'exemple insérées") + + except Exception as e: + print(f"❌ Erreur lors de l'insertion des données: {e}") + raise + + +def create_sqlite_tables(): + """Crée les tables SQLite nécessaires avec raw SQL.""" + + try: + conn = get_db_connection() + cursor = conn.cursor() + + # Créer les tables de référence principales + cursor.execute( + """ + CREATE TABLE IF NOT EXISTS neighborhood ( + id INTEGER PRIMARY KEY, + neighborhood_name VARCHAR(50) NOT NULL UNIQUE, + model_id INTEGER NOT NULL UNIQUE, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP + ); + """ + ) + + cursor.execute( + """ + CREATE TABLE IF NOT EXISTS building_type ( + id INTEGER PRIMARY KEY, + model_id INTEGER NOT NULL UNIQUE, + building_type_name VARCHAR(100) NOT NULL UNIQUE, + description TEXT, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP + ); + """ + ) + + cursor.execute( + """ + CREATE TABLE IF NOT EXISTS categories ( + id INTEGER PRIMARY KEY, + category_code VARCHAR(50) NOT NULL UNIQUE, + category_name VARCHAR(100) NOT NULL, + description TEXT, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP + ); + """ + ) + + cursor.execute( + """ + CREATE TABLE IF NOT EXISTS property ( + id INTEGER PRIMARY KEY, + model_id INTEGER NOT NULL UNIQUE, + property_name VARCHAR(150) NOT NULL UNIQUE, + category_id INTEGER NOT NULL, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (category_id) REFERENCES categories (id) + ); + """ + ) + + # Créer les tables principales + cursor.execute( + """ + CREATE TABLE IF NOT EXISTS building_energy_predictions ( + id INTEGER PRIMARY KEY, + building_id INTEGER NOT NULL, + site_energy_use_wn_kbtu FLOAT NOT NULL, + predicted BOOLEAN DEFAULT 0, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP + ); + """ + ) + + cursor.execute( + """ + CREATE TABLE IF NOT EXISTS building_models ( + id INTEGER PRIMARY KEY, + ose_building_id INTEGER NOT NULL UNIQUE, + address VARCHAR(255) NOT NULL, + city VARCHAR(100) NULL, + state VARCHAR(50) NULL, + zip_code VARCHAR(20) NULL, + tax_parcel_identification_number VARCHAR(100) NULL, + council_district_code varchar(20) NULL, + latitude FLOAT NULL, + longitude FLOAT NULL, + year_built INTEGER NULL, + number_of_buildings INTEGER NULL, + number_of_floors INTEGER NULL, + property_gfa_total FLOAT NULL, + property_gfa_parking FLOAT NULL, + second_largest_property_use_type_gfa FLOAT NULL, + third_largest_property_use_type_gfa FLOAT NULL, + multiusage BOOLEAN DEFAULT 0, + steam BOOLEAN DEFAULT 0, + electricity BOOLEAN DEFAULT 0, + natural_gas BOOLEAN DEFAULT 0, + neighborhood_id INTEGER NULL, + building_type_id INTEGER NULL, + largest_property_use_type_id INTEGER NULL, + primary_property_type_id INTEGER NULL, + second_largest_property_use_type_id INTEGER NULL, + third_largest_property_use_type_id INTEGER NULL, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (id) REFERENCES building_energy_predictions (building_id), + FOREIGN KEY (largest_property_use_type_id) REFERENCES property (id), + FOREIGN KEY (primary_property_type_id) REFERENCES property (id), + FOREIGN KEY (second_largest_property_use_type_id) REFERENCES property (id), + FOREIGN KEY (third_largest_property_use_type_id) REFERENCES property (id), + FOREIGN KEY (neighborhood_id) REFERENCES neighborhood (id), + FOREIGN KEY (building_type_id) REFERENCES building_type (id) + ); + """ + ) + + conn.commit() + # Ne pas fermer la connexion - elle reste en mémoire + print("🗄️ Structure des tables créée avec succès!") + + except Exception as e: + print(f"❌ Erreur lors de la création des tables: {e}") + raise + + +if __name__ == "__main__": + try: + print("🚀 Initialisation de la base de données SQLite...") + + # Créer les tables avec raw SQL + create_sqlite_tables() + + print("📊 Insertion des données de référence...") + init_sqlite_data() + print("✅ Base de données SQLite initialisée avec succès!") + + except Exception as e: + print(f"❌ Erreur lors de l'initialisation: {e}") + import traceback + + traceback.print_exc() + sys.exit(1) diff --git a/model/Makefile b/model/Makefile new file mode 100644 index 0000000000000000000000000000000000000000..bda0c0a8d4b0a000c01fb4f86c87deadb671220b --- /dev/null +++ b/model/Makefile @@ -0,0 +1,178 @@ +# Makefile pour la génération et gestion du modèle ML +.PHONY: help model train validate clean info backup restore test + +# Variables +PYTHON = python +MODEL_FILE = model.pkl +MODEL_INFO = model_info.json +BACKUP_DIR = backup +TIMESTAMP = $(shell date +%Y%m%d_%H%M%S) + +# Couleurs pour l'affichage +GREEN = \033[0;32m +YELLOW = \033[0;33m +RED = \033[0;31m +NC = \033[0m + +# Help - Affiche l'aide +help: ## Affiche cette aide + @echo "$(GREEN)Makefile pour la gestion du modele ML$(NC)" + @echo "" + @echo "$(YELLOW)Generation du modele:$(NC)" + @echo " $(YELLOW)model $(NC) Alias pour train" + @echo " $(YELLOW)train $(NC) Genere le modele ML via model.py" + @echo " $(YELLOW)retrain $(NC) Force la regeneration du modele" + @echo " $(YELLOW)setup $(NC) Configuration et generation complete" + @echo "" + @echo "$(YELLOW)Validation et tests:$(NC)" + @echo " $(YELLOW)validate $(NC) Valide l'existence et l'integrite du modele" + @echo " $(YELLOW)test $(NC) Lance les tests de validation du modele" + @echo " $(YELLOW)test-prediction $(NC) Test une prediction simple" + @echo "" + @echo "$(YELLOW)Gestion et maintenance:$(NC)" + @echo " $(YELLOW)info $(NC) Affiche les informations detaillees du modele" + @echo " $(YELLOW)model-size $(NC) Affiche la taille des fichiers du modele" + @echo " $(YELLOW)backup $(NC) Sauvegarde le modele actuel" + @echo " $(YELLOW)restore $(NC) Restaure le dernier modele sauvegarde" + @echo " $(YELLOW)list-backups $(NC) Liste toutes les sauvegardes disponibles" + @echo " $(YELLOW)clean $(NC) Supprime les fichiers du modele actuel" + @echo " $(YELLOW)clean-all $(NC) Supprime tout (modele + sauvegardes)" + @echo "" + @echo "$(YELLOW)Environnement et verifications:$(NC)" + @echo " $(YELLOW)check-deps $(NC) Verifie les dependances Python" + @echo " $(YELLOW)check-db $(NC) Verifie la connexion a la base de donnees" + +# Génération du modèle +model: train ## Alias pour train + +train: ## Génère le modèle ML via model.py + @echo "$(GREEN)Generation du modele de Machine Learning...$(NC)" + @if [ ! -f "$(MODEL_FILE)" ]; then \ + echo "$(YELLOW)Aucun modele existant trouve. Generation d'un nouveau modele...$(NC)"; \ + else \ + echo "$(YELLOW)Un modele existant a ete trouve. Sauvegarde avant regeneration...$(NC)"; \ + $(MAKE) backup; \ + fi + $(PYTHON) model.py + @echo "$(GREEN)✅ Modele genere avec succes!$(NC)" + @if [ -f "$(MODEL_INFO)" ]; then \ + echo "$(YELLOW)Informations du modele:$(NC)"; \ + cat $(MODEL_INFO) | head -10; \ + fi + +retrain: clean train ## Force la régénération du modèle + +# Validation et tests +validate: ## Valide l'existence et l'intégrité du modèle + @echo "$(GREEN)Validation du modele...$(NC)" + @if [ ! -f "$(MODEL_FILE)" ]; then \ + echo "$(RED)❌ Fichier modele manquant: $(MODEL_FILE)$(NC)"; \ + exit 1; \ + else \ + echo "$(GREEN)✅ Fichier modele trouve: $(MODEL_FILE)$(NC)"; \ + fi + @if [ ! -f "$(MODEL_INFO)" ]; then \ + echo "$(RED)❌ Fichier d'information manquant: $(MODEL_INFO)$(NC)"; \ + exit 1; \ + else \ + echo "$(GREEN)✅ Fichier d'information trouve: $(MODEL_INFO)$(NC)"; \ + fi + @echo "$(GREEN)✅ Modele valide avec succes!$(NC)" + +test: validate ## Lance les tests de validation du modèle + @echo "$(GREEN)Test de chargement du modele...$(NC)" + @$(PYTHON) -c "import joblib; model = joblib.load('$(MODEL_FILE)'); print('✅ Modele charge avec succes'); print(f'Type: {type(model).__name__}'); import json; info = json.load(open('$(MODEL_INFO)')); print(f'Accuracy: {info.get(\"accuracy\", \"N/A\")}'); print(f'Features: {len(info.get(\"features\", []))} variables')" + +test-prediction: validate ## Test une prédiction simple + @echo "$(GREEN)Test de prediction...$(NC)" + @$(PYTHON) -c "import joblib; import json; import numpy as np; model = joblib.load('$(MODEL_FILE)'); info = json.load(open('$(MODEL_INFO)')); features = info['features']; test_data = np.random.random((1, len(features))); pred = model.predict(test_data); print(f'✅ Prediction test reussie: {pred[0]:.4f}')" + +# Gestion et maintenance +info: ## Affiche les informations détaillées du modèle + @echo "$(GREEN)Informations du modele:$(NC)" + @if [ -f "$(MODEL_INFO)" ]; then \ + cat $(MODEL_INFO); \ + else \ + echo "$(RED)❌ Fichier d'information non trouve$(NC)"; \ + exit 1; \ + fi + +model-size: ## Affiche la taille des fichiers du modèle + @echo "$(GREEN)Taille des fichiers:$(NC)" + @if [ -f "$(MODEL_FILE)" ]; then \ + ls -lh $(MODEL_FILE) | awk '{print "Modele: " $$5 " (" $$9 ")"}'; \ + fi + @if [ -f "$(MODEL_INFO)" ]; then \ + ls -lh $(MODEL_INFO) | awk '{print "Info: " $$5 " (" $$9 ")"}'; \ + fi + +backup: ## Sauvegarde le modèle actuel + @echo "$(GREEN)Sauvegarde du modele...$(NC)" + @mkdir -p $(BACKUP_DIR) + @if [ -f "$(MODEL_FILE)" ]; then \ + cp $(MODEL_FILE) $(BACKUP_DIR)/model_$(TIMESTAMP).pkl; \ + echo "$(GREEN)✅ Modele sauvegarde: $(BACKUP_DIR)/model_$(TIMESTAMP).pkl$(NC)"; \ + fi + @if [ -f "$(MODEL_INFO)" ]; then \ + cp $(MODEL_INFO) $(BACKUP_DIR)/model_info_$(TIMESTAMP).json; \ + echo "$(GREEN)✅ Info sauvegardee: $(BACKUP_DIR)/model_info_$(TIMESTAMP).json$(NC)"; \ + fi + +restore: ## Restaure le dernier modèle sauvegardé + @echo "$(GREEN)Restauration du dernier modele...$(NC)" + @if [ ! -d "$(BACKUP_DIR)" ]; then \ + echo "$(RED)❌ Aucune sauvegarde trouvee$(NC)"; \ + exit 1; \ + fi + @LATEST_MODEL=$$(ls -t $(BACKUP_DIR)/model_*.pkl 2>/dev/null | head -1); \ + LATEST_INFO=$$(ls -t $(BACKUP_DIR)/model_info_*.json 2>/dev/null | head -1); \ + if [ -n "$$LATEST_MODEL" ]; then \ + cp "$$LATEST_MODEL" $(MODEL_FILE); \ + echo "$(GREEN)✅ Modele restaure: $$LATEST_MODEL$(NC)"; \ + fi; \ + if [ -n "$$LATEST_INFO" ]; then \ + cp "$$LATEST_INFO" $(MODEL_INFO); \ + echo "$(GREEN)✅ Info restauree: $$LATEST_INFO$(NC)"; \ + fi + +list-backups: ## Liste toutes les sauvegardes disponibles + @echo "$(GREEN)Sauvegardes disponibles:$(NC)" + @if [ -d "$(BACKUP_DIR)" ]; then \ + ls -la $(BACKUP_DIR)/ | grep -E '\.(pkl|json)$$' | awk '{print $$9 " (" $$5 " bytes, " $$6 " " $$7 " " $$8 ")"}' || echo "$(YELLOW)Aucune sauvegarde trouvee$(NC)"; \ + else \ + echo "$(YELLOW)Aucun dossier de sauvegarde$(NC)"; \ + fi + +clean: ## Supprime les fichiers du modèle actuel + @echo "$(GREEN)Nettoyage des fichiers du modele...$(NC)" + @if [ -f "$(MODEL_FILE)" ]; then \ + rm $(MODEL_FILE); \ + echo "$(GREEN)✅ $(MODEL_FILE) supprime$(NC)"; \ + fi + @if [ -f "$(MODEL_INFO)" ]; then \ + rm $(MODEL_INFO); \ + echo "$(GREEN)✅ $(MODEL_INFO) supprime$(NC)"; \ + fi + +clean-all: clean ## Supprime tout (modèle + sauvegardes) + @echo "$(GREEN)Nettoyage complet...$(NC)" + @if [ -d "$(BACKUP_DIR)" ]; then \ + rm -rf $(BACKUP_DIR); \ + echo "$(GREEN)✅ Dossier de sauvegarde supprime$(NC)"; \ + fi + +# Environnement et dépendances +check-deps: ## Vérifie les dépendances Python + @echo "$(GREEN)Verification des dependances...$(NC)" + @$(PYTHON) -c "import sklearn, pandas, numpy, joblib; print('✅ Toutes les dependances sont disponibles')" || (echo "$(RED)❌ Dependances manquantes$(NC)" && exit 1) + +check-db: ## Vérifie la connexion à la base de données + @echo "$(GREEN)Verification de la connexion DB...$(NC)" + @$(PYTHON) -c "from sqlalchemy import create_engine; import os; engine = create_engine(os.getenv('DATABASE_URL', 'postgresql://user:password@localhost/dbname')); engine.connect(); print('✅ Connexion DB reussie')" || (echo "$(RED)❌ Erreur de connexion DB$(NC)" && exit 1) + +# Workflow complet +setup: check-deps check-db train ## Configuration et génération complète + @echo "$(GREEN)✅ Setup complet termine!$(NC)" + +# Commande par défaut +.DEFAULT_GOAL := help \ No newline at end of file diff --git a/model/README.md b/model/README.md new file mode 100644 index 0000000000000000000000000000000000000000..b74c3654fd2ba1f74cd9fea589caf410108fddab --- /dev/null +++ b/model/README.md @@ -0,0 +1,205 @@ +# 🤖 Modèle de Machine Learning - Project5 + +Ce dossier contient le modèle de Machine Learning pour la prédiction de consommation énergétique des bâtiments. + +## 📁 Structure des fichiers + +``` +model/ +├── model.py # Script de génération du modèle +├── model.pkl # Modèle RandomForest sérialisé (26MB) +├── model_info.json # Métadonnées et métriques du modèle +├── generate_model_report.py # Générateur de rapport HTML +├── Makefile # Automatisation des tâches +├── backup/ # Sauvegardes des modèles +└── README.md # Cette documentation +``` + +## 🎯 Modèle actuel + +### Informations générales +- **Type** : RandomForestRegressor +- **Version** : 1.0 +- **Créé le** : 2025-09-21T11:07:42 +- **Taille** : 26.0 MB +- **Variables** : 17 features + +### Métriques de performance +```json +{ + "accuracy": 0.916, + "RMSE": 0.09, + "MAE": 0.22, + "R²": 0.92 +} +``` + +### Variables d'entrée (17 features) +``` +year_built +number_of_buildings +number_of_floors +property_gfa_total +property_gfa_parking +second_largest_property_use_type_gfa +third_largest_property_use_type_gfa +multiusage +steam +electricity +natural_gas +neighborhood_id +building_type_id +largest_property_use_type_id +primary_property_type_id +second_largest_property_use_type_id +third_largest_property_use_type_id +``` + +## 🚀 Utilisation rapide + +### Génération du modèle +```bash +# Installation et génération complète +make setup + +# Génération simple +make train + +# Force la régénération +make retrain +``` + +### Validation et tests +```bash +# Valider l'intégrité du modèle +make validate + +# Tester le chargement +make test + +# Tester une prédiction +make test-prediction +``` + +### Rapport et informations +```bash +# Générer un rapport HTML complet +make report + +# Informations rapides +make info + +# Taille des fichiers +make model-size +``` + +## 📊 Commandes disponibles + +### **Génération du modèle** +- `make model` / `make train` - Génère le modèle via model.py +- `make retrain` - Force la régénération (supprime + recrée) +- `make setup` - Workflow complet (dépendances + DB + génération) + +### **Validation et tests** +- `make validate` - Vérifie l'existence et l'intégrité des fichiers +- `make test` - Test de chargement et validation du modèle +- `make test-prediction` - Test d'une prédiction avec données aléatoires + +### **Gestion et maintenance** +- `make info` - Affiche les informations détaillées (JSON) +- `make model-size` - Affiche la taille des fichiers +- `make backup` - Sauvegarde avec timestamp +- `make restore` - Restaure la dernière sauvegarde +- `make list-backups` - Liste toutes les sauvegardes +- `make clean` / `make clean-all` - Nettoyage + +### **Environnement** +- `make check-deps` - Vérifie les dépendances Python +- `make check-db` - Teste la connexion à la base de données + + +## 🔧 Configuration technique + +### Prérequis +```python +# Dépendances principales +import sklearn +import pandas +import numpy +import joblib +``` + +### Variables d'environnement +```bash +DATABASE_URL=postgresql://user:password@localhost/dbname +``` + +### Source des données +Le modèle est entraîné sur la vue `model_view` de la base de données PostgreSQL qui agrège : +- Données Seattle (2016_Building_Energy_Benchmarking.csv) +- Données OSE (building_consumption_OSEBuildingID.csv) + +## 🛠️ Développement + +### Regeneration du modèle +1. Modifier les hyperparamètres dans `model.py` +2. Exécuter `make train` +3. Valider avec `make test` +4. Générer le rapport avec `make report` + +### Sauvegarde et versioning +```bash +# Sauvegarde manuelle +make backup + +# Lister les versions +make list-backups + +# Restaurer une version précédente +make restore +``` + +### Structure du modèle +```python +# Configuration RandomForest +RandomForestRegressor( + n_estimators=500, + max_features=0.5, + random_state=42, + min_samples_split=5, + max_depth=20 +) +``` + +## 📊 Pipeline de données + +1. **Source** : Vue `model_view` (PostgreSQL) +2. **Transformation** : Log de la variable cible `site_energy_use_wn_kbtu` +3. **Split** : 80/20 train/test (random_state=42) +4. **Validation** : Cross-validation 5-fold +5. **Métriques** : RMSE, MAE, R², Accuracy + +## 🔗 Intégration API + +Le modèle est automatiquement chargé par l'API FastAPI au démarrage : + +```python +# Dans main.py +from project5.ml.model import MLModel + +model = MLModel() +model.load_model() # Charge model.pkl +``` + +## 📞 Support + +Pour toute question ou problème : + +1. **Vérifier l'intégrité** : `make validate` +2. **Tester le modèle** : `make test` +3. **Générer un rapport** : `make report` +4. **Consulter les logs** : Vérifier les sorties des commandes make + +--- + +**🏢 Project5 - API de gestion énergétique des bâtiments** \ No newline at end of file diff --git a/model/model.pkl b/model/model.pkl new file mode 100644 index 0000000000000000000000000000000000000000..c65325a60473ce2b1bd9b248cf363357466b231c --- /dev/null +++ b/model/model.pkl @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:40c2d682207023def3bfbebf149e5c33e219aa80b5a95f7daec8452ccee1f697 +size 27298225 diff --git a/model/model.py b/model/model.py new file mode 100644 index 0000000000000000000000000000000000000000..213c03e2db9168be79cb2af5d988c538719431a1 --- /dev/null +++ b/model/model.py @@ -0,0 +1,130 @@ +# This Python file uses the following encoding: utf-8 +# Permet de générer le modèle à partir de la base de données +# Reprise et adaptation du notebook du projet3 +import json +import os +from datetime import datetime + +import joblib +import numpy as np +import pandas as pd +from sklearn.ensemble import RandomForestRegressor +from sklearn.metrics import mean_absolute_error, mean_squared_error, r2_score +from sklearn.model_selection import cross_validate, train_test_split +from sqlalchemy import create_engine + +DATABASE_URL = os.getenv("DATABASE_URL", "postgresql://user:password@localhost/dbname") + + +def get_for_model(features): + # Separation du jeu de données en train, test + X = building_consumption[features] + y = building_consumption["log_" + var_a_predire] + X_train, X_test, y_train, y_test = train_test_split( + X, y, test_size=0.2, random_state=42, shuffle=True + ) + + # X_train, X_val, y_train, y_val = train_test_split(X_train, y_train, test_size=0.25, random_state=42) + print(f"Shape de X_train: {X_train.shape}") + print(f"Shape de y_train: {y_train.shape}") + print(f"Shape de X_test: {X_test.shape}") + print(f"Shape de y_train: {y_test.shape}") + return X_train, X_test, y_train, y_test + + +def get_score(y, prediction): + mse = mean_squared_error(y, prediction) + mae = mean_absolute_error(y, prediction) + r2 = r2_score(y, prediction) + return mse, mae, r2 + + +scores = {} + +engine = create_engine(DATABASE_URL) + +query = "SELECT * FROM model_view" +building_consumption = pd.read_sql(query, engine) + +print(building_consumption.info()) +engine.dispose() + +var_a_predire = "site_energy_use_wn_kbtu" +building_consumption["log_" + var_a_predire] = np.log( + building_consumption[var_a_predire] +) + + +# Features +features = [ + "year_built", + "number_of_buildings", + "number_of_floors", + "property_gfa_total", + "property_gfa_parking", + "second_largest_property_use_type_gfa", + "third_largest_property_use_type_gfa", + "multiusage", + "steam", + "electricity", + "natural_gas", + "neighborhood_id", + "building_type_id", + "largest_property_use_type_id", + "primary_property_type_id", + "second_largest_property_use_type_id", + "third_largest_property_use_type_id", +] +X_train, X_test, y_train, y_test = get_for_model(features) + +# Initialisation du modèle +rf = RandomForestRegressor( + n_estimators=500, + max_features=0.5, + random_state=42, + min_samples_split=5, + max_depth=20, +) + +# Entraînement sur l'ensemble des données +X = building_consumption[features] +y = building_consumption["log_" + var_a_predire] +rf.fit(X, y) + +# Prédiction +y_pred = rf.predict(X_test) +scores_cross = cross_validate( + rf, X_train, y_train, cv=5 +) # cv=5 pour une validation croisée à 5 plis +fit_time = scores_cross["fit_time"].mean() +score_time = scores_cross["score_time"].mean() +scores.update( + {"RandomForestRegressor HP": get_score(y_test, y_pred) + (fit_time, score_time)} +) + +resultats = pd.DataFrame(scores).T +resultats.columns = ["RMSE", "MAE", "R^2", "Fit Time", "Score Time"] +resultats = resultats.round(2) +print(resultats[-1:1]["RMSE"].values) + +# Sauvegarde du modèle +# Informations du modèle +model_info = { + "model_type": type(rf).__name__, + "model_module": type(rf).__module__, + "has_feature_importances": hasattr(rf, "feature_importances_"), + "has_coefficients": hasattr(rf, "coef_"), + "has_predict_proba": hasattr(rf, "predict_proba"), + "version": "1.0", + "created_at": datetime.now().isoformat(), + "features": list(X_train.columns), + "accuracy": rf.score(X_test, y_test), + "RMSE": resultats[-1:1]["RMSE"].values[0], + "MAE": resultats[-1:1]["MAE"].values[0], + "R^2": resultats[-1:1]["R^2"].values[0], +} + +# Sauvegarder modèle et métadonnées +joblib.dump(rf, "model.pkl") +with open("model_info.json", "w") as f: + json.dump(model_info, f, indent=2) diff --git a/model/model_info.json b/model/model_info.json new file mode 100644 index 0000000000000000000000000000000000000000..fae0a67aedc440be8da83d39d20423733d497644 --- /dev/null +++ b/model/model_info.json @@ -0,0 +1,32 @@ +{ + "model_type": "RandomForestRegressor", + "model_module": "sklearn.ensemble._forest", + "has_feature_importances": true, + "has_coefficients": false, + "has_predict_proba": false, + "version": "1.0", + "created_at": "2025-09-21T11:07:42.926661", + "features": [ + "year_built", + "number_of_buildings", + "number_of_floors", + "property_gfa_total", + "property_gfa_parking", + "second_largest_property_use_type_gfa", + "third_largest_property_use_type_gfa", + "multiusage", + "steam", + "electricity", + "natural_gas", + "neighborhood_id", + "building_type_id", + "largest_property_use_type_id", + "primary_property_type_id", + "second_largest_property_use_type_id", + "third_largest_property_use_type_id" + ], + "accuracy": 0.9160018446396789, + "RMSE": 0.09, + "MAE": 0.22, + "R^2": 0.92 +} \ No newline at end of file diff --git a/model/model_report.html b/model/model_report.html new file mode 100644 index 0000000000000000000000000000000000000000..2df9d7ab2ae7ce3d74620bf08b74c5e55988af90 --- /dev/null +++ b/model/model_report.html @@ -0,0 +1,449 @@ + + + + + + + Rapport Modèle ML - Project5 + + + +
+
+

🤖 Rapport Modèle ML

+

Project5 - Prédiction énergétique des bâtiments

+

Généré le 21/09/2025 à 11:11:02

+
+ +
+ +
+

Informations Générales

+
+
+
Type de Modèle
+
RandomForestRegressor
+
+
+
Version
+
1.0
+
+
+
Date de Création
+
2025-09-21T11:07:42.926661
+
+
+
Module Python
+
sklearn.ensemble._forest
+
+
+
Taille du Modèle
+
26.0 MB
+
+
+
Nombre de Features
+
17
+
+
+
+ +
+

Métriques de Performance

+
+
+
0.916
+
Accuracy (R²)
+
+
+
0.090
+
RMSE
+
+
+
0.220
+
MAE
+
+
+
0.920
+
R² Score
+
+
+
+ +
+

Capacités du Modèle

+
+
+
Feature Importances
+
+ + ✓ Disponible + +
+
+
+
Coefficients
+
+ + ✗ Non disponible + +
+
+
+
Prédiction Probabiliste
+
+ + ✗ Non disponible + +
+
+
+
+ +
+

Variables d'Entrée (17 features)

+
+
year_built
+
number_of_buildings
+
number_of_floors
+
property_gfa_total
+
property_gfa_parking
+
second_largest_property_use_type_gfa
+
third_largest_property_use_type_gfa
+
multiusage
+
steam
+
electricity
+
natural_gas
+
neighborhood_id
+
building_type_id
+
largest_property_use_type_id
+
primary_property_type_id
+
second_largest_property_use_type_id
+
third_largest_property_use_type_id
+ +
+
+ +
+

Importance des Variables

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
VariableImportancePourcentageVisualisation
property_gfa_total0.415941.59% +
+
+
+
primary_property_type_id0.120512.05% +
+
+
+
largest_property_use_type_id0.09079.07% +
+
+
+
year_built0.07607.60% +
+
+
+
second_largest_property_use_type_gfa0.06656.65% +
+
+
+
number_of_floors0.06496.49% +
+
+
+
natural_gas0.04444.44% +
+
+
+
neighborhood_id0.03683.68% +
+
+
+
property_gfa_parking0.02092.09% +
+
+
+
second_largest_property_use_type_id0.02062.06% +
+
+
+
+
+
+ +
+ + +
+ + diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000000000000000000000000000000000000..3a7cd4283fb81851bfa2523d7bea47136d6f7ed6 --- /dev/null +++ b/poetry.lock @@ -0,0 +1,3537 @@ +# This file is automatically @generated by Poetry 2.1.3 and should not be changed by hand. + +[[package]] +name = "accessible-pygments" +version = "0.0.5" +description = "A collection of accessible pygments styles" +optional = false +python-versions = ">=3.9" +groups = ["docs"] +files = [ + {file = "accessible_pygments-0.0.5-py3-none-any.whl", hash = "sha256:88ae3211e68a1d0b011504b2ffc1691feafce124b845bd072ab6f9f66f34d4b7"}, + {file = "accessible_pygments-0.0.5.tar.gz", hash = "sha256:40918d3e6a2b619ad424cb91e556bd3bd8865443d9f22f1dcdf79e33c8046872"}, +] + +[package.dependencies] +pygments = ">=1.5" + +[package.extras] +dev = ["pillow", "pkginfo (>=1.10)", "playwright", "pre-commit", "setuptools", "twine (>=5.0)"] +tests = ["hypothesis", "pytest"] + +[[package]] +name = "aiosqlite" +version = "0.19.0" +description = "asyncio bridge to the standard sqlite3 module" +optional = false +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "aiosqlite-0.19.0-py3-none-any.whl", hash = "sha256:edba222e03453e094a3ce605db1b970c4b3376264e56f32e2a4959f948d66a96"}, + {file = "aiosqlite-0.19.0.tar.gz", hash = "sha256:95ee77b91c8d2808bd08a59fbebf66270e9090c3d92ffbf260dc0db0b979577d"}, +] + +[package.extras] +dev = ["aiounittest (==1.4.1) ; python_version < \"3.8\"", "attribution (==1.6.2)", "black (==23.3.0)", "coverage[toml] (==7.2.3)", "flake8 (==5.0.4)", "flake8-bugbear (==23.3.12)", "flit (==3.7.1)", "mypy (==1.2.0)", "ufmt (==2.1.0)", "usort (==1.0.6)"] +docs = ["sphinx (==6.1.3) ; python_version >= \"3.8\"", "sphinx-mdinclude (==0.5.3)"] + +[[package]] +name = "alabaster" +version = "0.7.16" +description = "A light, configurable Sphinx theme" +optional = false +python-versions = ">=3.9" +groups = ["main", "docs"] +files = [ + {file = "alabaster-0.7.16-py3-none-any.whl", hash = "sha256:b46733c07dce03ae4e150330b975c75737fa60f0a7c591b6c8bf4928a28e2c92"}, + {file = "alabaster-0.7.16.tar.gz", hash = "sha256:75a8b99c28a5dad50dd7f8ccdd447a121ddb3892da9e53d1ca5cca3106d58d65"}, +] + +[[package]] +name = "alembic" +version = "1.12.1" +description = "A database migration tool for SQLAlchemy." +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "alembic-1.12.1-py3-none-any.whl", hash = "sha256:47d52e3dfb03666ed945becb723d6482e52190917fdb47071440cfdba05d92cb"}, + {file = "alembic-1.12.1.tar.gz", hash = "sha256:bca5877e9678b454706347bc10b97cb7d67f300320fa5c3a94423e8266e2823f"}, +] + +[package.dependencies] +Mako = "*" +SQLAlchemy = ">=1.3.0" +typing-extensions = ">=4" + +[package.extras] +tz = ["python-dateutil"] + +[[package]] +name = "annotated-types" +version = "0.7.0" +description = "Reusable constraint types to use with typing.Annotated" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"}, + {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, +] + +[[package]] +name = "anyio" +version = "4.10.0" +description = "High-level concurrency and networking framework on top of asyncio or Trio" +optional = false +python-versions = ">=3.9" +groups = ["main", "dev"] +files = [ + {file = "anyio-4.10.0-py3-none-any.whl", hash = "sha256:60e474ac86736bbfd6f210f7a61218939c318f43f9972497381f1c5e930ed3d1"}, + {file = "anyio-4.10.0.tar.gz", hash = "sha256:3f3fae35c96039744587aa5b8371e7e8e603c0702999535961dd336026973ba6"}, +] + +[package.dependencies] +idna = ">=2.8" +sniffio = ">=1.1" +typing_extensions = {version = ">=4.5", markers = "python_version < \"3.13\""} + +[package.extras] +trio = ["trio (>=0.26.1)"] + +[[package]] +name = "astroid" +version = "3.3.11" +description = "An abstract syntax tree for Python with inference support." +optional = false +python-versions = ">=3.9.0" +groups = ["dev"] +files = [ + {file = "astroid-3.3.11-py3-none-any.whl", hash = "sha256:54c760ae8322ece1abd213057c4b5bba7c49818853fc901ef09719a60dbf9dec"}, + {file = "astroid-3.3.11.tar.gz", hash = "sha256:1e5a5011af2920c7c67a53f65d536d65bfa7116feeaf2354d8b94f29573bb0ce"}, +] + +[[package]] +name = "babel" +version = "2.17.0" +description = "Internationalization utilities" +optional = false +python-versions = ">=3.8" +groups = ["main", "docs"] +files = [ + {file = "babel-2.17.0-py3-none-any.whl", hash = "sha256:4d0b53093fdfb4b21c92b5213dba5a1b23885afa8383709427046b21c366e5f2"}, + {file = "babel-2.17.0.tar.gz", hash = "sha256:0c54cffb19f690cdcc52a3b50bcbf71e07a808d1c80d549f2459b9d2cf0afb9d"}, +] + +[package.extras] +dev = ["backports.zoneinfo ; python_version < \"3.9\"", "freezegun (>=1.0,<2.0)", "jinja2 (>=3.0)", "pytest (>=6.0)", "pytest-cov", "pytz", "setuptools", "tzdata ; sys_platform == \"win32\""] + +[[package]] +name = "bandit" +version = "1.8.6" +description = "Security oriented static analyser for python code." +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "bandit-1.8.6-py3-none-any.whl", hash = "sha256:3348e934d736fcdb68b6aa4030487097e23a501adf3e7827b63658df464dddd0"}, + {file = "bandit-1.8.6.tar.gz", hash = "sha256:dbfe9c25fc6961c2078593de55fd19f2559f9e45b99f1272341f5b95dea4e56b"}, +] + +[package.dependencies] +colorama = {version = ">=0.3.9", markers = "platform_system == \"Windows\""} +PyYAML = ">=5.3.1" +rich = "*" +stevedore = ">=1.20.0" + +[package.extras] +baseline = ["GitPython (>=3.1.30)"] +sarif = ["jschema-to-python (>=1.2.3)", "sarif-om (>=1.0.4)"] +test = ["beautifulsoup4 (>=4.8.0)", "coverage (>=4.5.4)", "fixtures (>=3.0.0)", "flake8 (>=4.0.0)", "pylint (==1.9.4)", "stestr (>=2.5.0)", "testscenarios (>=0.5.0)", "testtools (>=2.3.0)"] +toml = ["tomli (>=1.1.0) ; python_version < \"3.11\""] +yaml = ["PyYAML"] + +[[package]] +name = "bcrypt" +version = "4.3.0" +description = "Modern password hashing for your software and your servers" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "bcrypt-4.3.0-cp313-cp313t-macosx_10_12_universal2.whl", hash = "sha256:f01e060f14b6b57bbb72fc5b4a83ac21c443c9a2ee708e04a10e9192f90a6281"}, + {file = "bcrypt-4.3.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5eeac541cefd0bb887a371ef73c62c3cd78535e4887b310626036a7c0a817bb"}, + {file = "bcrypt-4.3.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:59e1aa0e2cd871b08ca146ed08445038f42ff75968c7ae50d2fdd7860ade2180"}, + {file = "bcrypt-4.3.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:0042b2e342e9ae3d2ed22727c1262f76cc4f345683b5c1715f0250cf4277294f"}, + {file = "bcrypt-4.3.0-cp313-cp313t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74a8d21a09f5e025a9a23e7c0fd2c7fe8e7503e4d356c0a2c1486ba010619f09"}, + {file = "bcrypt-4.3.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:0142b2cb84a009f8452c8c5a33ace5e3dfec4159e7735f5afe9a4d50a8ea722d"}, + {file = "bcrypt-4.3.0-cp313-cp313t-manylinux_2_34_aarch64.whl", hash = "sha256:12fa6ce40cde3f0b899729dbd7d5e8811cb892d31b6f7d0334a1f37748b789fd"}, + {file = "bcrypt-4.3.0-cp313-cp313t-manylinux_2_34_x86_64.whl", hash = "sha256:5bd3cca1f2aa5dbcf39e2aa13dd094ea181f48959e1071265de49cc2b82525af"}, + {file = "bcrypt-4.3.0-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:335a420cfd63fc5bc27308e929bee231c15c85cc4c496610ffb17923abf7f231"}, + {file = "bcrypt-4.3.0-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:0e30e5e67aed0187a1764911af023043b4542e70a7461ad20e837e94d23e1d6c"}, + {file = "bcrypt-4.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:3b8d62290ebefd49ee0b3ce7500f5dbdcf13b81402c05f6dafab9a1e1b27212f"}, + {file = "bcrypt-4.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2ef6630e0ec01376f59a006dc72918b1bf436c3b571b80fa1968d775fa02fe7d"}, + {file = "bcrypt-4.3.0-cp313-cp313t-win32.whl", hash = "sha256:7a4be4cbf241afee43f1c3969b9103a41b40bcb3a3f467ab19f891d9bc4642e4"}, + {file = "bcrypt-4.3.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5c1949bf259a388863ced887c7861da1df681cb2388645766c89fdfd9004c669"}, + {file = "bcrypt-4.3.0-cp38-abi3-macosx_10_12_universal2.whl", hash = "sha256:f81b0ed2639568bf14749112298f9e4e2b28853dab50a8b357e31798686a036d"}, + {file = "bcrypt-4.3.0-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:864f8f19adbe13b7de11ba15d85d4a428c7e2f344bac110f667676a0ff84924b"}, + {file = "bcrypt-4.3.0-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3e36506d001e93bffe59754397572f21bb5dc7c83f54454c990c74a468cd589e"}, + {file = "bcrypt-4.3.0-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:842d08d75d9fe9fb94b18b071090220697f9f184d4547179b60734846461ed59"}, + {file = "bcrypt-4.3.0-cp38-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7c03296b85cb87db865d91da79bf63d5609284fc0cab9472fdd8367bbd830753"}, + {file = "bcrypt-4.3.0-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:62f26585e8b219cdc909b6a0069efc5e4267e25d4a3770a364ac58024f62a761"}, + {file = "bcrypt-4.3.0-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:beeefe437218a65322fbd0069eb437e7c98137e08f22c4660ac2dc795c31f8bb"}, + {file = "bcrypt-4.3.0-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:97eea7408db3a5bcce4a55d13245ab3fa566e23b4c67cd227062bb49e26c585d"}, + {file = "bcrypt-4.3.0-cp38-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:191354ebfe305e84f344c5964c7cd5f924a3bfc5d405c75ad07f232b6dffb49f"}, + {file = "bcrypt-4.3.0-cp38-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:41261d64150858eeb5ff43c753c4b216991e0ae16614a308a15d909503617732"}, + {file = "bcrypt-4.3.0-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:33752b1ba962ee793fa2b6321404bf20011fe45b9afd2a842139de3011898fef"}, + {file = "bcrypt-4.3.0-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:50e6e80a4bfd23a25f5c05b90167c19030cf9f87930f7cb2eacb99f45d1c3304"}, + {file = "bcrypt-4.3.0-cp38-abi3-win32.whl", hash = "sha256:67a561c4d9fb9465ec866177e7aebcad08fe23aaf6fbd692a6fab69088abfc51"}, + {file = "bcrypt-4.3.0-cp38-abi3-win_amd64.whl", hash = "sha256:584027857bc2843772114717a7490a37f68da563b3620f78a849bcb54dc11e62"}, + {file = "bcrypt-4.3.0-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:0d3efb1157edebfd9128e4e46e2ac1a64e0c1fe46fb023158a407c7892b0f8c3"}, + {file = "bcrypt-4.3.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:08bacc884fd302b611226c01014eca277d48f0a05187666bca23aac0dad6fe24"}, + {file = "bcrypt-4.3.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6746e6fec103fcd509b96bacdfdaa2fbde9a553245dbada284435173a6f1aef"}, + {file = "bcrypt-4.3.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:afe327968aaf13fc143a56a3360cb27d4ad0345e34da12c7290f1b00b8fe9a8b"}, + {file = "bcrypt-4.3.0-cp39-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d9af79d322e735b1fc33404b5765108ae0ff232d4b54666d46730f8ac1a43676"}, + {file = "bcrypt-4.3.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f1e3ffa1365e8702dc48c8b360fef8d7afeca482809c5e45e653af82ccd088c1"}, + {file = "bcrypt-4.3.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:3004df1b323d10021fda07a813fd33e0fd57bef0e9a480bb143877f6cba996fe"}, + {file = "bcrypt-4.3.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:531457e5c839d8caea9b589a1bcfe3756b0547d7814e9ce3d437f17da75c32b0"}, + {file = "bcrypt-4.3.0-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:17a854d9a7a476a89dcef6c8bd119ad23e0f82557afbd2c442777a16408e614f"}, + {file = "bcrypt-4.3.0-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:6fb1fd3ab08c0cbc6826a2e0447610c6f09e983a281b919ed721ad32236b8b23"}, + {file = "bcrypt-4.3.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:e965a9c1e9a393b8005031ff52583cedc15b7884fce7deb8b0346388837d6cfe"}, + {file = "bcrypt-4.3.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:79e70b8342a33b52b55d93b3a59223a844962bef479f6a0ea318ebbcadf71505"}, + {file = "bcrypt-4.3.0-cp39-abi3-win32.whl", hash = "sha256:b4d4e57f0a63fd0b358eb765063ff661328f69a04494427265950c71b992a39a"}, + {file = "bcrypt-4.3.0-cp39-abi3-win_amd64.whl", hash = "sha256:e53e074b120f2877a35cc6c736b8eb161377caae8925c17688bd46ba56daaa5b"}, + {file = "bcrypt-4.3.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c950d682f0952bafcceaf709761da0a32a942272fad381081b51096ffa46cea1"}, + {file = "bcrypt-4.3.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:107d53b5c67e0bbc3f03ebf5b030e0403d24dda980f8e244795335ba7b4a027d"}, + {file = "bcrypt-4.3.0-pp310-pypy310_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:b693dbb82b3c27a1604a3dff5bfc5418a7e6a781bb795288141e5f80cf3a3492"}, + {file = "bcrypt-4.3.0-pp310-pypy310_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:b6354d3760fcd31994a14c89659dee887f1351a06e5dac3c1142307172a79f90"}, + {file = "bcrypt-4.3.0-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:a839320bf27d474e52ef8cb16449bb2ce0ba03ca9f44daba6d93fa1d8828e48a"}, + {file = "bcrypt-4.3.0-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:bdc6a24e754a555d7316fa4774e64c6c3997d27ed2d1964d55920c7c227bc4ce"}, + {file = "bcrypt-4.3.0-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:55a935b8e9a1d2def0626c4269db3fcd26728cbff1e84f0341465c31c4ee56d8"}, + {file = "bcrypt-4.3.0-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:57967b7a28d855313a963aaea51bf6df89f833db4320da458e5b3c5ab6d4c938"}, + {file = "bcrypt-4.3.0.tar.gz", hash = "sha256:3a3fd2204178b6d2adcf09cb4f6426ffef54762577a7c9b54c159008cb288c18"}, +] + +[package.extras] +tests = ["pytest (>=3.2.1,!=3.3.0)"] +typecheck = ["mypy"] + +[[package]] +name = "beautifulsoup4" +version = "4.13.5" +description = "Screen-scraping library" +optional = false +python-versions = ">=3.7.0" +groups = ["docs"] +files = [ + {file = "beautifulsoup4-4.13.5-py3-none-any.whl", hash = "sha256:642085eaa22233aceadff9c69651bc51e8bf3f874fb6d7104ece2beb24b47c4a"}, + {file = "beautifulsoup4-4.13.5.tar.gz", hash = "sha256:5e70131382930e7c3de33450a2f54a63d5e4b19386eab43a5b34d594268f3695"}, +] + +[package.dependencies] +soupsieve = ">1.2" +typing-extensions = ">=4.0.0" + +[package.extras] +cchardet = ["cchardet"] +chardet = ["chardet"] +charset-normalizer = ["charset-normalizer"] +html5lib = ["html5lib"] +lxml = ["lxml"] + +[[package]] +name = "black" +version = "24.10.0" +description = "The uncompromising code formatter." +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "black-24.10.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e6668650ea4b685440857138e5fe40cde4d652633b1bdffc62933d0db4ed9812"}, + {file = "black-24.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1c536fcf674217e87b8cc3657b81809d3c085d7bf3ef262ead700da345bfa6ea"}, + {file = "black-24.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:649fff99a20bd06c6f727d2a27f401331dc0cc861fb69cde910fe95b01b5928f"}, + {file = "black-24.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:fe4d6476887de70546212c99ac9bd803d90b42fc4767f058a0baa895013fbb3e"}, + {file = "black-24.10.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5a2221696a8224e335c28816a9d331a6c2ae15a2ee34ec857dcf3e45dbfa99ad"}, + {file = "black-24.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f9da3333530dbcecc1be13e69c250ed8dfa67f43c4005fb537bb426e19200d50"}, + {file = "black-24.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4007b1393d902b48b36958a216c20c4482f601569d19ed1df294a496eb366392"}, + {file = "black-24.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:394d4ddc64782e51153eadcaaca95144ac4c35e27ef9b0a42e121ae7e57a9175"}, + {file = "black-24.10.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b5e39e0fae001df40f95bd8cc36b9165c5e2ea88900167bddf258bacef9bbdc3"}, + {file = "black-24.10.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d37d422772111794b26757c5b55a3eade028aa3fde43121ab7b673d050949d65"}, + {file = "black-24.10.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:14b3502784f09ce2443830e3133dacf2c0110d45191ed470ecb04d0f5f6fcb0f"}, + {file = "black-24.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:30d2c30dc5139211dda799758559d1b049f7f14c580c409d6ad925b74a4208a8"}, + {file = "black-24.10.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1cbacacb19e922a1d75ef2b6ccaefcd6e93a2c05ede32f06a21386a04cedb981"}, + {file = "black-24.10.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1f93102e0c5bb3907451063e08b9876dbeac810e7da5a8bfb7aeb5a9ef89066b"}, + {file = "black-24.10.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ddacb691cdcdf77b96f549cf9591701d8db36b2f19519373d60d31746068dbf2"}, + {file = "black-24.10.0-cp313-cp313-win_amd64.whl", hash = "sha256:680359d932801c76d2e9c9068d05c6b107f2584b2a5b88831c83962eb9984c1b"}, + {file = "black-24.10.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:17374989640fbca88b6a448129cd1745c5eb8d9547b464f281b251dd00155ccd"}, + {file = "black-24.10.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:63f626344343083322233f175aaf372d326de8436f5928c042639a4afbbf1d3f"}, + {file = "black-24.10.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfa1d0cb6200857f1923b602f978386a3a2758a65b52e0950299ea014be6800"}, + {file = "black-24.10.0-cp39-cp39-win_amd64.whl", hash = "sha256:2cd9c95431d94adc56600710f8813ee27eea544dd118d45896bb734e9d7a0dc7"}, + {file = "black-24.10.0-py3-none-any.whl", hash = "sha256:3bb2b7a1f7b685f85b11fed1ef10f8a9148bceb49853e47a294a3dd963c1dd7d"}, + {file = "black-24.10.0.tar.gz", hash = "sha256:846ea64c97afe3bc677b761787993be4991810ecc7a4a937816dd6bddedc4875"}, +] + +[package.dependencies] +click = ">=8.0.0" +mypy-extensions = ">=0.4.3" +packaging = ">=22.0" +pathspec = ">=0.9.0" +platformdirs = ">=2" + +[package.extras] +colorama = ["colorama (>=0.4.3)"] +d = ["aiohttp (>=3.10)"] +jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] +uvloop = ["uvloop (>=0.15.2)"] + +[[package]] +name = "certifi" +version = "2025.8.3" +description = "Python package for providing Mozilla's CA Bundle." +optional = false +python-versions = ">=3.7" +groups = ["main", "dev", "docs"] +files = [ + {file = "certifi-2025.8.3-py3-none-any.whl", hash = "sha256:f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5"}, + {file = "certifi-2025.8.3.tar.gz", hash = "sha256:e564105f78ded564e3ae7c923924435e1daa7463faeab5bb932bc53ffae63407"}, +] + +[[package]] +name = "cffi" +version = "2.0.0" +description = "Foreign Function Interface for Python calling C code." +optional = false +python-versions = ">=3.9" +groups = ["main"] +markers = "platform_python_implementation != \"PyPy\"" +files = [ + {file = "cffi-2.0.0-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44"}, + {file = "cffi-2.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49"}, + {file = "cffi-2.0.0-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c"}, + {file = "cffi-2.0.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb"}, + {file = "cffi-2.0.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0"}, + {file = "cffi-2.0.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4"}, + {file = "cffi-2.0.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453"}, + {file = "cffi-2.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495"}, + {file = "cffi-2.0.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5"}, + {file = "cffi-2.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb"}, + {file = "cffi-2.0.0-cp310-cp310-win32.whl", hash = "sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a"}, + {file = "cffi-2.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739"}, + {file = "cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe"}, + {file = "cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c"}, + {file = "cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92"}, + {file = "cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93"}, + {file = "cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5"}, + {file = "cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664"}, + {file = "cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26"}, + {file = "cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9"}, + {file = "cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414"}, + {file = "cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743"}, + {file = "cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5"}, + {file = "cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5"}, + {file = "cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d"}, + {file = "cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d"}, + {file = "cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c"}, + {file = "cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe"}, + {file = "cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062"}, + {file = "cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e"}, + {file = "cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037"}, + {file = "cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba"}, + {file = "cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94"}, + {file = "cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187"}, + {file = "cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18"}, + {file = "cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5"}, + {file = "cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6"}, + {file = "cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb"}, + {file = "cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca"}, + {file = "cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b"}, + {file = "cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b"}, + {file = "cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2"}, + {file = "cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3"}, + {file = "cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26"}, + {file = "cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c"}, + {file = "cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b"}, + {file = "cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27"}, + {file = "cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75"}, + {file = "cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91"}, + {file = "cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5"}, + {file = "cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13"}, + {file = "cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b"}, + {file = "cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c"}, + {file = "cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef"}, + {file = "cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775"}, + {file = "cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205"}, + {file = "cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1"}, + {file = "cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f"}, + {file = "cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25"}, + {file = "cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad"}, + {file = "cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9"}, + {file = "cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d"}, + {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c"}, + {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8"}, + {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc"}, + {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592"}, + {file = "cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512"}, + {file = "cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4"}, + {file = "cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e"}, + {file = "cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6"}, + {file = "cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9"}, + {file = "cffi-2.0.0-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:fe562eb1a64e67dd297ccc4f5addea2501664954f2692b69a76449ec7913ecbf"}, + {file = "cffi-2.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:de8dad4425a6ca6e4e5e297b27b5c824ecc7581910bf9aee86cb6835e6812aa7"}, + {file = "cffi-2.0.0-cp39-cp39-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:4647afc2f90d1ddd33441e5b0e85b16b12ddec4fca55f0d9671fef036ecca27c"}, + {file = "cffi-2.0.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3f4d46d8b35698056ec29bca21546e1551a205058ae1a181d871e278b0b28165"}, + {file = "cffi-2.0.0-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:e6e73b9e02893c764e7e8d5bb5ce277f1a009cd5243f8228f75f842bf937c534"}, + {file = "cffi-2.0.0-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:cb527a79772e5ef98fb1d700678fe031e353e765d1ca2d409c92263c6d43e09f"}, + {file = "cffi-2.0.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:61d028e90346df14fedc3d1e5441df818d095f3b87d286825dfcbd6459b7ef63"}, + {file = "cffi-2.0.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:0f6084a0ea23d05d20c3edcda20c3d006f9b6f3fefeac38f59262e10cef47ee2"}, + {file = "cffi-2.0.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:1cd13c99ce269b3ed80b417dcd591415d3372bcac067009b6e0f59c7d4015e65"}, + {file = "cffi-2.0.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:89472c9762729b5ae1ad974b777416bfda4ac5642423fa93bd57a09204712322"}, + {file = "cffi-2.0.0-cp39-cp39-win32.whl", hash = "sha256:2081580ebb843f759b9f617314a24ed5738c51d2aee65d31e02f6f7a2b97707a"}, + {file = "cffi-2.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:b882b3df248017dba09d6b16defe9b5c407fe32fc7c65a9c69798e6175601be9"}, + {file = "cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529"}, +] + +[package.dependencies] +pycparser = {version = "*", markers = "implementation_name != \"PyPy\""} + +[[package]] +name = "charset-normalizer" +version = "3.4.3" +description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +optional = false +python-versions = ">=3.7" +groups = ["main", "dev", "docs"] +files = [ + {file = "charset_normalizer-3.4.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:fb7f67a1bfa6e40b438170ebdc8158b78dc465a5a67b6dde178a46987b244a72"}, + {file = "charset_normalizer-3.4.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cc9370a2da1ac13f0153780040f465839e6cccb4a1e44810124b4e22483c93fe"}, + {file = "charset_normalizer-3.4.3-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:07a0eae9e2787b586e129fdcbe1af6997f8d0e5abaa0bc98c0e20e124d67e601"}, + {file = "charset_normalizer-3.4.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:74d77e25adda8581ffc1c720f1c81ca082921329452eba58b16233ab1842141c"}, + {file = "charset_normalizer-3.4.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d0e909868420b7049dafd3a31d45125b31143eec59235311fc4c57ea26a4acd2"}, + {file = "charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c6f162aabe9a91a309510d74eeb6507fab5fff92337a15acbe77753d88d9dcf0"}, + {file = "charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:4ca4c094de7771a98d7fbd67d9e5dbf1eb73efa4f744a730437d8a3a5cf994f0"}, + {file = "charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:02425242e96bcf29a49711b0ca9f37e451da7c70562bc10e8ed992a5a7a25cc0"}, + {file = "charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:78deba4d8f9590fe4dae384aeff04082510a709957e968753ff3c48399f6f92a"}, + {file = "charset_normalizer-3.4.3-cp310-cp310-win32.whl", hash = "sha256:d79c198e27580c8e958906f803e63cddb77653731be08851c7df0b1a14a8fc0f"}, + {file = "charset_normalizer-3.4.3-cp310-cp310-win_amd64.whl", hash = "sha256:c6e490913a46fa054e03699c70019ab869e990270597018cef1d8562132c2669"}, + {file = "charset_normalizer-3.4.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:b256ee2e749283ef3ddcff51a675ff43798d92d746d1a6e4631bf8c707d22d0b"}, + {file = "charset_normalizer-3.4.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:13faeacfe61784e2559e690fc53fa4c5ae97c6fcedb8eb6fb8d0a15b475d2c64"}, + {file = "charset_normalizer-3.4.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:00237675befef519d9af72169d8604a067d92755e84fe76492fef5441db05b91"}, + {file = "charset_normalizer-3.4.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:585f3b2a80fbd26b048a0be90c5aae8f06605d3c92615911c3a2b03a8a3b796f"}, + {file = "charset_normalizer-3.4.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e78314bdc32fa80696f72fa16dc61168fda4d6a0c014e0380f9d02f0e5d8a07"}, + {file = "charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:96b2b3d1a83ad55310de8c7b4a2d04d9277d5591f40761274856635acc5fcb30"}, + {file = "charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:939578d9d8fd4299220161fdd76e86c6a251987476f5243e8864a7844476ba14"}, + {file = "charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:fd10de089bcdcd1be95a2f73dbe6254798ec1bda9f450d5828c96f93e2536b9c"}, + {file = "charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1e8ac75d72fa3775e0b7cb7e4629cec13b7514d928d15ef8ea06bca03ef01cae"}, + {file = "charset_normalizer-3.4.3-cp311-cp311-win32.whl", hash = "sha256:6cf8fd4c04756b6b60146d98cd8a77d0cdae0e1ca20329da2ac85eed779b6849"}, + {file = "charset_normalizer-3.4.3-cp311-cp311-win_amd64.whl", hash = "sha256:31a9a6f775f9bcd865d88ee350f0ffb0e25936a7f930ca98995c05abf1faf21c"}, + {file = "charset_normalizer-3.4.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e28e334d3ff134e88989d90ba04b47d84382a828c061d0d1027b1b12a62b39b1"}, + {file = "charset_normalizer-3.4.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0cacf8f7297b0c4fcb74227692ca46b4a5852f8f4f24b3c766dd94a1075c4884"}, + {file = "charset_normalizer-3.4.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c6fd51128a41297f5409deab284fecbe5305ebd7e5a1f959bee1c054622b7018"}, + {file = "charset_normalizer-3.4.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3cfb2aad70f2c6debfbcb717f23b7eb55febc0bb23dcffc0f076009da10c6392"}, + {file = "charset_normalizer-3.4.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1606f4a55c0fd363d754049cdf400175ee96c992b1f8018b993941f221221c5f"}, + {file = "charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:027b776c26d38b7f15b26a5da1044f376455fb3766df8fc38563b4efbc515154"}, + {file = "charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:42e5088973e56e31e4fa58eb6bd709e42fc03799c11c42929592889a2e54c491"}, + {file = "charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:cc34f233c9e71701040d772aa7490318673aa7164a0efe3172b2981218c26d93"}, + {file = "charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:320e8e66157cc4e247d9ddca8e21f427efc7a04bbd0ac8a9faf56583fa543f9f"}, + {file = "charset_normalizer-3.4.3-cp312-cp312-win32.whl", hash = "sha256:fb6fecfd65564f208cbf0fba07f107fb661bcd1a7c389edbced3f7a493f70e37"}, + {file = "charset_normalizer-3.4.3-cp312-cp312-win_amd64.whl", hash = "sha256:86df271bf921c2ee3818f0522e9a5b8092ca2ad8b065ece5d7d9d0e9f4849bcc"}, + {file = "charset_normalizer-3.4.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:14c2a87c65b351109f6abfc424cab3927b3bdece6f706e4d12faaf3d52ee5efe"}, + {file = "charset_normalizer-3.4.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:41d1fc408ff5fdfb910200ec0e74abc40387bccb3252f3f27c0676731df2b2c8"}, + {file = "charset_normalizer-3.4.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1bb60174149316da1c35fa5233681f7c0f9f514509b8e399ab70fea5f17e45c9"}, + {file = "charset_normalizer-3.4.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:30d006f98569de3459c2fc1f2acde170b7b2bd265dc1943e87e1a4efe1b67c31"}, + {file = "charset_normalizer-3.4.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:416175faf02e4b0810f1f38bcb54682878a4af94059a1cd63b8747244420801f"}, + {file = "charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6aab0f181c486f973bc7262a97f5aca3ee7e1437011ef0c2ec04b5a11d16c927"}, + {file = "charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:fdabf8315679312cfa71302f9bd509ded4f2f263fb5b765cf1433b39106c3cc9"}, + {file = "charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:bd28b817ea8c70215401f657edef3a8aa83c29d447fb0b622c35403780ba11d5"}, + {file = "charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:18343b2d246dc6761a249ba1fb13f9ee9a2bcd95decc767319506056ea4ad4dc"}, + {file = "charset_normalizer-3.4.3-cp313-cp313-win32.whl", hash = "sha256:6fb70de56f1859a3f71261cbe41005f56a7842cc348d3aeb26237560bfa5e0ce"}, + {file = "charset_normalizer-3.4.3-cp313-cp313-win_amd64.whl", hash = "sha256:cf1ebb7d78e1ad8ec2a8c4732c7be2e736f6e5123a4146c5b89c9d1f585f8cef"}, + {file = "charset_normalizer-3.4.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3cd35b7e8aedeb9e34c41385fda4f73ba609e561faedfae0a9e75e44ac558a15"}, + {file = "charset_normalizer-3.4.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b89bc04de1d83006373429975f8ef9e7932534b8cc9ca582e4db7d20d91816db"}, + {file = "charset_normalizer-3.4.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2001a39612b241dae17b4687898843f254f8748b796a2e16f1051a17078d991d"}, + {file = "charset_normalizer-3.4.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8dcfc373f888e4fb39a7bc57e93e3b845e7f462dacc008d9749568b1c4ece096"}, + {file = "charset_normalizer-3.4.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18b97b8404387b96cdbd30ad660f6407799126d26a39ca65729162fd810a99aa"}, + {file = "charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ccf600859c183d70eb47e05a44cd80a4ce77394d1ac0f79dbd2dd90a69a3a049"}, + {file = "charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:53cd68b185d98dde4ad8990e56a58dea83a4162161b1ea9272e5c9182ce415e0"}, + {file = "charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:30a96e1e1f865f78b030d65241c1ee850cdf422d869e9028e2fc1d5e4db73b92"}, + {file = "charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d716a916938e03231e86e43782ca7878fb602a125a91e7acb8b5112e2e96ac16"}, + {file = "charset_normalizer-3.4.3-cp314-cp314-win32.whl", hash = "sha256:c6dbd0ccdda3a2ba7c2ecd9d77b37f3b5831687d8dc1b6ca5f56a4880cc7b7ce"}, + {file = "charset_normalizer-3.4.3-cp314-cp314-win_amd64.whl", hash = "sha256:73dc19b562516fc9bcf6e5d6e596df0b4eb98d87e4f79f3ae71840e6ed21361c"}, + {file = "charset_normalizer-3.4.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:0f2be7e0cf7754b9a30eb01f4295cc3d4358a479843b31f328afd210e2c7598c"}, + {file = "charset_normalizer-3.4.3-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c60e092517a73c632ec38e290eba714e9627abe9d301c8c8a12ec32c314a2a4b"}, + {file = "charset_normalizer-3.4.3-cp38-cp38-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:252098c8c7a873e17dd696ed98bbe91dbacd571da4b87df3736768efa7a792e4"}, + {file = "charset_normalizer-3.4.3-cp38-cp38-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3653fad4fe3ed447a596ae8638b437f827234f01a8cd801842e43f3d0a6b281b"}, + {file = "charset_normalizer-3.4.3-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8999f965f922ae054125286faf9f11bc6932184b93011d138925a1773830bbe9"}, + {file = "charset_normalizer-3.4.3-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:d95bfb53c211b57198bb91c46dd5a2d8018b3af446583aab40074bf7988401cb"}, + {file = "charset_normalizer-3.4.3-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:5b413b0b1bfd94dbf4023ad6945889f374cd24e3f62de58d6bb102c4d9ae534a"}, + {file = "charset_normalizer-3.4.3-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:b5e3b2d152e74e100a9e9573837aba24aab611d39428ded46f4e4022ea7d1942"}, + {file = "charset_normalizer-3.4.3-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:a2d08ac246bb48479170408d6c19f6385fa743e7157d716e144cad849b2dd94b"}, + {file = "charset_normalizer-3.4.3-cp38-cp38-win32.whl", hash = "sha256:ec557499516fc90fd374bf2e32349a2887a876fbf162c160e3c01b6849eaf557"}, + {file = "charset_normalizer-3.4.3-cp38-cp38-win_amd64.whl", hash = "sha256:5d8d01eac18c423815ed4f4a2ec3b439d654e55ee4ad610e153cf02faf67ea40"}, + {file = "charset_normalizer-3.4.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:70bfc5f2c318afece2f5838ea5e4c3febada0be750fcf4775641052bbba14d05"}, + {file = "charset_normalizer-3.4.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:23b6b24d74478dc833444cbd927c338349d6ae852ba53a0d02a2de1fce45b96e"}, + {file = "charset_normalizer-3.4.3-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:34a7f768e3f985abdb42841e20e17b330ad3aaf4bb7e7aeeb73db2e70f077b99"}, + {file = "charset_normalizer-3.4.3-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fb731e5deb0c7ef82d698b0f4c5bb724633ee2a489401594c5c88b02e6cb15f7"}, + {file = "charset_normalizer-3.4.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:257f26fed7d7ff59921b78244f3cd93ed2af1800ff048c33f624c87475819dd7"}, + {file = "charset_normalizer-3.4.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1ef99f0456d3d46a50945c98de1774da86f8e992ab5c77865ea8b8195341fc19"}, + {file = "charset_normalizer-3.4.3-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:2c322db9c8c89009a990ef07c3bcc9f011a3269bc06782f916cd3d9eed7c9312"}, + {file = "charset_normalizer-3.4.3-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:511729f456829ef86ac41ca78c63a5cb55240ed23b4b737faca0eb1abb1c41bc"}, + {file = "charset_normalizer-3.4.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:88ab34806dea0671532d3f82d82b85e8fc23d7b2dd12fa837978dad9bb392a34"}, + {file = "charset_normalizer-3.4.3-cp39-cp39-win32.whl", hash = "sha256:16a8770207946ac75703458e2c743631c79c59c5890c80011d536248f8eaa432"}, + {file = "charset_normalizer-3.4.3-cp39-cp39-win_amd64.whl", hash = "sha256:d22dbedd33326a4a5190dd4fe9e9e693ef12160c77382d9e87919bce54f3d4ca"}, + {file = "charset_normalizer-3.4.3-py3-none-any.whl", hash = "sha256:ce571ab16d890d23b5c278547ba694193a45011ff86a9162a71307ed9f86759a"}, + {file = "charset_normalizer-3.4.3.tar.gz", hash = "sha256:6fce4b8500244f6fcb71465d4a4930d132ba9ab8e71a7859e6a5d59851068d14"}, +] + +[[package]] +name = "click" +version = "8.2.1" +description = "Composable command line interface toolkit" +optional = false +python-versions = ">=3.10" +groups = ["main", "dev"] +files = [ + {file = "click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b"}, + {file = "click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +groups = ["main", "dev", "docs"] +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] +markers = {main = "platform_system == \"Windows\" or sys_platform == \"win32\"", dev = "platform_system == \"Windows\" or sys_platform == \"win32\""} + +[[package]] +name = "coverage" +version = "7.10.4" +description = "Code coverage measurement for Python" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "coverage-7.10.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d92d6edb0ccafd20c6fbf9891ca720b39c2a6a4b4a6f9cf323ca2c986f33e475"}, + {file = "coverage-7.10.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7202da14dc0236884fcc45665ffb2d79d4991a53fbdf152ab22f69f70923cc22"}, + {file = "coverage-7.10.4-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:ada418633ae24ec8d0fcad5efe6fc7aa3c62497c6ed86589e57844ad04365674"}, + {file = "coverage-7.10.4-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b828e33eca6c3322adda3b5884456f98c435182a44917ded05005adfa1415500"}, + {file = "coverage-7.10.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:802793ba397afcfdbe9f91f89d65ae88b958d95edc8caf948e1f47d8b6b2b606"}, + {file = "coverage-7.10.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d0b23512338c54101d3bf7a1ab107d9d75abda1d5f69bc0887fd079253e4c27e"}, + {file = "coverage-7.10.4-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:f36b7dcf72d06a8c5e2dd3aca02be2b1b5db5f86404627dff834396efce958f2"}, + {file = "coverage-7.10.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:fce316c367a1dc2c411821365592eeb335ff1781956d87a0410eae248188ba51"}, + {file = "coverage-7.10.4-cp310-cp310-win32.whl", hash = "sha256:8c5dab29fc8070b3766b5fc85f8d89b19634584429a2da6d42da5edfadaf32ae"}, + {file = "coverage-7.10.4-cp310-cp310-win_amd64.whl", hash = "sha256:4b0d114616f0fccb529a1817457d5fb52a10e106f86c5fb3b0bd0d45d0d69b93"}, + {file = "coverage-7.10.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:05d5f98ec893d4a2abc8bc5f046f2f4367404e7e5d5d18b83de8fde1093ebc4f"}, + {file = "coverage-7.10.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9267efd28f8994b750d171e58e481e3bbd69e44baed540e4c789f8e368b24b88"}, + {file = "coverage-7.10.4-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:4456a039fdc1a89ea60823d0330f1ac6f97b0dbe9e2b6fb4873e889584b085fb"}, + {file = "coverage-7.10.4-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c2bfbd2a9f7e68a21c5bd191be94bfdb2691ac40d325bac9ef3ae45ff5c753d9"}, + {file = "coverage-7.10.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ab7765f10ae1df7e7fe37de9e64b5a269b812ee22e2da3f84f97b1c7732a0d8"}, + {file = "coverage-7.10.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0a09b13695166236e171ec1627ff8434b9a9bae47528d0ba9d944c912d33b3d2"}, + {file = "coverage-7.10.4-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5c9e75dfdc0167d5675e9804f04a56b2cf47fb83a524654297000b578b8adcb7"}, + {file = "coverage-7.10.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c751261bfe6481caba15ec005a194cb60aad06f29235a74c24f18546d8377df0"}, + {file = "coverage-7.10.4-cp311-cp311-win32.whl", hash = "sha256:051c7c9e765f003c2ff6e8c81ccea28a70fb5b0142671e4e3ede7cebd45c80af"}, + {file = "coverage-7.10.4-cp311-cp311-win_amd64.whl", hash = "sha256:1a647b152f10be08fb771ae4a1421dbff66141e3d8ab27d543b5eb9ea5af8e52"}, + {file = "coverage-7.10.4-cp311-cp311-win_arm64.whl", hash = "sha256:b09b9e4e1de0d406ca9f19a371c2beefe3193b542f64a6dd40cfcf435b7d6aa0"}, + {file = "coverage-7.10.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a1f0264abcabd4853d4cb9b3d164adbf1565da7dab1da1669e93f3ea60162d79"}, + {file = "coverage-7.10.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:536cbe6b118a4df231b11af3e0f974a72a095182ff8ec5f4868c931e8043ef3e"}, + {file = "coverage-7.10.4-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9a4c0d84134797b7bf3f080599d0cd501471f6c98b715405166860d79cfaa97e"}, + {file = "coverage-7.10.4-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7c155fc0f9cee8c9803ea0ad153ab6a3b956baa5d4cd993405dc0b45b2a0b9e0"}, + {file = "coverage-7.10.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a5f2ab6e451d4b07855d8bcf063adf11e199bff421a4ba57f5bb95b7444ca62"}, + {file = "coverage-7.10.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:685b67d99b945b0c221be0780c336b303a7753b3e0ec0d618c795aada25d5e7a"}, + {file = "coverage-7.10.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0c079027e50c2ae44da51c2e294596cbc9dbb58f7ca45b30651c7e411060fc23"}, + {file = "coverage-7.10.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3749aa72b93ce516f77cf5034d8e3c0dfd45c6e8a163a602ede2dc5f9a0bb927"}, + {file = "coverage-7.10.4-cp312-cp312-win32.whl", hash = "sha256:fecb97b3a52fa9bcd5a7375e72fae209088faf671d39fae67261f37772d5559a"}, + {file = "coverage-7.10.4-cp312-cp312-win_amd64.whl", hash = "sha256:26de58f355626628a21fe6a70e1e1fad95702dafebfb0685280962ae1449f17b"}, + {file = "coverage-7.10.4-cp312-cp312-win_arm64.whl", hash = "sha256:67e8885408f8325198862bc487038a4980c9277d753cb8812510927f2176437a"}, + {file = "coverage-7.10.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2b8e1d2015d5dfdbf964ecef12944c0c8c55b885bb5c0467ae8ef55e0e151233"}, + {file = "coverage-7.10.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:25735c299439018d66eb2dccf54f625aceb78645687a05f9f848f6e6c751e169"}, + {file = "coverage-7.10.4-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:715c06cb5eceac4d9b7cdf783ce04aa495f6aff657543fea75c30215b28ddb74"}, + {file = "coverage-7.10.4-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:e017ac69fac9aacd7df6dc464c05833e834dc5b00c914d7af9a5249fcccf07ef"}, + {file = "coverage-7.10.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bad180cc40b3fccb0f0e8c702d781492654ac2580d468e3ffc8065e38c6c2408"}, + {file = "coverage-7.10.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:becbdcd14f685fada010a5f792bf0895675ecf7481304fe159f0cd3f289550bd"}, + {file = "coverage-7.10.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:0b485ca21e16a76f68060911f97ebbe3e0d891da1dbbce6af7ca1ab3f98b9097"}, + {file = "coverage-7.10.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6c1d098ccfe8e1e0a1ed9a0249138899948afd2978cbf48eb1cc3fcd38469690"}, + {file = "coverage-7.10.4-cp313-cp313-win32.whl", hash = "sha256:8630f8af2ca84b5c367c3df907b1706621abe06d6929f5045fd628968d421e6e"}, + {file = "coverage-7.10.4-cp313-cp313-win_amd64.whl", hash = "sha256:f68835d31c421736be367d32f179e14ca932978293fe1b4c7a6a49b555dff5b2"}, + {file = "coverage-7.10.4-cp313-cp313-win_arm64.whl", hash = "sha256:6eaa61ff6724ca7ebc5326d1fae062d85e19b38dd922d50903702e6078370ae7"}, + {file = "coverage-7.10.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:702978108876bfb3d997604930b05fe769462cc3000150b0e607b7b444f2fd84"}, + {file = "coverage-7.10.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e8f978e8c5521d9c8f2086ac60d931d583fab0a16f382f6eb89453fe998e2484"}, + {file = "coverage-7.10.4-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:df0ac2ccfd19351411c45e43ab60932b74472e4648b0a9edf6a3b58846e246a9"}, + {file = "coverage-7.10.4-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:73a0d1aaaa3796179f336448e1576a3de6fc95ff4f07c2d7251d4caf5d18cf8d"}, + {file = "coverage-7.10.4-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:873da6d0ed6b3ffc0bc01f2c7e3ad7e2023751c0d8d86c26fe7322c314b031dc"}, + {file = "coverage-7.10.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c6446c75b0e7dda5daa876a1c87b480b2b52affb972fedd6c22edf1aaf2e00ec"}, + {file = "coverage-7.10.4-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:6e73933e296634e520390c44758d553d3b573b321608118363e52113790633b9"}, + {file = "coverage-7.10.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:52073d4b08d2cb571234c8a71eb32af3c6923149cf644a51d5957ac128cf6aa4"}, + {file = "coverage-7.10.4-cp313-cp313t-win32.whl", hash = "sha256:e24afb178f21f9ceb1aefbc73eb524769aa9b504a42b26857243f881af56880c"}, + {file = "coverage-7.10.4-cp313-cp313t-win_amd64.whl", hash = "sha256:be04507ff1ad206f4be3d156a674e3fb84bbb751ea1b23b142979ac9eebaa15f"}, + {file = "coverage-7.10.4-cp313-cp313t-win_arm64.whl", hash = "sha256:f3e3ff3f69d02b5dad67a6eac68cc9c71ae343b6328aae96e914f9f2f23a22e2"}, + {file = "coverage-7.10.4-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:a59fe0af7dd7211ba595cf7e2867458381f7e5d7b4cffe46274e0b2f5b9f4eb4"}, + {file = "coverage-7.10.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:3a6c35c5b70f569ee38dc3350cd14fdd0347a8b389a18bb37538cc43e6f730e6"}, + {file = "coverage-7.10.4-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:acb7baf49f513554c4af6ef8e2bd6e8ac74e6ea0c7386df8b3eb586d82ccccc4"}, + {file = "coverage-7.10.4-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a89afecec1ed12ac13ed203238b560cbfad3522bae37d91c102e690b8b1dc46c"}, + {file = "coverage-7.10.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:480442727f464407d8ade6e677b7f21f3b96a9838ab541b9a28ce9e44123c14e"}, + {file = "coverage-7.10.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a89bf193707f4a17f1ed461504031074d87f035153239f16ce86dfb8f8c7ac76"}, + {file = "coverage-7.10.4-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:3ddd912c2fc440f0fb3229e764feec85669d5d80a988ff1b336a27d73f63c818"}, + {file = "coverage-7.10.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:8a538944ee3a42265e61c7298aeba9ea43f31c01271cf028f437a7b4075592cf"}, + {file = "coverage-7.10.4-cp314-cp314-win32.whl", hash = "sha256:fd2e6002be1c62476eb862b8514b1ba7e7684c50165f2a8d389e77da6c9a2ebd"}, + {file = "coverage-7.10.4-cp314-cp314-win_amd64.whl", hash = "sha256:ec113277f2b5cf188d95fb66a65c7431f2b9192ee7e6ec9b72b30bbfb53c244a"}, + {file = "coverage-7.10.4-cp314-cp314-win_arm64.whl", hash = "sha256:9744954bfd387796c6a091b50d55ca7cac3d08767795b5eec69ad0f7dbf12d38"}, + {file = "coverage-7.10.4-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:5af4829904dda6aabb54a23879f0f4412094ba9ef153aaa464e3c1b1c9bc98e6"}, + {file = "coverage-7.10.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7bba5ed85e034831fac761ae506c0644d24fd5594727e174b5a73aff343a7508"}, + {file = "coverage-7.10.4-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d57d555b0719834b55ad35045de6cc80fc2b28e05adb6b03c98479f9553b387f"}, + {file = "coverage-7.10.4-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ba62c51a72048bb1ea72db265e6bd8beaabf9809cd2125bbb5306c6ce105f214"}, + {file = "coverage-7.10.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0acf0c62a6095f07e9db4ec365cc58c0ef5babb757e54745a1aa2ea2a2564af1"}, + {file = "coverage-7.10.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e1033bf0f763f5cf49ffe6594314b11027dcc1073ac590b415ea93463466deec"}, + {file = "coverage-7.10.4-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:92c29eff894832b6a40da1789b1f252305af921750b03ee4535919db9179453d"}, + {file = "coverage-7.10.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:822c4c830989c2093527e92acd97be4638a44eb042b1bdc0e7a278d84a070bd3"}, + {file = "coverage-7.10.4-cp314-cp314t-win32.whl", hash = "sha256:e694d855dac2e7cf194ba33653e4ba7aad7267a802a7b3fc4347d0517d5d65cd"}, + {file = "coverage-7.10.4-cp314-cp314t-win_amd64.whl", hash = "sha256:efcc54b38ef7d5bfa98050f220b415bc5bb3d432bd6350a861cf6da0ede2cdcd"}, + {file = "coverage-7.10.4-cp314-cp314t-win_arm64.whl", hash = "sha256:6f3a3496c0fa26bfac4ebc458747b778cff201c8ae94fa05e1391bab0dbc473c"}, + {file = "coverage-7.10.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:48fd4d52600c2a9d5622e52dfae674a7845c5e1dceaf68b88c99feb511fbcfd6"}, + {file = "coverage-7.10.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:56217b470d09d69e6b7dcae38200f95e389a77db801cb129101697a4553b18b6"}, + {file = "coverage-7.10.4-cp39-cp39-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:44ac3f21a6e28c5ff7f7a47bca5f87885f6a1e623e637899125ba47acd87334d"}, + {file = "coverage-7.10.4-cp39-cp39-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:3387739d72c84d17b4d2f7348749cac2e6700e7152026912b60998ee9a40066b"}, + {file = "coverage-7.10.4-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3f111ff20d9a6348e0125be892608e33408dd268f73b020940dfa8511ad05503"}, + {file = "coverage-7.10.4-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:01a852f0a9859734b018a3f483cc962d0b381d48d350b1a0c47d618c73a0c398"}, + {file = "coverage-7.10.4-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:225111dd06759ba4e37cee4c0b4f3df2b15c879e9e3c37bf986389300b9917c3"}, + {file = "coverage-7.10.4-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:2178d4183bd1ba608f0bb12e71e55838ba1b7dbb730264f8b08de9f8ef0c27d0"}, + {file = "coverage-7.10.4-cp39-cp39-win32.whl", hash = "sha256:93d175fe81913aee7a6ea430abbdf2a79f1d9fd451610e12e334e4fe3264f563"}, + {file = "coverage-7.10.4-cp39-cp39-win_amd64.whl", hash = "sha256:2221a823404bb941c7721cf0ef55ac6ee5c25d905beb60c0bba5e5e85415d353"}, + {file = "coverage-7.10.4-py3-none-any.whl", hash = "sha256:065d75447228d05121e5c938ca8f0e91eed60a1eb2d1258d42d5084fecfc3302"}, + {file = "coverage-7.10.4.tar.gz", hash = "sha256:25f5130af6c8e7297fd14634955ba9e1697f47143f289e2a23284177c0061d27"}, +] + +[package.extras] +toml = ["tomli ; python_full_version <= \"3.11.0a6\""] + +[[package]] +name = "cryptography" +version = "45.0.7" +description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." +optional = false +python-versions = "!=3.9.0,!=3.9.1,>=3.7" +groups = ["main"] +files = [ + {file = "cryptography-45.0.7-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:3be4f21c6245930688bd9e162829480de027f8bf962ede33d4f8ba7d67a00cee"}, + {file = "cryptography-45.0.7-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:67285f8a611b0ebc0857ced2081e30302909f571a46bfa7a3cc0ad303fe015c6"}, + {file = "cryptography-45.0.7-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:577470e39e60a6cd7780793202e63536026d9b8641de011ed9d8174da9ca5339"}, + {file = "cryptography-45.0.7-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:4bd3e5c4b9682bc112d634f2c6ccc6736ed3635fc3319ac2bb11d768cc5a00d8"}, + {file = "cryptography-45.0.7-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:465ccac9d70115cd4de7186e60cfe989de73f7bb23e8a7aa45af18f7412e75bf"}, + {file = "cryptography-45.0.7-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:16ede8a4f7929b4b7ff3642eba2bf79aa1d71f24ab6ee443935c0d269b6bc513"}, + {file = "cryptography-45.0.7-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:8978132287a9d3ad6b54fcd1e08548033cc09dc6aacacb6c004c73c3eb5d3ac3"}, + {file = "cryptography-45.0.7-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:b6a0e535baec27b528cb07a119f321ac024592388c5681a5ced167ae98e9fff3"}, + {file = "cryptography-45.0.7-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:a24ee598d10befaec178efdff6054bc4d7e883f615bfbcd08126a0f4931c83a6"}, + {file = "cryptography-45.0.7-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:fa26fa54c0a9384c27fcdc905a2fb7d60ac6e47d14bc2692145f2b3b1e2cfdbd"}, + {file = "cryptography-45.0.7-cp311-abi3-win32.whl", hash = "sha256:bef32a5e327bd8e5af915d3416ffefdbe65ed975b646b3805be81b23580b57b8"}, + {file = "cryptography-45.0.7-cp311-abi3-win_amd64.whl", hash = "sha256:3808e6b2e5f0b46d981c24d79648e5c25c35e59902ea4391a0dcb3e667bf7443"}, + {file = "cryptography-45.0.7-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:bfb4c801f65dd61cedfc61a83732327fafbac55a47282e6f26f073ca7a41c3b2"}, + {file = "cryptography-45.0.7-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:81823935e2f8d476707e85a78a405953a03ef7b7b4f55f93f7c2d9680e5e0691"}, + {file = "cryptography-45.0.7-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3994c809c17fc570c2af12c9b840d7cea85a9fd3e5c0e0491f4fa3c029216d59"}, + {file = "cryptography-45.0.7-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:dad43797959a74103cb59c5dac71409f9c27d34c8a05921341fb64ea8ccb1dd4"}, + {file = "cryptography-45.0.7-cp37-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:ce7a453385e4c4693985b4a4a3533e041558851eae061a58a5405363b098fcd3"}, + {file = "cryptography-45.0.7-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:b04f85ac3a90c227b6e5890acb0edbaf3140938dbecf07bff618bf3638578cf1"}, + {file = "cryptography-45.0.7-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:48c41a44ef8b8c2e80ca4527ee81daa4c527df3ecbc9423c41a420a9559d0e27"}, + {file = "cryptography-45.0.7-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:f3df7b3d0f91b88b2106031fd995802a2e9ae13e02c36c1fc075b43f420f3a17"}, + {file = "cryptography-45.0.7-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:dd342f085542f6eb894ca00ef70236ea46070c8a13824c6bde0dfdcd36065b9b"}, + {file = "cryptography-45.0.7-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:1993a1bb7e4eccfb922b6cd414f072e08ff5816702a0bdb8941c247a6b1b287c"}, + {file = "cryptography-45.0.7-cp37-abi3-win32.whl", hash = "sha256:18fcf70f243fe07252dcb1b268a687f2358025ce32f9f88028ca5c364b123ef5"}, + {file = "cryptography-45.0.7-cp37-abi3-win_amd64.whl", hash = "sha256:7285a89df4900ed3bfaad5679b1e668cb4b38a8de1ccbfc84b05f34512da0a90"}, + {file = "cryptography-45.0.7-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:de58755d723e86175756f463f2f0bddd45cc36fbd62601228a3f8761c9f58252"}, + {file = "cryptography-45.0.7-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:a20e442e917889d1a6b3c570c9e3fa2fdc398c20868abcea268ea33c024c4083"}, + {file = "cryptography-45.0.7-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:258e0dff86d1d891169b5af222d362468a9570e2532923088658aa866eb11130"}, + {file = "cryptography-45.0.7-pp310-pypy310_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:d97cf502abe2ab9eff8bd5e4aca274da8d06dd3ef08b759a8d6143f4ad65d4b4"}, + {file = "cryptography-45.0.7-pp310-pypy310_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:c987dad82e8c65ebc985f5dae5e74a3beda9d0a2a4daf8a1115f3772b59e5141"}, + {file = "cryptography-45.0.7-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:c13b1e3afd29a5b3b2656257f14669ca8fa8d7956d509926f0b130b600b50ab7"}, + {file = "cryptography-45.0.7-pp311-pypy311_pp73-macosx_10_9_x86_64.whl", hash = "sha256:4a862753b36620af6fc54209264f92c716367f2f0ff4624952276a6bbd18cbde"}, + {file = "cryptography-45.0.7-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:06ce84dc14df0bf6ea84666f958e6080cdb6fe1231be2a51f3fc1267d9f3fb34"}, + {file = "cryptography-45.0.7-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:d0c5c6bac22b177bf8da7435d9d27a6834ee130309749d162b26c3105c0795a9"}, + {file = "cryptography-45.0.7-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:2f641b64acc00811da98df63df7d59fd4706c0df449da71cb7ac39a0732b40ae"}, + {file = "cryptography-45.0.7-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:f5414a788ecc6ee6bc58560e85ca624258a55ca434884445440a810796ea0e0b"}, + {file = "cryptography-45.0.7-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:1f3d56f73595376f4244646dd5c5870c14c196949807be39e79e7bd9bac3da63"}, + {file = "cryptography-45.0.7.tar.gz", hash = "sha256:4b1654dfc64ea479c242508eb8c724044f1e964a47d1d1cacc5132292d851971"}, +] + +[package.dependencies] +cffi = {version = ">=1.14", markers = "platform_python_implementation != \"PyPy\""} + +[package.extras] +docs = ["sphinx (>=5.3.0)", "sphinx-inline-tabs ; python_full_version >= \"3.8.0\"", "sphinx-rtd-theme (>=3.0.0) ; python_full_version >= \"3.8.0\""] +docstest = ["pyenchant (>=3)", "readme-renderer (>=30.0)", "sphinxcontrib-spelling (>=7.3.1)"] +nox = ["nox (>=2024.4.15)", "nox[uv] (>=2024.3.2) ; python_full_version >= \"3.8.0\""] +pep8test = ["check-sdist ; python_full_version >= \"3.8.0\"", "click (>=8.0.1)", "mypy (>=1.4)", "ruff (>=0.3.6)"] +sdist = ["build (>=1.0.0)"] +ssh = ["bcrypt (>=3.1.5)"] +test = ["certifi (>=2024)", "cryptography-vectors (==45.0.7)", "pretend (>=0.7)", "pytest (>=7.4.0)", "pytest-benchmark (>=4.0)", "pytest-cov (>=2.10.1)", "pytest-xdist (>=3.5.0)"] +test-randomorder = ["pytest-randomly"] + +[[package]] +name = "dill" +version = "0.4.0" +description = "serialize all of Python" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "dill-0.4.0-py3-none-any.whl", hash = "sha256:44f54bf6412c2c8464c14e8243eb163690a9800dbe2c367330883b19c7561049"}, + {file = "dill-0.4.0.tar.gz", hash = "sha256:0633f1d2df477324f53a895b02c901fb961bdbf65a17122586ea7019292cbcf0"}, +] + +[package.extras] +graph = ["objgraph (>=1.7.2)"] +profile = ["gprof2dot (>=2022.7.29)"] + +[[package]] +name = "docutils" +version = "0.18.1" +description = "Docutils -- Python Documentation Utilities" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +groups = ["main", "docs"] +files = [ + {file = "docutils-0.18.1-py2.py3-none-any.whl", hash = "sha256:23010f129180089fbcd3bc08cfefccb3b890b0050e1ca00c867036e9d161b98c"}, + {file = "docutils-0.18.1.tar.gz", hash = "sha256:679987caf361a7539d76e584cbeddc311e3aee937877c87346f31debc63e9d06"}, +] + +[[package]] +name = "dparse" +version = "0.6.4" +description = "A parser for Python dependency files" +optional = false +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "dparse-0.6.4-py3-none-any.whl", hash = "sha256:fbab4d50d54d0e739fbb4dedfc3d92771003a5b9aa8545ca7a7045e3b174af57"}, + {file = "dparse-0.6.4.tar.gz", hash = "sha256:90b29c39e3edc36c6284c82c4132648eaf28a01863eb3c231c2512196132201a"}, +] + +[package.dependencies] +packaging = "*" + +[package.extras] +all = ["pipenv", "poetry", "pyyaml"] +conda = ["pyyaml"] +pipenv = ["pipenv"] +poetry = ["poetry"] + +[[package]] +name = "eralchemy" +version = "1.6.0" +description = "Simple entity relation (ER) diagrams generation" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "eralchemy-1.6.0-py3-none-any.whl", hash = "sha256:29f3c9c6211892306cdf0605c2df3239ac9d322c63c0385f3959b9a0228fd1f5"}, + {file = "eralchemy-1.6.0.tar.gz", hash = "sha256:8f82d329ec0cd9c04469adf36b8889b5ea2583e7e53c0fd2e784e176e1e27c7a"}, +] + +[package.dependencies] +sqlalchemy = ">=1.4.18" + +[package.extras] +dev = ["nox", "pre-commit"] +docs = ["pydata-sphinx-theme", "sphinx (>=6.2.1)", "sphinx-copybutton", "sphinx-design"] +graphviz = ["graphviz (>=0.20.3)"] +pygraphviz = ["pygraphviz (>=1.9)"] +test = ["flask-sqlalchemy (>=2.5.1)", "graphviz (>=0.20.3)", "psycopg2 (>=2.9.3)", "pygraphviz (>=1.9)", "pytest (>=7.4.3)", "pytest-cov"] + +[[package]] +name = "fastapi" +version = "0.116.1" +description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "fastapi-0.116.1-py3-none-any.whl", hash = "sha256:c46ac7c312df840f0c9e220f7964bada936781bc4e2e6eb71f1c4d7553786565"}, + {file = "fastapi-0.116.1.tar.gz", hash = "sha256:ed52cbf946abfd70c5a0dccb24673f0670deeb517a88b3544d03c2a6bf283143"}, +] + +[package.dependencies] +pydantic = ">=1.7.4,<1.8 || >1.8,<1.8.1 || >1.8.1,<2.0.0 || >2.0.0,<2.0.1 || >2.0.1,<2.1.0 || >2.1.0,<3.0.0" +starlette = ">=0.40.0,<0.48.0" +typing-extensions = ">=4.8.0" + +[package.extras] +all = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.8)", "httpx (>=0.23.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=3.1.5)", "orjson (>=3.2.1)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.18)", "pyyaml (>=5.3.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0)", "uvicorn[standard] (>=0.12.0)"] +standard = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.8)", "httpx (>=0.23.0)", "jinja2 (>=3.1.5)", "python-multipart (>=0.0.18)", "uvicorn[standard] (>=0.12.0)"] +standard-no-fastapi-cloud-cli = ["email-validator (>=2.0.0)", "fastapi-cli[standard-no-fastapi-cloud-cli] (>=0.0.8)", "httpx (>=0.23.0)", "jinja2 (>=3.1.5)", "python-multipart (>=0.0.18)", "uvicorn[standard] (>=0.12.0)"] + +[[package]] +name = "flake8" +version = "6.1.0" +description = "the modular source code checker: pep8 pyflakes and co" +optional = false +python-versions = ">=3.8.1" +groups = ["dev"] +files = [ + {file = "flake8-6.1.0-py2.py3-none-any.whl", hash = "sha256:ffdfce58ea94c6580c77888a86506937f9a1a227dfcd15f245d694ae20a6b6e5"}, + {file = "flake8-6.1.0.tar.gz", hash = "sha256:d5b3857f07c030bdb5bf41c7f53799571d75c4491748a3adcd47de929e34cd23"}, +] + +[package.dependencies] +mccabe = ">=0.7.0,<0.8.0" +pycodestyle = ">=2.11.0,<2.12.0" +pyflakes = ">=3.1.0,<3.2.0" + +[[package]] +name = "graphviz" +version = "0.20.3" +description = "Simple Python interface for Graphviz" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "graphviz-0.20.3-py3-none-any.whl", hash = "sha256:81f848f2904515d8cd359cc611faba817598d2feaac4027b266aa3eda7b3dde5"}, + {file = "graphviz-0.20.3.zip", hash = "sha256:09d6bc81e6a9fa392e7ba52135a9d49f1ed62526f96499325930e87ca1b5925d"}, +] + +[package.extras] +dev = ["flake8", "pep8-naming", "tox (>=3)", "twine", "wheel"] +docs = ["sphinx (>=5,<7)", "sphinx-autodoc-typehints", "sphinx-rtd-theme"] +test = ["coverage", "pytest (>=7,<8.1)", "pytest-cov", "pytest-mock (>=3)"] + +[[package]] +name = "greenlet" +version = "3.2.4" +description = "Lightweight in-process concurrent programming" +optional = false +python-versions = ">=3.9" +groups = ["main", "dev"] +markers = "python_version < \"3.14\" and (platform_machine == \"aarch64\" or platform_machine == \"ppc64le\" or platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"win32\" or platform_machine == \"WIN32\")" +files = [ + {file = "greenlet-3.2.4-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:8c68325b0d0acf8d91dde4e6f930967dd52a5302cd4062932a6b2e7c2969f47c"}, + {file = "greenlet-3.2.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:94385f101946790ae13da500603491f04a76b6e4c059dab271b3ce2e283b2590"}, + {file = "greenlet-3.2.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f10fd42b5ee276335863712fa3da6608e93f70629c631bf77145021600abc23c"}, + {file = "greenlet-3.2.4-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:c8c9e331e58180d0d83c5b7999255721b725913ff6bc6cf39fa2a45841a4fd4b"}, + {file = "greenlet-3.2.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:58b97143c9cc7b86fc458f215bd0932f1757ce649e05b640fea2e79b54cedb31"}, + {file = "greenlet-3.2.4-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c2ca18a03a8cfb5b25bc1cbe20f3d9a4c80d8c3b13ba3df49ac3961af0b1018d"}, + {file = "greenlet-3.2.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:9fe0a28a7b952a21e2c062cd5756d34354117796c6d9215a87f55e38d15402c5"}, + {file = "greenlet-3.2.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8854167e06950ca75b898b104b63cc646573aa5fef1353d4508ecdd1ee76254f"}, + {file = "greenlet-3.2.4-cp310-cp310-win_amd64.whl", hash = "sha256:73f49b5368b5359d04e18d15828eecc1806033db5233397748f4ca813ff1056c"}, + {file = "greenlet-3.2.4-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:96378df1de302bc38e99c3a9aa311967b7dc80ced1dcc6f171e99842987882a2"}, + {file = "greenlet-3.2.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1ee8fae0519a337f2329cb78bd7a8e128ec0f881073d43f023c7b8d4831d5246"}, + {file = "greenlet-3.2.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:94abf90142c2a18151632371140b3dba4dee031633fe614cb592dbb6c9e17bc3"}, + {file = "greenlet-3.2.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:4d1378601b85e2e5171b99be8d2dc85f594c79967599328f95c1dc1a40f1c633"}, + {file = "greenlet-3.2.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0db5594dce18db94f7d1650d7489909b57afde4c580806b8d9203b6e79cdc079"}, + {file = "greenlet-3.2.4-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2523e5246274f54fdadbce8494458a2ebdcdbc7b802318466ac5606d3cded1f8"}, + {file = "greenlet-3.2.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:1987de92fec508535687fb807a5cea1560f6196285a4cde35c100b8cd632cc52"}, + {file = "greenlet-3.2.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:55e9c5affaa6775e2c6b67659f3a71684de4c549b3dd9afca3bc773533d284fa"}, + {file = "greenlet-3.2.4-cp311-cp311-win_amd64.whl", hash = "sha256:9c40adce87eaa9ddb593ccb0fa6a07caf34015a29bf8d344811665b573138db9"}, + {file = "greenlet-3.2.4-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:3b67ca49f54cede0186854a008109d6ee71f66bd57bb36abd6d0a0267b540cdd"}, + {file = "greenlet-3.2.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ddf9164e7a5b08e9d22511526865780a576f19ddd00d62f8a665949327fde8bb"}, + {file = "greenlet-3.2.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f28588772bb5fb869a8eb331374ec06f24a83a9c25bfa1f38b6993afe9c1e968"}, + {file = "greenlet-3.2.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:5c9320971821a7cb77cfab8d956fa8e39cd07ca44b6070db358ceb7f8797c8c9"}, + {file = "greenlet-3.2.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c60a6d84229b271d44b70fb6e5fa23781abb5d742af7b808ae3f6efd7c9c60f6"}, + {file = "greenlet-3.2.4-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3b3812d8d0c9579967815af437d96623f45c0f2ae5f04e366de62a12d83a8fb0"}, + {file = "greenlet-3.2.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:abbf57b5a870d30c4675928c37278493044d7c14378350b3aa5d484fa65575f0"}, + {file = "greenlet-3.2.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:20fb936b4652b6e307b8f347665e2c615540d4b42b3b4c8a321d8286da7e520f"}, + {file = "greenlet-3.2.4-cp312-cp312-win_amd64.whl", hash = "sha256:a7d4e128405eea3814a12cc2605e0e6aedb4035bf32697f72deca74de4105e02"}, + {file = "greenlet-3.2.4-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:1a921e542453fe531144e91e1feedf12e07351b1cf6c9e8a3325ea600a715a31"}, + {file = "greenlet-3.2.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cd3c8e693bff0fff6ba55f140bf390fa92c994083f838fece0f63be121334945"}, + {file = "greenlet-3.2.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:710638eb93b1fa52823aa91bf75326f9ecdfd5e0466f00789246a5280f4ba0fc"}, + {file = "greenlet-3.2.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:c5111ccdc9c88f423426df3fd1811bfc40ed66264d35aa373420a34377efc98a"}, + {file = "greenlet-3.2.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d76383238584e9711e20ebe14db6c88ddcedc1829a9ad31a584389463b5aa504"}, + {file = "greenlet-3.2.4-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:23768528f2911bcd7e475210822ffb5254ed10d71f4028387e5a99b4c6699671"}, + {file = "greenlet-3.2.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:00fadb3fedccc447f517ee0d3fd8fe49eae949e1cd0f6a611818f4f6fb7dc83b"}, + {file = "greenlet-3.2.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:d25c5091190f2dc0eaa3f950252122edbbadbb682aa7b1ef2f8af0f8c0afefae"}, + {file = "greenlet-3.2.4-cp313-cp313-win_amd64.whl", hash = "sha256:554b03b6e73aaabec3745364d6239e9e012d64c68ccd0b8430c64ccc14939a8b"}, + {file = "greenlet-3.2.4-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:49a30d5fda2507ae77be16479bdb62a660fa51b1eb4928b524975b3bde77b3c0"}, + {file = "greenlet-3.2.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:299fd615cd8fc86267b47597123e3f43ad79c9d8a22bebdce535e53550763e2f"}, + {file = "greenlet-3.2.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:c17b6b34111ea72fc5a4e4beec9711d2226285f0386ea83477cbb97c30a3f3a5"}, + {file = "greenlet-3.2.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b4a1870c51720687af7fa3e7cda6d08d801dae660f75a76f3845b642b4da6ee1"}, + {file = "greenlet-3.2.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:061dc4cf2c34852b052a8620d40f36324554bc192be474b9e9770e8c042fd735"}, + {file = "greenlet-3.2.4-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:44358b9bf66c8576a9f57a590d5f5d6e72fa4228b763d0e43fee6d3b06d3a337"}, + {file = "greenlet-3.2.4-cp314-cp314-win_amd64.whl", hash = "sha256:e37ab26028f12dbb0ff65f29a8d3d44a765c61e729647bf2ddfbbed621726f01"}, + {file = "greenlet-3.2.4-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:b6a7c19cf0d2742d0809a4c05975db036fdff50cd294a93632d6a310bf9ac02c"}, + {file = "greenlet-3.2.4-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:27890167f55d2387576d1f41d9487ef171849ea0359ce1510ca6e06c8bece11d"}, + {file = "greenlet-3.2.4-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:18d9260df2b5fbf41ae5139e1be4e796d99655f023a636cd0e11e6406cca7d58"}, + {file = "greenlet-3.2.4-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:671df96c1f23c4a0d4077a325483c1503c96a1b7d9db26592ae770daa41233d4"}, + {file = "greenlet-3.2.4-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:16458c245a38991aa19676900d48bd1a6f2ce3e16595051a4db9d012154e8433"}, + {file = "greenlet-3.2.4-cp39-cp39-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c9913f1a30e4526f432991f89ae263459b1c64d1608c0d22a5c79c287b3c70df"}, + {file = "greenlet-3.2.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b90654e092f928f110e0007f572007c9727b5265f7632c2fa7415b4689351594"}, + {file = "greenlet-3.2.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:81701fd84f26330f0d5f4944d4e92e61afe6319dcd9775e39396e39d7c3e5f98"}, + {file = "greenlet-3.2.4-cp39-cp39-win32.whl", hash = "sha256:65458b409c1ed459ea899e939f0e1cdb14f58dbc803f2f93c5eab5694d32671b"}, + {file = "greenlet-3.2.4-cp39-cp39-win_amd64.whl", hash = "sha256:d2e685ade4dafd447ede19c31277a224a239a0a1a4eca4e6390efedf20260cfb"}, + {file = "greenlet-3.2.4.tar.gz", hash = "sha256:0dca0d95ff849f9a364385f36ab49f50065d76964944638be9691e1832e9f86d"}, +] + +[package.extras] +docs = ["Sphinx", "furo"] +test = ["objgraph", "psutil", "setuptools"] + +[[package]] +name = "h11" +version = "0.16.0" +description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" +optional = false +python-versions = ">=3.8" +groups = ["main", "dev"] +files = [ + {file = "h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86"}, + {file = "h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1"}, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +description = "A minimal low-level HTTP client." +optional = false +python-versions = ">=3.8" +groups = ["main", "dev"] +files = [ + {file = "httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55"}, + {file = "httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8"}, +] + +[package.dependencies] +certifi = "*" +h11 = ">=0.16" + +[package.extras] +asyncio = ["anyio (>=4.0,<5.0)"] +http2 = ["h2 (>=3,<5)"] +socks = ["socksio (==1.*)"] +trio = ["trio (>=0.22.0,<1.0)"] + +[[package]] +name = "httptools" +version = "0.6.4" +description = "A collection of framework independent HTTP protocol utils." +optional = false +python-versions = ">=3.8.0" +groups = ["main"] +files = [ + {file = "httptools-0.6.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3c73ce323711a6ffb0d247dcd5a550b8babf0f757e86a52558fe5b86d6fefcc0"}, + {file = "httptools-0.6.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:345c288418f0944a6fe67be8e6afa9262b18c7626c3ef3c28adc5eabc06a68da"}, + {file = "httptools-0.6.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:deee0e3343f98ee8047e9f4c5bc7cedbf69f5734454a94c38ee829fb2d5fa3c1"}, + {file = "httptools-0.6.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca80b7485c76f768a3bc83ea58373f8db7b015551117375e4918e2aa77ea9b50"}, + {file = "httptools-0.6.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:90d96a385fa941283ebd231464045187a31ad932ebfa541be8edf5b3c2328959"}, + {file = "httptools-0.6.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:59e724f8b332319e2875efd360e61ac07f33b492889284a3e05e6d13746876f4"}, + {file = "httptools-0.6.4-cp310-cp310-win_amd64.whl", hash = "sha256:c26f313951f6e26147833fc923f78f95604bbec812a43e5ee37f26dc9e5a686c"}, + {file = "httptools-0.6.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f47f8ed67cc0ff862b84a1189831d1d33c963fb3ce1ee0c65d3b0cbe7b711069"}, + {file = "httptools-0.6.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0614154d5454c21b6410fdf5262b4a3ddb0f53f1e1721cfd59d55f32138c578a"}, + {file = "httptools-0.6.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f8787367fbdfccae38e35abf7641dafc5310310a5987b689f4c32cc8cc3ee975"}, + {file = "httptools-0.6.4-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40b0f7fe4fd38e6a507bdb751db0379df1e99120c65fbdc8ee6c1d044897a636"}, + {file = "httptools-0.6.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:40a5ec98d3f49904b9fe36827dcf1aadfef3b89e2bd05b0e35e94f97c2b14721"}, + {file = "httptools-0.6.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:dacdd3d10ea1b4ca9df97a0a303cbacafc04b5cd375fa98732678151643d4988"}, + {file = "httptools-0.6.4-cp311-cp311-win_amd64.whl", hash = "sha256:288cd628406cc53f9a541cfaf06041b4c71d751856bab45e3702191f931ccd17"}, + {file = "httptools-0.6.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:df017d6c780287d5c80601dafa31f17bddb170232d85c066604d8558683711a2"}, + {file = "httptools-0.6.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:85071a1e8c2d051b507161f6c3e26155b5c790e4e28d7f236422dbacc2a9cc44"}, + {file = "httptools-0.6.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69422b7f458c5af875922cdb5bd586cc1f1033295aa9ff63ee196a87519ac8e1"}, + {file = "httptools-0.6.4-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:16e603a3bff50db08cd578d54f07032ca1631450ceb972c2f834c2b860c28ea2"}, + {file = "httptools-0.6.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ec4f178901fa1834d4a060320d2f3abc5c9e39766953d038f1458cb885f47e81"}, + {file = "httptools-0.6.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f9eb89ecf8b290f2e293325c646a211ff1c2493222798bb80a530c5e7502494f"}, + {file = "httptools-0.6.4-cp312-cp312-win_amd64.whl", hash = "sha256:db78cb9ca56b59b016e64b6031eda5653be0589dba2b1b43453f6e8b405a0970"}, + {file = "httptools-0.6.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ade273d7e767d5fae13fa637f4d53b6e961fb7fd93c7797562663f0171c26660"}, + {file = "httptools-0.6.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:856f4bc0478ae143bad54a4242fccb1f3f86a6e1be5548fecfd4102061b3a083"}, + {file = "httptools-0.6.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:322d20ea9cdd1fa98bd6a74b77e2ec5b818abdc3d36695ab402a0de8ef2865a3"}, + {file = "httptools-0.6.4-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4d87b29bd4486c0093fc64dea80231f7c7f7eb4dc70ae394d70a495ab8436071"}, + {file = "httptools-0.6.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:342dd6946aa6bda4b8f18c734576106b8a31f2fe31492881a9a160ec84ff4bd5"}, + {file = "httptools-0.6.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4b36913ba52008249223042dca46e69967985fb4051951f94357ea681e1f5dc0"}, + {file = "httptools-0.6.4-cp313-cp313-win_amd64.whl", hash = "sha256:28908df1b9bb8187393d5b5db91435ccc9c8e891657f9cbb42a2541b44c82fc8"}, + {file = "httptools-0.6.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:d3f0d369e7ffbe59c4b6116a44d6a8eb4783aae027f2c0b366cf0aa964185dba"}, + {file = "httptools-0.6.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:94978a49b8f4569ad607cd4946b759d90b285e39c0d4640c6b36ca7a3ddf2efc"}, + {file = "httptools-0.6.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40dc6a8e399e15ea525305a2ddba998b0af5caa2566bcd79dcbe8948181eeaff"}, + {file = "httptools-0.6.4-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ab9ba8dcf59de5181f6be44a77458e45a578fc99c31510b8c65b7d5acc3cf490"}, + {file = "httptools-0.6.4-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:fc411e1c0a7dcd2f902c7c48cf079947a7e65b5485dea9decb82b9105ca71a43"}, + {file = "httptools-0.6.4-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:d54efd20338ac52ba31e7da78e4a72570cf729fac82bc31ff9199bedf1dc7440"}, + {file = "httptools-0.6.4-cp38-cp38-win_amd64.whl", hash = "sha256:df959752a0c2748a65ab5387d08287abf6779ae9165916fe053e68ae1fbdc47f"}, + {file = "httptools-0.6.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:85797e37e8eeaa5439d33e556662cc370e474445d5fab24dcadc65a8ffb04003"}, + {file = "httptools-0.6.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:db353d22843cf1028f43c3651581e4bb49374d85692a85f95f7b9a130e1b2cab"}, + {file = "httptools-0.6.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d1ffd262a73d7c28424252381a5b854c19d9de5f56f075445d33919a637e3547"}, + {file = "httptools-0.6.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:703c346571fa50d2e9856a37d7cd9435a25e7fd15e236c397bf224afaa355fe9"}, + {file = "httptools-0.6.4-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:aafe0f1918ed07b67c1e838f950b1c1fabc683030477e60b335649b8020e1076"}, + {file = "httptools-0.6.4-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:0e563e54979e97b6d13f1bbc05a96109923e76b901f786a5eae36e99c01237bd"}, + {file = "httptools-0.6.4-cp39-cp39-win_amd64.whl", hash = "sha256:b799de31416ecc589ad79dd85a0b2657a8fe39327944998dea368c1d4c9e55e6"}, + {file = "httptools-0.6.4.tar.gz", hash = "sha256:4e93eee4add6493b59a5c514da98c939b244fce4a0d8879cd3f466562f4b7d5c"}, +] + +[package.extras] +test = ["Cython (>=0.29.24)"] + +[[package]] +name = "httpx" +version = "0.25.2" +description = "The next generation HTTP client." +optional = false +python-versions = ">=3.8" +groups = ["main", "dev"] +files = [ + {file = "httpx-0.25.2-py3-none-any.whl", hash = "sha256:a05d3d052d9b2dfce0e3896636467f8a5342fb2b902c819428e1ac65413ca118"}, + {file = "httpx-0.25.2.tar.gz", hash = "sha256:8b8fcaa0c8ea7b05edd69a094e63a2094c4efcb48129fb757361bc423c0ad9e8"}, +] + +[package.dependencies] +anyio = "*" +certifi = "*" +httpcore = "==1.*" +idna = "*" +sniffio = "*" + +[package.extras] +brotli = ["brotli ; platform_python_implementation == \"CPython\"", "brotlicffi ; platform_python_implementation != \"CPython\""] +cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"] +http2 = ["h2 (>=3,<5)"] +socks = ["socksio (==1.*)"] + +[[package]] +name = "idna" +version = "3.10" +description = "Internationalized Domain Names in Applications (IDNA)" +optional = false +python-versions = ">=3.6" +groups = ["main", "dev", "docs"] +files = [ + {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, + {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, +] + +[package.extras] +all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] + +[[package]] +name = "imagesize" +version = "1.4.1" +description = "Getting image size from png/jpeg/jpeg2000/gif file" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +groups = ["main", "docs"] +files = [ + {file = "imagesize-1.4.1-py2.py3-none-any.whl", hash = "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b"}, + {file = "imagesize-1.4.1.tar.gz", hash = "sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a"}, +] + +[[package]] +name = "inflect" +version = "7.5.0" +description = "Correctly generate plurals, singular nouns, ordinals, indefinite articles" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "inflect-7.5.0-py3-none-any.whl", hash = "sha256:2aea70e5e70c35d8350b8097396ec155ffd68def678c7ff97f51aa69c1d92344"}, + {file = "inflect-7.5.0.tar.gz", hash = "sha256:faf19801c3742ed5a05a8ce388e0d8fe1a07f8d095c82201eb904f5d27ad571f"}, +] + +[package.dependencies] +more_itertools = ">=8.5.0" +typeguard = ">=4.0.1" + +[package.extras] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""] +cover = ["pytest-cov"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +enabler = ["pytest-enabler (>=2.2)"] +test = ["pygments", "pytest (>=6,!=8.1.*)"] +type = ["pytest-mypy"] + +[[package]] +name = "iniconfig" +version = "2.1.0" +description = "brain-dead simple config-ini parsing" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760"}, + {file = "iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7"}, +] + +[[package]] +name = "isort" +version = "5.13.2" +description = "A Python utility / library to sort Python imports." +optional = false +python-versions = ">=3.8.0" +groups = ["dev"] +files = [ + {file = "isort-5.13.2-py3-none-any.whl", hash = "sha256:8ca5e72a8d85860d5a3fa69b8745237f2939afe12dbf656afbcb47fe72d947a6"}, + {file = "isort-5.13.2.tar.gz", hash = "sha256:48fdfcb9face5d58a4f6dde2e72a1fb8dcaf8ab26f95ab49fab84c2ddefb0109"}, +] + +[package.extras] +colors = ["colorama (>=0.4.6)"] + +[[package]] +name = "jinja2" +version = "3.1.6" +description = "A very fast and expressive template engine." +optional = false +python-versions = ">=3.7" +groups = ["main", "docs"] +files = [ + {file = "jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67"}, + {file = "jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d"}, +] + +[package.dependencies] +MarkupSafe = ">=2.0" + +[package.extras] +i18n = ["Babel (>=2.7)"] + +[[package]] +name = "joblib" +version = "1.5.2" +description = "Lightweight pipelining with Python functions" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "joblib-1.5.2-py3-none-any.whl", hash = "sha256:4e1f0bdbb987e6d843c70cf43714cb276623def372df3c22fe5266b2670bc241"}, + {file = "joblib-1.5.2.tar.gz", hash = "sha256:3faa5c39054b2f03ca547da9b2f52fde67c06240c31853f306aea97f13647b55"}, +] + +[[package]] +name = "jwcrypto" +version = "1.5.6" +description = "Implementation of JOSE Web standards" +optional = false +python-versions = ">= 3.8" +groups = ["main"] +files = [ + {file = "jwcrypto-1.5.6-py3-none-any.whl", hash = "sha256:150d2b0ebbdb8f40b77f543fb44ffd2baeff48788be71f67f03566692fd55789"}, + {file = "jwcrypto-1.5.6.tar.gz", hash = "sha256:771a87762a0c081ae6166958a954f80848820b2ab066937dc8b8379d65b1b039"}, +] + +[package.dependencies] +cryptography = ">=3.4" +typing-extensions = ">=4.5.0" + +[[package]] +name = "livereload" +version = "2.7.1" +description = "Python LiveReload is an awesome tool for web developers" +optional = false +python-versions = ">=3.7" +groups = ["docs"] +files = [ + {file = "livereload-2.7.1-py3-none-any.whl", hash = "sha256:5201740078c1b9433f4b2ba22cd2729a39b9d0ec0a2cc6b4d3df257df5ad0564"}, + {file = "livereload-2.7.1.tar.gz", hash = "sha256:3d9bf7c05673df06e32bea23b494b8d36ca6d10f7d5c3c8a6989608c09c986a9"}, +] + +[package.dependencies] +tornado = "*" + +[[package]] +name = "mako" +version = "1.3.10" +description = "A super-fast templating language that borrows the best ideas from the existing templating languages." +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "mako-1.3.10-py3-none-any.whl", hash = "sha256:baef24a52fc4fc514a0887ac600f9f1cff3d82c61d4d700a1fa84d597b88db59"}, + {file = "mako-1.3.10.tar.gz", hash = "sha256:99579a6f39583fa7e5630a28c3c1f440e4e97a414b80372649c0ce338da2ea28"}, +] + +[package.dependencies] +MarkupSafe = ">=0.9.2" + +[package.extras] +babel = ["Babel"] +lingua = ["lingua"] +testing = ["pytest"] + +[[package]] +name = "markdown-it-py" +version = "3.0.0" +description = "Python port of markdown-it. Markdown parsing, done right!" +optional = false +python-versions = ">=3.8" +groups = ["dev", "docs"] +files = [ + {file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"}, + {file = "markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1"}, +] + +[package.dependencies] +mdurl = ">=0.1,<1.0" + +[package.extras] +benchmarking = ["psutil", "pytest", "pytest-benchmark"] +code-style = ["pre-commit (>=3.0,<4.0)"] +compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "mistletoe (>=1.0,<2.0)", "mistune (>=2.0,<3.0)", "panflute (>=2.3,<3.0)"] +linkify = ["linkify-it-py (>=1,<3)"] +plugins = ["mdit-py-plugins"] +profiling = ["gprof2dot"] +rtd = ["jupyter_sphinx", "mdit-py-plugins", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"] +testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] + +[[package]] +name = "markupsafe" +version = "3.0.2" +description = "Safely add untrusted strings to HTML/XML markup." +optional = false +python-versions = ">=3.9" +groups = ["main", "docs"] +files = [ + {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-win32.whl", hash = "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:eaa0a10b7f72326f1372a713e73c3f739b524b3af41feb43e4921cb529f5929a"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:48032821bbdf20f5799ff537c7ac3d1fba0ba032cfc06194faffa8cda8b560ff"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a9d3f5f0901fdec14d8d2f66ef7d035f2157240a433441719ac9a3fba440b13"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88b49a3b9ff31e19998750c38e030fc7bb937398b1f78cfa599aaef92d693144"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cfad01eed2c2e0c01fd0ecd2ef42c492f7f93902e39a42fc9ee1692961443a29"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1225beacc926f536dc82e45f8a4d68502949dc67eea90eab715dea3a21c1b5f0"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3169b1eefae027567d1ce6ee7cae382c57fe26e82775f460f0b2778beaad66c0"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:eb7972a85c54febfb25b5c4b4f3af4dcc731994c7da0d8a0b4a6eb0640e1d178"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-win32.whl", hash = "sha256:8c4e8c3ce11e1f92f6536ff07154f9d49677ebaaafc32db9db4620bc11ed480f"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a"}, + {file = "markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0"}, +] + +[[package]] +name = "mccabe" +version = "0.7.0" +description = "McCabe checker, plugin for flake8" +optional = false +python-versions = ">=3.6" +groups = ["dev"] +files = [ + {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, + {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, +] + +[[package]] +name = "mdit-py-plugins" +version = "0.5.0" +description = "Collection of plugins for markdown-it-py" +optional = false +python-versions = ">=3.10" +groups = ["docs"] +files = [ + {file = "mdit_py_plugins-0.5.0-py3-none-any.whl", hash = "sha256:07a08422fc1936a5d26d146759e9155ea466e842f5ab2f7d2266dd084c8dab1f"}, + {file = "mdit_py_plugins-0.5.0.tar.gz", hash = "sha256:f4918cb50119f50446560513a8e311d574ff6aaed72606ddae6d35716fe809c6"}, +] + +[package.dependencies] +markdown-it-py = ">=2.0.0,<5.0.0" + +[package.extras] +code-style = ["pre-commit"] +rtd = ["myst-parser", "sphinx-book-theme"] +testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] + +[[package]] +name = "mdurl" +version = "0.1.2" +description = "Markdown URL utilities" +optional = false +python-versions = ">=3.7" +groups = ["dev", "docs"] +files = [ + {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, + {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, +] + +[[package]] +name = "mirakuru" +version = "2.6.1" +description = "Process executor (not only) for tests." +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "mirakuru-2.6.1-py3-none-any.whl", hash = "sha256:4be0bfd270744454fa0c0466b8127b66bd55f4decaf05bbee9b071f2acbd9473"}, + {file = "mirakuru-2.6.1.tar.gz", hash = "sha256:95d4f5a5ad406a625e9ca418f20f8e09386a35dad1ea30fd9073e0ae93f712c7"}, +] + +[package.dependencies] +psutil = {version = ">=4.0.0", markers = "sys_platform != \"cygwin\""} + +[[package]] +name = "more-itertools" +version = "10.8.0" +description = "More routines for operating on iterables, beyond itertools" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "more_itertools-10.8.0-py3-none-any.whl", hash = "sha256:52d4362373dcf7c52546bc4af9a86ee7c4579df9a8dc268be0a2f949d376cc9b"}, + {file = "more_itertools-10.8.0.tar.gz", hash = "sha256:f638ddf8a1a0d134181275fb5d58b086ead7c6a72429ad725c67503f13ba30bd"}, +] + +[[package]] +name = "mypy" +version = "1.17.1" +description = "Optional static typing for Python" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "mypy-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:3fbe6d5555bf608c47203baa3e72dbc6ec9965b3d7c318aa9a4ca76f465bd972"}, + {file = "mypy-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:80ef5c058b7bce08c83cac668158cb7edea692e458d21098c7d3bce35a5d43e7"}, + {file = "mypy-1.17.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4a580f8a70c69e4a75587bd925d298434057fe2a428faaf927ffe6e4b9a98df"}, + {file = "mypy-1.17.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dd86bb649299f09d987a2eebb4d52d10603224500792e1bee18303bbcc1ce390"}, + {file = "mypy-1.17.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:a76906f26bd8d51ea9504966a9c25419f2e668f012e0bdf3da4ea1526c534d94"}, + {file = "mypy-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:e79311f2d904ccb59787477b7bd5d26f3347789c06fcd7656fa500875290264b"}, + {file = "mypy-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ad37544be07c5d7fba814eb370e006df58fed8ad1ef33ed1649cb1889ba6ff58"}, + {file = "mypy-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:064e2ff508e5464b4bd807a7c1625bc5047c5022b85c70f030680e18f37273a5"}, + {file = "mypy-1.17.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:70401bbabd2fa1aa7c43bb358f54037baf0586f41e83b0ae67dd0534fc64edfd"}, + {file = "mypy-1.17.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e92bdc656b7757c438660f775f872a669b8ff374edc4d18277d86b63edba6b8b"}, + {file = "mypy-1.17.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c1fdf4abb29ed1cb091cf432979e162c208a5ac676ce35010373ff29247bcad5"}, + {file = "mypy-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:ff2933428516ab63f961644bc49bc4cbe42bbffb2cd3b71cc7277c07d16b1a8b"}, + {file = "mypy-1.17.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:69e83ea6553a3ba79c08c6e15dbd9bfa912ec1e493bf75489ef93beb65209aeb"}, + {file = "mypy-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1b16708a66d38abb1e6b5702f5c2c87e133289da36f6a1d15f6a5221085c6403"}, + {file = "mypy-1.17.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:89e972c0035e9e05823907ad5398c5a73b9f47a002b22359b177d40bdaee7056"}, + {file = "mypy-1.17.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:03b6d0ed2b188e35ee6d5c36b5580cffd6da23319991c49ab5556c023ccf1341"}, + {file = "mypy-1.17.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c837b896b37cd103570d776bda106eabb8737aa6dd4f248451aecf53030cdbeb"}, + {file = "mypy-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:665afab0963a4b39dff7c1fa563cc8b11ecff7910206db4b2e64dd1ba25aed19"}, + {file = "mypy-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:93378d3203a5c0800c6b6d850ad2f19f7a3cdf1a3701d3416dbf128805c6a6a7"}, + {file = "mypy-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:15d54056f7fe7a826d897789f53dd6377ec2ea8ba6f776dc83c2902b899fee81"}, + {file = "mypy-1.17.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:209a58fed9987eccc20f2ca94afe7257a8f46eb5df1fb69958650973230f91e6"}, + {file = "mypy-1.17.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:099b9a5da47de9e2cb5165e581f158e854d9e19d2e96b6698c0d64de911dd849"}, + {file = "mypy-1.17.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa6ffadfbe6994d724c5a1bb6123a7d27dd68fc9c059561cd33b664a79578e14"}, + {file = "mypy-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:9a2b7d9180aed171f033c9f2fc6c204c1245cf60b0cb61cf2e7acc24eea78e0a"}, + {file = "mypy-1.17.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:15a83369400454c41ed3a118e0cc58bd8123921a602f385cb6d6ea5df050c733"}, + {file = "mypy-1.17.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:55b918670f692fc9fba55c3298d8a3beae295c5cded0a55dccdc5bbead814acd"}, + {file = "mypy-1.17.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:62761474061feef6f720149d7ba876122007ddc64adff5ba6f374fda35a018a0"}, + {file = "mypy-1.17.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c49562d3d908fd49ed0938e5423daed8d407774a479b595b143a3d7f87cdae6a"}, + {file = "mypy-1.17.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:397fba5d7616a5bc60b45c7ed204717eaddc38f826e3645402c426057ead9a91"}, + {file = "mypy-1.17.1-cp314-cp314-win_amd64.whl", hash = "sha256:9d6b20b97d373f41617bd0708fd46aa656059af57f2ef72aa8c7d6a2b73b74ed"}, + {file = "mypy-1.17.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5d1092694f166a7e56c805caaf794e0585cabdbf1df36911c414e4e9abb62ae9"}, + {file = "mypy-1.17.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:79d44f9bfb004941ebb0abe8eff6504223a9c1ac51ef967d1263c6572bbebc99"}, + {file = "mypy-1.17.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b01586eed696ec905e61bd2568f48740f7ac4a45b3a468e6423a03d3788a51a8"}, + {file = "mypy-1.17.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43808d9476c36b927fbcd0b0255ce75efe1b68a080154a38ae68a7e62de8f0f8"}, + {file = "mypy-1.17.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:feb8cc32d319edd5859da2cc084493b3e2ce5e49a946377663cc90f6c15fb259"}, + {file = "mypy-1.17.1-cp39-cp39-win_amd64.whl", hash = "sha256:d7598cf74c3e16539d4e2f0b8d8c318e00041553d83d4861f87c7a72e95ac24d"}, + {file = "mypy-1.17.1-py3-none-any.whl", hash = "sha256:a9f52c0351c21fe24c21d8c0eb1f62967b262d6729393397b6f443c3b773c3b9"}, + {file = "mypy-1.17.1.tar.gz", hash = "sha256:25e01ec741ab5bb3eec8ba9cdb0f769230368a22c959c4937360efb89b7e9f01"}, +] + +[package.dependencies] +mypy_extensions = ">=1.0.0" +pathspec = ">=0.9.0" +typing_extensions = ">=4.6.0" + +[package.extras] +dmypy = ["psutil (>=4.0)"] +faster-cache = ["orjson"] +install-types = ["pip"] +mypyc = ["setuptools (>=50)"] +reports = ["lxml"] + +[[package]] +name = "mypy-extensions" +version = "1.1.0" +description = "Type system extensions for programs checked with the mypy type checker." +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505"}, + {file = "mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558"}, +] + +[[package]] +name = "myst-parser" +version = "2.0.0" +description = "An extended [CommonMark](https://spec.commonmark.org/) compliant parser," +optional = false +python-versions = ">=3.8" +groups = ["docs"] +files = [ + {file = "myst_parser-2.0.0-py3-none-any.whl", hash = "sha256:7c36344ae39c8e740dad7fdabf5aa6fc4897a813083c6cc9990044eb93656b14"}, + {file = "myst_parser-2.0.0.tar.gz", hash = "sha256:ea929a67a6a0b1683cdbe19b8d2e724cd7643f8aa3e7bb18dd65beac3483bead"}, +] + +[package.dependencies] +docutils = ">=0.16,<0.21" +jinja2 = "*" +markdown-it-py = ">=3.0,<4.0" +mdit-py-plugins = ">=0.4,<1.0" +pyyaml = "*" +sphinx = ">=6,<8" + +[package.extras] +code-style = ["pre-commit (>=3.0,<4.0)"] +linkify = ["linkify-it-py (>=2.0,<3.0)"] +rtd = ["ipython", "pydata-sphinx-theme (==v0.13.0rc4)", "sphinx-autodoc2 (>=0.4.2,<0.5.0)", "sphinx-book-theme (==1.0.0rc2)", "sphinx-copybutton", "sphinx-design2", "sphinx-pyscript", "sphinx-tippy (>=0.3.1)", "sphinx-togglebutton", "sphinxext-opengraph (>=0.8.2,<0.9.0)", "sphinxext-rediraffe (>=0.2.7,<0.3.0)"] +testing = ["beautifulsoup4", "coverage[toml]", "pytest (>=7,<8)", "pytest-cov", "pytest-param-files (>=0.3.4,<0.4.0)", "pytest-regressions", "sphinx-pytest"] +testing-docutils = ["pygments", "pytest (>=7,<8)", "pytest-param-files (>=0.3.4,<0.4.0)"] + +[[package]] +name = "numpy" +version = "2.3.3" +description = "Fundamental package for array computing in Python" +optional = false +python-versions = ">=3.11" +groups = ["main"] +files = [ + {file = "numpy-2.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0ffc4f5caba7dfcbe944ed674b7eef683c7e94874046454bb79ed7ee0236f59d"}, + {file = "numpy-2.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e7e946c7170858a0295f79a60214424caac2ffdb0063d4d79cb681f9aa0aa569"}, + {file = "numpy-2.3.3-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:cd4260f64bc794c3390a63bf0728220dd1a68170c169088a1e0dfa2fde1be12f"}, + {file = "numpy-2.3.3-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:f0ddb4b96a87b6728df9362135e764eac3cfa674499943ebc44ce96c478ab125"}, + {file = "numpy-2.3.3-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:afd07d377f478344ec6ca2b8d4ca08ae8bd44706763d1efb56397de606393f48"}, + {file = "numpy-2.3.3-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bc92a5dedcc53857249ca51ef29f5e5f2f8c513e22cfb90faeb20343b8c6f7a6"}, + {file = "numpy-2.3.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7af05ed4dc19f308e1d9fc759f36f21921eb7bbfc82843eeec6b2a2863a0aefa"}, + {file = "numpy-2.3.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:433bf137e338677cebdd5beac0199ac84712ad9d630b74eceeb759eaa45ddf30"}, + {file = "numpy-2.3.3-cp311-cp311-win32.whl", hash = "sha256:eb63d443d7b4ffd1e873f8155260d7f58e7e4b095961b01c91062935c2491e57"}, + {file = "numpy-2.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:ec9d249840f6a565f58d8f913bccac2444235025bbb13e9a4681783572ee3caa"}, + {file = "numpy-2.3.3-cp311-cp311-win_arm64.whl", hash = "sha256:74c2a948d02f88c11a3c075d9733f1ae67d97c6bdb97f2bb542f980458b257e7"}, + {file = "numpy-2.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:cfdd09f9c84a1a934cde1eec2267f0a43a7cd44b2cca4ff95b7c0d14d144b0bf"}, + {file = "numpy-2.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:cb32e3cf0f762aee47ad1ddc6672988f7f27045b0783c887190545baba73aa25"}, + {file = "numpy-2.3.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:396b254daeb0a57b1fe0ecb5e3cff6fa79a380fa97c8f7781a6d08cd429418fe"}, + {file = "numpy-2.3.3-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:067e3d7159a5d8f8a0b46ee11148fc35ca9b21f61e3c49fbd0a027450e65a33b"}, + {file = "numpy-2.3.3-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c02d0629d25d426585fb2e45a66154081b9fa677bc92a881ff1d216bc9919a8"}, + {file = "numpy-2.3.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d9192da52b9745f7f0766531dcfa978b7763916f158bb63bdb8a1eca0068ab20"}, + {file = "numpy-2.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:cd7de500a5b66319db419dc3c345244404a164beae0d0937283b907d8152e6ea"}, + {file = "numpy-2.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:93d4962d8f82af58f0b2eb85daaf1b3ca23fe0a85d0be8f1f2b7bb46034e56d7"}, + {file = "numpy-2.3.3-cp312-cp312-win32.whl", hash = "sha256:5534ed6b92f9b7dca6c0a19d6df12d41c68b991cef051d108f6dbff3babc4ebf"}, + {file = "numpy-2.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:497d7cad08e7092dba36e3d296fe4c97708c93daf26643a1ae4b03f6294d30eb"}, + {file = "numpy-2.3.3-cp312-cp312-win_arm64.whl", hash = "sha256:ca0309a18d4dfea6fc6262a66d06c26cfe4640c3926ceec90e57791a82b6eee5"}, + {file = "numpy-2.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f5415fb78995644253370985342cd03572ef8620b934da27d77377a2285955bf"}, + {file = "numpy-2.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d00de139a3324e26ed5b95870ce63be7ec7352171bc69a4cf1f157a48e3eb6b7"}, + {file = "numpy-2.3.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:9dc13c6a5829610cc07422bc74d3ac083bd8323f14e2827d992f9e52e22cd6a6"}, + {file = "numpy-2.3.3-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:d79715d95f1894771eb4e60fb23f065663b2298f7d22945d66877aadf33d00c7"}, + {file = "numpy-2.3.3-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:952cfd0748514ea7c3afc729a0fc639e61655ce4c55ab9acfab14bda4f402b4c"}, + {file = "numpy-2.3.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5b83648633d46f77039c29078751f80da65aa64d5622a3cd62aaef9d835b6c93"}, + {file = "numpy-2.3.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b001bae8cea1c7dfdb2ae2b017ed0a6f2102d7a70059df1e338e307a4c78a8ae"}, + {file = "numpy-2.3.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8e9aced64054739037d42fb84c54dd38b81ee238816c948c8f3ed134665dcd86"}, + {file = "numpy-2.3.3-cp313-cp313-win32.whl", hash = "sha256:9591e1221db3f37751e6442850429b3aabf7026d3b05542d102944ca7f00c8a8"}, + {file = "numpy-2.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:f0dadeb302887f07431910f67a14d57209ed91130be0adea2f9793f1a4f817cf"}, + {file = "numpy-2.3.3-cp313-cp313-win_arm64.whl", hash = "sha256:3c7cf302ac6e0b76a64c4aecf1a09e51abd9b01fc7feee80f6c43e3ab1b1dbc5"}, + {file = "numpy-2.3.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:eda59e44957d272846bb407aad19f89dc6f58fecf3504bd144f4c5cf81a7eacc"}, + {file = "numpy-2.3.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:823d04112bc85ef5c4fda73ba24e6096c8f869931405a80aa8b0e604510a26bc"}, + {file = "numpy-2.3.3-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:40051003e03db4041aa325da2a0971ba41cf65714e65d296397cc0e32de6018b"}, + {file = "numpy-2.3.3-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:6ee9086235dd6ab7ae75aba5662f582a81ced49f0f1c6de4260a78d8f2d91a19"}, + {file = "numpy-2.3.3-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:94fcaa68757c3e2e668ddadeaa86ab05499a70725811e582b6a9858dd472fb30"}, + {file = "numpy-2.3.3-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:da1a74b90e7483d6ce5244053399a614b1d6b7bc30a60d2f570e5071f8959d3e"}, + {file = "numpy-2.3.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2990adf06d1ecee3b3dcbb4977dfab6e9f09807598d647f04d385d29e7a3c3d3"}, + {file = "numpy-2.3.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:ed635ff692483b8e3f0fcaa8e7eb8a75ee71aa6d975388224f70821421800cea"}, + {file = "numpy-2.3.3-cp313-cp313t-win32.whl", hash = "sha256:a333b4ed33d8dc2b373cc955ca57babc00cd6f9009991d9edc5ddbc1bac36bcd"}, + {file = "numpy-2.3.3-cp313-cp313t-win_amd64.whl", hash = "sha256:4384a169c4d8f97195980815d6fcad04933a7e1ab3b530921c3fef7a1c63426d"}, + {file = "numpy-2.3.3-cp313-cp313t-win_arm64.whl", hash = "sha256:75370986cc0bc66f4ce5110ad35aae6d182cc4ce6433c40ad151f53690130bf1"}, + {file = "numpy-2.3.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:cd052f1fa6a78dee696b58a914b7229ecfa41f0a6d96dc663c1220a55e137593"}, + {file = "numpy-2.3.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:414a97499480067d305fcac9716c29cf4d0d76db6ebf0bf3cbce666677f12652"}, + {file = "numpy-2.3.3-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:50a5fe69f135f88a2be9b6ca0481a68a136f6febe1916e4920e12f1a34e708a7"}, + {file = "numpy-2.3.3-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:b912f2ed2b67a129e6a601e9d93d4fa37bef67e54cac442a2f588a54afe5c67a"}, + {file = "numpy-2.3.3-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9e318ee0596d76d4cb3d78535dc005fa60e5ea348cd131a51e99d0bdbe0b54fe"}, + {file = "numpy-2.3.3-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ce020080e4a52426202bdb6f7691c65bb55e49f261f31a8f506c9f6bc7450421"}, + {file = "numpy-2.3.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e6687dc183aa55dae4a705b35f9c0f8cb178bcaa2f029b241ac5356221d5c021"}, + {file = "numpy-2.3.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d8f3b1080782469fdc1718c4ed1d22549b5fb12af0d57d35e992158a772a37cf"}, + {file = "numpy-2.3.3-cp314-cp314-win32.whl", hash = "sha256:cb248499b0bc3be66ebd6578b83e5acacf1d6cb2a77f2248ce0e40fbec5a76d0"}, + {file = "numpy-2.3.3-cp314-cp314-win_amd64.whl", hash = "sha256:691808c2b26b0f002a032c73255d0bd89751425f379f7bcd22d140db593a96e8"}, + {file = "numpy-2.3.3-cp314-cp314-win_arm64.whl", hash = "sha256:9ad12e976ca7b10f1774b03615a2a4bab8addce37ecc77394d8e986927dc0dfe"}, + {file = "numpy-2.3.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9cc48e09feb11e1db00b320e9d30a4151f7369afb96bd0e48d942d09da3a0d00"}, + {file = "numpy-2.3.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:901bf6123879b7f251d3631967fd574690734236075082078e0571977c6a8e6a"}, + {file = "numpy-2.3.3-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:7f025652034199c301049296b59fa7d52c7e625017cae4c75d8662e377bf487d"}, + {file = "numpy-2.3.3-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:533ca5f6d325c80b6007d4d7fb1984c303553534191024ec6a524a4c92a5935a"}, + {file = "numpy-2.3.3-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0edd58682a399824633b66885d699d7de982800053acf20be1eaa46d92009c54"}, + {file = "numpy-2.3.3-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:367ad5d8fbec5d9296d18478804a530f1191e24ab4d75ab408346ae88045d25e"}, + {file = "numpy-2.3.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8f6ac61a217437946a1fa48d24c47c91a0c4f725237871117dea264982128097"}, + {file = "numpy-2.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:179a42101b845a816d464b6fe9a845dfaf308fdfc7925387195570789bb2c970"}, + {file = "numpy-2.3.3-cp314-cp314t-win32.whl", hash = "sha256:1250c5d3d2562ec4174bce2e3a1523041595f9b651065e4a4473f5f48a6bc8a5"}, + {file = "numpy-2.3.3-cp314-cp314t-win_amd64.whl", hash = "sha256:b37a0b2e5935409daebe82c1e42274d30d9dd355852529eab91dab8dcca7419f"}, + {file = "numpy-2.3.3-cp314-cp314t-win_arm64.whl", hash = "sha256:78c9f6560dc7e6b3990e32df7ea1a50bbd0e2a111e05209963f5ddcab7073b0b"}, + {file = "numpy-2.3.3-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:1e02c7159791cd481e1e6d5ddd766b62a4d5acf8df4d4d1afe35ee9c5c33a41e"}, + {file = "numpy-2.3.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:dca2d0fc80b3893ae72197b39f69d55a3cd8b17ea1b50aa4c62de82419936150"}, + {file = "numpy-2.3.3-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:99683cbe0658f8271b333a1b1b4bb3173750ad59c0c61f5bbdc5b318918fffe3"}, + {file = "numpy-2.3.3-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:d9d537a39cc9de668e5cd0e25affb17aec17b577c6b3ae8a3d866b479fbe88d0"}, + {file = "numpy-2.3.3-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8596ba2f8af5f93b01d97563832686d20206d303024777f6dfc2e7c7c3f1850e"}, + {file = "numpy-2.3.3-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e1ec5615b05369925bd1125f27df33f3b6c8bc10d788d5999ecd8769a1fa04db"}, + {file = "numpy-2.3.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:2e267c7da5bf7309670523896df97f93f6e469fb931161f483cd6882b3b1a5dc"}, + {file = "numpy-2.3.3.tar.gz", hash = "sha256:ddc7c39727ba62b80dfdbedf400d1c10ddfa8eefbd7ec8dcb118be8b56d31029"}, +] + +[[package]] +name = "packaging" +version = "25.0" +description = "Core utilities for Python packages" +optional = false +python-versions = ">=3.8" +groups = ["main", "dev", "docs"] +files = [ + {file = "packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484"}, + {file = "packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f"}, +] + +[[package]] +name = "pandas" +version = "2.3.2" +description = "Powerful data structures for data analysis, time series, and statistics" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "pandas-2.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:52bc29a946304c360561974c6542d1dd628ddafa69134a7131fdfd6a5d7a1a35"}, + {file = "pandas-2.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:220cc5c35ffaa764dd5bb17cf42df283b5cb7fdf49e10a7b053a06c9cb48ee2b"}, + {file = "pandas-2.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42c05e15111221384019897df20c6fe893b2f697d03c811ee67ec9e0bb5a3424"}, + {file = "pandas-2.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc03acc273c5515ab69f898df99d9d4f12c4d70dbfc24c3acc6203751d0804cf"}, + {file = "pandas-2.3.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d25c20a03e8870f6339bcf67281b946bd20b86f1a544ebbebb87e66a8d642cba"}, + {file = "pandas-2.3.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:21bb612d148bb5860b7eb2c10faacf1a810799245afd342cf297d7551513fbb6"}, + {file = "pandas-2.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:b62d586eb25cb8cb70a5746a378fc3194cb7f11ea77170d59f889f5dfe3cec7a"}, + {file = "pandas-2.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1333e9c299adcbb68ee89a9bb568fc3f20f9cbb419f1dd5225071e6cddb2a743"}, + {file = "pandas-2.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:76972bcbd7de8e91ad5f0ca884a9f2c477a2125354af624e022c49e5bd0dfff4"}, + {file = "pandas-2.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b98bdd7c456a05eef7cd21fd6b29e3ca243591fe531c62be94a2cc987efb5ac2"}, + {file = "pandas-2.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1d81573b3f7db40d020983f78721e9bfc425f411e616ef019a10ebf597aedb2e"}, + {file = "pandas-2.3.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e190b738675a73b581736cc8ec71ae113d6c3768d0bd18bffa5b9a0927b0b6ea"}, + {file = "pandas-2.3.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c253828cb08f47488d60f43c5fc95114c771bbfff085da54bfc79cb4f9e3a372"}, + {file = "pandas-2.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:9467697b8083f9667b212633ad6aa4ab32436dcbaf4cd57325debb0ddef2012f"}, + {file = "pandas-2.3.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3fbb977f802156e7a3f829e9d1d5398f6192375a3e2d1a9ee0803e35fe70a2b9"}, + {file = "pandas-2.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1b9b52693123dd234b7c985c68b709b0b009f4521000d0525f2b95c22f15944b"}, + {file = "pandas-2.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0bd281310d4f412733f319a5bc552f86d62cddc5f51d2e392c8787335c994175"}, + {file = "pandas-2.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:96d31a6b4354e3b9b8a2c848af75d31da390657e3ac6f30c05c82068b9ed79b9"}, + {file = "pandas-2.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:df4df0b9d02bb873a106971bb85d448378ef14b86ba96f035f50bbd3688456b4"}, + {file = "pandas-2.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:213a5adf93d020b74327cb2c1b842884dbdd37f895f42dcc2f09d451d949f811"}, + {file = "pandas-2.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:8c13b81a9347eb8c7548f53fd9a4f08d4dfe996836543f805c987bafa03317ae"}, + {file = "pandas-2.3.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0c6ecbac99a354a051ef21c5307601093cb9e0f4b1855984a084bfec9302699e"}, + {file = "pandas-2.3.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c6f048aa0fd080d6a06cc7e7537c09b53be6642d330ac6f54a600c3ace857ee9"}, + {file = "pandas-2.3.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0064187b80a5be6f2f9c9d6bdde29372468751dfa89f4211a3c5871854cfbf7a"}, + {file = "pandas-2.3.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4ac8c320bded4718b298281339c1a50fb00a6ba78cb2a63521c39bec95b0209b"}, + {file = "pandas-2.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:114c2fe4f4328cf98ce5716d1532f3ab79c5919f95a9cfee81d9140064a2e4d6"}, + {file = "pandas-2.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:48fa91c4dfb3b2b9bfdb5c24cd3567575f4e13f9636810462ffed8925352be5a"}, + {file = "pandas-2.3.2-cp313-cp313-win_amd64.whl", hash = "sha256:12d039facec710f7ba305786837d0225a3444af7bbd9c15c32ca2d40d157ed8b"}, + {file = "pandas-2.3.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:c624b615ce97864eb588779ed4046186f967374185c047070545253a52ab2d57"}, + {file = "pandas-2.3.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:0cee69d583b9b128823d9514171cabb6861e09409af805b54459bd0c821a35c2"}, + {file = "pandas-2.3.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2319656ed81124982900b4c37f0e0c58c015af9a7bbc62342ba5ad07ace82ba9"}, + {file = "pandas-2.3.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b37205ad6f00d52f16b6d09f406434ba928c1a1966e2771006a9033c736d30d2"}, + {file = "pandas-2.3.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:837248b4fc3a9b83b9c6214699a13f069dc13510a6a6d7f9ba33145d2841a012"}, + {file = "pandas-2.3.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:d2c3554bd31b731cd6490d94a28f3abb8dd770634a9e06eb6d2911b9827db370"}, + {file = "pandas-2.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:88080a0ff8a55eac9c84e3ff3c7665b3b5476c6fbc484775ca1910ce1c3e0b87"}, + {file = "pandas-2.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d4a558c7620340a0931828d8065688b3cc5b4c8eb674bcaf33d18ff4a6870b4a"}, + {file = "pandas-2.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45178cf09d1858a1509dc73ec261bf5b25a625a389b65be2e47b559905f0ab6a"}, + {file = "pandas-2.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:77cefe00e1b210f9c76c697fedd8fdb8d3dd86563e9c8adc9fa72b90f5e9e4c2"}, + {file = "pandas-2.3.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:13bd629c653856f00c53dc495191baa59bcafbbf54860a46ecc50d3a88421a96"}, + {file = "pandas-2.3.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:36d627906fd44b5fd63c943264e11e96e923f8de77d6016dc2f667b9ad193438"}, + {file = "pandas-2.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:a9d7ec92d71a420185dec44909c32e9a362248c4ae2238234b76d5be37f208cc"}, + {file = "pandas-2.3.2.tar.gz", hash = "sha256:ab7b58f8f82706890924ccdfb5f48002b83d2b5a3845976a9fb705d36c34dcdb"}, +] + +[package.dependencies] +numpy = [ + {version = ">=1.26.0", markers = "python_version >= \"3.12\""}, + {version = ">=1.23.2", markers = "python_version == \"3.11\""}, +] +python-dateutil = ">=2.8.2" +pytz = ">=2020.1" +tzdata = ">=2022.7" + +[package.extras] +all = ["PyQt5 (>=5.15.9)", "SQLAlchemy (>=2.0.0)", "adbc-driver-postgresql (>=0.8.0)", "adbc-driver-sqlite (>=0.8.0)", "beautifulsoup4 (>=4.11.2)", "bottleneck (>=1.3.6)", "dataframe-api-compat (>=0.1.7)", "fastparquet (>=2022.12.0)", "fsspec (>=2022.11.0)", "gcsfs (>=2022.11.0)", "html5lib (>=1.1)", "hypothesis (>=6.46.1)", "jinja2 (>=3.1.2)", "lxml (>=4.9.2)", "matplotlib (>=3.6.3)", "numba (>=0.56.4)", "numexpr (>=2.8.4)", "odfpy (>=1.4.1)", "openpyxl (>=3.1.0)", "pandas-gbq (>=0.19.0)", "psycopg2 (>=2.9.6)", "pyarrow (>=10.0.1)", "pymysql (>=1.0.2)", "pyreadstat (>=1.2.0)", "pytest (>=7.3.2)", "pytest-xdist (>=2.2.0)", "python-calamine (>=0.1.7)", "pyxlsb (>=1.0.10)", "qtpy (>=2.3.0)", "s3fs (>=2022.11.0)", "scipy (>=1.10.0)", "tables (>=3.8.0)", "tabulate (>=0.9.0)", "xarray (>=2022.12.0)", "xlrd (>=2.0.1)", "xlsxwriter (>=3.0.5)", "zstandard (>=0.19.0)"] +aws = ["s3fs (>=2022.11.0)"] +clipboard = ["PyQt5 (>=5.15.9)", "qtpy (>=2.3.0)"] +compression = ["zstandard (>=0.19.0)"] +computation = ["scipy (>=1.10.0)", "xarray (>=2022.12.0)"] +consortium-standard = ["dataframe-api-compat (>=0.1.7)"] +excel = ["odfpy (>=1.4.1)", "openpyxl (>=3.1.0)", "python-calamine (>=0.1.7)", "pyxlsb (>=1.0.10)", "xlrd (>=2.0.1)", "xlsxwriter (>=3.0.5)"] +feather = ["pyarrow (>=10.0.1)"] +fss = ["fsspec (>=2022.11.0)"] +gcp = ["gcsfs (>=2022.11.0)", "pandas-gbq (>=0.19.0)"] +hdf5 = ["tables (>=3.8.0)"] +html = ["beautifulsoup4 (>=4.11.2)", "html5lib (>=1.1)", "lxml (>=4.9.2)"] +mysql = ["SQLAlchemy (>=2.0.0)", "pymysql (>=1.0.2)"] +output-formatting = ["jinja2 (>=3.1.2)", "tabulate (>=0.9.0)"] +parquet = ["pyarrow (>=10.0.1)"] +performance = ["bottleneck (>=1.3.6)", "numba (>=0.56.4)", "numexpr (>=2.8.4)"] +plot = ["matplotlib (>=3.6.3)"] +postgresql = ["SQLAlchemy (>=2.0.0)", "adbc-driver-postgresql (>=0.8.0)", "psycopg2 (>=2.9.6)"] +pyarrow = ["pyarrow (>=10.0.1)"] +spss = ["pyreadstat (>=1.2.0)"] +sql-other = ["SQLAlchemy (>=2.0.0)", "adbc-driver-postgresql (>=0.8.0)", "adbc-driver-sqlite (>=0.8.0)"] +test = ["hypothesis (>=6.46.1)", "pytest (>=7.3.2)", "pytest-xdist (>=2.2.0)"] +xml = ["lxml (>=4.9.2)"] + +[[package]] +name = "passlib" +version = "1.7.4" +description = "comprehensive password hashing framework supporting over 30 schemes" +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "passlib-1.7.4-py2.py3-none-any.whl", hash = "sha256:aa6bca462b8d8bda89c70b382f0c298a20b5560af6cbfa2dce410c0a2fb669f1"}, + {file = "passlib-1.7.4.tar.gz", hash = "sha256:defd50f72b65c5402ab2c573830a6978e5f202ad0d984793c8dde2c4152ebe04"}, +] + +[package.dependencies] +bcrypt = {version = ">=3.1.0", optional = true, markers = "extra == \"bcrypt\""} + +[package.extras] +argon2 = ["argon2-cffi (>=18.2.0)"] +bcrypt = ["bcrypt (>=3.1.0)"] +build-docs = ["cloud-sptheme (>=1.10.1)", "sphinx (>=1.6)", "sphinxcontrib-fulltoc (>=1.2.0)"] +totp = ["cryptography"] + +[[package]] +name = "pathspec" +version = "0.12.1" +description = "Utility library for gitignore style pattern matching of file paths." +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, + {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, +] + +[[package]] +name = "pbr" +version = "7.0.0" +description = "Python Build Reasonableness" +optional = false +python-versions = ">=2.6" +groups = ["dev"] +files = [ + {file = "pbr-7.0.0-py2.py3-none-any.whl", hash = "sha256:b447e63a2bc04fd975fc0480b8d5ebf979179e2c0ae203bf1eff9ea20073bc38"}, + {file = "pbr-7.0.0.tar.gz", hash = "sha256:cf4127298723dafbce3afd13775ccf3885be5d3c8435751b867f9a6a10b71a39"}, +] + +[package.dependencies] +setuptools = "*" + +[[package]] +name = "platformdirs" +version = "4.3.8" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "platformdirs-4.3.8-py3-none-any.whl", hash = "sha256:ff7059bb7eb1179e2685604f4aaf157cfd9535242bd23742eadc3c13542139b4"}, + {file = "platformdirs-4.3.8.tar.gz", hash = "sha256:3d512d96e16bcb959a814c9f348431070822a6496326a4be0911c40b5a74c2bc"}, +] + +[package.extras] +docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.1.3)", "sphinx-autodoc-typehints (>=3)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.4)", "pytest-cov (>=6)", "pytest-mock (>=3.14)"] +type = ["mypy (>=1.14.1)"] + +[[package]] +name = "pluggy" +version = "1.6.0" +description = "plugin and hook calling mechanisms for python" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746"}, + {file = "pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3"}, +] + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["coverage", "pytest", "pytest-benchmark"] + +[[package]] +name = "pockets" +version = "0.9.1" +description = "A collection of helpful Python tools!" +optional = false +python-versions = "*" +groups = ["docs"] +files = [ + {file = "pockets-0.9.1-py2.py3-none-any.whl", hash = "sha256:68597934193c08a08eb2bf6a1d85593f627c22f9b065cc727a4f03f669d96d86"}, + {file = "pockets-0.9.1.tar.gz", hash = "sha256:9320f1a3c6f7a9133fe3b571f283bcf3353cd70249025ae8d618e40e9f7e92b3"}, +] + +[package.dependencies] +six = ">=1.5.2" + +[[package]] +name = "port-for" +version = "0.7.4" +description = "Utility that helps with local TCP ports management. It can find an unused TCP localhost port and remember the association." +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "port_for-0.7.4-py3-none-any.whl", hash = "sha256:08404aa072651a53dcefe8d7a598ee8a1dca320d9ac44ac464da16ccf2a02c4a"}, + {file = "port_for-0.7.4.tar.gz", hash = "sha256:fc7713e7b22f89442f335ce12536653656e8f35146739eccaeff43d28436028d"}, +] + +[[package]] +name = "psutil" +version = "7.0.0" +description = "Cross-platform lib for process and system monitoring in Python. NOTE: the syntax of this script MUST be kept compatible with Python 2.7." +optional = false +python-versions = ">=3.6" +groups = ["dev"] +markers = "sys_platform != \"cygwin\"" +files = [ + {file = "psutil-7.0.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:101d71dc322e3cffd7cea0650b09b3d08b8e7c4109dd6809fe452dfd00e58b25"}, + {file = "psutil-7.0.0-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:39db632f6bb862eeccf56660871433e111b6ea58f2caea825571951d4b6aa3da"}, + {file = "psutil-7.0.0-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1fcee592b4c6f146991ca55919ea3d1f8926497a713ed7faaf8225e174581e91"}, + {file = "psutil-7.0.0-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b1388a4f6875d7e2aff5c4ca1cc16c545ed41dd8bb596cefea80111db353a34"}, + {file = "psutil-7.0.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5f098451abc2828f7dc6b58d44b532b22f2088f4999a937557b603ce72b1993"}, + {file = "psutil-7.0.0-cp36-cp36m-win32.whl", hash = "sha256:84df4eb63e16849689f76b1ffcb36db7b8de703d1bc1fe41773db487621b6c17"}, + {file = "psutil-7.0.0-cp36-cp36m-win_amd64.whl", hash = "sha256:1e744154a6580bc968a0195fd25e80432d3afec619daf145b9e5ba16cc1d688e"}, + {file = "psutil-7.0.0-cp37-abi3-win32.whl", hash = "sha256:ba3fcef7523064a6c9da440fc4d6bd07da93ac726b5733c29027d7dc95b39d99"}, + {file = "psutil-7.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:4cf3d4eb1aa9b348dec30105c55cd9b7d4629285735a102beb4441e38db90553"}, + {file = "psutil-7.0.0.tar.gz", hash = "sha256:7be9c3eba38beccb6495ea33afd982a44074b78f28c434a1f51cc07fd315c456"}, +] + +[package.extras] +dev = ["abi3audit", "black (==24.10.0)", "check-manifest", "coverage", "packaging", "pylint", "pyperf", "pypinfo", "pytest", "pytest-cov", "pytest-xdist", "requests", "rstcheck", "ruff", "setuptools", "sphinx", "sphinx_rtd_theme", "toml-sort", "twine", "virtualenv", "vulture", "wheel"] +test = ["pytest", "pytest-xdist", "setuptools"] + +[[package]] +name = "psycopg" +version = "3.2.9" +description = "PostgreSQL database adapter for Python" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "psycopg-3.2.9-py3-none-any.whl", hash = "sha256:01a8dadccdaac2123c916208c96e06631641c0566b22005493f09663c7a8d3b6"}, + {file = "psycopg-3.2.9.tar.gz", hash = "sha256:2fbb46fcd17bc81f993f28c47f1ebea38d66ae97cc2dbc3cad73b37cefbff700"}, +] + +[package.dependencies] +typing-extensions = {version = ">=4.6", markers = "python_version < \"3.13\""} +tzdata = {version = "*", markers = "sys_platform == \"win32\""} + +[package.extras] +binary = ["psycopg-binary (==3.2.9) ; implementation_name != \"pypy\""] +c = ["psycopg-c (==3.2.9) ; implementation_name != \"pypy\""] +dev = ["ast-comments (>=1.1.2)", "black (>=24.1.0)", "codespell (>=2.2)", "dnspython (>=2.1)", "flake8 (>=4.0)", "isort-psycopg", "isort[colors] (>=6.0)", "mypy (>=1.14)", "pre-commit (>=4.0.1)", "types-setuptools (>=57.4)", "types-shapely (>=2.0)", "wheel (>=0.37)"] +docs = ["Sphinx (>=5.0)", "furo (==2022.6.21)", "sphinx-autobuild (>=2021.3.14)", "sphinx-autodoc-typehints (>=1.12)"] +pool = ["psycopg-pool"] +test = ["anyio (>=4.0)", "mypy (>=1.14)", "pproxy (>=2.7)", "pytest (>=6.2.5)", "pytest-cov (>=3.0)", "pytest-randomly (>=3.5)"] + +[[package]] +name = "psycopg2-binary" +version = "2.9.10" +description = "psycopg2 - Python-PostgreSQL Database Adapter" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "psycopg2-binary-2.9.10.tar.gz", hash = "sha256:4b3df0e6990aa98acda57d983942eff13d824135fe2250e6522edaa782a06de2"}, + {file = "psycopg2_binary-2.9.10-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:0ea8e3d0ae83564f2fc554955d327fa081d065c8ca5cc6d2abb643e2c9c1200f"}, + {file = "psycopg2_binary-2.9.10-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:3e9c76f0ac6f92ecfc79516a8034a544926430f7b080ec5a0537bca389ee0906"}, + {file = "psycopg2_binary-2.9.10-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2ad26b467a405c798aaa1458ba09d7e2b6e5f96b1ce0ac15d82fd9f95dc38a92"}, + {file = "psycopg2_binary-2.9.10-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:270934a475a0e4b6925b5f804e3809dd5f90f8613621d062848dd82f9cd62007"}, + {file = "psycopg2_binary-2.9.10-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:48b338f08d93e7be4ab2b5f1dbe69dc5e9ef07170fe1f86514422076d9c010d0"}, + {file = "psycopg2_binary-2.9.10-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f4152f8f76d2023aac16285576a9ecd2b11a9895373a1f10fd9db54b3ff06b4"}, + {file = "psycopg2_binary-2.9.10-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:32581b3020c72d7a421009ee1c6bf4a131ef5f0a968fab2e2de0c9d2bb4577f1"}, + {file = "psycopg2_binary-2.9.10-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:2ce3e21dc3437b1d960521eca599d57408a695a0d3c26797ea0f72e834c7ffe5"}, + {file = "psycopg2_binary-2.9.10-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:e984839e75e0b60cfe75e351db53d6db750b00de45644c5d1f7ee5d1f34a1ce5"}, + {file = "psycopg2_binary-2.9.10-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3c4745a90b78e51d9ba06e2088a2fe0c693ae19cc8cb051ccda44e8df8a6eb53"}, + {file = "psycopg2_binary-2.9.10-cp310-cp310-win32.whl", hash = "sha256:e5720a5d25e3b99cd0dc5c8a440570469ff82659bb09431c1439b92caf184d3b"}, + {file = "psycopg2_binary-2.9.10-cp310-cp310-win_amd64.whl", hash = "sha256:3c18f74eb4386bf35e92ab2354a12c17e5eb4d9798e4c0ad3a00783eae7cd9f1"}, + {file = "psycopg2_binary-2.9.10-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:04392983d0bb89a8717772a193cfaac58871321e3ec69514e1c4e0d4957b5aff"}, + {file = "psycopg2_binary-2.9.10-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:1a6784f0ce3fec4edc64e985865c17778514325074adf5ad8f80636cd029ef7c"}, + {file = "psycopg2_binary-2.9.10-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b5f86c56eeb91dc3135b3fd8a95dc7ae14c538a2f3ad77a19645cf55bab1799c"}, + {file = "psycopg2_binary-2.9.10-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2b3d2491d4d78b6b14f76881905c7a8a8abcf974aad4a8a0b065273a0ed7a2cb"}, + {file = "psycopg2_binary-2.9.10-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2286791ececda3a723d1910441c793be44625d86d1a4e79942751197f4d30341"}, + {file = "psycopg2_binary-2.9.10-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:512d29bb12608891e349af6a0cccedce51677725a921c07dba6342beaf576f9a"}, + {file = "psycopg2_binary-2.9.10-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5a507320c58903967ef7384355a4da7ff3f28132d679aeb23572753cbf2ec10b"}, + {file = "psycopg2_binary-2.9.10-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:6d4fa1079cab9018f4d0bd2db307beaa612b0d13ba73b5c6304b9fe2fb441ff7"}, + {file = "psycopg2_binary-2.9.10-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:851485a42dbb0bdc1edcdabdb8557c09c9655dfa2ca0460ff210522e073e319e"}, + {file = "psycopg2_binary-2.9.10-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:35958ec9e46432d9076286dda67942ed6d968b9c3a6a2fd62b48939d1d78bf68"}, + {file = "psycopg2_binary-2.9.10-cp311-cp311-win32.whl", hash = "sha256:ecced182e935529727401b24d76634a357c71c9275b356efafd8a2a91ec07392"}, + {file = "psycopg2_binary-2.9.10-cp311-cp311-win_amd64.whl", hash = "sha256:ee0e8c683a7ff25d23b55b11161c2663d4b099770f6085ff0a20d4505778d6b4"}, + {file = "psycopg2_binary-2.9.10-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:880845dfe1f85d9d5f7c412efea7a08946a46894537e4e5d091732eb1d34d9a0"}, + {file = "psycopg2_binary-2.9.10-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:9440fa522a79356aaa482aa4ba500b65f28e5d0e63b801abf6aa152a29bd842a"}, + {file = "psycopg2_binary-2.9.10-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e3923c1d9870c49a2d44f795df0c889a22380d36ef92440ff618ec315757e539"}, + {file = "psycopg2_binary-2.9.10-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7b2c956c028ea5de47ff3a8d6b3cc3330ab45cf0b7c3da35a2d6ff8420896526"}, + {file = "psycopg2_binary-2.9.10-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f758ed67cab30b9a8d2833609513ce4d3bd027641673d4ebc9c067e4d208eec1"}, + {file = "psycopg2_binary-2.9.10-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8cd9b4f2cfab88ed4a9106192de509464b75a906462fb846b936eabe45c2063e"}, + {file = "psycopg2_binary-2.9.10-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6dc08420625b5a20b53551c50deae6e231e6371194fa0651dbe0fb206452ae1f"}, + {file = "psycopg2_binary-2.9.10-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:d7cd730dfa7c36dbe8724426bf5612798734bff2d3c3857f36f2733f5bfc7c00"}, + {file = "psycopg2_binary-2.9.10-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:155e69561d54d02b3c3209545fb08938e27889ff5a10c19de8d23eb5a41be8a5"}, + {file = "psycopg2_binary-2.9.10-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c3cc28a6fd5a4a26224007712e79b81dbaee2ffb90ff406256158ec4d7b52b47"}, + {file = "psycopg2_binary-2.9.10-cp312-cp312-win32.whl", hash = "sha256:ec8a77f521a17506a24a5f626cb2aee7850f9b69a0afe704586f63a464f3cd64"}, + {file = "psycopg2_binary-2.9.10-cp312-cp312-win_amd64.whl", hash = "sha256:18c5ee682b9c6dd3696dad6e54cc7ff3a1a9020df6a5c0f861ef8bfd338c3ca0"}, + {file = "psycopg2_binary-2.9.10-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:26540d4a9a4e2b096f1ff9cce51253d0504dca5a85872c7f7be23be5a53eb18d"}, + {file = "psycopg2_binary-2.9.10-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:e217ce4d37667df0bc1c397fdcd8de5e81018ef305aed9415c3b093faaeb10fb"}, + {file = "psycopg2_binary-2.9.10-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:245159e7ab20a71d989da00f280ca57da7641fa2cdcf71749c193cea540a74f7"}, + {file = "psycopg2_binary-2.9.10-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c4ded1a24b20021ebe677b7b08ad10bf09aac197d6943bfe6fec70ac4e4690d"}, + {file = "psycopg2_binary-2.9.10-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3abb691ff9e57d4a93355f60d4f4c1dd2d68326c968e7db17ea96df3c023ef73"}, + {file = "psycopg2_binary-2.9.10-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8608c078134f0b3cbd9f89b34bd60a943b23fd33cc5f065e8d5f840061bd0673"}, + {file = "psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:230eeae2d71594103cd5b93fd29d1ace6420d0b86f4778739cb1a5a32f607d1f"}, + {file = "psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:bb89f0a835bcfc1d42ccd5f41f04870c1b936d8507c6df12b7737febc40f0909"}, + {file = "psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f0c2d907a1e102526dd2986df638343388b94c33860ff3bbe1384130828714b1"}, + {file = "psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f8157bed2f51db683f31306aa497311b560f2265998122abe1dce6428bd86567"}, + {file = "psycopg2_binary-2.9.10-cp313-cp313-win_amd64.whl", hash = "sha256:27422aa5f11fbcd9b18da48373eb67081243662f9b46e6fd07c3eb46e4535142"}, + {file = "psycopg2_binary-2.9.10-cp38-cp38-macosx_12_0_x86_64.whl", hash = "sha256:eb09aa7f9cecb45027683bb55aebaaf45a0df8bf6de68801a6afdc7947bb09d4"}, + {file = "psycopg2_binary-2.9.10-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b73d6d7f0ccdad7bc43e6d34273f70d587ef62f824d7261c4ae9b8b1b6af90e8"}, + {file = "psycopg2_binary-2.9.10-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ce5ab4bf46a211a8e924d307c1b1fcda82368586a19d0a24f8ae166f5c784864"}, + {file = "psycopg2_binary-2.9.10-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:056470c3dc57904bbf63d6f534988bafc4e970ffd50f6271fc4ee7daad9498a5"}, + {file = "psycopg2_binary-2.9.10-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:73aa0e31fa4bb82578f3a6c74a73c273367727de397a7a0f07bd83cbea696baa"}, + {file = "psycopg2_binary-2.9.10-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:8de718c0e1c4b982a54b41779667242bc630b2197948405b7bd8ce16bcecac92"}, + {file = "psycopg2_binary-2.9.10-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:5c370b1e4975df846b0277b4deba86419ca77dbc25047f535b0bb03d1a544d44"}, + {file = "psycopg2_binary-2.9.10-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:ffe8ed017e4ed70f68b7b371d84b7d4a790368db9203dfc2d222febd3a9c8863"}, + {file = "psycopg2_binary-2.9.10-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:8aecc5e80c63f7459a1a2ab2c64df952051df196294d9f739933a9f6687e86b3"}, + {file = "psycopg2_binary-2.9.10-cp39-cp39-macosx_12_0_x86_64.whl", hash = "sha256:7a813c8bdbaaaab1f078014b9b0b13f5de757e2b5d9be6403639b298a04d218b"}, + {file = "psycopg2_binary-2.9.10-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d00924255d7fc916ef66e4bf22f354a940c67179ad3fd7067d7a0a9c84d2fbfc"}, + {file = "psycopg2_binary-2.9.10-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7559bce4b505762d737172556a4e6ea8a9998ecac1e39b5233465093e8cee697"}, + {file = "psycopg2_binary-2.9.10-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e8b58f0a96e7a1e341fc894f62c1177a7c83febebb5ff9123b579418fdc8a481"}, + {file = "psycopg2_binary-2.9.10-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b269105e59ac96aba877c1707c600ae55711d9dcd3fc4b5012e4af68e30c648"}, + {file = "psycopg2_binary-2.9.10-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:79625966e176dc97ddabc142351e0409e28acf4660b88d1cf6adb876d20c490d"}, + {file = "psycopg2_binary-2.9.10-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:8aabf1c1a04584c168984ac678a668094d831f152859d06e055288fa515e4d30"}, + {file = "psycopg2_binary-2.9.10-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:19721ac03892001ee8fdd11507e6a2e01f4e37014def96379411ca99d78aeb2c"}, + {file = "psycopg2_binary-2.9.10-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7f5d859928e635fa3ce3477704acee0f667b3a3d3e4bb109f2b18d4005f38287"}, + {file = "psycopg2_binary-2.9.10-cp39-cp39-win32.whl", hash = "sha256:3216ccf953b3f267691c90c6fe742e45d890d8272326b4a8b20850a03d05b7b8"}, + {file = "psycopg2_binary-2.9.10-cp39-cp39-win_amd64.whl", hash = "sha256:30e34c4e97964805f715206c7b789d54a78b70f3ff19fbe590104b71c45600e5"}, +] + +[[package]] +name = "pycodestyle" +version = "2.11.1" +description = "Python style guide checker" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "pycodestyle-2.11.1-py2.py3-none-any.whl", hash = "sha256:44fe31000b2d866f2e41841b18528a505fbd7fef9017b04eff4e2648a0fadc67"}, + {file = "pycodestyle-2.11.1.tar.gz", hash = "sha256:41ba0e7afc9752dfb53ced5489e89f8186be00e599e712660695b7a75ff2663f"}, +] + +[[package]] +name = "pycparser" +version = "2.23" +description = "C parser in Python" +optional = false +python-versions = ">=3.8" +groups = ["main"] +markers = "platform_python_implementation != \"PyPy\" and implementation_name != \"PyPy\"" +files = [ + {file = "pycparser-2.23-py3-none-any.whl", hash = "sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934"}, + {file = "pycparser-2.23.tar.gz", hash = "sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2"}, +] + +[[package]] +name = "pydantic" +version = "2.5.3" +description = "Data validation using Python type hints" +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "pydantic-2.5.3-py3-none-any.whl", hash = "sha256:d0caf5954bee831b6bfe7e338c32b9e30c85dfe080c843680783ac2b631673b4"}, + {file = "pydantic-2.5.3.tar.gz", hash = "sha256:b3ef57c62535b0941697cce638c08900d87fcb67e29cfa99e8a68f747f393f7a"}, +] + +[package.dependencies] +annotated-types = ">=0.4.0" +pydantic-core = "2.14.6" +typing-extensions = ">=4.6.1" + +[package.extras] +email = ["email-validator (>=2.0.0)"] + +[[package]] +name = "pydantic-core" +version = "2.14.6" +description = "" +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "pydantic_core-2.14.6-cp310-cp310-macosx_10_7_x86_64.whl", hash = "sha256:72f9a942d739f09cd42fffe5dc759928217649f070056f03c70df14f5770acf9"}, + {file = "pydantic_core-2.14.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6a31d98c0d69776c2576dda4b77b8e0c69ad08e8b539c25c7d0ca0dc19a50d6c"}, + {file = "pydantic_core-2.14.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5aa90562bc079c6c290f0512b21768967f9968e4cfea84ea4ff5af5d917016e4"}, + {file = "pydantic_core-2.14.6-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:370ffecb5316ed23b667d99ce4debe53ea664b99cc37bfa2af47bc769056d534"}, + {file = "pydantic_core-2.14.6-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f85f3843bdb1fe80e8c206fe6eed7a1caeae897e496542cee499c374a85c6e08"}, + {file = "pydantic_core-2.14.6-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9862bf828112e19685b76ca499b379338fd4c5c269d897e218b2ae8fcb80139d"}, + {file = "pydantic_core-2.14.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:036137b5ad0cb0004c75b579445a1efccd072387a36c7f217bb8efd1afbe5245"}, + {file = "pydantic_core-2.14.6-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:92879bce89f91f4b2416eba4429c7b5ca22c45ef4a499c39f0c5c69257522c7c"}, + {file = "pydantic_core-2.14.6-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0c08de15d50fa190d577e8591f0329a643eeaed696d7771760295998aca6bc66"}, + {file = "pydantic_core-2.14.6-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:36099c69f6b14fc2c49d7996cbf4f87ec4f0e66d1c74aa05228583225a07b590"}, + {file = "pydantic_core-2.14.6-cp310-none-win32.whl", hash = "sha256:7be719e4d2ae6c314f72844ba9d69e38dff342bc360379f7c8537c48e23034b7"}, + {file = "pydantic_core-2.14.6-cp310-none-win_amd64.whl", hash = "sha256:36fa402dcdc8ea7f1b0ddcf0df4254cc6b2e08f8cd80e7010d4c4ae6e86b2a87"}, + {file = "pydantic_core-2.14.6-cp311-cp311-macosx_10_7_x86_64.whl", hash = "sha256:dea7fcd62915fb150cdc373212141a30037e11b761fbced340e9db3379b892d4"}, + {file = "pydantic_core-2.14.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ffff855100bc066ff2cd3aa4a60bc9534661816b110f0243e59503ec2df38421"}, + {file = "pydantic_core-2.14.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1b027c86c66b8627eb90e57aee1f526df77dc6d8b354ec498be9a757d513b92b"}, + {file = "pydantic_core-2.14.6-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:00b1087dabcee0b0ffd104f9f53d7d3eaddfaa314cdd6726143af6bc713aa27e"}, + {file = "pydantic_core-2.14.6-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:75ec284328b60a4e91010c1acade0c30584f28a1f345bc8f72fe8b9e46ec6a96"}, + {file = "pydantic_core-2.14.6-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7e1f4744eea1501404b20b0ac059ff7e3f96a97d3e3f48ce27a139e053bb370b"}, + {file = "pydantic_core-2.14.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b2602177668f89b38b9f84b7b3435d0a72511ddef45dc14446811759b82235a1"}, + {file = "pydantic_core-2.14.6-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6c8edaea3089bf908dd27da8f5d9e395c5b4dc092dbcce9b65e7156099b4b937"}, + {file = "pydantic_core-2.14.6-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:478e9e7b360dfec451daafe286998d4a1eeaecf6d69c427b834ae771cad4b622"}, + {file = "pydantic_core-2.14.6-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:b6ca36c12a5120bad343eef193cc0122928c5c7466121da7c20f41160ba00ba2"}, + {file = "pydantic_core-2.14.6-cp311-none-win32.whl", hash = "sha256:2b8719037e570639e6b665a4050add43134d80b687288ba3ade18b22bbb29dd2"}, + {file = "pydantic_core-2.14.6-cp311-none-win_amd64.whl", hash = "sha256:78ee52ecc088c61cce32b2d30a826f929e1708f7b9247dc3b921aec367dc1b23"}, + {file = "pydantic_core-2.14.6-cp311-none-win_arm64.whl", hash = "sha256:a19b794f8fe6569472ff77602437ec4430f9b2b9ec7a1105cfd2232f9ba355e6"}, + {file = "pydantic_core-2.14.6-cp312-cp312-macosx_10_7_x86_64.whl", hash = "sha256:667aa2eac9cd0700af1ddb38b7b1ef246d8cf94c85637cbb03d7757ca4c3fdec"}, + {file = "pydantic_core-2.14.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:cdee837710ef6b56ebd20245b83799fce40b265b3b406e51e8ccc5b85b9099b7"}, + {file = "pydantic_core-2.14.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2c5bcf3414367e29f83fd66f7de64509a8fd2368b1edf4351e862910727d3e51"}, + {file = "pydantic_core-2.14.6-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:26a92ae76f75d1915806b77cf459811e772d8f71fd1e4339c99750f0e7f6324f"}, + {file = "pydantic_core-2.14.6-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a983cca5ed1dd9a35e9e42ebf9f278d344603bfcb174ff99a5815f953925140a"}, + {file = "pydantic_core-2.14.6-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cb92f9061657287eded380d7dc455bbf115430b3aa4741bdc662d02977e7d0af"}, + {file = "pydantic_core-2.14.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4ace1e220b078c8e48e82c081e35002038657e4b37d403ce940fa679e57113b"}, + {file = "pydantic_core-2.14.6-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ef633add81832f4b56d3b4c9408b43d530dfca29e68fb1b797dcb861a2c734cd"}, + {file = "pydantic_core-2.14.6-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7e90d6cc4aad2cc1f5e16ed56e46cebf4877c62403a311af20459c15da76fd91"}, + {file = "pydantic_core-2.14.6-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:e8a5ac97ea521d7bde7621d86c30e86b798cdecd985723c4ed737a2aa9e77d0c"}, + {file = "pydantic_core-2.14.6-cp312-none-win32.whl", hash = "sha256:f27207e8ca3e5e021e2402ba942e5b4c629718e665c81b8b306f3c8b1ddbb786"}, + {file = "pydantic_core-2.14.6-cp312-none-win_amd64.whl", hash = "sha256:b3e5fe4538001bb82e2295b8d2a39356a84694c97cb73a566dc36328b9f83b40"}, + {file = "pydantic_core-2.14.6-cp312-none-win_arm64.whl", hash = "sha256:64634ccf9d671c6be242a664a33c4acf12882670b09b3f163cd00a24cffbd74e"}, + {file = "pydantic_core-2.14.6-cp37-cp37m-macosx_10_7_x86_64.whl", hash = "sha256:24368e31be2c88bd69340fbfe741b405302993242ccb476c5c3ff48aeee1afe0"}, + {file = "pydantic_core-2.14.6-cp37-cp37m-macosx_11_0_arm64.whl", hash = "sha256:e33b0834f1cf779aa839975f9d8755a7c2420510c0fa1e9fa0497de77cd35d2c"}, + {file = "pydantic_core-2.14.6-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6af4b3f52cc65f8a0bc8b1cd9676f8c21ef3e9132f21fed250f6958bd7223bed"}, + {file = "pydantic_core-2.14.6-cp37-cp37m-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d15687d7d7f40333bd8266f3814c591c2e2cd263fa2116e314f60d82086e353a"}, + {file = "pydantic_core-2.14.6-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:095b707bb287bfd534044166ab767bec70a9bba3175dcdc3371782175c14e43c"}, + {file = "pydantic_core-2.14.6-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:94fc0e6621e07d1e91c44e016cc0b189b48db053061cc22d6298a611de8071bb"}, + {file = "pydantic_core-2.14.6-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ce830e480f6774608dedfd4a90c42aac4a7af0a711f1b52f807130c2e434c06"}, + {file = "pydantic_core-2.14.6-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a306cdd2ad3a7d795d8e617a58c3a2ed0f76c8496fb7621b6cd514eb1532cae8"}, + {file = "pydantic_core-2.14.6-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:2f5fa187bde8524b1e37ba894db13aadd64faa884657473b03a019f625cee9a8"}, + {file = "pydantic_core-2.14.6-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:438027a975cc213a47c5d70672e0d29776082155cfae540c4e225716586be75e"}, + {file = "pydantic_core-2.14.6-cp37-none-win32.whl", hash = "sha256:f96ae96a060a8072ceff4cfde89d261837b4294a4f28b84a28765470d502ccc6"}, + {file = "pydantic_core-2.14.6-cp37-none-win_amd64.whl", hash = "sha256:e646c0e282e960345314f42f2cea5e0b5f56938c093541ea6dbf11aec2862391"}, + {file = "pydantic_core-2.14.6-cp38-cp38-macosx_10_7_x86_64.whl", hash = "sha256:db453f2da3f59a348f514cfbfeb042393b68720787bbef2b4c6068ea362c8149"}, + {file = "pydantic_core-2.14.6-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:3860c62057acd95cc84044e758e47b18dcd8871a328ebc8ccdefd18b0d26a21b"}, + {file = "pydantic_core-2.14.6-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:36026d8f99c58d7044413e1b819a67ca0e0b8ebe0f25e775e6c3d1fabb3c38fb"}, + {file = "pydantic_core-2.14.6-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8ed1af8692bd8d2a29d702f1a2e6065416d76897d726e45a1775b1444f5928a7"}, + {file = "pydantic_core-2.14.6-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:314ccc4264ce7d854941231cf71b592e30d8d368a71e50197c905874feacc8a8"}, + {file = "pydantic_core-2.14.6-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:982487f8931067a32e72d40ab6b47b1628a9c5d344be7f1a4e668fb462d2da42"}, + {file = "pydantic_core-2.14.6-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2dbe357bc4ddda078f79d2a36fc1dd0494a7f2fad83a0a684465b6f24b46fe80"}, + {file = "pydantic_core-2.14.6-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2f6ffc6701a0eb28648c845f4945a194dc7ab3c651f535b81793251e1185ac3d"}, + {file = "pydantic_core-2.14.6-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:7f5025db12fc6de7bc1104d826d5aee1d172f9ba6ca936bf6474c2148ac336c1"}, + {file = "pydantic_core-2.14.6-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:dab03ed811ed1c71d700ed08bde8431cf429bbe59e423394f0f4055f1ca0ea60"}, + {file = "pydantic_core-2.14.6-cp38-none-win32.whl", hash = "sha256:dfcbebdb3c4b6f739a91769aea5ed615023f3c88cb70df812849aef634c25fbe"}, + {file = "pydantic_core-2.14.6-cp38-none-win_amd64.whl", hash = "sha256:99b14dbea2fdb563d8b5a57c9badfcd72083f6006caf8e126b491519c7d64ca8"}, + {file = "pydantic_core-2.14.6-cp39-cp39-macosx_10_7_x86_64.whl", hash = "sha256:4ce8299b481bcb68e5c82002b96e411796b844d72b3e92a3fbedfe8e19813eab"}, + {file = "pydantic_core-2.14.6-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b9a9d92f10772d2a181b5ca339dee066ab7d1c9a34ae2421b2a52556e719756f"}, + {file = "pydantic_core-2.14.6-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fd9e98b408384989ea4ab60206b8e100d8687da18b5c813c11e92fd8212a98e0"}, + {file = "pydantic_core-2.14.6-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4f86f1f318e56f5cbb282fe61eb84767aee743ebe32c7c0834690ebea50c0a6b"}, + {file = "pydantic_core-2.14.6-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:86ce5fcfc3accf3a07a729779d0b86c5d0309a4764c897d86c11089be61da160"}, + {file = "pydantic_core-2.14.6-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3dcf1978be02153c6a31692d4fbcc2a3f1db9da36039ead23173bc256ee3b91b"}, + {file = "pydantic_core-2.14.6-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eedf97be7bc3dbc8addcef4142f4b4164066df0c6f36397ae4aaed3eb187d8ab"}, + {file = "pydantic_core-2.14.6-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d5f916acf8afbcab6bacbb376ba7dc61f845367901ecd5e328fc4d4aef2fcab0"}, + {file = "pydantic_core-2.14.6-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:8a14c192c1d724c3acbfb3f10a958c55a2638391319ce8078cb36c02283959b9"}, + {file = "pydantic_core-2.14.6-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:0348b1dc6b76041516e8a854ff95b21c55f5a411c3297d2ca52f5528e49d8411"}, + {file = "pydantic_core-2.14.6-cp39-none-win32.whl", hash = "sha256:de2a0645a923ba57c5527497daf8ec5df69c6eadf869e9cd46e86349146e5975"}, + {file = "pydantic_core-2.14.6-cp39-none-win_amd64.whl", hash = "sha256:aca48506a9c20f68ee61c87f2008f81f8ee99f8d7f0104bff3c47e2d148f89d9"}, + {file = "pydantic_core-2.14.6-pp310-pypy310_pp73-macosx_10_7_x86_64.whl", hash = "sha256:d5c28525c19f5bb1e09511669bb57353d22b94cf8b65f3a8d141c389a55dec95"}, + {file = "pydantic_core-2.14.6-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:78d0768ee59baa3de0f4adac9e3748b4b1fffc52143caebddfd5ea2961595277"}, + {file = "pydantic_core-2.14.6-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b93785eadaef932e4fe9c6e12ba67beb1b3f1e5495631419c784ab87e975670"}, + {file = "pydantic_core-2.14.6-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a874f21f87c485310944b2b2734cd6d318765bcbb7515eead33af9641816506e"}, + {file = "pydantic_core-2.14.6-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b89f4477d915ea43b4ceea6756f63f0288941b6443a2b28c69004fe07fde0d0d"}, + {file = "pydantic_core-2.14.6-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:172de779e2a153d36ee690dbc49c6db568d7b33b18dc56b69a7514aecbcf380d"}, + {file = "pydantic_core-2.14.6-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:dfcebb950aa7e667ec226a442722134539e77c575f6cfaa423f24371bb8d2e94"}, + {file = "pydantic_core-2.14.6-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:55a23dcd98c858c0db44fc5c04fc7ed81c4b4d33c653a7c45ddaebf6563a2f66"}, + {file = "pydantic_core-2.14.6-pp37-pypy37_pp73-macosx_10_7_x86_64.whl", hash = "sha256:4241204e4b36ab5ae466ecec5c4c16527a054c69f99bba20f6f75232a6a534e2"}, + {file = "pydantic_core-2.14.6-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e574de99d735b3fc8364cba9912c2bec2da78775eba95cbb225ef7dda6acea24"}, + {file = "pydantic_core-2.14.6-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1302a54f87b5cd8528e4d6d1bf2133b6aa7c6122ff8e9dc5220fbc1e07bffebd"}, + {file = "pydantic_core-2.14.6-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f8e81e4b55930e5ffab4a68db1af431629cf2e4066dbdbfef65348b8ab804ea8"}, + {file = "pydantic_core-2.14.6-pp37-pypy37_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:c99462ffc538717b3e60151dfaf91125f637e801f5ab008f81c402f1dff0cd0f"}, + {file = "pydantic_core-2.14.6-pp37-pypy37_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:e4cf2d5829f6963a5483ec01578ee76d329eb5caf330ecd05b3edd697e7d768a"}, + {file = "pydantic_core-2.14.6-pp38-pypy38_pp73-macosx_10_7_x86_64.whl", hash = "sha256:cf10b7d58ae4a1f07fccbf4a0a956d705356fea05fb4c70608bb6fa81d103cda"}, + {file = "pydantic_core-2.14.6-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:399ac0891c284fa8eb998bcfa323f2234858f5d2efca3950ae58c8f88830f145"}, + {file = "pydantic_core-2.14.6-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9c6a5c79b28003543db3ba67d1df336f253a87d3112dac3a51b94f7d48e4c0e1"}, + {file = "pydantic_core-2.14.6-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:599c87d79cab2a6a2a9df4aefe0455e61e7d2aeede2f8577c1b7c0aec643ee8e"}, + {file = "pydantic_core-2.14.6-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:43e166ad47ba900f2542a80d83f9fc65fe99eb63ceec4debec160ae729824052"}, + {file = "pydantic_core-2.14.6-pp38-pypy38_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:3a0b5db001b98e1c649dd55afa928e75aa4087e587b9524a4992316fa23c9fba"}, + {file = "pydantic_core-2.14.6-pp38-pypy38_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:747265448cb57a9f37572a488a57d873fd96bf51e5bb7edb52cfb37124516da4"}, + {file = "pydantic_core-2.14.6-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:7ebe3416785f65c28f4f9441e916bfc8a54179c8dea73c23023f7086fa601c5d"}, + {file = "pydantic_core-2.14.6-pp39-pypy39_pp73-macosx_10_7_x86_64.whl", hash = "sha256:86c963186ca5e50d5c8287b1d1c9d3f8f024cbe343d048c5bd282aec2d8641f2"}, + {file = "pydantic_core-2.14.6-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:e0641b506486f0b4cd1500a2a65740243e8670a2549bb02bc4556a83af84ae03"}, + {file = "pydantic_core-2.14.6-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71d72ca5eaaa8d38c8df16b7deb1a2da4f650c41b58bb142f3fb75d5ad4a611f"}, + {file = "pydantic_core-2.14.6-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:27e524624eace5c59af499cd97dc18bb201dc6a7a2da24bfc66ef151c69a5f2a"}, + {file = "pydantic_core-2.14.6-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a3dde6cac75e0b0902778978d3b1646ca9f438654395a362cb21d9ad34b24acf"}, + {file = "pydantic_core-2.14.6-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:00646784f6cd993b1e1c0e7b0fdcbccc375d539db95555477771c27555e3c556"}, + {file = "pydantic_core-2.14.6-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:23598acb8ccaa3d1d875ef3b35cb6376535095e9405d91a3d57a8c7db5d29341"}, + {file = "pydantic_core-2.14.6-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7f41533d7e3cf9520065f610b41ac1c76bc2161415955fbcead4981b22c7611e"}, + {file = "pydantic_core-2.14.6.tar.gz", hash = "sha256:1fd0c1d395372843fba13a51c28e3bb9d59bd7aebfeb17358ffaaa1e4dbbe948"}, +] + +[package.dependencies] +typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" + +[[package]] +name = "pydantic-settings" +version = "2.1.0" +description = "Settings management using Pydantic" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "pydantic_settings-2.1.0-py3-none-any.whl", hash = "sha256:7621c0cb5d90d1140d2f0ef557bdf03573aac7035948109adf2574770b77605a"}, + {file = "pydantic_settings-2.1.0.tar.gz", hash = "sha256:26b1492e0a24755626ac5e6d715e9077ab7ad4fb5f19a8b7ed7011d52f36141c"}, +] + +[package.dependencies] +pydantic = ">=2.3.0" +python-dotenv = ">=0.21.0" + +[[package]] +name = "pydata-sphinx-theme" +version = "0.15.4" +description = "Bootstrap-based Sphinx theme from the PyData community" +optional = false +python-versions = ">=3.9" +groups = ["docs"] +files = [ + {file = "pydata_sphinx_theme-0.15.4-py3-none-any.whl", hash = "sha256:2136ad0e9500d0949f96167e63f3e298620040aea8f9c74621959eda5d4cf8e6"}, + {file = "pydata_sphinx_theme-0.15.4.tar.gz", hash = "sha256:7762ec0ac59df3acecf49fd2f889e1b4565dbce8b88b2e29ee06fdd90645a06d"}, +] + +[package.dependencies] +accessible-pygments = "*" +Babel = "*" +beautifulsoup4 = "*" +docutils = "!=0.17.0" +packaging = "*" +pygments = ">=2.7" +sphinx = ">=5" +typing-extensions = "*" + +[package.extras] +a11y = ["pytest-playwright"] +dev = ["pandoc", "pre-commit", "pydata-sphinx-theme[doc,test]", "pyyaml", "sphinx-theme-builder[cli]", "tox"] +doc = ["ablog (>=0.11.8)", "colorama", "graphviz", "ipykernel", "ipyleaflet", "ipywidgets", "jupyter_sphinx", "jupyterlite-sphinx", "linkify-it-py", "matplotlib", "myst-parser", "nbsphinx", "numpy", "numpydoc", "pandas", "plotly", "rich", "sphinx-autoapi (>=3.0.0)", "sphinx-copybutton", "sphinx-design", "sphinx-favicon (>=1.0.1)", "sphinx-sitemap", "sphinx-togglebutton", "sphinxcontrib-youtube (>=1.4.1)", "sphinxext-rediraffe", "xarray"] +i18n = ["Babel", "jinja2"] +test = ["pytest", "pytest-cov", "pytest-regressions", "sphinx[test]"] + +[[package]] +name = "pyflakes" +version = "3.1.0" +description = "passive checker of Python programs" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "pyflakes-3.1.0-py2.py3-none-any.whl", hash = "sha256:4132f6d49cb4dae6819e5379898f2b8cce3c5f23994194c24b77d5da2e36f774"}, + {file = "pyflakes-3.1.0.tar.gz", hash = "sha256:a0aae034c444db0071aa077972ba4768d40c830d9539fd45bf4cd3f8f6992efc"}, +] + +[[package]] +name = "pygments" +version = "2.19.2" +description = "Pygments is a syntax highlighting package written in Python." +optional = false +python-versions = ">=3.8" +groups = ["main", "dev", "docs"] +files = [ + {file = "pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b"}, + {file = "pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887"}, +] + +[package.extras] +windows-terminal = ["colorama (>=0.4.6)"] + +[[package]] +name = "pylint" +version = "3.3.8" +description = "python code static checker" +optional = false +python-versions = ">=3.9.0" +groups = ["dev"] +files = [ + {file = "pylint-3.3.8-py3-none-any.whl", hash = "sha256:7ef94aa692a600e82fabdd17102b73fc226758218c97473c7ad67bd4cb905d83"}, + {file = "pylint-3.3.8.tar.gz", hash = "sha256:26698de19941363037e2937d3db9ed94fb3303fdadf7d98847875345a8bb6b05"}, +] + +[package.dependencies] +astroid = ">=3.3.8,<=3.4.0.dev0" +colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""} +dill = [ + {version = ">=0.3.7", markers = "python_version >= \"3.12\""}, + {version = ">=0.3.6", markers = "python_version == \"3.11\""}, +] +isort = ">=4.2.5,<5.13 || >5.13,<7" +mccabe = ">=0.6,<0.8" +platformdirs = ">=2.2" +tomlkit = ">=0.10.1" + +[package.extras] +spelling = ["pyenchant (>=3.2,<4.0)"] +testutils = ["gitpython (>3)"] + +[[package]] +name = "pytest" +version = "7.4.4" +description = "pytest: simple powerful testing with Python" +optional = false +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "pytest-7.4.4-py3-none-any.whl", hash = "sha256:b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8"}, + {file = "pytest-7.4.4.tar.gz", hash = "sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "sys_platform == \"win32\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=0.12,<2.0" + +[package.extras] +testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] + +[[package]] +name = "pytest-asyncio" +version = "0.21.2" +description = "Pytest support for asyncio" +optional = false +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "pytest_asyncio-0.21.2-py3-none-any.whl", hash = "sha256:ab664c88bb7998f711d8039cacd4884da6430886ae8bbd4eded552ed2004f16b"}, + {file = "pytest_asyncio-0.21.2.tar.gz", hash = "sha256:d67738fc232b94b326b9d060750beb16e0074210b98dd8b58a5239fa2a154f45"}, +] + +[package.dependencies] +pytest = ">=7.0.0" + +[package.extras] +docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1.0)"] +testing = ["coverage (>=6.2)", "flaky (>=3.5.0)", "hypothesis (>=5.7.1)", "mypy (>=0.931)", "pytest-trio (>=0.7.0)"] + +[[package]] +name = "pytest-cov" +version = "4.1.0" +description = "Pytest plugin for measuring coverage." +optional = false +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "pytest-cov-4.1.0.tar.gz", hash = "sha256:3904b13dfbfec47f003b8e77fd5b589cd11904a21ddf1ab38a64f204d6a10ef6"}, + {file = "pytest_cov-4.1.0-py3-none-any.whl", hash = "sha256:6ba70b9e97e69fcc3fb45bfeab2d0a138fb65c4d0d6a41ef33983ad114be8c3a"}, +] + +[package.dependencies] +coverage = {version = ">=5.2.1", extras = ["toml"]} +pytest = ">=4.6" + +[package.extras] +testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"] + +[[package]] +name = "pytest-postgresql" +version = "5.1.1" +description = "Postgresql fixtures and fixture factories for Pytest." +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "pytest-postgresql-5.1.1.tar.gz", hash = "sha256:edc1e83f65e9276bf465a983bfee98799866ee067defcee586ef6f889218e91f"}, + {file = "pytest_postgresql-5.1.1-py3-none-any.whl", hash = "sha256:8e737e3e74a487717bc515605c2ea577aaf639548af7919f1086546e341acc7b"}, +] + +[package.dependencies] +mirakuru = "*" +port-for = ">=0.6.0" +psycopg = ">=3.0.0" +pytest = ">=6.2" +setuptools = "*" + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +description = "Extensions to the standard Python datetime module" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +groups = ["main"] +files = [ + {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, + {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, +] + +[package.dependencies] +six = ">=1.5" + +[[package]] +name = "python-dotenv" +version = "1.0.1" +description = "Read key-value pairs from a .env file and set them as environment variables" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca"}, + {file = "python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a"}, +] + +[package.extras] +cli = ["click (>=5.0)"] + +[[package]] +name = "python-jwt" +version = "4.1.0" +description = "Module for generating and verifying JSON Web Tokens" +optional = false +python-versions = ">=3.6" +groups = ["main"] +files = [ + {file = "python_jwt-4.1.0-py2.py3-none-any.whl", hash = "sha256:1f4d44b6b9176375489c0374c71f18f27f52524e689174e11dd39a801170c91b"}, + {file = "python_jwt-4.1.0.tar.gz", hash = "sha256:f89af071d9bda4741bc80754bd1cfce73e434a2cbb7855086d8604a10bd3fdc5"}, +] + +[package.dependencies] +jwcrypto = ">=1.4.2" + +[[package]] +name = "pytz" +version = "2025.2" +description = "World timezone definitions, modern and historical" +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00"}, + {file = "pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3"}, +] + +[[package]] +name = "pyyaml" +version = "6.0.2" +description = "YAML parser and emitter for Python" +optional = false +python-versions = ">=3.8" +groups = ["main", "dev", "docs"] +files = [ + {file = "PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086"}, + {file = "PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed"}, + {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180"}, + {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68"}, + {file = "PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99"}, + {file = "PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e"}, + {file = "PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774"}, + {file = "PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85"}, + {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4"}, + {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e"}, + {file = "PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5"}, + {file = "PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44"}, + {file = "PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab"}, + {file = "PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476"}, + {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48"}, + {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b"}, + {file = "PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4"}, + {file = "PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8"}, + {file = "PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba"}, + {file = "PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5"}, + {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc"}, + {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652"}, + {file = "PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183"}, + {file = "PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563"}, + {file = "PyYAML-6.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a"}, + {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5"}, + {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d"}, + {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083"}, + {file = "PyYAML-6.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706"}, + {file = "PyYAML-6.0.2-cp38-cp38-win32.whl", hash = "sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a"}, + {file = "PyYAML-6.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff"}, + {file = "PyYAML-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d"}, + {file = "PyYAML-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f"}, + {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290"}, + {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12"}, + {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19"}, + {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e"}, + {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725"}, + {file = "PyYAML-6.0.2-cp39-cp39-win32.whl", hash = "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631"}, + {file = "PyYAML-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8"}, + {file = "pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e"}, +] + +[[package]] +name = "requests" +version = "2.32.5" +description = "Python HTTP for Humans." +optional = false +python-versions = ">=3.9" +groups = ["main", "dev", "docs"] +files = [ + {file = "requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6"}, + {file = "requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf"}, +] + +[package.dependencies] +certifi = ">=2017.4.17" +charset_normalizer = ">=2,<4" +idna = ">=2.5,<4" +urllib3 = ">=1.21.1,<3" + +[package.extras] +socks = ["PySocks (>=1.5.6,!=1.5.7)"] +use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] + +[[package]] +name = "rich" +version = "14.1.0" +description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" +optional = false +python-versions = ">=3.8.0" +groups = ["dev"] +files = [ + {file = "rich-14.1.0-py3-none-any.whl", hash = "sha256:536f5f1785986d6dbdea3c75205c473f970777b4a0d6c6dd1b696aa05a3fa04f"}, + {file = "rich-14.1.0.tar.gz", hash = "sha256:e497a48b844b0320d45007cdebfeaeed8db2a4f4bcf49f15e455cfc4af11eaa8"}, +] + +[package.dependencies] +markdown-it-py = ">=2.2.0" +pygments = ">=2.13.0,<3.0.0" + +[package.extras] +jupyter = ["ipywidgets (>=7.5.1,<9)"] + +[[package]] +name = "ruamel-yaml" +version = "0.18.15" +description = "ruamel.yaml is a YAML parser/emitter that supports roundtrip preservation of comments, seq/map flow style, and map key order" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "ruamel.yaml-0.18.15-py3-none-any.whl", hash = "sha256:148f6488d698b7a5eded5ea793a025308b25eca97208181b6a026037f391f701"}, + {file = "ruamel.yaml-0.18.15.tar.gz", hash = "sha256:dbfca74b018c4c3fba0b9cc9ee33e53c371194a9000e694995e620490fd40700"}, +] + +[package.dependencies] +"ruamel.yaml.clib" = {version = ">=0.2.7", markers = "platform_python_implementation == \"CPython\" and python_version < \"3.14\""} + +[package.extras] +docs = ["mercurial (>5.7)", "ryd"] +jinja2 = ["ruamel.yaml.jinja2 (>=0.2)"] + +[[package]] +name = "ruamel-yaml-clib" +version = "0.2.12" +description = "C version of reader, parser and emitter for ruamel.yaml derived from libyaml" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +markers = "platform_python_implementation == \"CPython\" and python_version < \"3.14\"" +files = [ + {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-macosx_13_0_arm64.whl", hash = "sha256:11f891336688faf5156a36293a9c362bdc7c88f03a8a027c2c1d8e0bcde998e5"}, + {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:a606ef75a60ecf3d924613892cc603b154178ee25abb3055db5062da811fd969"}, + {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd5415dded15c3822597455bc02bcd66e81ef8b7a48cb71a33628fc9fdde39df"}, + {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f66efbc1caa63c088dead1c4170d148eabc9b80d95fb75b6c92ac0aad2437d76"}, + {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:22353049ba4181685023b25b5b51a574bce33e7f51c759371a7422dcae5402a6"}, + {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:932205970b9f9991b34f55136be327501903f7c66830e9760a8ffb15b07f05cd"}, + {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a52d48f4e7bf9005e8f0a89209bf9a73f7190ddf0489eee5eb51377385f59f2a"}, + {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-win32.whl", hash = "sha256:3eac5a91891ceb88138c113f9db04f3cebdae277f5d44eaa3651a4f573e6a5da"}, + {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-win_amd64.whl", hash = "sha256:ab007f2f5a87bd08ab1499bdf96f3d5c6ad4dcfa364884cb4549aa0154b13a28"}, + {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-macosx_13_0_arm64.whl", hash = "sha256:4a6679521a58256a90b0d89e03992c15144c5f3858f40d7c18886023d7943db6"}, + {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-manylinux2014_aarch64.whl", hash = "sha256:d84318609196d6bd6da0edfa25cedfbabd8dbde5140a0a23af29ad4b8f91fb1e"}, + {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb43a269eb827806502c7c8efb7ae7e9e9d0573257a46e8e952f4d4caba4f31e"}, + {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:811ea1594b8a0fb466172c384267a4e5e367298af6b228931f273b111f17ef52"}, + {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:cf12567a7b565cbf65d438dec6cfbe2917d3c1bdddfce84a9930b7d35ea59642"}, + {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:7dd5adc8b930b12c8fc5b99e2d535a09889941aa0d0bd06f4749e9a9397c71d2"}, + {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1492a6051dab8d912fc2adeef0e8c72216b24d57bd896ea607cb90bb0c4981d3"}, + {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-win32.whl", hash = "sha256:bd0a08f0bab19093c54e18a14a10b4322e1eacc5217056f3c063bd2f59853ce4"}, + {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-win_amd64.whl", hash = "sha256:a274fb2cb086c7a3dea4322ec27f4cb5cc4b6298adb583ab0e211a4682f241eb"}, + {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:20b0f8dc160ba83b6dcc0e256846e1a02d044e13f7ea74a3d1d56ede4e48c632"}, + {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-manylinux2014_aarch64.whl", hash = "sha256:943f32bc9dedb3abff9879edc134901df92cfce2c3d5c9348f172f62eb2d771d"}, + {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95c3829bb364fdb8e0332c9931ecf57d9be3519241323c5274bd82f709cebc0c"}, + {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:749c16fcc4a2b09f28843cda5a193e0283e47454b63ec4b81eaa2242f50e4ccd"}, + {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bf165fef1f223beae7333275156ab2022cffe255dcc51c27f066b4370da81e31"}, + {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:32621c177bbf782ca5a18ba4d7af0f1082a3f6e517ac2a18b3974d4edf349680"}, + {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b82a7c94a498853aa0b272fd5bc67f29008da798d4f93a2f9f289feb8426a58d"}, + {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-win32.whl", hash = "sha256:e8c4ebfcfd57177b572e2040777b8abc537cdef58a2120e830124946aa9b42c5"}, + {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-win_amd64.whl", hash = "sha256:0467c5965282c62203273b838ae77c0d29d7638c8a4e3a1c8bdd3602c10904e4"}, + {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:4c8c5d82f50bb53986a5e02d1b3092b03622c02c2eb78e29bec33fd9593bae1a"}, + {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-manylinux2014_aarch64.whl", hash = "sha256:e7e3736715fbf53e9be2a79eb4db68e4ed857017344d697e8b9749444ae57475"}, + {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b7e75b4965e1d4690e93021adfcecccbca7d61c7bddd8e22406ef2ff20d74ef"}, + {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:96777d473c05ee3e5e3c3e999f5d23c6f4ec5b0c38c098b3a5229085f74236c6"}, + {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:3bc2a80e6420ca8b7d3590791e2dfc709c88ab9152c00eeb511c9875ce5778bf"}, + {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:e188d2699864c11c36cdfdada94d781fd5d6b0071cd9c427bceb08ad3d7c70e1"}, + {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4f6f3eac23941b32afccc23081e1f50612bdbe4e982012ef4f5797986828cd01"}, + {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-win32.whl", hash = "sha256:6442cb36270b3afb1b4951f060eccca1ce49f3d087ca1ca4563a6eb479cb3de6"}, + {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-win_amd64.whl", hash = "sha256:e5b8daf27af0b90da7bb903a876477a9e6d7270be6146906b276605997c7e9a3"}, + {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:fc4b630cd3fa2cf7fce38afa91d7cfe844a9f75d7f0f36393fa98815e911d987"}, + {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:bc5f1e1c28e966d61d2519f2a3d451ba989f9ea0f2307de7bc45baa526de9e45"}, + {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5a0e060aace4c24dcaf71023bbd7d42674e3b230f7e7b97317baf1e953e5b519"}, + {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e2f1c3765db32be59d18ab3953f43ab62a761327aafc1594a2a1fbe038b8b8a7"}, + {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:d85252669dc32f98ebcd5d36768f5d4faeaeaa2d655ac0473be490ecdae3c285"}, + {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:e143ada795c341b56de9418c58d028989093ee611aa27ffb9b7f609c00d813ed"}, + {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:2c59aa6170b990d8d2719323e628aaf36f3bfbc1c26279c0eeeb24d05d2d11c7"}, + {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-win32.whl", hash = "sha256:beffaed67936fbbeffd10966a4eb53c402fafd3d6833770516bf7314bc6ffa12"}, + {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-win_amd64.whl", hash = "sha256:040ae85536960525ea62868b642bdb0c2cc6021c9f9d507810c0c604e66f5a7b"}, + {file = "ruamel.yaml.clib-0.2.12.tar.gz", hash = "sha256:6c8fbb13ec503f99a91901ab46e0b07ae7941cd527393187039aec586fdfd36f"}, +] + +[[package]] +name = "safety" +version = "2.3.4" +description = "Checks installed dependencies for known vulnerabilities and licenses." +optional = false +python-versions = "*" +groups = ["dev"] +files = [ + {file = "safety-2.3.4-py3-none-any.whl", hash = "sha256:6224dcd9b20986a2b2c5e7acfdfba6bca42bb11b2783b24ed04f32317e5167ea"}, + {file = "safety-2.3.4.tar.gz", hash = "sha256:b9e74e794e82f54d11f4091c5d820c4d2d81de9f953bf0b4f33ac8bc402ae72c"}, +] + +[package.dependencies] +Click = ">=8.0.2" +dparse = ">=0.6.2" +packaging = ">=21.0" +requests = "*" +"ruamel.yaml" = ">=0.17.21" +setuptools = ">=19.3" + +[package.extras] +github = ["jinja2 (>=3.1.0)", "pygithub (>=1.43.3)"] +gitlab = ["python-gitlab (>=1.3.0)"] + +[[package]] +name = "scikit-learn" +version = "1.7.1" +description = "A set of python modules for machine learning and data mining" +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "scikit_learn-1.7.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:406204dd4004f0517f0b23cf4b28c6245cbd51ab1b6b78153bc784def214946d"}, + {file = "scikit_learn-1.7.1-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:16af2e44164f05d04337fd1fc3ae7c4ea61fd9b0d527e22665346336920fe0e1"}, + {file = "scikit_learn-1.7.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2f2e78e56a40c7587dea9a28dc4a49500fa2ead366869418c66f0fd75b80885c"}, + {file = "scikit_learn-1.7.1-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b62b76ad408a821475b43b7bb90a9b1c9a4d8d125d505c2df0539f06d6e631b1"}, + {file = "scikit_learn-1.7.1-cp310-cp310-win_amd64.whl", hash = "sha256:9963b065677a4ce295e8ccdee80a1dd62b37249e667095039adcd5bce6e90deb"}, + {file = "scikit_learn-1.7.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:90c8494ea23e24c0fb371afc474618c1019dc152ce4a10e4607e62196113851b"}, + {file = "scikit_learn-1.7.1-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:bb870c0daf3bf3be145ec51df8ac84720d9972170786601039f024bf6d61a518"}, + {file = "scikit_learn-1.7.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:40daccd1b5623f39e8943ab39735cadf0bdce80e67cdca2adcb5426e987320a8"}, + {file = "scikit_learn-1.7.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:30d1f413cfc0aa5a99132a554f1d80517563c34a9d3e7c118fde2d273c6fe0f7"}, + {file = "scikit_learn-1.7.1-cp311-cp311-win_amd64.whl", hash = "sha256:c711d652829a1805a95d7fe96654604a8f16eab5a9e9ad87b3e60173415cb650"}, + {file = "scikit_learn-1.7.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3cee419b49b5bbae8796ecd690f97aa412ef1674410c23fc3257c6b8b85b8087"}, + {file = "scikit_learn-1.7.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:2fd8b8d35817b0d9ebf0b576f7d5ffbbabdb55536b0655a8aaae629d7ffd2e1f"}, + {file = "scikit_learn-1.7.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:588410fa19a96a69763202f1d6b7b91d5d7a5d73be36e189bc6396bfb355bd87"}, + {file = "scikit_learn-1.7.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e3142f0abe1ad1d1c31a2ae987621e41f6b578144a911ff4ac94781a583adad7"}, + {file = "scikit_learn-1.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:3ddd9092c1bd469acab337d87930067c87eac6bd544f8d5027430983f1e1ae88"}, + {file = "scikit_learn-1.7.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b7839687fa46d02e01035ad775982f2470be2668e13ddd151f0f55a5bf123bae"}, + {file = "scikit_learn-1.7.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:a10f276639195a96c86aa572ee0698ad64ee939a7b042060b98bd1930c261d10"}, + {file = "scikit_learn-1.7.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:13679981fdaebc10cc4c13c43344416a86fcbc61449cb3e6517e1df9d12c8309"}, + {file = "scikit_learn-1.7.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4f1262883c6a63f067a980a8cdd2d2e7f2513dddcef6a9eaada6416a7a7cbe43"}, + {file = "scikit_learn-1.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:ca6d31fb10e04d50bfd2b50d66744729dbb512d4efd0223b864e2fdbfc4cee11"}, + {file = "scikit_learn-1.7.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:781674d096303cfe3d351ae6963ff7c958db61cde3421cd490e3a5a58f2a94ae"}, + {file = "scikit_learn-1.7.1-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:10679f7f125fe7ecd5fad37dd1aa2daae7e3ad8df7f3eefa08901b8254b3e12c"}, + {file = "scikit_learn-1.7.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1f812729e38c8cb37f760dce71a9b83ccfb04f59b3dca7c6079dcdc60544fa9e"}, + {file = "scikit_learn-1.7.1-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:88e1a20131cf741b84b89567e1717f27a2ced228e0f29103426102bc2e3b8ef7"}, + {file = "scikit_learn-1.7.1-cp313-cp313t-win_amd64.whl", hash = "sha256:b1bd1d919210b6a10b7554b717c9000b5485aa95a1d0f177ae0d7ee8ec750da5"}, + {file = "scikit_learn-1.7.1.tar.gz", hash = "sha256:24b3f1e976a4665aa74ee0fcaac2b8fccc6ae77c8e07ab25da3ba6d3292b9802"}, +] + +[package.dependencies] +joblib = ">=1.2.0" +numpy = ">=1.22.0" +scipy = ">=1.8.0" +threadpoolctl = ">=3.1.0" + +[package.extras] +benchmark = ["matplotlib (>=3.5.0)", "memory_profiler (>=0.57.0)", "pandas (>=1.4.0)"] +build = ["cython (>=3.0.10)", "meson-python (>=0.17.1)", "numpy (>=1.22.0)", "scipy (>=1.8.0)"] +docs = ["Pillow (>=8.4.0)", "matplotlib (>=3.5.0)", "memory_profiler (>=0.57.0)", "numpydoc (>=1.2.0)", "pandas (>=1.4.0)", "plotly (>=5.14.0)", "polars (>=0.20.30)", "pooch (>=1.6.0)", "pydata-sphinx-theme (>=0.15.3)", "scikit-image (>=0.19.0)", "seaborn (>=0.9.0)", "sphinx (>=7.3.7)", "sphinx-copybutton (>=0.5.2)", "sphinx-design (>=0.5.0)", "sphinx-design (>=0.6.0)", "sphinx-gallery (>=0.17.1)", "sphinx-prompt (>=1.4.0)", "sphinx-remove-toctrees (>=1.0.0.post1)", "sphinxcontrib-sass (>=0.3.4)", "sphinxext-opengraph (>=0.9.1)", "towncrier (>=24.8.0)"] +examples = ["matplotlib (>=3.5.0)", "pandas (>=1.4.0)", "plotly (>=5.14.0)", "pooch (>=1.6.0)", "scikit-image (>=0.19.0)", "seaborn (>=0.9.0)"] +install = ["joblib (>=1.2.0)", "numpy (>=1.22.0)", "scipy (>=1.8.0)", "threadpoolctl (>=3.1.0)"] +maintenance = ["conda-lock (==3.0.1)"] +tests = ["matplotlib (>=3.5.0)", "mypy (>=1.15)", "numpydoc (>=1.2.0)", "pandas (>=1.4.0)", "polars (>=0.20.30)", "pooch (>=1.6.0)", "pyamg (>=4.2.1)", "pyarrow (>=12.0.0)", "pytest (>=7.1.2)", "pytest-cov (>=2.9.0)", "ruff (>=0.11.7)", "scikit-image (>=0.19.0)"] + +[[package]] +name = "scipy" +version = "1.16.1" +description = "Fundamental algorithms for scientific computing in Python" +optional = false +python-versions = ">=3.11" +groups = ["main"] +files = [ + {file = "scipy-1.16.1-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:c033fa32bab91dc98ca59d0cf23bb876454e2bb02cbe592d5023138778f70030"}, + {file = "scipy-1.16.1-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:6e5c2f74e5df33479b5cd4e97a9104c511518fbd979aa9b8f6aec18b2e9ecae7"}, + {file = "scipy-1.16.1-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:0a55ffe0ba0f59666e90951971a884d1ff6f4ec3275a48f472cfb64175570f77"}, + {file = "scipy-1.16.1-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:f8a5d6cd147acecc2603fbd382fed6c46f474cccfcf69ea32582e033fb54dcfe"}, + {file = "scipy-1.16.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cb18899127278058bcc09e7b9966d41a5a43740b5bb8dcba401bd983f82e885b"}, + {file = "scipy-1.16.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:adccd93a2fa937a27aae826d33e3bfa5edf9aa672376a4852d23a7cd67a2e5b7"}, + {file = "scipy-1.16.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:18aca1646a29ee9a0625a1be5637fa798d4d81fdf426481f06d69af828f16958"}, + {file = "scipy-1.16.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d85495cef541729a70cdddbbf3e6b903421bc1af3e8e3a9a72a06751f33b7c39"}, + {file = "scipy-1.16.1-cp311-cp311-win_amd64.whl", hash = "sha256:226652fca853008119c03a8ce71ffe1b3f6d2844cc1686e8f9806edafae68596"}, + {file = "scipy-1.16.1-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:81b433bbeaf35728dad619afc002db9b189e45eebe2cd676effe1fb93fef2b9c"}, + {file = "scipy-1.16.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:886cc81fdb4c6903a3bb0464047c25a6d1016fef77bb97949817d0c0d79f9e04"}, + {file = "scipy-1.16.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:15240c3aac087a522b4eaedb09f0ad061753c5eebf1ea430859e5bf8640d5919"}, + {file = "scipy-1.16.1-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:65f81a25805f3659b48126b5053d9e823d3215e4a63730b5e1671852a1705921"}, + {file = "scipy-1.16.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6c62eea7f607f122069b9bad3f99489ddca1a5173bef8a0c75555d7488b6f725"}, + {file = "scipy-1.16.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f965bbf3235b01c776115ab18f092a95aa74c271a52577bcb0563e85738fd618"}, + {file = "scipy-1.16.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f006e323874ffd0b0b816d8c6a8e7f9a73d55ab3b8c3f72b752b226d0e3ac83d"}, + {file = "scipy-1.16.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e8fd15fc5085ab4cca74cb91fe0a4263b1f32e4420761ddae531ad60934c2119"}, + {file = "scipy-1.16.1-cp312-cp312-win_amd64.whl", hash = "sha256:f7b8013c6c066609577d910d1a2a077021727af07b6fab0ee22c2f901f22352a"}, + {file = "scipy-1.16.1-cp313-cp313-macosx_10_14_x86_64.whl", hash = "sha256:5451606823a5e73dfa621a89948096c6528e2896e40b39248295d3a0138d594f"}, + {file = "scipy-1.16.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:89728678c5ca5abd610aee148c199ac1afb16e19844401ca97d43dc548a354eb"}, + {file = "scipy-1.16.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:e756d688cb03fd07de0fffad475649b03cb89bee696c98ce508b17c11a03f95c"}, + {file = "scipy-1.16.1-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:5aa2687b9935da3ed89c5dbed5234576589dd28d0bf7cd237501ccfbdf1ad608"}, + {file = "scipy-1.16.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0851f6a1e537fe9399f35986897e395a1aa61c574b178c0d456be5b1a0f5ca1f"}, + {file = "scipy-1.16.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fedc2cbd1baed37474b1924c331b97bdff611d762c196fac1a9b71e67b813b1b"}, + {file = "scipy-1.16.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2ef500e72f9623a6735769e4b93e9dcb158d40752cdbb077f305487e3e2d1f45"}, + {file = "scipy-1.16.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:978d8311674b05a8f7ff2ea6c6bce5d8b45a0cb09d4c5793e0318f448613ea65"}, + {file = "scipy-1.16.1-cp313-cp313-win_amd64.whl", hash = "sha256:81929ed0fa7a5713fcdd8b2e6f73697d3b4c4816d090dd34ff937c20fa90e8ab"}, + {file = "scipy-1.16.1-cp313-cp313t-macosx_10_14_x86_64.whl", hash = "sha256:bcc12db731858abda693cecdb3bdc9e6d4bd200213f49d224fe22df82687bdd6"}, + {file = "scipy-1.16.1-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:744d977daa4becb9fc59135e75c069f8d301a87d64f88f1e602a9ecf51e77b27"}, + {file = "scipy-1.16.1-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:dc54f76ac18073bcecffb98d93f03ed6b81a92ef91b5d3b135dcc81d55a724c7"}, + {file = "scipy-1.16.1-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:367d567ee9fc1e9e2047d31f39d9d6a7a04e0710c86e701e053f237d14a9b4f6"}, + {file = "scipy-1.16.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:4cf5785e44e19dcd32a0e4807555e1e9a9b8d475c6afff3d21c3c543a6aa84f4"}, + {file = "scipy-1.16.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3d0b80fb26d3e13a794c71d4b837e2a589d839fd574a6bbb4ee1288c213ad4a3"}, + {file = "scipy-1.16.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:8503517c44c18d1030d666cb70aaac1cc8913608816e06742498833b128488b7"}, + {file = "scipy-1.16.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:30cc4bb81c41831ecfd6dc450baf48ffd80ef5aed0f5cf3ea775740e80f16ecc"}, + {file = "scipy-1.16.1-cp313-cp313t-win_amd64.whl", hash = "sha256:c24fa02f7ed23ae514460a22c57eca8f530dbfa50b1cfdbf4f37c05b5309cc39"}, + {file = "scipy-1.16.1-cp314-cp314-macosx_10_14_x86_64.whl", hash = "sha256:796a5a9ad36fa3a782375db8f4241ab02a091308eb079746bc0f874c9b998318"}, + {file = "scipy-1.16.1-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:3ea0733a2ff73fd6fdc5fecca54ee9b459f4d74f00b99aced7d9a3adb43fb1cc"}, + {file = "scipy-1.16.1-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:85764fb15a2ad994e708258bb4ed8290d1305c62a4e1ef07c414356a24fcfbf8"}, + {file = "scipy-1.16.1-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:ca66d980469cb623b1759bdd6e9fd97d4e33a9fad5b33771ced24d0cb24df67e"}, + {file = "scipy-1.16.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e7cc1ffcc230f568549fc56670bcf3df1884c30bd652c5da8138199c8c76dae0"}, + {file = "scipy-1.16.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3ddfb1e8d0b540cb4ee9c53fc3dea3186f97711248fb94b4142a1b27178d8b4b"}, + {file = "scipy-1.16.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4dc0e7be79e95d8ba3435d193e0d8ce372f47f774cffd882f88ea4e1e1ddc731"}, + {file = "scipy-1.16.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f23634f9e5adb51b2a77766dac217063e764337fbc816aa8ad9aaebcd4397fd3"}, + {file = "scipy-1.16.1-cp314-cp314-win_amd64.whl", hash = "sha256:57d75524cb1c5a374958a2eae3d84e1929bb971204cc9d52213fb8589183fc19"}, + {file = "scipy-1.16.1-cp314-cp314t-macosx_10_14_x86_64.whl", hash = "sha256:d8da7c3dd67bcd93f15618938f43ed0995982eb38973023d46d4646c4283ad65"}, + {file = "scipy-1.16.1-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:cc1d2f2fd48ba1e0620554fe5bc44d3e8f5d4185c8c109c7fbdf5af2792cfad2"}, + {file = "scipy-1.16.1-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:21a611ced9275cb861bacadbada0b8c0623bc00b05b09eb97f23b370fc2ae56d"}, + {file = "scipy-1.16.1-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:8dfbb25dffc4c3dd9371d8ab456ca81beeaf6f9e1c2119f179392f0dc1ab7695"}, + {file = "scipy-1.16.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f0ebb7204f063fad87fc0a0e4ff4a2ff40b2a226e4ba1b7e34bf4b79bf97cd86"}, + {file = "scipy-1.16.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f1b9e5962656f2734c2b285a8745358ecb4e4efbadd00208c80a389227ec61ff"}, + {file = "scipy-1.16.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5e1a106f8c023d57a2a903e771228bf5c5b27b5d692088f457acacd3b54511e4"}, + {file = "scipy-1.16.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:709559a1db68a9abc3b2c8672c4badf1614f3b440b3ab326d86a5c0491eafae3"}, + {file = "scipy-1.16.1-cp314-cp314t-win_amd64.whl", hash = "sha256:c0c804d60492a0aad7f5b2bb1862f4548b990049e27e828391ff2bf6f7199998"}, + {file = "scipy-1.16.1.tar.gz", hash = "sha256:44c76f9e8b6e8e488a586190ab38016e4ed2f8a038af7cd3defa903c0a2238b3"}, +] + +[package.dependencies] +numpy = ">=1.25.2,<2.6" + +[package.extras] +dev = ["cython-lint (>=0.12.2)", "doit (>=0.36.0)", "mypy (==1.10.0)", "pycodestyle", "pydevtool", "rich-click", "ruff (>=0.0.292)", "types-psutil", "typing_extensions"] +doc = ["intersphinx_registry", "jupyterlite-pyodide-kernel", "jupyterlite-sphinx (>=0.19.1)", "jupytext", "linkify-it-py", "matplotlib (>=3.5)", "myst-nb (>=1.2.0)", "numpydoc", "pooch", "pydata-sphinx-theme (>=0.15.2)", "sphinx (>=5.0.0,<8.2.0)", "sphinx-copybutton", "sphinx-design (>=0.4.0)"] +test = ["Cython", "array-api-strict (>=2.3.1)", "asv", "gmpy2", "hypothesis (>=6.30)", "meson", "mpmath", "ninja ; sys_platform != \"emscripten\"", "pooch", "pytest", "pytest-cov", "pytest-timeout", "pytest-xdist", "scikit-umfpack", "threadpoolctl"] + +[[package]] +name = "setuptools" +version = "80.9.0" +description = "Easily download, build, install, upgrade, and uninstall Python packages" +optional = false +python-versions = ">=3.9" +groups = ["dev", "docs"] +files = [ + {file = "setuptools-80.9.0-py3-none-any.whl", hash = "sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922"}, + {file = "setuptools-80.9.0.tar.gz", hash = "sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c"}, +] + +[package.extras] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\"", "ruff (>=0.8.0) ; sys_platform != \"cygwin\""] +core = ["importlib_metadata (>=6) ; python_version < \"3.10\"", "jaraco.functools (>=4)", "jaraco.text (>=3.7)", "more_itertools", "more_itertools (>=8.8)", "packaging (>=24.2)", "platformdirs (>=4.2.2)", "tomli (>=2.0.1) ; python_version < \"3.11\"", "wheel (>=0.43.0)"] +cover = ["pytest-cov"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier", "towncrier (<24.7)"] +enabler = ["pytest-enabler (>=2.2)"] +test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21) ; python_version >= \"3.9\" and sys_platform != \"cygwin\"", "jaraco.envs (>=2.2)", "jaraco.path (>=3.7.2)", "jaraco.test (>=5.5)", "packaging (>=24.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-home (>=0.5)", "pytest-perf ; sys_platform != \"cygwin\"", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel (>=0.44.0)"] +type = ["importlib_metadata (>=7.0.2) ; python_version < \"3.10\"", "jaraco.develop (>=7.21) ; sys_platform != \"cygwin\"", "mypy (==1.14.*)", "pytest-mypy"] + +[[package]] +name = "shibuya" +version = "2025.8.16" +description = "A clean, responsive, and customizable Sphinx documentation theme with light/dark mode." +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "shibuya-2025.8.16-py3-none-any.whl", hash = "sha256:90739b5d14ac38b44b99a392b9b0be7e83137890f36420d52a2079cc29fc1a74"}, + {file = "shibuya-2025.8.16.tar.gz", hash = "sha256:1c639cf646a33026b48eee5c29fd51476e767709f7a87f5656d21d2665d7ee22"}, +] + +[package.dependencies] +Sphinx = "*" + +[[package]] +name = "six" +version = "1.17.0" +description = "Python 2 and 3 compatibility utilities" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +groups = ["main", "docs"] +files = [ + {file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"}, + {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"}, +] + +[[package]] +name = "sniffio" +version = "1.3.1" +description = "Sniff out which async library your code is running under" +optional = false +python-versions = ">=3.7" +groups = ["main", "dev"] +files = [ + {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, + {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, +] + +[[package]] +name = "snowballstemmer" +version = "3.0.1" +description = "This package provides 32 stemmers for 30 languages generated from Snowball algorithms." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*" +groups = ["main", "docs"] +files = [ + {file = "snowballstemmer-3.0.1-py3-none-any.whl", hash = "sha256:6cd7b3897da8d6c9ffb968a6781fa6532dce9c3618a4b127d920dab764a19064"}, + {file = "snowballstemmer-3.0.1.tar.gz", hash = "sha256:6d5eeeec8e9f84d4d56b847692bacf79bc2c8e90c7f80ca4444ff8b6f2e52895"}, +] + +[[package]] +name = "soupsieve" +version = "2.8" +description = "A modern CSS selector implementation for Beautiful Soup." +optional = false +python-versions = ">=3.9" +groups = ["docs"] +files = [ + {file = "soupsieve-2.8-py3-none-any.whl", hash = "sha256:0cc76456a30e20f5d7f2e14a98a4ae2ee4e5abdc7c5ea0aafe795f344bc7984c"}, + {file = "soupsieve-2.8.tar.gz", hash = "sha256:e2dd4a40a628cb5f28f6d4b0db8800b8f581b65bb380b97de22ba5ca8d72572f"}, +] + +[[package]] +name = "sphinx" +version = "7.3.7" +description = "Python documentation generator" +optional = false +python-versions = ">=3.9" +groups = ["main", "docs"] +files = [ + {file = "sphinx-7.3.7-py3-none-any.whl", hash = "sha256:413f75440be4cacf328f580b4274ada4565fb2187d696a84970c23f77b64d8c3"}, + {file = "sphinx-7.3.7.tar.gz", hash = "sha256:a4a7db75ed37531c05002d56ed6948d4c42f473a36f46e1382b0bd76ca9627bc"}, +] + +[package.dependencies] +alabaster = ">=0.7.14,<0.8.0" +babel = ">=2.9" +colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""} +docutils = ">=0.18.1,<0.22" +imagesize = ">=1.3" +Jinja2 = ">=3.0" +packaging = ">=21.0" +Pygments = ">=2.14" +requests = ">=2.25.0" +snowballstemmer = ">=2.0" +sphinxcontrib-applehelp = "*" +sphinxcontrib-devhelp = "*" +sphinxcontrib-htmlhelp = ">=2.0.0" +sphinxcontrib-jsmath = "*" +sphinxcontrib-qthelp = "*" +sphinxcontrib-serializinghtml = ">=1.1.9" + +[package.extras] +docs = ["sphinxcontrib-websupport"] +lint = ["flake8 (>=3.5.0)", "importlib_metadata", "mypy (==1.9.0)", "pytest (>=6.0)", "ruff (==0.3.7)", "sphinx-lint", "tomli", "types-docutils", "types-requests"] +test = ["cython (>=3.0)", "defusedxml (>=0.7.1)", "pytest (>=6.0)", "setuptools (>=67.0)"] + +[[package]] +name = "sphinx-autobuild" +version = "2021.3.14" +description = "Rebuild Sphinx documentation on changes, with live-reload in the browser." +optional = false +python-versions = ">=3.6" +groups = ["docs"] +files = [ + {file = "sphinx-autobuild-2021.3.14.tar.gz", hash = "sha256:de1ca3b66e271d2b5b5140c35034c89e47f263f2cd5db302c9217065f7443f05"}, + {file = "sphinx_autobuild-2021.3.14-py3-none-any.whl", hash = "sha256:8fe8cbfdb75db04475232f05187c776f46f6e9e04cacf1e49ce81bdac649ccac"}, +] + +[package.dependencies] +colorama = "*" +livereload = "*" +sphinx = "*" + +[package.extras] +test = ["pytest", "pytest-cov"] + +[[package]] +name = "sphinx-autodoc-typehints" +version = "1.25.3" +description = "Type hints (PEP 484) support for the Sphinx autodoc extension" +optional = false +python-versions = ">=3.8" +groups = ["docs"] +files = [ + {file = "sphinx_autodoc_typehints-1.25.3-py3-none-any.whl", hash = "sha256:d3da7fa9a9761eff6ff09f8b1956ae3090a2d4f4ad54aebcade8e458d6340835"}, + {file = "sphinx_autodoc_typehints-1.25.3.tar.gz", hash = "sha256:70db10b391acf4e772019765991d2de0ff30ec0899b9ba137706dc0b3c4835e0"}, +] + +[package.dependencies] +sphinx = ">=7.1.2" + +[package.extras] +docs = ["furo (>=2023.9.10)"] +numpy = ["nptyping (>=2.5)"] +testing = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "diff-cover (>=8.0.1)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "sphobjinv (>=2.3.1)", "typing-extensions (>=4.8)"] + +[[package]] +name = "sphinx-book-theme" +version = "1.1.4" +description = "A clean book theme for scientific explanations and documentation with Sphinx" +optional = false +python-versions = ">=3.9" +groups = ["docs"] +files = [ + {file = "sphinx_book_theme-1.1.4-py3-none-any.whl", hash = "sha256:843b3f5c8684640f4a2d01abd298beb66452d1b2394cd9ef5be5ebd5640ea0e1"}, + {file = "sphinx_book_theme-1.1.4.tar.gz", hash = "sha256:73efe28af871d0a89bd05856d300e61edce0d5b2fbb7984e84454be0fedfe9ed"}, +] + +[package.dependencies] +pydata-sphinx-theme = "0.15.4" +sphinx = ">=6.1" + +[package.extras] +code-style = ["pre-commit"] +doc = ["ablog", "folium", "ipywidgets", "matplotlib", "myst-nb", "nbclient", "numpy", "numpydoc", "pandas", "plotly", "sphinx-copybutton", "sphinx-design", "sphinx-examples", "sphinx-tabs", "sphinx-thebe", "sphinx-togglebutton", "sphinxcontrib-bibtex", "sphinxcontrib-youtube", "sphinxext-opengraph"] +test = ["beautifulsoup4", "coverage", "defusedxml", "myst-nb", "pytest", "pytest-cov", "pytest-regressions", "sphinx_thebe"] + +[[package]] +name = "sphinx-copybutton" +version = "0.5.2" +description = "Add a copy button to each of your code cells." +optional = false +python-versions = ">=3.7" +groups = ["docs"] +files = [ + {file = "sphinx-copybutton-0.5.2.tar.gz", hash = "sha256:4cf17c82fb9646d1bc9ca92ac280813a3b605d8c421225fd9913154103ee1fbd"}, + {file = "sphinx_copybutton-0.5.2-py3-none-any.whl", hash = "sha256:fb543fd386d917746c9a2c50360c7905b605726b9355cd26e9974857afeae06e"}, +] + +[package.dependencies] +sphinx = ">=1.8" + +[package.extras] +code-style = ["pre-commit (==2.12.1)"] +rtd = ["ipython", "myst-nb", "sphinx", "sphinx-book-theme", "sphinx-examples"] + +[[package]] +name = "sphinx-design" +version = "0.5.0" +description = "A sphinx extension for designing beautiful, view size responsive web components." +optional = false +python-versions = ">=3.8" +groups = ["docs"] +files = [ + {file = "sphinx_design-0.5.0-py3-none-any.whl", hash = "sha256:1af1267b4cea2eedd6724614f19dcc88fe2e15aff65d06b2f6252cee9c4f4c1e"}, + {file = "sphinx_design-0.5.0.tar.gz", hash = "sha256:e8e513acea6f92d15c6de3b34e954458f245b8e761b45b63950f65373352ab00"}, +] + +[package.dependencies] +sphinx = ">=5,<8" + +[package.extras] +code-style = ["pre-commit (>=3,<4)"] +rtd = ["myst-parser (>=1,<3)"] +testing = ["myst-parser (>=1,<3)", "pytest (>=7.1,<8.0)", "pytest-cov", "pytest-regressions"] +theme-furo = ["furo (>=2023.7.0,<2023.8.0)"] +theme-pydata = ["pydata-sphinx-theme (>=0.13.0,<0.14.0)"] +theme-rtd = ["sphinx-rtd-theme (>=1.0,<2.0)"] +theme-sbt = ["sphinx-book-theme (>=1.0,<2.0)"] + +[[package]] +name = "sphinx-rtd-theme" +version = "1.3.0" +description = "Read the Docs theme for Sphinx" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" +groups = ["docs"] +files = [ + {file = "sphinx_rtd_theme-1.3.0-py2.py3-none-any.whl", hash = "sha256:46ddef89cc2416a81ecfbeaceab1881948c014b1b6e4450b815311a89fb977b0"}, + {file = "sphinx_rtd_theme-1.3.0.tar.gz", hash = "sha256:590b030c7abb9cf038ec053b95e5380b5c70d61591eb0b552063fbe7c41f0931"}, +] + +[package.dependencies] +docutils = "<0.19" +sphinx = ">=1.6,<8" +sphinxcontrib-jquery = ">=4,<5" + +[package.extras] +dev = ["bump2version", "sphinxcontrib-httpdomain", "transifex-client", "wheel"] + +[[package]] +name = "sphinx-tabs" +version = "3.4.7" +description = "Tabbed views for Sphinx" +optional = false +python-versions = ">=3.7" +groups = ["docs"] +files = [ + {file = "sphinx-tabs-3.4.7.tar.gz", hash = "sha256:991ad4a424ff54119799ba1491701aa8130dd43509474aef45a81c42d889784d"}, + {file = "sphinx_tabs-3.4.7-py3-none-any.whl", hash = "sha256:c12d7a36fd413b369e9e9967a0a4015781b71a9c393575419834f19204bd1915"}, +] + +[package.dependencies] +docutils = "*" +pygments = "*" +sphinx = ">=1.8" + +[package.extras] +code-style = ["pre-commit (==2.13.0)"] +testing = ["bs4", "coverage", "pygments", "pytest (>=7.1,<8)", "pytest-cov", "pytest-regressions", "rinohtype"] + +[[package]] +name = "sphinx-togglebutton" +version = "0.3.2" +description = "Toggle page content and collapse admonitions in Sphinx." +optional = false +python-versions = "*" +groups = ["docs"] +files = [ + {file = "sphinx-togglebutton-0.3.2.tar.gz", hash = "sha256:ab0c8b366427b01e4c89802d5d078472c427fa6e9d12d521c34fa0442559dc7a"}, + {file = "sphinx_togglebutton-0.3.2-py3-none-any.whl", hash = "sha256:9647ba7874b7d1e2d43413d8497153a85edc6ac95a3fea9a75ef9c1e08aaae2b"}, +] + +[package.dependencies] +docutils = "*" +setuptools = "*" +sphinx = "*" +wheel = "*" + +[package.extras] +sphinx = ["matplotlib", "myst-nb", "numpy", "sphinx-book-theme", "sphinx-design", "sphinx-examples"] + +[[package]] +name = "sphinxcontrib-applehelp" +version = "2.0.0" +description = "sphinxcontrib-applehelp is a Sphinx extension which outputs Apple help books" +optional = false +python-versions = ">=3.9" +groups = ["main", "docs"] +files = [ + {file = "sphinxcontrib_applehelp-2.0.0-py3-none-any.whl", hash = "sha256:4cd3f0ec4ac5dd9c17ec65e9ab272c9b867ea77425228e68ecf08d6b28ddbdb5"}, + {file = "sphinxcontrib_applehelp-2.0.0.tar.gz", hash = "sha256:2f29ef331735ce958efa4734873f084941970894c6090408b079c61b2e1c06d1"}, +] + +[package.extras] +lint = ["mypy", "ruff (==0.5.5)", "types-docutils"] +standalone = ["Sphinx (>=5)"] +test = ["pytest"] + +[[package]] +name = "sphinxcontrib-devhelp" +version = "2.0.0" +description = "sphinxcontrib-devhelp is a sphinx extension which outputs Devhelp documents" +optional = false +python-versions = ">=3.9" +groups = ["main", "docs"] +files = [ + {file = "sphinxcontrib_devhelp-2.0.0-py3-none-any.whl", hash = "sha256:aefb8b83854e4b0998877524d1029fd3e6879210422ee3780459e28a1f03a8a2"}, + {file = "sphinxcontrib_devhelp-2.0.0.tar.gz", hash = "sha256:411f5d96d445d1d73bb5d52133377b4248ec79db5c793ce7dbe59e074b4dd1ad"}, +] + +[package.extras] +lint = ["mypy", "ruff (==0.5.5)", "types-docutils"] +standalone = ["Sphinx (>=5)"] +test = ["pytest"] + +[[package]] +name = "sphinxcontrib-htmlhelp" +version = "2.1.0" +description = "sphinxcontrib-htmlhelp is a sphinx extension which renders HTML help files" +optional = false +python-versions = ">=3.9" +groups = ["main", "docs"] +files = [ + {file = "sphinxcontrib_htmlhelp-2.1.0-py3-none-any.whl", hash = "sha256:166759820b47002d22914d64a075ce08f4c46818e17cfc9470a9786b759b19f8"}, + {file = "sphinxcontrib_htmlhelp-2.1.0.tar.gz", hash = "sha256:c9e2916ace8aad64cc13a0d233ee22317f2b9025b9cf3295249fa985cc7082e9"}, +] + +[package.extras] +lint = ["mypy", "ruff (==0.5.5)", "types-docutils"] +standalone = ["Sphinx (>=5)"] +test = ["html5lib", "pytest"] + +[[package]] +name = "sphinxcontrib-jquery" +version = "4.1" +description = "Extension to include jQuery on newer Sphinx releases" +optional = false +python-versions = ">=2.7" +groups = ["docs"] +files = [ + {file = "sphinxcontrib-jquery-4.1.tar.gz", hash = "sha256:1620739f04e36a2c779f1a131a2dfd49b2fd07351bf1968ced074365933abc7a"}, + {file = "sphinxcontrib_jquery-4.1-py2.py3-none-any.whl", hash = "sha256:f936030d7d0147dd026a4f2b5a57343d233f1fc7b363f68b3d4f1cb0993878ae"}, +] + +[package.dependencies] +Sphinx = ">=1.8" + +[[package]] +name = "sphinxcontrib-jsmath" +version = "1.0.1" +description = "A sphinx extension which renders display math in HTML via JavaScript" +optional = false +python-versions = ">=3.5" +groups = ["main", "docs"] +files = [ + {file = "sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8"}, + {file = "sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178"}, +] + +[package.extras] +test = ["flake8", "mypy", "pytest"] + +[[package]] +name = "sphinxcontrib-napoleon" +version = "0.7" +description = "Sphinx \"napoleon\" extension." +optional = false +python-versions = "*" +groups = ["docs"] +files = [ + {file = "sphinxcontrib-napoleon-0.7.tar.gz", hash = "sha256:407382beed396e9f2d7f3043fad6afda95719204a1e1a231ac865f40abcbfcf8"}, + {file = "sphinxcontrib_napoleon-0.7-py2.py3-none-any.whl", hash = "sha256:711e41a3974bdf110a484aec4c1a556799eb0b3f3b897521a018ad7e2db13fef"}, +] + +[package.dependencies] +pockets = ">=0.3" +six = ">=1.5.2" + +[[package]] +name = "sphinxcontrib-qthelp" +version = "2.0.0" +description = "sphinxcontrib-qthelp is a sphinx extension which outputs QtHelp documents" +optional = false +python-versions = ">=3.9" +groups = ["main", "docs"] +files = [ + {file = "sphinxcontrib_qthelp-2.0.0-py3-none-any.whl", hash = "sha256:b18a828cdba941ccd6ee8445dbe72ffa3ef8cbe7505d8cd1fa0d42d3f2d5f3eb"}, + {file = "sphinxcontrib_qthelp-2.0.0.tar.gz", hash = "sha256:4fe7d0ac8fc171045be623aba3e2a8f613f8682731f9153bb2e40ece16b9bbab"}, +] + +[package.extras] +lint = ["mypy", "ruff (==0.5.5)", "types-docutils"] +standalone = ["Sphinx (>=5)"] +test = ["defusedxml (>=0.7.1)", "pytest"] + +[[package]] +name = "sphinxcontrib-serializinghtml" +version = "2.0.0" +description = "sphinxcontrib-serializinghtml is a sphinx extension which outputs \"serialized\" HTML files (json and pickle)" +optional = false +python-versions = ">=3.9" +groups = ["main", "docs"] +files = [ + {file = "sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl", hash = "sha256:6e2cb0eef194e10c27ec0023bfeb25badbbb5868244cf5bc5bdc04e4464bf331"}, + {file = "sphinxcontrib_serializinghtml-2.0.0.tar.gz", hash = "sha256:e9d912827f872c029017a53f0ef2180b327c3f7fd23c87229f7a8e8b70031d4d"}, +] + +[package.extras] +lint = ["mypy", "ruff (==0.5.5)", "types-docutils"] +standalone = ["Sphinx (>=5)"] +test = ["pytest"] + +[[package]] +name = "sphinxext-rediraffe" +version = "0.2.7" +description = "Sphinx Extension that redirects non-existent pages to working pages." +optional = false +python-versions = ">=3.6" +groups = ["docs"] +files = [ + {file = "sphinxext-rediraffe-0.2.7.tar.gz", hash = "sha256:651dcbfae5ffda9ffd534dfb8025f36120e5efb6ea1a33f5420023862b9f725d"}, + {file = "sphinxext_rediraffe-0.2.7-py3-none-any.whl", hash = "sha256:9e430a52d4403847f4ffb3a8dd6dfc34a9fe43525305131f52ed899743a5fd8c"}, +] + +[package.dependencies] +sphinx = ">=2.0" + +[[package]] +name = "sqlacodegen" +version = "3.1.0" +description = "Automatic model code generator for SQLAlchemy" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "sqlacodegen-3.1.0-py3-none-any.whl", hash = "sha256:877efdaefd43b143351f9ddbff51fcba89a4d8ce24054873d48830f6797715d3"}, + {file = "sqlacodegen-3.1.0.tar.gz", hash = "sha256:b4042c7b553e66faf3d9edfdc14d134ee6d06e1b11ed83430d58095993199a62"}, +] + +[package.dependencies] +inflect = ">=4.0.0" +SQLAlchemy = ">=2.0.29,<2.0.42" + +[package.extras] +citext = ["sqlalchemy-citext (>=1.7.0)"] +geoalchemy2 = ["geoalchemy2 (>=0.11.1)"] +pgvector = ["pgvector (>=0.2.4)"] +sqlmodel = ["sqlmodel (>=0.0.22)"] +test = ["coverage (>=7)", "mysql-connector-python", "psycopg[binary]", "pytest (>=7.4)", "sqlacodegen[pgvector,sqlmodel]"] + +[[package]] +name = "sqlalchemy" +version = "2.0.41" +description = "Database Abstraction Library" +optional = false +python-versions = ">=3.7" +groups = ["main", "dev"] +files = [ + {file = "SQLAlchemy-2.0.41-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:6854175807af57bdb6425e47adbce7d20a4d79bbfd6f6d6519cd10bb7109a7f8"}, + {file = "SQLAlchemy-2.0.41-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:05132c906066142103b83d9c250b60508af556982a385d96c4eaa9fb9720ac2b"}, + {file = "SQLAlchemy-2.0.41-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8b4af17bda11e907c51d10686eda89049f9ce5669b08fbe71a29747f1e876036"}, + {file = "SQLAlchemy-2.0.41-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:c0b0e5e1b5d9f3586601048dd68f392dc0cc99a59bb5faf18aab057ce00d00b2"}, + {file = "SQLAlchemy-2.0.41-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:0b3dbf1e7e9bc95f4bac5e2fb6d3fb2f083254c3fdd20a1789af965caf2d2348"}, + {file = "SQLAlchemy-2.0.41-cp37-cp37m-win32.whl", hash = "sha256:1e3f196a0c59b0cae9a0cd332eb1a4bda4696e863f4f1cf84ab0347992c548c2"}, + {file = "SQLAlchemy-2.0.41-cp37-cp37m-win_amd64.whl", hash = "sha256:6ab60a5089a8f02009f127806f777fca82581c49e127f08413a66056bd9166dd"}, + {file = "sqlalchemy-2.0.41-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b1f09b6821406ea1f94053f346f28f8215e293344209129a9c0fcc3578598d7b"}, + {file = "sqlalchemy-2.0.41-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1936af879e3db023601196a1684d28e12f19ccf93af01bf3280a3262c4b6b4e5"}, + {file = "sqlalchemy-2.0.41-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2ac41acfc8d965fb0c464eb8f44995770239668956dc4cdf502d1b1ffe0d747"}, + {file = "sqlalchemy-2.0.41-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:81c24e0c0fde47a9723c81d5806569cddef103aebbf79dbc9fcbb617153dea30"}, + {file = "sqlalchemy-2.0.41-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:23a8825495d8b195c4aa9ff1c430c28f2c821e8c5e2d98089228af887e5d7e29"}, + {file = "sqlalchemy-2.0.41-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:60c578c45c949f909a4026b7807044e7e564adf793537fc762b2489d522f3d11"}, + {file = "sqlalchemy-2.0.41-cp310-cp310-win32.whl", hash = "sha256:118c16cd3f1b00c76d69343e38602006c9cfb9998fa4f798606d28d63f23beda"}, + {file = "sqlalchemy-2.0.41-cp310-cp310-win_amd64.whl", hash = "sha256:7492967c3386df69f80cf67efd665c0f667cee67032090fe01d7d74b0e19bb08"}, + {file = "sqlalchemy-2.0.41-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6375cd674fe82d7aa9816d1cb96ec592bac1726c11e0cafbf40eeee9a4516b5f"}, + {file = "sqlalchemy-2.0.41-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9f8c9fdd15a55d9465e590a402f42082705d66b05afc3ffd2d2eb3c6ba919560"}, + {file = "sqlalchemy-2.0.41-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:32f9dc8c44acdee06c8fc6440db9eae8b4af8b01e4b1aee7bdd7241c22edff4f"}, + {file = "sqlalchemy-2.0.41-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90c11ceb9a1f482c752a71f203a81858625d8df5746d787a4786bca4ffdf71c6"}, + {file = "sqlalchemy-2.0.41-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:911cc493ebd60de5f285bcae0491a60b4f2a9f0f5c270edd1c4dbaef7a38fc04"}, + {file = "sqlalchemy-2.0.41-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:03968a349db483936c249f4d9cd14ff2c296adfa1290b660ba6516f973139582"}, + {file = "sqlalchemy-2.0.41-cp311-cp311-win32.whl", hash = "sha256:293cd444d82b18da48c9f71cd7005844dbbd06ca19be1ccf6779154439eec0b8"}, + {file = "sqlalchemy-2.0.41-cp311-cp311-win_amd64.whl", hash = "sha256:3d3549fc3e40667ec7199033a4e40a2f669898a00a7b18a931d3efb4c7900504"}, + {file = "sqlalchemy-2.0.41-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:81f413674d85cfd0dfcd6512e10e0f33c19c21860342a4890c3a2b59479929f9"}, + {file = "sqlalchemy-2.0.41-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:598d9ebc1e796431bbd068e41e4de4dc34312b7aa3292571bb3674a0cb415dd1"}, + {file = "sqlalchemy-2.0.41-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a104c5694dfd2d864a6f91b0956eb5d5883234119cb40010115fd45a16da5e70"}, + {file = "sqlalchemy-2.0.41-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6145afea51ff0af7f2564a05fa95eb46f542919e6523729663a5d285ecb3cf5e"}, + {file = "sqlalchemy-2.0.41-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b46fa6eae1cd1c20e6e6f44e19984d438b6b2d8616d21d783d150df714f44078"}, + {file = "sqlalchemy-2.0.41-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41836fe661cc98abfae476e14ba1906220f92c4e528771a8a3ae6a151242d2ae"}, + {file = "sqlalchemy-2.0.41-cp312-cp312-win32.whl", hash = "sha256:a8808d5cf866c781150d36a3c8eb3adccfa41a8105d031bf27e92c251e3969d6"}, + {file = "sqlalchemy-2.0.41-cp312-cp312-win_amd64.whl", hash = "sha256:5b14e97886199c1f52c14629c11d90c11fbb09e9334fa7bb5f6d068d9ced0ce0"}, + {file = "sqlalchemy-2.0.41-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4eeb195cdedaf17aab6b247894ff2734dcead6c08f748e617bfe05bd5a218443"}, + {file = "sqlalchemy-2.0.41-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d4ae769b9c1c7757e4ccce94b0641bc203bbdf43ba7a2413ab2523d8d047d8dc"}, + {file = "sqlalchemy-2.0.41-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a62448526dd9ed3e3beedc93df9bb6b55a436ed1474db31a2af13b313a70a7e1"}, + {file = "sqlalchemy-2.0.41-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc56c9788617b8964ad02e8fcfeed4001c1f8ba91a9e1f31483c0dffb207002a"}, + {file = "sqlalchemy-2.0.41-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c153265408d18de4cc5ded1941dcd8315894572cddd3c58df5d5b5705b3fa28d"}, + {file = "sqlalchemy-2.0.41-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4f67766965996e63bb46cfbf2ce5355fc32d9dd3b8ad7e536a920ff9ee422e23"}, + {file = "sqlalchemy-2.0.41-cp313-cp313-win32.whl", hash = "sha256:bfc9064f6658a3d1cadeaa0ba07570b83ce6801a1314985bf98ec9b95d74e15f"}, + {file = "sqlalchemy-2.0.41-cp313-cp313-win_amd64.whl", hash = "sha256:82ca366a844eb551daff9d2e6e7a9e5e76d2612c8564f58db6c19a726869c1df"}, + {file = "sqlalchemy-2.0.41-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:90144d3b0c8b139408da50196c5cad2a6909b51b23df1f0538411cd23ffa45d3"}, + {file = "sqlalchemy-2.0.41-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:023b3ee6169969beea3bb72312e44d8b7c27c75b347942d943cf49397b7edeb5"}, + {file = "sqlalchemy-2.0.41-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:725875a63abf7c399d4548e686debb65cdc2549e1825437096a0af1f7e374814"}, + {file = "sqlalchemy-2.0.41-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:81965cc20848ab06583506ef54e37cf15c83c7e619df2ad16807c03100745dea"}, + {file = "sqlalchemy-2.0.41-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:dd5ec3aa6ae6e4d5b5de9357d2133c07be1aff6405b136dad753a16afb6717dd"}, + {file = "sqlalchemy-2.0.41-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:ff8e80c4c4932c10493ff97028decfdb622de69cae87e0f127a7ebe32b4069c6"}, + {file = "sqlalchemy-2.0.41-cp38-cp38-win32.whl", hash = "sha256:4d44522480e0bf34c3d63167b8cfa7289c1c54264c2950cc5fc26e7850967e45"}, + {file = "sqlalchemy-2.0.41-cp38-cp38-win_amd64.whl", hash = "sha256:81eedafa609917040d39aa9332e25881a8e7a0862495fcdf2023a9667209deda"}, + {file = "sqlalchemy-2.0.41-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9a420a91913092d1e20c86a2f5f1fc85c1a8924dbcaf5e0586df8aceb09c9cc2"}, + {file = "sqlalchemy-2.0.41-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:906e6b0d7d452e9a98e5ab8507c0da791856b2380fdee61b765632bb8698026f"}, + {file = "sqlalchemy-2.0.41-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a373a400f3e9bac95ba2a06372c4fd1412a7cee53c37fc6c05f829bf672b8769"}, + {file = "sqlalchemy-2.0.41-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:087b6b52de812741c27231b5a3586384d60c353fbd0e2f81405a814b5591dc8b"}, + {file = "sqlalchemy-2.0.41-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:34ea30ab3ec98355235972dadc497bb659cc75f8292b760394824fab9cf39826"}, + {file = "sqlalchemy-2.0.41-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:8280856dd7c6a68ab3a164b4a4b1c51f7691f6d04af4d4ca23d6ecf2261b7923"}, + {file = "sqlalchemy-2.0.41-cp39-cp39-win32.whl", hash = "sha256:b50eab9994d64f4a823ff99a0ed28a6903224ddbe7fef56a6dd865eec9243440"}, + {file = "sqlalchemy-2.0.41-cp39-cp39-win_amd64.whl", hash = "sha256:5e22575d169529ac3e0a120cf050ec9daa94b6a9597993d1702884f6954a7d71"}, + {file = "sqlalchemy-2.0.41-py3-none-any.whl", hash = "sha256:57df5dc6fdb5ed1a88a1ed2195fd31927e705cad62dedd86b46972752a80f576"}, + {file = "sqlalchemy-2.0.41.tar.gz", hash = "sha256:edba70118c4be3c2b1f90754d308d0b79c6fe2c0fdc52d8ddf603916f83f4db9"}, +] + +[package.dependencies] +greenlet = {version = ">=1", markers = "python_version < \"3.14\" and (platform_machine == \"aarch64\" or platform_machine == \"ppc64le\" or platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"win32\" or platform_machine == \"WIN32\")"} +typing-extensions = ">=4.6.0" + +[package.extras] +aiomysql = ["aiomysql (>=0.2.0)", "greenlet (>=1)"] +aioodbc = ["aioodbc", "greenlet (>=1)"] +aiosqlite = ["aiosqlite", "greenlet (>=1)", "typing_extensions (!=3.10.0.1)"] +asyncio = ["greenlet (>=1)"] +asyncmy = ["asyncmy (>=0.2.3,!=0.2.4,!=0.2.6)", "greenlet (>=1)"] +mariadb-connector = ["mariadb (>=1.0.1,!=1.1.2,!=1.1.5,!=1.1.10)"] +mssql = ["pyodbc"] +mssql-pymssql = ["pymssql"] +mssql-pyodbc = ["pyodbc"] +mypy = ["mypy (>=0.910)"] +mysql = ["mysqlclient (>=1.4.0)"] +mysql-connector = ["mysql-connector-python"] +oracle = ["cx_oracle (>=8)"] +oracle-oracledb = ["oracledb (>=1.0.1)"] +postgresql = ["psycopg2 (>=2.7)"] +postgresql-asyncpg = ["asyncpg", "greenlet (>=1)"] +postgresql-pg8000 = ["pg8000 (>=1.29.1)"] +postgresql-psycopg = ["psycopg (>=3.0.7)"] +postgresql-psycopg2binary = ["psycopg2-binary"] +postgresql-psycopg2cffi = ["psycopg2cffi"] +postgresql-psycopgbinary = ["psycopg[binary] (>=3.0.7)"] +pymysql = ["pymysql"] +sqlcipher = ["sqlcipher3_binary"] + +[[package]] +name = "starlette" +version = "0.47.2" +description = "The little ASGI library that shines." +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "starlette-0.47.2-py3-none-any.whl", hash = "sha256:c5847e96134e5c5371ee9fac6fdf1a67336d5815e09eb2a01fdb57a351ef915b"}, + {file = "starlette-0.47.2.tar.gz", hash = "sha256:6ae9aa5db235e4846decc1e7b79c4f346adf41e9777aebeb49dfd09bbd7023d8"}, +] + +[package.dependencies] +anyio = ">=3.6.2,<5" +typing-extensions = {version = ">=4.10.0", markers = "python_version < \"3.13\""} + +[package.extras] +full = ["httpx (>=0.27.0,<0.29.0)", "itsdangerous", "jinja2", "python-multipart (>=0.0.18)", "pyyaml"] + +[[package]] +name = "stevedore" +version = "5.4.1" +description = "Manage dynamic plugins for Python applications" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "stevedore-5.4.1-py3-none-any.whl", hash = "sha256:d10a31c7b86cba16c1f6e8d15416955fc797052351a56af15e608ad20811fcfe"}, + {file = "stevedore-5.4.1.tar.gz", hash = "sha256:3135b5ae50fe12816ef291baff420acb727fcd356106e3e9cbfa9e5985cd6f4b"}, +] + +[package.dependencies] +pbr = ">=2.0.0" + +[[package]] +name = "threadpoolctl" +version = "3.6.0" +description = "threadpoolctl" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "threadpoolctl-3.6.0-py3-none-any.whl", hash = "sha256:43a0b8fd5a2928500110039e43a5eed8480b918967083ea48dc3ab9f13c4a7fb"}, + {file = "threadpoolctl-3.6.0.tar.gz", hash = "sha256:8ab8b4aa3491d812b623328249fab5302a68d2d71745c8a4c719a2fcaba9f44e"}, +] + +[[package]] +name = "tomlkit" +version = "0.13.3" +description = "Style preserving TOML library" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "tomlkit-0.13.3-py3-none-any.whl", hash = "sha256:c89c649d79ee40629a9fda55f8ace8c6a1b42deb912b2a8fd8d942ddadb606b0"}, + {file = "tomlkit-0.13.3.tar.gz", hash = "sha256:430cf247ee57df2b94ee3fbe588e71d362a941ebb545dec29b53961d61add2a1"}, +] + +[[package]] +name = "tornado" +version = "6.5.2" +description = "Tornado is a Python web framework and asynchronous networking library, originally developed at FriendFeed." +optional = false +python-versions = ">=3.9" +groups = ["docs"] +files = [ + {file = "tornado-6.5.2-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:2436822940d37cde62771cff8774f4f00b3c8024fe482e16ca8387b8a2724db6"}, + {file = "tornado-6.5.2-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:583a52c7aa94ee046854ba81d9ebb6c81ec0fd30386d96f7640c96dad45a03ef"}, + {file = "tornado-6.5.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b0fe179f28d597deab2842b86ed4060deec7388f1fd9c1b4a41adf8af058907e"}, + {file = "tornado-6.5.2-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b186e85d1e3536d69583d2298423744740986018e393d0321df7340e71898882"}, + {file = "tornado-6.5.2-cp39-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e792706668c87709709c18b353da1f7662317b563ff69f00bab83595940c7108"}, + {file = "tornado-6.5.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:06ceb1300fd70cb20e43b1ad8aaee0266e69e7ced38fa910ad2e03285009ce7c"}, + {file = "tornado-6.5.2-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:74db443e0f5251be86cbf37929f84d8c20c27a355dd452a5cfa2aada0d001ec4"}, + {file = "tornado-6.5.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b5e735ab2889d7ed33b32a459cac490eda71a1ba6857b0118de476ab6c366c04"}, + {file = "tornado-6.5.2-cp39-abi3-win32.whl", hash = "sha256:c6f29e94d9b37a95013bb669616352ddb82e3bfe8326fccee50583caebc8a5f0"}, + {file = "tornado-6.5.2-cp39-abi3-win_amd64.whl", hash = "sha256:e56a5af51cc30dd2cae649429af65ca2f6571da29504a07995175df14c18f35f"}, + {file = "tornado-6.5.2-cp39-abi3-win_arm64.whl", hash = "sha256:d6c33dc3672e3a1f3618eb63b7ef4683a7688e7b9e6e8f0d9aa5726360a004af"}, + {file = "tornado-6.5.2.tar.gz", hash = "sha256:ab53c8f9a0fa351e2c0741284e06c7a45da86afb544133201c5cc8578eb076a0"}, +] + +[[package]] +name = "typeguard" +version = "4.4.4" +description = "Run-time type checker for Python" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "typeguard-4.4.4-py3-none-any.whl", hash = "sha256:b5f562281b6bfa1f5492470464730ef001646128b180769880468bd84b68b09e"}, + {file = "typeguard-4.4.4.tar.gz", hash = "sha256:3a7fd2dffb705d4d0efaed4306a704c89b9dee850b688f060a8b1615a79e5f74"}, +] + +[package.dependencies] +typing_extensions = ">=4.14.0" + +[[package]] +name = "typing-extensions" +version = "4.14.1" +description = "Backported and Experimental Type Hints for Python 3.9+" +optional = false +python-versions = ">=3.9" +groups = ["main", "dev", "docs"] +files = [ + {file = "typing_extensions-4.14.1-py3-none-any.whl", hash = "sha256:d1e1e3b58374dc93031d6eda2420a48ea44a36c2b4766a4fdeb3710755731d76"}, + {file = "typing_extensions-4.14.1.tar.gz", hash = "sha256:38b39f4aeeab64884ce9f74c94263ef78f3c22467c8724005483154c26648d36"}, +] + +[[package]] +name = "tzdata" +version = "2025.2" +description = "Provider of IANA time zone data" +optional = false +python-versions = ">=2" +groups = ["main", "dev"] +files = [ + {file = "tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8"}, + {file = "tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9"}, +] +markers = {dev = "sys_platform == \"win32\""} + +[[package]] +name = "urllib3" +version = "2.5.0" +description = "HTTP library with thread-safe connection pooling, file post, and more." +optional = false +python-versions = ">=3.9" +groups = ["main", "dev", "docs"] +files = [ + {file = "urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc"}, + {file = "urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760"}, +] + +[package.extras] +brotli = ["brotli (>=1.0.9) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=0.8.0) ; platform_python_implementation != \"CPython\""] +h2 = ["h2 (>=4,<5)"] +socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] +zstd = ["zstandard (>=0.18.0)"] + +[[package]] +name = "uvicorn" +version = "0.35.0" +description = "The lightning-fast ASGI server." +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "uvicorn-0.35.0-py3-none-any.whl", hash = "sha256:197535216b25ff9b785e29a0b79199f55222193d47f820816e7da751e9bc8d4a"}, + {file = "uvicorn-0.35.0.tar.gz", hash = "sha256:bc662f087f7cf2ce11a1d7fd70b90c9f98ef2e2831556dd078d131b96cc94a01"}, +] + +[package.dependencies] +click = ">=7.0" +colorama = {version = ">=0.4", optional = true, markers = "sys_platform == \"win32\" and extra == \"standard\""} +h11 = ">=0.8" +httptools = {version = ">=0.6.3", optional = true, markers = "extra == \"standard\""} +python-dotenv = {version = ">=0.13", optional = true, markers = "extra == \"standard\""} +pyyaml = {version = ">=5.1", optional = true, markers = "extra == \"standard\""} +uvloop = {version = ">=0.15.1", optional = true, markers = "sys_platform != \"win32\" and sys_platform != \"cygwin\" and platform_python_implementation != \"PyPy\" and extra == \"standard\""} +watchfiles = {version = ">=0.13", optional = true, markers = "extra == \"standard\""} +websockets = {version = ">=10.4", optional = true, markers = "extra == \"standard\""} + +[package.extras] +standard = ["colorama (>=0.4) ; sys_platform == \"win32\"", "httptools (>=0.6.3)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.15.1) ; sys_platform != \"win32\" and sys_platform != \"cygwin\" and platform_python_implementation != \"PyPy\"", "watchfiles (>=0.13)", "websockets (>=10.4)"] + +[[package]] +name = "uvloop" +version = "0.21.0" +description = "Fast implementation of asyncio event loop on top of libuv" +optional = false +python-versions = ">=3.8.0" +groups = ["main"] +markers = "sys_platform != \"win32\" and sys_platform != \"cygwin\" and platform_python_implementation != \"PyPy\"" +files = [ + {file = "uvloop-0.21.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ec7e6b09a6fdded42403182ab6b832b71f4edaf7f37a9a0e371a01db5f0cb45f"}, + {file = "uvloop-0.21.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:196274f2adb9689a289ad7d65700d37df0c0930fd8e4e743fa4834e850d7719d"}, + {file = "uvloop-0.21.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f38b2e090258d051d68a5b14d1da7203a3c3677321cf32a95a6f4db4dd8b6f26"}, + {file = "uvloop-0.21.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:87c43e0f13022b998eb9b973b5e97200c8b90823454d4bc06ab33829e09fb9bb"}, + {file = "uvloop-0.21.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:10d66943def5fcb6e7b37310eb6b5639fd2ccbc38df1177262b0640c3ca68c1f"}, + {file = "uvloop-0.21.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:67dd654b8ca23aed0a8e99010b4c34aca62f4b7fce88f39d452ed7622c94845c"}, + {file = "uvloop-0.21.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c0f3fa6200b3108919f8bdabb9a7f87f20e7097ea3c543754cabc7d717d95cf8"}, + {file = "uvloop-0.21.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0878c2640cf341b269b7e128b1a5fed890adc4455513ca710d77d5e93aa6d6a0"}, + {file = "uvloop-0.21.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b9fb766bb57b7388745d8bcc53a359b116b8a04c83a2288069809d2b3466c37e"}, + {file = "uvloop-0.21.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a375441696e2eda1c43c44ccb66e04d61ceeffcd76e4929e527b7fa401b90fb"}, + {file = "uvloop-0.21.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:baa0e6291d91649c6ba4ed4b2f982f9fa165b5bbd50a9e203c416a2797bab3c6"}, + {file = "uvloop-0.21.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4509360fcc4c3bd2c70d87573ad472de40c13387f5fda8cb58350a1d7475e58d"}, + {file = "uvloop-0.21.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:359ec2c888397b9e592a889c4d72ba3d6befba8b2bb01743f72fffbde663b59c"}, + {file = "uvloop-0.21.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f7089d2dc73179ce5ac255bdf37c236a9f914b264825fdaacaded6990a7fb4c2"}, + {file = "uvloop-0.21.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:baa4dcdbd9ae0a372f2167a207cd98c9f9a1ea1188a8a526431eef2f8116cc8d"}, + {file = "uvloop-0.21.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:86975dca1c773a2c9864f4c52c5a55631038e387b47eaf56210f873887b6c8dc"}, + {file = "uvloop-0.21.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:461d9ae6660fbbafedd07559c6a2e57cd553b34b0065b6550685f6653a98c1cb"}, + {file = "uvloop-0.21.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:183aef7c8730e54c9a3ee3227464daed66e37ba13040bb3f350bc2ddc040f22f"}, + {file = "uvloop-0.21.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:bfd55dfcc2a512316e65f16e503e9e450cab148ef11df4e4e679b5e8253a5281"}, + {file = "uvloop-0.21.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:787ae31ad8a2856fc4e7c095341cccc7209bd657d0e71ad0dc2ea83c4a6fa8af"}, + {file = "uvloop-0.21.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5ee4d4ef48036ff6e5cfffb09dd192c7a5027153948d85b8da7ff705065bacc6"}, + {file = "uvloop-0.21.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3df876acd7ec037a3d005b3ab85a7e4110422e4d9c1571d4fc89b0fc41b6816"}, + {file = "uvloop-0.21.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bd53ecc9a0f3d87ab847503c2e1552b690362e005ab54e8a48ba97da3924c0dc"}, + {file = "uvloop-0.21.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a5c39f217ab3c663dc699c04cbd50c13813e31d917642d459fdcec07555cc553"}, + {file = "uvloop-0.21.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:17df489689befc72c39a08359efac29bbee8eee5209650d4b9f34df73d22e414"}, + {file = "uvloop-0.21.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:bc09f0ff191e61c2d592a752423c767b4ebb2986daa9ed62908e2b1b9a9ae206"}, + {file = "uvloop-0.21.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f0ce1b49560b1d2d8a2977e3ba4afb2414fb46b86a1b64056bc4ab929efdafbe"}, + {file = "uvloop-0.21.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e678ad6fe52af2c58d2ae3c73dc85524ba8abe637f134bf3564ed07f555c5e79"}, + {file = "uvloop-0.21.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:460def4412e473896ef179a1671b40c039c7012184b627898eea5072ef6f017a"}, + {file = "uvloop-0.21.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:10da8046cc4a8f12c91a1c39d1dd1585c41162a15caaef165c2174db9ef18bdc"}, + {file = "uvloop-0.21.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c097078b8031190c934ed0ebfee8cc5f9ba9642e6eb88322b9958b649750f72b"}, + {file = "uvloop-0.21.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:46923b0b5ee7fc0020bef24afe7836cb068f5050ca04caf6b487c513dc1a20b2"}, + {file = "uvloop-0.21.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:53e420a3afe22cdcf2a0f4846e377d16e718bc70103d7088a4f7623567ba5fb0"}, + {file = "uvloop-0.21.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88cb67cdbc0e483da00af0b2c3cdad4b7c61ceb1ee0f33fe00e09c81e3a6cb75"}, + {file = "uvloop-0.21.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:221f4f2a1f46032b403bf3be628011caf75428ee3cc204a22addf96f586b19fd"}, + {file = "uvloop-0.21.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:2d1f581393673ce119355d56da84fe1dd9d2bb8b3d13ce792524e1607139feff"}, + {file = "uvloop-0.21.0.tar.gz", hash = "sha256:3bf12b0fda68447806a7ad847bfa591613177275d35b6724b1ee573faa3704e3"}, +] + +[package.extras] +dev = ["Cython (>=3.0,<4.0)", "setuptools (>=60)"] +docs = ["Sphinx (>=4.1.2,<4.2.0)", "sphinx-rtd-theme (>=0.5.2,<0.6.0)", "sphinxcontrib-asyncio (>=0.3.0,<0.4.0)"] +test = ["aiohttp (>=3.10.5)", "flake8 (>=5.0,<6.0)", "mypy (>=0.800)", "psutil", "pyOpenSSL (>=23.0.0,<23.1.0)", "pycodestyle (>=2.9.0,<2.10.0)"] + +[[package]] +name = "watchfiles" +version = "1.1.0" +description = "Simple, modern and high performance file watching and code reload in python." +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "watchfiles-1.1.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:27f30e14aa1c1e91cb653f03a63445739919aef84c8d2517997a83155e7a2fcc"}, + {file = "watchfiles-1.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3366f56c272232860ab45c77c3ca7b74ee819c8e1f6f35a7125556b198bbc6df"}, + {file = "watchfiles-1.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8412eacef34cae2836d891836a7fff7b754d6bcac61f6c12ba5ca9bc7e427b68"}, + {file = "watchfiles-1.1.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:df670918eb7dd719642e05979fc84704af913d563fd17ed636f7c4783003fdcc"}, + {file = "watchfiles-1.1.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d7642b9bc4827b5518ebdb3b82698ada8c14c7661ddec5fe719f3e56ccd13c97"}, + {file = "watchfiles-1.1.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:199207b2d3eeaeb80ef4411875a6243d9ad8bc35b07fc42daa6b801cc39cc41c"}, + {file = "watchfiles-1.1.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a479466da6db5c1e8754caee6c262cd373e6e6c363172d74394f4bff3d84d7b5"}, + {file = "watchfiles-1.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:935f9edd022ec13e447e5723a7d14456c8af254544cefbc533f6dd276c9aa0d9"}, + {file = "watchfiles-1.1.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:8076a5769d6bdf5f673a19d51da05fc79e2bbf25e9fe755c47595785c06a8c72"}, + {file = "watchfiles-1.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:86b1e28d4c37e89220e924305cd9f82866bb0ace666943a6e4196c5df4d58dcc"}, + {file = "watchfiles-1.1.0-cp310-cp310-win32.whl", hash = "sha256:d1caf40c1c657b27858f9774d5c0e232089bca9cb8ee17ce7478c6e9264d2587"}, + {file = "watchfiles-1.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:a89c75a5b9bc329131115a409d0acc16e8da8dfd5867ba59f1dd66ae7ea8fa82"}, + {file = "watchfiles-1.1.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:c9649dfc57cc1f9835551deb17689e8d44666315f2e82d337b9f07bd76ae3aa2"}, + {file = "watchfiles-1.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:406520216186b99374cdb58bc48e34bb74535adec160c8459894884c983a149c"}, + {file = "watchfiles-1.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb45350fd1dc75cd68d3d72c47f5b513cb0578da716df5fba02fff31c69d5f2d"}, + {file = "watchfiles-1.1.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:11ee4444250fcbeb47459a877e5e80ed994ce8e8d20283857fc128be1715dac7"}, + {file = "watchfiles-1.1.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bda8136e6a80bdea23e5e74e09df0362744d24ffb8cd59c4a95a6ce3d142f79c"}, + {file = "watchfiles-1.1.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b915daeb2d8c1f5cee4b970f2e2c988ce6514aace3c9296e58dd64dc9aa5d575"}, + {file = "watchfiles-1.1.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ed8fc66786de8d0376f9f913c09e963c66e90ced9aa11997f93bdb30f7c872a8"}, + {file = "watchfiles-1.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fe4371595edf78c41ef8ac8df20df3943e13defd0efcb732b2e393b5a8a7a71f"}, + {file = "watchfiles-1.1.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:b7c5f6fe273291f4d414d55b2c80d33c457b8a42677ad14b4b47ff025d0893e4"}, + {file = "watchfiles-1.1.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:7738027989881e70e3723c75921f1efa45225084228788fc59ea8c6d732eb30d"}, + {file = "watchfiles-1.1.0-cp311-cp311-win32.whl", hash = "sha256:622d6b2c06be19f6e89b1d951485a232e3b59618def88dbeda575ed8f0d8dbf2"}, + {file = "watchfiles-1.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:48aa25e5992b61debc908a61ab4d3f216b64f44fdaa71eb082d8b2de846b7d12"}, + {file = "watchfiles-1.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:00645eb79a3faa70d9cb15c8d4187bb72970b2470e938670240c7998dad9f13a"}, + {file = "watchfiles-1.1.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:9dc001c3e10de4725c749d4c2f2bdc6ae24de5a88a339c4bce32300a31ede179"}, + {file = "watchfiles-1.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d9ba68ec283153dead62cbe81872d28e053745f12335d037de9cbd14bd1877f5"}, + {file = "watchfiles-1.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:130fc497b8ee68dce163e4254d9b0356411d1490e868bd8790028bc46c5cc297"}, + {file = "watchfiles-1.1.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:50a51a90610d0845a5931a780d8e51d7bd7f309ebc25132ba975aca016b576a0"}, + {file = "watchfiles-1.1.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dc44678a72ac0910bac46fa6a0de6af9ba1355669b3dfaf1ce5f05ca7a74364e"}, + {file = "watchfiles-1.1.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a543492513a93b001975ae283a51f4b67973662a375a403ae82f420d2c7205ee"}, + {file = "watchfiles-1.1.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8ac164e20d17cc285f2b94dc31c384bc3aa3dd5e7490473b3db043dd70fbccfd"}, + {file = "watchfiles-1.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f7590d5a455321e53857892ab8879dce62d1f4b04748769f5adf2e707afb9d4f"}, + {file = "watchfiles-1.1.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:37d3d3f7defb13f62ece99e9be912afe9dd8a0077b7c45ee5a57c74811d581a4"}, + {file = "watchfiles-1.1.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:7080c4bb3efd70a07b1cc2df99a7aa51d98685be56be6038c3169199d0a1c69f"}, + {file = "watchfiles-1.1.0-cp312-cp312-win32.whl", hash = "sha256:cbcf8630ef4afb05dc30107bfa17f16c0896bb30ee48fc24bf64c1f970f3b1fd"}, + {file = "watchfiles-1.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:cbd949bdd87567b0ad183d7676feb98136cde5bb9025403794a4c0db28ed3a47"}, + {file = "watchfiles-1.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:0a7d40b77f07be87c6faa93d0951a0fcd8cbca1ddff60a1b65d741bac6f3a9f6"}, + {file = "watchfiles-1.1.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:5007f860c7f1f8df471e4e04aaa8c43673429047d63205d1630880f7637bca30"}, + {file = "watchfiles-1.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:20ecc8abbd957046f1fe9562757903f5eaf57c3bce70929fda6c7711bb58074a"}, + {file = "watchfiles-1.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2f0498b7d2a3c072766dba3274fe22a183dbea1f99d188f1c6c72209a1063dc"}, + {file = "watchfiles-1.1.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:239736577e848678e13b201bba14e89718f5c2133dfd6b1f7846fa1b58a8532b"}, + {file = "watchfiles-1.1.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eff4b8d89f444f7e49136dc695599a591ff769300734446c0a86cba2eb2f9895"}, + {file = "watchfiles-1.1.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12b0a02a91762c08f7264e2e79542f76870c3040bbc847fb67410ab81474932a"}, + {file = "watchfiles-1.1.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:29e7bc2eee15cbb339c68445959108803dc14ee0c7b4eea556400131a8de462b"}, + {file = "watchfiles-1.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d9481174d3ed982e269c090f780122fb59cee6c3796f74efe74e70f7780ed94c"}, + {file = "watchfiles-1.1.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:80f811146831c8c86ab17b640801c25dc0a88c630e855e2bef3568f30434d52b"}, + {file = "watchfiles-1.1.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:60022527e71d1d1fda67a33150ee42869042bce3d0fcc9cc49be009a9cded3fb"}, + {file = "watchfiles-1.1.0-cp313-cp313-win32.whl", hash = "sha256:32d6d4e583593cb8576e129879ea0991660b935177c0f93c6681359b3654bfa9"}, + {file = "watchfiles-1.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:f21af781a4a6fbad54f03c598ab620e3a77032c5878f3d780448421a6e1818c7"}, + {file = "watchfiles-1.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:5366164391873ed76bfdf618818c82084c9db7fac82b64a20c44d335eec9ced5"}, + {file = "watchfiles-1.1.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:17ab167cca6339c2b830b744eaf10803d2a5b6683be4d79d8475d88b4a8a4be1"}, + {file = "watchfiles-1.1.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:328dbc9bff7205c215a7807da7c18dce37da7da718e798356212d22696404339"}, + {file = "watchfiles-1.1.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f7208ab6e009c627b7557ce55c465c98967e8caa8b11833531fdf95799372633"}, + {file = "watchfiles-1.1.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a8f6f72974a19efead54195bc9bed4d850fc047bb7aa971268fd9a8387c89011"}, + {file = "watchfiles-1.1.0-cp313-cp313t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d181ef50923c29cf0450c3cd47e2f0557b62218c50b2ab8ce2ecaa02bd97e670"}, + {file = "watchfiles-1.1.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:adb4167043d3a78280d5d05ce0ba22055c266cf8655ce942f2fb881262ff3cdf"}, + {file = "watchfiles-1.1.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c5701dc474b041e2934a26d31d39f90fac8a3dee2322b39f7729867f932b1d4"}, + {file = "watchfiles-1.1.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b067915e3c3936966a8607f6fe5487df0c9c4afb85226613b520890049deea20"}, + {file = "watchfiles-1.1.0-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:9c733cda03b6d636b4219625a4acb5c6ffb10803338e437fb614fef9516825ef"}, + {file = "watchfiles-1.1.0-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:cc08ef8b90d78bfac66f0def80240b0197008e4852c9f285907377b2947ffdcb"}, + {file = "watchfiles-1.1.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:9974d2f7dc561cce3bb88dfa8eb309dab64c729de85fba32e98d75cf24b66297"}, + {file = "watchfiles-1.1.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c68e9f1fcb4d43798ad8814c4c1b61547b014b667216cb754e606bfade587018"}, + {file = "watchfiles-1.1.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:95ab1594377effac17110e1352989bdd7bdfca9ff0e5eeccd8c69c5389b826d0"}, + {file = "watchfiles-1.1.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fba9b62da882c1be1280a7584ec4515d0a6006a94d6e5819730ec2eab60ffe12"}, + {file = "watchfiles-1.1.0-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3434e401f3ce0ed6b42569128b3d1e3af773d7ec18751b918b89cd49c14eaafb"}, + {file = "watchfiles-1.1.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fa257a4d0d21fcbca5b5fcba9dca5a78011cb93c0323fb8855c6d2dfbc76eb77"}, + {file = "watchfiles-1.1.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7fd1b3879a578a8ec2076c7961076df540b9af317123f84569f5a9ddee64ce92"}, + {file = "watchfiles-1.1.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:62cc7a30eeb0e20ecc5f4bd113cd69dcdb745a07c68c0370cea919f373f65d9e"}, + {file = "watchfiles-1.1.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:891c69e027748b4a73847335d208e374ce54ca3c335907d381fde4e41661b13b"}, + {file = "watchfiles-1.1.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:12fe8eaffaf0faa7906895b4f8bb88264035b3f0243275e0bf24af0436b27259"}, + {file = "watchfiles-1.1.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:bfe3c517c283e484843cb2e357dd57ba009cff351edf45fb455b5fbd1f45b15f"}, + {file = "watchfiles-1.1.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a9ccbf1f129480ed3044f540c0fdbc4ee556f7175e5ab40fe077ff6baf286d4e"}, + {file = "watchfiles-1.1.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba0e3255b0396cac3cc7bbace76404dd72b5438bf0d8e7cefa2f79a7f3649caa"}, + {file = "watchfiles-1.1.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4281cd9fce9fc0a9dbf0fc1217f39bf9cf2b4d315d9626ef1d4e87b84699e7e8"}, + {file = "watchfiles-1.1.0-cp314-cp314t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6d2404af8db1329f9a3c9b79ff63e0ae7131986446901582067d9304ae8aaf7f"}, + {file = "watchfiles-1.1.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e78b6ed8165996013165eeabd875c5dfc19d41b54f94b40e9fff0eb3193e5e8e"}, + {file = "watchfiles-1.1.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:249590eb75ccc117f488e2fabd1bfa33c580e24b96f00658ad88e38844a040bb"}, + {file = "watchfiles-1.1.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d05686b5487cfa2e2c28ff1aa370ea3e6c5accfe6435944ddea1e10d93872147"}, + {file = "watchfiles-1.1.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:d0e10e6f8f6dc5762adee7dece33b722282e1f59aa6a55da5d493a97282fedd8"}, + {file = "watchfiles-1.1.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:af06c863f152005c7592df1d6a7009c836a247c9d8adb78fef8575a5a98699db"}, + {file = "watchfiles-1.1.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:865c8e95713744cf5ae261f3067861e9da5f1370ba91fc536431e29b418676fa"}, + {file = "watchfiles-1.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:42f92befc848bb7a19658f21f3e7bae80d7d005d13891c62c2cd4d4d0abb3433"}, + {file = "watchfiles-1.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa0cc8365ab29487eb4f9979fd41b22549853389e22d5de3f134a6796e1b05a4"}, + {file = "watchfiles-1.1.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:90ebb429e933645f3da534c89b29b665e285048973b4d2b6946526888c3eb2c7"}, + {file = "watchfiles-1.1.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c588c45da9b08ab3da81d08d7987dae6d2a3badd63acdb3e206a42dbfa7cb76f"}, + {file = "watchfiles-1.1.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7c55b0f9f68590115c25272b06e63f0824f03d4fc7d6deed43d8ad5660cabdbf"}, + {file = "watchfiles-1.1.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd17a1e489f02ce9117b0de3c0b1fab1c3e2eedc82311b299ee6b6faf6c23a29"}, + {file = "watchfiles-1.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:da71945c9ace018d8634822f16cbc2a78323ef6c876b1d34bbf5d5222fd6a72e"}, + {file = "watchfiles-1.1.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:51556d5004887045dba3acdd1fdf61dddea2be0a7e18048b5e853dcd37149b86"}, + {file = "watchfiles-1.1.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04e4ed5d1cd3eae68c89bcc1a485a109f39f2fd8de05f705e98af6b5f1861f1f"}, + {file = "watchfiles-1.1.0-cp39-cp39-win32.whl", hash = "sha256:c600e85f2ffd9f1035222b1a312aff85fd11ea39baff1d705b9b047aad2ce267"}, + {file = "watchfiles-1.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:3aba215958d88182e8d2acba0fdaf687745180974946609119953c0e112397dc"}, + {file = "watchfiles-1.1.0-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:3a6fd40bbb50d24976eb275ccb55cd1951dfb63dbc27cae3066a6ca5f4beabd5"}, + {file = "watchfiles-1.1.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:9f811079d2f9795b5d48b55a37aa7773680a5659afe34b54cc1d86590a51507d"}, + {file = "watchfiles-1.1.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a2726d7bfd9f76158c84c10a409b77a320426540df8c35be172444394b17f7ea"}, + {file = "watchfiles-1.1.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:df32d59cb9780f66d165a9a7a26f19df2c7d24e3bd58713108b41d0ff4f929c6"}, + {file = "watchfiles-1.1.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:0ece16b563b17ab26eaa2d52230c9a7ae46cf01759621f4fbbca280e438267b3"}, + {file = "watchfiles-1.1.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:51b81e55d40c4b4aa8658427a3ee7ea847c591ae9e8b81ef94a90b668999353c"}, + {file = "watchfiles-1.1.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2bcdc54ea267fe72bfc7d83c041e4eb58d7d8dc6f578dfddb52f037ce62f432"}, + {file = "watchfiles-1.1.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:923fec6e5461c42bd7e3fd5ec37492c6f3468be0499bc0707b4bbbc16ac21792"}, + {file = "watchfiles-1.1.0-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:7b3443f4ec3ba5aa00b0e9fa90cf31d98321cbff8b925a7c7b84161619870bc9"}, + {file = "watchfiles-1.1.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:7049e52167fc75fc3cc418fc13d39a8e520cbb60ca08b47f6cedb85e181d2f2a"}, + {file = "watchfiles-1.1.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54062ef956807ba806559b3c3d52105ae1827a0d4ab47b621b31132b6b7e2866"}, + {file = "watchfiles-1.1.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a7bd57a1bb02f9d5c398c0c1675384e7ab1dd39da0ca50b7f09af45fa435277"}, + {file = "watchfiles-1.1.0.tar.gz", hash = "sha256:693ed7ec72cbfcee399e92c895362b6e66d63dac6b91e2c11ae03d10d503e575"}, +] + +[package.dependencies] +anyio = ">=3.0.0" + +[[package]] +name = "websockets" +version = "15.0.1" +description = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "websockets-15.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d63efaa0cd96cf0c5fe4d581521d9fa87744540d4bc999ae6e08595a1014b45b"}, + {file = "websockets-15.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ac60e3b188ec7574cb761b08d50fcedf9d77f1530352db4eef1707fe9dee7205"}, + {file = "websockets-15.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5756779642579d902eed757b21b0164cd6fe338506a8083eb58af5c372e39d9a"}, + {file = "websockets-15.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fdfe3e2a29e4db3659dbd5bbf04560cea53dd9610273917799f1cde46aa725e"}, + {file = "websockets-15.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c2529b320eb9e35af0fa3016c187dffb84a3ecc572bcee7c3ce302bfeba52bf"}, + {file = "websockets-15.0.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac1e5c9054fe23226fb11e05a6e630837f074174c4c2f0fe442996112a6de4fb"}, + {file = "websockets-15.0.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:5df592cd503496351d6dc14f7cdad49f268d8e618f80dce0cd5a36b93c3fc08d"}, + {file = "websockets-15.0.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0a34631031a8f05657e8e90903e656959234f3a04552259458aac0b0f9ae6fd9"}, + {file = "websockets-15.0.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3d00075aa65772e7ce9e990cab3ff1de702aa09be3940d1dc88d5abf1ab8a09c"}, + {file = "websockets-15.0.1-cp310-cp310-win32.whl", hash = "sha256:1234d4ef35db82f5446dca8e35a7da7964d02c127b095e172e54397fb6a6c256"}, + {file = "websockets-15.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:39c1fec2c11dc8d89bba6b2bf1556af381611a173ac2b511cf7231622058af41"}, + {file = "websockets-15.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:823c248b690b2fd9303ba00c4f66cd5e2d8c3ba4aa968b2779be9532a4dad431"}, + {file = "websockets-15.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678999709e68425ae2593acf2e3ebcbcf2e69885a5ee78f9eb80e6e371f1bf57"}, + {file = "websockets-15.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d50fd1ee42388dcfb2b3676132c78116490976f1300da28eb629272d5d93e905"}, + {file = "websockets-15.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d99e5546bf73dbad5bf3547174cd6cb8ba7273062a23808ffea025ecb1cf8562"}, + {file = "websockets-15.0.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:66dd88c918e3287efc22409d426c8f729688d89a0c587c88971a0faa2c2f3792"}, + {file = "websockets-15.0.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8dd8327c795b3e3f219760fa603dcae1dcc148172290a8ab15158cf85a953413"}, + {file = "websockets-15.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8fdc51055e6ff4adeb88d58a11042ec9a5eae317a0a53d12c062c8a8865909e8"}, + {file = "websockets-15.0.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:693f0192126df6c2327cce3baa7c06f2a117575e32ab2308f7f8216c29d9e2e3"}, + {file = "websockets-15.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:54479983bd5fb469c38f2f5c7e3a24f9a4e70594cd68cd1fa6b9340dadaff7cf"}, + {file = "websockets-15.0.1-cp311-cp311-win32.whl", hash = "sha256:16b6c1b3e57799b9d38427dda63edcbe4926352c47cf88588c0be4ace18dac85"}, + {file = "websockets-15.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:27ccee0071a0e75d22cb35849b1db43f2ecd3e161041ac1ee9d2352ddf72f065"}, + {file = "websockets-15.0.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:3e90baa811a5d73f3ca0bcbf32064d663ed81318ab225ee4f427ad4e26e5aff3"}, + {file = "websockets-15.0.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:592f1a9fe869c778694f0aa806ba0374e97648ab57936f092fd9d87f8bc03665"}, + {file = "websockets-15.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0701bc3cfcb9164d04a14b149fd74be7347a530ad3bbf15ab2c678a2cd3dd9a2"}, + {file = "websockets-15.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8b56bdcdb4505c8078cb6c7157d9811a85790f2f2b3632c7d1462ab5783d215"}, + {file = "websockets-15.0.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0af68c55afbd5f07986df82831c7bff04846928ea8d1fd7f30052638788bc9b5"}, + {file = "websockets-15.0.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64dee438fed052b52e4f98f76c5790513235efaa1ef7f3f2192c392cd7c91b65"}, + {file = "websockets-15.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d5f6b181bb38171a8ad1d6aa58a67a6aa9d4b38d0f8c5f496b9e42561dfc62fe"}, + {file = "websockets-15.0.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5d54b09eba2bada6011aea5375542a157637b91029687eb4fdb2dab11059c1b4"}, + {file = "websockets-15.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3be571a8b5afed347da347bfcf27ba12b069d9d7f42cb8c7028b5e98bbb12597"}, + {file = "websockets-15.0.1-cp312-cp312-win32.whl", hash = "sha256:c338ffa0520bdb12fbc527265235639fb76e7bc7faafbb93f6ba80d9c06578a9"}, + {file = "websockets-15.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcd5cf9e305d7b8338754470cf69cf81f420459dbae8a3b40cee57417f4614a7"}, + {file = "websockets-15.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee443ef070bb3b6ed74514f5efaa37a252af57c90eb33b956d35c8e9c10a1931"}, + {file = "websockets-15.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5a939de6b7b4e18ca683218320fc67ea886038265fd1ed30173f5ce3f8e85675"}, + {file = "websockets-15.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:746ee8dba912cd6fc889a8147168991d50ed70447bf18bcda7039f7d2e3d9151"}, + {file = "websockets-15.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:595b6c3969023ecf9041b2936ac3827e4623bfa3ccf007575f04c5a6aa318c22"}, + {file = "websockets-15.0.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c714d2fc58b5ca3e285461a4cc0c9a66bd0e24c5da9911e30158286c9b5be7f"}, + {file = "websockets-15.0.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f3c1e2ab208db911594ae5b4f79addeb3501604a165019dd221c0bdcabe4db8"}, + {file = "websockets-15.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:229cf1d3ca6c1804400b0a9790dc66528e08a6a1feec0d5040e8b9eb14422375"}, + {file = "websockets-15.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:756c56e867a90fb00177d530dca4b097dd753cde348448a1012ed6c5131f8b7d"}, + {file = "websockets-15.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:558d023b3df0bffe50a04e710bc87742de35060580a293c2a984299ed83bc4e4"}, + {file = "websockets-15.0.1-cp313-cp313-win32.whl", hash = "sha256:ba9e56e8ceeeedb2e080147ba85ffcd5cd0711b89576b83784d8605a7df455fa"}, + {file = "websockets-15.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:e09473f095a819042ecb2ab9465aee615bd9c2028e4ef7d933600a8401c79561"}, + {file = "websockets-15.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:5f4c04ead5aed67c8a1a20491d54cdfba5884507a48dd798ecaf13c74c4489f5"}, + {file = "websockets-15.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:abdc0c6c8c648b4805c5eacd131910d2a7f6455dfd3becab248ef108e89ab16a"}, + {file = "websockets-15.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a625e06551975f4b7ea7102bc43895b90742746797e2e14b70ed61c43a90f09b"}, + {file = "websockets-15.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d591f8de75824cbb7acad4e05d2d710484f15f29d4a915092675ad3456f11770"}, + {file = "websockets-15.0.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:47819cea040f31d670cc8d324bb6435c6f133b8c7a19ec3d61634e62f8d8f9eb"}, + {file = "websockets-15.0.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac017dd64572e5c3bd01939121e4d16cf30e5d7e110a119399cf3133b63ad054"}, + {file = "websockets-15.0.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:4a9fac8e469d04ce6c25bb2610dc535235bd4aa14996b4e6dbebf5e007eba5ee"}, + {file = "websockets-15.0.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:363c6f671b761efcb30608d24925a382497c12c506b51661883c3e22337265ed"}, + {file = "websockets-15.0.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:2034693ad3097d5355bfdacfffcbd3ef5694f9718ab7f29c29689a9eae841880"}, + {file = "websockets-15.0.1-cp39-cp39-win32.whl", hash = "sha256:3b1ac0d3e594bf121308112697cf4b32be538fb1444468fb0a6ae4feebc83411"}, + {file = "websockets-15.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:b7643a03db5c95c799b89b31c036d5f27eeb4d259c798e878d6937d71832b1e4"}, + {file = "websockets-15.0.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0c9e74d766f2818bb95f84c25be4dea09841ac0f734d1966f415e4edfc4ef1c3"}, + {file = "websockets-15.0.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:1009ee0c7739c08a0cd59de430d6de452a55e42d6b522de7aa15e6f67db0b8e1"}, + {file = "websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76d1f20b1c7a2fa82367e04982e708723ba0e7b8d43aa643d3dcd404d74f1475"}, + {file = "websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f29d80eb9a9263b8d109135351caf568cc3f80b9928bccde535c235de55c22d9"}, + {file = "websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b359ed09954d7c18bbc1680f380c7301f92c60bf924171629c5db97febb12f04"}, + {file = "websockets-15.0.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:cad21560da69f4ce7658ca2cb83138fb4cf695a2ba3e475e0559e05991aa8122"}, + {file = "websockets-15.0.1-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7f493881579c90fc262d9cdbaa05a6b54b3811c2f300766748db79f098db9940"}, + {file = "websockets-15.0.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:47b099e1f4fbc95b701b6e85768e1fcdaf1630f3cbe4765fa216596f12310e2e"}, + {file = "websockets-15.0.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67f2b6de947f8c757db2db9c71527933ad0019737ec374a8a6be9a956786aaf9"}, + {file = "websockets-15.0.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d08eb4c2b7d6c41da6ca0600c077e93f5adcfd979cd777d747e9ee624556da4b"}, + {file = "websockets-15.0.1-pp39-pypy39_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b826973a4a2ae47ba357e4e82fa44a463b8f168e1ca775ac64521442b19e87f"}, + {file = "websockets-15.0.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:21c1fa28a6a7e3cbdc171c694398b6df4744613ce9b36b1a498e816787e28123"}, + {file = "websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f"}, + {file = "websockets-15.0.1.tar.gz", hash = "sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee"}, +] + +[[package]] +name = "wheel" +version = "0.45.1" +description = "A built-package format for Python" +optional = false +python-versions = ">=3.8" +groups = ["docs"] +files = [ + {file = "wheel-0.45.1-py3-none-any.whl", hash = "sha256:708e7481cc80179af0e556bbf0cc00b8444c7321e2700b8d8580231d13017248"}, + {file = "wheel-0.45.1.tar.gz", hash = "sha256:661e1abd9198507b1409a20c02106d9670b2576e916d58f520316666abca6729"}, +] + +[package.extras] +test = ["pytest (>=6.0.0)", "setuptools (>=65)"] + +[metadata] +lock-version = "2.1" +python-versions = "^3.11" +content-hash = "0cd887abb262277f4fda11a31a27086d6e9f70709f1fb21b89799cb6dc84c152" diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000000000000000000000000000000000000..b5211104c7e6b1d3170e6e85517dbf3b630cfcb6 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,168 @@ +# pyproject.toml corrigé pour Poetry +[tool.poetry] +name = "project5" +version = "0.1.0" +description = "Prédiction de consommation énergétique des bâtiments" +authors = ["francois hellebuyck "] +packages = [{include = "project5", from = "src"}] + +[tool.poetry.group.docs.dependencies] +# Sphinx et extensions essentielles +sphinx = "^7.2.6" +sphinx-rtd-theme = "^1.3.0" +sphinx-autodoc-typehints = "^1.25.2" +sphinx-copybutton = "^0.5.2" +myst-parser = "^2.0.0" +sphinx-autobuild = "^2021.3.14" +sphinxcontrib-napoleon = "^0.7" + +# Extensions optionnelles mais recommandées +sphinx-book-theme = "^1.0.1" # Thème moderne alternatif +sphinxext-rediraffe = "^0.2.7" # Redirections +sphinx-design = "^0.5.0" # Composants modernes +sphinx-tabs = "^3.4.4" # Onglets +sphinx-togglebutton = "^0.3.2" # Boutons pliables + +[tool.poetry.dependencies] +python = "^3.11" +fastapi = "^0.116.1" +uvicorn = {extras = ["standard"], version = "^0.35.0"} +# Base de données +sqlalchemy = "^2.0.23" +alembic = "^1.12.1" +psycopg2-binary = "^2.9.9" +# Configuration et sécurité +pydantic = "^2.5.0" +pydantic-settings = "^2.1.0" +python-dotenv = "^1.0.0" +passlib = {extras = ["bcrypt"], version = "^1.7.4"} +# Utilitaires +httpx = "^0.25.0" +python-jwt = "^4.1.0" +joblib = "^1.5.2" +numpy = "^2.3.3" +scikit-learn = "1.7.1" +pandas = "^2.3.2" +shibuya = "^2025.8.16" +eralchemy = "^1.3.0" +graphviz = "^0.20.0" + + +[tool.poetry.group.dev.dependencies] +pylint = "^3.3.8" +pytest = "^7.4.0" +pytest-cov = "^4.1.0" +pytest-asyncio = "^0.21.0" +httpx = "^0.25.0" +black = "^24.0.0" +isort = "^5.12.0" +flake8 = "^6.1.0" +mypy = "^1.6.0" +bandit = "^1.7.5" +safety = "^2.3.0" +# Tests de base de données +pytest-postgresql = "^5.0.0" +aiosqlite = "^0.19.0" +sqlacodegen = "^3.1.0" + + +[build-system] +requires = ["poetry-core>=2.0.0,<3.0.0"] +build-backend = "poetry.core.masonry.api" + +[tool.black] +line-length = 88 +target-version = ['py311'] +include = '\.pyi?$' +extend-exclude = ''' +/( + # directories + \.eggs + | \.git + | \.hg + | \.mypy_cache + | \.tox + | \.venv + | build + | dist + | alembic/versions +)/ +''' + +[tool.isort] +profile = "black" +multi_line_output = 3 +include_trailing_comma = true +force_grid_wrap = 0 +use_parentheses = true +ensure_newline_before_comments = true +line_length = 88 + +[tool.mypy] +python_version = "3.11" +warn_return_any = true +warn_unused_configs = true +disallow_untyped_defs = true +plugins = ["sqlalchemy.ext.mypy.plugin"] + +[tool.pytest.ini_options] +minversion = "6.0" +addopts = "-ra -q --strict-markers" +testpaths = [ + "tests", +] +pythonpath = ["./src"] +asyncio_mode = "auto" +markers = [ + "unit: Unit tests", + "integration: Integration tests", + "database: Tests requiring database", + "slow: Slow running tests", +] + +[tool.coverage.run] +source = ["src"] +omit = [ + "*/tests/*", + "*/venv/*", + "*/.venv/*", + "*/env/*", + "*/alembic/*", + "*/migrations/*", +] + +[tool.coverage.report] +exclude_lines = [ + "pragma: no cover", + "def __repr__", + "if self.debug:", + "if settings.DEBUG", + "raise AssertionError", + "raise NotImplementedError", + "if 0:", + "if __name__ == .__main__.:", + "class .*\\bProtocol\\):", + "@(abc\\.)?abstractmethod", +] + +[tool.flake8] +max-line-length = 88 +extend-ignore = [ + "E203", + "W503", + "E402", + "E501", +] +exclude = [ + ".git", + "__pycache__", + ".venv", + "venv", + ".jupyter", + "alembic/versions", + "migrations", +] +per-file-ignores = [ + "*.ipynb:E402", + "alembic/*:E402,F401", +] diff --git a/src/project5/__init__.py b/src/project5/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..6634f6e97d9aa581497c639e7b762dfb41681082 --- /dev/null +++ b/src/project5/__init__.py @@ -0,0 +1,10 @@ +"""API REST pour gérer les bâtiments""" + +__version__ = "1.0.0" +__author__ = "François Hellebuyck" + +# Exposer certaines classes au niveau du package +from .database import engine, get_db +from .models import Neighborhood + +__all__ = ["get_db", "engine", "Neighborhood"] diff --git a/src/project5/alembic.ini b/src/project5/alembic.ini new file mode 100644 index 0000000000000000000000000000000000000000..d9de61508ef2ff610483fa20fce5e5e98503e5de --- /dev/null +++ b/src/project5/alembic.ini @@ -0,0 +1,72 @@ +# A generic, single database configuration. + +[alembic] +# path to migration scripts +script_location = alembic + +# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s +# Uncomment the line below if you want the files to be prepended with date and time +file_template = %%(year)d%%(month).2d%%(day).2d_%%(hour).2d%%(minute).2d_%%(rev)s_%%(slug)s + +# sys.path path, will be prepended to sys.path if present. +prepend_sys_path = . + +# timezone to use when rendering the date within the migration file +# as well as the filename. +# If specified, requires the python-dateutil library that can be +# installed by adding `alembic[tz]` to the pip requirements +# string value is passed to dateutil.tz.gettz() +# leave blank for localtime +# timezone = + +# max length of characters to apply to the +# "slug" field +# truncate_slug_length = 40 + +# set to 'true' to run the environment during +# the 'revision' command, regardless of autogenerate +# revision_environment = false + +# set to 'true' to allow .pyc and .pyo files without +# a source .py file to be detected as revisions in the +# versions/ directory +# sourceless = false + +# version number format. This value should be incrementing integer, +# typically starting at 1. +# version_num_format = %d + +# Logging configuration +[loggers] +keys = root,sqlalchemy,alembic + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARN +handlers = console +qualname = + +[logger_sqlalchemy] +level = WARN +handlers = +qualname = sqlalchemy.engine + +[logger_alembic] +level = INFO +handlers = +qualname = alembic + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(levelname)-5.5s [%(name)s] %(message)s +datefmt = %H:%M:%S \ No newline at end of file diff --git a/src/project5/config.py b/src/project5/config.py new file mode 100644 index 0000000000000000000000000000000000000000..3f4c3400654cd0e3a74c8027dfbda8f7e3b6708d --- /dev/null +++ b/src/project5/config.py @@ -0,0 +1,63 @@ +"""Configuration de l'application.""" + +from typing import Optional + +from pydantic import Field +from pydantic_settings import BaseSettings + + +class Settings(BaseSettings): + """Configuration de l'application via variables d'environnement.""" + + # Configuration de l'application + app_name: str = Field( + default="API de gestion énergétique des bâtiments", env="APP_NAME" + ) + app_version: str = Field(default="1.0.0", env="APP_VERSION") + debug: bool = Field(default=False, env="DEBUG") + environment: str = Field(default="production", env="ENVIRONMENT") + + # Configuration de la base de données + database_url: str = Field(..., env="DATABASE_URL") + db_pool_size: int = Field(default=20, env="DB_POOL_SIZE") + db_max_overflow: int = Field(default=0, env="DB_MAX_OVERFLOW") + db_pool_pre_ping: bool = Field(default=True, env="DB_POOL_PRE_PING") + db_echo: bool = Field(default=False, env="DB_ECHO") + + # Configuration de l'API + api_prefix: str = Field(default="/api/v1", env="API_PREFIX") + cors_origins: list = Field(default=["*"], env="CORS_ORIGINS") + cors_allow_credentials: bool = Field(default=True, env="CORS_ALLOW_CREDENTIALS") + cors_allow_methods: list = Field(default=["GET"], env="CORS_ALLOW_METHODS") + cors_allow_headers: list = Field(default=["*"], env="CORS_ALLOW_HEADERS") + + # Configuration du logging + log_level: str = Field(default="INFO", env="LOG_LEVEL") + log_format: str = Field( + default="%(asctime)s - %(name)s - %(levelname)s - %(message)s", env="LOG_FORMAT" + ) + + # Configuration de sécurité + allowed_hosts: list = Field(default=["*"], env="ALLOWED_HOSTS") + + # Configuration de cache (pour extensions futures) + redis_url: Optional[str] = Field(None, env="REDIS_URL") + cache_ttl: int = Field(default=300, env="CACHE_TTL") # 5 minutes + + # Configuration de monitoring + enable_metrics: bool = Field(default=False, env="ENABLE_METRICS") + metrics_port: int = Field(default=9090, env="METRICS_PORT") + + class Config: + env_file = ".env" + case_sensitive = False + extra = "ignore" # Ignore extra fields from .env + + +# Instance globale des paramètres +settings = Settings() + + +def get_settings() -> Settings: + """Récupère l'instance des paramètres.""" + return settings diff --git a/src/project5/database.py b/src/project5/database.py new file mode 100644 index 0000000000000000000000000000000000000000..96ccbb869d2a69f37e52e20af9b012fdb9a46456 --- /dev/null +++ b/src/project5/database.py @@ -0,0 +1,339 @@ +""" +Configuration et gestion de la base de données pour l'application de prédiction énergétique. + +Ce module configure la connectivité à la base de données avec support adaptatif pour +multiples environnements : PostgreSQL pour la production standard, SQLite en mémoire +pour le déploiement HuggingFace Spaces, et SQLite fichier pour les tests. + +Architecture de déploiement : + - Production normale : PostgreSQL avec pool de connexions optimisé + - HuggingFace Spaces : SQLite en mémoire partagée pour contourner les limitations + - Tests/Développement : SQLite fichier local pour isolation + +Fonctionnalités principales : + - Configuration automatique basée sur variables d'environnement + - Pool de connexions PostgreSQL avec reconnexion automatique + - SQLite en mémoire partagée thread-safe pour HuggingFace + - Initialisation automatique des données en production + - Session management avec pattern Dependency Injection + - Migration et création de tables automatique + +Gestion des environnements : + Le module détecte automatiquement l'environnement via DATABASE_URL et ENVIRONMENT : + - ENVIRONMENT="production" + SQLite -> Mode HuggingFace Spaces + - DATABASE_URL PostgreSQL -> Mode production standard + - Autres cas -> Mode développement/test + +Thread Safety : + - PostgreSQL : Thread-safe natif avec pool de connexions + - SQLite en mémoire : Connexion partagée avec check_same_thread=False + - Sessions SQLAlchemy : Thread-safe par design + +Performance : + - PostgreSQL : Pool configuré pour 20 connexions simultanées + - SQLite : Connexion unique partagée pour minimiser l'empreinte mémoire + - Pool pre-ping activé pour éviter les connexions fermées + +Variables d'environnement : + DATABASE_URL : URL de connexion PostgreSQL ou SQLite + ENVIRONMENT : "production" pour activer le mode HuggingFace Spaces + +Exemple d'usage : + # Dans une route FastAPI + @app.get("/buildings/") + def get_buildings(db: Session = Depends(get_db)): + return db.query(Building).all() + + # Initialisation manuelle + from project5.database import create_tables + create_tables() + +Note : + Le design de ce module prend en compte les contraintes spécifiques de + HuggingFace Spaces qui ne permet pas l'écriture sur disque persistante. +""" + +import os +import sqlite3 +import sys + +from dotenv import load_dotenv +from sqlalchemy import create_engine +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.orm import sessionmaker + +# Charger les variables d'environnement +load_dotenv() + +# URL de connexion à la base de données PostgreSQL +# Exemple : postgresql://user:password@localhost/dbname +DATABASE_URL = os.getenv("DATABASE_URL", "postgresql://user:password@localhost/dbname") +ENVIRONMENT = os.getenv("ENVIRONMENT", "dev") + +# Connexion SQLite partagée pour la production +_shared_sqlite_conn = None + + +def get_shared_sqlite_connection(): + """ + Retourne la connexion SQLite partagée en mémoire pour HuggingFace Spaces. + + Cette fonction implémente un pattern Singleton pour partager une unique + connexion SQLite en mémoire à travers toute l'application. Elle est + spécifiquement conçue pour le déploiement sur HuggingFace Spaces qui + ne permet pas l'écriture sur disque persistante. + + Returns: + sqlite3.Connection: Connexion SQLite en mémoire partagée et thread-safe + + Side Effects: + - Crée la connexion globale lors du premier appel + - Initialise automatiquement les données si ENVIRONMENT="production" + - Modifie la variable globale _shared_sqlite_conn + + Thread Safety: + Connexion configurée avec check_same_thread=False pour permettre + l'utilisation depuis multiple threads FastAPI. + + Performance: + - Base de données entièrement en RAM pour performances maximales + - Pas d'I/O disque, idéal pour HuggingFace Spaces + - Initialisation une seule fois au démarrage + + Example: + >>> conn = get_shared_sqlite_connection() + >>> cursor = conn.cursor() + >>> cursor.execute("SELECT COUNT(*) FROM buildings") + >>> print(cursor.fetchone()[0]) + 9895 + + Note: + Cette fonction ne doit être utilisée qu'en mode production HuggingFace. + Pour les autres environnements, utiliser les sessions SQLAlchemy normales. + + Raises: + Exception: Si l'initialisation des données de production échoue + """ + global _shared_sqlite_conn + if _shared_sqlite_conn is None: + _shared_sqlite_conn = sqlite3.connect(":memory:", check_same_thread=False) + # Initialiser les données si en production + if ENVIRONMENT == "production": + _initialize_production_data() + return _shared_sqlite_conn + + +def _initialize_production_data(): + """ + Initialise les données de production dans la base SQLite en mémoire. + + Cette fonction privée charge automatiquement le dataset complet de + 9895 bâtiments de Seattle dans la base de données en mémoire lors + du démarrage en mode production HuggingFace Spaces. + + Architecture d'initialisation : + 1. Modification dynamique du chemin Python pour accéder à init_db + 2. Monkey-patching de get_db_connection pour utiliser la connexion partagée + 3. Création des tables via create_sqlite_tables() + 4. Insertion des données via init_sqlite_data() + + Side Effects: + - Modifie sys.path temporairement + - Remplace la fonction get_db_connection dans le module init_db + - Crée toutes les tables nécessaires + - Insère 9895 enregistrements de bâtiments + - Affiche des messages de statut + + Performance : + - Opération d'initialisation unique au démarrage + - Toutes les données chargées en RAM pour accès rapide + - Optimisé pour le déploiement HuggingFace Spaces + + Error Handling : + - Capture toutes les exceptions et les re-lève après logging + - Empêche le démarrage de l'application en cas d'échec + - Messages d'erreur détaillés pour debugging + + Example Output: + ✅ Base de données SQLite en mémoire initialisée pour la production + + Raises: + Exception: Toute erreur durant l'initialisation est propagée + après avoir été loggée + + Note : + Cette fonction ne doit être appelée que depuis get_shared_sqlite_connection() + et uniquement en mode production. + """ + try: + # Import des fonctions d'initialisation + sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..")) + # Remplacer temporairement la fonction get_db_connection + import init_db + from init_db import create_sqlite_tables, init_sqlite_data + + init_db.get_db_connection = get_shared_sqlite_connection + + # Créer les tables et insérer les données + create_sqlite_tables() + init_sqlite_data() + + print("✅ Base de données SQLite en mémoire initialisée pour la production") + + except Exception as e: + print(f"❌ Erreur lors de l'initialisation des données de production: {e}") + raise + + +# Créer le moteur de base de données avec configuration adaptative +if DATABASE_URL.startswith("sqlite"): + if ENVIRONMENT == "production": + print( + "🐳 Utilisation de SQLite en mémoire partagée pour la production HuggingFace" + ) + # Créer un moteur qui utilise la connexion partagée + engine = create_engine( + "sqlite:///:memory:", + echo=False, + connect_args={"check_same_thread": False}, + creator=get_shared_sqlite_connection, + ) + else: + print("⚠️ Utilisation de SQLite pour les tests uniquement.") + # Configuration pour SQLite (tests) + engine = create_engine( + DATABASE_URL, echo=False, connect_args={"check_same_thread": False} + ) +else: + # Configuration pour PostgreSQL (production normale) + engine = create_engine( + DATABASE_URL, + pool_size=20, + max_overflow=0, + pool_pre_ping=True, + echo=False, # Mettre à True pour voir les requêtes SQL + ) + +# Créer la session +SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) + +# Base pour nos modèles +Base = declarative_base() + + +def get_db(): + """ + Générateur de sessions de base de données pour injection de dépendance FastAPI. + + Cette fonction implémente le pattern Dependency Injection de FastAPI pour + fournir des sessions de base de données aux routes. Elle garantit la fermeture + automatique des sessions même en cas d'exception. + + Yields: + sqlalchemy.orm.Session: Session de base de données configurée et prête + + Architecture : + - Pattern Generator pour gestion automatique du cycle de vie + - Utilise SessionLocal configuré selon l'environnement + - Fermeture garantie via bloc finally + - Compatible avec toutes les configurations (PostgreSQL/SQLite) + + Usage dans FastAPI : + @app.get("/buildings/{building_id}") + def get_building(building_id: int, db: Session = Depends(get_db)): + return db.query(Building).filter(Building.id == building_id).first() + + Thread Safety : + Chaque appel crée une nouvelle session isolée, garantissant + la thread-safety dans un environnement multi-threadé. + + Performance : + - Sessions créées à la demande (lazy loading) + - Réutilisation du pool de connexions configuré + - Pas de connexions persistantes inutiles + + Error Handling : + Les exceptions dans les routes n'empêchent pas la fermeture + de la session grâce au bloc finally. + + Example: + # Utilisation directe (non recommandé) + for db in get_db(): + buildings = db.query(Building).all() + break + + # Utilisation recommandée avec FastAPI + @app.get("/predict") + def predict(data: PredictionRequest, db: Session = Depends(get_db)): + # db est automatiquement fournie et fermée + return prediction_service.predict(data, db) + + Note : + Cette fonction est conçue pour être utilisée exclusivement comme + dépendance FastAPI. Pour un usage direct, considérer SessionLocal(). + """ + db = SessionLocal() + try: + yield db + finally: + db.close() + + +def create_tables(): + """ + Crée toutes les tables dans la base de données selon les modèles définis. + + Cette fonction utilise SQLAlchemy pour créer automatiquement toutes les + tables nécessaires à l'application basées sur les métadonnées des modèles. + Elle est idempotente et peut être appelée multiple fois sans effet de bord. + + Architecture : + - Utilise Base.metadata.create_all() pour création déclarative + - Compatible avec toutes les configurations de base de données + - Respecte les contraintes et index définis dans les modèles + - Opération idempotente (pas d'erreur si tables existent) + + Tables créées : + - buildings : Table principale des bâtiments avec 17 attributs + - Autres tables selon les modèles définis dans project5.models + + Side Effects : + - Crée les tables physiques dans la base de données + - Affiche un message de confirmation + - Met à jour le schéma de base de données + + Usage : + # Lors du déploiement initial + from project5.database import create_tables + create_tables() + + # Dans un script d'initialisation + if __name__ == "__main__": + create_tables() + print("Base de données initialisée") + + Compatibilité : + - PostgreSQL : Utilise les types natifs et contraintes + - SQLite : Adaptation automatique des types + - Tous environnements : dev, test, production + + Performance : + - Opération rapide sur bases vides + - Sur bases existantes : vérification de schéma uniquement + - Pas de perte de données sur tables existantes + + Example Output : + Tables created successfully! + + Error Handling : + Les erreurs SQLAlchemy sont propagées vers l'appelant + pour gestion appropriée selon le contexte. + + Note : + Cette fonction doit être appelée après l'importation des modèles + pour garantir que toutes les métadonnées sont disponibles. + """ + from project5.models.base import Base + + Base.metadata.create_all(bind=engine) + print("Tables created successfully!") diff --git a/src/project5/main.py b/src/project5/main.py new file mode 100644 index 0000000000000000000000000000000000000000..077fba52e499d6197d56c3b351a7f85283c5af3e --- /dev/null +++ b/src/project5/main.py @@ -0,0 +1,605 @@ +""" +Point d'entrée principal de l'API de prédiction énergétique des bâtiments de Seattle. + +Ce module configure et lance l'application FastAPI complète pour la prédiction +de consommation énergétique des bâtiments. Il inclut la gestion du cycle de vie, +la configuration des middlewares, la gestion d'erreurs centralisée, et l'intégration +du modèle de machine learning. + +Architecture de l'application: + - FastAPI avec OpenAPI/Swagger automatique + - Middleware CORS pour les appels cross-origin + - Gestion centralisée des exceptions avec mapping HTTP + - Logging structuré des requêtes et réponses + - Chargement automatique du modèle ML au démarrage + - Configuration adaptative selon l'environnement + +Environnements supportés: + - Développement: Base PostgreSQL locale, logging verbose + - Production: Base SQLite en mémoire (HuggingFace Spaces) + - Test: Base SQLite locale pour les tests + +Fonctionnalités principales: + - API REST complète pour la gestion des bâtiments et prédictions + - Modèle ML RandomForestRegressor pour prédictions énergétiques + - Authentification bearer token en production + - Documentation interactive Swagger/ReDoc + - Monitoring avec health checks + - Gestion d'erreurs robuste avec logging + +Sécurité: + - Authentication bearer token en environnement production + - Validation des données avec Pydantic + - Gestion sécurisée des erreurs (pas d'exposition de détails techniques) + - Configuration CORS appropriée + +Performance: + - Pool de connexions PostgreSQL optimisé + - Cache du modèle ML en mémoire + - Logging asynchrone pour minimiser l'impact + - Middleware de timing des requêtes + +Routes principales: + - /: Informations sur le service + - /health: Health check pour monitoring + - /docs: Documentation Swagger interactive + - /redoc: Documentation ReDoc alternative + - /neighborhoods/*: Gestion des quartiers de Seattle + - /building-models/*: Gestion des modèles de bâtiments + - /predictions/*: Prédictions énergétiques ML + +Note: + Configuration adaptative pour déploiement sur HuggingFace Spaces + avec base SQLite en mémoire et données pré-initialisées. +""" + +import logging +import os +import time +from contextlib import asynccontextmanager + +from fastapi import FastAPI, Request +from fastapi.middleware.cors import CORSMiddleware +from fastapi.openapi.utils import get_openapi +from fastapi.responses import JSONResponse +from sqlalchemy.exc import SQLAlchemyError + +from project5.config import get_settings +from project5.database import Base, engine +from project5.schemas import ErrorResponse, ServiceInfo +from project5.utils.exceptions import NotFoundError, ServiceError, ValidationError +from project5.utils.ml_model import MLModel +from project5.utils.model_registry import get_ml_model, set_ml_model + +# Configuration du logging +settings = get_settings() +logging.basicConfig( + level=getattr(logging, settings.log_level.upper()), format=settings.log_format +) +logger = logging.getLogger(__name__) + +ML_MODEL_PATH = os.getenv("ML_MODEL_PATH", "../model/model.pkl") + +ENVIRONMENT = os.getenv("ENVIRONMENT", "dev") + + +@asynccontextmanager +async def lifespan(app: FastAPI): + """ + Gestionnaire du cycle de vie de l'application FastAPI. + + Cette fonction gère les phases de démarrage et d'arrêt de l'application, + incluant l'initialisation de la base de données et le chargement du modèle ML. + + Phases de démarrage: + 1. Création des tables de base de données + 2. Chargement du modèle ML depuis le fichier + 3. Enregistrement du modèle dans le registre global + 4. Validation que l'application est prête + + Phases d'arrêt: + 1. Nettoyage des ressources + 2. Logging de l'arrêt + + Args: + app (FastAPI): Instance de l'application FastAPI + + Yields: + None: Contrôle rendu à l'application pendant son exécution + + Raises: + RuntimeError: Si le modèle ML ne peut pas être chargé + Exception: Toute erreur durant l'initialisation + + Environment Variables: + ML_MODEL_PATH: Chemin vers le fichier du modèle (défaut: "../model/model.pkl") + + Note: + En cas d'échec de démarrage, l'application s'arrête immédiatement + pour éviter un état incohérent (fail-fast pattern). + """ + # Startup + logger.info("🚀 Démarrage de l'application project5 API") + + try: + # Créer les tables si nécessaire + Base.metadata.create_all(bind=engine) + + """Gestionnaire du cycle de vie de l'application""" + # Startup + model = MLModel() + if not model.load_model(ML_MODEL_PATH): + raise RuntimeError("❌ Impossible de charger le modèle") + # Instance globale du modèle + set_ml_model(model) + logger.info(f"✅ Setting ML Model {get_ml_model()}") + logger.info("🎯 Application prête à recevoir des requêtes") + + except Exception as e: + logger.error(f"❌ Erreur lors du démarrage: {e}") + raise + + yield + + # Shutdown + logger.info("🛑 Arrêt de l'application") + + +# Création de l'application FastAPI +app = FastAPI( + title=settings.app_name, + description="API REST pour la gestion énergétique des bâtiments", + version=settings.app_version, + lifespan=lifespan, + docs_url="/docs", + redoc_url="/redoc", + openapi_url="/openapi.json", + contact={ + "name": "François Hellebuyck", + "email": "formation@pm.me", + }, + license_info={ + "name": "", + }, +) + + +def custom_openapi(): + """ + Génère un schéma OpenAPI personnalisé avec configuration de sécurité. + + Cette fonction étend le schéma OpenAPI par défaut pour ajouter : + - Configuration des serveurs (développement et production) + - Schéma d'authentification Bearer Token + - Application automatique de la sécurité à tous les endpoints + + Returns: + dict: Schéma OpenAPI complet avec extensions de sécurité + + Configuration des serveurs: + - Serveur local: http://localhost:8000 (développement) + - Serveur production: https://FrancoisFormation-project5.hf.space + + Authentification: + - Type: HTTP Bearer Token + - Appliquée automatiquement à tous les endpoints + - Affichée dans l'interface Swagger pour les tests + + Caching: + Le schéma est généré une seule fois et mis en cache + pour optimiser les performances. + + Note: + Cette configuration permet l'authentification en production + tout en gardant une interface de test claire. + """ + if app.openapi_schema: + return app.openapi_schema + + openapi_schema = get_openapi( + title=app.title, + version=app.version, + description=app.description, + routes=app.routes, + ) + + openapi_schema["servers"] = [ + {"url": "http://localhost:8000", "description": "Serveur de développement"}, + { + "url": "https://FrancoisFormation-project5.hf.space", + "description": "Serveur de production", + }, + ] + + openapi_schema["components"]["securitySchemes"] = { + "BearerAuth": {"type": "http", "scheme": "bearer"} + } + + for path, path_item in openapi_schema["paths"].items(): + for method, operation in path_item.items(): + if isinstance(operation, dict): + operation["security"] = [{"BearerAuth": []}] + + app.openapi_schema = openapi_schema + return app.openapi_schema + + +app.openapi = custom_openapi + +# Configuration CORS +app.add_middleware( + CORSMiddleware, + allow_origins=settings.cors_origins, + allow_credentials=settings.cors_allow_credentials, + allow_methods=settings.cors_allow_methods, + allow_headers=settings.cors_allow_headers, +) + +# Inclusion des routers +from project5.routers import ( # Importer les routers ici + building_energy_prediction_routes, + building_model_routes, + building_types_routes, + categories_routes, + neighborhoods_routes, + prediction_routes, + properties_routes, +) + +app.include_router(neighborhoods_routes.router) +app.include_router(building_types_routes.router) +app.include_router(categories_routes.router) +app.include_router(properties_routes.router) +app.include_router(building_energy_prediction_routes.router) +app.include_router(building_model_routes.router) +app.include_router(prediction_routes.router) + + +# Gestionnaires d'exceptions personnalisés +@app.exception_handler(NotFoundError) +async def neighborhood_not_found_handler(request: Request, exc: NotFoundError): + """ + Gestionnaire d'exception pour les ressources non trouvées (404). + + Transforme les NotFoundError en réponses HTTP 404 structurées + avec logging approprié et format de réponse standardisé. + + Args: + request (Request): Requête FastAPI avec métadonnées (URL, méthode, etc.) + exc (NotFoundError): Exception personnalisée avec message et code d'erreur + + Returns: + JSONResponse: Réponse HTTP 404 avec : + - detail: Message d'erreur descriptif + - error_code: Code d'erreur standardisé + - path: Chemin de l'endpoint qui a échoué + + Logging: + Log niveau WARNING avec message et chemin pour traçabilité + + Example response: + { + "detail": "Bâtiment avec l'ID 999 non trouvé", + "error_code": "NOT_FOUND", + "path": "/building-models/999" + } + """ + logger.warning(f"Entité non trouvé: {exc.message} - Path: {request.url.path}") + return JSONResponse( + status_code=404, + content=ErrorResponse( + detail=exc.message, error_code=exc.error_code, path=str(request.url.path) + ).dict(), + ) + + +@app.exception_handler(ValidationError) +async def validation_error_handler(request: Request, exc: ValidationError): + """ + Gestionnaire d'exception pour les erreurs de validation métier (422). + + Transforme les ValidationError en réponses HTTP 422 pour indiquer + que les données sont syntaxiquement correctes mais violent des + règles métier. + + Args: + request (Request): Requête FastAPI avec contexte + exc (ValidationError): Exception de validation avec détails + + Returns: + JSONResponse: Réponse HTTP 422 avec format standardisé + + Usage: + Utilisé pour les validations métier qui ne peuvent pas être + exprimées dans les schémas Pydantic (règles complexes, + validations cross-champs, contraintes temporelles). + + Distinction HTTP: + - 400: Erreur de format/syntaxe + - 422: Erreur de validation métier + - 404: Ressource non trouvée + """ + logger.warning(f"Erreur de validation: {exc.message} - Path: {request.url.path}") + return JSONResponse( + status_code=422, + content=ErrorResponse( + detail=exc.message, error_code=exc.error_code, path=str(request.url.path) + ).dict(), + ) + + +@app.exception_handler(ServiceError) +async def service_error_handler(request: Request, exc: ServiceError): + """ + Gestionnaire d'exception pour les erreurs internes de service (500). + + Transforme les ServiceError en réponses HTTP 500 avec masquage + des détails techniques pour la sécurité. + + Args: + request (Request): Requête FastAPI avec contexte + exc (ServiceError): Exception de service avec détails internes + + Returns: + JSONResponse: Réponse HTTP 500 avec message générique + + Sécurité: + Le message détaillé est loggé mais pas exposé au client + pour éviter la fuite d'informations techniques. + + Logging: + Log niveau ERROR avec stack trace complet pour debugging. + + Monitoring: + Ces erreurs devraient déclencher des alertes de monitoring + car elles indiquent des problèmes techniques internes. + """ + logger.error(f"Erreur de service: {exc.message} - Path: {request.url.path}") + return JSONResponse( + status_code=500, + content=ErrorResponse( + detail="Erreur interne du service", + error_code=exc.error_code, + path=str(request.url.path), + ).dict(), + ) + + +@app.exception_handler(SQLAlchemyError) +async def sqlalchemy_error_handler(request: Request, exc: SQLAlchemyError): + """ + Gestionnaire d'exception pour les erreurs de base de données (503). + + Transforme les erreurs SQLAlchemy en réponses HTTP 503 pour indiquer + une indisponibilité temporaire du service de persistance. + + Args: + request (Request): Requête FastAPI avec contexte + exc (SQLAlchemyError): Exception SQLAlchemy (connexion, transaction, etc.) + + Returns: + JSONResponse: Réponse HTTP 503 (Service Unavailable) + + Rationale HTTP 503: + Les erreurs de base de données sont souvent temporaires + (surcharge, maintenance, réseau) et peuvent être résolues + en retentant la requête plus tard. + + Sécurité: + Détails techniques de la base masqués pour éviter l'exposition + d'informations sur la structure de données. + + Monitoring: + Ces erreurs nécessitent une attention immédiate car elles + peuvent indiquer des problèmes d'infrastructure critiques. + """ + logger.error(f"Erreur base de données: {str(exc)} - Path: {request.url.path}") + return JSONResponse( + status_code=503, + content=ErrorResponse( + detail="Service temporairement indisponible", + error_code="DATABASE_ERROR", + path=str(request.url.path), + ).dict(), + ) + + +@app.exception_handler(Exception) +async def general_exception_handler(request: Request, exc: Exception): + """ + Gestionnaire d'exception catch-all pour toutes les erreurs non capturées. + + Ce gestionnaire sert de filet de sécurité pour capturer toutes les + exceptions non prévues et éviter les crashes de l'application. + + Args: + request (Request): Requête FastAPI avec contexte complet + exc (Exception): Exception Python non capturée précédemment + + Returns: + JSONResponse: Réponse HTTP 500 générique sécurisée + + Logging: + Log niveau ERROR avec exc_info=True pour capturer la stack trace + complète, essentielle pour le debugging des erreurs inattendues. + + Sécurité: + Aucun détail technique exposé au client pour éviter la fuite + d'informations potentiellement sensibles. + + Monitoring: + Ces erreurs indiquent des bugs ou des cas limites non prévus + et nécessitent une investigation immédiate. + + Note: + Ce gestionnaire est appelé en dernier recours après tous + les gestionnaires spécifiques. + """ + logger.error( + f"Erreur non gérée: {str(exc)} - Path: {request.url.path}", exc_info=True + ) + return JSONResponse( + status_code=500, + content=ErrorResponse( + detail="Erreur interne du serveur", + error_code="INTERNAL_ERROR", + path=str(request.url.path), + ).dict(), + ) + + +# Middleware de logging des requêtes +@app.middleware("http") +async def log_requests(request: Request, call_next): + """ + Middleware pour le logging structuré de toutes les requêtes HTTP. + + Ce middleware capture et log toutes les requêtes entrantes et sortantes + avec timing et métriques pour le monitoring et le debugging. + + Args: + request (Request): Requête HTTP entrante avec métadonnées + call_next: Fonction pour passer au middleware/route suivant + + Returns: + Response: Réponse HTTP avec headers et timing ajoutés + + Métriques capturées: + - Méthode HTTP (GET, POST, etc.) + - Chemin de l'endpoint + - Adresse IP du client (si disponible) + - Code de statut de réponse + - Temps de traitement en secondes + + Format des logs: + - Entrée: ➡️ GET /neighborhoods - 192.168.1.100 + - Sortie: ⬅️ GET /neighborhoods - Status: 200 - Time: 0.045s + + Performance: + Impact minimal grâce au logging asynchrone et à la + mesure de temps optimisée. + + Monitoring: + Ces logs sont essentiels pour l'analyse des performances, + le debugging des problèmes, et la détection d'anomalies. + """ + start_time = time.time() + + # Log de la requête entrante + logger.info( + f"➡️ {request.method} {request.url.path} - {request.client.host if request.client else 'unknown'}" + ) + + response = await call_next(request) + + # Log de la réponse + process_time = time.time() - start_time + logger.info( + f"⬅️ {request.method} {request.url.path} - " + f"Status: {response.status_code} - " + f"Time: {process_time:.3f}s" + ) + + return response + + +# Routes principales +@app.get( + "/", + response_model=ServiceInfo, + tags=["Info"], + summary="Informations du service", + description="Informations générales sur l'API", +) +async def root(): + """ + Endpoint racine fournissant les informations du service. + + Retourne les métadonnées essentielles sur l'API pour la découverte + automatique des services et la documentation dynamique. + + Returns: + ServiceInfo: Informations structurées du service contenant : + - name: Nom de l'application depuis la configuration + - version: Version actuelle de l'API + - description: Description fonctionnelle du service + - endpoints: Points d'entrée principaux de documentation + + Usage: + - Point d'entrée pour la découverte du service + - Validation que l'API est opérationnelle + - Navigation vers la documentation interactive + + Example response: + { + "name": "Project5 API", + "version": "1.0.0", + "description": "API REST pour la gestion énergétique des bâtiments", + "endpoints": { + "docs": "/docs", + "redoc": "/redoc" + } + } + + Note: + Endpoint public accessible sans authentification pour faciliter + la découverte et les vérifications de santé automatiques. + """ + service_info = ServiceInfo( + name=settings.app_name, + version=settings.app_version, + description="API REST pour la gestion énergétique des bâtiments", + endpoints={ + "docs": "/docs", + "redoc": "/redoc", + }, + ) + + return service_info + + +@app.get( + "/health", + tags=["Info"], + summary="Vérification de l'état de santé", + description="Endpoint de santé pour les vérifications de disponibilité", +) +async def health_check(): + """ + Endpoint de health check pour le monitoring et l'orchestration. + + Fournit un point de contrôle rapide pour vérifier que l'API + est opérationnelle et prête à traiter les requêtes. + + Returns: + dict: Statut de santé avec métadonnées d'environnement : + - status: "healthy" si le service fonctionne + - environment: Environnement actuel (dev/production) + - authentication_required: Booléen selon l'environnement + + Usage: + - Monitoring automatique par les systèmes d'orchestration + - Health checks Kubernetes/Docker + - Load balancer health probes + - Surveillance continue des services + + Performance: + Endpoint ultra-rapide sans opération coûteuse (pas de DB/ML) + pour des vérifications fréquentes sans impact. + + Example response: + { + "status": "healthy", + "environment": "production", + "authentication_required": true + } + + Note: + Endpoint public accessible sans authentification pour permettre + les vérifications de santé par l'infrastructure. + """ + return { + "status": "healthy", + "environment": ENVIRONMENT, + "authentication_required": ENVIRONMENT == "production", + } diff --git a/src/project5/models/README.md b/src/project5/models/README.md new file mode 100644 index 0000000000000000000000000000000000000000..de339b9ba6d1e44acae56d1c5b12319b4ae03ffa --- /dev/null +++ b/src/project5/models/README.md @@ -0,0 +1,2 @@ +poetry add --group dev sqlacodegen +poetry run sqlacodegen postgresql://project5_user:project5_password@localhost:5432/project5_db --outfile models.py \ No newline at end of file diff --git a/src/project5/models/__init__.py b/src/project5/models/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..94a3fc7c7c5f6c45dedcb59b1d29d6339f52c248 --- /dev/null +++ b/src/project5/models/__init__.py @@ -0,0 +1,41 @@ +""" +Package des modèles SQLAlchemy pour l'API de prédiction énergétique. + +Ce package contient tous les modèles de données utilisés par l'application +pour la gestion des bâtiments et des prédictions énergétiques. + +Modèles disponibles : +- Base : Classe de base DeclarativeBase pour tous les modèles +- Neighborhood : Quartiers de Seattle pour la géolocalisation +- BuildingType : Types de bâtiments (résidentiel, commercial, etc.) +- Categorie : Catégories d'usage (bureaux, logement, éducation, etc.) +- Property : Propriétés d'usage spécifiques (office, restaurant, etc.) +- BuildingModels : Modèle principal des bâtiments avec toutes caractéristiques +- BuildingEnergyPredictions : Prédictions énergétiques calculées par le ML + +Architecture relationnelle : +- Les bâtiments (BuildingModels) sont liés aux quartiers, types, et propriétés +- Les propriétés appartiennent à des catégories +- Les prédictions (BuildingEnergyPredictions) sont liées aux bâtiments +- Tous les modèles incluent des model_id pour l'encodage ML +""" + +# Import du modèle Neighborhood depuis le fichier approprié +from .base import Base +from .building_energy_prediction import BuildingEnergyPredictions +from .building_models import BuildingModels +from .building_type import BuildingType +from .categorie import Categorie +from .neighborhood import Neighborhood +from .property import Property + +# Exposer les modèles au niveau du package +__all__ = [ + "Base", + "Neighborhood", + "BuildingType", + "Categorie", + "Property", + "BuildingModels", + "BuildingEnergyPredictions", +] diff --git a/src/project5/models/base.py b/src/project5/models/base.py new file mode 100644 index 0000000000000000000000000000000000000000..905e58706ba9ba0e8fa4afa4c7391a5f5d08eb52 --- /dev/null +++ b/src/project5/models/base.py @@ -0,0 +1,26 @@ +""" +Base SQLAlchemy pour tous les modèles de données. + +Ce module définit la classe de base DeclarativeBase utilisée par tous +les modèles SQLAlchemy de l'application de prédiction énergétique. +""" + +from sqlalchemy.orm import DeclarativeBase, registry + +mapper_registry = registry() + + +class Base(DeclarativeBase): + """ + Classe de base DeclarativeBase pour tous les modèles SQLAlchemy. + + Cette classe sert de base à tous les modèles de données de l'application. + Elle utilise le registre SQLAlchemy pour gérer les mappings entre + les classes Python et les tables de base de données. + + Note: + Tous les modèles (Neighborhood, BuildingType, Property, etc.) + héritent de cette classe pour bénéficier de la fonctionnalité ORM. + """ + + registry = mapper_registry diff --git a/src/project5/models/building_energy_prediction.py b/src/project5/models/building_energy_prediction.py new file mode 100644 index 0000000000000000000000000000000000000000..e0017440a5e584a53e976cf74eb68047060c58fc --- /dev/null +++ b/src/project5/models/building_energy_prediction.py @@ -0,0 +1,88 @@ +""" +Modèle SQLAlchemy pour les prédictions énergétiques des bâtiments. + +Ce module définit le modèle de données pour stocker les prédictions de consommation +énergétique calculées par le modèle de machine learning. +""" + +import datetime +import decimal +from typing import Optional + +from sqlalchemy import ( + Boolean, + DateTime, + ForeignKeyConstraint, + Index, + Integer, + Numeric, + PrimaryKeyConstraint, + UniqueConstraint, + text, +) +from sqlalchemy.orm import Mapped, mapped_column # TODO: ,relationship + +from .base import Base + + +class BuildingEnergyPredictions(Base): + """ + Modèle représentant une prédiction de consommation énergétique. + + Cette table stocke les résultats des prédictions énergétiques calculées + par le modèle de machine learning pour chaque bâtiment. Elle permet de + conserver l'historique des prédictions et de suivre leur évolution. + + Attributes: + id (int): Identifiant unique de la prédiction (clé primaire) + building_id (int): Identifiant du bâtiment concerné (clé étrangère) + site_energy_use_wn_kbtu (Decimal, optional): Consommation énergétique prédite en kBTU + avec normalisation météorologique + predicted (bool, optional): Indique si la valeur est prédite (True) ou mesurée (False) + updated_at (datetime, optional): Date et heure de la prédiction + + Note: + La consommation est exprimée en kBTU (thousand British Thermal Units) + avec normalisation météorologique pour compenser les variations climatiques. + Le champ 'predicted' permet de distinguer les prédictions ML des mesures réelles. + """ + + __tablename__ = "building_energy_predictions" + __table_args__ = ( + ForeignKeyConstraint( + ["building_id"], + ["building_models.id"], + name="fk_building_energy_predictions", + ), + PrimaryKeyConstraint("id", name="building_energy_predictions_pkey"), + UniqueConstraint( + "building_id", name="building_energy_predictions_building_id_key" + ), + Index("idx_building_energy_predictions_building_id", "building_id"), + {"comment": "Table des prédictions de consommation énergétique des bâtiments"}, + ) + + id: Mapped[int] = mapped_column( + Integer, + primary_key=True, + comment="Identifiant unique de la prédiction (clé primaire auto-incrémentée)", + ) + building_id: Mapped[int] = mapped_column( + Integer, + nullable=False, + comment="Identifiant du bâtiment concerné (clé étrangère vers buildings.id)", + ) + site_energy_use_wn_kbtu: Mapped[Optional[decimal.Decimal]] = mapped_column( + Numeric(15, 2), + comment="Consommation énergétique du site en kBTU avec normalisation météorologique (15 chiffres, 2 décimales)", + ) + predicted: Mapped[Optional[bool]] = mapped_column( + Boolean, + server_default=text("true"), + comment="Indique si la valeur est prédite (TRUE) ou mesurée (FALSE)", + ) + updated_at: Mapped[Optional[datetime.datetime]] = mapped_column( + DateTime(True), + server_default=text("CURRENT_TIMESTAMP"), + comment="Date et heure de la prédiction avec fuseau horaire", + ) diff --git a/src/project5/models/building_models.py b/src/project5/models/building_models.py new file mode 100644 index 0000000000000000000000000000000000000000..c445c0f0849a57cd18cb1dd815b71d5863983412 --- /dev/null +++ b/src/project5/models/building_models.py @@ -0,0 +1,261 @@ +""" +Modèle SQLAlchemy principal pour les bâtiments. + +Ce module définit le modèle de données central pour les bâtiments avec toutes +leurs caractéristiques utilisées pour les prédictions énergétiques. +""" + +import datetime +import decimal +from typing import TYPE_CHECKING, Optional + +from sqlalchemy import ( + CheckConstraint, + DateTime, + ForeignKeyConstraint, + Index, + Integer, + Numeric, + PrimaryKeyConstraint, + String, + Text, + UniqueConstraint, + text, +) +from sqlalchemy.orm import Mapped, mapped_column, relationship + +from .base import Base + +# from project5.models import BuildingType, Neighborhood, Property +# Import seulement pour le type checking +if TYPE_CHECKING: + from .building_type import BuildingType + from .neighborhood import Neighborhood + from .property import Property + + +class BuildingModels(Base): + """ + Modèle principal représentant un bâtiment avec toutes ses caractéristiques. + + Cette table centrale contient toutes les informations d'un bâtiment nécessaires + pour les prédictions énergétiques : localisation, caractéristiques physiques, + types d'énergie utilisés, et références vers les tables de classification. + + Attributes: + id (int): Identifiant unique du bâtiment (clé primaire) + ose_building_id (int): Identifiant OSE (Office of Sustainability & Environment) + address (str): Adresse complète du bâtiment + city (str, optional): Ville (généralement Seattle) + state (str, optional): État (généralement WA) + zip_code (str, optional): Code postal + tax_parcel_identification_number (str, optional): Numéro de parcelle fiscale + council_district_code (str, optional): Code du district municipal + latitude (Decimal, optional): Latitude GPS (-90 à 90) + longitude (Decimal, optional): Longitude GPS (-180 à 180) + year_built (int, optional): Année de construction + number_of_buildings (int, optional): Nombre de bâtiments sur la propriété + number_of_floors (int, optional): Nombre d'étages + property_gfa_total (Decimal, optional): Surface brute totale en pieds carrés + property_gfa_parking (Decimal, optional): Surface de parking en pieds carrés + second_largest_property_use_type_gfa (Decimal, optional): Surface du 2e usage + third_largest_property_use_type_gfa (Decimal, optional): Surface du 3e usage + multiusage (bool, optional): Indique si le bâtiment a plusieurs usages + steam (bool, optional): Utilise la vapeur comme source d'énergie + electricity (bool, optional): Utilise l'électricité + natural_gas (bool, optional): Utilise le gaz naturel + neighborhood_id (int, optional): Identifiant du quartier (clé étrangère) + building_type_id (int, optional): Identifiant du type de bâtiment (clé étrangère) + largest_property_use_type_id (int, optional): Usage principal (clé étrangère) + primary_property_type_id (int, optional): Type principal (clé étrangère) + second_largest_property_use_type_id (int, optional): Usage secondaire (clé étrangère) + third_largest_property_use_type_id (int, optional): Usage tertiaire (clé étrangère) + created_at (datetime): Date de création + updated_at (datetime): Date de modification + + Relations: + neighborhood: Quartier associé + building_type: Type de bâtiment associé + largest_property_use_type: Usage principal + primary_property_type: Type principal + second_largest_property_use_type: Usage secondaire + third_largest_property_use_type: Usage tertiaire + + Note: + Ce modèle est au centre du système de prédiction énergétique. + Toutes les caractéristiques sont utilisées par le modèle ML pour + calculer la consommation énergétique prédite. + """ + + __tablename__ = "building_models" + __table_args__ = ( + CheckConstraint( + "latitude IS NULL AND longitude IS NULL OR latitude >= '-90'::integer::numeric AND latitude <= 90::numeric AND longitude >= '-180'::integer::numeric AND longitude <= 180::numeric", + name="valid_coordinates", + ), + ForeignKeyConstraint( + ["building_type_id"], ["building_type.id"], name="fk_building_type" + ), + ForeignKeyConstraint( + ["largest_property_use_type_id"], + ["property.id"], + name="fk_largest_property_use_type_id", + ), + ForeignKeyConstraint( + ["neighborhood_id"], ["neighborhood.id"], name="fk_neighborhood" + ), + ForeignKeyConstraint( + ["primary_property_type_id"], + ["property.id"], + name="fk_primary_property_type", + ), + ForeignKeyConstraint( + ["second_largest_property_use_type_id"], + ["property.id"], + name="fk_second_property_type", + ), + ForeignKeyConstraint( + ["third_largest_property_use_type_id"], + ["property.id"], + name="fk_third_property_type", + ), + PrimaryKeyConstraint("id", name="building_models_pkey"), + UniqueConstraint("ose_building_id", name="building_models_ose_building_id_key"), + Index("idx_building_models_building_type_id", "building_type_id"), + Index("idx_building_models_neighborhood_id", "neighborhood_id"), + Index( + "idx_building_models_primary_property_type_id", "primary_property_type_id" + ), + Index( + "idx_building_models_second_property_use_type_id", + "second_largest_property_use_type_id", + ), + Index( + "idx_building_models_third_property_use_type_id", + "third_largest_property_use_type_id", + ), + Index("idx_largest_property_use_type_id", "largest_property_use_type_id"), + { + "comment": "Table contenant les données des modèles de bâtiments avec " + "informations énergétiques et structurelles" + }, + ) + + id: Mapped[int] = mapped_column( + Integer, + primary_key=True, + comment="Identifiant du bâtiment concerné (clé étrangère vers buildings.id)", + ) + ose_building_id: Mapped[str] = mapped_column( + String(50), + nullable=False, + comment="Identifiant unique du bâtiment dans le système OSE", + ) + address: Mapped[Optional[str]] = mapped_column(Text, comment="Adresse du bâtiment") + city: Mapped[Optional[str]] = mapped_column( + String(100), + server_default=text("'Seattle'::character varying"), + comment="Ville", + ) + state: Mapped[Optional[str]] = mapped_column( + String(10), server_default=text("'WA'::character varying"), comment="État" + ) + zip_code: Mapped[Optional[str]] = mapped_column(String(20), comment="Code postal") + tax_parcel_identification_number: Mapped[Optional[str]] = mapped_column( + String(100), comment="Numéro d'identification fiscale de la parcelle" + ) + council_district_code: Mapped[Optional[str]] = mapped_column(String(20)) + latitude: Mapped[Optional[decimal.Decimal]] = mapped_column( + Numeric(10, 8), comment="Latitude géographique" + ) + longitude: Mapped[Optional[decimal.Decimal]] = mapped_column( + Numeric(11, 8), comment="Longitude géographique" + ) + year_built: Mapped[Optional[int]] = mapped_column( + Integer, comment="Année de construction du bâtiment" + ) + number_of_buildings: Mapped[Optional[int]] = mapped_column( + Integer, comment="Nombre de bâtiments" + ) + number_of_floors: Mapped[Optional[int]] = mapped_column( + Integer, comment="Nombre d'étages" + ) + property_gfa_total: Mapped[Optional[decimal.Decimal]] = mapped_column( + Numeric(12, 2), comment="Surface brute totale (GFA) en pieds carrés" + ) + property_gfa_parking: Mapped[Optional[decimal.Decimal]] = mapped_column( + Numeric(12, 2), comment="Surface brute de parking en pieds carrés" + ) + second_largest_property_use_type_gfa: Mapped[Optional[decimal.Decimal]] = ( + mapped_column( + Numeric(12, 2), comment="Surface GFA du deuxième type d'usage principal" + ) + ) + third_largest_property_use_type_gfa: Mapped[Optional[decimal.Decimal]] = ( + mapped_column( + Numeric(12, 2), comment="Surface GFA du troisième type d'usage principal" + ) + ) + multiusage: Mapped[Optional[int]] = mapped_column( + Integer, comment="Indicateur si le bâtiment a plusieurs usages" + ) + steam: Mapped[Optional[int]] = mapped_column( + Integer, comment="Indicateur d'utilisation de vapeur" + ) + electricity: Mapped[Optional[int]] = mapped_column( + Integer, comment="Indicateur d'utilisation d'électricité" + ) + natural_gas: Mapped[Optional[int]] = mapped_column( + Integer, comment="Indicateur d'utilisation de gaz naturel" + ) + neighborhood_id: Mapped[Optional[int]] = mapped_column( + Integer, comment="Identifiant du quartier" + ) + building_type_id: Mapped[Optional[int]] = mapped_column( + Integer, comment="Identifiant du type de bâtiment" + ) + largest_property_use_type_id: Mapped[Optional[int]] = mapped_column( + Integer, comment="ID du type d'usage principal le plus important" + ) + primary_property_type_id: Mapped[Optional[int]] = mapped_column( + Integer, comment="ID du type de propriété principal" + ) + second_largest_property_use_type_id: Mapped[Optional[int]] = mapped_column( + Integer, comment="ID du deuxième type d'usage principal" + ) + third_largest_property_use_type_id: Mapped[Optional[int]] = mapped_column( + Integer, comment="ID du troisième type d'usage principal" + ) + created_at: Mapped[Optional[datetime.datetime]] = mapped_column( + DateTime, server_default=text("CURRENT_TIMESTAMP") + ) + updated_at: Mapped[Optional[datetime.datetime]] = mapped_column( + DateTime, server_default=text("CURRENT_TIMESTAMP") + ) + + building_type: Mapped[Optional["BuildingType"]] = relationship( + "BuildingType", back_populates="building_models" + ) + largest_property_use_type: Mapped[Optional["Property"]] = relationship( + "Property", + foreign_keys=[largest_property_use_type_id], + back_populates="building_models", + ) + neighborhood: Mapped[Optional["Neighborhood"]] = relationship( + "Neighborhood", back_populates="building_models" + ) + primary_property_type: Mapped[Optional["Property"]] = relationship( + "Property", + foreign_keys=[primary_property_type_id], + back_populates="building_models_", + ) + second_largest_property_use_type: Mapped[Optional["Property"]] = relationship( + "Property", + foreign_keys=[second_largest_property_use_type_id], + back_populates="building_models1", + ) + third_largest_property_use_type: Mapped[Optional["Property"]] = relationship( + "Property", + foreign_keys=[third_largest_property_use_type_id], + back_populates="building_models2", + ) diff --git a/src/project5/models/building_type.py b/src/project5/models/building_type.py new file mode 100644 index 0000000000000000000000000000000000000000..5b5f3ac41169b63089abd4577f527bd394ea4ed4 --- /dev/null +++ b/src/project5/models/building_type.py @@ -0,0 +1,95 @@ +""" +Modèle SQLAlchemy pour les types de bâtiments. + +Ce module définit le modèle de données pour les types de bâtiments utilisés +dans la classification des constructions pour les prédictions énergétiques. +""" + +import datetime +from typing import TYPE_CHECKING, Optional + +from sqlalchemy import ( + DateTime, + Integer, + PrimaryKeyConstraint, + String, + Text, + UniqueConstraint, + text, +) +from sqlalchemy.orm import Mapped, mapped_column, relationship + +if TYPE_CHECKING: + from .building_models import BuildingModels + +from .base import Base + + +class BuildingType(Base): + """ + Modèle représentant un type de bâtiment. + + Les types de bâtiments sont utilisés pour catégoriser les constructions + selon leur structure et usage général (résidentiel, commercial, industriel, etc.). + Ils sont essentiels pour les prédictions énergétiques car ils déterminent + les caractéristiques de consommation typiques. + + Attributes: + id (int): Identifiant unique du type de bâtiment (clé primaire) + model_id (int): Identifiant utilisé par le modèle ML pour l'encodage + building_type_name (str): Nom du type (CAMPUS, NONRESIDENTIAL, etc.) + description (str, optional): Description détaillée du type + created_at (datetime): Date de création de l'enregistrement + updated_at (datetime): Date de dernière modification + building_models (list[BuildingModels]): Bâtiments de ce type + + Note: + Le model_id est utilisé par le modèle de machine learning pour encoder + numériquement le type de bâtiment lors des prédictions énergétiques. + """ + + __tablename__ = "building_type" + __table_args__ = ( + PrimaryKeyConstraint("id", name="building_type_pkey"), + UniqueConstraint( + "building_type_name", name="building_type_building_type_name_key" + ), + UniqueConstraint("model_id", name="building_type_model_id_key"), + { + "comment": "Table de référence des types de bâtiments pour la classification " + "des constructions" + }, + ) + + id: Mapped[int] = mapped_column( + Integer, + primary_key=True, + comment="Identifiant unique du type de bâtiment (clé primaire auto-incrémentée)", + ) + model_id: Mapped[int] = mapped_column( + Integer, + nullable=False, + comment="Identifiant au modèle associé (clé unique, obligatoire)", + ) + building_type_name: Mapped[str] = mapped_column( + String(100), + nullable=False, + comment="Nom du type de bâtiment (unique, obligatoire, max 100 caractères)", + ) + description: Mapped[Optional[str]] = mapped_column( + Text, comment="Description détaillée du type de bâtiment (optionnel)" + ) + created_at: Mapped[Optional[datetime.datetime]] = mapped_column( + DateTime, + server_default=text("CURRENT_TIMESTAMP"), + comment="Date et heure de création de l'enregistrement", + ) + updated_at: Mapped[Optional[datetime.datetime]] = mapped_column( + DateTime, + server_default=text("CURRENT_TIMESTAMP"), + comment="Date et heure de maj de l'enregistrement", + ) + + building_models: Mapped[list["BuildingModels"]] = relationship( + "BuildingModels", back_populates="building_type" + ) diff --git a/src/project5/models/categorie.py b/src/project5/models/categorie.py new file mode 100644 index 0000000000000000000000000000000000000000..632c9af70d7a2fada94b8032569894788ce6ea90 --- /dev/null +++ b/src/project5/models/categorie.py @@ -0,0 +1,84 @@ +""" +Modèle SQLAlchemy pour les catégories de propriétés. + +Ce module définit le modèle de données pour les catégories d'usage des bâtiments +utilisées dans la classification pour les prédictions énergétiques. +""" + +import datetime +from typing import Optional + +from sqlalchemy import ( + DateTime, + Integer, + PrimaryKeyConstraint, + String, + Text, + UniqueConstraint, + text, +) +from sqlalchemy.orm import Mapped, mapped_column # TODO:, relationship + +from .base import Base + + +class Categorie(Base): + """ + Modèle représentant une catégorie d'usage de propriété. + + Les catégories regroupent les différents usages des bâtiments par domaine + d'activité (résidentiel, commercial, bureaux, éducation, santé, etc.). + Elles servent à classifier les propriétés pour les prédictions énergétiques. + + Attributes: + id (int): Identifiant unique de la catégorie (clé primaire) + category_code (str): Code court de la catégorie (OFFICE, RESIDENTIAL, etc.) + category_name (str): Nom complet en français (Bureaux, Résidentiel, etc.) + description (str, optional): Description détaillée de la catégorie + created_at (datetime): Date de création de l'enregistrement + updated_at (datetime): Date de dernière modification + + Note: + Les catégories sont liées aux propriétés (Property) qui sont plus + granulaires et utilisées directement par le modèle ML. + """ + + __tablename__ = "categories" + __table_args__ = ( + PrimaryKeyConstraint("id", name="categories_pkey"), + UniqueConstraint("category_code", name="categories_category_code_key"), + {"comment": "Table de référence des catégories de bâtiments"}, + ) + + id: Mapped[int] = mapped_column( + Integer, + primary_key=True, + comment="Identifiant unique de la catégorie (clé primaire auto-incrémentée)", + ) + category_code: Mapped[str] = mapped_column( + String(50), + nullable=False, + comment="Code unique de la catégorie (identifiant court, max 50 caractères)", + ) + category_name: Mapped[str] = mapped_column( + String(100), + nullable=False, + comment="Nom complet de la catégorie (obligatoire, max 100 caractères)", + ) + description: Mapped[Optional[str]] = mapped_column( + Text, comment="Description détaillée de la catégorie (optionnel)" + ) + created_at: Mapped[Optional[datetime.datetime]] = mapped_column( + DateTime, + server_default=text("CURRENT_TIMESTAMP"), + comment="Date et heure de création de l'enregistrement", + ) + updated_at: Mapped[Optional[datetime.datetime]] = mapped_column( + DateTime, + server_default=text("CURRENT_TIMESTAMP"), + comment="Date et heure de maj de l'enregistrement", + ) + + # TODO: property: Mapped[list["Property"]] = relationship( + # "Property", back_populates="category" + # ) diff --git a/src/project5/models/neighborhood.py b/src/project5/models/neighborhood.py new file mode 100644 index 0000000000000000000000000000000000000000..040e32e21e110a27e48f39686f4bc0a0e14c42b2 --- /dev/null +++ b/src/project5/models/neighborhood.py @@ -0,0 +1,90 @@ +""" +Modèle SQLAlchemy pour les quartiers de Seattle. + +Ce module définit le modèle de données pour les quartiers (neighborhoods) utilisés +dans la classification géographique des bâtiments pour les prédictions énergétiques. +""" + +import datetime +from typing import TYPE_CHECKING, Optional + +from sqlalchemy import ( + DateTime, + Integer, + PrimaryKeyConstraint, + String, + UniqueConstraint, + text, +) +from sqlalchemy.orm import Mapped, mapped_column, relationship + +if TYPE_CHECKING: + from .building_models import BuildingModels + +from .base import Base + + +class Neighborhood(Base): + """ + Modèle représentant un quartier de Seattle. + + Les quartiers sont utilisés pour la géolocalisation et la classification + géographique des bâtiments. Ils influencent les prédictions énergétiques + car différents quartiers ont des caractéristiques climatiques et urbaines + distinctes. + + Attributes: + id (int): Identifiant unique du quartier (clé primaire) + neighborhood_name (str): Nom du quartier (BALLARD, DOWNTOWN, etc.) + model_id (int): Identifiant utilisé par le modèle ML pour l'encodage + created_at (datetime): Date de création de l'enregistrement + updated_at (datetime): Date de dernière modification + building_models (list[BuildingModels]): Bâtiments associés à ce quartier + + Note: + Le model_id est utilisé par le modèle de machine learning pour encoder + numériquement le quartier lors des prédictions énergétiques. + """ + + __tablename__ = "neighborhood" + __table_args__ = ( + PrimaryKeyConstraint("id", name="neighborhood_pkey"), + UniqueConstraint("model_id", name="neighborhood_model_id_key"), + UniqueConstraint( + "neighborhood_name", name="neighborhood_neighborhood_name_key" + ), + { + "comment": "Table des quartiers/arrondissements pour la classification " + "géographique des bâtiments" + }, + ) + + id: Mapped[int] = mapped_column( + Integer, + primary_key=True, + comment="Identifiant unique du quartier (clé primaire auto-incrémentée)", + ) + neighborhood_name: Mapped[str] = mapped_column( + String(50), + nullable=False, + comment="Nom du quartier (unique, obligatoire, max 50 caractères)", + ) + model_id: Mapped[int] = mapped_column( + Integer, + nullable=False, + comment="Identifiant au modèle associé (clé unique, obligatoire)", + ) + created_at: Mapped[Optional[datetime.datetime]] = mapped_column( + DateTime, + server_default=text("CURRENT_TIMESTAMP"), + comment="Date et heure de création de l'enregistrement", + ) + updated_at: Mapped[Optional[datetime.datetime]] = mapped_column( + DateTime, + server_default=text("CURRENT_TIMESTAMP"), + comment="Date et heure de maj de l'enregistrement", + ) + + building_models: Mapped[list["BuildingModels"]] = relationship( + "BuildingModels", back_populates="neighborhood" + ) diff --git a/src/project5/models/property.py b/src/project5/models/property.py new file mode 100644 index 0000000000000000000000000000000000000000..64322527bc2600bfac21863df3bba4b4891b4b2b --- /dev/null +++ b/src/project5/models/property.py @@ -0,0 +1,119 @@ +""" +Modèle SQLAlchemy pour les propriétés d'usage des bâtiments. + +Ce module définit le modèle de données pour les propriétés d'usage spécifiques +des bâtiments, utilisées pour une classification fine dans les prédictions énergétiques. +""" + +import datetime +from typing import TYPE_CHECKING, Optional + +from sqlalchemy import ( + DateTime, + ForeignKeyConstraint, + Index, + Integer, + PrimaryKeyConstraint, + String, + UniqueConstraint, + text, +) +from sqlalchemy.orm import Mapped, mapped_column, relationship + +if TYPE_CHECKING: + from .building_models import BuildingModels + +from .base import Base + + +class Property(Base): + """ + Modèle représentant une propriété d'usage spécifique d'un bâtiment. + + Les propriétés sont plus granulaires que les catégories et permettent + une classification fine des usages (OFFICE, RETAIL STORE, RESTAURANT, etc.). + Elles sont directement utilisées par le modèle ML pour les prédictions + énergétiques car différents usages ont des profils de consommation distincts. + + Attributes: + id (int): Identifiant unique de la propriété (clé primaire) + model_id (int): Identifiant utilisé par le modèle ML pour l'encodage + property_name (str): Nom de la propriété (OFFICE, RESTAURANT, etc.) + category_id (int): Identifiant de la catégorie parent (clé étrangère) + created_at (datetime): Date de création de l'enregistrement + updated_at (datetime): Date de dernière modification + building_models (list[BuildingModels]): Bâtiments avec cette propriété principale + building_models_ (list[BuildingModels]): Bâtiments avec cette propriété primaire + building_models1 (list[BuildingModels]): Bâtiments avec cette propriété secondaire + building_models2 (list[BuildingModels]): Bâtiments avec cette propriété tertiaire + + Note: + Le model_id est utilisé directement par le modèle de machine learning + pour encoder numériquement l'usage lors des prédictions énergétiques. + Les relations multiples permettent de gérer les bâtiments multi-usages. + """ + + __tablename__ = "property" + __table_args__ = ( + ForeignKeyConstraint( + ["category_id"], ["categories.id"], name="fk_property_category" + ), + PrimaryKeyConstraint("id", name="property_pkey"), + UniqueConstraint("model_id", name="property_model_id_key"), + UniqueConstraint("property_name", name="property_property_name_key"), + Index("idx_property_category", "category_id"), + {"comment": "Table des propriétés/bâtiments avec leur catégorie associée"}, + ) + + id: Mapped[int] = mapped_column( + Integer, + primary_key=True, + comment="Identifiant unique de la propriété (clé primaire auto-incrémentée)", + ) + model_id: Mapped[int] = mapped_column( + Integer, + nullable=False, + comment="Identifiant au modèle associé (clé unique, obligatoire)", + ) + property_name: Mapped[str] = mapped_column( + String(150), + nullable=False, + comment="Nom de la propriété (unique, obligatoire, max 150 caractères)", + ) + category_id: Mapped[int] = mapped_column( + Integer, + nullable=False, + comment="Identifiant de la catégorie associée (clé étrangère vers categories.id)", + ) + created_at: Mapped[Optional[datetime.datetime]] = mapped_column( + DateTime, + server_default=text("CURRENT_TIMESTAMP"), + comment="Date et heure de création de l'enregistrement", + ) + updated_at: Mapped[Optional[datetime.datetime]] = mapped_column( + DateTime, + server_default=text("CURRENT_TIMESTAMP"), + comment="Date et heure de maj de l'enregistrement", + ) + + # category: Mapped['Categories'] = relationship('Categories', back_populates='property') + building_models: Mapped[list["BuildingModels"]] = relationship( + "BuildingModels", + foreign_keys="[BuildingModels.largest_property_use_type_id]", + back_populates="largest_property_use_type", + ) + building_models_: Mapped[list["BuildingModels"]] = relationship( + "BuildingModels", + foreign_keys="[BuildingModels.primary_property_type_id]", + back_populates="primary_property_type", + ) + building_models1: Mapped[list["BuildingModels"]] = relationship( + "BuildingModels", + foreign_keys="[BuildingModels.second_largest_property_use_type_id]", + back_populates="second_largest_property_use_type", + ) + building_models2: Mapped[list["BuildingModels"]] = relationship( + "BuildingModels", + foreign_keys="[BuildingModels.third_largest_property_use_type_id]", + back_populates="third_largest_property_use_type", + ) diff --git a/src/project5/routers/__init__.py b/src/project5/routers/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/src/project5/routers/building_energy_prediction_routes.py b/src/project5/routers/building_energy_prediction_routes.py new file mode 100644 index 0000000000000000000000000000000000000000..77283756c9dc8c979294937deaaa07fa2cea2438 --- /dev/null +++ b/src/project5/routers/building_energy_prediction_routes.py @@ -0,0 +1,334 @@ +""" +Routes API pour la gestion des prédictions énergétiques des bâtiments. + +Ce module contient les endpoints FastAPI pour : +- Récupération de la liste des prédictions énergétiques avec pagination +- Récupération d'une prédiction spécifique par ID +- Gestion des erreurs et logging approprié + +Endpoints disponibles : +- GET /api/v1/predictions/ : Liste paginée des prédictions +- GET /api/v1/predictions/{id} : Prédiction spécifique par ID +- POST /api/v1/predictions/building/{building_id} : Effectuer une prédiction pour un bâtiment et la sauvegarder + +Auteur: François Hellebuyck +Projet: Building Energy Prediction API - OpenClassrooms Projet 5 +""" + +import logging +from typing import List + +from fastapi import APIRouter, Depends, HTTPException, Query, status +from fastapi.encoders import jsonable_encoder +from fastapi.responses import JSONResponse +from sqlalchemy.orm import Session + +from project5.database import get_db +from project5.schemas.building_energy_prediction import ( + BuildingEnergyPredictionsList, + BuildingEnergyPredictionsResponse, +) +from project5.schemas.building_model import PredictionResponse +from project5.services.building_ensergy_prediction_service import ( + BuildingEnergyPredictionService, +) +from project5.services.building_model_service import ( + BuildingModelsService, + PredictionService, +) +from project5.utils.exceptions import NotFoundError + +logger = logging.getLogger(__name__) + +router = APIRouter(prefix="/api/v1/predictions", tags=["predictions"]) + + +@router.get( + "/", + response_model=List[BuildingEnergyPredictionsList], + summary="Récupérer toutes les prédictions énergétiques des bâtiments", + description=""" + Retourne la liste paginée de toutes les prédictions énergétiques des bâtiments. + + Cette endpoint permet de récupérer l'ensemble des prédictions de consommation + énergétique calculées par le modèle de machine learning. Les résultats incluent + les données du bâtiment associé et la valeur prédite. + + Paramètres de pagination : + - skip : nombre d'enregistrements à ignorer (par défaut 0) + - limit : nombre maximum d'enregistrements à retourner (par défaut 100, max 1000) + """, + responses={ + 200: { + "description": "Liste des prédictions récupérée avec succès", + "content": { + "application/json": { + "example": [ + { + "id": 1, + "building_id": 123, + "site_energy_use_kbtu": 45678.9, + "predicted": True, + "updated_at": "2025-09-17T10:30:00Z", + } + ] + } + }, + }, + 500: {"description": "Erreur interne du serveur"}, + }, +) +def get_predictions( + skip: int = Query(0, ge=0, description="Nombre d'éléments à ignorer (pagination)"), + limit: int = Query( + 100, ge=1, le=1000, description="Nombre maximum d'éléments à retourner" + ), + db: Session = Depends(get_db), +): + """ + Récupère la liste paginée des prédictions énergétiques des bâtiments. + + Args: + skip (int): Nombre d'enregistrements à ignorer pour la pagination + limit (int): Nombre maximum d'enregistrements à retourner + db (Session): Session de base de données injectée par FastAPI + + Returns: + JSONResponse: Liste des prédictions énergétiques au format JSON + + Raises: + HTTPException: Erreur 500 si problème lors de la récupération des données + + Example: + GET /api/v1/predictions/?skip=0&limit=50 + """ + try: + predictions = BuildingEnergyPredictionService.get_all( + db=db, skip=skip, limit=limit + ) + return JSONResponse(jsonable_encoder(predictions)) + except Exception as e: + logger.error( + f"Erreur lors de la récupération des predictions énergétiques des bâtiments: {e}" + ) + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Erreur lors de la récupération des predictions énergétiques des bâtiments", + ) + + +@router.get( + "/{id}", + response_model=BuildingEnergyPredictionsResponse, + summary="Récupérer une prédiction énergétique par ID", + description=""" + Retourne les détails d'une prédiction énergétique spécifique. + + Récupère une prédiction énergétique complète avec toutes les informations + associées au bâtiment et aux calculs de consommation énergétique. + + L'ID doit correspondre à une prédiction existante dans la base de données. + """, + responses={ + 200: { + "description": "Prédiction trouvée et retournée avec succès", + "content": { + "application/json": { + "example": { + "id": 1, + "building_id": 123, + "site_energy_use_kbtu": 45678.9, + "predicted": True, + "updated_at": "2025-09-17T10:30:00Z", + "building": { + "id": 123, + "address": "123 Main St", + "neighborhood": "Downtown", + "building_type": "Office", + }, + } + } + }, + }, + 404: { + "description": "Prédiction non trouvée", + "content": { + "application/json": { + "example": {"detail": "Prédiction avec l'ID 999 non trouvée"} + } + }, + }, + 500: {"description": "Erreur interne du serveur"}, + }, +) +def get_prediction(id: int, db: Session = Depends(get_db)): + """ + Récupère une prédiction énergétique spécifique par son ID. + + Args: + id (int): Identifiant unique de la prédiction à récupérer + db (Session): Session de base de données injectée par FastAPI + + Returns: + BuildingEnergyPredictionsResponse: Prédiction énergétique avec détails complets + + Raises: + HTTPException: + - 404 si la prédiction n'existe pas + - 500 si erreur lors de la récupération + + Example: + GET /api/v1/predictions/123 + """ + try: + pred = BuildingEnergyPredictionService.get_by_id(db=db, prediction_id=id) + return pred + except Exception as e: + logger.error(f"Erreur lors de la récupération de la prediction: {e}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Erreur lors de la récupération de la prediction", + ) + + +@router.post( + "/building/{building_id}", + response_model=PredictionResponse, + status_code=status.HTTP_201_CREATED, + summary="Effectuer une prédiction pour un bâtiment spécifique", + description=""" + Effectue une prédiction énergétique pour un bâtiment spécifique et sauvegarde automatiquement le résultat. + + Cette endpoint : + 1. Récupère les données du bâtiment spécifié + 2. Effectue une prédiction énergétique avec le modèle ML + 3. Sauvegarde automatiquement le résultat dans building_energy_predictions avec predicted=True + 4. Retourne la prédiction avec intervalle de confiance + + Le bâtiment doit exister et avoir toutes les données nécessaires pour les prédictions ML. + """, + responses={ + 201: { + "description": "Prédiction effectuée et sauvegardée avec succès", + "content": { + "application/json": { + "example": { + "prediction": 65432.10, + "prediction_log": 11.09, + "model_version": "v1.0", + "confidence_interval_log": {"lower": 10.85, "upper": 11.33}, + } + } + }, + }, + 404: { + "description": "Bâtiment non trouvé", + "content": { + "application/json": { + "example": {"detail": "Bâtiment avec l'ID 999 non trouvé"} + } + }, + }, + 422: { + "description": "Données du bâtiment insuffisantes pour la prédiction", + "content": { + "application/json": { + "example": { + "detail": "Le bâtiment n'a pas toutes les données nécessaires pour les prédictions ML" + } + } + }, + }, + 500: {"description": "Erreur interne du serveur"}, + }, +) +async def predict_for_building(building_id: int, db: Session = Depends(get_db)): + """ + Effectue une prédiction énergétique pour un bâtiment spécifique et la sauvegarde. + + Args: + building_id (int): Identifiant unique du bâtiment + db (Session): Session de base de données injectée par FastAPI + + Returns: + PredictionResponse: Prédiction avec consommation énergétique et intervalle de confiance + + Raises: + HTTPException: + - 404 si le bâtiment n'existe pas + - 422 si les données du bâtiment sont insuffisantes + - 500 si erreur lors de la prédiction ou sauvegarde + + Example: + POST /api/v1/predictions/building/123 + + Note: + La prédiction est automatiquement sauvegardée dans la table + building_energy_predictions avec le flag predicted=True + """ + try: + # Récupérer les données du bâtiment pour la prédiction + building_data = BuildingModelsService.get_model_data_by_id( + db=db, building_id=building_id + ) + + # Convertir en BuildingFeatures pour la prédiction + from project5.schemas.building_model import convert_query_result_to_schema + + model_schema = convert_query_result_to_schema(building_data) + + # Créer l'objet BuildingFeatures + from project5.schemas.building_model import BuildingFeatures + + features = BuildingFeatures( + year_built=model_schema.year_built, + number_of_buildings=model_schema.number_of_buildings, + number_of_floors=model_schema.number_of_floors, + property_gfa_total=float(model_schema.property_gfa_total), + property_gfa_parking=float(model_schema.property_gfa_parking), + second_largest_property_use_type_gfa=float( + model_schema.second_largest_property_use_type_gfa + ), + third_largest_property_use_type_gfa=float( + model_schema.third_largest_property_use_type_gfa + ), + multiusage=int(model_schema.multiusage), + steam=int(model_schema.steam), + electricity=int(model_schema.electricity), + natural_gas=int(model_schema.natural_gas), + neighborhood_id=model_schema.neighborhood_id, + building_type_id=model_schema.building_type_id, + largest_property_use_type_id=model_schema.largest_property_use_type_id, + primary_property_type_id=model_schema.primary_property_type_id, + second_largest_property_use_type_id=model_schema.second_largest_property_use_type_id, + third_largest_property_use_type_id=model_schema.third_largest_property_use_type_id, + ) + + # Effectuer la prédiction avec sauvegarde automatique + service = PredictionService() + result = await service.predict(features, building_id=building_id, db=db) + + logger.info( + f"Prédiction effectuée et sauvegardée pour le bâtiment {building_id}: {result['prediction']} kBTU" + ) + + return PredictionResponse(**result) + + except NotFoundError as e: + # Bâtiment non trouvé - retourner 404 + logger.warning(f"Bâtiment {building_id} non trouvé: {e}") + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"Bâtiment avec l'ID {building_id} non trouvé", + ) + except HTTPException: + # Propager les erreurs HTTP (404, etc.) + raise + except Exception as e: + logger.error( + f"Erreur lors de la prédiction pour le bâtiment {building_id}: {e}" + ) + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Erreur lors de la prédiction: {str(e)}", + ) diff --git a/src/project5/routers/building_model_routes.py b/src/project5/routers/building_model_routes.py new file mode 100644 index 0000000000000000000000000000000000000000..ea21af15af9f0c83ab3b02ba8b3516332f9385fd --- /dev/null +++ b/src/project5/routers/building_model_routes.py @@ -0,0 +1,438 @@ +""" +Routes API pour la gestion des modèles de bâtiments. + +Ce module contient les endpoints FastAPI pour : +- Récupération de la liste des bâtiments avec pagination +- Récupération d'un bâtiment spécifique par ID +- Récupération des données formatées pour le modèle ML +- Gestion des erreurs et logging approprié + +Endpoints disponibles : +- GET /api/v1/buildings/ : Liste paginée des bâtiments +- GET /api/v1/buildings/{id} : Bâtiment spécifique par ID +- GET /api/v1/buildings/model/{id} : Données formatées pour le modèle ML +- POST /api/v1/buildings/ : Créer un nouveau bâtiment + +Auteur: François Hellebuyck +Projet: Building Energy Prediction API - OpenClassrooms Projet 5 +""" + +import logging +from typing import List + +from fastapi import APIRouter, Depends, HTTPException, Query, status +from fastapi.encoders import jsonable_encoder +from fastapi.responses import JSONResponse +from sqlalchemy.orm import Session + +from project5.database import get_db +from project5.models.building_models import BuildingModels +from project5.schemas.building_model import ( + BuildingModelsCreate, + BuildingModelsList, + BuildingModelsResponse, + ModelViewSchema, + convert_query_result_to_schema, +) +from project5.services.building_model_service import BuildingModelsService +from project5.utils.exceptions import ValidationError + +logger = logging.getLogger(__name__) + +router = APIRouter(prefix="/api/v1/buildings", tags=["buildings"]) + + +@router.get( + "/", + response_model=List[BuildingModelsList], + summary="Récupérer tous les bâtiments", + description=""" + Retourne la liste paginée de tous les bâtiments enregistrés dans le système. + + Cette endpoint permet de récupérer l'ensemble des bâtiments avec leurs + informations de base comme l'adresse, le quartier, le type de bâtiment, + et les caractéristiques principales utilisées pour les prédictions énergétiques. + + Paramètres de pagination : + - skip : nombre d'enregistrements à ignorer (par défaut 0) + - limit : nombre maximum d'enregistrements à retourner (par défaut 100, max 1000) + """, + responses={ + 200: { + "description": "Liste des bâtiments récupérée avec succès", + "content": { + "application/json": { + "example": [ + { + "id": 1, + "ose_building_id": 12345, + "address": "123 Main St", + "city": "Seattle", + "neighborhood": "Downtown", + "building_type": "Office", + "year_built": 1995, + "property_gfa_total": 50000.0, + } + ] + } + }, + }, + 500: {"description": "Erreur interne du serveur"}, + }, +) +def get_building_types( + skip: int = Query(0, ge=0, description="Nombre d'éléments à ignorer (pagination)"), + limit: int = Query( + 100, ge=1, le=1000, description="Nombre maximum d'éléments à retourner" + ), + db: Session = Depends(get_db), +): + """ + Récupère la liste paginée des bâtiments. + + Args: + skip (int): Nombre d'enregistrements à ignorer pour la pagination + limit (int): Nombre maximum d'enregistrements à retourner + db (Session): Session de base de données injectée par FastAPI + + Returns: + JSONResponse: Liste des bâtiments au format JSON + + Raises: + HTTPException: Erreur 500 si problème lors de la récupération des données + + Example: + GET /api/v1/buildings/?skip=0&limit=50 + """ + try: + buildings = BuildingModelsService.get_all(db=db, skip=skip, limit=limit) + return JSONResponse(jsonable_encoder(buildings)) + except Exception as e: + logger.error(f"Erreur lors de la récupération des bâtiments: {e}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Erreur lors de la récupération des bâtiments", + ) + + +@router.get( + "/{id}", + response_model=BuildingModelsResponse, + summary="Récupérer un bâtiment par ID", + description=""" + Retourne les détails complets d'un bâtiment spécifique. + + Récupère toutes les informations détaillées d'un bâtiment incluant : + - Informations générales (adresse, ville, quartier) + - Caractéristiques techniques (année de construction, nombre d'étages) + - Données énergétiques (types d'énergie utilisés) + - Relations avec les types et catégories de propriétés + + L'ID doit correspondre à un bâtiment existant dans la base de données. + """, + responses={ + 200: { + "description": "Bâtiment trouvé et retourné avec succès", + "content": { + "application/json": { + "example": { + "id": 1, + "ose_building_id": 12345, + "address": "123 Main St", + "city": "Seattle", + "state": "WA", + "zip_code": "98101", + "neighborhood": "Downtown", + "building_type": "Office", + "year_built": 1995, + "number_of_buildings": 1, + "number_of_floors": 15, + "property_gfa_total": 50000.0, + "multiusage": False, + "steam": True, + "electricity": True, + "natural_gas": True, + } + } + }, + }, + 404: { + "description": "Bâtiment non trouvé", + "content": { + "application/json": { + "example": {"detail": "Bâtiment avec l'ID 999 non trouvé"} + } + }, + }, + 500: {"description": "Erreur interne du serveur"}, + }, +) +def get_building(id: int, db: Session = Depends(get_db)): + """ + Récupère un bâtiment spécifique par son ID. + + Args: + id (int): Identifiant unique du bâtiment à récupérer + db (Session): Session de base de données injectée par FastAPI + + Returns: + BuildingModelsResponse: Bâtiment avec détails complets + + Raises: + HTTPException: + - 404 si le bâtiment n'existe pas + - 500 si erreur lors de la récupération + + Example: + GET /api/v1/buildings/123 + """ + try: + building = BuildingModelsService.get_by_id(db=db, building_id=id) + return building + except Exception as e: + logger.error(f"Erreur lors du bâtiment: {e}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Erreur lors de la récupération du bâtiment", + ) + + +@router.get( + "/model/{id}", + response_model=ModelViewSchema, + summary="Récupérer les données formatées pour le modèle ML", + description=""" + Retourne les données d'un bâtiment formatées spécifiquement pour le modèle de machine learning. + + Cette endpoint transforme les données brutes du bâtiment en format approprié + pour effectuer des prédictions énergétiques avec le modèle ML. Les données + sont pré-traitées et encodées selon les exigences du modèle : + + - Variables catégorielles encodées numériquement + - Variables continues normalisées si nécessaire + - Format de données compatible avec scikit-learn + - Gestion des valeurs manquantes + + L'ID doit correspondre à un bâtiment existant dans la base de données. + """, + responses={ + 200: { + "description": "Données du modèle récupérées avec succès", + "content": { + "application/json": { + "example": { + "building_id": 123, + "neighborhood_encoded": 3, + "building_type_encoded": 1, + "largest_property_use_type_encoded": 18, + "primary_property_type_encoded": 18, + "year_built": 1995, + "number_of_buildings": 1, + "number_of_floors": 15, + "property_gfa_total": 50000.0, + "steam": 1, + "electricity": 1, + "natural_gas": 1, + } + } + }, + }, + 404: { + "description": "Bâtiment non trouvé", + "content": { + "application/json": { + "example": {"detail": "Bâtiment avec l'ID 999 non trouvé"} + } + }, + }, + 500: {"description": "Erreur interne du serveur"}, + }, +) +def get_model_data_by_id(id: int, db: Session = Depends(get_db)): + """ + Récupère les données d'un bâtiment formatées pour le modèle ML. + + Args: + id (int): Identifiant unique du bâtiment + db (Session): Session de base de données injectée par FastAPI + + Returns: + JSONResponse: Données formatées pour le modèle ML au format JSON + + Raises: + HTTPException: + - 404 si le bâtiment n'existe pas + - 500 si erreur lors de la récupération ou du formatage + + Example: + GET /api/v1/buildings/model/123 + + Note: + Cette endpoint est principalement utilisée par l'endpoint de prédiction + pour préparer les données avant d'appeler le modèle ML. + """ + try: + building = BuildingModelsService.get_model_data_by_id(db=db, building_id=id) + return JSONResponse(jsonable_encoder(convert_query_result_to_schema(building))) + except Exception as e: + logger.error(f"Erreur lors du bâtiment: {e}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Erreur lors de la récupération du bâtiment", + ) + + +@router.post( + "/", + response_model=BuildingModelsResponse, + status_code=status.HTTP_201_CREATED, + summary="Créer un nouveau bâtiment", + description=""" + Ajoute un nouveau bâtiment dans le système. + + Cette endpoint permet de créer un nouveau bâtiment avec ses caractéristiques + complètes. Les données sont validées avant l'insertion en base de données. + + Champs obligatoires pour les prédictions ML (17 features) : + - ose_building_id : Identifiant unique du bâtiment OSE + - year_built : Année de construction + - number_of_buildings : Nombre de bâtiments + - number_of_floors : Nombre d'étages + - property_gfa_total : Surface totale GFA + - property_gfa_parking : Surface parking GFA + - second_largest_property_use_type_gfa : Surface 2e type d'usage + - third_largest_property_use_type_gfa : Surface 3e type d'usage + - multiusage : Indicateur multi-usage (0 ou 1) + - steam : Indicateur vapeur (0 ou 1) + - electricity : Indicateur électricité (0 ou 1) + - natural_gas : Indicateur gaz naturel (0 ou 1) + - neighborhood_id : ID du quartier + - building_type_id : ID du type de bâtiment + - largest_property_use_type_id : ID du plus grand type d'usage + - primary_property_type_id : ID du type de propriété principal + - second_largest_property_use_type_id : ID du 2e type d'usage + - third_largest_property_use_type_id : ID du 3e type d'usage + + Validation automatique : + - Format et valeurs des champs + - Contraintes métier (années, surfaces positives) + - Unicité de l'identifiant OSE + """, + responses={ + 201: { + "description": "Bâtiment créé avec succès", + "content": { + "application/json": { + "example": { + "id": 1001, + "ose_building_id": "NEW12345", + "address": "456 New Street", + "city": "Seattle", + "state": "WA", + "year_built": 2020, + "number_of_buildings": 1, + "number_of_floors": 5, + "property_gfa_total": 25000.0, + "property_gfa_parking": 2000.0, + "second_largest_property_use_type_gfa": 0.0, + "third_largest_property_use_type_gfa": 0.0, + "multiusage": 0, + "steam": 0, + "electricity": 1, + "natural_gas": 1, + "neighborhood_id": 1, + "building_type_id": 1, + "largest_property_use_type_id": 18, + "primary_property_type_id": 18, + "second_largest_property_use_type_id": 1, + "third_largest_property_use_type_id": 1, + "created_at": "2025-09-21T12:00:00", + "updated_at": "2025-09-21T12:00:00", + } + } + }, + }, + 400: { + "description": "Données invalides ou manquantes", + "content": { + "application/json": { + "example": {"detail": "L'année de construction est obligatoire"} + } + }, + }, + 422: { + "description": "Erreur de validation des données", + "content": { + "application/json": { + "example": {"detail": "La surface totale GFA est obligatoire"} + } + }, + }, + 500: {"description": "Erreur interne du serveur"}, + }, +) +def create_building(building_data: BuildingModelsCreate, db: Session = Depends(get_db)): + """ + Crée un nouveau bâtiment dans le système. + + Args: + building_data (BuildingModelsCreate): Données du bâtiment à créer + db (Session): Session de base de données injectée par FastAPI + + Returns: + BuildingModelsResponse: Bâtiment créé avec son ID généré et timestamps + + Raises: + HTTPException: + - 400 si les données obligatoires sont manquantes + - 422 si erreur de validation des données + - 500 si erreur lors de la création + + Example: + POST /api/v1/buildings/ + { + "ose_building_id": "NEW12345", + "address": "456 New Street", + "city": "Seattle", + "year_built": 2020, + "number_of_buildings": 1, + "number_of_floors": 5, + "property_gfa_total": 25000.0, + "property_gfa_parking": 2000.0, + "second_largest_property_use_type_gfa": 0.0, + "third_largest_property_use_type_gfa": 0.0, + "multiusage": 0, + "steam": 0, + "electricity": 1, + "natural_gas": 1, + "neighborhood_id": 1, + "building_type_id": 1, + "largest_property_use_type_id": 18, + "primary_property_type_id": 18, + "second_largest_property_use_type_id": 1, + "third_largest_property_use_type_id": 1 + } + """ + try: + # Convertir le schéma Pydantic en modèle SQLAlchemy + building_model = BuildingModels(**building_data.model_dump(exclude_unset=True)) + + # Appeler le service pour créer le bâtiment + created_building = BuildingModelsService.set_new_building( + db=db, building_model=building_model + ) + + return created_building + + except ValidationError as e: + logger.error(f"Erreur de validation lors de la création du bâtiment: {e}") + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e)) + except ValueError as e: + logger.error(f"Erreur de validation lors de la création du bâtiment: {e}") + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e)) + except Exception as e: + logger.error(f"Erreur lors de la création du bâtiment: {e}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Erreur lors de la création du bâtiment", + ) diff --git a/src/project5/routers/building_types_routes.py b/src/project5/routers/building_types_routes.py new file mode 100644 index 0000000000000000000000000000000000000000..31db6a31c3a1e5d85361ff817135d952b75bda36 --- /dev/null +++ b/src/project5/routers/building_types_routes.py @@ -0,0 +1,205 @@ +""" +Routes API pour la gestion des types de bâtiments. + +Ce module contient les endpoints FastAPI pour : +- Récupération de la liste des types de bâtiments avec pagination +- Récupération d'un type de bâtiment spécifique par ID +- Gestion des erreurs et logging approprié + +Les types de bâtiments sont utilisés pour catégoriser les bâtiments selon leur +usage principal (résidentiel, commercial, industriel, etc.) et sont essentiels +pour les prédictions énergétiques du modèle ML. + +Endpoints disponibles : +- GET /api/v1/building-types/ : Liste paginée des types de bâtiments +- GET /api/v1/building-types/{id} : Type de bâtiment spécifique par ID + +Auteur: François Hellebuyck +Projet: Building Energy Prediction API - OpenClassrooms Projet 5 +""" + +import logging +from typing import List + +from fastapi import APIRouter, Depends, HTTPException, Query, status +from sqlalchemy.orm import Session + +from project5.database import get_db +from project5.schemas.building_type import BuildingTypeList, BuildingTypeResponse +from project5.services.building_type_service import BuildingTypeService + +logger = logging.getLogger(__name__) + +router = APIRouter(prefix="/api/v1/building-types", tags=["building-types"]) + + +@router.get( + "/", + response_model=List[BuildingTypeList], + summary="Récupérer tous les types de bâtiments", + description=""" + Retourne la liste paginée de tous les types de bâtiments disponibles dans le système. + + Cette endpoint permet de récupérer l'ensemble des catégories de bâtiments + utilisées pour la classification et les prédictions énergétiques. Chaque type + de bâtiment correspond à un usage spécifique (résidentiel, commercial, + industriel, etc.) avec ses caractéristiques énergétiques propres. + + Types de bâtiments disponibles : + - Campus building complex + - Non-residential building + - Seattle Public Schools District K-12 + - Multifamily (Low-Rise, Mid-Rise, High-Rise) + - Et autres catégories spécialisées + + Paramètres de pagination : + - skip : nombre d'enregistrements à ignorer (par défaut 0) + - limit : nombre maximum d'enregistrements à retourner (par défaut 100, max 1000) + """, + responses={ + 200: { + "description": "Liste des types de bâtiments récupérée avec succès", + "content": { + "application/json": { + "example": [ + { + "id": 1, + "model_id": 0, + "building_type_name": "CAMPUS", + "description": "Campus building complex", + "created_at": "2025-09-09T09:56:21Z", + "updated_at": "2025-09-09T09:56:21Z", + }, + { + "id": 2, + "model_id": 1, + "building_type_name": "NONRESIDENTIAL", + "description": "Non-residential building", + "created_at": "2025-09-09T09:56:21Z", + "updated_at": "2025-09-09T09:56:21Z", + }, + ] + } + }, + }, + 500: {"description": "Erreur interne du serveur"}, + }, +) +def get_building_types( + skip: int = Query(0, ge=0, description="Nombre d'éléments à ignorer (pagination)"), + limit: int = Query( + 100, ge=1, le=1000, description="Nombre maximum d'éléments à retourner" + ), + db: Session = Depends(get_db), +): + """ + Récupère la liste paginée des types de bâtiments. + + Args: + skip (int): Nombre d'enregistrements à ignorer pour la pagination + limit (int): Nombre maximum d'enregistrements à retourner + db (Session): Session de base de données injectée par FastAPI + + Returns: + List[BuildingTypeList]: Liste des types de bâtiments + + Raises: + HTTPException: Erreur 500 si problème lors de la récupération des données + + Example: + GET /api/v1/building-types/?skip=0&limit=20 + + Note: + Les types de bâtiments sont référencés par leur model_id dans le modèle ML + pour effectuer les prédictions énergétiques. + """ + try: + buildingTypes = BuildingTypeService.get_all(db=db, skip=skip, limit=limit) + return buildingTypes + except Exception as e: + logger.error(f"Erreur lors de la récupération des des types de bâtiment: {e}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Erreur lors de la récupération des types de bâtiment", + ) + + +@router.get( + "/{building_type_id}", + response_model=BuildingTypeResponse, + summary="Récupérer un type de bâtiment par ID", + description=""" + Retourne les détails complets d'un type de bâtiment spécifique. + + Récupère toutes les informations d'un type de bâtiment incluant : + - Identifiant unique dans la base de données + - Identifiant utilisé par le modèle ML (model_id) + - Nom du type de bâtiment + - Description détaillée + - Horodatages de création et modification + + Le type de bâtiment est un élément clé pour la classification et les + prédictions énergétiques, car il détermine les caractéristiques de + consommation énergétique typiques. + + L'ID doit correspondre à un type de bâtiment existant dans la base de données. + """, + responses={ + 200: { + "description": "Type de bâtiment trouvé et retourné avec succès", + "content": { + "application/json": { + "example": { + "id": 3, + "model_id": 1, + "building_type_name": "NONRESIDENTIAL", + "description": "Non-residential building", + "created_at": "2025-09-09T09:56:21Z", + "updated_at": "2025-09-09T09:56:21Z", + } + } + }, + }, + 404: { + "description": "Type de bâtiment non trouvé", + "content": { + "application/json": { + "example": {"detail": "Type de bâtiment avec l'ID 999 non trouvé"} + } + }, + }, + 500: {"description": "Erreur interne du serveur"}, + }, +) +def get_building_type(building_type_id: int, db: Session = Depends(get_db)): + """ + Récupère un type de bâtiment spécifique par son ID. + + Args: + building_type_id (int): Identifiant unique du type de bâtiment à récupérer + db (Session): Session de base de données injectée par FastAPI + + Returns: + BuildingTypeResponse: Type de bâtiment avec détails complets + + Raises: + HTTPException: + - 404 si le type de bâtiment n'existe pas + - 500 si erreur lors de la récupération + + Example: + GET /api/v1/building-types/3 + + Note: + Le model_id retourné est utilisé par le modèle ML pour encoder + le type de bâtiment lors des prédictions énergétiques. + """ + try: + bt = BuildingTypeService.get_by_id(db=db, id=building_type_id) + return bt + except Exception as e: + logger.error(f"Erreur lors de la récupération du type de bâtiment: {e}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Erreur lors de la récupération du type de bâtiment", + ) diff --git a/src/project5/routers/categories_routes.py b/src/project5/routers/categories_routes.py new file mode 100644 index 0000000000000000000000000000000000000000..47e3cdf0298eae9b9ca05fb8495464397b01c153 --- /dev/null +++ b/src/project5/routers/categories_routes.py @@ -0,0 +1,221 @@ +""" +Routes API pour la gestion des catégories de propriétés. + +Ce module contient les endpoints FastAPI pour : +- Récupération de la liste des catégories de propriétés avec pagination +- Récupération d'une catégorie spécifique par ID +- Gestion des erreurs et logging approprié + +Les catégories de propriétés regroupent les différents usages des bâtiments +(résidentiel, commercial, bureaux, éducation, santé, etc.) et servent à +classifier les propriétés pour les prédictions énergétiques du modèle ML. + +Catégories principales disponibles : +- RESIDENTIAL : Logements et habitations +- OFFICE : Espaces de bureaux et administratifs +- EDUCATION : Établissements d'enseignement +- HEALTHCARE : Hôpitaux et établissements de soins +- RETAIL : Magasins et commerces +- INDUSTRIAL : Usines et installations industrielles +- Et autres catégories spécialisées + +Endpoints disponibles : +- GET /api/v1/categories/ : Liste paginée des catégories +- GET /api/v1/categories/{id} : Catégorie spécifique par ID + +Auteur: François Hellebuyck +Projet: Building Energy Prediction API - OpenClassrooms Projet 5 +""" + +import logging +from typing import List + +from fastapi import APIRouter, Depends, HTTPException, Query, status +from sqlalchemy.orm import Session + +from project5.database import get_db +from project5.schemas.categorie import CategoryList, CategoryResponse +from project5.services.categorie_service import CategorieService + +logger = logging.getLogger(__name__) + +router = APIRouter(prefix="/api/v1/categories", tags=["categories"]) + + +@router.get( + "/", + response_model=List[CategoryList], + summary="Récupérer toutes les catégories de propriétés", + description=""" + Retourne la liste paginée de toutes les catégories de propriétés disponibles dans le système. + + Cette endpoint permet de récupérer l'ensemble des catégories d'usage des bâtiments + utilisées pour la classification et les prédictions énergétiques. Chaque catégorie + regroupe des propriétés ayant des caractéristiques énergétiques similaires. + + Catégories disponibles dans le système : + - RESIDENTIAL : Logements et habitations + - OFFICE : Espaces de bureaux et administratifs + - EDUCATION : Établissements d'enseignement et de formation + - HEALTHCARE : Hôpitaux, cliniques et établissements de soins médicaux + - RETAIL : Magasins et commerces de détail + - RESTAURANT : Restaurants et services alimentaires + - INDUSTRIAL : Usines et installations industrielles + - ENTERTAINMENT : Théâtres, cinémas et espaces de divertissement + - LODGING : Hôtels et logements temporaires + - PARKING : Structures et espaces de stationnement + - PUBLIC : Services gouvernementaux et publics + - RELIGIOUS : Églises et lieux de culte + - Et autres catégories spécialisées + + Paramètres de pagination : + - skip : nombre d'enregistrements à ignorer (par défaut 0) + - limit : nombre maximum d'enregistrements à retourner (par défaut 100, max 1000) + """, + responses={ + 200: { + "description": "Liste des catégories récupérée avec succès", + "content": { + "application/json": { + "example": [ + { + "id": 11, + "category_code": "OFFICE", + "category_name": "Bureaux", + "description": "Espaces de bureaux et administratifs", + "created_at": "2025-09-09T09:56:21Z", + "updated_at": "2025-09-09T09:56:21Z", + }, + { + "id": 16, + "category_code": "RESIDENTIAL", + "category_name": "Résidentiel", + "description": "Logements et habitations", + "created_at": "2025-09-09T09:56:21Z", + "updated_at": "2025-09-09T09:56:21Z", + }, + ] + } + }, + }, + 500: {"description": "Erreur interne du serveur"}, + }, +) +def get_categories( + skip: int = Query(0, ge=0, description="Nombre d'éléments à ignorer (pagination)"), + limit: int = Query( + 100, ge=1, le=1000, description="Nombre maximum d'éléments à retourner" + ), + db: Session = Depends(get_db), +): + """ + Récupère la liste paginée des catégories de propriétés. + + Args: + skip (int): Nombre d'enregistrements à ignorer pour la pagination + limit (int): Nombre maximum d'enregistrements à retourner + db (Session): Session de base de données injectée par FastAPI + + Returns: + List[CategoryList]: Liste des catégories de propriétés + + Raises: + HTTPException: Erreur 500 si problème lors de la récupération des données + + Example: + GET /api/v1/categories/?skip=0&limit=20 + + Note: + Les catégories sont utilisées pour regrouper les propriétés par usage + et déterminer les caractéristiques énergétiques typiques lors des prédictions. + """ + try: + categories = CategorieService.get_all(db=db, skip=skip, limit=limit) + return categories + except Exception as e: + logger.error(f"Erreur lors de la récupération des categories: {e}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Erreur lors de la récupération des categories", + ) + + +@router.get( + "/{categorie_id}", + response_model=CategoryResponse, + summary="Récupérer une catégorie par ID", + description=""" + Retourne les détails complets d'une catégorie de propriété spécifique. + + Récupère toutes les informations d'une catégorie incluant : + - Identifiant unique dans la base de données + - Code de la catégorie (nom court en anglais) + - Nom complet de la catégorie (en français) + - Description détaillée de l'usage + - Horodatages de création et modification + + Les catégories servent à classifier les propriétés selon leur usage principal + et sont essentielles pour déterminer les profils de consommation énergétique + lors des prédictions du modèle ML. + + L'ID doit correspondre à une catégorie existante dans la base de données. + """, + responses={ + 200: { + "description": "Catégorie trouvée et retournée avec succès", + "content": { + "application/json": { + "example": { + "id": 11, + "category_code": "OFFICE", + "category_name": "Bureaux", + "description": "Espaces de bureaux et administratifs", + "created_at": "2025-09-09T09:56:21Z", + "updated_at": "2025-09-09T09:56:21Z", + } + } + }, + }, + 404: { + "description": "Catégorie non trouvée", + "content": { + "application/json": { + "example": {"detail": "Catégorie avec l'ID 999 non trouvée"} + } + }, + }, + 500: {"description": "Erreur interne du serveur"}, + }, +) +def get_categorie(categorie_id: int, db: Session = Depends(get_db)): + """ + Récupère une catégorie spécifique par son ID. + + Args: + categorie_id (int): Identifiant unique de la catégorie à récupérer + db (Session): Session de base de données injectée par FastAPI + + Returns: + CategoryResponse: Catégorie avec détails complets + + Raises: + HTTPException: + - 404 si la catégorie n'existe pas + - 500 si erreur lors de la récupération + + Example: + GET /api/v1/categories/11 + + Note: + Les catégories sont liées aux propriétés et influencent directement + les prédictions énergétiques basées sur l'usage du bâtiment. + """ + try: + categorie = CategorieService.get_by_id(db=db, categorie_id=categorie_id) + return categorie + except Exception as e: + logger.error(f"Erreur lors de la récupération de la category: {e}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Erreur lors de la récupération de la category", + ) diff --git a/src/project5/routers/neighborhoods_routes.py b/src/project5/routers/neighborhoods_routes.py new file mode 100644 index 0000000000000000000000000000000000000000..32dfa4682b44049c37caa2c15ad35e6ad5ffd9a1 --- /dev/null +++ b/src/project5/routers/neighborhoods_routes.py @@ -0,0 +1,328 @@ +""" +Routes API pour la gestion des quartiers de Seattle. + +Ce module contient les endpoints FastAPI pour : +- Récupération de la liste des quartiers avec pagination +- Récupération d'un quartier spécifique par ID +- Recherche de quartiers par nom +- Gestion des erreurs et logging approprié + +Les quartiers (neighborhoods) représentent les divisions géographiques de Seattle +et sont un facteur important pour les prédictions énergétiques car ils influencent +les caractéristiques de consommation selon la localisation, le climat local, et +le type d'urbanisation. + +Quartiers disponibles dans le système : +- BALLARD, CENTRAL, DELRIDGE, DOWNTOWN +- EAST, GREATER DUWAMISH, LAKE UNION +- MAGNOLIA / QUEEN ANNE, NORTH, NORTHEAST +- NORTHWEST, SOUTHEAST, SOUTHWEST, WEST +- UNKNOWN (pour les données non géolocalisées) + +Endpoints disponibles : +- GET /api/v1/neighborhoods/ : Liste paginée des quartiers +- GET /api/v1/neighborhoods/{id} : Quartier spécifique par ID +- GET /api/v1/neighborhoods/search/ : Recherche par nom + +Auteur: François Hellebuyck +Projet: Building Energy Prediction API - OpenClassrooms Projet 5 +""" + +import logging +from typing import List + +from fastapi import APIRouter, Depends, HTTPException, Query, status +from sqlalchemy.orm import Session + +from project5.database import get_db +from project5.models import Neighborhood as NeighborhoodModel +from project5.schemas import Neighborhood +from project5.services.neighborhood_service import NeighborhoodService + +logger = logging.getLogger(__name__) + +router = APIRouter( + prefix="/api/v1/neighborhoods", + tags=["neighborhoods"], + responses={404: {"description": "Quartier non trouvé"}}, +) + + +@router.get( + "/", + response_model=List[Neighborhood], + summary="Récupérer tous les quartiers de Seattle", + description=""" + Retourne la liste paginée de tous les quartiers de Seattle disponibles dans le système. + + Cette endpoint permet de récupérer l'ensemble des quartiers (neighborhoods) de Seattle + utilisés pour la géolocalisation et les prédictions énergétiques. Chaque quartier + représente une zone géographique avec ses caractéristiques climatiques et urbaines + propres qui influencent la consommation énergétique des bâtiments. + + Quartiers de Seattle disponibles : + - BALLARD : Quartier nord-ouest, résidentiel et commercial + - CENTRAL : Zone centrale de Seattle + - DELRIDGE : Quartier sud-ouest, principalement résidentiel + - DOWNTOWN : Centre-ville, zone commerciale et de bureaux + - EAST : Quartiers est de Seattle + - GREATER DUWAMISH : Zone industrielle sud + - LAKE UNION : Zone autour du lac Union, mixte + - MAGNOLIA / QUEEN ANNE : Quartiers résidentiels aisés + - NORTH, NORTHEAST : Zones résidentielles nord + - NORTHWEST, SOUTHEAST, SOUTHWEST, WEST : Autres zones géographiques + - UNKNOWN : Pour les bâtiments sans géolocalisation précise + + Paramètres de pagination : + - skip : nombre d'enregistrements à ignorer (par défaut 0) + - limit : nombre maximum d'enregistrements à retourner (par défaut 100, max 1000) + """, + responses={ + 200: { + "description": "Liste des quartiers récupérée avec succès", + "content": { + "application/json": { + "example": [ + { + "id": 5, + "neighborhood_name": "DOWNTOWN", + "model_id": 3, + "created_at": "2025-09-09T09:56:21Z", + "updated_at": "2025-09-09T09:56:21Z", + }, + { + "id": 2, + "neighborhood_name": "BALLARD", + "model_id": 0, + "created_at": "2025-09-09T09:56:21Z", + "updated_at": "2025-09-09T09:56:21Z", + }, + ] + } + }, + }, + 500: {"description": "Erreur interne du serveur"}, + }, +) +def get_neighborhoods( + skip: int = Query(0, ge=0, description="Nombre d'éléments à ignorer (pagination)"), + limit: int = Query( + 100, ge=1, le=1000, description="Nombre maximum d'éléments à retourner" + ), + db: Session = Depends(get_db), +): + """ + Récupère la liste paginée des quartiers de Seattle. + + Args: + skip (int): Nombre d'enregistrements à ignorer pour la pagination + limit (int): Nombre maximum d'enregistrements à retourner + db (Session): Session de base de données injectée par FastAPI + + Returns: + List[Neighborhood]: Liste des quartiers de Seattle + + Raises: + HTTPException: Erreur 500 si problème lors de la récupération des données + + Example: + GET /api/v1/neighborhoods/?skip=0&limit=20 + + Note: + Les quartiers sont référencés par leur model_id dans le modèle ML + pour tenir compte de la localisation géographique dans les prédictions. + """ + try: + neighborhoods = NeighborhoodService.get_all(db=db, skip=skip, limit=limit) + return neighborhoods + except Exception as e: + logger.error(f"Erreur lors de la récupération des quartiers: {e}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Erreur lors de la récupération des quartiers", + ) + + +@router.get( + "/{neighborhood_id}", + response_model=Neighborhood, + summary="Récupérer un quartier par ID", + description=""" + Retourne les détails complets d'un quartier de Seattle spécifique. + + Récupère toutes les informations d'un quartier incluant : + - Identifiant unique dans la base de données + - Nom du quartier (neighborhood_name) + - Identifiant utilisé par le modèle ML (model_id) + - Horodatages de création et modification + + Les quartiers sont des facteurs géographiques importants pour les prédictions + énergétiques car ils déterminent : + - Les conditions climatiques locales + - Le type d'urbanisation (résidentiel, commercial, industriel) + - Les réglementations énergétiques locales + - Les habitudes de consommation régionales + + L'ID doit correspondre à un quartier existant dans la base de données. + """, + responses={ + 200: { + "description": "Quartier trouvé et retourné avec succès", + "content": { + "application/json": { + "example": { + "id": 5, + "neighborhood_name": "DOWNTOWN", + "model_id": 3, + "created_at": "2025-09-09T09:56:21Z", + "updated_at": "2025-09-09T09:56:21Z", + } + } + }, + }, + 404: { + "description": "Quartier non trouvé", + "content": { + "application/json": { + "example": {"detail": "Quartier avec l'ID 999 non trouvé"} + } + }, + }, + 500: {"description": "Erreur interne du serveur"}, + }, +) +def get_neighborhood(neighborhood_id: int, db: Session = Depends(get_db)): + """ + Récupère un quartier spécifique par son ID. + + Args: + neighborhood_id (int): Identifiant unique du quartier à récupérer + db (Session): Session de base de données injectée par FastAPI + + Returns: + Neighborhood: Quartier avec détails complets + + Raises: + HTTPException: + - 404 si le quartier n'existe pas + - 500 si erreur lors de la récupération + + Example: + GET /api/v1/neighborhoods/5 + + Note: + Le model_id retourné est utilisé par le modèle ML pour encoder + la localisation géographique lors des prédictions énergétiques. + """ + try: + neighborhood = NeighborhoodService.get_by_id( + db=db, neighborhood_id=neighborhood_id + ) + return neighborhood + except Exception as e: + logger.error(f"Erreur lors de la récupération du quartier: {e}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Erreur lors de la récupération du quartier", + ) + + +@router.get( + "/search/", + response_model=List[Neighborhood], + summary="Rechercher des quartiers par nom", + description=""" + Recherche des quartiers de Seattle par nom avec correspondance partielle. + + Cette endpoint permet de rechercher des quartiers en utilisant une correspondance + partielle (insensible à la casse) sur le nom du quartier. Utile pour : + - Auto-complétion dans les interfaces utilisateur + - Recherche floue de quartiers + - Filtrage rapide de la liste des quartiers + + La recherche s'effectue sur le champ neighborhood_name avec une correspondance + partielle utilisant l'opérateur SQL ILIKE (insensible à la casse). + + Exemples de recherche : + - "down" trouvera "DOWNTOWN" + - "north" trouvera "NORTH", "NORTHEAST", "NORTHWEST" + - "queen" trouvera "MAGNOLIA / QUEEN ANNE" + + Les résultats sont triés par ordre alphabétique du nom de quartier. + """, + responses={ + 200: { + "description": "Quartiers trouvés avec succès", + "content": { + "application/json": { + "example": [ + { + "id": 10, + "neighborhood_name": "NORTH", + "model_id": 8, + "created_at": "2025-09-09T09:56:21Z", + "updated_at": "2025-09-09T09:56:21Z", + }, + { + "id": 11, + "neighborhood_name": "NORTHEAST", + "model_id": 9, + "created_at": "2025-09-09T09:56:21Z", + "updated_at": "2025-09-09T09:56:21Z", + }, + ] + } + }, + }, + 422: { + "description": "Paramètres de recherche invalides", + "content": { + "application/json": { + "example": { + "detail": "Le terme de recherche doit contenir au moins 1 caractère" + } + } + }, + }, + 500: {"description": "Erreur interne du serveur"}, + }, +) +def search_neighborhoods( + q: str = Query( + ..., min_length=1, description="Terme de recherche (insensible à la casse)" + ), + limit: int = Query(50, ge=1, le=100, description="Nombre maximum de résultats"), + db: Session = Depends(get_db), +): + """ + Recherche des quartiers par nom avec correspondance partielle. + + Args: + q (str): Terme de recherche à chercher dans les noms de quartiers + limit (int): Nombre maximum de résultats à retourner (par défaut 50, max 100) + db (Session): Session de base de données injectée par FastAPI + + Returns: + List[Neighborhood]: Liste des quartiers correspondant à la recherche + + Raises: + HTTPException: + - 422 si les paramètres de recherche sont invalides + - 500 si erreur lors de la recherche + + Example: + GET /api/v1/neighborhoods/search/?q=north&limit=10 + + Note: + La recherche utilise ILIKE pour une correspondance insensible à la casse + et retourne les résultats triés par ordre alphabétique. + """ + neighborhoods = ( + db.query(NeighborhoodModel) + .filter(NeighborhoodModel.neighborhood_name.ilike(f"%{q.upper()}%")) + .order_by(NeighborhoodModel.neighborhood_name) + .limit(limit) + .all() + ) + + return neighborhoods diff --git a/src/project5/routers/prediction_routes.py b/src/project5/routers/prediction_routes.py new file mode 100644 index 0000000000000000000000000000000000000000..76807ad6d667ebb77a1ad3225bd79daa48185da7 --- /dev/null +++ b/src/project5/routers/prediction_routes.py @@ -0,0 +1,251 @@ +""" +Routes API pour les prédictions énergétiques des bâtiments. + +Ce module contient les endpoints FastAPI pour : +- Prédiction de consommation énergétique basée sur les caractéristiques du bâtiment +- Récupération d'informations sur le modèle de machine learning +- Gestion des erreurs et logging approprié + +Le modèle de prédiction utilise un RandomForestRegressor entraîné sur les données +énergétiques de Seattle pour prédire la consommation en kBTU/an. Les caractéristiques +principales incluent : +- Localisation (quartier) +- Type de bâtiment et usage principal +- Caractéristiques physiques (surface, étages, année de construction) +- Types d'énergie utilisés (électricité, gaz naturel, vapeur) + +Endpoints disponibles : +- POST /api/v1/predict : Prédiction énergétique pour un bâtiment +- GET /api/v1/model/info : Informations sur le modèle ML + +Auteur: François Hellebuyck +Projet: Building Energy Prediction API - OpenClassrooms Projet 5 +""" + +import logging + +from fastapi import APIRouter, Depends, HTTPException + +from project5.schemas.building_model import BuildingFeatures, PredictionResponse +from project5.services.building_model_service import PredictionService + +logger = logging.getLogger(__name__) + +# Instance globale du service (peut être injectée via des dépendances) +prediction_service = PredictionService() + + +def get_prediction_service() -> PredictionService: + """ + Injection de dépendance pour le service de prédiction. + + Returns: + PredictionService: Instance du service de prédiction configuré + + Note: + Cette fonction permet l'injection de dépendance FastAPI pour le service + de prédiction, facilitant les tests et la modularité. + """ + return prediction_service + + +router = APIRouter(prefix="/api/v1", tags=["predictions"]) + + +@router.post( + "/predict", + response_model=PredictionResponse, + summary="Prédire la consommation énergétique d'un bâtiment", + description=""" + Effectue une prédiction de consommation énergétique basée sur les caractéristiques du bâtiment. + + Cette endpoint utilise un modèle de machine learning (RandomForestRegressor) entraîné + sur les données énergétiques de Seattle pour prédire la consommation annuelle en kBTU. + + Caractéristiques requises pour la prédiction : + - **neighborhood_encoded** : Quartier encodé numériquement (0-13, -1 pour UNKNOWN) + - **building_type_encoded** : Type de bâtiment encodé (0-7, -1 pour UNKNOWN) + - **largest_property_use_type_encoded** : Usage principal encodé (0-27, -1 pour UNKNOWN) + - **primary_property_type_encoded** : Type de propriété principal encodé + - **year_built** : Année de construction du bâtiment + - **number_of_buildings** : Nombre de bâtiments sur la propriété + - **number_of_floors** : Nombre d'étages + - **property_gfa_total** : Surface totale brute en pieds carrés + - **steam** : Utilisation de vapeur (0/1) + - **electricity** : Utilisation d'électricité (0/1) + - **natural_gas** : Utilisation de gaz naturel (0/1) + + Le modèle retourne une prédiction de consommation énergétique avec un intervalle + de confiance basé sur la variance des arbres de la forêt aléatoire. + """, + responses={ + 200: { + "description": "Prédiction effectuée avec succès", + "content": { + "application/json": { + "example": { + "predicted_energy_use": 45678.9, + "confidence_interval": {"lower": 42000.5, "upper": 49357.3}, + "model_version": "1.0", + "features_used": [ + "neighborhood_encoded", + "building_type_encoded", + "year_built", + "property_gfa_total", + ], + } + } + }, + }, + 422: { + "description": "Données d'entrée invalides", + "content": { + "application/json": { + "example": { + "detail": [ + { + "loc": ["body", "year_built"], + "msg": "ensure this value is greater than 1800", + "type": "value_error.number.not_gt", + } + ] + } + } + }, + }, + 500: {"description": "Erreur interne du serveur lors de la prédiction"}, + }, +) +async def predict_building_value( + features: BuildingFeatures, + service: PredictionService = Depends(get_prediction_service), +): + """ + Effectue une prédiction de consommation énergétique basée sur les caractéristiques du bâtiment. + + Args: + features (BuildingFeatures): Caractéristiques encodées du bâtiment pour la prédiction + service (PredictionService): Service de prédiction injecté par FastAPI + + Returns: + PredictionResponse: Prédiction avec consommation énergétique et intervalle de confiance + + Raises: + HTTPException: + - 422 si les données d'entrée sont invalides + - 500 si erreur lors de la prédiction ML + + Example: + POST /api/v1/predict + { + "neighborhood_encoded": 3, + "building_type_encoded": 1, + "largest_property_use_type_encoded": 18, + "primary_property_type_encoded": 18, + "year_built": 1995, + "number_of_buildings": 1, + "number_of_floors": 15, + "property_gfa_total": 50000.0, + "steam": 1, + "electricity": 1, + "natural_gas": 1 + } + + Note: + Les valeurs encodées doivent correspondre aux encodages utilisés lors + de l'entraînement du modèle. Utilisez l'endpoint /buildings/model/{id} + pour obtenir les données pré-encodées d'un bâtiment existant. + """ + try: + result = await service.predict(features) + return PredictionResponse(**result) + + except Exception as e: + logger.error(f"Erreur dans la prédiction: {e}") + raise HTTPException( + status_code=500, detail=f"Erreur lors de la prédiction: {str(e)}" + ) + + +@router.get( + "/model/info", + summary="Informations sur le modèle de machine learning", + description=""" + Retourne des informations détaillées sur le modèle de prédiction énergétique. + + Cette endpoint fournit des métadonnées essentielles sur le modèle ML utilisé : + - Version du modèle déployé + - Type d'algorithme (RandomForestRegressor) + - Nombre de caractéristiques utilisées + - Importance des caractéristiques (feature importance) + + L'importance des caractéristiques indique quelles variables ont le plus + d'influence sur les prédictions : + - Valeurs plus élevées = plus d'importance + - Somme totale des importances = 1.0 + - Utile pour comprendre quels facteurs influencent le plus la consommation + + Ces informations sont utiles pour : + - Validation du modèle en production + - Debugging des prédictions + - Analyse de l'importance des variables + - Monitoring de la performance du modèle + """, + responses={ + 200: { + "description": "Informations du modèle récupérées avec succès", + "content": { + "application/json": { + "example": { + "model_version": "1.0", + "model_type": "RandomForestRegressor", + "feature_count": 11, + "feature_importance": { + "property_gfa_total": 0.45, + "year_built": 0.18, + "neighborhood_encoded": 0.12, + "building_type_encoded": 0.08, + "number_of_floors": 0.07, + "largest_property_use_type_encoded": 0.05, + "electricity": 0.02, + "natural_gas": 0.02, + "steam": 0.01, + }, + } + } + }, + }, + 500: {"description": "Erreur interne du serveur"}, + }, +) +async def get_model_info(service: PredictionService = Depends(get_prediction_service)): + """ + Retourne des informations détaillées sur le modèle de machine learning. + + Args: + service (PredictionService): Service de prédiction injecté par FastAPI + + Returns: + dict: Informations sur le modèle incluant version, type, et importance des caractéristiques + + Raises: + HTTPException: Erreur 500 si problème lors de la récupération des informations + + Example: + GET /api/v1/model/info + + Note: + L'importance des caractéristiques est calculée par le RandomForestRegressor + et indique la contribution relative de chaque variable aux prédictions. + """ + try: + feature_importance = service.get_feature_importance() + return { + "model_version": service.model_version, + "model_type": "RandomForestRegressor", + "feature_count": len(service.feature_names), + "feature_importance": feature_importance, + } + except Exception as e: + logger.error(f"Erreur lors de la récupération des infos du modèle: {e}") + raise HTTPException(status_code=500, detail=f"Erreur: {str(e)}") diff --git a/src/project5/routers/properties_routes.py b/src/project5/routers/properties_routes.py new file mode 100644 index 0000000000000000000000000000000000000000..bd44faca4645c3e78841c1bd2e99a07e4e7569e4 --- /dev/null +++ b/src/project5/routers/properties_routes.py @@ -0,0 +1,246 @@ +""" +Routes API pour la gestion des propriétés d'usage des bâtiments. + +Ce module contient les endpoints FastAPI pour : +- Récupération de la liste des propriétés d'usage avec pagination +- Récupération d'une propriété spécifique par ID +- Gestion des erreurs et logging approprié + +Les propriétés d'usage représentent les utilisations spécifiques des bâtiments +et sont plus granulaires que les catégories. Elles permettent une classification +fine pour les prédictions énergétiques du modèle ML. + +Propriétés principales disponibles : +- OFFICE : Bureaux +- RETAIL STORE : Magasin de détail +- RESTAURANT : Restaurant +- K-12 SCHOOL : École primaire/secondaire +- COLLEGE/UNIVERSITY : Enseignement supérieur +- HOSPITAL : Hôpital +- HOTEL : Hôtel +- MULTIFAMILY HOUSING : Logement collectif +- WAREHOUSE : Entrepôt +- DATA CENTER : Centre de données +- Et autres usages spécialisés + +Chaque propriété est liée à une catégorie parent et possède un model_id +utilisé par le modèle ML pour l'encodage numérique. + +Endpoints disponibles : +- GET /api/v1/properties/ : Liste paginée des propriétés +- GET /api/v1/properties/{id} : Propriété spécifique par ID + +Auteur: François Hellebuyck +Projet: Building Energy Prediction API - OpenClassrooms Projet 5 +""" + +import logging +from typing import List + +from fastapi import APIRouter, Depends, HTTPException, Query, status +from sqlalchemy.orm import Session + +from project5.database import get_db +from project5.schemas.property import PropertyList, PropertyResponse +from project5.services.property_service import PropertyService + +logger = logging.getLogger(__name__) + +router = APIRouter(prefix="/api/v1/properties", tags=["properties"]) + + +@router.get( + "/", + response_model=List[PropertyList], + summary="Récupérer toutes les propriétés d'usage", + description=""" + Retourne la liste paginée de toutes les propriétés d'usage des bâtiments disponibles dans le système. + + Cette endpoint permet de récupérer l'ensemble des types d'usage spécifiques des + bâtiments utilisés pour une classification fine lors des prédictions énergétiques. + Les propriétés sont plus granulaires que les catégories et permettent de distinguer + des usages précis ayant des profils énergétiques différents. + + Propriétés d'usage disponibles dans le système : + - **OFFICE** : Bureaux et espaces administratifs + - **RETAIL STORE** : Magasins et commerces de détail + - **RESTAURANT** : Restaurants et services de restauration + - **K-12 SCHOOL** : Écoles primaires et secondaires + - **COLLEGE/UNIVERSITY** : Établissements d'enseignement supérieur + - **HOSPITAL** : Hôpitaux et centres médicaux + - **HOTEL** : Hôtels et hébergements + - **MULTIFAMILY HOUSING** : Logements collectifs et appartements + - **WAREHOUSE** : Entrepôts et stockage + - **DATA CENTER** : Centres de données et serveurs + - **BANK BRANCH** : Agences bancaires + - **COURTHOUSE** : Tribunaux et bâtiments judiciaires + - **LIBRARY** : Bibliothèques + - **MEDICAL OFFICE** : Cabinets médicaux + - **PARKING** : Structures de stationnement + - Et autres usages spécialisés + + Chaque propriété appartient à une catégorie parent et possède un model_id + unique utilisé par le modèle ML pour l'encodage numérique lors des prédictions. + + Paramètres de pagination : + - skip : nombre d'enregistrements à ignorer (par défaut 0) + - limit : nombre maximum d'enregistrements à retourner (par défaut 100, max 1000) + """, + responses={ + 200: { + "description": "Liste des propriétés récupérée avec succès", + "content": { + "application/json": { + "example": [ + { + "id": 20, + "model_id": 18, + "property_name": "OFFICE", + "category_id": 11, + "category_name": "Bureaux", + "created_at": "2025-09-09T09:56:21Z", + "updated_at": "2025-09-09T09:56:21Z", + }, + { + "id": 24, + "model_id": 22, + "property_name": "RETAIL STORE", + "category_id": 18, + "category_name": "Commerce de détail", + "created_at": "2025-09-09T09:56:21Z", + "updated_at": "2025-09-09T09:56:21Z", + }, + ] + } + }, + }, + 500: {"description": "Erreur interne du serveur"}, + }, +) +def get_properties( + skip: int = Query(0, ge=0, description="Nombre d'éléments à ignorer (pagination)"), + limit: int = Query( + 100, ge=1, le=1000, description="Nombre maximum d'éléments à retourner" + ), + db: Session = Depends(get_db), +): + """ + Récupère la liste paginée des propriétés d'usage des bâtiments. + + Args: + skip (int): Nombre d'enregistrements à ignorer pour la pagination + limit (int): Nombre maximum d'enregistrements à retourner + db (Session): Session de base de données injectée par FastAPI + + Returns: + List[PropertyList]: Liste des propriétés d'usage avec informations de catégorie + + Raises: + HTTPException: Erreur 500 si problème lors de la récupération des données + + Example: + GET /api/v1/properties/?skip=0&limit=30 + + Note: + Les propriétés sont référencées par leur model_id dans le modèle ML + pour effectuer une classification fine des usages lors des prédictions. + Chaque propriété hérite des caractéristiques de sa catégorie parent. + """ + try: + categories = PropertyService.get_all(db=db, skip=skip, limit=limit) + return categories + except Exception as e: + logger.error(f"Erreur lors de la récupération des properties: {e}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Erreur lors de la récupération des properties", + ) + + +@router.get( + "/{id}", + response_model=PropertyResponse, + summary="Récupérer une propriété d'usage par ID", + description=""" + Retourne les détails complets d'une propriété d'usage spécifique. + + Récupère toutes les informations d'une propriété d'usage incluant : + - Identifiant unique dans la base de données + - Identifiant utilisé par le modèle ML (model_id) + - Nom de la propriété d'usage + - Relation avec la catégorie parent (category_id et nom) + - Horodatages de création et modification + + Les propriétés d'usage permettent une classification fine des bâtiments + selon leur utilisation spécifique, au-delà des catégories générales. + Elles sont essentielles pour les prédictions énergétiques car différents + usages ont des profils de consommation très différents : + + - **OFFICE** vs **DATA CENTER** : consommation informatique différente + - **RESTAURANT** vs **RETAIL STORE** : besoins énergétiques distincts + - **HOSPITAL** vs **MEDICAL OFFICE** : intensité énergétique variable + - **K-12 SCHOOL** vs **COLLEGE/UNIVERSITY** : profils d'occupation différents + + L'ID doit correspondre à une propriété existante dans la base de données. + """, + responses={ + 200: { + "description": "Propriété trouvée et retournée avec succès", + "content": { + "application/json": { + "example": { + "id": 20, + "model_id": 18, + "property_name": "OFFICE", + "category_id": 11, + "category_name": "Bureaux", + "created_at": "2025-09-09T09:56:21Z", + "updated_at": "2025-09-09T09:56:21Z", + } + } + }, + }, + 404: { + "description": "Propriété non trouvée", + "content": { + "application/json": { + "example": {"detail": "Propriété avec l'ID 999 non trouvée"} + } + }, + }, + 500: {"description": "Erreur interne du serveur"}, + }, +) +def get_property(id: int, db: Session = Depends(get_db)): + """ + Récupère une propriété d'usage spécifique par son ID. + + Args: + id (int): Identifiant unique de la propriété à récupérer + db (Session): Session de base de données injectée par FastAPI + + Returns: + PropertyResponse: Propriété d'usage avec détails complets et catégorie parent + + Raises: + HTTPException: + - 404 si la propriété n'existe pas + - 500 si erreur lors de la récupération + + Example: + GET /api/v1/properties/20 + + Note: + Le model_id retourné est utilisé par le modèle ML pour encoder + l'usage spécifique lors des prédictions énergétiques. Cette granularité + permet des prédictions plus précises que les catégories générales. + """ + try: + property = PropertyService.get_by_id(db=db, property_id=id) + return property + except Exception as e: + logger.error(f"Erreur lors de la récupération de la propriété: {e}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Erreur lors de la récupération de la propriété", + ) diff --git a/src/project5/schemas/__init__.py b/src/project5/schemas/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..78015287f793182518b7699012ac2b74bb6d5993 --- /dev/null +++ b/src/project5/schemas/__init__.py @@ -0,0 +1,45 @@ +""" +Package des schémas Pydantic pour l'API de prédiction énergétique. + +Ce package contient tous les schémas de validation et sérialisation des données +utilisés par l'API FastAPI pour la gestion des bâtiments et prédictions énergétiques. + +Schémas disponibles : +- Neighborhood : Schémas pour les quartiers de Seattle +- NeighborhoodExists : Validation d'existence de quartier +- PaginatedNeighborhoods : Réponses paginées de quartiers +- HealthCheck : Schéma de vérification de l'état de l'API +- ServiceInfo : Informations sur le service API +- ErrorResponse : Schéma de réponse d'erreur standardisé + +Architecture des schémas : +- Validation des données d'entrée avec Pydantic Field contraints +- Sérialisation automatique depuis les modèles SQLAlchemy +- Support de la pagination pour les listes d'éléments +- Gestion des erreurs avec messages structurés +- Configuration de JSON encoders pour les types spéciaux (Decimal, datetime) + +Note : +Ces schémas assurent la validation côté API et la documentation automatique +OpenAPI/Swagger. Ils définissent les contrats d'interface entre clients et serveur. +""" + +# Import du schema Neighborhood depuis le fichier approprié +from .neighborhood import ( + ErrorResponse, + HealthCheck, + Neighborhood, + NeighborhoodExists, + PaginatedNeighborhoods, + ServiceInfo, +) + +# Exposer les schemas au niveau du package +__all__ = [ + "Neighborhood", + "NeighborhoodExists", + "PaginatedNeighborhoods", + "HealthCheck", + "ServiceInfo", + "ErrorResponse", +] diff --git a/src/project5/schemas/building_energy_prediction.py b/src/project5/schemas/building_energy_prediction.py new file mode 100644 index 0000000000000000000000000000000000000000..80fcdc6aef4b48817c09208de7cde49948175325 --- /dev/null +++ b/src/project5/schemas/building_energy_prediction.py @@ -0,0 +1,253 @@ +""" +Schémas Pydantic pour les prédictions énergétiques des bâtiments. + +Ce module définit tous les schémas de validation et sérialisation pour les données +de prédictions énergétiques calculées par le modèle de machine learning. + +Classes disponibles : +- BuildingEnergyPredictionsBase : Schéma de base avec les champs communs +- BuildingEnergyPredictionsCreate : Schéma pour la création de nouvelles prédictions +- BuildingEnergyPredictionsUpdate : Schéma pour la mise à jour de prédictions +- BuildingEnergyPredictionsResponse : Schéma de réponse avec ID et métadonnées +- BuildingEnergyPredictionsList : Schéma pour les réponses paginées +- BuildingEnergyPredictionsStats : Schéma pour les statistiques agrégées + +Fonctionnalités : +- Validation automatique des types et contraintes (valeurs positives, etc.) +- Sérialisation automatique depuis les modèles SQLAlchemy +- Encodage JSON personnalisé pour Decimal et datetime +- Support de la pagination avec métadonnées (total, pages, etc.) +- Calculs statistiques (moyennes, min/max, compteurs) + +Note : +Les prédictions énergétiques sont exprimées en kBTU (thousand British Thermal Units) +avec normalisation météorologique pour compenser les variations climatiques. +""" + +from datetime import datetime +from decimal import Decimal +from typing import Optional + +from pydantic import BaseModel, ConfigDict, Field + + +class BuildingEnergyPredictionsBase(BaseModel): + """ + Schéma de base pour les prédictions énergétiques des bâtiments. + + Cette classe définit les champs communs utilisés par tous les schémas + de prédictions énergétiques (création, mise à jour, réponse). + + Attributes: + building_id (int): Identifiant du bâtiment concerné (obligatoire, > 0) + site_energy_use_wn_kbtu (Decimal, optional): Consommation énergétique + en kBTU avec normalisation météorologique (>= 0) + predicted (bool, optional): Indique si la valeur est prédite (True) + ou mesurée (False). Par défaut True. + updated_at (datetime, optional): Date et heure de la prédiction + avec fuseau horaire + + Note: + La normalisation météorologique ajuste les données de consommation + pour compenser les variations climatiques saisonnières. + """ + + building_id: int = Field( + ..., + description="Identifiant du bâtiment concerné (clé étrangère vers buildings.id)", + gt=0, + ) + site_energy_use_wn_kbtu: Optional[Decimal] = Field( + None, + description="Consommation énergétique du site en kBTU avec normalisation météorologique", + ge=0, + ) + predicted: Optional[bool] = Field( + True, description="Indique si la valeur est prédite (TRUE) ou mesurée (FALSE)" + ) + updated_at: Optional[datetime] = Field( + None, description="Date et heure de la prédiction avec fuseau horaire" + ) + + +class BuildingEnergyPredictionsCreate(BuildingEnergyPredictionsBase): + """ + Schéma pour la création d'une nouvelle prédiction énergétique. + + Hérite de BuildingEnergyPredictionsBase et utilise tous ses champs + sans modification. Ce schéma est utilisé lors des requêtes POST + pour créer de nouvelles prédictions dans la base de données. + + Usage: + - Endpoint POST /building-energy-predictions/ + - Validation automatique des contraintes de la classe de base + - Génération automatique de l'ID lors de l'insertion en base + + Example: + { + "building_id": 12345, + "site_energy_use_wn_kbtu": 2500.75, + "predicted": true, + "updated_at": "2024-01-15T10:30:00Z" + } + """ + + pass + + +class BuildingEnergyPredictionsUpdate(BaseModel): + """ + Schéma pour la mise à jour partielle d'une prédiction énergétique. + + Tous les champs sont optionnels pour permettre les mises à jour partielles. + Seuls les champs fournis seront mis à jour dans la base de données. + + Attributes: + site_energy_use_wn_kbtu (Decimal, optional): Nouvelle consommation + énergétique en kBTU (>= 0) + predicted (bool, optional): Nouveau statut prédiction/mesure + updated_at (datetime, optional): Nouvelle date de prédiction + + Usage: + - Endpoint PATCH /building-energy-predictions/{id} + - Permet la mise à jour de champs individuels + - building_id non modifiable (contrainte métier) + + Note: + L'ID du bâtiment (building_id) n'est pas modifiable pour préserver + l'intégrité référentielle des données. + """ + + site_energy_use_wn_kbtu: Optional[Decimal] = Field( + None, + description="Consommation énergétique du site en kBTU avec normalisation météorologique", + ge=0, + ) + predicted: Optional[bool] = Field( + None, description="Indique si la valeur est prédite (TRUE) ou mesurée (FALSE)" + ) + updated_at: Optional[datetime] = Field( + None, description="Date et heure de la prédiction avec fuseau horaire" + ) + + +class BuildingEnergyPredictionsResponse(BuildingEnergyPredictionsBase): + """ + Schéma de réponse pour une prédiction énergétique individuelle. + + Hérite de BuildingEnergyPredictionsBase et ajoute l'ID généré + automatiquement par la base de données. Inclut la configuration + de sérialisation pour la conversion depuis les modèles SQLAlchemy. + + Attributes: + id (int): Identifiant unique de la prédiction (clé primaire) + + tous les champs de BuildingEnergyPredictionsBase + + Configuration: + - from_attributes=True : Conversion automatique depuis SQLAlchemy + - str_strip_whitespace=True : Nettoyage automatique des chaînes + - validate_assignment=True : Validation lors des assignations + - json_encoders : Encodage personnalisé Decimal->float, datetime->ISO + + Usage: + - Réponses des endpoints GET /building-energy-predictions/ + - Sérialisation automatique des objets BuildingEnergyPredictions + """ + + id: int = Field( + ..., + description="Identifiant unique de la prédiction (clé primaire auto-incrémentée)", + ) + + model_config = ConfigDict( + from_attributes=True, + # Permet la conversion automatique depuis les objets SQLAlchemy + str_strip_whitespace=True, + # Validation des types + validate_assignment=True, + # Configuration pour les décimales + json_encoders={ + Decimal: lambda v: float(v) if v is not None else None, + datetime: lambda v: v.isoformat() if v is not None else None, + }, + ) + + +class BuildingEnergyPredictionsList(BaseModel): + """ + Schéma pour une liste paginée de prédictions énergétiques. + + Structure de réponse standardisée pour les endpoints retournant + plusieurs prédictions avec support de la pagination. + + Attributes: + items (list[BuildingEnergyPredictionsResponse]): Liste des prédictions + total (int): Nombre total d'éléments dans la base (toutes pages) + page (int): Numéro de la page actuelle (commence à 1) + size (int): Nombre d'éléments par page (1-100) + pages (int): Nombre total de pages calculé + + Usage: + - Endpoint GET /building-energy-predictions/ avec paramètres de pagination + - Calcul automatique du nombre de pages : ceil(total / size) + - Navigation facilitée avec métadonnées de pagination + + Example de réponse: + { + "items": [...], + "total": 1500, + "page": 2, + "size": 50, + "pages": 30 + } + """ + + items: list[BuildingEnergyPredictionsResponse] + total: int = Field(..., description="Nombre total d'éléments") + page: int = Field(..., description="Page actuelle", ge=1) + size: int = Field(..., description="Taille de la page", ge=1, le=100) + pages: int = Field(..., description="Nombre total de pages") + + +class BuildingEnergyPredictionsStats(BaseModel): + """ + Schéma pour les statistiques agrégées des prédictions énergétiques. + + Fournit une vue d'ensemble des données de prédictions avec des + métriques statistiques calculées côté base de données. + + Attributes: + total_predictions (int): Nombre total de prédictions en base + predicted_count (int): Nombre de valeurs prédites (ML) + measured_count (int): Nombre de valeurs mesurées (réelles) + avg_energy_consumption (Decimal, optional): Consommation moyenne en kBTU + min_energy_consumption (Decimal, optional): Consommation minimale en kBTU + max_energy_consumption (Decimal, optional): Consommation maximale en kBTU + latest_updated_at (datetime, optional): Date de la prédiction + la plus récente + + Usage: + - Endpoint GET /building-energy-predictions/stats + - Tableaux de bord et reporting + - Monitoring de la qualité des prédictions + + Note: + Les statistiques sont calculées en temps réel via des requêtes + d'agrégation SQL pour garantir la fraîcheur des données. + """ + + total_predictions: int = Field(..., description="Nombre total de prédictions") + predicted_count: int = Field(..., description="Nombre de valeurs prédites") + measured_count: int = Field(..., description="Nombre de valeurs mesurées") + avg_energy_consumption: Optional[Decimal] = Field( + None, description="Consommation énergétique moyenne en kBTU" + ) + min_energy_consumption: Optional[Decimal] = Field( + None, description="Consommation énergétique minimale en kBTU" + ) + max_energy_consumption: Optional[Decimal] = Field( + None, description="Consommation énergétique maximale en kBTU" + ) + latest_updated_at: Optional[datetime] = Field( + None, description="Date de la prédiction la plus récente" + ) diff --git a/src/project5/schemas/building_model.py b/src/project5/schemas/building_model.py new file mode 100644 index 0000000000000000000000000000000000000000..9fac16ab91f2cee04b33fdbff2951cec7aa52e15 --- /dev/null +++ b/src/project5/schemas/building_model.py @@ -0,0 +1,760 @@ +""" +Schémas Pydantic pour les modèles de bâtiments et leurs caractéristiques. + +Ce module définit tous les schémas de validation et sérialisation pour les données +des bâtiments utilisées dans le système de prédiction énergétique de Seattle. + +Classes principales : +- BuildingModelsBase : Schéma de base avec toutes les caractéristiques +- BuildingModelsCreate : Schéma pour la création de nouveaux bâtiments +- BuildingModelsUpdate : Schéma pour les mises à jour partielles +- BuildingModelsResponse : Schéma de réponse avec métadonnées +- BuildingModelsWithRelations : Schéma incluant les relations (quartier, type, etc.) +- BuildingModelsList : Schéma pour les réponses paginées +- BuildingModelsStats : Schéma pour les statistiques agrégées +- BuildingModelsSearch : Schéma pour les critères de recherche + +Classes spécialisées : +- ModelViewSchema : Schéma pour les requêtes ML avec champs optimisés +- BuildingFeatures : Schéma des caractéristiques pour les prédictions ML +- PredictionResponse : Schéma de réponse des prédictions énergétiques + +Fonctionnalités : +- Validation exhaustive des coordonnées GPS, surfaces, dates +- Contraintes métier (années de construction, nombre d'étages, etc.) +- Validation de formats (codes postaux, précision des décimales) +- Support de la recherche multicritères avec filtres +- Fonctions de conversion depuis les résultats SQLAlchemy +- Configuration JSON personnalisée pour types complexes + +Note : +Ce module est central dans l'application car il gère les données principales +des bâtiments utilisées par le modèle ML pour les prédictions énergétiques. +""" + +from datetime import datetime +from decimal import Decimal +from typing import Optional + +from pydantic import BaseModel, ConfigDict, Field, field_validator, validator + + +class BuildingModelsBase(BaseModel): + """Schéma de base pour les modèles de bâtiments""" + + ose_building_id: str = Field( + ..., + max_length=50, + description="Identifiant unique du bâtiment dans le système OSE", + ) + address: Optional[str] = Field(None, description="Adresse du bâtiment") + city: Optional[str] = Field("Seattle", max_length=100, description="Ville") + state: Optional[str] = Field("WA", max_length=10, description="État") + zip_code: Optional[str] = Field(None, max_length=20, description="Code postal") + tax_parcel_identification_number: Optional[str] = Field( + None, + max_length=100, + description="Numéro d'identification fiscale de la parcelle", + ) + council_district_code: Optional[str] = Field( + None, max_length=20, description="Code du district du conseil" + ) + latitude: Optional[Decimal] = Field( + None, description="Latitude géographique", ge=-90, le=90 + ) + longitude: Optional[Decimal] = Field( + None, description="Longitude géographique", ge=-180, le=180 + ) + year_built: Optional[int] = Field( + None, description="Année de construction du bâtiment", ge=1800, le=2030 + ) + number_of_buildings: Optional[int] = Field( + None, description="Nombre de bâtiments", ge=0 + ) + number_of_floors: Optional[int] = Field(None, description="Nombre d'étages", ge=0) + property_gfa_total: Optional[Decimal] = Field( + None, description="Surface brute totale (GFA) en pieds carrés", ge=0 + ) + property_gfa_parking: Optional[Decimal] = Field( + None, description="Surface brute de parking en pieds carrés", ge=0 + ) + second_largest_property_use_type_gfa: Optional[Decimal] = Field( + None, description="Surface GFA du deuxième type d'usage principal", ge=0 + ) + third_largest_property_use_type_gfa: Optional[Decimal] = Field( + None, description="Surface GFA du troisième type d'usage principal", ge=0 + ) + multiusage: Optional[int] = Field( + None, + description="Indicateur si le bâtiment a plusieurs usages (0 ou 1)", + ge=0, + le=1, + ) + steam: Optional[int] = Field( + None, description="Indicateur d'utilisation de vapeur (0 ou 1)", ge=0, le=1 + ) + electricity: Optional[int] = Field( + None, description="Indicateur d'utilisation d'électricité (0 ou 1)", ge=0, le=1 + ) + natural_gas: Optional[int] = Field( + None, description="Indicateur d'utilisation de gaz naturel (0 ou 1)", ge=0, le=1 + ) + neighborhood_id: Optional[int] = Field( + None, description="Identifiant du quartier", gt=0 + ) + building_type_id: Optional[int] = Field( + None, description="Identifiant du type de bâtiment", gt=0 + ) + largest_property_use_type_id: Optional[int] = Field( + None, description="ID du type d'usage principal le plus important", gt=0 + ) + primary_property_type_id: Optional[int] = Field( + None, description="ID du type de propriété principal", gt=0 + ) + second_largest_property_use_type_id: Optional[int] = Field( + None, description="ID du deuxième type d'usage principal", gt=0 + ) + third_largest_property_use_type_id: Optional[int] = Field( + None, description="ID du troisième type d'usage principal", gt=0 + ) + + @field_validator("zip_code") + @classmethod + def validate_zip_code(cls, v): + if v and not v.replace("-", "").isdigit(): + raise ValueError( + "Code postal doit contenir uniquement des chiffres et tirets" + ) + return v + + @field_validator("latitude", "longitude") + @classmethod + def validate_coordinates(cls, v): + if v is not None: + # Vérifier le nombre de décimales (max 8) + if v.as_tuple().exponent < -8: + raise ValueError("Maximum 8 decimal places allowed for coordinates") + return v + + @field_validator( + "property_gfa_total", + "property_gfa_parking", + "second_largest_property_use_type_gfa", + "third_largest_property_use_type_gfa", + ) + @classmethod + def validate_gfa_values(cls, v): + if v is not None: + # Vérifier le nombre de décimales (max 2) + if v.as_tuple().exponent < -2: + raise ValueError("Maximum 2 decimal places allowed for GFA values") + # Vérifier le nombre total de chiffres (max 12) + total_digits = len([d for d in v.as_tuple().digits]) + if total_digits > 12: + raise ValueError("Maximum 12 digits allowed for GFA values") + return v + + +class BuildingModelsCreate(BuildingModelsBase): + """Schéma pour la création d'un modèle de bâtiment avec champs obligatoires pour les prédictions ML""" + + # Champs obligatoires pour les prédictions ML (17 features du modèle) + year_built: int = Field( + ..., + description="Année de construction du bâtiment (OBLIGATOIRE)", + ge=1800, + le=2030, + ) + number_of_buildings: int = Field( + ..., description="Nombre de bâtiments (OBLIGATOIRE)", ge=1 + ) + number_of_floors: int = Field( + ..., description="Nombre d'étages (OBLIGATOIRE)", ge=1 + ) + property_gfa_total: Decimal = Field( + ..., + description="Surface brute totale (GFA) en pieds carrés (OBLIGATOIRE)", + gt=0, + ) + property_gfa_parking: Decimal = Field( + ..., description="Surface brute de parking en pieds carrés (OBLIGATOIRE)", ge=0 + ) + second_largest_property_use_type_gfa: Decimal = Field( + ..., + description="Surface GFA du deuxième type d'usage principal (OBLIGATOIRE)", + ge=0, + ) + third_largest_property_use_type_gfa: Decimal = Field( + ..., + description="Surface GFA du troisième type d'usage principal (OBLIGATOIRE)", + ge=0, + ) + multiusage: int = Field( + ..., + description="Indicateur si le bâtiment a plusieurs usages - 0 ou 1 (OBLIGATOIRE)", + ge=0, + le=1, + ) + steam: int = Field( + ..., + description="Indicateur d'utilisation de vapeur - 0 ou 1 (OBLIGATOIRE)", + ge=0, + le=1, + ) + electricity: int = Field( + ..., + description="Indicateur d'utilisation d'électricité - 0 ou 1 (OBLIGATOIRE)", + ge=0, + le=1, + ) + natural_gas: int = Field( + ..., + description="Indicateur d'utilisation de gaz naturel - 0 ou 1 (OBLIGATOIRE)", + ge=0, + le=1, + ) + neighborhood_id: int = Field( + ..., description="Identifiant du quartier (OBLIGATOIRE)", gt=0 + ) + building_type_id: int = Field( + ..., description="Identifiant du type de bâtiment (OBLIGATOIRE)", gt=0 + ) + largest_property_use_type_id: int = Field( + ..., + description="ID du type d'usage principal le plus important (OBLIGATOIRE)", + gt=0, + ) + primary_property_type_id: int = Field( + ..., description="ID du type de propriété principal (OBLIGATOIRE)", gt=0 + ) + second_largest_property_use_type_id: int = Field( + ..., description="ID du deuxième type d'usage principal (OBLIGATOIRE)", gt=0 + ) + third_largest_property_use_type_id: int = Field( + ..., description="ID du troisième type d'usage principal (OBLIGATOIRE)", gt=0 + ) + + +class BuildingModelsUpdate(BaseModel): + """Schéma pour la mise à jour d'un modèle de bâtiment""" + + ose_building_id: Optional[str] = Field( + None, + max_length=50, + description="Identifiant unique du bâtiment dans le système OSE", + ) + address: Optional[str] = Field(None, description="Adresse du bâtiment") + city: Optional[str] = Field(None, max_length=100, description="Ville") + state: Optional[str] = Field(None, max_length=10, description="État") + zip_code: Optional[str] = Field(None, max_length=20, description="Code postal") + tax_parcel_identification_number: Optional[str] = Field( + None, + max_length=100, + description="Numéro d'identification fiscale de la parcelle", + ) + council_district_code: Optional[str] = Field( + None, max_length=20, description="Code du district du conseil" + ) + latitude: Optional[Decimal] = Field( + None, description="Latitude géographique", ge=-90, le=90 + ) + longitude: Optional[Decimal] = Field( + None, description="Longitude géographique", ge=-180, le=180 + ) + year_built: Optional[int] = Field( + None, description="Année de construction du bâtiment", ge=1800, le=2030 + ) + number_of_buildings: Optional[int] = Field( + None, description="Nombre de bâtiments", ge=0 + ) + number_of_floors: Optional[int] = Field(None, description="Nombre d'étages", ge=0) + property_gfa_total: Optional[Decimal] = Field( + None, description="Surface brute totale (GFA) en pieds carrés", ge=0 + ) + property_gfa_parking: Optional[Decimal] = Field( + None, description="Surface brute de parking en pieds carrés", ge=0 + ) + second_largest_property_use_type_gfa: Optional[Decimal] = Field( + None, description="Surface GFA du deuxième type d'usage principal", ge=0 + ) + third_largest_property_use_type_gfa: Optional[Decimal] = Field( + None, description="Surface GFA du troisième type d'usage principal", ge=0 + ) + multiusage: Optional[int] = Field( + None, + description="Indicateur si le bâtiment a plusieurs usages (0 ou 1)", + ge=0, + le=1, + ) + steam: Optional[int] = Field( + None, description="Indicateur d'utilisation de vapeur (0 ou 1)", ge=0, le=1 + ) + electricity: Optional[int] = Field( + None, description="Indicateur d'utilisation d'électricité (0 ou 1)", ge=0, le=1 + ) + natural_gas: Optional[int] = Field( + None, description="Indicateur d'utilisation de gaz naturel (0 ou 1)", ge=0, le=1 + ) + neighborhood_id: Optional[int] = Field( + None, description="Identifiant du quartier", gt=0 + ) + building_type_id: Optional[int] = Field( + None, description="Identifiant du type de bâtiment", gt=0 + ) + largest_property_use_type_id: Optional[int] = Field( + None, description="ID du type d'usage principal le plus important", gt=0 + ) + primary_property_type_id: Optional[int] = Field( + None, description="ID du type de propriété principal", gt=0 + ) + second_largest_property_use_type_id: Optional[int] = Field( + None, description="ID du deuxième type d'usage principal", gt=0 + ) + third_largest_property_use_type_id: Optional[int] = Field( + None, description="ID du troisième type d'usage principal", gt=0 + ) + + +class BuildingModelsResponse(BuildingModelsBase): + """Schéma de réponse pour un modèle de bâtiment""" + + id: int = Field(..., description="Identifiant unique du bâtiment (clé primaire)") + created_at: Optional[datetime] = Field(None, description="Date de création") + updated_at: Optional[datetime] = Field( + None, description="Date de dernière mise à jour" + ) + + model_config = ConfigDict( + from_attributes=True, + str_strip_whitespace=True, + validate_assignment=True, + json_encoders={ + Decimal: lambda v: float(v) if v is not None else None, + datetime: lambda v: v.isoformat() if v is not None else None, + }, + ) + + +class BuildingModelsWithRelations(BuildingModelsResponse): + """Schéma de réponse avec les relations""" + + # Relations (pour éviter les imports circulaires, on utilise des dictionnaires) + building_type: Optional[dict] = Field(None, description="Type de bâtiment associé") + neighborhood: Optional[dict] = Field(None, description="Quartier associé") + largest_property_use_type: Optional[dict] = Field( + None, description="Type d'usage principal le plus important" + ) + primary_property_type: Optional[dict] = Field( + None, description="Type de propriété principal" + ) + second_largest_property_use_type: Optional[dict] = Field( + None, description="Deuxième type d'usage principal" + ) + third_largest_property_use_type: Optional[dict] = Field( + None, description="Troisième type d'usage principal" + ) + + +class BuildingModelsList(BaseModel): + """Schéma pour une liste paginée de modèles de bâtiments""" + + items: list[BuildingModelsResponse] + total: int = Field(..., description="Nombre total d'éléments") + page: int = Field(..., description="Page actuelle", ge=1) + size: int = Field(..., description="Taille de la page", ge=1, le=100) + pages: int = Field(..., description="Nombre total de pages") + + +class ModelViewSchema(BaseModel): + """ + Schéma Pydantic pour le résultat de la requête complexe BuildingModels + Correspond exactement aux colonnes sélectionnées dans votre requête + """ + + # Données principales du bâtiment (de BuildingModels) + id: int = Field(..., description="Identifiant unique du bâtiment") + year_built: Optional[int] = Field( + None, description="Année de construction du bâtiment" + ) + number_of_buildings: Optional[int] = Field( + None, description="Nombre de bâtiments dans le complexe" + ) + number_of_floors: Optional[int] = Field(None, description="Nombre d'étages") + + # Surfaces (GFA - Gross Floor Area) + property_gfa_total: Optional[Decimal] = Field( + None, description="Surface brute totale (pieds carrés)" + ) + property_gfa_parking: Optional[Decimal] = Field( + None, description="Surface brute de stationnement (pieds carrés)" + ) + second_largest_property_use_type_gfa: Optional[Decimal] = Field( + None, description="Surface du deuxième type d'usage principal (pieds carrés)" + ) + third_largest_property_use_type_gfa: Optional[Decimal] = Field( + None, description="Surface du troisième type d'usage principal (pieds carrés)" + ) + + # Caractéristiques d'usage + multiusage: Optional[bool] = Field( + None, description="Indicateur de bâtiment multi-usage" + ) + + # Types d'énergie utilisés + steam: Optional[bool] = Field(None, description="Utilisation de vapeur") + electricity: Optional[bool] = Field(None, description="Utilisation d'électricité") + natural_gas: Optional[bool] = Field(None, description="Utilisation de gaz naturel") + + # IDs des modèles ( des tables de référence) + neighborhood_id: Optional[int] = Field( + None, description="Model ID du quartier associé" + ) + building_type_id: Optional[int] = Field( + None, description="Model ID du type de bâtiment associé" + ) + largest_property_use_type_id: Optional[int] = Field( + None, description="Model ID du type d'usage principal le plus important" + ) + primary_property_type_id: Optional[int] = Field( + None, description="Model ID du type de propriété principal" + ) + second_largest_property_use_type_id: Optional[int] = Field( + None, description="Model ID du deuxième type d'usage principal" + ) + third_largest_property_use_type_id: Optional[int] = Field( + None, description="Model ID du troisième type d'usage principal" + ) + + +class BuildingModelsStats(BaseModel): + """Schéma pour les statistiques des modèles de bâtiments""" + + total_buildings: int = Field(..., description="Nombre total de bâtiments") + avg_year_built: Optional[float] = Field( + None, description="Année moyenne de construction" + ) + avg_floors: Optional[float] = Field(None, description="Nombre moyen d'étages") + avg_gfa_total: Optional[Decimal] = Field( + None, description="Surface GFA totale moyenne" + ) + buildings_by_neighborhood: dict = Field( + default_factory=dict, description="Répartition par quartier" + ) + buildings_by_type: dict = Field( + default_factory=dict, description="Répartition par type de bâtiment" + ) + energy_sources: dict = Field( + default_factory=dict, description="Répartition des sources d'énergie" + ) + + +class BuildingModelsSearch(BaseModel): + """Schéma pour la recherche de bâtiments""" + + city: Optional[str] = None + state: Optional[str] = None + zip_code: Optional[str] = None + neighborhood_id: Optional[int] = None + building_type_id: Optional[int] = None + year_built_min: Optional[int] = Field(None, ge=1800) + year_built_max: Optional[int] = Field(None, le=2030) + floors_min: Optional[int] = Field(None, ge=0) + floors_max: Optional[int] = Field(None, ge=0) + gfa_min: Optional[Decimal] = Field(None, ge=0) + gfa_max: Optional[Decimal] = Field(None, ge=0) + has_steam: Optional[bool] = None + has_electricity: Optional[bool] = None + has_natural_gas: Optional[bool] = None + is_multiuse: Optional[bool] = None + + +# Fonction utilitaire pour convertir les résultats de requête +def convert_query_result_to_schema(query_result) -> ModelViewSchema: + """ + Convertit un résultat de requête SQLAlchemy vers le schéma Pydantic + + Args: + query_result: Résultat de votre requête db.query(...) + + Returns: + ModelViewSchema: Instance du schéma Pydantic + """ + try: + # Débogage - afficher le type et le contenu + print(f"Type de query_result: {type(query_result)}") + print(f"Contenu: {query_result}") + + # Si c'est un objet Row de SQLAlchemy (le cas le plus probable) + if hasattr(query_result, "_mapping"): + # SQLAlchemy Row avec _mapping + data = dict(query_result._mapping) + return ModelViewSchema(**data) + + # Si c'est un objet Row avec accès par index + elif hasattr(query_result, "__getitem__") and hasattr(query_result, "_fields"): + # Row avec _fields + data = { + field: query_result[i] for i, field in enumerate(query_result._fields) + } + return ModelViewSchema(**data) + + # Si c'est un Row SQLAlchemy moderne (version récente) + elif str(type(query_result)).endswith("Row'>"): + # Convertir Row en dictionnaire + field_names = [ + "id", + "year_built", + "number_of_buildings", + "number_of_floors", + "property_gfa_total", + "property_gfa_parking", + "second_largest_property_use_type_gfa", + "third_largest_property_use_type_gfa", + "multiusage", + "steam", + "electricity", + "natural_gas", + "neighborhood_id", + "building_type_id", + "largest_property_use_type_id", + "primary_property_type_id", + "second_largest_property_use_type_id", + "third_largest_property_use_type_id", + ] + + # Créer le dictionnaire en mappant les valeurs par position + data = {} + for i, field_name in enumerate(field_names): + if i < len(query_result): + data[field_name] = query_result[i] + else: + data[field_name] = None + + return ModelViewSchema(**data) + + # Si c'est déjà un objet avec des attributs nommés + elif hasattr(query_result, "id"): + return ModelViewSchema.model_validate(query_result) + + # Si c'est un tuple/liste (résultat de query avec colonnes spécifiques) + elif isinstance(query_result, (tuple, list)): + field_names = [ + "id", + "year_built", + "number_of_buildings", + "number_of_floors", + "property_gfa_total", + "property_gfa_parking", + "second_largest_property_use_type_gfa", + "third_largest_property_use_type_gfa", + "multiusage", + "steam", + "electricity", + "natural_gas", + "neighborhood_id", + "building_type_id", + "largest_property_use_type_id", + "primary_property_type_id", + "second_largest_property_use_type_id", + "third_largest_property_use_type_id", + ] + + data = {field_names[i]: query_result[i] for i in range(len(query_result))} + return ModelViewSchema(**data) + + # Si c'est un dictionnaire + elif isinstance(query_result, dict): + return ModelViewSchema(**data) + + else: + raise ValueError(f"Type de résultat non supporté: {type(query_result)}") + + except Exception as e: + print(f"Erreur détaillée lors de la conversion: {e}") + print(f"Type: {type(query_result)}") + print(f"Dir: {dir(query_result)}") + raise ValueError(f"Erreur lors de la conversion: {e}") + + +# Version alternative plus robuste +def convert_row_to_dict(row, field_names=None): + """ + Convertit spécifiquement un objet Row SQLAlchemy en dictionnaire + """ + if field_names is None: + field_names = [ + "id", + "year_built", + "number_of_buildings", + "number_of_floors", + "property_gfa_total", + "property_gfa_parking", + "second_largest_property_use_type_gfa", + "third_largest_property_use_type_gfa", + "multiusage", + "steam", + "electricity", + "natural_gas", + "neighborhood_id", + "building_type_id", + "largest_property_use_type_id", + "primary_property_type_id", + "second_largest_property_use_type_id", + "third_largest_property_use_type_id", + ] + + try: + # Méthode 1: Via _mapping (SQLAlchemy moderne) + if hasattr(row, "_mapping"): + return dict(row._mapping) + + # Méthode 2: Via _asdict() si disponible + elif hasattr(row, "_asdict"): + return row._asdict() + + # Méthode 3: Via keys() et values() + elif hasattr(row, "keys") and hasattr(row, "values"): + return dict(zip(row.keys(), row.values())) + + # Méthode 4: Accès par index + elif hasattr(row, "__getitem__"): + return { + field_names[i]: row[i] for i in range(len(field_names)) if i < len(row) + } + + # Méthode 5: Conversion en tuple puis dictionnaire + else: + row_tuple = tuple(row) + return { + field_names[i]: row_tuple[i] + for i in range(len(field_names)) + if i < len(row_tuple) + } + + except Exception as e: + print(f"Erreur dans convert_row_to_dict: {e}") + # Fallback: essayer de convertir en liste puis dictionnaire + try: + row_list = list(row) + return { + field_names[i]: row_list[i] + for i in range(min(len(field_names), len(row_list))) + } + except Exception as fallback_error: + print(f"Erreur fallback: {fallback_error}") + print(f"Type de row: {type(row)}") + print(f"Longueur field_names: {len(field_names)}") + raise ValueError( + f"Impossible de convertir le Row. Erreur originale: {e}, Erreur fallback: {fallback_error}" + ) + + +# Fonction simplifiée recommandée +def convert_sqlalchemy_row_to_schema(row) -> ModelViewSchema: + """ + Version simplifiée et robuste pour convertir un Row SQLAlchemy + """ + try: + # Convertir le Row en dictionnaire + row_dict = convert_row_to_dict(row) + + # Créer le schéma Pydantic + return ModelViewSchema(**row_dict) + + except Exception as e: + print(f"Erreur dans convert_sqlalchemy_row_to_schema: {e}") + print(f"Type de row: {type(row)}") + print(f"Contenu de row: {row}") + raise + + +class BuildingFeatures(BaseModel): + """Schéma Pydantic pour les caractéristiques du bâtiment""" + + year_built: int = Field(..., description="Année de construction", ge=1800, le=2024) + number_of_buildings: int = Field(..., description="Nombre de bâtiments", ge=1) + number_of_floors: int = Field(..., description="Nombre d'étages", ge=1) + property_gfa_total: float = Field(..., description="Surface totale brute") + property_gfa_parking: float = Field(..., description="Surface de parking") + second_largest_property_use_type_gfa: float = Field( + ..., description="Surface du 2e type d'usage" + ) + third_largest_property_use_type_gfa: float = Field( + ..., description="Surface du 3e type d'usage" + ) + multiusage: int = Field(..., description="Usage multiple") + steam: int = Field(..., description="Chauffage vapeur") + electricity: int = Field(..., description="Électricité") + natural_gas: int = Field(..., description="Gaz naturel") + neighborhood_id: int = Field(..., description="ID du quartier") + building_type_id: int = Field(..., description="ID du type de bâtiment") + largest_property_use_type_id: int = Field( + ..., description="ID du plus grand type d'usage" + ) + primary_property_type_id: int = Field( + ..., description="ID du type de propriété principal" + ) + second_largest_property_use_type_id: int = Field( + ..., description="ID du 2e type d'usage" + ) + third_largest_property_use_type_id: int = Field( + ..., description="ID du 3e type d'usage" + ) + + @validator( + "property_gfa_total", + "property_gfa_parking", + "second_largest_property_use_type_gfa", + "third_largest_property_use_type_gfa", + ) + def validate_gfa_fields(cls, v): + """Valide et convertit les champs GFA en float""" + try: + return float(v) + except ValueError: + raise ValueError("Les champs GFA doivent être des nombres valides") + + class Config: + json_schema_extra = { + "example": { + "year_built": 1926, + "number_of_buildings": 1, + "number_of_floors": 11, + "property_gfa_total": "83008.00", + "property_gfa_parking": "0.00", + "second_largest_property_use_type_gfa": "0.00", + "third_largest_property_use_type_gfa": "0.00", + "multiusage": False, + "steam": False, + "electricity": True, + "natural_gas": True, + "neighborhood_id": 3, + "building_type_id": 1, + "largest_property_use_type_id": 18, + "primary_property_type_id": 18, + "second_largest_property_use_type_id": -1, + "third_largest_property_use_type_id": -1, + } + } + + +class PredictionResponse(BaseModel): + """Schéma de réponse pour les prédictions""" + + prediction: float = Field(..., description="Valeur prédite par le modèle") + prediction_log: float = Field(..., description="Valeur prédite par le modèle") + model_version: str = Field(..., description="Version du modèle utilisé") + confidence_interval_log: Optional[dict] = Field( + None, description="Intervalle de confiance" + ) + + class Config: + protected_namespaces = () + json_schema_extra = { + "example": { + "prediction": 6371813.11, + "prediction_log": 15.67, + "model_version": "v1.0", + "confidence_interval_log": {"lower": 15.15, "upper": 16.05}, + } + } diff --git a/src/project5/schemas/building_type.py b/src/project5/schemas/building_type.py new file mode 100644 index 0000000000000000000000000000000000000000..0351783ac6f6498d55c6282488a8ece88e9dca36 --- /dev/null +++ b/src/project5/schemas/building_type.py @@ -0,0 +1,253 @@ +""" +Schémas Pydantic pour les types de bâtiments (Building Types). + +Ce module définit tous les schémas de validation et sérialisation pour les données +des types de bâtiments utilisés dans la classification énergétique de Seattle. + +Classes disponibles : +- BuildingTypeBase : Schéma de base avec les champs communs +- BuildingTypeCreate : Schéma pour la création de nouveaux types +- BuildingTypeUpdate : Schéma pour les mises à jour partielles +- BuildingTypeResponse : Schéma de réponse complet avec métadonnées +- BuildingTypeList : Schéma optimisé pour les listes (performance) +- BuildingTypeRef : Schéma de référence simple pour les relations + +Fonctionnalités : +- Validation automatique des contraintes de longueur et unicité +- Support des relations avec les bâtiments +- Optimisation des performances pour les listes avec schémas allégés +- Configuration pour la conversion depuis SQLAlchemy +- Gestion des métadonnées temporelles (created_at, updated_at) + +Usage : +Les types de bâtiments permettent de classifier les bâtiments selon leur fonction +principale (résidentiel, commercial, industriel, etc.) pour améliorer la précision +des prédictions énergétiques du modèle ML. +""" + +from datetime import datetime +from typing import Optional + +from pydantic import BaseModel, ConfigDict, Field + + +# Schéma de base avec les champs communs +class BuildingTypeBase(BaseModel): + """ + Schéma de base pour les types de bâtiments avec champs communs. + + Cette classe définit les attributs fondamentaux partagés par tous + les schémas de types de bâtiments (création, mise à jour, réponse). + + Attributes: + model_id (int): Identifiant unique pour le modèle ML (obligatoire) + Utilisé pour l'encodage catégoriel dans les prédictions + building_type_name (str): Nom du type de bâtiment (unique, max 100 car.) + Ex: "Residential", "Commercial", "Industrial", "Mixed Use" + description (str, optional): Description détaillée du type + Permet d'ajouter des précisions sur l'usage ou les caractéristiques + + Contraintes: + - model_id : Doit être unique dans la table (contrainte métier) + - building_type_name : Unique, obligatoire, max 100 caractères + - description : Optionnel, texte libre + + Note: + Le model_id est crucial pour le modèle ML car il permet l'encodage + numérique des catégories de bâtiments pour les prédictions. + """ + + model_id: int = Field( + ..., description="Identifiant au modèle associé (clé unique, obligatoire)" + ) + building_type_name: str = Field( + ..., + max_length=100, + description="Nom du type de bâtiment (unique, obligatoire, max 100 caractères)", + ) + description: Optional[str] = Field( + None, description="Description détaillée du type de bâtiment (optionnel)" + ) + model_config = ConfigDict(protected_namespaces=()) + + +# Schéma pour la création (sans id, created_at, updated_at) +class BuildingTypeCreate(BuildingTypeBase): + """ + Schéma pour la création d'un nouveau type de bâtiment. + + Hérite de BuildingTypeBase et utilise tous ses champs sans modification. + Utilisé lors des requêtes POST pour ajouter de nouveaux types de bâtiments. + + Validation automatique : + - Vérification de l'unicité du model_id et building_type_name + - Contraintes de longueur respectées + - Génération automatique de l'ID et timestamps lors de l'insertion + + Usage: + - Endpoint POST /building-types/ + - Administration pour enrichir la taxonomie des types + - Extension du modèle avec de nouveaux types de bâtiments + + Example: + { + "model_id": 5, + "building_type_name": "Educational", + "description": "Bâtiments éducatifs: écoles, universités, bibliothèques" + } + """ + + pass + + +# Schéma pour la mise à jour (tous les champs optionnels sauf id) +class BuildingTypeUpdate(BaseModel): + """ + Schéma pour la mise à jour partielle d'un type de bâtiment. + + Tous les champs sont optionnels pour permettre les mises à jour + sélectives. Seuls les champs fournis seront modifiés. + + Attributes: + model_id (int, optional): Nouvel identifiant modèle ML + building_type_name (str, optional): Nouveau nom du type + description (str, optional): Nouvelle description + + Contraintes: + - model_id : Doit rester unique si modifié + - building_type_name : Doit rester unique si modifié + - Validation des longueurs appliquée si fournie + + Usage: + - Endpoint PATCH /building-types/{id} + - Correction de données existantes + - Mise à jour des descriptions ou renommage + + Warning: + Modifier le model_id peut impacter les prédictions ML existantes + car ce champ est utilisé pour l'encodage catégoriel. + """ + + model_id: Optional[int] = Field(None, description="Identifiant au modèle associé") + building_type_name: Optional[str] = Field( + None, max_length=100, description="Nom du type de bâtiment" + ) + description: Optional[str] = Field( + None, description="Description détaillée du type de bâtiment" + ) + + model_config = ConfigDict(protected_namespaces=()) + + +# Schéma pour la réponse (avec tous les champs) +class BuildingTypeResponse(BuildingTypeBase): + """ + Schéma de réponse complet pour un type de bâtiment. + + Hérite de BuildingTypeBase et ajoute les métadonnées générées + automatiquement par la base de données (ID, timestamps). + + Attributes: + id (int): Identifiant unique auto-incrémenté (clé primaire) + created_at (datetime, optional): Date de création de l'enregistrement + updated_at (datetime, optional): Date de dernière modification + + tous les champs de BuildingTypeBase + + Configuration: + - from_attributes=True : Conversion automatique depuis SQLAlchemy + - Support de la sérialisation JSON avec encodage datetime + + Usage: + - Réponses des endpoints GET /building-types/ + - Réponses POST après création réussie + - Réponses PATCH après mise à jour + + Note: + Ce schéma est optimisé pour la sérialisation complète avec + toutes les métadonnées pour les réponses détaillées. + """ + + id: int = Field( + ..., + description="Identifiant unique du type de bâtiment (clé primaire auto-incrémentée)", + ) + created_at: Optional[datetime] = Field( + None, description="Date et heure de création de l'enregistrement" + ) + updated_at: Optional[datetime] = Field( + None, description="Date et heure de maj de l'enregistrement" + ) + + model_config = ConfigDict(from_attributes=True) + + +# Schéma minimal pour les listes (performance optimisée) +class BuildingTypeList(BaseModel): + """ + Schéma minimal optimisé pour les listes de types de bâtiments. + + Version allégée contenant uniquement les champs essentiels pour + améliorer les performances lors du retour de listes importantes. + + Attributes: + id (int): Identifiant unique du type + building_type_name (str): Nom du type de bâtiment + model_id (int): Identifiant pour le modèle ML + + Optimisations: + - Exclusion des timestamps pour réduire la taille des réponses + - Exclusion de la description pour accélérer la sérialisation + - Champs essentiels seulement pour les listes de sélection + + Usage: + - Listes déroulantes dans les interfaces utilisateur + - Endpoints de liste avec pagination importante + - Références rapides pour l'intégration avec d'autres services + + Performance: + Jusqu'à 50% de réduction de taille par rapport au schéma complet. + """ + + id: int + building_type_name: str + model_id: int + + model_config = ConfigDict(from_attributes=True, protected_namespaces=()) + + +# Schéma pour les relations (référence simple) +class BuildingTypeRef(BaseModel): + """ + Schéma de référence simple pour les relations avec d'autres entités. + + Version ultra-légère utilisée lors de l'inclusion des types de bâtiments + dans les réponses d'autres entités (ex: bâtiments avec leur type). + + Attributes: + id (int): Identifiant unique du type + building_type_name (str): Nom du type pour affichage + + Usage: + - Relations dans BuildingModelsWithRelations + - Références croisées dans les API de bâtiments + - Affichage des types dans les interfaces sans surcharge + + Example dans une relation: + { + "id": 123, + "address": "1234 Main St", + "building_type": { + "id": 1, + "building_type_name": "Commercial" + } + } + + Note: + Exclut le model_id et autres métadonnées pour éviter la redondance + dans les structures imbriquées. + """ + + id: int + building_type_name: str + + model_config = ConfigDict(from_attributes=True) diff --git a/src/project5/schemas/categorie.py b/src/project5/schemas/categorie.py new file mode 100644 index 0000000000000000000000000000000000000000..4ae2c54c8d3d97787598dd113d1a1a7868b7f04b --- /dev/null +++ b/src/project5/schemas/categorie.py @@ -0,0 +1,202 @@ +""" +Schémas Pydantic pour les catégories d'usage des propriétés (Categories). + +Ce module définit tous les schémas de validation et sérialisation pour les données +des catégories d'usage de propriétés dans le système de classification énergétique. + +Classes disponibles : +- CategoryBase : Schéma de base avec les champs communs +- CategoryResponse : Schéma de réponse complet avec métadonnées +- CategoryList : Schéma optimisé pour les listes (performance) +- CategoryRef : Schéma de référence simple pour les relations + +Architecture des catégories : +Les catégories regroupent les propriétés selon leur fonction principale : +- "Office" : Bureaux, espaces administratifs +- "Retail" : Commerces, magasins +- "Residential" : Logements, appartements +- "Education" : Écoles, universités +- "Healthcare" : Hôpitaux, cliniques +- "Entertainment" : Théâtres, cinémas, restaurants + +Fonctionnalités : +- Validation des codes catégories (identifiants courts) +- Contraintes de longueur et d'unicité +- Support des relations hiérarchiques avec les propriétés +- Optimisation pour les réponses de listes importantes +- Configuration pour la conversion depuis SQLAlchemy + +Usage : +Les catégories permettent une classification hiérarchique des usages de propriétés, +facilitant l'analyse énergétique par secteur d'activité et améliorant la précision +des prédictions ML en groupant les usages similaires. +""" + +from datetime import datetime +from typing import Optional + +from pydantic import BaseModel, ConfigDict, Field + + +# Schéma de base avec les champs communs +class CategoryBase(BaseModel): + """ + Schéma de base pour les catégories d'usage avec champs communs. + + Cette classe définit les attributs fondamentaux pour toutes les + catégories d'usage de propriétés dans le système de classification. + + Attributes: + category_code (str): Code court unique de la catégorie (max 50 car.) + Ex: "OFF", "RET", "RES", "EDU", "HLT", "ENT" + Utilisé pour les références et la compression des données + category_name (str): Nom complet de la catégorie (max 100 car.) + Ex: "Office", "Retail", "Residential", "Education" + Nom d'affichage pour les interfaces utilisateur + description (str, optional): Description détaillée de la catégorie + Explique le type d'usages regroupés sous cette catégorie + + Contraintes: + - category_code : Unique, obligatoire, max 50 caractères + - category_name : Unique, obligatoire, max 100 caractères + - description : Optionnel, texte libre + + Note: + Les catégories forment une classification hiérarchique permettant + de regrouper les propriétés par secteur d'activité pour l'analyse. + """ + + category_code: str = Field( + ..., + max_length=50, + description="Code unique de la catégorie (identifiant court, max 50 caractères)", + ) + category_name: str = Field( + ..., + max_length=100, + description="Nom complet de la catégorie (obligatoire, max 100 caractères)", + ) + description: Optional[str] = Field( + None, description="Description détaillée de la catégorie (optionnel)" + ) + + +# Schéma pour la réponse (avec tous les champs) +class CategoryResponse(CategoryBase): + """ + Schéma de réponse complet pour une catégorie d'usage. + + Hérite de CategoryBase et ajoute les métadonnées générées + automatiquement par la base de données (ID, timestamps). + + Attributes: + id (int): Identifiant unique auto-incrémenté (clé primaire) + created_at (datetime, optional): Date de création de l'enregistrement + updated_at (datetime, optional): Date de dernière modification + + tous les champs de CategoryBase + + Configuration: + - from_attributes=True : Conversion automatique depuis SQLAlchemy + - Support de la sérialisation JSON avec encodage datetime + + Usage: + - Réponses des endpoints GET /categories/ + - Réponses POST après création réussie + - Réponses PATCH après mise à jour + - Affichage détaillé avec toutes les métadonnées + + Relations: + Chaque catégorie peut être liée à plusieurs propriétés via + la clé étrangère category_id dans la table properties. + """ + + id: int = Field( + ..., + description="Identifiant unique de la catégorie (clé primaire auto-incrémentée)", + ) + created_at: Optional[datetime] = Field( + None, description="Date et heure de création de l'enregistrement" + ) + updated_at: Optional[datetime] = Field( + None, description="Date et heure de maj de l'enregistrement" + ) + + model_config = ConfigDict(from_attributes=True) + + +# Schéma minimal pour les listes (performance optimisée) +class CategoryList(BaseModel): + """ + Schéma minimal optimisé pour les listes de catégories. + + Version allégée contenant uniquement les champs essentiels pour + améliorer les performances lors du retour de listes importantes. + + Attributes: + id (int): Identifiant unique de la catégorie + category_code (str): Code court de la catégorie + category_name (str): Nom complet de la catégorie + + Optimisations: + - Exclusion des timestamps pour réduire la taille des réponses + - Exclusion de la description pour accélérer la sérialisation + - Champs essentiels pour les listes de sélection + + Usage: + - Listes déroulantes pour sélection de catégories + - Endpoints de liste avec pagination importante + - Tables de référence pour les interfaces + - Filtres de recherche par catégorie + + Performance: + Réduction significative de la taille des réponses pour + les listes contenant de nombreuses catégories. + """ + + id: int + category_code: str + category_name: str + + model_config = ConfigDict(from_attributes=True) + + +# Schéma pour les relations (référence simple) +class CategoryRef(BaseModel): + """ + Schéma de référence simple pour les relations avec d'autres entités. + + Version ultra-légère utilisée lors de l'inclusion des catégories + dans les réponses d'autres entités (ex: propriétés avec leur catégorie). + + Attributes: + id (int): Identifiant unique de la catégorie + category_code (str): Code court pour référence rapide + category_name (str): Nom complet pour affichage + + Usage: + - Relations dans PropertyResponse avec catégorie associée + - Références croisées dans les API de propriétés + - Affichage des catégories dans les interfaces sans surcharge + - Filtres et groupements par catégorie + + Example dans une relation: + { + "id": 456, + "property_name": "Office Building", + "category": { + "id": 1, + "category_code": "OFF", + "category_name": "Office" + } + } + + Note: + Optimisé pour éviter la redondance dans les structures imbriquées + tout en conservant les informations essentielles d'identification. + """ + + id: int + category_code: str + category_name: str + + model_config = ConfigDict(from_attributes=True) diff --git a/src/project5/schemas/neighborhood.py b/src/project5/schemas/neighborhood.py new file mode 100644 index 0000000000000000000000000000000000000000..bce0db681630dfc2527bc6d5bb7befd3ce22f348 --- /dev/null +++ b/src/project5/schemas/neighborhood.py @@ -0,0 +1,311 @@ +""" +Schémas Pydantic complets pour l'API des quartiers de Seattle (Neighborhoods). + +Ce module définit tous les schémas de validation et sérialisation pour les données +des quartiers de Seattle utilisés dans le système de prédiction énergétique. + +Classes principales : +- Neighborhood : Schéma principal pour retourner un quartier complet +- NeighborhoodSummary : Schéma résumé sans timestamps pour optimiser +- NeighborhoodSearchResult : Schéma pour les résultats de recherche par nom +- NeighborhoodStats : Schéma pour les statistiques agrégées des quartiers +- NeighborhoodCount : Schéma pour le comptage simple des quartiers +- NeighborhoodExists : Schéma pour vérifier l'existence d'un quartier +- PaginatedNeighborhoods : Schéma pour les réponses paginées avancées + +Classes utilitaires : +- HealthCheck : Schéma de vérification de l'état de l'API +- ErrorResponse : Schéma standardisé pour les réponses d'erreur +- ValidationErrorResponse : Schéma détaillé pour les erreurs de validation (422) +- MessageResponse : Réponse simple avec message de succès +- ServiceInfo : Informations complètes sur le service API + +Fonctionnalités avancées : +- Recherche textuelle avec limitation et comptage des résultats +- Statistiques complètes avec distribution alphabétique +- Pagination avancée avec métadonnées (has_next, has_prev) +- Monitoring de santé avec statut de base de données +- Gestion d'erreurs standardisée avec timestamps et codes +- Support des filtres actif/inactif pour les quartiers + +Usage géographique : +Les quartiers de Seattle servent de référence géographique pour localiser +les bâtiments et analyser les patterns de consommation énergétique par zone. +Le model_id permet l'encodage numérique pour les prédictions ML. + +Configuration JSON : +- Encodage automatique datetime vers ISO format +- Exemples détaillés pour la documentation OpenAPI +- Support des espaces de noms protégés pour éviter les conflits +""" + +from datetime import datetime +from typing import Any, Dict, List, Optional + +from pydantic import BaseModel, Field + + +class Neighborhood(BaseModel): + """Schéma principal pour retourner un quartier.""" + + id: int = Field(..., description="Identifiant unique du quartier") + neighborhood_name: str = Field(..., description="Nom du quartier") + model_id: int = Field(..., description="Identifiant unique du quartier") + created_at: Optional[datetime] = Field(None, description="Date de création") + updated_at: Optional[datetime] = Field(None, description="Date de maj") + + class Config: + """Configuration Pydantic.""" + + protected_namespaces = () + from_attributes = True + json_encoders = {datetime: lambda v: v.isoformat() if v else None} + json_schema_extra = { + "example": { + "id": 1, + "neighborhood_name": "BALLARD", + "model_id": 0, + "created_at": "2024-01-15T10:30:00Z", + "updated_at": None, + } + } + + +class NeighborhoodSummary(BaseModel): + """Schéma résumé d'un quartier (sans timestamps).""" + + id: int = Field(..., description="Identifiant unique") + neighborhood_name: str = Field(..., description="Nom du quartier") + is_active: bool = Field(..., description="Statut actif/inactif") + + class Config: + from_attributes = True + + +class NeighborhoodSearchResult(BaseModel): + """Schéma pour les résultats de recherche.""" + + query: str = Field(..., description="Terme de recherche utilisé") + results: List[Neighborhood] = Field(..., description="Quartiers trouvés") + count: int = Field(..., description="Nombre de résultats") + limited: bool = Field(..., description="Résultats limités par le paramètre limit") + + class Config: + json_schema_extra = { + "example": { + "query": "BALL", + "results": [ + { + "id": 1, + "neighborhood_name": "BALLARD", + "created_at": "2024-01-15T10:30:00Z", + } + ], + "count": 1, + "limited": False, + } + } + + +class NeighborhoodStats(BaseModel): + """Schéma pour les statistiques des quartiers.""" + + total_neighborhoods: int = Field(..., description="Nombre total de quartiers") + active_neighborhoods: int = Field(..., description="Nombre de quartiers actifs") + inactive_neighborhoods: int = Field(..., description="Nombre de quartiers inactifs") + active_percentage: float = Field(..., description="Pourcentage de quartiers actifs") + recent_neighborhoods: List[Dict[str, Any]] = Field( + default_factory=list, description="Quartiers récemment créés" + ) + alphabet_distribution: Dict[str, int] = Field( + default_factory=dict, description="Répartition alphabétique des quartiers" + ) + database: str = Field(default="PostgreSQL", description="Type de base de données") + status: str = Field(default="read_only", description="Statut du service") + + class Config: + json_schema_extra = { + "example": { + "total_neighborhoods": 13, + "active_neighborhoods": 13, + "inactive_neighborhoods": 0, + "active_percentage": 100.0, + "recent_neighborhoods": [ + { + "id": 13, + "name": "SOUTHWEST", + "created_at": "2024-01-15T10:30:00Z", + } + ], + "alphabet_distribution": { + "B": 1, + "C": 1, + "D": 2, + "E": 1, + "G": 1, + "L": 1, + "M": 1, + "N": 3, + "S": 2, + }, + "database": "PostgreSQL", + "status": "read_only", + } + } + + +class NeighborhoodCount(BaseModel): + """Schéma pour le comptage des quartiers.""" + + count: int = Field(..., description="Nombre de quartiers") + active_only: bool = Field(..., description="Comptage limité aux actifs") + + class Config: + json_schema_extra = {"example": {"count": 13, "active_only": True}} + + +class NeighborhoodExists(BaseModel): + """Schéma pour vérifier l'existence d'un quartier.""" + + id: int = Field(..., description="ID du quartier vérifié") + exists: bool = Field(..., description="Le quartier existe-t-il") + + class Config: + json_schema_extra = {"example": {"id": 1, "exists": True}} + + +class PaginatedNeighborhoods(BaseModel): + """Schéma pour les réponses paginées avancées.""" + + items: List[Neighborhood] = Field(..., description="Quartiers de la page courante") + total: int = Field(..., description="Nombre total de quartiers") + page: int = Field(..., description="Numéro de page courante") + size: int = Field(..., description="Taille de la page") + pages: int = Field(..., description="Nombre total de pages") + has_next: bool = Field(..., description="Y a-t-il une page suivante") + has_prev: bool = Field(..., description="Y a-t-il une page précédente") + + class Config: + json_schema_extra = { + "example": { + "items": [ + { + "id": 1, + "neighborhood_name": "BALLARD", + "is_active": True, + "created_at": "2024-01-15T10:30:00Z", + } + ], + "total": 13, + "page": 1, + "size": 5, + "pages": 3, + "has_next": True, + "has_prev": False, + } + } + + +class HealthCheck(BaseModel): + """Schéma pour le health check.""" + + service: str = Field(default="neighborhoods", description="Nom du service") + status: str = Field(..., description="Statut de santé") + total_neighborhoods: Optional[int] = Field(None, description="Nombre de quartiers") + database_connected: bool = Field(..., description="Connexion DB active") + timestamp: datetime = Field( + default_factory=datetime.now, description="Timestamp du check" + ) + + class Config: + json_encoders = {datetime: lambda v: v.isoformat()} + json_schema_extra = { + "example": { + "service": "neighborhoods", + "status": "healthy", + "total_neighborhoods": 13, + "database_connected": True, + "timestamp": "2024-01-15T10:30:00Z", + } + } + + +class ErrorResponse(BaseModel): + """Schéma pour les réponses d'erreur.""" + + detail: str = Field(..., description="Message d'erreur") + error_code: Optional[str] = Field(None, description="Code d'erreur") + timestamp: datetime = Field( + default_factory=datetime.now, description="Timestamp de l'erreur" + ) + path: Optional[str] = Field(None, description="Chemin de l'endpoint") + + class Config: + json_encoders = {datetime: lambda v: v.isoformat()} + + +class ValidationErrorDetail(BaseModel): + """Détail d'une erreur de validation.""" + + field: str = Field(..., description="Champ en erreur") + message: str = Field(..., description="Message d'erreur") + invalid_value: Optional[Any] = Field(None, description="Valeur invalide") + + +class ValidationErrorResponse(BaseModel): + """Réponse pour les erreurs de validation (422).""" + + detail: str = Field(..., description="Message d'erreur général") + errors: List[ValidationErrorDetail] = Field(..., description="Détails des erreurs") + timestamp: datetime = Field(default_factory=datetime.now) + + class Config: + json_encoders = {datetime: lambda v: v.isoformat()} + + +# Schémas pour les réponses HTTP communes +class MessageResponse(BaseModel): + """Réponse simple avec message.""" + + message: str = Field(..., description="Message de réponse") + success: bool = Field(True, description="Succès de l'opération") + + +class ListResponse(BaseModel): + """Réponse générique pour les listes.""" + + items: List[Any] = Field(..., description="Éléments de la liste") + count: int = Field(..., description="Nombre d'éléments") + + +# Schémas pour les métadonnées +class ServiceInfo(BaseModel): + """Informations sur le service.""" + + name: str = Field(default="neighborhoods-api", description="Nom du service") + version: str = Field(default="1.0.0", description="Version du service") + description: str = Field( + default="API de lecture des quartiers de Seattle", + description="Description du service", + ) + mode: str = Field(default="read_only", description="Mode d'opération") + endpoints: Dict[str, str] = Field( + default_factory=dict, description="Endpoints disponibles" + ) + + class Config: + json_schema_extra = { + "example": { + "name": "neighborhoods-api", + "version": "1.0.0", + "description": "API de lecture des quartiers de Seattle", + "mode": "read_only", + "endpoints": { + "list": "/neighborhoods/", + "get": "/neighborhoods/{id}", + "search": "/neighborhoods/search/", + "stats": "/neighborhoods/stats/", + "health": "/neighborhoods/health/", + }, + } + } diff --git a/src/project5/schemas/property.py b/src/project5/schemas/property.py new file mode 100644 index 0000000000000000000000000000000000000000..ddc73b59394c98a4ac9663169e6c7b0cff9f993b --- /dev/null +++ b/src/project5/schemas/property.py @@ -0,0 +1,192 @@ +""" +Schémas Pydantic pour les propriétés d'usage des bâtiments (Properties). + +Ce module définit tous les schémas de validation et sérialisation pour les données +des propriétés d'usage spécifiques dans le système de classification énergétique. + +Classes disponibles : +- PropertyBase : Schéma de base avec les champs communs +- PropertyResponse : Schéma de réponse complet avec métadonnées +- PropertyList : Schéma optimisé pour les listes (performance) + +Architecture des propriétés : +Les propriétés représentent les usages spécifiques des bâtiments et appartiennent +à des catégories plus larges. Exemples par catégorie : + +Office (Bureaux) : +- "Office" : Bureaux standards +- "Bank Branch" : Agences bancaires +- "Courthouse" : Tribunaux + +Retail (Commerce) : +- "Retail Store" : Magasins de détail +- "Restaurant" : Restaurants +- "Supermarket/Grocery Store" : Supermarchés + +Residential (Résidentiel) : +- "Multifamily Housing" : Logements collectifs +- "Senior Care Community" : Résidences seniors + +Education (Éducation) : +- "K-12 School" : Écoles primaires/secondaires +- "College/University" : Universités + +Fonctionnalités : +- Validation des contraintes de longueur et relations +- Gestion des clés étrangères vers les catégories +- Support du model_id pour l'encodage ML +- Optimisation pour les listes avec schéma allégé +- Configuration pour la conversion depuis SQLAlchemy + +Usage dans le ML : +Le model_id permet l'encodage numérique des propriétés pour les prédictions +énergétiques, tandis que la relation avec les catégories permet une classification +hiérarchique pour l'analyse par secteur d'activité. +""" + +from datetime import datetime +from typing import Optional + +from pydantic import BaseModel, ConfigDict, Field + + +# Schéma de base avec les champs communs +class PropertyBase(BaseModel): + """ + Schéma de base pour les propriétés d'usage avec champs communs. + + Cette classe définit les attributs fondamentaux pour toutes les + propriétés d'usage spécifiques dans le système de classification. + + Attributes: + model_id (int): Identifiant unique pour le modèle ML (obligatoire) + Utilisé pour l'encodage numérique des propriétés dans les prédictions + Doit être unique dans la table (contrainte d'unicité) + property_name (str): Nom de la propriété (unique, max 150 car.) + Ex: "Office", "Restaurant", "Multifamily Housing", "K-12 School" + Nom d'affichage pour les interfaces et rapports + category_id (int): Identifiant de la catégorie parente (clé étrangère) + Lie la propriété à sa catégorie d'usage (Office, Retail, etc.) + Doit exister dans la table categories (contrainte d'intégrité) + + Contraintes: + - model_id : Unique, obligatoire, entier positif + - property_name : Unique, obligatoire, max 150 caractères + - category_id : Obligatoire, doit exister en table categories + + Relations: + - Appartient à une catégorie (relation many-to-one) + - Peut être utilisée par plusieurs bâtiments (relation one-to-many) + + Note: + Le model_id est crucial pour le modèle ML car il permet l'encodage + catégoriel des types d'usage pour les prédictions énergétiques. + """ + + model_id: int = Field( + ..., description="Identifiant au modèle associé (clé unique, obligatoire)" + ) + property_name: str = Field( + ..., + max_length=150, + description="Nom de la propriété (unique, obligatoire, max 150 caractères)", + ) + category_id: int = Field( + ..., + gt=0, + description="Identifiant de la catégorie associée (clé étrangère vers categories.id)", + ) + + model_config = ConfigDict(protected_namespaces=()) + + +# Schéma pour la réponse (avec tous les champs) +class PropertyResponse(PropertyBase): + """ + Schéma de réponse complet pour une propriété d'usage. + + Hérite de PropertyBase et ajoute les métadonnées générées + automatiquement par la base de données (ID, timestamps). + + Attributes: + id (int): Identifiant unique auto-incrémenté (clé primaire) + created_at (datetime, optional): Date de création de l'enregistrement + updated_at (datetime, optional): Date de dernière modification + + tous les champs de PropertyBase + + Configuration: + - from_attributes=True : Conversion automatique depuis SQLAlchemy + - protected_namespaces=() : Permet l'usage de mots-clés réservés + - Support de la sérialisation JSON avec encodage datetime + + Usage: + - Réponses des endpoints GET /properties/ + - Réponses POST après création réussie + - Réponses PATCH après mise à jour + - Affichage détaillé avec toutes les métadonnées + + Relations potentielles: + Peut inclure la catégorie associée et la liste des bâtiments + utilisant cette propriété selon les besoins de l'endpoint. + + Note: + Ce schéma est optimisé pour la sérialisation complète avec + toutes les métadonnées pour les réponses détaillées. + """ + + id: int = Field( + ..., + description="Identifiant unique de la propriété (clé primaire auto-incrémentée)", + ) + created_at: Optional[datetime] = Field( + None, description="Date et heure de création de l'enregistrement" + ) + updated_at: Optional[datetime] = Field( + None, description="Date et heure de maj de l'enregistrement" + ) + + model_config = ConfigDict(from_attributes=True, protected_namespaces=()) + + +# Schéma minimal pour les listes (performance optimisée) +class PropertyList(BaseModel): + """ + Schéma minimal optimisé pour les listes de propriétés. + + Version allégée contenant uniquement les champs essentiels pour + améliorer les performances lors du retour de listes importantes. + + Attributes: + id (int): Identifiant unique de la propriété + model_id (int): Identifiant pour le modèle ML + property_name (str): Nom de la propriété + category_id (int): Identifiant de la catégorie parente + + Optimisations: + - Exclusion des timestamps pour réduire la taille des réponses + - Champs essentiels pour les listes de sélection et filtres + - Amélioration des performances de sérialisation + + Usage: + - Listes déroulantes pour sélection de propriétés + - Endpoints de liste avec pagination importante + - Filtres de recherche par propriété ou catégorie + - Tables de référence pour les interfaces + - Export de données pour analyse + + Performance: + Réduction significative de la taille des réponses pour + les listes contenant de nombreuses propriétés (potentiellement + plusieurs centaines d'usages différents). + + Note: + Conserve les informations essentielles pour l'identification + et les relations sans surcharger les réponses avec des métadonnées. + """ + + id: int + model_id: int + property_name: str + category_id: int + + model_config = ConfigDict(from_attributes=True, protected_namespaces=()) diff --git a/src/project5/services/building_ensergy_prediction_service.py b/src/project5/services/building_ensergy_prediction_service.py new file mode 100644 index 0000000000000000000000000000000000000000..2408b6406cdd76dc5886feba4d03e1ff049d7a17 --- /dev/null +++ b/src/project5/services/building_ensergy_prediction_service.py @@ -0,0 +1,217 @@ +""" +Service métier pour la gestion des prédictions énergétiques des bâtiments. + +Ce module implémente la couche de logique métier pour les opérations CRUD +sur les prédictions énergétiques dans le système de prédiction de consommation +énergétique des bâtiments de Seattle. + +Classes: + BuildingEnergyPredictionService: Service principal pour les prédictions énergétiques + +Fonctionnalités: + - Récupération des prédictions avec pagination et tri + - Recherche par ID avec gestion des erreurs + - Validation des paramètres métier + - Logging des opérations pour traçabilité + - Gestion centralisée des exceptions + +Architecture: + Ce service suit le pattern Repository et sert d'interface entre la couche + de présentation (routes) et la couche de données (modèles SQLAlchemy). + Il encapsule la logique métier et les validations spécifiques aux prédictions. + +Note: + Ce service est en lecture seule pour préserver l'intégrité des prédictions + générées par le modèle ML. Les prédictions sont créées automatiquement + par le système de machine learning. +""" + +import logging +from typing import List + +from sqlalchemy import asc, desc +from sqlalchemy.orm import Session + +from project5.models import BuildingEnergyPredictions as BuildingEnergyPredictionModel +from project5.utils.exceptions import NotFoundError, ServiceError + +logger = logging.getLogger(__name__) + + +class BuildingEnergyPredictionService: + """ + Service métier pour la gestion des prédictions énergétiques des bâtiments. + + Cette classe implémente la logique métier pour l'accès aux données de prédictions + énergétiques. Elle fournit une interface standardisée pour les opérations de + lecture avec gestion des erreurs, validation et logging. + + Responsabilités: + - Récupération paginée des prédictions avec tri configurable + - Recherche de prédictions individuelles par ID + - Validation des paramètres d'entrée + - Gestion centralisée des exceptions métier + - Logging détaillé pour traçabilité et debugging + + Architecture: + Suit le pattern Service Layer pour séparer la logique métier + des détails d'implémentation de la base de données. Utilise + les modèles SQLAlchemy pour l'accès aux données. + + Thread Safety: + Les méthodes statiques sont thread-safe car elles ne maintiennent + pas d'état interne. Chaque opération utilise sa propre session DB. + + Note: + Service en lecture seule (read-only) car les prédictions sont générées + automatiquement par le modèle ML et ne doivent pas être modifiées manuellement. + """ + + @staticmethod + def get_all( + db: Session, + skip: int = 0, + limit: int = 100, + sort_by: str = "name", + sort_order: str = "asc", + ) -> List[BuildingEnergyPredictionModel]: + """ + Récupère toutes les prédictions énergétiques avec options de pagination et tri. + + Cette méthode implémente la récupération paginée des prédictions énergétiques + stockées en base de données. Elle supporte le tri configurable et la pagination + pour optimiser les performances sur de gros volumes de données. + + Args: + db (Session): Session SQLAlchemy pour l'accès à la base de données + Doit être une session active et valide + skip (int, optional): Nombre d'éléments à ignorer pour la pagination. + Defaults to 0. Doit être >= 0 + limit (int, optional): Nombre maximum d'éléments à retourner. + Defaults to 100. Limité à 1000 pour éviter les surcharges + sort_by (str, optional): Champ de tri. Defaults to "name". + Actuellement seul 'id' est supporté pour la cohérence + sort_order (str, optional): Ordre de tri ('asc', 'desc'). + Defaults to "asc" + + Returns: + List[BuildingEnergyPredictionModel]: Liste des prédictions énergétiques + correspondant aux critères de pagination et tri. Liste vide si aucun résultat. + + Raises: + ServiceError: En cas d'erreur lors de l'accès à la base de données + ou de problème technique durant la récupération + + Example: + >>> service = BuildingEnergyPredictionService() + >>> predictions = service.get_all(db, skip=0, limit=50, sort_order="desc") + >>> print(f"Trouvé {len(predictions)} prédictions") + + Performance: + Utilise LIMIT/OFFSET SQL pour la pagination efficace. + Le tri par ID est optimisé grâce à l'index de clé primaire. + + Note: + Le paramètre sort_by est conservé pour compatibilité future + mais seul le tri par 'id' est actuellement implémenté. + """ + try: + logger.info( + f"Récupération des predictions énergétiques des bâtiments : skip={skip}, limit={limit}, " + f"sort={sort_by}:{sort_order}" + ) + + query = db.query(BuildingEnergyPredictionModel) + + # Tri + sort_field = "id" + if sort_order.lower() == "desc": + query = query.order_by(desc(sort_field)) + else: + query = query.order_by(asc(sort_field)) + + # Pagination + building_pred = query.offset(skip).limit(limit).all() + + logger.info(f"Récupérés {len(building_pred)} prédictions énergétiques") + return building_pred + + except Exception as e: + logger.error( + f"Erreur lors de la récupération des prédictions énergétiques: {e}", + exc_info=True, + ) + raise ServiceError( + f"Impossible de récupérer les prédictions énergétiques: {str(e)}" + ) + + @staticmethod + def get_by_id(db: Session, prediction_id: int) -> BuildingEnergyPredictionModel: + """ + Récupère une prédiction énergétique spécifique par son identifiant unique. + + Cette méthode effectue une recherche directe par clé primaire pour récupérer + une prédiction énergétique. Elle inclut une gestion robuste des erreurs + et un logging détaillé pour le suivi des opérations. + + Args: + db (Session): Session SQLAlchemy active pour l'accès à la base de données + La session doit être valide et connectée + prediction_id (int): Identifiant unique de la prédiction à récupérer + Doit être un entier positif correspondant à une clé primaire valide + + Returns: + BuildingEnergyPredictionModel: Instance du modèle contenant toutes les + données de la prédiction énergétique (ID, building_id, consommation, + date de prédiction, etc.) + + Raises: + NotFoundError: Si aucune prédiction n'existe avec l'ID spécifié + Inclut l'ID recherché dans le message d'erreur pour le debugging + ServiceError: En cas d'erreur technique lors de l'accès à la base + (problème de connexion, corruption de données, etc.) + + Example: + >>> service = BuildingEnergyPredictionService() + >>> try: + ... prediction = service.get_by_id(db, 12345) + ... print(f"Consommation prédite: {prediction.site_energy_use_wn_kbtu} kBTU") + ... except NotFoundError: + ... print("Prédiction non trouvée") + + Performance: + Utilise un filtre sur la clé primaire, optimisé par l'index automatique. + Temps de réponse constant O(1) grâce à l'index de clé primaire. + + Logging: + - INFO: Début et fin de recherche avec ID + - WARNING: Prédiction non trouvée + - ERROR: Erreurs techniques avec détails + + Note: + Cette méthode est optimisée pour la récupération d'une seule prédiction. + Pour plusieurs prédictions, utiliser get_all() avec des filtres appropriés. + """ + try: + logger.info(f"Récupération de la prédiction ID: {prediction_id}") + + prediction = ( + db.query(BuildingEnergyPredictionModel) + .filter(BuildingEnergyPredictionModel.id == prediction_id) + .first() + ) + + if prediction is None: + logger.warning(f"Prediction {prediction_id} non trouvée") + raise NotFoundError(f"Prediction avec l'ID {prediction_id} non trouvée") + + logger.info(f"Prediction trouvée: {prediction_id}") + return prediction + except NotFoundError: + raise + except Exception as e: + logger.error( + f"Erreur lors de la récupération de la prédiction {prediction_id}: {e}", + exc_info=True, + ) + raise ServiceError(f"Impossible de récupérer la prédiction: {str(e)}") diff --git a/src/project5/services/building_model_service.py b/src/project5/services/building_model_service.py new file mode 100644 index 0000000000000000000000000000000000000000..bdb7a5b80c65c40c67397e0e03626d4ee68ccefd --- /dev/null +++ b/src/project5/services/building_model_service.py @@ -0,0 +1,628 @@ +""" +Service métier pour la gestion des modèles de bâtiments et prédictions ML. + +Ce module implémente la couche de logique métier pour les opérations sur les modèles +de bâtiments et les prédictions énergétiques utilisant le machine learning. + +Classes: + BuildingModelsService: Service pour les opérations CRUD sur les bâtiments + PredictionService: Service spécialisé pour les prédictions ML avec RandomForest + +Fonctionnalités principales: + - Gestion complète des données de bâtiments avec relations + - Prédictions énergétiques en temps réel via RandomForestRegressor + - Récupération optimisée des données pour le ML + - Preprocessing automatique des caractéristiques + - Calcul d'intervalles de confiance pour les prédictions + - Analyse d'importance des features + +Architecture ML: + Le service utilise un modèle RandomForestRegressor pré-entraîné pour + prédire la consommation énergétique en log(kBTU) puis convertit en kBTU. + Les features incluent les caractéristiques physiques, géographiques + et d'usage des bâtiments. + +Performance: + - Requêtes optimisées avec JOINs pour récupérer les model_ids + - Cache du modèle ML en mémoire pour éviter les rechargements + - Preprocessing vectorisé avec pandas/numpy pour performance + +Note: + Ce module est central dans l'application car il combine l'accès aux données + métier avec les capacités de machine learning pour les prédictions énergétiques. +""" + +import logging +from typing import Any, Dict, List, Optional + +import numpy as np +import pandas as pd +from sklearn.ensemble import RandomForestRegressor +from sqlalchemy import asc, desc +from sqlalchemy.orm import Session, aliased + +from project5.models import BuildingModels as BuildingModelsModel +from project5.models import BuildingType as BuildingTypeModel +from project5.models import Neighborhood as NeighborhoodModel +from project5.models import Property as PropertyModel +from project5.models.building_energy_prediction import BuildingEnergyPredictions +from project5.schemas.building_model import BuildingFeatures +from project5.utils.exceptions import NotFoundError, ServiceError, ValidationError + +logger = logging.getLogger(__name__) + + +class BuildingModelsService: + """ + Service métier pour la gestion des modèles de bâtiments (lecture seule). + + Cette classe implémente la logique métier pour l'accès aux données des bâtiments + dans le système de prédiction énergétique. Elle fournit des méthodes optimisées + pour la récupération des caractéristiques des bâtiments nécessaires au ML. + + Responsabilités: + - Récupération paginée des bâtiments avec tri + - Recherche de bâtiments individuels par ID + - Récupération optimisée des données ML avec model_ids + - Jointures complexes pour récupérer les relations (quartier, type, propriétés) + - Gestion des erreurs et logging détaillé + + Méthodes principales: + - get_all(): Récupération paginée standard + - get_by_id(): Recherche par clé primaire + - get_model_data_by_id(): Récupération optimisée pour ML + + Optimisations ML: + La méthode get_model_data_by_id() utilise des JOINs optimisés pour + récupérer directement les model_ids nécessaires au modèle ML, + évitant des requêtes multiples. + + Thread Safety: + Méthodes statiques thread-safe. Chaque opération utilise sa propre session DB. + + Note: + Service en lecture seule pour préserver l'intégrité des données de référence. + Les bâtiments sont des données maîtres importées depuis des sources externes. + """ + + @staticmethod + def get_all( + db: Session, + skip: int = 0, + limit: int = 100, + sort_by: str = "name", + sort_order: str = "asc", + ) -> List[BuildingModelsModel]: + """Récupère toutes les bâtiments avec options de filtrage et tri. + + Args: + db: Session de base de données + skip: Nombre d'éléments à ignorer (pagination) + limit: Nombre maximum d'éléments à retourner + sort_by: Champ de tri ('name', 'created_at', 'id') + sort_order: Ordre de tri ('asc', 'desc') + + Returns: + Liste des bâtiments correspondantes aux critères + + Raises: + ServiceError: En cas d'erreur lors de la récupération + """ + try: + logger.info( + f"Récupération des bâtiments : skip={skip}, limit={limit}, " + f"sort={sort_by}:{sort_order}" + ) + + query = db.query(BuildingModelsModel) + + # Tri + sort_field = "id" + if sort_order.lower() == "desc": + query = query.order_by(desc(sort_field)) + else: + query = query.order_by(asc(sort_field)) + + # Pagination + properties = query.offset(skip).limit(limit).all() + + logger.info(f"Récupérés {len(properties)} bâtiments") + return properties + + except Exception as e: + logger.error(f"Erreur lors de la récupération des bâtiments: {e}") + raise ServiceError(f"Impossible de récupérer les bâtiments: {str(e)}") + + @staticmethod + def get_by_id(db: Session, building_id: int) -> BuildingModelsModel: + """Récupère un bâtiment par son ID. + + Args: + db: Session de base de données + prediction_id: ID type du bâtiment à récupérer + + Returns: + Le bâtiment correspondante + + Raises: + NotFoundError: N'existe pas + ServiceError: En cas d'erreur technique + """ + try: + logger.info(f"Récupération du bâtiment ID: {building_id}") + + building = ( + db.query(BuildingModelsModel) + .filter(BuildingModelsModel.id == building_id) + .first() + ) + + if building is None: + logger.warning(f"bâtiment {building_id} non trouvé") + raise NotFoundError(f"bâtiment avec l'ID {building_id} non trouvé") + + logger.info(f"bâtiment trouvé: {building_id}") + return building + except NotFoundError: + raise + except Exception as e: + logger.error( + f"Erreur lors de la récupération de la prediction {building_id}: {e}" + ) + raise ServiceError(f"Impossible de récupérer la prediction: {str(e)}") + + @staticmethod + def get_model_data_by_id(db: Session, building_id: int) -> BuildingModelsModel: + """Récupère les données modèle nécessaire à un bâtiment par son ID. + + Args: + db: Session de base de données + prediction_id: ID type du bâtiment à récupérer + + Returns: + Le bâtiment correspondante + + Raises: + NotFoundError: N'existe pas + ServiceError: En cas d'erreur technique + """ + try: + logger.info(f"Récupération du bâtiment ID: {building_id}") + # Créer les alias pour les tables Property + pp = aliased(PropertyModel) # primary_property + lp = aliased(PropertyModel) # largest_property_use_type + slp = aliased(PropertyModel) # second_largest_property_use_type + tlp = aliased(PropertyModel) # third_largest_property_use_type + + building = ( + db.query( + BuildingModelsModel.id, + BuildingModelsModel.year_built, + BuildingModelsModel.number_of_buildings, + BuildingModelsModel.number_of_floors, + BuildingModelsModel.property_gfa_total, + BuildingModelsModel.property_gfa_parking, + BuildingModelsModel.second_largest_property_use_type_gfa, + BuildingModelsModel.third_largest_property_use_type_gfa, + BuildingModelsModel.multiusage, + BuildingModelsModel.steam, + BuildingModelsModel.electricity, + BuildingModelsModel.natural_gas, + NeighborhoodModel.model_id.label("neighborhood_id"), + BuildingTypeModel.model_id.label("building_type_id"), + lp.model_id.label("largest_property_use_type_id"), + pp.model_id.label("primary_property_type_id"), + slp.model_id.label("second_largest_property_use_type_id"), + tlp.model_id.label("third_largest_property_use_type_id"), + ) + .outerjoin( + BuildingTypeModel, + BuildingModelsModel.building_type_id == BuildingTypeModel.id, + ) + .outerjoin( + NeighborhoodModel, + BuildingModelsModel.neighborhood_id == NeighborhoodModel.id, + ) + .outerjoin(pp, BuildingModelsModel.primary_property_type_id == pp.id) + .outerjoin( + lp, BuildingModelsModel.largest_property_use_type_id == lp.id + ) + .outerjoin( + slp, + BuildingModelsModel.second_largest_property_use_type_id == slp.id, + ) + .outerjoin( + tlp, + BuildingModelsModel.third_largest_property_use_type_id == tlp.id, + ) + ) + + if building_id: + result = building.filter(BuildingModelsModel.id == building_id).first() + if result is None: + raise NotFoundError( + f"Bâtiment avec l'ID {building_id} non trouvé", + "BUILDING_NOT_FOUND", + ) + return result + + except NotFoundError: + raise + except Exception as e: + logger.error( + f"Erreur lors de la récupération de la prediction {building_id}: {e}" + ) + raise ServiceError(f"Impossible de récupérer la prediction: {str(e)}") + + @staticmethod + def set_new_building( + db: Session, building_model: BuildingModelsModel + ) -> BuildingModelsModel: + """ + Ajoute un nouveau modèle de bâtiment dans la base de données. + + Args: + db (Session): Session de base de données + building_model (BuildingModelsModel): Le modèle de bâtiment à ajouter + + Returns: + BuildingModelsModel: Le modèle de bâtiment ajouté avec son ID généré + + Raises: + ValidationError: Si les données du bâtiment ne sont pas valides + ServiceError: Si une erreur survient lors de l'ajout + """ + try: + logger.info("Ajout d'un nouveau bâtiment") + + # Validation des données obligatoires pour les prédictions ML (17 features) + required_fields = [ + ("year_built", "L'année de construction est obligatoire"), + ("number_of_buildings", "Le nombre de bâtiments est obligatoire"), + ("number_of_floors", "Le nombre d'étages est obligatoire"), + ("property_gfa_total", "La surface totale GFA est obligatoire"), + ("property_gfa_parking", "La surface de parking GFA est obligatoire"), + ( + "second_largest_property_use_type_gfa", + "La surface du 2e type d'usage principal est obligatoire", + ), + ( + "third_largest_property_use_type_gfa", + "La surface du 3e type d'usage principal est obligatoire", + ), + ("multiusage", "L'indicateur multiusage est obligatoire"), + ("steam", "L'indicateur vapeur est obligatoire"), + ("electricity", "L'indicateur électricité est obligatoire"), + ("natural_gas", "L'indicateur gaz naturel est obligatoire"), + ("neighborhood_id", "L'identifiant du quartier est obligatoire"), + ( + "building_type_id", + "L'identifiant du type de bâtiment est obligatoire", + ), + ( + "largest_property_use_type_id", + "L'ID du plus grand type d'usage est obligatoire", + ), + ( + "primary_property_type_id", + "L'ID du type de propriété principal est obligatoire", + ), + ( + "second_largest_property_use_type_id", + "L'ID du 2e type d'usage principal est obligatoire", + ), + ( + "third_largest_property_use_type_id", + "L'ID du 3e type d'usage principal est obligatoire", + ), + ] + + for field_name, error_message in required_fields: + field_value = getattr(building_model, field_name, None) + if field_value is None: + raise ValidationError( + error_message, f"MISSING_{field_name.upper()}" + ) + + # Ajouter le nouveau bâtiment à la session + db.add(building_model) + + # Sauvegarder les changements + db.commit() + + # Rafraîchir l'objet pour récupérer l'ID généré + db.refresh(building_model) + + logger.info(f"Bâtiment ajouté avec succès, ID: {building_model.id}") + return building_model + + except (ValidationError, NotFoundError): + db.rollback() + raise + except Exception as e: + # Annuler les changements en cas d'erreur + db.rollback() + logger.error(f"Erreur lors de l'ajout du bâtiment: {e}") + raise ServiceError( + f"Erreur lors de l'ajout du bâtiment: {str(e)}", "DATABASE_ERROR" + ) + + +class PredictionService: + """ + Service spécialisé pour les prédictions énergétiques avec RandomForestRegressor. + + Cette classe implémente l'interface entre les données métier et le modèle + de machine learning pour générer des prédictions de consommation énergétique + des bâtiments de Seattle. + + Architecture ML: + - Modèle: RandomForestRegressor pré-entraîné + - Prédiction: log(consommation en kBTU) puis conversion exponentielle + - Features: 17 caractéristiques du bâtiment (physiques, géographiques, usage) + - Intervalle de confiance: Calculé à partir des prédictions individuelles des arbres + + Caractéristiques d'entrée: + - Physiques: année de construction, nombre d'étages, surfaces GFA + - Géographiques: neighborhood_id (modèle) + - Usage: type de bâtiment, propriétés d'usage (principal, secondaire, tertiaire) + - Énergie: sources utilisées (vapeur, électricité, gaz naturel) + - Multi-usage: indicateur de diversité d'usage + + Pipeline de prédiction: + 1. Chargement du modèle depuis le registre + 2. Preprocessing des features (conversion types, normalisation) + 3. Création DataFrame avec colonnes ordonnées + 4. Prédiction en log-space + 5. Conversion exponentielle vers kBTU réels + 6. Calcul intervalle de confiance (percentiles 5-95) + + Performance: + - Cache du modèle en mémoire pour éviter les rechargements + - Preprocessing vectorisé avec pandas/numpy + - Modèle dummy automatique en cas d'échec de chargement + + Méthodes principales: + - predict(): Prédiction principale avec intervalle de confiance + - get_feature_importance(): Analyse d'importance des variables + - _preprocess_features(): Preprocessing interne des données + - _calculate_prediction_interval(): Calcul statistique de l'intervalle + + Thread Safety: + Non thread-safe à cause du cache de modèle. Utiliser une instance par thread. + + Note: + Le modèle prédit en log-space pour stabilité numérique puis convertit + via exponentielle. L'intervalle de confiance est calculé en log-space. + """ + + def __init__(self): + # Le modèle sera chargé à la première utilisation + self.model: Optional[RandomForestRegressor] = None + self.model_version = "v1.0" + self.feature_names = [ + "year_built", + "number_of_buildings", + "number_of_floors", + "property_gfa_total", + "property_gfa_parking", + "second_largest_property_use_type_gfa", + "third_largest_property_use_type_gfa", + "multiusage", + "steam", + "electricity", + "natural_gas", + "neighborhood_id", + "building_type_id", + "largest_property_use_type_id", + "primary_property_type_id", + "second_largest_property_use_type_id", + "third_largest_property_use_type_id", + ] + + def _create_dummy_model(self) -> RandomForestRegressor: + """Crée un modèle dummy pour les tests""" + logger.info("Création d'un modèle dummy pour les tests") + model = RandomForestRegressor(n_estimators=10, random_state=42) + # Données d'exemple pour l'entraînement dummy + X_dummy = np.random.rand(100, len(self.feature_names)) + y_dummy = np.random.rand(100) * 200 + model.fit(X_dummy, y_dummy) + return model + + def _preprocess_features(self, features: BuildingFeatures) -> np.ndarray: + """Préprocesse les caractéristiques d'entrée""" + # Convertir le modèle Pydantic en dictionnaire + feature_dict = features.model_dump() + + # Convertir les champs GFA de string à float + gfa_fields = [ + "property_gfa_total", + "property_gfa_parking", + "second_largest_property_use_type_gfa", + "third_largest_property_use_type_gfa", + ] + + for field in gfa_fields: + if isinstance(feature_dict[field], str): + feature_dict[field] = float(feature_dict[field]) + + # Créer un DataFrame avec les bonnes colonnes dans le bon ordre + df = pd.DataFrame([feature_dict]) + + # S'assurer que toutes les features attendues sont présentes + for feature in self.feature_names: + if feature not in df.columns: + df[feature] = 0 + + # Réorganiser les colonnes dans le bon ordre + df = df[self.feature_names] + + logger.debug(f"Features préprocessées: {df.iloc[0].to_dict()}") + return df.values + + def _calculate_prediction_interval(self, X_df: pd.DataFrame) -> Dict[str, float]: + """Calcule l'intervalle de prédiction basé sur les arbres individuels""" + if self.model is None or not hasattr(self.model, "estimators_"): + return None + + # Prédictions de tous les arbres + try: + tree_predictions = np.array( + [tree.predict(X_df)[0] for tree in self.model.estimators_] + ) + except Exception as e: + logger.warning(f"Erreur lors du calcul de l'intervalle de confiance: {e}") + return None + + # Calcul de l'intervalle de confiance (percentiles 5 et 95) + lower_bound = np.percentile(tree_predictions, 5) + upper_bound = np.percentile(tree_predictions, 95) + + return { + "lower": round(float(lower_bound), 2), + "upper": round(float(upper_bound), 2), + } + + async def predict( + self, features: BuildingFeatures, building_id: int = None, db: Session = None + ) -> Dict[str, Any]: + """ + Effectue une prédiction et sauvegarde automatiquement le résultat en base. + + Args: + features (BuildingFeatures): Caractéristiques du bâtiment + building_id (int, optional): ID du bâtiment pour sauvegarde + db (Session, optional): Session de base de données pour sauvegarde + + Returns: + Dict[str, Any]: Résultat de la prédiction avec métadonnées + + Note: + Si building_id et db sont fournis, la prédiction est automatiquement + sauvegardée dans la table building_energy_predictions avec predicted=True + """ + from project5.utils.model_registry import get_ml_model + + try: + if self.model is None: + ml_model_wrapper = get_ml_model() + if ml_model_wrapper is None: + raise ValueError("Aucun modèle ML n'est disponible") + self.model = ml_model_wrapper.get_model() + if self.model is None: + raise ValueError("Le modèle ML n'est pas chargé") + + # Préprocessing + # print(f"-----------> Features : {self.model.feature_names_in_}") + X = self._preprocess_features(features) + # Convertir en DataFrame avec les noms des features du modèle + feature_names = getattr(self.model, "feature_names_in_", None) + if feature_names is None: + # Fallback : utiliser les noms des features depuis le modèle de données + feature_names = [ + "year_built", + "number_of_buildings", + "number_of_floors", + "property_gfa_total", + "property_gfa_parking", + "second_largest_property_use_type_gfa", + "third_largest_property_use_type_gfa", + "multiusage", + "steam", + "electricity", + "natural_gas", + "neighborhood_id", + "building_type_id", + "largest_property_use_type_id", + "primary_property_type_id", + "second_largest_property_use_type_id", + "third_largest_property_use_type_id", + ] + + X_df = pd.DataFrame(X, columns=feature_names) + # print(f"-----------> X : {X_df}") + # Prédiction + prediction = self.model.predict(X_df)[0] + + # Calcul de l'intervalle de confiance + confidence_interval = self._calculate_prediction_interval(X_df) + + result = { + "prediction": round(float(np.exp(prediction)), 2), + "prediction_log": round(float(prediction), 2), + "model_version": self.model_version, + "confidence_interval_log": confidence_interval, + } + + logger.info(f"Prédiction effectuée: {result['prediction']}") + + # Sauvegarde automatique si building_id et db sont fournis + if building_id is not None and db is not None: + self._save_prediction_to_db(db, building_id, result["prediction"]) + + return result + + except Exception as e: + logger.error(f"Erreur lors de la prédiction: {e}") + raise + + def _save_prediction_to_db( + self, db: Session, building_id: int, prediction_value: float + ): + """ + Sauvegarde une prédiction dans la table building_energy_predictions. + + Args: + db (Session): Session de base de données + building_id (int): ID du bâtiment + prediction_value (float): Valeur prédite en kBTU + """ + try: + logger.info( + f"Sauvegarde de la prédiction pour le bâtiment {building_id}: {prediction_value} kBTU" + ) + + # Vérifier si une prédiction existe déjà pour ce bâtiment + existing_prediction = ( + db.query(BuildingEnergyPredictions) + .filter(BuildingEnergyPredictions.building_id == building_id) + .first() + ) + + if existing_prediction: + # Mettre à jour la prédiction existante + existing_prediction.site_energy_use_wn_kbtu = prediction_value + existing_prediction.predicted = True + # updated_at sera automatiquement mise à jour par le trigger + logger.info(f"Prédiction mise à jour pour le bâtiment {building_id}") + else: + # Créer une nouvelle prédiction + new_prediction = BuildingEnergyPredictions( + building_id=building_id, + site_energy_use_wn_kbtu=prediction_value, + predicted=True, + # updated_at sera automatiquement définie par la base + ) + db.add(new_prediction) + logger.info(f"Nouvelle prédiction créée pour le bâtiment {building_id}") + + db.commit() + logger.info("Prédiction sauvegardée avec succès en base de données") + + except Exception as e: + db.rollback() + logger.error(f"Erreur lors de la sauvegarde de la prédiction: {e}") + # Ne pas propager l'erreur pour ne pas faire échouer la prédiction + # La prédiction reste valide même si la sauvegarde échoue + + def get_feature_importance(self) -> Dict[str, float]: + """Retourne l'importance des features""" + if not hasattr(self.model, "feature_importances_"): + return {} + + importances = dict(zip(self.feature_names, self.model.feature_importances_)) + return { + k: round(float(v), 4) + for k, v in sorted(importances.items(), key=lambda x: x[1], reverse=True) + } diff --git a/src/project5/services/building_type_service.py b/src/project5/services/building_type_service.py new file mode 100644 index 0000000000000000000000000000000000000000..57366e4383f5371a31b15a23fe55ff86f5aef100 --- /dev/null +++ b/src/project5/services/building_type_service.py @@ -0,0 +1,246 @@ +""" +Service métier pour la gestion des types de bâtiments (lecture seule). + +Ce module implémente la couche de logique métier pour les opérations de lecture +sur les types de bâtiments dans le système de classification énergétique de Seattle. + +Classes: + BuildingTypeService: Service principal pour la gestion des types de bâtiments + +Fonctionnalités: + - Récupération paginée des types avec tri configurable + - Recherche par ID avec gestion robuste des erreurs + - Validation des paramètres métier + - Logging détaillé pour traçabilité des opérations + - Gestion centralisée des exceptions métier + +Classification des types: + Les types de bâtiments permettent une classification fonctionnelle pour + améliorer la précision des prédictions énergétiques : + - Residential: Bâtiments résidentiels (appartements, maisons) + - Commercial: Bureaux, magasins, restaurants + - Industrial: Usines, entrepôts, installations industrielles + - Mixed Use: Bâtiments à usage mixte (résidentiel + commercial) + - Institutional: Écoles, hôpitaux, bâtiments publics + +Architecture: + Service en lecture seule suivant le pattern Repository pour séparer + la logique métier de l'accès aux données. Les types de bâtiments sont + des données de référence importées et ne changent pas fréquemment. + +Note: + Chaque type possède un model_id unique utilisé pour l'encodage + catégoriel dans le modèle de machine learning. +""" + +import logging +from typing import List + +from sqlalchemy import asc, desc +from sqlalchemy.orm import Session + +from project5.models import BuildingType as BuildingTypeModel +from project5.utils.exceptions import NotFoundError, ServiceError + +logger = logging.getLogger(__name__) + + +class BuildingTypeService: + """ + Service métier pour la gestion des types de bâtiments (lecture seule). + + Cette classe implémente la logique métier pour l'accès aux données des types + de bâtiments utilisés dans la classification énergétique. Elle fournit une + interface standardisée pour les opérations de lecture avec gestion d'erreurs. + + Responsabilités: + - Récupération paginée des types de bâtiments avec tri + - Recherche de types individuels par identifiant unique + - Validation des paramètres d'entrée + - Gestion robuste des exceptions avec logging détaillé + - Interface unifiée pour l'accès aux données de classification + + Types de bâtiments supportés: + - Residential: Bâtiments résidentiels (logements, appartements) + - Commercial: Bâtiments commerciaux (bureaux, magasins, restaurants) + - Industrial: Bâtiments industriels (usines, entrepôts) + - Mixed Use: Bâtiments à usage mixte (résidentiel + commercial) + - Institutional: Bâtiments institutionnels (écoles, hôpitaux) + + Architecture: + Suit le pattern Service Layer pour séparer la logique métier des + détails d'implémentation. Utilise SQLAlchemy pour l'accès aux données + et un système de logging pour la traçabilité. + + Thread Safety: + Méthodes statiques thread-safe. Pas d'état partagé entre les appels. + + Performance: + Optimisé pour la lecture avec tri par clé primaire (index automatique). + Support de la pagination pour éviter la surcharge mémoire. + + Note: + Service en lecture seule car les types de bâtiments sont des données + de référence stables importées depuis des sources externes. + Le model_id de chaque type est crucial pour l'encodage ML. + """ + + @staticmethod + def get_all( + db: Session, + skip: int = 0, + limit: int = 100, + sort_by: str = "name", + sort_order: str = "asc", + ) -> List[BuildingTypeModel]: + """ + Récupère tous les types de bâtiments avec options de pagination et tri. + + Cette méthode implémente la récupération paginée des types de bâtiments + avec support du tri configurable. Optimisée pour les listes de sélection + et les interfaces d'administration. + + Args: + db (Session): Session SQLAlchemy active pour l'accès à la base de données + Doit être une session valide et connectée + skip (int, optional): Nombre d'éléments à ignorer pour la pagination. + Defaults to 0. Doit être >= 0 pour éviter les erreurs + limit (int, optional): Nombre maximum d'éléments à retourner. + Defaults to 100. Recommandé <= 1000 pour les performances + sort_by (str, optional): Champ de tri. Defaults to "name". + Actuellement seul 'id' est implémenté pour la cohérence + sort_order (str, optional): Ordre de tri ('asc', 'desc'). + Defaults to "asc". Insensible à la casse + + Returns: + List[BuildingTypeModel]: Liste des types de bâtiments ordonnés + selon les critères spécifiés. Inclut toutes les propriétés : + id, model_id, building_type_name, description, timestamps + + Raises: + ServiceError: En cas d'erreur lors de l'accès à la base de données + ou de problème technique durant la récupération + + Example: + >>> service = BuildingTypeService() + >>> types = service.get_all(db, skip=0, limit=20, sort_order="asc") + >>> for building_type in types: + ... print(f"{building_type.building_type_name}: {building_type.model_id}") + + Performance: + - Utilise LIMIT/OFFSET SQL pour pagination efficace + - Tri par clé primaire optimisé par index automatique + - Temps de réponse constant indépendant du volume total + + Usage typique: + - Listes déroulantes dans les interfaces utilisateur + - Sélection de types pour la création/modification de bâtiments + - Exports de données de référence + - APIs de consultation des types disponibles + + Note: + Le paramètre sort_by est conservé pour compatibilité future mais + seul le tri par 'id' est actuellement supporté. + """ + try: + logger.info( + f"Récupération des types de bâtiment : skip={skip}, limit={limit}, " + f"sort={sort_by}:{sort_order}" + ) + + query = db.query(BuildingTypeModel) + + # Tri + sort_field = "id" + if sort_order.lower() == "desc": + query = query.order_by(desc(sort_field)) + else: + query = query.order_by(asc(sort_field)) + + # Pagination + building_types = query.offset(skip).limit(limit).all() + + logger.info(f"Récupérés {len(building_types)} types de bâtiment") + return building_types + + except Exception as e: + logger.error(f"Erreur lors de la récupération des types de bâtiment: {e}") + raise ServiceError( + f"Impossible de récupérer les types de bâtiment: {str(e)}" + ) + + @staticmethod + def get_by_id(db: Session, id: int) -> BuildingTypeModel: + """ + Récupère un type de bâtiment spécifique par son identifiant unique. + + Cette méthode effectue une recherche directe par clé primaire pour + récupérer un type de bâtiment avec toutes ses propriétés. Inclut une + gestion robuste des erreurs et un logging détaillé. + + Args: + db (Session): Session SQLAlchemy active pour l'accès à la base de données + La session doit être valide et connectée + id (int): Identifiant unique du type de bâtiment à récupérer + Doit être un entier positif correspondant à une clé primaire valide + + Returns: + BuildingTypeModel: Instance complète du modèle contenant : + - id: Identifiant unique (clé primaire) + - model_id: Identifiant pour le modèle ML + - building_type_name: Nom du type (ex: "Commercial", "Residential") + - description: Description détaillée du type + - created_at: Date de création + - updated_at: Date de dernière modification + + Raises: + NotFoundError: Si aucun type de bâtiment n'existe avec l'ID spécifié + Le message d'erreur inclut l'ID recherché pour faciliter le debugging + ServiceError: En cas d'erreur technique lors de l'accès à la base + (problème de connexion, corruption de données, etc.) + + Example: + >>> service = BuildingTypeService() + >>> try: + ... building_type = service.get_by_id(db, 1) + ... print(f"Type: {building_type.building_type_name}") + ... print(f"Model ID: {building_type.model_id}") + ... except NotFoundError: + ... print("Type de bâtiment non trouvé") + + Performance: + - Recherche optimisée par clé primaire (index automatique) + - Temps de réponse constant O(1) indépendant du volume + - Récupération d'un seul enregistrement sans surcharge + + Logging: + - INFO: Début et fin de recherche avec ID et nom du type trouvé + - WARNING: Type de bâtiment non trouvé avec ID spécifié + - ERROR: Erreurs techniques avec stack trace + + Usage typique: + - Détails d'un type de bâtiment pour affichage + - Validation d'existence avant utilisation + - Récupération du model_id pour prédictions ML + - APIs de consultation détaillée + + Note: + Cette méthode est optimisée pour la récupération d'un seul type. + Pour plusieurs types, utiliser get_all() avec des filtres appropriés. + """ + try: + logger.info(f"Récupération type de bâtiment ID: {id}") + + bt = db.query(BuildingTypeModel).filter(BuildingTypeModel.id == id).first() + + if bt is None: + logger.warning(f"type de bâtiment {id} non trouvé") + raise NotFoundError(f"type de bâtiment avec l'ID {id} non trouvé") + + logger.info(f"type de bâtiment trouvé: {bt.building_type_name}") + return bt + except NotFoundError: + raise + except Exception as e: + logger.error(f"Erreur lors de la récupération du type de bâtiment{id}: {e}") + raise ServiceError(f"Impossible de récupérer le type de bâtiment: {str(e)}") diff --git a/src/project5/services/categorie_service.py b/src/project5/services/categorie_service.py new file mode 100644 index 0000000000000000000000000000000000000000..2fe193812fd446cacef891e9cb331a23b6e94231 --- /dev/null +++ b/src/project5/services/categorie_service.py @@ -0,0 +1,279 @@ +""" +Service métier pour la gestion des catégories d'usage des propriétés (lecture seule). + +Ce module implémente la couche de logique métier pour les opérations de lecture +sur les catégories d'usage dans le système de classification énergétique. + +Classes: + CategorieService: Service principal pour la gestion des catégories + +Fonctionnalités: + - Récupération paginée des catégories avec tri + - Recherche par ID avec validation robuste + - Gestion des erreurs et logging détaillé + - Interface unifiée pour l'accès aux données de classification + +Classification hiérarchique: + Les catégories regroupent les propriétés d'usage selon leur fonction principale : + - Office: Espaces de bureaux et administratifs + - Retail: Commerces, magasins, points de vente + - Residential: Logements résidentiels et habitations + - Education: Établissements d'enseignement et formation + - Healthcare: Structures de soins et santé + - Entertainment: Lieux de divertissement et restauration + +Architecture: + Service en lecture seule suivant le pattern Repository. Les catégories + forment la couche supérieure de la hiérarchie de classification, + chaque catégorie contenant plusieurs types de propriétés spécifiques. + +Usage dans le ML: + Les catégories permettent un regroupement sémantique des propriétés + pour l'analyse énergétique par secteur d'activité, améliorant ainsi + la précision des prédictions du modèle de machine learning. + +Note: + Service en lecture seule car les catégories sont des données de référence + stables définissant la taxonomie métier du système. +""" + +import logging +from typing import List + +from sqlalchemy import asc, desc +from sqlalchemy.orm import Session + +from project5.models import Categorie as CategorieModel +from project5.utils.exceptions import NotFoundError, ServiceError + +logger = logging.getLogger(__name__) + + +class CategorieService: + """ + Service métier pour la gestion des catégories d'usage des propriétés (lecture seule). + + Cette classe implémente la logique métier pour l'accès aux données des catégories + dans le système de classification hiérarchique des usages de propriétés. + + Responsabilités: + - Récupération paginée des catégories avec tri + - Recherche de catégories individuelles par identifiant + - Validation des paramètres d'entrée + - Gestion robuste des exceptions avec logging + - Interface unifiée pour la classification hiérarchique + + Hiérarchie de classification: + Catégories -> Propriétés -> Bâtiments + Chaque catégorie regroupe plusieurs propriétés d'usage similaires : + + Office: + - Office (bureaux standards) + - Bank Branch (agences bancaires) + - Courthouse (tribunaux) + + Retail: + - Retail Store (magasins) + - Restaurant (restauration) + - Supermarket/Grocery Store (supermarchés) + + Residential: + - Multifamily Housing (logements collectifs) + - Senior Care Community (résidences seniors) + + Education: + - K-12 School (écoles primaires/secondaires) + - College/University (universités) + + Architecture: + Suit le pattern Service Layer avec accès aux données via SQLAlchemy. + Les catégories sont des données de référence stables qui évoluent peu. + + Thread Safety: + Méthodes statiques thread-safe sans état partagé. + + Performance: + Optimisé pour la lecture avec tri par clé primaire et pagination efficace. + + Usage dans l'analyse énergétique: + Les catégories permettent l'analyse des patterns de consommation + par secteur d'activité, facilitant les comparaisons et prédictions. + + Note: + Service en lecture seule car les catégories définissent la taxonomie + métier stable du système de classification. + """ + + @staticmethod + def get_all( + db: Session, + skip: int = 0, + limit: int = 100, + sort_by: str = "name", + sort_order: str = "asc", + ) -> List[CategorieModel]: + """ + Récupère toutes les catégories d'usage avec options de pagination et tri. + + Cette méthode implémente la récupération paginée des catégories d'usage + des propriétés. Optimisée pour les interfaces de sélection et d'administration. + + Args: + db (Session): Session SQLAlchemy active pour l'accès à la base de données + Doit être une session valide et connectée + skip (int, optional): Nombre d'éléments à ignorer pour la pagination. + Defaults to 0. Doit être >= 0 + limit (int, optional): Nombre maximum d'éléments à retourner. + Defaults to 100. Recommandé <= 500 pour les performances + sort_by (str, optional): Champ de tri. Defaults to "name". + Actuellement seul 'id' est implémenté pour la cohérence + sort_order (str, optional): Ordre de tri ('asc', 'desc'). + Defaults to "asc". Insensible à la casse + + Returns: + List[CategorieModel]: Liste des catégories ordonnées selon les critères. + Chaque élément contient : + - id: Identifiant unique de la catégorie + - category_code: Code court (ex: "OFF", "RET", "RES") + - category_name: Nom complet (ex: "Office", "Retail") + - description: Description détaillée de la catégorie + - created_at/updated_at: Timestamps de gestion + + Raises: + ServiceError: En cas d'erreur lors de l'accès à la base de données + ou de problème technique durant la récupération + + Example: + >>> service = CategorieService() + >>> categories = service.get_all(db, skip=0, limit=10) + >>> for cat in categories: + ... print(f"{cat.category_code}: {cat.category_name}") + OFF: Office + RET: Retail + RES: Residential + + Performance: + - Pagination SQL avec LIMIT/OFFSET pour éviter la surcharge mémoire + - Tri par clé primaire optimisé par index automatique + - Volume de données limité (nombre de catégories stable) + + Usage typique: + - Listes déroulantes pour sélection de catégories + - Interfaces d'administration des taxonomies + - APIs de consultation des catégories disponibles + - Exports de données de référence + + Note: + Le nombre de catégories étant limité, la pagination est surtout + utile pour la cohérence d'interface avec les autres services. + """ + try: + logger.info( + f"Récupération des categories : skip={skip}, limit={limit}, " + f"sort={sort_by}:{sort_order}" + ) + + query = db.query(CategorieModel) + + # Tri + sort_field = "id" + if sort_order.lower() == "desc": + query = query.order_by(desc(sort_field)) + else: + query = query.order_by(asc(sort_field)) + + # Pagination + cat = query.offset(skip).limit(limit).all() + + logger.info(f"Récupérés {len(cat)} categories") + return cat + + except Exception as e: + logger.error(f"Erreur lors de la récupération des categories: {e}") + raise ServiceError(f"Impossible de récupérer les categories: {str(e)}") + + @staticmethod + def get_by_id(db: Session, categorie_id: int) -> CategorieModel: + """ + Récupère une catégorie d'usage spécifique par son identifiant unique. + + Cette méthode effectue une recherche directe par clé primaire pour + récupérer une catégorie avec toutes ses propriétés. + + Args: + db (Session): Session SQLAlchemy active pour l'accès à la base de données + La session doit être valide et connectée + categorie_id (int): Identifiant unique de la catégorie à récupérer + Doit être un entier positif correspondant à une clé primaire valide + + Returns: + CategorieModel: Instance complète du modèle contenant : + - id: Identifiant unique (clé primaire) + - category_code: Code court de la catégorie (ex: "OFF", "RET") + - category_name: Nom complet (ex: "Office", "Retail") + - description: Description détaillée de la catégorie + - created_at: Date de création de l'enregistrement + - updated_at: Date de dernière modification + + Raises: + NotFoundError: Si aucune catégorie n'existe avec l'ID spécifié + Le message d'erreur inclut l'ID recherché pour faciliter le debugging + ServiceError: En cas d'erreur technique lors de l'accès à la base + (problème de connexion, corruption de données, etc.) + + Example: + >>> service = CategorieService() + >>> try: + ... category = service.get_by_id(db, 1) + ... print(f"Catégorie: {category.category_name}") + ... print(f"Code: {category.category_code}") + ... print(f"Description: {category.description}") + ... except NotFoundError: + ... print("Catégorie non trouvée") + + Performance: + - Recherche optimisée par clé primaire (index automatique) + - Temps de réponse constant O(1) indépendant du volume + - Récupération d'un seul enregistrement + + Logging: + - INFO: Début et fin de recherche avec ID et nom de la catégorie + - WARNING: Catégorie non trouvée avec ID spécifié + - ERROR: Erreurs techniques avec détails + + Usage typique: + - Détails d'une catégorie pour affichage + - Validation d'existence avant utilisation + - Navigation hiérarchique dans la taxonomie + - APIs de consultation détaillée + + Relations: + La catégorie récupérée peut être liée à plusieurs propriétés + via la relation category_id dans la table properties. + + Note: + Cette méthode est optimisée pour la récupération d'une seule catégorie. + Pour plusieurs catégories, utiliser get_all() avec filtres appropriés. + """ + try: + logger.info(f"Récupération de la category ID: {categorie_id}") + + categorie = ( + db.query(CategorieModel) + .filter(CategorieModel.id == categorie_id) + .first() + ) + + if categorie is None: + logger.warning(f"category {categorie_id} non trouvée") + raise NotFoundError(f"category avec l'ID {categorie_id} non trouvée") + + logger.info(f"category trouvée: {categorie.category_name}") + return categorie + except NotFoundError: + raise + except Exception as e: + logger.error( + f"Erreur lors de la récupération de la category {categorie_id}: {e}" + ) + raise ServiceError(f"Impossible de récupérer la category: {str(e)}") diff --git a/src/project5/services/neighborhood_service.py b/src/project5/services/neighborhood_service.py new file mode 100644 index 0000000000000000000000000000000000000000..dba74471ee9982c7a4915aff0a6fea9c0459168b --- /dev/null +++ b/src/project5/services/neighborhood_service.py @@ -0,0 +1,508 @@ +""" +Service métier avancé pour la gestion des quartiers de Seattle (lecture seule). + +Ce module implémente une couche de logique métier complète pour les opérations +sur les quartiers de Seattle utilisés dans le système de géolocalisation +des bâtiments pour les prédictions énergétiques. + +Classes: + NeighborhoodService: Service principal avec fonctionnalités étendues + +Fonctionnalités avancées: + - Récupération paginée avec tri configurable + - Recherche textuelle avancée avec correspondance partielle + - Recherche par nom exacte et approximative + - Statistiques complètes avec répartition alphabétique + - Validation et comptage avec filtres + - Vérification d'existence optimisée + - Health check pour monitoring + - Gestion des noms de quartiers + +Quartiers de Seattle: + Le service gère les 13 quartiers principaux de Seattle : + - BALLARD, CAPITOL HILL, DOWNTOWN, EASTLAKE + - FREMONT, GEORGETOWN, GREENWOOD, LAKE CITY + - MAGNOLIA, NORTHGATE, QUEEN ANNE, SOUTHEAST, SOUTHWEST + +Architecture géospatiale: + Les quartiers servent de référence géographique pour localiser + les bâtiments et analyser les patterns de consommation énergétique + par zone urbaine. Chaque quartier possède un model_id pour l'encodage ML. + +Performance: + - Requêtes optimisées avec index sur neighborhood_name + - Recherche textuelle avec ILIKE pour correspondance partielle + - Tri alphabétique pour améliorer l'expérience utilisateur + - Statistiques calculées en temps réel + +Note: + Service en lecture seule car les quartiers sont des données géographiques + de référence stables définies par la municipalité de Seattle. +""" + +import logging +from typing import Any, Dict, List, Optional + +from sqlalchemy import asc, desc +from sqlalchemy.orm import Session + +from project5.models import Neighborhood as NeighborhoodModel +from project5.utils.exceptions import NotFoundError, ServiceError + +logger = logging.getLogger(__name__) + + +class NeighborhoodService: + @staticmethod + def get_all( + db: Session, + skip: int = 0, + limit: int = 100, + sort_by: str = "name", + sort_order: str = "asc", + ) -> List[NeighborhoodModel]: + """Récupère tous les quartiers avec options de filtrage et tri. + + Args: + db: Session de base de données + skip: Nombre d'éléments à ignorer (pagination) + limit: Nombre maximum d'éléments à retourner + sort_by: Champ de tri ('name', 'created_at', 'id') + sort_order: Ordre de tri ('asc', 'desc') + + Returns: + Liste des quartiers correspondants aux critères + + Raises: + ServiceError: En cas d'erreur lors de la récupération + """ + try: + logger.info( + f"Récupération des quartiers: skip={skip}, limit={limit}, " + f"sort={sort_by}:{sort_order}" + ) + + query = db.query(NeighborhoodModel) + + # Tri + sort_field = NeighborhoodService._get_sort_field(sort_by) + if sort_order.lower() == "desc": + query = query.order_by(desc(sort_field)) + else: + query = query.order_by(asc(sort_field)) + + # Pagination + neighborhoods = query.offset(skip).limit(limit).all() + + logger.info(f"Récupérés {len(neighborhoods)} quartiers") + return neighborhoods + + except Exception as e: + logger.error(f"Erreur lors de la récupération des quartiers: {e}") + raise ServiceError(f"Impossible de récupérer les quartiers: {str(e)}") + + @staticmethod + def get_by_id(db: Session, neighborhood_id: int) -> NeighborhoodModel: + """Récupère un quartier par son ID. + + Args: + db: Session de base de données + neighborhood_id: ID du quartier + + Returns: + Le quartier correspondant + + Raises: + NotFoundError: Si le quartier n'existe pas + ServiceError: En cas d'erreur technique + """ + try: + logger.info(f"Récupération du quartier ID: {neighborhood_id}") + + neighborhood = ( + db.query(NeighborhoodModel) + .filter(NeighborhoodModel.id == neighborhood_id) + .first() + ) + + if neighborhood is None: + logger.warning(f"Quartier {neighborhood_id} non trouvé") + raise NotFoundError(f"Quartier avec l'ID {neighborhood_id} non trouvé") + + logger.info(f"Quartier trouvé: {neighborhood.neighborhood_name}") + return neighborhood + + except NotFoundError: + raise + except Exception as e: + logger.error( + f"Erreur lors de la récupération du quartier {neighborhood_id}: {e}" + ) + raise ServiceError(f"Impossible de récupérer le quartier: {str(e)}") + + @staticmethod + def get_by_name( + db: Session, neighborhood_name: str, exact_match: bool = True + ) -> Optional[NeighborhoodModel]: + """Récupère un quartier par son nom. + + Args: + db: Session de base de données + neighborhood_name: Nom du quartier + exact_match: Si True, recherche exacte, sinon recherche partielle + + Returns: + Le quartier trouvé ou None + + Raises: + ServiceError: En cas d'erreur technique + """ + try: + logger.info( + f"Recherche du quartier par nom: '{neighborhood_name}' (exact: {exact_match})" + ) + + query = db.query(NeighborhoodModel) + + if exact_match: + neighborhood = query.filter( + NeighborhoodModel.neighborhood_name == neighborhood_name.upper() + ).first() + else: + neighborhood = query.filter( + NeighborhoodModel.neighborhood_name.ilike( + f"%{neighborhood_name.upper()}%" + ) + ).first() + + if neighborhood: + logger.info(f"Quartier trouvé: {neighborhood.neighborhood_name}") + else: + logger.info("Aucun quartier trouvé") + + return neighborhood + + except Exception as e: + logger.error( + f"Erreur lors de la recherche par nom '{neighborhood_name}': {e}" + ) + raise ServiceError(f"Impossible de rechercher le quartier: {str(e)}") + + @staticmethod + def search( + db: Session, + query_text: str, + limit: int = 50, + active_only: bool = True, + min_length: int = 1, + ) -> List[NeighborhoodModel]: + """Recherche textuelle avancée dans les quartiers. + + Args: + db: Session de base de données + query_text: Texte à rechercher + limit: Nombre maximum de résultats + active_only: Filtrer seulement les quartiers actifs + min_length: Longueur minimale du terme de recherche + + Returns: + Liste des quartiers correspondants + + Raises: + ServiceError: En cas d'erreur technique + """ + try: + # Validation métier + if len(query_text.strip()) < min_length: + logger.warning(f"Terme de recherche trop court: '{query_text}'") + return [] + + clean_query = query_text.strip().upper() + logger.info(f"Recherche textuelle: '{clean_query}' (limit: {limit})") + + query = db.query(NeighborhoodModel).filter( + NeighborhoodModel.neighborhood_name.ilike(f"%{clean_query}%") + ) + + # Note: is_active field not available in current model + # if active_only: + # query = query.filter(NeighborhoodModel.is_active == True) + + neighborhoods = ( + query.order_by(NeighborhoodModel.neighborhood_name).limit(limit).all() + ) + + logger.info(f"Recherche '{clean_query}': {len(neighborhoods)} résultats") + return neighborhoods + + except Exception as e: + logger.error(f"Erreur lors de la recherche textuelle '{query_text}': {e}") + raise ServiceError(f"Impossible d'effectuer la recherche: {str(e)}") + + @staticmethod + def get_names_list( + db: Session, active_only: bool = True, sort: bool = True + ) -> List[str]: + """Récupère la liste des noms de quartiers. + + Args: + db: Session de base de données + active_only: Filtrer seulement les quartiers actifs + sort: Trier les résultats alphabétiquement + + Returns: + Liste des noms de quartiers + + Raises: + ServiceError: En cas d'erreur technique + """ + try: + logger.info( + f"Récupération des noms de quartiers (active_only: {active_only})" + ) + + query = db.query(NeighborhoodModel.neighborhood_name) + + # Note: is_active field not available in current model + # if active_only: + # query = query.filter(NeighborhoodModel.is_active == True) + + if sort: + query = query.order_by(NeighborhoodModel.neighborhood_name) + + neighborhoods = query.all() + names = [n.neighborhood_name for n in neighborhoods] + + logger.info(f"Récupérés {len(names)} noms de quartiers") + return names + + except Exception as e: + logger.error(f"Erreur lors de la récupération des noms: {e}") + raise ServiceError(f"Impossible de récupérer les noms: {str(e)}") + + @staticmethod + def get_statistics(db: Session) -> Dict[str, Any]: + """Génère des statistiques complètes sur les quartiers. + + Args: + db: Session de base de données + + Returns: + Dictionnaire avec les statistiques + + Raises: + ServiceError: En cas d'erreur technique + """ + try: + logger.info("Génération des statistiques des quartiers") + + # Comptages de base + total = db.query(NeighborhoodModel).count() + # Note: is_active field not available in current model + # active = ( + # db.query(NeighborhoodModel) + # .filter(NeighborhoodModel.is_active == True) + # .count() + # ) + active = total # Since no is_active field, assume all are active + inactive = 0 # Since no is_active field, assume no inactive ones + + # Quartiers les plus récents (si created_at existe) + recent_neighborhoods = [] + try: + recent = ( + db.query(NeighborhoodModel) + .order_by(desc(NeighborhoodModel.created_at)) + .limit(5) + .all() + ) + recent_neighborhoods = [ + { + "id": n.id, + "name": n.neighborhood_name, + "created_at": ( + n.created_at.isoformat() if n.created_at else None + ), + } + for n in recent + ] + except AttributeError: + logger.info("created_at non disponible, pas de quartiers récents") + + # Répartition alphabétique (première lettre) + alphabet_distribution = {} + try: + neighborhoods = db.query(NeighborhoodModel.neighborhood_name).all() + for n in neighborhoods: + first_letter = ( + n.neighborhood_name[0] if n.neighborhood_name else "?" + ) + alphabet_distribution[first_letter] = ( + alphabet_distribution.get(first_letter, 0) + 1 + ) + except Exception: + logger.warning("Impossible de calculer la répartition alphabétique") + + stats = { + "total_neighborhoods": total, + "active_neighborhoods": active, + "inactive_neighborhoods": inactive, + "active_percentage": round( + (active / total * 100) if total > 0 else 0, 2 + ), + "recent_neighborhoods": recent_neighborhoods, + "alphabet_distribution": dict(sorted(alphabet_distribution.items())), + "database": "PostgreSQL", + "status": "read_only", + } + + logger.info(f"Statistiques générées: {total} quartiers total") + return stats + + except Exception as e: + logger.error(f"Erreur lors de la génération des statistiques: {e}") + raise ServiceError(f"Impossible de générer les statistiques: {str(e)}") + + @staticmethod + def count( + db: Session, active_only: bool = True, filters: Optional[Dict[str, Any]] = None + ) -> int: + """Compte le nombre de quartiers selon des critères. + + Args: + db: Session de base de données + active_only: Filtrer seulement les quartiers actifs + filters: Filtres additionnels (pour extensions futures) + + Returns: + Nombre de quartiers correspondants + + Raises: + ServiceError: En cas d'erreur technique + """ + try: + logger.info(f"Comptage des quartiers (active_only: {active_only})") + + query = db.query(NeighborhoodModel) + + # Note: is_active field not available in current model + # if active_only: + # query = query.filter(NeighborhoodModel.is_active == True) + + # Filtres additionnels (extensibilité) + if filters: + for field, value in filters.items(): + if hasattr(NeighborhoodModel, field): + query = query.filter(getattr(NeighborhoodModel, field) == value) + + count = query.count() + logger.info(f"Comptage terminé: {count} quartiers") + return count + + except Exception as e: + logger.error(f"Erreur lors du comptage: {e}") + raise ServiceError(f"Impossible de compter les quartiers: {str(e)}") + + @staticmethod + def exists(db: Session, neighborhood_id: int) -> bool: + """Vérifie si un quartier existe. + + Args: + db: Session de base de données + neighborhood_id: ID du quartier à vérifier + + Returns: + True si le quartier existe, False sinon + + Raises: + ServiceError: En cas d'erreur technique + """ + try: + logger.info( + f"Vérification de l'existence du quartier ID: {neighborhood_id}" + ) + + exists = ( + db.query(NeighborhoodModel) + .filter(NeighborhoodModel.id == neighborhood_id) + .first() + is not None + ) + + logger.info(f"Quartier {neighborhood_id} existe: {exists}") + return exists + + except Exception as e: + logger.error( + f"Erreur lors de la vérification d'existence {neighborhood_id}: {e}" + ) + raise ServiceError(f"Impossible de vérifier l'existence: {str(e)}") + + @staticmethod + def validate_sort_parameters(sort_by: str, sort_order: str) -> None: + """Valide les paramètres de tri. + + Args: + sort_by: Champ de tri + sort_order: Ordre de tri + + Raises: + ValueError: Si les paramètres sont invalides + """ + valid_sort_fields = ["name", "created_at", "id"] + valid_sort_orders = ["asc", "desc"] + + if sort_by not in valid_sort_fields: + raise ValueError(f"sort_by doit être un de: {', '.join(valid_sort_fields)}") + + if sort_order.lower() not in valid_sort_orders: + raise ValueError( + f"sort_order doit être un de: {', '.join(valid_sort_orders)}" + ) + + @staticmethod + def _get_sort_field(sort_by: str): + """Retourne le champ SQLAlchemy correspondant au paramètre de tri. + + Args: + sort_by: Nom du champ de tri + + Returns: + Champ SQLAlchemy correspondant + """ + sort_mapping = { + "name": NeighborhoodModel.neighborhood_name, + "created_at": NeighborhoodModel.created_at, + "id": NeighborhoodModel.id, + } + + return sort_mapping.get(sort_by, NeighborhoodModel.neighborhood_name) + + @staticmethod + def get_health_info(db: Session) -> Dict[str, Any]: + """Informations de santé du service neighborhoods. + + Args: + db: Session de base de données + + Returns: + Dictionnaire avec les informations de santé + """ + try: + # Test simple de la connectivité + count = db.query(NeighborhoodModel).count() + + return { + "service": "neighborhoods", + "status": "healthy", + "total_neighborhoods": count, + "database_connected": True, + } + except Exception as e: + logger.error(f"Health check failed: {e}") + return { + "service": "neighborhoods", + "status": "unhealthy", + "error": str(e), + "database_connected": False, + } diff --git a/src/project5/services/property_service.py b/src/project5/services/property_service.py new file mode 100644 index 0000000000000000000000000000000000000000..ee72af8b5a4e314c3c64d2e066a34bf56fd0df53 --- /dev/null +++ b/src/project5/services/property_service.py @@ -0,0 +1,298 @@ +""" +Service métier pour la gestion des propriétés d'usage des bâtiments (lecture seule). + +Ce module implémente la couche de logique métier pour les opérations de lecture +sur les propriétés d'usage spécifiques dans le système de classification énergétique. + +Classes: + PropertyService: Service principal pour la gestion des propriétés d'usage + +Fonctionnalités: + - Récupération paginée des propriétés avec tri + - Recherche par ID avec gestion robuste des erreurs + - Validation des paramètres métier + - Logging détaillé pour traçabilité + - Gestion centralisée des exceptions + +Classification des propriétés: + Les propriétés représentent les usages spécifiques des bâtiments organisés + par catégories hiérarchiques pour améliorer la précision des prédictions ML : + + Office (Bureaux): + - Office: Bureaux standards et espaces administratifs + - Bank Branch: Agences bancaires et institutions financières + - Courthouse: Tribunaux et bâtiments judiciaires + + Retail (Commerce): + - Retail Store: Magasins de détail et boutiques + - Restaurant: Restaurants et services de restauration + - Supermarket/Grocery Store: Supermarchés et épiceries + + Residential (Résidentiel): + - Multifamily Housing: Logements collectifs et appartements + - Senior Care Community: Résidences pour personnes âgées + + Education (Éducation): + - K-12 School: Écoles primaires et secondaires + - College/University: Universités et établissements supérieurs + +Architecture hiérarchique: + Catégories -> Propriétés -> Bâtiments + Chaque propriété appartient à une catégorie et possède un model_id + unique pour l'encodage ML. + +Note: + Service en lecture seule car les propriétés sont des données de référence + définissant la taxonomie détaillée des usages de bâtiments. +""" + +import logging +from typing import List + +from sqlalchemy import asc, desc +from sqlalchemy.orm import Session + +from project5.models import Property as PropertyModel +from project5.utils.exceptions import NotFoundError, ServiceError + +logger = logging.getLogger(__name__) + + +class PropertyService: + """ + Service métier pour la gestion des propriétés d'usage des bâtiments (lecture seule). + + Cette classe implémente la logique métier pour l'accès aux données des propriétés + d'usage spécifiques dans le système de classification hiérarchique. + + Responsabilités: + - Récupération paginée des propriétés avec tri + - Recherche de propriétés individuelles par identifiant + - Validation des paramètres d'entrée + - Gestion robuste des exceptions avec logging + - Interface unifiée pour la taxonomie détaillée + + Hiérarchie de classification: + Catégories -> Propriétés -> Bâtiments + + Les propriétés forment le niveau de détail le plus fin de la classification : + + Exemples par catégorie : + + Office (category_id: office): + - Office (model_id: unique): Bureaux standards + - Bank Branch (model_id: unique): Agences bancaires + - Courthouse (model_id: unique): Tribunaux + + Retail (category_id: retail): + - Retail Store (model_id: unique): Magasins + - Restaurant (model_id: unique): Restauration + - Supermarket/Grocery Store (model_id: unique): Supermarchés + + Residential (category_id: residential): + - Multifamily Housing (model_id: unique): Logements collectifs + - Senior Care Community (model_id: unique): Résidences seniors + + Architecture: + Suit le pattern Service Layer avec accès aux données via SQLAlchemy. + Les propriétés sont des données de référence stables liées aux catégories. + + Thread Safety: + Méthodes statiques thread-safe sans état partagé. + + Performance: + Optimisé pour la lecture avec tri par clé primaire et pagination efficace. + Volume de données modéré (quelques centaines de propriétés). + + Usage dans le ML: + Les propriétés fournissent le niveau de granularité optimal pour + les prédictions énergétiques. Le model_id permet l'encodage + catégoriel direct dans le modèle ML. + + Relations: + - Appartient à une catégorie (many-to-one via category_id) + - Utilisée par plusieurs bâtiments (one-to-many) + - Référencée multiple fois par bâtiment (usage principal, secondaire, tertiaire) + + Note: + Service en lecture seule car les propriétés définissent la taxonomie + détaillée stable des usages de bâtiments. + """ + + @staticmethod + def get_all( + db: Session, + skip: int = 0, + limit: int = 100, + sort_by: str = "name", + sort_order: str = "asc", + ) -> List[PropertyModel]: + """ + Récupère toutes les propriétés d'usage avec options de pagination et tri. + + Cette méthode implémente la récupération paginée des propriétés d'usage + spécifiques des bâtiments. Optimisée pour les interfaces de sélection + et la navigation dans la taxonomie détaillée. + + Args: + db (Session): Session SQLAlchemy active pour l'accès à la base de données + Doit être une session valide et connectée + skip (int, optional): Nombre d'éléments à ignorer pour la pagination. + Defaults to 0. Doit être >= 0 + limit (int, optional): Nombre maximum d'éléments à retourner. + Defaults to 100. Recommandé <= 500 pour les performances + sort_by (str, optional): Champ de tri. Defaults to "name". + Actuellement seul 'id' est implémenté pour la cohérence + sort_order (str, optional): Ordre de tri ('asc', 'desc'). + Defaults to "asc". Insensible à la casse + + Returns: + List[PropertyModel]: Liste des propriétés ordonnées selon les critères. + Chaque élément contient : + - id: Identifiant unique de la propriété + - model_id: Identifiant pour le modèle ML + - property_name: Nom de la propriété (ex: "Office", "Restaurant") + - category_id: Identifiant de la catégorie parente + - created_at/updated_at: Timestamps de gestion + + Raises: + ServiceError: En cas d'erreur lors de l'accès à la base de données + ou de problème technique durant la récupération + + Example: + >>> service = PropertyService() + >>> properties = service.get_all(db, skip=0, limit=20) + >>> for prop in properties: + ... print(f"{prop.property_name} (model_id: {prop.model_id})") + Office (model_id: 18) + Restaurant (model_id: 25) + Multifamily Housing (model_id: 12) + + Performance: + - Pagination SQL avec LIMIT/OFFSET pour gérer le volume + - Tri par clé primaire optimisé par index automatique + - Volume modéré (quelques centaines de propriétés) + + Usage typique: + - Listes déroulantes pour sélection d'usage de bâtiment + - Navigation dans la taxonomie détaillée + - APIs de consultation des usages disponibles + - Exports pour analyse des types d'usage + + Relations: + Chaque propriété peut être utilisée par plusieurs bâtiments + comme usage principal, secondaire ou tertiaire. + + Note: + Le volume de propriétés étant modéré, la pagination est surtout + utile pour la cohérence d'interface et les futures extensions. + """ + try: + logger.info( + f"Récupération des propriétés : skip={skip}, limit={limit}, " + f"sort={sort_by}:{sort_order}" + ) + + query = db.query(PropertyModel) + + # Tri + sort_field = "id" + if sort_order.lower() == "desc": + query = query.order_by(desc(sort_field)) + else: + query = query.order_by(asc(sort_field)) + + # Pagination + properties = query.offset(skip).limit(limit).all() + + logger.info(f"Récupérés {len(properties)} propriétés") + return properties + + except Exception as e: + logger.error(f"Erreur lors de la récupération des propriétés: {e}") + raise ServiceError(f"Impossible de récupérer les propriétés: {str(e)}") + + @staticmethod + def get_by_id(db: Session, property_id: int) -> PropertyModel: + """ + Récupère une propriété d'usage spécifique par son identifiant unique. + + Cette méthode effectue une recherche directe par clé primaire pour + récupérer une propriété d'usage avec toutes ses métadonnées. + + Args: + db (Session): Session SQLAlchemy active pour l'accès à la base de données + La session doit être valide et connectée + property_id (int): Identifiant unique de la propriété à récupérer + Doit être un entier positif correspondant à une clé primaire valide + + Returns: + PropertyModel: Instance complète du modèle contenant : + - id: Identifiant unique (clé primaire) + - model_id: Identifiant pour le modèle ML (unique) + - property_name: Nom de la propriété (ex: "Office", "Restaurant") + - category_id: Identifiant de la catégorie parente (clé étrangère) + - created_at: Date de création de l'enregistrement + - updated_at: Date de dernière modification + + Raises: + NotFoundError: Si aucune propriété n'existe avec l'ID spécifié + Le message d'erreur inclut l'ID recherché pour faciliter le debugging + ServiceError: En cas d'erreur technique lors de l'accès à la base + (problème de connexion, corruption de données, etc.) + + Example: + >>> service = PropertyService() + >>> try: + ... property = service.get_by_id(db, 18) + ... print(f"Propriété: {property.property_name}") + ... print(f"Model ID: {property.model_id}") + ... print(f"Catégorie ID: {property.category_id}") + ... except NotFoundError: + ... print("Propriété non trouvée") + + Performance: + - Recherche optimisée par clé primaire (index automatique) + - Temps de réponse constant O(1) indépendant du volume + - Récupération d'un seul enregistrement + + Logging: + - INFO: Début et fin de recherche avec ID et nom de la propriété + - WARNING: Propriété non trouvée avec ID spécifié + - ERROR: Erreurs techniques avec détails + + Usage typique: + - Détails d'une propriété pour affichage + - Validation d'existence avant attribution à un bâtiment + - Récupération du model_id pour prédictions ML + - Navigation hiérarchique dans la taxonomie + - APIs de consultation détaillée + + Relations: + La propriété récupérée peut être liée à plusieurs bâtiments + comme usage principal, secondaire ou tertiaire. + + Note: + Cette méthode est optimisée pour la récupération d'une seule propriété. + Pour plusieurs propriétés, utiliser get_all() avec filtres appropriés. + """ + try: + logger.info(f"Récupération de la propriété ID: {property_id}") + + property = ( + db.query(PropertyModel).filter(PropertyModel.id == property_id).first() + ) + + if property is None: + logger.warning(f"Propriété {property_id} non trouvée") + raise NotFoundError(f"Propriété avec l'ID {property_id} non trouvée") + + logger.info(f"Propriété trouvée: {property.property_name}") + return property + except NotFoundError: + raise + except Exception as e: + logger.error( + f"Erreur lors de la récupération de la propriété {property_id}: {e}" + ) + raise ServiceError(f"Impossible de récupérer la propriété: {str(e)}") diff --git a/src/project5/utils/__init__.py b/src/project5/utils/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..6742e764074d6d96003b7899884ff6e79a1c407d --- /dev/null +++ b/src/project5/utils/__init__.py @@ -0,0 +1,51 @@ +""" +Package utilitaire pour l'application de prédiction énergétique des bâtiments. + +Ce package contient les modules utilitaires essentiels pour le fonctionnement +de l'application de prédiction de consommation énergétique des bâtiments de Seattle. + +Modules disponibles: + exceptions: Hiérarchie d'exceptions personnalisées pour la gestion d'erreurs + ml_model: Classe wrapper pour la gestion des modèles de machine learning + model_registry: Registre global pour le partage d'instances de modèles ML + +Exceptions exportées: + AppException: Classe de base pour toutes les exceptions de l'application + ServiceError: Erreurs génériques des services métier + NotFoundError: Erreurs de ressources non trouvées (404) + ValidationError: Erreurs de validation des données métier + DatabaseError: Erreurs liées aux opérations de base de données + ConfigurationError: Erreurs de configuration de l'application + +Architecture: + Ce package suit le pattern Common Library en fournissant des utilitaires + réutilisables à travers toute l'application. Les exceptions suivent une + hiérarchie cohérente pour une gestion d'erreurs unifiée. + +Usage: + from project5.utils import NotFoundError, ServiceError + from project5.utils.ml_model import MLModel + from project5.utils.model_registry import get_ml_model + +Note: + Ces utilitaires sont conçus pour être thread-safe et réutilisables + dans tous les contextes de l'application (services, routes, etc.). +""" + +from .exceptions import ( + AppException, + ConfigurationError, + DatabaseError, + NotFoundError, + ServiceError, + ValidationError, +) + +__all__ = [ + "AppException", + "ServiceError", + "NotFoundError", + "ValidationError", + "DatabaseError", + "ConfigurationError", +] diff --git a/src/project5/utils/exceptions.py b/src/project5/utils/exceptions.py new file mode 100644 index 0000000000000000000000000000000000000000..c6438a4e4079f3f53d0b9c5b9b6116f4b266693c --- /dev/null +++ b/src/project5/utils/exceptions.py @@ -0,0 +1,272 @@ +""" +Hiérarchie d'exceptions personnalisées pour l'application de prédiction énergétique. + +Ce module définit une hiérarchie structurée d'exceptions pour une gestion d'erreurs +cohérente et informative à travers toute l'application. + +Classes d'exceptions: + AppException: Classe de base pour toutes les exceptions de l'application + ServiceError: Erreurs génériques des services métier + NotFoundError: Ressources non trouvées (équivalent HTTP 404) + ValidationError: Erreurs de validation des données métier + DatabaseError: Erreurs liées aux opérations de base de données + ConfigurationError: Erreurs de configuration de l'application + +Architecture: + Toutes les exceptions héritent de AppException qui fournit une structure + uniforme avec message d'erreur et code d'erreur optionnel. Cette approche + facilite la gestion centralisée des erreurs et leur mapping vers les + codes de réponse HTTP appropriés. + +Usage pattern: + try: + # Opération métier + result = service.operation() + except NotFoundError as e: + # Gestion spécifique 404 + return JSONResponse(status_code=404, content={"error": e.message}) + except ServiceError as e: + # Gestion générale des erreurs de service + return JSONResponse(status_code=500, content={"error": e.message}) + +Codes d'erreur: + Les codes d'erreur standardisés facilitent l'intégration avec les systèmes + de monitoring et le debugging. Ils suivent une nomenclature cohérente : + - SERVICE_ERROR: Erreurs génériques de logique métier + - NOT_FOUND: Ressources non trouvées + - VALIDATION_ERROR: Données invalides + - DATABASE_ERROR: Problèmes de persistance + - CONFIGURATION_ERROR: Problèmes de configuration + +Note: + Ces exceptions sont thread-safe et peuvent être utilisées dans tous + les contextes de l'application (services, routes, workers, etc.). +""" + + +class AppException(Exception): + """ + Classe de base pour toutes les exceptions de l'application. + + Cette classe fournit une structure uniforme pour toutes les exceptions + personnalisées de l'application avec support pour les messages d'erreur + structurés et les codes d'erreur standardisés. + + Attributes: + message (str): Message d'erreur descriptif pour l'utilisateur ou le développeur + error_code (str, optional): Code d'erreur standardisé pour la catégorisation + et l'intégration avec les systèmes de monitoring + + Args: + message (str): Message d'erreur détaillé expliquant la cause du problème + error_code (str, optional): Code d'erreur alphanuméririque pour classification. + Defaults to None. + + Example: + >>> try: + ... raise AppException("Erreur de traitement", "PROC_001") + ... except AppException as e: + ... print(f"Erreur {e.error_code}: {e.message}") + Erreur PROC_001: Erreur de traitement + + Note: + Cette classe sert de base à toutes les exceptions spécialisées + et ne devrait généralement pas être utilisée directement. + Préférer les sous-classes spécialisées (ServiceError, NotFoundError, etc.). + """ + + def __init__(self, message: str, error_code: str = None): + self.message = message + self.error_code = error_code + super().__init__(self.message) + + +class ServiceError(AppException): + """ + Exception pour les erreurs génériques des services métier. + + Cette exception est levée lorsqu'une erreur survient dans la logique métier + des services, incluant les problèmes de traitement, les erreurs techniques + non spécifiques, et les échecs d'opérations internes. + + Usage typique: + - Erreurs lors du traitement des données métier + - Échecs d'appels à des services externes + - Erreurs techniques non catégorisées + - Problèmes de logique métier complexe + + Args: + message (str): Description détaillée de l'erreur de service + error_code (str, optional): Code d'erreur. Defaults to "SERVICE_ERROR". + + Example: + >>> if not data_processing_successful: + ... raise ServiceError("Échec du traitement des données de prédiction") + + HTTP Mapping: + Généralement mappée vers HTTP 500 (Internal Server Error) + dans les handlers d'exception des routes FastAPI. + + Note: + Utilisée comme exception générique quand aucune exception + plus spécifique (NotFoundError, ValidationError) ne convient. + """ + + def __init__(self, message: str, error_code: str = "SERVICE_ERROR"): + super().__init__(message, error_code) + + +class NotFoundError(AppException): + """ + Exception levée quand une entité ou ressource demandée n'est pas trouvée. + + Cette exception est spécifiquement utilisée pour indiquer qu'une ressource + demandée (bâtiment, quartier, prédiction, etc.) n'existe pas dans le système. + Elle correspond directement au code de statut HTTP 404. + + Usage typique: + - Recherche d'un bâtiment par ID inexistant + - Accès à un quartier qui n'existe pas + - Tentative de récupération d'une prédiction inexistante + - Recherche dans des données de référence vides + + Args: + message (str, optional): Message décrivant ce qui n'a pas été trouvé. + Defaults to "Non trouvé". + error_code (str, optional): Code d'erreur. Defaults to "NOT_FOUND". + + Example: + >>> building = db.query(Building).filter(Building.id == 999).first() + >>> if not building: + ... raise NotFoundError(f"Bâtiment avec l'ID 999 non trouvé") + + HTTP Mapping: + Directement mappée vers HTTP 404 (Not Found) dans les routes FastAPI. + + Best Practice: + Inclure dans le message l'identifiant de la ressource recherchée + pour faciliter le debugging et améliorer l'expérience utilisateur. + """ + + def __init__( + self, + message: str = "Non trouvé", + error_code: str = "NOT_FOUND", + ): + super().__init__(message, error_code) + + +class ValidationError(AppException): + """ + Exception pour les erreurs de validation des données métier. + + Cette exception est levée lorsque des données fournies ne respectent pas + les règles de validation métier, au-delà de la validation de schéma Pydantic. + Elle couvre les validations complexes et les contraintes métier spécifiques. + + Usage typique: + - Validation de règles métier complexes + - Contraintes de cohérence entre champs + - Validation de formats spécifiques (codes postaux, coordonnées GPS) + - Vérification de contraintes de plages de valeurs métier + - Validation de dépendances entre entités + + Args: + message (str): Description détaillée de l'erreur de validation + error_code (str, optional): Code d'erreur. Defaults to "VALIDATION_ERROR". + + Example: + >>> if year_built > current_year: + ... raise ValidationError( + ... f"L'année de construction {year_built} ne peut pas être future" + ... ) + + HTTP Mapping: + Généralement mappée vers HTTP 422 (Unprocessable Entity) + ou HTTP 400 (Bad Request) selon le contexte. + + Distinction avec Pydantic: + Cette exception complète la validation Pydantic en gérant + les règles métier qui ne peuvent pas être exprimées dans les schémas. + """ + + def __init__(self, message: str, error_code: str = "VALIDATION_ERROR"): + super().__init__(message, error_code) + + +class DatabaseError(AppException): + """ + Exception pour les erreurs liées aux opérations de base de données. + + Cette exception encapsule tous les problèmes liés à la persistance des données, + incluant les erreurs de connexion, les violations de contraintes, les timeouts, + et autres problèmes de base de données. + + Usage typique: + - Erreurs de connexion à la base de données + - Violations de contraintes d'intégrité (clés étrangères, unicité) + - Timeouts lors d'opérations longues + - Erreurs de transaction (rollback, commit) + - Problèmes de configuration de la base de données + - Espace disque insuffisant + + Args: + message (str): Description de l'erreur de base de données + error_code (str, optional): Code d'erreur. Defaults to "DATABASE_ERROR". + + Example: + >>> try: + ... db.commit() + ... except SQLAlchemyError as e: + ... raise DatabaseError(f"Échec de commit de transaction: {str(e)}") + + HTTP Mapping: + Généralement mappée vers HTTP 500 (Internal Server Error) + car ces erreurs sont typiquement des problèmes d'infrastructure. + + Security Note: + Attention à ne pas exposer de détails techniques sensibles + dans les messages d'erreur retournés aux clients. + """ + + def __init__(self, message: str, error_code: str = "DATABASE_ERROR"): + super().__init__(message, error_code) + + +class ConfigurationError(AppException): + """ + Exception pour les erreurs de configuration de l'application. + + Cette exception est levée lors de problèmes de configuration de l'application, + incluant les variables d'environnement manquantes, les fichiers de configuration + invalides, et les paramètres de configuration incohérents. + + Usage typique: + - Variables d'environnement manquantes ou invalides + - Fichiers de configuration corrompus ou inaccessibles + - Paramètres de configuration avec des valeurs invalides + - Problèmes de configuration des services externes (URLs, tokens) + - Configuration de modèles ML manquante + - Paramètres de base de données incorrects + + Args: + message (str): Description du problème de configuration + error_code (str, optional): Code d'erreur. Defaults to "CONFIGURATION_ERROR". + + Example: + >>> if not os.getenv("DATABASE_URL"): + ... raise ConfigurationError( + ... "Variable d'environnement DATABASE_URL manquante" + ... ) + + HTTP Mapping: + Généralement mappée vers HTTP 500 (Internal Server Error) + car ces erreurs empêchent le fonctionnement normal de l'application. + + Best Practice: + Ces erreurs devraient idéalement être détectées au démarrage + de l'application pour un échec rapide (fail-fast pattern). + """ + + def __init__(self, message: str, error_code: str = "CONFIGURATION_ERROR"): + super().__init__(message, error_code) diff --git a/src/project5/utils/ml_model.py b/src/project5/utils/ml_model.py new file mode 100644 index 0000000000000000000000000000000000000000..08b220ab60aecb654efbd5d19d25b9267251deeb --- /dev/null +++ b/src/project5/utils/ml_model.py @@ -0,0 +1,317 @@ +""" +Gestionnaire de modèles de machine learning pour les prédictions énergétiques. + +Ce module fournit une classe wrapper générique pour la gestion des modèles de +machine learning sérialisés avec joblib, optimisée pour les modèles scikit-learn +utilisés dans les prédictions de consommation énergétique des bâtiments. + +Classes: + MLModel: Wrapper générique pour modèles ML avec chargement et prédiction + +Fonctionnalités: + - Chargement sécurisé de modèles joblib + - Introspection automatique des capacités du modèle + - Gestion des prédictions avec support des probabilités + - Validation de l'état du modèle + - Logging détaillé pour monitoring et debugging + +Modèles supportés: + - RandomForestRegressor (principal pour prédictions énergétiques) + - Tout modèle scikit-learn sérialisable avec joblib + - Modèles avec predict() et optionnellement predict_proba() + +Architecture: + Cette classe suit le pattern Adapter pour fournir une interface + unifiée aux différents types de modèles ML. Elle encapsule la + complexité du chargement et de l'introspection des modèles. + +Thread Safety: + La classe n'est pas thread-safe. Utiliser une instance par thread + ou implémenter une synchronisation externe. + +Usage: + from project5.utils.ml_model import MLModel + + model = MLModel() + if model.load_model("path/to/model.joblib"): + result = model.predict(input_data) + +Note: + Optimisé pour les modèles de régression énergétique mais suffisamment + générique pour d'autres types de modèles scikit-learn. +""" + +import logging +from typing import Any, Dict + +import joblib +import numpy as np + +# Configuration du logging +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + + +class MLModel: + """ + Wrapper générique pour la gestion de modèles de machine learning. + + Cette classe fournit une interface unifiée pour charger, valider et utiliser + des modèles de machine learning sérialisés avec joblib. Elle inclut des + capacités d'introspection automatique et de gestion d'état. + + Attributes: + model: Instance du modèle ML chargé (None si pas chargé) + model_info (dict): Métadonnées du modèle (type, capacités, classes, etc.) + + Capacités détectées automatiquement: + - Type de modèle (RandomForestRegressor, etc.) + - Présence de feature_importances_ + - Présence de coefficients (modèles linéaires) + - Support de predict_proba() (classifieurs) + - Classes disponibles (pour classification) + + États du modèle: + - Non chargé: model=None, model_info={} + - Chargé: model=instance, model_info=métadonnées + - En erreur: model=None, model_info={} (après échec de chargement) + + Thread Safety: + Non thread-safe. Chaque thread doit avoir sa propre instance + ou utiliser une synchronisation externe. + + Example: + >>> model = MLModel() + >>> if model.load_model("energy_model.joblib"): + ... print(f"Modèle chargé: {model.get_info()['model_type']}") + ... prediction = model.predict(input_features) + ... print(f"Prédiction: {prediction['prediction']}") + + Note: + Optimisé pour les modèles scikit-learn mais compatible avec + tout objet Python sérialisable ayant une méthode predict(). + """ + + def __init__(self): + """ + Initialise une nouvelle instance de MLModel. + + Crée un wrapper vide prêt à recevoir un modèle ML. + Le modèle doit être chargé explicitement avec load_model(). + """ + self.model = None + self.model_info = {} + + def get_model(self): + """ + Retourne l'instance du modèle ML chargé. + + Returns: + object: Instance du modèle ML chargé ou None si aucun modèle n'est chargé + + Example: + >>> model = MLModel() + >>> model.load_model("model.joblib") + >>> ml_instance = model.get_model() + >>> if ml_instance: + ... # Utilisation directe du modèle + ... raw_prediction = ml_instance.predict(data) + + Note: + Accès direct au modèle pour des opérations avancées. + Pour un usage normal, préférer la méthode predict() de cette classe. + """ + return self.model + + def load_model(self, model_path: str) -> bool: + """ + Charge un modèle ML sérialisé avec joblib depuis un fichier. + + Cette méthode charge le modèle et effectue automatiquement une introspection + pour déterminer ses capacités (feature importances, probabilités, etc.). + + Args: + model_path (str): Chemin vers le fichier du modèle sérialisé + Doit être un fichier .joblib ou .pkl valide + + Returns: + bool: True si le chargement a réussi, False sinon + + Side Effects: + - Met à jour self.model avec l'instance chargée + - Remplit self.model_info avec les métadonnées + - Log le succès ou l'échec du chargement + + Example: + >>> model = MLModel() + >>> success = model.load_model("models/energy_predictor.joblib") + >>> if success: + ... print("Modèle prêt à utiliser") + ... else: + ... print("Erreur de chargement") + + Métadonnées collectées: + - model_type: Nom de la classe du modèle + - model_module: Module Python d'origine + - has_feature_importances: Booléen pour feature_importances_ + - has_coefficients: Booléen pour coef_ (modèles linéaires) + - has_predict_proba: Booléen pour predict_proba() + - classes: Liste des classes (si disponible) + + Error Handling: + Capture toutes les exceptions lors du chargement et retourne False. + Les erreurs sont loggées avec détails pour le debugging. + + Note: + Le modèle précédent est écrasé même en cas d'échec de chargement. + """ + try: + with open(model_path, "rb") as f: + self.model = joblib.load(f) + + # Stockage des informations du modèle + self.model_info = { + "model_type": type(self.model).__name__, + "model_module": type(self.model).__module__, + "has_feature_importances": hasattr(self.model, "feature_importances_"), + "has_coefficients": hasattr(self.model, "coef_"), + "has_predict_proba": hasattr(self.model, "predict_proba"), + } + + if hasattr(self.model, "classes_"): + self.model_info["classes"] = self.model.classes_.tolist() + + logger.info(f"✅ Modèle {self.model_info['model_type']} chargé avec succès") + return True + + except Exception as e: + logger.error(f"❌ Erreur lors du chargement du modèle: {e}") + return False + + def is_loaded(self) -> bool: + """ + Vérifie si un modèle ML est actuellement chargé en mémoire. + + Returns: + bool: True si un modèle est chargé et prêt à utiliser, False sinon + + Example: + >>> model = MLModel() + >>> print(model.is_loaded()) # False + >>> model.load_model("model.joblib") + >>> print(model.is_loaded()) # True (si chargement réussi) + + Usage: + Utilisé pour valider l'état avant d'effectuer des prédictions + ou pour implmenter des patterns de chargement paresseux. + + Note: + Simple vérification que self.model n'est pas None. + Ne valide pas que le modèle est fonctionnel. + """ + return self.model is not None + + def predict(self, input_data) -> Dict[str, Any]: + """ + Effectue une prédiction avec le modèle chargé. + + Cette méthode utilise le modèle pour générer des prédictions et inclut + automatiquement les probabilités si le modèle les supporte. + + Args: + input_data: Données d'entrée pour la prédiction + Format attendu dépend du modèle (array-like, DataFrame, etc.) + Pour les modèles d'énergie: array 2D avec 17 features + + Returns: + Dict[str, Any]: Dictionnaire contenant : + - prediction: Résultat de la prédiction (list ou valeur) + - probabilities: Probabilités (si disponible) + + Raises: + ValueError: Si aucun modèle n'est chargé + Exception: Toute erreur durant la prédiction (remontée telle quelle) + + Example: + >>> model = MLModel() + >>> model.load_model("energy_model.joblib") + >>> features = np.array([[1926, 1, 11, 83008, ...]]) # 17 features + >>> result = model.predict(features) + >>> print(f"Consommation prédite: {result['prediction'][0]} kBTU") + + Format des données: + - input_data: Compatible avec model.predict() (array-like) + - prediction: Convertie en liste Python (depuis numpy) + - probabilities: Convertie en liste Python (si disponible) + + Performance: + Conversion automatique numpy -> Python pour sérialisation JSON. + Pour des prédictions batch importantes, considérer l'accès direct + au modèle via get_model(). + + Note: + Pour les modèles de régression, probabilities ne sera pas inclus. + Pour les classificateurs, probabilities contiendra les probabilités de classe. + """ + if not self.is_loaded(): + raise ValueError("❌ Modèle non chargé") + + try: + prediction = self.model.predict(input_data) + + # Conversion en list si numpy array + if isinstance(prediction, np.ndarray): + prediction = prediction.tolist() + + result = {"prediction": prediction} + + # Ajout des probabilités si disponible + if hasattr(self.model, "predict_proba"): + probabilities = self.model.predict_proba(input_data) + result["probabilities"] = probabilities.tolist() + + return result + + except Exception as e: + logger.error(f"❌ Erreur lors de la prédiction: {e}") + raise + + def get_info(self) -> Dict[str, Any]: + """ + Retourne les métadonnées et informations sur le modèle chargé. + + Returns: + Dict[str, Any]: Dictionnaire des métadonnées contenant : + - model_type: Nom de la classe du modèle (ex: "RandomForestRegressor") + - model_module: Module Python d'origine (ex: "sklearn.ensemble._forest") + - has_feature_importances: Booléen indiquant la disponibilité + - has_coefficients: Booléen pour les modèles linéaires + - has_predict_proba: Booléen pour les classificateurs + - classes: Liste des classes (si modèle de classification) + + Example: + >>> model = MLModel() + >>> model.load_model("rf_energy_model.joblib") + >>> info = model.get_info() + >>> print(f"Type: {info['model_type']}") + RandomForestRegressor + >>> print(f"Feature importances: {info['has_feature_importances']}") + True + + Usage: + - Debugging et validation du modèle + - Décisions conditionnelles sur les capacités + - Métadonnées pour logging et monitoring + - Documentation automatique des modèles + + Cas particuliers: + - Dictionnaire vide {} si aucun modèle chargé + - Certains champs peuvent être absents selon le type de modèle + - classes absent pour les modèles de régression + + Note: + Les informations sont collectées au moment du chargement + et mises en cache pour des accès rapides ultérieurs. + """ + return self.model_info + return self.model_info diff --git a/src/project5/utils/model_registry.py b/src/project5/utils/model_registry.py new file mode 100644 index 0000000000000000000000000000000000000000..28d67d6e3749620fe8e62762ee9972fc72866a9e --- /dev/null +++ b/src/project5/utils/model_registry.py @@ -0,0 +1,120 @@ +""" +Registre global pour le partage d'instances de modèles ML dans l'application. + +Ce module implémente un registre singleton simple pour partager des instances +de modèles de machine learning à travers toute l'application. Il permet d'éviter +le rechargement multiple de modèles coûteux en mémoire. + +Variables globales: + ml_model: Instance globale du modèle ML (None par défaut) + +Fonctions: + set_ml_model(model): Définit le modèle global + get_ml_model(): Récupère le modèle global + +Architecture: + Pattern Singleton simple utilisant une variable de module globale. + Approprié pour une application monolithe avec un seul modèle principal. + +Thread Safety: + Non thread-safe. Les opérations de lecture/écriture ne sont pas synchronisées. + Utilisation recommandée : initialisation au démarrage, lecture en mode multi-thread. + +Usage pattern recommandé: + 1. Au démarrage de l'application : + from project5.utils.ml_model import MLModel + from project5.utils.model_registry import set_ml_model + + model = MLModel() + model.load_model("path/to/model.joblib") + set_ml_model(model) + + 2. Dans les services : + from project5.utils.model_registry import get_ml_model + + model = get_ml_model() + if model and model.is_loaded(): + prediction = model.predict(data) + +Limitations: + - Un seul modèle global (pas de registry multi-modèles) + - Pas de gestion automatique du cycle de vie + - Pas de thread safety + - Pas de validation des types + +Note: + Pour des besoins plus complexes (multi-modèles, thread safety), + considérer une implémentation plus avancée avec classe Registry. +""" + +# Instance globale du modèle ML partagée dans l'application +ml_model = None + + +def set_ml_model(model): + """ + Définit l'instance globale du modèle ML. + + Cette fonction met à jour la variable globale ml_model avec une nouvelle + instance de modèle. Elle est typiquement appelée une fois au démarrage + de l'application. + + Args: + model: Instance du modèle ML à enregistrer globalement + Peut être une instance de MLModel ou tout autre objet modèle + Peut être None pour réinitialiser le registre + + Side Effects: + Met à jour la variable globale ml_model + + Example: + >>> from project5.utils.ml_model import MLModel + >>> model = MLModel() + >>> model.load_model("energy_model.joblib") + >>> set_ml_model(model) + + Thread Safety: + Non thread-safe. Éviter les appels concurrents. + Recommandation : appeler uniquement au démarrage. + + Note: + Aucune validation n'est effectuée sur le paramètre model. + L'appelant est responsable de fournir un objet valide. + """ + global ml_model + ml_model = model + + +def get_ml_model(): + """ + Récupère l'instance globale du modèle ML. + + Cette fonction retourne l'instance de modèle actuellement enregistrée + dans le registre global. Elle peut être appelée depuis n'importe où + dans l'application. + + Returns: + object: Instance du modèle ML enregistré ou None si aucun modèle + n'est enregistré + + Example: + >>> model = get_ml_model() + >>> if model and model.is_loaded(): + ... result = model.predict(input_data) + ... else: + ... print("Aucun modèle disponible") + + Thread Safety: + Lecture thread-safe en pratique (lecture d'une référence). + Cependant, pas de garantie si modification concurrente. + + Usage patterns: + - Vérification d'existence : if get_ml_model(): + - Utilisation directe : get_ml_model().predict(data) + - Assignation locale : model = get_ml_model() + + Note: + Retourne None si aucun modèle n'a été enregistré avec set_ml_model(). + L'appelant doit vérifier que le retour n'est pas None. + """ + return ml_model diff --git a/test.db b/test.db new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391