quantilica-cli 0.3.0__tar.gz → 0.3.2__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.
Files changed (21) hide show
  1. {quantilica_cli-0.3.0 → quantilica_cli-0.3.2}/CHANGELOG.md +11 -0
  2. {quantilica_cli-0.3.0 → quantilica_cli-0.3.2}/PKG-INFO +3 -2
  3. {quantilica_cli-0.3.0 → quantilica_cli-0.3.2}/README.md +1 -0
  4. {quantilica_cli-0.3.0 → quantilica_cli-0.3.2}/pyproject.toml +4 -6
  5. {quantilica_cli-0.3.0 → quantilica_cli-0.3.2/src}/quantilica/cli/cli.py +1 -0
  6. {quantilica_cli-0.3.0 → quantilica_cli-0.3.2/src}/quantilica/cli/manifests.py +17 -1
  7. {quantilica_cli-0.3.0 → quantilica_cli-0.3.2/src}/quantilica/cli/progress.py +17 -1
  8. {quantilica_cli-0.3.0 → quantilica_cli-0.3.2/src}/quantilica/cli/sdk.py +44 -5
  9. {quantilica_cli-0.3.0 → quantilica_cli-0.3.2/src}/quantilica/cli/sources.py +65 -13
  10. {quantilica_cli-0.3.0 → quantilica_cli-0.3.2/src}/quantilica/cli/ui.py +55 -5
  11. {quantilica_cli-0.3.0 → quantilica_cli-0.3.2}/.githooks/pre-commit +0 -0
  12. {quantilica_cli-0.3.0 → quantilica_cli-0.3.2}/.github/workflows/publish.yml +0 -0
  13. {quantilica_cli-0.3.0 → quantilica_cli-0.3.2}/.github/workflows/test.yml +0 -0
  14. {quantilica_cli-0.3.0 → quantilica_cli-0.3.2}/.gitignore +0 -0
  15. {quantilica_cli-0.3.0 → quantilica_cli-0.3.2}/LICENSE +0 -0
  16. {quantilica_cli-0.3.0 → quantilica_cli-0.3.2/src}/quantilica/cli/__init__.py +0 -0
  17. {quantilica_cli-0.3.0 → quantilica_cli-0.3.2/src}/quantilica/py.typed +0 -0
  18. {quantilica_cli-0.3.0 → quantilica_cli-0.3.2}/tests/__init__.py +0 -0
  19. {quantilica_cli-0.3.0 → quantilica_cli-0.3.2}/tests/test_manifests.py +0 -0
  20. {quantilica_cli-0.3.0 → quantilica_cli-0.3.2}/tests/test_sources.py +0 -0
  21. {quantilica_cli-0.3.0 → quantilica_cli-0.3.2}/tests/test_ui.py +0 -0
@@ -5,6 +5,17 @@ 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.3.2] - 2026-08-22
9
+
10
+ ### Corrigido
11
+ - **Crítico:** wheels publicados desde a 0.3.0 vinham **sem os módulos do pacote** — o diretório `quantilica/cli/` não era incluído no build por configuração incorreta do hatchling (`sources` na seção global e `packages` apontando para o subpacote). Instalações via pip/uv reportavam sucesso, mas `import quantilica.cli` falhava em qualquer ambiente não-editable. Configuração realinhada ao padrão dos pacotes irmãos (`packages = ["src/quantilica"]` dentro de `[tool.hatch.build.targets.wheel]`).
12
+
13
+ ## [0.3.1] - 2026-08-22
14
+
15
+ ### Corrigido
16
+ - `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 no ADR de distribuição de 2026-08-22).
17
+ - `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`).
18
+
8
19
  ## [0.3.0] - 2026-08-10
9
20
 
10
21
  ### Adicionado
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: quantilica-cli
3
- Version: 0.3.0
3
+ Version: 0.3.2
4
4
  Summary: Unified CLI for Quantilica open data fetchers
5
5
  Author-email: "Komesu, D.K." <daniel@dkko.me>
6
6
  License-Expression: MIT
@@ -24,6 +24,7 @@ Description-Content-Type: text/markdown
24
24
  # quantilica-cli
25
25
 
26
26
  CLI unificada para o ecossistema Quantilica de dados abertos brasileiros.
27
+ For full documentation, please visit [https://docs.quantilica.com](https://docs.quantilica.com).
27
28
 
28
29
  ## Instalação
29
30
 
@@ -1,6 +1,7 @@
1
1
  # quantilica-cli
2
2
 
3
3
  CLI unificada para o ecossistema Quantilica de dados abertos brasileiros.
4
+ For full documentation, please visit [https://docs.quantilica.com](https://docs.quantilica.com).
4
5
 
5
6
  ## Instalação
6
7
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "quantilica-cli"
3
- version = "0.3.0"
3
+ version = "0.3.2"
4
4
  description = "Unified CLI for Quantilica open data fetchers"
5
5
  readme = "README.md"
6
6
  authors = [{ name = "Komesu, D.K.", email = "daniel@dkko.me" }]
@@ -38,16 +38,14 @@ dev = ["ruff>=0.9.0", "pytest>=8.0.0"]
38
38
 
39
39
  # Para desenvolvimento local do quantilica-core em conjunto com o CLI:
40
40
  # cd quantilica-cli && uv pip install -e ../quantilica-core (sem [tool.uv.sources])
41
- # Em produção, quantilica-core é instalado via git+https (declarado em dependencies).
41
+ # Em produção, quantilica-core resolve pelo PyPI (pin de registro em dependencies).
42
42
 
43
43
  # [tool.hatch.metadata]
44
44
  # allow-direct-references = true
45
45
 
46
- [tool.hatch.build]
47
- sources = ["src"]
48
-
49
46
  [tool.hatch.build.targets.wheel]
50
- packages = ["src/quantilica/cli"]
47
+ sources = ["src"]
48
+ packages = ["src/quantilica"]
51
49
 
52
50
  [build-system]
53
51
  requires = ["hatchling>=1.27"]
@@ -138,6 +138,7 @@ def list_sources(
138
138
 
139
139
 
140
140
  def main() -> None:
141
+ """Main entry point for the quantilica CLI."""
141
142
  if hasattr(sys.stdout, "reconfigure"):
142
143
  sys.stdout.reconfigure(encoding="utf-8", errors="replace") # type: ignore
143
144
  logging.basicConfig(
@@ -47,7 +47,17 @@ _STALE_GRACE = 1.5
47
47
 
48
48
 
49
49
  def parse_since(value: str) -> datetime:
50
- """Parse a relative duration ('7d', '24h') or ISO date into a UTC cutoff."""
50
+ """Parse a relative duration ('7d', '24h') or ISO date into a UTC cutoff.
51
+
52
+ Args:
53
+ value: The string duration or ISO date to parse.
54
+
55
+ Returns:
56
+ The computed UTC cutoff datetime.
57
+
58
+ Raises:
59
+ typer.BadParameter: If the value is neither a valid duration nor an ISO date.
60
+ """
51
61
  match = _DURATION_RE.match(value.strip().lower())
52
62
  if match:
53
63
  amount, unit = int(match.group(1)), match.group(2)
@@ -101,6 +111,12 @@ def iter_manifests(root: Path) -> Iterator[tuple[Path, dict[str, Any]]]:
101
111
  """Yield ``(manifest_path, data)`` for every manifest found under ``root``.
102
112
 
103
113
  Malformed JSON files are skipped with a warning rather than aborting.
114
+
115
+ Args:
116
+ root: The root directory to search for manifests.
117
+
118
+ Yields:
119
+ A tuple containing the path to the manifest and its parsed JSON data.
104
120
  """
105
121
  for path in sorted(root.glob(MANIFEST_GLOB)):
106
122
  if not path.is_file():
@@ -21,6 +21,14 @@ def file_progress(
21
21
 
22
22
  Designed for use with HttpClient.download_with_manifest(progress=...).
23
23
  ``total`` is the expected file size in bytes; pass 0 when unknown.
24
+
25
+ Args:
26
+ description: The text description to show on the progress bar.
27
+ total: Expected file size in bytes. Defaults to 0.
28
+ leave: Whether to leave the progress bar upon completion. Defaults to False.
29
+
30
+ Yields:
31
+ A callback function to update progress.
24
32
  """
25
33
  pbar: tqdm[int] = tqdm(
26
34
  total=total or None,
@@ -56,6 +64,14 @@ def batch_progress(
56
64
  *,
57
65
  total: int,
58
66
  ) -> Generator[tqdm[int], None, None]:
59
- """Context manager for tracking overall batch progress (file count)."""
67
+ """Context manager for tracking overall batch progress (file count).
68
+
69
+ Args:
70
+ description: The text description for the batch progress.
71
+ total: Total number of files/items to track.
72
+
73
+ Yields:
74
+ A tqdm progress bar instance.
75
+ """
60
76
  with tqdm(total=total, desc=description, unit="arquivo", leave=True) as pbar:
61
77
  yield pbar
@@ -34,6 +34,11 @@ logger = get_logger(__name__)
34
34
 
35
35
 
36
36
  def default_client() -> HttpClient:
37
+ """Create a default HttpClient with standard configuration.
38
+
39
+ Returns:
40
+ A pre-configured HttpClient instance.
41
+ """
37
42
  return HttpClient(
38
43
  timeout=180.0,
39
44
  verify=True,
@@ -50,7 +55,18 @@ def default_client() -> HttpClient:
50
55
 
51
56
 
52
57
  class FetcherApp:
53
- """Standard orchestrator for Quantilica fetchers."""
58
+ """Standard orchestrator for Quantilica fetchers.
59
+
60
+ Args:
61
+ name: Name of the fetcher (e.g., 'comex-fetcher').
62
+ help: Help text for the CLI.
63
+ groups_dict: Dictionary of dataset groups and their metadata.
64
+ aliases_dict: Dictionary of alias mappings to dataset groups.
65
+ list_datasets: Callback to list datasets given a group ID.
66
+ path_builder: Callback to build the destination path.
67
+ default_output: Default output directory path.
68
+ client: HTTP or FTP client instance. Defaults to default_client().
69
+ """
54
70
 
55
71
  def __init__(
56
72
  self,
@@ -108,7 +124,21 @@ class FetcherApp:
108
124
  dry_run: bool = False,
109
125
  progress: ProgressCallback | None = None,
110
126
  ) -> Path:
111
- """Download one dataset entry and return the destination path."""
127
+ """Download one dataset entry and return the destination path.
128
+
129
+ Args:
130
+ entry: Dictionary containing dataset metadata (url, id, etc).
131
+ output_dir: Destination directory.
132
+ dry_run: If True, computes the destination path without downloading.
133
+ progress: Optional callback to track download progress.
134
+
135
+ Returns:
136
+ The local path where the file was (or would be) saved.
137
+
138
+ Raises:
139
+ FetchError: If no valid URLs could be downloaded.
140
+ HttpStatusError: On non-404 HTTP errors.
141
+ """
112
142
  urls_to_try = [entry["url"]]
113
143
  if "fallback_urls" in entry and entry["fallback_urls"]:
114
144
  urls_to_try.extend(entry["fallback_urls"])
@@ -155,9 +185,18 @@ class FetcherApp:
155
185
  output_dir: Path,
156
186
  workers: int = 4,
157
187
  ) -> tuple[int, int, list[tuple[str, str]]]:
158
- """
159
- Executes parallel download for a list of dataset entries.
160
- Returns a tuple: (downloaded_count, total_count, errors_list).
188
+ """Executes parallel download for a list of dataset entries.
189
+
190
+ Args:
191
+ entries: List of dataset entries to download.
192
+ output_dir: The target base directory for downloads.
193
+ workers: Maximum number of parallel downloads.
194
+
195
+ Returns:
196
+ A tuple containing:
197
+ - downloaded_count: Number of successfully downloaded files.
198
+ - total_count: Total number of files attempted.
199
+ - errors_list: A list of tuples containing the dataset ID and the error message.
161
200
  """
162
201
  console = get_console()
163
202
  total = len(entries)
@@ -22,7 +22,7 @@ app = typer.Typer(
22
22
  console = Console()
23
23
 
24
24
  FETCHER_GROUP = "quantilica.fetchers"
25
- DEFAULT_INDEX_URL = "https://quantilica.com/quantilica-index/simple/"
25
+ DEFAULT_INDEX_URL = "https://index.quantilica.com/simple/"
26
26
 
27
27
  # Mapeamento estático padrão de comandos CLI para nome das distribuições
28
28
  SOURCES_REGISTRY: dict[str, str] = {
@@ -40,7 +40,11 @@ SOURCES_REGISTRY: dict[str, str] = {
40
40
 
41
41
 
42
42
  def get_config_dir() -> Path:
43
- """Retorna o diretório de configuração do quantilica-cli."""
43
+ """Retorna o diretório de configuração do quantilica-cli.
44
+
45
+ Returns:
46
+ O caminho do diretório de configuração.
47
+ """
44
48
  custom = os.environ.get("QUANTILICA_CONFIG_DIR")
45
49
  if custom:
46
50
  return Path(custom)
@@ -48,12 +52,20 @@ def get_config_dir() -> Path:
48
52
 
49
53
 
50
54
  def get_state_file() -> Path:
51
- """Retorna o caminho para o arquivo de estado local."""
55
+ """Retorna o caminho para o arquivo de estado local.
56
+
57
+ Returns:
58
+ O caminho para o arquivo de estado `state.toml`.
59
+ """
52
60
  return get_config_dir() / "state.toml"
53
61
 
54
62
 
55
63
  def load_state() -> dict[str, Any]:
56
- """Lê o arquivo de estado em ~/.config/quantilica/state.toml."""
64
+ """Lê o arquivo de estado em ~/.config/quantilica/state.toml.
65
+
66
+ Returns:
67
+ Um dicionário contendo o estado carregado.
68
+ """
57
69
  state_file = get_state_file()
58
70
  if not state_file.exists():
59
71
  return {"installed": {}}
@@ -71,7 +83,11 @@ def load_state() -> dict[str, Any]:
71
83
 
72
84
 
73
85
  def save_state(state: dict[str, Any]) -> None:
74
- """Salva o arquivo de estado em ~/.config/quantilica/state.toml."""
86
+ """Salva o arquivo de estado em ~/.config/quantilica/state.toml.
87
+
88
+ Args:
89
+ state: O dicionário de estado a ser salvo.
90
+ """
75
91
  config_dir = get_config_dir()
76
92
  config_dir.mkdir(parents=True, exist_ok=True)
77
93
  state_file = get_state_file()
@@ -85,18 +101,34 @@ def save_state(state: dict[str, Any]) -> None:
85
101
 
86
102
 
87
103
  def get_installed_entry_points() -> set[str]:
88
- """Retorna o conjunto de nomes de entry points de fetchers atualmente instalados."""
104
+ """Retorna o conjunto de nomes de entry points de fetchers atualmente instalados.
105
+
106
+ Returns:
107
+ Um conjunto de nomes de entry points.
108
+ """
89
109
  eps = entry_points(group=FETCHER_GROUP)
90
110
  return {ep.name for ep in eps}
91
111
 
92
112
 
93
113
  def get_index_url() -> str:
94
- """Retorna a URL do índice pip customizado ou default."""
114
+ """Retorna a URL do índice pip customizado ou default.
115
+
116
+ Returns:
117
+ A URL do índice pip.
118
+ """
95
119
  return os.environ.get("QUANTILICA_INDEX_URL", DEFAULT_INDEX_URL)
96
120
 
97
121
 
98
122
  def install_package(dist_name: str, index_url: str | None = None) -> None:
99
- """Instala um pacote Python usando uv pip install se disponível, ou pip."""
123
+ """Instala um pacote Python usando uv pip install se disponível, ou pip.
124
+
125
+ Args:
126
+ dist_name: Nome do pacote a instalar.
127
+ index_url: URL do índice pip. Padrão para a URL do índice global se não especificada.
128
+
129
+ Raises:
130
+ RuntimeError: Se a instalação falhar.
131
+ """
100
132
  idx = index_url or get_index_url()
101
133
 
102
134
  # Prepara o ambiente garantindo que VIRTUAL_ENV aponte para o ambiente atual
@@ -135,7 +167,14 @@ def install_package(dist_name: str, index_url: str | None = None) -> None:
135
167
 
136
168
 
137
169
  def uninstall_package(dist_name: str) -> None:
138
- """Desinstala um pacote Python usando uv pip uninstall se disponível, ou pip."""
170
+ """Desinstala um pacote Python usando uv pip uninstall se disponível, ou pip.
171
+
172
+ Args:
173
+ dist_name: Nome do pacote a desinstalar.
174
+
175
+ Raises:
176
+ RuntimeError: Se a desinstalação falhar.
177
+ """
139
178
  env = os.environ.copy()
140
179
  venv_dir = str(Path(sys.executable).parent.parent)
141
180
  env["VIRTUAL_ENV"] = venv_dir
@@ -154,7 +193,11 @@ def uninstall_package(dist_name: str) -> None:
154
193
 
155
194
 
156
195
  def fetch_remote_sources() -> dict[str, str]:
157
- """Obtém a lista remota de fontes disponíveis do arquivo sources.json do índice."""
196
+ """Obtém a lista remota de fontes disponíveis do arquivo sources.json do índice.
197
+
198
+ Returns:
199
+ Um dicionário mapeando os nomes das fontes para os nomes das distribuições.
200
+ """
158
201
  index_url = get_index_url()
159
202
  # Converte simple/ index url para URL base de sources.json
160
203
  base_url = index_url.rstrip("/")
@@ -178,7 +221,11 @@ def fetch_remote_sources() -> dict[str, str]:
178
221
 
179
222
 
180
223
  def reexec_cli(args: list[str]) -> None:
181
- """Recarrega o processo CLI com os novos entry points instalados."""
224
+ """Recarrega o processo CLI com os novos entry points instalados.
225
+
226
+ Args:
227
+ args: Argumentos a serem passados para a nova execução da CLI.
228
+ """
182
229
  if sys.platform == "win32":
183
230
  code = subprocess.call([sys.executable, "-m", "quantilica.cli.cli", *args])
184
231
  sys.exit(code)
@@ -186,6 +233,11 @@ def reexec_cli(args: list[str]) -> None:
186
233
  os.execv(sys.executable, [sys.executable, "-m", "quantilica.cli.cli", *args])
187
234
 
188
235
 
236
+ def _merged_registry() -> dict[str, str]:
237
+ """Combina o registro remoto (sources.json) com o local; o local tem precedência."""
238
+ return {**fetch_remote_sources(), **SOURCES_REGISTRY}
239
+
240
+
189
241
  @app.command("install")
190
242
  def cmd_install(
191
243
  source: str = typer.Argument(
@@ -196,7 +248,7 @@ def cmd_install(
196
248
  ),
197
249
  ) -> None:
198
250
  """Instala uma fonte de dados (fetcher) sob demanda."""
199
- registry = SOURCES_REGISTRY
251
+ registry = _merged_registry()
200
252
  dist_name = registry.get(source, source)
201
253
 
202
254
  installed_eps = get_installed_entry_points()
@@ -242,7 +294,7 @@ def cmd_uninstall(
242
294
  ),
243
295
  ) -> None:
244
296
  """Desinstala uma fonte de dados (fetcher)."""
245
- registry = SOURCES_REGISTRY
297
+ registry = _merged_registry()
246
298
  dist_name = registry.get(source, source)
247
299
 
248
300
  try:
@@ -38,7 +38,11 @@ _console: Console | None = None
38
38
 
39
39
 
40
40
  def get_console() -> Console:
41
- """Return a process-wide shared Rich console."""
41
+ """Return a process-wide shared Rich console.
42
+
43
+ Returns:
44
+ The shared Rich Console instance.
45
+ """
42
46
  global _console
43
47
  if _console is None:
44
48
  _console = Console()
@@ -53,6 +57,10 @@ def setup_rich_logging(
53
57
  """Configure logging via ``RichHandler`` without breaking progress bars.
54
58
 
55
59
  ``verbose=False`` → WARNING only; ``verbose=True`` → DEBUG.
60
+
61
+ Args:
62
+ verbose: Whether to enable verbose logging.
63
+ console: Optional Rich Console to use for logging.
56
64
  """
57
65
  level = logging.DEBUG if verbose else logging.WARNING
58
66
  logging.basicConfig(
@@ -65,7 +73,14 @@ def setup_rich_logging(
65
73
 
66
74
 
67
75
  def make_batch_progress(console: Console | None = None) -> Progress:
68
- """Build a Progress for overall/batch tracking (file counts)."""
76
+ """Build a Progress for overall/batch tracking (file counts).
77
+
78
+ Args:
79
+ console: Optional Rich Console to use.
80
+
81
+ Returns:
82
+ A Rich Progress instance configured for batch tracking.
83
+ """
69
84
  return Progress(
70
85
  SpinnerColumn(),
71
86
  TextColumn("[progress.description]{task.description}"),
@@ -78,7 +93,14 @@ def make_batch_progress(console: Console | None = None) -> Progress:
78
93
 
79
94
 
80
95
  def make_download_progress(console: Console | None = None) -> Progress:
81
- """Build a Progress for individual file downloads (bytes/speed)."""
96
+ """Build a Progress for individual file downloads (bytes/speed).
97
+
98
+ Args:
99
+ console: Optional Rich Console to use.
100
+
101
+ Returns:
102
+ A Rich Progress instance configured for individual file downloads.
103
+ """
82
104
  return Progress(
83
105
  SpinnerColumn(),
84
106
  TextColumn("[dim]{task.description}[/dim]"),
@@ -99,6 +121,14 @@ def expand_years_cli(
99
121
 
100
122
  If ``years`` is empty and ``default_range`` is provided, it expands the
101
123
  default range. Prints a warning to the console/stderr for any invalid specs.
124
+
125
+ Args:
126
+ years: List of year strings or ranges to expand.
127
+ default_range: A default range to use if years is empty.
128
+ console: Optional Rich Console for printing warnings.
129
+
130
+ Returns:
131
+ A list of expanded year integers.
102
132
  """
103
133
  from quantilica.core.dates import expand_year_range
104
134
 
@@ -114,7 +144,12 @@ def expand_years_cli(
114
144
 
115
145
 
116
146
  class ProgressPool:
117
- """Manages a fixed pool of rich progress bars for concurrent workers."""
147
+ """Manages a fixed pool of rich progress bars for concurrent workers.
148
+
149
+ Args:
150
+ workers: The number of progress bar tasks to initialize.
151
+ file_prog: The Rich Progress instance to add tasks to.
152
+ """
118
153
 
119
154
  def __init__(self, workers: int, file_prog: Progress):
120
155
  self.lock = threading.Lock()
@@ -127,6 +162,14 @@ class ProgressPool:
127
162
  def acquire(
128
163
  self, description: str
129
164
  ) -> Generator[Callable[[int, int], None], None, None]:
165
+ """Acquires a progress bar task and returns an update callback.
166
+
167
+ Args:
168
+ description: The description to show for this task.
169
+
170
+ Yields:
171
+ A callback function to update progress with downloaded and total bytes.
172
+ """
130
173
  with self.lock:
131
174
  task_id = self.available.pop(0)
132
175
  self.file_prog.update(task_id, description=description, completed=0, total=None)
@@ -154,7 +197,14 @@ class ProgressPool:
154
197
  def graceful_executor(
155
198
  max_workers: int,
156
199
  ) -> Generator[concurrent.futures.ThreadPoolExecutor, None, None]:
157
- """ThreadPoolExecutor that cancels futures and shuts down on KeyboardInterrupt."""
200
+ """ThreadPoolExecutor that cancels futures and shuts down on KeyboardInterrupt.
201
+
202
+ Args:
203
+ max_workers: Maximum number of threads in the pool.
204
+
205
+ Yields:
206
+ A thread pool executor instance.
207
+ """
158
208
  executor = concurrent.futures.ThreadPoolExecutor(max_workers=max_workers)
159
209
  # Python 3.9+ supports cancel_futures=True, which automatically
160
210
  # cancels pending futures during shutdown.
File without changes