quantilica-cli 0.12.0__tar.gz → 0.13.0__tar.gz
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.
- quantilica_cli-0.13.0/.github/workflows/public-hygiene.yml +104 -0
- {quantilica_cli-0.12.0 → quantilica_cli-0.13.0}/CHANGELOG.md +18 -5
- {quantilica_cli-0.12.0 → quantilica_cli-0.13.0}/PKG-INFO +1 -1
- {quantilica_cli-0.12.0 → quantilica_cli-0.13.0}/pyproject.toml +1 -1
- {quantilica_cli-0.12.0 → quantilica_cli-0.13.0}/src/quantilica/cli/sdk.py +165 -30
- {quantilica_cli-0.12.0 → quantilica_cli-0.13.0}/src/quantilica/cli/sources.py +69 -15
- {quantilica_cli-0.12.0 → quantilica_cli-0.13.0}/tests/test_sdk.py +139 -0
- {quantilica_cli-0.12.0 → quantilica_cli-0.13.0}/tests/test_sdk_onda_a2.py +8 -5
- {quantilica_cli-0.12.0 → quantilica_cli-0.13.0}/tests/test_sources.py +31 -0
- {quantilica_cli-0.12.0 → quantilica_cli-0.13.0}/.githooks/pre-commit +0 -0
- {quantilica_cli-0.12.0 → quantilica_cli-0.13.0}/.githooks/pre-push +0 -0
- {quantilica_cli-0.12.0 → quantilica_cli-0.13.0}/.github/workflows/publish.yml +0 -0
- {quantilica_cli-0.12.0 → quantilica_cli-0.13.0}/.github/workflows/test.yml +0 -0
- {quantilica_cli-0.12.0 → quantilica_cli-0.13.0}/.gitignore +0 -0
- {quantilica_cli-0.12.0 → quantilica_cli-0.13.0}/LICENSE +0 -0
- {quantilica_cli-0.12.0 → quantilica_cli-0.13.0}/README.md +0 -0
- {quantilica_cli-0.12.0 → quantilica_cli-0.13.0}/src/quantilica/cli/__init__.py +0 -0
- {quantilica_cli-0.12.0 → quantilica_cli-0.13.0}/src/quantilica/cli/cli.py +0 -0
- {quantilica_cli-0.12.0 → quantilica_cli-0.13.0}/src/quantilica/cli/health.py +0 -0
- {quantilica_cli-0.12.0 → quantilica_cli-0.13.0}/src/quantilica/cli/manifests.py +0 -0
- {quantilica_cli-0.12.0 → quantilica_cli-0.13.0}/src/quantilica/cli/ui.py +0 -0
- {quantilica_cli-0.12.0 → quantilica_cli-0.13.0}/src/quantilica/py.typed +0 -0
- {quantilica_cli-0.12.0 → quantilica_cli-0.13.0}/tests/__init__.py +0 -0
- {quantilica_cli-0.12.0 → quantilica_cli-0.13.0}/tests/test_health.py +0 -0
- {quantilica_cli-0.12.0 → quantilica_cli-0.13.0}/tests/test_manifests.py +0 -0
- {quantilica_cli-0.12.0 → quantilica_cli-0.13.0}/tests/test_package_hygiene.py +0 -0
- {quantilica_cli-0.12.0 → quantilica_cli-0.13.0}/tests/test_sdk_force_probe.py +0 -0
- {quantilica_cli-0.12.0 → quantilica_cli-0.13.0}/tests/test_sdk_mes_string.py +0 -0
- {quantilica_cli-0.12.0 → quantilica_cli-0.13.0}/tests/test_ui.py +0 -0
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
name: Public hygiene
|
|
2
|
+
|
|
3
|
+
# Veta termos internos (planos, decisões, repositórios privados, infraestrutura)
|
|
4
|
+
# em CHANGELOG.md, README e nas mensagens de commit de PRs. O repositório é
|
|
5
|
+
# público: o que entra aqui fica no histórico para sempre.
|
|
6
|
+
# Regra e lista de termos: docs/normas/changelog.md, seção 5.1.
|
|
7
|
+
# Falso positivo legítimo: termine a linha com `<!-- hygiene-ok -->`.
|
|
8
|
+
|
|
9
|
+
on:
|
|
10
|
+
push:
|
|
11
|
+
branches: [main]
|
|
12
|
+
pull_request:
|
|
13
|
+
branches: [main]
|
|
14
|
+
workflow_dispatch:
|
|
15
|
+
|
|
16
|
+
permissions:
|
|
17
|
+
contents: read
|
|
18
|
+
|
|
19
|
+
jobs:
|
|
20
|
+
hygiene:
|
|
21
|
+
name: Termos internos
|
|
22
|
+
runs-on: ubuntu-latest
|
|
23
|
+
steps:
|
|
24
|
+
- uses: actions/checkout@v4
|
|
25
|
+
with:
|
|
26
|
+
fetch-depth: 0
|
|
27
|
+
|
|
28
|
+
- name: Escrever lista de termos
|
|
29
|
+
run: |
|
|
30
|
+
cat > "$RUNNER_TEMP/termos-comuns.txt" <<'EOF'
|
|
31
|
+
\bOnda\b
|
|
32
|
+
\bADR\b
|
|
33
|
+
\bplanning/
|
|
34
|
+
\bplano (`|[a-z0-9-]+-[a-z0-9-]+|§)
|
|
35
|
+
\bPlano:
|
|
36
|
+
uploader
|
|
37
|
+
\bCAS\b
|
|
38
|
+
\bVPS\b
|
|
39
|
+
crontab
|
|
40
|
+
\bcrons?\b
|
|
41
|
+
systemd
|
|
42
|
+
runs\.csv
|
|
43
|
+
quantilica-(workers|cloud|web|events|portal|manifests-db)
|
|
44
|
+
bcb-sgs-app
|
|
45
|
+
rtn-app
|
|
46
|
+
crons-dashboard
|
|
47
|
+
datasus-analytics
|
|
48
|
+
[a-z-]+-metadata-db
|
|
49
|
+
tddata-db
|
|
50
|
+
dev-plugins
|
|
51
|
+
\bOKF\b
|
|
52
|
+
Flamingo
|
|
53
|
+
Alpaca
|
|
54
|
+
Hedgehog
|
|
55
|
+
Sloth
|
|
56
|
+
\bFish\b
|
|
57
|
+
Telegram
|
|
58
|
+
Umami
|
|
59
|
+
Cloudflare
|
|
60
|
+
(^|[^[:alnum:].])/home/[a-z]+
|
|
61
|
+
\.agents/
|
|
62
|
+
CLAUDE\.md
|
|
63
|
+
AGENTS\.md
|
|
64
|
+
EOF
|
|
65
|
+
cat > "$RUNNER_TEMP/termos-arquivos.txt" <<'EOF'
|
|
66
|
+
\bworkspace\b
|
|
67
|
+
ingestão efêmera
|
|
68
|
+
retenção efêmera
|
|
69
|
+
pipeline de ingestão
|
|
70
|
+
EOF
|
|
71
|
+
cat "$RUNNER_TEMP/termos-comuns.txt" "$RUNNER_TEMP/termos-arquivos.txt" \
|
|
72
|
+
> "$RUNNER_TEMP/termos-todos.txt"
|
|
73
|
+
|
|
74
|
+
- name: CHANGELOG e README
|
|
75
|
+
run: |
|
|
76
|
+
files=$(ls CHANGELOG.md README*.md docs/changelog.md 2>/dev/null || true)
|
|
77
|
+
if [ -z "$files" ]; then
|
|
78
|
+
echo "Nenhum CHANGELOG/README para verificar."
|
|
79
|
+
exit 0
|
|
80
|
+
fi
|
|
81
|
+
# shellcheck disable=SC2086
|
|
82
|
+
hits=$(grep -nIiE -f "$RUNNER_TEMP/termos-todos.txt" $files /dev/null \
|
|
83
|
+
| grep -v 'hygiene-ok' || true)
|
|
84
|
+
if [ -n "$hits" ]; then
|
|
85
|
+
echo "$hits"
|
|
86
|
+
echo "::error::Termos internos em changelog/README. Reescreva em termos públicos (docs/normas/changelog.md, seção 5.1)."
|
|
87
|
+
exit 1
|
|
88
|
+
fi
|
|
89
|
+
echo "Limpo."
|
|
90
|
+
|
|
91
|
+
- name: Mensagens de commit do PR
|
|
92
|
+
if: github.event_name == 'pull_request'
|
|
93
|
+
run: |
|
|
94
|
+
range="${{ github.event.pull_request.base.sha }}..${{ github.event.pull_request.head.sha }}"
|
|
95
|
+
msgs=$(git log --format='%h %s%n%b' "$range")
|
|
96
|
+
hits=$(printf '%s\n' "$msgs" \
|
|
97
|
+
| grep -iE -f "$RUNNER_TEMP/termos-comuns.txt" \
|
|
98
|
+
| grep -v 'hygiene-ok' || true)
|
|
99
|
+
if [ -n "$hits" ]; then
|
|
100
|
+
echo "$hits"
|
|
101
|
+
echo "::error::Termos internos em mensagem de commit. Reescreva o commit (docs/normas/changelog.md, seção 5.1)."
|
|
102
|
+
exit 1
|
|
103
|
+
fi
|
|
104
|
+
echo "Limpo."
|
|
@@ -5,6 +5,19 @@ Todas as mudanças notáveis deste projeto serão documentadas neste arquivo.
|
|
|
5
5
|
O formato segue [Keep a Changelog](https://keepachangelog.com/pt-BR/1.1.0/),
|
|
6
6
|
e este projeto adere ao [Semantic Versioning](https://semver.org/lang/pt-BR/).
|
|
7
7
|
|
|
8
|
+
## [0.13.0] - 2026-10-27
|
|
9
|
+
|
|
10
|
+
### Adicionado
|
|
11
|
+
|
|
12
|
+
- `convert`: novas opções `--lake` (raiz do lake de destino; padrão `lake_root()` de `quantilica.core.lake`, ou `default_output` se o core não tiver o módulo), `--dataset` (repetível) e `--force`. `-o/--output` continua aceito como alias de `--lake`. O conversor é chamado como `func(input, lake)` e recebe `dataset=` e `force=` por keyword só quando a assinatura os aceita (ou tem `**kwargs`); pedir `--dataset`/`--force` a um conversor que não os aceita sai com código 2.
|
|
13
|
+
- `convert` e `pipeline`: se o conversor devolver um objeto (ou dicionário) com `converted`, `skipped` e `errors`, o sumário é exibido, e `errors > 0` sai com código 1.
|
|
14
|
+
- `pipeline --lake` (com `--parquet-dir` mantido como alias), repassado ao conversor.
|
|
15
|
+
- `quantilica doctor`: informa, por fonte, se há conversor do lake registrado no entry point `quantilica.lake`.
|
|
16
|
+
|
|
17
|
+
### Alterado
|
|
18
|
+
|
|
19
|
+
- `pipeline`: o destino da conversão passa a ser `lake_root()` por padrão, em vez de igual a `--output`.
|
|
20
|
+
|
|
8
21
|
## [0.12.0] - 2026-10-19
|
|
9
22
|
|
|
10
23
|
### Adicionado
|
|
@@ -46,7 +59,7 @@ e este projeto adere ao [Semantic Versioning](https://semver.org/lang/pt-BR/).
|
|
|
46
59
|
### Corrigido
|
|
47
60
|
|
|
48
61
|
- `sdk`: `check_entry` considera `local.exists() or sidecar.exists()`, preservando
|
|
49
|
-
o status de `manifest-fresh` em ambientes
|
|
62
|
+
o status de `manifest-fresh` em ambientes sem retenção local dos arquivos sem forçar
|
|
50
63
|
re-download redundante quando o dado bruto já foi espelhado.
|
|
51
64
|
|
|
52
65
|
## [0.10.0] - 2026-10-07
|
|
@@ -71,7 +84,7 @@ e este projeto adere ao [Semantic Versioning](https://semver.org/lang/pt-BR/).
|
|
|
71
84
|
|
|
72
85
|
## [0.9.1] - 2026-10-04
|
|
73
86
|
|
|
74
|
-
Correção de escape inválido introduzido na v0.8.0 (
|
|
87
|
+
Correção de escape inválido introduzido na v0.8.0 (extensões do SDK).
|
|
75
88
|
|
|
76
89
|
### Corrigido
|
|
77
90
|
- `SyntaxWarning: invalid escape sequence '\['` nas mensagens de `convert`,
|
|
@@ -95,7 +108,7 @@ fetcher (wheel no GitHub Releases, entrada no índice PEP 503 do ecossistema).
|
|
|
95
108
|
|
|
96
109
|
## [0.8.0] - 2026-10-03
|
|
97
110
|
|
|
98
|
-
|
|
111
|
+
Padronização do SDK e consolidação dos fetchers —
|
|
99
112
|
decorators de ciclo de vida no SDK (`quantilica.cli.sdk`) e abstração de
|
|
100
113
|
pré-visualização tabular de sincronização.
|
|
101
114
|
|
|
@@ -126,7 +139,7 @@ pré-visualização tabular de sincronização.
|
|
|
126
139
|
|
|
127
140
|
## [0.7.0] - 2026-10-02
|
|
128
141
|
|
|
129
|
-
|
|
142
|
+
Extensões do SDK (`quantilica.cli.sdk`) para eliminação de
|
|
130
143
|
boilerplate nos fetchers (decisão
|
|
131
144
|
`2026-10-02-padronizacao-e-deduplicacao-fetchers`).
|
|
132
145
|
|
|
@@ -160,7 +173,7 @@ boilerplate nos fetchers (decisão
|
|
|
160
173
|
## [0.3.1] - 2026-08-22
|
|
161
174
|
|
|
162
175
|
### Corrigido
|
|
163
|
-
- `DEFAULT_INDEX_URL` apontado para `https://index.quantilica.com/simple/` — a URL anterior (`quantilica.com/quantilica-index/`) parou de ser servida quando o domínio passou ao portal, quebrando o `quantilica install` para fetchers fora do PyPI legado (detalhes
|
|
176
|
+
- `DEFAULT_INDEX_URL` apontado para `https://index.quantilica.com/simple/` — a URL anterior (`quantilica.com/quantilica-index/`) parou de ser servida quando o domínio passou ao portal, quebrando o `quantilica install` para fetchers fora do PyPI legado (detalhes na decisão de distribuição de 2026-08-22).
|
|
164
177
|
- `install`/`uninstall` agora mesclam o registro remoto (`sources.json`) com o registro local, resolvendo também nomes canônicos do índice (ex.: `tesouro-direto`, além de `td`).
|
|
165
178
|
|
|
166
179
|
## [0.3.0] - 2026-08-10
|
|
@@ -9,6 +9,7 @@ import concurrent.futures
|
|
|
9
9
|
import contextlib
|
|
10
10
|
import datetime as dt
|
|
11
11
|
import email.utils
|
|
12
|
+
import inspect
|
|
12
13
|
import json
|
|
13
14
|
import os
|
|
14
15
|
import threading
|
|
@@ -39,6 +40,7 @@ from quantilica.core.storage import (
|
|
|
39
40
|
)
|
|
40
41
|
from rich.console import Console, Group
|
|
41
42
|
from rich.live import Live
|
|
43
|
+
from rich.markup import escape
|
|
42
44
|
from rich.rule import Rule
|
|
43
45
|
from rich.table import Table
|
|
44
46
|
|
|
@@ -608,24 +610,132 @@ class FetcherApp:
|
|
|
608
610
|
pass
|
|
609
611
|
return f"quantilica install {key} --extra analysis"
|
|
610
612
|
|
|
611
|
-
def
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
613
|
+
def _default_lake(self) -> Path:
|
|
614
|
+
"""Raiz padrão do lake: ``lake_root()`` do core, se disponível.
|
|
615
|
+
|
|
616
|
+
``quantilica.core.lake`` só existe a partir de versões recentes do core;
|
|
617
|
+
sem ele, cai no ``default_output`` (comportamento anterior).
|
|
618
|
+
"""
|
|
619
|
+
try:
|
|
620
|
+
from quantilica.core.lake import lake_root
|
|
621
|
+
except ImportError: # pragma: no cover - core sem o módulo lake
|
|
622
|
+
return self.default_output
|
|
623
|
+
return lake_root()
|
|
624
|
+
|
|
625
|
+
def _run_convert(
|
|
626
|
+
self,
|
|
627
|
+
func: Callable[..., Any],
|
|
628
|
+
input: Path,
|
|
629
|
+
lake: Path,
|
|
630
|
+
*,
|
|
631
|
+
dataset: list[str] | None = None,
|
|
632
|
+
force: bool = False,
|
|
633
|
+
) -> Any:
|
|
634
|
+
"""Chama ``func`` com os argumentos que ela aceita.
|
|
635
|
+
|
|
636
|
+
Conversores legados têm a forma ``func(input, lake)``. ``dataset`` e
|
|
637
|
+
``force`` só são repassados (como keyword) quando a assinatura os aceita
|
|
638
|
+
ou tem ``**kwargs``. Se o usuário pediu um deles e ``func`` não o
|
|
639
|
+
aceita, sai com código 2 em vez de ignorar a opção em silêncio.
|
|
640
|
+
"""
|
|
641
|
+
try:
|
|
642
|
+
params = inspect.signature(func).parameters
|
|
643
|
+
except (TypeError, ValueError): # pragma: no cover - builtins/C callables
|
|
644
|
+
return func(input, lake)
|
|
645
|
+
var_kw = any(p.kind is inspect.Parameter.VAR_KEYWORD for p in params.values())
|
|
646
|
+
wanted = {"dataset": dataset or None, "force": force or None}
|
|
647
|
+
kwargs: dict[str, Any] = {}
|
|
648
|
+
for name, value in wanted.items():
|
|
649
|
+
if name in params or var_kw:
|
|
650
|
+
kwargs[name] = dataset if name == "dataset" else force
|
|
651
|
+
elif value is not None:
|
|
652
|
+
get_console().print(
|
|
653
|
+
f"[red]Erro:[/red] este conversor não suporta --{name}."
|
|
654
|
+
)
|
|
655
|
+
raise typer.Exit(2)
|
|
656
|
+
return func(input, lake, **kwargs)
|
|
657
|
+
|
|
658
|
+
@staticmethod
|
|
659
|
+
def _convert_counts(result: Any) -> dict[str, Any] | None:
|
|
660
|
+
"""Extrai ``converted``/``skipped``/``errors`` do retorno de um conversor.
|
|
615
661
|
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
662
|
+
Aceita um objeto com esses atributos ou um dicionário de contagens.
|
|
663
|
+
Valores podem ser inteiros ou coleções (usa-se o tamanho nas contagens).
|
|
664
|
+
|
|
665
|
+
Returns:
|
|
666
|
+
``{"converted": int, "skipped": int, "errors": int,
|
|
667
|
+
"error_items": list}`` ou ``None`` se o retorno não tiver o formato.
|
|
668
|
+
"""
|
|
669
|
+
keys = ("converted", "skipped", "errors")
|
|
670
|
+
if isinstance(result, dict):
|
|
671
|
+
raw = {k: result.get(k) for k in keys}
|
|
672
|
+
present = any(k in result for k in keys)
|
|
673
|
+
else:
|
|
674
|
+
raw = {k: getattr(result, k, None) for k in keys}
|
|
675
|
+
present = any(hasattr(result, k) for k in keys)
|
|
676
|
+
if not present:
|
|
677
|
+
return None
|
|
678
|
+
|
|
679
|
+
def count(value: Any) -> int:
|
|
680
|
+
if value is None:
|
|
681
|
+
return 0
|
|
682
|
+
if isinstance(value, int | float):
|
|
683
|
+
return int(value)
|
|
684
|
+
return len(value)
|
|
685
|
+
|
|
686
|
+
errors = raw["errors"]
|
|
687
|
+
items = (
|
|
688
|
+
list(errors)
|
|
689
|
+
if errors is not None and not isinstance(errors, int | float)
|
|
690
|
+
else []
|
|
691
|
+
)
|
|
692
|
+
return {
|
|
693
|
+
"converted": count(raw["converted"]),
|
|
694
|
+
"skipped": count(raw["skipped"]),
|
|
695
|
+
"errors": count(errors),
|
|
696
|
+
"error_items": items,
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
def _report_convert(self, result: Any, console: Console) -> bool:
|
|
700
|
+
"""Imprime o sumário da conversão. Retorna ``True`` se houve erros."""
|
|
701
|
+
counts = self._convert_counts(result)
|
|
702
|
+
if counts is None:
|
|
703
|
+
return False
|
|
704
|
+
table = Table(show_header=False, box=None, pad_edge=False)
|
|
705
|
+
table.add_row("[green]convertidos[/green]", str(counts["converted"]))
|
|
706
|
+
table.add_row("[dim]pulados[/dim]", str(counts["skipped"]))
|
|
707
|
+
table.add_row(
|
|
708
|
+
"[red]erros[/red]" if counts["errors"] else "[dim]erros[/dim]",
|
|
709
|
+
str(counts["errors"]),
|
|
710
|
+
)
|
|
711
|
+
console.print(table)
|
|
712
|
+
for item in counts["error_items"]:
|
|
713
|
+
console.print(f" [red]✗[/red] {escape(str(item))}")
|
|
714
|
+
return counts["errors"] > 0
|
|
715
|
+
|
|
716
|
+
def command_convert(self, func: Callable[..., Any]) -> Callable[..., Any]:
|
|
717
|
+
"""Register the standard ``convert`` subcommand (raw → lake).
|
|
718
|
+
|
|
719
|
+
The generated command exposes ``-i/--input``, ``--lake`` (aliases
|
|
720
|
+
``-o``/``--output``; default ``lake_root()``), ``--dataset`` (repeatable),
|
|
721
|
+
``--force`` and ``--verbose``. ``func`` is called as
|
|
722
|
+
``func(input, lake, dataset=..., force=...)``; the keywords are only
|
|
723
|
+
passed when its signature accepts them, so legacy ``func(input, output)``
|
|
724
|
+
converters keep working.
|
|
725
|
+
|
|
726
|
+
If ``func`` returns an object (or dict) with ``converted``, ``skipped``
|
|
727
|
+
and ``errors``, a summary is rendered and the command exits with code 1
|
|
728
|
+
when there are errors. An ``ImportError`` propagating from ``func`` is
|
|
729
|
+
treated as missing analytical extras and exits with code 1.
|
|
620
730
|
|
|
621
731
|
Args:
|
|
622
|
-
func: Callable
|
|
623
|
-
the conversion.
|
|
732
|
+
func: Callable performing the conversion.
|
|
624
733
|
|
|
625
734
|
Returns:
|
|
626
735
|
The original ``func`` (the method can be used as a decorator).
|
|
627
736
|
"""
|
|
628
737
|
console = get_console()
|
|
738
|
+
default_lake = self._default_lake()
|
|
629
739
|
|
|
630
740
|
@self.app.command("convert")
|
|
631
741
|
def convert(
|
|
@@ -637,26 +747,47 @@ class FetcherApp:
|
|
|
637
747
|
help="Diretório de origem com arquivos brutos",
|
|
638
748
|
),
|
|
639
749
|
] = self.default_output,
|
|
640
|
-
|
|
750
|
+
lake: Annotated[
|
|
641
751
|
Path,
|
|
642
|
-
typer.Option(
|
|
643
|
-
|
|
752
|
+
typer.Option(
|
|
753
|
+
"--lake",
|
|
754
|
+
"-o",
|
|
755
|
+
"--output",
|
|
756
|
+
help="Raiz do lake de destino (padrão: QUANTILICA_LAKE_DIR)",
|
|
757
|
+
),
|
|
758
|
+
] = default_lake,
|
|
759
|
+
dataset: Annotated[
|
|
760
|
+
list[str] | None,
|
|
761
|
+
typer.Option(
|
|
762
|
+
"--dataset", help="Filtrar dataset(s) específico(s) a converter"
|
|
763
|
+
),
|
|
764
|
+
] = None,
|
|
765
|
+
force: Annotated[
|
|
766
|
+
bool,
|
|
767
|
+
typer.Option(
|
|
768
|
+
"--force", help="Forçar reconversão mesmo se sha256 coincidir"
|
|
769
|
+
),
|
|
770
|
+
] = False,
|
|
644
771
|
verbose: Annotated[
|
|
645
772
|
bool, typer.Option("--verbose", help="Logs detalhados")
|
|
646
773
|
] = False,
|
|
647
774
|
) -> None:
|
|
648
775
|
setup_rich_logging(verbose, console=console)
|
|
649
776
|
try:
|
|
650
|
-
|
|
777
|
+
result = self._run_convert(
|
|
778
|
+
func, input, lake, dataset=dataset, force=force
|
|
779
|
+
)
|
|
651
780
|
except ImportError:
|
|
652
781
|
console.print(
|
|
653
782
|
f"[red]Erro:[/red] convert requer extras de análise: "
|
|
654
783
|
f"{self._analysis_hint()}"
|
|
655
784
|
)
|
|
656
785
|
raise typer.Exit(1) from None
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
786
|
+
failed = self._report_convert(result, console)
|
|
787
|
+
if failed:
|
|
788
|
+
console.print("[red]✗[/red] Conversão concluída com erros.")
|
|
789
|
+
raise typer.Exit(1)
|
|
790
|
+
console.print(f"[green]✓[/green] Conversão concluída em [dim]{lake}[/dim].")
|
|
660
791
|
|
|
661
792
|
return func
|
|
662
793
|
|
|
@@ -711,26 +842,27 @@ class FetcherApp:
|
|
|
711
842
|
|
|
712
843
|
return func
|
|
713
844
|
|
|
714
|
-
def command_pipeline(
|
|
715
|
-
self, convert_func: Callable[[Path, Path], Any]
|
|
716
|
-
) -> Callable[[Path, Path], Any]:
|
|
845
|
+
def command_pipeline(self, convert_func: Callable[..., Any]) -> Callable[..., Any]:
|
|
717
846
|
"""Register the standard ``pipeline`` subcommand (sync → convert).
|
|
718
847
|
|
|
719
848
|
Step 1/2 invokes the built-in ``sync`` command (same flags: groups,
|
|
720
|
-
``-o/--output``, ``--
|
|
721
|
-
|
|
849
|
+
``-o/--output``, ``--workers``, ``--dry-run``, ``--verbose``) and step
|
|
850
|
+
2/2 invokes ``convert_func(output, lake)`` as in :meth:`command_convert`
|
|
851
|
+
(``--lake`` defaults to ``lake_root()``; ``--parquet-dir`` is kept as an
|
|
852
|
+
alias for backwards compatibility).
|
|
722
853
|
With ``--dry-run`` the plan is only listed and the conversion step is
|
|
723
854
|
skipped. An ``ImportError`` from ``convert_func`` is treated as
|
|
724
855
|
missing analytical extras.
|
|
725
856
|
|
|
726
857
|
Args:
|
|
727
|
-
convert_func: Callable receiving ``(output,
|
|
728
|
-
|
|
858
|
+
convert_func: Callable receiving ``(output, lake)`` paths and
|
|
859
|
+
performing the conversion.
|
|
729
860
|
|
|
730
861
|
Returns:
|
|
731
862
|
The original ``convert_func`` (usable as a decorator).
|
|
732
863
|
"""
|
|
733
864
|
console = get_console()
|
|
865
|
+
default_lake = self._default_lake()
|
|
734
866
|
|
|
735
867
|
@self.app.command("pipeline")
|
|
736
868
|
def pipeline(
|
|
@@ -746,11 +878,12 @@ class FetcherApp:
|
|
|
746
878
|
Path | None,
|
|
747
879
|
typer.Option("-o", "--output", help="Diretório de saída"),
|
|
748
880
|
] = None,
|
|
749
|
-
|
|
881
|
+
lake: Annotated[
|
|
750
882
|
Path | None,
|
|
751
883
|
typer.Option(
|
|
884
|
+
"--lake",
|
|
752
885
|
"--parquet-dir",
|
|
753
|
-
help="
|
|
886
|
+
help="Raiz do lake de destino (padrão: QUANTILICA_LAKE_DIR)",
|
|
754
887
|
),
|
|
755
888
|
] = None,
|
|
756
889
|
workers: Annotated[
|
|
@@ -765,7 +898,7 @@ class FetcherApp:
|
|
|
765
898
|
) -> None:
|
|
766
899
|
setup_rich_logging(verbose, console=console)
|
|
767
900
|
actual_output = output or self.default_output
|
|
768
|
-
|
|
901
|
+
lake_out = lake or default_lake
|
|
769
902
|
|
|
770
903
|
console.print(Rule("[bold]Passo 1/2: Download[/bold]"))
|
|
771
904
|
sync_cmd = next(
|
|
@@ -799,16 +932,18 @@ class FetcherApp:
|
|
|
799
932
|
|
|
800
933
|
console.print(Rule("[bold]Passo 2/2: Conversão[/bold]"))
|
|
801
934
|
try:
|
|
802
|
-
convert_func
|
|
935
|
+
result = self._run_convert(convert_func, actual_output, lake_out)
|
|
803
936
|
except ImportError:
|
|
804
937
|
console.print(
|
|
805
938
|
f"[red]Erro:[/red] pipeline (conversão) requer extras de "
|
|
806
939
|
f"análise: {self._analysis_hint()}"
|
|
807
940
|
)
|
|
808
941
|
raise typer.Exit(1) from None
|
|
942
|
+
if self._report_convert(result, console):
|
|
943
|
+
console.print("[red]✗[/red] Pipeline concluído com erros na conversão.")
|
|
944
|
+
raise typer.Exit(1)
|
|
809
945
|
console.print(
|
|
810
|
-
f"[green]✓[/green] Pipeline concluído:
|
|
811
|
-
f"[dim]{parquet_out}[/dim]."
|
|
946
|
+
f"[green]✓[/green] Pipeline concluído: lake em [dim]{lake_out}[/dim]."
|
|
812
947
|
)
|
|
813
948
|
|
|
814
949
|
return convert_func
|
|
@@ -24,6 +24,7 @@ app = typer.Typer(
|
|
|
24
24
|
console = Console()
|
|
25
25
|
|
|
26
26
|
FETCHER_GROUP = "quantilica.fetchers"
|
|
27
|
+
LAKE_GROUP = "quantilica.lake"
|
|
27
28
|
DEFAULT_INDEX_URL = "https://index.quantilica.com/simple/"
|
|
28
29
|
|
|
29
30
|
# Mapeamento estático padrão de comandos CLI para nome das distribuições
|
|
@@ -31,15 +32,22 @@ SOURCES_REGISTRY: dict[str, str] = {
|
|
|
31
32
|
"anac": "anac-fetcher",
|
|
32
33
|
"anp": "anp-fetcher",
|
|
33
34
|
"b3": "b3-fetcher",
|
|
35
|
+
"bcb": "bcb-sgs-fetcher",
|
|
34
36
|
"bcb-sgs": "bcb-sgs-fetcher",
|
|
35
37
|
"comex": "comex-fetcher",
|
|
36
38
|
"cvm": "cvm-fetcher",
|
|
37
39
|
"datasus": "datasus-fetcher",
|
|
40
|
+
"inep": "inep-fetcher",
|
|
38
41
|
"inmet": "inmet-fetcher",
|
|
39
42
|
"pdet": "pdet-fetcher",
|
|
43
|
+
"rfb": "rfb-cnpj-fetcher",
|
|
44
|
+
"rfb-cnpj": "rfb-cnpj-fetcher",
|
|
40
45
|
"rtn": "rtn-fetcher",
|
|
41
46
|
"sidra": "sidra-fetcher",
|
|
47
|
+
"sql": "quantilica-sql",
|
|
42
48
|
"td": "tesouro-direto-fetcher",
|
|
49
|
+
"tesouro-direto": "tesouro-direto-fetcher",
|
|
50
|
+
"tesouro_direto": "tesouro-direto-fetcher",
|
|
43
51
|
"tse": "tse-fetcher",
|
|
44
52
|
}
|
|
45
53
|
|
|
@@ -115,6 +123,34 @@ def get_installed_entry_points() -> set[str]:
|
|
|
115
123
|
return {ep.name for ep in eps}
|
|
116
124
|
|
|
117
125
|
|
|
126
|
+
def get_lake_entry_points() -> list[tuple[str, str | None]]:
|
|
127
|
+
"""Lista os conversores do lake (entry points ``quantilica.lake``) instalados.
|
|
128
|
+
|
|
129
|
+
Não carrega os entry points: importar o conversor exige os extras de
|
|
130
|
+
análise, e o ``doctor`` só precisa saber se ele está registrado.
|
|
131
|
+
|
|
132
|
+
Returns:
|
|
133
|
+
Pares ``(nome do entry point, nome da distribuição ou None)``.
|
|
134
|
+
"""
|
|
135
|
+
return [
|
|
136
|
+
(ep.name, ep.dist.name if ep.dist is not None else None)
|
|
137
|
+
for ep in entry_points(group=LAKE_GROUP)
|
|
138
|
+
]
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def has_lake_converter(
|
|
142
|
+
source: str, lake_eps: list[tuple[str, str | None]], dist: str | None = None
|
|
143
|
+
) -> bool:
|
|
144
|
+
"""Indica se a fonte tem conversor do lake registrado.
|
|
145
|
+
|
|
146
|
+
Casa pelo nome do entry point (``source_id``) ou pela distribuição da fonte.
|
|
147
|
+
"""
|
|
148
|
+
dists = {d for _, d in lake_eps if d}
|
|
149
|
+
return any(name == source for name, _ in lake_eps) or (
|
|
150
|
+
dist is not None and dist in dists
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
|
|
118
154
|
def get_index_url() -> str:
|
|
119
155
|
"""Retorna a URL do índice pip customizado ou default.
|
|
120
156
|
|
|
@@ -361,7 +397,10 @@ def cmd_uninstall(
|
|
|
361
397
|
|
|
362
398
|
@app.command("doctor")
|
|
363
399
|
def cmd_doctor() -> None:
|
|
364
|
-
"""Verifica inconsistências entre o estado registrado e os plugins instalados.
|
|
400
|
+
"""Verifica inconsistências entre o estado registrado e os plugins instalados.
|
|
401
|
+
|
|
402
|
+
Também informa, por fonte, se há conversor do lake (``quantilica.lake``).
|
|
403
|
+
"""
|
|
365
404
|
state = load_state()
|
|
366
405
|
registered = state.get("installed", {})
|
|
367
406
|
installed_eps = get_installed_entry_points()
|
|
@@ -375,18 +414,33 @@ def cmd_doctor() -> None:
|
|
|
375
414
|
console.print(
|
|
376
415
|
"[green]✓ Todas as fontes registradas estão instaladas e ativas.[/green]"
|
|
377
416
|
)
|
|
378
|
-
|
|
417
|
+
else:
|
|
418
|
+
console.print(
|
|
419
|
+
f"[yellow]Encontradas {len(missing)} fonte(s) registrada(s) mas ausente(s) no ambiente:[/yellow]"
|
|
420
|
+
)
|
|
421
|
+
for name, dist in missing.items():
|
|
422
|
+
console.print(f" - {name} ({dist})")
|
|
379
423
|
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
424
|
+
for name, dist in missing.items():
|
|
425
|
+
console.print(f"[cyan]Reinstalando {name} ({dist})...[/cyan]")
|
|
426
|
+
try:
|
|
427
|
+
install_package(dist)
|
|
428
|
+
console.print(f"[green]✓ {name} recuperado.[/green]")
|
|
429
|
+
except Exception as exc:
|
|
430
|
+
console.print(f"[red]Falha ao recuperar {name}:[/red] {exc}")
|
|
431
|
+
|
|
432
|
+
_print_lake_status(installed_eps | set(registered), registered)
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
def _print_lake_status(sources: set[str], registered: dict[str, str]) -> None:
|
|
436
|
+
"""Imprime, por fonte instalada/registrada, se há conversor do lake."""
|
|
437
|
+
if not sources:
|
|
438
|
+
return
|
|
439
|
+
lake_eps = get_lake_entry_points()
|
|
440
|
+
console.print("\n[bold]Lake (conversor quantilica.lake):[/bold]")
|
|
441
|
+
for name in sorted(sources):
|
|
442
|
+
dist = registered.get(name) or SOURCES_REGISTRY.get(name)
|
|
443
|
+
if has_lake_converter(name, lake_eps, dist):
|
|
444
|
+
console.print(f" - {name}: [green]✓[/green] lake: sim")
|
|
445
|
+
else:
|
|
446
|
+
console.print(f" - {name}: [dim]lake: não[/dim]")
|
|
@@ -580,3 +580,142 @@ def test_check_entry_403_head_without_probe_support_downloads(tmp_path: Path):
|
|
|
580
580
|
)
|
|
581
581
|
item = app.check_entry(sample_list_datasets("exp")[0], tmp_path)
|
|
582
582
|
assert item.action == "download"
|
|
583
|
+
|
|
584
|
+
|
|
585
|
+
# ---------------------------------------------------------------
|
|
586
|
+
# command_convert / command_pipeline: Lake Hive
|
|
587
|
+
# ---------------------------------------------------------------
|
|
588
|
+
|
|
589
|
+
|
|
590
|
+
def test_convert_lake_flag_and_output_alias(tmp_path: Path):
|
|
591
|
+
app = make_app()
|
|
592
|
+
calls: list[tuple[Path, Path]] = []
|
|
593
|
+
|
|
594
|
+
@app.command_convert
|
|
595
|
+
def conv(input: Path, lake: Path) -> None:
|
|
596
|
+
calls.append((input, lake))
|
|
597
|
+
|
|
598
|
+
for flag in ("--lake", "-o", "--output"):
|
|
599
|
+
result = runner.invoke(
|
|
600
|
+
_grouped(app.app),
|
|
601
|
+
["fd", "convert", "-i", str(tmp_path), flag, str(tmp_path / "l")],
|
|
602
|
+
)
|
|
603
|
+
assert result.exit_code == 0, result.output
|
|
604
|
+
assert calls == [(tmp_path, tmp_path / "l")] * 3
|
|
605
|
+
|
|
606
|
+
|
|
607
|
+
def test_convert_passes_dataset_and_force_when_accepted(tmp_path: Path):
|
|
608
|
+
app = make_app()
|
|
609
|
+
seen: dict = {}
|
|
610
|
+
|
|
611
|
+
@app.command_convert
|
|
612
|
+
def conv(input: Path, lake: Path, dataset=None, force=False) -> None:
|
|
613
|
+
seen.update(dataset=dataset, force=force)
|
|
614
|
+
|
|
615
|
+
result = runner.invoke(
|
|
616
|
+
_grouped(app.app),
|
|
617
|
+
["fd", "convert", "--dataset", "a", "--dataset", "b", "--force"],
|
|
618
|
+
)
|
|
619
|
+
assert result.exit_code == 0, result.output
|
|
620
|
+
assert seen == {"dataset": ["a", "b"], "force": True}
|
|
621
|
+
|
|
622
|
+
|
|
623
|
+
def test_convert_kwargs_func_receives_dataset_and_force():
|
|
624
|
+
app = make_app()
|
|
625
|
+
seen: dict = {}
|
|
626
|
+
|
|
627
|
+
@app.command_convert
|
|
628
|
+
def conv(input: Path, lake: Path, **kwargs) -> None:
|
|
629
|
+
seen.update(kwargs)
|
|
630
|
+
|
|
631
|
+
result = runner.invoke(_grouped(app.app), ["fd", "convert"])
|
|
632
|
+
assert result.exit_code == 0, result.output
|
|
633
|
+
assert seen == {"dataset": None, "force": False}
|
|
634
|
+
|
|
635
|
+
|
|
636
|
+
def test_convert_legacy_two_arg_func_ignores_unused_defaults():
|
|
637
|
+
app = make_app()
|
|
638
|
+
calls: list[tuple[Path, Path]] = []
|
|
639
|
+
|
|
640
|
+
@app.command_convert
|
|
641
|
+
def conv(input: Path, lake: Path) -> None:
|
|
642
|
+
calls.append((input, lake))
|
|
643
|
+
|
|
644
|
+
result = runner.invoke(_grouped(app.app), ["fd", "convert"])
|
|
645
|
+
assert result.exit_code == 0, result.output
|
|
646
|
+
assert len(calls) == 1
|
|
647
|
+
|
|
648
|
+
|
|
649
|
+
@pytest.mark.parametrize("flags", [["--dataset", "x"], ["--force"]])
|
|
650
|
+
def test_convert_legacy_func_rejects_requested_unsupported_flag(flags: list[str]):
|
|
651
|
+
app = make_app()
|
|
652
|
+
|
|
653
|
+
@app.command_convert
|
|
654
|
+
def conv(input: Path, lake: Path) -> None: ...
|
|
655
|
+
|
|
656
|
+
result = runner.invoke(_grouped(app.app), ["fd", "convert", *flags])
|
|
657
|
+
assert result.exit_code == 2
|
|
658
|
+
assert "não suporta" in result.output
|
|
659
|
+
|
|
660
|
+
|
|
661
|
+
def test_convert_summary_from_object_and_exit_0_without_errors():
|
|
662
|
+
from types import SimpleNamespace
|
|
663
|
+
|
|
664
|
+
app = make_app()
|
|
665
|
+
|
|
666
|
+
@app.command_convert
|
|
667
|
+
def conv(input: Path, lake: Path):
|
|
668
|
+
return SimpleNamespace(converted=3, skipped=2, errors=0)
|
|
669
|
+
|
|
670
|
+
result = runner.invoke(_grouped(app.app), ["fd", "convert"])
|
|
671
|
+
assert result.exit_code == 0, result.output
|
|
672
|
+
assert "convertidos" in result.output
|
|
673
|
+
assert "pulados" in result.output
|
|
674
|
+
assert "Conversão concluída" in result.output
|
|
675
|
+
|
|
676
|
+
|
|
677
|
+
def test_convert_summary_from_dict_with_errors_exits_1():
|
|
678
|
+
app = make_app()
|
|
679
|
+
|
|
680
|
+
@app.command_convert
|
|
681
|
+
def conv(input: Path, lake: Path):
|
|
682
|
+
return {"converted": ["a"], "skipped": [], "errors": ["boom [x]"]}
|
|
683
|
+
|
|
684
|
+
result = runner.invoke(_grouped(app.app), ["fd", "convert"])
|
|
685
|
+
assert result.exit_code == 1
|
|
686
|
+
assert "boom [x]" in result.output
|
|
687
|
+
assert "com erros" in result.output
|
|
688
|
+
|
|
689
|
+
|
|
690
|
+
def test_convert_unrelated_return_value_is_ignored():
|
|
691
|
+
app = make_app()
|
|
692
|
+
|
|
693
|
+
@app.command_convert
|
|
694
|
+
def conv(input: Path, lake: Path):
|
|
695
|
+
return 42
|
|
696
|
+
|
|
697
|
+
result = runner.invoke(_grouped(app.app), ["fd", "convert"])
|
|
698
|
+
assert result.exit_code == 0, result.output
|
|
699
|
+
assert "convertidos" not in result.output
|
|
700
|
+
|
|
701
|
+
|
|
702
|
+
def test_pipeline_lake_flag_and_parquet_dir_alias(
|
|
703
|
+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
704
|
+
):
|
|
705
|
+
app = make_app()
|
|
706
|
+
monkeypatch.setattr(
|
|
707
|
+
app, "download_datasets", lambda entries, output_dir, workers=4: (0, 0, [])
|
|
708
|
+
)
|
|
709
|
+
calls: list[tuple[Path, Path]] = []
|
|
710
|
+
|
|
711
|
+
@app.command_pipeline
|
|
712
|
+
def conv(input: Path, lake: Path) -> None:
|
|
713
|
+
calls.append((input, lake))
|
|
714
|
+
|
|
715
|
+
for flag in ("--lake", "--parquet-dir"):
|
|
716
|
+
result = runner.invoke(
|
|
717
|
+
_grouped(app.app),
|
|
718
|
+
["fd", "pipeline", "exp", "-o", str(tmp_path), flag, str(tmp_path / "l")],
|
|
719
|
+
)
|
|
720
|
+
assert result.exit_code == 0, result.output
|
|
721
|
+
assert calls == [(tmp_path, tmp_path / "l")] * 2
|
|
@@ -104,7 +104,10 @@ def test_command_convert_success(tmp_path: Path):
|
|
|
104
104
|
assert "✓" in result.output
|
|
105
105
|
|
|
106
106
|
|
|
107
|
-
def
|
|
107
|
+
def test_command_convert_defaults_to_lake_root(
|
|
108
|
+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
109
|
+
):
|
|
110
|
+
monkeypatch.setenv("QUANTILICA_LAKE_DIR", str(tmp_path / "lake"))
|
|
108
111
|
app = make_app()
|
|
109
112
|
calls: list[tuple[Path, Path]] = []
|
|
110
113
|
|
|
@@ -112,10 +115,9 @@ def test_command_convert_defaults_to_default_output(tmp_path: Path):
|
|
|
112
115
|
def conv(input: Path, output: Path) -> None:
|
|
113
116
|
calls.append((input, output))
|
|
114
117
|
|
|
115
|
-
default = app.default_output
|
|
116
118
|
result = runner.invoke(_grouped(app.app), ["fd", "convert"])
|
|
117
119
|
assert result.exit_code == 0, result.output
|
|
118
|
-
assert calls == [(
|
|
120
|
+
assert calls == [(app.default_output, tmp_path / "lake")]
|
|
119
121
|
|
|
120
122
|
|
|
121
123
|
def test_command_convert_graceful_import_error(tmp_path: Path):
|
|
@@ -235,9 +237,10 @@ def test_command_pipeline_graceful_import_error(
|
|
|
235
237
|
assert "--extra analysis" in saida
|
|
236
238
|
|
|
237
239
|
|
|
238
|
-
def
|
|
240
|
+
def test_command_pipeline_lake_defaults_to_lake_root(
|
|
239
241
|
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
240
242
|
):
|
|
243
|
+
monkeypatch.setenv("QUANTILICA_LAKE_DIR", str(tmp_path / "lake"))
|
|
241
244
|
app = make_app()
|
|
242
245
|
monkeypatch.setattr(
|
|
243
246
|
app, "download_datasets", lambda entries, output_dir, workers=4: (0, 0, [])
|
|
@@ -251,7 +254,7 @@ def test_command_pipeline_parquet_dir_defaults_to_output(
|
|
|
251
254
|
raw = tmp_path / "raw"
|
|
252
255
|
result = runner.invoke(_grouped(app.app), ["fd", "pipeline", "exp", "-o", str(raw)])
|
|
253
256
|
assert result.exit_code == 0, result.output
|
|
254
|
-
assert calls == [(raw,
|
|
257
|
+
assert calls == [(raw, tmp_path / "lake")]
|
|
255
258
|
|
|
256
259
|
|
|
257
260
|
def test_command_pipeline_requires_sync_command():
|
|
@@ -122,6 +122,37 @@ def test_cmd_doctor(temp_config_dir: Path):
|
|
|
122
122
|
mock_install.assert_called_once_with("rtn-fetcher")
|
|
123
123
|
|
|
124
124
|
|
|
125
|
+
def test_cmd_doctor_lists_lake_converters(temp_config_dir: Path):
|
|
126
|
+
from quantilica.cli.cli import app
|
|
127
|
+
|
|
128
|
+
save_state({"installed": {"comex": "comex-fetcher", "rtn": "rtn-fetcher"}})
|
|
129
|
+
with (
|
|
130
|
+
patch(
|
|
131
|
+
"quantilica.cli.sources.get_installed_entry_points",
|
|
132
|
+
return_value={"comex", "rtn"},
|
|
133
|
+
),
|
|
134
|
+
patch(
|
|
135
|
+
"quantilica.cli.sources.get_lake_entry_points",
|
|
136
|
+
return_value=[("comex", "comex-fetcher")],
|
|
137
|
+
),
|
|
138
|
+
):
|
|
139
|
+
result = CliRunner().invoke(app, ["doctor"])
|
|
140
|
+
assert result.exit_code == 0
|
|
141
|
+
assert "quantilica.lake" in result.output
|
|
142
|
+
assert "comex: ✓ lake: sim" in result.output
|
|
143
|
+
assert "rtn: lake: não" in result.output
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def test_has_lake_converter_matches_by_name_or_dist():
|
|
147
|
+
from quantilica.cli.sources import has_lake_converter
|
|
148
|
+
|
|
149
|
+
eps = [("pdet", "pdet-fetcher")]
|
|
150
|
+
assert has_lake_converter("pdet", eps)
|
|
151
|
+
assert has_lake_converter("outro", eps, "pdet-fetcher")
|
|
152
|
+
assert not has_lake_converter("outro", eps, "x-fetcher")
|
|
153
|
+
assert not has_lake_converter("outro", [])
|
|
154
|
+
|
|
155
|
+
|
|
125
156
|
def test_dist_spec_monta_extras():
|
|
126
157
|
from quantilica.cli.sources import _dist_spec
|
|
127
158
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|