qedro 0.2.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.
- qedro/__init__.py +9 -0
- qedro/__main__.py +505 -0
- qedro/config.py +248 -0
- qedro/declared.py +110 -0
- qedro/deployer.py +414 -0
- qedro/document.py +87 -0
- qedro/errors.py +37 -0
- qedro/events.py +257 -0
- qedro/mark.py +44 -0
- qedro/provenance.py +478 -0
- qedro/quality.py +438 -0
- qedro/render.py +973 -0
- qedro/ropa.py +561 -0
- qedro/scope.py +119 -0
- qedro/sources.py +443 -0
- qedro/vocabularies/__init__.py +0 -0
- qedro/vocabularies/dsgvo.yaml +62 -0
- qedro/vocabulary.py +169 -0
- qedro/words.py +31 -0
- qedro/xlsx.py +763 -0
- qedro-0.2.0.dist-info/METADATA +557 -0
- qedro-0.2.0.dist-info/RECORD +26 -0
- qedro-0.2.0.dist-info/WHEEL +5 -0
- qedro-0.2.0.dist-info/entry_points.txt +2 -0
- qedro-0.2.0.dist-info/licenses/LICENSE +202 -0
- qedro-0.2.0.dist-info/top_level.txt +1 -0
qedro/__init__.py
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"""Qedro — turns emitted evidence into the artefacts an auditor asks for."""
|
|
2
|
+
|
|
3
|
+
#: The single source. `pyproject.toml` reads this rather than restating it.
|
|
4
|
+
__version__ = "0.2.0"
|
|
5
|
+
|
|
6
|
+
#: U+220E END OF PROOF. Printed only when an artefact stands on its own
|
|
7
|
+
#: evidence — see `complete` in qedro.mark. Not decoration: withholding it is
|
|
8
|
+
#: the signal, so nothing should print it unconditionally.
|
|
9
|
+
TOMBSTONE = "∎"
|
qedro/__main__.py
ADDED
|
@@ -0,0 +1,505 @@
|
|
|
1
|
+
"""CLI entry point.
|
|
2
|
+
|
|
3
|
+
One command so far. ``qedro events`` reads a directory or a Marquez-compatible
|
|
4
|
+
API and says what it found — not a projection, and it does not pretend to be
|
|
5
|
+
one, but it is the thing to run first to find out whether your lineage is
|
|
6
|
+
readable at all.
|
|
7
|
+
|
|
8
|
+
The projections come next. This exists so the reader is usable before they
|
|
9
|
+
land, and so the completeness rule is exercised end to end rather than only
|
|
10
|
+
in tests.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import argparse
|
|
16
|
+
import os
|
|
17
|
+
import sys
|
|
18
|
+
from collections import Counter
|
|
19
|
+
from datetime import UTC, datetime, timedelta
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
|
|
22
|
+
from . import __version__, declared, deployer, provenance, quality, render, ropa, vocabulary, words
|
|
23
|
+
from . import config as config_module
|
|
24
|
+
from .errors import ConfigError, QedroError, UsageError
|
|
25
|
+
from .mark import Completeness
|
|
26
|
+
from .sources import read
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _instant(value: str) -> datetime:
|
|
30
|
+
"""A `--since` / `--until` argument.
|
|
31
|
+
|
|
32
|
+
A bare date is the start of that day. `--since 2026-01-01` meaning
|
|
33
|
+
*"anything on or after the first"* is what everyone expects it to mean, and
|
|
34
|
+
`fromisoformat` already reads both spellings on the 3.12 floor.
|
|
35
|
+
"""
|
|
36
|
+
try:
|
|
37
|
+
return datetime.fromisoformat(value)
|
|
38
|
+
except ValueError:
|
|
39
|
+
raise argparse.ArgumentTypeError(
|
|
40
|
+
f"{value!r} is not an ISO-8601 date or timestamp (try 2026-01-01)"
|
|
41
|
+
) from None
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _window(args: argparse.Namespace) -> tuple[datetime | None, datetime | None]:
|
|
45
|
+
"""`--days 90`, resolved against `--until` or against now.
|
|
46
|
+
|
|
47
|
+
Sugar over `--since`, because *the last quarter* is how anybody asks for an
|
|
48
|
+
assertion history and computing the date by hand is a papercut. An explicit
|
|
49
|
+
`--since` wins: two flags meaning the same thing must not silently
|
|
50
|
+
disagree, and the one the user typed is the one they meant.
|
|
51
|
+
"""
|
|
52
|
+
since, until = args.since, args.until
|
|
53
|
+
if args.days is not None and since is None:
|
|
54
|
+
end = until or datetime.now(UTC)
|
|
55
|
+
since = end - timedelta(days=args.days)
|
|
56
|
+
return since, until
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _events(target: str, *, symbol: bool, since: datetime | None, until: datetime | None) -> int:
|
|
60
|
+
events, report = read(target, since=since, until=until)
|
|
61
|
+
|
|
62
|
+
completeness = Completeness()
|
|
63
|
+
for reason in report.reasons():
|
|
64
|
+
completeness = completeness.degraded(reason)
|
|
65
|
+
|
|
66
|
+
if not events and report.clean:
|
|
67
|
+
# A clean read of nothing is not a proof of anything. The mark says
|
|
68
|
+
# *this stands on its own evidence*, and there is no evidence here —
|
|
69
|
+
# which is the difference between "nothing happened" and "nothing was
|
|
70
|
+
# recorded" that a reviewer needs flagged.
|
|
71
|
+
completeness = completeness.degraded(
|
|
72
|
+
"no events in the window — nothing was recorded, "
|
|
73
|
+
"which is not the same as nothing having happened"
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
if not events and not report.clean:
|
|
77
|
+
# Nothing read and something wrong: a failure, not a quiet success
|
|
78
|
+
# with an empty result.
|
|
79
|
+
for reason in report.reasons():
|
|
80
|
+
print(f" {reason}", file=sys.stderr)
|
|
81
|
+
return 1
|
|
82
|
+
|
|
83
|
+
by_type = Counter(e.event_type for e in events)
|
|
84
|
+
jobs = {e.job.key for e in events}
|
|
85
|
+
datasets = {d.key for e in events for d in e.datasets}
|
|
86
|
+
|
|
87
|
+
# One of the two is always zero — a directory has files and an API has
|
|
88
|
+
# pages, and naming the wrong one reads as a bug in the tool.
|
|
89
|
+
fetched = (
|
|
90
|
+
words.count(report.files, "file") if report.files else words.count(report.pages, "page")
|
|
91
|
+
)
|
|
92
|
+
summary = " · ".join(
|
|
93
|
+
[
|
|
94
|
+
words.count(report.events, "event"),
|
|
95
|
+
words.count(len(jobs), "job"),
|
|
96
|
+
words.count(len(datasets), "dataset"),
|
|
97
|
+
fetched,
|
|
98
|
+
]
|
|
99
|
+
)
|
|
100
|
+
suffix = completeness.suffix(symbol=symbol)
|
|
101
|
+
print(f" {summary}{' ' + suffix if suffix else ''}")
|
|
102
|
+
|
|
103
|
+
if by_type:
|
|
104
|
+
print(" " + ", ".join(f"{t.lower()} {n}" for t, n in sorted(by_type.items())))
|
|
105
|
+
|
|
106
|
+
# Reasons are printed, not only counted. A reader who cannot see why the
|
|
107
|
+
# mark is missing has been told nothing useful.
|
|
108
|
+
for reason in completeness.reasons:
|
|
109
|
+
print(f" ! {reason}", file=sys.stderr)
|
|
110
|
+
|
|
111
|
+
return 0
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _ropa(
|
|
115
|
+
source: str,
|
|
116
|
+
*,
|
|
117
|
+
symbol: bool,
|
|
118
|
+
since: datetime | None,
|
|
119
|
+
until: datetime | None,
|
|
120
|
+
config_path: str | None,
|
|
121
|
+
vocabulary_path: str | None,
|
|
122
|
+
fmt: str | None,
|
|
123
|
+
out: str | None,
|
|
124
|
+
view: str = "art30",
|
|
125
|
+
activities_path: str | None = None,
|
|
126
|
+
) -> int:
|
|
127
|
+
fmt = _format(fmt, out)
|
|
128
|
+
settings = config_module.load(config_module.find(config_path))
|
|
129
|
+
# `--vocabulary` beats `vocabulary:` in the config, which beats the shipped
|
|
130
|
+
# default. The flag is what someone reaches for while trying one out.
|
|
131
|
+
words = vocabulary.load(vocabulary_path or settings.vocabulary or None)
|
|
132
|
+
# Read before the events too: a typo in a document that asserts a lawful
|
|
133
|
+
# basis should fail in the first millisecond, not after a long read.
|
|
134
|
+
declared_uses = declared.load(activities_path, vocabulary=words) if activities_path else ()
|
|
135
|
+
|
|
136
|
+
events, report = read(source, since=since, until=until)
|
|
137
|
+
record = ropa.build(
|
|
138
|
+
events,
|
|
139
|
+
config=settings,
|
|
140
|
+
vocabulary=words,
|
|
141
|
+
report=report,
|
|
142
|
+
since=since,
|
|
143
|
+
until=until,
|
|
144
|
+
declared=declared_uses,
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
if not record.activities and not report.clean:
|
|
148
|
+
# Nothing projected and something wrong reading: a failure, not an
|
|
149
|
+
# empty record that looks like an answer.
|
|
150
|
+
for reason in report.reasons():
|
|
151
|
+
print(f" {reason}", file=sys.stderr)
|
|
152
|
+
return 1
|
|
153
|
+
|
|
154
|
+
if view == "deployer":
|
|
155
|
+
# Built from the Art. 30 record rather than from the events alone, so
|
|
156
|
+
# the two views share purpose and legal basis instead of restating them.
|
|
157
|
+
view_record = deployer.build(
|
|
158
|
+
record,
|
|
159
|
+
events,
|
|
160
|
+
report=report,
|
|
161
|
+
since=since,
|
|
162
|
+
until=until,
|
|
163
|
+
)
|
|
164
|
+
return _emit(view_record, fmt=fmt, out=out, symbol=symbol)
|
|
165
|
+
|
|
166
|
+
return _emit(record, fmt=fmt, out=out, symbol=symbol)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def _quality(
|
|
170
|
+
source: str,
|
|
171
|
+
*,
|
|
172
|
+
symbol: bool,
|
|
173
|
+
since: datetime | None,
|
|
174
|
+
until: datetime | None,
|
|
175
|
+
domains: list[str],
|
|
176
|
+
config_path: str | None,
|
|
177
|
+
fmt: str | None,
|
|
178
|
+
out: str | None,
|
|
179
|
+
) -> int:
|
|
180
|
+
fmt = _format(fmt, out)
|
|
181
|
+
settings = config_module.load(config_module.find(config_path))
|
|
182
|
+
|
|
183
|
+
events, report = read(source, since=since, until=until)
|
|
184
|
+
record = quality.build(
|
|
185
|
+
events,
|
|
186
|
+
config=settings,
|
|
187
|
+
report=report,
|
|
188
|
+
since=since,
|
|
189
|
+
until=until,
|
|
190
|
+
domains=domains,
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
if not record.datasets and not record.unchecked and not report.clean:
|
|
194
|
+
for reason in report.reasons():
|
|
195
|
+
print(f" {reason}", file=sys.stderr)
|
|
196
|
+
return 1
|
|
197
|
+
|
|
198
|
+
return _emit(record, fmt=fmt, out=out, symbol=symbol)
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def _provenance(
|
|
202
|
+
source: str,
|
|
203
|
+
*,
|
|
204
|
+
symbol: bool,
|
|
205
|
+
since: datetime | None,
|
|
206
|
+
until: datetime | None,
|
|
207
|
+
dataset: str,
|
|
208
|
+
depth: int,
|
|
209
|
+
config_path: str | None,
|
|
210
|
+
fmt: str | None,
|
|
211
|
+
out: str | None,
|
|
212
|
+
) -> int:
|
|
213
|
+
fmt = _format(fmt, out)
|
|
214
|
+
settings = config_module.load(config_module.find(config_path))
|
|
215
|
+
|
|
216
|
+
events, report = read(source, since=since, until=until)
|
|
217
|
+
events = list(events)
|
|
218
|
+
|
|
219
|
+
# Nobody types the namespace, so a bare name is matched — but an ambiguous
|
|
220
|
+
# one is handed back as a question. Picking the first would produce a chain
|
|
221
|
+
# for a dataset the user did not ask about, and nothing downstream would
|
|
222
|
+
# ever say so.
|
|
223
|
+
matches = provenance.resolve(events, dataset)
|
|
224
|
+
if not matches:
|
|
225
|
+
raise ConfigError(
|
|
226
|
+
f"no dataset named {dataset!r} appears in {report.origin or source} "
|
|
227
|
+
"— check the spelling, or widen the window"
|
|
228
|
+
)
|
|
229
|
+
if len(matches) > 1:
|
|
230
|
+
listed = "\n ".join(matches)
|
|
231
|
+
raise ConfigError(
|
|
232
|
+
f"{dataset!r} matches more than one dataset. Name one in full:\n {listed}"
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
record = provenance.build(
|
|
236
|
+
events,
|
|
237
|
+
dataset=matches[0],
|
|
238
|
+
config=settings,
|
|
239
|
+
report=report,
|
|
240
|
+
since=since,
|
|
241
|
+
until=until,
|
|
242
|
+
depth=depth,
|
|
243
|
+
)
|
|
244
|
+
return _emit(record, fmt=fmt, out=out, symbol=symbol)
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def _format(fmt: str | None, out: str | None) -> str:
|
|
248
|
+
"""Resolve the output format before a single event is read.
|
|
249
|
+
|
|
250
|
+
An explicit `--format` wins; otherwise `--out history.md` means markdown
|
|
251
|
+
and `--out history.xlsx` means a workbook. A combination that cannot work
|
|
252
|
+
should fail in the first millisecond rather than after a long read against
|
|
253
|
+
a remote backend.
|
|
254
|
+
"""
|
|
255
|
+
fmt = fmt or render.infer(out)
|
|
256
|
+
if render.is_binary(fmt) and not out:
|
|
257
|
+
raise UsageError(
|
|
258
|
+
f"--format {fmt} produces a file rather than text, and there is nowhere "
|
|
259
|
+
f"to put it — add --out record.{fmt}"
|
|
260
|
+
)
|
|
261
|
+
return fmt
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def _emit(record, *, fmt: str, out: str | None, symbol: bool) -> int:
|
|
265
|
+
"""Render and deliver, the same way for every projection."""
|
|
266
|
+
renderer = render.FORMATS[fmt]
|
|
267
|
+
rendered = renderer(record, symbol=symbol) if fmt == "text" else renderer(record)
|
|
268
|
+
|
|
269
|
+
if not out:
|
|
270
|
+
print(rendered, end="")
|
|
271
|
+
return 0
|
|
272
|
+
|
|
273
|
+
path = Path(out)
|
|
274
|
+
if isinstance(rendered, bytes):
|
|
275
|
+
path.write_bytes(rendered)
|
|
276
|
+
else:
|
|
277
|
+
path.write_text(rendered, encoding="utf-8")
|
|
278
|
+
|
|
279
|
+
mark = record.completeness.suffix(symbol=symbol)
|
|
280
|
+
print(f" wrote {out}{' ' + mark if mark else ''}")
|
|
281
|
+
# Reasons go to stderr even when the file was written. The run succeeded;
|
|
282
|
+
# the artefact simply does not claim to be a proof.
|
|
283
|
+
for reason in record.completeness.reasons:
|
|
284
|
+
print(f" ! {reason}", file=sys.stderr)
|
|
285
|
+
return 0
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def main(argv: list[str] | None = None) -> int:
|
|
289
|
+
# Shared flags go on a parent parser so they are accepted on either side
|
|
290
|
+
# of the subcommand. `qedro events ./ --no-symbol` is how people actually
|
|
291
|
+
# type it, and rejecting that is a papercut nobody reports, they just stop.
|
|
292
|
+
#
|
|
293
|
+
# SUPPRESS as the default matters: without it the subparser's default
|
|
294
|
+
# overwrites a value already set at the top level, and the flag silently
|
|
295
|
+
# does nothing when written before the subcommand.
|
|
296
|
+
common = argparse.ArgumentParser(add_help=False)
|
|
297
|
+
common.add_argument(
|
|
298
|
+
"--no-symbol",
|
|
299
|
+
action="store_true",
|
|
300
|
+
default=argparse.SUPPRESS,
|
|
301
|
+
help="print [complete] instead of the tombstone, for terminals with no glyph for U+220E",
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
parser = argparse.ArgumentParser(
|
|
305
|
+
prog="qedro",
|
|
306
|
+
parents=[common],
|
|
307
|
+
description="Turns emitted evidence into the artefacts an auditor asks for.",
|
|
308
|
+
)
|
|
309
|
+
parser.add_argument("-V", "--version", action="version", version=f"qedro {__version__}")
|
|
310
|
+
|
|
311
|
+
sub = parser.add_subparsers(dest="command")
|
|
312
|
+
events = sub.add_parser(
|
|
313
|
+
"events",
|
|
314
|
+
parents=[common],
|
|
315
|
+
help="read OpenLineage events from a directory or an API and summarise them",
|
|
316
|
+
)
|
|
317
|
+
events.add_argument(
|
|
318
|
+
"source",
|
|
319
|
+
help="a directory of .json, .ndjson or .jsonl events, "
|
|
320
|
+
"or the base URL of a Marquez-compatible API",
|
|
321
|
+
)
|
|
322
|
+
events.add_argument("--since", type=_instant, help="ignore events before this date")
|
|
323
|
+
events.add_argument("--until", type=_instant, help="ignore events after this date")
|
|
324
|
+
|
|
325
|
+
ropa_cmd = sub.add_parser(
|
|
326
|
+
"ropa",
|
|
327
|
+
parents=[common],
|
|
328
|
+
help="produce a GDPR Art. 30 record of processing activities",
|
|
329
|
+
)
|
|
330
|
+
ropa_cmd.add_argument(
|
|
331
|
+
"source",
|
|
332
|
+
help="a directory of .json, .ndjson or .jsonl events, "
|
|
333
|
+
"or the base URL of a Marquez-compatible API",
|
|
334
|
+
)
|
|
335
|
+
ropa_cmd.add_argument("--since", type=_instant, help="ignore events before this date")
|
|
336
|
+
ropa_cmd.add_argument("--until", type=_instant, help="ignore events after this date")
|
|
337
|
+
ropa_cmd.add_argument(
|
|
338
|
+
"--config",
|
|
339
|
+
help="qedro.yaml (or .json/.toml). Defaults to one beside the working directory",
|
|
340
|
+
)
|
|
341
|
+
ropa_cmd.add_argument(
|
|
342
|
+
"--vocabulary",
|
|
343
|
+
help="a vocabulary document to read terms from, instead of the shipped GDPR baseline",
|
|
344
|
+
)
|
|
345
|
+
ropa_cmd.add_argument(
|
|
346
|
+
"--format",
|
|
347
|
+
dest="fmt",
|
|
348
|
+
choices=sorted(render.names()),
|
|
349
|
+
default=None,
|
|
350
|
+
help="output format. Defaults to the one implied by --out, else text",
|
|
351
|
+
)
|
|
352
|
+
ropa_cmd.add_argument("--out", help="write to this file instead of standard output")
|
|
353
|
+
ropa_cmd.add_argument(
|
|
354
|
+
"--view",
|
|
355
|
+
choices=("art30", "deployer"),
|
|
356
|
+
default="art30",
|
|
357
|
+
help="art30: the GDPR Art. 30 record (default). deployer: the same record, "
|
|
358
|
+
"per AI use case, with the model version, inputs and run records a deployer "
|
|
359
|
+
"under the AI Act is asked about",
|
|
360
|
+
)
|
|
361
|
+
ropa_cmd.add_argument(
|
|
362
|
+
"--activities",
|
|
363
|
+
metavar="PATH",
|
|
364
|
+
help="a document of declared activities — processing that emits no lineage. "
|
|
365
|
+
"Marked as declared in the output, and never counted as evidence",
|
|
366
|
+
)
|
|
367
|
+
|
|
368
|
+
quality_cmd = sub.add_parser(
|
|
369
|
+
"quality",
|
|
370
|
+
parents=[common],
|
|
371
|
+
help="what was asserted about each dataset, and what was never checked",
|
|
372
|
+
)
|
|
373
|
+
quality_cmd.add_argument(
|
|
374
|
+
"source",
|
|
375
|
+
help="a directory of .json, .ndjson or .jsonl events, "
|
|
376
|
+
"or the base URL of a Marquez-compatible API",
|
|
377
|
+
)
|
|
378
|
+
quality_cmd.add_argument("--since", type=_instant, help="ignore events before this date")
|
|
379
|
+
quality_cmd.add_argument("--until", type=_instant, help="ignore events after this date")
|
|
380
|
+
quality_cmd.add_argument(
|
|
381
|
+
"--days",
|
|
382
|
+
type=int,
|
|
383
|
+
help="shorthand for --since N days before --until (or before now)",
|
|
384
|
+
)
|
|
385
|
+
quality_cmd.add_argument(
|
|
386
|
+
"--domain",
|
|
387
|
+
action="append",
|
|
388
|
+
default=[],
|
|
389
|
+
dest="domains",
|
|
390
|
+
metavar="NAME",
|
|
391
|
+
help="only datasets in this domain. Repeatable",
|
|
392
|
+
)
|
|
393
|
+
quality_cmd.add_argument(
|
|
394
|
+
"--config",
|
|
395
|
+
help="qedro.yaml (or .json/.toml). Defaults to one beside the working directory",
|
|
396
|
+
)
|
|
397
|
+
quality_cmd.add_argument(
|
|
398
|
+
"--format",
|
|
399
|
+
dest="fmt",
|
|
400
|
+
choices=sorted(render.names()),
|
|
401
|
+
default=None,
|
|
402
|
+
help="output format. Defaults to the one implied by --out, else text",
|
|
403
|
+
)
|
|
404
|
+
quality_cmd.add_argument("--out", help="write to this file instead of standard output")
|
|
405
|
+
|
|
406
|
+
provenance_cmd = sub.add_parser(
|
|
407
|
+
"provenance",
|
|
408
|
+
parents=[common],
|
|
409
|
+
help="the chain from a dataset back to the commits that produced it",
|
|
410
|
+
)
|
|
411
|
+
provenance_cmd.add_argument(
|
|
412
|
+
"source",
|
|
413
|
+
help="a directory of .json, .ndjson or .jsonl events, "
|
|
414
|
+
"or the base URL of a Marquez-compatible API",
|
|
415
|
+
)
|
|
416
|
+
provenance_cmd.add_argument(
|
|
417
|
+
"--dataset",
|
|
418
|
+
required=True,
|
|
419
|
+
help="the dataset to trace. A bare name is enough unless it is ambiguous",
|
|
420
|
+
)
|
|
421
|
+
provenance_cmd.add_argument(
|
|
422
|
+
"--depth",
|
|
423
|
+
type=int,
|
|
424
|
+
default=provenance.DEPTH,
|
|
425
|
+
help=f"how many hops upstream to follow (default {provenance.DEPTH})",
|
|
426
|
+
)
|
|
427
|
+
provenance_cmd.add_argument("--since", type=_instant, help="ignore events before this date")
|
|
428
|
+
provenance_cmd.add_argument("--until", type=_instant, help="ignore events after this date")
|
|
429
|
+
provenance_cmd.add_argument(
|
|
430
|
+
"--days",
|
|
431
|
+
type=int,
|
|
432
|
+
help="shorthand for --since N days before --until (or before now)",
|
|
433
|
+
)
|
|
434
|
+
provenance_cmd.add_argument(
|
|
435
|
+
"--config",
|
|
436
|
+
help="qedro.yaml (or .json/.toml). Defaults to one beside the working directory",
|
|
437
|
+
)
|
|
438
|
+
provenance_cmd.add_argument(
|
|
439
|
+
"--format",
|
|
440
|
+
dest="fmt",
|
|
441
|
+
choices=sorted(render.names()),
|
|
442
|
+
default=None,
|
|
443
|
+
help="output format. Defaults to the one implied by --out, else text",
|
|
444
|
+
)
|
|
445
|
+
provenance_cmd.add_argument("--out", help="write to this file instead of standard output")
|
|
446
|
+
|
|
447
|
+
args = parser.parse_args(sys.argv[1:] if argv is None else argv)
|
|
448
|
+
|
|
449
|
+
# The env var is for CI logs and containers, where nobody is around to
|
|
450
|
+
# pass a flag and a tofu box reads as a bug in the tool.
|
|
451
|
+
no_symbol = getattr(args, "no_symbol", False) or os.environ.get("QEDRO_NO_SYMBOL") == "1"
|
|
452
|
+
|
|
453
|
+
try:
|
|
454
|
+
if args.command == "events":
|
|
455
|
+
return _events(args.source, symbol=not no_symbol, since=args.since, until=args.until)
|
|
456
|
+
if args.command == "quality":
|
|
457
|
+
since, until = _window(args)
|
|
458
|
+
return _quality(
|
|
459
|
+
args.source,
|
|
460
|
+
symbol=not no_symbol,
|
|
461
|
+
since=since,
|
|
462
|
+
until=until,
|
|
463
|
+
domains=args.domains,
|
|
464
|
+
config_path=args.config,
|
|
465
|
+
fmt=args.fmt,
|
|
466
|
+
out=args.out,
|
|
467
|
+
)
|
|
468
|
+
if args.command == "provenance":
|
|
469
|
+
since, until = _window(args)
|
|
470
|
+
return _provenance(
|
|
471
|
+
args.source,
|
|
472
|
+
symbol=not no_symbol,
|
|
473
|
+
since=since,
|
|
474
|
+
until=until,
|
|
475
|
+
dataset=args.dataset,
|
|
476
|
+
depth=args.depth,
|
|
477
|
+
config_path=args.config,
|
|
478
|
+
fmt=args.fmt,
|
|
479
|
+
out=args.out,
|
|
480
|
+
)
|
|
481
|
+
if args.command == "ropa":
|
|
482
|
+
return _ropa(
|
|
483
|
+
args.source,
|
|
484
|
+
symbol=not no_symbol,
|
|
485
|
+
since=args.since,
|
|
486
|
+
until=args.until,
|
|
487
|
+
config_path=args.config,
|
|
488
|
+
vocabulary_path=args.vocabulary,
|
|
489
|
+
fmt=args.fmt,
|
|
490
|
+
out=args.out,
|
|
491
|
+
view=args.view,
|
|
492
|
+
activities_path=args.activities,
|
|
493
|
+
)
|
|
494
|
+
except QedroError as exc:
|
|
495
|
+
# What the user wrote, handed back as a sentence rather than a
|
|
496
|
+
# traceback. Evidence never gets here — it is counted, not raised.
|
|
497
|
+
print(f" {exc}", file=sys.stderr)
|
|
498
|
+
return 2
|
|
499
|
+
|
|
500
|
+
parser.print_help(sys.stderr)
|
|
501
|
+
return 1
|
|
502
|
+
|
|
503
|
+
|
|
504
|
+
if __name__ == "__main__":
|
|
505
|
+
raise SystemExit(main())
|