onebudgetspec-sdk 0.1.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.
- onebudgetspec_sdk/__init__.py +45 -0
- onebudgetspec_sdk/_client.py +394 -0
- onebudgetspec_sdk/_generated/__init__.py +7 -0
- onebudgetspec_sdk/_generated/check_report.py +137 -0
- onebudgetspec_sdk/_generated/list_report.py +89 -0
- onebudgetspec_sdk/_generated/schemas.py +283 -0
- onebudgetspec_sdk/py.typed +0 -0
- onebudgetspec_sdk-0.1.0.dist-info/METADATA +59 -0
- onebudgetspec_sdk-0.1.0.dist-info/RECORD +10 -0
- onebudgetspec_sdk-0.1.0.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""The onebudgetspec Python SDK: check, validate and list budgets through the binary.
|
|
2
|
+
|
|
3
|
+
Each call runs the ``onebudgetspec`` binary once and returns its JSON report as a model
|
|
4
|
+
generated from ``onebudgetspec schema``.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from ._client import (
|
|
8
|
+
BINARY_ENV,
|
|
9
|
+
CLI_DISTRIBUTION,
|
|
10
|
+
OnebudgetspecError,
|
|
11
|
+
StrPath,
|
|
12
|
+
check,
|
|
13
|
+
list_budgets,
|
|
14
|
+
resolve_binary,
|
|
15
|
+
schema,
|
|
16
|
+
validate,
|
|
17
|
+
)
|
|
18
|
+
from ._generated import SCHEMA_BUNDLE_VERSION
|
|
19
|
+
from ._generated.check_report import CheckReport, CheckResult, Direction, Host, Verdict
|
|
20
|
+
from ._generated.list_report import ListedBudget, ListReport, Measure
|
|
21
|
+
|
|
22
|
+
__all__ = [
|
|
23
|
+
"BINARY_ENV",
|
|
24
|
+
"CLI_DISTRIBUTION",
|
|
25
|
+
"SCHEMA_BUNDLE_VERSION",
|
|
26
|
+
"CheckReport",
|
|
27
|
+
"CheckResult",
|
|
28
|
+
"Direction",
|
|
29
|
+
"Host",
|
|
30
|
+
"ListReport",
|
|
31
|
+
"ListedBudget",
|
|
32
|
+
"Measure",
|
|
33
|
+
"OnebudgetspecError",
|
|
34
|
+
"StrPath",
|
|
35
|
+
"Verdict",
|
|
36
|
+
"__version__",
|
|
37
|
+
"check",
|
|
38
|
+
"list_budgets",
|
|
39
|
+
"resolve_binary",
|
|
40
|
+
"schema",
|
|
41
|
+
"validate",
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
#: The version of this package, which releases in lock step with the ``onebudgetspec`` binary.
|
|
45
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
"""Run the ``onebudgetspec`` binary once per call and read the JSON report it prints.
|
|
2
|
+
|
|
3
|
+
Nothing here measures, loads or selects a budget: the binary does all of it, and this
|
|
4
|
+
module turns a call into its argv and its stdout into the generated report type, after
|
|
5
|
+
validating it against the schema that type was generated from.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import importlib.metadata
|
|
9
|
+
import json
|
|
10
|
+
import math
|
|
11
|
+
import os
|
|
12
|
+
import re
|
|
13
|
+
import shutil
|
|
14
|
+
import subprocess
|
|
15
|
+
from collections.abc import Callable, Sequence
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from typing import TypeVar
|
|
18
|
+
|
|
19
|
+
from jsonschema import Draft202012Validator, FormatChecker
|
|
20
|
+
from pydantic import BaseModel, JsonValue, ValidationError
|
|
21
|
+
|
|
22
|
+
from ._generated import BUNDLE_ROOTS
|
|
23
|
+
from ._generated.check_report import CheckReport
|
|
24
|
+
from ._generated.list_report import ListReport
|
|
25
|
+
from ._generated.schemas import REPORT_SCHEMAS
|
|
26
|
+
|
|
27
|
+
#: The environment variable naming the binary when no explicit one is passed.
|
|
28
|
+
BINARY_ENV = "ONEBUDGETSPEC_BIN"
|
|
29
|
+
#: The distribution whose wheel carries the binary.
|
|
30
|
+
CLI_DISTRIBUTION = "onebudgetspec-cli"
|
|
31
|
+
_EXECUTABLE = "onebudgetspec"
|
|
32
|
+
#: The statuses whose stdout is a report: within, over and error. A verdict is in the
|
|
33
|
+
#: report, so none of them is an exception.
|
|
34
|
+
_REPORTED = frozenset({0, 1, 3})
|
|
35
|
+
|
|
36
|
+
#: A filesystem path, as a string or a path object.
|
|
37
|
+
StrPath = str | os.PathLike[str]
|
|
38
|
+
_Report = TypeVar("_Report", bound=BaseModel)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class OnebudgetspecError(RuntimeError):
|
|
42
|
+
"""The binary refused the call, could not run, or printed no report.
|
|
43
|
+
|
|
44
|
+
``str(error)`` is the binary's own message when it gave one. ``exit_code`` is its exit
|
|
45
|
+
status, ``2`` for an invalid invocation or budgets file, and ``None`` when no binary ran.
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
def __init__(self, message: str, *, exit_code: int | None) -> None:
|
|
49
|
+
"""Keep the message and the exit status, ``None`` when nothing ran."""
|
|
50
|
+
super().__init__(message)
|
|
51
|
+
self.exit_code = exit_code
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _bundled() -> Path | None:
|
|
55
|
+
"""The binary the ``onebudgetspec-cli`` wheel installed, when it is installed."""
|
|
56
|
+
try:
|
|
57
|
+
distribution = importlib.metadata.distribution(CLI_DISTRIBUTION)
|
|
58
|
+
except importlib.metadata.PackageNotFoundError:
|
|
59
|
+
return None
|
|
60
|
+
for file in distribution.files or ():
|
|
61
|
+
if file.name == _EXECUTABLE and file.parent.name == "bin":
|
|
62
|
+
path = Path(os.path.normpath(Path(str(distribution.locate_file(file)))))
|
|
63
|
+
if path.is_file():
|
|
64
|
+
return path
|
|
65
|
+
return None
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def resolve_binary(binary: StrPath | None = None) -> Path:
|
|
69
|
+
"""The binary a call runs.
|
|
70
|
+
|
|
71
|
+
In order: ``binary`` when given, then ``ONEBUDGETSPEC_BIN`` when set and non-empty, then
|
|
72
|
+
the binary the ``onebudgetspec-cli`` wheel installed, then ``onebudgetspec`` on ``PATH``.
|
|
73
|
+
|
|
74
|
+
Raises:
|
|
75
|
+
OnebudgetspecError: none of them names a binary.
|
|
76
|
+
"""
|
|
77
|
+
if binary is not None:
|
|
78
|
+
return Path(binary)
|
|
79
|
+
from_environment = os.environ.get(BINARY_ENV)
|
|
80
|
+
if from_environment:
|
|
81
|
+
return Path(from_environment)
|
|
82
|
+
bundled = _bundled()
|
|
83
|
+
if bundled is not None:
|
|
84
|
+
return bundled
|
|
85
|
+
on_path = shutil.which(_EXECUTABLE)
|
|
86
|
+
if on_path is not None:
|
|
87
|
+
return Path(on_path)
|
|
88
|
+
raise OnebudgetspecError(
|
|
89
|
+
f"onebudgetspec: no binary found; install {CLI_DISTRIBUTION}, put {_EXECUTABLE} "
|
|
90
|
+
f"on PATH, set {BINARY_ENV}, or pass binary=",
|
|
91
|
+
exit_code=None,
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _values(name: str, values: Sequence[str] | None) -> list[str]:
|
|
96
|
+
"""``values`` as a list, refusing a lone string, which would split into characters."""
|
|
97
|
+
if values is None:
|
|
98
|
+
return []
|
|
99
|
+
if isinstance(values, str) or not all(isinstance(value, str) for value in values):
|
|
100
|
+
raise TypeError(f"{name} must be a sequence of strings, not {values!r}")
|
|
101
|
+
return list(values)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _paths(paths: Sequence[StrPath] | None) -> list[str]:
|
|
105
|
+
if paths is None:
|
|
106
|
+
return []
|
|
107
|
+
if isinstance(paths, (str, os.PathLike)):
|
|
108
|
+
raise TypeError(f"paths must be a sequence of paths, not {paths!r}")
|
|
109
|
+
return [os.fspath(path) for path in paths]
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _files(paths: Sequence[StrPath] | None, recursive: bool) -> list[str]:
|
|
113
|
+
"""The flags and operands naming the budgets files, after ``--`` so none reads as a flag."""
|
|
114
|
+
if not isinstance(recursive, bool):
|
|
115
|
+
raise TypeError(f"recursive must be True or False, not {recursive!r}")
|
|
116
|
+
return [*(["--recursive"] if recursive else []), "--", *_paths(paths)]
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _selection(
|
|
120
|
+
ids: Sequence[str] | None,
|
|
121
|
+
labels: Sequence[str] | None,
|
|
122
|
+
exclude_labels: Sequence[str] | None,
|
|
123
|
+
) -> list[str]:
|
|
124
|
+
"""The selection flags, each value joined to its flag so none can read as a flag."""
|
|
125
|
+
return [
|
|
126
|
+
f"{flag}={value}"
|
|
127
|
+
for flag, name, values in (
|
|
128
|
+
("--id", "ids", ids),
|
|
129
|
+
("--label", "labels", labels),
|
|
130
|
+
("--exclude-label", "exclude_labels", exclude_labels),
|
|
131
|
+
)
|
|
132
|
+
for value in _values(name, values)
|
|
133
|
+
]
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _run(binary: StrPath | None, args: list[str], cwd: StrPath | None) -> bytes:
|
|
137
|
+
"""Run the binary with ``args`` and return its stdout, or raise with its message."""
|
|
138
|
+
program = resolve_binary(binary)
|
|
139
|
+
try:
|
|
140
|
+
completed = subprocess.run(
|
|
141
|
+
[os.fspath(program), *args],
|
|
142
|
+
cwd=cwd,
|
|
143
|
+
stdin=subprocess.DEVNULL,
|
|
144
|
+
capture_output=True,
|
|
145
|
+
check=False,
|
|
146
|
+
)
|
|
147
|
+
except OSError as error:
|
|
148
|
+
raise OnebudgetspecError(
|
|
149
|
+
f"onebudgetspec: cannot run {program}: {error}", exit_code=None
|
|
150
|
+
) from error
|
|
151
|
+
if completed.returncode in _REPORTED:
|
|
152
|
+
return completed.stdout
|
|
153
|
+
status = completed.returncode
|
|
154
|
+
message = completed.stderr.decode(errors="replace").strip()
|
|
155
|
+
if not message:
|
|
156
|
+
ended = f"was terminated by signal {-status}" if status < 0 else f"exited {status}"
|
|
157
|
+
message = f"onebudgetspec: {program} {ended} with no message"
|
|
158
|
+
raise OnebudgetspecError(message, exit_code=status)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
#: An RFC 3339 timestamp, as the reports' ``date-time`` fields carry.
|
|
162
|
+
_RFC3339 = re.compile(
|
|
163
|
+
r"(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.\d+)?(?:Z|[+-](\d{2}):(\d{2}))",
|
|
164
|
+
re.IGNORECASE,
|
|
165
|
+
)
|
|
166
|
+
_DAYS_IN_MONTH = (31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31)
|
|
167
|
+
#: Every format the report schemas name, checked: the timestamps, and the numeric widths
|
|
168
|
+
#: the binary's Rust types carry. A format applies only to its own JSON type; the schema's
|
|
169
|
+
#: ``type`` refuses any other.
|
|
170
|
+
_FORMATS = FormatChecker(formats=())
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
@_FORMATS.checks("date-time")
|
|
174
|
+
def _is_date_time(value: object) -> bool:
|
|
175
|
+
"""An RFC 3339 instant: a day its month has, and every field in range.
|
|
176
|
+
|
|
177
|
+
A second of 60 is refused: a leap second cannot be told from a nonexistent one here.
|
|
178
|
+
"""
|
|
179
|
+
if not isinstance(value, str):
|
|
180
|
+
return True
|
|
181
|
+
match = _RFC3339.fullmatch(value)
|
|
182
|
+
if match is None:
|
|
183
|
+
return False
|
|
184
|
+
year, month, day, hour, minute, second = (int(part) for part in match.groups()[:6])
|
|
185
|
+
offset_hours, offset_minutes = (int(part or 0) for part in match.groups()[6:])
|
|
186
|
+
if not 1 <= month <= 12:
|
|
187
|
+
return False
|
|
188
|
+
leap = year % 4 == 0 and (year % 100 != 0 or year % 400 == 0)
|
|
189
|
+
days = _DAYS_IN_MONTH[month - 1] + (1 if month == 2 and leap else 0)
|
|
190
|
+
return (
|
|
191
|
+
1 <= day <= days
|
|
192
|
+
and hour <= 23
|
|
193
|
+
and minute <= 59
|
|
194
|
+
and second <= 59
|
|
195
|
+
and offset_hours <= 23
|
|
196
|
+
and offset_minutes <= 59
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def _is_unsigned(bits: int) -> Callable[[object], bool]:
|
|
201
|
+
"""A check that an integer fits in ``bits`` unsigned bits."""
|
|
202
|
+
|
|
203
|
+
def check(value: object) -> bool:
|
|
204
|
+
if isinstance(value, bool) or not isinstance(value, int):
|
|
205
|
+
return True
|
|
206
|
+
return 0 <= value < 2**bits
|
|
207
|
+
|
|
208
|
+
return check
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
_FORMATS.checks("uint32")(_is_unsigned(32))
|
|
212
|
+
_FORMATS.checks("uint64")(_is_unsigned(64))
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
@_FORMATS.checks("double")
|
|
216
|
+
def _is_double(value: object) -> bool:
|
|
217
|
+
"""A finite number; JSON holds no infinity, but a literal too large reads as one."""
|
|
218
|
+
return not isinstance(value, float) or math.isfinite(value)
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def _formats_named(schema: object) -> set[str]:
|
|
222
|
+
"""Every ``format`` a JSON Schema names, at any depth."""
|
|
223
|
+
match schema:
|
|
224
|
+
case {"format": str(name), **rest}:
|
|
225
|
+
return {name}.union(*(_formats_named(value) for value in rest.values()))
|
|
226
|
+
case dict():
|
|
227
|
+
return set().union(*(_formats_named(value) for value in schema.values()))
|
|
228
|
+
case list():
|
|
229
|
+
return set().union(*(_formats_named(item) for item in schema))
|
|
230
|
+
case _:
|
|
231
|
+
return set()
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def _require_validators(schemas: object) -> None:
|
|
235
|
+
"""Refuse report schemas naming a format no validator here checks.
|
|
236
|
+
|
|
237
|
+
``FormatChecker`` passes a format it has no check for, so a report schema naming one would
|
|
238
|
+
let its values through unchecked; this makes that a failure at import instead.
|
|
239
|
+
|
|
240
|
+
Raises:
|
|
241
|
+
OnebudgetspecError: a format the schemas name has no validator in this module.
|
|
242
|
+
"""
|
|
243
|
+
unchecked = sorted(_formats_named(schemas) - set(_FORMATS.checkers))
|
|
244
|
+
if unchecked:
|
|
245
|
+
raise OnebudgetspecError(
|
|
246
|
+
f"onebudgetspec: the report schemas name the format(s) {unchecked}, which this SDK "
|
|
247
|
+
"has no validator for, so it cannot check the binary's reports; install the "
|
|
248
|
+
"onebudgetspec-sdk release that matches the binary, or, developing this SDK, add a "
|
|
249
|
+
"validator for each to onebudgetspec_sdk/_client.py",
|
|
250
|
+
exit_code=None,
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
_require_validators(REPORT_SCHEMAS)
|
|
255
|
+
_VALIDATORS = {
|
|
256
|
+
root: Draft202012Validator(schema, format_checker=_FORMATS)
|
|
257
|
+
for root, schema in REPORT_SCHEMAS.items()
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def _report(model: type[_Report], root: str, stdout: bytes) -> _Report:
|
|
262
|
+
"""``stdout`` as ``model``, once it is valid against the schema root ``root``."""
|
|
263
|
+
try:
|
|
264
|
+
document = json.loads(stdout)
|
|
265
|
+
except ValueError as error:
|
|
266
|
+
raise OnebudgetspecError(
|
|
267
|
+
f"onebudgetspec: the binary printed no JSON: {error}", exit_code=None
|
|
268
|
+
) from error
|
|
269
|
+
problems = [
|
|
270
|
+
f"{'/'.join(str(part) for part in error.absolute_path) or '(report)'}: {error.message}"
|
|
271
|
+
for error in _VALIDATORS[root].iter_errors(document)
|
|
272
|
+
]
|
|
273
|
+
if problems:
|
|
274
|
+
raise OnebudgetspecError(
|
|
275
|
+
f"onebudgetspec: the binary printed no valid {root}: {'; '.join(sorted(problems))}",
|
|
276
|
+
exit_code=None,
|
|
277
|
+
)
|
|
278
|
+
try:
|
|
279
|
+
# Strict: a report field of the wrong JSON type is refused, never coerced.
|
|
280
|
+
return model.model_validate_json(stdout, strict=True)
|
|
281
|
+
except ValidationError as error:
|
|
282
|
+
raise OnebudgetspecError(
|
|
283
|
+
f"onebudgetspec: the binary printed no valid {root}: {error}", exit_code=None
|
|
284
|
+
) from error
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def check(
|
|
288
|
+
paths: Sequence[StrPath] | None = None,
|
|
289
|
+
ids: Sequence[str] | None = None,
|
|
290
|
+
labels: Sequence[str] | None = None,
|
|
291
|
+
exclude_labels: Sequence[str] | None = None,
|
|
292
|
+
recursive: bool = False,
|
|
293
|
+
cwd: StrPath | None = None,
|
|
294
|
+
*,
|
|
295
|
+
binary: StrPath | None = None,
|
|
296
|
+
) -> CheckReport:
|
|
297
|
+
"""Measure every selected budget once: ``onebudgetspec check``.
|
|
298
|
+
|
|
299
|
+
Args:
|
|
300
|
+
paths: budgets files, or directories with ``recursive``; ``./budgets.yaml`` when
|
|
301
|
+
omitted. Relative paths are read from ``cwd``.
|
|
302
|
+
ids: keep only these budgets; an id no file registers is refused.
|
|
303
|
+
labels: keep budgets carrying at least one of these labels.
|
|
304
|
+
exclude_labels: drop budgets carrying any of these labels.
|
|
305
|
+
recursive: search directories for files named ``budgets.yaml``.
|
|
306
|
+
cwd: the directory to run from; the current one when omitted.
|
|
307
|
+
binary: the binary to run; see :func:`resolve_binary`.
|
|
308
|
+
|
|
309
|
+
Returns:
|
|
310
|
+
The report, whatever its verdicts: within, over and error are all in it.
|
|
311
|
+
|
|
312
|
+
Raises:
|
|
313
|
+
OnebudgetspecError: the invocation or a budgets file is invalid (exit status 2),
|
|
314
|
+
with the binary's message, or the binary could not run.
|
|
315
|
+
"""
|
|
316
|
+
args = ["check", "--json", *_selection(ids, labels, exclude_labels), *_files(paths, recursive)]
|
|
317
|
+
return _report(CheckReport, "check-report", _run(binary, args, cwd))
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def validate(
|
|
321
|
+
paths: Sequence[StrPath] | None = None,
|
|
322
|
+
recursive: bool = False,
|
|
323
|
+
cwd: StrPath | None = None,
|
|
324
|
+
*,
|
|
325
|
+
binary: StrPath | None = None,
|
|
326
|
+
) -> ListReport:
|
|
327
|
+
"""Check the files' shape and id uniqueness, running no command: ``onebudgetspec validate``.
|
|
328
|
+
|
|
329
|
+
Args:
|
|
330
|
+
paths: as for :func:`check`.
|
|
331
|
+
recursive: as for :func:`check`.
|
|
332
|
+
cwd: as for :func:`check`.
|
|
333
|
+
binary: as for :func:`check`.
|
|
334
|
+
|
|
335
|
+
Returns:
|
|
336
|
+
Every budget the valid files register, as ``list`` reports them.
|
|
337
|
+
|
|
338
|
+
Raises:
|
|
339
|
+
OnebudgetspecError: a file is invalid (exit status 2), with the binary's message.
|
|
340
|
+
"""
|
|
341
|
+
args = ["validate", "--json", *_files(paths, recursive)]
|
|
342
|
+
return _report(ListReport, "list-report", _run(binary, args, cwd))
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def list_budgets(
|
|
346
|
+
paths: Sequence[StrPath] | None = None,
|
|
347
|
+
ids: Sequence[str] | None = None,
|
|
348
|
+
labels: Sequence[str] | None = None,
|
|
349
|
+
exclude_labels: Sequence[str] | None = None,
|
|
350
|
+
recursive: bool = False,
|
|
351
|
+
cwd: StrPath | None = None,
|
|
352
|
+
*,
|
|
353
|
+
binary: StrPath | None = None,
|
|
354
|
+
) -> ListReport:
|
|
355
|
+
"""Report the selected budgets, running no command: ``onebudgetspec list``.
|
|
356
|
+
|
|
357
|
+
Takes the arguments of :func:`check`.
|
|
358
|
+
|
|
359
|
+
Raises:
|
|
360
|
+
OnebudgetspecError: the invocation or a budgets file is invalid (exit status 2).
|
|
361
|
+
"""
|
|
362
|
+
args = ["list", "--json", *_selection(ids, labels, exclude_labels), *_files(paths, recursive)]
|
|
363
|
+
return _report(ListReport, "list-report", _run(binary, args, cwd))
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
def schema(*, binary: StrPath | None = None) -> dict[str, JsonValue]:
|
|
367
|
+
"""The JSON Schema bundle the binary prints: ``onebudgetspec schema``.
|
|
368
|
+
|
|
369
|
+
Raises:
|
|
370
|
+
OnebudgetspecError: the binary could not run, or printed no schema bundle.
|
|
371
|
+
"""
|
|
372
|
+
stdout = _run(binary, ["schema"], None)
|
|
373
|
+
try:
|
|
374
|
+
bundle = json.loads(stdout)
|
|
375
|
+
except ValueError as error:
|
|
376
|
+
raise OnebudgetspecError(
|
|
377
|
+
f"onebudgetspec: the schema is not JSON: {error}", exit_code=None
|
|
378
|
+
) from error
|
|
379
|
+
if not isinstance(bundle, dict):
|
|
380
|
+
raise OnebudgetspecError("onebudgetspec: the schema is not a JSON object", exit_code=None)
|
|
381
|
+
version, roots = bundle.get("version"), bundle.get("roots")
|
|
382
|
+
if (
|
|
383
|
+
isinstance(version, bool)
|
|
384
|
+
or not isinstance(version, int)
|
|
385
|
+
or not isinstance(roots, dict)
|
|
386
|
+
or not all(isinstance(roots.get(root), dict) for root in BUNDLE_ROOTS)
|
|
387
|
+
):
|
|
388
|
+
raise OnebudgetspecError(
|
|
389
|
+
"onebudgetspec: the binary's output is not a schema bundle: it needs an integer "
|
|
390
|
+
f"`version` and the `roots` {list(BUNDLE_ROOTS)}, each an object; point "
|
|
391
|
+
f"{BINARY_ENV} at an onebudgetspec binary, or reinstall {CLI_DISTRIBUTION}",
|
|
392
|
+
exit_code=None,
|
|
393
|
+
)
|
|
394
|
+
return bundle
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
"""Models generated from `onebudgetspec schema`; see sdks/python/generate.py."""
|
|
2
|
+
|
|
3
|
+
#: The version of the schema bundle these models were generated from.
|
|
4
|
+
SCHEMA_BUNDLE_VERSION = 1
|
|
5
|
+
|
|
6
|
+
#: Every root the schema bundle carries, which the client requires of one.
|
|
7
|
+
BUNDLE_ROOTS = ("budgets-file", "check-report", "list-report")
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# Generated by sdks/python/generate.py from the 'check-report' root of `onebudgetspec schema`.
|
|
2
|
+
# Do not edit: re-run the generator. Each docstring is the schema's description verbatim.
|
|
3
|
+
# ruff: noqa: D200, D205, D212, D400, D415, E501
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from typing import Literal
|
|
8
|
+
|
|
9
|
+
from pydantic import AwareDatetime, BaseModel, ConfigDict, Field
|
|
10
|
+
from typing_extensions import TypeAliasType
|
|
11
|
+
|
|
12
|
+
Direction = TypeAliasType("Direction", Literal["max", "min"])
|
|
13
|
+
"""
|
|
14
|
+
Which side of the threshold is within.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class Host(BaseModel):
|
|
19
|
+
"""
|
|
20
|
+
The host conditions recorded beside a result.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
model_config = ConfigDict(
|
|
24
|
+
extra="forbid",
|
|
25
|
+
)
|
|
26
|
+
conditions: dict[str, str]
|
|
27
|
+
"""
|
|
28
|
+
Each condition the file declares, and for a `reported` result each condition its
|
|
29
|
+
command returned, by name.
|
|
30
|
+
"""
|
|
31
|
+
cpus: int = Field(..., ge=0)
|
|
32
|
+
"""
|
|
33
|
+
The CPUs available to this process.
|
|
34
|
+
"""
|
|
35
|
+
load1: float | None
|
|
36
|
+
"""
|
|
37
|
+
The 1-minute load average; null where unreadable.
|
|
38
|
+
"""
|
|
39
|
+
mem_available_mib: int | None = Field(..., ge=0)
|
|
40
|
+
"""
|
|
41
|
+
Available memory in MiB; null where unreadable.
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
Verdict = TypeAliasType("Verdict", Literal["within", "over", "error"])
|
|
46
|
+
"""
|
|
47
|
+
A result's verdict.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class CheckResult(BaseModel):
|
|
52
|
+
"""
|
|
53
|
+
One budget's measurement.
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
model_config = ConfigDict(
|
|
57
|
+
extra="forbid",
|
|
58
|
+
)
|
|
59
|
+
actual: float | None
|
|
60
|
+
"""
|
|
61
|
+
The measured value; null on an error.
|
|
62
|
+
"""
|
|
63
|
+
detail: str | None
|
|
64
|
+
"""
|
|
65
|
+
The `detail` a `reported` command wrote; null when it wrote none.
|
|
66
|
+
"""
|
|
67
|
+
direction: Direction
|
|
68
|
+
"""
|
|
69
|
+
The budget's direction.
|
|
70
|
+
"""
|
|
71
|
+
ended_at: AwareDatetime
|
|
72
|
+
"""
|
|
73
|
+
When the measurement ended (RFC 3339), never before `started_at`.
|
|
74
|
+
"""
|
|
75
|
+
error: str | None
|
|
76
|
+
"""
|
|
77
|
+
Why the measurement failed; null unless `verdict` is `error`.
|
|
78
|
+
"""
|
|
79
|
+
file: str
|
|
80
|
+
"""
|
|
81
|
+
The budgets file's path, as discovered or given.
|
|
82
|
+
"""
|
|
83
|
+
headroom: float | None
|
|
84
|
+
"""
|
|
85
|
+
`threshold - actual` under `max`, `actual - threshold` under `min`; negative when
|
|
86
|
+
over, null on an error.
|
|
87
|
+
"""
|
|
88
|
+
headroom_percent: float | None
|
|
89
|
+
"""
|
|
90
|
+
`headroom / threshold * 100`; null on an error or when the threshold is 0.
|
|
91
|
+
"""
|
|
92
|
+
host: Host
|
|
93
|
+
"""
|
|
94
|
+
The host conditions recorded beside this result.
|
|
95
|
+
"""
|
|
96
|
+
id: str
|
|
97
|
+
"""
|
|
98
|
+
The budget's id.
|
|
99
|
+
"""
|
|
100
|
+
labels: list[str]
|
|
101
|
+
"""
|
|
102
|
+
The budget's labels, empty when it has none.
|
|
103
|
+
"""
|
|
104
|
+
started_at: AwareDatetime
|
|
105
|
+
"""
|
|
106
|
+
When the measurement started (RFC 3339).
|
|
107
|
+
"""
|
|
108
|
+
threshold: float
|
|
109
|
+
"""
|
|
110
|
+
The budget's threshold.
|
|
111
|
+
"""
|
|
112
|
+
unit: str
|
|
113
|
+
"""
|
|
114
|
+
The budget's unit.
|
|
115
|
+
"""
|
|
116
|
+
verdict: Verdict
|
|
117
|
+
"""
|
|
118
|
+
`within`, `over` or `error`.
|
|
119
|
+
"""
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
class CheckReport(BaseModel):
|
|
123
|
+
"""
|
|
124
|
+
What `check` answers with: one result per selected budget, in measurement order.
|
|
125
|
+
"""
|
|
126
|
+
|
|
127
|
+
model_config = ConfigDict(
|
|
128
|
+
extra="forbid",
|
|
129
|
+
)
|
|
130
|
+
results: list[CheckResult]
|
|
131
|
+
"""
|
|
132
|
+
One result per selected budget.
|
|
133
|
+
"""
|
|
134
|
+
schema_version: Literal[1]
|
|
135
|
+
"""
|
|
136
|
+
Always `1`.
|
|
137
|
+
"""
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# Generated by sdks/python/generate.py from the 'list-report' root of `onebudgetspec schema`.
|
|
2
|
+
# Do not edit: re-run the generator. Each docstring is the schema's description verbatim.
|
|
3
|
+
# ruff: noqa: D200, D205, D212, D400, D415, E501
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from typing import Literal
|
|
8
|
+
|
|
9
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
10
|
+
from typing_extensions import TypeAliasType
|
|
11
|
+
|
|
12
|
+
Direction = TypeAliasType("Direction", Literal["max", "min"])
|
|
13
|
+
"""
|
|
14
|
+
Which side of the threshold is within.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
Measure = TypeAliasType("Measure", Literal["elapsed", "reported"])
|
|
19
|
+
"""
|
|
20
|
+
How a budget's value is measured.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class ListedBudget(BaseModel):
|
|
25
|
+
"""
|
|
26
|
+
One budget as `list` reports it: every field the file gives, plus the file.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
model_config = ConfigDict(
|
|
30
|
+
extra="forbid",
|
|
31
|
+
)
|
|
32
|
+
command: list[str]
|
|
33
|
+
"""
|
|
34
|
+
The argv that measures it.
|
|
35
|
+
"""
|
|
36
|
+
description: str | None
|
|
37
|
+
"""
|
|
38
|
+
The budget's description; null when it has none.
|
|
39
|
+
"""
|
|
40
|
+
direction: Direction
|
|
41
|
+
"""
|
|
42
|
+
The budget's direction.
|
|
43
|
+
"""
|
|
44
|
+
file: str
|
|
45
|
+
"""
|
|
46
|
+
The budgets file's path, as discovered or given.
|
|
47
|
+
"""
|
|
48
|
+
id: str
|
|
49
|
+
"""
|
|
50
|
+
The budget's id.
|
|
51
|
+
"""
|
|
52
|
+
labels: list[str]
|
|
53
|
+
"""
|
|
54
|
+
The budget's labels, empty when it has none.
|
|
55
|
+
"""
|
|
56
|
+
measure: Measure
|
|
57
|
+
"""
|
|
58
|
+
How the value is measured.
|
|
59
|
+
"""
|
|
60
|
+
threshold: float
|
|
61
|
+
"""
|
|
62
|
+
The budget's threshold.
|
|
63
|
+
"""
|
|
64
|
+
timeout_seconds: int | None = Field(..., ge=0)
|
|
65
|
+
"""
|
|
66
|
+
The budget's timeout; null when it has none.
|
|
67
|
+
"""
|
|
68
|
+
unit: str
|
|
69
|
+
"""
|
|
70
|
+
The budget's unit.
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class ListReport(BaseModel):
|
|
75
|
+
"""
|
|
76
|
+
What `list` answers with: the selected budgets, in file order.
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
model_config = ConfigDict(
|
|
80
|
+
extra="forbid",
|
|
81
|
+
)
|
|
82
|
+
budgets: list[ListedBudget]
|
|
83
|
+
"""
|
|
84
|
+
The selected budgets.
|
|
85
|
+
"""
|
|
86
|
+
schema_version: Literal[1]
|
|
87
|
+
"""
|
|
88
|
+
Always `1`.
|
|
89
|
+
"""
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
# ruff: noqa: E501 # The schema's descriptions are kept verbatim.
|
|
2
|
+
"""The report roots of `onebudgetspec schema`, generated by sdks/python/generate.py.
|
|
3
|
+
|
|
4
|
+
The client validates every report against them. Do not edit.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
#: Each report root, by name, as the binary emits it.
|
|
8
|
+
REPORT_SCHEMAS: dict[str, dict[str, object]] = {
|
|
9
|
+
"check-report": {
|
|
10
|
+
"$defs": {
|
|
11
|
+
"CheckResult": {
|
|
12
|
+
"additionalProperties": False,
|
|
13
|
+
"description": "One budget's measurement.",
|
|
14
|
+
"properties": {
|
|
15
|
+
"actual": {
|
|
16
|
+
"description": "The measured value; null on an error.",
|
|
17
|
+
"format": "double",
|
|
18
|
+
"type": ["number", "null"],
|
|
19
|
+
},
|
|
20
|
+
"detail": {
|
|
21
|
+
"description": "The `detail` a `reported` command wrote; null when it wrote none.",
|
|
22
|
+
"type": ["string", "null"],
|
|
23
|
+
},
|
|
24
|
+
"direction": {
|
|
25
|
+
"$ref": "#/$defs/Direction",
|
|
26
|
+
"description": "The budget's direction.",
|
|
27
|
+
},
|
|
28
|
+
"ended_at": {
|
|
29
|
+
"description": "When the measurement ended (RFC 3339), never before `started_at`.",
|
|
30
|
+
"format": "date-time",
|
|
31
|
+
"type": "string",
|
|
32
|
+
},
|
|
33
|
+
"error": {
|
|
34
|
+
"description": "Why the measurement failed; null unless `verdict` is `error`.",
|
|
35
|
+
"type": ["string", "null"],
|
|
36
|
+
},
|
|
37
|
+
"file": {
|
|
38
|
+
"description": "The budgets file's path, as discovered or given.",
|
|
39
|
+
"type": "string",
|
|
40
|
+
},
|
|
41
|
+
"headroom": {
|
|
42
|
+
"description": "`threshold - actual` under `max`, `actual - threshold` under `min`; negative when\nover, null on an error.",
|
|
43
|
+
"format": "double",
|
|
44
|
+
"type": ["number", "null"],
|
|
45
|
+
},
|
|
46
|
+
"headroom_percent": {
|
|
47
|
+
"description": "`headroom / threshold * 100`; null on an error or when the threshold is 0.",
|
|
48
|
+
"format": "double",
|
|
49
|
+
"type": ["number", "null"],
|
|
50
|
+
},
|
|
51
|
+
"host": {
|
|
52
|
+
"$ref": "#/$defs/Host",
|
|
53
|
+
"description": "The host conditions recorded beside this result.",
|
|
54
|
+
},
|
|
55
|
+
"id": {"description": "The budget's id.", "type": "string"},
|
|
56
|
+
"labels": {
|
|
57
|
+
"description": "The budget's labels, empty when it has none.",
|
|
58
|
+
"items": {"type": "string"},
|
|
59
|
+
"type": "array",
|
|
60
|
+
},
|
|
61
|
+
"started_at": {
|
|
62
|
+
"description": "When the measurement started (RFC 3339).",
|
|
63
|
+
"format": "date-time",
|
|
64
|
+
"type": "string",
|
|
65
|
+
},
|
|
66
|
+
"threshold": {
|
|
67
|
+
"description": "The budget's threshold.",
|
|
68
|
+
"format": "double",
|
|
69
|
+
"type": "number",
|
|
70
|
+
},
|
|
71
|
+
"unit": {"description": "The budget's unit.", "type": "string"},
|
|
72
|
+
"verdict": {
|
|
73
|
+
"$ref": "#/$defs/Verdict",
|
|
74
|
+
"description": "`within`, `over` or `error`.",
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
"required": [
|
|
78
|
+
"actual",
|
|
79
|
+
"detail",
|
|
80
|
+
"direction",
|
|
81
|
+
"ended_at",
|
|
82
|
+
"error",
|
|
83
|
+
"file",
|
|
84
|
+
"headroom",
|
|
85
|
+
"headroom_percent",
|
|
86
|
+
"host",
|
|
87
|
+
"id",
|
|
88
|
+
"labels",
|
|
89
|
+
"started_at",
|
|
90
|
+
"threshold",
|
|
91
|
+
"unit",
|
|
92
|
+
"verdict",
|
|
93
|
+
],
|
|
94
|
+
"type": "object",
|
|
95
|
+
},
|
|
96
|
+
"Direction": {
|
|
97
|
+
"description": "Which side of the threshold is within.",
|
|
98
|
+
"oneOf": [
|
|
99
|
+
{
|
|
100
|
+
"const": "max",
|
|
101
|
+
"description": "Within when `actual <= threshold`.",
|
|
102
|
+
"type": "string",
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
"const": "min",
|
|
106
|
+
"description": "Within when `actual >= threshold`.",
|
|
107
|
+
"type": "string",
|
|
108
|
+
},
|
|
109
|
+
],
|
|
110
|
+
},
|
|
111
|
+
"Host": {
|
|
112
|
+
"additionalProperties": False,
|
|
113
|
+
"description": "The host conditions recorded beside a result.",
|
|
114
|
+
"properties": {
|
|
115
|
+
"conditions": {
|
|
116
|
+
"additionalProperties": {"type": "string"},
|
|
117
|
+
"description": "Each condition the file declares, and for a `reported` result each condition its\ncommand returned, by name.",
|
|
118
|
+
"type": "object",
|
|
119
|
+
},
|
|
120
|
+
"cpus": {
|
|
121
|
+
"description": "The CPUs available to this process.",
|
|
122
|
+
"format": "uint32",
|
|
123
|
+
"minimum": 0,
|
|
124
|
+
"type": "integer",
|
|
125
|
+
},
|
|
126
|
+
"load1": {
|
|
127
|
+
"description": "The 1-minute load average; null where unreadable.",
|
|
128
|
+
"format": "double",
|
|
129
|
+
"type": ["number", "null"],
|
|
130
|
+
},
|
|
131
|
+
"mem_available_mib": {
|
|
132
|
+
"description": "Available memory in MiB; null where unreadable.",
|
|
133
|
+
"format": "uint64",
|
|
134
|
+
"minimum": 0,
|
|
135
|
+
"type": ["integer", "null"],
|
|
136
|
+
},
|
|
137
|
+
},
|
|
138
|
+
"required": ["conditions", "cpus", "load1", "mem_available_mib"],
|
|
139
|
+
"type": "object",
|
|
140
|
+
},
|
|
141
|
+
"Verdict": {
|
|
142
|
+
"description": "A result's verdict.",
|
|
143
|
+
"oneOf": [
|
|
144
|
+
{
|
|
145
|
+
"const": "within",
|
|
146
|
+
"description": "The actual value is on the budget's side of the threshold.",
|
|
147
|
+
"type": "string",
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
"const": "over",
|
|
151
|
+
"description": "The actual value is past the threshold.",
|
|
152
|
+
"type": "string",
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
"const": "error",
|
|
156
|
+
"description": "No value was measured; `error` says why.",
|
|
157
|
+
"type": "string",
|
|
158
|
+
},
|
|
159
|
+
],
|
|
160
|
+
},
|
|
161
|
+
},
|
|
162
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
163
|
+
"additionalProperties": False,
|
|
164
|
+
"description": "What `check` answers with: one result per selected budget, in measurement order.",
|
|
165
|
+
"properties": {
|
|
166
|
+
"results": {
|
|
167
|
+
"description": "One result per selected budget.",
|
|
168
|
+
"items": {"$ref": "#/$defs/CheckResult"},
|
|
169
|
+
"type": "array",
|
|
170
|
+
},
|
|
171
|
+
"schema_version": {"const": 1, "description": "Always `1`.", "type": "integer"},
|
|
172
|
+
},
|
|
173
|
+
"required": ["schema_version", "results"],
|
|
174
|
+
"title": "check-report",
|
|
175
|
+
"type": "object",
|
|
176
|
+
},
|
|
177
|
+
"list-report": {
|
|
178
|
+
"$defs": {
|
|
179
|
+
"Direction": {
|
|
180
|
+
"description": "Which side of the threshold is within.",
|
|
181
|
+
"oneOf": [
|
|
182
|
+
{
|
|
183
|
+
"const": "max",
|
|
184
|
+
"description": "Within when `actual <= threshold`.",
|
|
185
|
+
"type": "string",
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
"const": "min",
|
|
189
|
+
"description": "Within when `actual >= threshold`.",
|
|
190
|
+
"type": "string",
|
|
191
|
+
},
|
|
192
|
+
],
|
|
193
|
+
},
|
|
194
|
+
"ListedBudget": {
|
|
195
|
+
"additionalProperties": False,
|
|
196
|
+
"description": "One budget as `list` reports it: every field the file gives, plus the file.",
|
|
197
|
+
"properties": {
|
|
198
|
+
"command": {
|
|
199
|
+
"description": "The argv that measures it.",
|
|
200
|
+
"items": {"type": "string"},
|
|
201
|
+
"type": "array",
|
|
202
|
+
},
|
|
203
|
+
"description": {
|
|
204
|
+
"description": "The budget's description; null when it has none.",
|
|
205
|
+
"type": ["string", "null"],
|
|
206
|
+
},
|
|
207
|
+
"direction": {
|
|
208
|
+
"$ref": "#/$defs/Direction",
|
|
209
|
+
"description": "The budget's direction.",
|
|
210
|
+
},
|
|
211
|
+
"file": {
|
|
212
|
+
"description": "The budgets file's path, as discovered or given.",
|
|
213
|
+
"type": "string",
|
|
214
|
+
},
|
|
215
|
+
"id": {"description": "The budget's id.", "type": "string"},
|
|
216
|
+
"labels": {
|
|
217
|
+
"description": "The budget's labels, empty when it has none.",
|
|
218
|
+
"items": {"type": "string"},
|
|
219
|
+
"type": "array",
|
|
220
|
+
},
|
|
221
|
+
"measure": {
|
|
222
|
+
"$ref": "#/$defs/Measure",
|
|
223
|
+
"description": "How the value is measured.",
|
|
224
|
+
},
|
|
225
|
+
"threshold": {
|
|
226
|
+
"description": "The budget's threshold.",
|
|
227
|
+
"format": "double",
|
|
228
|
+
"type": "number",
|
|
229
|
+
},
|
|
230
|
+
"timeout_seconds": {
|
|
231
|
+
"description": "The budget's timeout; null when it has none.",
|
|
232
|
+
"format": "uint64",
|
|
233
|
+
"minimum": 0,
|
|
234
|
+
"type": ["integer", "null"],
|
|
235
|
+
},
|
|
236
|
+
"unit": {"description": "The budget's unit.", "type": "string"},
|
|
237
|
+
},
|
|
238
|
+
"required": [
|
|
239
|
+
"command",
|
|
240
|
+
"description",
|
|
241
|
+
"direction",
|
|
242
|
+
"file",
|
|
243
|
+
"id",
|
|
244
|
+
"labels",
|
|
245
|
+
"measure",
|
|
246
|
+
"threshold",
|
|
247
|
+
"timeout_seconds",
|
|
248
|
+
"unit",
|
|
249
|
+
],
|
|
250
|
+
"type": "object",
|
|
251
|
+
},
|
|
252
|
+
"Measure": {
|
|
253
|
+
"description": "How a budget's value is measured.",
|
|
254
|
+
"oneOf": [
|
|
255
|
+
{
|
|
256
|
+
"const": "elapsed",
|
|
257
|
+
"description": "The command's wall clock, in seconds; a non-zero exit is an error.",
|
|
258
|
+
"type": "string",
|
|
259
|
+
},
|
|
260
|
+
{
|
|
261
|
+
"const": "reported",
|
|
262
|
+
"description": "The value the command writes to the file `ONEBUDGETSPEC_RESULT` names.",
|
|
263
|
+
"type": "string",
|
|
264
|
+
},
|
|
265
|
+
],
|
|
266
|
+
},
|
|
267
|
+
},
|
|
268
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
269
|
+
"additionalProperties": False,
|
|
270
|
+
"description": "What `list` answers with: the selected budgets, in file order.",
|
|
271
|
+
"properties": {
|
|
272
|
+
"budgets": {
|
|
273
|
+
"description": "The selected budgets.",
|
|
274
|
+
"items": {"$ref": "#/$defs/ListedBudget"},
|
|
275
|
+
"type": "array",
|
|
276
|
+
},
|
|
277
|
+
"schema_version": {"const": 1, "description": "Always `1`.", "type": "integer"},
|
|
278
|
+
},
|
|
279
|
+
"required": ["schema_version", "budgets"],
|
|
280
|
+
"title": "list-report",
|
|
281
|
+
"type": "object",
|
|
282
|
+
},
|
|
283
|
+
}
|
|
File without changes
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: onebudgetspec-sdk
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: The onebudgetspec Python SDK: check, validate and list budgets through the onebudgetspec binary, with report models generated from its schema.
|
|
5
|
+
Project-URL: Repository, https://github.com/nickderobertis/onebudgetspec
|
|
6
|
+
Author: Nick DeRobertis
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
Classifier: Typing :: Typed
|
|
9
|
+
Requires-Python: >=3.11
|
|
10
|
+
Requires-Dist: jsonschema>=4.23
|
|
11
|
+
Requires-Dist: onebudgetspec-cli==0.1.0
|
|
12
|
+
Requires-Dist: pydantic>=2.12
|
|
13
|
+
Requires-Dist: typing-extensions>=4.12
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
|
|
16
|
+
# onebudgetspec-sdk
|
|
17
|
+
|
|
18
|
+
The Python SDK for [onebudgetspec](https://github.com/nickderobertis/onebudgetspec), imported
|
|
19
|
+
as `onebudgetspec_sdk`. It releases at the same version as the `onebudgetspec` command line
|
|
20
|
+
and requires `onebudgetspec-cli` at that version, whose wheel installs the binary.
|
|
21
|
+
|
|
22
|
+
```sh
|
|
23
|
+
pip install onebudgetspec-sdk
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Each call runs the `onebudgetspec` binary once, validates the JSON report it prints against
|
|
27
|
+
`onebudgetspec schema`, and returns it as a pydantic model generated from that schema:
|
|
28
|
+
|
|
29
|
+
```python
|
|
30
|
+
from onebudgetspec_sdk import check, list_budgets, schema, validate
|
|
31
|
+
|
|
32
|
+
report = check(
|
|
33
|
+
paths=None, ids=None, labels=["api"], exclude_labels=["slow"], recursive=False, cwd="."
|
|
34
|
+
)
|
|
35
|
+
for result in report.results:
|
|
36
|
+
print(result.id, result.verdict, result.actual, result.threshold, result.headroom)
|
|
37
|
+
print(result.host.conditions) # declared conditions, and any the command returned
|
|
38
|
+
|
|
39
|
+
listed = list_budgets(labels=["api"]) # ListReport; runs no command
|
|
40
|
+
validated = validate(recursive=True, paths=["."]) # ListReport of every budget; runs no command
|
|
41
|
+
bundle = schema() # the JSON Schema bundle, as a dict
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
- `check(paths=None, ids=None, labels=None, exclude_labels=None, recursive=False, cwd=None) -> CheckReport`
|
|
45
|
+
- `validate(paths=None, recursive=False, cwd=None) -> ListReport`
|
|
46
|
+
- `list_budgets(paths=None, ids=None, labels=None, exclude_labels=None, recursive=False, cwd=None) -> ListReport`
|
|
47
|
+
- `schema() -> dict`
|
|
48
|
+
|
|
49
|
+
Exit statuses `0` (within), `1` (over) and `3` (a measurement errored) all return the
|
|
50
|
+
report, since the verdicts are in it. Status `2`, an invalid invocation or budgets file,
|
|
51
|
+
raises `OnebudgetspecError` with the CLI's own message and `exit_code == 2`.
|
|
52
|
+
|
|
53
|
+
The binary is, in order: the `binary=` keyword every call takes, then `ONEBUDGETSPEC_BIN`,
|
|
54
|
+
then the `onebudgetspec` the `onebudgetspec-cli` wheel installed, then `onebudgetspec` on
|
|
55
|
+
`PATH`. `resolve_binary()` says which.
|
|
56
|
+
|
|
57
|
+
The models are generated by `generate.py` from the binary's own schema. In the
|
|
58
|
+
repository, `just generate` regenerates them and `just lint` fails while they and the
|
|
59
|
+
schema part.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
onebudgetspec_sdk/__init__.py,sha256=bLF60eZfj8el1XgwZhkAUlNowltqH_QiGfrzBIohKMY,1084
|
|
2
|
+
onebudgetspec_sdk/_client.py,sha256=jPSFTA0QenflMrNqhrIQloBvr-3ONRG8xzEi43yIppM,14748
|
|
3
|
+
onebudgetspec_sdk/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
|
+
onebudgetspec_sdk/_generated/__init__.py,sha256=AhDOYr_x7dAphRo16masPrNZKVVhE3wgW1vD5IheAT0,317
|
|
5
|
+
onebudgetspec_sdk/_generated/check_report.py,sha256=Etfev6a5pC9dKNWm43zoGSFrH7JImFIxMkhuMxfrYX4,3068
|
|
6
|
+
onebudgetspec_sdk/_generated/list_report.py,sha256=i50nhyWcpdyM_UdhHZRsIX63ArTwRfHqDiFIy1essJQ,1874
|
|
7
|
+
onebudgetspec_sdk/_generated/schemas.py,sha256=PGM6vBjCJmYmz-pxno4bo8X5fG7e6P0fPzqvaa4h_9U,12043
|
|
8
|
+
onebudgetspec_sdk-0.1.0.dist-info/METADATA,sha256=Na-QLNL7Y8Kp4wpl4zNunb7RFDy2TSsWtphAZdcxpHM,2662
|
|
9
|
+
onebudgetspec_sdk-0.1.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
|
|
10
|
+
onebudgetspec_sdk-0.1.0.dist-info/RECORD,,
|