tabalyst 0.2.1__tar.gz → 0.3.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.
- {tabalyst-0.2.1/src/tabalyst.egg-info → tabalyst-0.3.0}/PKG-INFO +46 -7
- {tabalyst-0.2.1 → tabalyst-0.3.0}/README.md +45 -6
- {tabalyst-0.2.1 → tabalyst-0.3.0}/pyproject.toml +1 -1
- {tabalyst-0.2.1 → tabalyst-0.3.0}/src/tabalyst/__init__.py +6 -0
- {tabalyst-0.2.1 → tabalyst-0.3.0}/src/tabalyst/cli/app.py +2 -0
- tabalyst-0.3.0/src/tabalyst/cli/sample.py +132 -0
- tabalyst-0.3.0/src/tabalyst/sampling.py +342 -0
- tabalyst-0.3.0/src/tabalyst/sampling_service.py +172 -0
- {tabalyst-0.2.1 → tabalyst-0.3.0/src/tabalyst.egg-info}/PKG-INFO +46 -7
- {tabalyst-0.2.1 → tabalyst-0.3.0}/src/tabalyst.egg-info/SOURCES.txt +5 -1
- {tabalyst-0.2.1 → tabalyst-0.3.0}/tests/test_cli.py +2 -2
- tabalyst-0.3.0/tests/test_sampling.py +346 -0
- {tabalyst-0.2.1 → tabalyst-0.3.0}/LICENSE +0 -0
- {tabalyst-0.2.1 → tabalyst-0.3.0}/MANIFEST.in +0 -0
- {tabalyst-0.2.1 → tabalyst-0.3.0}/examples/README.md +0 -0
- {tabalyst-0.2.1 → tabalyst-0.3.0}/examples/config.json +0 -0
- {tabalyst-0.2.1 → tabalyst-0.3.0}/examples/input/basic.csv +0 -0
- {tabalyst-0.2.1 → tabalyst-0.3.0}/examples/input/insurance-customers.csv +0 -0
- {tabalyst-0.2.1 → tabalyst-0.3.0}/setup.cfg +0 -0
- {tabalyst-0.2.1 → tabalyst-0.3.0}/src/tabalyst/__main__.py +0 -0
- {tabalyst-0.2.1 → tabalyst-0.3.0}/src/tabalyst/_version.py +0 -0
- {tabalyst-0.2.1 → tabalyst-0.3.0}/src/tabalyst/analysis.py +0 -0
- {tabalyst-0.2.1 → tabalyst-0.3.0}/src/tabalyst/cli/__init__.py +0 -0
- {tabalyst-0.2.1 → tabalyst-0.3.0}/src/tabalyst/cli/report.py +0 -0
- {tabalyst-0.2.1 → tabalyst-0.3.0}/src/tabalyst/config.py +0 -0
- {tabalyst-0.2.1 → tabalyst-0.3.0}/src/tabalyst/errors.py +0 -0
- {tabalyst-0.2.1 → tabalyst-0.3.0}/src/tabalyst/execution_log.py +0 -0
- {tabalyst-0.2.1 → tabalyst-0.3.0}/src/tabalyst/ingestion.py +0 -0
- {tabalyst-0.2.1 → tabalyst-0.3.0}/src/tabalyst/models.py +0 -0
- {tabalyst-0.2.1 → tabalyst-0.3.0}/src/tabalyst/progress.py +0 -0
- {tabalyst-0.2.1 → tabalyst-0.3.0}/src/tabalyst/report_service.py +0 -0
- {tabalyst-0.2.1 → tabalyst-0.3.0}/src/tabalyst/reporting.py +0 -0
- {tabalyst-0.2.1 → tabalyst-0.3.0}/src/tabalyst/service.py +0 -0
- {tabalyst-0.2.1 → tabalyst-0.3.0}/src/tabalyst/static/report.js +0 -0
- {tabalyst-0.2.1 → tabalyst-0.3.0}/src/tabalyst/static/theme.css +0 -0
- {tabalyst-0.2.1 → tabalyst-0.3.0}/src/tabalyst/templates/report.html +0 -0
- {tabalyst-0.2.1 → tabalyst-0.3.0}/src/tabalyst.egg-info/dependency_links.txt +0 -0
- {tabalyst-0.2.1 → tabalyst-0.3.0}/src/tabalyst.egg-info/entry_points.txt +0 -0
- {tabalyst-0.2.1 → tabalyst-0.3.0}/src/tabalyst.egg-info/requires.txt +0 -0
- {tabalyst-0.2.1 → tabalyst-0.3.0}/src/tabalyst.egg-info/top_level.txt +0 -0
- {tabalyst-0.2.1 → tabalyst-0.3.0}/tests/test_analysis.py +0 -0
- {tabalyst-0.2.1 → tabalyst-0.3.0}/tests/test_public_api.py +0 -0
- {tabalyst-0.2.1 → tabalyst-0.3.0}/tests/test_reporting.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tabalyst
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Open-source toolkit for understanding and working with unfamiliar data.
|
|
5
5
|
Author: Gregory Borelli
|
|
6
6
|
License-Expression: MIT
|
|
@@ -51,9 +51,12 @@ the shared toolkit architecture is being established.
|
|
|
51
51
|
Tabalyst supports Python 3.11, 3.12, 3.13, and 3.14.
|
|
52
52
|
|
|
53
53
|
```console
|
|
54
|
-
pip install tabalyst
|
|
54
|
+
pip install --upgrade tabalyst
|
|
55
55
|
```
|
|
56
56
|
|
|
57
|
+
The same command installs Tabalyst or updates it. Tabalyst is in alpha and
|
|
58
|
+
changes often: update it before each new test.
|
|
59
|
+
|
|
57
60
|
## Tabalyst Report
|
|
58
61
|
|
|
59
62
|
| Use case | Command | Destination |
|
|
@@ -170,6 +173,34 @@ tabalyst --version
|
|
|
170
173
|
|
|
171
174
|
`python -m tabalyst` accepts the same commands.
|
|
172
175
|
|
|
176
|
+
## Tabalyst Sample
|
|
177
|
+
|
|
178
|
+
Create a smaller CSV without modifying the source:
|
|
179
|
+
|
|
180
|
+
```console
|
|
181
|
+
tabalyst sample customers.csv --sample-method random --rows 1000 --seed 42
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
The default output is `customers.sample.csv` beside the source. Use `-o` to
|
|
185
|
+
name the output for one input, or `-d` to sample several files into one
|
|
186
|
+
directory:
|
|
187
|
+
|
|
188
|
+
```console
|
|
189
|
+
tabalyst sample customers.csv --sample-method first --rows 100 -o test.csv
|
|
190
|
+
tabalyst sample *.csv --sample-method random --percent 5 -d samples/
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
Available methods are `first`, `last`, `random` and `stratified`. Stratified
|
|
194
|
+
sampling approximately preserves the distribution of a selected field:
|
|
195
|
+
|
|
196
|
+
```console
|
|
197
|
+
tabalyst sample customers.csv --sample-method stratified --field province --rows 1000 --seed 42
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
Sampling reads CSV records as a stream. Random and stratified sampling keep
|
|
201
|
+
only the requested sample, plus stratum counts, in memory. Existing outputs
|
|
202
|
+
require `--force`, and an input file is never overwritten.
|
|
203
|
+
|
|
173
204
|
## What the report analyzes
|
|
174
205
|
|
|
175
206
|
- Dataset dimensions, missing cells, duplicates, and quality observations.
|
|
@@ -200,6 +231,13 @@ batch = tabalyst.generate_reports(
|
|
|
200
231
|
["*.csv"],
|
|
201
232
|
output_dir="reports",
|
|
202
233
|
)
|
|
234
|
+
|
|
235
|
+
sample = tabalyst.sample_csv(
|
|
236
|
+
"customers.csv",
|
|
237
|
+
method="random",
|
|
238
|
+
rows=1000,
|
|
239
|
+
seed=42,
|
|
240
|
+
)
|
|
203
241
|
```
|
|
204
242
|
|
|
205
243
|
`analyze()` returns the JSON-serializable profile for one report.
|
|
@@ -224,7 +262,7 @@ Configuration files are strict JSON. A minimal file is:
|
|
|
224
262
|
|
|
225
263
|
Explicit CLI or Python arguments override the configuration file, which overrides
|
|
226
264
|
Tabalyst defaults. See the
|
|
227
|
-
[configuration reference](https://github.com/loribel-labs/tabalyst/blob/main/docs/configuration.md)
|
|
265
|
+
[configuration reference](https://github.com/loribel-labs/tabalyst/blob/main/docs/en/reference/configuration.md)
|
|
228
266
|
for all analysis settings.
|
|
229
267
|
|
|
230
268
|
## Local-first behavior and current limits
|
|
@@ -233,8 +271,9 @@ Tabalyst performs analysis locally and adds no telemetry or remote processing.
|
|
|
233
271
|
Generated JSON and HTML may contain source values and should be shared
|
|
234
272
|
accordingly.
|
|
235
273
|
|
|
236
|
-
|
|
237
|
-
|
|
274
|
+
Report analysis currently loads the complete CSV into memory. Sampling uses a
|
|
275
|
+
streaming reader with bounded row storage. File and phase progress is available
|
|
276
|
+
for reports; row percentages, throughput estimates, recursive directory input,
|
|
238
277
|
and parallel batch execution will require later ingestion work.
|
|
239
278
|
|
|
240
279
|
## Examples and development
|
|
@@ -251,8 +290,8 @@ python -m build
|
|
|
251
290
|
|
|
252
291
|
Additional documentation:
|
|
253
292
|
|
|
254
|
-
- [Architecture](https://github.com/loribel-labs/tabalyst/blob/main/docs/architecture.md)
|
|
255
|
-
- [Configuration](https://github.com/loribel-labs/tabalyst/blob/main/docs/configuration.md)
|
|
293
|
+
- [Architecture](https://github.com/loribel-labs/tabalyst/blob/main/docs/dev/architecture.md)
|
|
294
|
+
- [Configuration](https://github.com/loribel-labs/tabalyst/blob/main/docs/en/reference/configuration.md)
|
|
256
295
|
- [Release procedure](https://github.com/loribel-labs/tabalyst/blob/main/RELEASING.md)
|
|
257
296
|
|
|
258
297
|
Please report defects and feature requests through the
|
|
@@ -17,9 +17,12 @@ the shared toolkit architecture is being established.
|
|
|
17
17
|
Tabalyst supports Python 3.11, 3.12, 3.13, and 3.14.
|
|
18
18
|
|
|
19
19
|
```console
|
|
20
|
-
pip install tabalyst
|
|
20
|
+
pip install --upgrade tabalyst
|
|
21
21
|
```
|
|
22
22
|
|
|
23
|
+
The same command installs Tabalyst or updates it. Tabalyst is in alpha and
|
|
24
|
+
changes often: update it before each new test.
|
|
25
|
+
|
|
23
26
|
## Tabalyst Report
|
|
24
27
|
|
|
25
28
|
| Use case | Command | Destination |
|
|
@@ -136,6 +139,34 @@ tabalyst --version
|
|
|
136
139
|
|
|
137
140
|
`python -m tabalyst` accepts the same commands.
|
|
138
141
|
|
|
142
|
+
## Tabalyst Sample
|
|
143
|
+
|
|
144
|
+
Create a smaller CSV without modifying the source:
|
|
145
|
+
|
|
146
|
+
```console
|
|
147
|
+
tabalyst sample customers.csv --sample-method random --rows 1000 --seed 42
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
The default output is `customers.sample.csv` beside the source. Use `-o` to
|
|
151
|
+
name the output for one input, or `-d` to sample several files into one
|
|
152
|
+
directory:
|
|
153
|
+
|
|
154
|
+
```console
|
|
155
|
+
tabalyst sample customers.csv --sample-method first --rows 100 -o test.csv
|
|
156
|
+
tabalyst sample *.csv --sample-method random --percent 5 -d samples/
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
Available methods are `first`, `last`, `random` and `stratified`. Stratified
|
|
160
|
+
sampling approximately preserves the distribution of a selected field:
|
|
161
|
+
|
|
162
|
+
```console
|
|
163
|
+
tabalyst sample customers.csv --sample-method stratified --field province --rows 1000 --seed 42
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
Sampling reads CSV records as a stream. Random and stratified sampling keep
|
|
167
|
+
only the requested sample, plus stratum counts, in memory. Existing outputs
|
|
168
|
+
require `--force`, and an input file is never overwritten.
|
|
169
|
+
|
|
139
170
|
## What the report analyzes
|
|
140
171
|
|
|
141
172
|
- Dataset dimensions, missing cells, duplicates, and quality observations.
|
|
@@ -166,6 +197,13 @@ batch = tabalyst.generate_reports(
|
|
|
166
197
|
["*.csv"],
|
|
167
198
|
output_dir="reports",
|
|
168
199
|
)
|
|
200
|
+
|
|
201
|
+
sample = tabalyst.sample_csv(
|
|
202
|
+
"customers.csv",
|
|
203
|
+
method="random",
|
|
204
|
+
rows=1000,
|
|
205
|
+
seed=42,
|
|
206
|
+
)
|
|
169
207
|
```
|
|
170
208
|
|
|
171
209
|
`analyze()` returns the JSON-serializable profile for one report.
|
|
@@ -190,7 +228,7 @@ Configuration files are strict JSON. A minimal file is:
|
|
|
190
228
|
|
|
191
229
|
Explicit CLI or Python arguments override the configuration file, which overrides
|
|
192
230
|
Tabalyst defaults. See the
|
|
193
|
-
[configuration reference](https://github.com/loribel-labs/tabalyst/blob/main/docs/configuration.md)
|
|
231
|
+
[configuration reference](https://github.com/loribel-labs/tabalyst/blob/main/docs/en/reference/configuration.md)
|
|
194
232
|
for all analysis settings.
|
|
195
233
|
|
|
196
234
|
## Local-first behavior and current limits
|
|
@@ -199,8 +237,9 @@ Tabalyst performs analysis locally and adds no telemetry or remote processing.
|
|
|
199
237
|
Generated JSON and HTML may contain source values and should be shared
|
|
200
238
|
accordingly.
|
|
201
239
|
|
|
202
|
-
|
|
203
|
-
|
|
240
|
+
Report analysis currently loads the complete CSV into memory. Sampling uses a
|
|
241
|
+
streaming reader with bounded row storage. File and phase progress is available
|
|
242
|
+
for reports; row percentages, throughput estimates, recursive directory input,
|
|
204
243
|
and parallel batch execution will require later ingestion work.
|
|
205
244
|
|
|
206
245
|
## Examples and development
|
|
@@ -217,8 +256,8 @@ python -m build
|
|
|
217
256
|
|
|
218
257
|
Additional documentation:
|
|
219
258
|
|
|
220
|
-
- [Architecture](https://github.com/loribel-labs/tabalyst/blob/main/docs/architecture.md)
|
|
221
|
-
- [Configuration](https://github.com/loribel-labs/tabalyst/blob/main/docs/configuration.md)
|
|
259
|
+
- [Architecture](https://github.com/loribel-labs/tabalyst/blob/main/docs/dev/architecture.md)
|
|
260
|
+
- [Configuration](https://github.com/loribel-labs/tabalyst/blob/main/docs/en/reference/configuration.md)
|
|
222
261
|
- [Release procedure](https://github.com/loribel-labs/tabalyst/blob/main/RELEASING.md)
|
|
223
262
|
|
|
224
263
|
Please report defects and feature requests through the
|
|
@@ -6,6 +6,10 @@ from tabalyst.config import AnalysisConfig as AnalysisConfig
|
|
|
6
6
|
from tabalyst.errors import ConfigurationError, InputError, ReportError, TabalystError
|
|
7
7
|
from tabalyst.report_service import generate_reports as generate_reports
|
|
8
8
|
from tabalyst.reporting import render_report as render_report
|
|
9
|
+
from tabalyst.sampling import SampleMethod as SampleMethod
|
|
10
|
+
from tabalyst.sampling import SampleResult as SampleResult
|
|
11
|
+
from tabalyst.sampling import sample_csv as sample_csv
|
|
12
|
+
from tabalyst.sampling_service import generate_samples as generate_samples
|
|
9
13
|
from tabalyst.service import analyze
|
|
10
14
|
from tabalyst.service import analyze_csv as analyze_csv
|
|
11
15
|
|
|
@@ -17,6 +21,8 @@ __all__ = [
|
|
|
17
21
|
"__version__",
|
|
18
22
|
"analyze",
|
|
19
23
|
"generate_reports",
|
|
24
|
+
"generate_samples",
|
|
25
|
+
"sample_csv",
|
|
20
26
|
]
|
|
21
27
|
|
|
22
28
|
# Alpha compatibility imports remain available, but are not part of the 0.1.0 API.
|
|
@@ -6,6 +6,7 @@ import typer
|
|
|
6
6
|
|
|
7
7
|
from tabalyst import __version__
|
|
8
8
|
from tabalyst.cli.report import report_command
|
|
9
|
+
from tabalyst.cli.sample import sample_command
|
|
9
10
|
|
|
10
11
|
app = typer.Typer(
|
|
11
12
|
add_completion=False,
|
|
@@ -37,6 +38,7 @@ def root(
|
|
|
37
38
|
|
|
38
39
|
|
|
39
40
|
app.command("report")(report_command)
|
|
41
|
+
app.command("sample")(sample_command)
|
|
40
42
|
|
|
41
43
|
|
|
42
44
|
def main() -> None:
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"""CLI adapter for CSV sampling."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Annotated
|
|
5
|
+
|
|
6
|
+
import typer
|
|
7
|
+
|
|
8
|
+
from tabalyst.errors import ConfigurationError, InputError, TabalystError
|
|
9
|
+
from tabalyst.sampling import SampleMethod, SampleResult
|
|
10
|
+
from tabalyst.sampling_service import generate_samples
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _print_result(result: SampleResult) -> None:
|
|
14
|
+
typer.echo("Sample created successfully.", err=True)
|
|
15
|
+
typer.echo("", err=True)
|
|
16
|
+
typer.echo(f"Source: {result.source}", err=True)
|
|
17
|
+
typer.echo(f"Method: {result.method.value}", err=True)
|
|
18
|
+
if result.field is not None:
|
|
19
|
+
typer.echo(f"Field: {result.field}", err=True)
|
|
20
|
+
typer.echo(f"Source rows: {result.source_rows:,}", err=True)
|
|
21
|
+
typer.echo(f"Sample rows: {result.sample_rows:,}", err=True)
|
|
22
|
+
typer.echo(f"Sample rate: {result.sample_rate:.2f}%", err=True)
|
|
23
|
+
if result.seed is not None and result.method in {
|
|
24
|
+
SampleMethod.RANDOM,
|
|
25
|
+
SampleMethod.STRATIFIED,
|
|
26
|
+
}:
|
|
27
|
+
typer.echo(f"Seed: {result.seed}", err=True)
|
|
28
|
+
typer.echo(f"Output: {result.output.resolve()}", err=True)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def sample_command(
|
|
32
|
+
inputs: Annotated[
|
|
33
|
+
list[str],
|
|
34
|
+
typer.Argument(
|
|
35
|
+
metavar="INPUT...",
|
|
36
|
+
help="One or more CSV files or non-recursive glob patterns.",
|
|
37
|
+
),
|
|
38
|
+
],
|
|
39
|
+
method: Annotated[
|
|
40
|
+
SampleMethod,
|
|
41
|
+
typer.Option("--sample-method", help="Sampling strategy."),
|
|
42
|
+
],
|
|
43
|
+
rows: Annotated[
|
|
44
|
+
int | None,
|
|
45
|
+
typer.Option("--rows", help="Number of data rows to select."),
|
|
46
|
+
] = None,
|
|
47
|
+
percent: Annotated[
|
|
48
|
+
float | None,
|
|
49
|
+
typer.Option("--percent", help="Percentage of data rows to select."),
|
|
50
|
+
] = None,
|
|
51
|
+
field: Annotated[
|
|
52
|
+
str | None,
|
|
53
|
+
typer.Option("--field", help="Field used for stratified sampling."),
|
|
54
|
+
] = None,
|
|
55
|
+
seed: Annotated[
|
|
56
|
+
int | None,
|
|
57
|
+
typer.Option("--seed", help="Seed for reproducible random selection."),
|
|
58
|
+
] = None,
|
|
59
|
+
output: Annotated[
|
|
60
|
+
Path | None,
|
|
61
|
+
typer.Option("--output", "-o", help="Output CSV filename."),
|
|
62
|
+
] = None,
|
|
63
|
+
output_dir: Annotated[
|
|
64
|
+
Path | None,
|
|
65
|
+
typer.Option(
|
|
66
|
+
"--output-dir",
|
|
67
|
+
"-d",
|
|
68
|
+
help="Directory for samples named after their source files.",
|
|
69
|
+
),
|
|
70
|
+
] = None,
|
|
71
|
+
config: Annotated[
|
|
72
|
+
Path | None,
|
|
73
|
+
typer.Option("--config", "-c", help="JSON configuration file."),
|
|
74
|
+
] = None,
|
|
75
|
+
delimiter: Annotated[
|
|
76
|
+
str | None,
|
|
77
|
+
typer.Option("--delimiter", help="One-character CSV delimiter."),
|
|
78
|
+
] = None,
|
|
79
|
+
encoding: Annotated[
|
|
80
|
+
str | None,
|
|
81
|
+
typer.Option("--encoding", help="CSV text encoding."),
|
|
82
|
+
] = None,
|
|
83
|
+
force: Annotated[
|
|
84
|
+
bool,
|
|
85
|
+
typer.Option("--force", "-f", help="Replace an existing sample file."),
|
|
86
|
+
] = False,
|
|
87
|
+
) -> None:
|
|
88
|
+
"""Create a CSV sample without modifying the source file."""
|
|
89
|
+
|
|
90
|
+
try:
|
|
91
|
+
batch = generate_samples(
|
|
92
|
+
inputs,
|
|
93
|
+
method=method,
|
|
94
|
+
rows=rows,
|
|
95
|
+
percent=percent,
|
|
96
|
+
field=field,
|
|
97
|
+
seed=seed,
|
|
98
|
+
output=output,
|
|
99
|
+
output_dir=output_dir,
|
|
100
|
+
config_path=config,
|
|
101
|
+
delimiter=delimiter,
|
|
102
|
+
encoding=encoding,
|
|
103
|
+
force=force,
|
|
104
|
+
)
|
|
105
|
+
except TabalystError as exc:
|
|
106
|
+
typer.echo(f"Error: {exc}", err=True)
|
|
107
|
+
if isinstance(exc, ConfigurationError):
|
|
108
|
+
code = 2
|
|
109
|
+
elif isinstance(exc, InputError):
|
|
110
|
+
code = 4
|
|
111
|
+
else:
|
|
112
|
+
code = 1
|
|
113
|
+
raise typer.Exit(code) from exc
|
|
114
|
+
|
|
115
|
+
for index, success in enumerate(batch.successes):
|
|
116
|
+
if index:
|
|
117
|
+
typer.echo("", err=True)
|
|
118
|
+
_print_result(success.result)
|
|
119
|
+
for failure in batch.failures:
|
|
120
|
+
typer.echo(f"Error [{failure.job.source}]: {failure.error}", err=True)
|
|
121
|
+
if len(batch.plan.jobs) > 1:
|
|
122
|
+
typer.echo(
|
|
123
|
+
f"{len(batch.successes)} succeeded, {len(batch.failures)} failed",
|
|
124
|
+
err=True,
|
|
125
|
+
)
|
|
126
|
+
if batch.failures:
|
|
127
|
+
code = (
|
|
128
|
+
4
|
|
129
|
+
if all(isinstance(item.error, InputError) for item in batch.failures)
|
|
130
|
+
else 1
|
|
131
|
+
)
|
|
132
|
+
raise typer.Exit(code)
|