@dataif/cli 0.1.0

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 (183) hide show
  1. package/README.md +16 -0
  2. package/bin/dataif.js +623 -0
  3. package/package.json +26 -0
  4. package/scripts/build-template.mjs +72 -0
  5. package/templates/dataif/README.md +157 -0
  6. package/templates/dataif/infra/.env.example +119 -0
  7. package/templates/dataif/infra/.env.stg.example +119 -0
  8. package/templates/dataif/infra/airflow/Dockerfile +11 -0
  9. package/templates/dataif/infra/airflow/Dockerfile.release +17 -0
  10. package/templates/dataif/infra/airflow/requirements.txt +3 -0
  11. package/templates/dataif/infra/docker-compose.yml +306 -0
  12. package/templates/dataif/infra/init-db/01-init-dataif.sh +129 -0
  13. package/templates/dataif/infra/init-db/pnp-curated-views.sqlinc +444 -0
  14. package/templates/dataif/infra/init-db/pnp-raw-staging-curated.sqlinc +701 -0
  15. package/templates/dataif/infra/keycloak/Dockerfile +4 -0
  16. package/templates/dataif/infra/keycloak/realm-dataif.json +73 -0
  17. package/templates/dataif/infra/ollama/Dockerfile +9 -0
  18. package/templates/dataif/infra/ollama/bootstrap-model.sh +100 -0
  19. package/templates/dataif/infra/ollama/sabia-7b.Modelfile +14 -0
  20. package/templates/dataif/infra/postgres/Dockerfile +4 -0
  21. package/templates/dataif/pipelines/airflow/dags/generated/.gitkeep +1 -0
  22. package/templates/dataif/pipelines/airflow/dags/generated/2020_financeiro_fcc6f1f3_sync.py +9 -0
  23. package/templates/dataif/pipelines/dataif_pipelines/__init__.py +1 -0
  24. package/templates/dataif/pipelines/dataif_pipelines/airflow/__init__.py +1 -0
  25. package/templates/dataif/pipelines/dataif_pipelines/airflow/pnp_pipeline_factory.py +167 -0
  26. package/templates/dataif/pipelines/dataif_pipelines/connectors/__init__.py +1 -0
  27. package/templates/dataif/pipelines/dataif_pipelines/connectors/base/__init__.py +1 -0
  28. package/templates/dataif/pipelines/dataif_pipelines/connectors/base/connector.py +28 -0
  29. package/templates/dataif/pipelines/dataif_pipelines/connectors/base/types.py +14 -0
  30. package/templates/dataif/pipelines/dataif_pipelines/connectors/nilo_pecanha/__init__.py +1 -0
  31. package/templates/dataif/pipelines/dataif_pipelines/connectors/nilo_pecanha/config.py +19 -0
  32. package/templates/dataif/pipelines/dataif_pipelines/connectors/nilo_pecanha/connector.py +558 -0
  33. package/templates/dataif/pipelines/dataif_pipelines/connectors/nilo_pecanha/powerbi_microdados.py +728 -0
  34. package/templates/dataif/pipelines/dataif_pipelines/connectors/nilo_pecanha/transform.py +296 -0
  35. package/templates/dataif/pipelines/dataif_pipelines/jobs/__init__.py +1 -0
  36. package/templates/dataif/pipelines/dataif_pipelines/jobs/nilo_pipeline.py +112 -0
  37. package/templates/dataif/pipelines/dataif_pipelines/orchestration/__init__.py +21 -0
  38. package/templates/dataif/pipelines/dataif_pipelines/orchestration/pnp_workflow.py +783 -0
  39. package/templates/dataif/pipelines/dataif_pipelines/repositories/__init__.py +1 -0
  40. package/templates/dataif/pipelines/dataif_pipelines/repositories/pnp_raw_repository.py +860 -0
  41. package/templates/dataif/pipelines/dataif_pipelines/services/__init__.py +19 -0
  42. package/templates/dataif/pipelines/dataif_pipelines/services/pnp_curated_service.py +66 -0
  43. package/templates/dataif/pipelines/dataif_pipelines/services/pnp_download_service.py +534 -0
  44. package/templates/dataif/pipelines/dataif_pipelines/services/pnp_quality_service.py +9 -0
  45. package/templates/dataif/pipelines/dataif_pipelines/services/pnp_raw_ingestion_service.py +124 -0
  46. package/templates/dataif/pipelines/dataif_pipelines/services/pnp_staging_service.py +271 -0
  47. package/templates/dataif/pipelines/dataif_pipelines/services/powerbi_catalog_service.py +159 -0
  48. package/templates/dataif/pipelines/sql/staging/020_pnp_matriculas.sql +112 -0
  49. package/templates/dataif/pipelines/sql/staging/030_pnp_eficiencia_academica.sql +83 -0
  50. package/templates/dataif/pipelines/sql/staging/040_pnp_servidores.sql +90 -0
  51. package/templates/dataif/pipelines/sql/staging/050_pnp_financeiro.sql +72 -0
  52. package/templates/dataif/pipelines/sql/views_curated/004_mv_pnp_dashboard_fast.sql +204 -0
  53. package/templates/dataif/pipelines/sql/views_curated/010_vw_pnp_admin_ingestao.sql +51 -0
  54. package/templates/dataif/pipelines/sql/views_curated/020_vw_pnp_qualidade_dados.sql +114 -0
  55. package/templates/dataif/pipelines/sql/views_curated/030_vw_pnp_matriculas.sql +67 -0
  56. package/templates/dataif/pipelines/sql/views_curated/040_vw_pnp_eficiencia.sql +33 -0
  57. package/templates/dataif/pipelines/sql/views_curated/050_vw_pnp_servidores.sql +30 -0
  58. package/templates/dataif/pipelines/sql/views_curated/060_vw_pnp_financeiro.sql +22 -0
  59. package/templates/dataif/pipelines/sql/views_curated/070_vw_pnp_vanna.sql +115 -0
  60. package/templates/dataif/scripts/configure-env.sh +149 -0
  61. package/templates/dataif/scripts/create_metabase_pnp_dashboard.py +943 -0
  62. package/templates/dataif/scripts/create_metabase_pnp_matriculas_dashboard.py +580 -0
  63. package/templates/dataif/scripts/deploy.sh +79 -0
  64. package/templates/dataif/scripts/fix_metabase_template_tag_ids.py +91 -0
  65. package/templates/dataif/scripts/pnp_powerbi_microdados_probe.py +14 -0
  66. package/templates/dataif/scripts/pnp_validate_raw_run.py +330 -0
  67. package/templates/dataif/scripts/publish-images.sh +31 -0
  68. package/templates/dataif/scripts/sync_metabase_dashboard_field_filters.py +241 -0
  69. package/templates/dataif/scripts/use-vanna-ollama.sh +139 -0
  70. package/templates/dataif/services/api/.dockerignore +18 -0
  71. package/templates/dataif/services/api/Dockerfile +12 -0
  72. package/templates/dataif/services/api/app/__init__.py +1 -0
  73. package/templates/dataif/services/api/app/auth.py +48 -0
  74. package/templates/dataif/services/api/app/config.py +59 -0
  75. package/templates/dataif/services/api/app/keycloak_admin.py +215 -0
  76. package/templates/dataif/services/api/app/main.py +2432 -0
  77. package/templates/dataif/services/api/app/metabase_admin.py +191 -0
  78. package/templates/dataif/services/api/app/metabase_bootstrap.py +44 -0
  79. package/templates/dataif/services/api/app/metabase_embed.py +15 -0
  80. package/templates/dataif/services/api/app/pnp_dag_provisioner.py +113 -0
  81. package/templates/dataif/services/api/app/pnp_instance_repository.py +951 -0
  82. package/templates/dataif/services/api/app/pnp_powerbi.py +438 -0
  83. package/templates/dataif/services/api/app/vanna_client.py +32 -0
  84. package/templates/dataif/services/api/requirements.txt +9 -0
  85. package/templates/dataif/services/vanna/.dockerignore +18 -0
  86. package/templates/dataif/services/vanna/Dockerfile +12 -0
  87. package/templates/dataif/services/vanna/app/config.py +57 -0
  88. package/templates/dataif/services/vanna/app/main.py +108 -0
  89. package/templates/dataif/services/vanna/app/runtime_config.py +114 -0
  90. package/templates/dataif/services/vanna/app/sql_guard.py +123 -0
  91. package/templates/dataif/services/vanna/app/vanna_engine.py +382 -0
  92. package/templates/dataif/services/vanna/requirements.txt +8 -0
  93. package/templates/dataif/services/web/.dockerignore +13 -0
  94. package/templates/dataif/services/web/Dockerfile +16 -0
  95. package/templates/dataif/services/web/index.html +12 -0
  96. package/templates/dataif/services/web/nginx.conf +74 -0
  97. package/templates/dataif/services/web/package-lock.json +4397 -0
  98. package/templates/dataif/services/web/package.json +32 -0
  99. package/templates/dataif/services/web/postcss.config.mjs +5 -0
  100. package/templates/dataif/services/web/src/App.jsx +2817 -0
  101. package/templates/dataif/services/web/src/adminAuth.js +245 -0
  102. package/templates/dataif/services/web/src/assets/avatar_placeholder.png +0 -0
  103. package/templates/dataif/services/web/src/assets/github_logo_icon_229278.svg +1 -0
  104. package/templates/dataif/services/web/src/assets/if-logo.png +0 -0
  105. package/templates/dataif/services/web/src/assets/if.svg +0 -0
  106. package/templates/dataif/services/web/src/assets/pnp-horizontal.svg +1 -0
  107. package/templates/dataif/services/web/src/components/AppHeader.jsx +233 -0
  108. package/templates/dataif/services/web/src/components/application/app-navigation/base-components/mobile-header.tsx +56 -0
  109. package/templates/dataif/services/web/src/components/application/app-navigation/base-components/nav-account-card.tsx +209 -0
  110. package/templates/dataif/services/web/src/components/application/app-navigation/base-components/nav-item-button.tsx +67 -0
  111. package/templates/dataif/services/web/src/components/application/app-navigation/base-components/nav-item.tsx +108 -0
  112. package/templates/dataif/services/web/src/components/application/app-navigation/base-components/nav-list.tsx +83 -0
  113. package/templates/dataif/services/web/src/components/application/app-navigation/config.ts +23 -0
  114. package/templates/dataif/services/web/src/components/application/app-navigation/header-navigation.tsx +240 -0
  115. package/templates/dataif/services/web/src/components/application/pagination/pagination-base.tsx +376 -0
  116. package/templates/dataif/services/web/src/components/application/pagination/pagination-dot.tsx +52 -0
  117. package/templates/dataif/services/web/src/components/application/pagination/pagination-line.tsx +48 -0
  118. package/templates/dataif/services/web/src/components/application/pagination/pagination.tsx +328 -0
  119. package/templates/dataif/services/web/src/components/application/tabs/tabs.tsx +223 -0
  120. package/templates/dataif/services/web/src/components/base/avatar/avatar-label-group.tsx +28 -0
  121. package/templates/dataif/services/web/src/components/base/avatar/avatar.tsx +129 -0
  122. package/templates/dataif/services/web/src/components/base/avatar/base-components/avatar-add-button.tsx +32 -0
  123. package/templates/dataif/services/web/src/components/base/avatar/base-components/avatar-company-icon.tsx +24 -0
  124. package/templates/dataif/services/web/src/components/base/avatar/base-components/avatar-online-indicator.tsx +29 -0
  125. package/templates/dataif/services/web/src/components/base/avatar/base-components/index.tsx +4 -0
  126. package/templates/dataif/services/web/src/components/base/avatar/base-components/verified-tick.tsx +32 -0
  127. package/templates/dataif/services/web/src/components/base/badges/badge-types.ts +264 -0
  128. package/templates/dataif/services/web/src/components/base/badges/badges.tsx +415 -0
  129. package/templates/dataif/services/web/src/components/base/button-group/button-group.tsx +104 -0
  130. package/templates/dataif/services/web/src/components/base/buttons/button.tsx +267 -0
  131. package/templates/dataif/services/web/src/components/base/input/hint-text.tsx +31 -0
  132. package/templates/dataif/services/web/src/components/base/input/input.tsx +269 -0
  133. package/templates/dataif/services/web/src/components/base/input/label.tsx +48 -0
  134. package/templates/dataif/services/web/src/components/base/radio-buttons/radio-buttons.tsx +127 -0
  135. package/templates/dataif/services/web/src/components/base/select/combobox.tsx +150 -0
  136. package/templates/dataif/services/web/src/components/base/select/multi-select.tsx +361 -0
  137. package/templates/dataif/services/web/src/components/base/select/popover.tsx +32 -0
  138. package/templates/dataif/services/web/src/components/base/select/select-item.tsx +95 -0
  139. package/templates/dataif/services/web/src/components/base/select/select-native.tsx +67 -0
  140. package/templates/dataif/services/web/src/components/base/select/select.tsx +144 -0
  141. package/templates/dataif/services/web/src/components/base/tags/base-components/tag-close-x.tsx +32 -0
  142. package/templates/dataif/services/web/src/components/base/tooltip/tooltip.tsx +107 -0
  143. package/templates/dataif/services/web/src/components/foundations/dot-icon.tsx +22 -0
  144. package/templates/dataif/services/web/src/components/foundations/logo/untitledui-logo-minimal.tsx +170 -0
  145. package/templates/dataif/services/web/src/components/foundations/logo/untitledui-logo.tsx +58 -0
  146. package/templates/dataif/services/web/src/hooks/use-breakpoint.ts +34 -0
  147. package/templates/dataif/services/web/src/hooks/use-resize-observer.ts +67 -0
  148. package/templates/dataif/services/web/src/main.jsx +14 -0
  149. package/templates/dataif/services/web/src/providers/theme-provider.jsx +62 -0
  150. package/templates/dataif/services/web/src/styles/globals.css +60 -0
  151. package/templates/dataif/services/web/src/styles/theme.css +1326 -0
  152. package/templates/dataif/services/web/src/styles/typography.css +430 -0
  153. package/templates/dataif/services/web/src/styles.css +1287 -0
  154. package/templates/dataif/services/web/src/utils/cx.ts +24 -0
  155. package/templates/dataif/services/web/src/utils/is-react-component.ts +33 -0
  156. package/templates/dataif/services/web/vite.config.js +14 -0
  157. package/templates/dataif/sql/ddl/001_schemas.sql +6 -0
  158. package/templates/dataif/sql/ddl/003_pnp_raw_staging_curated.sql +699 -0
  159. package/templates/dataif/sql/migrations/001_pnp_phase1_backfill.sql +3 -0
  160. package/templates/dataif/sql/migrations/002_pnp_phase2_admin_config_backfill.sql +184 -0
  161. package/templates/dataif/sql/migrations/003_pnp_phase3_raw_tabular_backfill.sql +3 -0
  162. package/templates/dataif/sql/migrations/004_pnp_phase3_raw_backfill_support_index.sql +3 -0
  163. package/templates/dataif/sql/migrations/005_pnp_phase7_staging_support_indexes.sql +2 -0
  164. package/templates/dataif/sql/migrations/006_pnp_phase7_staging_autovacuum_tuning.sql +2 -0
  165. package/templates/dataif/sql/migrations/007_pnp_phase7b_run_packages.sql +20 -0
  166. package/templates/dataif/sql/migrations/008_pnp_phase7a_pipeline_endpoints.sql +169 -0
  167. package/templates/dataif/sql/migrations/009_pnp_phase8_curated.sql +35 -0
  168. package/templates/dataif/sql/migrations/010_pnp_phase10_staging_incremental_upsert.sql +3 -0
  169. package/templates/dataif/sql/migrations/010_pnp_pipeline_uuid.sql +51 -0
  170. package/templates/dataif/sql/migrations/011_app_settings.sql +7 -0
  171. package/templates/dataif/sql/staging/020_pnp_matriculas.sql +112 -0
  172. package/templates/dataif/sql/staging/030_pnp_eficiencia_academica.sql +83 -0
  173. package/templates/dataif/sql/staging/040_pnp_servidores.sql +90 -0
  174. package/templates/dataif/sql/staging/050_pnp_financeiro.sql +72 -0
  175. package/templates/dataif/sql/views_curated/003_vw_pnp_microdados_admin.sql +160 -0
  176. package/templates/dataif/sql/views_curated/004_mv_pnp_dashboard_fast.sql +204 -0
  177. package/templates/dataif/sql/views_curated/010_vw_pnp_admin_ingestao.sql +51 -0
  178. package/templates/dataif/sql/views_curated/020_vw_pnp_qualidade_dados.sql +114 -0
  179. package/templates/dataif/sql/views_curated/030_vw_pnp_matriculas.sql +67 -0
  180. package/templates/dataif/sql/views_curated/040_vw_pnp_eficiencia.sql +33 -0
  181. package/templates/dataif/sql/views_curated/050_vw_pnp_servidores.sql +30 -0
  182. package/templates/dataif/sql/views_curated/060_vw_pnp_financeiro.sql +22 -0
  183. package/templates/dataif/sql/views_curated/070_vw_pnp_vanna.sql +115 -0
@@ -0,0 +1,241 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import os
5
+ import re
6
+ import urllib.error
7
+ import urllib.request
8
+
9
+
10
+ API_BASE = os.getenv("METABASE_API_URL", "http://localhost:3001/api").rstrip("/")
11
+ API_KEY = os.getenv("METABASE_API_KEY")
12
+ DATABASE_ID = int(os.getenv("METABASE_DATABASE_ID", "2"))
13
+ DASHBOARD_ID = int(os.getenv("METABASE_DASHBOARD_ID", "3"))
14
+
15
+ if not API_KEY:
16
+ raise SystemExit("METABASE_API_KEY is required")
17
+
18
+
19
+ def api(method: str, path: str, payload: dict | list | None = None) -> dict | list:
20
+ data = None
21
+ headers = {"x-api-key": API_KEY, "Accept": "application/json"}
22
+ if payload is not None:
23
+ data = json.dumps(payload).encode("utf-8")
24
+ headers["Content-Type"] = "application/json"
25
+
26
+ request = urllib.request.Request(f"{API_BASE}{path}", data=data, headers=headers, method=method)
27
+ try:
28
+ with urllib.request.urlopen(request) as response:
29
+ body = response.read().decode("utf-8")
30
+ return json.loads(body) if body else {}
31
+ except urllib.error.HTTPError as exc:
32
+ body = exc.read().decode("utf-8", errors="replace")
33
+ raise RuntimeError(f"{method} {path} failed: {exc.code} {body}") from exc
34
+
35
+
36
+ def find_source_name(query: str) -> str | None:
37
+ match = re.search(r"\bFROM\s+([a-zA-Z0-9_.]+)", query, flags=re.IGNORECASE)
38
+ return match.group(1).split(".")[-1] if match else None
39
+
40
+
41
+ def normalize_filter_clause(query: str, slugs: list[str]) -> str:
42
+ normalized = []
43
+ for line in query.splitlines():
44
+ stripped = line.strip()
45
+ replaced = False
46
+ if stripped.startswith("[[ AND"):
47
+ for slug in slugs:
48
+ if f"{{{{{slug}}}}}" in line:
49
+ normalized.append("[[ AND {{" + slug + "}} ]]")
50
+ replaced = True
51
+ break
52
+ if not replaced:
53
+ normalized.append(line)
54
+ return "\n".join(normalized)
55
+
56
+
57
+ def widget_type(slug: str) -> str:
58
+ if slug == "ano":
59
+ return "number/="
60
+ if slug == "municipio":
61
+ return "string/contains"
62
+ return "string/="
63
+
64
+
65
+ def fetch_field_map() -> dict[str, dict[str, int]]:
66
+ metadata = api("GET", f"/database/{DATABASE_ID}/metadata")
67
+ result: dict[str, dict[str, int]] = {}
68
+ for table in metadata.get("tables", []):
69
+ if table.get("schema") != "curated":
70
+ continue
71
+ result[table["name"]] = {field["name"]: int(field["id"]) for field in table.get("fields", [])}
72
+ return result
73
+
74
+
75
+ def build_template_tag(slug: str, field_id: int, current_tag: dict) -> dict:
76
+ tag = {
77
+ "id": current_tag.get("id"),
78
+ "name": slug,
79
+ "display-name": current_tag.get("display-name") or slug.replace("_", " ").title(),
80
+ "type": "dimension",
81
+ "required": False,
82
+ "widget-type": widget_type(slug),
83
+ "dimension": ["field", field_id, None],
84
+ }
85
+ if tag["widget-type"] == "string/contains":
86
+ tag["options"] = {"case-sensitive": False}
87
+ return tag
88
+
89
+
90
+ def build_parameter(slug: str, tag: dict, current_parameter: dict | None) -> dict:
91
+ parameter = {
92
+ "id": tag["id"],
93
+ "type": tag["widget-type"],
94
+ "target": ["dimension", ["template-tag", slug]],
95
+ "name": tag["display-name"],
96
+ "slug": slug,
97
+ "required": False,
98
+ "isMultiSelect": True,
99
+ }
100
+ if current_parameter:
101
+ for key in ("values_query_type", "default", "options"):
102
+ if key in current_parameter:
103
+ parameter[key] = current_parameter[key]
104
+ if "options" in tag:
105
+ parameter["options"] = tag["options"]
106
+ return parameter
107
+
108
+
109
+ def minimal_dashcard_payload(dashcard: dict, parameter_mappings: list[dict]) -> dict:
110
+ payload = {
111
+ "id": dashcard["id"],
112
+ "card_id": dashcard.get("card_id"),
113
+ "row": dashcard["row"],
114
+ "col": dashcard["col"],
115
+ "size_x": dashcard["size_x"],
116
+ "size_y": dashcard["size_y"],
117
+ "dashboard_tab_id": dashcard.get("dashboard_tab_id"),
118
+ "parameter_mappings": parameter_mappings,
119
+ "visualization_settings": dashcard.get("visualization_settings") or {},
120
+ }
121
+ return payload
122
+
123
+
124
+ def sync() -> dict[str, int]:
125
+ field_map = fetch_field_map()
126
+ dashboard = api("GET", f"/dashboard/{DASHBOARD_ID}")
127
+ dashcards = dashboard.get("dashcards", [])
128
+
129
+ card_cache: dict[int, dict] = {}
130
+ updated_cards = 0
131
+ for dashcard in dashcards:
132
+ card_id = dashcard.get("card_id")
133
+ if not card_id:
134
+ continue
135
+
136
+ card = api("GET", f"/card/{card_id}")
137
+ card_cache[int(card_id)] = card
138
+ dataset_query = card.get("dataset_query") or {}
139
+ native = dataset_query.get("native") or {}
140
+ template_tags = native.get("template-tags") or {}
141
+ if not template_tags:
142
+ continue
143
+
144
+ source_name = find_source_name(native.get("query") or "")
145
+ if not source_name or source_name not in field_map:
146
+ continue
147
+
148
+ current_parameters = {item.get("slug"): item for item in (card.get("parameters") or []) if item.get("slug")}
149
+ normalized_tags = {}
150
+ ordered_slugs = [slug for slug in template_tags if slug in field_map[source_name]]
151
+ for slug in ordered_slugs:
152
+ normalized_tags[slug] = build_template_tag(slug, field_map[source_name][slug], template_tags[slug])
153
+
154
+ normalized_query = normalize_filter_clause(native["query"], ordered_slugs)
155
+ parameters = [build_parameter(slug, normalized_tags[slug], current_parameters.get(slug)) for slug in ordered_slugs]
156
+
157
+ payload = {
158
+ "name": card["name"],
159
+ "description": card.get("description"),
160
+ "display": card["display"],
161
+ "dataset_query": {
162
+ **dataset_query,
163
+ "native": {
164
+ **native,
165
+ "query": normalized_query,
166
+ "template-tags": normalized_tags,
167
+ },
168
+ },
169
+ "visualization_settings": card.get("visualization_settings") or {},
170
+ "parameters": parameters,
171
+ }
172
+ api("PUT", f"/card/{card_id}", payload)
173
+ updated_cards += 1
174
+ card_cache[int(card_id)] = api("GET", f"/card/{card_id}")
175
+
176
+ used_slugs = []
177
+ for card in card_cache.values():
178
+ tags = (card.get("dataset_query") or {}).get("native", {}).get("template-tags", {})
179
+ for slug in tags:
180
+ if slug not in used_slugs:
181
+ used_slugs.append(slug)
182
+
183
+ dashboard_params = []
184
+ seen_slugs = set()
185
+ for parameter in dashboard.get("parameters", []):
186
+ slug = parameter.get("slug")
187
+ if slug not in used_slugs or slug in seen_slugs:
188
+ continue
189
+ dashboard_params.append(parameter)
190
+ seen_slugs.add(slug)
191
+
192
+ dashcards_payload = []
193
+ mapped_dashcards = 0
194
+ for dashcard in dashcards:
195
+ card_id = dashcard.get("card_id")
196
+ if not card_id:
197
+ dashcards_payload.append(minimal_dashcard_payload(dashcard, []))
198
+ continue
199
+
200
+ card = card_cache[int(card_id)]
201
+ tags = (card.get("dataset_query") or {}).get("native", {}).get("template-tags", {})
202
+ parameter_mappings = []
203
+ for parameter in dashboard_params:
204
+ slug = parameter["slug"]
205
+ if slug not in tags:
206
+ continue
207
+ parameter_mappings.append(
208
+ {
209
+ "parameter_id": parameter["id"],
210
+ "card_id": card_id,
211
+ "target": ["dimension", ["template-tag", slug], {"stage-number": 0}],
212
+ }
213
+ )
214
+ if parameter_mappings:
215
+ mapped_dashcards += 1
216
+ dashcards_payload.append(minimal_dashcard_payload(dashcard, parameter_mappings))
217
+
218
+ api(
219
+ "PUT",
220
+ f"/dashboard/{DASHBOARD_ID}",
221
+ {
222
+ "name": dashboard["name"],
223
+ "description": dashboard.get("description"),
224
+ "width": dashboard.get("width", "fixed"),
225
+ "auto_apply_filters": dashboard.get("auto_apply_filters", True),
226
+ "parameters": dashboard_params,
227
+ "tabs": dashboard.get("tabs", []),
228
+ "dashcards": dashcards_payload,
229
+ },
230
+ )
231
+
232
+ return {
233
+ "updated_cards": updated_cards,
234
+ "dashboard_params": len(dashboard_params),
235
+ "mapped_dashcards": mapped_dashcards,
236
+ "dashcards_total": len([dc for dc in dashcards if dc.get("card_id")]),
237
+ }
238
+
239
+
240
+ if __name__ == "__main__":
241
+ print(json.dumps(sync(), ensure_ascii=False))
@@ -0,0 +1,139 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
5
+ INFRA_DIR="${ROOT_DIR}/infra"
6
+ ENV_FILE="${INFRA_DIR}/.env"
7
+
8
+ run_bootstrap=true
9
+
10
+ usage() {
11
+ cat <<'EOF'
12
+ Uso: ./scripts/use-vanna-ollama.sh [--no-bootstrap]
13
+
14
+ Alterna o Vanna para Ollama, preservando a chave Maritaca no infra/.env.
15
+
16
+ Opcoes:
17
+ --no-bootstrap Nao executa ollama-model-bootstrap. Use quando o modelo ja existir.
18
+ -h, --help Mostra esta ajuda.
19
+ EOF
20
+ }
21
+
22
+ while [ "$#" -gt 0 ]; do
23
+ case "$1" in
24
+ --no-bootstrap)
25
+ run_bootstrap=false
26
+ ;;
27
+ -h|--help)
28
+ usage
29
+ exit 0
30
+ ;;
31
+ *)
32
+ echo "Opcao desconhecida: $1" >&2
33
+ usage >&2
34
+ exit 2
35
+ ;;
36
+ esac
37
+ shift
38
+ done
39
+
40
+ if [ ! -f "${ENV_FILE}" ]; then
41
+ echo "Arquivo ${ENV_FILE} nao encontrado. Crie-o a partir de infra/.env.example antes de continuar." >&2
42
+ exit 1
43
+ fi
44
+
45
+ get_env_value() {
46
+ local key="$1"
47
+
48
+ awk -v key="${key}" '
49
+ $0 ~ "^[[:space:]]*(export[[:space:]]+)?" key "[[:space:]]*=" {
50
+ value = $0
51
+ sub("^[[:space:]]*(export[[:space:]]+)?" key "[[:space:]]*=[[:space:]]*", "", value)
52
+ sub(/[[:space:]]*$/, "", value)
53
+ if ((value ~ /^".*"$/) || (value ~ /^'\''.*'\''$/)) {
54
+ value = substr(value, 2, length(value) - 2)
55
+ }
56
+ found = 1
57
+ }
58
+ END {
59
+ if (found) {
60
+ print value
61
+ }
62
+ }
63
+ ' "${ENV_FILE}"
64
+ }
65
+
66
+ set_env_value() {
67
+ local key="$1"
68
+ local value="$2"
69
+ local tmp
70
+
71
+ tmp="$(mktemp "${ENV_FILE}.tmp.XXXXXX")"
72
+ awk -v key="${key}" -v value="${value}" '
73
+ $0 ~ "^[[:space:]]*(export[[:space:]]+)?" key "[[:space:]]*=" {
74
+ if (!written) {
75
+ print key "=" value
76
+ written = 1
77
+ }
78
+ next
79
+ }
80
+ { print }
81
+ END {
82
+ if (!written) {
83
+ print key "=" value
84
+ }
85
+ }
86
+ ' "${ENV_FILE}" > "${tmp}"
87
+
88
+ chmod --reference="${ENV_FILE}" "${tmp}" 2>/dev/null || true
89
+ mv "${tmp}" "${ENV_FILE}"
90
+ }
91
+
92
+ ollama_model_name="$(get_env_value OLLAMA_MODEL_NAME)"
93
+ if [ -z "${ollama_model_name}" ]; then
94
+ ollama_model_name="sabia-7b"
95
+ fi
96
+
97
+ vanna_port="$(get_env_value VANNA_PORT)"
98
+ if [ -z "${vanna_port}" ]; then
99
+ vanna_port="9000"
100
+ fi
101
+
102
+ echo "Atualizando ${ENV_FILE}: VANNA_LLM_PROVIDER=ollama"
103
+ set_env_value "VANNA_LLM_PROVIDER" "ollama"
104
+ set_env_value "VANNA_OLLAMA_BASE_URL" "http://ollama:11434"
105
+ set_env_value "VANNA_OLLAMA_MODEL" "${ollama_model_name}"
106
+
107
+ cd "${INFRA_DIR}"
108
+
109
+ echo "Subindo Ollama..."
110
+ docker compose --profile llm up -d ollama
111
+
112
+ if [ "${run_bootstrap}" = true ]; then
113
+ echo "Carregando modelo Ollama (${ollama_model_name})..."
114
+ docker compose --profile llm run --rm ollama-model-bootstrap
115
+ else
116
+ echo "Bootstrap do modelo pulado (--no-bootstrap)."
117
+ fi
118
+
119
+ echo "Reiniciando Vanna..."
120
+ docker compose up -d --build vanna
121
+
122
+ health_url="http://localhost:${vanna_port}/health"
123
+ echo "Verificando healthcheck: curl ${health_url}"
124
+
125
+ for attempt in $(seq 1 30); do
126
+ if curl -fsS "${health_url}"; then
127
+ echo
128
+ exit 0
129
+ fi
130
+
131
+ if [ "${attempt}" -eq 30 ]; then
132
+ echo
133
+ echo "Vanna nao respondeu em ${health_url}. Tente novamente com:" >&2
134
+ echo "curl ${health_url}" >&2
135
+ exit 1
136
+ fi
137
+
138
+ sleep 2
139
+ done
@@ -0,0 +1,18 @@
1
+ .env
2
+ .venv
3
+ venv/
4
+
5
+ __pycache__/
6
+ **/__pycache__/
7
+ *.pyc
8
+ *.pyo
9
+ .pytest_cache/
10
+ .mypy_cache/
11
+ .ruff_cache/
12
+
13
+ *.db
14
+ *.sqlite
15
+ *.sqlite3
16
+ *.log
17
+ tmp/
18
+ temp/
@@ -0,0 +1,12 @@
1
+ FROM python:3.11-slim
2
+
3
+ ENV PYTHONDONTWRITEBYTECODE=1
4
+ ENV PYTHONUNBUFFERED=1
5
+
6
+ WORKDIR /app
7
+ COPY requirements.txt /app/requirements.txt
8
+ RUN pip install --no-cache-dir -r /app/requirements.txt
9
+
10
+ COPY app /app/app
11
+
12
+ CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
@@ -0,0 +1,48 @@
1
+ from __future__ import annotations
2
+
3
+ from functools import lru_cache
4
+ from typing import Any
5
+
6
+ import jwt
7
+ from fastapi import HTTPException, Security, status
8
+ from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
9
+ from jwt import PyJWKClient
10
+
11
+ from .config import settings
12
+
13
+ security = HTTPBearer(auto_error=False)
14
+
15
+
16
+ @lru_cache(maxsize=1)
17
+ def _jwks_client() -> PyJWKClient:
18
+ jwks_url = f"{settings.keycloak_url}/realms/{settings.keycloak_realm}/protocol/openid-connect/certs"
19
+ return PyJWKClient(jwks_url)
20
+
21
+
22
+ def verify_optional_bearer(credentials: HTTPAuthorizationCredentials | None = Security(security)) -> dict[str, Any] | None:
23
+ if credentials is None:
24
+ return None
25
+
26
+ token = credentials.credentials
27
+ try:
28
+ signing_key = _jwks_client().get_signing_key_from_jwt(token)
29
+ payload = jwt.decode(
30
+ token,
31
+ signing_key.key,
32
+ algorithms=["RS256"],
33
+ audience=settings.keycloak_audience,
34
+ options={"verify_exp": True},
35
+ )
36
+ return payload
37
+ except Exception as exc:
38
+ raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail=f"Invalid token: {exc}") from exc
39
+
40
+
41
+ def require_admin(payload: dict[str, Any] | None) -> None:
42
+ if payload is None:
43
+ raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Authentication required")
44
+
45
+ realm_access = payload.get("realm_access") or {}
46
+ roles = realm_access.get("roles") or []
47
+ if "admin" not in roles:
48
+ raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Admin role required")
@@ -0,0 +1,59 @@
1
+ from __future__ import annotations
2
+
3
+ from pydantic import Field
4
+ from pydantic_settings import BaseSettings
5
+
6
+
7
+ class Settings(BaseSettings):
8
+ api_host: str = Field(default="0.0.0.0", alias="API_HOST")
9
+ api_port: int = Field(default=8000, alias="API_PORT")
10
+
11
+ metabase_site_url: str = Field(default="http://localhost:3000", alias="METABASE_SITE_URL")
12
+ metabase_api_url: str = Field(default="http://metabase:3000", alias="METABASE_API_URL")
13
+ metabase_embed_secret: str = Field(default="replace_with_secure_secret", alias="METABASE_EMBED_SECRET")
14
+ metabase_allowed_dashboard_ids: str = Field(default="1,2,3", alias="METABASE_ALLOWED_DASHBOARD_IDS")
15
+ metabase_default_dashboard_id: str = Field(default="", alias="METABASE_DEFAULT_DASHBOARD_ID")
16
+ metabase_admin_email: str = Field(default="admin@dataif.local", alias="METABASE_ADMIN_EMAIL")
17
+ metabase_admin_password: str = Field(default="admin", alias="METABASE_ADMIN_PASSWORD")
18
+ metabase_admin_first_name: str = Field(default="DataIF", alias="METABASE_ADMIN_FIRST_NAME")
19
+ metabase_admin_last_name: str = Field(default="Metabase", alias="METABASE_ADMIN_LAST_NAME")
20
+ metabase_site_name: str = Field(default="dataif", alias="METABASE_SITE_NAME")
21
+ metabase_allow_tracking: bool = Field(default=False, alias="METABASE_ALLOW_TRACKING")
22
+
23
+ vanna_service_url: str = Field(default="http://localhost:9000", alias="VANNA_SERVICE_URL")
24
+ vanna_llm_provider: str = Field(default="ollama", alias="VANNA_LLM_PROVIDER")
25
+ vanna_ollama_base_url: str = Field(default="http://ollama:11434", alias="VANNA_OLLAMA_BASE_URL")
26
+ vanna_ollama_model: str = Field(default="sabia-7b", alias="VANNA_OLLAMA_MODEL")
27
+ vanna_maritaca_api_url: str = Field(
28
+ default="https://chat.maritaca.ai/api/chat/completions",
29
+ alias="VANNA_MARITACA_API_URL",
30
+ )
31
+ vanna_maritaca_api_key: str = Field(default="", alias="VANNA_MARITACA_API_KEY")
32
+ vanna_maritaca_model: str = Field(default="sabia-4", alias="VANNA_MARITACA_MODEL")
33
+ vanna_maritaca_timeout_seconds: int = Field(default=60, alias="VANNA_MARITACA_TIMEOUT_SECONDS")
34
+
35
+ keycloak_url: str = Field(default="http://localhost:8081", alias="KEYCLOAK_URL")
36
+ keycloak_realm: str = Field(default="dataif", alias="KEYCLOAK_REALM")
37
+ keycloak_audience: str = Field(default="dataif-api", alias="KEYCLOAK_AUDIENCE")
38
+ keycloak_client_id: str = Field(default="dataif-web", alias="KEYCLOAK_CLIENT_ID")
39
+ keycloak_client_secret: str = Field(default="", alias="KEYCLOAK_CLIENT_SECRET")
40
+ keycloak_admin_realm: str = Field(default="master", alias="KEYCLOAK_ADMIN_REALM")
41
+ keycloak_admin_client_id: str = Field(default="admin-cli", alias="KEYCLOAK_ADMIN_CLIENT_ID")
42
+ keycloak_admin_username: str = Field(default="admin", alias="KEYCLOAK_ADMIN")
43
+ keycloak_admin_password: str = Field(default="admin", alias="KEYCLOAK_ADMIN_PASSWORD")
44
+
45
+ warehouse_dsn: str = Field(default="", alias="WAREHOUSE_DSN")
46
+ airflow_api_url: str = Field(default="http://airflow-webserver:8080/airflow", alias="AIRFLOW_API_URL")
47
+ airflow_admin_user: str = Field(default="admin", alias="AIRFLOW_ADMIN_USER")
48
+ airflow_admin_password: str = Field(default="admin", alias="AIRFLOW_ADMIN_PASSWORD")
49
+
50
+ cors_allow_origins: str = Field(default="http://localhost:5173", alias="CORS_ALLOW_ORIGINS")
51
+ nilo_timeout_seconds: int = Field(default=60, alias="NILO_TIMEOUT_SECONDS")
52
+ pnp_catalog_cache_ttl_seconds: int = Field(default=900, alias="PNP_CATALOG_CACHE_TTL_SECONDS")
53
+
54
+ class Config:
55
+ env_file = ".env"
56
+ case_sensitive = False
57
+
58
+
59
+ settings = Settings()