GitHub Action commited on
Commit
66a0674
·
0 Parent(s):

Deploy to HuggingFace Spaces from main branch

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .env.example +188 -0
  2. .flake8 +18 -0
  3. .gitattributes +37 -0
  4. .github/workflows/cicd.yml +197 -0
  5. .github/workflows/doc.yml +53 -0
  6. .gitignore +685 -0
  7. .vscode/launch.json +13 -0
  8. .vscode/settings.json +48 -0
  9. .vscode/tasks.json +24 -0
  10. Dockerfile +47 -0
  11. Dockerfile_app +47 -0
  12. Makefile +73 -0
  13. README.md +20 -0
  14. alembic.ini +116 -0
  15. alembic/README +1 -0
  16. alembic/env.py +75 -0
  17. alembic/script.py.mako +26 -0
  18. init_db.py +746 -0
  19. model/Makefile +178 -0
  20. model/README.md +205 -0
  21. model/model.pkl +3 -0
  22. model/model.py +130 -0
  23. model/model_info.json +32 -0
  24. model/model_report.html +449 -0
  25. poetry.lock +0 -0
  26. pyproject.toml +168 -0
  27. src/project5/__init__.py +10 -0
  28. src/project5/alembic.ini +72 -0
  29. src/project5/config.py +63 -0
  30. src/project5/database.py +339 -0
  31. src/project5/main.py +605 -0
  32. src/project5/models/README.md +2 -0
  33. src/project5/models/__init__.py +41 -0
  34. src/project5/models/base.py +26 -0
  35. src/project5/models/building_energy_prediction.py +88 -0
  36. src/project5/models/building_models.py +261 -0
  37. src/project5/models/building_type.py +95 -0
  38. src/project5/models/categorie.py +84 -0
  39. src/project5/models/neighborhood.py +90 -0
  40. src/project5/models/property.py +119 -0
  41. src/project5/routers/__init__.py +0 -0
  42. src/project5/routers/building_energy_prediction_routes.py +334 -0
  43. src/project5/routers/building_model_routes.py +438 -0
  44. src/project5/routers/building_types_routes.py +205 -0
  45. src/project5/routers/categories_routes.py +221 -0
  46. src/project5/routers/neighborhoods_routes.py +328 -0
  47. src/project5/routers/prediction_routes.py +251 -0
  48. src/project5/routers/properties_routes.py +246 -0
  49. src/project5/schemas/__init__.py +45 -0
  50. src/project5/schemas/building_energy_prediction.py +253 -0
.env.example ADDED
@@ -0,0 +1,188 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # =============================================================================
2
+ # FICHIER DE CONFIGURATION ENVIRONNEMENT - PROJECT5
3
+ # =============================================================================
4
+ # Copiez ce fichier vers .env et modifiez les valeurs selon vos besoins
5
+ # ATTENTION: Ne commitez jamais le fichier .env (il contient des secrets)
6
+
7
+ # =============================================================================
8
+ # CONFIGURATION DE L'APPLICATION
9
+ # =============================================================================
10
+ APP_NAME=Project5 API
11
+ VERSION=0.1.0
12
+ ENVIRONMENT=development
13
+ DEBUG=true
14
+
15
+ # Host et port pour le serveur
16
+ HOST=0.0.0.0
17
+ PORT=8000
18
+
19
+ # =============================================================================
20
+ # SÉCURITÉ - CHANGEZ CES VALEURS EN PRODUCTION !
21
+ # =============================================================================
22
+ # Clé secrète pour JWT et sessions (génération: openssl rand -hex 32)
23
+ SECRET_KEY=votre-cle-secrete-super-longue-et-complexe-changez-moi-en-production-123456789abcdef
24
+
25
+ # Algorithme de hachage pour JWT
26
+ ALGORITHM=HS256
27
+
28
+ # Durée de validité des tokens (en minutes)
29
+ ACCESS_TOKEN_EXPIRE_MINUTES=30
30
+
31
+ # Clé pour le refresh token (optionnel)
32
+ REFRESH_TOKEN_EXPIRE_DAYS=7
33
+
34
+ # =============================================================================
35
+ # BASE DE DONNÉES - POSTGRESQL
36
+ # =============================================================================
37
+ # URL complète de la base de données PostgreSQL
38
+ DATABASE_URL=postgresql://project5_user:project5_password@localhost:5432/project5_db
39
+
40
+ # Paramètres séparés (utilisés par Docker Compose)
41
+ POSTGRES_HOST=localhost
42
+ POSTGRES_PORT=5432
43
+ POSTGRES_DB=project5_db
44
+ POSTGRES_USER=project5_user
45
+ POSTGRES_PASSWORD=project5_password
46
+
47
+ # Configuration SQLAlchemy
48
+ SQLALCHEMY_ECHO=false
49
+ SQLALCHEMY_POOL_SIZE=5
50
+ SQLALCHEMY_MAX_OVERFLOW=10
51
+ SQLALCHEMY_POOL_PRE_PING=true
52
+ SQLALCHEMY_POOL_RECYCLE=300
53
+
54
+ # =============================================================================
55
+ # BASE DE DONNÉES DE TEST - SQLITE
56
+ # =============================================================================
57
+ # Utilisée pour les tests unitaires (plus rapide que PostgreSQL)
58
+ TEST_DATABASE_URL=sqlite:///./test_project5.db
59
+
60
+ # =============================================================================
61
+ # LOGGING
62
+ # =============================================================================
63
+ LOG_LEVEL=INFO
64
+ LOG_FORMAT=%(asctime)s - %(name)s - %(levelname)s - %(message)s
65
+ LOG_FILE=logs/project5.log
66
+ LOG_MAX_SIZE=10MB
67
+ LOG_BACKUP_COUNT=5
68
+
69
+ # =============================================================================
70
+ # CORS - Configuration des domaines autorisés
71
+ # =============================================================================
72
+ # Domaines autorisés pour les requêtes CORS (séparés par des virgules)
73
+ ALLOWED_ORIGINS=http://localhost:3000,http://localhost:8080,http://127.0.0.1:3000
74
+ ALLOWED_METHODS=GET,POST,PUT,DELETE,PATCH,OPTIONS
75
+ ALLOWED_HEADERS=*
76
+
77
+ # =============================================================================
78
+ # REDIS - Cache et sessions (optionnel)
79
+ # =============================================================================
80
+ # REDIS_URL=redis://localhost:6379/0
81
+ # REDIS_HOST=localhost
82
+ # REDIS_PORT=6379
83
+ # REDIS_DB=0
84
+ # REDIS_PASSWORD=
85
+ # REDIS_EXPIRE_TIME=3600
86
+
87
+ # =============================================================================
88
+ # EMAIL - Configuration SMTP (optionnel)
89
+ # =============================================================================
90
+ # SMTP_HOST=smtp.gmail.com
91
+ # SMTP_PORT=587
92
+ # SMTP_USER=votre.email@gmail.com
93
+ # SMTP_PASSWORD=votre-mot-de-passe-app
94
+ # SMTP_TLS=true
95
+ # SMTP_FROM_EMAIL=noreply@project5.com
96
+ # SMTP_FROM_NAME=Project5 API
97
+
98
+ # =============================================================================
99
+ # STOCKAGE DE FICHIERS (optionnel)
100
+ # =============================================================================
101
+ # Répertoire local pour les uploads
102
+ # UPLOAD_DIRECTORY=uploads
103
+ # MAX_FILE_SIZE=10MB
104
+ # ALLOWED_FILE_EXTENSIONS=jpg,jpeg,png,gif,pdf,doc,docx
105
+
106
+ # Configuration AWS S3 (optionnel)
107
+ # AWS_ACCESS_KEY_ID=votre-access-key
108
+ # AWS_SECRET_ACCESS_KEY=votre-secret-key
109
+ # AWS_BUCKET_NAME=votre-bucket
110
+ # AWS_REGION=eu-west-1
111
+
112
+ # =============================================================================
113
+ # MONITORING ET OBSERVABILITÉ (optionnel)
114
+ # =============================================================================
115
+ # Sentry pour le monitoring d'erreurs
116
+ # SENTRY_DSN=https://votre-sentry-dsn@sentry.io/project-id
117
+
118
+ # Prometheus metrics
119
+ # ENABLE_METRICS=false
120
+ # METRICS_PORT=9090
121
+
122
+ # =============================================================================
123
+ # RATE LIMITING (optionnel)
124
+ # =============================================================================
125
+ # RATE_LIMIT_REQUESTS=100
126
+ # RATE_LIMIT_WINDOW=3600
127
+ # RATE_LIMIT_STORAGE=memory
128
+
129
+ # =============================================================================
130
+ # CONFIGURATION DOCKER COMPOSE
131
+ # =============================================================================
132
+ # Variables utilisées par docker-compose.yml
133
+ COMPOSE_PROJECT_NAME=project5
134
+ COMPOSE_FILE=docker-compose.yml
135
+
136
+ # Ports exposés
137
+ API_PORT=8000
138
+ DB_PORT=5432
139
+
140
+ # =============================================================================
141
+ # DÉVELOPPEMENT LOCAL
142
+ # =============================================================================
143
+ # Rechargement automatique en développement
144
+ AUTO_RELOAD=true
145
+
146
+ # Affichage détaillé des requêtes SQL
147
+ SQL_DEBUG=false
148
+
149
+ # Mode de développement pour FastAPI
150
+ FASTAPI_DEBUG=true
151
+ FASTAPI_RELOAD=true
152
+
153
+ # =============================================================================
154
+ # TESTS
155
+ # =============================================================================
156
+ # Configuration spécifique aux tests
157
+ TESTING=false
158
+ TEST_LOG_LEVEL=WARNING
159
+
160
+ # Base de données de test séparée
161
+ TEST_POSTGRES_DB=project5_test_db
162
+ TEST_POSTGRES_USER=project5_test_user
163
+ TEST_POSTGRES_PASSWORD=project5_test_password
164
+
165
+ # =============================================================================
166
+ # PRODUCTION - Variables critiques à définir
167
+ # =============================================================================
168
+ # En production, assurez-vous de définir:
169
+ # - SECRET_KEY (généré de manière sécurisée)
170
+ # - DATABASE_URL (avec les vrais credentials)
171
+ # - ALLOWED_ORIGINS (domaines de production)
172
+ # - ENVIRONMENT=production
173
+ # - DEBUG=false
174
+ # - LOG_LEVEL=WARNING ou ERROR
175
+
176
+ # =============================================================================
177
+ # NOTES D'UTILISATION
178
+ # =============================================================================
179
+ # 1. Copiez ce fichier: cp .env.example .env
180
+ # 2. Modifiez les valeurs dans .env selon vos besoins
181
+ # 3. Le fichier .env ne doit JAMAIS être commité dans Git
182
+ # 4. Utilisez des valeurs différentes pour dev/test/production
183
+ # 5. Générez des clés secrètes fortes pour la production
184
+ #
185
+ # Génération de SECRET_KEY sécurisée:
186
+ # python -c "import secrets; print(secrets.token_urlsafe(32))"
187
+ # ou
188
+ # openssl rand -base64 32
.flake8 ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [flake8]
2
+ max-line-length = 88
3
+ extend-ignore =
4
+ E203,
5
+ W503,
6
+ E402,
7
+ E501
8
+
9
+ exclude =
10
+ .git,
11
+ __pycache__,
12
+ .venv,
13
+ venv,
14
+ .jupyter,
15
+ .ipynb_checkpoints
16
+
17
+ per-file-ignores =
18
+ *.ipynb:E402,E501
.gitattributes ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Git LFS configuration for large files (e.g., models, datasets)
2
+ *.7z filter=lfs diff=lfs merge=lfs -text
3
+ *.arrow filter=lfs diff=lfs merge=lfs -text
4
+ *.bin filter=lfs diff=lfs merge=lfs -text
5
+ *.bz2 filter=lfs diff=lfs merge=lfs -text
6
+ *.ckpt filter=lfs diff=lfs merge=lfs -text
7
+ *.ftz filter=lfs diff=lfs merge=lfs -text
8
+ *.gz filter=lfs diff=lfs merge=lfs -text
9
+ *.h5 filter=lfs diff=lfs merge=lfs -text
10
+ *.joblib filter=lfs diff=lfs merge=lfs -text
11
+ *.lfs.* filter=lfs diff=lfs merge=lfs -text
12
+ *.mlmodel filter=lfs diff=lfs merge=lfs -text
13
+ *.model filter=lfs diff=lfs merge=lfs -text
14
+ *.msgpack filter=lfs diff=lfs merge=lfs -text
15
+ *.npy filter=lfs diff=lfs merge=lfs -text
16
+ *.npz filter=lfs diff=lfs merge=lfs -text
17
+ *.onnx filter=lfs diff=lfs merge=lfs -text
18
+ *.ot filter=lfs diff=lfs merge=lfs -text
19
+ *.parquet filter=lfs diff=lfs merge=lfs -text
20
+ *.pb filter=lfs diff=lfs merge=lfs -text
21
+ *.pickle filter=lfs diff=lfs merge=lfs -text
22
+ *.pkl filter=lfs diff=lfs merge=lfs -text
23
+ *.pt filter=lfs diff=lfs merge=lfs -text
24
+ *.pth filter=lfs diff=lfs merge=lfs -text
25
+ *.rar filter=lfs diff=lfs merge=lfs -text
26
+ *.safetensors filter=lfs diff=lfs merge=lfs -text
27
+ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
28
+ *.tar.* filter=lfs diff=lfs merge=lfs -text
29
+ *.tar filter=lfs diff=lfs merge=lfs -text
30
+ *.tflite filter=lfs diff=lfs merge=lfs -text
31
+ *.tgz filter=lfs diff=lfs merge=lfs -text
32
+ *.wasm filter=lfs diff=lfs merge=lfs -text
33
+ *.xz filter=lfs diff=lfs merge=lfs -text
34
+ *.zip filter=lfs diff=lfs merge=lfs -text
35
+ *.zst filter=lfs diff=lfs merge=lfs -text
36
+ *tfevents* filter=lfs diff=lfs merge=lfs -text
37
+ model/model.pkl filter=lfs diff=lfs merge=lfs -text
.github/workflows/cicd.yml ADDED
@@ -0,0 +1,197 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: Project5 CI/CD
2
+
3
+ on:
4
+ push:
5
+ branches: [ main, develop ]
6
+ pull_request:
7
+ branches: [ main, develop ]
8
+ workflow_dispatch:
9
+ inputs:
10
+ environment:
11
+ description: 'Environnement'
12
+ type: choice
13
+ options: ['dev', 'production']
14
+ jobs:
15
+ test:
16
+ runs-on: ubuntu-latest
17
+ strategy:
18
+ matrix:
19
+ python-version: ["3.11"]
20
+
21
+ steps:
22
+ - uses: actions/checkout@v4
23
+
24
+ - name: Set up Python ${{ matrix.python-version }}
25
+ uses: actions/setup-python@v4
26
+ with:
27
+ python-version: ${{ matrix.python-version }}
28
+
29
+ - name: Install Poetry
30
+ uses: snok/install-poetry@v1
31
+ with:
32
+ version: latest
33
+ virtualenvs-create: true
34
+ virtualenvs-in-project: true
35
+ installer-parallel: true
36
+
37
+ - name: Load cached venv
38
+ id: cached-poetry-dependencies
39
+ uses: actions/cache@v3
40
+ with:
41
+ path: .venv
42
+ key: venv-${{ runner.os }}-${{ matrix.python-version }}-${{ hashFiles('**/poetry.lock') }}
43
+
44
+ - name: Install dependencies
45
+ if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true'
46
+ run: poetry install --no-interaction --no-root
47
+
48
+ - name: Install project
49
+ run: poetry install --no-interaction
50
+
51
+ - name: Run tests with pytest
52
+ run: |
53
+ 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
54
+
55
+ - name: Upload coverage to Codecov
56
+ uses: codecov/codecov-action@v3
57
+ with:
58
+ token: ${{ secrets.CODECOV_TOKEN }}
59
+ file: ./coverage.xml
60
+ flags: unittests
61
+ name: codecov-umbrella
62
+
63
+ lint:
64
+ runs-on: ubuntu-latest
65
+ steps:
66
+ - uses: actions/checkout@v4
67
+
68
+ - name: Set up Python
69
+ uses: actions/setup-python@v4
70
+ with:
71
+ python-version: "3.11"
72
+
73
+ - name: Install Poetry
74
+ uses: snok/install-poetry@v1
75
+ with:
76
+ version: latest
77
+ virtualenvs-create: true
78
+ virtualenvs-in-project: true
79
+
80
+ - name: Install dependencies
81
+ run: poetry install --no-interaction
82
+
83
+ - name: Run Black
84
+ run: poetry run black --check ./src
85
+
86
+ - name: Run isort
87
+ run: poetry run isort --check-only .
88
+
89
+ - name: Run flake8
90
+ run: poetry run flake8 ./src
91
+
92
+ # - name: Run mypy
93
+ # run: poetry run mypy ./src
94
+
95
+ security:
96
+ runs-on: ubuntu-latest
97
+ steps:
98
+ - uses: actions/checkout@v4
99
+
100
+ - name: Set up Python
101
+ uses: actions/setup-python@v4
102
+ with:
103
+ python-version: "3.11"
104
+
105
+ - name: Install Poetry
106
+ uses: snok/install-poetry@v1
107
+
108
+ - name: Install dependencies
109
+ run: poetry install --no-interaction
110
+
111
+ - name: Run Bandit security linter
112
+ run: poetry run bandit -r ./src -f json
113
+
114
+ - name: Run Safety check
115
+ run: poetry run safety check --output json
116
+
117
+ deploy:
118
+ runs-on: ubuntu-latest
119
+ needs: [test, lint, security]
120
+ if: github.ref == 'refs/heads/main' && github.event.inputs.environment == 'production'
121
+ environment: production # Nécessite approbation dans Settings > Environments
122
+
123
+ steps:
124
+ - name: Setup Git LFS
125
+ run: |
126
+ git lfs install
127
+
128
+ - uses: actions/checkout@v4
129
+ with:
130
+ fetch-depth: 0
131
+ lfs: true
132
+
133
+ - name: Pull LFS files
134
+ run: git lfs pull
135
+
136
+ - name: Push to Hugging Face Space
137
+ env:
138
+ HF_TOKEN: ${{ secrets.HF_TOKEN }}
139
+ HF_SPACE_NAME: ${{ secrets.HF_SPACE_NAME }}
140
+ run: |
141
+ echo "Configuration Git pour HF"
142
+ git config --global user.email "action@github.com"
143
+ git config --global user.name "GitHub Action"
144
+
145
+ echo "Ajouter remote HF si pas déjà présent"
146
+ if ! git remote | grep -q huggingface; then
147
+ git remote add huggingface https://huggingface.co/spaces/$HF_SPACE_NAME
148
+ fi
149
+
150
+ echo "Créer une branche orpheline pour éviter l'historique"
151
+ git checkout --orphan deploy-hf
152
+
153
+ echo "Configuration Git LFS pour HuggingFace"
154
+ git lfs track "*.pkl"
155
+
156
+ echo "Copier le Dockerfile pour HF (renommer)"
157
+ cp Dockerfile_app Dockerfile
158
+ git rm -f README.md
159
+ echo "Créer README.md pour HF Spaces si absent"
160
+ if [ ! -f README.md ]; then
161
+ cat > README.md << EOF
162
+ ---
163
+ title: Building Energy Prediction API
164
+ emoji: 🏢
165
+ colorFrom: blue
166
+ colorTo: green
167
+ sdk: docker
168
+ app_port: 7860
169
+ pinned: false
170
+ license: mit
171
+ ---
172
+
173
+ # Building Energy Prediction API
174
+
175
+ API FastAPI pour la prédiction de consommation énergétique des bâtiments.
176
+
177
+ ## Fonctionnalités
178
+ - 🏢 Gestion des quartiers, bâtiments et propriétés
179
+ - 🤖 Prédictions ML avec RandomForest
180
+ - 📊 API REST complète avec documentation Swagger
181
+
182
+ EOF
183
+ fi
184
+
185
+ echo "Echo suppression des données non nécessaires en production"
186
+ git rm -f *.pdf
187
+ git rm -rf docs
188
+ git rm -rf sql
189
+ git rm -rf tests
190
+ echo "Ajouter tous les fichiers nécessaires (y compris LFS)"
191
+ git add -A
192
+ git commit -m "Deploy to HuggingFace Spaces from main branch"
193
+
194
+ echo "Push vers HF avec authentification (LFS supporté)"
195
+ git push https://oauth2:$HF_TOKEN@huggingface.co/spaces/$HF_SPACE_NAME deploy-hf:main --force
196
+
197
+
.github/workflows/doc.yml ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: Build and Deploy Documentation
2
+
3
+ on:
4
+ push:
5
+ branches: [ main ]
6
+ pull_request:
7
+ branches: [ main ]
8
+
9
+ jobs:
10
+ build-and-deploy:
11
+ runs-on: ubuntu-latest
12
+
13
+ steps:
14
+ - uses: actions/checkout@v3
15
+
16
+ - name: Set up Python
17
+ uses: actions/setup-python@v4
18
+ with:
19
+ python-version: '3.11'
20
+
21
+ - name: Install Poetry
22
+ uses: snok/install-poetry@v1
23
+ with:
24
+ version: latest
25
+ virtualenvs-create: true
26
+ virtualenvs-in-project: true
27
+
28
+ - name: Load cached venv
29
+ id: cached-poetry-dependencies
30
+ uses: actions/cache@v3
31
+ with:
32
+ path: .venv
33
+ key: venv-${{ runner.os }}-${{ steps.setup-python.outputs.python-version }}-${{ hashFiles('**/poetry.lock') }}
34
+
35
+ - name: Install dependencies
36
+ if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true'
37
+ run: poetry install --no-interaction --no-root
38
+
39
+ - name: Build Sphinx documentation
40
+ run: |
41
+ cd docs && make sphinx-html
42
+
43
+ - name: Deploy to GitHub Pages
44
+ uses: peaceiris/actions-gh-pages@v3
45
+ if: github.ref == 'refs/heads/main'
46
+ with:
47
+ personal_token: ${{ secrets.DOCS_DEPLOY_TOKEN }}
48
+ external_repository: ${{ secrets.DOCS_REPOSITORY }}
49
+ publish_dir: ./docs/build/html
50
+ publish_branch: gh-pages
51
+ user_name: 'github-actions[bot]'
52
+ user_email: 'github-actions[bot]@users.noreply.github.com'
53
+ commit_message: 'Deploy documentation from ${{ github.repository }}@${{ github.sha }}'
.gitignore ADDED
@@ -0,0 +1,685 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Created by https://www.toptal.com/developers/gitignore/api/pycharm,python,git
2
+ # Edit at https://www.toptal.com/developers/gitignore?templates=pycharm,python,git
3
+
4
+ ### Git ###
5
+ # Created by git for backups. To disable backups in Git:
6
+ # $ git config --global mergetool.keepBackup false
7
+ *.orig
8
+
9
+ # Created by git when using merge tools for conflicts
10
+ *.BACKUP.*
11
+ *.BASE.*
12
+ *.LOCAL.*
13
+ *.REMOTE.*
14
+ *_BACKUP_*.txt
15
+ *_BASE_*.txt
16
+ *_LOCAL_*.txt
17
+ *_REMOTE_*.txt
18
+
19
+ ### PyCharm ###
20
+ # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
21
+ # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
22
+
23
+ # User-specific stuff
24
+ .idea/**/workspace.xml
25
+ .idea/**/tasks.xml
26
+ .idea/**/usage.statistics.xml
27
+ .idea/**/dictionaries
28
+ .idea/**/shelf
29
+
30
+ # AWS User-specific
31
+ .idea/**/aws.xml
32
+
33
+ # Generated files
34
+ .idea/**/contentModel.xml
35
+
36
+ # Sensitive or high-churn files
37
+ .idea/**/dataSources/
38
+ .idea/**/dataSources.ids
39
+ .idea/**/dataSources.local.xml
40
+ .idea/**/sqlDataSources.xml
41
+ .idea/**/dynamic.xml
42
+ .idea/**/uiDesigner.xml
43
+ .idea/**/dbnavigator.xml
44
+
45
+ # Gradle
46
+ .idea/**/gradle.xml
47
+ .idea/**/libraries
48
+
49
+ # Gradle and Maven with auto-import
50
+ # When using Gradle or Maven with auto-import, you should exclude module files,
51
+ # since they will be recreated, and may cause churn. Uncomment if using
52
+ # auto-import.
53
+ # .idea/artifacts
54
+ # .idea/compiler.xml
55
+ # .idea/jarRepositories.xml
56
+ # .idea/modules.xml
57
+ # .idea/*.iml
58
+ # .idea/modules
59
+ # *.iml
60
+ # *.ipr
61
+
62
+ # CMake
63
+ cmake-build-*/
64
+
65
+ # Mongo Explorer plugin
66
+ .idea/**/mongoSettings.xml
67
+
68
+ # File-based project format
69
+ *.iws
70
+
71
+ # IntelliJ
72
+ out/
73
+
74
+ # mpeltonen/sbt-idea plugin
75
+ .idea_modules/
76
+
77
+ # JIRA plugin
78
+ atlassian-ide-plugin.xml
79
+
80
+ # Cursive Clojure plugin
81
+ .idea/replstate.xml
82
+
83
+ # SonarLint plugin
84
+ .idea/sonarlint/
85
+
86
+ # Crashlytics plugin (for Android Studio and IntelliJ)
87
+ com_crashlytics_export_strings.xml
88
+ crashlytics.properties
89
+ crashlytics-build.properties
90
+ fabric.properties
91
+
92
+ # Editor-based Rest Client
93
+ .idea/httpRequests
94
+
95
+ # Android studio 3.1+ serialized cache file
96
+ .idea/caches/build_file_checksums.ser
97
+
98
+ ### PyCharm Patch ###
99
+ # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721
100
+
101
+ # *.iml
102
+ # modules.xml
103
+ # .idea/misc.xml
104
+ # *.ipr
105
+
106
+ # Sonarlint plugin
107
+ # https://plugins.jetbrains.com/plugin/7973-sonarlint
108
+ .idea/**/sonarlint/
109
+
110
+ # SonarQube Plugin
111
+ # https://plugins.jetbrains.com/plugin/7238-sonarqube-community-plugin
112
+ .idea/**/sonarIssues.xml
113
+
114
+ # Markdown Navigator plugin
115
+ # https://plugins.jetbrains.com/plugin/7896-markdown-navigator-enhanced
116
+ .idea/**/markdown-navigator.xml
117
+ .idea/**/markdown-navigator-enh.xml
118
+ .idea/**/markdown-navigator/
119
+
120
+ # Cache file creation bug
121
+ # See https://youtrack.jetbrains.com/issue/JBR-2257
122
+ .idea/$CACHE_FILE$
123
+
124
+ # CodeStream plugin
125
+ # https://plugins.jetbrains.com/plugin/12206-codestream
126
+ .idea/codestream.xml
127
+
128
+ # Azure Toolkit for IntelliJ plugin
129
+ # https://plugins.jetbrains.com/plugin/8053-azure-toolkit-for-intellij
130
+ .idea/**/azureSettings.xml
131
+
132
+ ### Python ###
133
+ # Byte-compiled / optimized / DLL files
134
+ __pycache__/
135
+ *.py[cod]
136
+ *$py.class
137
+
138
+ # C extensions
139
+ *.so
140
+
141
+ # Distribution / packaging
142
+ .Python
143
+ build/
144
+ develop-eggs/
145
+ dist/
146
+ downloads/
147
+ eggs/
148
+ .eggs/
149
+ lib/
150
+ lib64/
151
+ parts/
152
+ sdist/
153
+ var/
154
+ wheels/
155
+ share/python-wheels/
156
+ *.egg-info/
157
+ .installed.cfg
158
+ *.egg
159
+ MANIFEST
160
+
161
+ # PyInstaller
162
+ # Usually these files are written by a python script from a template
163
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
164
+ *.manifest
165
+ *.spec
166
+
167
+ # Installer logs
168
+ pip-log.txt
169
+ pip-delete-this-directory.txt
170
+
171
+ # Unit test / coverage reports
172
+ htmlcov/
173
+ .tox/
174
+ .nox/
175
+ .coverage
176
+ .coverage.*
177
+ .cache
178
+ nosetests.xml
179
+ coverage.xml
180
+ *.cover
181
+ *.py,cover
182
+ .hypothesis/
183
+ .pytest_cache/
184
+ cover/
185
+
186
+ # Translations
187
+ *.mo
188
+ *.pot
189
+
190
+ # Django stuff:
191
+ *.log
192
+ local_settings.py
193
+ db.sqlite3
194
+ db.sqlite3-journal
195
+
196
+ # Flask stuff:
197
+ instance/
198
+ .webassets-cache
199
+
200
+ # Scrapy stuff:
201
+ .scrapy
202
+
203
+ # Sphinx documentation
204
+ docs/_build/
205
+
206
+ # PyBuilder
207
+ .pybuilder/
208
+ target/
209
+
210
+ # Jupyter Notebook
211
+ .ipynb_checkpoints
212
+
213
+ # IPython
214
+ profile_default/
215
+ ipython_config.py
216
+
217
+ # pyenv
218
+ # For a library or package, you might want to ignore these files since the code is
219
+ # intended to run in multiple environments; otherwise, check them in:
220
+ # .python-version
221
+
222
+ # pipenv
223
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
224
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
225
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
226
+ # install all needed dependencies.
227
+ #Pipfile.lock
228
+
229
+ # poetry
230
+ # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
231
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
232
+ # commonly ignored for libraries.
233
+ # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
234
+ #poetry.lock
235
+
236
+ # pdm
237
+ # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
238
+ #pdm.lock
239
+ # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
240
+ # in version control.
241
+ # https://pdm.fming.dev/#use-with-ide
242
+ .pdm.toml
243
+
244
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
245
+ __pypackages__/
246
+
247
+ # Celery stuff
248
+ celerybeat-schedule
249
+ celerybeat.pid
250
+
251
+ # SageMath parsed files
252
+ *.sage.py
253
+
254
+ # Environments
255
+ .env
256
+ .venv
257
+ env/
258
+ venv/
259
+ ENV/
260
+ env.bak/
261
+ venv.bak/
262
+
263
+ # Spyder project settings
264
+ .spyderproject
265
+ .spyproject
266
+
267
+ # Rope project settings
268
+ .ropeproject
269
+
270
+ # mkdocs documentation
271
+ /site
272
+
273
+ # mypy
274
+ .mypy_cache/
275
+ .dmypy.json
276
+ dmypy.json
277
+
278
+ # Pyre type checker
279
+ .pyre/
280
+
281
+ # pytype static type analyzer
282
+ .pytype/
283
+
284
+ # Cython debug symbols
285
+ cython_debug/
286
+
287
+ # PyCharm
288
+ # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
289
+ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
290
+ # and can be added to the global gitignore or merged into this file. For a more nuclear
291
+ # option (not recommended) you can uncomment the following to ignore the entire idea folder.
292
+ #.idea/
293
+
294
+ ### Python Patch ###
295
+ # Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration
296
+ poetry.toml
297
+
298
+ # ruff
299
+ .ruff_cache/
300
+
301
+ # LSP config files
302
+ pyrightconfig.json
303
+
304
+ # End of https://www.toptal.com/developers/gitignore/api/pycharm,python,git
305
+ .env
306
+
307
+ # Created by https://www.toptal.com/developers/gitignore/api/visualstudiocode
308
+ # Edit at https://www.toptal.com/developers/gitignore?templates=visualstudiocode
309
+
310
+ ### VisualStudioCode ###
311
+ .vscode/*
312
+ !.vscode/settings.json
313
+ !.vscode/tasks.json
314
+ !.vscode/launch.json
315
+ !.vscode/extensions.json
316
+ !.vscode/*.code-snippets
317
+
318
+ # Local History for Visual Studio Code
319
+ .history/
320
+
321
+ # Built Visual Studio Code Extensions
322
+ *.vsix
323
+
324
+ ### VisualStudioCode Patch ###
325
+ # Ignore all local history of files
326
+ .history
327
+ .ionide
328
+
329
+ # End of https://www.toptal.com/developers/gitignore/api/visualstudiocode
330
+ # Created by https://www.toptal.com/developers/gitignore/api/macos
331
+ # Edit at https://www.toptal.com/developers/gitignore?templates=macos
332
+
333
+ ### macOS ###
334
+ # General
335
+ .DS_Store
336
+ .AppleDouble
337
+ .LSOverride
338
+
339
+ # Icon must end with two \r
340
+ Icon
341
+
342
+
343
+ # Thumbnails
344
+ ._*
345
+
346
+ # Files that might appear in the root of a volume
347
+ .DocumentRevisions-V100
348
+ .fseventsd
349
+ .Spotlight-V100
350
+ .TemporaryItems
351
+ .Trashes
352
+ .VolumeIcon.icns
353
+ .com.apple.timemachine.donotpresent
354
+
355
+ # Directories potentially created on remote AFP share
356
+ .AppleDB
357
+ .AppleDesktop
358
+ Network Trash Folder
359
+ Temporary Items
360
+ .apdisk
361
+
362
+ ### macOS Patch ###
363
+ # iCloud generated files
364
+ *.icloud
365
+
366
+ # End of https://www.toptal.com/developers/gitignore/api/macos
367
+ # Created by https://www.toptal.com/developers/gitignore/api/latex
368
+ # Edit at https://www.toptal.com/developers/gitignore?templates=latex
369
+
370
+ ### LaTeX ###
371
+ ## Core latex/pdflatex auxiliary files:
372
+ *.aux
373
+ *.lof
374
+ *.log
375
+ *.lot
376
+ *.fls
377
+ *.out
378
+ *.toc
379
+ *.fmt
380
+ *.fot
381
+ *.cb
382
+ *.cb2
383
+ .*.lb
384
+
385
+ ## Intermediate documents:
386
+ *.dvi
387
+ *.xdv
388
+ *-converted-to.*
389
+ # these rules might exclude image files for figures etc.
390
+ # *.ps
391
+ # *.eps
392
+ # *.pdf
393
+
394
+ ## Generated if empty string is given at "Please type another file name for output:"
395
+ .pdf
396
+
397
+ ## Bibliography auxiliary files (bibtex/biblatex/biber):
398
+ *.bbl
399
+ *.bcf
400
+ *.blg
401
+ *-blx.aux
402
+ *-blx.bib
403
+ *.run.xml
404
+
405
+ ## Build tool auxiliary files:
406
+ *.fdb_latexmk
407
+ *.synctex
408
+ *.synctex(busy)
409
+ *.synctex.gz
410
+ *.synctex.gz(busy)
411
+ *.pdfsync
412
+
413
+ ## Build tool directories for auxiliary files
414
+ # latexrun
415
+ latex.out/
416
+
417
+ ## Auxiliary and intermediate files from other packages:
418
+ # algorithms
419
+ *.alg
420
+ *.loa
421
+
422
+ # achemso
423
+ acs-*.bib
424
+
425
+ # amsthm
426
+ *.thm
427
+
428
+ # beamer
429
+ *.nav
430
+ *.pre
431
+ *.snm
432
+ *.vrb
433
+
434
+ # changes
435
+ *.soc
436
+
437
+ # comment
438
+ *.cut
439
+
440
+ # cprotect
441
+ *.cpt
442
+
443
+ # elsarticle (documentclass of Elsevier journals)
444
+ *.spl
445
+
446
+ # endnotes
447
+ *.ent
448
+
449
+ # fixme
450
+ *.lox
451
+
452
+ # feynmf/feynmp
453
+ *.mf
454
+ *.mp
455
+ *.t[1-9]
456
+ *.t[1-9][0-9]
457
+ *.tfm
458
+
459
+ #(r)(e)ledmac/(r)(e)ledpar
460
+ *.end
461
+ *.?end
462
+ *.[1-9]
463
+ *.[1-9][0-9]
464
+ *.[1-9][0-9][0-9]
465
+ *.[1-9]R
466
+ *.[1-9][0-9]R
467
+ *.[1-9][0-9][0-9]R
468
+ *.eledsec[1-9]
469
+ *.eledsec[1-9]R
470
+ *.eledsec[1-9][0-9]
471
+ *.eledsec[1-9][0-9]R
472
+ *.eledsec[1-9][0-9][0-9]
473
+ *.eledsec[1-9][0-9][0-9]R
474
+
475
+ # glossaries
476
+ *.acn
477
+ *.acr
478
+ *.glg
479
+ *.glo
480
+ *.gls
481
+ *.glsdefs
482
+ *.lzo
483
+ *.lzs
484
+ *.slg
485
+ *.slo
486
+ *.sls
487
+
488
+ # uncomment this for glossaries-extra (will ignore makeindex's style files!)
489
+ # *.ist
490
+
491
+ # gnuplot
492
+ *.gnuplot
493
+ *.table
494
+
495
+ # gnuplottex
496
+ *-gnuplottex-*
497
+
498
+ # gregoriotex
499
+ *.gaux
500
+ *.glog
501
+ *.gtex
502
+
503
+ # htlatex
504
+ *.4ct
505
+ *.4tc
506
+ *.idv
507
+ *.lg
508
+ *.trc
509
+ *.xref
510
+
511
+ # hyperref
512
+ *.brf
513
+
514
+ # knitr
515
+ *-concordance.tex
516
+ # TODO Uncomment the next line if you use knitr and want to ignore its generated tikz files
517
+ # *.tikz
518
+ *-tikzDictionary
519
+
520
+ # listings
521
+ *.lol
522
+
523
+ # luatexja-ruby
524
+ *.ltjruby
525
+
526
+ # makeidx
527
+ *.idx
528
+ *.ilg
529
+ *.ind
530
+
531
+ # minitoc
532
+ *.maf
533
+ *.mlf
534
+ *.mlt
535
+ *.mtc[0-9]*
536
+ *.slf[0-9]*
537
+ *.slt[0-9]*
538
+ *.stc[0-9]*
539
+
540
+ # minted
541
+ _minted*
542
+ *.pyg
543
+
544
+ # morewrites
545
+ *.mw
546
+
547
+ # newpax
548
+ *.newpax
549
+
550
+ # nomencl
551
+ *.nlg
552
+ *.nlo
553
+ *.nls
554
+
555
+ # pax
556
+ *.pax
557
+
558
+ # pdfpcnotes
559
+ *.pdfpc
560
+
561
+ # sagetex
562
+ *.sagetex.sage
563
+ *.sagetex.py
564
+ *.sagetex.scmd
565
+
566
+ # scrwfile
567
+ *.wrt
568
+
569
+ # svg
570
+ svg-inkscape/
571
+
572
+ # sympy
573
+ *.sout
574
+ *.sympy
575
+ sympy-plots-for-*.tex/
576
+
577
+ # pdfcomment
578
+ *.upa
579
+ *.upb
580
+
581
+ # pythontex
582
+ *.pytxcode
583
+ pythontex-files-*/
584
+
585
+ # tcolorbox
586
+ *.listing
587
+
588
+ # thmtools
589
+ *.loe
590
+
591
+ # TikZ & PGF
592
+ *.dpth
593
+ *.md5
594
+ *.auxlock
595
+
596
+ # titletoc
597
+ *.ptc
598
+
599
+ # todonotes
600
+ *.tdo
601
+
602
+ # vhistory
603
+ *.hst
604
+ *.ver
605
+
606
+ # easy-todo
607
+ *.lod
608
+
609
+ # xcolor
610
+ *.xcp
611
+
612
+ # xmpincl
613
+ *.xmpi
614
+
615
+ # xindy
616
+ *.xdy
617
+
618
+ # xypic precompiled matrices and outlines
619
+ *.xyc
620
+ *.xyd
621
+
622
+ # endfloat
623
+ *.ttt
624
+ *.fff
625
+
626
+ # Latexian
627
+ TSWLatexianTemp*
628
+
629
+ ## Editors:
630
+ # WinEdt
631
+ *.bak
632
+ *.sav
633
+
634
+ # Texpad
635
+ .texpadtmp
636
+
637
+ # LyX
638
+ *.lyx~
639
+
640
+ # Kile
641
+ *.backup
642
+
643
+ # gummi
644
+ .*.swp
645
+
646
+ # KBibTeX
647
+ *~[0-9]*
648
+
649
+ # TeXnicCenter
650
+ *.tps
651
+
652
+ # auto folder when using emacs and auctex
653
+ ./auto/*
654
+ *.el
655
+
656
+ # expex forward references with \gathertags
657
+ *-tags.tex
658
+
659
+ # standalone packages
660
+ *.sta
661
+
662
+ # Makeindex log files
663
+ *.lpz
664
+
665
+ # xwatermark package
666
+ *.xwm
667
+
668
+ # REVTeX puts footnotes in the bibliography by default, unless the nofootinbib
669
+ # option is specified. Footnotes are the stored in a file with suffix Notes.bib.
670
+ # Uncomment the next line to have this generated file ignored.
671
+ #*Notes.bib
672
+
673
+ ### LaTeX Patch ###
674
+ # LIPIcs / OASIcs
675
+ *.vtc
676
+
677
+ # glossaries
678
+ *.glstex
679
+
680
+ # End of https://www.toptal.com/developers/gitignore/api/latex
681
+ .gradio
682
+ .env
683
+ sql/data/psql_data
684
+ app.db
685
+ model/backup
.vscode/launch.json ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "version": "0.2.0",
3
+ "configurations": [
4
+ {
5
+ "name": "Python: Current File",
6
+ "type": "debugpy",
7
+ "request": "launch",
8
+ "program": "${file}",
9
+ "console": "integratedTerminal",
10
+ "python": "${workspaceFolder}/.venv/bin/python"
11
+ }
12
+ ]
13
+ }
.vscode/settings.json ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "python.defaultInterpreterPath": "./venv/bin/python",
3
+ "python.terminal.activateEnvironment": true,
4
+ // Linting avec Poetry
5
+ "python.linting.enabled": true,
6
+ "python.linting.flake8Enabled": true,
7
+ "python.linting.lintOnSave": true,
8
+ "python.linting.flake8Path": "./venv/bin/flake8",
9
+ // Configuration spécifique pour Jupyter
10
+ "jupyter.linting.enabled": true,
11
+ // Formatting (si vous utilisez Black)
12
+ "python.formatting.provider": "black",
13
+ "python.formatting.blackPath": "./venv/bin/black",
14
+ "editor.formatOnSave": true,
15
+ // Désactiver autres linters
16
+ "python.linting.pylintEnabled": false,
17
+ "python.linting.pycodestyleEnabled": false,
18
+ "makefile.configureOnOpen": false,
19
+ "python.linting.flake8Args": [
20
+ "--max-line-length=88",
21
+ "--extend-ignore=E203,W503,E402"
22
+ ],
23
+ "editor.rulers": [
24
+ 88
25
+ ],
26
+ // === ACTIONS SUR SAUVEGARDE NOTEBOOKS ===
27
+ "notebook.codeActionsOnSave": {
28
+ "source.organizeImports": true, // Organise les imports
29
+ "source.fixAll": false, // Désactiver les fix auto (optionnel)
30
+ },
31
+ // === ACTIONS SUR SAUVEGARDE FICHIERS PYTHON ===
32
+ "[python]": {
33
+ "editor.codeActionsOnSave": {
34
+ "source.organizeImports": "explicit",
35
+ "source.fixAll.flake8": "explicit"
36
+ },
37
+ "editor.defaultFormatter": "ms-python.black-formatter",
38
+ "editor.formatOnSave": true
39
+ },
40
+ // === CONFIGURATION ISORT (pour organiser imports) ===
41
+ "python.sortImports.args": [
42
+ "--profile=black", // Compatible avec Black
43
+ "--line-length=88"
44
+ ],
45
+ "black-formatter.args": [
46
+ "--line-length=88"
47
+ ]
48
+ }
.vscode/tasks.json ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "version": "2.0.0",
3
+ "tasks": [
4
+ {
5
+ "label": "poetry install",
6
+ "type": "shell",
7
+ "command": "poetry",
8
+ "args": [
9
+ "install"
10
+ ],
11
+ "group": "build"
12
+ },
13
+ {
14
+ "label": "poetry run tests",
15
+ "type": "shell",
16
+ "command": "poetry",
17
+ "args": [
18
+ "run",
19
+ "pytest"
20
+ ],
21
+ "group": "test"
22
+ }
23
+ ]
24
+ }
Dockerfile ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.11-slim
2
+
3
+ # Installer Poetry et les dépendances système pour SQLite
4
+ RUN apt-get update && apt-get install -y \
5
+ sqlite3 \
6
+ libsqlite3-dev \
7
+ && rm -rf /var/lib/apt/lists/* \
8
+ && pip install poetry
9
+
10
+ # Configuration Poetry
11
+ ENV POETRY_NO_INTERACTION=1 \
12
+ POETRY_VENV_IN_PROJECT=1 \
13
+ POETRY_CACHE_DIR=/tmp/poetry_cache \
14
+ VIRTUAL_ENV=/code/.venv \
15
+ PATH="/code/.venv/bin:$PATH"
16
+
17
+ WORKDIR /code
18
+
19
+ # Copier les fichiers de dépendances
20
+ COPY pyproject.toml poetry.lock ./
21
+
22
+ # Copier le code source d'abord
23
+ COPY . .
24
+
25
+ # Configurer Poetry pour créer le venv dans le projet
26
+
27
+ RUN poetry config virtualenvs.in-project true && \
28
+ poetry install --only main --no-interaction --no-ansi
29
+
30
+ # Installer toutes les dépendances et le projet
31
+ #RUN poetry install --only=main && rm -rf $POETRY_CACHE_DIR
32
+
33
+ # Configuration pour SQLite et HuggingFace Spaces
34
+ ENV DATABASE_URL="sqlite:///:memory:" \
35
+ ENVIRONMENT="production" \
36
+ PORT=7860
37
+
38
+ ENV ML_MODEL_PATH=../model/model.pkl
39
+
40
+ # Exposer le port requis par HF Spaces
41
+ EXPOSE 7860
42
+
43
+ # Rendre le script d'initialisation exécutable
44
+ RUN chmod +x init_db.py
45
+
46
+ # Commande de démarrage avec initialisation de la base
47
+ CMD ["sh", "-c", " poetry env activate && cd src && poetry run uvicorn project5.main:app --host 0.0.0.0 --port 7860"]
Dockerfile_app ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.11-slim
2
+
3
+ # Installer Poetry et les dépendances système pour SQLite
4
+ RUN apt-get update && apt-get install -y \
5
+ sqlite3 \
6
+ libsqlite3-dev \
7
+ && rm -rf /var/lib/apt/lists/* \
8
+ && pip install poetry
9
+
10
+ # Configuration Poetry
11
+ ENV POETRY_NO_INTERACTION=1 \
12
+ POETRY_VENV_IN_PROJECT=1 \
13
+ POETRY_CACHE_DIR=/tmp/poetry_cache \
14
+ VIRTUAL_ENV=/code/.venv \
15
+ PATH="/code/.venv/bin:$PATH"
16
+
17
+ WORKDIR /code
18
+
19
+ # Copier les fichiers de dépendances
20
+ COPY pyproject.toml poetry.lock ./
21
+
22
+ # Copier le code source d'abord
23
+ COPY . .
24
+
25
+ # Configurer Poetry pour créer le venv dans le projet
26
+
27
+ RUN poetry config virtualenvs.in-project true && \
28
+ poetry install --only main --no-interaction --no-ansi
29
+
30
+ # Installer toutes les dépendances et le projet
31
+ #RUN poetry install --only=main && rm -rf $POETRY_CACHE_DIR
32
+
33
+ # Configuration pour SQLite et HuggingFace Spaces
34
+ ENV DATABASE_URL="sqlite:///:memory:" \
35
+ ENVIRONMENT="production" \
36
+ PORT=7860
37
+
38
+ ENV ML_MODEL_PATH=../model/model.pkl
39
+
40
+ # Exposer le port requis par HF Spaces
41
+ EXPOSE 7860
42
+
43
+ # Rendre le script d'initialisation exécutable
44
+ RUN chmod +x init_db.py
45
+
46
+ # Commande de démarrage avec initialisation de la base
47
+ CMD ["sh", "-c", " poetry env activate && cd src && poetry run uvicorn project5.main:app --host 0.0.0.0 --port 7860"]
Makefile ADDED
@@ -0,0 +1,73 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Makefile pour FastAPI avec Poetry et Docker
2
+ .PHONY: help build up down restart logs shell db-shell test lint format clean install migrate seed backup restore
3
+
4
+ # Variables
5
+ DOCKER_COMPOSE = docker-compose
6
+ SERVICE_API = api
7
+ SERVICE_DB = db
8
+ PYTHON_FILES = src/project5/ tests/ docs/
9
+ PROJECT_NAME = project5
10
+
11
+ # Format des images par default
12
+ FORMAT=png
13
+
14
+ # Couleurs pour l'affichage
15
+ GREEN = \033[0;32m
16
+ YELLOW = \033[0;33m
17
+ RED = \033[0;31m
18
+ NC = \033[0m
19
+
20
+ # Help - Affiche l'aide
21
+ help: ## Affiche cette aide
22
+ @echo "$(GREEN)Makefile pour $(PROJECT_NAME) - FastAPI avec Poetry$(NC)"
23
+ @echo ""
24
+ @echo "$(YELLOW)Developpement LOCAL (recommande):$(NC)"
25
+ @grep -E '^[a-zA-Z_-]*-local:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf " $(YELLOW)%-25s$(NC) %s\n", $$1, $$2}'
26
+
27
+ # Developpement local (sans Docker)
28
+ dev-local: ## Lance l'API en local avec Poetry
29
+ @echo "$(GREEN)Demarrage local avec Poetry...$(NC)"
30
+ poetry run uvicorn src.$(PROJECT_NAME).main:app --host 0.0.0.0 --port 8000 --reload
31
+
32
+ start-local: ## Alias pour dev-local
33
+ $(MAKE) dev-local
34
+
35
+ install-local: ## Installe les dependances avec Poetry
36
+ @echo "$(GREEN)Installation des dependances avec Poetry...$(NC)"
37
+ poetry install
38
+
39
+ update-local: ## Met a jour les dependances
40
+ @echo "$(GREEN)Mise a jour des dependances...$(NC)"
41
+ poetry update
42
+
43
+
44
+ # Qualite de code (local)
45
+ lint-local: ## Verifie le code avec flake8 et mypy en local
46
+ @echo "$(GREEN)Verification du code en local...$(NC)"
47
+ poetry run flake8 $(PYTHON_FILES)
48
+ poetry run mypy $(PYTHON_FILES) --ignore-missing-imports
49
+
50
+ format-local: ## Formate le code avec black et isort en local
51
+ @echo "$(GREEN)Formatage du code en local...$(NC)"
52
+ poetry run black $(PYTHON_FILES)
53
+ poetry run isort $(PYTHON_FILES)
54
+
55
+ format-check-local: ## Verifie le formatage sans modifier en local
56
+ @echo "$(GREEN)Verification du formatage en local...$(NC)"
57
+ poetry run black --check $(PYTHON_FILES)
58
+ poetry run isort --check-only $(PYTHON_FILES)
59
+
60
+ check-local: format-check-local lint-local ## Verifie le formatage et la qualite du code en local
61
+
62
+ # Tests local
63
+ test-local: ## Lance les tests en local avec Poetry
64
+ @echo "$(GREEN)Execution des tests en local...$(NC)"
65
+ poetry run pytest -v
66
+
67
+ test-cov-local: ## Lance les tests avec couverture en local
68
+ @echo "$(GREEN)Tests avec couverture en local...$(NC)"
69
+ poetry run pytest --cov=src/$(PROJECT_NAME) --cov-report=html --cov-report=term-missing
70
+
71
+
72
+ # Commande par defaut
73
+ .DEFAULT_GOAL := help
README.md ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: Building Energy Prediction API
3
+ emoji: 🏢
4
+ colorFrom: blue
5
+ colorTo: green
6
+ sdk: docker
7
+ app_port: 7860
8
+ pinned: false
9
+ license: mit
10
+ ---
11
+
12
+ # Building Energy Prediction API
13
+
14
+ API FastAPI pour la prédiction de consommation énergétique des bâtiments.
15
+
16
+ ## Fonctionnalités
17
+ - 🏢 Gestion des quartiers, bâtiments et propriétés
18
+ - 🤖 Prédictions ML avec RandomForest
19
+ - 📊 API REST complète avec documentation Swagger
20
+
alembic.ini ADDED
@@ -0,0 +1,116 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # A generic, single database configuration.
2
+
3
+ [alembic]
4
+ # path to migration scripts
5
+ script_location = alembic
6
+
7
+ # template used to generate migration file names; The default value is %%(rev)s_%%(slug)s
8
+ # Uncomment the line below if you want the files to be prepended with date and time
9
+ # see https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file
10
+ # for all available tokens
11
+ # file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s
12
+
13
+ # sys.path path, will be prepended to sys.path if present.
14
+ # defaults to the current working directory.
15
+ prepend_sys_path = .
16
+
17
+ # timezone to use when rendering the date within the migration file
18
+ # as well as the filename.
19
+ # If specified, requires the python-dateutil library that can be
20
+ # installed by adding `alembic[tz]` to the pip requirements
21
+ # string value is passed to dateutil.tz.gettz()
22
+ # leave blank for localtime
23
+ # timezone =
24
+
25
+ # max length of characters to apply to the
26
+ # "slug" field
27
+ # truncate_slug_length = 40
28
+
29
+ # set to 'true' to run the environment during
30
+ # the 'revision' command, regardless of autogenerate
31
+ # revision_environment = false
32
+
33
+ # set to 'true' to allow .pyc and .pyo files without
34
+ # a source .py file to be detected as revisions in the
35
+ # versions/ directory
36
+ # sourceless = false
37
+
38
+ # version location specification; This defaults
39
+ # to alembic/versions. When using multiple version
40
+ # directories, initial revisions must be specified with --version-path.
41
+ # The path separator used here should be the separator specified by "version_path_separator" below.
42
+ # version_locations = %(here)s/bar:%(here)s/bat:alembic/versions
43
+
44
+ # version path separator; As mentioned above, this is the character used to split
45
+ # version_locations. The default within new alembic.ini files is "os", which uses os.pathsep.
46
+ # If this key is omitted entirely, it falls back to the legacy behavior of splitting on spaces and/or commas.
47
+ # Valid values for version_path_separator are:
48
+ #
49
+ # version_path_separator = :
50
+ # version_path_separator = ;
51
+ # version_path_separator = space
52
+ version_path_separator = os # Use os.pathsep. Default configuration used for new projects.
53
+
54
+ # set to 'true' to search source files recursively
55
+ # in each "version_locations" directory
56
+ # new in Alembic version 1.10
57
+ # recursive_version_locations = false
58
+
59
+ # the output encoding used when revision files
60
+ # are written from script.py.mako
61
+ # output_encoding = utf-8
62
+
63
+ sqlalchemy.url = driver://user:pass@localhost/dbname
64
+
65
+
66
+ [post_write_hooks]
67
+ # post_write_hooks defines scripts or Python functions that are run
68
+ # on newly generated revision scripts. See the documentation for further
69
+ # detail and examples
70
+
71
+ # format using "black" - use the console_scripts runner, against the "black" entrypoint
72
+ # hooks = black
73
+ # black.type = console_scripts
74
+ # black.entrypoint = black
75
+ # black.options = -l 79 REVISION_SCRIPT_FILENAME
76
+
77
+ # lint with attempts to fix using "ruff" - use the exec runner, execute a binary
78
+ # hooks = ruff
79
+ # ruff.type = exec
80
+ # ruff.executable = %(here)s/.venv/bin/ruff
81
+ # ruff.options = --fix REVISION_SCRIPT_FILENAME
82
+
83
+ # Logging configuration
84
+ [loggers]
85
+ keys = root,sqlalchemy,alembic
86
+
87
+ [handlers]
88
+ keys = console
89
+
90
+ [formatters]
91
+ keys = generic
92
+
93
+ [logger_root]
94
+ level = WARN
95
+ handlers = console
96
+ qualname =
97
+
98
+ [logger_sqlalchemy]
99
+ level = WARN
100
+ handlers =
101
+ qualname = sqlalchemy.engine
102
+
103
+ [logger_alembic]
104
+ level = INFO
105
+ handlers =
106
+ qualname = alembic
107
+
108
+ [handler_console]
109
+ class = StreamHandler
110
+ args = (sys.stderr,)
111
+ level = NOTSET
112
+ formatter = generic
113
+
114
+ [formatter_generic]
115
+ format = %(levelname)-5.5s [%(name)s] %(message)s
116
+ datefmt = %H:%M:%S
alembic/README ADDED
@@ -0,0 +1 @@
 
 
1
+ Generic single-database configuration.
alembic/env.py ADDED
@@ -0,0 +1,75 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from logging.config import fileConfig
2
+
3
+ from sqlalchemy import engine_from_config, pool
4
+
5
+ from alembic import context
6
+
7
+ # this is the Alembic Config object, which provides
8
+ # access to the values within the .ini file in use.
9
+ config = context.config
10
+
11
+ # Interpret the config file for Python logging.
12
+ # This line sets up loggers basically.
13
+ if config.config_file_name is not None:
14
+ fileConfig(config.config_file_name)
15
+
16
+ # add your model's MetaData object here
17
+ # for 'autogenerate' support
18
+ # from myapp import mymodel
19
+ # target_metadata = mymodel.Base.metadata
20
+ target_metadata = None
21
+
22
+ # other values from the config, defined by the needs of env.py,
23
+ # can be acquired:
24
+ # my_important_option = config.get_main_option("my_important_option")
25
+ # ... etc.
26
+
27
+
28
+ def run_migrations_offline() -> None:
29
+ """Run migrations in 'offline' mode.
30
+
31
+ This configures the context with just a URL
32
+ and not an Engine, though an Engine is acceptable
33
+ here as well. By skipping the Engine creation
34
+ we don't even need a DBAPI to be available.
35
+
36
+ Calls to context.execute() here emit the given string to the
37
+ script output.
38
+
39
+ """
40
+ url = config.get_main_option("sqlalchemy.url")
41
+ context.configure(
42
+ url=url,
43
+ target_metadata=target_metadata,
44
+ literal_binds=True,
45
+ dialect_opts={"paramstyle": "named"},
46
+ )
47
+
48
+ with context.begin_transaction():
49
+ context.run_migrations()
50
+
51
+
52
+ def run_migrations_online() -> None:
53
+ """Run migrations in 'online' mode.
54
+
55
+ In this scenario we need to create an Engine
56
+ and associate a connection with the context.
57
+
58
+ """
59
+ connectable = engine_from_config(
60
+ config.get_section(config.config_ini_section, {}),
61
+ prefix="sqlalchemy.",
62
+ poolclass=pool.NullPool,
63
+ )
64
+
65
+ with connectable.connect() as connection:
66
+ context.configure(connection=connection, target_metadata=target_metadata)
67
+
68
+ with context.begin_transaction():
69
+ context.run_migrations()
70
+
71
+
72
+ if context.is_offline_mode():
73
+ run_migrations_offline()
74
+ else:
75
+ run_migrations_online()
alembic/script.py.mako ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """${message}
2
+
3
+ Revision ID: ${up_revision}
4
+ Revises: ${down_revision | comma,n}
5
+ Create Date: ${create_date}
6
+
7
+ """
8
+ from typing import Sequence, Union
9
+
10
+ from alembic import op
11
+ import sqlalchemy as sa
12
+ ${imports if imports else ""}
13
+
14
+ # revision identifiers, used by Alembic.
15
+ revision: str = ${repr(up_revision)}
16
+ down_revision: Union[str, None] = ${repr(down_revision)}
17
+ branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)}
18
+ depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)}
19
+
20
+
21
+ def upgrade() -> None:
22
+ ${upgrades if upgrades else "pass"}
23
+
24
+
25
+ def downgrade() -> None:
26
+ ${downgrades if downgrades else "pass"}
init_db.py ADDED
@@ -0,0 +1,746 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Script d'initialisation de la base de données SQLite pour HuggingFace Spaces."""
3
+
4
+ import os
5
+ import sqlite3
6
+ import sys
7
+
8
+ # Ajouter le répertoire src au path pour les imports
9
+ sys.path.insert(0, os.path.join(os.path.dirname(__file__), "src"))
10
+
11
+ # Connexion globale en mémoire partagée
12
+ _db_connection = None
13
+
14
+
15
+ def get_db_connection():
16
+ """Retourne la connexion SQLite partagée en mémoire."""
17
+ global _db_connection
18
+ if _db_connection is None:
19
+ _db_connection = sqlite3.connect(":memory:", check_same_thread=False)
20
+ return _db_connection
21
+
22
+
23
+ def init_sqlite_data():
24
+ """Initialise la base SQLite avec les données essentielles en utilisant SQLite direct."""
25
+ try:
26
+ conn = get_db_connection()
27
+ cursor = conn.cursor()
28
+
29
+ # Vérifier les tables existantes
30
+ cursor.execute("SELECT name FROM sqlite_master WHERE type='table';")
31
+ tables = cursor.fetchall()
32
+ print(f"📋 Tables existantes: {[table[0] for table in tables]}")
33
+
34
+ print("📊 Insertion des données de référence...")
35
+
36
+ # Données Neighborhoods
37
+ neighborhoods = [
38
+ (1, "UNKNOWN", -1, "2025-09-09 09:56:21", "2025-09-09 09:56:21"),
39
+ (2, "BALLARD", 0, "2025-09-09 09:56:21", "2025-09-09 09:56:21"),
40
+ (3, "CENTRAL", 1, "2025-09-09 09:56:21", "2025-09-09 09:56:21"),
41
+ (4, "DELRIDGE", 2, "2025-09-09 09:56:21", "2025-09-09 09:56:21"),
42
+ (5, "DOWNTOWN", 3, "2025-09-09 09:56:21", "2025-09-09 09:56:21"),
43
+ (6, "EAST", 4, "2025-09-09 09:56:21", "2025-09-09 09:56:21"),
44
+ (7, "GREATER DUWAMISH", 5, "2025-09-09 09:56:21", "2025-09-09 09:56:21"),
45
+ (8, "LAKE UNION", 6, "2025-09-09 09:56:21", "2025-09-09 09:56:21"),
46
+ (
47
+ 9,
48
+ "MAGNOLIA / QUEEN ANNE",
49
+ 7,
50
+ "2025-09-09 09:56:21",
51
+ "2025-09-09 09:56:21",
52
+ ),
53
+ (10, "NORTH", 8, "2025-09-09 09:56:21", "2025-09-09 09:56:21"),
54
+ (11, "NORTHEAST", 9, "2025-09-09 09:56:21", "2025-09-09 09:56:21"),
55
+ (12, "NORTHWEST", 10, "2025-09-09 09:56:21", "2025-09-09 09:56:21"),
56
+ (13, "SOUTHEAST", 11, "2025-09-09 09:56:21", "2025-09-09 09:56:21"),
57
+ (14, "SOUTHWEST", 12, "2025-09-09 09:56:21", "2025-09-09 09:56:21"),
58
+ (15, "WEST", 13, "2025-09-09 09:56:21", "2025-09-09 09:56:21"),
59
+ ]
60
+
61
+ cursor.executemany(
62
+ "INSERT OR REPLACE INTO neighborhood (id, neighborhood_name, model_id, created_at, updated_at) VALUES (?, ?, ?, ?, ?)",
63
+ neighborhoods,
64
+ )
65
+
66
+ # Données Building Types
67
+ building_types = [
68
+ (
69
+ 1,
70
+ -1,
71
+ "UNKNOWN",
72
+ "Type de bâtiment inconnu ou non spécifié",
73
+ "2025-09-09 09:56:21",
74
+ "2025-09-09 09:56:21",
75
+ ),
76
+ (
77
+ 2,
78
+ 0,
79
+ "CAMPUS",
80
+ "Campus building complex",
81
+ "2025-09-09 09:56:21",
82
+ "2025-09-09 09:56:21",
83
+ ),
84
+ (
85
+ 3,
86
+ 1,
87
+ "NONRESIDENTIAL",
88
+ "Non-residential building",
89
+ "2025-09-09 09:56:21",
90
+ "2025-09-09 09:56:21",
91
+ ),
92
+ (
93
+ 4,
94
+ 2,
95
+ "NONRESIDENTIAL COS",
96
+ "Non-residential COS type",
97
+ "2025-09-09 09:56:21",
98
+ "2025-09-09 09:56:21",
99
+ ),
100
+ (
101
+ 5,
102
+ 3,
103
+ "NONRESIDENTIAL WA",
104
+ "Non-residential WA type",
105
+ "2025-09-09 09:56:21",
106
+ "2025-09-09 09:56:21",
107
+ ),
108
+ (
109
+ 6,
110
+ 4,
111
+ "SPS-DISTRICT K-12",
112
+ "Seattle Public Schools District K-12",
113
+ "2025-09-09 09:56:21",
114
+ "2025-09-09 09:56:21",
115
+ ),
116
+ (
117
+ 7,
118
+ 5,
119
+ "Multifamily MR (5-9)",
120
+ "Multifamily Mid-Rise 5-9 units",
121
+ "2025-09-09 09:56:21",
122
+ "2025-09-09 09:56:21",
123
+ ),
124
+ (
125
+ 8,
126
+ 6,
127
+ "Multifamily HR (10+)",
128
+ "Multifamily High-Rise 10+ units",
129
+ "2025-09-09 09:56:21",
130
+ "2025-09-09 09:56:21",
131
+ ),
132
+ (
133
+ 9,
134
+ 7,
135
+ "Multifamily LR (2-4)",
136
+ "Multifamily Low-Rise 2-4 units",
137
+ "2025-09-09 09:56:21",
138
+ "2025-09-09 09:56:21",
139
+ ),
140
+ ]
141
+
142
+ cursor.executemany(
143
+ "INSERT OR REPLACE INTO building_type (id, model_id, building_type_name, description, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)",
144
+ building_types,
145
+ )
146
+
147
+ # Données Categories
148
+ categories = [
149
+ (
150
+ 1,
151
+ "UNKNOWN",
152
+ "UNKNOWN",
153
+ "Catégorie inconnue ou non spécifiée",
154
+ "2025-09-09 09:56:21",
155
+ "2025-09-09 09:56:21",
156
+ ),
157
+ (
158
+ 2,
159
+ "CAMPUS",
160
+ "Campus",
161
+ "Complexes de campus et installations multiples",
162
+ "2025-09-09 09:56:21",
163
+ "2025-09-09 09:56:21",
164
+ ),
165
+ (
166
+ 3,
167
+ "EDUCATION",
168
+ "Éducation",
169
+ "Établissements denseignement et de formation",
170
+ "2025-09-09 09:56:21",
171
+ "2025-09-09 09:56:21",
172
+ ),
173
+ (
174
+ 4,
175
+ "ENTERTAINMENT",
176
+ "Divertissement",
177
+ "Théâtres, cinémas et espaces de divertissement",
178
+ "2025-09-09 09:56:21",
179
+ "2025-09-09 09:56:21",
180
+ ),
181
+ (
182
+ 5,
183
+ "FINANCIAL",
184
+ "Services financiers",
185
+ "Banques, bureaux financiers et services monétaires",
186
+ "2025-09-09 09:56:21",
187
+ "2025-09-09 09:56:21",
188
+ ),
189
+ (
190
+ 6,
191
+ "HEALTHCARE",
192
+ "Santé",
193
+ "Hôpitaux, cliniques et établissements de soins médicaux",
194
+ "2025-09-09 09:56:21",
195
+ "2025-09-09 09:56:21",
196
+ ),
197
+ (
198
+ 7,
199
+ "INDUSTRIAL",
200
+ "Industrie",
201
+ "Usines et installations industrielles",
202
+ "2025-09-09 09:56:21",
203
+ "2025-09-09 09:56:21",
204
+ ),
205
+ (
206
+ 8,
207
+ "LODGING",
208
+ "Hébergement",
209
+ "Hôtels et logements temporaires",
210
+ "2025-09-09 09:56:21",
211
+ "2025-09-09 09:56:21",
212
+ ),
213
+ (
214
+ 9,
215
+ "MIXED",
216
+ "Usage mixte",
217
+ "Propriétés à usage multiple",
218
+ "2025-09-09 09:56:21",
219
+ "2025-09-09 09:56:21",
220
+ ),
221
+ (
222
+ 10,
223
+ "NONE",
224
+ "Aucun",
225
+ "Aucune utilisation spécifique",
226
+ "2025-09-09 09:56:21",
227
+ "2025-09-09 09:56:21",
228
+ ),
229
+ (
230
+ 11,
231
+ "OFFICE",
232
+ "Bureaux",
233
+ "Espaces de bureaux et administratifs",
234
+ "2025-09-09 09:56:21",
235
+ "2025-09-09 09:56:21",
236
+ ),
237
+ (
238
+ 12,
239
+ "PARKING",
240
+ "Stationnement",
241
+ "Structures et espaces de stationnement",
242
+ "2025-09-09 09:56:21",
243
+ "2025-09-09 09:56:21",
244
+ ),
245
+ (
246
+ 13,
247
+ "PUBLIC",
248
+ "Services publics",
249
+ "Services gouvernementaux et publics",
250
+ "2025-09-09 09:56:21",
251
+ "2025-09-09 09:56:21",
252
+ ),
253
+ (
254
+ 14,
255
+ "RECREATION",
256
+ "Loisirs",
257
+ "Installations sportives et récréatives",
258
+ "2025-09-09 09:56:21",
259
+ "2025-09-09 09:56:21",
260
+ ),
261
+ (
262
+ 15,
263
+ "RELIGIOUS",
264
+ "Religieux",
265
+ "Églises et lieux de culte",
266
+ "2025-09-09 09:56:21",
267
+ "2025-09-09 09:56:21",
268
+ ),
269
+ (
270
+ 16,
271
+ "RESIDENTIAL",
272
+ "Résidentiel",
273
+ "Logements et habitations",
274
+ "2025-09-09 09:56:21",
275
+ "2025-09-09 09:56:21",
276
+ ),
277
+ (
278
+ 17,
279
+ "RESTAURANT",
280
+ "Restauration",
281
+ "Restaurants et services alimentaires",
282
+ "2025-09-09 09:56:21",
283
+ "2025-09-09 09:56:21",
284
+ ),
285
+ (
286
+ 18,
287
+ "RETAIL",
288
+ "Commerce de détail",
289
+ "Magasins et commerces",
290
+ "2025-09-09 09:56:21",
291
+ "2025-09-09 09:56:21",
292
+ ),
293
+ (
294
+ 19,
295
+ "SOCIAL",
296
+ "Social",
297
+ "Clubs et espaces sociaux",
298
+ "2025-09-09 09:56:21",
299
+ "2025-09-09 09:56:21",
300
+ ),
301
+ (
302
+ 20,
303
+ "STORE",
304
+ "Magasins",
305
+ "Commerces et magasins divers",
306
+ "2025-09-09 09:56:21",
307
+ "2025-09-09 09:56:21",
308
+ ),
309
+ ]
310
+
311
+ cursor.executemany(
312
+ "INSERT OR REPLACE INTO categories (id, category_code, category_name, description, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)",
313
+ categories,
314
+ )
315
+
316
+ # Données Properties (sélection des principales)
317
+ properties = [
318
+ (1, -1, "UNKNOWN", 1, "2025-09-09 09:56:21", "2025-09-09 09:56:21"),
319
+ (2, 0, "ADULT EDUCATION", 3, "2025-09-09 09:56:21", "2025-09-09 09:56:21"),
320
+ (
321
+ 3,
322
+ 1,
323
+ "AUTOMOBILE DEALERSHIP",
324
+ 20,
325
+ "2025-09-09 09:56:21",
326
+ "2025-09-09 09:56:21",
327
+ ),
328
+ (4, 2, "BANK BRANCH", 5, "2025-09-09 09:56:21", "2025-09-09 09:56:21"),
329
+ (5, 3, "BAR/NIGHTCLUB", 19, "2025-09-09 09:56:21", "2025-09-09 09:56:21"),
330
+ (
331
+ 6,
332
+ 4,
333
+ "COLLEGE/UNIVERSITY",
334
+ 3,
335
+ "2025-09-09 09:56:21",
336
+ "2025-09-09 09:56:21",
337
+ ),
338
+ (
339
+ 7,
340
+ 5,
341
+ "CONVENIENCE STORE WITHOUT GAS STATION",
342
+ 20,
343
+ "2025-09-09 09:56:21",
344
+ "2025-09-09 09:56:21",
345
+ ),
346
+ (8, 6, "COURTHOUSE", 15, "2025-09-09 09:56:21", "2025-09-09 09:56:21"),
347
+ (9, 7, "DATA CENTER", 11, "2025-09-09 09:56:21", "2025-09-09 09:56:21"),
348
+ (
349
+ 10,
350
+ 8,
351
+ "DISTRIBUTION CENTER",
352
+ 7,
353
+ "2025-09-09 09:56:21",
354
+ "2025-09-09 09:56:21",
355
+ ),
356
+ (
357
+ 11,
358
+ 9,
359
+ "FINANCIAL OFFICE",
360
+ 5,
361
+ "2025-09-09 09:56:21",
362
+ "2025-09-09 09:56:21",
363
+ ),
364
+ (12, 10, "FOOD SALES", 17, "2025-09-09 09:56:21", "2025-09-09 09:56:21"),
365
+ (
366
+ 13,
367
+ 11,
368
+ "HOSPITAL (GENERAL MEDICAL & SURGICAL)",
369
+ 6,
370
+ "2025-09-09 09:56:21",
371
+ "2025-09-09 09:56:21",
372
+ ),
373
+ (14, 12, "HOTEL", 8, "2025-09-09 09:56:21", "2025-09-09 09:56:21"),
374
+ (15, 13, "K-12 SCHOOL", 3, "2025-09-09 09:56:21", "2025-09-09 09:56:21"),
375
+ (16, 14, "LIBRARY", 3, "2025-09-09 09:56:21", "2025-09-09 09:56:21"),
376
+ (17, 15, "MEDICAL OFFICE", 6, "2025-09-09 09:56:21", "2025-09-09 09:56:21"),
377
+ (
378
+ 18,
379
+ 16,
380
+ "MULTIFAMILY HOUSING",
381
+ 16,
382
+ "2025-09-09 09:56:21",
383
+ "2025-09-09 09:56:21",
384
+ ),
385
+ (
386
+ 19,
387
+ 17,
388
+ "MUNICIPAL WASTEWATER TREATMENT PLANT",
389
+ 7,
390
+ "2025-09-09 09:56:21",
391
+ "2025-09-09 09:56:21",
392
+ ),
393
+ (20, 18, "OFFICE", 11, "2025-09-09 09:56:21", "2025-09-09 09:56:21"),
394
+ (21, 19, "OTHER", 1, "2025-09-09 09:56:21", "2025-09-09 09:56:21"),
395
+ (22, 20, "PARKING", 12, "2025-09-09 09:56:21", "2025-09-09 09:56:21"),
396
+ (23, 21, "RESTAURANT", 17, "2025-09-09 09:56:21", "2025-09-09 09:56:21"),
397
+ (24, 22, "RETAIL STORE", 18, "2025-09-09 09:56:21", "2025-09-09 09:56:21"),
398
+ (
399
+ 25,
400
+ 23,
401
+ "SELF-STORAGE FACILITY",
402
+ 7,
403
+ "2025-09-09 09:56:21",
404
+ "2025-09-09 09:56:21",
405
+ ),
406
+ (
407
+ 26,
408
+ 24,
409
+ "SENIOR LIVING COMMUNITY",
410
+ 8,
411
+ "2025-09-09 09:56:21",
412
+ "2025-09-09 09:56:21",
413
+ ),
414
+ (
415
+ 27,
416
+ 25,
417
+ "SUPERMARKET/GROCERY STORE",
418
+ 17,
419
+ "2025-09-09 09:56:21",
420
+ "2025-09-09 09:56:21",
421
+ ),
422
+ (
423
+ 28,
424
+ 26,
425
+ "WAREHOUSE (UNREFRIGERATED)",
426
+ 7,
427
+ "2025-09-09 09:56:21",
428
+ "2025-09-09 09:56:21",
429
+ ),
430
+ (
431
+ 29,
432
+ 27,
433
+ "WORSHIP FACILITY",
434
+ 15,
435
+ "2025-09-09 09:56:21",
436
+ "2025-09-09 09:56:21",
437
+ ),
438
+ ]
439
+
440
+ cursor.executemany(
441
+ "INSERT OR REPLACE INTO property (id, model_id, property_name, category_id, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)",
442
+ properties,
443
+ )
444
+
445
+ # Données Building Models (exemples pour tester l'API)
446
+ building_models = [
447
+ (
448
+ 1,
449
+ "SEATTLE001",
450
+ "123 Main Street",
451
+ "Seattle",
452
+ "WA",
453
+ "98101",
454
+ "TAX001",
455
+ "DISTRICT1",
456
+ 47.6062,
457
+ -122.3321,
458
+ 1995,
459
+ 1,
460
+ 15,
461
+ 50000.0,
462
+ 5000.0,
463
+ 2000.0,
464
+ 500.0,
465
+ 0, # multiusage
466
+ 0, # steam
467
+ 1, # electricity
468
+ 1, # natural_gas
469
+ 2, # neighborhood_id (BALLARD)
470
+ 3, # building_type_id (NONRESIDENTIAL)
471
+ 20, # largest_property_use_type_id (OFFICE)
472
+ 20, # primary_property_type_id (OFFICE)
473
+ 1, # second_largest_property_use_type_id (UNKNOWN)
474
+ 1, # third_largest_property_use_type_id (UNKNOWN)
475
+ "2025-09-09 09:56:21",
476
+ "2025-09-09 09:56:21",
477
+ ),
478
+ (
479
+ 2,
480
+ "SEATTLE002",
481
+ "456 Pine Avenue",
482
+ "Seattle",
483
+ "WA",
484
+ "98102",
485
+ "TAX002",
486
+ "DISTRICT2",
487
+ 47.6205,
488
+ -122.3493,
489
+ 2010,
490
+ 1,
491
+ 8,
492
+ 25000.0,
493
+ 2000.0,
494
+ 0.0,
495
+ 0.0,
496
+ 0, # multiusage
497
+ 0, # steam
498
+ 1, # electricity
499
+ 1, # natural_gas
500
+ 5, # neighborhood_id (DOWNTOWN)
501
+ 7, # building_type_id (Multifamily MR 5-9)
502
+ 18, # largest_property_use_type_id (MULTIFAMILY HOUSING)
503
+ 18, # primary_property_type_id (MULTIFAMILY HOUSING)
504
+ 1, # second_largest_property_use_type_id (UNKNOWN)
505
+ 1, # third_largest_property_use_type_id (UNKNOWN)
506
+ "2025-09-09 09:56:21",
507
+ "2025-09-09 09:56:21",
508
+ ),
509
+ (
510
+ 3,
511
+ "SEATTLE003",
512
+ "789 University Way",
513
+ "Seattle",
514
+ "WA",
515
+ "98105",
516
+ "TAX003",
517
+ "DISTRICT3",
518
+ 47.6587,
519
+ -122.3128,
520
+ 1980,
521
+ 1,
522
+ 3,
523
+ 15000.0,
524
+ 1000.0,
525
+ 500.0,
526
+ 200.0,
527
+ 1, # multiusage
528
+ 0, # steam
529
+ 1, # electricity
530
+ 1, # natural_gas
531
+ 11, # neighborhood_id (NORTHEAST)
532
+ 6, # building_type_id (SPS-DISTRICT K-12)
533
+ 15, # largest_property_use_type_id (K-12 SCHOOL)
534
+ 15, # primary_property_type_id (K-12 SCHOOL)
535
+ 16, # second_largest_property_use_type_id (LIBRARY)
536
+ 1, # third_largest_property_use_type_id (UNKNOWN)
537
+ "2025-09-09 09:56:21",
538
+ "2025-09-09 09:56:21",
539
+ ),
540
+ ]
541
+
542
+ cursor.executemany(
543
+ """INSERT OR REPLACE INTO building_models (
544
+ id, ose_building_id, address, city, state, zip_code,
545
+ tax_parcel_identification_number, council_district_code,
546
+ latitude, longitude, year_built, number_of_buildings, number_of_floors,
547
+ property_gfa_total, property_gfa_parking,
548
+ second_largest_property_use_type_gfa, third_largest_property_use_type_gfa,
549
+ multiusage, steam, electricity, natural_gas,
550
+ neighborhood_id, building_type_id,
551
+ largest_property_use_type_id, primary_property_type_id,
552
+ second_largest_property_use_type_id, third_largest_property_use_type_id,
553
+ created_at, updated_at
554
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
555
+ building_models,
556
+ )
557
+
558
+ # Données Building Energy Predictions (exemples de prédictions)
559
+ energy_predictions = [
560
+ (
561
+ 1,
562
+ 1, # building_id (correspond à SEATTLE001)
563
+ 45678.5, # site_energy_use_wn_kbtu
564
+ 1, # predicted (True)
565
+ "2025-09-09 10:30:00",
566
+ ),
567
+ (
568
+ 2,
569
+ 2, # building_id (correspond à SEATTLE002)
570
+ 23456.7, # site_energy_use_wn_kbtu
571
+ 1, # predicted (True)
572
+ "2025-09-09 11:15:00",
573
+ ),
574
+ (
575
+ 3,
576
+ 3, # building_id (correspond à SEATTLE003)
577
+ 12345.3, # site_energy_use_wn_kbtu
578
+ 1, # predicted (True)
579
+ "2025-09-09 12:00:00",
580
+ ),
581
+ ]
582
+
583
+ cursor.executemany(
584
+ """INSERT OR REPLACE INTO building_energy_predictions (
585
+ id, building_id, site_energy_use_wn_kbtu, predicted, updated_at
586
+ ) VALUES (?, ?, ?, ?, ?)""",
587
+ energy_predictions,
588
+ )
589
+
590
+ conn.commit()
591
+ # Ne pas fermer la connexion - elle reste en mémoire
592
+
593
+ print(f"✅ {len(neighborhoods)} quartiers insérés")
594
+ print(f"✅ {len(building_types)} types de bâtiments insérés")
595
+ print(f"✅ {len(categories)} catégories insérées")
596
+ print(f"✅ {len(properties)} propriétés insérées")
597
+ print(f"✅ {len(building_models)} bâtiments d'exemple insérés")
598
+ print(f"✅ {len(energy_predictions)} prédictions d'exemple insérées")
599
+
600
+ except Exception as e:
601
+ print(f"❌ Erreur lors de l'insertion des données: {e}")
602
+ raise
603
+
604
+
605
+ def create_sqlite_tables():
606
+ """Crée les tables SQLite nécessaires avec raw SQL."""
607
+
608
+ try:
609
+ conn = get_db_connection()
610
+ cursor = conn.cursor()
611
+
612
+ # Créer les tables de référence principales
613
+ cursor.execute(
614
+ """
615
+ CREATE TABLE IF NOT EXISTS neighborhood (
616
+ id INTEGER PRIMARY KEY,
617
+ neighborhood_name VARCHAR(50) NOT NULL UNIQUE,
618
+ model_id INTEGER NOT NULL UNIQUE,
619
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
620
+ updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
621
+ );
622
+ """
623
+ )
624
+
625
+ cursor.execute(
626
+ """
627
+ CREATE TABLE IF NOT EXISTS building_type (
628
+ id INTEGER PRIMARY KEY,
629
+ model_id INTEGER NOT NULL UNIQUE,
630
+ building_type_name VARCHAR(100) NOT NULL UNIQUE,
631
+ description TEXT,
632
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
633
+ updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
634
+ );
635
+ """
636
+ )
637
+
638
+ cursor.execute(
639
+ """
640
+ CREATE TABLE IF NOT EXISTS categories (
641
+ id INTEGER PRIMARY KEY,
642
+ category_code VARCHAR(50) NOT NULL UNIQUE,
643
+ category_name VARCHAR(100) NOT NULL,
644
+ description TEXT,
645
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
646
+ updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
647
+ );
648
+ """
649
+ )
650
+
651
+ cursor.execute(
652
+ """
653
+ CREATE TABLE IF NOT EXISTS property (
654
+ id INTEGER PRIMARY KEY,
655
+ model_id INTEGER NOT NULL UNIQUE,
656
+ property_name VARCHAR(150) NOT NULL UNIQUE,
657
+ category_id INTEGER NOT NULL,
658
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
659
+ updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
660
+ FOREIGN KEY (category_id) REFERENCES categories (id)
661
+ );
662
+ """
663
+ )
664
+
665
+ # Créer les tables principales
666
+ cursor.execute(
667
+ """
668
+ CREATE TABLE IF NOT EXISTS building_energy_predictions (
669
+ id INTEGER PRIMARY KEY,
670
+ building_id INTEGER NOT NULL,
671
+ site_energy_use_wn_kbtu FLOAT NOT NULL,
672
+ predicted BOOLEAN DEFAULT 0,
673
+ updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
674
+ );
675
+ """
676
+ )
677
+
678
+ cursor.execute(
679
+ """
680
+ CREATE TABLE IF NOT EXISTS building_models (
681
+ id INTEGER PRIMARY KEY,
682
+ ose_building_id INTEGER NOT NULL UNIQUE,
683
+ address VARCHAR(255) NOT NULL,
684
+ city VARCHAR(100) NULL,
685
+ state VARCHAR(50) NULL,
686
+ zip_code VARCHAR(20) NULL,
687
+ tax_parcel_identification_number VARCHAR(100) NULL,
688
+ council_district_code varchar(20) NULL,
689
+ latitude FLOAT NULL,
690
+ longitude FLOAT NULL,
691
+ year_built INTEGER NULL,
692
+ number_of_buildings INTEGER NULL,
693
+ number_of_floors INTEGER NULL,
694
+ property_gfa_total FLOAT NULL,
695
+ property_gfa_parking FLOAT NULL,
696
+ second_largest_property_use_type_gfa FLOAT NULL,
697
+ third_largest_property_use_type_gfa FLOAT NULL,
698
+ multiusage BOOLEAN DEFAULT 0,
699
+ steam BOOLEAN DEFAULT 0,
700
+ electricity BOOLEAN DEFAULT 0,
701
+ natural_gas BOOLEAN DEFAULT 0,
702
+ neighborhood_id INTEGER NULL,
703
+ building_type_id INTEGER NULL,
704
+ largest_property_use_type_id INTEGER NULL,
705
+ primary_property_type_id INTEGER NULL,
706
+ second_largest_property_use_type_id INTEGER NULL,
707
+ third_largest_property_use_type_id INTEGER NULL,
708
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
709
+ updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
710
+ FOREIGN KEY (id) REFERENCES building_energy_predictions (building_id),
711
+ FOREIGN KEY (largest_property_use_type_id) REFERENCES property (id),
712
+ FOREIGN KEY (primary_property_type_id) REFERENCES property (id),
713
+ FOREIGN KEY (second_largest_property_use_type_id) REFERENCES property (id),
714
+ FOREIGN KEY (third_largest_property_use_type_id) REFERENCES property (id),
715
+ FOREIGN KEY (neighborhood_id) REFERENCES neighborhood (id),
716
+ FOREIGN KEY (building_type_id) REFERENCES building_type (id)
717
+ );
718
+ """
719
+ )
720
+
721
+ conn.commit()
722
+ # Ne pas fermer la connexion - elle reste en mémoire
723
+ print("🗄️ Structure des tables créée avec succès!")
724
+
725
+ except Exception as e:
726
+ print(f"❌ Erreur lors de la création des tables: {e}")
727
+ raise
728
+
729
+
730
+ if __name__ == "__main__":
731
+ try:
732
+ print("🚀 Initialisation de la base de données SQLite...")
733
+
734
+ # Créer les tables avec raw SQL
735
+ create_sqlite_tables()
736
+
737
+ print("📊 Insertion des données de référence...")
738
+ init_sqlite_data()
739
+ print("✅ Base de données SQLite initialisée avec succès!")
740
+
741
+ except Exception as e:
742
+ print(f"❌ Erreur lors de l'initialisation: {e}")
743
+ import traceback
744
+
745
+ traceback.print_exc()
746
+ sys.exit(1)
model/Makefile ADDED
@@ -0,0 +1,178 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Makefile pour la génération et gestion du modèle ML
2
+ .PHONY: help model train validate clean info backup restore test
3
+
4
+ # Variables
5
+ PYTHON = python
6
+ MODEL_FILE = model.pkl
7
+ MODEL_INFO = model_info.json
8
+ BACKUP_DIR = backup
9
+ TIMESTAMP = $(shell date +%Y%m%d_%H%M%S)
10
+
11
+ # Couleurs pour l'affichage
12
+ GREEN = \033[0;32m
13
+ YELLOW = \033[0;33m
14
+ RED = \033[0;31m
15
+ NC = \033[0m
16
+
17
+ # Help - Affiche l'aide
18
+ help: ## Affiche cette aide
19
+ @echo "$(GREEN)Makefile pour la gestion du modele ML$(NC)"
20
+ @echo ""
21
+ @echo "$(YELLOW)Generation du modele:$(NC)"
22
+ @echo " $(YELLOW)model $(NC) Alias pour train"
23
+ @echo " $(YELLOW)train $(NC) Genere le modele ML via model.py"
24
+ @echo " $(YELLOW)retrain $(NC) Force la regeneration du modele"
25
+ @echo " $(YELLOW)setup $(NC) Configuration et generation complete"
26
+ @echo ""
27
+ @echo "$(YELLOW)Validation et tests:$(NC)"
28
+ @echo " $(YELLOW)validate $(NC) Valide l'existence et l'integrite du modele"
29
+ @echo " $(YELLOW)test $(NC) Lance les tests de validation du modele"
30
+ @echo " $(YELLOW)test-prediction $(NC) Test une prediction simple"
31
+ @echo ""
32
+ @echo "$(YELLOW)Gestion et maintenance:$(NC)"
33
+ @echo " $(YELLOW)info $(NC) Affiche les informations detaillees du modele"
34
+ @echo " $(YELLOW)model-size $(NC) Affiche la taille des fichiers du modele"
35
+ @echo " $(YELLOW)backup $(NC) Sauvegarde le modele actuel"
36
+ @echo " $(YELLOW)restore $(NC) Restaure le dernier modele sauvegarde"
37
+ @echo " $(YELLOW)list-backups $(NC) Liste toutes les sauvegardes disponibles"
38
+ @echo " $(YELLOW)clean $(NC) Supprime les fichiers du modele actuel"
39
+ @echo " $(YELLOW)clean-all $(NC) Supprime tout (modele + sauvegardes)"
40
+ @echo ""
41
+ @echo "$(YELLOW)Environnement et verifications:$(NC)"
42
+ @echo " $(YELLOW)check-deps $(NC) Verifie les dependances Python"
43
+ @echo " $(YELLOW)check-db $(NC) Verifie la connexion a la base de donnees"
44
+
45
+ # Génération du modèle
46
+ model: train ## Alias pour train
47
+
48
+ train: ## Génère le modèle ML via model.py
49
+ @echo "$(GREEN)Generation du modele de Machine Learning...$(NC)"
50
+ @if [ ! -f "$(MODEL_FILE)" ]; then \
51
+ echo "$(YELLOW)Aucun modele existant trouve. Generation d'un nouveau modele...$(NC)"; \
52
+ else \
53
+ echo "$(YELLOW)Un modele existant a ete trouve. Sauvegarde avant regeneration...$(NC)"; \
54
+ $(MAKE) backup; \
55
+ fi
56
+ $(PYTHON) model.py
57
+ @echo "$(GREEN)✅ Modele genere avec succes!$(NC)"
58
+ @if [ -f "$(MODEL_INFO)" ]; then \
59
+ echo "$(YELLOW)Informations du modele:$(NC)"; \
60
+ cat $(MODEL_INFO) | head -10; \
61
+ fi
62
+
63
+ retrain: clean train ## Force la régénération du modèle
64
+
65
+ # Validation et tests
66
+ validate: ## Valide l'existence et l'intégrité du modèle
67
+ @echo "$(GREEN)Validation du modele...$(NC)"
68
+ @if [ ! -f "$(MODEL_FILE)" ]; then \
69
+ echo "$(RED)❌ Fichier modele manquant: $(MODEL_FILE)$(NC)"; \
70
+ exit 1; \
71
+ else \
72
+ echo "$(GREEN)✅ Fichier modele trouve: $(MODEL_FILE)$(NC)"; \
73
+ fi
74
+ @if [ ! -f "$(MODEL_INFO)" ]; then \
75
+ echo "$(RED)❌ Fichier d'information manquant: $(MODEL_INFO)$(NC)"; \
76
+ exit 1; \
77
+ else \
78
+ echo "$(GREEN)✅ Fichier d'information trouve: $(MODEL_INFO)$(NC)"; \
79
+ fi
80
+ @echo "$(GREEN)✅ Modele valide avec succes!$(NC)"
81
+
82
+ test: validate ## Lance les tests de validation du modèle
83
+ @echo "$(GREEN)Test de chargement du modele...$(NC)"
84
+ @$(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')"
85
+
86
+ test-prediction: validate ## Test une prédiction simple
87
+ @echo "$(GREEN)Test de prediction...$(NC)"
88
+ @$(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}')"
89
+
90
+ # Gestion et maintenance
91
+ info: ## Affiche les informations détaillées du modèle
92
+ @echo "$(GREEN)Informations du modele:$(NC)"
93
+ @if [ -f "$(MODEL_INFO)" ]; then \
94
+ cat $(MODEL_INFO); \
95
+ else \
96
+ echo "$(RED)❌ Fichier d'information non trouve$(NC)"; \
97
+ exit 1; \
98
+ fi
99
+
100
+ model-size: ## Affiche la taille des fichiers du modèle
101
+ @echo "$(GREEN)Taille des fichiers:$(NC)"
102
+ @if [ -f "$(MODEL_FILE)" ]; then \
103
+ ls -lh $(MODEL_FILE) | awk '{print "Modele: " $$5 " (" $$9 ")"}'; \
104
+ fi
105
+ @if [ -f "$(MODEL_INFO)" ]; then \
106
+ ls -lh $(MODEL_INFO) | awk '{print "Info: " $$5 " (" $$9 ")"}'; \
107
+ fi
108
+
109
+ backup: ## Sauvegarde le modèle actuel
110
+ @echo "$(GREEN)Sauvegarde du modele...$(NC)"
111
+ @mkdir -p $(BACKUP_DIR)
112
+ @if [ -f "$(MODEL_FILE)" ]; then \
113
+ cp $(MODEL_FILE) $(BACKUP_DIR)/model_$(TIMESTAMP).pkl; \
114
+ echo "$(GREEN)✅ Modele sauvegarde: $(BACKUP_DIR)/model_$(TIMESTAMP).pkl$(NC)"; \
115
+ fi
116
+ @if [ -f "$(MODEL_INFO)" ]; then \
117
+ cp $(MODEL_INFO) $(BACKUP_DIR)/model_info_$(TIMESTAMP).json; \
118
+ echo "$(GREEN)✅ Info sauvegardee: $(BACKUP_DIR)/model_info_$(TIMESTAMP).json$(NC)"; \
119
+ fi
120
+
121
+ restore: ## Restaure le dernier modèle sauvegardé
122
+ @echo "$(GREEN)Restauration du dernier modele...$(NC)"
123
+ @if [ ! -d "$(BACKUP_DIR)" ]; then \
124
+ echo "$(RED)❌ Aucune sauvegarde trouvee$(NC)"; \
125
+ exit 1; \
126
+ fi
127
+ @LATEST_MODEL=$$(ls -t $(BACKUP_DIR)/model_*.pkl 2>/dev/null | head -1); \
128
+ LATEST_INFO=$$(ls -t $(BACKUP_DIR)/model_info_*.json 2>/dev/null | head -1); \
129
+ if [ -n "$$LATEST_MODEL" ]; then \
130
+ cp "$$LATEST_MODEL" $(MODEL_FILE); \
131
+ echo "$(GREEN)✅ Modele restaure: $$LATEST_MODEL$(NC)"; \
132
+ fi; \
133
+ if [ -n "$$LATEST_INFO" ]; then \
134
+ cp "$$LATEST_INFO" $(MODEL_INFO); \
135
+ echo "$(GREEN)✅ Info restauree: $$LATEST_INFO$(NC)"; \
136
+ fi
137
+
138
+ list-backups: ## Liste toutes les sauvegardes disponibles
139
+ @echo "$(GREEN)Sauvegardes disponibles:$(NC)"
140
+ @if [ -d "$(BACKUP_DIR)" ]; then \
141
+ ls -la $(BACKUP_DIR)/ | grep -E '\.(pkl|json)$$' | awk '{print $$9 " (" $$5 " bytes, " $$6 " " $$7 " " $$8 ")"}' || echo "$(YELLOW)Aucune sauvegarde trouvee$(NC)"; \
142
+ else \
143
+ echo "$(YELLOW)Aucun dossier de sauvegarde$(NC)"; \
144
+ fi
145
+
146
+ clean: ## Supprime les fichiers du modèle actuel
147
+ @echo "$(GREEN)Nettoyage des fichiers du modele...$(NC)"
148
+ @if [ -f "$(MODEL_FILE)" ]; then \
149
+ rm $(MODEL_FILE); \
150
+ echo "$(GREEN)✅ $(MODEL_FILE) supprime$(NC)"; \
151
+ fi
152
+ @if [ -f "$(MODEL_INFO)" ]; then \
153
+ rm $(MODEL_INFO); \
154
+ echo "$(GREEN)✅ $(MODEL_INFO) supprime$(NC)"; \
155
+ fi
156
+
157
+ clean-all: clean ## Supprime tout (modèle + sauvegardes)
158
+ @echo "$(GREEN)Nettoyage complet...$(NC)"
159
+ @if [ -d "$(BACKUP_DIR)" ]; then \
160
+ rm -rf $(BACKUP_DIR); \
161
+ echo "$(GREEN)✅ Dossier de sauvegarde supprime$(NC)"; \
162
+ fi
163
+
164
+ # Environnement et dépendances
165
+ check-deps: ## Vérifie les dépendances Python
166
+ @echo "$(GREEN)Verification des dependances...$(NC)"
167
+ @$(PYTHON) -c "import sklearn, pandas, numpy, joblib; print('✅ Toutes les dependances sont disponibles')" || (echo "$(RED)❌ Dependances manquantes$(NC)" && exit 1)
168
+
169
+ check-db: ## Vérifie la connexion à la base de données
170
+ @echo "$(GREEN)Verification de la connexion DB...$(NC)"
171
+ @$(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)
172
+
173
+ # Workflow complet
174
+ setup: check-deps check-db train ## Configuration et génération complète
175
+ @echo "$(GREEN)✅ Setup complet termine!$(NC)"
176
+
177
+ # Commande par défaut
178
+ .DEFAULT_GOAL := help
model/README.md ADDED
@@ -0,0 +1,205 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 🤖 Modèle de Machine Learning - Project5
2
+
3
+ Ce dossier contient le modèle de Machine Learning pour la prédiction de consommation énergétique des bâtiments.
4
+
5
+ ## 📁 Structure des fichiers
6
+
7
+ ```
8
+ model/
9
+ ├── model.py # Script de génération du modèle
10
+ ├── model.pkl # Modèle RandomForest sérialisé (26MB)
11
+ ├── model_info.json # Métadonnées et métriques du modèle
12
+ ├── generate_model_report.py # Générateur de rapport HTML
13
+ ├── Makefile # Automatisation des tâches
14
+ ├── backup/ # Sauvegardes des modèles
15
+ └── README.md # Cette documentation
16
+ ```
17
+
18
+ ## 🎯 Modèle actuel
19
+
20
+ ### Informations générales
21
+ - **Type** : RandomForestRegressor
22
+ - **Version** : 1.0
23
+ - **Créé le** : 2025-09-21T11:07:42
24
+ - **Taille** : 26.0 MB
25
+ - **Variables** : 17 features
26
+
27
+ ### Métriques de performance
28
+ ```json
29
+ {
30
+ "accuracy": 0.916,
31
+ "RMSE": 0.09,
32
+ "MAE": 0.22,
33
+ "R²": 0.92
34
+ }
35
+ ```
36
+
37
+ ### Variables d'entrée (17 features)
38
+ ```
39
+ year_built
40
+ number_of_buildings
41
+ number_of_floors
42
+ property_gfa_total
43
+ property_gfa_parking
44
+ second_largest_property_use_type_gfa
45
+ third_largest_property_use_type_gfa
46
+ multiusage
47
+ steam
48
+ electricity
49
+ natural_gas
50
+ neighborhood_id
51
+ building_type_id
52
+ largest_property_use_type_id
53
+ primary_property_type_id
54
+ second_largest_property_use_type_id
55
+ third_largest_property_use_type_id
56
+ ```
57
+
58
+ ## 🚀 Utilisation rapide
59
+
60
+ ### Génération du modèle
61
+ ```bash
62
+ # Installation et génération complète
63
+ make setup
64
+
65
+ # Génération simple
66
+ make train
67
+
68
+ # Force la régénération
69
+ make retrain
70
+ ```
71
+
72
+ ### Validation et tests
73
+ ```bash
74
+ # Valider l'intégrité du modèle
75
+ make validate
76
+
77
+ # Tester le chargement
78
+ make test
79
+
80
+ # Tester une prédiction
81
+ make test-prediction
82
+ ```
83
+
84
+ ### Rapport et informations
85
+ ```bash
86
+ # Générer un rapport HTML complet
87
+ make report
88
+
89
+ # Informations rapides
90
+ make info
91
+
92
+ # Taille des fichiers
93
+ make model-size
94
+ ```
95
+
96
+ ## 📊 Commandes disponibles
97
+
98
+ ### **Génération du modèle**
99
+ - `make model` / `make train` - Génère le modèle via model.py
100
+ - `make retrain` - Force la régénération (supprime + recrée)
101
+ - `make setup` - Workflow complet (dépendances + DB + génération)
102
+
103
+ ### **Validation et tests**
104
+ - `make validate` - Vérifie l'existence et l'intégrité des fichiers
105
+ - `make test` - Test de chargement et validation du modèle
106
+ - `make test-prediction` - Test d'une prédiction avec données aléatoires
107
+
108
+ ### **Gestion et maintenance**
109
+ - `make info` - Affiche les informations détaillées (JSON)
110
+ - `make model-size` - Affiche la taille des fichiers
111
+ - `make backup` - Sauvegarde avec timestamp
112
+ - `make restore` - Restaure la dernière sauvegarde
113
+ - `make list-backups` - Liste toutes les sauvegardes
114
+ - `make clean` / `make clean-all` - Nettoyage
115
+
116
+ ### **Environnement**
117
+ - `make check-deps` - Vérifie les dépendances Python
118
+ - `make check-db` - Teste la connexion à la base de données
119
+
120
+
121
+ ## 🔧 Configuration technique
122
+
123
+ ### Prérequis
124
+ ```python
125
+ # Dépendances principales
126
+ import sklearn
127
+ import pandas
128
+ import numpy
129
+ import joblib
130
+ ```
131
+
132
+ ### Variables d'environnement
133
+ ```bash
134
+ DATABASE_URL=postgresql://user:password@localhost/dbname
135
+ ```
136
+
137
+ ### Source des données
138
+ Le modèle est entraîné sur la vue `model_view` de la base de données PostgreSQL qui agrège :
139
+ - Données Seattle (2016_Building_Energy_Benchmarking.csv)
140
+ - Données OSE (building_consumption_OSEBuildingID.csv)
141
+
142
+ ## 🛠️ Développement
143
+
144
+ ### Regeneration du modèle
145
+ 1. Modifier les hyperparamètres dans `model.py`
146
+ 2. Exécuter `make train`
147
+ 3. Valider avec `make test`
148
+ 4. Générer le rapport avec `make report`
149
+
150
+ ### Sauvegarde et versioning
151
+ ```bash
152
+ # Sauvegarde manuelle
153
+ make backup
154
+
155
+ # Lister les versions
156
+ make list-backups
157
+
158
+ # Restaurer une version précédente
159
+ make restore
160
+ ```
161
+
162
+ ### Structure du modèle
163
+ ```python
164
+ # Configuration RandomForest
165
+ RandomForestRegressor(
166
+ n_estimators=500,
167
+ max_features=0.5,
168
+ random_state=42,
169
+ min_samples_split=5,
170
+ max_depth=20
171
+ )
172
+ ```
173
+
174
+ ## 📊 Pipeline de données
175
+
176
+ 1. **Source** : Vue `model_view` (PostgreSQL)
177
+ 2. **Transformation** : Log de la variable cible `site_energy_use_wn_kbtu`
178
+ 3. **Split** : 80/20 train/test (random_state=42)
179
+ 4. **Validation** : Cross-validation 5-fold
180
+ 5. **Métriques** : RMSE, MAE, R², Accuracy
181
+
182
+ ## 🔗 Intégration API
183
+
184
+ Le modèle est automatiquement chargé par l'API FastAPI au démarrage :
185
+
186
+ ```python
187
+ # Dans main.py
188
+ from project5.ml.model import MLModel
189
+
190
+ model = MLModel()
191
+ model.load_model() # Charge model.pkl
192
+ ```
193
+
194
+ ## 📞 Support
195
+
196
+ Pour toute question ou problème :
197
+
198
+ 1. **Vérifier l'intégrité** : `make validate`
199
+ 2. **Tester le modèle** : `make test`
200
+ 3. **Générer un rapport** : `make report`
201
+ 4. **Consulter les logs** : Vérifier les sorties des commandes make
202
+
203
+ ---
204
+
205
+ **🏢 Project5 - API de gestion énergétique des bâtiments**
model/model.pkl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:40c2d682207023def3bfbebf149e5c33e219aa80b5a95f7daec8452ccee1f697
3
+ size 27298225
model/model.py ADDED
@@ -0,0 +1,130 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # This Python file uses the following encoding: utf-8
2
+ # Permet de générer le modèle à partir de la base de données
3
+ # Reprise et adaptation du notebook du projet3
4
+ import json
5
+ import os
6
+ from datetime import datetime
7
+
8
+ import joblib
9
+ import numpy as np
10
+ import pandas as pd
11
+ from sklearn.ensemble import RandomForestRegressor
12
+ from sklearn.metrics import mean_absolute_error, mean_squared_error, r2_score
13
+ from sklearn.model_selection import cross_validate, train_test_split
14
+ from sqlalchemy import create_engine
15
+
16
+ DATABASE_URL = os.getenv("DATABASE_URL", "postgresql://user:password@localhost/dbname")
17
+
18
+
19
+ def get_for_model(features):
20
+ # Separation du jeu de données en train, test
21
+ X = building_consumption[features]
22
+ y = building_consumption["log_" + var_a_predire]
23
+ X_train, X_test, y_train, y_test = train_test_split(
24
+ X, y, test_size=0.2, random_state=42, shuffle=True
25
+ )
26
+
27
+ # X_train, X_val, y_train, y_val = train_test_split(X_train, y_train, test_size=0.25, random_state=42)
28
+ print(f"Shape de X_train: {X_train.shape}")
29
+ print(f"Shape de y_train: {y_train.shape}")
30
+ print(f"Shape de X_test: {X_test.shape}")
31
+ print(f"Shape de y_train: {y_test.shape}")
32
+ return X_train, X_test, y_train, y_test
33
+
34
+
35
+ def get_score(y, prediction):
36
+ mse = mean_squared_error(y, prediction)
37
+ mae = mean_absolute_error(y, prediction)
38
+ r2 = r2_score(y, prediction)
39
+ return mse, mae, r2
40
+
41
+
42
+ scores = {}
43
+
44
+ engine = create_engine(DATABASE_URL)
45
+
46
+ query = "SELECT * FROM model_view"
47
+ building_consumption = pd.read_sql(query, engine)
48
+
49
+ print(building_consumption.info())
50
+ engine.dispose()
51
+
52
+ var_a_predire = "site_energy_use_wn_kbtu"
53
+ building_consumption["log_" + var_a_predire] = np.log(
54
+ building_consumption[var_a_predire]
55
+ )
56
+
57
+
58
+ # Features
59
+ features = [
60
+ "year_built",
61
+ "number_of_buildings",
62
+ "number_of_floors",
63
+ "property_gfa_total",
64
+ "property_gfa_parking",
65
+ "second_largest_property_use_type_gfa",
66
+ "third_largest_property_use_type_gfa",
67
+ "multiusage",
68
+ "steam",
69
+ "electricity",
70
+ "natural_gas",
71
+ "neighborhood_id",
72
+ "building_type_id",
73
+ "largest_property_use_type_id",
74
+ "primary_property_type_id",
75
+ "second_largest_property_use_type_id",
76
+ "third_largest_property_use_type_id",
77
+ ]
78
+ X_train, X_test, y_train, y_test = get_for_model(features)
79
+
80
+ # Initialisation du modèle
81
+ rf = RandomForestRegressor(
82
+ n_estimators=500,
83
+ max_features=0.5,
84
+ random_state=42,
85
+ min_samples_split=5,
86
+ max_depth=20,
87
+ )
88
+
89
+ # Entraînement sur l'ensemble des données
90
+ X = building_consumption[features]
91
+ y = building_consumption["log_" + var_a_predire]
92
+ rf.fit(X, y)
93
+
94
+ # Prédiction
95
+ y_pred = rf.predict(X_test)
96
+ scores_cross = cross_validate(
97
+ rf, X_train, y_train, cv=5
98
+ ) # cv=5 pour une validation croisée à 5 plis
99
+ fit_time = scores_cross["fit_time"].mean()
100
+ score_time = scores_cross["score_time"].mean()
101
+ scores.update(
102
+ {"RandomForestRegressor HP": get_score(y_test, y_pred) + (fit_time, score_time)}
103
+ )
104
+
105
+ resultats = pd.DataFrame(scores).T
106
+ resultats.columns = ["RMSE", "MAE", "R^2", "Fit Time", "Score Time"]
107
+ resultats = resultats.round(2)
108
+ print(resultats[-1:1]["RMSE"].values)
109
+
110
+ # Sauvegarde du modèle
111
+ # Informations du modèle
112
+ model_info = {
113
+ "model_type": type(rf).__name__,
114
+ "model_module": type(rf).__module__,
115
+ "has_feature_importances": hasattr(rf, "feature_importances_"),
116
+ "has_coefficients": hasattr(rf, "coef_"),
117
+ "has_predict_proba": hasattr(rf, "predict_proba"),
118
+ "version": "1.0",
119
+ "created_at": datetime.now().isoformat(),
120
+ "features": list(X_train.columns),
121
+ "accuracy": rf.score(X_test, y_test),
122
+ "RMSE": resultats[-1:1]["RMSE"].values[0],
123
+ "MAE": resultats[-1:1]["MAE"].values[0],
124
+ "R^2": resultats[-1:1]["R^2"].values[0],
125
+ }
126
+
127
+ # Sauvegarder modèle et métadonnées
128
+ joblib.dump(rf, "model.pkl")
129
+ with open("model_info.json", "w") as f:
130
+ json.dump(model_info, f, indent=2)
model/model_info.json ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "model_type": "RandomForestRegressor",
3
+ "model_module": "sklearn.ensemble._forest",
4
+ "has_feature_importances": true,
5
+ "has_coefficients": false,
6
+ "has_predict_proba": false,
7
+ "version": "1.0",
8
+ "created_at": "2025-09-21T11:07:42.926661",
9
+ "features": [
10
+ "year_built",
11
+ "number_of_buildings",
12
+ "number_of_floors",
13
+ "property_gfa_total",
14
+ "property_gfa_parking",
15
+ "second_largest_property_use_type_gfa",
16
+ "third_largest_property_use_type_gfa",
17
+ "multiusage",
18
+ "steam",
19
+ "electricity",
20
+ "natural_gas",
21
+ "neighborhood_id",
22
+ "building_type_id",
23
+ "largest_property_use_type_id",
24
+ "primary_property_type_id",
25
+ "second_largest_property_use_type_id",
26
+ "third_largest_property_use_type_id"
27
+ ],
28
+ "accuracy": 0.9160018446396789,
29
+ "RMSE": 0.09,
30
+ "MAE": 0.22,
31
+ "R^2": 0.92
32
+ }
model/model_report.html ADDED
@@ -0,0 +1,449 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ <!DOCTYPE html>
3
+ <html lang="fr">
4
+ <head>
5
+ <meta charset="UTF-8">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
+ <title>Rapport Modèle ML - Project5</title>
8
+ <style>
9
+ body {
10
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
11
+ line-height: 1.6;
12
+ margin: 0;
13
+ padding: 20px;
14
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
15
+ color: #333;
16
+ min-height: 100vh;
17
+ }
18
+ .container {
19
+ max-width: 1200px;
20
+ margin: 0 auto;
21
+ background: white;
22
+ border-radius: 15px;
23
+ box-shadow: 0 20px 40px rgba(0,0,0,0.1);
24
+ overflow: hidden;
25
+ }
26
+ .header {
27
+ background: linear-gradient(135deg, #2c3e50 0%, #3498db 100%);
28
+ color: white;
29
+ padding: 30px;
30
+ text-align: center;
31
+ }
32
+ .header h1 {
33
+ margin: 0;
34
+ font-size: 2.5em;
35
+ font-weight: 300;
36
+ }
37
+ .header p {
38
+ margin: 10px 0 0 0;
39
+ opacity: 0.9;
40
+ font-size: 1.1em;
41
+ }
42
+ .content {
43
+ padding: 30px;
44
+ }
45
+ .section {
46
+ margin-bottom: 30px;
47
+ background: #f8f9fa;
48
+ border-radius: 10px;
49
+ padding: 25px;
50
+ border-left: 5px solid #3498db;
51
+ }
52
+ .section h2 {
53
+ color: #2c3e50;
54
+ margin-top: 0;
55
+ margin-bottom: 20px;
56
+ font-size: 1.5em;
57
+ display: flex;
58
+ align-items: center;
59
+ }
60
+ .section h2::before {
61
+ content: "📊";
62
+ margin-right: 10px;
63
+ font-size: 1.2em;
64
+ }
65
+ .metrics-grid {
66
+ display: grid;
67
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
68
+ gap: 20px;
69
+ margin: 20px 0;
70
+ }
71
+ .metric-card {
72
+ background: white;
73
+ padding: 20px;
74
+ border-radius: 8px;
75
+ text-align: center;
76
+ box-shadow: 0 4px 6px rgba(0,0,0,0.1);
77
+ border: 1px solid #e1e8ed;
78
+ }
79
+ .metric-value {
80
+ font-size: 2em;
81
+ font-weight: bold;
82
+ color: #3498db;
83
+ margin-bottom: 5px;
84
+ }
85
+ .metric-label {
86
+ color: #7f8c8d;
87
+ font-size: 0.9em;
88
+ text-transform: uppercase;
89
+ letter-spacing: 1px;
90
+ }
91
+ .info-grid {
92
+ display: grid;
93
+ grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
94
+ gap: 15px;
95
+ }
96
+ .info-item {
97
+ background: white;
98
+ padding: 15px;
99
+ border-radius: 8px;
100
+ border-left: 4px solid #3498db;
101
+ }
102
+ .info-label {
103
+ font-weight: bold;
104
+ color: #2c3e50;
105
+ margin-bottom: 5px;
106
+ }
107
+ .info-value {
108
+ color: #7f8c8d;
109
+ }
110
+ .features-list {
111
+ display: grid;
112
+ grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
113
+ gap: 10px;
114
+ margin-top: 15px;
115
+ }
116
+ .feature-item {
117
+ background: white;
118
+ padding: 10px 15px;
119
+ border-radius: 6px;
120
+ border-left: 3px solid #2ecc71;
121
+ font-size: 0.9em;
122
+ }
123
+ .importance-table {
124
+ background: white;
125
+ border-radius: 8px;
126
+ overflow: hidden;
127
+ margin-top: 20px;
128
+ }
129
+ .importance-table table {
130
+ width: 100%;
131
+ border-collapse: collapse;
132
+ }
133
+ .importance-table th {
134
+ background: #3498db;
135
+ color: white;
136
+ padding: 15px;
137
+ text-align: left;
138
+ }
139
+ .importance-table td {
140
+ padding: 12px 15px;
141
+ border-bottom: 1px solid #ecf0f1;
142
+ }
143
+ .importance-table tr:nth-child(even) {
144
+ background: #f8f9fa;
145
+ }
146
+ .importance-bar {
147
+ background: #ecf0f1;
148
+ height: 20px;
149
+ border-radius: 10px;
150
+ overflow: hidden;
151
+ position: relative;
152
+ }
153
+ .importance-fill {
154
+ background: linear-gradient(90deg, #3498db, #2ecc71);
155
+ height: 100%;
156
+ border-radius: 10px;
157
+ transition: width 0.3s ease;
158
+ }
159
+ .footer {
160
+ background: #2c3e50;
161
+ color: white;
162
+ text-align: center;
163
+ padding: 20px;
164
+ margin-top: 30px;
165
+ }
166
+ .status-badge {
167
+ display: inline-block;
168
+ padding: 5px 15px;
169
+ border-radius: 20px;
170
+ font-size: 0.8em;
171
+ font-weight: bold;
172
+ text-transform: uppercase;
173
+ }
174
+ .status-success {
175
+ background: #2ecc71;
176
+ color: white;
177
+ }
178
+ .status-info {
179
+ background: #3498db;
180
+ color: white;
181
+ }
182
+ @media (max-width: 768px) {
183
+ .container {
184
+ margin: 10px;
185
+ border-radius: 0;
186
+ }
187
+ .metrics-grid {
188
+ grid-template-columns: 1fr;
189
+ }
190
+ .info-grid {
191
+ grid-template-columns: 1fr;
192
+ }
193
+ }
194
+ </style>
195
+ </head>
196
+ <body>
197
+ <div class="container">
198
+ <div class="header">
199
+ <h1>🤖 Rapport Modèle ML</h1>
200
+ <p>Project5 - Prédiction énergétique des bâtiments</p>
201
+ <p>Généré le 21/09/2025 à 11:11:02</p>
202
+ </div>
203
+
204
+ <div class="content">
205
+
206
+ <div class="section">
207
+ <h2>Informations Générales</h2>
208
+ <div class="info-grid">
209
+ <div class="info-item">
210
+ <div class="info-label">Type de Modèle</div>
211
+ <div class="info-value">RandomForestRegressor</div>
212
+ </div>
213
+ <div class="info-item">
214
+ <div class="info-label">Version</div>
215
+ <div class="info-value">1.0</div>
216
+ </div>
217
+ <div class="info-item">
218
+ <div class="info-label">Date de Création</div>
219
+ <div class="info-value">2025-09-21T11:07:42.926661</div>
220
+ </div>
221
+ <div class="info-item">
222
+ <div class="info-label">Module Python</div>
223
+ <div class="info-value">sklearn.ensemble._forest</div>
224
+ </div>
225
+ <div class="info-item">
226
+ <div class="info-label">Taille du Modèle</div>
227
+ <div class="info-value">26.0 MB</div>
228
+ </div>
229
+ <div class="info-item">
230
+ <div class="info-label">Nombre de Features</div>
231
+ <div class="info-value">17</div>
232
+ </div>
233
+ </div>
234
+ </div>
235
+
236
+ <div class="section">
237
+ <h2>Métriques de Performance</h2>
238
+ <div class="metrics-grid">
239
+ <div class="metric-card">
240
+ <div class="metric-value">0.916</div>
241
+ <div class="metric-label">Accuracy (R²)</div>
242
+ </div>
243
+ <div class="metric-card">
244
+ <div class="metric-value">0.090</div>
245
+ <div class="metric-label">RMSE</div>
246
+ </div>
247
+ <div class="metric-card">
248
+ <div class="metric-value">0.220</div>
249
+ <div class="metric-label">MAE</div>
250
+ </div>
251
+ <div class="metric-card">
252
+ <div class="metric-value">0.920</div>
253
+ <div class="metric-label">R² Score</div>
254
+ </div>
255
+ </div>
256
+ </div>
257
+
258
+ <div class="section">
259
+ <h2>Capacités du Modèle</h2>
260
+ <div class="info-grid">
261
+ <div class="info-item">
262
+ <div class="info-label">Feature Importances</div>
263
+ <div class="info-value">
264
+ <span class="status-badge status-success">
265
+ ✓ Disponible
266
+ </span>
267
+ </div>
268
+ </div>
269
+ <div class="info-item">
270
+ <div class="info-label">Coefficients</div>
271
+ <div class="info-value">
272
+ <span class="status-badge status-info">
273
+ ✗ Non disponible
274
+ </span>
275
+ </div>
276
+ </div>
277
+ <div class="info-item">
278
+ <div class="info-label">Prédiction Probabiliste</div>
279
+ <div class="info-value">
280
+ <span class="status-badge status-info">
281
+ ✗ Non disponible
282
+ </span>
283
+ </div>
284
+ </div>
285
+ </div>
286
+ </div>
287
+
288
+ <div class="section">
289
+ <h2>Variables d'Entrée (17 features)</h2>
290
+ <div class="features-list">
291
+ <div class="feature-item">year_built</div>
292
+ <div class="feature-item">number_of_buildings</div>
293
+ <div class="feature-item">number_of_floors</div>
294
+ <div class="feature-item">property_gfa_total</div>
295
+ <div class="feature-item">property_gfa_parking</div>
296
+ <div class="feature-item">second_largest_property_use_type_gfa</div>
297
+ <div class="feature-item">third_largest_property_use_type_gfa</div>
298
+ <div class="feature-item">multiusage</div>
299
+ <div class="feature-item">steam</div>
300
+ <div class="feature-item">electricity</div>
301
+ <div class="feature-item">natural_gas</div>
302
+ <div class="feature-item">neighborhood_id</div>
303
+ <div class="feature-item">building_type_id</div>
304
+ <div class="feature-item">largest_property_use_type_id</div>
305
+ <div class="feature-item">primary_property_type_id</div>
306
+ <div class="feature-item">second_largest_property_use_type_id</div>
307
+ <div class="feature-item">third_largest_property_use_type_id</div>
308
+
309
+ </div>
310
+ </div>
311
+
312
+ <div class="section">
313
+ <h2>Importance des Variables</h2>
314
+ <div class="importance-table">
315
+ <table>
316
+ <thead>
317
+ <tr>
318
+ <th>Variable</th>
319
+ <th>Importance</th>
320
+ <th>Pourcentage</th>
321
+ <th>Visualisation</th>
322
+ </tr>
323
+ </thead>
324
+ <tbody>
325
+
326
+ <tr>
327
+ <td><strong>property_gfa_total</strong></td>
328
+ <td>0.4159</td>
329
+ <td>41.59%</td>
330
+ <td>
331
+ <div class="importance-bar">
332
+ <div class="importance-fill" style="width: 100.0%"></div>
333
+ </div>
334
+ </td>
335
+ </tr>
336
+
337
+ <tr>
338
+ <td><strong>primary_property_type_id</strong></td>
339
+ <td>0.1205</td>
340
+ <td>12.05%</td>
341
+ <td>
342
+ <div class="importance-bar">
343
+ <div class="importance-fill" style="width: 28.96743295740477%"></div>
344
+ </div>
345
+ </td>
346
+ </tr>
347
+
348
+ <tr>
349
+ <td><strong>largest_property_use_type_id</strong></td>
350
+ <td>0.0907</td>
351
+ <td>9.07%</td>
352
+ <td>
353
+ <div class="importance-bar">
354
+ <div class="importance-fill" style="width: 21.803967049724744%"></div>
355
+ </div>
356
+ </td>
357
+ </tr>
358
+
359
+ <tr>
360
+ <td><strong>year_built</strong></td>
361
+ <td>0.0760</td>
362
+ <td>7.60%</td>
363
+ <td>
364
+ <div class="importance-bar">
365
+ <div class="importance-fill" style="width: 18.264056040746727%"></div>
366
+ </div>
367
+ </td>
368
+ </tr>
369
+
370
+ <tr>
371
+ <td><strong>second_largest_property_use_type_gfa</strong></td>
372
+ <td>0.0665</td>
373
+ <td>6.65%</td>
374
+ <td>
375
+ <div class="importance-bar">
376
+ <div class="importance-fill" style="width: 15.990714913050171%"></div>
377
+ </div>
378
+ </td>
379
+ </tr>
380
+
381
+ <tr>
382
+ <td><strong>number_of_floors</strong></td>
383
+ <td>0.0649</td>
384
+ <td>6.49%</td>
385
+ <td>
386
+ <div class="importance-bar">
387
+ <div class="importance-fill" style="width: 15.614344234190247%"></div>
388
+ </div>
389
+ </td>
390
+ </tr>
391
+
392
+ <tr>
393
+ <td><strong>natural_gas</strong></td>
394
+ <td>0.0444</td>
395
+ <td>4.44%</td>
396
+ <td>
397
+ <div class="importance-bar">
398
+ <div class="importance-fill" style="width: 10.669995866214208%"></div>
399
+ </div>
400
+ </td>
401
+ </tr>
402
+
403
+ <tr>
404
+ <td><strong>neighborhood_id</strong></td>
405
+ <td>0.0368</td>
406
+ <td>3.68%</td>
407
+ <td>
408
+ <div class="importance-bar">
409
+ <div class="importance-fill" style="width: 8.854359706938872%"></div>
410
+ </div>
411
+ </td>
412
+ </tr>
413
+
414
+ <tr>
415
+ <td><strong>property_gfa_parking</strong></td>
416
+ <td>0.0209</td>
417
+ <td>2.09%</td>
418
+ <td>
419
+ <div class="importance-bar">
420
+ <div class="importance-fill" style="width: 5.020163876222049%"></div>
421
+ </div>
422
+ </td>
423
+ </tr>
424
+
425
+ <tr>
426
+ <td><strong>second_largest_property_use_type_id</strong></td>
427
+ <td>0.0206</td>
428
+ <td>2.06%</td>
429
+ <td>
430
+ <div class="importance-bar">
431
+ <div class="importance-fill" style="width: 4.95439652964564%"></div>
432
+ </div>
433
+ </td>
434
+ </tr>
435
+
436
+ </tbody>
437
+ </table>
438
+ </div>
439
+ </div>
440
+
441
+ </div>
442
+
443
+ <div class="footer">
444
+ <p>🏢 Project5 - API de gestion énergétique des bâtiments</p>
445
+ <p>Généré automatiquement le 21/09/2025 à 11:11:02</p>
446
+ </div>
447
+ </div>
448
+ </body>
449
+ </html>
poetry.lock ADDED
The diff for this file is too large to render. See raw diff
 
pyproject.toml ADDED
@@ -0,0 +1,168 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # pyproject.toml corrigé pour Poetry
2
+ [tool.poetry]
3
+ name = "project5"
4
+ version = "0.1.0"
5
+ description = "Prédiction de consommation énergétique des bâtiments"
6
+ authors = ["francois hellebuyck <formation.fhellebuyck@pm.me>"]
7
+ packages = [{include = "project5", from = "src"}]
8
+
9
+ [tool.poetry.group.docs.dependencies]
10
+ # Sphinx et extensions essentielles
11
+ sphinx = "^7.2.6"
12
+ sphinx-rtd-theme = "^1.3.0"
13
+ sphinx-autodoc-typehints = "^1.25.2"
14
+ sphinx-copybutton = "^0.5.2"
15
+ myst-parser = "^2.0.0"
16
+ sphinx-autobuild = "^2021.3.14"
17
+ sphinxcontrib-napoleon = "^0.7"
18
+
19
+ # Extensions optionnelles mais recommandées
20
+ sphinx-book-theme = "^1.0.1" # Thème moderne alternatif
21
+ sphinxext-rediraffe = "^0.2.7" # Redirections
22
+ sphinx-design = "^0.5.0" # Composants modernes
23
+ sphinx-tabs = "^3.4.4" # Onglets
24
+ sphinx-togglebutton = "^0.3.2" # Boutons pliables
25
+
26
+ [tool.poetry.dependencies]
27
+ python = "^3.11"
28
+ fastapi = "^0.116.1"
29
+ uvicorn = {extras = ["standard"], version = "^0.35.0"}
30
+ # Base de données
31
+ sqlalchemy = "^2.0.23"
32
+ alembic = "^1.12.1"
33
+ psycopg2-binary = "^2.9.9"
34
+ # Configuration et sécurité
35
+ pydantic = "^2.5.0"
36
+ pydantic-settings = "^2.1.0"
37
+ python-dotenv = "^1.0.0"
38
+ passlib = {extras = ["bcrypt"], version = "^1.7.4"}
39
+ # Utilitaires
40
+ httpx = "^0.25.0"
41
+ python-jwt = "^4.1.0"
42
+ joblib = "^1.5.2"
43
+ numpy = "^2.3.3"
44
+ scikit-learn = "1.7.1"
45
+ pandas = "^2.3.2"
46
+ shibuya = "^2025.8.16"
47
+ eralchemy = "^1.3.0"
48
+ graphviz = "^0.20.0"
49
+
50
+
51
+ [tool.poetry.group.dev.dependencies]
52
+ pylint = "^3.3.8"
53
+ pytest = "^7.4.0"
54
+ pytest-cov = "^4.1.0"
55
+ pytest-asyncio = "^0.21.0"
56
+ httpx = "^0.25.0"
57
+ black = "^24.0.0"
58
+ isort = "^5.12.0"
59
+ flake8 = "^6.1.0"
60
+ mypy = "^1.6.0"
61
+ bandit = "^1.7.5"
62
+ safety = "^2.3.0"
63
+ # Tests de base de données
64
+ pytest-postgresql = "^5.0.0"
65
+ aiosqlite = "^0.19.0"
66
+ sqlacodegen = "^3.1.0"
67
+
68
+
69
+ [build-system]
70
+ requires = ["poetry-core>=2.0.0,<3.0.0"]
71
+ build-backend = "poetry.core.masonry.api"
72
+
73
+ [tool.black]
74
+ line-length = 88
75
+ target-version = ['py311']
76
+ include = '\.pyi?$'
77
+ extend-exclude = '''
78
+ /(
79
+ # directories
80
+ \.eggs
81
+ | \.git
82
+ | \.hg
83
+ | \.mypy_cache
84
+ | \.tox
85
+ | \.venv
86
+ | build
87
+ | dist
88
+ | alembic/versions
89
+ )/
90
+ '''
91
+
92
+ [tool.isort]
93
+ profile = "black"
94
+ multi_line_output = 3
95
+ include_trailing_comma = true
96
+ force_grid_wrap = 0
97
+ use_parentheses = true
98
+ ensure_newline_before_comments = true
99
+ line_length = 88
100
+
101
+ [tool.mypy]
102
+ python_version = "3.11"
103
+ warn_return_any = true
104
+ warn_unused_configs = true
105
+ disallow_untyped_defs = true
106
+ plugins = ["sqlalchemy.ext.mypy.plugin"]
107
+
108
+ [tool.pytest.ini_options]
109
+ minversion = "6.0"
110
+ addopts = "-ra -q --strict-markers"
111
+ testpaths = [
112
+ "tests",
113
+ ]
114
+ pythonpath = ["./src"]
115
+ asyncio_mode = "auto"
116
+ markers = [
117
+ "unit: Unit tests",
118
+ "integration: Integration tests",
119
+ "database: Tests requiring database",
120
+ "slow: Slow running tests",
121
+ ]
122
+
123
+ [tool.coverage.run]
124
+ source = ["src"]
125
+ omit = [
126
+ "*/tests/*",
127
+ "*/venv/*",
128
+ "*/.venv/*",
129
+ "*/env/*",
130
+ "*/alembic/*",
131
+ "*/migrations/*",
132
+ ]
133
+
134
+ [tool.coverage.report]
135
+ exclude_lines = [
136
+ "pragma: no cover",
137
+ "def __repr__",
138
+ "if self.debug:",
139
+ "if settings.DEBUG",
140
+ "raise AssertionError",
141
+ "raise NotImplementedError",
142
+ "if 0:",
143
+ "if __name__ == .__main__.:",
144
+ "class .*\\bProtocol\\):",
145
+ "@(abc\\.)?abstractmethod",
146
+ ]
147
+
148
+ [tool.flake8]
149
+ max-line-length = 88
150
+ extend-ignore = [
151
+ "E203",
152
+ "W503",
153
+ "E402",
154
+ "E501",
155
+ ]
156
+ exclude = [
157
+ ".git",
158
+ "__pycache__",
159
+ ".venv",
160
+ "venv",
161
+ ".jupyter",
162
+ "alembic/versions",
163
+ "migrations",
164
+ ]
165
+ per-file-ignores = [
166
+ "*.ipynb:E402",
167
+ "alembic/*:E402,F401",
168
+ ]
src/project5/__init__.py ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ """API REST pour gérer les bâtiments"""
2
+
3
+ __version__ = "1.0.0"
4
+ __author__ = "François Hellebuyck"
5
+
6
+ # Exposer certaines classes au niveau du package
7
+ from .database import engine, get_db
8
+ from .models import Neighborhood
9
+
10
+ __all__ = ["get_db", "engine", "Neighborhood"]
src/project5/alembic.ini ADDED
@@ -0,0 +1,72 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # A generic, single database configuration.
2
+
3
+ [alembic]
4
+ # path to migration scripts
5
+ script_location = alembic
6
+
7
+ # template used to generate migration file names; The default value is %%(rev)s_%%(slug)s
8
+ # Uncomment the line below if you want the files to be prepended with date and time
9
+ file_template = %%(year)d%%(month).2d%%(day).2d_%%(hour).2d%%(minute).2d_%%(rev)s_%%(slug)s
10
+
11
+ # sys.path path, will be prepended to sys.path if present.
12
+ prepend_sys_path = .
13
+
14
+ # timezone to use when rendering the date within the migration file
15
+ # as well as the filename.
16
+ # If specified, requires the python-dateutil library that can be
17
+ # installed by adding `alembic[tz]` to the pip requirements
18
+ # string value is passed to dateutil.tz.gettz()
19
+ # leave blank for localtime
20
+ # timezone =
21
+
22
+ # max length of characters to apply to the
23
+ # "slug" field
24
+ # truncate_slug_length = 40
25
+
26
+ # set to 'true' to run the environment during
27
+ # the 'revision' command, regardless of autogenerate
28
+ # revision_environment = false
29
+
30
+ # set to 'true' to allow .pyc and .pyo files without
31
+ # a source .py file to be detected as revisions in the
32
+ # versions/ directory
33
+ # sourceless = false
34
+
35
+ # version number format. This value should be incrementing integer,
36
+ # typically starting at 1.
37
+ # version_num_format = %d
38
+
39
+ # Logging configuration
40
+ [loggers]
41
+ keys = root,sqlalchemy,alembic
42
+
43
+ [handlers]
44
+ keys = console
45
+
46
+ [formatters]
47
+ keys = generic
48
+
49
+ [logger_root]
50
+ level = WARN
51
+ handlers = console
52
+ qualname =
53
+
54
+ [logger_sqlalchemy]
55
+ level = WARN
56
+ handlers =
57
+ qualname = sqlalchemy.engine
58
+
59
+ [logger_alembic]
60
+ level = INFO
61
+ handlers =
62
+ qualname = alembic
63
+
64
+ [handler_console]
65
+ class = StreamHandler
66
+ args = (sys.stderr,)
67
+ level = NOTSET
68
+ formatter = generic
69
+
70
+ [formatter_generic]
71
+ format = %(levelname)-5.5s [%(name)s] %(message)s
72
+ datefmt = %H:%M:%S
src/project5/config.py ADDED
@@ -0,0 +1,63 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Configuration de l'application."""
2
+
3
+ from typing import Optional
4
+
5
+ from pydantic import Field
6
+ from pydantic_settings import BaseSettings
7
+
8
+
9
+ class Settings(BaseSettings):
10
+ """Configuration de l'application via variables d'environnement."""
11
+
12
+ # Configuration de l'application
13
+ app_name: str = Field(
14
+ default="API de gestion énergétique des bâtiments", env="APP_NAME"
15
+ )
16
+ app_version: str = Field(default="1.0.0", env="APP_VERSION")
17
+ debug: bool = Field(default=False, env="DEBUG")
18
+ environment: str = Field(default="production", env="ENVIRONMENT")
19
+
20
+ # Configuration de la base de données
21
+ database_url: str = Field(..., env="DATABASE_URL")
22
+ db_pool_size: int = Field(default=20, env="DB_POOL_SIZE")
23
+ db_max_overflow: int = Field(default=0, env="DB_MAX_OVERFLOW")
24
+ db_pool_pre_ping: bool = Field(default=True, env="DB_POOL_PRE_PING")
25
+ db_echo: bool = Field(default=False, env="DB_ECHO")
26
+
27
+ # Configuration de l'API
28
+ api_prefix: str = Field(default="/api/v1", env="API_PREFIX")
29
+ cors_origins: list = Field(default=["*"], env="CORS_ORIGINS")
30
+ cors_allow_credentials: bool = Field(default=True, env="CORS_ALLOW_CREDENTIALS")
31
+ cors_allow_methods: list = Field(default=["GET"], env="CORS_ALLOW_METHODS")
32
+ cors_allow_headers: list = Field(default=["*"], env="CORS_ALLOW_HEADERS")
33
+
34
+ # Configuration du logging
35
+ log_level: str = Field(default="INFO", env="LOG_LEVEL")
36
+ log_format: str = Field(
37
+ default="%(asctime)s - %(name)s - %(levelname)s - %(message)s", env="LOG_FORMAT"
38
+ )
39
+
40
+ # Configuration de sécurité
41
+ allowed_hosts: list = Field(default=["*"], env="ALLOWED_HOSTS")
42
+
43
+ # Configuration de cache (pour extensions futures)
44
+ redis_url: Optional[str] = Field(None, env="REDIS_URL")
45
+ cache_ttl: int = Field(default=300, env="CACHE_TTL") # 5 minutes
46
+
47
+ # Configuration de monitoring
48
+ enable_metrics: bool = Field(default=False, env="ENABLE_METRICS")
49
+ metrics_port: int = Field(default=9090, env="METRICS_PORT")
50
+
51
+ class Config:
52
+ env_file = ".env"
53
+ case_sensitive = False
54
+ extra = "ignore" # Ignore extra fields from .env
55
+
56
+
57
+ # Instance globale des paramètres
58
+ settings = Settings()
59
+
60
+
61
+ def get_settings() -> Settings:
62
+ """Récupère l'instance des paramètres."""
63
+ return settings
src/project5/database.py ADDED
@@ -0,0 +1,339 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Configuration et gestion de la base de données pour l'application de prédiction énergétique.
3
+
4
+ Ce module configure la connectivité à la base de données avec support adaptatif pour
5
+ multiples environnements : PostgreSQL pour la production standard, SQLite en mémoire
6
+ pour le déploiement HuggingFace Spaces, et SQLite fichier pour les tests.
7
+
8
+ Architecture de déploiement :
9
+ - Production normale : PostgreSQL avec pool de connexions optimisé
10
+ - HuggingFace Spaces : SQLite en mémoire partagée pour contourner les limitations
11
+ - Tests/Développement : SQLite fichier local pour isolation
12
+
13
+ Fonctionnalités principales :
14
+ - Configuration automatique basée sur variables d'environnement
15
+ - Pool de connexions PostgreSQL avec reconnexion automatique
16
+ - SQLite en mémoire partagée thread-safe pour HuggingFace
17
+ - Initialisation automatique des données en production
18
+ - Session management avec pattern Dependency Injection
19
+ - Migration et création de tables automatique
20
+
21
+ Gestion des environnements :
22
+ Le module détecte automatiquement l'environnement via DATABASE_URL et ENVIRONMENT :
23
+ - ENVIRONMENT="production" + SQLite -> Mode HuggingFace Spaces
24
+ - DATABASE_URL PostgreSQL -> Mode production standard
25
+ - Autres cas -> Mode développement/test
26
+
27
+ Thread Safety :
28
+ - PostgreSQL : Thread-safe natif avec pool de connexions
29
+ - SQLite en mémoire : Connexion partagée avec check_same_thread=False
30
+ - Sessions SQLAlchemy : Thread-safe par design
31
+
32
+ Performance :
33
+ - PostgreSQL : Pool configuré pour 20 connexions simultanées
34
+ - SQLite : Connexion unique partagée pour minimiser l'empreinte mémoire
35
+ - Pool pre-ping activé pour éviter les connexions fermées
36
+
37
+ Variables d'environnement :
38
+ DATABASE_URL : URL de connexion PostgreSQL ou SQLite
39
+ ENVIRONMENT : "production" pour activer le mode HuggingFace Spaces
40
+
41
+ Exemple d'usage :
42
+ # Dans une route FastAPI
43
+ @app.get("/buildings/")
44
+ def get_buildings(db: Session = Depends(get_db)):
45
+ return db.query(Building).all()
46
+
47
+ # Initialisation manuelle
48
+ from project5.database import create_tables
49
+ create_tables()
50
+
51
+ Note :
52
+ Le design de ce module prend en compte les contraintes spécifiques de
53
+ HuggingFace Spaces qui ne permet pas l'écriture sur disque persistante.
54
+ """
55
+
56
+ import os
57
+ import sqlite3
58
+ import sys
59
+
60
+ from dotenv import load_dotenv
61
+ from sqlalchemy import create_engine
62
+ from sqlalchemy.ext.declarative import declarative_base
63
+ from sqlalchemy.orm import sessionmaker
64
+
65
+ # Charger les variables d'environnement
66
+ load_dotenv()
67
+
68
+ # URL de connexion à la base de données PostgreSQL
69
+ # Exemple : postgresql://user:password@localhost/dbname
70
+ DATABASE_URL = os.getenv("DATABASE_URL", "postgresql://user:password@localhost/dbname")
71
+ ENVIRONMENT = os.getenv("ENVIRONMENT", "dev")
72
+
73
+ # Connexion SQLite partagée pour la production
74
+ _shared_sqlite_conn = None
75
+
76
+
77
+ def get_shared_sqlite_connection():
78
+ """
79
+ Retourne la connexion SQLite partagée en mémoire pour HuggingFace Spaces.
80
+
81
+ Cette fonction implémente un pattern Singleton pour partager une unique
82
+ connexion SQLite en mémoire à travers toute l'application. Elle est
83
+ spécifiquement conçue pour le déploiement sur HuggingFace Spaces qui
84
+ ne permet pas l'écriture sur disque persistante.
85
+
86
+ Returns:
87
+ sqlite3.Connection: Connexion SQLite en mémoire partagée et thread-safe
88
+
89
+ Side Effects:
90
+ - Crée la connexion globale lors du premier appel
91
+ - Initialise automatiquement les données si ENVIRONMENT="production"
92
+ - Modifie la variable globale _shared_sqlite_conn
93
+
94
+ Thread Safety:
95
+ Connexion configurée avec check_same_thread=False pour permettre
96
+ l'utilisation depuis multiple threads FastAPI.
97
+
98
+ Performance:
99
+ - Base de données entièrement en RAM pour performances maximales
100
+ - Pas d'I/O disque, idéal pour HuggingFace Spaces
101
+ - Initialisation une seule fois au démarrage
102
+
103
+ Example:
104
+ >>> conn = get_shared_sqlite_connection()
105
+ >>> cursor = conn.cursor()
106
+ >>> cursor.execute("SELECT COUNT(*) FROM buildings")
107
+ >>> print(cursor.fetchone()[0])
108
+ 9895
109
+
110
+ Note:
111
+ Cette fonction ne doit être utilisée qu'en mode production HuggingFace.
112
+ Pour les autres environnements, utiliser les sessions SQLAlchemy normales.
113
+
114
+ Raises:
115
+ Exception: Si l'initialisation des données de production échoue
116
+ """
117
+ global _shared_sqlite_conn
118
+ if _shared_sqlite_conn is None:
119
+ _shared_sqlite_conn = sqlite3.connect(":memory:", check_same_thread=False)
120
+ # Initialiser les données si en production
121
+ if ENVIRONMENT == "production":
122
+ _initialize_production_data()
123
+ return _shared_sqlite_conn
124
+
125
+
126
+ def _initialize_production_data():
127
+ """
128
+ Initialise les données de production dans la base SQLite en mémoire.
129
+
130
+ Cette fonction privée charge automatiquement le dataset complet de
131
+ 9895 bâtiments de Seattle dans la base de données en mémoire lors
132
+ du démarrage en mode production HuggingFace Spaces.
133
+
134
+ Architecture d'initialisation :
135
+ 1. Modification dynamique du chemin Python pour accéder à init_db
136
+ 2. Monkey-patching de get_db_connection pour utiliser la connexion partagée
137
+ 3. Création des tables via create_sqlite_tables()
138
+ 4. Insertion des données via init_sqlite_data()
139
+
140
+ Side Effects:
141
+ - Modifie sys.path temporairement
142
+ - Remplace la fonction get_db_connection dans le module init_db
143
+ - Crée toutes les tables nécessaires
144
+ - Insère 9895 enregistrements de bâtiments
145
+ - Affiche des messages de statut
146
+
147
+ Performance :
148
+ - Opération d'initialisation unique au démarrage
149
+ - Toutes les données chargées en RAM pour accès rapide
150
+ - Optimisé pour le déploiement HuggingFace Spaces
151
+
152
+ Error Handling :
153
+ - Capture toutes les exceptions et les re-lève après logging
154
+ - Empêche le démarrage de l'application en cas d'échec
155
+ - Messages d'erreur détaillés pour debugging
156
+
157
+ Example Output:
158
+ ✅ Base de données SQLite en mémoire initialisée pour la production
159
+
160
+ Raises:
161
+ Exception: Toute erreur durant l'initialisation est propagée
162
+ après avoir été loggée
163
+
164
+ Note :
165
+ Cette fonction ne doit être appelée que depuis get_shared_sqlite_connection()
166
+ et uniquement en mode production.
167
+ """
168
+ try:
169
+ # Import des fonctions d'initialisation
170
+ sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", ".."))
171
+ # Remplacer temporairement la fonction get_db_connection
172
+ import init_db
173
+ from init_db import create_sqlite_tables, init_sqlite_data
174
+
175
+ init_db.get_db_connection = get_shared_sqlite_connection
176
+
177
+ # Créer les tables et insérer les données
178
+ create_sqlite_tables()
179
+ init_sqlite_data()
180
+
181
+ print("✅ Base de données SQLite en mémoire initialisée pour la production")
182
+
183
+ except Exception as e:
184
+ print(f"❌ Erreur lors de l'initialisation des données de production: {e}")
185
+ raise
186
+
187
+
188
+ # Créer le moteur de base de données avec configuration adaptative
189
+ if DATABASE_URL.startswith("sqlite"):
190
+ if ENVIRONMENT == "production":
191
+ print(
192
+ "🐳 Utilisation de SQLite en mémoire partagée pour la production HuggingFace"
193
+ )
194
+ # Créer un moteur qui utilise la connexion partagée
195
+ engine = create_engine(
196
+ "sqlite:///:memory:",
197
+ echo=False,
198
+ connect_args={"check_same_thread": False},
199
+ creator=get_shared_sqlite_connection,
200
+ )
201
+ else:
202
+ print("⚠️ Utilisation de SQLite pour les tests uniquement.")
203
+ # Configuration pour SQLite (tests)
204
+ engine = create_engine(
205
+ DATABASE_URL, echo=False, connect_args={"check_same_thread": False}
206
+ )
207
+ else:
208
+ # Configuration pour PostgreSQL (production normale)
209
+ engine = create_engine(
210
+ DATABASE_URL,
211
+ pool_size=20,
212
+ max_overflow=0,
213
+ pool_pre_ping=True,
214
+ echo=False, # Mettre à True pour voir les requêtes SQL
215
+ )
216
+
217
+ # Créer la session
218
+ SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
219
+
220
+ # Base pour nos modèles
221
+ Base = declarative_base()
222
+
223
+
224
+ def get_db():
225
+ """
226
+ Générateur de sessions de base de données pour injection de dépendance FastAPI.
227
+
228
+ Cette fonction implémente le pattern Dependency Injection de FastAPI pour
229
+ fournir des sessions de base de données aux routes. Elle garantit la fermeture
230
+ automatique des sessions même en cas d'exception.
231
+
232
+ Yields:
233
+ sqlalchemy.orm.Session: Session de base de données configurée et prête
234
+
235
+ Architecture :
236
+ - Pattern Generator pour gestion automatique du cycle de vie
237
+ - Utilise SessionLocal configuré selon l'environnement
238
+ - Fermeture garantie via bloc finally
239
+ - Compatible avec toutes les configurations (PostgreSQL/SQLite)
240
+
241
+ Usage dans FastAPI :
242
+ @app.get("/buildings/{building_id}")
243
+ def get_building(building_id: int, db: Session = Depends(get_db)):
244
+ return db.query(Building).filter(Building.id == building_id).first()
245
+
246
+ Thread Safety :
247
+ Chaque appel crée une nouvelle session isolée, garantissant
248
+ la thread-safety dans un environnement multi-threadé.
249
+
250
+ Performance :
251
+ - Sessions créées à la demande (lazy loading)
252
+ - Réutilisation du pool de connexions configuré
253
+ - Pas de connexions persistantes inutiles
254
+
255
+ Error Handling :
256
+ Les exceptions dans les routes n'empêchent pas la fermeture
257
+ de la session grâce au bloc finally.
258
+
259
+ Example:
260
+ # Utilisation directe (non recommandé)
261
+ for db in get_db():
262
+ buildings = db.query(Building).all()
263
+ break
264
+
265
+ # Utilisation recommandée avec FastAPI
266
+ @app.get("/predict")
267
+ def predict(data: PredictionRequest, db: Session = Depends(get_db)):
268
+ # db est automatiquement fournie et fermée
269
+ return prediction_service.predict(data, db)
270
+
271
+ Note :
272
+ Cette fonction est conçue pour être utilisée exclusivement comme
273
+ dépendance FastAPI. Pour un usage direct, considérer SessionLocal().
274
+ """
275
+ db = SessionLocal()
276
+ try:
277
+ yield db
278
+ finally:
279
+ db.close()
280
+
281
+
282
+ def create_tables():
283
+ """
284
+ Crée toutes les tables dans la base de données selon les modèles définis.
285
+
286
+ Cette fonction utilise SQLAlchemy pour créer automatiquement toutes les
287
+ tables nécessaires à l'application basées sur les métadonnées des modèles.
288
+ Elle est idempotente et peut être appelée multiple fois sans effet de bord.
289
+
290
+ Architecture :
291
+ - Utilise Base.metadata.create_all() pour création déclarative
292
+ - Compatible avec toutes les configurations de base de données
293
+ - Respecte les contraintes et index définis dans les modèles
294
+ - Opération idempotente (pas d'erreur si tables existent)
295
+
296
+ Tables créées :
297
+ - buildings : Table principale des bâtiments avec 17 attributs
298
+ - Autres tables selon les modèles définis dans project5.models
299
+
300
+ Side Effects :
301
+ - Crée les tables physiques dans la base de données
302
+ - Affiche un message de confirmation
303
+ - Met à jour le schéma de base de données
304
+
305
+ Usage :
306
+ # Lors du déploiement initial
307
+ from project5.database import create_tables
308
+ create_tables()
309
+
310
+ # Dans un script d'initialisation
311
+ if __name__ == "__main__":
312
+ create_tables()
313
+ print("Base de données initialisée")
314
+
315
+ Compatibilité :
316
+ - PostgreSQL : Utilise les types natifs et contraintes
317
+ - SQLite : Adaptation automatique des types
318
+ - Tous environnements : dev, test, production
319
+
320
+ Performance :
321
+ - Opération rapide sur bases vides
322
+ - Sur bases existantes : vérification de schéma uniquement
323
+ - Pas de perte de données sur tables existantes
324
+
325
+ Example Output :
326
+ Tables created successfully!
327
+
328
+ Error Handling :
329
+ Les erreurs SQLAlchemy sont propagées vers l'appelant
330
+ pour gestion appropriée selon le contexte.
331
+
332
+ Note :
333
+ Cette fonction doit être appelée après l'importation des modèles
334
+ pour garantir que toutes les métadonnées sont disponibles.
335
+ """
336
+ from project5.models.base import Base
337
+
338
+ Base.metadata.create_all(bind=engine)
339
+ print("Tables created successfully!")
src/project5/main.py ADDED
@@ -0,0 +1,605 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Point d'entrée principal de l'API de prédiction énergétique des bâtiments de Seattle.
3
+
4
+ Ce module configure et lance l'application FastAPI complète pour la prédiction
5
+ de consommation énergétique des bâtiments. Il inclut la gestion du cycle de vie,
6
+ la configuration des middlewares, la gestion d'erreurs centralisée, et l'intégration
7
+ du modèle de machine learning.
8
+
9
+ Architecture de l'application:
10
+ - FastAPI avec OpenAPI/Swagger automatique
11
+ - Middleware CORS pour les appels cross-origin
12
+ - Gestion centralisée des exceptions avec mapping HTTP
13
+ - Logging structuré des requêtes et réponses
14
+ - Chargement automatique du modèle ML au démarrage
15
+ - Configuration adaptative selon l'environnement
16
+
17
+ Environnements supportés:
18
+ - Développement: Base PostgreSQL locale, logging verbose
19
+ - Production: Base SQLite en mémoire (HuggingFace Spaces)
20
+ - Test: Base SQLite locale pour les tests
21
+
22
+ Fonctionnalités principales:
23
+ - API REST complète pour la gestion des bâtiments et prédictions
24
+ - Modèle ML RandomForestRegressor pour prédictions énergétiques
25
+ - Authentification bearer token en production
26
+ - Documentation interactive Swagger/ReDoc
27
+ - Monitoring avec health checks
28
+ - Gestion d'erreurs robuste avec logging
29
+
30
+ Sécurité:
31
+ - Authentication bearer token en environnement production
32
+ - Validation des données avec Pydantic
33
+ - Gestion sécurisée des erreurs (pas d'exposition de détails techniques)
34
+ - Configuration CORS appropriée
35
+
36
+ Performance:
37
+ - Pool de connexions PostgreSQL optimisé
38
+ - Cache du modèle ML en mémoire
39
+ - Logging asynchrone pour minimiser l'impact
40
+ - Middleware de timing des requêtes
41
+
42
+ Routes principales:
43
+ - /: Informations sur le service
44
+ - /health: Health check pour monitoring
45
+ - /docs: Documentation Swagger interactive
46
+ - /redoc: Documentation ReDoc alternative
47
+ - /neighborhoods/*: Gestion des quartiers de Seattle
48
+ - /building-models/*: Gestion des modèles de bâtiments
49
+ - /predictions/*: Prédictions énergétiques ML
50
+
51
+ Note:
52
+ Configuration adaptative pour déploiement sur HuggingFace Spaces
53
+ avec base SQLite en mémoire et données pré-initialisées.
54
+ """
55
+
56
+ import logging
57
+ import os
58
+ import time
59
+ from contextlib import asynccontextmanager
60
+
61
+ from fastapi import FastAPI, Request
62
+ from fastapi.middleware.cors import CORSMiddleware
63
+ from fastapi.openapi.utils import get_openapi
64
+ from fastapi.responses import JSONResponse
65
+ from sqlalchemy.exc import SQLAlchemyError
66
+
67
+ from project5.config import get_settings
68
+ from project5.database import Base, engine
69
+ from project5.schemas import ErrorResponse, ServiceInfo
70
+ from project5.utils.exceptions import NotFoundError, ServiceError, ValidationError
71
+ from project5.utils.ml_model import MLModel
72
+ from project5.utils.model_registry import get_ml_model, set_ml_model
73
+
74
+ # Configuration du logging
75
+ settings = get_settings()
76
+ logging.basicConfig(
77
+ level=getattr(logging, settings.log_level.upper()), format=settings.log_format
78
+ )
79
+ logger = logging.getLogger(__name__)
80
+
81
+ ML_MODEL_PATH = os.getenv("ML_MODEL_PATH", "../model/model.pkl")
82
+
83
+ ENVIRONMENT = os.getenv("ENVIRONMENT", "dev")
84
+
85
+
86
+ @asynccontextmanager
87
+ async def lifespan(app: FastAPI):
88
+ """
89
+ Gestionnaire du cycle de vie de l'application FastAPI.
90
+
91
+ Cette fonction gère les phases de démarrage et d'arrêt de l'application,
92
+ incluant l'initialisation de la base de données et le chargement du modèle ML.
93
+
94
+ Phases de démarrage:
95
+ 1. Création des tables de base de données
96
+ 2. Chargement du modèle ML depuis le fichier
97
+ 3. Enregistrement du modèle dans le registre global
98
+ 4. Validation que l'application est prête
99
+
100
+ Phases d'arrêt:
101
+ 1. Nettoyage des ressources
102
+ 2. Logging de l'arrêt
103
+
104
+ Args:
105
+ app (FastAPI): Instance de l'application FastAPI
106
+
107
+ Yields:
108
+ None: Contrôle rendu à l'application pendant son exécution
109
+
110
+ Raises:
111
+ RuntimeError: Si le modèle ML ne peut pas être chargé
112
+ Exception: Toute erreur durant l'initialisation
113
+
114
+ Environment Variables:
115
+ ML_MODEL_PATH: Chemin vers le fichier du modèle (défaut: "../model/model.pkl")
116
+
117
+ Note:
118
+ En cas d'échec de démarrage, l'application s'arrête immédiatement
119
+ pour éviter un état incohérent (fail-fast pattern).
120
+ """
121
+ # Startup
122
+ logger.info("🚀 Démarrage de l'application project5 API")
123
+
124
+ try:
125
+ # Créer les tables si nécessaire
126
+ Base.metadata.create_all(bind=engine)
127
+
128
+ """Gestionnaire du cycle de vie de l'application"""
129
+ # Startup
130
+ model = MLModel()
131
+ if not model.load_model(ML_MODEL_PATH):
132
+ raise RuntimeError("❌ Impossible de charger le modèle")
133
+ # Instance globale du modèle
134
+ set_ml_model(model)
135
+ logger.info(f"✅ Setting ML Model {get_ml_model()}")
136
+ logger.info("🎯 Application prête à recevoir des requêtes")
137
+
138
+ except Exception as e:
139
+ logger.error(f"❌ Erreur lors du démarrage: {e}")
140
+ raise
141
+
142
+ yield
143
+
144
+ # Shutdown
145
+ logger.info("🛑 Arrêt de l'application")
146
+
147
+
148
+ # Création de l'application FastAPI
149
+ app = FastAPI(
150
+ title=settings.app_name,
151
+ description="API REST pour la gestion énergétique des bâtiments",
152
+ version=settings.app_version,
153
+ lifespan=lifespan,
154
+ docs_url="/docs",
155
+ redoc_url="/redoc",
156
+ openapi_url="/openapi.json",
157
+ contact={
158
+ "name": "François Hellebuyck",
159
+ "email": "formation@pm.me",
160
+ },
161
+ license_info={
162
+ "name": "",
163
+ },
164
+ )
165
+
166
+
167
+ def custom_openapi():
168
+ """
169
+ Génère un schéma OpenAPI personnalisé avec configuration de sécurité.
170
+
171
+ Cette fonction étend le schéma OpenAPI par défaut pour ajouter :
172
+ - Configuration des serveurs (développement et production)
173
+ - Schéma d'authentification Bearer Token
174
+ - Application automatique de la sécurité à tous les endpoints
175
+
176
+ Returns:
177
+ dict: Schéma OpenAPI complet avec extensions de sécurité
178
+
179
+ Configuration des serveurs:
180
+ - Serveur local: http://localhost:8000 (développement)
181
+ - Serveur production: https://FrancoisFormation-project5.hf.space
182
+
183
+ Authentification:
184
+ - Type: HTTP Bearer Token
185
+ - Appliquée automatiquement à tous les endpoints
186
+ - Affichée dans l'interface Swagger pour les tests
187
+
188
+ Caching:
189
+ Le schéma est généré une seule fois et mis en cache
190
+ pour optimiser les performances.
191
+
192
+ Note:
193
+ Cette configuration permet l'authentification en production
194
+ tout en gardant une interface de test claire.
195
+ """
196
+ if app.openapi_schema:
197
+ return app.openapi_schema
198
+
199
+ openapi_schema = get_openapi(
200
+ title=app.title,
201
+ version=app.version,
202
+ description=app.description,
203
+ routes=app.routes,
204
+ )
205
+
206
+ openapi_schema["servers"] = [
207
+ {"url": "http://localhost:8000", "description": "Serveur de développement"},
208
+ {
209
+ "url": "https://FrancoisFormation-project5.hf.space",
210
+ "description": "Serveur de production",
211
+ },
212
+ ]
213
+
214
+ openapi_schema["components"]["securitySchemes"] = {
215
+ "BearerAuth": {"type": "http", "scheme": "bearer"}
216
+ }
217
+
218
+ for path, path_item in openapi_schema["paths"].items():
219
+ for method, operation in path_item.items():
220
+ if isinstance(operation, dict):
221
+ operation["security"] = [{"BearerAuth": []}]
222
+
223
+ app.openapi_schema = openapi_schema
224
+ return app.openapi_schema
225
+
226
+
227
+ app.openapi = custom_openapi
228
+
229
+ # Configuration CORS
230
+ app.add_middleware(
231
+ CORSMiddleware,
232
+ allow_origins=settings.cors_origins,
233
+ allow_credentials=settings.cors_allow_credentials,
234
+ allow_methods=settings.cors_allow_methods,
235
+ allow_headers=settings.cors_allow_headers,
236
+ )
237
+
238
+ # Inclusion des routers
239
+ from project5.routers import ( # Importer les routers ici
240
+ building_energy_prediction_routes,
241
+ building_model_routes,
242
+ building_types_routes,
243
+ categories_routes,
244
+ neighborhoods_routes,
245
+ prediction_routes,
246
+ properties_routes,
247
+ )
248
+
249
+ app.include_router(neighborhoods_routes.router)
250
+ app.include_router(building_types_routes.router)
251
+ app.include_router(categories_routes.router)
252
+ app.include_router(properties_routes.router)
253
+ app.include_router(building_energy_prediction_routes.router)
254
+ app.include_router(building_model_routes.router)
255
+ app.include_router(prediction_routes.router)
256
+
257
+
258
+ # Gestionnaires d'exceptions personnalisés
259
+ @app.exception_handler(NotFoundError)
260
+ async def neighborhood_not_found_handler(request: Request, exc: NotFoundError):
261
+ """
262
+ Gestionnaire d'exception pour les ressources non trouvées (404).
263
+
264
+ Transforme les NotFoundError en réponses HTTP 404 structurées
265
+ avec logging approprié et format de réponse standardisé.
266
+
267
+ Args:
268
+ request (Request): Requête FastAPI avec métadonnées (URL, méthode, etc.)
269
+ exc (NotFoundError): Exception personnalisée avec message et code d'erreur
270
+
271
+ Returns:
272
+ JSONResponse: Réponse HTTP 404 avec :
273
+ - detail: Message d'erreur descriptif
274
+ - error_code: Code d'erreur standardisé
275
+ - path: Chemin de l'endpoint qui a échoué
276
+
277
+ Logging:
278
+ Log niveau WARNING avec message et chemin pour traçabilité
279
+
280
+ Example response:
281
+ {
282
+ "detail": "Bâtiment avec l'ID 999 non trouvé",
283
+ "error_code": "NOT_FOUND",
284
+ "path": "/building-models/999"
285
+ }
286
+ """
287
+ logger.warning(f"Entité non trouvé: {exc.message} - Path: {request.url.path}")
288
+ return JSONResponse(
289
+ status_code=404,
290
+ content=ErrorResponse(
291
+ detail=exc.message, error_code=exc.error_code, path=str(request.url.path)
292
+ ).dict(),
293
+ )
294
+
295
+
296
+ @app.exception_handler(ValidationError)
297
+ async def validation_error_handler(request: Request, exc: ValidationError):
298
+ """
299
+ Gestionnaire d'exception pour les erreurs de validation métier (422).
300
+
301
+ Transforme les ValidationError en réponses HTTP 422 pour indiquer
302
+ que les données sont syntaxiquement correctes mais violent des
303
+ règles métier.
304
+
305
+ Args:
306
+ request (Request): Requête FastAPI avec contexte
307
+ exc (ValidationError): Exception de validation avec détails
308
+
309
+ Returns:
310
+ JSONResponse: Réponse HTTP 422 avec format standardisé
311
+
312
+ Usage:
313
+ Utilisé pour les validations métier qui ne peuvent pas être
314
+ exprimées dans les schémas Pydantic (règles complexes,
315
+ validations cross-champs, contraintes temporelles).
316
+
317
+ Distinction HTTP:
318
+ - 400: Erreur de format/syntaxe
319
+ - 422: Erreur de validation métier
320
+ - 404: Ressource non trouvée
321
+ """
322
+ logger.warning(f"Erreur de validation: {exc.message} - Path: {request.url.path}")
323
+ return JSONResponse(
324
+ status_code=422,
325
+ content=ErrorResponse(
326
+ detail=exc.message, error_code=exc.error_code, path=str(request.url.path)
327
+ ).dict(),
328
+ )
329
+
330
+
331
+ @app.exception_handler(ServiceError)
332
+ async def service_error_handler(request: Request, exc: ServiceError):
333
+ """
334
+ Gestionnaire d'exception pour les erreurs internes de service (500).
335
+
336
+ Transforme les ServiceError en réponses HTTP 500 avec masquage
337
+ des détails techniques pour la sécurité.
338
+
339
+ Args:
340
+ request (Request): Requête FastAPI avec contexte
341
+ exc (ServiceError): Exception de service avec détails internes
342
+
343
+ Returns:
344
+ JSONResponse: Réponse HTTP 500 avec message générique
345
+
346
+ Sécurité:
347
+ Le message détaillé est loggé mais pas exposé au client
348
+ pour éviter la fuite d'informations techniques.
349
+
350
+ Logging:
351
+ Log niveau ERROR avec stack trace complet pour debugging.
352
+
353
+ Monitoring:
354
+ Ces erreurs devraient déclencher des alertes de monitoring
355
+ car elles indiquent des problèmes techniques internes.
356
+ """
357
+ logger.error(f"Erreur de service: {exc.message} - Path: {request.url.path}")
358
+ return JSONResponse(
359
+ status_code=500,
360
+ content=ErrorResponse(
361
+ detail="Erreur interne du service",
362
+ error_code=exc.error_code,
363
+ path=str(request.url.path),
364
+ ).dict(),
365
+ )
366
+
367
+
368
+ @app.exception_handler(SQLAlchemyError)
369
+ async def sqlalchemy_error_handler(request: Request, exc: SQLAlchemyError):
370
+ """
371
+ Gestionnaire d'exception pour les erreurs de base de données (503).
372
+
373
+ Transforme les erreurs SQLAlchemy en réponses HTTP 503 pour indiquer
374
+ une indisponibilité temporaire du service de persistance.
375
+
376
+ Args:
377
+ request (Request): Requête FastAPI avec contexte
378
+ exc (SQLAlchemyError): Exception SQLAlchemy (connexion, transaction, etc.)
379
+
380
+ Returns:
381
+ JSONResponse: Réponse HTTP 503 (Service Unavailable)
382
+
383
+ Rationale HTTP 503:
384
+ Les erreurs de base de données sont souvent temporaires
385
+ (surcharge, maintenance, réseau) et peuvent être résolues
386
+ en retentant la requête plus tard.
387
+
388
+ Sécurité:
389
+ Détails techniques de la base masqués pour éviter l'exposition
390
+ d'informations sur la structure de données.
391
+
392
+ Monitoring:
393
+ Ces erreurs nécessitent une attention immédiate car elles
394
+ peuvent indiquer des problèmes d'infrastructure critiques.
395
+ """
396
+ logger.error(f"Erreur base de données: {str(exc)} - Path: {request.url.path}")
397
+ return JSONResponse(
398
+ status_code=503,
399
+ content=ErrorResponse(
400
+ detail="Service temporairement indisponible",
401
+ error_code="DATABASE_ERROR",
402
+ path=str(request.url.path),
403
+ ).dict(),
404
+ )
405
+
406
+
407
+ @app.exception_handler(Exception)
408
+ async def general_exception_handler(request: Request, exc: Exception):
409
+ """
410
+ Gestionnaire d'exception catch-all pour toutes les erreurs non capturées.
411
+
412
+ Ce gestionnaire sert de filet de sécurité pour capturer toutes les
413
+ exceptions non prévues et éviter les crashes de l'application.
414
+
415
+ Args:
416
+ request (Request): Requête FastAPI avec contexte complet
417
+ exc (Exception): Exception Python non capturée précédemment
418
+
419
+ Returns:
420
+ JSONResponse: Réponse HTTP 500 générique sécurisée
421
+
422
+ Logging:
423
+ Log niveau ERROR avec exc_info=True pour capturer la stack trace
424
+ complète, essentielle pour le debugging des erreurs inattendues.
425
+
426
+ Sécurité:
427
+ Aucun détail technique exposé au client pour éviter la fuite
428
+ d'informations potentiellement sensibles.
429
+
430
+ Monitoring:
431
+ Ces erreurs indiquent des bugs ou des cas limites non prévus
432
+ et nécessitent une investigation immédiate.
433
+
434
+ Note:
435
+ Ce gestionnaire est appelé en dernier recours après tous
436
+ les gestionnaires spécifiques.
437
+ """
438
+ logger.error(
439
+ f"Erreur non gérée: {str(exc)} - Path: {request.url.path}", exc_info=True
440
+ )
441
+ return JSONResponse(
442
+ status_code=500,
443
+ content=ErrorResponse(
444
+ detail="Erreur interne du serveur",
445
+ error_code="INTERNAL_ERROR",
446
+ path=str(request.url.path),
447
+ ).dict(),
448
+ )
449
+
450
+
451
+ # Middleware de logging des requêtes
452
+ @app.middleware("http")
453
+ async def log_requests(request: Request, call_next):
454
+ """
455
+ Middleware pour le logging structuré de toutes les requêtes HTTP.
456
+
457
+ Ce middleware capture et log toutes les requêtes entrantes et sortantes
458
+ avec timing et métriques pour le monitoring et le debugging.
459
+
460
+ Args:
461
+ request (Request): Requête HTTP entrante avec métadonnées
462
+ call_next: Fonction pour passer au middleware/route suivant
463
+
464
+ Returns:
465
+ Response: Réponse HTTP avec headers et timing ajoutés
466
+
467
+ Métriques capturées:
468
+ - Méthode HTTP (GET, POST, etc.)
469
+ - Chemin de l'endpoint
470
+ - Adresse IP du client (si disponible)
471
+ - Code de statut de réponse
472
+ - Temps de traitement en secondes
473
+
474
+ Format des logs:
475
+ - Entrée: ➡️ GET /neighborhoods - 192.168.1.100
476
+ - Sortie: ⬅️ GET /neighborhoods - Status: 200 - Time: 0.045s
477
+
478
+ Performance:
479
+ Impact minimal grâce au logging asynchrone et à la
480
+ mesure de temps optimisée.
481
+
482
+ Monitoring:
483
+ Ces logs sont essentiels pour l'analyse des performances,
484
+ le debugging des problèmes, et la détection d'anomalies.
485
+ """
486
+ start_time = time.time()
487
+
488
+ # Log de la requête entrante
489
+ logger.info(
490
+ f"➡️ {request.method} {request.url.path} - {request.client.host if request.client else 'unknown'}"
491
+ )
492
+
493
+ response = await call_next(request)
494
+
495
+ # Log de la réponse
496
+ process_time = time.time() - start_time
497
+ logger.info(
498
+ f"⬅️ {request.method} {request.url.path} - "
499
+ f"Status: {response.status_code} - "
500
+ f"Time: {process_time:.3f}s"
501
+ )
502
+
503
+ return response
504
+
505
+
506
+ # Routes principales
507
+ @app.get(
508
+ "/",
509
+ response_model=ServiceInfo,
510
+ tags=["Info"],
511
+ summary="Informations du service",
512
+ description="Informations générales sur l'API",
513
+ )
514
+ async def root():
515
+ """
516
+ Endpoint racine fournissant les informations du service.
517
+
518
+ Retourne les métadonnées essentielles sur l'API pour la découverte
519
+ automatique des services et la documentation dynamique.
520
+
521
+ Returns:
522
+ ServiceInfo: Informations structurées du service contenant :
523
+ - name: Nom de l'application depuis la configuration
524
+ - version: Version actuelle de l'API
525
+ - description: Description fonctionnelle du service
526
+ - endpoints: Points d'entrée principaux de documentation
527
+
528
+ Usage:
529
+ - Point d'entrée pour la découverte du service
530
+ - Validation que l'API est opérationnelle
531
+ - Navigation vers la documentation interactive
532
+
533
+ Example response:
534
+ {
535
+ "name": "Project5 API",
536
+ "version": "1.0.0",
537
+ "description": "API REST pour la gestion énergétique des bâtiments",
538
+ "endpoints": {
539
+ "docs": "/docs",
540
+ "redoc": "/redoc"
541
+ }
542
+ }
543
+
544
+ Note:
545
+ Endpoint public accessible sans authentification pour faciliter
546
+ la découverte et les vérifications de santé automatiques.
547
+ """
548
+ service_info = ServiceInfo(
549
+ name=settings.app_name,
550
+ version=settings.app_version,
551
+ description="API REST pour la gestion énergétique des bâtiments",
552
+ endpoints={
553
+ "docs": "/docs",
554
+ "redoc": "/redoc",
555
+ },
556
+ )
557
+
558
+ return service_info
559
+
560
+
561
+ @app.get(
562
+ "/health",
563
+ tags=["Info"],
564
+ summary="Vérification de l'état de santé",
565
+ description="Endpoint de santé pour les vérifications de disponibilité",
566
+ )
567
+ async def health_check():
568
+ """
569
+ Endpoint de health check pour le monitoring et l'orchestration.
570
+
571
+ Fournit un point de contrôle rapide pour vérifier que l'API
572
+ est opérationnelle et prête à traiter les requêtes.
573
+
574
+ Returns:
575
+ dict: Statut de santé avec métadonnées d'environnement :
576
+ - status: "healthy" si le service fonctionne
577
+ - environment: Environnement actuel (dev/production)
578
+ - authentication_required: Booléen selon l'environnement
579
+
580
+ Usage:
581
+ - Monitoring automatique par les systèmes d'orchestration
582
+ - Health checks Kubernetes/Docker
583
+ - Load balancer health probes
584
+ - Surveillance continue des services
585
+
586
+ Performance:
587
+ Endpoint ultra-rapide sans opération coûteuse (pas de DB/ML)
588
+ pour des vérifications fréquentes sans impact.
589
+
590
+ Example response:
591
+ {
592
+ "status": "healthy",
593
+ "environment": "production",
594
+ "authentication_required": true
595
+ }
596
+
597
+ Note:
598
+ Endpoint public accessible sans authentification pour permettre
599
+ les vérifications de santé par l'infrastructure.
600
+ """
601
+ return {
602
+ "status": "healthy",
603
+ "environment": ENVIRONMENT,
604
+ "authentication_required": ENVIRONMENT == "production",
605
+ }
src/project5/models/README.md ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ poetry add --group dev sqlacodegen
2
+ poetry run sqlacodegen postgresql://project5_user:project5_password@localhost:5432/project5_db --outfile models.py
src/project5/models/__init__.py ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Package des modèles SQLAlchemy pour l'API de prédiction énergétique.
3
+
4
+ Ce package contient tous les modèles de données utilisés par l'application
5
+ pour la gestion des bâtiments et des prédictions énergétiques.
6
+
7
+ Modèles disponibles :
8
+ - Base : Classe de base DeclarativeBase pour tous les modèles
9
+ - Neighborhood : Quartiers de Seattle pour la géolocalisation
10
+ - BuildingType : Types de bâtiments (résidentiel, commercial, etc.)
11
+ - Categorie : Catégories d'usage (bureaux, logement, éducation, etc.)
12
+ - Property : Propriétés d'usage spécifiques (office, restaurant, etc.)
13
+ - BuildingModels : Modèle principal des bâtiments avec toutes caractéristiques
14
+ - BuildingEnergyPredictions : Prédictions énergétiques calculées par le ML
15
+
16
+ Architecture relationnelle :
17
+ - Les bâtiments (BuildingModels) sont liés aux quartiers, types, et propriétés
18
+ - Les propriétés appartiennent à des catégories
19
+ - Les prédictions (BuildingEnergyPredictions) sont liées aux bâtiments
20
+ - Tous les modèles incluent des model_id pour l'encodage ML
21
+ """
22
+
23
+ # Import du modèle Neighborhood depuis le fichier approprié
24
+ from .base import Base
25
+ from .building_energy_prediction import BuildingEnergyPredictions
26
+ from .building_models import BuildingModels
27
+ from .building_type import BuildingType
28
+ from .categorie import Categorie
29
+ from .neighborhood import Neighborhood
30
+ from .property import Property
31
+
32
+ # Exposer les modèles au niveau du package
33
+ __all__ = [
34
+ "Base",
35
+ "Neighborhood",
36
+ "BuildingType",
37
+ "Categorie",
38
+ "Property",
39
+ "BuildingModels",
40
+ "BuildingEnergyPredictions",
41
+ ]
src/project5/models/base.py ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Base SQLAlchemy pour tous les modèles de données.
3
+
4
+ Ce module définit la classe de base DeclarativeBase utilisée par tous
5
+ les modèles SQLAlchemy de l'application de prédiction énergétique.
6
+ """
7
+
8
+ from sqlalchemy.orm import DeclarativeBase, registry
9
+
10
+ mapper_registry = registry()
11
+
12
+
13
+ class Base(DeclarativeBase):
14
+ """
15
+ Classe de base DeclarativeBase pour tous les modèles SQLAlchemy.
16
+
17
+ Cette classe sert de base à tous les modèles de données de l'application.
18
+ Elle utilise le registre SQLAlchemy pour gérer les mappings entre
19
+ les classes Python et les tables de base de données.
20
+
21
+ Note:
22
+ Tous les modèles (Neighborhood, BuildingType, Property, etc.)
23
+ héritent de cette classe pour bénéficier de la fonctionnalité ORM.
24
+ """
25
+
26
+ registry = mapper_registry
src/project5/models/building_energy_prediction.py ADDED
@@ -0,0 +1,88 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Modèle SQLAlchemy pour les prédictions énergétiques des bâtiments.
3
+
4
+ Ce module définit le modèle de données pour stocker les prédictions de consommation
5
+ énergétique calculées par le modèle de machine learning.
6
+ """
7
+
8
+ import datetime
9
+ import decimal
10
+ from typing import Optional
11
+
12
+ from sqlalchemy import (
13
+ Boolean,
14
+ DateTime,
15
+ ForeignKeyConstraint,
16
+ Index,
17
+ Integer,
18
+ Numeric,
19
+ PrimaryKeyConstraint,
20
+ UniqueConstraint,
21
+ text,
22
+ )
23
+ from sqlalchemy.orm import Mapped, mapped_column # TODO: ,relationship
24
+
25
+ from .base import Base
26
+
27
+
28
+ class BuildingEnergyPredictions(Base):
29
+ """
30
+ Modèle représentant une prédiction de consommation énergétique.
31
+
32
+ Cette table stocke les résultats des prédictions énergétiques calculées
33
+ par le modèle de machine learning pour chaque bâtiment. Elle permet de
34
+ conserver l'historique des prédictions et de suivre leur évolution.
35
+
36
+ Attributes:
37
+ id (int): Identifiant unique de la prédiction (clé primaire)
38
+ building_id (int): Identifiant du bâtiment concerné (clé étrangère)
39
+ site_energy_use_wn_kbtu (Decimal, optional): Consommation énergétique prédite en kBTU
40
+ avec normalisation météorologique
41
+ predicted (bool, optional): Indique si la valeur est prédite (True) ou mesurée (False)
42
+ updated_at (datetime, optional): Date et heure de la prédiction
43
+
44
+ Note:
45
+ La consommation est exprimée en kBTU (thousand British Thermal Units)
46
+ avec normalisation météorologique pour compenser les variations climatiques.
47
+ Le champ 'predicted' permet de distinguer les prédictions ML des mesures réelles.
48
+ """
49
+
50
+ __tablename__ = "building_energy_predictions"
51
+ __table_args__ = (
52
+ ForeignKeyConstraint(
53
+ ["building_id"],
54
+ ["building_models.id"],
55
+ name="fk_building_energy_predictions",
56
+ ),
57
+ PrimaryKeyConstraint("id", name="building_energy_predictions_pkey"),
58
+ UniqueConstraint(
59
+ "building_id", name="building_energy_predictions_building_id_key"
60
+ ),
61
+ Index("idx_building_energy_predictions_building_id", "building_id"),
62
+ {"comment": "Table des prédictions de consommation énergétique des bâtiments"},
63
+ )
64
+
65
+ id: Mapped[int] = mapped_column(
66
+ Integer,
67
+ primary_key=True,
68
+ comment="Identifiant unique de la prédiction (clé primaire auto-incrémentée)",
69
+ )
70
+ building_id: Mapped[int] = mapped_column(
71
+ Integer,
72
+ nullable=False,
73
+ comment="Identifiant du bâtiment concerné (clé étrangère vers buildings.id)",
74
+ )
75
+ site_energy_use_wn_kbtu: Mapped[Optional[decimal.Decimal]] = mapped_column(
76
+ Numeric(15, 2),
77
+ comment="Consommation énergétique du site en kBTU avec normalisation météorologique (15 chiffres, 2 décimales)",
78
+ )
79
+ predicted: Mapped[Optional[bool]] = mapped_column(
80
+ Boolean,
81
+ server_default=text("true"),
82
+ comment="Indique si la valeur est prédite (TRUE) ou mesurée (FALSE)",
83
+ )
84
+ updated_at: Mapped[Optional[datetime.datetime]] = mapped_column(
85
+ DateTime(True),
86
+ server_default=text("CURRENT_TIMESTAMP"),
87
+ comment="Date et heure de la prédiction avec fuseau horaire",
88
+ )
src/project5/models/building_models.py ADDED
@@ -0,0 +1,261 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Modèle SQLAlchemy principal pour les bâtiments.
3
+
4
+ Ce module définit le modèle de données central pour les bâtiments avec toutes
5
+ leurs caractéristiques utilisées pour les prédictions énergétiques.
6
+ """
7
+
8
+ import datetime
9
+ import decimal
10
+ from typing import TYPE_CHECKING, Optional
11
+
12
+ from sqlalchemy import (
13
+ CheckConstraint,
14
+ DateTime,
15
+ ForeignKeyConstraint,
16
+ Index,
17
+ Integer,
18
+ Numeric,
19
+ PrimaryKeyConstraint,
20
+ String,
21
+ Text,
22
+ UniqueConstraint,
23
+ text,
24
+ )
25
+ from sqlalchemy.orm import Mapped, mapped_column, relationship
26
+
27
+ from .base import Base
28
+
29
+ # from project5.models import BuildingType, Neighborhood, Property
30
+ # Import seulement pour le type checking
31
+ if TYPE_CHECKING:
32
+ from .building_type import BuildingType
33
+ from .neighborhood import Neighborhood
34
+ from .property import Property
35
+
36
+
37
+ class BuildingModels(Base):
38
+ """
39
+ Modèle principal représentant un bâtiment avec toutes ses caractéristiques.
40
+
41
+ Cette table centrale contient toutes les informations d'un bâtiment nécessaires
42
+ pour les prédictions énergétiques : localisation, caractéristiques physiques,
43
+ types d'énergie utilisés, et références vers les tables de classification.
44
+
45
+ Attributes:
46
+ id (int): Identifiant unique du bâtiment (clé primaire)
47
+ ose_building_id (int): Identifiant OSE (Office of Sustainability & Environment)
48
+ address (str): Adresse complète du bâtiment
49
+ city (str, optional): Ville (généralement Seattle)
50
+ state (str, optional): État (généralement WA)
51
+ zip_code (str, optional): Code postal
52
+ tax_parcel_identification_number (str, optional): Numéro de parcelle fiscale
53
+ council_district_code (str, optional): Code du district municipal
54
+ latitude (Decimal, optional): Latitude GPS (-90 à 90)
55
+ longitude (Decimal, optional): Longitude GPS (-180 à 180)
56
+ year_built (int, optional): Année de construction
57
+ number_of_buildings (int, optional): Nombre de bâtiments sur la propriété
58
+ number_of_floors (int, optional): Nombre d'étages
59
+ property_gfa_total (Decimal, optional): Surface brute totale en pieds carrés
60
+ property_gfa_parking (Decimal, optional): Surface de parking en pieds carrés
61
+ second_largest_property_use_type_gfa (Decimal, optional): Surface du 2e usage
62
+ third_largest_property_use_type_gfa (Decimal, optional): Surface du 3e usage
63
+ multiusage (bool, optional): Indique si le bâtiment a plusieurs usages
64
+ steam (bool, optional): Utilise la vapeur comme source d'énergie
65
+ electricity (bool, optional): Utilise l'électricité
66
+ natural_gas (bool, optional): Utilise le gaz naturel
67
+ neighborhood_id (int, optional): Identifiant du quartier (clé étrangère)
68
+ building_type_id (int, optional): Identifiant du type de bâtiment (clé étrangère)
69
+ largest_property_use_type_id (int, optional): Usage principal (clé étrangère)
70
+ primary_property_type_id (int, optional): Type principal (clé étrangère)
71
+ second_largest_property_use_type_id (int, optional): Usage secondaire (clé étrangère)
72
+ third_largest_property_use_type_id (int, optional): Usage tertiaire (clé étrangère)
73
+ created_at (datetime): Date de création
74
+ updated_at (datetime): Date de modification
75
+
76
+ Relations:
77
+ neighborhood: Quartier associé
78
+ building_type: Type de bâtiment associé
79
+ largest_property_use_type: Usage principal
80
+ primary_property_type: Type principal
81
+ second_largest_property_use_type: Usage secondaire
82
+ third_largest_property_use_type: Usage tertiaire
83
+
84
+ Note:
85
+ Ce modèle est au centre du système de prédiction énergétique.
86
+ Toutes les caractéristiques sont utilisées par le modèle ML pour
87
+ calculer la consommation énergétique prédite.
88
+ """
89
+
90
+ __tablename__ = "building_models"
91
+ __table_args__ = (
92
+ CheckConstraint(
93
+ "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",
94
+ name="valid_coordinates",
95
+ ),
96
+ ForeignKeyConstraint(
97
+ ["building_type_id"], ["building_type.id"], name="fk_building_type"
98
+ ),
99
+ ForeignKeyConstraint(
100
+ ["largest_property_use_type_id"],
101
+ ["property.id"],
102
+ name="fk_largest_property_use_type_id",
103
+ ),
104
+ ForeignKeyConstraint(
105
+ ["neighborhood_id"], ["neighborhood.id"], name="fk_neighborhood"
106
+ ),
107
+ ForeignKeyConstraint(
108
+ ["primary_property_type_id"],
109
+ ["property.id"],
110
+ name="fk_primary_property_type",
111
+ ),
112
+ ForeignKeyConstraint(
113
+ ["second_largest_property_use_type_id"],
114
+ ["property.id"],
115
+ name="fk_second_property_type",
116
+ ),
117
+ ForeignKeyConstraint(
118
+ ["third_largest_property_use_type_id"],
119
+ ["property.id"],
120
+ name="fk_third_property_type",
121
+ ),
122
+ PrimaryKeyConstraint("id", name="building_models_pkey"),
123
+ UniqueConstraint("ose_building_id", name="building_models_ose_building_id_key"),
124
+ Index("idx_building_models_building_type_id", "building_type_id"),
125
+ Index("idx_building_models_neighborhood_id", "neighborhood_id"),
126
+ Index(
127
+ "idx_building_models_primary_property_type_id", "primary_property_type_id"
128
+ ),
129
+ Index(
130
+ "idx_building_models_second_property_use_type_id",
131
+ "second_largest_property_use_type_id",
132
+ ),
133
+ Index(
134
+ "idx_building_models_third_property_use_type_id",
135
+ "third_largest_property_use_type_id",
136
+ ),
137
+ Index("idx_largest_property_use_type_id", "largest_property_use_type_id"),
138
+ {
139
+ "comment": "Table contenant les données des modèles de bâtiments avec "
140
+ "informations énergétiques et structurelles"
141
+ },
142
+ )
143
+
144
+ id: Mapped[int] = mapped_column(
145
+ Integer,
146
+ primary_key=True,
147
+ comment="Identifiant du bâtiment concerné (clé étrangère vers buildings.id)",
148
+ )
149
+ ose_building_id: Mapped[str] = mapped_column(
150
+ String(50),
151
+ nullable=False,
152
+ comment="Identifiant unique du bâtiment dans le système OSE",
153
+ )
154
+ address: Mapped[Optional[str]] = mapped_column(Text, comment="Adresse du bâtiment")
155
+ city: Mapped[Optional[str]] = mapped_column(
156
+ String(100),
157
+ server_default=text("'Seattle'::character varying"),
158
+ comment="Ville",
159
+ )
160
+ state: Mapped[Optional[str]] = mapped_column(
161
+ String(10), server_default=text("'WA'::character varying"), comment="État"
162
+ )
163
+ zip_code: Mapped[Optional[str]] = mapped_column(String(20), comment="Code postal")
164
+ tax_parcel_identification_number: Mapped[Optional[str]] = mapped_column(
165
+ String(100), comment="Numéro d'identification fiscale de la parcelle"
166
+ )
167
+ council_district_code: Mapped[Optional[str]] = mapped_column(String(20))
168
+ latitude: Mapped[Optional[decimal.Decimal]] = mapped_column(
169
+ Numeric(10, 8), comment="Latitude géographique"
170
+ )
171
+ longitude: Mapped[Optional[decimal.Decimal]] = mapped_column(
172
+ Numeric(11, 8), comment="Longitude géographique"
173
+ )
174
+ year_built: Mapped[Optional[int]] = mapped_column(
175
+ Integer, comment="Année de construction du bâtiment"
176
+ )
177
+ number_of_buildings: Mapped[Optional[int]] = mapped_column(
178
+ Integer, comment="Nombre de bâtiments"
179
+ )
180
+ number_of_floors: Mapped[Optional[int]] = mapped_column(
181
+ Integer, comment="Nombre d'étages"
182
+ )
183
+ property_gfa_total: Mapped[Optional[decimal.Decimal]] = mapped_column(
184
+ Numeric(12, 2), comment="Surface brute totale (GFA) en pieds carrés"
185
+ )
186
+ property_gfa_parking: Mapped[Optional[decimal.Decimal]] = mapped_column(
187
+ Numeric(12, 2), comment="Surface brute de parking en pieds carrés"
188
+ )
189
+ second_largest_property_use_type_gfa: Mapped[Optional[decimal.Decimal]] = (
190
+ mapped_column(
191
+ Numeric(12, 2), comment="Surface GFA du deuxième type d'usage principal"
192
+ )
193
+ )
194
+ third_largest_property_use_type_gfa: Mapped[Optional[decimal.Decimal]] = (
195
+ mapped_column(
196
+ Numeric(12, 2), comment="Surface GFA du troisième type d'usage principal"
197
+ )
198
+ )
199
+ multiusage: Mapped[Optional[int]] = mapped_column(
200
+ Integer, comment="Indicateur si le bâtiment a plusieurs usages"
201
+ )
202
+ steam: Mapped[Optional[int]] = mapped_column(
203
+ Integer, comment="Indicateur d'utilisation de vapeur"
204
+ )
205
+ electricity: Mapped[Optional[int]] = mapped_column(
206
+ Integer, comment="Indicateur d'utilisation d'électricité"
207
+ )
208
+ natural_gas: Mapped[Optional[int]] = mapped_column(
209
+ Integer, comment="Indicateur d'utilisation de gaz naturel"
210
+ )
211
+ neighborhood_id: Mapped[Optional[int]] = mapped_column(
212
+ Integer, comment="Identifiant du quartier"
213
+ )
214
+ building_type_id: Mapped[Optional[int]] = mapped_column(
215
+ Integer, comment="Identifiant du type de bâtiment"
216
+ )
217
+ largest_property_use_type_id: Mapped[Optional[int]] = mapped_column(
218
+ Integer, comment="ID du type d'usage principal le plus important"
219
+ )
220
+ primary_property_type_id: Mapped[Optional[int]] = mapped_column(
221
+ Integer, comment="ID du type de propriété principal"
222
+ )
223
+ second_largest_property_use_type_id: Mapped[Optional[int]] = mapped_column(
224
+ Integer, comment="ID du deuxième type d'usage principal"
225
+ )
226
+ third_largest_property_use_type_id: Mapped[Optional[int]] = mapped_column(
227
+ Integer, comment="ID du troisième type d'usage principal"
228
+ )
229
+ created_at: Mapped[Optional[datetime.datetime]] = mapped_column(
230
+ DateTime, server_default=text("CURRENT_TIMESTAMP")
231
+ )
232
+ updated_at: Mapped[Optional[datetime.datetime]] = mapped_column(
233
+ DateTime, server_default=text("CURRENT_TIMESTAMP")
234
+ )
235
+
236
+ building_type: Mapped[Optional["BuildingType"]] = relationship(
237
+ "BuildingType", back_populates="building_models"
238
+ )
239
+ largest_property_use_type: Mapped[Optional["Property"]] = relationship(
240
+ "Property",
241
+ foreign_keys=[largest_property_use_type_id],
242
+ back_populates="building_models",
243
+ )
244
+ neighborhood: Mapped[Optional["Neighborhood"]] = relationship(
245
+ "Neighborhood", back_populates="building_models"
246
+ )
247
+ primary_property_type: Mapped[Optional["Property"]] = relationship(
248
+ "Property",
249
+ foreign_keys=[primary_property_type_id],
250
+ back_populates="building_models_",
251
+ )
252
+ second_largest_property_use_type: Mapped[Optional["Property"]] = relationship(
253
+ "Property",
254
+ foreign_keys=[second_largest_property_use_type_id],
255
+ back_populates="building_models1",
256
+ )
257
+ third_largest_property_use_type: Mapped[Optional["Property"]] = relationship(
258
+ "Property",
259
+ foreign_keys=[third_largest_property_use_type_id],
260
+ back_populates="building_models2",
261
+ )
src/project5/models/building_type.py ADDED
@@ -0,0 +1,95 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Modèle SQLAlchemy pour les types de bâtiments.
3
+
4
+ Ce module définit le modèle de données pour les types de bâtiments utilisés
5
+ dans la classification des constructions pour les prédictions énergétiques.
6
+ """
7
+
8
+ import datetime
9
+ from typing import TYPE_CHECKING, Optional
10
+
11
+ from sqlalchemy import (
12
+ DateTime,
13
+ Integer,
14
+ PrimaryKeyConstraint,
15
+ String,
16
+ Text,
17
+ UniqueConstraint,
18
+ text,
19
+ )
20
+ from sqlalchemy.orm import Mapped, mapped_column, relationship
21
+
22
+ if TYPE_CHECKING:
23
+ from .building_models import BuildingModels
24
+
25
+ from .base import Base
26
+
27
+
28
+ class BuildingType(Base):
29
+ """
30
+ Modèle représentant un type de bâtiment.
31
+
32
+ Les types de bâtiments sont utilisés pour catégoriser les constructions
33
+ selon leur structure et usage général (résidentiel, commercial, industriel, etc.).
34
+ Ils sont essentiels pour les prédictions énergétiques car ils déterminent
35
+ les caractéristiques de consommation typiques.
36
+
37
+ Attributes:
38
+ id (int): Identifiant unique du type de bâtiment (clé primaire)
39
+ model_id (int): Identifiant utilisé par le modèle ML pour l'encodage
40
+ building_type_name (str): Nom du type (CAMPUS, NONRESIDENTIAL, etc.)
41
+ description (str, optional): Description détaillée du type
42
+ created_at (datetime): Date de création de l'enregistrement
43
+ updated_at (datetime): Date de dernière modification
44
+ building_models (list[BuildingModels]): Bâtiments de ce type
45
+
46
+ Note:
47
+ Le model_id est utilisé par le modèle de machine learning pour encoder
48
+ numériquement le type de bâtiment lors des prédictions énergétiques.
49
+ """
50
+
51
+ __tablename__ = "building_type"
52
+ __table_args__ = (
53
+ PrimaryKeyConstraint("id", name="building_type_pkey"),
54
+ UniqueConstraint(
55
+ "building_type_name", name="building_type_building_type_name_key"
56
+ ),
57
+ UniqueConstraint("model_id", name="building_type_model_id_key"),
58
+ {
59
+ "comment": "Table de référence des types de bâtiments pour la classification "
60
+ "des constructions"
61
+ },
62
+ )
63
+
64
+ id: Mapped[int] = mapped_column(
65
+ Integer,
66
+ primary_key=True,
67
+ comment="Identifiant unique du type de bâtiment (clé primaire auto-incrémentée)",
68
+ )
69
+ model_id: Mapped[int] = mapped_column(
70
+ Integer,
71
+ nullable=False,
72
+ comment="Identifiant au modèle associé (clé unique, obligatoire)",
73
+ )
74
+ building_type_name: Mapped[str] = mapped_column(
75
+ String(100),
76
+ nullable=False,
77
+ comment="Nom du type de bâtiment (unique, obligatoire, max 100 caractères)",
78
+ )
79
+ description: Mapped[Optional[str]] = mapped_column(
80
+ Text, comment="Description détaillée du type de bâtiment (optionnel)"
81
+ )
82
+ created_at: Mapped[Optional[datetime.datetime]] = mapped_column(
83
+ DateTime,
84
+ server_default=text("CURRENT_TIMESTAMP"),
85
+ comment="Date et heure de création de l'enregistrement",
86
+ )
87
+ updated_at: Mapped[Optional[datetime.datetime]] = mapped_column(
88
+ DateTime,
89
+ server_default=text("CURRENT_TIMESTAMP"),
90
+ comment="Date et heure de maj de l'enregistrement",
91
+ )
92
+
93
+ building_models: Mapped[list["BuildingModels"]] = relationship(
94
+ "BuildingModels", back_populates="building_type"
95
+ )
src/project5/models/categorie.py ADDED
@@ -0,0 +1,84 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Modèle SQLAlchemy pour les catégories de propriétés.
3
+
4
+ Ce module définit le modèle de données pour les catégories d'usage des bâtiments
5
+ utilisées dans la classification pour les prédictions énergétiques.
6
+ """
7
+
8
+ import datetime
9
+ from typing import Optional
10
+
11
+ from sqlalchemy import (
12
+ DateTime,
13
+ Integer,
14
+ PrimaryKeyConstraint,
15
+ String,
16
+ Text,
17
+ UniqueConstraint,
18
+ text,
19
+ )
20
+ from sqlalchemy.orm import Mapped, mapped_column # TODO:, relationship
21
+
22
+ from .base import Base
23
+
24
+
25
+ class Categorie(Base):
26
+ """
27
+ Modèle représentant une catégorie d'usage de propriété.
28
+
29
+ Les catégories regroupent les différents usages des bâtiments par domaine
30
+ d'activité (résidentiel, commercial, bureaux, éducation, santé, etc.).
31
+ Elles servent à classifier les propriétés pour les prédictions énergétiques.
32
+
33
+ Attributes:
34
+ id (int): Identifiant unique de la catégorie (clé primaire)
35
+ category_code (str): Code court de la catégorie (OFFICE, RESIDENTIAL, etc.)
36
+ category_name (str): Nom complet en français (Bureaux, Résidentiel, etc.)
37
+ description (str, optional): Description détaillée de la catégorie
38
+ created_at (datetime): Date de création de l'enregistrement
39
+ updated_at (datetime): Date de dernière modification
40
+
41
+ Note:
42
+ Les catégories sont liées aux propriétés (Property) qui sont plus
43
+ granulaires et utilisées directement par le modèle ML.
44
+ """
45
+
46
+ __tablename__ = "categories"
47
+ __table_args__ = (
48
+ PrimaryKeyConstraint("id", name="categories_pkey"),
49
+ UniqueConstraint("category_code", name="categories_category_code_key"),
50
+ {"comment": "Table de référence des catégories de bâtiments"},
51
+ )
52
+
53
+ id: Mapped[int] = mapped_column(
54
+ Integer,
55
+ primary_key=True,
56
+ comment="Identifiant unique de la catégorie (clé primaire auto-incrémentée)",
57
+ )
58
+ category_code: Mapped[str] = mapped_column(
59
+ String(50),
60
+ nullable=False,
61
+ comment="Code unique de la catégorie (identifiant court, max 50 caractères)",
62
+ )
63
+ category_name: Mapped[str] = mapped_column(
64
+ String(100),
65
+ nullable=False,
66
+ comment="Nom complet de la catégorie (obligatoire, max 100 caractères)",
67
+ )
68
+ description: Mapped[Optional[str]] = mapped_column(
69
+ Text, comment="Description détaillée de la catégorie (optionnel)"
70
+ )
71
+ created_at: Mapped[Optional[datetime.datetime]] = mapped_column(
72
+ DateTime,
73
+ server_default=text("CURRENT_TIMESTAMP"),
74
+ comment="Date et heure de création de l'enregistrement",
75
+ )
76
+ updated_at: Mapped[Optional[datetime.datetime]] = mapped_column(
77
+ DateTime,
78
+ server_default=text("CURRENT_TIMESTAMP"),
79
+ comment="Date et heure de maj de l'enregistrement",
80
+ )
81
+
82
+ # TODO: property: Mapped[list["Property"]] = relationship(
83
+ # "Property", back_populates="category"
84
+ # )
src/project5/models/neighborhood.py ADDED
@@ -0,0 +1,90 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Modèle SQLAlchemy pour les quartiers de Seattle.
3
+
4
+ Ce module définit le modèle de données pour les quartiers (neighborhoods) utilisés
5
+ dans la classification géographique des bâtiments pour les prédictions énergétiques.
6
+ """
7
+
8
+ import datetime
9
+ from typing import TYPE_CHECKING, Optional
10
+
11
+ from sqlalchemy import (
12
+ DateTime,
13
+ Integer,
14
+ PrimaryKeyConstraint,
15
+ String,
16
+ UniqueConstraint,
17
+ text,
18
+ )
19
+ from sqlalchemy.orm import Mapped, mapped_column, relationship
20
+
21
+ if TYPE_CHECKING:
22
+ from .building_models import BuildingModels
23
+
24
+ from .base import Base
25
+
26
+
27
+ class Neighborhood(Base):
28
+ """
29
+ Modèle représentant un quartier de Seattle.
30
+
31
+ Les quartiers sont utilisés pour la géolocalisation et la classification
32
+ géographique des bâtiments. Ils influencent les prédictions énergétiques
33
+ car différents quartiers ont des caractéristiques climatiques et urbaines
34
+ distinctes.
35
+
36
+ Attributes:
37
+ id (int): Identifiant unique du quartier (clé primaire)
38
+ neighborhood_name (str): Nom du quartier (BALLARD, DOWNTOWN, etc.)
39
+ model_id (int): Identifiant utilisé par le modèle ML pour l'encodage
40
+ created_at (datetime): Date de création de l'enregistrement
41
+ updated_at (datetime): Date de dernière modification
42
+ building_models (list[BuildingModels]): Bâtiments associés à ce quartier
43
+
44
+ Note:
45
+ Le model_id est utilisé par le modèle de machine learning pour encoder
46
+ numériquement le quartier lors des prédictions énergétiques.
47
+ """
48
+
49
+ __tablename__ = "neighborhood"
50
+ __table_args__ = (
51
+ PrimaryKeyConstraint("id", name="neighborhood_pkey"),
52
+ UniqueConstraint("model_id", name="neighborhood_model_id_key"),
53
+ UniqueConstraint(
54
+ "neighborhood_name", name="neighborhood_neighborhood_name_key"
55
+ ),
56
+ {
57
+ "comment": "Table des quartiers/arrondissements pour la classification "
58
+ "géographique des bâtiments"
59
+ },
60
+ )
61
+
62
+ id: Mapped[int] = mapped_column(
63
+ Integer,
64
+ primary_key=True,
65
+ comment="Identifiant unique du quartier (clé primaire auto-incrémentée)",
66
+ )
67
+ neighborhood_name: Mapped[str] = mapped_column(
68
+ String(50),
69
+ nullable=False,
70
+ comment="Nom du quartier (unique, obligatoire, max 50 caractères)",
71
+ )
72
+ model_id: Mapped[int] = mapped_column(
73
+ Integer,
74
+ nullable=False,
75
+ comment="Identifiant au modèle associé (clé unique, obligatoire)",
76
+ )
77
+ created_at: Mapped[Optional[datetime.datetime]] = mapped_column(
78
+ DateTime,
79
+ server_default=text("CURRENT_TIMESTAMP"),
80
+ comment="Date et heure de création de l'enregistrement",
81
+ )
82
+ updated_at: Mapped[Optional[datetime.datetime]] = mapped_column(
83
+ DateTime,
84
+ server_default=text("CURRENT_TIMESTAMP"),
85
+ comment="Date et heure de maj de l'enregistrement",
86
+ )
87
+
88
+ building_models: Mapped[list["BuildingModels"]] = relationship(
89
+ "BuildingModels", back_populates="neighborhood"
90
+ )
src/project5/models/property.py ADDED
@@ -0,0 +1,119 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Modèle SQLAlchemy pour les propriétés d'usage des bâtiments.
3
+
4
+ Ce module définit le modèle de données pour les propriétés d'usage spécifiques
5
+ des bâtiments, utilisées pour une classification fine dans les prédictions énergétiques.
6
+ """
7
+
8
+ import datetime
9
+ from typing import TYPE_CHECKING, Optional
10
+
11
+ from sqlalchemy import (
12
+ DateTime,
13
+ ForeignKeyConstraint,
14
+ Index,
15
+ Integer,
16
+ PrimaryKeyConstraint,
17
+ String,
18
+ UniqueConstraint,
19
+ text,
20
+ )
21
+ from sqlalchemy.orm import Mapped, mapped_column, relationship
22
+
23
+ if TYPE_CHECKING:
24
+ from .building_models import BuildingModels
25
+
26
+ from .base import Base
27
+
28
+
29
+ class Property(Base):
30
+ """
31
+ Modèle représentant une propriété d'usage spécifique d'un bâtiment.
32
+
33
+ Les propriétés sont plus granulaires que les catégories et permettent
34
+ une classification fine des usages (OFFICE, RETAIL STORE, RESTAURANT, etc.).
35
+ Elles sont directement utilisées par le modèle ML pour les prédictions
36
+ énergétiques car différents usages ont des profils de consommation distincts.
37
+
38
+ Attributes:
39
+ id (int): Identifiant unique de la propriété (clé primaire)
40
+ model_id (int): Identifiant utilisé par le modèle ML pour l'encodage
41
+ property_name (str): Nom de la propriété (OFFICE, RESTAURANT, etc.)
42
+ category_id (int): Identifiant de la catégorie parent (clé étrangère)
43
+ created_at (datetime): Date de création de l'enregistrement
44
+ updated_at (datetime): Date de dernière modification
45
+ building_models (list[BuildingModels]): Bâtiments avec cette propriété principale
46
+ building_models_ (list[BuildingModels]): Bâtiments avec cette propriété primaire
47
+ building_models1 (list[BuildingModels]): Bâtiments avec cette propriété secondaire
48
+ building_models2 (list[BuildingModels]): Bâtiments avec cette propriété tertiaire
49
+
50
+ Note:
51
+ Le model_id est utilisé directement par le modèle de machine learning
52
+ pour encoder numériquement l'usage lors des prédictions énergétiques.
53
+ Les relations multiples permettent de gérer les bâtiments multi-usages.
54
+ """
55
+
56
+ __tablename__ = "property"
57
+ __table_args__ = (
58
+ ForeignKeyConstraint(
59
+ ["category_id"], ["categories.id"], name="fk_property_category"
60
+ ),
61
+ PrimaryKeyConstraint("id", name="property_pkey"),
62
+ UniqueConstraint("model_id", name="property_model_id_key"),
63
+ UniqueConstraint("property_name", name="property_property_name_key"),
64
+ Index("idx_property_category", "category_id"),
65
+ {"comment": "Table des propriétés/bâtiments avec leur catégorie associée"},
66
+ )
67
+
68
+ id: Mapped[int] = mapped_column(
69
+ Integer,
70
+ primary_key=True,
71
+ comment="Identifiant unique de la propriété (clé primaire auto-incrémentée)",
72
+ )
73
+ model_id: Mapped[int] = mapped_column(
74
+ Integer,
75
+ nullable=False,
76
+ comment="Identifiant au modèle associé (clé unique, obligatoire)",
77
+ )
78
+ property_name: Mapped[str] = mapped_column(
79
+ String(150),
80
+ nullable=False,
81
+ comment="Nom de la propriété (unique, obligatoire, max 150 caractères)",
82
+ )
83
+ category_id: Mapped[int] = mapped_column(
84
+ Integer,
85
+ nullable=False,
86
+ comment="Identifiant de la catégorie associée (clé étrangère vers categories.id)",
87
+ )
88
+ created_at: Mapped[Optional[datetime.datetime]] = mapped_column(
89
+ DateTime,
90
+ server_default=text("CURRENT_TIMESTAMP"),
91
+ comment="Date et heure de création de l'enregistrement",
92
+ )
93
+ updated_at: Mapped[Optional[datetime.datetime]] = mapped_column(
94
+ DateTime,
95
+ server_default=text("CURRENT_TIMESTAMP"),
96
+ comment="Date et heure de maj de l'enregistrement",
97
+ )
98
+
99
+ # category: Mapped['Categories'] = relationship('Categories', back_populates='property')
100
+ building_models: Mapped[list["BuildingModels"]] = relationship(
101
+ "BuildingModels",
102
+ foreign_keys="[BuildingModels.largest_property_use_type_id]",
103
+ back_populates="largest_property_use_type",
104
+ )
105
+ building_models_: Mapped[list["BuildingModels"]] = relationship(
106
+ "BuildingModels",
107
+ foreign_keys="[BuildingModels.primary_property_type_id]",
108
+ back_populates="primary_property_type",
109
+ )
110
+ building_models1: Mapped[list["BuildingModels"]] = relationship(
111
+ "BuildingModels",
112
+ foreign_keys="[BuildingModels.second_largest_property_use_type_id]",
113
+ back_populates="second_largest_property_use_type",
114
+ )
115
+ building_models2: Mapped[list["BuildingModels"]] = relationship(
116
+ "BuildingModels",
117
+ foreign_keys="[BuildingModels.third_largest_property_use_type_id]",
118
+ back_populates="third_largest_property_use_type",
119
+ )
src/project5/routers/__init__.py ADDED
File without changes
src/project5/routers/building_energy_prediction_routes.py ADDED
@@ -0,0 +1,334 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Routes API pour la gestion des prédictions énergétiques des bâtiments.
3
+
4
+ Ce module contient les endpoints FastAPI pour :
5
+ - Récupération de la liste des prédictions énergétiques avec pagination
6
+ - Récupération d'une prédiction spécifique par ID
7
+ - Gestion des erreurs et logging approprié
8
+
9
+ Endpoints disponibles :
10
+ - GET /api/v1/predictions/ : Liste paginée des prédictions
11
+ - GET /api/v1/predictions/{id} : Prédiction spécifique par ID
12
+ - POST /api/v1/predictions/building/{building_id} : Effectuer une prédiction pour un bâtiment et la sauvegarder
13
+
14
+ Auteur: François Hellebuyck
15
+ Projet: Building Energy Prediction API - OpenClassrooms Projet 5
16
+ """
17
+
18
+ import logging
19
+ from typing import List
20
+
21
+ from fastapi import APIRouter, Depends, HTTPException, Query, status
22
+ from fastapi.encoders import jsonable_encoder
23
+ from fastapi.responses import JSONResponse
24
+ from sqlalchemy.orm import Session
25
+
26
+ from project5.database import get_db
27
+ from project5.schemas.building_energy_prediction import (
28
+ BuildingEnergyPredictionsList,
29
+ BuildingEnergyPredictionsResponse,
30
+ )
31
+ from project5.schemas.building_model import PredictionResponse
32
+ from project5.services.building_ensergy_prediction_service import (
33
+ BuildingEnergyPredictionService,
34
+ )
35
+ from project5.services.building_model_service import (
36
+ BuildingModelsService,
37
+ PredictionService,
38
+ )
39
+ from project5.utils.exceptions import NotFoundError
40
+
41
+ logger = logging.getLogger(__name__)
42
+
43
+ router = APIRouter(prefix="/api/v1/predictions", tags=["predictions"])
44
+
45
+
46
+ @router.get(
47
+ "/",
48
+ response_model=List[BuildingEnergyPredictionsList],
49
+ summary="Récupérer toutes les prédictions énergétiques des bâtiments",
50
+ description="""
51
+ Retourne la liste paginée de toutes les prédictions énergétiques des bâtiments.
52
+
53
+ Cette endpoint permet de récupérer l'ensemble des prédictions de consommation
54
+ énergétique calculées par le modèle de machine learning. Les résultats incluent
55
+ les données du bâtiment associé et la valeur prédite.
56
+
57
+ Paramètres de pagination :
58
+ - skip : nombre d'enregistrements à ignorer (par défaut 0)
59
+ - limit : nombre maximum d'enregistrements à retourner (par défaut 100, max 1000)
60
+ """,
61
+ responses={
62
+ 200: {
63
+ "description": "Liste des prédictions récupérée avec succès",
64
+ "content": {
65
+ "application/json": {
66
+ "example": [
67
+ {
68
+ "id": 1,
69
+ "building_id": 123,
70
+ "site_energy_use_kbtu": 45678.9,
71
+ "predicted": True,
72
+ "updated_at": "2025-09-17T10:30:00Z",
73
+ }
74
+ ]
75
+ }
76
+ },
77
+ },
78
+ 500: {"description": "Erreur interne du serveur"},
79
+ },
80
+ )
81
+ def get_predictions(
82
+ skip: int = Query(0, ge=0, description="Nombre d'éléments à ignorer (pagination)"),
83
+ limit: int = Query(
84
+ 100, ge=1, le=1000, description="Nombre maximum d'éléments à retourner"
85
+ ),
86
+ db: Session = Depends(get_db),
87
+ ):
88
+ """
89
+ Récupère la liste paginée des prédictions énergétiques des bâtiments.
90
+
91
+ Args:
92
+ skip (int): Nombre d'enregistrements à ignorer pour la pagination
93
+ limit (int): Nombre maximum d'enregistrements à retourner
94
+ db (Session): Session de base de données injectée par FastAPI
95
+
96
+ Returns:
97
+ JSONResponse: Liste des prédictions énergétiques au format JSON
98
+
99
+ Raises:
100
+ HTTPException: Erreur 500 si problème lors de la récupération des données
101
+
102
+ Example:
103
+ GET /api/v1/predictions/?skip=0&limit=50
104
+ """
105
+ try:
106
+ predictions = BuildingEnergyPredictionService.get_all(
107
+ db=db, skip=skip, limit=limit
108
+ )
109
+ return JSONResponse(jsonable_encoder(predictions))
110
+ except Exception as e:
111
+ logger.error(
112
+ f"Erreur lors de la récupération des predictions énergétiques des bâtiments: {e}"
113
+ )
114
+ raise HTTPException(
115
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
116
+ detail="Erreur lors de la récupération des predictions énergétiques des bâtiments",
117
+ )
118
+
119
+
120
+ @router.get(
121
+ "/{id}",
122
+ response_model=BuildingEnergyPredictionsResponse,
123
+ summary="Récupérer une prédiction énergétique par ID",
124
+ description="""
125
+ Retourne les détails d'une prédiction énergétique spécifique.
126
+
127
+ Récupère une prédiction énergétique complète avec toutes les informations
128
+ associées au bâtiment et aux calculs de consommation énergétique.
129
+
130
+ L'ID doit correspondre à une prédiction existante dans la base de données.
131
+ """,
132
+ responses={
133
+ 200: {
134
+ "description": "Prédiction trouvée et retournée avec succès",
135
+ "content": {
136
+ "application/json": {
137
+ "example": {
138
+ "id": 1,
139
+ "building_id": 123,
140
+ "site_energy_use_kbtu": 45678.9,
141
+ "predicted": True,
142
+ "updated_at": "2025-09-17T10:30:00Z",
143
+ "building": {
144
+ "id": 123,
145
+ "address": "123 Main St",
146
+ "neighborhood": "Downtown",
147
+ "building_type": "Office",
148
+ },
149
+ }
150
+ }
151
+ },
152
+ },
153
+ 404: {
154
+ "description": "Prédiction non trouvée",
155
+ "content": {
156
+ "application/json": {
157
+ "example": {"detail": "Prédiction avec l'ID 999 non trouvée"}
158
+ }
159
+ },
160
+ },
161
+ 500: {"description": "Erreur interne du serveur"},
162
+ },
163
+ )
164
+ def get_prediction(id: int, db: Session = Depends(get_db)):
165
+ """
166
+ Récupère une prédiction énergétique spécifique par son ID.
167
+
168
+ Args:
169
+ id (int): Identifiant unique de la prédiction à récupérer
170
+ db (Session): Session de base de données injectée par FastAPI
171
+
172
+ Returns:
173
+ BuildingEnergyPredictionsResponse: Prédiction énergétique avec détails complets
174
+
175
+ Raises:
176
+ HTTPException:
177
+ - 404 si la prédiction n'existe pas
178
+ - 500 si erreur lors de la récupération
179
+
180
+ Example:
181
+ GET /api/v1/predictions/123
182
+ """
183
+ try:
184
+ pred = BuildingEnergyPredictionService.get_by_id(db=db, prediction_id=id)
185
+ return pred
186
+ except Exception as e:
187
+ logger.error(f"Erreur lors de la récupération de la prediction: {e}")
188
+ raise HTTPException(
189
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
190
+ detail="Erreur lors de la récupération de la prediction",
191
+ )
192
+
193
+
194
+ @router.post(
195
+ "/building/{building_id}",
196
+ response_model=PredictionResponse,
197
+ status_code=status.HTTP_201_CREATED,
198
+ summary="Effectuer une prédiction pour un bâtiment spécifique",
199
+ description="""
200
+ Effectue une prédiction énergétique pour un bâtiment spécifique et sauvegarde automatiquement le résultat.
201
+
202
+ Cette endpoint :
203
+ 1. Récupère les données du bâtiment spécifié
204
+ 2. Effectue une prédiction énergétique avec le modèle ML
205
+ 3. Sauvegarde automatiquement le résultat dans building_energy_predictions avec predicted=True
206
+ 4. Retourne la prédiction avec intervalle de confiance
207
+
208
+ Le bâtiment doit exister et avoir toutes les données nécessaires pour les prédictions ML.
209
+ """,
210
+ responses={
211
+ 201: {
212
+ "description": "Prédiction effectuée et sauvegardée avec succès",
213
+ "content": {
214
+ "application/json": {
215
+ "example": {
216
+ "prediction": 65432.10,
217
+ "prediction_log": 11.09,
218
+ "model_version": "v1.0",
219
+ "confidence_interval_log": {"lower": 10.85, "upper": 11.33},
220
+ }
221
+ }
222
+ },
223
+ },
224
+ 404: {
225
+ "description": "Bâtiment non trouvé",
226
+ "content": {
227
+ "application/json": {
228
+ "example": {"detail": "Bâtiment avec l'ID 999 non trouvé"}
229
+ }
230
+ },
231
+ },
232
+ 422: {
233
+ "description": "Données du bâtiment insuffisantes pour la prédiction",
234
+ "content": {
235
+ "application/json": {
236
+ "example": {
237
+ "detail": "Le bâtiment n'a pas toutes les données nécessaires pour les prédictions ML"
238
+ }
239
+ }
240
+ },
241
+ },
242
+ 500: {"description": "Erreur interne du serveur"},
243
+ },
244
+ )
245
+ async def predict_for_building(building_id: int, db: Session = Depends(get_db)):
246
+ """
247
+ Effectue une prédiction énergétique pour un bâtiment spécifique et la sauvegarde.
248
+
249
+ Args:
250
+ building_id (int): Identifiant unique du bâtiment
251
+ db (Session): Session de base de données injectée par FastAPI
252
+
253
+ Returns:
254
+ PredictionResponse: Prédiction avec consommation énergétique et intervalle de confiance
255
+
256
+ Raises:
257
+ HTTPException:
258
+ - 404 si le bâtiment n'existe pas
259
+ - 422 si les données du bâtiment sont insuffisantes
260
+ - 500 si erreur lors de la prédiction ou sauvegarde
261
+
262
+ Example:
263
+ POST /api/v1/predictions/building/123
264
+
265
+ Note:
266
+ La prédiction est automatiquement sauvegardée dans la table
267
+ building_energy_predictions avec le flag predicted=True
268
+ """
269
+ try:
270
+ # Récupérer les données du bâtiment pour la prédiction
271
+ building_data = BuildingModelsService.get_model_data_by_id(
272
+ db=db, building_id=building_id
273
+ )
274
+
275
+ # Convertir en BuildingFeatures pour la prédiction
276
+ from project5.schemas.building_model import convert_query_result_to_schema
277
+
278
+ model_schema = convert_query_result_to_schema(building_data)
279
+
280
+ # Créer l'objet BuildingFeatures
281
+ from project5.schemas.building_model import BuildingFeatures
282
+
283
+ features = BuildingFeatures(
284
+ year_built=model_schema.year_built,
285
+ number_of_buildings=model_schema.number_of_buildings,
286
+ number_of_floors=model_schema.number_of_floors,
287
+ property_gfa_total=float(model_schema.property_gfa_total),
288
+ property_gfa_parking=float(model_schema.property_gfa_parking),
289
+ second_largest_property_use_type_gfa=float(
290
+ model_schema.second_largest_property_use_type_gfa
291
+ ),
292
+ third_largest_property_use_type_gfa=float(
293
+ model_schema.third_largest_property_use_type_gfa
294
+ ),
295
+ multiusage=int(model_schema.multiusage),
296
+ steam=int(model_schema.steam),
297
+ electricity=int(model_schema.electricity),
298
+ natural_gas=int(model_schema.natural_gas),
299
+ neighborhood_id=model_schema.neighborhood_id,
300
+ building_type_id=model_schema.building_type_id,
301
+ largest_property_use_type_id=model_schema.largest_property_use_type_id,
302
+ primary_property_type_id=model_schema.primary_property_type_id,
303
+ second_largest_property_use_type_id=model_schema.second_largest_property_use_type_id,
304
+ third_largest_property_use_type_id=model_schema.third_largest_property_use_type_id,
305
+ )
306
+
307
+ # Effectuer la prédiction avec sauvegarde automatique
308
+ service = PredictionService()
309
+ result = await service.predict(features, building_id=building_id, db=db)
310
+
311
+ logger.info(
312
+ f"Prédiction effectuée et sauvegardée pour le bâtiment {building_id}: {result['prediction']} kBTU"
313
+ )
314
+
315
+ return PredictionResponse(**result)
316
+
317
+ except NotFoundError as e:
318
+ # Bâtiment non trouvé - retourner 404
319
+ logger.warning(f"Bâtiment {building_id} non trouvé: {e}")
320
+ raise HTTPException(
321
+ status_code=status.HTTP_404_NOT_FOUND,
322
+ detail=f"Bâtiment avec l'ID {building_id} non trouvé",
323
+ )
324
+ except HTTPException:
325
+ # Propager les erreurs HTTP (404, etc.)
326
+ raise
327
+ except Exception as e:
328
+ logger.error(
329
+ f"Erreur lors de la prédiction pour le bâtiment {building_id}: {e}"
330
+ )
331
+ raise HTTPException(
332
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
333
+ detail=f"Erreur lors de la prédiction: {str(e)}",
334
+ )
src/project5/routers/building_model_routes.py ADDED
@@ -0,0 +1,438 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Routes API pour la gestion des modèles de bâtiments.
3
+
4
+ Ce module contient les endpoints FastAPI pour :
5
+ - Récupération de la liste des bâtiments avec pagination
6
+ - Récupération d'un bâtiment spécifique par ID
7
+ - Récupération des données formatées pour le modèle ML
8
+ - Gestion des erreurs et logging approprié
9
+
10
+ Endpoints disponibles :
11
+ - GET /api/v1/buildings/ : Liste paginée des bâtiments
12
+ - GET /api/v1/buildings/{id} : Bâtiment spécifique par ID
13
+ - GET /api/v1/buildings/model/{id} : Données formatées pour le modèle ML
14
+ - POST /api/v1/buildings/ : Créer un nouveau bâtiment
15
+
16
+ Auteur: François Hellebuyck
17
+ Projet: Building Energy Prediction API - OpenClassrooms Projet 5
18
+ """
19
+
20
+ import logging
21
+ from typing import List
22
+
23
+ from fastapi import APIRouter, Depends, HTTPException, Query, status
24
+ from fastapi.encoders import jsonable_encoder
25
+ from fastapi.responses import JSONResponse
26
+ from sqlalchemy.orm import Session
27
+
28
+ from project5.database import get_db
29
+ from project5.models.building_models import BuildingModels
30
+ from project5.schemas.building_model import (
31
+ BuildingModelsCreate,
32
+ BuildingModelsList,
33
+ BuildingModelsResponse,
34
+ ModelViewSchema,
35
+ convert_query_result_to_schema,
36
+ )
37
+ from project5.services.building_model_service import BuildingModelsService
38
+ from project5.utils.exceptions import ValidationError
39
+
40
+ logger = logging.getLogger(__name__)
41
+
42
+ router = APIRouter(prefix="/api/v1/buildings", tags=["buildings"])
43
+
44
+
45
+ @router.get(
46
+ "/",
47
+ response_model=List[BuildingModelsList],
48
+ summary="Récupérer tous les bâtiments",
49
+ description="""
50
+ Retourne la liste paginée de tous les bâtiments enregistrés dans le système.
51
+
52
+ Cette endpoint permet de récupérer l'ensemble des bâtiments avec leurs
53
+ informations de base comme l'adresse, le quartier, le type de bâtiment,
54
+ et les caractéristiques principales utilisées pour les prédictions énergétiques.
55
+
56
+ Paramètres de pagination :
57
+ - skip : nombre d'enregistrements à ignorer (par défaut 0)
58
+ - limit : nombre maximum d'enregistrements à retourner (par défaut 100, max 1000)
59
+ """,
60
+ responses={
61
+ 200: {
62
+ "description": "Liste des bâtiments récupérée avec succès",
63
+ "content": {
64
+ "application/json": {
65
+ "example": [
66
+ {
67
+ "id": 1,
68
+ "ose_building_id": 12345,
69
+ "address": "123 Main St",
70
+ "city": "Seattle",
71
+ "neighborhood": "Downtown",
72
+ "building_type": "Office",
73
+ "year_built": 1995,
74
+ "property_gfa_total": 50000.0,
75
+ }
76
+ ]
77
+ }
78
+ },
79
+ },
80
+ 500: {"description": "Erreur interne du serveur"},
81
+ },
82
+ )
83
+ def get_building_types(
84
+ skip: int = Query(0, ge=0, description="Nombre d'éléments à ignorer (pagination)"),
85
+ limit: int = Query(
86
+ 100, ge=1, le=1000, description="Nombre maximum d'éléments à retourner"
87
+ ),
88
+ db: Session = Depends(get_db),
89
+ ):
90
+ """
91
+ Récupère la liste paginée des bâtiments.
92
+
93
+ Args:
94
+ skip (int): Nombre d'enregistrements à ignorer pour la pagination
95
+ limit (int): Nombre maximum d'enregistrements à retourner
96
+ db (Session): Session de base de données injectée par FastAPI
97
+
98
+ Returns:
99
+ JSONResponse: Liste des bâtiments au format JSON
100
+
101
+ Raises:
102
+ HTTPException: Erreur 500 si problème lors de la récupération des données
103
+
104
+ Example:
105
+ GET /api/v1/buildings/?skip=0&limit=50
106
+ """
107
+ try:
108
+ buildings = BuildingModelsService.get_all(db=db, skip=skip, limit=limit)
109
+ return JSONResponse(jsonable_encoder(buildings))
110
+ except Exception as e:
111
+ logger.error(f"Erreur lors de la récupération des bâtiments: {e}")
112
+ raise HTTPException(
113
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
114
+ detail="Erreur lors de la récupération des bâtiments",
115
+ )
116
+
117
+
118
+ @router.get(
119
+ "/{id}",
120
+ response_model=BuildingModelsResponse,
121
+ summary="Récupérer un bâtiment par ID",
122
+ description="""
123
+ Retourne les détails complets d'un bâtiment spécifique.
124
+
125
+ Récupère toutes les informations détaillées d'un bâtiment incluant :
126
+ - Informations générales (adresse, ville, quartier)
127
+ - Caractéristiques techniques (année de construction, nombre d'étages)
128
+ - Données énergétiques (types d'énergie utilisés)
129
+ - Relations avec les types et catégories de propriétés
130
+
131
+ L'ID doit correspondre à un bâtiment existant dans la base de données.
132
+ """,
133
+ responses={
134
+ 200: {
135
+ "description": "Bâtiment trouvé et retourné avec succès",
136
+ "content": {
137
+ "application/json": {
138
+ "example": {
139
+ "id": 1,
140
+ "ose_building_id": 12345,
141
+ "address": "123 Main St",
142
+ "city": "Seattle",
143
+ "state": "WA",
144
+ "zip_code": "98101",
145
+ "neighborhood": "Downtown",
146
+ "building_type": "Office",
147
+ "year_built": 1995,
148
+ "number_of_buildings": 1,
149
+ "number_of_floors": 15,
150
+ "property_gfa_total": 50000.0,
151
+ "multiusage": False,
152
+ "steam": True,
153
+ "electricity": True,
154
+ "natural_gas": True,
155
+ }
156
+ }
157
+ },
158
+ },
159
+ 404: {
160
+ "description": "Bâtiment non trouvé",
161
+ "content": {
162
+ "application/json": {
163
+ "example": {"detail": "Bâtiment avec l'ID 999 non trouvé"}
164
+ }
165
+ },
166
+ },
167
+ 500: {"description": "Erreur interne du serveur"},
168
+ },
169
+ )
170
+ def get_building(id: int, db: Session = Depends(get_db)):
171
+ """
172
+ Récupère un bâtiment spécifique par son ID.
173
+
174
+ Args:
175
+ id (int): Identifiant unique du bâtiment à récupérer
176
+ db (Session): Session de base de données injectée par FastAPI
177
+
178
+ Returns:
179
+ BuildingModelsResponse: Bâtiment avec détails complets
180
+
181
+ Raises:
182
+ HTTPException:
183
+ - 404 si le bâtiment n'existe pas
184
+ - 500 si erreur lors de la récupération
185
+
186
+ Example:
187
+ GET /api/v1/buildings/123
188
+ """
189
+ try:
190
+ building = BuildingModelsService.get_by_id(db=db, building_id=id)
191
+ return building
192
+ except Exception as e:
193
+ logger.error(f"Erreur lors du bâtiment: {e}")
194
+ raise HTTPException(
195
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
196
+ detail="Erreur lors de la récupération du bâtiment",
197
+ )
198
+
199
+
200
+ @router.get(
201
+ "/model/{id}",
202
+ response_model=ModelViewSchema,
203
+ summary="Récupérer les données formatées pour le modèle ML",
204
+ description="""
205
+ Retourne les données d'un bâtiment formatées spécifiquement pour le modèle de machine learning.
206
+
207
+ Cette endpoint transforme les données brutes du bâtiment en format approprié
208
+ pour effectuer des prédictions énergétiques avec le modèle ML. Les données
209
+ sont pré-traitées et encodées selon les exigences du modèle :
210
+
211
+ - Variables catégorielles encodées numériquement
212
+ - Variables continues normalisées si nécessaire
213
+ - Format de données compatible avec scikit-learn
214
+ - Gestion des valeurs manquantes
215
+
216
+ L'ID doit correspondre à un bâtiment existant dans la base de données.
217
+ """,
218
+ responses={
219
+ 200: {
220
+ "description": "Données du modèle récupérées avec succès",
221
+ "content": {
222
+ "application/json": {
223
+ "example": {
224
+ "building_id": 123,
225
+ "neighborhood_encoded": 3,
226
+ "building_type_encoded": 1,
227
+ "largest_property_use_type_encoded": 18,
228
+ "primary_property_type_encoded": 18,
229
+ "year_built": 1995,
230
+ "number_of_buildings": 1,
231
+ "number_of_floors": 15,
232
+ "property_gfa_total": 50000.0,
233
+ "steam": 1,
234
+ "electricity": 1,
235
+ "natural_gas": 1,
236
+ }
237
+ }
238
+ },
239
+ },
240
+ 404: {
241
+ "description": "Bâtiment non trouvé",
242
+ "content": {
243
+ "application/json": {
244
+ "example": {"detail": "Bâtiment avec l'ID 999 non trouvé"}
245
+ }
246
+ },
247
+ },
248
+ 500: {"description": "Erreur interne du serveur"},
249
+ },
250
+ )
251
+ def get_model_data_by_id(id: int, db: Session = Depends(get_db)):
252
+ """
253
+ Récupère les données d'un bâtiment formatées pour le modèle ML.
254
+
255
+ Args:
256
+ id (int): Identifiant unique du bâtiment
257
+ db (Session): Session de base de données injectée par FastAPI
258
+
259
+ Returns:
260
+ JSONResponse: Données formatées pour le modèle ML au format JSON
261
+
262
+ Raises:
263
+ HTTPException:
264
+ - 404 si le bâtiment n'existe pas
265
+ - 500 si erreur lors de la récupération ou du formatage
266
+
267
+ Example:
268
+ GET /api/v1/buildings/model/123
269
+
270
+ Note:
271
+ Cette endpoint est principalement utilisée par l'endpoint de prédiction
272
+ pour préparer les données avant d'appeler le modèle ML.
273
+ """
274
+ try:
275
+ building = BuildingModelsService.get_model_data_by_id(db=db, building_id=id)
276
+ return JSONResponse(jsonable_encoder(convert_query_result_to_schema(building)))
277
+ except Exception as e:
278
+ logger.error(f"Erreur lors du bâtiment: {e}")
279
+ raise HTTPException(
280
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
281
+ detail="Erreur lors de la récupération du bâtiment",
282
+ )
283
+
284
+
285
+ @router.post(
286
+ "/",
287
+ response_model=BuildingModelsResponse,
288
+ status_code=status.HTTP_201_CREATED,
289
+ summary="Créer un nouveau bâtiment",
290
+ description="""
291
+ Ajoute un nouveau bâtiment dans le système.
292
+
293
+ Cette endpoint permet de créer un nouveau bâtiment avec ses caractéristiques
294
+ complètes. Les données sont validées avant l'insertion en base de données.
295
+
296
+ Champs obligatoires pour les prédictions ML (17 features) :
297
+ - ose_building_id : Identifiant unique du bâtiment OSE
298
+ - year_built : Année de construction
299
+ - number_of_buildings : Nombre de bâtiments
300
+ - number_of_floors : Nombre d'étages
301
+ - property_gfa_total : Surface totale GFA
302
+ - property_gfa_parking : Surface parking GFA
303
+ - second_largest_property_use_type_gfa : Surface 2e type d'usage
304
+ - third_largest_property_use_type_gfa : Surface 3e type d'usage
305
+ - multiusage : Indicateur multi-usage (0 ou 1)
306
+ - steam : Indicateur vapeur (0 ou 1)
307
+ - electricity : Indicateur électricité (0 ou 1)
308
+ - natural_gas : Indicateur gaz naturel (0 ou 1)
309
+ - neighborhood_id : ID du quartier
310
+ - building_type_id : ID du type de bâtiment
311
+ - largest_property_use_type_id : ID du plus grand type d'usage
312
+ - primary_property_type_id : ID du type de propriété principal
313
+ - second_largest_property_use_type_id : ID du 2e type d'usage
314
+ - third_largest_property_use_type_id : ID du 3e type d'usage
315
+
316
+ Validation automatique :
317
+ - Format et valeurs des champs
318
+ - Contraintes métier (années, surfaces positives)
319
+ - Unicité de l'identifiant OSE
320
+ """,
321
+ responses={
322
+ 201: {
323
+ "description": "Bâtiment créé avec succès",
324
+ "content": {
325
+ "application/json": {
326
+ "example": {
327
+ "id": 1001,
328
+ "ose_building_id": "NEW12345",
329
+ "address": "456 New Street",
330
+ "city": "Seattle",
331
+ "state": "WA",
332
+ "year_built": 2020,
333
+ "number_of_buildings": 1,
334
+ "number_of_floors": 5,
335
+ "property_gfa_total": 25000.0,
336
+ "property_gfa_parking": 2000.0,
337
+ "second_largest_property_use_type_gfa": 0.0,
338
+ "third_largest_property_use_type_gfa": 0.0,
339
+ "multiusage": 0,
340
+ "steam": 0,
341
+ "electricity": 1,
342
+ "natural_gas": 1,
343
+ "neighborhood_id": 1,
344
+ "building_type_id": 1,
345
+ "largest_property_use_type_id": 18,
346
+ "primary_property_type_id": 18,
347
+ "second_largest_property_use_type_id": 1,
348
+ "third_largest_property_use_type_id": 1,
349
+ "created_at": "2025-09-21T12:00:00",
350
+ "updated_at": "2025-09-21T12:00:00",
351
+ }
352
+ }
353
+ },
354
+ },
355
+ 400: {
356
+ "description": "Données invalides ou manquantes",
357
+ "content": {
358
+ "application/json": {
359
+ "example": {"detail": "L'année de construction est obligatoire"}
360
+ }
361
+ },
362
+ },
363
+ 422: {
364
+ "description": "Erreur de validation des données",
365
+ "content": {
366
+ "application/json": {
367
+ "example": {"detail": "La surface totale GFA est obligatoire"}
368
+ }
369
+ },
370
+ },
371
+ 500: {"description": "Erreur interne du serveur"},
372
+ },
373
+ )
374
+ def create_building(building_data: BuildingModelsCreate, db: Session = Depends(get_db)):
375
+ """
376
+ Crée un nouveau bâtiment dans le système.
377
+
378
+ Args:
379
+ building_data (BuildingModelsCreate): Données du bâtiment à créer
380
+ db (Session): Session de base de données injectée par FastAPI
381
+
382
+ Returns:
383
+ BuildingModelsResponse: Bâtiment créé avec son ID généré et timestamps
384
+
385
+ Raises:
386
+ HTTPException:
387
+ - 400 si les données obligatoires sont manquantes
388
+ - 422 si erreur de validation des données
389
+ - 500 si erreur lors de la création
390
+
391
+ Example:
392
+ POST /api/v1/buildings/
393
+ {
394
+ "ose_building_id": "NEW12345",
395
+ "address": "456 New Street",
396
+ "city": "Seattle",
397
+ "year_built": 2020,
398
+ "number_of_buildings": 1,
399
+ "number_of_floors": 5,
400
+ "property_gfa_total": 25000.0,
401
+ "property_gfa_parking": 2000.0,
402
+ "second_largest_property_use_type_gfa": 0.0,
403
+ "third_largest_property_use_type_gfa": 0.0,
404
+ "multiusage": 0,
405
+ "steam": 0,
406
+ "electricity": 1,
407
+ "natural_gas": 1,
408
+ "neighborhood_id": 1,
409
+ "building_type_id": 1,
410
+ "largest_property_use_type_id": 18,
411
+ "primary_property_type_id": 18,
412
+ "second_largest_property_use_type_id": 1,
413
+ "third_largest_property_use_type_id": 1
414
+ }
415
+ """
416
+ try:
417
+ # Convertir le schéma Pydantic en modèle SQLAlchemy
418
+ building_model = BuildingModels(**building_data.model_dump(exclude_unset=True))
419
+
420
+ # Appeler le service pour créer le bâtiment
421
+ created_building = BuildingModelsService.set_new_building(
422
+ db=db, building_model=building_model
423
+ )
424
+
425
+ return created_building
426
+
427
+ except ValidationError as e:
428
+ logger.error(f"Erreur de validation lors de la création du bâtiment: {e}")
429
+ raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
430
+ except ValueError as e:
431
+ logger.error(f"Erreur de validation lors de la création du bâtiment: {e}")
432
+ raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
433
+ except Exception as e:
434
+ logger.error(f"Erreur lors de la création du bâtiment: {e}")
435
+ raise HTTPException(
436
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
437
+ detail="Erreur lors de la création du bâtiment",
438
+ )
src/project5/routers/building_types_routes.py ADDED
@@ -0,0 +1,205 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Routes API pour la gestion des types de bâtiments.
3
+
4
+ Ce module contient les endpoints FastAPI pour :
5
+ - Récupération de la liste des types de bâtiments avec pagination
6
+ - Récupération d'un type de bâtiment spécifique par ID
7
+ - Gestion des erreurs et logging approprié
8
+
9
+ Les types de bâtiments sont utilisés pour catégoriser les bâtiments selon leur
10
+ usage principal (résidentiel, commercial, industriel, etc.) et sont essentiels
11
+ pour les prédictions énergétiques du modèle ML.
12
+
13
+ Endpoints disponibles :
14
+ - GET /api/v1/building-types/ : Liste paginée des types de bâtiments
15
+ - GET /api/v1/building-types/{id} : Type de bâtiment spécifique par ID
16
+
17
+ Auteur: François Hellebuyck
18
+ Projet: Building Energy Prediction API - OpenClassrooms Projet 5
19
+ """
20
+
21
+ import logging
22
+ from typing import List
23
+
24
+ from fastapi import APIRouter, Depends, HTTPException, Query, status
25
+ from sqlalchemy.orm import Session
26
+
27
+ from project5.database import get_db
28
+ from project5.schemas.building_type import BuildingTypeList, BuildingTypeResponse
29
+ from project5.services.building_type_service import BuildingTypeService
30
+
31
+ logger = logging.getLogger(__name__)
32
+
33
+ router = APIRouter(prefix="/api/v1/building-types", tags=["building-types"])
34
+
35
+
36
+ @router.get(
37
+ "/",
38
+ response_model=List[BuildingTypeList],
39
+ summary="Récupérer tous les types de bâtiments",
40
+ description="""
41
+ Retourne la liste paginée de tous les types de bâtiments disponibles dans le système.
42
+
43
+ Cette endpoint permet de récupérer l'ensemble des catégories de bâtiments
44
+ utilisées pour la classification et les prédictions énergétiques. Chaque type
45
+ de bâtiment correspond à un usage spécifique (résidentiel, commercial,
46
+ industriel, etc.) avec ses caractéristiques énergétiques propres.
47
+
48
+ Types de bâtiments disponibles :
49
+ - Campus building complex
50
+ - Non-residential building
51
+ - Seattle Public Schools District K-12
52
+ - Multifamily (Low-Rise, Mid-Rise, High-Rise)
53
+ - Et autres catégories spécialisées
54
+
55
+ Paramètres de pagination :
56
+ - skip : nombre d'enregistrements à ignorer (par défaut 0)
57
+ - limit : nombre maximum d'enregistrements à retourner (par défaut 100, max 1000)
58
+ """,
59
+ responses={
60
+ 200: {
61
+ "description": "Liste des types de bâtiments récupérée avec succès",
62
+ "content": {
63
+ "application/json": {
64
+ "example": [
65
+ {
66
+ "id": 1,
67
+ "model_id": 0,
68
+ "building_type_name": "CAMPUS",
69
+ "description": "Campus building complex",
70
+ "created_at": "2025-09-09T09:56:21Z",
71
+ "updated_at": "2025-09-09T09:56:21Z",
72
+ },
73
+ {
74
+ "id": 2,
75
+ "model_id": 1,
76
+ "building_type_name": "NONRESIDENTIAL",
77
+ "description": "Non-residential building",
78
+ "created_at": "2025-09-09T09:56:21Z",
79
+ "updated_at": "2025-09-09T09:56:21Z",
80
+ },
81
+ ]
82
+ }
83
+ },
84
+ },
85
+ 500: {"description": "Erreur interne du serveur"},
86
+ },
87
+ )
88
+ def get_building_types(
89
+ skip: int = Query(0, ge=0, description="Nombre d'éléments à ignorer (pagination)"),
90
+ limit: int = Query(
91
+ 100, ge=1, le=1000, description="Nombre maximum d'éléments à retourner"
92
+ ),
93
+ db: Session = Depends(get_db),
94
+ ):
95
+ """
96
+ Récupère la liste paginée des types de bâtiments.
97
+
98
+ Args:
99
+ skip (int): Nombre d'enregistrements à ignorer pour la pagination
100
+ limit (int): Nombre maximum d'enregistrements à retourner
101
+ db (Session): Session de base de données injectée par FastAPI
102
+
103
+ Returns:
104
+ List[BuildingTypeList]: Liste des types de bâtiments
105
+
106
+ Raises:
107
+ HTTPException: Erreur 500 si problème lors de la récupération des données
108
+
109
+ Example:
110
+ GET /api/v1/building-types/?skip=0&limit=20
111
+
112
+ Note:
113
+ Les types de bâtiments sont référencés par leur model_id dans le modèle ML
114
+ pour effectuer les prédictions énergétiques.
115
+ """
116
+ try:
117
+ buildingTypes = BuildingTypeService.get_all(db=db, skip=skip, limit=limit)
118
+ return buildingTypes
119
+ except Exception as e:
120
+ logger.error(f"Erreur lors de la récupération des des types de bâtiment: {e}")
121
+ raise HTTPException(
122
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
123
+ detail="Erreur lors de la récupération des types de bâtiment",
124
+ )
125
+
126
+
127
+ @router.get(
128
+ "/{building_type_id}",
129
+ response_model=BuildingTypeResponse,
130
+ summary="Récupérer un type de bâtiment par ID",
131
+ description="""
132
+ Retourne les détails complets d'un type de bâtiment spécifique.
133
+
134
+ Récupère toutes les informations d'un type de bâtiment incluant :
135
+ - Identifiant unique dans la base de données
136
+ - Identifiant utilisé par le modèle ML (model_id)
137
+ - Nom du type de bâtiment
138
+ - Description détaillée
139
+ - Horodatages de création et modification
140
+
141
+ Le type de bâtiment est un élément clé pour la classification et les
142
+ prédictions énergétiques, car il détermine les caractéristiques de
143
+ consommation énergétique typiques.
144
+
145
+ L'ID doit correspondre à un type de bâtiment existant dans la base de données.
146
+ """,
147
+ responses={
148
+ 200: {
149
+ "description": "Type de bâtiment trouvé et retourné avec succès",
150
+ "content": {
151
+ "application/json": {
152
+ "example": {
153
+ "id": 3,
154
+ "model_id": 1,
155
+ "building_type_name": "NONRESIDENTIAL",
156
+ "description": "Non-residential building",
157
+ "created_at": "2025-09-09T09:56:21Z",
158
+ "updated_at": "2025-09-09T09:56:21Z",
159
+ }
160
+ }
161
+ },
162
+ },
163
+ 404: {
164
+ "description": "Type de bâtiment non trouvé",
165
+ "content": {
166
+ "application/json": {
167
+ "example": {"detail": "Type de bâtiment avec l'ID 999 non trouvé"}
168
+ }
169
+ },
170
+ },
171
+ 500: {"description": "Erreur interne du serveur"},
172
+ },
173
+ )
174
+ def get_building_type(building_type_id: int, db: Session = Depends(get_db)):
175
+ """
176
+ Récupère un type de bâtiment spécifique par son ID.
177
+
178
+ Args:
179
+ building_type_id (int): Identifiant unique du type de bâtiment à récupérer
180
+ db (Session): Session de base de données injectée par FastAPI
181
+
182
+ Returns:
183
+ BuildingTypeResponse: Type de bâtiment avec détails complets
184
+
185
+ Raises:
186
+ HTTPException:
187
+ - 404 si le type de bâtiment n'existe pas
188
+ - 500 si erreur lors de la récupération
189
+
190
+ Example:
191
+ GET /api/v1/building-types/3
192
+
193
+ Note:
194
+ Le model_id retourné est utilisé par le modèle ML pour encoder
195
+ le type de bâtiment lors des prédictions énergétiques.
196
+ """
197
+ try:
198
+ bt = BuildingTypeService.get_by_id(db=db, id=building_type_id)
199
+ return bt
200
+ except Exception as e:
201
+ logger.error(f"Erreur lors de la récupération du type de bâtiment: {e}")
202
+ raise HTTPException(
203
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
204
+ detail="Erreur lors de la récupération du type de bâtiment",
205
+ )
src/project5/routers/categories_routes.py ADDED
@@ -0,0 +1,221 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Routes API pour la gestion des catégories de propriétés.
3
+
4
+ Ce module contient les endpoints FastAPI pour :
5
+ - Récupération de la liste des catégories de propriétés avec pagination
6
+ - Récupération d'une catégorie spécifique par ID
7
+ - Gestion des erreurs et logging approprié
8
+
9
+ Les catégories de propriétés regroupent les différents usages des bâtiments
10
+ (résidentiel, commercial, bureaux, éducation, santé, etc.) et servent à
11
+ classifier les propriétés pour les prédictions énergétiques du modèle ML.
12
+
13
+ Catégories principales disponibles :
14
+ - RESIDENTIAL : Logements et habitations
15
+ - OFFICE : Espaces de bureaux et administratifs
16
+ - EDUCATION : Établissements d'enseignement
17
+ - HEALTHCARE : Hôpitaux et établissements de soins
18
+ - RETAIL : Magasins et commerces
19
+ - INDUSTRIAL : Usines et installations industrielles
20
+ - Et autres catégories spécialisées
21
+
22
+ Endpoints disponibles :
23
+ - GET /api/v1/categories/ : Liste paginée des catégories
24
+ - GET /api/v1/categories/{id} : Catégorie spécifique par ID
25
+
26
+ Auteur: François Hellebuyck
27
+ Projet: Building Energy Prediction API - OpenClassrooms Projet 5
28
+ """
29
+
30
+ import logging
31
+ from typing import List
32
+
33
+ from fastapi import APIRouter, Depends, HTTPException, Query, status
34
+ from sqlalchemy.orm import Session
35
+
36
+ from project5.database import get_db
37
+ from project5.schemas.categorie import CategoryList, CategoryResponse
38
+ from project5.services.categorie_service import CategorieService
39
+
40
+ logger = logging.getLogger(__name__)
41
+
42
+ router = APIRouter(prefix="/api/v1/categories", tags=["categories"])
43
+
44
+
45
+ @router.get(
46
+ "/",
47
+ response_model=List[CategoryList],
48
+ summary="Récupérer toutes les catégories de propriétés",
49
+ description="""
50
+ Retourne la liste paginée de toutes les catégories de propriétés disponibles dans le système.
51
+
52
+ Cette endpoint permet de récupérer l'ensemble des catégories d'usage des bâtiments
53
+ utilisées pour la classification et les prédictions énergétiques. Chaque catégorie
54
+ regroupe des propriétés ayant des caractéristiques énergétiques similaires.
55
+
56
+ Catégories disponibles dans le système :
57
+ - RESIDENTIAL : Logements et habitations
58
+ - OFFICE : Espaces de bureaux et administratifs
59
+ - EDUCATION : Établissements d'enseignement et de formation
60
+ - HEALTHCARE : Hôpitaux, cliniques et établissements de soins médicaux
61
+ - RETAIL : Magasins et commerces de détail
62
+ - RESTAURANT : Restaurants et services alimentaires
63
+ - INDUSTRIAL : Usines et installations industrielles
64
+ - ENTERTAINMENT : Théâtres, cinémas et espaces de divertissement
65
+ - LODGING : Hôtels et logements temporaires
66
+ - PARKING : Structures et espaces de stationnement
67
+ - PUBLIC : Services gouvernementaux et publics
68
+ - RELIGIOUS : Églises et lieux de culte
69
+ - Et autres catégories spécialisées
70
+
71
+ Paramètres de pagination :
72
+ - skip : nombre d'enregistrements à ignorer (par défaut 0)
73
+ - limit : nombre maximum d'enregistrements à retourner (par défaut 100, max 1000)
74
+ """,
75
+ responses={
76
+ 200: {
77
+ "description": "Liste des catégories récupérée avec succès",
78
+ "content": {
79
+ "application/json": {
80
+ "example": [
81
+ {
82
+ "id": 11,
83
+ "category_code": "OFFICE",
84
+ "category_name": "Bureaux",
85
+ "description": "Espaces de bureaux et administratifs",
86
+ "created_at": "2025-09-09T09:56:21Z",
87
+ "updated_at": "2025-09-09T09:56:21Z",
88
+ },
89
+ {
90
+ "id": 16,
91
+ "category_code": "RESIDENTIAL",
92
+ "category_name": "Résidentiel",
93
+ "description": "Logements et habitations",
94
+ "created_at": "2025-09-09T09:56:21Z",
95
+ "updated_at": "2025-09-09T09:56:21Z",
96
+ },
97
+ ]
98
+ }
99
+ },
100
+ },
101
+ 500: {"description": "Erreur interne du serveur"},
102
+ },
103
+ )
104
+ def get_categories(
105
+ skip: int = Query(0, ge=0, description="Nombre d'éléments à ignorer (pagination)"),
106
+ limit: int = Query(
107
+ 100, ge=1, le=1000, description="Nombre maximum d'éléments à retourner"
108
+ ),
109
+ db: Session = Depends(get_db),
110
+ ):
111
+ """
112
+ Récupère la liste paginée des catégories de propriétés.
113
+
114
+ Args:
115
+ skip (int): Nombre d'enregistrements à ignorer pour la pagination
116
+ limit (int): Nombre maximum d'enregistrements à retourner
117
+ db (Session): Session de base de données injectée par FastAPI
118
+
119
+ Returns:
120
+ List[CategoryList]: Liste des catégories de propriétés
121
+
122
+ Raises:
123
+ HTTPException: Erreur 500 si problème lors de la récupération des données
124
+
125
+ Example:
126
+ GET /api/v1/categories/?skip=0&limit=20
127
+
128
+ Note:
129
+ Les catégories sont utilisées pour regrouper les propriétés par usage
130
+ et déterminer les caractéristiques énergétiques typiques lors des prédictions.
131
+ """
132
+ try:
133
+ categories = CategorieService.get_all(db=db, skip=skip, limit=limit)
134
+ return categories
135
+ except Exception as e:
136
+ logger.error(f"Erreur lors de la récupération des categories: {e}")
137
+ raise HTTPException(
138
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
139
+ detail="Erreur lors de la récupération des categories",
140
+ )
141
+
142
+
143
+ @router.get(
144
+ "/{categorie_id}",
145
+ response_model=CategoryResponse,
146
+ summary="Récupérer une catégorie par ID",
147
+ description="""
148
+ Retourne les détails complets d'une catégorie de propriété spécifique.
149
+
150
+ Récupère toutes les informations d'une catégorie incluant :
151
+ - Identifiant unique dans la base de données
152
+ - Code de la catégorie (nom court en anglais)
153
+ - Nom complet de la catégorie (en français)
154
+ - Description détaillée de l'usage
155
+ - Horodatages de création et modification
156
+
157
+ Les catégories servent à classifier les propriétés selon leur usage principal
158
+ et sont essentielles pour déterminer les profils de consommation énergétique
159
+ lors des prédictions du modèle ML.
160
+
161
+ L'ID doit correspondre à une catégorie existante dans la base de données.
162
+ """,
163
+ responses={
164
+ 200: {
165
+ "description": "Catégorie trouvée et retournée avec succès",
166
+ "content": {
167
+ "application/json": {
168
+ "example": {
169
+ "id": 11,
170
+ "category_code": "OFFICE",
171
+ "category_name": "Bureaux",
172
+ "description": "Espaces de bureaux et administratifs",
173
+ "created_at": "2025-09-09T09:56:21Z",
174
+ "updated_at": "2025-09-09T09:56:21Z",
175
+ }
176
+ }
177
+ },
178
+ },
179
+ 404: {
180
+ "description": "Catégorie non trouvée",
181
+ "content": {
182
+ "application/json": {
183
+ "example": {"detail": "Catégorie avec l'ID 999 non trouvée"}
184
+ }
185
+ },
186
+ },
187
+ 500: {"description": "Erreur interne du serveur"},
188
+ },
189
+ )
190
+ def get_categorie(categorie_id: int, db: Session = Depends(get_db)):
191
+ """
192
+ Récupère une catégorie spécifique par son ID.
193
+
194
+ Args:
195
+ categorie_id (int): Identifiant unique de la catégorie à récupérer
196
+ db (Session): Session de base de données injectée par FastAPI
197
+
198
+ Returns:
199
+ CategoryResponse: Catégorie avec détails complets
200
+
201
+ Raises:
202
+ HTTPException:
203
+ - 404 si la catégorie n'existe pas
204
+ - 500 si erreur lors de la récupération
205
+
206
+ Example:
207
+ GET /api/v1/categories/11
208
+
209
+ Note:
210
+ Les catégories sont liées aux propriétés et influencent directement
211
+ les prédictions énergétiques basées sur l'usage du bâtiment.
212
+ """
213
+ try:
214
+ categorie = CategorieService.get_by_id(db=db, categorie_id=categorie_id)
215
+ return categorie
216
+ except Exception as e:
217
+ logger.error(f"Erreur lors de la récupération de la category: {e}")
218
+ raise HTTPException(
219
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
220
+ detail="Erreur lors de la récupération de la category",
221
+ )
src/project5/routers/neighborhoods_routes.py ADDED
@@ -0,0 +1,328 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Routes API pour la gestion des quartiers de Seattle.
3
+
4
+ Ce module contient les endpoints FastAPI pour :
5
+ - Récupération de la liste des quartiers avec pagination
6
+ - Récupération d'un quartier spécifique par ID
7
+ - Recherche de quartiers par nom
8
+ - Gestion des erreurs et logging approprié
9
+
10
+ Les quartiers (neighborhoods) représentent les divisions géographiques de Seattle
11
+ et sont un facteur important pour les prédictions énergétiques car ils influencent
12
+ les caractéristiques de consommation selon la localisation, le climat local, et
13
+ le type d'urbanisation.
14
+
15
+ Quartiers disponibles dans le système :
16
+ - BALLARD, CENTRAL, DELRIDGE, DOWNTOWN
17
+ - EAST, GREATER DUWAMISH, LAKE UNION
18
+ - MAGNOLIA / QUEEN ANNE, NORTH, NORTHEAST
19
+ - NORTHWEST, SOUTHEAST, SOUTHWEST, WEST
20
+ - UNKNOWN (pour les données non géolocalisées)
21
+
22
+ Endpoints disponibles :
23
+ - GET /api/v1/neighborhoods/ : Liste paginée des quartiers
24
+ - GET /api/v1/neighborhoods/{id} : Quartier spécifique par ID
25
+ - GET /api/v1/neighborhoods/search/ : Recherche par nom
26
+
27
+ Auteur: François Hellebuyck
28
+ Projet: Building Energy Prediction API - OpenClassrooms Projet 5
29
+ """
30
+
31
+ import logging
32
+ from typing import List
33
+
34
+ from fastapi import APIRouter, Depends, HTTPException, Query, status
35
+ from sqlalchemy.orm import Session
36
+
37
+ from project5.database import get_db
38
+ from project5.models import Neighborhood as NeighborhoodModel
39
+ from project5.schemas import Neighborhood
40
+ from project5.services.neighborhood_service import NeighborhoodService
41
+
42
+ logger = logging.getLogger(__name__)
43
+
44
+ router = APIRouter(
45
+ prefix="/api/v1/neighborhoods",
46
+ tags=["neighborhoods"],
47
+ responses={404: {"description": "Quartier non trouvé"}},
48
+ )
49
+
50
+
51
+ @router.get(
52
+ "/",
53
+ response_model=List[Neighborhood],
54
+ summary="Récupérer tous les quartiers de Seattle",
55
+ description="""
56
+ Retourne la liste paginée de tous les quartiers de Seattle disponibles dans le système.
57
+
58
+ Cette endpoint permet de récupérer l'ensemble des quartiers (neighborhoods) de Seattle
59
+ utilisés pour la géolocalisation et les prédictions énergétiques. Chaque quartier
60
+ représente une zone géographique avec ses caractéristiques climatiques et urbaines
61
+ propres qui influencent la consommation énergétique des bâtiments.
62
+
63
+ Quartiers de Seattle disponibles :
64
+ - BALLARD : Quartier nord-ouest, résidentiel et commercial
65
+ - CENTRAL : Zone centrale de Seattle
66
+ - DELRIDGE : Quartier sud-ouest, principalement résidentiel
67
+ - DOWNTOWN : Centre-ville, zone commerciale et de bureaux
68
+ - EAST : Quartiers est de Seattle
69
+ - GREATER DUWAMISH : Zone industrielle sud
70
+ - LAKE UNION : Zone autour du lac Union, mixte
71
+ - MAGNOLIA / QUEEN ANNE : Quartiers résidentiels aisés
72
+ - NORTH, NORTHEAST : Zones résidentielles nord
73
+ - NORTHWEST, SOUTHEAST, SOUTHWEST, WEST : Autres zones géographiques
74
+ - UNKNOWN : Pour les bâtiments sans géolocalisation précise
75
+
76
+ Paramètres de pagination :
77
+ - skip : nombre d'enregistrements à ignorer (par défaut 0)
78
+ - limit : nombre maximum d'enregistrements à retourner (par défaut 100, max 1000)
79
+ """,
80
+ responses={
81
+ 200: {
82
+ "description": "Liste des quartiers récupérée avec succès",
83
+ "content": {
84
+ "application/json": {
85
+ "example": [
86
+ {
87
+ "id": 5,
88
+ "neighborhood_name": "DOWNTOWN",
89
+ "model_id": 3,
90
+ "created_at": "2025-09-09T09:56:21Z",
91
+ "updated_at": "2025-09-09T09:56:21Z",
92
+ },
93
+ {
94
+ "id": 2,
95
+ "neighborhood_name": "BALLARD",
96
+ "model_id": 0,
97
+ "created_at": "2025-09-09T09:56:21Z",
98
+ "updated_at": "2025-09-09T09:56:21Z",
99
+ },
100
+ ]
101
+ }
102
+ },
103
+ },
104
+ 500: {"description": "Erreur interne du serveur"},
105
+ },
106
+ )
107
+ def get_neighborhoods(
108
+ skip: int = Query(0, ge=0, description="Nombre d'éléments à ignorer (pagination)"),
109
+ limit: int = Query(
110
+ 100, ge=1, le=1000, description="Nombre maximum d'éléments à retourner"
111
+ ),
112
+ db: Session = Depends(get_db),
113
+ ):
114
+ """
115
+ Récupère la liste paginée des quartiers de Seattle.
116
+
117
+ Args:
118
+ skip (int): Nombre d'enregistrements à ignorer pour la pagination
119
+ limit (int): Nombre maximum d'enregistrements à retourner
120
+ db (Session): Session de base de données injectée par FastAPI
121
+
122
+ Returns:
123
+ List[Neighborhood]: Liste des quartiers de Seattle
124
+
125
+ Raises:
126
+ HTTPException: Erreur 500 si problème lors de la récupération des données
127
+
128
+ Example:
129
+ GET /api/v1/neighborhoods/?skip=0&limit=20
130
+
131
+ Note:
132
+ Les quartiers sont référencés par leur model_id dans le modèle ML
133
+ pour tenir compte de la localisation géographique dans les prédictions.
134
+ """
135
+ try:
136
+ neighborhoods = NeighborhoodService.get_all(db=db, skip=skip, limit=limit)
137
+ return neighborhoods
138
+ except Exception as e:
139
+ logger.error(f"Erreur lors de la récupération des quartiers: {e}")
140
+ raise HTTPException(
141
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
142
+ detail="Erreur lors de la récupération des quartiers",
143
+ )
144
+
145
+
146
+ @router.get(
147
+ "/{neighborhood_id}",
148
+ response_model=Neighborhood,
149
+ summary="Récupérer un quartier par ID",
150
+ description="""
151
+ Retourne les détails complets d'un quartier de Seattle spécifique.
152
+
153
+ Récupère toutes les informations d'un quartier incluant :
154
+ - Identifiant unique dans la base de données
155
+ - Nom du quartier (neighborhood_name)
156
+ - Identifiant utilisé par le modèle ML (model_id)
157
+ - Horodatages de création et modification
158
+
159
+ Les quartiers sont des facteurs géographiques importants pour les prédictions
160
+ énergétiques car ils déterminent :
161
+ - Les conditions climatiques locales
162
+ - Le type d'urbanisation (résidentiel, commercial, industriel)
163
+ - Les réglementations énergétiques locales
164
+ - Les habitudes de consommation régionales
165
+
166
+ L'ID doit correspondre à un quartier existant dans la base de données.
167
+ """,
168
+ responses={
169
+ 200: {
170
+ "description": "Quartier trouvé et retourné avec succès",
171
+ "content": {
172
+ "application/json": {
173
+ "example": {
174
+ "id": 5,
175
+ "neighborhood_name": "DOWNTOWN",
176
+ "model_id": 3,
177
+ "created_at": "2025-09-09T09:56:21Z",
178
+ "updated_at": "2025-09-09T09:56:21Z",
179
+ }
180
+ }
181
+ },
182
+ },
183
+ 404: {
184
+ "description": "Quartier non trouvé",
185
+ "content": {
186
+ "application/json": {
187
+ "example": {"detail": "Quartier avec l'ID 999 non trouvé"}
188
+ }
189
+ },
190
+ },
191
+ 500: {"description": "Erreur interne du serveur"},
192
+ },
193
+ )
194
+ def get_neighborhood(neighborhood_id: int, db: Session = Depends(get_db)):
195
+ """
196
+ Récupère un quartier spécifique par son ID.
197
+
198
+ Args:
199
+ neighborhood_id (int): Identifiant unique du quartier à récupérer
200
+ db (Session): Session de base de données injectée par FastAPI
201
+
202
+ Returns:
203
+ Neighborhood: Quartier avec détails complets
204
+
205
+ Raises:
206
+ HTTPException:
207
+ - 404 si le quartier n'existe pas
208
+ - 500 si erreur lors de la récupération
209
+
210
+ Example:
211
+ GET /api/v1/neighborhoods/5
212
+
213
+ Note:
214
+ Le model_id retourné est utilisé par le modèle ML pour encoder
215
+ la localisation géographique lors des prédictions énergétiques.
216
+ """
217
+ try:
218
+ neighborhood = NeighborhoodService.get_by_id(
219
+ db=db, neighborhood_id=neighborhood_id
220
+ )
221
+ return neighborhood
222
+ except Exception as e:
223
+ logger.error(f"Erreur lors de la récupération du quartier: {e}")
224
+ raise HTTPException(
225
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
226
+ detail="Erreur lors de la récupération du quartier",
227
+ )
228
+
229
+
230
+ @router.get(
231
+ "/search/",
232
+ response_model=List[Neighborhood],
233
+ summary="Rechercher des quartiers par nom",
234
+ description="""
235
+ Recherche des quartiers de Seattle par nom avec correspondance partielle.
236
+
237
+ Cette endpoint permet de rechercher des quartiers en utilisant une correspondance
238
+ partielle (insensible à la casse) sur le nom du quartier. Utile pour :
239
+ - Auto-complétion dans les interfaces utilisateur
240
+ - Recherche floue de quartiers
241
+ - Filtrage rapide de la liste des quartiers
242
+
243
+ La recherche s'effectue sur le champ neighborhood_name avec une correspondance
244
+ partielle utilisant l'opérateur SQL ILIKE (insensible à la casse).
245
+
246
+ Exemples de recherche :
247
+ - "down" trouvera "DOWNTOWN"
248
+ - "north" trouvera "NORTH", "NORTHEAST", "NORTHWEST"
249
+ - "queen" trouvera "MAGNOLIA / QUEEN ANNE"
250
+
251
+ Les résultats sont triés par ordre alphabétique du nom de quartier.
252
+ """,
253
+ responses={
254
+ 200: {
255
+ "description": "Quartiers trouvés avec succès",
256
+ "content": {
257
+ "application/json": {
258
+ "example": [
259
+ {
260
+ "id": 10,
261
+ "neighborhood_name": "NORTH",
262
+ "model_id": 8,
263
+ "created_at": "2025-09-09T09:56:21Z",
264
+ "updated_at": "2025-09-09T09:56:21Z",
265
+ },
266
+ {
267
+ "id": 11,
268
+ "neighborhood_name": "NORTHEAST",
269
+ "model_id": 9,
270
+ "created_at": "2025-09-09T09:56:21Z",
271
+ "updated_at": "2025-09-09T09:56:21Z",
272
+ },
273
+ ]
274
+ }
275
+ },
276
+ },
277
+ 422: {
278
+ "description": "Paramètres de recherche invalides",
279
+ "content": {
280
+ "application/json": {
281
+ "example": {
282
+ "detail": "Le terme de recherche doit contenir au moins 1 caractère"
283
+ }
284
+ }
285
+ },
286
+ },
287
+ 500: {"description": "Erreur interne du serveur"},
288
+ },
289
+ )
290
+ def search_neighborhoods(
291
+ q: str = Query(
292
+ ..., min_length=1, description="Terme de recherche (insensible à la casse)"
293
+ ),
294
+ limit: int = Query(50, ge=1, le=100, description="Nombre maximum de résultats"),
295
+ db: Session = Depends(get_db),
296
+ ):
297
+ """
298
+ Recherche des quartiers par nom avec correspondance partielle.
299
+
300
+ Args:
301
+ q (str): Terme de recherche à chercher dans les noms de quartiers
302
+ limit (int): Nombre maximum de résultats à retourner (par défaut 50, max 100)
303
+ db (Session): Session de base de données injectée par FastAPI
304
+
305
+ Returns:
306
+ List[Neighborhood]: Liste des quartiers correspondant à la recherche
307
+
308
+ Raises:
309
+ HTTPException:
310
+ - 422 si les paramètres de recherche sont invalides
311
+ - 500 si erreur lors de la recherche
312
+
313
+ Example:
314
+ GET /api/v1/neighborhoods/search/?q=north&limit=10
315
+
316
+ Note:
317
+ La recherche utilise ILIKE pour une correspondance insensible à la casse
318
+ et retourne les résultats triés par ordre alphabétique.
319
+ """
320
+ neighborhoods = (
321
+ db.query(NeighborhoodModel)
322
+ .filter(NeighborhoodModel.neighborhood_name.ilike(f"%{q.upper()}%"))
323
+ .order_by(NeighborhoodModel.neighborhood_name)
324
+ .limit(limit)
325
+ .all()
326
+ )
327
+
328
+ return neighborhoods
src/project5/routers/prediction_routes.py ADDED
@@ -0,0 +1,251 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Routes API pour les prédictions énergétiques des bâtiments.
3
+
4
+ Ce module contient les endpoints FastAPI pour :
5
+ - Prédiction de consommation énergétique basée sur les caractéristiques du bâtiment
6
+ - Récupération d'informations sur le modèle de machine learning
7
+ - Gestion des erreurs et logging approprié
8
+
9
+ Le modèle de prédiction utilise un RandomForestRegressor entraîné sur les données
10
+ énergétiques de Seattle pour prédire la consommation en kBTU/an. Les caractéristiques
11
+ principales incluent :
12
+ - Localisation (quartier)
13
+ - Type de bâtiment et usage principal
14
+ - Caractéristiques physiques (surface, étages, année de construction)
15
+ - Types d'énergie utilisés (électricité, gaz naturel, vapeur)
16
+
17
+ Endpoints disponibles :
18
+ - POST /api/v1/predict : Prédiction énergétique pour un bâtiment
19
+ - GET /api/v1/model/info : Informations sur le modèle ML
20
+
21
+ Auteur: François Hellebuyck
22
+ Projet: Building Energy Prediction API - OpenClassrooms Projet 5
23
+ """
24
+
25
+ import logging
26
+
27
+ from fastapi import APIRouter, Depends, HTTPException
28
+
29
+ from project5.schemas.building_model import BuildingFeatures, PredictionResponse
30
+ from project5.services.building_model_service import PredictionService
31
+
32
+ logger = logging.getLogger(__name__)
33
+
34
+ # Instance globale du service (peut être injectée via des dépendances)
35
+ prediction_service = PredictionService()
36
+
37
+
38
+ def get_prediction_service() -> PredictionService:
39
+ """
40
+ Injection de dépendance pour le service de prédiction.
41
+
42
+ Returns:
43
+ PredictionService: Instance du service de prédiction configuré
44
+
45
+ Note:
46
+ Cette fonction permet l'injection de dépendance FastAPI pour le service
47
+ de prédiction, facilitant les tests et la modularité.
48
+ """
49
+ return prediction_service
50
+
51
+
52
+ router = APIRouter(prefix="/api/v1", tags=["predictions"])
53
+
54
+
55
+ @router.post(
56
+ "/predict",
57
+ response_model=PredictionResponse,
58
+ summary="Prédire la consommation énergétique d'un bâtiment",
59
+ description="""
60
+ Effectue une prédiction de consommation énergétique basée sur les caractéristiques du bâtiment.
61
+
62
+ Cette endpoint utilise un modèle de machine learning (RandomForestRegressor) entraîné
63
+ sur les données énergétiques de Seattle pour prédire la consommation annuelle en kBTU.
64
+
65
+ Caractéristiques requises pour la prédiction :
66
+ - **neighborhood_encoded** : Quartier encodé numériquement (0-13, -1 pour UNKNOWN)
67
+ - **building_type_encoded** : Type de bâtiment encodé (0-7, -1 pour UNKNOWN)
68
+ - **largest_property_use_type_encoded** : Usage principal encodé (0-27, -1 pour UNKNOWN)
69
+ - **primary_property_type_encoded** : Type de propriété principal encodé
70
+ - **year_built** : Année de construction du bâtiment
71
+ - **number_of_buildings** : Nombre de bâtiments sur la propriété
72
+ - **number_of_floors** : Nombre d'étages
73
+ - **property_gfa_total** : Surface totale brute en pieds carrés
74
+ - **steam** : Utilisation de vapeur (0/1)
75
+ - **electricity** : Utilisation d'électricité (0/1)
76
+ - **natural_gas** : Utilisation de gaz naturel (0/1)
77
+
78
+ Le modèle retourne une prédiction de consommation énergétique avec un intervalle
79
+ de confiance basé sur la variance des arbres de la forêt aléatoire.
80
+ """,
81
+ responses={
82
+ 200: {
83
+ "description": "Prédiction effectuée avec succès",
84
+ "content": {
85
+ "application/json": {
86
+ "example": {
87
+ "predicted_energy_use": 45678.9,
88
+ "confidence_interval": {"lower": 42000.5, "upper": 49357.3},
89
+ "model_version": "1.0",
90
+ "features_used": [
91
+ "neighborhood_encoded",
92
+ "building_type_encoded",
93
+ "year_built",
94
+ "property_gfa_total",
95
+ ],
96
+ }
97
+ }
98
+ },
99
+ },
100
+ 422: {
101
+ "description": "Données d'entrée invalides",
102
+ "content": {
103
+ "application/json": {
104
+ "example": {
105
+ "detail": [
106
+ {
107
+ "loc": ["body", "year_built"],
108
+ "msg": "ensure this value is greater than 1800",
109
+ "type": "value_error.number.not_gt",
110
+ }
111
+ ]
112
+ }
113
+ }
114
+ },
115
+ },
116
+ 500: {"description": "Erreur interne du serveur lors de la prédiction"},
117
+ },
118
+ )
119
+ async def predict_building_value(
120
+ features: BuildingFeatures,
121
+ service: PredictionService = Depends(get_prediction_service),
122
+ ):
123
+ """
124
+ Effectue une prédiction de consommation énergétique basée sur les caractéristiques du bâtiment.
125
+
126
+ Args:
127
+ features (BuildingFeatures): Caractéristiques encodées du bâtiment pour la prédiction
128
+ service (PredictionService): Service de prédiction injecté par FastAPI
129
+
130
+ Returns:
131
+ PredictionResponse: Prédiction avec consommation énergétique et intervalle de confiance
132
+
133
+ Raises:
134
+ HTTPException:
135
+ - 422 si les données d'entrée sont invalides
136
+ - 500 si erreur lors de la prédiction ML
137
+
138
+ Example:
139
+ POST /api/v1/predict
140
+ {
141
+ "neighborhood_encoded": 3,
142
+ "building_type_encoded": 1,
143
+ "largest_property_use_type_encoded": 18,
144
+ "primary_property_type_encoded": 18,
145
+ "year_built": 1995,
146
+ "number_of_buildings": 1,
147
+ "number_of_floors": 15,
148
+ "property_gfa_total": 50000.0,
149
+ "steam": 1,
150
+ "electricity": 1,
151
+ "natural_gas": 1
152
+ }
153
+
154
+ Note:
155
+ Les valeurs encodées doivent correspondre aux encodages utilisés lors
156
+ de l'entraînement du modèle. Utilisez l'endpoint /buildings/model/{id}
157
+ pour obtenir les données pré-encodées d'un bâtiment existant.
158
+ """
159
+ try:
160
+ result = await service.predict(features)
161
+ return PredictionResponse(**result)
162
+
163
+ except Exception as e:
164
+ logger.error(f"Erreur dans la prédiction: {e}")
165
+ raise HTTPException(
166
+ status_code=500, detail=f"Erreur lors de la prédiction: {str(e)}"
167
+ )
168
+
169
+
170
+ @router.get(
171
+ "/model/info",
172
+ summary="Informations sur le modèle de machine learning",
173
+ description="""
174
+ Retourne des informations détaillées sur le modèle de prédiction énergétique.
175
+
176
+ Cette endpoint fournit des métadonnées essentielles sur le modèle ML utilisé :
177
+ - Version du modèle déployé
178
+ - Type d'algorithme (RandomForestRegressor)
179
+ - Nombre de caractéristiques utilisées
180
+ - Importance des caractéristiques (feature importance)
181
+
182
+ L'importance des caractéristiques indique quelles variables ont le plus
183
+ d'influence sur les prédictions :
184
+ - Valeurs plus élevées = plus d'importance
185
+ - Somme totale des importances = 1.0
186
+ - Utile pour comprendre quels facteurs influencent le plus la consommation
187
+
188
+ Ces informations sont utiles pour :
189
+ - Validation du modèle en production
190
+ - Debugging des prédictions
191
+ - Analyse de l'importance des variables
192
+ - Monitoring de la performance du modèle
193
+ """,
194
+ responses={
195
+ 200: {
196
+ "description": "Informations du modèle récupérées avec succès",
197
+ "content": {
198
+ "application/json": {
199
+ "example": {
200
+ "model_version": "1.0",
201
+ "model_type": "RandomForestRegressor",
202
+ "feature_count": 11,
203
+ "feature_importance": {
204
+ "property_gfa_total": 0.45,
205
+ "year_built": 0.18,
206
+ "neighborhood_encoded": 0.12,
207
+ "building_type_encoded": 0.08,
208
+ "number_of_floors": 0.07,
209
+ "largest_property_use_type_encoded": 0.05,
210
+ "electricity": 0.02,
211
+ "natural_gas": 0.02,
212
+ "steam": 0.01,
213
+ },
214
+ }
215
+ }
216
+ },
217
+ },
218
+ 500: {"description": "Erreur interne du serveur"},
219
+ },
220
+ )
221
+ async def get_model_info(service: PredictionService = Depends(get_prediction_service)):
222
+ """
223
+ Retourne des informations détaillées sur le modèle de machine learning.
224
+
225
+ Args:
226
+ service (PredictionService): Service de prédiction injecté par FastAPI
227
+
228
+ Returns:
229
+ dict: Informations sur le modèle incluant version, type, et importance des caractéristiques
230
+
231
+ Raises:
232
+ HTTPException: Erreur 500 si problème lors de la récupération des informations
233
+
234
+ Example:
235
+ GET /api/v1/model/info
236
+
237
+ Note:
238
+ L'importance des caractéristiques est calculée par le RandomForestRegressor
239
+ et indique la contribution relative de chaque variable aux prédictions.
240
+ """
241
+ try:
242
+ feature_importance = service.get_feature_importance()
243
+ return {
244
+ "model_version": service.model_version,
245
+ "model_type": "RandomForestRegressor",
246
+ "feature_count": len(service.feature_names),
247
+ "feature_importance": feature_importance,
248
+ }
249
+ except Exception as e:
250
+ logger.error(f"Erreur lors de la récupération des infos du modèle: {e}")
251
+ raise HTTPException(status_code=500, detail=f"Erreur: {str(e)}")
src/project5/routers/properties_routes.py ADDED
@@ -0,0 +1,246 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Routes API pour la gestion des propriétés d'usage des bâtiments.
3
+
4
+ Ce module contient les endpoints FastAPI pour :
5
+ - Récupération de la liste des propriétés d'usage avec pagination
6
+ - Récupération d'une propriété spécifique par ID
7
+ - Gestion des erreurs et logging approprié
8
+
9
+ Les propriétés d'usage représentent les utilisations spécifiques des bâtiments
10
+ et sont plus granulaires que les catégories. Elles permettent une classification
11
+ fine pour les prédictions énergétiques du modèle ML.
12
+
13
+ Propriétés principales disponibles :
14
+ - OFFICE : Bureaux
15
+ - RETAIL STORE : Magasin de détail
16
+ - RESTAURANT : Restaurant
17
+ - K-12 SCHOOL : École primaire/secondaire
18
+ - COLLEGE/UNIVERSITY : Enseignement supérieur
19
+ - HOSPITAL : Hôpital
20
+ - HOTEL : Hôtel
21
+ - MULTIFAMILY HOUSING : Logement collectif
22
+ - WAREHOUSE : Entrepôt
23
+ - DATA CENTER : Centre de données
24
+ - Et autres usages spécialisés
25
+
26
+ Chaque propriété est liée à une catégorie parent et possède un model_id
27
+ utilisé par le modèle ML pour l'encodage numérique.
28
+
29
+ Endpoints disponibles :
30
+ - GET /api/v1/properties/ : Liste paginée des propriétés
31
+ - GET /api/v1/properties/{id} : Propriété spécifique par ID
32
+
33
+ Auteur: François Hellebuyck
34
+ Projet: Building Energy Prediction API - OpenClassrooms Projet 5
35
+ """
36
+
37
+ import logging
38
+ from typing import List
39
+
40
+ from fastapi import APIRouter, Depends, HTTPException, Query, status
41
+ from sqlalchemy.orm import Session
42
+
43
+ from project5.database import get_db
44
+ from project5.schemas.property import PropertyList, PropertyResponse
45
+ from project5.services.property_service import PropertyService
46
+
47
+ logger = logging.getLogger(__name__)
48
+
49
+ router = APIRouter(prefix="/api/v1/properties", tags=["properties"])
50
+
51
+
52
+ @router.get(
53
+ "/",
54
+ response_model=List[PropertyList],
55
+ summary="Récupérer toutes les propriétés d'usage",
56
+ description="""
57
+ Retourne la liste paginée de toutes les propriétés d'usage des bâtiments disponibles dans le système.
58
+
59
+ Cette endpoint permet de récupérer l'ensemble des types d'usage spécifiques des
60
+ bâtiments utilisés pour une classification fine lors des prédictions énergétiques.
61
+ Les propriétés sont plus granulaires que les catégories et permettent de distinguer
62
+ des usages précis ayant des profils énergétiques différents.
63
+
64
+ Propriétés d'usage disponibles dans le système :
65
+ - **OFFICE** : Bureaux et espaces administratifs
66
+ - **RETAIL STORE** : Magasins et commerces de détail
67
+ - **RESTAURANT** : Restaurants et services de restauration
68
+ - **K-12 SCHOOL** : Écoles primaires et secondaires
69
+ - **COLLEGE/UNIVERSITY** : Établissements d'enseignement supérieur
70
+ - **HOSPITAL** : Hôpitaux et centres médicaux
71
+ - **HOTEL** : Hôtels et hébergements
72
+ - **MULTIFAMILY HOUSING** : Logements collectifs et appartements
73
+ - **WAREHOUSE** : Entrepôts et stockage
74
+ - **DATA CENTER** : Centres de données et serveurs
75
+ - **BANK BRANCH** : Agences bancaires
76
+ - **COURTHOUSE** : Tribunaux et bâtiments judiciaires
77
+ - **LIBRARY** : Bibliothèques
78
+ - **MEDICAL OFFICE** : Cabinets médicaux
79
+ - **PARKING** : Structures de stationnement
80
+ - Et autres usages spécialisés
81
+
82
+ Chaque propriété appartient à une catégorie parent et possède un model_id
83
+ unique utilisé par le modèle ML pour l'encodage numérique lors des prédictions.
84
+
85
+ Paramètres de pagination :
86
+ - skip : nombre d'enregistrements à ignorer (par défaut 0)
87
+ - limit : nombre maximum d'enregistrements à retourner (par défaut 100, max 1000)
88
+ """,
89
+ responses={
90
+ 200: {
91
+ "description": "Liste des propriétés récupérée avec succès",
92
+ "content": {
93
+ "application/json": {
94
+ "example": [
95
+ {
96
+ "id": 20,
97
+ "model_id": 18,
98
+ "property_name": "OFFICE",
99
+ "category_id": 11,
100
+ "category_name": "Bureaux",
101
+ "created_at": "2025-09-09T09:56:21Z",
102
+ "updated_at": "2025-09-09T09:56:21Z",
103
+ },
104
+ {
105
+ "id": 24,
106
+ "model_id": 22,
107
+ "property_name": "RETAIL STORE",
108
+ "category_id": 18,
109
+ "category_name": "Commerce de détail",
110
+ "created_at": "2025-09-09T09:56:21Z",
111
+ "updated_at": "2025-09-09T09:56:21Z",
112
+ },
113
+ ]
114
+ }
115
+ },
116
+ },
117
+ 500: {"description": "Erreur interne du serveur"},
118
+ },
119
+ )
120
+ def get_properties(
121
+ skip: int = Query(0, ge=0, description="Nombre d'éléments à ignorer (pagination)"),
122
+ limit: int = Query(
123
+ 100, ge=1, le=1000, description="Nombre maximum d'éléments à retourner"
124
+ ),
125
+ db: Session = Depends(get_db),
126
+ ):
127
+ """
128
+ Récupère la liste paginée des propriétés d'usage des bâtiments.
129
+
130
+ Args:
131
+ skip (int): Nombre d'enregistrements à ignorer pour la pagination
132
+ limit (int): Nombre maximum d'enregistrements à retourner
133
+ db (Session): Session de base de données injectée par FastAPI
134
+
135
+ Returns:
136
+ List[PropertyList]: Liste des propriétés d'usage avec informations de catégorie
137
+
138
+ Raises:
139
+ HTTPException: Erreur 500 si problème lors de la récupération des données
140
+
141
+ Example:
142
+ GET /api/v1/properties/?skip=0&limit=30
143
+
144
+ Note:
145
+ Les propriétés sont référencées par leur model_id dans le modèle ML
146
+ pour effectuer une classification fine des usages lors des prédictions.
147
+ Chaque propriété hérite des caractéristiques de sa catégorie parent.
148
+ """
149
+ try:
150
+ categories = PropertyService.get_all(db=db, skip=skip, limit=limit)
151
+ return categories
152
+ except Exception as e:
153
+ logger.error(f"Erreur lors de la récupération des properties: {e}")
154
+ raise HTTPException(
155
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
156
+ detail="Erreur lors de la récupération des properties",
157
+ )
158
+
159
+
160
+ @router.get(
161
+ "/{id}",
162
+ response_model=PropertyResponse,
163
+ summary="Récupérer une propriété d'usage par ID",
164
+ description="""
165
+ Retourne les détails complets d'une propriété d'usage spécifique.
166
+
167
+ Récupère toutes les informations d'une propriété d'usage incluant :
168
+ - Identifiant unique dans la base de données
169
+ - Identifiant utilisé par le modèle ML (model_id)
170
+ - Nom de la propriété d'usage
171
+ - Relation avec la catégorie parent (category_id et nom)
172
+ - Horodatages de création et modification
173
+
174
+ Les propriétés d'usage permettent une classification fine des bâtiments
175
+ selon leur utilisation spécifique, au-delà des catégories générales.
176
+ Elles sont essentielles pour les prédictions énergétiques car différents
177
+ usages ont des profils de consommation très différents :
178
+
179
+ - **OFFICE** vs **DATA CENTER** : consommation informatique différente
180
+ - **RESTAURANT** vs **RETAIL STORE** : besoins énergétiques distincts
181
+ - **HOSPITAL** vs **MEDICAL OFFICE** : intensité énergétique variable
182
+ - **K-12 SCHOOL** vs **COLLEGE/UNIVERSITY** : profils d'occupation différents
183
+
184
+ L'ID doit correspondre à une propriété existante dans la base de données.
185
+ """,
186
+ responses={
187
+ 200: {
188
+ "description": "Propriété trouvée et retournée avec succès",
189
+ "content": {
190
+ "application/json": {
191
+ "example": {
192
+ "id": 20,
193
+ "model_id": 18,
194
+ "property_name": "OFFICE",
195
+ "category_id": 11,
196
+ "category_name": "Bureaux",
197
+ "created_at": "2025-09-09T09:56:21Z",
198
+ "updated_at": "2025-09-09T09:56:21Z",
199
+ }
200
+ }
201
+ },
202
+ },
203
+ 404: {
204
+ "description": "Propriété non trouvée",
205
+ "content": {
206
+ "application/json": {
207
+ "example": {"detail": "Propriété avec l'ID 999 non trouvée"}
208
+ }
209
+ },
210
+ },
211
+ 500: {"description": "Erreur interne du serveur"},
212
+ },
213
+ )
214
+ def get_property(id: int, db: Session = Depends(get_db)):
215
+ """
216
+ Récupère une propriété d'usage spécifique par son ID.
217
+
218
+ Args:
219
+ id (int): Identifiant unique de la propriété à récupérer
220
+ db (Session): Session de base de données injectée par FastAPI
221
+
222
+ Returns:
223
+ PropertyResponse: Propriété d'usage avec détails complets et catégorie parent
224
+
225
+ Raises:
226
+ HTTPException:
227
+ - 404 si la propriété n'existe pas
228
+ - 500 si erreur lors de la récupération
229
+
230
+ Example:
231
+ GET /api/v1/properties/20
232
+
233
+ Note:
234
+ Le model_id retourné est utilisé par le modèle ML pour encoder
235
+ l'usage spécifique lors des prédictions énergétiques. Cette granularité
236
+ permet des prédictions plus précises que les catégories générales.
237
+ """
238
+ try:
239
+ property = PropertyService.get_by_id(db=db, property_id=id)
240
+ return property
241
+ except Exception as e:
242
+ logger.error(f"Erreur lors de la récupération de la propriété: {e}")
243
+ raise HTTPException(
244
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
245
+ detail="Erreur lors de la récupération de la propriété",
246
+ )
src/project5/schemas/__init__.py ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Package des schémas Pydantic pour l'API de prédiction énergétique.
3
+
4
+ Ce package contient tous les schémas de validation et sérialisation des données
5
+ utilisés par l'API FastAPI pour la gestion des bâtiments et prédictions énergétiques.
6
+
7
+ Schémas disponibles :
8
+ - Neighborhood : Schémas pour les quartiers de Seattle
9
+ - NeighborhoodExists : Validation d'existence de quartier
10
+ - PaginatedNeighborhoods : Réponses paginées de quartiers
11
+ - HealthCheck : Schéma de vérification de l'état de l'API
12
+ - ServiceInfo : Informations sur le service API
13
+ - ErrorResponse : Schéma de réponse d'erreur standardisé
14
+
15
+ Architecture des schémas :
16
+ - Validation des données d'entrée avec Pydantic Field contraints
17
+ - Sérialisation automatique depuis les modèles SQLAlchemy
18
+ - Support de la pagination pour les listes d'éléments
19
+ - Gestion des erreurs avec messages structurés
20
+ - Configuration de JSON encoders pour les types spéciaux (Decimal, datetime)
21
+
22
+ Note :
23
+ Ces schémas assurent la validation côté API et la documentation automatique
24
+ OpenAPI/Swagger. Ils définissent les contrats d'interface entre clients et serveur.
25
+ """
26
+
27
+ # Import du schema Neighborhood depuis le fichier approprié
28
+ from .neighborhood import (
29
+ ErrorResponse,
30
+ HealthCheck,
31
+ Neighborhood,
32
+ NeighborhoodExists,
33
+ PaginatedNeighborhoods,
34
+ ServiceInfo,
35
+ )
36
+
37
+ # Exposer les schemas au niveau du package
38
+ __all__ = [
39
+ "Neighborhood",
40
+ "NeighborhoodExists",
41
+ "PaginatedNeighborhoods",
42
+ "HealthCheck",
43
+ "ServiceInfo",
44
+ "ErrorResponse",
45
+ ]
src/project5/schemas/building_energy_prediction.py ADDED
@@ -0,0 +1,253 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Schémas Pydantic pour les prédictions énergétiques des bâtiments.
3
+
4
+ Ce module définit tous les schémas de validation et sérialisation pour les données
5
+ de prédictions énergétiques calculées par le modèle de machine learning.
6
+
7
+ Classes disponibles :
8
+ - BuildingEnergyPredictionsBase : Schéma de base avec les champs communs
9
+ - BuildingEnergyPredictionsCreate : Schéma pour la création de nouvelles prédictions
10
+ - BuildingEnergyPredictionsUpdate : Schéma pour la mise à jour de prédictions
11
+ - BuildingEnergyPredictionsResponse : Schéma de réponse avec ID et métadonnées
12
+ - BuildingEnergyPredictionsList : Schéma pour les réponses paginées
13
+ - BuildingEnergyPredictionsStats : Schéma pour les statistiques agrégées
14
+
15
+ Fonctionnalités :
16
+ - Validation automatique des types et contraintes (valeurs positives, etc.)
17
+ - Sérialisation automatique depuis les modèles SQLAlchemy
18
+ - Encodage JSON personnalisé pour Decimal et datetime
19
+ - Support de la pagination avec métadonnées (total, pages, etc.)
20
+ - Calculs statistiques (moyennes, min/max, compteurs)
21
+
22
+ Note :
23
+ Les prédictions énergétiques sont exprimées en kBTU (thousand British Thermal Units)
24
+ avec normalisation météorologique pour compenser les variations climatiques.
25
+ """
26
+
27
+ from datetime import datetime
28
+ from decimal import Decimal
29
+ from typing import Optional
30
+
31
+ from pydantic import BaseModel, ConfigDict, Field
32
+
33
+
34
+ class BuildingEnergyPredictionsBase(BaseModel):
35
+ """
36
+ Schéma de base pour les prédictions énergétiques des bâtiments.
37
+
38
+ Cette classe définit les champs communs utilisés par tous les schémas
39
+ de prédictions énergétiques (création, mise à jour, réponse).
40
+
41
+ Attributes:
42
+ building_id (int): Identifiant du bâtiment concerné (obligatoire, > 0)
43
+ site_energy_use_wn_kbtu (Decimal, optional): Consommation énergétique
44
+ en kBTU avec normalisation météorologique (>= 0)
45
+ predicted (bool, optional): Indique si la valeur est prédite (True)
46
+ ou mesurée (False). Par défaut True.
47
+ updated_at (datetime, optional): Date et heure de la prédiction
48
+ avec fuseau horaire
49
+
50
+ Note:
51
+ La normalisation météorologique ajuste les données de consommation
52
+ pour compenser les variations climatiques saisonnières.
53
+ """
54
+
55
+ building_id: int = Field(
56
+ ...,
57
+ description="Identifiant du bâtiment concerné (clé étrangère vers buildings.id)",
58
+ gt=0,
59
+ )
60
+ site_energy_use_wn_kbtu: Optional[Decimal] = Field(
61
+ None,
62
+ description="Consommation énergétique du site en kBTU avec normalisation météorologique",
63
+ ge=0,
64
+ )
65
+ predicted: Optional[bool] = Field(
66
+ True, description="Indique si la valeur est prédite (TRUE) ou mesurée (FALSE)"
67
+ )
68
+ updated_at: Optional[datetime] = Field(
69
+ None, description="Date et heure de la prédiction avec fuseau horaire"
70
+ )
71
+
72
+
73
+ class BuildingEnergyPredictionsCreate(BuildingEnergyPredictionsBase):
74
+ """
75
+ Schéma pour la création d'une nouvelle prédiction énergétique.
76
+
77
+ Hérite de BuildingEnergyPredictionsBase et utilise tous ses champs
78
+ sans modification. Ce schéma est utilisé lors des requêtes POST
79
+ pour créer de nouvelles prédictions dans la base de données.
80
+
81
+ Usage:
82
+ - Endpoint POST /building-energy-predictions/
83
+ - Validation automatique des contraintes de la classe de base
84
+ - Génération automatique de l'ID lors de l'insertion en base
85
+
86
+ Example:
87
+ {
88
+ "building_id": 12345,
89
+ "site_energy_use_wn_kbtu": 2500.75,
90
+ "predicted": true,
91
+ "updated_at": "2024-01-15T10:30:00Z"
92
+ }
93
+ """
94
+
95
+ pass
96
+
97
+
98
+ class BuildingEnergyPredictionsUpdate(BaseModel):
99
+ """
100
+ Schéma pour la mise à jour partielle d'une prédiction énergétique.
101
+
102
+ Tous les champs sont optionnels pour permettre les mises à jour partielles.
103
+ Seuls les champs fournis seront mis à jour dans la base de données.
104
+
105
+ Attributes:
106
+ site_energy_use_wn_kbtu (Decimal, optional): Nouvelle consommation
107
+ énergétique en kBTU (>= 0)
108
+ predicted (bool, optional): Nouveau statut prédiction/mesure
109
+ updated_at (datetime, optional): Nouvelle date de prédiction
110
+
111
+ Usage:
112
+ - Endpoint PATCH /building-energy-predictions/{id}
113
+ - Permet la mise à jour de champs individuels
114
+ - building_id non modifiable (contrainte métier)
115
+
116
+ Note:
117
+ L'ID du bâtiment (building_id) n'est pas modifiable pour préserver
118
+ l'intégrité référentielle des données.
119
+ """
120
+
121
+ site_energy_use_wn_kbtu: Optional[Decimal] = Field(
122
+ None,
123
+ description="Consommation énergétique du site en kBTU avec normalisation météorologique",
124
+ ge=0,
125
+ )
126
+ predicted: Optional[bool] = Field(
127
+ None, description="Indique si la valeur est prédite (TRUE) ou mesurée (FALSE)"
128
+ )
129
+ updated_at: Optional[datetime] = Field(
130
+ None, description="Date et heure de la prédiction avec fuseau horaire"
131
+ )
132
+
133
+
134
+ class BuildingEnergyPredictionsResponse(BuildingEnergyPredictionsBase):
135
+ """
136
+ Schéma de réponse pour une prédiction énergétique individuelle.
137
+
138
+ Hérite de BuildingEnergyPredictionsBase et ajoute l'ID généré
139
+ automatiquement par la base de données. Inclut la configuration
140
+ de sérialisation pour la conversion depuis les modèles SQLAlchemy.
141
+
142
+ Attributes:
143
+ id (int): Identifiant unique de la prédiction (clé primaire)
144
+ + tous les champs de BuildingEnergyPredictionsBase
145
+
146
+ Configuration:
147
+ - from_attributes=True : Conversion automatique depuis SQLAlchemy
148
+ - str_strip_whitespace=True : Nettoyage automatique des chaînes
149
+ - validate_assignment=True : Validation lors des assignations
150
+ - json_encoders : Encodage personnalisé Decimal->float, datetime->ISO
151
+
152
+ Usage:
153
+ - Réponses des endpoints GET /building-energy-predictions/
154
+ - Sérialisation automatique des objets BuildingEnergyPredictions
155
+ """
156
+
157
+ id: int = Field(
158
+ ...,
159
+ description="Identifiant unique de la prédiction (clé primaire auto-incrémentée)",
160
+ )
161
+
162
+ model_config = ConfigDict(
163
+ from_attributes=True,
164
+ # Permet la conversion automatique depuis les objets SQLAlchemy
165
+ str_strip_whitespace=True,
166
+ # Validation des types
167
+ validate_assignment=True,
168
+ # Configuration pour les décimales
169
+ json_encoders={
170
+ Decimal: lambda v: float(v) if v is not None else None,
171
+ datetime: lambda v: v.isoformat() if v is not None else None,
172
+ },
173
+ )
174
+
175
+
176
+ class BuildingEnergyPredictionsList(BaseModel):
177
+ """
178
+ Schéma pour une liste paginée de prédictions énergétiques.
179
+
180
+ Structure de réponse standardisée pour les endpoints retournant
181
+ plusieurs prédictions avec support de la pagination.
182
+
183
+ Attributes:
184
+ items (list[BuildingEnergyPredictionsResponse]): Liste des prédictions
185
+ total (int): Nombre total d'éléments dans la base (toutes pages)
186
+ page (int): Numéro de la page actuelle (commence à 1)
187
+ size (int): Nombre d'éléments par page (1-100)
188
+ pages (int): Nombre total de pages calculé
189
+
190
+ Usage:
191
+ - Endpoint GET /building-energy-predictions/ avec paramètres de pagination
192
+ - Calcul automatique du nombre de pages : ceil(total / size)
193
+ - Navigation facilitée avec métadonnées de pagination
194
+
195
+ Example de réponse:
196
+ {
197
+ "items": [...],
198
+ "total": 1500,
199
+ "page": 2,
200
+ "size": 50,
201
+ "pages": 30
202
+ }
203
+ """
204
+
205
+ items: list[BuildingEnergyPredictionsResponse]
206
+ total: int = Field(..., description="Nombre total d'éléments")
207
+ page: int = Field(..., description="Page actuelle", ge=1)
208
+ size: int = Field(..., description="Taille de la page", ge=1, le=100)
209
+ pages: int = Field(..., description="Nombre total de pages")
210
+
211
+
212
+ class BuildingEnergyPredictionsStats(BaseModel):
213
+ """
214
+ Schéma pour les statistiques agrégées des prédictions énergétiques.
215
+
216
+ Fournit une vue d'ensemble des données de prédictions avec des
217
+ métriques statistiques calculées côté base de données.
218
+
219
+ Attributes:
220
+ total_predictions (int): Nombre total de prédictions en base
221
+ predicted_count (int): Nombre de valeurs prédites (ML)
222
+ measured_count (int): Nombre de valeurs mesurées (réelles)
223
+ avg_energy_consumption (Decimal, optional): Consommation moyenne en kBTU
224
+ min_energy_consumption (Decimal, optional): Consommation minimale en kBTU
225
+ max_energy_consumption (Decimal, optional): Consommation maximale en kBTU
226
+ latest_updated_at (datetime, optional): Date de la prédiction
227
+ la plus récente
228
+
229
+ Usage:
230
+ - Endpoint GET /building-energy-predictions/stats
231
+ - Tableaux de bord et reporting
232
+ - Monitoring de la qualité des prédictions
233
+
234
+ Note:
235
+ Les statistiques sont calculées en temps réel via des requêtes
236
+ d'agrégation SQL pour garantir la fraîcheur des données.
237
+ """
238
+
239
+ total_predictions: int = Field(..., description="Nombre total de prédictions")
240
+ predicted_count: int = Field(..., description="Nombre de valeurs prédites")
241
+ measured_count: int = Field(..., description="Nombre de valeurs mesurées")
242
+ avg_energy_consumption: Optional[Decimal] = Field(
243
+ None, description="Consommation énergétique moyenne en kBTU"
244
+ )
245
+ min_energy_consumption: Optional[Decimal] = Field(
246
+ None, description="Consommation énergétique minimale en kBTU"
247
+ )
248
+ max_energy_consumption: Optional[Decimal] = Field(
249
+ None, description="Consommation énergétique maximale en kBTU"
250
+ )
251
+ latest_updated_at: Optional[datetime] = Field(
252
+ None, description="Date de la prédiction la plus récente"
253
+ )