Upload Application Gradio principale
Browse files
app.py
ADDED
|
@@ -0,0 +1,1674 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import gradio as gr
|
| 2 |
+
import requests
|
| 3 |
+
import json
|
| 4 |
+
from typing import Dict, List, Tuple, Optional
|
| 5 |
+
from datetime import datetime, timedelta
|
| 6 |
+
import os
|
| 7 |
+
from dotenv import load_dotenv
|
| 8 |
+
import logging
|
| 9 |
+
import time
|
| 10 |
+
import locale
|
| 11 |
+
|
| 12 |
+
# Configuration locale pour les dates en français
|
| 13 |
+
try:
|
| 14 |
+
locale.setlocale(locale.LC_TIME, 'fr_FR.UTF-8')
|
| 15 |
+
except:
|
| 16 |
+
try:
|
| 17 |
+
locale.setlocale(locale.LC_TIME, 'French_France.1252')
|
| 18 |
+
except:
|
| 19 |
+
pass # Garde la locale par défaut si français non disponible
|
| 20 |
+
|
| 21 |
+
# Configuration du logging
|
| 22 |
+
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
| 23 |
+
logger = logging.getLogger(__name__)
|
| 24 |
+
|
| 25 |
+
load_dotenv()
|
| 26 |
+
|
| 27 |
+
def format_timestamp(timestamp, format_type="datetime"):
|
| 28 |
+
"""
|
| 29 |
+
Convertit un timestamp Unix en format français lisible.
|
| 30 |
+
|
| 31 |
+
Args:
|
| 32 |
+
timestamp (int|float): Timestamp Unix à convertir
|
| 33 |
+
format_type (str): Type de format souhaité
|
| 34 |
+
- "datetime": "01/07/2025 à 14:30" (par défaut)
|
| 35 |
+
- "date": "Mar 01/07"
|
| 36 |
+
- "time": "14:30"
|
| 37 |
+
- autre: format strftime personnalisé
|
| 38 |
+
|
| 39 |
+
Returns:
|
| 40 |
+
str: Date formatée en français ou "--" si erreur
|
| 41 |
+
"""
|
| 42 |
+
if not timestamp or timestamp == 0:
|
| 43 |
+
return "--"
|
| 44 |
+
|
| 45 |
+
try:
|
| 46 |
+
dt = datetime.fromtimestamp(timestamp)
|
| 47 |
+
if format_type == "date":
|
| 48 |
+
return dt.strftime("%a %d/%m")
|
| 49 |
+
elif format_type == "time":
|
| 50 |
+
return dt.strftime("%H:%M")
|
| 51 |
+
elif format_type == "datetime":
|
| 52 |
+
return dt.strftime("%d/%m/%Y à %H:%M")
|
| 53 |
+
else:
|
| 54 |
+
return dt.strftime(format_type)
|
| 55 |
+
except (ValueError, OSError):
|
| 56 |
+
return str(timestamp)
|
| 57 |
+
|
| 58 |
+
class MeteoFranceAPI:
|
| 59 |
+
"""
|
| 60 |
+
Client pour l'API privée Météo-France.
|
| 61 |
+
|
| 62 |
+
Utilise l'API interne de Météo-France (même que l'app mobile officielle)
|
| 63 |
+
pour récupérer prévisions, observations, alertes et données de pluie.
|
| 64 |
+
|
| 65 |
+
Attributes:
|
| 66 |
+
base_url (str): URL de base de l'API Météo-France
|
| 67 |
+
token (str): Token d'authentification depuis variable d'environnement
|
| 68 |
+
headers (dict): Headers HTTP pour les requêtes
|
| 69 |
+
"""
|
| 70 |
+
|
| 71 |
+
def __init__(self):
|
| 72 |
+
# API privée Météo-France utilisée par les apps mobiles officielles
|
| 73 |
+
self.base_url = "https://webservice.meteofrance.com"
|
| 74 |
+
|
| 75 |
+
# Token depuis variable d'environnement obligatoire
|
| 76 |
+
self.token = os.getenv('METEOFRANCE_TOKEN')
|
| 77 |
+
|
| 78 |
+
if not self.token:
|
| 79 |
+
raise ValueError("Token Météo-France manquant. Définissez METEOFRANCE_TOKEN dans votre fichier .env")
|
| 80 |
+
|
| 81 |
+
self.headers = {
|
| 82 |
+
"User-Agent": "MeteoApp/1.0"
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
def get_location_forecast(self, lat: float, lon: float) -> dict:
|
| 86 |
+
"""
|
| 87 |
+
Récupère les prévisions météo complètes pour une position géographique.
|
| 88 |
+
|
| 89 |
+
Args:
|
| 90 |
+
lat (float): Latitude en degrés décimaux
|
| 91 |
+
lon (float): Longitude en degrés décimaux
|
| 92 |
+
|
| 93 |
+
Returns:
|
| 94 |
+
dict: Données de prévision avec clés principales:
|
| 95 |
+
- position: Infos localisation (nom, département, altitude...)
|
| 96 |
+
- forecast: Prévisions horaires sur 7 jours
|
| 97 |
+
- daily_forecast: Prévisions quotidiennes sur 10 jours
|
| 98 |
+
- updated_on: Timestamp de mise à jour
|
| 99 |
+
En cas d'erreur: {"error": "message d'erreur"}
|
| 100 |
+
"""
|
| 101 |
+
logger.info(f"🌤️ Récupération prévisions météo pour: lat={lat}, lon={lon}")
|
| 102 |
+
|
| 103 |
+
try:
|
| 104 |
+
# API privée Météo-France - Prévisions complètes
|
| 105 |
+
url = f"{self.base_url}/forecast"
|
| 106 |
+
params = {
|
| 107 |
+
"lat": lat,
|
| 108 |
+
"lon": lon,
|
| 109 |
+
"lang": "fr",
|
| 110 |
+
"token": self.token # Token passé en paramètre de requête
|
| 111 |
+
}
|
| 112 |
+
|
| 113 |
+
# Masquer le token dans les logs
|
| 114 |
+
safe_params = {k: "***" if k == "token" else v for k, v in params.items()}
|
| 115 |
+
logger.info(f"📡 Appel API Météo-France: {url}")
|
| 116 |
+
logger.info(f"📋 Paramètres: {safe_params}")
|
| 117 |
+
|
| 118 |
+
response = requests.get(url, headers=self.headers, params=params)
|
| 119 |
+
|
| 120 |
+
if response.status_code == 200:
|
| 121 |
+
data = response.json()
|
| 122 |
+
logger.info(f"✅ Données météo reçues")
|
| 123 |
+
return data
|
| 124 |
+
else:
|
| 125 |
+
logger.error(f"❌ Erreur API Météo-France {response.status_code}: {response.text}")
|
| 126 |
+
return {"error": f"Erreur API: {response.status_code}"}
|
| 127 |
+
except Exception as e:
|
| 128 |
+
logger.error(f"❌ Erreur connexion Météo-France: {e}")
|
| 129 |
+
return {"error": f"Erreur de connexion: {str(e)}"}
|
| 130 |
+
|
| 131 |
+
def get_wind_alerts_by_department(self, department: str) -> dict:
|
| 132 |
+
"""
|
| 133 |
+
Récupère les alertes météo pour un département français spécifique.
|
| 134 |
+
|
| 135 |
+
Args:
|
| 136 |
+
department (str): Code département français (ex: "29", "75", "2A")
|
| 137 |
+
|
| 138 |
+
Returns:
|
| 139 |
+
dict: Données d'alertes avec clés principales:
|
| 140 |
+
- domain_id: Code du département
|
| 141 |
+
- phenomenons_max_colors: Liste des phénomènes avec niveaux d'alerte
|
| 142 |
+
- update_time: Timestamp de mise à jour
|
| 143 |
+
- end_validity_time: Fin de validité des alertes
|
| 144 |
+
En cas d'erreur: {"error": "message d'erreur"}
|
| 145 |
+
"""
|
| 146 |
+
logger.info(f"⚠️ Récupération alertes pour département: {department}")
|
| 147 |
+
|
| 148 |
+
try:
|
| 149 |
+
# API privée Météo-France - Alertes météo par département
|
| 150 |
+
url = f"{self.base_url}/v3/warning/currentphenomenons"
|
| 151 |
+
params = {
|
| 152 |
+
"domain": department,
|
| 153 |
+
"depth": 1,
|
| 154 |
+
"with_coastal_bulletin": "true",
|
| 155 |
+
"token": self.token
|
| 156 |
+
}
|
| 157 |
+
|
| 158 |
+
# Masquer le token dans les logs
|
| 159 |
+
safe_params = {k: "***" if k == "token" else v for k, v in params.items()}
|
| 160 |
+
logger.info(f"📡 Appel API Alertes pour département {department}")
|
| 161 |
+
|
| 162 |
+
response = requests.get(url, headers=self.headers, params=params)
|
| 163 |
+
|
| 164 |
+
if response.status_code == 200:
|
| 165 |
+
data = response.json()
|
| 166 |
+
logger.info(f"✅ Alertes reçues")
|
| 167 |
+
return data
|
| 168 |
+
else:
|
| 169 |
+
logger.error(f"❌ Erreur API Alertes {response.status_code}: {response.text}")
|
| 170 |
+
return {"error": f"Erreur API alertes: {response.status_code}"}
|
| 171 |
+
except Exception as e:
|
| 172 |
+
logger.error(f"❌ Erreur alertes: {e}")
|
| 173 |
+
return {"error": f"Erreur alertes: {str(e)}"}
|
| 174 |
+
|
| 175 |
+
def get_wind_alerts(self, lat: float, lon: float) -> dict:
|
| 176 |
+
"""Récupère les alertes vent pour une position (fallback)"""
|
| 177 |
+
logger.info(f"⚠️ Récupération alertes vent pour: lat={lat}, lon={lon}")
|
| 178 |
+
|
| 179 |
+
# Déterminer le département à partir des coordonnées GPS
|
| 180 |
+
department = self._get_department_from_coords(lat, lon)
|
| 181 |
+
|
| 182 |
+
return self.get_wind_alerts_by_department(department)
|
| 183 |
+
|
| 184 |
+
def _get_department_from_coords(self, lat: float, lon: float) -> str:
|
| 185 |
+
"""Approximation simple du département depuis les coordonnées GPS"""
|
| 186 |
+
logger.info(f"🗺️ Détermination département pour: lat={lat}, lon={lon}")
|
| 187 |
+
|
| 188 |
+
# Logique simplifiée pour les principales régions
|
| 189 |
+
# Paris et Ile-de-France
|
| 190 |
+
if 48.8 <= lat <= 49.0 and 2.2 <= lon <= 2.5:
|
| 191 |
+
dept = "75" # Paris
|
| 192 |
+
elif 48.1 <= lat <= 49.2 and 1.4 <= lon <= 3.6:
|
| 193 |
+
dept = "77" # Seine-et-Marne (approximation IDF)
|
| 194 |
+
# Marseille
|
| 195 |
+
elif 43.2 <= lat <= 43.4 and 5.3 <= lon <= 5.5:
|
| 196 |
+
dept = "13" # Bouches-du-Rhône
|
| 197 |
+
# Lyon
|
| 198 |
+
elif 45.7 <= lat <= 45.8 and 4.8 <= lon <= 4.9:
|
| 199 |
+
dept = "69" # Rhône
|
| 200 |
+
# Toulouse
|
| 201 |
+
elif 43.5 <= lat <= 43.7 and 1.3 <= lon <= 1.5:
|
| 202 |
+
dept = "31" # Haute-Garonne
|
| 203 |
+
# Par défaut, utiliser "france" pour l'ensemble du territoire
|
| 204 |
+
else:
|
| 205 |
+
dept = "france"
|
| 206 |
+
|
| 207 |
+
logger.info(f"🏷️ Département déterminé: {dept}")
|
| 208 |
+
return dept
|
| 209 |
+
|
| 210 |
+
def get_current_observation(self, lat: float, lon: float) -> dict:
|
| 211 |
+
"""Récupère les observations météo actuelles"""
|
| 212 |
+
logger.info(f"🌡️ Récupération observations actuelles pour: lat={lat}, lon={lon}")
|
| 213 |
+
|
| 214 |
+
try:
|
| 215 |
+
url = f"{self.base_url}/v2/observation"
|
| 216 |
+
params = {
|
| 217 |
+
"lat": lat,
|
| 218 |
+
"lon": lon,
|
| 219 |
+
"lang": "fr",
|
| 220 |
+
"token": self.token # Token passé en paramètre de requête
|
| 221 |
+
}
|
| 222 |
+
|
| 223 |
+
response = requests.get(url, headers=self.headers, params=params)
|
| 224 |
+
|
| 225 |
+
if response.status_code == 200:
|
| 226 |
+
data = response.json()
|
| 227 |
+
logger.info(f"✅ Observations reçues")
|
| 228 |
+
return data
|
| 229 |
+
else:
|
| 230 |
+
logger.error(f"❌ Erreur API Observations {response.status_code}: {response.text}")
|
| 231 |
+
return {"error": f"Erreur API observations: {response.status_code}"}
|
| 232 |
+
except Exception as e:
|
| 233 |
+
logger.error(f"❌ Erreur observations: {e}")
|
| 234 |
+
return {"error": f"Erreur observations: {str(e)}"}
|
| 235 |
+
|
| 236 |
+
def get_rain_forecast(self, lat: float, lon: float) -> dict:
|
| 237 |
+
"""Récupère les prévisions de pluie dans l'heure"""
|
| 238 |
+
logger.info(f"🌧️ Récupération prévisions pluie pour: lat={lat}, lon={lon}")
|
| 239 |
+
|
| 240 |
+
try:
|
| 241 |
+
url = f"{self.base_url}/rain"
|
| 242 |
+
params = {
|
| 243 |
+
"lat": round(lat, 3), # Précision minimum requise
|
| 244 |
+
"lon": round(lon, 3),
|
| 245 |
+
"lang": "fr",
|
| 246 |
+
"token": self.token # Token passé en paramètre de requête
|
| 247 |
+
}
|
| 248 |
+
|
| 249 |
+
response = requests.get(url, headers=self.headers, params=params)
|
| 250 |
+
|
| 251 |
+
if response.status_code == 200:
|
| 252 |
+
data = response.json()
|
| 253 |
+
logger.info(f"✅ Prévisions pluie reçues")
|
| 254 |
+
return data
|
| 255 |
+
else:
|
| 256 |
+
logger.error(f"❌ Erreur API Pluie {response.status_code}: {response.text}")
|
| 257 |
+
return {"error": f"Erreur API pluie: {response.status_code}"}
|
| 258 |
+
except Exception as e:
|
| 259 |
+
logger.error(f"❌ Erreur pluie: {e}")
|
| 260 |
+
return {"error": f"Erreur pluie: {str(e)}"}
|
| 261 |
+
|
| 262 |
+
|
| 263 |
+
class GeocodingAPI:
|
| 264 |
+
"""
|
| 265 |
+
Client pour l'API de géocodage IGN Géoplateforme.
|
| 266 |
+
|
| 267 |
+
Utilise l'API officielle française pour convertir des adresses
|
| 268 |
+
en coordonnées GPS avec métadonnées françaises précises.
|
| 269 |
+
|
| 270 |
+
Attributes:
|
| 271 |
+
base_url (str): URL de l'API de géocodage IGN
|
| 272 |
+
"""
|
| 273 |
+
|
| 274 |
+
def __init__(self):
|
| 275 |
+
# L'API IGN ne nécessite pas de clé d'accès
|
| 276 |
+
self.base_url = "https://data.geopf.fr/geocodage/search"
|
| 277 |
+
|
| 278 |
+
def geocode_address(self, address: str) -> dict:
|
| 279 |
+
"""
|
| 280 |
+
Convertit une adresse française en coordonnées GPS via l'API IGN.
|
| 281 |
+
|
| 282 |
+
Args:
|
| 283 |
+
address (str): Adresse à géolocaliser (ex: "Brest, France")
|
| 284 |
+
|
| 285 |
+
Returns:
|
| 286 |
+
dict: Résultat avec clés:
|
| 287 |
+
- lat (float): Latitude en degrés décimaux
|
| 288 |
+
- lon (float): Longitude en degrés décimaux
|
| 289 |
+
- full_data (dict): Données complètes IGN (propriétés, géométrie...)
|
| 290 |
+
None en cas d'erreur
|
| 291 |
+
"""
|
| 292 |
+
logger.info(f"🔍 Géocodage IGN de l'adresse: {address}")
|
| 293 |
+
|
| 294 |
+
try:
|
| 295 |
+
params = {
|
| 296 |
+
"q": address,
|
| 297 |
+
"index": "address", # Recherche par adresse
|
| 298 |
+
"limit": 1,
|
| 299 |
+
"returntruegeometry": "true"
|
| 300 |
+
}
|
| 301 |
+
|
| 302 |
+
response = requests.get(self.base_url, params=params)
|
| 303 |
+
|
| 304 |
+
if response.status_code == 200:
|
| 305 |
+
data = response.json()
|
| 306 |
+
logger.info(f"✅ Géolocalisation réussie")
|
| 307 |
+
|
| 308 |
+
if data.get("features") and len(data["features"]) > 0:
|
| 309 |
+
feature = data["features"][0]
|
| 310 |
+
coords = feature["geometry"]["coordinates"]
|
| 311 |
+
logger.info(f"📍 Coordonnées trouvées: lat={coords[1]}, lon={coords[0]}")
|
| 312 |
+
return {
|
| 313 |
+
"lat": coords[1],
|
| 314 |
+
"lon": coords[0],
|
| 315 |
+
"full_data": feature # Conserver toutes les données pour la carte
|
| 316 |
+
}
|
| 317 |
+
else:
|
| 318 |
+
logger.warning("⚠️ Aucune coordonnée trouvée dans la réponse IGN")
|
| 319 |
+
else:
|
| 320 |
+
logger.error(f"❌ Erreur HTTP IGN {response.status_code}: {response.text}")
|
| 321 |
+
return None
|
| 322 |
+
except Exception as e:
|
| 323 |
+
logger.error(f"❌ Erreur géocodage IGN: {e}")
|
| 324 |
+
return None
|
| 325 |
+
|
| 326 |
+
|
| 327 |
+
def format_weather_data(data: dict) -> str:
|
| 328 |
+
"""
|
| 329 |
+
Formate les données météo brutes en texte lisible markdown.
|
| 330 |
+
|
| 331 |
+
Args:
|
| 332 |
+
data (dict): Données météo brutes de l'API Météo-France
|
| 333 |
+
avec clés position, forecast, daily_forecast, updated_on
|
| 334 |
+
|
| 335 |
+
Returns:
|
| 336 |
+
str: Données météo formatées en markdown avec prévisions
|
| 337 |
+
quotidiennes et horaires, ou message d'erreur si échec
|
| 338 |
+
"""
|
| 339 |
+
if "error" in data:
|
| 340 |
+
return f"❌ {data['error']}"
|
| 341 |
+
|
| 342 |
+
formatted = "🌤️ **Prévisions Météo**\n\n"
|
| 343 |
+
|
| 344 |
+
try:
|
| 345 |
+
if "position" in data:
|
| 346 |
+
pos = data["position"]
|
| 347 |
+
formatted += f"📍 **Localisation:** {pos.get('name', 'Inconnu')}\n"
|
| 348 |
+
formatted += f"Coordonnées: {pos.get('lat', 0):.3f}, {pos.get('lon', 0):.3f}\n\n"
|
| 349 |
+
|
| 350 |
+
if "updated_on" in data:
|
| 351 |
+
update_time = format_timestamp(data['updated_on'])
|
| 352 |
+
formatted += f"⏰ **Mise à jour:** {update_time}\n\n"
|
| 353 |
+
|
| 354 |
+
# Prévisions quotidiennes
|
| 355 |
+
if "daily_forecast" in data and data["daily_forecast"]:
|
| 356 |
+
formatted += "📅 **Prévisions à 10 jours:**\n"
|
| 357 |
+
for day in data["daily_forecast"][:10]:
|
| 358 |
+
timestamp = day.get("dt", 0)
|
| 359 |
+
date_str = format_timestamp(timestamp, "date")
|
| 360 |
+
temp_min = day.get("T", {}).get("min", "--")
|
| 361 |
+
temp_max = day.get("T", {}).get("max", "--")
|
| 362 |
+
weather = day.get("weather12H", {}).get("desc", "--")
|
| 363 |
+
formatted += f" • {date_str}: {temp_min}°/{temp_max}°C - {weather}\n"
|
| 364 |
+
formatted += "\n"
|
| 365 |
+
|
| 366 |
+
# Prévisions horaires (aujourd'hui)
|
| 367 |
+
if "forecast" in data and data["forecast"]:
|
| 368 |
+
formatted += "🕰️ **Aujourd'hui (par heure):**\n"
|
| 369 |
+
for hour in data["forecast"][:12]: # 12 premières heures
|
| 370 |
+
timestamp = hour.get("dt", 0)
|
| 371 |
+
time_str = format_timestamp(timestamp, "time")
|
| 372 |
+
temp = hour.get("T", {}).get("value", "--")
|
| 373 |
+
weather = hour.get("weather", {}).get("desc", "--")
|
| 374 |
+
formatted += f" • {time_str}: {temp}°C - {weather}\n"
|
| 375 |
+
|
| 376 |
+
except Exception as e:
|
| 377 |
+
logger.error(f"Erreur formatage météo: {e}")
|
| 378 |
+
formatted += f"Données brutes: {json.dumps(data, indent=2, ensure_ascii=False)[:500]}..."
|
| 379 |
+
|
| 380 |
+
return formatted
|
| 381 |
+
|
| 382 |
+
def format_current_conditions(current_data: dict, rain_data: dict = None, forecast_data: dict = None) -> str:
|
| 383 |
+
"""
|
| 384 |
+
Formate les conditions météorologiques actuelles avec détails complets.
|
| 385 |
+
|
| 386 |
+
Combine observations actuelles, prévisions de pluie et données de localisation
|
| 387 |
+
pour créer un rapport météo actuel détaillé.
|
| 388 |
+
|
| 389 |
+
Args:
|
| 390 |
+
current_data (dict): Observations météo actuelles de l'API
|
| 391 |
+
rain_data (dict, optional): Prévisions pluie dans l'heure. Defaults to None.
|
| 392 |
+
forecast_data (dict, optional): Données de prévision pour la localisation. Defaults to None.
|
| 393 |
+
|
| 394 |
+
Returns:
|
| 395 |
+
str: Conditions actuelles formatées en markdown avec température,
|
| 396 |
+
vent, précipitations, humidité, pression et visibilité
|
| 397 |
+
"""
|
| 398 |
+
if "error" in current_data:
|
| 399 |
+
return f"❌ {current_data['error']}"
|
| 400 |
+
|
| 401 |
+
formatted = "🌡️ **Conditions Actuelles**\n\n"
|
| 402 |
+
|
| 403 |
+
try:
|
| 404 |
+
# Localisation
|
| 405 |
+
if forecast_data and "position" in forecast_data:
|
| 406 |
+
pos = forecast_data["position"]
|
| 407 |
+
formatted += f"📍 **Lieu**: {pos.get('name', 'Localisation inconnue')}\n"
|
| 408 |
+
formatted += f"Coordonnées: {pos.get('lat', 0):.3f}, {pos.get('lon', 0):.3f}\n\n"
|
| 409 |
+
|
| 410 |
+
# Heure d'observation depuis properties.gridded.time
|
| 411 |
+
gridded = current_data.get("properties", {}).get("gridded", {})
|
| 412 |
+
if "time" in gridded:
|
| 413 |
+
obs_time = gridded["time"]
|
| 414 |
+
# Convertir format ISO vers format lisible
|
| 415 |
+
if obs_time:
|
| 416 |
+
try:
|
| 417 |
+
from datetime import datetime
|
| 418 |
+
dt = datetime.fromisoformat(obs_time.replace('Z', '+00:00'))
|
| 419 |
+
formatted += f"⏰ **Observation**: {dt.strftime('%d/%m/%Y à %H:%M')}\n\n"
|
| 420 |
+
except:
|
| 421 |
+
formatted += f"⏰ **Observation**: {obs_time}\n\n"
|
| 422 |
+
|
| 423 |
+
# Température depuis properties.gridded.T
|
| 424 |
+
temp = gridded.get("T", "--")
|
| 425 |
+
formatted += f"🌡️ **Température**: {temp}°C\n"
|
| 426 |
+
|
| 427 |
+
# Vent depuis properties.gridded
|
| 428 |
+
wind_speed = gridded.get("wind_speed", "--")
|
| 429 |
+
wind_dir = gridded.get("wind_direction", "--")
|
| 430 |
+
wind_icon = gridded.get("wind_icon", "")
|
| 431 |
+
|
| 432 |
+
formatted += f"🌬️ **Vent**: {wind_speed} km/h - {wind_dir}° {wind_icon}\n"
|
| 433 |
+
|
| 434 |
+
# Conditions météo depuis properties.gridded
|
| 435 |
+
weather_desc = gridded.get("weather_description", "--")
|
| 436 |
+
weather_icon = gridded.get("weather_icon", "")
|
| 437 |
+
formatted += f"🌤️ **Temps**: {weather_desc} {weather_icon}\n"
|
| 438 |
+
|
| 439 |
+
# Affichage des précipitations (selon meteofrance-api officiel)
|
| 440 |
+
if rain_data and "error" not in rain_data:
|
| 441 |
+
# Vérifier la disponibilité du service pluie
|
| 442 |
+
rain_available = rain_data.get("position", {}).get("rain_product_available", 0)
|
| 443 |
+
|
| 444 |
+
if rain_available == 0:
|
| 445 |
+
formatted += f"💧 **Précipitations**: Service radar indisponible pour cette zone\n"
|
| 446 |
+
elif "forecast" in rain_data and rain_data["forecast"]:
|
| 447 |
+
# Prendre la première prévision (maintenant)
|
| 448 |
+
current_rain = rain_data["forecast"][0] if rain_data["forecast"] else None
|
| 449 |
+
if current_rain:
|
| 450 |
+
rain_intensity = current_rain.get("rain", 0)
|
| 451 |
+
rain_desc = current_rain.get("desc", "Pas de données")
|
| 452 |
+
|
| 453 |
+
# Interprétation selon meteofrance-api : > 1 = pluie
|
| 454 |
+
if rain_intensity <= 1:
|
| 455 |
+
formatted += f"💧 **Précipitations**: Aucune - {rain_desc}\n"
|
| 456 |
+
elif rain_intensity == 2:
|
| 457 |
+
formatted += f"💧 **Précipitations**: 🌦️ Pluie faible - {rain_desc}\n"
|
| 458 |
+
elif rain_intensity == 3:
|
| 459 |
+
formatted += f"💧 **Précipitations**: 🌧️ Pluie modérée - {rain_desc}\n"
|
| 460 |
+
elif rain_intensity >= 4:
|
| 461 |
+
formatted += f"💧 **Précipitations**: ⛈️ Pluie forte - {rain_desc}\n"
|
| 462 |
+
else:
|
| 463 |
+
formatted += f"💧 **Précipitations**: Intensité {rain_intensity} - {rain_desc}\n"
|
| 464 |
+
else:
|
| 465 |
+
formatted += f"💧 **Précipitations**: Données non disponibles\n"
|
| 466 |
+
else:
|
| 467 |
+
formatted += f"💧 **Précipitations**: Pas de prévisions disponibles\n"
|
| 468 |
+
else:
|
| 469 |
+
formatted += f"💧 **Précipitations**: Service indisponible\n"
|
| 470 |
+
|
| 471 |
+
# Humidité
|
| 472 |
+
humidity = current_data.get("humidity", "--")
|
| 473 |
+
if humidity != "--":
|
| 474 |
+
formatted += f"💧 **Humidité**: {humidity}%\n"
|
| 475 |
+
|
| 476 |
+
# Pression
|
| 477 |
+
pressure = current_data.get("pressure", {}).get("value", "--")
|
| 478 |
+
if pressure != "--":
|
| 479 |
+
formatted += f"📊 **Pression**: {pressure} hPa\n"
|
| 480 |
+
|
| 481 |
+
# Visibilité
|
| 482 |
+
visibility = current_data.get("visibility", "--")
|
| 483 |
+
if visibility != "--":
|
| 484 |
+
formatted += f"👁️ **Visibilité**: {visibility} km\n"
|
| 485 |
+
|
| 486 |
+
# Prévisions de pluie dans l'heure (améliorées)
|
| 487 |
+
if rain_data and "error" not in rain_data:
|
| 488 |
+
formatted += "\n🌧️ **Pluie dans l'heure:**\n"
|
| 489 |
+
|
| 490 |
+
# Informations sur la localisation pluie
|
| 491 |
+
if "position" in rain_data:
|
| 492 |
+
rain_pos = rain_data["position"]
|
| 493 |
+
quality = rain_data.get("quality", 0)
|
| 494 |
+
updated = rain_data.get("updated_on", "")
|
| 495 |
+
if isinstance(updated, (int, float)):
|
| 496 |
+
update_str = format_timestamp(updated)
|
| 497 |
+
else:
|
| 498 |
+
update_str = updated
|
| 499 |
+
formatted += f"📍 Lieu: {rain_pos.get('name', 'Inconnu')} (Dept. {rain_pos.get('dept', '??')})\n"
|
| 500 |
+
formatted += f"⏰ Mis à jour: {update_str}\n"
|
| 501 |
+
formatted += f"📊 Qualité: {quality}/10\n\n"
|
| 502 |
+
|
| 503 |
+
if "forecast" in rain_data and rain_data["forecast"]:
|
| 504 |
+
next_rain = rain_data["forecast"][:6] # 6 prochains points (1h30)
|
| 505 |
+
for point in next_rain:
|
| 506 |
+
rain_intensity = point.get("rain", 0)
|
| 507 |
+
rain_desc_api = point.get("desc", "")
|
| 508 |
+
time_point = point.get("dt", "")
|
| 509 |
+
|
| 510 |
+
if isinstance(time_point, (int, float)):
|
| 511 |
+
time_str = format_timestamp(time_point, "time")
|
| 512 |
+
else:
|
| 513 |
+
time_str = time_point
|
| 514 |
+
|
| 515 |
+
# Utiliser la description de l'API si disponible, sinon notre mapping
|
| 516 |
+
if rain_desc_api and rain_desc_api != "Pas de valeur":
|
| 517 |
+
rain_desc = rain_desc_api
|
| 518 |
+
else:
|
| 519 |
+
if rain_intensity == 0:
|
| 520 |
+
rain_desc = "Pas de pluie"
|
| 521 |
+
elif rain_intensity == 1:
|
| 522 |
+
rain_desc = "Pluie faible"
|
| 523 |
+
elif rain_intensity == 2:
|
| 524 |
+
rain_desc = "Pluie modérée"
|
| 525 |
+
elif rain_intensity == 3:
|
| 526 |
+
rain_desc = "Pluie forte"
|
| 527 |
+
else:
|
| 528 |
+
rain_desc = f"Intensité {rain_intensity}"
|
| 529 |
+
|
| 530 |
+
# Émoji selon l'intensité
|
| 531 |
+
if rain_intensity == 0:
|
| 532 |
+
emoji = "☀️"
|
| 533 |
+
elif rain_intensity == 1:
|
| 534 |
+
emoji = "🌦️"
|
| 535 |
+
elif rain_intensity == 2:
|
| 536 |
+
emoji = "🌧️"
|
| 537 |
+
elif rain_intensity >= 3:
|
| 538 |
+
emoji = "⛈️"
|
| 539 |
+
else:
|
| 540 |
+
emoji = "❓"
|
| 541 |
+
|
| 542 |
+
formatted += f" • {time_str}: {emoji} {rain_desc}\n"
|
| 543 |
+
else:
|
| 544 |
+
formatted += " • Données non disponibles\n"
|
| 545 |
+
|
| 546 |
+
except Exception as e:
|
| 547 |
+
logger.error(f"Erreur formatage conditions actuelles: {e}")
|
| 548 |
+
formatted += f"\nErreur formatage: {e}\n"
|
| 549 |
+
|
| 550 |
+
return formatted
|
| 551 |
+
|
| 552 |
+
def format_hourly_forecast(forecast_data: dict, rain_data: dict = None) -> str:
|
| 553 |
+
"""
|
| 554 |
+
Formate les prévisions météo horaires sur 24h avec intégration des précipitations.
|
| 555 |
+
|
| 556 |
+
Args:
|
| 557 |
+
forecast_data (dict): Données de prévision horaire de l'API Météo-France
|
| 558 |
+
rain_data (dict, optional): Données de précipitations à intégrer. Defaults to None.
|
| 559 |
+
|
| 560 |
+
Returns:
|
| 561 |
+
str: Prévisions horaires formatées en markdown avec température,
|
| 562 |
+
météo, vent et informations de pluie pour chaque heure
|
| 563 |
+
"""
|
| 564 |
+
if "error" in forecast_data:
|
| 565 |
+
return f"❌ {forecast_data['error']}"
|
| 566 |
+
|
| 567 |
+
formatted = "🕰️ **Prévisions Heure par Heure (24h)**\n\n"
|
| 568 |
+
|
| 569 |
+
try:
|
| 570 |
+
# Créer un dictionnaire des prévisions pluie par timestamp pour lookup rapide
|
| 571 |
+
rain_by_time = {}
|
| 572 |
+
if rain_data and "forecast" in rain_data and rain_data["forecast"]:
|
| 573 |
+
for rain_point in rain_data["forecast"]:
|
| 574 |
+
rain_timestamp = rain_point.get("dt", 0)
|
| 575 |
+
rain_by_time[rain_timestamp] = rain_point
|
| 576 |
+
|
| 577 |
+
if "forecast" in forecast_data and forecast_data["forecast"]:
|
| 578 |
+
for hour in forecast_data["forecast"][:24]:
|
| 579 |
+
timestamp = hour.get("dt", 0)
|
| 580 |
+
time_str = format_timestamp(timestamp, "time")
|
| 581 |
+
|
| 582 |
+
temp = hour.get("T", {}).get("value", "--")
|
| 583 |
+
weather = hour.get("weather", {}).get("desc", "--")
|
| 584 |
+
wind_speed = hour.get("wind", {}).get("speed", "--")
|
| 585 |
+
wind_dir = hour.get("wind", {}).get("direction", "--")
|
| 586 |
+
wind_gust = hour.get("wind", {}).get("gust", "--")
|
| 587 |
+
|
| 588 |
+
# Récupérer les données de pluie pour cette heure
|
| 589 |
+
rain_info = ""
|
| 590 |
+
if timestamp in rain_by_time:
|
| 591 |
+
rain_point = rain_by_time[timestamp]
|
| 592 |
+
rain_intensity = rain_point.get("rain", 0)
|
| 593 |
+
rain_desc = rain_point.get("desc", "")
|
| 594 |
+
|
| 595 |
+
if rain_intensity <= 1:
|
| 596 |
+
rain_info = " - ☀️ Sec"
|
| 597 |
+
elif rain_intensity == 2:
|
| 598 |
+
rain_info = " - 🌦️ Pluie faible"
|
| 599 |
+
elif rain_intensity == 3:
|
| 600 |
+
rain_info = " - 🌧️ Pluie modérée"
|
| 601 |
+
elif rain_intensity >= 4:
|
| 602 |
+
rain_info = " - ⛈️ Pluie forte"
|
| 603 |
+
else:
|
| 604 |
+
rain_info = f" - 💧 Intensité {rain_intensity}"
|
| 605 |
+
|
| 606 |
+
formatted += f"**{time_str}**: {temp}°C - {weather}{rain_info}\n"
|
| 607 |
+
formatted += f" Vent: {wind_speed} km/h ({wind_dir}°)"
|
| 608 |
+
if wind_gust != "--" and wind_gust != 0:
|
| 609 |
+
formatted += f" - Rafales: {wind_gust} km/h"
|
| 610 |
+
formatted += "\n\n"
|
| 611 |
+
|
| 612 |
+
except Exception as e:
|
| 613 |
+
logger.error(f"Erreur formatage prévisions horaires: {e}")
|
| 614 |
+
formatted += f"Erreur: {e}\n"
|
| 615 |
+
|
| 616 |
+
return formatted
|
| 617 |
+
|
| 618 |
+
def format_daily_forecast(forecast_data: dict) -> str:
|
| 619 |
+
"""
|
| 620 |
+
Formate les prévisions météorologiques quotidiennes sur 10 jours.
|
| 621 |
+
|
| 622 |
+
Args:
|
| 623 |
+
forecast_data (dict): Données de prévision quotidienne de l'API Météo-France
|
| 624 |
+
avec clé daily_forecast contenant la liste des jours
|
| 625 |
+
|
| 626 |
+
Returns:
|
| 627 |
+
str: Prévisions sur 10 jours formatées en markdown avec températures
|
| 628 |
+
min/max, description météo et données de vent pour chaque jour
|
| 629 |
+
"""
|
| 630 |
+
if "error" in forecast_data:
|
| 631 |
+
return f"❌ {forecast_data['error']}"
|
| 632 |
+
|
| 633 |
+
formatted = "📅 **Prévisions à 10 Jours**\n\n"
|
| 634 |
+
|
| 635 |
+
try:
|
| 636 |
+
if "daily_forecast" in forecast_data and forecast_data["daily_forecast"]:
|
| 637 |
+
for day in forecast_data["daily_forecast"][:10]:
|
| 638 |
+
timestamp = day.get("dt", 0)
|
| 639 |
+
date_str = format_timestamp(timestamp, "date")
|
| 640 |
+
|
| 641 |
+
temp_min = day.get("T", {}).get("min", "--")
|
| 642 |
+
temp_max = day.get("T", {}).get("max", "--")
|
| 643 |
+
weather = day.get("weather12H", {}).get("desc", "--")
|
| 644 |
+
wind_speed = day.get("wind", {}).get("speed", "--")
|
| 645 |
+
wind_dir = day.get("wind", {}).get("direction", "--")
|
| 646 |
+
|
| 647 |
+
formatted += f"**{date_str}**: {temp_min}°/{temp_max}°C\n"
|
| 648 |
+
formatted += f" {weather}\n"
|
| 649 |
+
if wind_speed != "--":
|
| 650 |
+
formatted += f" Vent: {wind_speed} km/h ({wind_dir}°)\n"
|
| 651 |
+
formatted += "\n"
|
| 652 |
+
|
| 653 |
+
except Exception as e:
|
| 654 |
+
logger.error(f"Erreur formatage prévisions 10 jours: {e}")
|
| 655 |
+
formatted += f"Erreur: {e}\n"
|
| 656 |
+
|
| 657 |
+
return formatted
|
| 658 |
+
|
| 659 |
+
def format_wind_data(forecast_data: dict, current_data: dict = None) -> str:
|
| 660 |
+
"""
|
| 661 |
+
Formate les données de vent actuelles et prévisions pour l'affichage.
|
| 662 |
+
|
| 663 |
+
Args:
|
| 664 |
+
forecast_data (dict): Prévisions météo avec données de vent horaires et quotidiennes
|
| 665 |
+
current_data (dict, optional): Observations actuelles de vent. Defaults to None.
|
| 666 |
+
|
| 667 |
+
Returns:
|
| 668 |
+
str: Données de vent formatées en markdown avec conditions actuelles,
|
| 669 |
+
prévisions horaires sur 24h et prévisions quotidiennes sur 10 jours
|
| 670 |
+
"""
|
| 671 |
+
if "error" in forecast_data:
|
| 672 |
+
return f"❌ {forecast_data['error']}"
|
| 673 |
+
|
| 674 |
+
formatted = "💨 **Informations Vent**\n\n"
|
| 675 |
+
|
| 676 |
+
try:
|
| 677 |
+
# Conditions actuelles de vent
|
| 678 |
+
if current_data and "error" not in current_data:
|
| 679 |
+
formatted += "🌬️ **Conditions actuelles:**\n"
|
| 680 |
+
wind_speed = current_data.get("wind_speed", "--")
|
| 681 |
+
wind_dir = current_data.get("wind_direction", "--")
|
| 682 |
+
wind_icon = current_data.get("wind_icon", "")
|
| 683 |
+
formatted += f" • Vitesse: {wind_speed} km/h\n"
|
| 684 |
+
formatted += f" • Direction: {wind_dir}° {wind_icon}\n\n"
|
| 685 |
+
|
| 686 |
+
# Prévisions de vent par heure
|
| 687 |
+
if "forecast" in forecast_data and forecast_data["forecast"]:
|
| 688 |
+
formatted += "🕰️ **Prévisions vent (24h):**\n"
|
| 689 |
+
for hour in forecast_data["forecast"][:24]:
|
| 690 |
+
timestamp = hour.get("dt", 0)
|
| 691 |
+
time_str = format_timestamp(timestamp, "time")
|
| 692 |
+
wind_speed = hour.get("wind", {}).get("speed", "--")
|
| 693 |
+
wind_dir = hour.get("wind", {}).get("direction", "--")
|
| 694 |
+
wind_gust = hour.get("wind", {}).get("gust", "--")
|
| 695 |
+
|
| 696 |
+
formatted += f" • {time_str}: {wind_speed} km/h"
|
| 697 |
+
if wind_gust != "--" and wind_gust != 0:
|
| 698 |
+
formatted += f" (rafales: {wind_gust} km/h)"
|
| 699 |
+
formatted += f" - {wind_dir}°\n"
|
| 700 |
+
|
| 701 |
+
# Prévisions quotidiennes de vent
|
| 702 |
+
if "daily_forecast" in forecast_data and forecast_data["daily_forecast"]:
|
| 703 |
+
formatted += "\n📅 **Vent à 10 jours:**\n"
|
| 704 |
+
for day in forecast_data["daily_forecast"][:10]:
|
| 705 |
+
timestamp = day.get("dt", 0)
|
| 706 |
+
date_str = format_timestamp(timestamp, "date")
|
| 707 |
+
wind_speed = day.get("wind", {}).get("speed", "--")
|
| 708 |
+
wind_dir = day.get("wind", {}).get("direction", "--")
|
| 709 |
+
formatted += f" • {date_str}: {wind_speed} km/h - {wind_dir}°\n"
|
| 710 |
+
|
| 711 |
+
except Exception as e:
|
| 712 |
+
logger.error(f"Erreur formatage vent: {e}")
|
| 713 |
+
formatted += f"Données brutes: {json.dumps(forecast_data, indent=2, ensure_ascii=False)[:500]}..."
|
| 714 |
+
|
| 715 |
+
return formatted
|
| 716 |
+
|
| 717 |
+
def format_alerts_data(data: dict) -> str:
|
| 718 |
+
"""
|
| 719 |
+
Formate les alertes météorologiques Météo-France selon leur niveau de danger.
|
| 720 |
+
|
| 721 |
+
Analyse les données d'alertes brutes, détermine le niveau maximum d'alerte
|
| 722 |
+
et formate l'affichage avec codes couleur appropriés (vert/jaune/orange/rouge).
|
| 723 |
+
|
| 724 |
+
Args:
|
| 725 |
+
data (dict): Données d'alertes de l'API Météo-France avec clés:
|
| 726 |
+
- phenomenons_max_colors: Liste des phénomènes avec niveaux
|
| 727 |
+
- domain_id: Code département
|
| 728 |
+
- update_time: Timestamp de mise à jour
|
| 729 |
+
|
| 730 |
+
Returns:
|
| 731 |
+
str: Alertes formatées en markdown avec niveau global, détail des
|
| 732 |
+
phénomènes actifs et descriptions selon référentiel officiel
|
| 733 |
+
"""
|
| 734 |
+
if "error" in data:
|
| 735 |
+
return f"❌ {data['error']}"
|
| 736 |
+
|
| 737 |
+
try:
|
| 738 |
+
# Calculer le niveau d'alerte maximum selon la logique meteofrance-api
|
| 739 |
+
max_color_id = 1 # Par défaut vert
|
| 740 |
+
active_alerts = []
|
| 741 |
+
|
| 742 |
+
# Traitement pour les données directes de l'API
|
| 743 |
+
if "phenomenons_max_colors" in data and data["phenomenons_max_colors"] is not None:
|
| 744 |
+
for item in data["phenomenons_max_colors"]:
|
| 745 |
+
if item is not None:
|
| 746 |
+
color_id = item.get("phenomenon_max_color_id", 1)
|
| 747 |
+
if color_id > max_color_id:
|
| 748 |
+
max_color_id = color_id
|
| 749 |
+
if color_id > 1:
|
| 750 |
+
active_alerts.append(item)
|
| 751 |
+
|
| 752 |
+
# Traitement pour le format avec timelaps (cas multi-départements)
|
| 753 |
+
elif "timelaps" in data and isinstance(data["timelaps"], list):
|
| 754 |
+
# Trouver le département avec le niveau d'alerte le plus élevé
|
| 755 |
+
# ou chercher un département spécifique si on peut l'identifier
|
| 756 |
+
target_dept = None
|
| 757 |
+
|
| 758 |
+
# data["timelaps"] est une liste de départements
|
| 759 |
+
for dept_data in data["timelaps"]:
|
| 760 |
+
if dept_data and "phenomenons_max_color" in dept_data:
|
| 761 |
+
dept_max = 1
|
| 762 |
+
dept_alerts = []
|
| 763 |
+
|
| 764 |
+
for item in dept_data["phenomenons_max_color"]:
|
| 765 |
+
if item is not None:
|
| 766 |
+
color_id = item.get("phenomenon_max_color_id", 1)
|
| 767 |
+
dept_max = max(dept_max, color_id)
|
| 768 |
+
if color_id > 1:
|
| 769 |
+
dept_alerts.append(item)
|
| 770 |
+
|
| 771 |
+
# Si ce département a un niveau plus élevé que l'actuel
|
| 772 |
+
if dept_max > max_color_id:
|
| 773 |
+
max_color_id = dept_max
|
| 774 |
+
active_alerts = dept_alerts
|
| 775 |
+
target_dept = dept_data.get("domain_id", "Inconnu")
|
| 776 |
+
|
| 777 |
+
# Traitement pour les sous-domaines si pas d'alertes directes
|
| 778 |
+
elif "subdomains_phenomenons_max_color" in data and data["subdomains_phenomenons_max_color"] is not None:
|
| 779 |
+
for subdomain in data["subdomains_phenomenons_max_color"]:
|
| 780 |
+
if subdomain and "phenomenons_max_color" in subdomain and subdomain["phenomenons_max_color"] is not None:
|
| 781 |
+
for item in subdomain["phenomenons_max_color"]:
|
| 782 |
+
if item is not None:
|
| 783 |
+
color_id = item.get("phenomenon_max_color_id", 1)
|
| 784 |
+
if color_id > max_color_id:
|
| 785 |
+
max_color_id = color_id
|
| 786 |
+
if color_id > 1:
|
| 787 |
+
active_alerts.append(item)
|
| 788 |
+
|
| 789 |
+
# Traitement pour le format alternatif
|
| 790 |
+
elif "phenomenon_items" in data:
|
| 791 |
+
for phenomenon_id, phenomenon_data in data["phenomenon_items"].items():
|
| 792 |
+
color_id = phenomenon_data.get("phenomenon_max_color_id", 1)
|
| 793 |
+
if color_id > max_color_id:
|
| 794 |
+
max_color_id = color_id
|
| 795 |
+
if color_id > 1:
|
| 796 |
+
active_alerts.append({
|
| 797 |
+
"phenomenon_id": phenomenon_id,
|
| 798 |
+
"phenomenon_max_color_id": color_id
|
| 799 |
+
})
|
| 800 |
+
|
| 801 |
+
# Noms des phénomènes (référence officielle meteofrance-api)
|
| 802 |
+
phenomena_names = {
|
| 803 |
+
"0": None,
|
| 804 |
+
"1": "Vent violent",
|
| 805 |
+
"2": "Pluie-inondation",
|
| 806 |
+
"3": "Orages",
|
| 807 |
+
"4": "Inondation",
|
| 808 |
+
"5": "Neige-verglas",
|
| 809 |
+
"6": "Canicule",
|
| 810 |
+
"7": "Grand-froid",
|
| 811 |
+
"8": "Avalanches",
|
| 812 |
+
"9": "Vagues-submersion"
|
| 813 |
+
}
|
| 814 |
+
|
| 815 |
+
# Correspondance couleurs (CORRIGÉE)
|
| 816 |
+
colors = {
|
| 817 |
+
1: "🟢 Vert",
|
| 818 |
+
2: "🟡 Jaune",
|
| 819 |
+
3: "🟠 Orange",
|
| 820 |
+
4: "🔴 Rouge"
|
| 821 |
+
}
|
| 822 |
+
|
| 823 |
+
color_descriptions = {
|
| 824 |
+
1: "Pas de vigilance particulière",
|
| 825 |
+
2: "Soyez attentifs",
|
| 826 |
+
3: "Soyez très vigilants",
|
| 827 |
+
4: "Vigilance absolue"
|
| 828 |
+
}
|
| 829 |
+
|
| 830 |
+
# Formatage selon le niveau maximum
|
| 831 |
+
if max_color_id >= 4:
|
| 832 |
+
formatted = "🔴 **ALERTE ROUGE - VIGILANCE ABSOLUE**\n\n"
|
| 833 |
+
elif max_color_id >= 3:
|
| 834 |
+
formatted = "🟠 **ALERTE ORANGE - SOYEZ TRÈS VIGILANTS**\n\n"
|
| 835 |
+
elif max_color_id >= 2:
|
| 836 |
+
formatted = "🟡 **VIGILANCE JAUNE - SOYEZ ATTENTIFS**\n\n"
|
| 837 |
+
else:
|
| 838 |
+
formatted = "🟢 **Pas d'alerte en cours**\n\n"
|
| 839 |
+
|
| 840 |
+
# Affichage du département si disponible
|
| 841 |
+
if 'target_dept' in locals() and target_dept:
|
| 842 |
+
formatted += f"📍 **Département**: {target_dept}\n"
|
| 843 |
+
elif "domain_id" in data:
|
| 844 |
+
formatted += f"📍 **Département**: {data['domain_id']}\n"
|
| 845 |
+
|
| 846 |
+
# Niveau global
|
| 847 |
+
formatted += f"🏷️ **Niveau global**: {colors.get(max_color_id, 'Inconnu')} - {color_descriptions.get(max_color_id, '')}\n\n"
|
| 848 |
+
|
| 849 |
+
# Détail des alertes actives
|
| 850 |
+
if active_alerts:
|
| 851 |
+
formatted += "🚨 **Détail des alertes:**\n"
|
| 852 |
+
for alert in active_alerts:
|
| 853 |
+
phenomenon_id = str(alert.get("phenomenon_id", ""))
|
| 854 |
+
color_id = alert.get("phenomenon_max_color_id", 1)
|
| 855 |
+
phenomenon_name = phenomena_names.get(phenomenon_id, f"Phénomène {phenomenon_id}")
|
| 856 |
+
color_desc = colors.get(color_id, f"Niveau {color_id}")
|
| 857 |
+
|
| 858 |
+
formatted += f" • **{phenomenon_name}**: {color_desc}\n"
|
| 859 |
+
|
| 860 |
+
# Timestamp de mise à jour
|
| 861 |
+
if "update_time" in data:
|
| 862 |
+
update_time = data['update_time']
|
| 863 |
+
if isinstance(update_time, (int, float)):
|
| 864 |
+
time_str = format_timestamp(update_time)
|
| 865 |
+
else:
|
| 866 |
+
time_str = update_time
|
| 867 |
+
formatted += f"\n⏰ **Mise à jour**: {time_str}\n"
|
| 868 |
+
|
| 869 |
+
except Exception as e:
|
| 870 |
+
logger.error(f"Erreur formatage alertes: {e}")
|
| 871 |
+
formatted = f"❌ Erreur formatage alertes: {e}\n\n"
|
| 872 |
+
formatted += f"Données brutes: {json.dumps(data, indent=2, ensure_ascii=False)[:500]}..."
|
| 873 |
+
|
| 874 |
+
return formatted
|
| 875 |
+
|
| 876 |
+
def get_weather_forecast(address: str):
|
| 877 |
+
"""
|
| 878 |
+
Récupère les prévisions météo complètes pour une adresse française.
|
| 879 |
+
|
| 880 |
+
Effectue la géolocalisation via IGN, récupère les données météo via l'API officielle
|
| 881 |
+
Météo-France, et formate les résultats pour l'affichage avec alertes, conditions
|
| 882 |
+
actuelles et prévisions.
|
| 883 |
+
|
| 884 |
+
Args:
|
| 885 |
+
address (str): Adresse française à géolocaliser (ex: "Brest, France", "75001 Paris", "Nice")
|
| 886 |
+
|
| 887 |
+
Returns:
|
| 888 |
+
tuple: Quatre éléments formatés en markdown:
|
| 889 |
+
- alerts_info (str): Alertes météo avec niveaux de vigilance (vert/jaune/orange/rouge)
|
| 890 |
+
- current_conditions (str): Conditions actuelles (température, vent, précipitations)
|
| 891 |
+
- hourly_forecast (str): Prévisions horaires sur 24h avec données de pluie
|
| 892 |
+
- daily_forecast (str): Prévisions quotidiennes sur 10 jours
|
| 893 |
+
|
| 894 |
+
Note:
|
| 895 |
+
Utilise les APIs officielles françaises :
|
| 896 |
+
- IGN Géoplateforme pour la géolocalisation
|
| 897 |
+
- Météo-France pour les données météorologiques
|
| 898 |
+
|
| 899 |
+
Examples:
|
| 900 |
+
>>> alerts, current, hourly, daily = get_weather_forecast("Paris, France")
|
| 901 |
+
>>> print(current) # Affiche température, vent, etc.
|
| 902 |
+
"""
|
| 903 |
+
logger.info(f"🚀 Début de la requête pour: {address}")
|
| 904 |
+
|
| 905 |
+
if not address.strip():
|
| 906 |
+
logger.warning("⚠️ Adresse vide fournie")
|
| 907 |
+
error_msg = "❌ Veuillez saisir une adresse"
|
| 908 |
+
return error_msg, error_msg, error_msg, error_msg
|
| 909 |
+
|
| 910 |
+
# Géocodage de l'adresse
|
| 911 |
+
geocoder = GeocodingAPI()
|
| 912 |
+
geocoding_result = geocoder.geocode_address(address)
|
| 913 |
+
|
| 914 |
+
if not geocoding_result:
|
| 915 |
+
error_msg = "❌ Impossible de localiser cette adresse"
|
| 916 |
+
logger.error(f"❌ Géocodage échoué pour: {address}")
|
| 917 |
+
return error_msg, error_msg, error_msg, error_msg
|
| 918 |
+
|
| 919 |
+
lat = geocoding_result["lat"]
|
| 920 |
+
lon = geocoding_result["lon"]
|
| 921 |
+
logger.info(f"✅ Coordonnées obtenues: lat={lat}, lon={lon}")
|
| 922 |
+
|
| 923 |
+
# Récupération des données météo
|
| 924 |
+
meteo_api = MeteoFranceAPI()
|
| 925 |
+
weather_data = meteo_api.get_location_forecast(lat, lon)
|
| 926 |
+
current_weather = meteo_api.get_current_observation(lat, lon)
|
| 927 |
+
rain_forecast = meteo_api.get_rain_forecast(lat, lon)
|
| 928 |
+
|
| 929 |
+
# Utiliser le département depuis l'API forecast pour les alertes
|
| 930 |
+
department = None
|
| 931 |
+
if weather_data and "position" in weather_data:
|
| 932 |
+
department = weather_data["position"].get("dept")
|
| 933 |
+
|
| 934 |
+
if department:
|
| 935 |
+
logger.info(f"🏷️ Utilisation département depuis API forecast: {department}")
|
| 936 |
+
wind_alerts = meteo_api.get_wind_alerts_by_department(department)
|
| 937 |
+
else:
|
| 938 |
+
logger.warning("⚠️ Pas de département trouvé, utilisation géolocalisation")
|
| 939 |
+
wind_alerts = meteo_api.get_wind_alerts(lat, lon)
|
| 940 |
+
|
| 941 |
+
# Formatage des résultats
|
| 942 |
+
alerts_info = format_alerts_data(wind_alerts)
|
| 943 |
+
current_conditions = format_current_conditions(current_weather, rain_forecast, weather_data)
|
| 944 |
+
hourly_forecast = format_hourly_forecast(weather_data, rain_forecast)
|
| 945 |
+
daily_forecast = format_daily_forecast(weather_data)
|
| 946 |
+
|
| 947 |
+
logger.info("✅ Requête terminée avec succès")
|
| 948 |
+
return alerts_info, current_conditions, hourly_forecast, daily_forecast
|
| 949 |
+
|
| 950 |
+
# Fonctions individuelles pour MCP
|
| 951 |
+
def get_weather_alerts(address: str) -> str:
|
| 952 |
+
"""
|
| 953 |
+
Récupère les alertes météorologiques pour une adresse française.
|
| 954 |
+
|
| 955 |
+
Retourne les niveaux de vigilance Météo-France (vert/jaune/orange/rouge)
|
| 956 |
+
avec détail des phénomènes dangereux.
|
| 957 |
+
|
| 958 |
+
Args:
|
| 959 |
+
address (str): Adresse française à analyser (ex: "Paris, France", "29200 Brest")
|
| 960 |
+
|
| 961 |
+
Returns:
|
| 962 |
+
str: Alertes météo formatées avec niveaux de vigilance et phénomènes actifs
|
| 963 |
+
"""
|
| 964 |
+
alerts_info, _, _, _ = get_weather_forecast(address)
|
| 965 |
+
return alerts_info
|
| 966 |
+
|
| 967 |
+
def get_current_weather(address: str) -> str:
|
| 968 |
+
"""
|
| 969 |
+
Récupère les conditions météorologiques actuelles pour une adresse.
|
| 970 |
+
|
| 971 |
+
Fournit température, vent, précipitations, humidité, pression et visibilité
|
| 972 |
+
en temps réel depuis les stations Météo-France.
|
| 973 |
+
|
| 974 |
+
Args:
|
| 975 |
+
address (str): Adresse française à analyser (ex: "Nice", "75001 Paris")
|
| 976 |
+
|
| 977 |
+
Returns:
|
| 978 |
+
str: Conditions actuelles détaillées avec données de vent et précipitations
|
| 979 |
+
"""
|
| 980 |
+
_, current_conditions, _, _ = get_weather_forecast(address)
|
| 981 |
+
return current_conditions
|
| 982 |
+
|
| 983 |
+
def get_hourly_forecast(address: str) -> str:
|
| 984 |
+
"""
|
| 985 |
+
Récupère les prévisions météorologiques horaires sur 24h.
|
| 986 |
+
|
| 987 |
+
Prévisions détaillées heure par heure avec température, météo,
|
| 988 |
+
vent, rafales et précipitations intégrées.
|
| 989 |
+
|
| 990 |
+
Args:
|
| 991 |
+
address (str): Adresse française à analyser (ex: "Toulouse", "13001 Marseille")
|
| 992 |
+
|
| 993 |
+
Returns:
|
| 994 |
+
str: Prévisions horaires sur 24h avec toutes les données météo
|
| 995 |
+
"""
|
| 996 |
+
_, _, hourly_forecast, _ = get_weather_forecast(address)
|
| 997 |
+
return hourly_forecast
|
| 998 |
+
|
| 999 |
+
def get_daily_forecast(address: str) -> str:
|
| 1000 |
+
"""
|
| 1001 |
+
Récupère les prévisions météorologiques quotidiennes sur 10 jours.
|
| 1002 |
+
|
| 1003 |
+
Prévisions étendues avec températures minimales et maximales,
|
| 1004 |
+
conditions générales et données de vent quotidiennes.
|
| 1005 |
+
|
| 1006 |
+
Args:
|
| 1007 |
+
address (str): Adresse française à analyser (ex: "Bordeaux", "69001 Lyon")
|
| 1008 |
+
|
| 1009 |
+
Returns:
|
| 1010 |
+
str: Prévisions quotidiennes sur 10 jours avec températures et météo
|
| 1011 |
+
"""
|
| 1012 |
+
_, _, _, daily_forecast = get_weather_forecast(address)
|
| 1013 |
+
return daily_forecast
|
| 1014 |
+
|
| 1015 |
+
def get_complete_weather_forecast(address: str) -> str:
|
| 1016 |
+
"""
|
| 1017 |
+
Récupère un rapport météorologique complet pour une adresse française.
|
| 1018 |
+
|
| 1019 |
+
Combine alertes, conditions actuelles, prévisions horaires et quotidiennes
|
| 1020 |
+
en un rapport unifié depuis les APIs officielles françaises.
|
| 1021 |
+
|
| 1022 |
+
Args:
|
| 1023 |
+
address (str): Adresse française à analyser (ex: "Brest", "06000 Nice")
|
| 1024 |
+
|
| 1025 |
+
Returns:
|
| 1026 |
+
str: Rapport météo complet avec alertes, conditions actuelles et prévisions
|
| 1027 |
+
"""
|
| 1028 |
+
alerts_info, current_conditions, hourly_forecast, daily_forecast = get_weather_forecast(address)
|
| 1029 |
+
|
| 1030 |
+
complete_report = f"""
|
| 1031 |
+
{alerts_info}
|
| 1032 |
+
|
| 1033 |
+
{current_conditions}
|
| 1034 |
+
|
| 1035 |
+
{hourly_forecast}
|
| 1036 |
+
|
| 1037 |
+
{daily_forecast}
|
| 1038 |
+
"""
|
| 1039 |
+
return complete_report
|
| 1040 |
+
|
| 1041 |
+
def get_weather_emoji(description: str) -> str:
|
| 1042 |
+
"""
|
| 1043 |
+
Retourne l'émoji météo approprié selon la description textuelle.
|
| 1044 |
+
|
| 1045 |
+
Args:
|
| 1046 |
+
description (str): Description météo en français (ex: "ensoleillé", "nuageux")
|
| 1047 |
+
|
| 1048 |
+
Returns:
|
| 1049 |
+
str: Émoji Unicode correspondant à la condition météo
|
| 1050 |
+
(☀️, ☁️, 🌧️, ⛈️, ❄️, 🌫️, ⛅, 🌤️)
|
| 1051 |
+
"""
|
| 1052 |
+
description = description.lower()
|
| 1053 |
+
if "ensoleill" in description or "clair" in description:
|
| 1054 |
+
return "☀️"
|
| 1055 |
+
elif "nuage" in description:
|
| 1056 |
+
return "☁️"
|
| 1057 |
+
elif "pluie" in description or "averse" in description:
|
| 1058 |
+
return "🌧️"
|
| 1059 |
+
elif "orage" in description:
|
| 1060 |
+
return "⛈️"
|
| 1061 |
+
elif "neige" in description:
|
| 1062 |
+
return "❄️"
|
| 1063 |
+
elif "brouillard" in description:
|
| 1064 |
+
return "🌫️"
|
| 1065 |
+
elif "éclaircies" in description:
|
| 1066 |
+
return "⛅"
|
| 1067 |
+
else:
|
| 1068 |
+
return "🌤️"
|
| 1069 |
+
|
| 1070 |
+
def create_wind_compass(direction: int, speed: float) -> str:
|
| 1071 |
+
"""
|
| 1072 |
+
Génère une boussole HTML interactive pour visualiser la direction du vent.
|
| 1073 |
+
|
| 1074 |
+
Crée un élément SVG-like en HTML/CSS avec flèche orientée selon la direction
|
| 1075 |
+
et couleur selon la vitesse du vent.
|
| 1076 |
+
|
| 1077 |
+
Args:
|
| 1078 |
+
direction (int): Direction du vent en degrés (0-360°, 0° = Nord)
|
| 1079 |
+
speed (float): Vitesse du vent en km/h
|
| 1080 |
+
|
| 1081 |
+
Returns:
|
| 1082 |
+
str: Code HTML de la boussole avec flèche orientée et colorée:
|
| 1083 |
+
- Vert: < 10 km/h
|
| 1084 |
+
- Orange: 10-20 km/h
|
| 1085 |
+
- Rouge: 20-30 km/h
|
| 1086 |
+
- Violet: > 30 km/h
|
| 1087 |
+
"""
|
| 1088 |
+
if not direction or direction < 0:
|
| 1089 |
+
direction = 0
|
| 1090 |
+
|
| 1091 |
+
# Convertir en radians pour le CSS transform
|
| 1092 |
+
rotation = direction - 90 # Ajuster pour que 0° = Nord
|
| 1093 |
+
|
| 1094 |
+
# Couleur selon la vitesse
|
| 1095 |
+
if speed < 10:
|
| 1096 |
+
color = "#4CAF50" # Vert
|
| 1097 |
+
elif speed < 20:
|
| 1098 |
+
color = "#FF9800" # Orange
|
| 1099 |
+
elif speed < 30:
|
| 1100 |
+
color = "#F44336" # Rouge
|
| 1101 |
+
else:
|
| 1102 |
+
color = "#9C27B0" # Violet
|
| 1103 |
+
|
| 1104 |
+
compass_html = f"""
|
| 1105 |
+
<div style="display: inline-block; position: relative; width: 60px; height: 60px;
|
| 1106 |
+
border: 2px solid {color}; border-radius: 50%; margin: 0 10px;">
|
| 1107 |
+
<!-- Boussole background -->
|
| 1108 |
+
<div style="position: absolute; top: 2px; left: 50%; transform: translateX(-50%);
|
| 1109 |
+
font-size: 8px; color: {color}; font-weight: bold;">N</div>
|
| 1110 |
+
<div style="position: absolute; bottom: 2px; left: 50%; transform: translateX(-50%);
|
| 1111 |
+
font-size: 8px; color: {color}; font-weight: bold;">S</div>
|
| 1112 |
+
<div style="position: absolute; left: 2px; top: 50%; transform: translateY(-50%);
|
| 1113 |
+
font-size: 8px; color: {color}; font-weight: bold;">O</div>
|
| 1114 |
+
<div style="position: absolute; right: 2px; top: 50%; transform: translateY(-50%);
|
| 1115 |
+
font-size: 8px; color: {color}; font-weight: bold;">E</div>
|
| 1116 |
+
|
| 1117 |
+
<!-- Flèche du vent -->
|
| 1118 |
+
<div style="position: absolute; top: 50%; left: 50%;
|
| 1119 |
+
width: 3px; height: 20px; background: {color};
|
| 1120 |
+
transform: translate(-50%, -50%) rotate({rotation}deg);
|
| 1121 |
+
transform-origin: center; border-radius: 2px;">
|
| 1122 |
+
<!-- Pointe de la flèche -->
|
| 1123 |
+
<div style="position: absolute; top: -3px; left: 50%;
|
| 1124 |
+
transform: translateX(-50%);
|
| 1125 |
+
width: 0; height: 0;
|
| 1126 |
+
border-left: 4px solid transparent;
|
| 1127 |
+
border-right: 4px solid transparent;
|
| 1128 |
+
border-bottom: 6px solid {color};"></div>
|
| 1129 |
+
</div>
|
| 1130 |
+
|
| 1131 |
+
<!-- Centre -->
|
| 1132 |
+
<div style="position: absolute; top: 50%; left: 50%;
|
| 1133 |
+
width: 4px; height: 4px; background: {color};
|
| 1134 |
+
border-radius: 50%; transform: translate(-50%, -50%);"></div>
|
| 1135 |
+
</div>
|
| 1136 |
+
"""
|
| 1137 |
+
return compass_html
|
| 1138 |
+
|
| 1139 |
+
def create_wind_compass_from_text(wind_text: str) -> str:
|
| 1140 |
+
"""
|
| 1141 |
+
Extrait vitesse et direction du vent depuis un texte formaté et génère une boussole.
|
| 1142 |
+
|
| 1143 |
+
Parse le texte au format "X.X km/h - Y° [direction]" pour extraire
|
| 1144 |
+
les valeurs numériques et créer la visualisation boussole.
|
| 1145 |
+
|
| 1146 |
+
Args:
|
| 1147 |
+
wind_text (str): Texte formaté de vent (ex: "12.5 km/h - 270° O")
|
| 1148 |
+
|
| 1149 |
+
Returns:
|
| 1150 |
+
str: Code HTML de boussole générée, ou chaîne vide si parsing échoue
|
| 1151 |
+
"""
|
| 1152 |
+
import re
|
| 1153 |
+
|
| 1154 |
+
# Regex pour extraire vitesse et direction: "2.8 km/h - 322° NO"
|
| 1155 |
+
match = re.search(r'(\d+\.?\d*)\s*km/h.*?(\d+)°', wind_text)
|
| 1156 |
+
if match:
|
| 1157 |
+
speed = float(match.group(1))
|
| 1158 |
+
direction = int(match.group(2))
|
| 1159 |
+
return create_wind_compass(direction, speed)
|
| 1160 |
+
return ""
|
| 1161 |
+
|
| 1162 |
+
|
| 1163 |
+
def format_alerts_card_html(alerts_info: str) -> str:
|
| 1164 |
+
"""
|
| 1165 |
+
Convertit les alertes météo formatées en carte HTML stylisée.
|
| 1166 |
+
|
| 1167 |
+
Applique les styles CSS appropriés selon le niveau d'alerte détecté
|
| 1168 |
+
dans le texte (rouge, orange, jaune, ou standard).
|
| 1169 |
+
|
| 1170 |
+
Args:
|
| 1171 |
+
alerts_info (str): Alertes formatées en markdown depuis format_alerts_data()
|
| 1172 |
+
|
| 1173 |
+
Returns:
|
| 1174 |
+
str: Carte HTML avec classe CSS appropriée au niveau d'alerte:
|
| 1175 |
+
- alert-card-red: Alerte rouge
|
| 1176 |
+
- alert-card-orange: Alerte orange
|
| 1177 |
+
- alert-card-yellow: Alerte jaune
|
| 1178 |
+
- weather-card: Aucune alerte ou erreur
|
| 1179 |
+
"""
|
| 1180 |
+
if "❌" in alerts_info:
|
| 1181 |
+
return f'<div class="weather-card">{alerts_info}</div>'
|
| 1182 |
+
|
| 1183 |
+
# Déterminer la classe CSS selon le niveau d'alerte
|
| 1184 |
+
if "ROUGE" in alerts_info.upper():
|
| 1185 |
+
card_class = "alert-card-red"
|
| 1186 |
+
elif "ORANGE" in alerts_info.upper():
|
| 1187 |
+
card_class = "alert-card-orange"
|
| 1188 |
+
elif "JAUNE" in alerts_info.upper():
|
| 1189 |
+
card_class = "alert-card-yellow"
|
| 1190 |
+
else:
|
| 1191 |
+
card_class = "weather-card"
|
| 1192 |
+
|
| 1193 |
+
# Nettoyer le texte markdown
|
| 1194 |
+
clean_text = alerts_info.replace("**", "<strong>").replace("**", "</strong>")
|
| 1195 |
+
clean_text = clean_text.replace("\n", "<br>")
|
| 1196 |
+
|
| 1197 |
+
return f'<div class="{card_class}">{clean_text}</div>'
|
| 1198 |
+
|
| 1199 |
+
def format_current_card_html(current_conditions: str) -> str:
|
| 1200 |
+
"""
|
| 1201 |
+
Convertit les conditions actuelles en carte HTML avec layout optimisé.
|
| 1202 |
+
|
| 1203 |
+
Parse les données markdown pour extraire température, localisation, météo,
|
| 1204 |
+
vent et précipitations, puis génère une carte avec boussole de vent intégrée.
|
| 1205 |
+
|
| 1206 |
+
Args:
|
| 1207 |
+
current_conditions (str): Conditions actuelles formatées depuis format_current_conditions()
|
| 1208 |
+
|
| 1209 |
+
Returns:
|
| 1210 |
+
str: Carte HTML avec layout responsive affichant:
|
| 1211 |
+
- Localisation et description météo
|
| 1212 |
+
- Température avec émoji large
|
| 1213 |
+
- Boussole de vent interactive
|
| 1214 |
+
- Informations de précipitations
|
| 1215 |
+
"""
|
| 1216 |
+
if "❌" in current_conditions:
|
| 1217 |
+
return f'<div class="weather-card">{current_conditions}</div>'
|
| 1218 |
+
|
| 1219 |
+
# Extraire les informations principales
|
| 1220 |
+
lines = current_conditions.split("\n")
|
| 1221 |
+
location = "Localisation inconnue"
|
| 1222 |
+
temp = "--"
|
| 1223 |
+
weather_desc = "--"
|
| 1224 |
+
wind = "--"
|
| 1225 |
+
observation_time = "--"
|
| 1226 |
+
precipitation = "Données non disponibles"
|
| 1227 |
+
|
| 1228 |
+
for line in lines:
|
| 1229 |
+
if "Lieu**:" in line:
|
| 1230 |
+
location = line.split(":", 1)[1].strip()
|
| 1231 |
+
elif "Température**:" in line:
|
| 1232 |
+
temp = line.split(":", 1)[1].strip()
|
| 1233 |
+
elif "Temps**:" in line:
|
| 1234 |
+
weather_desc = line.split(":", 1)[1].strip().split()[0] # Premier mot
|
| 1235 |
+
elif "Vent**:" in line:
|
| 1236 |
+
wind = line.split(":", 1)[1].strip()
|
| 1237 |
+
elif "Observation**:" in line:
|
| 1238 |
+
observation_time = line.split(":", 1)[1].strip()
|
| 1239 |
+
elif "Précipitations**:" in line:
|
| 1240 |
+
precipitation = line.split(":", 1)[1].strip()
|
| 1241 |
+
|
| 1242 |
+
weather_emoji = get_weather_emoji(weather_desc)
|
| 1243 |
+
temp_num = temp.replace("°C", "")
|
| 1244 |
+
|
| 1245 |
+
html = f"""
|
| 1246 |
+
<div class="current-card">
|
| 1247 |
+
<div style="display: flex; justify-content: space-between; align-items: center;">
|
| 1248 |
+
<div style="flex: 1;">
|
| 1249 |
+
<div style="font-size: 1.1em; opacity: 0.8;">{location}</div>
|
| 1250 |
+
<div style="font-size: 0.9em; opacity: 0.7;">{weather_desc}</div>
|
| 1251 |
+
<div style="font-size: 0.8em; opacity: 0.7; margin-top: 2px;">💧 {precipitation}</div>
|
| 1252 |
+
<div style="font-size: 0.8em; opacity: 0.6; margin-top: 4px;">{observation_time}</div>
|
| 1253 |
+
</div>
|
| 1254 |
+
<div style="display: flex; align-items: center; gap: 15px;">
|
| 1255 |
+
<!-- Vent à droite -->
|
| 1256 |
+
<div style="text-align: center; font-size: 0.85em;">
|
| 1257 |
+
<div style="opacity: 0.8; margin-bottom: 5px;">🌬️ Vent</div>
|
| 1258 |
+
<div style="font-weight: bold;">{wind.split(' - ')[0] if ' - ' in wind else wind}</div>
|
| 1259 |
+
{create_wind_compass_from_text(wind)}
|
| 1260 |
+
</div>
|
| 1261 |
+
<!-- Température -->
|
| 1262 |
+
<div style="text-align: center;">
|
| 1263 |
+
<div style="font-size: 2.5em;">{weather_emoji}</div>
|
| 1264 |
+
<div class="temp-large">{temp_num}°</div>
|
| 1265 |
+
</div>
|
| 1266 |
+
</div>
|
| 1267 |
+
</div>
|
| 1268 |
+
</div>
|
| 1269 |
+
"""
|
| 1270 |
+
return html
|
| 1271 |
+
|
| 1272 |
+
def format_hourly_card_html(hourly_forecast: str) -> str:
|
| 1273 |
+
"""
|
| 1274 |
+
Convertit les prévisions horaires en carte HTML avec grille horizontale.
|
| 1275 |
+
|
| 1276 |
+
Parse les données markdown pour créer une grille de prévisions horaires
|
| 1277 |
+
avec émojis météo, températures et vitesses de vent.
|
| 1278 |
+
|
| 1279 |
+
Args:
|
| 1280 |
+
hourly_forecast (str): Prévisions horaires formatées depuis format_hourly_forecast()
|
| 1281 |
+
|
| 1282 |
+
Returns:
|
| 1283 |
+
str: Carte HTML avec grille d'éléments horaires (max 12h):
|
| 1284 |
+
- Heure, émoji météo, température, vitesse vent
|
| 1285 |
+
- Layout responsive avec items arrondis
|
| 1286 |
+
"""
|
| 1287 |
+
if "❌" in hourly_forecast:
|
| 1288 |
+
return f'<div class="weather-card">{hourly_forecast}</div>'
|
| 1289 |
+
|
| 1290 |
+
# Parser les données horaires
|
| 1291 |
+
lines = hourly_forecast.split("\n")
|
| 1292 |
+
hourly_items = []
|
| 1293 |
+
|
| 1294 |
+
i = 0
|
| 1295 |
+
while i < len(lines):
|
| 1296 |
+
line = lines[i].strip()
|
| 1297 |
+
if "**" in line and ":" in line: # Ligne d'heure
|
| 1298 |
+
time_part = line.split("**")[1].split(":")[0]
|
| 1299 |
+
temp_weather = line.split(": ", 1)[1] if ": " in line else "--"
|
| 1300 |
+
|
| 1301 |
+
# Récupérer le vent sur la ligne suivante
|
| 1302 |
+
wind_info = "--"
|
| 1303 |
+
if i + 1 < len(lines) and "Vent:" in lines[i + 1]:
|
| 1304 |
+
wind_line = lines[i + 1].strip()
|
| 1305 |
+
# Extraire seulement la vitesse du vent
|
| 1306 |
+
if "km/h" in wind_line:
|
| 1307 |
+
wind_speed = wind_line.split()[1] if len(wind_line.split()) > 1 else "--"
|
| 1308 |
+
wind_info = f"{wind_speed} km/h"
|
| 1309 |
+
|
| 1310 |
+
temp = temp_weather.split(" - ")[0] if " - " in temp_weather else "--"
|
| 1311 |
+
weather = temp_weather.split(" - ")[1] if " - " in temp_weather else "--"
|
| 1312 |
+
|
| 1313 |
+
hourly_items.append({
|
| 1314 |
+
"time": time_part,
|
| 1315 |
+
"temp": temp,
|
| 1316 |
+
"weather": weather,
|
| 1317 |
+
"wind": wind_info
|
| 1318 |
+
})
|
| 1319 |
+
i += 1
|
| 1320 |
+
|
| 1321 |
+
# Générer le HTML
|
| 1322 |
+
items_html = ""
|
| 1323 |
+
for item in hourly_items[:12]: # Prendre les 12 premières heures
|
| 1324 |
+
emoji = get_weather_emoji(item["weather"])
|
| 1325 |
+
items_html += f"""
|
| 1326 |
+
<div class="hourly-item">
|
| 1327 |
+
<div style="font-weight: bold; min-width: 50px;">{item["time"]}</div>
|
| 1328 |
+
<div style="font-size: 1.3em; margin: 0 8px;">{emoji}</div>
|
| 1329 |
+
<div style="font-weight: bold; min-width: 45px;">{item["temp"]}</div>
|
| 1330 |
+
<div style="font-size: 0.8em; opacity: 0.8; min-width: 50px; text-align: right;">{item["wind"]}</div>
|
| 1331 |
+
</div>
|
| 1332 |
+
"""
|
| 1333 |
+
|
| 1334 |
+
html = f"""
|
| 1335 |
+
<div class="hourly-card">
|
| 1336 |
+
<div style="font-size: 1.3em; margin-bottom: 12px;"><strong>🕐 Prochaines heures</strong></div>
|
| 1337 |
+
{items_html}
|
| 1338 |
+
</div>
|
| 1339 |
+
"""
|
| 1340 |
+
return html
|
| 1341 |
+
|
| 1342 |
+
def format_daily_card_html(daily_forecast: str) -> str:
|
| 1343 |
+
"""
|
| 1344 |
+
Convertit les prévisions quotidiennes en carte HTML compacte.
|
| 1345 |
+
|
| 1346 |
+
Parse les prévisions sur 10 jours pour créer une liste verticale
|
| 1347 |
+
avec émojis météo et plages de températures.
|
| 1348 |
+
|
| 1349 |
+
Args:
|
| 1350 |
+
daily_forecast (str): Prévisions quotidiennes formatées depuis format_daily_forecast()
|
| 1351 |
+
|
| 1352 |
+
Returns:
|
| 1353 |
+
str: Carte HTML avec liste de jours (max 7 affichés):
|
| 1354 |
+
- Jour, émoji météo, températures min/max
|
| 1355 |
+
- Items avec fond semi-transparent
|
| 1356 |
+
"""
|
| 1357 |
+
if "❌" in daily_forecast:
|
| 1358 |
+
return f'<div class="weather-card">{daily_forecast}</div>'
|
| 1359 |
+
|
| 1360 |
+
# Parser les données quotidiennes
|
| 1361 |
+
lines = daily_forecast.split("\n")
|
| 1362 |
+
daily_items = []
|
| 1363 |
+
|
| 1364 |
+
i = 0
|
| 1365 |
+
while i < len(lines):
|
| 1366 |
+
line = lines[i].strip()
|
| 1367 |
+
if "**" in line and "°" in line: # Ligne de jour
|
| 1368 |
+
day_part = line.split("**")[1].split(":")[0]
|
| 1369 |
+
temps_part = line.split(": ", 1)[1] if ": " in line else "--"
|
| 1370 |
+
|
| 1371 |
+
# Récupérer la météo sur la ligne suivante
|
| 1372 |
+
weather_desc = "--"
|
| 1373 |
+
if i + 1 < len(lines) and lines[i + 1].strip():
|
| 1374 |
+
weather_desc = lines[i + 1].strip()
|
| 1375 |
+
|
| 1376 |
+
daily_items.append({
|
| 1377 |
+
"day": day_part,
|
| 1378 |
+
"temps": temps_part,
|
| 1379 |
+
"weather": weather_desc
|
| 1380 |
+
})
|
| 1381 |
+
i += 1
|
| 1382 |
+
|
| 1383 |
+
# Générer le HTML
|
| 1384 |
+
items_html = ""
|
| 1385 |
+
for item in daily_items[:7]: # Prendre les 7 premiers jours
|
| 1386 |
+
emoji = get_weather_emoji(item["weather"])
|
| 1387 |
+
items_html += f"""
|
| 1388 |
+
<div class="daily-item">
|
| 1389 |
+
<div style="font-weight: bold; min-width: 55px; font-size: 0.9em;">{item["day"]}</div>
|
| 1390 |
+
<div style="font-size: 1.3em; margin: 0 8px;">{emoji}</div>
|
| 1391 |
+
<div style="font-weight: bold; text-align: right; font-size: 0.9em;">{item["temps"]}</div>
|
| 1392 |
+
</div>
|
| 1393 |
+
"""
|
| 1394 |
+
|
| 1395 |
+
html = f"""
|
| 1396 |
+
<div class="daily-card">
|
| 1397 |
+
<div style="font-size: 1.3em; margin-bottom: 12px;"><strong>📅 Prochains jours</strong></div>
|
| 1398 |
+
{items_html}
|
| 1399 |
+
</div>
|
| 1400 |
+
"""
|
| 1401 |
+
return html
|
| 1402 |
+
|
| 1403 |
+
# Fonctions HTML pour les cartes visuelles (conservées)
|
| 1404 |
+
def get_weather_forecast_html(address: str):
|
| 1405 |
+
"""
|
| 1406 |
+
Version HTML des prévisions météo avec cartes visuelles.
|
| 1407 |
+
|
| 1408 |
+
Args:
|
| 1409 |
+
address (str): Adresse à géolocaliser (ex: "Paris, France")
|
| 1410 |
+
|
| 1411 |
+
Returns:
|
| 1412 |
+
tuple: (alerts_html, current_html, hourly_html, daily_html)
|
| 1413 |
+
- alerts_html: Carte HTML des alertes météo
|
| 1414 |
+
- current_html: Carte HTML des conditions actuelles
|
| 1415 |
+
- hourly_html: Carte HTML des prévisions horaires
|
| 1416 |
+
- daily_html: Carte HTML des prévisions sur 10 jours
|
| 1417 |
+
"""
|
| 1418 |
+
# Récupérer les données météo formatées
|
| 1419 |
+
alerts_info, current_conditions, hourly_forecast, daily_forecast = get_weather_forecast(address)
|
| 1420 |
+
|
| 1421 |
+
# Convertir en cartes HTML stylisées
|
| 1422 |
+
alerts_html = format_alerts_card_html(alerts_info)
|
| 1423 |
+
current_html = format_current_card_html(current_conditions)
|
| 1424 |
+
hourly_html = format_hourly_card_html(hourly_forecast)
|
| 1425 |
+
daily_html = format_daily_card_html(daily_forecast)
|
| 1426 |
+
|
| 1427 |
+
return alerts_html, current_html, hourly_html, daily_html
|
| 1428 |
+
|
| 1429 |
+
# Interface Gradio pour MCP avec cartes visuelles
|
| 1430 |
+
def create_mcp_interface():
|
| 1431 |
+
"""
|
| 1432 |
+
Crée l'interface Gradio pour le serveur MCP avec cartes visuelles et fonctions individuelles.
|
| 1433 |
+
|
| 1434 |
+
Combine la belle interface avec cartes HTML et les fonctions MCP séparées :
|
| 1435 |
+
- Interface web: Cartes visuelles avec dégradés et boussoles
|
| 1436 |
+
- Fonctions MCP: 5 outils texte individuels pour Claude Desktop
|
| 1437 |
+
|
| 1438 |
+
Returns:
|
| 1439 |
+
gr.Blocks: Interface Gradio hybride optimisée pour MCP et web
|
| 1440 |
+
"""
|
| 1441 |
+
# CSS personnalisé pour le style cartes météo
|
| 1442 |
+
custom_css = """
|
| 1443 |
+
.gradio-container {
|
| 1444 |
+
max-width: 900px !important;
|
| 1445 |
+
margin: 0 auto !important;
|
| 1446 |
+
}
|
| 1447 |
+
|
| 1448 |
+
.weather-card {
|
| 1449 |
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
| 1450 |
+
border-radius: 20px;
|
| 1451 |
+
padding: 20px;
|
| 1452 |
+
margin: 8px 0;
|
| 1453 |
+
color: white;
|
| 1454 |
+
box-shadow: 0 8px 32px rgba(0,0,0,0.1);
|
| 1455 |
+
backdrop-filter: blur(10px);
|
| 1456 |
+
width: 100%;
|
| 1457 |
+
}
|
| 1458 |
+
|
| 1459 |
+
.alert-card-red {
|
| 1460 |
+
background: linear-gradient(135deg, #ff416c 0%, #ff4757 100%);
|
| 1461 |
+
border-radius: 20px;
|
| 1462 |
+
padding: 20px;
|
| 1463 |
+
margin: 8px 0;
|
| 1464 |
+
color: white;
|
| 1465 |
+
box-shadow: 0 8px 32px rgba(255,65,108,0.3);
|
| 1466 |
+
border: 2px solid #ff4757;
|
| 1467 |
+
width: 100%;
|
| 1468 |
+
}
|
| 1469 |
+
|
| 1470 |
+
.alert-card-orange {
|
| 1471 |
+
background: linear-gradient(135deg, #f39c12 0%, #e67e22 100%);
|
| 1472 |
+
border-radius: 20px;
|
| 1473 |
+
padding: 20px;
|
| 1474 |
+
margin: 8px 0;
|
| 1475 |
+
color: white;
|
| 1476 |
+
box-shadow: 0 8px 32px rgba(243,156,18,0.3);
|
| 1477 |
+
width: 100%;
|
| 1478 |
+
}
|
| 1479 |
+
|
| 1480 |
+
.alert-card-yellow {
|
| 1481 |
+
background: linear-gradient(135deg, #f1c40f 0%, #f39c12 100%);
|
| 1482 |
+
border-radius: 20px;
|
| 1483 |
+
padding: 20px;
|
| 1484 |
+
margin: 8px 0;
|
| 1485 |
+
color: white;
|
| 1486 |
+
box-shadow: 0 8px 32px rgba(241,196,15,0.3);
|
| 1487 |
+
width: 100%;
|
| 1488 |
+
}
|
| 1489 |
+
|
| 1490 |
+
.current-card {
|
| 1491 |
+
background: linear-gradient(135deg, #2c3e50 0%, #3498db 100%);
|
| 1492 |
+
border-radius: 20px;
|
| 1493 |
+
padding: 20px;
|
| 1494 |
+
margin: 8px 0;
|
| 1495 |
+
color: white;
|
| 1496 |
+
box-shadow: 0 8px 32px rgba(52,152,219,0.2);
|
| 1497 |
+
width: 100%;
|
| 1498 |
+
}
|
| 1499 |
+
|
| 1500 |
+
.hourly-card {
|
| 1501 |
+
background: linear-gradient(135deg, #8e44ad 0%, #3498db 100%);
|
| 1502 |
+
border-radius: 20px;
|
| 1503 |
+
padding: 20px;
|
| 1504 |
+
margin: 8px 0;
|
| 1505 |
+
color: white;
|
| 1506 |
+
box-shadow: 0 8px 32px rgba(142,68,173,0.2);
|
| 1507 |
+
width: 100%;
|
| 1508 |
+
}
|
| 1509 |
+
|
| 1510 |
+
.daily-card {
|
| 1511 |
+
background: linear-gradient(135deg, #27ae60 0%, #2ecc71 100%);
|
| 1512 |
+
border-radius: 20px;
|
| 1513 |
+
padding: 20px;
|
| 1514 |
+
margin: 8px 0;
|
| 1515 |
+
color: white;
|
| 1516 |
+
box-shadow: 0 8px 32px rgba(46,204,113,0.2);
|
| 1517 |
+
width: 100%;
|
| 1518 |
+
}
|
| 1519 |
+
|
| 1520 |
+
.temp-large {
|
| 1521 |
+
font-size: 2.5em;
|
| 1522 |
+
font-weight: bold;
|
| 1523 |
+
margin: 5px 0;
|
| 1524 |
+
}
|
| 1525 |
+
|
| 1526 |
+
.hourly-item {
|
| 1527 |
+
display: flex;
|
| 1528 |
+
justify-content: space-between;
|
| 1529 |
+
align-items: center;
|
| 1530 |
+
padding: 8px 12px;
|
| 1531 |
+
margin: 4px 0;
|
| 1532 |
+
background: rgba(255,255,255,0.1);
|
| 1533 |
+
border-radius: 12px;
|
| 1534 |
+
font-size: 0.9em;
|
| 1535 |
+
}
|
| 1536 |
+
|
| 1537 |
+
.daily-item {
|
| 1538 |
+
display: flex;
|
| 1539 |
+
justify-content: space-between;
|
| 1540 |
+
align-items: center;
|
| 1541 |
+
padding: 10px 12px;
|
| 1542 |
+
margin: 4px 0;
|
| 1543 |
+
background: rgba(255,255,255,0.1);
|
| 1544 |
+
border-radius: 12px;
|
| 1545 |
+
}
|
| 1546 |
+
"""
|
| 1547 |
+
|
| 1548 |
+
with gr.Blocks(
|
| 1549 |
+
title="🌦️ Météo France - Serveur MCP",
|
| 1550 |
+
theme=gr.themes.Soft(),
|
| 1551 |
+
css=custom_css
|
| 1552 |
+
) as interface:
|
| 1553 |
+
|
| 1554 |
+
gr.Markdown("# 🌦️ Météo France")
|
| 1555 |
+
gr.Markdown("""
|
| 1556 |
+
Prévisions météorologiques détaillées avec alertes
|
| 1557 |
+
|
| 1558 |
+
**5 fonctions MCP disponibles :**
|
| 1559 |
+
`get_weather_alerts` | `get_current_weather` | `get_hourly_forecast` | `get_daily_forecast` | `get_complete_weather_forecast`
|
| 1560 |
+
""")
|
| 1561 |
+
|
| 1562 |
+
# Interface principale avec cartes visuelles
|
| 1563 |
+
with gr.Row():
|
| 1564 |
+
address_input = gr.Textbox(
|
| 1565 |
+
label="📍 Adresse",
|
| 1566 |
+
placeholder="Entrez une adresse (ex: Paris, France)",
|
| 1567 |
+
scale=3
|
| 1568 |
+
)
|
| 1569 |
+
submit_btn = gr.Button("🔄 Actualiser", variant="primary", scale=1)
|
| 1570 |
+
|
| 1571 |
+
# Alertes en premier (priorité haute)
|
| 1572 |
+
with gr.Row():
|
| 1573 |
+
alerts_output = gr.HTML(label="🚨 Alertes Météo")
|
| 1574 |
+
|
| 1575 |
+
# Conditions actuelles
|
| 1576 |
+
with gr.Row():
|
| 1577 |
+
current_output = gr.HTML(label="🌡️ Maintenant")
|
| 1578 |
+
|
| 1579 |
+
# Prévisions heure par heure
|
| 1580 |
+
with gr.Row():
|
| 1581 |
+
hourly_output = gr.HTML(label="🕐 Heure par Heure")
|
| 1582 |
+
|
| 1583 |
+
# Prévisions à 10 jours
|
| 1584 |
+
with gr.Row():
|
| 1585 |
+
daily_output = gr.HTML(label="📅 10 Jours")
|
| 1586 |
+
|
| 1587 |
+
# Connexions interface web (cartes HTML)
|
| 1588 |
+
submit_btn.click(
|
| 1589 |
+
fn=get_weather_forecast_html,
|
| 1590 |
+
inputs=[address_input],
|
| 1591 |
+
outputs=[alerts_output, current_output, hourly_output, daily_output]
|
| 1592 |
+
)
|
| 1593 |
+
|
| 1594 |
+
address_input.submit(
|
| 1595 |
+
fn=get_weather_forecast_html,
|
| 1596 |
+
inputs=[address_input],
|
| 1597 |
+
outputs=[alerts_output, current_output, hourly_output, daily_output]
|
| 1598 |
+
)
|
| 1599 |
+
|
| 1600 |
+
# Boutons invisibles pour exposer les fonctions MCP
|
| 1601 |
+
# Ces boutons créent les endpoints API nécessaires pour MCP
|
| 1602 |
+
with gr.Row(visible=False):
|
| 1603 |
+
mcp_input = gr.Textbox()
|
| 1604 |
+
mcp_output = gr.Textbox()
|
| 1605 |
+
|
| 1606 |
+
alerts_mcp_btn = gr.Button("MCP Alerts")
|
| 1607 |
+
current_mcp_btn = gr.Button("MCP Current")
|
| 1608 |
+
hourly_mcp_btn = gr.Button("MCP Hourly")
|
| 1609 |
+
daily_mcp_btn = gr.Button("MCP Daily")
|
| 1610 |
+
complete_mcp_btn = gr.Button("MCP Complete")
|
| 1611 |
+
|
| 1612 |
+
# Connexions MCP (invisibles mais exposées via API)
|
| 1613 |
+
alerts_mcp_btn.click(
|
| 1614 |
+
fn=get_weather_alerts,
|
| 1615 |
+
inputs=mcp_input,
|
| 1616 |
+
outputs=mcp_output,
|
| 1617 |
+
show_api=True,
|
| 1618 |
+
api_name="get_weather_alerts"
|
| 1619 |
+
)
|
| 1620 |
+
|
| 1621 |
+
current_mcp_btn.click(
|
| 1622 |
+
fn=get_current_weather,
|
| 1623 |
+
inputs=mcp_input,
|
| 1624 |
+
outputs=mcp_output,
|
| 1625 |
+
show_api=True,
|
| 1626 |
+
api_name="get_current_weather"
|
| 1627 |
+
)
|
| 1628 |
+
|
| 1629 |
+
hourly_mcp_btn.click(
|
| 1630 |
+
fn=get_hourly_forecast,
|
| 1631 |
+
inputs=mcp_input,
|
| 1632 |
+
outputs=mcp_output,
|
| 1633 |
+
show_api=True,
|
| 1634 |
+
api_name="get_hourly_forecast"
|
| 1635 |
+
)
|
| 1636 |
+
|
| 1637 |
+
daily_mcp_btn.click(
|
| 1638 |
+
fn=get_daily_forecast,
|
| 1639 |
+
inputs=mcp_input,
|
| 1640 |
+
outputs=mcp_output,
|
| 1641 |
+
show_api=True,
|
| 1642 |
+
api_name="get_daily_forecast"
|
| 1643 |
+
)
|
| 1644 |
+
|
| 1645 |
+
complete_mcp_btn.click(
|
| 1646 |
+
fn=get_complete_weather_forecast,
|
| 1647 |
+
inputs=mcp_input,
|
| 1648 |
+
outputs=mcp_output,
|
| 1649 |
+
show_api=True,
|
| 1650 |
+
api_name="get_complete_weather_forecast"
|
| 1651 |
+
)
|
| 1652 |
+
|
| 1653 |
+
# Crédit Météo-France obligatoire
|
| 1654 |
+
with gr.Row():
|
| 1655 |
+
gr.HTML("""
|
| 1656 |
+
<div style="text-align: center; margin-top: 20px; padding: 15px;
|
| 1657 |
+
background: rgba(0,0,0,0.05); border-radius: 10px;
|
| 1658 |
+
font-size: 0.9em; color: #666;">
|
| 1659 |
+
📡 <strong>Données météorologiques</strong> fournies par
|
| 1660 |
+
<a href="https://meteofrance.fr" target="_blank" style="color: #0066cc; text-decoration: none;">
|
| 1661 |
+
<strong>Météo-France</strong>
|
| 1662 |
+
</a> |
|
| 1663 |
+
🗺️ <strong>Géolocalisation</strong> par
|
| 1664 |
+
<a href="https://geoservices.ign.fr" target="_blank" style="color: #0066cc; text-decoration: none;">
|
| 1665 |
+
<strong>IGN Géoplateforme</strong>
|
| 1666 |
+
</a>
|
| 1667 |
+
</div>
|
| 1668 |
+
""")
|
| 1669 |
+
|
| 1670 |
+
return interface
|
| 1671 |
+
|
| 1672 |
+
if __name__ == "__main__":
|
| 1673 |
+
interface = create_mcp_interface()
|
| 1674 |
+
interface.launch(mcp_server=True)
|