@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.
- package/README.md +16 -0
- package/bin/dataif.js +623 -0
- package/package.json +26 -0
- package/scripts/build-template.mjs +72 -0
- package/templates/dataif/README.md +157 -0
- package/templates/dataif/infra/.env.example +119 -0
- package/templates/dataif/infra/.env.stg.example +119 -0
- package/templates/dataif/infra/airflow/Dockerfile +11 -0
- package/templates/dataif/infra/airflow/Dockerfile.release +17 -0
- package/templates/dataif/infra/airflow/requirements.txt +3 -0
- package/templates/dataif/infra/docker-compose.yml +306 -0
- package/templates/dataif/infra/init-db/01-init-dataif.sh +129 -0
- package/templates/dataif/infra/init-db/pnp-curated-views.sqlinc +444 -0
- package/templates/dataif/infra/init-db/pnp-raw-staging-curated.sqlinc +701 -0
- package/templates/dataif/infra/keycloak/Dockerfile +4 -0
- package/templates/dataif/infra/keycloak/realm-dataif.json +73 -0
- package/templates/dataif/infra/ollama/Dockerfile +9 -0
- package/templates/dataif/infra/ollama/bootstrap-model.sh +100 -0
- package/templates/dataif/infra/ollama/sabia-7b.Modelfile +14 -0
- package/templates/dataif/infra/postgres/Dockerfile +4 -0
- package/templates/dataif/pipelines/airflow/dags/generated/.gitkeep +1 -0
- package/templates/dataif/pipelines/airflow/dags/generated/2020_financeiro_fcc6f1f3_sync.py +9 -0
- package/templates/dataif/pipelines/dataif_pipelines/__init__.py +1 -0
- package/templates/dataif/pipelines/dataif_pipelines/airflow/__init__.py +1 -0
- package/templates/dataif/pipelines/dataif_pipelines/airflow/pnp_pipeline_factory.py +167 -0
- package/templates/dataif/pipelines/dataif_pipelines/connectors/__init__.py +1 -0
- package/templates/dataif/pipelines/dataif_pipelines/connectors/base/__init__.py +1 -0
- package/templates/dataif/pipelines/dataif_pipelines/connectors/base/connector.py +28 -0
- package/templates/dataif/pipelines/dataif_pipelines/connectors/base/types.py +14 -0
- package/templates/dataif/pipelines/dataif_pipelines/connectors/nilo_pecanha/__init__.py +1 -0
- package/templates/dataif/pipelines/dataif_pipelines/connectors/nilo_pecanha/config.py +19 -0
- package/templates/dataif/pipelines/dataif_pipelines/connectors/nilo_pecanha/connector.py +558 -0
- package/templates/dataif/pipelines/dataif_pipelines/connectors/nilo_pecanha/powerbi_microdados.py +728 -0
- package/templates/dataif/pipelines/dataif_pipelines/connectors/nilo_pecanha/transform.py +296 -0
- package/templates/dataif/pipelines/dataif_pipelines/jobs/__init__.py +1 -0
- package/templates/dataif/pipelines/dataif_pipelines/jobs/nilo_pipeline.py +112 -0
- package/templates/dataif/pipelines/dataif_pipelines/orchestration/__init__.py +21 -0
- package/templates/dataif/pipelines/dataif_pipelines/orchestration/pnp_workflow.py +783 -0
- package/templates/dataif/pipelines/dataif_pipelines/repositories/__init__.py +1 -0
- package/templates/dataif/pipelines/dataif_pipelines/repositories/pnp_raw_repository.py +860 -0
- package/templates/dataif/pipelines/dataif_pipelines/services/__init__.py +19 -0
- package/templates/dataif/pipelines/dataif_pipelines/services/pnp_curated_service.py +66 -0
- package/templates/dataif/pipelines/dataif_pipelines/services/pnp_download_service.py +534 -0
- package/templates/dataif/pipelines/dataif_pipelines/services/pnp_quality_service.py +9 -0
- package/templates/dataif/pipelines/dataif_pipelines/services/pnp_raw_ingestion_service.py +124 -0
- package/templates/dataif/pipelines/dataif_pipelines/services/pnp_staging_service.py +271 -0
- package/templates/dataif/pipelines/dataif_pipelines/services/powerbi_catalog_service.py +159 -0
- package/templates/dataif/pipelines/sql/staging/020_pnp_matriculas.sql +112 -0
- package/templates/dataif/pipelines/sql/staging/030_pnp_eficiencia_academica.sql +83 -0
- package/templates/dataif/pipelines/sql/staging/040_pnp_servidores.sql +90 -0
- package/templates/dataif/pipelines/sql/staging/050_pnp_financeiro.sql +72 -0
- package/templates/dataif/pipelines/sql/views_curated/004_mv_pnp_dashboard_fast.sql +204 -0
- package/templates/dataif/pipelines/sql/views_curated/010_vw_pnp_admin_ingestao.sql +51 -0
- package/templates/dataif/pipelines/sql/views_curated/020_vw_pnp_qualidade_dados.sql +114 -0
- package/templates/dataif/pipelines/sql/views_curated/030_vw_pnp_matriculas.sql +67 -0
- package/templates/dataif/pipelines/sql/views_curated/040_vw_pnp_eficiencia.sql +33 -0
- package/templates/dataif/pipelines/sql/views_curated/050_vw_pnp_servidores.sql +30 -0
- package/templates/dataif/pipelines/sql/views_curated/060_vw_pnp_financeiro.sql +22 -0
- package/templates/dataif/pipelines/sql/views_curated/070_vw_pnp_vanna.sql +115 -0
- package/templates/dataif/scripts/configure-env.sh +149 -0
- package/templates/dataif/scripts/create_metabase_pnp_dashboard.py +943 -0
- package/templates/dataif/scripts/create_metabase_pnp_matriculas_dashboard.py +580 -0
- package/templates/dataif/scripts/deploy.sh +79 -0
- package/templates/dataif/scripts/fix_metabase_template_tag_ids.py +91 -0
- package/templates/dataif/scripts/pnp_powerbi_microdados_probe.py +14 -0
- package/templates/dataif/scripts/pnp_validate_raw_run.py +330 -0
- package/templates/dataif/scripts/publish-images.sh +31 -0
- package/templates/dataif/scripts/sync_metabase_dashboard_field_filters.py +241 -0
- package/templates/dataif/scripts/use-vanna-ollama.sh +139 -0
- package/templates/dataif/services/api/.dockerignore +18 -0
- package/templates/dataif/services/api/Dockerfile +12 -0
- package/templates/dataif/services/api/app/__init__.py +1 -0
- package/templates/dataif/services/api/app/auth.py +48 -0
- package/templates/dataif/services/api/app/config.py +59 -0
- package/templates/dataif/services/api/app/keycloak_admin.py +215 -0
- package/templates/dataif/services/api/app/main.py +2432 -0
- package/templates/dataif/services/api/app/metabase_admin.py +191 -0
- package/templates/dataif/services/api/app/metabase_bootstrap.py +44 -0
- package/templates/dataif/services/api/app/metabase_embed.py +15 -0
- package/templates/dataif/services/api/app/pnp_dag_provisioner.py +113 -0
- package/templates/dataif/services/api/app/pnp_instance_repository.py +951 -0
- package/templates/dataif/services/api/app/pnp_powerbi.py +438 -0
- package/templates/dataif/services/api/app/vanna_client.py +32 -0
- package/templates/dataif/services/api/requirements.txt +9 -0
- package/templates/dataif/services/vanna/.dockerignore +18 -0
- package/templates/dataif/services/vanna/Dockerfile +12 -0
- package/templates/dataif/services/vanna/app/config.py +57 -0
- package/templates/dataif/services/vanna/app/main.py +108 -0
- package/templates/dataif/services/vanna/app/runtime_config.py +114 -0
- package/templates/dataif/services/vanna/app/sql_guard.py +123 -0
- package/templates/dataif/services/vanna/app/vanna_engine.py +382 -0
- package/templates/dataif/services/vanna/requirements.txt +8 -0
- package/templates/dataif/services/web/.dockerignore +13 -0
- package/templates/dataif/services/web/Dockerfile +16 -0
- package/templates/dataif/services/web/index.html +12 -0
- package/templates/dataif/services/web/nginx.conf +74 -0
- package/templates/dataif/services/web/package-lock.json +4397 -0
- package/templates/dataif/services/web/package.json +32 -0
- package/templates/dataif/services/web/postcss.config.mjs +5 -0
- package/templates/dataif/services/web/src/App.jsx +2817 -0
- package/templates/dataif/services/web/src/adminAuth.js +245 -0
- package/templates/dataif/services/web/src/assets/avatar_placeholder.png +0 -0
- package/templates/dataif/services/web/src/assets/github_logo_icon_229278.svg +1 -0
- package/templates/dataif/services/web/src/assets/if-logo.png +0 -0
- package/templates/dataif/services/web/src/assets/if.svg +0 -0
- package/templates/dataif/services/web/src/assets/pnp-horizontal.svg +1 -0
- package/templates/dataif/services/web/src/components/AppHeader.jsx +233 -0
- package/templates/dataif/services/web/src/components/application/app-navigation/base-components/mobile-header.tsx +56 -0
- package/templates/dataif/services/web/src/components/application/app-navigation/base-components/nav-account-card.tsx +209 -0
- package/templates/dataif/services/web/src/components/application/app-navigation/base-components/nav-item-button.tsx +67 -0
- package/templates/dataif/services/web/src/components/application/app-navigation/base-components/nav-item.tsx +108 -0
- package/templates/dataif/services/web/src/components/application/app-navigation/base-components/nav-list.tsx +83 -0
- package/templates/dataif/services/web/src/components/application/app-navigation/config.ts +23 -0
- package/templates/dataif/services/web/src/components/application/app-navigation/header-navigation.tsx +240 -0
- package/templates/dataif/services/web/src/components/application/pagination/pagination-base.tsx +376 -0
- package/templates/dataif/services/web/src/components/application/pagination/pagination-dot.tsx +52 -0
- package/templates/dataif/services/web/src/components/application/pagination/pagination-line.tsx +48 -0
- package/templates/dataif/services/web/src/components/application/pagination/pagination.tsx +328 -0
- package/templates/dataif/services/web/src/components/application/tabs/tabs.tsx +223 -0
- package/templates/dataif/services/web/src/components/base/avatar/avatar-label-group.tsx +28 -0
- package/templates/dataif/services/web/src/components/base/avatar/avatar.tsx +129 -0
- package/templates/dataif/services/web/src/components/base/avatar/base-components/avatar-add-button.tsx +32 -0
- package/templates/dataif/services/web/src/components/base/avatar/base-components/avatar-company-icon.tsx +24 -0
- package/templates/dataif/services/web/src/components/base/avatar/base-components/avatar-online-indicator.tsx +29 -0
- package/templates/dataif/services/web/src/components/base/avatar/base-components/index.tsx +4 -0
- package/templates/dataif/services/web/src/components/base/avatar/base-components/verified-tick.tsx +32 -0
- package/templates/dataif/services/web/src/components/base/badges/badge-types.ts +264 -0
- package/templates/dataif/services/web/src/components/base/badges/badges.tsx +415 -0
- package/templates/dataif/services/web/src/components/base/button-group/button-group.tsx +104 -0
- package/templates/dataif/services/web/src/components/base/buttons/button.tsx +267 -0
- package/templates/dataif/services/web/src/components/base/input/hint-text.tsx +31 -0
- package/templates/dataif/services/web/src/components/base/input/input.tsx +269 -0
- package/templates/dataif/services/web/src/components/base/input/label.tsx +48 -0
- package/templates/dataif/services/web/src/components/base/radio-buttons/radio-buttons.tsx +127 -0
- package/templates/dataif/services/web/src/components/base/select/combobox.tsx +150 -0
- package/templates/dataif/services/web/src/components/base/select/multi-select.tsx +361 -0
- package/templates/dataif/services/web/src/components/base/select/popover.tsx +32 -0
- package/templates/dataif/services/web/src/components/base/select/select-item.tsx +95 -0
- package/templates/dataif/services/web/src/components/base/select/select-native.tsx +67 -0
- package/templates/dataif/services/web/src/components/base/select/select.tsx +144 -0
- package/templates/dataif/services/web/src/components/base/tags/base-components/tag-close-x.tsx +32 -0
- package/templates/dataif/services/web/src/components/base/tooltip/tooltip.tsx +107 -0
- package/templates/dataif/services/web/src/components/foundations/dot-icon.tsx +22 -0
- package/templates/dataif/services/web/src/components/foundations/logo/untitledui-logo-minimal.tsx +170 -0
- package/templates/dataif/services/web/src/components/foundations/logo/untitledui-logo.tsx +58 -0
- package/templates/dataif/services/web/src/hooks/use-breakpoint.ts +34 -0
- package/templates/dataif/services/web/src/hooks/use-resize-observer.ts +67 -0
- package/templates/dataif/services/web/src/main.jsx +14 -0
- package/templates/dataif/services/web/src/providers/theme-provider.jsx +62 -0
- package/templates/dataif/services/web/src/styles/globals.css +60 -0
- package/templates/dataif/services/web/src/styles/theme.css +1326 -0
- package/templates/dataif/services/web/src/styles/typography.css +430 -0
- package/templates/dataif/services/web/src/styles.css +1287 -0
- package/templates/dataif/services/web/src/utils/cx.ts +24 -0
- package/templates/dataif/services/web/src/utils/is-react-component.ts +33 -0
- package/templates/dataif/services/web/vite.config.js +14 -0
- package/templates/dataif/sql/ddl/001_schemas.sql +6 -0
- package/templates/dataif/sql/ddl/003_pnp_raw_staging_curated.sql +699 -0
- package/templates/dataif/sql/migrations/001_pnp_phase1_backfill.sql +3 -0
- package/templates/dataif/sql/migrations/002_pnp_phase2_admin_config_backfill.sql +184 -0
- package/templates/dataif/sql/migrations/003_pnp_phase3_raw_tabular_backfill.sql +3 -0
- package/templates/dataif/sql/migrations/004_pnp_phase3_raw_backfill_support_index.sql +3 -0
- package/templates/dataif/sql/migrations/005_pnp_phase7_staging_support_indexes.sql +2 -0
- package/templates/dataif/sql/migrations/006_pnp_phase7_staging_autovacuum_tuning.sql +2 -0
- package/templates/dataif/sql/migrations/007_pnp_phase7b_run_packages.sql +20 -0
- package/templates/dataif/sql/migrations/008_pnp_phase7a_pipeline_endpoints.sql +169 -0
- package/templates/dataif/sql/migrations/009_pnp_phase8_curated.sql +35 -0
- package/templates/dataif/sql/migrations/010_pnp_phase10_staging_incremental_upsert.sql +3 -0
- package/templates/dataif/sql/migrations/010_pnp_pipeline_uuid.sql +51 -0
- package/templates/dataif/sql/migrations/011_app_settings.sql +7 -0
- package/templates/dataif/sql/staging/020_pnp_matriculas.sql +112 -0
- package/templates/dataif/sql/staging/030_pnp_eficiencia_academica.sql +83 -0
- package/templates/dataif/sql/staging/040_pnp_servidores.sql +90 -0
- package/templates/dataif/sql/staging/050_pnp_financeiro.sql +72 -0
- package/templates/dataif/sql/views_curated/003_vw_pnp_microdados_admin.sql +160 -0
- package/templates/dataif/sql/views_curated/004_mv_pnp_dashboard_fast.sql +204 -0
- package/templates/dataif/sql/views_curated/010_vw_pnp_admin_ingestao.sql +51 -0
- package/templates/dataif/sql/views_curated/020_vw_pnp_qualidade_dados.sql +114 -0
- package/templates/dataif/sql/views_curated/030_vw_pnp_matriculas.sql +67 -0
- package/templates/dataif/sql/views_curated/040_vw_pnp_eficiencia.sql +33 -0
- package/templates/dataif/sql/views_curated/050_vw_pnp_servidores.sql +30 -0
- package/templates/dataif/sql/views_curated/060_vw_pnp_financeiro.sql +22 -0
- 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,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 @@
|
|
|
1
|
+
|
|
@@ -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()
|