From 647a22cb2cd5b2f257c0fb79d87557bd91e65a66 Mon Sep 17 00:00:00 2001 From: scayac Date: Sun, 22 Mar 2026 20:37:35 +0100 Subject: [PATCH] First commit --- .gitignore | 1 + arduino/.gitignore | 5 + arduino/.vscode/extensions.json | 10 + arduino/README_ARDUINO.md | 78 +++ arduino/platformio.ini | 5 + arduino/src/main.cpp | 74 +++ backend/README_BACKEND.md | 123 +++++ backend/app/__init__.py | 0 .../app/__pycache__/__init__.cpython-312.pyc | Bin 0 -> 160 bytes .../__pycache__/audio_player.cpython-312.pyc | Bin 0 -> 3288 bytes .../__pycache__/config_store.cpython-312.pyc | Bin 0 -> 3441 bytes backend/app/__pycache__/main.cpython-312.pyc | Bin 0 -> 9274 bytes .../app/__pycache__/models.cpython-312.pyc | Bin 0 -> 3199 bytes .../app/__pycache__/security.cpython-312.pyc | Bin 0 -> 2562 bytes .../serial_listener.cpython-312.pyc | Bin 0 -> 5189 bytes backend/app/audio_player.py | 53 +++ backend/app/config_store.py | 50 ++ backend/app/main.py | 180 +++++++ backend/app/models.py | 52 ++ backend/app/security.py | 62 +++ backend/app/serial_listener.py | 72 +++ backend/certs/cert.pem | 19 + backend/certs/key.pem | 28 ++ backend/data/conf.json | 34 ++ backend/data/musiques/.gitkeep | 0 backend/data/musiques/SONNERIE A.mp3 | Bin 0 -> 334073 bytes backend/data/musiques/SONNERIE B.mp3 | Bin 0 -> 169142 bytes backend/data/musiques/SONNERIE C.mp3 | Bin 0 -> 195088 bytes backend/data/musiques/SONNERIE D.mp3 | Bin 0 -> 197395 bytes backend/data/musiques/SONNERIE E.mp3 | Bin 0 -> 148807 bytes backend/data/musiques/SONNERIE F.mp3 | Bin 0 -> 197623 bytes backend/data/musiques/SONNERIE G.mp3 | Bin 0 -> 174373 bytes backend/data/musiques/SONNERIE H.mp3 | Bin 0 -> 177863 bytes backend/data/musiques/SONNERIE I.mp3 | Bin 0 -> 172463 bytes backend/data/musiques/SONNERIE J.mp3 | Bin 0 -> 294141 bytes backend/data/musiques/SONNERIE K.mp3 | Bin 0 -> 173083 bytes backend/data/musiques/SONNERIE L.mp3 | Bin 0 -> 151933 bytes backend/data/musiques/SONNERIE M.mp3 | Bin 0 -> 147581 bytes backend/data/musiques/SONNERIE N.mp3 | Bin 0 -> 175389 bytes backend/data/musiques/SONNERIE O.mp3 | Bin 0 -> 198249 bytes backend/data/musiques/SONNERIE P.mp3 | Bin 0 -> 207286 bytes backend/data/musiques/SONNERIE_P.mp3 | Bin 0 -> 207286 bytes backend/requirements.txt | 3 + backend/run.py | 36 ++ backend/systemd/pysonnerie-backend.service | 14 + frontend/README_FRONTEND.md | 36 ++ frontend/app/__init__.py | 23 + .../app/__pycache__/__init__.cpython-312.pyc | Bin 0 -> 1196 bytes .../backend_client.cpython-312.pyc | Bin 0 -> 5717 bytes .../app/__pycache__/routes.cpython-312.pyc | Bin 0 -> 15620 bytes frontend/app/backend_client.py | 104 ++++ frontend/app/routes.py | 326 +++++++++++++ frontend/app/static/css/style.css | 444 ++++++++++++++++++ frontend/app/templates/base.html | 49 ++ frontend/app/templates/dashboard.html | 347 ++++++++++++++ frontend/app/templates/login.html | 23 + frontend/requirements.txt | 2 + frontend/run.py | 8 + 58 files changed, 2261 insertions(+) create mode 100644 .gitignore create mode 100644 arduino/.gitignore create mode 100644 arduino/.vscode/extensions.json create mode 100644 arduino/README_ARDUINO.md create mode 100644 arduino/platformio.ini create mode 100644 arduino/src/main.cpp create mode 100644 backend/README_BACKEND.md create mode 100644 backend/app/__init__.py create mode 100644 backend/app/__pycache__/__init__.cpython-312.pyc create mode 100644 backend/app/__pycache__/audio_player.cpython-312.pyc create mode 100644 backend/app/__pycache__/config_store.cpython-312.pyc create mode 100644 backend/app/__pycache__/main.cpython-312.pyc create mode 100644 backend/app/__pycache__/models.cpython-312.pyc create mode 100644 backend/app/__pycache__/security.cpython-312.pyc create mode 100644 backend/app/__pycache__/serial_listener.cpython-312.pyc create mode 100644 backend/app/audio_player.py create mode 100644 backend/app/config_store.py create mode 100644 backend/app/main.py create mode 100644 backend/app/models.py create mode 100644 backend/app/security.py create mode 100644 backend/app/serial_listener.py create mode 100644 backend/certs/cert.pem create mode 100644 backend/certs/key.pem create mode 100644 backend/data/conf.json create mode 100644 backend/data/musiques/.gitkeep create mode 100644 backend/data/musiques/SONNERIE A.mp3 create mode 100644 backend/data/musiques/SONNERIE B.mp3 create mode 100644 backend/data/musiques/SONNERIE C.mp3 create mode 100644 backend/data/musiques/SONNERIE D.mp3 create mode 100644 backend/data/musiques/SONNERIE E.mp3 create mode 100644 backend/data/musiques/SONNERIE F.mp3 create mode 100644 backend/data/musiques/SONNERIE G.mp3 create mode 100644 backend/data/musiques/SONNERIE H.mp3 create mode 100644 backend/data/musiques/SONNERIE I.mp3 create mode 100644 backend/data/musiques/SONNERIE J.mp3 create mode 100644 backend/data/musiques/SONNERIE K.mp3 create mode 100644 backend/data/musiques/SONNERIE L.mp3 create mode 100644 backend/data/musiques/SONNERIE M.mp3 create mode 100644 backend/data/musiques/SONNERIE N.mp3 create mode 100644 backend/data/musiques/SONNERIE O.mp3 create mode 100644 backend/data/musiques/SONNERIE P.mp3 create mode 100644 backend/data/musiques/SONNERIE_P.mp3 create mode 100644 backend/requirements.txt create mode 100644 backend/run.py create mode 100644 backend/systemd/pysonnerie-backend.service create mode 100644 frontend/README_FRONTEND.md create mode 100644 frontend/app/__init__.py create mode 100644 frontend/app/__pycache__/__init__.cpython-312.pyc create mode 100644 frontend/app/__pycache__/backend_client.cpython-312.pyc create mode 100644 frontend/app/__pycache__/routes.cpython-312.pyc create mode 100644 frontend/app/backend_client.py create mode 100644 frontend/app/routes.py create mode 100644 frontend/app/static/css/style.css create mode 100644 frontend/app/templates/base.html create mode 100644 frontend/app/templates/dashboard.html create mode 100644 frontend/app/templates/login.html create mode 100644 frontend/requirements.txt create mode 100644 frontend/run.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b694934 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.venv \ No newline at end of file diff --git a/arduino/.gitignore b/arduino/.gitignore new file mode 100644 index 0000000..89cc49c --- /dev/null +++ b/arduino/.gitignore @@ -0,0 +1,5 @@ +.pio +.vscode/.browse.c_cpp.db* +.vscode/c_cpp_properties.json +.vscode/launch.json +.vscode/ipch diff --git a/arduino/.vscode/extensions.json b/arduino/.vscode/extensions.json new file mode 100644 index 0000000..080e70d --- /dev/null +++ b/arduino/.vscode/extensions.json @@ -0,0 +1,10 @@ +{ + // See http://go.microsoft.com/fwlink/?LinkId=827846 + // for the documentation about the extensions.json format + "recommendations": [ + "platformio.platformio-ide" + ], + "unwantedRecommendations": [ + "ms-vscode.cpptools-extension-pack" + ] +} diff --git a/arduino/README_ARDUINO.md b/arduino/README_ARDUINO.md new file mode 100644 index 0000000..92bd69b --- /dev/null +++ b/arduino/README_ARDUINO.md @@ -0,0 +1,78 @@ +# Arduino Uno - pySonnerie + +Firmware Arduino Uno compatible avec le backend existant. + +## Projet PlatformIO + +Le dossier `arduino/` est un projet PlatformIO: + +- `platformio.ini` +- `src/main.cpp` +- `pysonnerie_esp32.ino` (version Arduino IDE conservee) + +Configuration par defaut dans `platformio.ini`: + +- carte: `uno` +- framework: `arduino` +- liaison serie: `115200` + +## Comportement + +- Ouvre la liaison serie a `115200` bauds. +- Sur impulsion detectee, envoie une ligne au format exact: + - `GPIO2` + - `GPIO4` +- Le backend lit ligne par ligne (`readline`) et utilise cette valeur comme `trigger_id`. + +## Fichier principal + +- `src/main.cpp` + +## Configuration rapide + +Dans `src/main.cpp`: + +- `MONITORED_PINS`: liste des GPIO surveillees +- `ACTIVE_LOW`: + - `true` si capteur/bouton actif a l'etat bas (montage avec pull-up) + - `false` si actif a l'etat haut +- `DEBOUNCE_MS`: anti-rebond software + +## Cablage typique (bouton) + +- GPIO configure en `INPUT_PULLUP` +- Bouton entre GPIO et GND +- Appui => front actif => envoi `GPIOX` + +## Cote backend + +Verifier dans `backend/data/conf.json`: + +- `serial.enabled: true` +- `serial.port`: port reel (`/dev/ttyUSB0`, `/dev/ttyACM0`, ...) +- `serial.baudrate: 115200` +- Presence des triggers correspondants (`GPIO23`, etc.) + +## Test rapide + +1. Depuis `esp32/`, compiler: + +```bash +pio run +``` + +2. Flasher l'Arduino Uno (adapter le port): + +```bash +pio run -t upload --upload-port /dev/ttyUSB0 +``` + +3. Ouvrir le moniteur serie (optionnel): + +```bash +pio device monitor -b 115200 -p /dev/ttyUSB0 +``` + +4. Demarrer le backend. +5. Appuyer sur le bouton. +6. Verifier les logs backend: reception `Serial trigger received: GPIOX`. diff --git a/arduino/platformio.ini b/arduino/platformio.ini new file mode 100644 index 0000000..4b782ad --- /dev/null +++ b/arduino/platformio.ini @@ -0,0 +1,5 @@ +[env:uno] +platform = atmelavr +board = uno +framework = arduino +monitor_speed = 115200 diff --git a/arduino/src/main.cpp b/arduino/src/main.cpp new file mode 100644 index 0000000..98618b4 --- /dev/null +++ b/arduino/src/main.cpp @@ -0,0 +1,74 @@ +/* + pySonnerie Arduino Uno trigger sender (PlatformIO) + + - Monitors one or more GPIO inputs + - Sends messages in the format "GPIOX" over serial at 115200 baud + - Compatible with backend/app/serial_listener.py +*/ + +#include + +static const uint32_t SERIAL_BAUDRATE = 115200; + +// INPUT_PULLUP: idle HIGH, active LOW (button wired to GND). +static const bool ACTIVE_LOW = true; +static const uint32_t DEBOUNCE_MS = 40; +static const uint16_t POLL_DELAY_MS = 2; + +// Trigger format sent to backend: "GPIO". +// Arduino Uno default uses D2 here. +static const uint8_t MONITORED_PINS[] = {3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13}; +static const size_t PIN_COUNT = sizeof(MONITORED_PINS) / sizeof(MONITORED_PINS[0]); + +struct PinState { + bool lastRead; + bool stableState; + uint32_t lastChangeMs; +}; + +PinState states[PIN_COUNT]; + +static inline bool isActiveLevel(bool rawLevel) { + return ACTIVE_LOW ? (rawLevel == LOW) : (rawLevel == HIGH); +} + +void setup() { + Serial.begin(SERIAL_BAUDRATE); + + for (size_t i = 0; i < PIN_COUNT; ++i) { + pinMode(MONITORED_PINS[i], INPUT_PULLUP); + const bool level = digitalRead(MONITORED_PINS[i]); + states[i] = {level, level, millis()}; + } +} + +void loop() { + const uint32_t now = millis(); + + for (size_t i = 0; i < PIN_COUNT; ++i) { + const uint8_t pin = MONITORED_PINS[i]; + const bool currentRead = digitalRead(pin); + + if (currentRead != states[i].lastRead) { + states[i].lastRead = currentRead; + states[i].lastChangeMs = now; + } + + // Accept state change only if stable for debounce duration. + if ((now - states[i].lastChangeMs) >= DEBOUNCE_MS && currentRead != states[i].stableState) { + const bool previousStable = states[i].stableState; + states[i].stableState = currentRead; + + const bool wasActive = isActiveLevel(previousStable); + const bool isActive = isActiveLevel(states[i].stableState); + + // Trigger only on active edge. + if (!wasActive && isActive) { + Serial.print("GPIO"); + Serial.println(pin); + } + } + } + + delay(POLL_DELAY_MS); +} diff --git a/backend/README_BACKEND.md b/backend/README_BACKEND.md new file mode 100644 index 0000000..758f99b --- /dev/null +++ b/backend/README_BACKEND.md @@ -0,0 +1,123 @@ +# pySonnerie Backend + +Backend Python avec API REST HTTPS authentifiee pour piloter la lecture audio a partir de triggers serie (ESP32). + +## Fonctions implementees + +- API REST securisee par authentification HTTP Basic +- HTTPS avec generation auto d'un certificat autosigne si absent +- Gestion des triggers via `data/conf.json` +- Lecture audio par trigger serie (`GPIOX`) ou via API de forgage manuel +- Arret de la sortie audio via API + +## Arborescence + +```text +backend/ + app/ + data/ + conf.json + musiques/ + certs/ # cree au premier lancement + run.py + requirements.txt +``` + +## Prerequis + +- Python 3.11+ +- `ffplay` installe (paquet ffmpeg) +- acces au port serie (exemple: `/dev/ttyUSB0`) + +## Installation + +```bash +cd backend +python3 -m venv .venv +source .venv/bin/activate +pip install -r requirements.txt +``` + +## Configuration + +Le fichier `data/conf.json` contient: + +- `server.host`, `server.port`, `server.tls_cert`, `server.tls_key` +- `auth.username`, `auth.password` +- `serial.enabled`, `serial.port`, `serial.baudrate`, `serial.timeout` +- `triggers` + +Exemple d'entree trigger: + +```json +"GPIO23": { + "name": "Bouton entree", + "type": "GPIO23", + "music_file": "bell.mp3", + "start_seconds": 2.5, + "end_seconds": 10.0 +} +``` + +## Lancement + +```bash +cd backend +source .venv/bin/activate +python run.py +``` + +API dispo sur `https://:`. + +## Endpoints REST + +Tous sauf `/api/health` exigent auth Basic. + +- `GET /api/health` +- `GET /api/config` +- `GET /api/triggers` +- `PUT /api/triggers/{trigger_id}` +- `PATCH /api/triggers/{trigger_id}` +- `DELETE /api/triggers/{trigger_id}` +- `GET /api/play/{trigger_id}` +- `GET /api/stop` + +### Exemples cURL + +```bash +curl -k -u admin:change-me https://127.0.0.1:8443/api/triggers +``` + +```bash +curl -k -u admin:change-me \ + -H "Content-Type: application/json" \ + -X PUT https://127.0.0.1:8443/api/triggers/GPIO23 \ + -d '{ + "name": "Bouton entree", + "type": "GPIO23", + "music_file": "bell.mp3", + "start_seconds": 0, + "end_seconds": null + }' +``` + +```bash +curl -k -u admin:change-me \ + https://127.0.0.1:8443/api/play/GPIO23 +``` + +```bash +curl -k -u admin:change-me https://127.0.0.1:8443/api/stop +``` + +## Service Debian + +Le fichier `systemd/pysonnerie-backend.service` est fourni comme base. + +```bash +sudo cp backend/systemd/pysonnerie-backend.service /etc/systemd/system/ +sudo systemctl daemon-reload +sudo systemctl enable --now pysonnerie-backend +``` + +Adapte les chemins `WorkingDirectory` et `ExecStart` avant activation. diff --git a/backend/app/__init__.py b/backend/app/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/app/__pycache__/__init__.cpython-312.pyc b/backend/app/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7e68c8287c170a07c9f5a2b8d6e2a6f53f7a6242 GIT binary patch literal 160 zcmX@j%ge<81gqG#WP<3&AOanHW&w&!XQ*V*Wb|9fP{ah}eFmxdWv`!+pPQKEC5`0Czt>L literal 0 HcmV?d00001 diff --git a/backend/app/__pycache__/audio_player.cpython-312.pyc b/backend/app/__pycache__/audio_player.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..679c8c4a1922bb0201837c7c563a64e3348459cc GIT binary patch literal 3288 zcmai0O>7&-6`uX$@<$RWi>gE`mT5_ry|EKm8E~OP(KoyNlPbFd za^}sO_ujmD^Sw9wU1w(yLHoP^EA5_$(2sP|XuL--o6|t7AdE1hqb!qR7)rA`n_}IO zOK~uAI-eC%g1g4+zO0xM8N{MrA}riT*!PU}tfl-5tR((KClcd?6vHrW#nw#2O2VAK zs@U@|`mf|DPtnt!me+IMz-&$cv4T{TVlV{1ST}(G0b&J?pP?cx!ON zFA`!B$EV%_Bv_89Su(ZYbYFr#Y1)^l6<0>$NEdkP1Y7<%fwW>^4J)>4(VK|J<0MRF zdgS_UvL{UaF6`filK&N;npO@v08DRe<&))4o@UCuN2}3O<>;wO^vq(Q778z&dvLBA zI#3QBsDy@BPgRecEyFK#c74wqrF|DGdoGnaF4e;Q)$smuc>h}Xz-p{2oi0nK*CY@b zE{&f3I`#E*Y2n3M5PZe$ow_CezI%gBb;{i@~RG*RR6hal}w`?t;N$6C-zd zkPyJyj@G~c6gUR6n8Q34u&;%(ViO5-;$5N0{q|syE3yUf>#ncp!{W`o2mvaa=)?Xt zX35uBnPUrpH?St`hRQY#zM;_=8Pq~*hN56cD++c;gO!OisMh%U)h~n3>M?{; z*E$h>7Eq(mGiU(~qhrWoZnF%!0gz>xWh=yP zh!ogy8!D>Lvay-B-4QHGK|+sD2B3p#3l-JjESr#CI`={CgiV~Fm7k#nS+y)jxM~8= z5#PCZJ^9h}G@)ex5|u`l#Hdo|wq2c2o0p9Qq&ekq=`42qI8VGsj}|FNLfX7Uxe8}esKBWuQq(B zJ64OpZ@~Ta)gry?k)FrzRU$(hVSi88;)RVpC^AqAkJbi8SND|%PCWM4dIp|+`iHmH z_l}efepK0ey%fD(i}XC<|D1k$`15S3Z>$^{d(723!b?*Rrv79-IsM1qtaTh(j}2F2 zQaL8A4u6%Yo_M!>;@!1tA6KuX%h%FtF}(QB!>N~tsIxC!mp#X~EP4TtCMPcN+&_5X zLZ_In7inN4YTGZT@X(nrfgU|>f``mK2+wQb9?`SQbZ=i>7->#?P zD^rw9{zMJK$CzkqSrw8;@_R``vG4FXQ`g~BMkQIzfZTKV+lpqB5xUm(p{H>1PEI2# zcKDl`u1k!YEfzhohjnrQ$ig_A#GqW<7 zC=H&hMWQRgt(1RzGWQ+pB|lOCMURW!f%{E!+vu_r1Y8b z$s@wIhgqP%eUqW|5suOm!sJO&V%^7_(5Fh`-BLk@fOdRDwe!UA7T7spxh)AnruD=X zB8Px-gjwBGY;qXrLb&l={ISdYH91Zt_@rs5uoIn-lh8>b{nj&-tSZY+P?obM&g+!! zkmXPFiteotTKC-&NX95duj8-CeOujHL@4mxj$+xOCFnldWI5r)X3!_ZUPPjT%Dfo! z3xl2x6_)~Zzu7#T4fZ4Sm2fdZD3s}!j6JVwGw!J!pS_TSk6=O{TW_y3l%j9DyZ@H| stmpVS@;2}&aNN&RxEY3dj)s0j-9MnnbJYJF4ZQS?F@rxN=v@8(0%Nwt-T(jq literal 0 HcmV?d00001 diff --git a/backend/app/__pycache__/config_store.cpython-312.pyc b/backend/app/__pycache__/config_store.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a8ccb9fb1ac34639571c2a441e4ab7f7b67903b0 GIT binary patch literal 3441 zcmcInO>7%Q6rR~X@7hkBpN8PHRH1PVo2E5HEmZso^LqdVL@r=JE7!Y8*4Vpdc1>~R zB7}Mf5A&=q9z0k?Ysd@i|9nB6;h=IN|EO(T!Hg@zQ9AzSA?oq z5Pg|Yk*cvm%;&|5TvZB6HC~8Q!jb2RE?ppcY?BLSFC-eAD*uchD)kaY%d*{~Yuc8x z0N=817`2jJYXC32Ty$4d&g1h|!xNq{OKvHcBB0@XwDSynw}?RsR43q}qSFfkco^3? z=mkBY^U#a%EkG~nNnM0K2E7EmtPkli=#_@74tt5GZEML~UUY3{l)?r4)sW{2i56?p zCeZ>nfVvu_VmnpdZuI`%mXq?5dF~tuWJZ?b?cwd#VoB_1w$MUe47Ti?(GM%rtUm zilw&57CtQeN;qY1cXXn4c0;pzu>GH~z}*Q8O->gsi<{xG1Ke?CMD;CA zmm4xR*)3_@2V8YU< zHwDBK^m?`CiD#MV8Y&-PMGDR^I|K?2Zfkma+J_6&h~hiOAYO3f04i32^x=h9vVV&g zZYT?Q89C5W)|B<^hI%Fc{m4VRBk5LZEwv$B9XYxug32BbbKE3A_Bi;wNr3G42D^Zn z=Qn5bS#fJZ&d>8(SuvkewvNS+pBM9w#s-Q9R!+ZooCKyt+-_lkVi6_~1l<-@)hKm~ zgkfsoP~k-W=~1a4GzVu2qa-gIu7=mkV9AR{G49SdkoU>;)a1JGrSiG*MPetF*_Ja5 z$4mX5V7c-=eh(NQ;$5F%AZ(GJ=lLerlBgF z^ywnG#4UKT%gp6vgE;^eEFNCmF7;5P&EiDr%RnJZJ7o|bjJ!t(T5P%z$r1V<)vxBSB?&;@OrE7A!EvMIKH_q(H z>Q}RE`P8nQ{8af+xtP5wr*~5m7t8I`y&JRb)bVZk`0dd~pZsWl1I)jO_8nt=UVvL% zc!#ot+l_Yb`uG53L3GRA3kMbsTa)X{1t}}hc&{JGRyY%2R0R#V7BD~T7!Xf>!Du|o zn9V$?Uek-N5u{4=Q^n>`?>G|73!4SvptG2qFgru=dMK@QlauaG&VXnJjqznSn49tR zwed`QJhM@_GPW~*a#h|5-OL)YY)wmh@((pCB7 zVDcu{7c?0?5c!utfw?>!^ zp_dKz0Ib!2R)PfdVvn2Yg??B!Du!!xV?S^(24$E?=Q$w%A^6Dy7Yp0T>2Acom(zZj z7ogbTqmUsY;F%~f!>u!Ge;A+2uRAWAn}(P3n{e#?4LpRZr+|3Ef^8Xa>!4E}4~hZA zx(W8u=i{!o%)EastUb z60Go-h`)#6t%K)>A;JG94w_sb9gY+4>&Uz~*NF>awv(jd(T*gDCpyWPnE8E75~qS0 z&`wRFGfQ>YKRCk>LH;GmhJf<~toZ)D;KlH*T`|x2G4mw1QG<6HgFixqAzXYX7}nsP zcl+{V!7iU*c@SWYckTsplc$v4AV+>8i66+w4U)b=?*5S++LP|5Q@;@){uF-#qs^^w literal 0 HcmV?d00001 diff --git a/backend/app/__pycache__/main.cpython-312.pyc b/backend/app/__pycache__/main.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9864359879c7078212d148e1b598fb3aee462ebb GIT binary patch literal 9274 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 literal 0 HcmV?d00001 diff --git a/backend/app/__pycache__/models.cpython-312.pyc b/backend/app/__pycache__/models.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..470933199706ac5fc1c022c6fd9434286053a241 GIT binary patch literal 3199 zcmc&$O>7%Q6rT02|Gl;I3vnnlNki)v;+8_Is%fe={I#fQXaT8i!)oi9I7`>N?(Dk7 zAxHrgsp90GLQgHXMC~C54mtLS#3f)Kk`){fLa4Wvgi9{)-mdN1Rj339u;jOI-g`6i zX5QQH+wWvKLg05l^o=$bBIFkwG=6?h5!N{<^Mn#A=%gTI1p#%R?kk8{(bYv=D)_Vh zLLeI`1hYYb_{dv?O4kYXGyh%DTPGWGtpTtGTdZN%8Uky$#TqF`($NPvF)cVz)i6w3 zwKdbQU>G`6L{-((q7y!$TI{q*neO=C)R<14_!K^rtE#S1)i!y~+uf7RBdx%Ao-mRX zD9QTXhlH6(1?r>XJ*k*K5waP!1ab(ygnLfnH_Zf(WXB}<$K$j zvqLlvYbToCjy7-D^>)_15!!iI@OEWU+D*HzOLx5?X*{#Ba!0zyk>BH5KF|1=X-sMP z;RiVD!32o3&xsT?L(!R$x2Hi@KW^xvWphU|)B@WKfB4qK$uk$}-kosG!J=y0j2n(* zmy7JdpRfwdD_L4jnbLIjTRj89@j@1TPLQ%GwWQmQFV8mPjT&8;(oNN#bY#m`xvf|% zXByNRccRRojZv=NA?}J2NEi$2mq3{(v%Zg9bG!Jcbw=HVYU6l*T!P5Z(h~X1FD*6r9ygU1F&`XDVDo%AQt@RNFBYMMr`r zQynqSY$x)8s+ZUsoSS@6a>Sf&@qSp;ku+n{nPT~zX&8)aEOSZCU52DGYO$CpV4qvV#j?{~Z*Qj9T7fCoG=rMgW(L}; zzg0{e5?DV3VvalvkiPA=!?(h>h7QLuXy#fc~i%qu zX?dL$g?FN>8--g3yfPld$Y@SiEvvxnX_N97!LmsBZuAbJ2sZ@47O`ePd_mR{J(b)Y zb?HPcacJ)K8-rEpkY^gHCH8rSeQUk_UyUssUmmRWj?SH`h*fEHZD`NZf#v#cX`6=2DdK|2mViD;M+05=TQuR zXhOG(J}m&;!Wc6{PhQAy6N6jA5X$&26xg_JXGB_qzp$PK<)4vXqh?5bmOLw3t)%B` zcs@bslT$+^0k@4#6-Xy;wH?L}xQTHa&5+8(V-g~?gHC*Lyucio{p;%@K zn&fF$(-Jm}%i+uJ(o=78{`}4J^OH9x?~GJ?_T7lA#rwYA{RaR|UaLw6+R@~x6!Q=%=246UdMX#I{((9NC7T>Hh#ZvkE&-Dp zPe+l7r%E<_QUJkXQ#k?nh|`Qb$1e~kR4h}~u(ce=?;;Opk#ap!bwSiUYWwjwLn9u$ zkMd&>jNg_PhCLJoLHLOb{7U+NBL}}H2R8z;&|f*ZLEy2G@(V{Rhc^g3HWJ-JvQqwo Iz{5@VFC4_u=Kufz literal 0 HcmV?d00001 diff --git a/backend/app/__pycache__/security.cpython-312.pyc b/backend/app/__pycache__/security.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2790120bd42aeccf78a28895a98e3b30e81feda8 GIT binary patch literal 2562 zcmai0&2Jk;6rZ)%yKBdGKAlfmh0;P@HE|k}wn;;wU!swSsc5PqSIBI3Ch;cru9;c4 zvE@hsDizcV98eqe&>oA@Ll67`9Jp~wl%kkXDk>ETaX~==gnHu5I_sn%RY&rhx9`1~ zd2inDz44EcNC?6BUi;3xr6BZFe0alGWu%Q+U~VCdu#`nPDJ4mQ_ho%4AI$!&KPRVT zS0-lzIVGjId?2glw3L<$rh>UpD&)$PY+Wv#3g;rJh=hEIgcmo@N=0!9tJmdJJ+8wV z%ndkr9iHa?U@^kp;+^Ui@K z6D@A8n)5keUma!NuGT*6_pBE;cboISbd|izD%lBmXH5%FcZ{+$t?dxOXSsIzT;Qea zU0giBM_Kl~pHs>K>MJX3uM_cXoT$f5H+bCEj00G~swq9t7B_F{GpUH;!6cXUf|KD| zfe~uyIl>Xw@;YPJY>G?Gqt;bDYvOKWD@&z@%a*wbM=YS_8FPDvV@Qv@_39$GJmDu}p79usIu z5A+>LUrL_3G=Bd54@Ohx&c;=)7`8QOPVu^J{_&s)2wboxc_W$46T=}mT?=nZ6gqkEVp&K($ z00}*xPe619YC6Tb_d8jsx-9T3Vj{Aee_=cO2g~( zqigM5w@2@eEsWhi^l)P3m7$gP;Uzh?yY$y+Y&E)nIlBMemDK}7%Lj&*PQ14g9h-l5 z?L~^8!_5ooTC{7eeeY`fz;gRQrTJhb_Qw6TrIsUqsd9a29m&Dax*v6PZ-9H~xv~_{ za?A1$U|v*~UR$A!Pzbw}ZA}jlgu>ol_ToSo)3x%dehZ2vEIsgfB>)Z0k5Se?=YwJo zZdrL8maCA*8V=%+Q|HOcl2`h;ZkrU|ozeN$Ihm6j({^vBkd>BIef1IIxG-*&+% z$vuJwdazz(Tz>QDU>|(2!o+l`C2=PCR=&tK5!Rcv4Lu7jH(+FlF%ws~;<~`NQuT=k zax>VZJOr!r)HVoX;7!4zF>nydGiVU%r1Q|Xxdw;39PI%e6FmX&PP<{|tq=!5Jndvz zTC6Z9-7=>~K)DBg>~kQ1TD4(SZCh5`9;qF-Gk5I;`_bM+MTBdc=DvfQ#NcP`7F z_XZyBUy?f?$;Y0`u_to$M)K3-lW?pOZmfixE8&)k;9G&Nh3mi4ZjM#LO@a&FOuG6e zm+o8NQxBNzM)l3>uc1Km->4~|(0$;Cu^nGB2wZgb19JoL>D07N4P{=YCnfCr0s)n- zN9LrmbP=v7`M>R&#T#@Gw);lfzMT(!DS0q1*WMrr0Zb%#9o-6#`)DUDzYSnA2?GsNu>=`CsRh?{nHvUi913D}B>PWkUVTGKif4&6C? zzwRe(z`d5Z?4C`=K*SsDDf&mcFK_ISwA8+EIWjA-|5SR@M z({>=@=#eu&oL?Ck`v*yZ!F2_NyCB*_QT!w#5DBdkG6cHN5O&~ z0}@wgLhvGow6Df(>6>^cot`W}BqW`t0U54MHyEm8PztNpOHtN&+O<30;JCZ6xGf_wE{y zMs6}w(s5ckAv2wUd?o#=GcZ#=`oSmD{E+rXI}%;7ai{6zD}QcHh8gnHbN3+`Mb4|e z8r^&Dz2}~L_MCIiJ?H+dy}cDdxfuC#=HoVmJ|!Km_#J>+RRFk-BqUKeG)qlU6oGv? z-;~d8=_wjoI>*eiQ|zpN%0C;J3e0j-oQu~o)#5`hAc?txBv$4m|E<7H#-TmcYR_;m z({j`2jNljrDs${J0>s|_gpHMOP$Nf7V~^2ty|pB=Ez(sXSJNn z&&d)QOgn^}nsyp$bt?eZk&LD&2~GJz zRg2biY|$6wyu~P~S=r`76bUGbR;wt^s?vOpz(G;`#e6E~&UA|68s0_m1GGw5c|0+C zUY(Uk)8}zU)7822^60bkSWeB49>?m;=v@AUswgtf$fM^{=?k(Vji%=2Ml~2o<-{B; zkzqDDm&YwY%&v$AwIEb)qH0Ug`PzgcYnes<(Skw!?$!twh~)cY-xFig_gXjuke{JjK-ABJq9m)uX*0@<#U8vUE4tMt5><+_-3jE>nHe(K zG9&9?L|{s=mJG^Rpe;s|bJMsRIK-Qw(ukyZp2rbrElw0ON=6sOLaeEbliqxYEPV#5 zH&C^A+agopx~n~di_F!|>aKmz1ebdU-`oH0{&H`^o?MQ`%;<;_9VrQ8<>-$VncoF% z6G_+90JO-7x0QAkMz15iHdjR1nt}kwE!mn1yb*GiB%gPZJ;5(fMM~=f8m>pFV&aoQv97~4v4nKboll>7-=~?z8HAW*;NgO-ah))Q8U#S*ZhNxSeMWQy7eYYK%*yJ#F0hWCX$@?F7yy0>DvQag8AYIM-D<=O zxbBG|Zm!U?o?6wpt$?ddKm~58u7*B?>#K0F^)$9dyYHt;!T$Tfkrf*C?_M1HvO