lovarch-cli 0.2.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (122) hide show
  1. lovarch_cli/__init__.py +16 -0
  2. lovarch_cli/__main__.py +10 -0
  3. lovarch_cli/ai/__init__.py +21 -0
  4. lovarch_cli/ai/gateway.py +240 -0
  5. lovarch_cli/api.py +111 -0
  6. lovarch_cli/auth/__init__.py +32 -0
  7. lovarch_cli/auth/keyring_store.py +214 -0
  8. lovarch_cli/auth/local_server.py +165 -0
  9. lovarch_cli/auth/pkce.py +57 -0
  10. lovarch_cli/auth/session.py +189 -0
  11. lovarch_cli/cli.py +262 -0
  12. lovarch_cli/clients/__init__.py +33 -0
  13. lovarch_cli/clients/factory.py +54 -0
  14. lovarch_cli/clients/local_client.py +432 -0
  15. lovarch_cli/clients/lovarch_storage.py +174 -0
  16. lovarch_cli/clients/lovarch_supabase.py +295 -0
  17. lovarch_cli/clients/persistence.py +166 -0
  18. lovarch_cli/clients/storage.py +66 -0
  19. lovarch_cli/commands/__init__.py +10 -0
  20. lovarch_cli/commands/account.py +172 -0
  21. lovarch_cli/commands/audit.py +394 -0
  22. lovarch_cli/commands/config_cmd.py +80 -0
  23. lovarch_cli/commands/consolidate.py +217 -0
  24. lovarch_cli/commands/context_cmd.py +73 -0
  25. lovarch_cli/commands/dev.py +287 -0
  26. lovarch_cli/commands/do_cmd.py +120 -0
  27. lovarch_cli/commands/init.py +218 -0
  28. lovarch_cli/commands/jobs_cmd.py +95 -0
  29. lovarch_cli/commands/login.py +202 -0
  30. lovarch_cli/commands/mcp_cmd.py +26 -0
  31. lovarch_cli/commands/run.py +375 -0
  32. lovarch_cli/commands/signup.py +185 -0
  33. lovarch_cli/commands/status.py +243 -0
  34. lovarch_cli/commands/upgrade.py +108 -0
  35. lovarch_cli/commands/verifica_cmd.py +174 -0
  36. lovarch_cli/config.py +101 -0
  37. lovarch_cli/config_store.py +111 -0
  38. lovarch_cli/credits/__init__.py +35 -0
  39. lovarch_cli/credits/base.py +84 -0
  40. lovarch_cli/credits/factory.py +36 -0
  41. lovarch_cli/credits/local.py +34 -0
  42. lovarch_cli/credits/lovarch.py +56 -0
  43. lovarch_cli/i18n/__init__.py +27 -0
  44. lovarch_cli/i18n/loader.py +121 -0
  45. lovarch_cli/i18n/translations/en.json +168 -0
  46. lovarch_cli/i18n/translations/es.json +168 -0
  47. lovarch_cli/i18n/translations/it.json +168 -0
  48. lovarch_cli/i18n/translations/pt.json +168 -0
  49. lovarch_cli/mcp/__init__.py +9 -0
  50. lovarch_cli/mcp/server.py +199 -0
  51. lovarch_cli/mcp/tools.py +372 -0
  52. lovarch_cli/sample_downloader.py +255 -0
  53. lovarch_cli/squad/README.md +206 -0
  54. lovarch_cli/squad/agents/auditor-input.md +353 -0
  55. lovarch_cli/squad/agents/bim-engineer.md +404 -0
  56. lovarch_cli/squad/agents/briefing-architect.md +249 -0
  57. lovarch_cli/squad/agents/cad-engineer.md +278 -0
  58. lovarch_cli/squad/agents/capitolato-writer.md +256 -0
  59. lovarch_cli/squad/agents/computo-engineer.md +258 -0
  60. lovarch_cli/squad/agents/concept-designer.md +399 -0
  61. lovarch_cli/squad/agents/contratto-architect.md +243 -0
  62. lovarch_cli/squad/agents/deliverable-builder.md +253 -0
  63. lovarch_cli/squad/agents/energy-prelim.md +388 -0
  64. lovarch_cli/squad/agents/pratiche-it.md +251 -0
  65. lovarch_cli/squad/agents/progetto-chief.md +768 -0
  66. lovarch_cli/squad/agents/quality-dati.md +409 -0
  67. lovarch_cli/squad/agents/quality-misure.md +418 -0
  68. lovarch_cli/squad/agents/quality-normativa.md +417 -0
  69. lovarch_cli/squad/agents/quality-output.md +436 -0
  70. lovarch_cli/squad/agents/regolatorio-it.md +278 -0
  71. lovarch_cli/squad/checklists/handoff-quality-gate.md +232 -0
  72. lovarch_cli/squad/checklists/quality-dati-checklist.md +134 -0
  73. lovarch_cli/squad/checklists/quality-misure-checklist.md +139 -0
  74. lovarch_cli/squad/checklists/quality-normativa-checklist.md +121 -0
  75. lovarch_cli/squad/checklists/quality-output-checklist.md +116 -0
  76. lovarch_cli/squad/config.yaml +408 -0
  77. lovarch_cli/squad/data/CHANGELOG.md +272 -0
  78. lovarch_cli/squad/data/agents-prd.md +428 -0
  79. lovarch_cli/squad/data/architettura-progetto-rules.md +328 -0
  80. lovarch_cli/squad/data/handoff-card-template.md +231 -0
  81. lovarch_cli/squad/data/mocks/catasto-visura.json +72 -0
  82. lovarch_cli/squad/data/mocks/firma-envelope.json +43 -0
  83. lovarch_cli/squad/data/prezzario-lombardia-sample.json +312 -0
  84. lovarch_cli/squad/scripts/api_clients.py +206 -0
  85. lovarch_cli/squad/scripts/architect_profile.py +276 -0
  86. lovarch_cli/squad/scripts/deliverable_generators.py +844 -0
  87. lovarch_cli/squad/scripts/generate_attico_brera_dwg.py +369 -0
  88. lovarch_cli/squad/scripts/generate_chianti_dxf.py +368 -0
  89. lovarch_cli/squad/scripts/generate_chianti_images.py +223 -0
  90. lovarch_cli/squad/scripts/generate_real_sample_images.py +189 -0
  91. lovarch_cli/squad/scripts/generate_sample_assets.py +382 -0
  92. lovarch_cli/squad/scripts/lovarch_client.py +1046 -0
  93. lovarch_cli/squad/scripts/pipeline_runner.py +2095 -0
  94. lovarch_cli/squad/scripts/render_dxf_to_png.py +57 -0
  95. lovarch_cli/squad/scripts/run_palestra_demo.sh +277 -0
  96. lovarch_cli/squad/scripts/simulate_squad_execution.py +515 -0
  97. lovarch_cli/squad/scripts/validate-squad.py +383 -0
  98. lovarch_cli/squad/tasks/audit-input.md +146 -0
  99. lovarch_cli/squad/tasks/compute-metric.md +105 -0
  100. lovarch_cli/squad/tasks/consolidate-dossier.md +187 -0
  101. lovarch_cli/squad/tasks/generate-cad-plan.md +120 -0
  102. lovarch_cli/squad/tasks/generate-ifc-model.md +108 -0
  103. lovarch_cli/squad/tasks/write-capitolato.md +100 -0
  104. lovarch_cli/squad/templates/asseverazione-tecnica.md +126 -0
  105. lovarch_cli/squad/templates/capitolato-uni-11337.md +235 -0
  106. lovarch_cli/squad/templates/cila-comune-milano.md +177 -0
  107. lovarch_cli/squad/templates/contratto-cnappc.md +220 -0
  108. lovarch_cli/squad/workflows/dal-brief-al-cantiere.yaml +218 -0
  109. lovarch_cli/squad_loader.py +114 -0
  110. lovarch_cli/verify/__init__.py +15 -0
  111. lovarch_cli/verify/contratto.py +110 -0
  112. lovarch_cli/verify/dossier.py +97 -0
  113. lovarch_cli/verify/misure.py +83 -0
  114. lovarch_cli/verify/normativa.py +178 -0
  115. lovarch_cli/version.py +13 -0
  116. lovarch_cli/workflows/__init__.py +9 -0
  117. lovarch_cli/workflows/platform.py +212 -0
  118. lovarch_cli-0.2.1.dist-info/METADATA +232 -0
  119. lovarch_cli-0.2.1.dist-info/RECORD +122 -0
  120. lovarch_cli-0.2.1.dist-info/WHEEL +4 -0
  121. lovarch_cli-0.2.1.dist-info/entry_points.txt +3 -0
  122. lovarch_cli-0.2.1.dist-info/licenses/LICENSE +38 -0
@@ -0,0 +1,312 @@
1
+ {
2
+ "meta": {
3
+ "source": "Prezzario Regione Lombardia 2025 (sample subset)",
4
+ "valid_until": "2026-06-30",
5
+ "currency": "EUR",
6
+ "iva_default": 0.10,
7
+ "url_official": "https://prezzario.regione.lombardia.it/",
8
+ "version": "LOM252",
9
+ "note": "Subset di ~50 voci principali per ristrutturazione residenziale. In produzione: parsing completo PDF/XLS ufficiale."
10
+ },
11
+ "voci": [
12
+ {
13
+ "codice": "1.A.01.01.001",
14
+ "categoria": "Demolizioni",
15
+ "descrizione": "Demolizione di pareti divisorie in laterizio forato spessore 8-12 cm",
16
+ "unita": "m²",
17
+ "prezzo": 18.50,
18
+ "note": "Inclusa cernita rifiuti, escluso trasporto in discarica"
19
+ },
20
+ {
21
+ "codice": "1.A.01.01.002",
22
+ "categoria": "Demolizioni",
23
+ "descrizione": "Rimozione pavimentazione in piastrelle ceramica/gres fino a 50 m²",
24
+ "unita": "m²",
25
+ "prezzo": 12.30,
26
+ "note": "Esclusa rimozione massetto"
27
+ },
28
+ {
29
+ "codice": "1.A.01.02.001",
30
+ "categoria": "Demolizioni",
31
+ "descrizione": "Trasporto a discarica autorizzata di rifiuti inerti edilizi",
32
+ "unita": "m³",
33
+ "prezzo": 24.00,
34
+ "note": "Escluso costo conferimento (FIR), CAM 2025: tracciabilità obbligatoria"
35
+ },
36
+ {
37
+ "codice": "2.A.01.01.001",
38
+ "categoria": "Murature",
39
+ "descrizione": "Tramezza in laterizio forato cm 8, malta cementizia",
40
+ "unita": "m²",
41
+ "prezzo": 38.50,
42
+ "note": "Conforme CAM 2025, riciclato ≥30%"
43
+ },
44
+ {
45
+ "codice": "2.A.01.01.002",
46
+ "categoria": "Murature",
47
+ "descrizione": "Tramezza in laterizio forato cm 12 (divisori bagno)",
48
+ "unita": "m²",
49
+ "prezzo": 45.20,
50
+ "note": "Insonorizzazione migliorata, conforme CAM 2025"
51
+ },
52
+ {
53
+ "codice": "2.A.02.01.001",
54
+ "categoria": "Intonaci",
55
+ "descrizione": "Intonaco a calce naturale, sp. 1.5 cm su tramezze interne",
56
+ "unita": "m²",
57
+ "prezzo": 22.50,
58
+ "note": "Calce idraulica naturale, no chimici"
59
+ },
60
+ {
61
+ "codice": "3.A.01.01.001",
62
+ "categoria": "Pavimenti",
63
+ "descrizione": "Massetto cementizio per riscaldamento a pavimento, sp. 8 cm",
64
+ "unita": "m²",
65
+ "prezzo": 28.00,
66
+ "note": "Incluso additivo, escluso panello radiante"
67
+ },
68
+ {
69
+ "codice": "3.A.02.01.001",
70
+ "categoria": "Pavimenti",
71
+ "descrizione": "Parquet rovere prefinito spazzolato sp. 14/3.5 mm",
72
+ "unita": "m²",
73
+ "prezzo": 95.00,
74
+ "note": "Posa flottante, esclusa preparazione sottofondo"
75
+ },
76
+ {
77
+ "codice": "3.A.02.01.002",
78
+ "categoria": "Pavimenti",
79
+ "descrizione": "Parquet rovere massello sp. 22 mm",
80
+ "unita": "m²",
81
+ "prezzo": 145.00,
82
+ "note": "Posa incollata, finitura olio"
83
+ },
84
+ {
85
+ "codice": "3.A.03.01.001",
86
+ "categoria": "Pavimenti",
87
+ "descrizione": "Gres porcellanato rettificato 60×120 effetto travertino",
88
+ "unita": "m²",
89
+ "prezzo": 78.00,
90
+ "note": "Posa rettificata fuga 2 mm, materiale di fascia media"
91
+ },
92
+ {
93
+ "codice": "3.A.03.01.002",
94
+ "categoria": "Pavimenti",
95
+ "descrizione": "Gres porcellanato 60×60 standard",
96
+ "unita": "m²",
97
+ "prezzo": 42.00,
98
+ "note": "Posa standard fuga 4 mm"
99
+ },
100
+ {
101
+ "codice": "4.A.01.01.001",
102
+ "categoria": "Rivestimenti bagno",
103
+ "descrizione": "Rivestimento bagno gres rettificato 60×120 effetto travertino",
104
+ "unita": "m²",
105
+ "prezzo": 88.00,
106
+ "note": "Inclusa fuga epossidica colore"
107
+ },
108
+ {
109
+ "codice": "4.A.02.01.001",
110
+ "categoria": "Rivestimenti cucina",
111
+ "descrizione": "Rivestimento cucina gres 30×60",
112
+ "unita": "m²",
113
+ "prezzo": 52.00,
114
+ "note": "Posa standard"
115
+ },
116
+ {
117
+ "codice": "5.A.01.01.001",
118
+ "categoria": "Impianto elettrico",
119
+ "descrizione": "Punto luce comando interrotto cavo LSZH",
120
+ "unita": "cad",
121
+ "prezzo": 38.00,
122
+ "note": "Cavo in tubo flessibile o passacavo, scatola 503"
123
+ },
124
+ {
125
+ "codice": "5.A.01.01.002",
126
+ "categoria": "Impianto elettrico",
127
+ "descrizione": "Presa shuko 16A + USB-C",
128
+ "unita": "cad",
129
+ "prezzo": 52.00,
130
+ "note": "Marca civile media"
131
+ },
132
+ {
133
+ "codice": "5.A.02.01.001",
134
+ "categoria": "Impianto elettrico",
135
+ "descrizione": "Quadro generale appartamento 24 moduli",
136
+ "unita": "cad",
137
+ "prezzo": 480.00,
138
+ "note": "Inclusi differenziali e magnetotermici"
139
+ },
140
+ {
141
+ "codice": "5.A.03.01.001",
142
+ "categoria": "Domotica",
143
+ "descrizione": "Sistema domotica leggera 1 zona (KNX o equivalente)",
144
+ "unita": "zona",
145
+ "prezzo": 850.00,
146
+ "note": "Per stanza/area, escluso server centrale"
147
+ },
148
+ {
149
+ "codice": "5.A.03.01.002",
150
+ "categoria": "Domotica",
151
+ "descrizione": "Server centrale domotica + tablet di controllo",
152
+ "unita": "cad",
153
+ "prezzo": 1850.00,
154
+ "note": "Una tantum, configurazione inclusa"
155
+ },
156
+ {
157
+ "codice": "6.A.01.01.001",
158
+ "categoria": "Impianto idraulico",
159
+ "descrizione": "Punto acqua calda+fredda + scarico WC",
160
+ "unita": "cad",
161
+ "prezzo": 145.00,
162
+ "note": "Incluso PEX, esclusa rubinetteria finita"
163
+ },
164
+ {
165
+ "codice": "6.A.01.01.002",
166
+ "categoria": "Impianto idraulico",
167
+ "descrizione": "Punto acqua bagno (lavabo/doccia/vasca)",
168
+ "unita": "cad",
169
+ "prezzo": 165.00,
170
+ "note": "Incluso scarico, esclusa rubinetteria"
171
+ },
172
+ {
173
+ "codice": "6.A.02.01.001",
174
+ "categoria": "Impianto idraulico",
175
+ "descrizione": "Sanitario sospeso WC con cassetta incasso",
176
+ "unita": "cad",
177
+ "prezzo": 285.00,
178
+ "note": "Marca media (Ideal Standard, Roca, equivalenti)"
179
+ },
180
+ {
181
+ "codice": "7.A.01.01.001",
182
+ "categoria": "Riscaldamento",
183
+ "descrizione": "Pannello radiante a pavimento PEX-A passo 10 cm",
184
+ "unita": "m²",
185
+ "prezzo": 68.00,
186
+ "note": "Incluso isolante e collettore di zona"
187
+ },
188
+ {
189
+ "codice": "7.A.01.01.002",
190
+ "categoria": "Riscaldamento",
191
+ "descrizione": "Collettore zona riscaldamento (4-6 zone)",
192
+ "unita": "cad",
193
+ "prezzo": 280.00,
194
+ "note": "Termoregolazione singola zona"
195
+ },
196
+ {
197
+ "codice": "7.A.02.01.001",
198
+ "categoria": "VMC",
199
+ "descrizione": "Sistema VMC centralizzato con recupero ≥75%",
200
+ "unita": "appartamento",
201
+ "prezzo": 4800.00,
202
+ "note": "120 m², filtri F7+ePM1, classe A+"
203
+ },
204
+ {
205
+ "codice": "8.A.01.01.001",
206
+ "categoria": "Serramenti interni",
207
+ "descrizione": "Porta interna in legno tamburato + telaio",
208
+ "unita": "cad",
209
+ "prezzo": 380.00,
210
+ "note": "Standard, finitura laccato bianco"
211
+ },
212
+ {
213
+ "codice": "8.A.01.01.002",
214
+ "categoria": "Serramenti interni",
215
+ "descrizione": "Porta scorrevole interna a scomparsa con cassonetto",
216
+ "unita": "cad",
217
+ "prezzo": 920.00,
218
+ "note": "Acustica, ideale studio"
219
+ },
220
+ {
221
+ "codice": "9.A.01.01.001",
222
+ "categoria": "Falegnameria su misura",
223
+ "descrizione": "Cucina su misura cm 360×60 con isola, ante laccate",
224
+ "unita": "modulo",
225
+ "prezzo": 14500.00,
226
+ "note": "Top in pietra naturale escluso"
227
+ },
228
+ {
229
+ "codice": "9.A.01.01.002",
230
+ "categoria": "Falegnameria su misura",
231
+ "descrizione": "Top cucina in granito sp. 3 cm con bordo dritto",
232
+ "unita": "m²",
233
+ "prezzo": 380.00,
234
+ "note": "Materiale di fascia media"
235
+ },
236
+ {
237
+ "codice": "9.A.02.01.001",
238
+ "categoria": "Falegnameria su misura",
239
+ "descrizione": "Cabina armadio su misura camera padronale",
240
+ "unita": "m²",
241
+ "prezzo": 850.00,
242
+ "note": "Pareti modulari, ante battenti, organizzazione interna"
243
+ },
244
+ {
245
+ "codice": "9.A.03.01.001",
246
+ "categoria": "Falegnameria su misura",
247
+ "descrizione": "Libreria a parete su misura sp. 35 cm",
248
+ "unita": "m²",
249
+ "prezzo": 720.00,
250
+ "note": "Rovere massello, montanti verticali fissi"
251
+ },
252
+ {
253
+ "codice": "10.A.01.01.001",
254
+ "categoria": "Tinteggiature",
255
+ "descrizione": "Tinteggiatura a calce naturale ambienti interni",
256
+ "unita": "m²",
257
+ "prezzo": 12.50,
258
+ "note": "Due mani, calce naturale, no chimici"
259
+ },
260
+ {
261
+ "codice": "10.A.01.01.002",
262
+ "categoria": "Tinteggiatura",
263
+ "descrizione": "Pittura lavabile classe E1 anti-formaldeide",
264
+ "unita": "m²",
265
+ "prezzo": 9.80,
266
+ "note": "Camera Sofia (richiesta esplicita)"
267
+ },
268
+ {
269
+ "codice": "11.A.01.01.001",
270
+ "categoria": "Restauro decori",
271
+ "descrizione": "Restauro decoro soffitto, pulizia + integrazione",
272
+ "unita": "m²",
273
+ "prezzo": 240.00,
274
+ "note": "Restauro conservativo, materiali tradizionali"
275
+ },
276
+ {
277
+ "codice": "11.A.02.01.001",
278
+ "categoria": "Restauro pavimenti",
279
+ "descrizione": "Restauro seminato veneziano esistente",
280
+ "unita": "m²",
281
+ "prezzo": 320.00,
282
+ "note": "Lucidatura, integrazione fessurazioni minori"
283
+ },
284
+ {
285
+ "codice": "12.A.01.01.001",
286
+ "categoria": "Pulizia finale",
287
+ "descrizione": "Pulizia post-cantiere completo appartamento",
288
+ "unita": "m²",
289
+ "prezzo": 8.50,
290
+ "note": "Inclusa rimozione protezioni e residui"
291
+ }
292
+ ],
293
+ "categorie_riepilogative": [
294
+ "Demolizioni",
295
+ "Murature",
296
+ "Intonaci",
297
+ "Pavimenti",
298
+ "Rivestimenti bagno",
299
+ "Rivestimenti cucina",
300
+ "Impianto elettrico",
301
+ "Domotica",
302
+ "Impianto idraulico",
303
+ "Riscaldamento",
304
+ "VMC",
305
+ "Serramenti interni",
306
+ "Falegnameria su misura",
307
+ "Tinteggiature",
308
+ "Restauro decori",
309
+ "Restauro pavimenti",
310
+ "Pulizia finale"
311
+ ]
312
+ }
@@ -0,0 +1,206 @@
1
+ """
2
+ API Clients for squad architettura-progetto.
3
+
4
+ Centralizes all external API calls used by the agents.
5
+ Reads secrets from ~/.lovarch/secrets.env (NOT committed to git).
6
+
7
+ Usage:
8
+ from squads.architettura_progetto.scripts.api_clients import (
9
+ mapbox_geocode,
10
+ catasto_visura,
11
+ firma_qes_create_envelope,
12
+ )
13
+
14
+ result = mapbox_geocode("Via Fiori Chiari 17, Milano")
15
+ print(result["lat"], result["lon"], result["comune"])
16
+ """
17
+ import json
18
+ import os
19
+ from pathlib import Path
20
+ from typing import Any, Dict, Optional
21
+ from urllib.parse import quote
22
+
23
+ import urllib.request
24
+ import urllib.error
25
+
26
+
27
+ SECRETS_FILE = Path.home() / ".lovarch" / "secrets.env"
28
+ MOCKS_DIR = Path(__file__).parent.parent / "data" / "mocks"
29
+
30
+
31
+ def _load_secrets() -> Dict[str, str]:
32
+ """Read ~/.lovarch/secrets.env (KEY=VALUE format)."""
33
+ secrets: Dict[str, str] = {}
34
+ if not SECRETS_FILE.exists():
35
+ return secrets
36
+ for line in SECRETS_FILE.read_text(encoding="utf-8").splitlines():
37
+ line = line.strip()
38
+ if not line or line.startswith("#"):
39
+ continue
40
+ if "=" in line:
41
+ k, v = line.split("=", 1)
42
+ secrets[k.strip()] = v.strip()
43
+ return secrets
44
+
45
+
46
+ _SECRETS = _load_secrets()
47
+
48
+
49
+ # ----------------------------------------------------------------------------
50
+ # Mapbox Geocoding · REAL API (P1 critico)
51
+ # ----------------------------------------------------------------------------
52
+
53
+ def mapbox_geocode(
54
+ address: str,
55
+ *,
56
+ country: str = "IT",
57
+ limit: int = 1,
58
+ ) -> Optional[Dict[str, Any]]:
59
+ """
60
+ Geocoda un indirizzo italiano via Mapbox Geocoding API.
61
+
62
+ Returns:
63
+ dict con keys: lat, lon, place_name, comune, postcode, region.
64
+ None se nessun risultato.
65
+
66
+ Raises:
67
+ ValueError: se MAPBOX_TOKEN non è configurato.
68
+ urllib.error.HTTPError: se la API ritorna errore.
69
+ """
70
+ token = _SECRETS.get("MAPBOX_TOKEN") or os.environ.get("MAPBOX_TOKEN")
71
+ if not token:
72
+ raise ValueError(
73
+ "MAPBOX_TOKEN missing. Add to ~/.lovarch/secrets.env or env."
74
+ )
75
+
76
+ url = (
77
+ f"https://api.mapbox.com/geocoding/v5/mapbox.places/{quote(address)}.json"
78
+ f"?access_token={token}&country={country}&limit={limit}"
79
+ )
80
+
81
+ with urllib.request.urlopen(url, timeout=10) as resp:
82
+ data = json.load(resp)
83
+
84
+ features = data.get("features", [])
85
+ if not features:
86
+ return None
87
+
88
+ f = features[0]
89
+ coords = f.get("center", [None, None])
90
+
91
+ # Extract context fields (postcode, place=comune, region)
92
+ ctx = {item.get("id", "").split(".")[0]: item for item in f.get("context", [])}
93
+
94
+ return {
95
+ "lat": coords[1],
96
+ "lon": coords[0],
97
+ "place_name": f.get("place_name"),
98
+ "comune": ctx.get("place", {}).get("text"),
99
+ "postcode": ctx.get("postcode", {}).get("text"),
100
+ "region": ctx.get("region", {}).get("text"),
101
+ "country": ctx.get("country", {}).get("text"),
102
+ "raw": f, # full Mapbox feature for advanced use
103
+ }
104
+
105
+
106
+ # ----------------------------------------------------------------------------
107
+ # Catasto · MOCK (OpenAPI.com substitute)
108
+ # ----------------------------------------------------------------------------
109
+
110
+ def catasto_visura(
111
+ address: str,
112
+ comune: str,
113
+ *,
114
+ foglio: Optional[str] = None,
115
+ mappale: Optional[str] = None,
116
+ ) -> Dict[str, Any]:
117
+ """
118
+ Restituisce una visura catastale.
119
+
120
+ Implementazione MOCK · per il demo. In produzione: OpenAPI.com Catasto.
121
+ Carica `data/mocks/catasto-visura.json` e sostituisce i campi address/comune
122
+ con i parametri ricevuti per dare l'aspetto di una risposta personalizzata.
123
+ """
124
+ mock_path = MOCKS_DIR / "catasto-visura.json"
125
+ visura = json.loads(mock_path.read_text(encoding="utf-8"))
126
+
127
+ visura["request"]["indirizzo"] = address
128
+ visura["request"]["comune"] = comune
129
+ if foglio:
130
+ visura["dati_catastali"]["foglio"] = foglio
131
+ if mappale:
132
+ visura["dati_catastali"]["mappale"] = mappale
133
+
134
+ visura["meta"]["mock"] = True
135
+ return visura
136
+
137
+
138
+ # ----------------------------------------------------------------------------
139
+ # Firma Digitale QES · MOCK (Yousign / OpenAPI.com firma substitute)
140
+ # ----------------------------------------------------------------------------
141
+
142
+ def firma_qes_create_envelope(
143
+ *,
144
+ document_path: str,
145
+ signer_name: str,
146
+ signer_email: str,
147
+ signer_cf: str,
148
+ document_title: str = "Documento da firmare",
149
+ ) -> Dict[str, Any]:
150
+ """
151
+ Crea un envelope di firma digitale qualificata (eIDAS).
152
+
153
+ Implementazione MOCK · per il demo. In produzione: Yousign API o OpenAPI.com firma.
154
+ Restituisce un URL di firma simulato che il cliente cliccherebbe.
155
+ """
156
+ mock_path = MOCKS_DIR / "firma-envelope.json"
157
+ envelope = json.loads(mock_path.read_text(encoding="utf-8"))
158
+
159
+ envelope["envelope"]["document_path"] = document_path
160
+ envelope["envelope"]["document_title"] = document_title
161
+ envelope["envelope"]["signer"]["name"] = signer_name
162
+ envelope["envelope"]["signer"]["email"] = signer_email
163
+ envelope["envelope"]["signer"]["codice_fiscale"] = signer_cf
164
+ envelope["meta"]["mock"] = True
165
+ return envelope
166
+
167
+
168
+ # ----------------------------------------------------------------------------
169
+ # CLI test
170
+ # ----------------------------------------------------------------------------
171
+
172
+ if __name__ == "__main__":
173
+ import sys
174
+
175
+ if len(sys.argv) < 2:
176
+ print("Usage: python api_clients.py <command> [args]")
177
+ print("Commands:")
178
+ print(" geocode <address> — Mapbox real API")
179
+ print(" catasto <address> <comune> — Catasto mock")
180
+ print(" firma <doc_path> <name> <email> <cf> — Firma QES mock")
181
+ sys.exit(1)
182
+
183
+ cmd = sys.argv[1]
184
+
185
+ if cmd == "geocode":
186
+ addr = " ".join(sys.argv[2:])
187
+ result = mapbox_geocode(addr)
188
+ print(json.dumps(result, indent=2, ensure_ascii=False))
189
+
190
+ elif cmd == "catasto":
191
+ addr = sys.argv[2]
192
+ comune = sys.argv[3]
193
+ result = catasto_visura(addr, comune)
194
+ print(json.dumps(result, indent=2, ensure_ascii=False))
195
+
196
+ elif cmd == "firma":
197
+ doc, name, email, cf = sys.argv[2:6]
198
+ result = firma_qes_create_envelope(
199
+ document_path=doc, signer_name=name,
200
+ signer_email=email, signer_cf=cf,
201
+ )
202
+ print(json.dumps(result, indent=2, ensure_ascii=False))
203
+
204
+ else:
205
+ print(f"Unknown command: {cmd}")
206
+ sys.exit(1)