@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,943 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import os
5
+ import sys
6
+ import urllib.error
7
+ import urllib.request
8
+ from dataclasses import dataclass
9
+ from hashlib import md5
10
+
11
+
12
+ API_BASE = os.getenv("METABASE_API_URL", "http://localhost:3001/api").rstrip("/")
13
+ API_KEY = os.getenv("METABASE_API_KEY")
14
+ DATABASE_ID = int(os.getenv("METABASE_DATABASE_ID", "2"))
15
+ DASHBOARD_NAME = os.getenv(
16
+ "METABASE_DASHBOARD_NAME",
17
+ "PNP 2024 - Painel Integrado de Gestao Administrativa",
18
+ )
19
+
20
+ if not API_KEY:
21
+ raise SystemExit("METABASE_API_KEY is required")
22
+
23
+
24
+ def api(method: str, path: str, payload: dict | list | None = None) -> dict | list:
25
+ data = None
26
+ headers = {"x-api-key": API_KEY, "Accept": "application/json"}
27
+ if payload is not None:
28
+ data = json.dumps(payload).encode("utf-8")
29
+ headers["Content-Type"] = "application/json"
30
+
31
+ request = urllib.request.Request(f"{API_BASE}{path}", data=data, headers=headers, method=method)
32
+ try:
33
+ with urllib.request.urlopen(request) as response:
34
+ body = response.read().decode("utf-8")
35
+ return json.loads(body) if body else {}
36
+ except urllib.error.HTTPError as exc:
37
+ body = exc.read().decode("utf-8", errors="replace")
38
+ raise RuntimeError(f"{method} {path} failed: {exc.code} {body}") from exc
39
+
40
+
41
+ def find_dashboard_by_name(name: str) -> dict | None:
42
+ for dashboard in api("GET", "/dashboard"):
43
+ if dashboard.get("name") == name and not dashboard.get("archived"):
44
+ return dashboard
45
+ return None
46
+
47
+
48
+ def list_cards() -> list[dict]:
49
+ return list(api("GET", "/card"))
50
+
51
+
52
+ def archive_card(card_id: int) -> None:
53
+ api("PUT", f"/card/{card_id}", {"archived": True})
54
+
55
+
56
+ def cleanup_previous_pnp_cards() -> None:
57
+ prefix = "PNP 2024 - "
58
+ for card in list_cards():
59
+ if card.get("archived"):
60
+ continue
61
+ name = card.get("name") or ""
62
+ if name.startswith(prefix):
63
+ archive_card(int(card["id"]))
64
+
65
+
66
+ def ensure_dashboard() -> int:
67
+ existing = find_dashboard_by_name(DASHBOARD_NAME)
68
+ if existing:
69
+ dashboard_id = int(existing["id"])
70
+ api(
71
+ "PUT",
72
+ f"/dashboard/{dashboard_id}",
73
+ {
74
+ "name": DASHBOARD_NAME,
75
+ "description": DASHBOARD_DESCRIPTION,
76
+ "width": "fixed",
77
+ "auto_apply_filters": True,
78
+ "parameters": DASHBOARD_PARAMETERS,
79
+ "tabs": [],
80
+ "dashcards": [],
81
+ },
82
+ )
83
+ return dashboard_id
84
+
85
+ response = api(
86
+ "POST",
87
+ "/dashboard",
88
+ {
89
+ "name": DASHBOARD_NAME,
90
+ "description": DASHBOARD_DESCRIPTION,
91
+ "width": "fixed",
92
+ "auto_apply_filters": True,
93
+ "collection_id": None,
94
+ "parameters": DASHBOARD_PARAMETERS,
95
+ },
96
+ )
97
+ return int(response["id"])
98
+
99
+
100
+ def create_card(card: "CardDef") -> int:
101
+ response = api(
102
+ "POST",
103
+ "/card",
104
+ {
105
+ "name": card.name,
106
+ "description": card.description,
107
+ "display": card.display,
108
+ "database_id": DATABASE_ID,
109
+ "collection_id": None,
110
+ "dataset_query": {
111
+ "type": "native",
112
+ "database": DATABASE_ID,
113
+ "native": {
114
+ "query": card.sql,
115
+ "template-tags": card.template_tags(),
116
+ },
117
+ },
118
+ "visualization_settings": card.visualization_settings,
119
+ },
120
+ )
121
+ return int(response["id"])
122
+
123
+
124
+ def update_dashboard(dashboard_id: int, tabs: list[dict], dashcards: list[dict]) -> None:
125
+ api(
126
+ "PUT",
127
+ f"/dashboard/{dashboard_id}",
128
+ {
129
+ "name": DASHBOARD_NAME,
130
+ "description": DASHBOARD_DESCRIPTION,
131
+ "width": "fixed",
132
+ "auto_apply_filters": True,
133
+ "parameters": DASHBOARD_PARAMETERS,
134
+ "tabs": tabs,
135
+ "dashcards": dashcards,
136
+ },
137
+ )
138
+
139
+
140
+ @dataclass(frozen=True)
141
+ class CardDef:
142
+ name: str
143
+ description: str
144
+ tab: str
145
+ row: int
146
+ col: int
147
+ size_x: int
148
+ size_y: int
149
+ sql: str
150
+ display: str
151
+ filters: tuple[str, ...]
152
+ visualization_settings: dict
153
+
154
+ def template_tags(self) -> dict:
155
+ return {
156
+ slug: {
157
+ **TEMPLATE_TAGS[slug],
158
+ # Metabase requires template tag ids so the SQL editor can persist
159
+ # card parameter definitions on subsequent edits.
160
+ "id": md5(f"{self.name}:{slug}".encode("utf-8")).hexdigest()[:12],
161
+ }
162
+ for slug in self.filters
163
+ }
164
+
165
+ def parameter_mappings(self) -> list[dict]:
166
+ return [
167
+ {
168
+ "parameter_id": PARAM_ID_BY_SLUG[slug],
169
+ "target": ["variable", ["template-tag", slug]],
170
+ }
171
+ for slug in self.filters
172
+ ]
173
+
174
+
175
+ @dataclass(frozen=True)
176
+ class VirtualCardDef:
177
+ tab: str
178
+ text: str
179
+ row: int
180
+ col: int
181
+ size_x: int
182
+ size_y: int
183
+
184
+
185
+ DASHBOARD_DESCRIPTION = (
186
+ "Painel da PNP organizado em guias tematicas leves, com filtros do cabecalho "
187
+ "ligados apenas aos conjuntos de perguntas correspondentes."
188
+ )
189
+
190
+ TAB_LAYOUT = ["Matriculas", "Eficiencia Academica", "Servidores", "Financeiro", "Qualidade e Ingestao"]
191
+
192
+ DASHBOARD_PARAMETERS = [
193
+ {"id": "p_ano", "name": "Ano", "slug": "ano", "type": "number/=", "sectionId": "number"},
194
+ {"id": "p_instituicao", "name": "Instituicao", "slug": "instituicao", "type": "string/=", "sectionId": "string"},
195
+ {"id": "p_regiao", "name": "Regiao", "slug": "regiao", "type": "string/=", "sectionId": "string"},
196
+ {"id": "p_uf", "name": "UF", "slug": "uf", "type": "string/=", "sectionId": "string"},
197
+ {"id": "p_municipio", "name": "Municipio", "slug": "municipio", "type": "string/=", "sectionId": "string"},
198
+ {"id": "p_sexo", "name": "Sexo", "slug": "sexo", "type": "string/=", "sectionId": "string"},
199
+ {"id": "p_cor_raca", "name": "Cor / Raca", "slug": "cor_raca", "type": "string/=", "sectionId": "string"},
200
+ {"id": "p_renda_familiar", "name": "Renda Familiar", "slug": "renda_familiar", "type": "string/=", "sectionId": "string"},
201
+ {"id": "p_faixa_etaria", "name": "Faixa Etaria", "slug": "faixa_etaria", "type": "string/=", "sectionId": "string"},
202
+ {"id": "p_situacao_matricula", "name": "Situacao de Matricula", "slug": "situacao_matricula", "type": "string/=", "sectionId": "string"},
203
+ {"id": "p_modalidade_ensino", "name": "Modalidade de Ensino", "slug": "modalidade_ensino", "type": "string/=", "sectionId": "string"},
204
+ {"id": "p_tipo_curso", "name": "Tipo de Curso", "slug": "tipo_curso", "type": "string/=", "sectionId": "string"},
205
+ {"id": "p_tipo_oferta", "name": "Tipo de Oferta", "slug": "tipo_oferta", "type": "string/=", "sectionId": "string"},
206
+ {"id": "p_turno", "name": "Turno", "slug": "turno", "type": "string/=", "sectionId": "string"},
207
+ {"id": "p_nome_curso", "name": "Nome do Curso", "slug": "nome_curso", "type": "string/=", "sectionId": "string"},
208
+ {"id": "p_matricula_atendida", "name": "Matricula Atendida", "slug": "matricula_atendida", "type": "string/=", "sectionId": "string"},
209
+ {"id": "p_classe", "name": "Classe", "slug": "classe", "type": "string/=", "sectionId": "string"},
210
+ {"id": "p_jornada_trabalho", "name": "Jornada de Trabalho", "slug": "jornada_trabalho", "type": "string/=", "sectionId": "string"},
211
+ {"id": "p_titulacao", "name": "Titulacao", "slug": "titulacao", "type": "string/=", "sectionId": "string"},
212
+ {"id": "p_vinculo_carreira", "name": "Vinculo de Carreira", "slug": "vinculo_carreira", "type": "string/=", "sectionId": "string"},
213
+ {"id": "p_vinculo_contrato", "name": "Vinculo de Contrato", "slug": "vinculo_contrato", "type": "string/=", "sectionId": "string"},
214
+ {"id": "p_vinculo_professor", "name": "Vinculo Professor", "slug": "vinculo_professor", "type": "string/=", "sectionId": "string"},
215
+ {"id": "p_nome_uo", "name": "Nome UO", "slug": "nome_uo", "type": "string/=", "sectionId": "string"},
216
+ {"id": "p_grupo_despesa", "name": "Grupo de Despesa", "slug": "grupo_despesa", "type": "string/=", "sectionId": "string"},
217
+ {"id": "p_cod_acao", "name": "Codigo da Acao", "slug": "cod_acao", "type": "string/=", "sectionId": "string"},
218
+ {"id": "p_nome_acao", "name": "Nome da Acao", "slug": "nome_acao", "type": "string/=", "sectionId": "string"},
219
+ ]
220
+
221
+ PARAM_ID_BY_SLUG = {item["slug"]: item["id"] for item in DASHBOARD_PARAMETERS}
222
+
223
+ TEMPLATE_TAGS = {
224
+ slug: {
225
+ "name": slug,
226
+ "display-name": next(item["name"] for item in DASHBOARD_PARAMETERS if item["slug"] == slug),
227
+ "type": "number" if slug == "ano" else "text",
228
+ "required": False,
229
+ }
230
+ for slug in PARAM_ID_BY_SLUG
231
+ }
232
+
233
+
234
+ def where_clause(filters: tuple[str, ...]) -> str:
235
+ clauses = {
236
+ "ano": "ano = {{ano}}",
237
+ "instituicao": "instituicao = {{instituicao}}",
238
+ "regiao": "regiao = {{regiao}}",
239
+ "uf": "uf = {{uf}}",
240
+ "municipio": "municipio = {{municipio}}",
241
+ "sexo": "sexo = {{sexo}}",
242
+ "cor_raca": "cor_raca = {{cor_raca}}",
243
+ "renda_familiar": "renda_familiar = {{renda_familiar}}",
244
+ "faixa_etaria": "faixa_etaria = {{faixa_etaria}}",
245
+ "situacao_matricula": "situacao_matricula = {{situacao_matricula}}",
246
+ "modalidade_ensino": "modalidade_ensino = {{modalidade_ensino}}",
247
+ "tipo_curso": "tipo_curso = {{tipo_curso}}",
248
+ "tipo_oferta": "tipo_oferta = {{tipo_oferta}}",
249
+ "turno": "turno = {{turno}}",
250
+ "nome_curso": "nome_curso = {{nome_curso}}",
251
+ "matricula_atendida": "matricula_atendida = {{matricula_atendida}}",
252
+ "classe": "classe = {{classe}}",
253
+ "jornada_trabalho": "jornada_trabalho = {{jornada_trabalho}}",
254
+ "titulacao": "titulacao = {{titulacao}}",
255
+ "vinculo_carreira": "vinculo_carreira = {{vinculo_carreira}}",
256
+ "vinculo_contrato": "vinculo_contrato = {{vinculo_contrato}}",
257
+ "vinculo_professor": "vinculo_professor = {{vinculo_professor}}",
258
+ "nome_uo": "nome_uo = {{nome_uo}}",
259
+ "grupo_despesa": "grupo_despesa = {{grupo_despesa}}",
260
+ "cod_acao": "cod_acao = {{cod_acao}}",
261
+ "nome_acao": "nome_acao = {{nome_acao}}",
262
+ }
263
+ parts = ["WHERE 1=1"]
264
+ for slug in filters:
265
+ parts.append(f"[[ AND {clauses[slug]} ]]")
266
+ return "\n".join(parts)
267
+
268
+
269
+ MATRICULAS_FILTERS = (
270
+ "ano",
271
+ "instituicao",
272
+ "regiao",
273
+ "uf",
274
+ "municipio",
275
+ "sexo",
276
+ "cor_raca",
277
+ "renda_familiar",
278
+ "faixa_etaria",
279
+ "situacao_matricula",
280
+ "modalidade_ensino",
281
+ "tipo_curso",
282
+ "tipo_oferta",
283
+ "turno",
284
+ "nome_curso",
285
+ )
286
+
287
+ EFICIENCIA_FILTERS = (
288
+ "ano",
289
+ "instituicao",
290
+ "regiao",
291
+ "uf",
292
+ "municipio",
293
+ "sexo",
294
+ "cor_raca",
295
+ "renda_familiar",
296
+ "faixa_etaria",
297
+ "situacao_matricula",
298
+ "matricula_atendida",
299
+ )
300
+
301
+ SERVIDORES_FILTERS = (
302
+ "ano",
303
+ "instituicao",
304
+ "regiao",
305
+ "classe",
306
+ "jornada_trabalho",
307
+ "titulacao",
308
+ "vinculo_carreira",
309
+ "vinculo_contrato",
310
+ "vinculo_professor",
311
+ )
312
+
313
+ FINANCEIRO_FILTERS = ("ano", "nome_uo", "grupo_despesa", "cod_acao", "nome_acao")
314
+
315
+ MATRICULAS_SOURCE = "curated.mv_pnp_dashboard_matriculas"
316
+ EFICIENCIA_SOURCE = "curated.mv_pnp_dashboard_eficiencia"
317
+ SERVIDORES_SOURCE = "curated.mv_pnp_dashboard_servidores"
318
+ FINANCEIRO_SOURCE = "curated.mv_pnp_dashboard_financeiro"
319
+ QUALIDADE_SOURCE = "curated.mv_pnp_dashboard_qualidade"
320
+ INGESTAO_SOURCE = "curated.mv_pnp_dashboard_ingestao"
321
+
322
+ VIRTUALS = [
323
+ VirtualCardDef("Matriculas", "Oferta, procura, perfil discente e situacao das matriculas.", 0, 0, 24, 1),
324
+ VirtualCardDef("Eficiencia Academica", "Permanencia, conclusao e evasao por perfil e territorio.", 0, 0, 24, 1),
325
+ VirtualCardDef("Servidores", "Composicao do quadro por instituicao, titulacao, jornada e vinculos.", 0, 0, 24, 1),
326
+ VirtualCardDef("Financeiro", "Execucao financeira por UO, acao e grupo de despesa.", 0, 0, 24, 1),
327
+ VirtualCardDef("Qualidade e Ingestao", "Qualidade basica dos microdados e trilha operacional da carga.", 0, 0, 24, 1),
328
+ ]
329
+
330
+ CARDS = [
331
+ CardDef(
332
+ name="PNP 2024 - KPI Matriculas",
333
+ description="Total de matriculas no recorte.",
334
+ tab="Matriculas",
335
+ row=1,
336
+ col=0,
337
+ size_x=6,
338
+ size_y=4,
339
+ sql=f"SELECT COALESCE(SUM(matriculas), 0) AS matriculas FROM {MATRICULAS_SOURCE}\n{where_clause(MATRICULAS_FILTERS)}",
340
+ display="scalar",
341
+ filters=MATRICULAS_FILTERS,
342
+ visualization_settings={},
343
+ ),
344
+ CardDef(
345
+ name="PNP 2024 - KPI Inscritos",
346
+ description="Total de inscritos no recorte.",
347
+ tab="Matriculas",
348
+ row=1,
349
+ col=6,
350
+ size_x=6,
351
+ size_y=4,
352
+ sql=f"SELECT COALESCE(SUM(inscritos), 0) AS inscritos FROM {MATRICULAS_SOURCE}\n{where_clause(MATRICULAS_FILTERS)}",
353
+ display="scalar",
354
+ filters=MATRICULAS_FILTERS,
355
+ visualization_settings={},
356
+ ),
357
+ CardDef(
358
+ name="PNP 2024 - KPI Vagas Ofertadas",
359
+ description="Total de vagas ofertadas no recorte.",
360
+ tab="Matriculas",
361
+ row=1,
362
+ col=12,
363
+ size_x=6,
364
+ size_y=4,
365
+ sql=f"SELECT COALESCE(SUM(vagas_ofertadas), 0) AS vagas_ofertadas FROM {MATRICULAS_SOURCE}\n{where_clause(MATRICULAS_FILTERS)}",
366
+ display="scalar",
367
+ filters=MATRICULAS_FILTERS,
368
+ visualization_settings={},
369
+ ),
370
+ CardDef(
371
+ name="PNP 2024 - KPI Relacao Inscritos por Vaga",
372
+ description="Razao entre inscritos e vagas ofertadas.",
373
+ tab="Matriculas",
374
+ row=1,
375
+ col=18,
376
+ size_x=6,
377
+ size_y=4,
378
+ sql=f"""SELECT
379
+ ROUND(COALESCE(SUM(inscritos), 0) / NULLIF(COALESCE(SUM(vagas_ofertadas), 0), 0), 2) AS inscritos_por_vaga
380
+ FROM {MATRICULAS_SOURCE}
381
+ {where_clause(MATRICULAS_FILTERS)}""",
382
+ display="scalar",
383
+ filters=MATRICULAS_FILTERS,
384
+ visualization_settings={},
385
+ ),
386
+ CardDef(
387
+ name="PNP 2024 - Matriculas por Situacao",
388
+ description="Distribuicao das matriculas por situacao.",
389
+ tab="Matriculas",
390
+ row=5,
391
+ col=0,
392
+ size_x=8,
393
+ size_y=6,
394
+ sql=f"""SELECT situacao_matricula, SUM(matriculas) AS matriculas
395
+ FROM {MATRICULAS_SOURCE}
396
+ {where_clause(MATRICULAS_FILTERS)}
397
+ GROUP BY situacao_matricula
398
+ ORDER BY matriculas DESC NULLS LAST""",
399
+ display="bar",
400
+ filters=MATRICULAS_FILTERS,
401
+ visualization_settings={"graph.dimensions": ["situacao_matricula"], "graph.metrics": ["matriculas"]},
402
+ ),
403
+ CardDef(
404
+ name="PNP 2024 - Matriculas por Sexo",
405
+ description="Perfil por sexo.",
406
+ tab="Matriculas",
407
+ row=5,
408
+ col=8,
409
+ size_x=8,
410
+ size_y=6,
411
+ sql=f"""SELECT sexo, SUM(matriculas) AS matriculas
412
+ FROM {MATRICULAS_SOURCE}
413
+ {where_clause(MATRICULAS_FILTERS)}
414
+ GROUP BY sexo
415
+ ORDER BY matriculas DESC NULLS LAST""",
416
+ display="bar",
417
+ filters=MATRICULAS_FILTERS,
418
+ visualization_settings={"graph.dimensions": ["sexo"], "graph.metrics": ["matriculas"]},
419
+ ),
420
+ CardDef(
421
+ name="PNP 2024 - Matriculas por Tipo de Curso",
422
+ description="Distribuicao por tipo de curso.",
423
+ tab="Matriculas",
424
+ row=5,
425
+ col=16,
426
+ size_x=8,
427
+ size_y=6,
428
+ sql=f"""SELECT tipo_curso, SUM(matriculas) AS matriculas
429
+ FROM {MATRICULAS_SOURCE}
430
+ {where_clause(MATRICULAS_FILTERS)}
431
+ GROUP BY tipo_curso
432
+ ORDER BY matriculas DESC NULLS LAST""",
433
+ display="bar",
434
+ filters=MATRICULAS_FILTERS,
435
+ visualization_settings={"graph.dimensions": ["tipo_curso"], "graph.metrics": ["matriculas"]},
436
+ ),
437
+ CardDef(
438
+ name="PNP 2024 - Oferta por Curso",
439
+ description="Oferta, inscritos e matriculas por curso.",
440
+ tab="Matriculas",
441
+ row=11,
442
+ col=0,
443
+ size_x=24,
444
+ size_y=7,
445
+ sql=f"""SELECT
446
+ nome_curso,
447
+ tipo_curso,
448
+ modalidade_ensino,
449
+ tipo_oferta,
450
+ turno,
451
+ SUM(matriculas) AS matriculas,
452
+ SUM(vagas_ofertadas) AS vagas_ofertadas,
453
+ SUM(inscritos) AS inscritos
454
+ FROM {MATRICULAS_SOURCE}
455
+ {where_clause(MATRICULAS_FILTERS)}
456
+ GROUP BY nome_curso, tipo_curso, modalidade_ensino, tipo_oferta, turno
457
+ ORDER BY matriculas DESC NULLS LAST
458
+ LIMIT 20""",
459
+ display="table",
460
+ filters=MATRICULAS_FILTERS,
461
+ visualization_settings={},
462
+ ),
463
+ CardDef(
464
+ name="PNP 2024 - KPI Registros de Eficiencia",
465
+ description="Total de registros de eficiencia academica.",
466
+ tab="Eficiencia Academica",
467
+ row=1,
468
+ col=0,
469
+ size_x=6,
470
+ size_y=4,
471
+ sql=f"SELECT COALESCE(SUM(registros), 0) AS registros_eficiencia FROM {EFICIENCIA_SOURCE}\n{where_clause(EFICIENCIA_FILTERS)}",
472
+ display="scalar",
473
+ filters=EFICIENCIA_FILTERS,
474
+ visualization_settings={},
475
+ ),
476
+ CardDef(
477
+ name="PNP 2024 - KPI Concluintes",
478
+ description="Total de concluintes no recorte.",
479
+ tab="Eficiencia Academica",
480
+ row=1,
481
+ col=6,
482
+ size_x=6,
483
+ size_y=4,
484
+ sql=f"""SELECT COALESCE(SUM(registros), 0) AS concluintes
485
+ FROM {EFICIENCIA_SOURCE}
486
+ {where_clause(EFICIENCIA_FILTERS)}
487
+ AND categoria_situacao = 'Concluintes'""",
488
+ display="scalar",
489
+ filters=EFICIENCIA_FILTERS,
490
+ visualization_settings={},
491
+ ),
492
+ CardDef(
493
+ name="PNP 2024 - KPI Evadidos",
494
+ description="Total de evadidos no recorte.",
495
+ tab="Eficiencia Academica",
496
+ row=1,
497
+ col=12,
498
+ size_x=6,
499
+ size_y=4,
500
+ sql=f"""SELECT COALESCE(SUM(registros), 0) AS evadidos
501
+ FROM {EFICIENCIA_SOURCE}
502
+ {where_clause(EFICIENCIA_FILTERS)}
503
+ AND categoria_situacao = 'Evadidos'""",
504
+ display="scalar",
505
+ filters=EFICIENCIA_FILTERS,
506
+ visualization_settings={},
507
+ ),
508
+ CardDef(
509
+ name="PNP 2024 - KPI Em Curso",
510
+ description="Total de registros em curso.",
511
+ tab="Eficiencia Academica",
512
+ row=1,
513
+ col=18,
514
+ size_x=6,
515
+ size_y=4,
516
+ sql=f"""SELECT COALESCE(SUM(registros), 0) AS em_curso
517
+ FROM {EFICIENCIA_SOURCE}
518
+ {where_clause(EFICIENCIA_FILTERS)}
519
+ AND categoria_situacao = 'Em curso'""",
520
+ display="scalar",
521
+ filters=EFICIENCIA_FILTERS,
522
+ visualization_settings={},
523
+ ),
524
+ CardDef(
525
+ name="PNP 2024 - Eficiencia por Categoria",
526
+ description="Distribuicao por categoria.",
527
+ tab="Eficiencia Academica",
528
+ row=5,
529
+ col=0,
530
+ size_x=12,
531
+ size_y=6,
532
+ sql=f"""SELECT categoria_situacao, SUM(registros) AS registros
533
+ FROM {EFICIENCIA_SOURCE}
534
+ {where_clause(EFICIENCIA_FILTERS)}
535
+ GROUP BY categoria_situacao
536
+ ORDER BY registros DESC NULLS LAST""",
537
+ display="bar",
538
+ filters=EFICIENCIA_FILTERS,
539
+ visualization_settings={"graph.dimensions": ["categoria_situacao"], "graph.metrics": ["registros"]},
540
+ ),
541
+ CardDef(
542
+ name="PNP 2024 - Eficiencia por UF",
543
+ description="Distribuicao territorial.",
544
+ tab="Eficiencia Academica",
545
+ row=5,
546
+ col=12,
547
+ size_x=12,
548
+ size_y=6,
549
+ sql=f"""SELECT uf, SUM(registros) AS registros
550
+ FROM {EFICIENCIA_SOURCE}
551
+ {where_clause(EFICIENCIA_FILTERS)}
552
+ GROUP BY uf
553
+ ORDER BY registros DESC NULLS LAST""",
554
+ display="bar",
555
+ filters=EFICIENCIA_FILTERS,
556
+ visualization_settings={"graph.dimensions": ["uf"], "graph.metrics": ["registros"]},
557
+ ),
558
+ CardDef(
559
+ name="PNP 2024 - Situacao Academica por Instituicao",
560
+ description="Detalhamento por instituicao e categoria.",
561
+ tab="Eficiencia Academica",
562
+ row=11,
563
+ col=0,
564
+ size_x=24,
565
+ size_y=7,
566
+ sql=f"""SELECT instituicao, categoria_situacao, SUM(registros) AS registros
567
+ FROM {EFICIENCIA_SOURCE}
568
+ {where_clause(EFICIENCIA_FILTERS)}
569
+ GROUP BY instituicao, categoria_situacao
570
+ ORDER BY registros DESC NULLS LAST
571
+ LIMIT 25""",
572
+ display="table",
573
+ filters=EFICIENCIA_FILTERS,
574
+ visualization_settings={},
575
+ ),
576
+ CardDef(
577
+ name="PNP 2024 - KPI Total de Servidores",
578
+ description="Total de servidores no recorte.",
579
+ tab="Servidores",
580
+ row=1,
581
+ col=0,
582
+ size_x=6,
583
+ size_y=4,
584
+ sql=f"SELECT COALESCE(SUM(servidores), 0) AS servidores FROM {SERVIDORES_SOURCE}\n{where_clause(SERVIDORES_FILTERS)}",
585
+ display="scalar",
586
+ filters=SERVIDORES_FILTERS,
587
+ visualization_settings={},
588
+ ),
589
+ CardDef(
590
+ name="PNP 2024 - KPI Instituicoes com Servidores",
591
+ description="Quantidade de instituicoes no recorte.",
592
+ tab="Servidores",
593
+ row=1,
594
+ col=6,
595
+ size_x=6,
596
+ size_y=4,
597
+ sql=f"SELECT COUNT(DISTINCT instituicao) AS instituicoes FROM {SERVIDORES_SOURCE}\n{where_clause(SERVIDORES_FILTERS)}",
598
+ display="scalar",
599
+ filters=SERVIDORES_FILTERS,
600
+ visualization_settings={},
601
+ ),
602
+ CardDef(
603
+ name="PNP 2024 - KPI Titulacoes Distintas",
604
+ description="Quantidade de titulacoes distintas.",
605
+ tab="Servidores",
606
+ row=1,
607
+ col=12,
608
+ size_x=6,
609
+ size_y=4,
610
+ sql=f"SELECT COUNT(DISTINCT titulacao) AS titulacoes_distintas FROM {SERVIDORES_SOURCE}\n{where_clause(SERVIDORES_FILTERS)}",
611
+ display="scalar",
612
+ filters=SERVIDORES_FILTERS,
613
+ visualization_settings={},
614
+ ),
615
+ CardDef(
616
+ name="PNP 2024 - KPI Vinculos Distintos",
617
+ description="Quantidade de vinculos distintos.",
618
+ tab="Servidores",
619
+ row=1,
620
+ col=18,
621
+ size_x=6,
622
+ size_y=4,
623
+ sql=f"SELECT COUNT(DISTINCT vinculo_carreira) AS vinculos_distintos FROM {SERVIDORES_SOURCE}\n{where_clause(SERVIDORES_FILTERS)}",
624
+ display="scalar",
625
+ filters=SERVIDORES_FILTERS,
626
+ visualization_settings={},
627
+ ),
628
+ CardDef(
629
+ name="PNP 2024 - Servidores por Titulacao",
630
+ description="Distribuicao por titulacao.",
631
+ tab="Servidores",
632
+ row=5,
633
+ col=0,
634
+ size_x=8,
635
+ size_y=6,
636
+ sql=f"""SELECT titulacao, SUM(servidores) AS servidores
637
+ FROM {SERVIDORES_SOURCE}
638
+ {where_clause(SERVIDORES_FILTERS)}
639
+ GROUP BY titulacao
640
+ ORDER BY servidores DESC NULLS LAST""",
641
+ display="bar",
642
+ filters=SERVIDORES_FILTERS,
643
+ visualization_settings={"graph.dimensions": ["titulacao"], "graph.metrics": ["servidores"]},
644
+ ),
645
+ CardDef(
646
+ name="PNP 2024 - Servidores por Jornada",
647
+ description="Distribuicao por jornada.",
648
+ tab="Servidores",
649
+ row=5,
650
+ col=8,
651
+ size_x=8,
652
+ size_y=6,
653
+ sql=f"""SELECT jornada_trabalho, SUM(servidores) AS servidores
654
+ FROM {SERVIDORES_SOURCE}
655
+ {where_clause(SERVIDORES_FILTERS)}
656
+ GROUP BY jornada_trabalho
657
+ ORDER BY servidores DESC NULLS LAST""",
658
+ display="bar",
659
+ filters=SERVIDORES_FILTERS,
660
+ visualization_settings={"graph.dimensions": ["jornada_trabalho"], "graph.metrics": ["servidores"]},
661
+ ),
662
+ CardDef(
663
+ name="PNP 2024 - Servidores por Vinculo",
664
+ description="Distribuicao por vinculo de carreira.",
665
+ tab="Servidores",
666
+ row=5,
667
+ col=16,
668
+ size_x=8,
669
+ size_y=6,
670
+ sql=f"""SELECT vinculo_carreira, SUM(servidores) AS servidores
671
+ FROM {SERVIDORES_SOURCE}
672
+ {where_clause(SERVIDORES_FILTERS)}
673
+ GROUP BY vinculo_carreira
674
+ ORDER BY servidores DESC NULLS LAST""",
675
+ display="bar",
676
+ filters=SERVIDORES_FILTERS,
677
+ visualization_settings={"graph.dimensions": ["vinculo_carreira"], "graph.metrics": ["servidores"]},
678
+ ),
679
+ CardDef(
680
+ name="PNP 2024 - Servidores por Instituicao",
681
+ description="Ranking institucional.",
682
+ tab="Servidores",
683
+ row=11,
684
+ col=0,
685
+ size_x=24,
686
+ size_y=7,
687
+ sql=f"""SELECT instituicao, SUM(servidores) AS servidores
688
+ FROM {SERVIDORES_SOURCE}
689
+ {where_clause(SERVIDORES_FILTERS)}
690
+ GROUP BY instituicao
691
+ ORDER BY servidores DESC NULLS LAST
692
+ LIMIT 20""",
693
+ display="table",
694
+ filters=SERVIDORES_FILTERS,
695
+ visualization_settings={},
696
+ ),
697
+ CardDef(
698
+ name="PNP 2024 - KPI Liquidacoes Totais",
699
+ description="Total liquidado no recorte.",
700
+ tab="Financeiro",
701
+ row=1,
702
+ col=0,
703
+ size_x=6,
704
+ size_y=4,
705
+ sql=f"SELECT COALESCE(SUM(liquidacoes_totais), 0) AS liquidacoes_totais FROM {FINANCEIRO_SOURCE}\n{where_clause(FINANCEIRO_FILTERS)}",
706
+ display="scalar",
707
+ filters=FINANCEIRO_FILTERS,
708
+ visualization_settings={},
709
+ ),
710
+ CardDef(
711
+ name="PNP 2024 - KPI UOs Distintas",
712
+ description="Quantidade de UOs distintas.",
713
+ tab="Financeiro",
714
+ row=1,
715
+ col=6,
716
+ size_x=6,
717
+ size_y=4,
718
+ sql=f"SELECT COUNT(DISTINCT nome_uo) AS uos_distintas FROM {FINANCEIRO_SOURCE}\n{where_clause(FINANCEIRO_FILTERS)}",
719
+ display="scalar",
720
+ filters=FINANCEIRO_FILTERS,
721
+ visualization_settings={},
722
+ ),
723
+ CardDef(
724
+ name="PNP 2024 - KPI Acoes Distintas",
725
+ description="Quantidade de acoes distintas.",
726
+ tab="Financeiro",
727
+ row=1,
728
+ col=12,
729
+ size_x=6,
730
+ size_y=4,
731
+ sql=f"SELECT COUNT(DISTINCT cod_acao) AS acoes_distintas FROM {FINANCEIRO_SOURCE}\n{where_clause(FINANCEIRO_FILTERS)}",
732
+ display="scalar",
733
+ filters=FINANCEIRO_FILTERS,
734
+ visualization_settings={},
735
+ ),
736
+ CardDef(
737
+ name="PNP 2024 - KPI Grupos de Despesa Distintos",
738
+ description="Quantidade de grupos de despesa distintos.",
739
+ tab="Financeiro",
740
+ row=1,
741
+ col=18,
742
+ size_x=6,
743
+ size_y=4,
744
+ sql=f"SELECT COUNT(DISTINCT grupo_despesa) AS grupos_despesa_distintos FROM {FINANCEIRO_SOURCE}\n{where_clause(FINANCEIRO_FILTERS)}",
745
+ display="scalar",
746
+ filters=FINANCEIRO_FILTERS,
747
+ visualization_settings={},
748
+ ),
749
+ CardDef(
750
+ name="PNP 2024 - Liquidacoes por Grupo de Despesa",
751
+ description="Distribuicao por grupo de despesa.",
752
+ tab="Financeiro",
753
+ row=5,
754
+ col=0,
755
+ size_x=10,
756
+ size_y=6,
757
+ sql=f"""SELECT grupo_despesa, SUM(liquidacoes_totais) AS liquidacoes_totais
758
+ FROM {FINANCEIRO_SOURCE}
759
+ {where_clause(FINANCEIRO_FILTERS)}
760
+ GROUP BY grupo_despesa
761
+ ORDER BY liquidacoes_totais DESC NULLS LAST""",
762
+ display="bar",
763
+ filters=FINANCEIRO_FILTERS,
764
+ visualization_settings={"graph.dimensions": ["grupo_despesa"], "graph.metrics": ["liquidacoes_totais"]},
765
+ ),
766
+ CardDef(
767
+ name="PNP 2024 - Top UOs por Liquidacoes",
768
+ description="Ranking de UOs por liquidacoes.",
769
+ tab="Financeiro",
770
+ row=5,
771
+ col=10,
772
+ size_x=14,
773
+ size_y=6,
774
+ sql=f"""SELECT nome_uo, SUM(liquidacoes_totais) AS liquidacoes_totais
775
+ FROM {FINANCEIRO_SOURCE}
776
+ {where_clause(FINANCEIRO_FILTERS)}
777
+ GROUP BY nome_uo
778
+ ORDER BY liquidacoes_totais DESC NULLS LAST
779
+ LIMIT 15""",
780
+ display="bar",
781
+ filters=FINANCEIRO_FILTERS,
782
+ visualization_settings={"graph.dimensions": ["nome_uo"], "graph.metrics": ["liquidacoes_totais"]},
783
+ ),
784
+ CardDef(
785
+ name="PNP 2024 - Execucao por Acao",
786
+ description="Detalhamento financeiro por acao.",
787
+ tab="Financeiro",
788
+ row=11,
789
+ col=0,
790
+ size_x=24,
791
+ size_y=7,
792
+ sql=f"""SELECT
793
+ nome_uo,
794
+ cod_acao,
795
+ nome_acao,
796
+ grupo_despesa,
797
+ SUM(liquidacoes_totais) AS liquidacoes_totais
798
+ FROM {FINANCEIRO_SOURCE}
799
+ {where_clause(FINANCEIRO_FILTERS)}
800
+ GROUP BY nome_uo, cod_acao, nome_acao, grupo_despesa
801
+ ORDER BY liquidacoes_totais DESC NULLS LAST
802
+ LIMIT 25""",
803
+ display="table",
804
+ filters=FINANCEIRO_FILTERS,
805
+ visualization_settings={},
806
+ ),
807
+ CardDef(
808
+ name="PNP 2024 - Qualidade por Tipo de Microdado",
809
+ description="Cobertura basica dos microdados.",
810
+ tab="Qualidade e Ingestao",
811
+ row=1,
812
+ col=0,
813
+ size_x=14,
814
+ size_y=8,
815
+ sql=f"""SELECT
816
+ tipo_microdados,
817
+ registros,
818
+ registros_sem_instituicao,
819
+ registros_sem_uf,
820
+ registros_sem_sexo,
821
+ registros_sem_cor_raca,
822
+ registros_sem_renda_familiar,
823
+ registros_sem_faixa_etaria,
824
+ registros_financeiros_sem_valor,
825
+ registros_servidores_sem_quantidade,
826
+ pct_sem_instituicao,
827
+ pct_sem_uf
828
+ FROM {QUALIDADE_SOURCE}
829
+ ORDER BY tipo_microdados""",
830
+ display="table",
831
+ filters=(),
832
+ visualization_settings={},
833
+ ),
834
+ CardDef(
835
+ name="PNP 2024 - Execucoes de Ingestao",
836
+ description="Trilha operacional das execucoes da carga.",
837
+ tab="Qualidade e Ingestao",
838
+ row=1,
839
+ col=14,
840
+ size_x=10,
841
+ size_y=8,
842
+ sql=f"""SELECT
843
+ run_id,
844
+ status,
845
+ endpoint_key,
846
+ loaded_count,
847
+ registros_raw,
848
+ downloads_total,
849
+ manifests_total,
850
+ started_at,
851
+ finished_at
852
+ FROM {INGESTAO_SOURCE}
853
+ ORDER BY started_at DESC NULLS LAST
854
+ LIMIT 20""",
855
+ display="table",
856
+ filters=(),
857
+ visualization_settings={},
858
+ ),
859
+ ]
860
+
861
+
862
+ def build_virtual_dashcard(card: VirtualCardDef, tab_id: int, dashcard_id: int) -> dict:
863
+ return {
864
+ "id": dashcard_id,
865
+ "card_id": None,
866
+ "dashboard_tab_id": tab_id,
867
+ "row": card.row,
868
+ "col": card.col,
869
+ "size_x": card.size_x,
870
+ "size_y": card.size_y,
871
+ "parameter_mappings": [],
872
+ "visualization_settings": {
873
+ "dashcard.background": False,
874
+ "text": card.text,
875
+ "virtual_card": {
876
+ "archived": False,
877
+ "dataset_query": {},
878
+ "display": "text",
879
+ "visualization_settings": {},
880
+ },
881
+ },
882
+ }
883
+
884
+
885
+ def build_real_dashcard(card: CardDef, tab_id: int, dashcard_id: int, card_id: int) -> dict:
886
+ return {
887
+ "id": dashcard_id,
888
+ "card_id": card_id,
889
+ "dashboard_tab_id": tab_id,
890
+ "row": card.row,
891
+ "col": card.col,
892
+ "size_x": card.size_x,
893
+ "size_y": card.size_y,
894
+ "parameter_mappings": card.parameter_mappings(),
895
+ }
896
+
897
+
898
+ def main() -> int:
899
+ cleanup_previous_pnp_cards()
900
+ dashboard_id = ensure_dashboard()
901
+
902
+ next_tab_id = -1
903
+ tabs_payload = []
904
+ tab_ids: dict[str, int] = {}
905
+ for position, tab_name in enumerate(TAB_LAYOUT):
906
+ tab_ids[tab_name] = next_tab_id
907
+ tabs_payload.append({"id": next_tab_id, "name": tab_name, "position": position})
908
+ next_tab_id -= 1
909
+
910
+ next_dashcard_id = -1
911
+ dashcards = []
912
+
913
+ for virtual in VIRTUALS:
914
+ dashcards.append(build_virtual_dashcard(virtual, tab_ids[virtual.tab], next_dashcard_id))
915
+ next_dashcard_id -= 1
916
+
917
+ created_cards = []
918
+ for card in CARDS:
919
+ card_id = create_card(card)
920
+ created_cards.append(card_id)
921
+ dashcards.append(build_real_dashcard(card, tab_ids[card.tab], next_dashcard_id, card_id))
922
+ next_dashcard_id -= 1
923
+
924
+ update_dashboard(dashboard_id, tabs_payload, dashcards)
925
+ dashboard = api("GET", f"/dashboard/{dashboard_id}")
926
+
927
+ print(
928
+ json.dumps(
929
+ {
930
+ "dashboard_id": dashboard_id,
931
+ "tabs_total": len(dashboard.get("tabs", [])),
932
+ "dashcards_total": len(dashboard.get("dashcards", [])),
933
+ "cards_created": len(created_cards),
934
+ "filters_total": len(dashboard.get("parameters", [])),
935
+ },
936
+ ensure_ascii=True,
937
+ )
938
+ )
939
+ return 0
940
+
941
+
942
+ if __name__ == "__main__":
943
+ sys.exit(main())