sidra-sql 1.3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- sidra_sql/__init__.py +15 -0
- sidra_sql/cli.py +631 -0
- sidra_sql/config.py +137 -0
- sidra_sql/database.py +793 -0
- sidra_sql/exporter.py +171 -0
- sidra_sql/models.py +230 -0
- sidra_sql/plugin_manager.py +200 -0
- sidra_sql/runner.py +76 -0
- sidra_sql/scaffold.py +248 -0
- sidra_sql/sidra.py +362 -0
- sidra_sql/storage.py +156 -0
- sidra_sql/toml_runner.py +374 -0
- sidra_sql/transform_runner.py +153 -0
- sidra_sql/utils.py +106 -0
- sidra_sql/validator.py +222 -0
- sidra_sql-1.3.0.dist-info/METADATA +629 -0
- sidra_sql-1.3.0.dist-info/RECORD +20 -0
- sidra_sql-1.3.0.dist-info/WHEEL +4 -0
- sidra_sql-1.3.0.dist-info/entry_points.txt +2 -0
- sidra_sql-1.3.0.dist-info/licenses/LICENSE +21 -0
sidra_sql/__init__.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
3
|
+
|
|
4
|
+
from . import config, database, sidra, storage
|
|
5
|
+
|
|
6
|
+
try:
|
|
7
|
+
__version__ = version("sidra-sql")
|
|
8
|
+
except PackageNotFoundError:
|
|
9
|
+
__version__ = "0.0.0"
|
|
10
|
+
|
|
11
|
+
__all__ = ["config", "database", "sidra", "storage"]
|
|
12
|
+
|
|
13
|
+
logging.getLogger(__name__).addHandler(logging.NullHandler())
|
|
14
|
+
|
|
15
|
+
config.setup_logging(__name__, "sidra-sql.log")
|
sidra_sql/cli.py
ADDED
|
@@ -0,0 +1,631 @@
|
|
|
1
|
+
import configparser
|
|
2
|
+
import datetime as dt
|
|
3
|
+
import logging
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import typer
|
|
7
|
+
from rich.align import Align
|
|
8
|
+
from rich.console import Console
|
|
9
|
+
from rich.panel import Panel
|
|
10
|
+
from rich.table import Table
|
|
11
|
+
from rich.text import Text
|
|
12
|
+
|
|
13
|
+
from sidra_sql import __version__
|
|
14
|
+
from sidra_sql.config import (
|
|
15
|
+
GLOBAL_CONFIG_PATH,
|
|
16
|
+
LOCAL_CONFIG_PATH,
|
|
17
|
+
Config,
|
|
18
|
+
ConfigError,
|
|
19
|
+
)
|
|
20
|
+
from sidra_sql.exporter import Exporter
|
|
21
|
+
from sidra_sql.plugin_manager import PluginManager
|
|
22
|
+
from sidra_sql.runner import run_subtree
|
|
23
|
+
from sidra_sql.scaffold import PipelineAdder, PluginScaffolder
|
|
24
|
+
from sidra_sql.transform_runner import TransformRunner
|
|
25
|
+
from sidra_sql.validator import PluginValidator, Severity
|
|
26
|
+
|
|
27
|
+
app = typer.Typer(help=f"Sidra-SQL CLI v{__version__} - Manage and run data pipelines")
|
|
28
|
+
plugin_app = typer.Typer(help="Manage pipeline plugins")
|
|
29
|
+
config_app = typer.Typer(help="Manage sidra-sql configuration")
|
|
30
|
+
app.add_typer(plugin_app, name="plugin")
|
|
31
|
+
app.add_typer(config_app, name="config")
|
|
32
|
+
|
|
33
|
+
console = Console()
|
|
34
|
+
manager = PluginManager()
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _print_header() -> None:
|
|
38
|
+
content = Align.center(
|
|
39
|
+
Text.assemble(
|
|
40
|
+
("sidra-sql", "bold cyan"),
|
|
41
|
+
(f" v{__version__}", "dim"),
|
|
42
|
+
"\n",
|
|
43
|
+
("Tabelas de dados agregados do IBGE", "dim"),
|
|
44
|
+
)
|
|
45
|
+
)
|
|
46
|
+
console.print(Panel(content, border_style="cyan dim", padding=(0, 0)))
|
|
47
|
+
console.print()
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _config_path(use_global: bool) -> Path:
|
|
51
|
+
return GLOBAL_CONFIG_PATH if use_global else LOCAL_CONFIG_PATH
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _read_config(path: Path) -> configparser.ConfigParser:
|
|
55
|
+
cfg = configparser.ConfigParser()
|
|
56
|
+
if path.exists():
|
|
57
|
+
cfg.read(path)
|
|
58
|
+
return cfg
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _write_config(cfg: configparser.ConfigParser, path: Path) -> None:
|
|
62
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
63
|
+
with open(path, "w") as f:
|
|
64
|
+
cfg.write(f)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@config_app.command("set")
|
|
68
|
+
def config_set(
|
|
69
|
+
key: str = typer.Argument(
|
|
70
|
+
..., help="Config key in section.option format (e.g. database.host)"
|
|
71
|
+
),
|
|
72
|
+
value: str = typer.Argument(..., help="Value to set"),
|
|
73
|
+
use_global: bool = typer.Option(
|
|
74
|
+
False,
|
|
75
|
+
"--global",
|
|
76
|
+
help="Write to global config (~/.config/quantilica/sidra-sql/config.ini)",
|
|
77
|
+
),
|
|
78
|
+
):
|
|
79
|
+
"""Set a configuration value."""
|
|
80
|
+
if "." not in key:
|
|
81
|
+
console.print(
|
|
82
|
+
"[bold red]Error:[/bold red] key must be in 'section.option' "
|
|
83
|
+
"format (e.g. database.host)"
|
|
84
|
+
)
|
|
85
|
+
raise typer.Exit(1)
|
|
86
|
+
|
|
87
|
+
section, option = key.split(".", 1)
|
|
88
|
+
path = _config_path(use_global)
|
|
89
|
+
cfg = _read_config(path)
|
|
90
|
+
|
|
91
|
+
if not cfg.has_section(section):
|
|
92
|
+
cfg.add_section(section)
|
|
93
|
+
cfg.set(section, option, value)
|
|
94
|
+
_write_config(cfg, path)
|
|
95
|
+
|
|
96
|
+
scope = "global" if use_global else "local"
|
|
97
|
+
console.print(f"[green]Set[/green] {key} = {value} ([dim]{scope}[/dim])")
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
@config_app.command("get")
|
|
101
|
+
def config_get(
|
|
102
|
+
key: str = typer.Argument(
|
|
103
|
+
..., help="Config key in section.option format (e.g. database.host)"
|
|
104
|
+
),
|
|
105
|
+
):
|
|
106
|
+
"""Get a configuration value (local overrides global)."""
|
|
107
|
+
if "." not in key:
|
|
108
|
+
console.print(
|
|
109
|
+
"[bold red]Error:[/bold red] key must be in 'section.option' "
|
|
110
|
+
"format (e.g. database.host)"
|
|
111
|
+
)
|
|
112
|
+
raise typer.Exit(1)
|
|
113
|
+
|
|
114
|
+
section, option = key.split(".", 1)
|
|
115
|
+
cfg = configparser.ConfigParser()
|
|
116
|
+
cfg.read([GLOBAL_CONFIG_PATH, LOCAL_CONFIG_PATH])
|
|
117
|
+
|
|
118
|
+
if not cfg.has_option(section, option):
|
|
119
|
+
console.print(f"[yellow]Key not found:[/yellow] {key}")
|
|
120
|
+
raise typer.Exit(1)
|
|
121
|
+
|
|
122
|
+
console.print(cfg.get(section, option))
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
@config_app.command("list")
|
|
126
|
+
def config_list(
|
|
127
|
+
use_global: bool = typer.Option(False, "--global", help="Show only global config"),
|
|
128
|
+
local: bool = typer.Option(False, "--local", help="Show only local config"),
|
|
129
|
+
):
|
|
130
|
+
"""List configuration values.
|
|
131
|
+
|
|
132
|
+
Without flags, shows merged view (local overrides global).
|
|
133
|
+
"""
|
|
134
|
+
if use_global:
|
|
135
|
+
paths = [GLOBAL_CONFIG_PATH]
|
|
136
|
+
label = "Global config"
|
|
137
|
+
elif local:
|
|
138
|
+
paths = [LOCAL_CONFIG_PATH]
|
|
139
|
+
label = "Local config"
|
|
140
|
+
else:
|
|
141
|
+
paths = [GLOBAL_CONFIG_PATH, LOCAL_CONFIG_PATH]
|
|
142
|
+
label = "Merged config (global + local)"
|
|
143
|
+
|
|
144
|
+
cfg = configparser.ConfigParser()
|
|
145
|
+
cfg.read(paths)
|
|
146
|
+
|
|
147
|
+
if not cfg.sections():
|
|
148
|
+
console.print("[yellow]No configuration found.[/yellow]")
|
|
149
|
+
return
|
|
150
|
+
|
|
151
|
+
table = Table(title=label, show_header=True, header_style="bold cyan")
|
|
152
|
+
table.add_column("Section")
|
|
153
|
+
table.add_column("Option")
|
|
154
|
+
table.add_column("Value")
|
|
155
|
+
|
|
156
|
+
for section in cfg.sections():
|
|
157
|
+
for option, value in cfg.items(section):
|
|
158
|
+
display = value if option != "password" else "*" * len(value)
|
|
159
|
+
table.add_row(section, option, display)
|
|
160
|
+
|
|
161
|
+
console.print(table)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _version_callback(value: bool):
|
|
165
|
+
if value:
|
|
166
|
+
console.print(f"sidra-sql {__version__}")
|
|
167
|
+
raise typer.Exit()
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
@app.callback()
|
|
171
|
+
def bootstrap(
|
|
172
|
+
version: bool | None = typer.Option(
|
|
173
|
+
None,
|
|
174
|
+
"--version",
|
|
175
|
+
"-V",
|
|
176
|
+
callback=_version_callback,
|
|
177
|
+
is_eager=True,
|
|
178
|
+
help="Exibe a versão e encerra.",
|
|
179
|
+
),
|
|
180
|
+
):
|
|
181
|
+
"""Inicialização automática do sistema."""
|
|
182
|
+
manager.ensure_defaults()
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
@plugin_app.command("install")
|
|
186
|
+
def install_plugin(
|
|
187
|
+
url: str,
|
|
188
|
+
alias: str | None = typer.Option(None, help="Alias for the plugin"),
|
|
189
|
+
):
|
|
190
|
+
"""Install a new plugin from a Git URL."""
|
|
191
|
+
try:
|
|
192
|
+
manager.install(url, alias)
|
|
193
|
+
console.print("[green]Plugin installed successfully.[/green]")
|
|
194
|
+
except Exception as e:
|
|
195
|
+
console.print(f"[red]Error installing plugin:[/red] {e}")
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
@plugin_app.command("update")
|
|
199
|
+
def update_plugin(
|
|
200
|
+
alias: str | None = typer.Argument(
|
|
201
|
+
None, help="Alias of the plugin to update (updates all if omitted)"
|
|
202
|
+
),
|
|
203
|
+
):
|
|
204
|
+
"""Update installed plugin(s) from Git."""
|
|
205
|
+
try:
|
|
206
|
+
manager.update(alias)
|
|
207
|
+
console.print("[green]Update completed.[/green]")
|
|
208
|
+
except Exception as e:
|
|
209
|
+
console.print(f"[red]Error updating plugin:[/red] {e}")
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
@plugin_app.command("remove")
|
|
213
|
+
def remove_plugin(
|
|
214
|
+
alias: str = typer.Argument(..., help="Alias of the plugin to remove"),
|
|
215
|
+
):
|
|
216
|
+
"""Remove an installed plugin."""
|
|
217
|
+
try:
|
|
218
|
+
manager.remove(alias)
|
|
219
|
+
console.print(f"[green]Plugin '{alias}' removed.[/green]")
|
|
220
|
+
except Exception as e:
|
|
221
|
+
console.print(f"[red]Error removing plugin:[/red] {e}")
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
@plugin_app.command("scaffold")
|
|
225
|
+
def scaffold_plugin(
|
|
226
|
+
name: str = typer.Argument(..., help="Nome do plugin (vira o diretório raiz)"),
|
|
227
|
+
description: str = typer.Option(
|
|
228
|
+
"", "--description", "-d", help="Descrição do plugin"
|
|
229
|
+
),
|
|
230
|
+
version: str = typer.Option("1.0.0", "--version", help="Versão semântica"),
|
|
231
|
+
output_dir: Path = typer.Option(
|
|
232
|
+
Path("."), "--output-dir", "-o", help="Diretório de saída"
|
|
233
|
+
),
|
|
234
|
+
git_init: bool = typer.Option(
|
|
235
|
+
True, "--git-init/--no-git-init", help="Inicializar repositório Git"
|
|
236
|
+
),
|
|
237
|
+
):
|
|
238
|
+
"""Cria a estrutura de arquivos para um novo plugin com templates prontos."""
|
|
239
|
+
try:
|
|
240
|
+
scaffolder = PluginScaffolder(name, description, version, output_dir, git_init)
|
|
241
|
+
plugin_dir = scaffolder.create()
|
|
242
|
+
slug = scaffolder.slug
|
|
243
|
+
|
|
244
|
+
console.print(
|
|
245
|
+
f"\n[bold green]Plugin '{name}' criado em {plugin_dir}[/bold green]\n"
|
|
246
|
+
)
|
|
247
|
+
console.print(" manifest.toml")
|
|
248
|
+
console.print(f" {slug}/")
|
|
249
|
+
console.print(" fetch.toml")
|
|
250
|
+
console.print(" transform.toml")
|
|
251
|
+
console.print(f" {slug}.sql")
|
|
252
|
+
console.print(" README.md")
|
|
253
|
+
if git_init:
|
|
254
|
+
console.print(" .gitignore")
|
|
255
|
+
|
|
256
|
+
console.print("\n[bold]Próximos passos:[/bold]")
|
|
257
|
+
console.print(
|
|
258
|
+
" 1. Edite [cyan]manifest.toml[/cyan] e ajuste a descrição do pipeline"
|
|
259
|
+
)
|
|
260
|
+
console.print(
|
|
261
|
+
f" 2. Em [cyan]{slug}/fetch.toml[/cyan], substitua XXXX pelo "
|
|
262
|
+
"ID da tabela SIDRA"
|
|
263
|
+
)
|
|
264
|
+
console.print(
|
|
265
|
+
f" 3. Ajuste [cyan]{slug}/{slug}.sql[/cyan] para a sua transformação"
|
|
266
|
+
)
|
|
267
|
+
console.print(
|
|
268
|
+
" 4. Publique o repositório e instale: "
|
|
269
|
+
"[dim]sidra-sql plugin install <git-url>[/dim]\n"
|
|
270
|
+
)
|
|
271
|
+
except FileExistsError as e:
|
|
272
|
+
console.print(f"[red]Erro:[/red] {e}")
|
|
273
|
+
raise typer.Exit(1) from e
|
|
274
|
+
except RuntimeError as e:
|
|
275
|
+
console.print(f"[red]Erro:[/red] {e}")
|
|
276
|
+
raise typer.Exit(1) from e
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
@plugin_app.command("add-pipeline")
|
|
280
|
+
def add_pipeline(
|
|
281
|
+
pipeline_id: str = typer.Argument(
|
|
282
|
+
..., help="ID do pipeline (usado em 'sidra-sql run')"
|
|
283
|
+
),
|
|
284
|
+
description: str = typer.Option(
|
|
285
|
+
"", "--description", "-d", help="Descrição do pipeline"
|
|
286
|
+
),
|
|
287
|
+
path: str = typer.Option(
|
|
288
|
+
"",
|
|
289
|
+
"--path",
|
|
290
|
+
"-p",
|
|
291
|
+
help="Caminho do diretório relativo ao plugin (default: pipeline-id)",
|
|
292
|
+
),
|
|
293
|
+
plugin_dir: Path = typer.Option(
|
|
294
|
+
Path("."),
|
|
295
|
+
"--plugin-dir",
|
|
296
|
+
help="Diretório raiz do plugin (default: diretório atual)",
|
|
297
|
+
),
|
|
298
|
+
):
|
|
299
|
+
"""Adiciona um novo pipeline a um plugin existente."""
|
|
300
|
+
try:
|
|
301
|
+
adder = PipelineAdder(pipeline_id, description, path, plugin_dir)
|
|
302
|
+
adder.add()
|
|
303
|
+
|
|
304
|
+
console.print(
|
|
305
|
+
f"\n[bold green]Pipeline '{pipeline_id}' adicionado[/bold green]\n"
|
|
306
|
+
)
|
|
307
|
+
console.print(f" {adder.path}/")
|
|
308
|
+
console.print(" fetch.toml")
|
|
309
|
+
console.print(" transform.toml")
|
|
310
|
+
console.print(f" {adder.slug}.sql")
|
|
311
|
+
console.print(" manifest.toml [dim](atualizado)[/dim]\n")
|
|
312
|
+
|
|
313
|
+
console.print("[bold]Próximos passos:[/bold]")
|
|
314
|
+
console.print(
|
|
315
|
+
f" 1. Em [cyan]{adder.path}/fetch.toml[/cyan], substitua XXXX "
|
|
316
|
+
"pelo ID da tabela SIDRA"
|
|
317
|
+
)
|
|
318
|
+
console.print(
|
|
319
|
+
f" 2. Ajuste [cyan]{adder.path}/{adder.slug}.sql[/cyan] para "
|
|
320
|
+
"a sua transformação"
|
|
321
|
+
)
|
|
322
|
+
console.print(f" 3. Execute: [dim]sidra-sql run <alias> {pipeline_id}[/dim]\n")
|
|
323
|
+
except (FileNotFoundError, FileExistsError, ValueError) as e:
|
|
324
|
+
console.print(f"[red]Erro:[/red] {e}")
|
|
325
|
+
raise typer.Exit(1) from e
|
|
326
|
+
except Exception as e:
|
|
327
|
+
console.print(f"[red]Erro ao adicionar pipeline:[/red] {e}")
|
|
328
|
+
raise typer.Exit(1) from e
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
@plugin_app.command("validate")
|
|
332
|
+
def validate_plugin(
|
|
333
|
+
alias: str | None = typer.Argument(
|
|
334
|
+
None, help="Alias do plugin instalado (omitir para usar --plugin-dir)"
|
|
335
|
+
),
|
|
336
|
+
plugin_dir: Path = typer.Option(
|
|
337
|
+
Path("."),
|
|
338
|
+
"--plugin-dir",
|
|
339
|
+
help="Diretório raiz do plugin (default: diretório atual)",
|
|
340
|
+
),
|
|
341
|
+
):
|
|
342
|
+
"""Valida a estrutura e os arquivos de um plugin."""
|
|
343
|
+
if alias is not None:
|
|
344
|
+
target_dir = manager.registry.get_plugin_path(alias)
|
|
345
|
+
if not target_dir.exists():
|
|
346
|
+
console.print(f"[red]Erro:[/red] Plugin '{alias}' não encontrado.")
|
|
347
|
+
raise typer.Exit(1)
|
|
348
|
+
else:
|
|
349
|
+
target_dir = plugin_dir
|
|
350
|
+
|
|
351
|
+
console.print(f"\n[bold]Validando plugin em[/bold] {target_dir}\n")
|
|
352
|
+
|
|
353
|
+
report = PluginValidator(target_dir).validate()
|
|
354
|
+
|
|
355
|
+
severity_style = {
|
|
356
|
+
Severity.OK: "[green]OK[/green]",
|
|
357
|
+
Severity.WARN: "[yellow]AVISO[/yellow]",
|
|
358
|
+
Severity.ERROR: "[red]ERRO[/red]",
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
for section in report.sections:
|
|
362
|
+
console.print(f"[bold cyan]{section.title}[/bold cyan]")
|
|
363
|
+
for issue in section.issues:
|
|
364
|
+
tag = severity_style[issue.severity]
|
|
365
|
+
console.print(f" [{tag}] {issue.message}")
|
|
366
|
+
console.print()
|
|
367
|
+
|
|
368
|
+
if report.is_valid:
|
|
369
|
+
summary = "[bold green]Válido[/bold green]"
|
|
370
|
+
else:
|
|
371
|
+
summary = "[bold red]Inválido[/bold red]"
|
|
372
|
+
|
|
373
|
+
parts = [summary]
|
|
374
|
+
if report.error_count:
|
|
375
|
+
parts.append(f"[red]{report.error_count} erro(s)[/red]")
|
|
376
|
+
if report.warning_count:
|
|
377
|
+
parts.append(f"[yellow]{report.warning_count} aviso(s)[/yellow]")
|
|
378
|
+
if not report.error_count and not report.warning_count:
|
|
379
|
+
parts.append("sem erros ou avisos")
|
|
380
|
+
|
|
381
|
+
console.print("Resultado: " + ", ".join(parts) + "\n")
|
|
382
|
+
|
|
383
|
+
if not report.is_valid:
|
|
384
|
+
raise typer.Exit(1)
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
@plugin_app.command("list")
|
|
388
|
+
def list_plugins():
|
|
389
|
+
"""List installed plugins and their pipelines."""
|
|
390
|
+
try:
|
|
391
|
+
pipelines = manager.list_pipelines()
|
|
392
|
+
|
|
393
|
+
table = Table(title="Installed Pipelines")
|
|
394
|
+
table.add_column("Plugin Alias", style="cyan")
|
|
395
|
+
table.add_column("Pipeline ID", style="magenta")
|
|
396
|
+
table.add_column("Description", style="green")
|
|
397
|
+
|
|
398
|
+
for alias, _plugin_name, pipeline in pipelines:
|
|
399
|
+
table.add_row(alias, pipeline.id, pipeline.description)
|
|
400
|
+
|
|
401
|
+
console.print(table)
|
|
402
|
+
except Exception as e:
|
|
403
|
+
console.print(f"[red]Error listing plugins:[/red] {e}")
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
@app.command("run")
|
|
407
|
+
def run_pipeline(
|
|
408
|
+
alias: str = typer.Argument(..., help="Plugin alias"),
|
|
409
|
+
pipeline_id: str | None = typer.Argument(
|
|
410
|
+
None, help="Pipeline ID to run (omit to run all)"
|
|
411
|
+
),
|
|
412
|
+
force_metadata: bool = typer.Option(
|
|
413
|
+
False, "--force-metadata", help="Force refresh metadata"
|
|
414
|
+
),
|
|
415
|
+
force_load: bool = typer.Option(
|
|
416
|
+
False,
|
|
417
|
+
"--force-load",
|
|
418
|
+
help="Recarregar arquivos já registrados em arquivo_carregado",
|
|
419
|
+
),
|
|
420
|
+
):
|
|
421
|
+
"""Run pipeline(s) from an installed plugin. Omit pipeline_id to run all."""
|
|
422
|
+
try:
|
|
423
|
+
config = Config()
|
|
424
|
+
|
|
425
|
+
if pipeline_id is None:
|
|
426
|
+
manifest = manager.read_manifest(alias)
|
|
427
|
+
pipelines = manifest.pipelines
|
|
428
|
+
if not pipelines:
|
|
429
|
+
console.print(f"[yellow]No pipelines found in '{alias}'.[/yellow]")
|
|
430
|
+
return
|
|
431
|
+
_print_header()
|
|
432
|
+
for p in pipelines:
|
|
433
|
+
console.print(f"\n[cyan]→ {p.id}[/cyan]")
|
|
434
|
+
run_subtree(
|
|
435
|
+
config,
|
|
436
|
+
p.path,
|
|
437
|
+
force_metadata=force_metadata,
|
|
438
|
+
force_load=force_load,
|
|
439
|
+
console=console,
|
|
440
|
+
)
|
|
441
|
+
console.print(
|
|
442
|
+
"\n[bold green]All pipelines completed successfully![/bold green]"
|
|
443
|
+
)
|
|
444
|
+
else:
|
|
445
|
+
pipeline = manager.get_pipeline(alias, pipeline_id)
|
|
446
|
+
|
|
447
|
+
_print_header()
|
|
448
|
+
run_subtree(
|
|
449
|
+
config,
|
|
450
|
+
pipeline.path,
|
|
451
|
+
force_metadata=force_metadata,
|
|
452
|
+
force_load=force_load,
|
|
453
|
+
console=console,
|
|
454
|
+
)
|
|
455
|
+
|
|
456
|
+
console.print("[bold green]Pipeline completed successfully![/bold green]")
|
|
457
|
+
|
|
458
|
+
except ConfigError as e:
|
|
459
|
+
console.print(f"[bold yellow]{e}[/bold yellow]")
|
|
460
|
+
raise typer.Exit(1) from e
|
|
461
|
+
except Exception as e:
|
|
462
|
+
console.print(f"[bold red]Pipeline failed:[/bold red] {e}")
|
|
463
|
+
import traceback
|
|
464
|
+
|
|
465
|
+
traceback.print_exc()
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
@app.command("run-path")
|
|
469
|
+
def run_pipeline_path(
|
|
470
|
+
path: Path = typer.Argument(..., help="Path to the pipeline directory"),
|
|
471
|
+
force_metadata: bool = typer.Option(
|
|
472
|
+
False, "--force-metadata", help="Force refresh metadata"
|
|
473
|
+
),
|
|
474
|
+
force_load: bool = typer.Option(
|
|
475
|
+
False,
|
|
476
|
+
"--force-load",
|
|
477
|
+
help="Recarregar arquivos já registrados em arquivo_carregado",
|
|
478
|
+
),
|
|
479
|
+
):
|
|
480
|
+
"""Run a pipeline directly from a directory path, without a registered plugin."""
|
|
481
|
+
try:
|
|
482
|
+
resolved = path.resolve()
|
|
483
|
+
if not resolved.is_dir():
|
|
484
|
+
console.print(f"[bold red]Directory not found:[/bold red] {resolved}")
|
|
485
|
+
raise typer.Exit(1)
|
|
486
|
+
|
|
487
|
+
config = Config()
|
|
488
|
+
_print_header()
|
|
489
|
+
run_subtree(
|
|
490
|
+
config,
|
|
491
|
+
resolved,
|
|
492
|
+
force_metadata=force_metadata,
|
|
493
|
+
force_load=force_load,
|
|
494
|
+
console=console,
|
|
495
|
+
)
|
|
496
|
+
console.print("[bold green]Pipeline completed successfully![/bold green]")
|
|
497
|
+
except ConfigError as e:
|
|
498
|
+
console.print(f"[bold yellow]{e}[/bold yellow]")
|
|
499
|
+
raise typer.Exit(1) from e
|
|
500
|
+
except Exception as e:
|
|
501
|
+
console.print(f"[bold red]Pipeline failed:[/bold red] {e}")
|
|
502
|
+
import traceback
|
|
503
|
+
|
|
504
|
+
traceback.print_exc()
|
|
505
|
+
raise typer.Exit(1) from e
|
|
506
|
+
|
|
507
|
+
|
|
508
|
+
@app.command("transform")
|
|
509
|
+
def transform_pipeline(
|
|
510
|
+
alias: str = typer.Argument(..., help="Plugin alias"),
|
|
511
|
+
pipeline_id: str = typer.Argument(..., help="Pipeline ID to transform"),
|
|
512
|
+
):
|
|
513
|
+
"""Run only the transform step of a pipeline, without fetch or recursion."""
|
|
514
|
+
try:
|
|
515
|
+
config = Config()
|
|
516
|
+
pipeline = manager.get_pipeline(alias, pipeline_id)
|
|
517
|
+
|
|
518
|
+
transform_path = pipeline.path / "transform.toml"
|
|
519
|
+
if not transform_path.exists():
|
|
520
|
+
console.print(f"[red]No transform.toml found at {transform_path}[/red]")
|
|
521
|
+
raise typer.Exit(1)
|
|
522
|
+
|
|
523
|
+
console.print(f"[bold blue]Transforming {pipeline_id} from {alias}[/bold blue]")
|
|
524
|
+
TransformRunner(config, transform_path).run()
|
|
525
|
+
console.print("[bold green]Transform completed successfully![/bold green]")
|
|
526
|
+
|
|
527
|
+
except typer.Exit:
|
|
528
|
+
raise
|
|
529
|
+
except ConfigError as e:
|
|
530
|
+
console.print(f"[bold yellow]{e}[/bold yellow]")
|
|
531
|
+
raise typer.Exit(1) from e
|
|
532
|
+
except Exception as e:
|
|
533
|
+
console.print(f"[bold red]Transform failed:[/bold red] {e}")
|
|
534
|
+
import traceback
|
|
535
|
+
|
|
536
|
+
traceback.print_exc()
|
|
537
|
+
|
|
538
|
+
|
|
539
|
+
@app.command("export")
|
|
540
|
+
def export_pipeline(
|
|
541
|
+
alias: str = typer.Argument(..., help="Plugin alias"),
|
|
542
|
+
pipeline_id: str | None = typer.Argument(
|
|
543
|
+
None, help="Pipeline ID to export (omit to export all)"
|
|
544
|
+
),
|
|
545
|
+
output_dir: Path = typer.Option(
|
|
546
|
+
Path("export"),
|
|
547
|
+
"--output-dir",
|
|
548
|
+
"-o",
|
|
549
|
+
help="Diretório de saída dos CSVs",
|
|
550
|
+
),
|
|
551
|
+
as_of: str | None = typer.Option(
|
|
552
|
+
None,
|
|
553
|
+
"--as-of",
|
|
554
|
+
help="Snapshot do vintage publicado até a data (YYYY-MM-DD)",
|
|
555
|
+
),
|
|
556
|
+
):
|
|
557
|
+
"""Export a pipeline's analytics outputs to CSV (optionally an as-of snapshot).
|
|
558
|
+
|
|
559
|
+
Without --as-of, dumps the materialized analytics tables. With --as-of DATE,
|
|
560
|
+
reconstructs each output as IBGE published it on that date, without touching
|
|
561
|
+
the persisted analytics tables.
|
|
562
|
+
"""
|
|
563
|
+
try:
|
|
564
|
+
config = Config()
|
|
565
|
+
|
|
566
|
+
asof_date: dt.date | None = None
|
|
567
|
+
if as_of is not None:
|
|
568
|
+
try:
|
|
569
|
+
asof_date = dt.date.fromisoformat(as_of)
|
|
570
|
+
except ValueError as e:
|
|
571
|
+
console.print(
|
|
572
|
+
f"[bold red]Data inválida para --as-of:[/bold red] {as_of} "
|
|
573
|
+
"(use YYYY-MM-DD)"
|
|
574
|
+
)
|
|
575
|
+
raise typer.Exit(1) from e
|
|
576
|
+
|
|
577
|
+
if pipeline_id is None:
|
|
578
|
+
manifest = manager.read_manifest(alias)
|
|
579
|
+
pipelines = manifest.pipelines
|
|
580
|
+
if not pipelines:
|
|
581
|
+
console.print(f"[yellow]No pipelines found in '{alias}'.[/yellow]")
|
|
582
|
+
return
|
|
583
|
+
targets = [(p.id, p.path) for p in pipelines]
|
|
584
|
+
else:
|
|
585
|
+
pipeline = manager.get_pipeline(alias, pipeline_id)
|
|
586
|
+
targets = [(pipeline.id, pipeline.path)]
|
|
587
|
+
|
|
588
|
+
_print_header()
|
|
589
|
+
total = 0
|
|
590
|
+
for pid, path in targets:
|
|
591
|
+
toml_path = path / "transform.toml"
|
|
592
|
+
if not toml_path.exists():
|
|
593
|
+
console.print(
|
|
594
|
+
f"[yellow]Pipeline '{pid}' sem transform.toml — pulando.[/yellow]"
|
|
595
|
+
)
|
|
596
|
+
continue
|
|
597
|
+
console.print(f"\n[cyan]→ {pid}[/cyan]")
|
|
598
|
+
written = Exporter(config, toml_path, console=console).export(
|
|
599
|
+
output_dir, asof=asof_date
|
|
600
|
+
)
|
|
601
|
+
total += len(written)
|
|
602
|
+
|
|
603
|
+
console.print(
|
|
604
|
+
f"\n[bold green]Export concluído: {total} arquivo(s) em "
|
|
605
|
+
f"{output_dir}[/bold green]"
|
|
606
|
+
)
|
|
607
|
+
|
|
608
|
+
except typer.Exit:
|
|
609
|
+
raise
|
|
610
|
+
except ConfigError as e:
|
|
611
|
+
console.print(f"[bold yellow]{e}[/bold yellow]")
|
|
612
|
+
raise typer.Exit(1) from e
|
|
613
|
+
except Exception as e:
|
|
614
|
+
console.print(f"[bold red]Export failed:[/bold red] {e}")
|
|
615
|
+
import traceback
|
|
616
|
+
|
|
617
|
+
traceback.print_exc()
|
|
618
|
+
raise typer.Exit(1) from e
|
|
619
|
+
|
|
620
|
+
|
|
621
|
+
def main():
|
|
622
|
+
import sys
|
|
623
|
+
|
|
624
|
+
if hasattr(sys.stdout, "reconfigure"):
|
|
625
|
+
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
|
626
|
+
logging.basicConfig(level=logging.WARNING)
|
|
627
|
+
app()
|
|
628
|
+
|
|
629
|
+
|
|
630
|
+
if __name__ == "__main__":
|
|
631
|
+
main()
|