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
+
+
+
+
+
+
+
+
+
+
Informations Générales
+
+
+
Type de Modèle
+
RandomForestRegressor
+
+
+
+
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²)
+
+
+
+
+
+
+
+
+
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
+
+
+
+
+ | Variable |
+ Importance |
+ Pourcentage |
+ Visualisation |
+
+
+
+
+
+ | property_gfa_total |
+ 0.4159 |
+ 41.59% |
+
+
+ |
+
+
+
+ | primary_property_type_id |
+ 0.1205 |
+ 12.05% |
+
+
+ |
+
+
+
+ | largest_property_use_type_id |
+ 0.0907 |
+ 9.07% |
+
+
+ |
+
+
+
+ | year_built |
+ 0.0760 |
+ 7.60% |
+
+
+ |
+
+
+
+ | second_largest_property_use_type_gfa |
+ 0.0665 |
+ 6.65% |
+
+
+ |
+
+
+
+ | number_of_floors |
+ 0.0649 |
+ 6.49% |
+
+
+ |
+
+
+
+ | natural_gas |
+ 0.0444 |
+ 4.44% |
+
+
+ |
+
+
+
+ | neighborhood_id |
+ 0.0368 |
+ 3.68% |
+
+
+ |
+
+
+
+ | property_gfa_parking |
+ 0.0209 |
+ 2.09% |
+
+
+ |
+
+
+
+ | second_largest_property_use_type_id |
+ 0.0206 |
+ 2.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