schemify-cli 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.
- schemify/__init__.py +21 -0
- schemify/cli/__init__.py +1 -0
- schemify/cli/dependencies/__init__.py +4 -0
- schemify/cli/dependencies/console.py +9 -0
- schemify/cli/dependencies/schema.py +128 -0
- schemify/cli/main.py +138 -0
- schemify/computers.py +682 -0
- schemify/fillers.py +272 -0
- schemify/formatters.py +66 -0
- schemify/generators.py +323 -0
- schemify/models.py +6 -0
- schemify/py.typed +0 -0
- schemify/registry.py +69 -0
- schemify/validators.py +300 -0
- schemify_cli-0.1.0.dist-info/METADATA +93 -0
- schemify_cli-0.1.0.dist-info/RECORD +18 -0
- schemify_cli-0.1.0.dist-info/WHEEL +4 -0
- schemify_cli-0.1.0.dist-info/entry_points.txt +3 -0
schemify/registry.py
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""A registry of functions picked by name, used by the schema generators and data fillers."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Callable
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class NotSupportedError(Exception):
|
|
8
|
+
"""Raised when no function is registered under a name."""
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Registry[F: Callable[..., Any]]:
|
|
12
|
+
"""Maps names (case-insensitive) to functions.
|
|
13
|
+
|
|
14
|
+
Example:
|
|
15
|
+
```python
|
|
16
|
+
generators = Registry[Callable[[], Schema]]("Type")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@generators.register("string")
|
|
20
|
+
def generate_string() -> Schema: ...
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
schema = generators["string"]()
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
def __init__(self, kind: str) -> None:
|
|
29
|
+
"""Create an empty registry, `kind` names its entries in error messages."""
|
|
30
|
+
self._kind = kind
|
|
31
|
+
self._fns: dict[str, F] = {}
|
|
32
|
+
|
|
33
|
+
def __contains__(self, name: object) -> bool:
|
|
34
|
+
"""Whether a function is registered under `name`."""
|
|
35
|
+
return isinstance(name, str) and name.lower() in self._fns
|
|
36
|
+
|
|
37
|
+
def __getitem__(self, name: str) -> F:
|
|
38
|
+
"""Return the function registered under `name`.
|
|
39
|
+
|
|
40
|
+
Raises:
|
|
41
|
+
NotSupportedError: Nothing is registered under `name`.
|
|
42
|
+
|
|
43
|
+
"""
|
|
44
|
+
if name not in self:
|
|
45
|
+
supported = ", ".join(self.names)
|
|
46
|
+
message = f"{self._kind} '{name}' is not supported, supported: {supported}"
|
|
47
|
+
raise NotSupportedError(message)
|
|
48
|
+
|
|
49
|
+
return self._fns[name.lower()]
|
|
50
|
+
|
|
51
|
+
@property
|
|
52
|
+
def names(self) -> list[str]:
|
|
53
|
+
"""The registered names, sorted."""
|
|
54
|
+
return sorted(self._fns)
|
|
55
|
+
|
|
56
|
+
def register(self, *names: str) -> Callable[[F], F]:
|
|
57
|
+
"""Register the decorated function under `names`."""
|
|
58
|
+
|
|
59
|
+
def decorator(fn: F) -> F:
|
|
60
|
+
for name in names:
|
|
61
|
+
self.add(fn, name)
|
|
62
|
+
|
|
63
|
+
return fn
|
|
64
|
+
|
|
65
|
+
return decorator
|
|
66
|
+
|
|
67
|
+
def add(self, fn: F, name: str) -> None:
|
|
68
|
+
"""Register `fn` under `name`, replacing any existing one."""
|
|
69
|
+
self._fns[name.lower()] = fn
|
schemify/validators.py
ADDED
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
"""Validators and parsers for prompt answers and values, they never prompt.
|
|
2
|
+
|
|
3
|
+
Prompt validators follow the questionary `validate=` contract:
|
|
4
|
+
they return `True` when the answer is valid, else the error message.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import contextlib
|
|
8
|
+
import json
|
|
9
|
+
import math
|
|
10
|
+
import re
|
|
11
|
+
from collections.abc import Callable, Collection
|
|
12
|
+
from datetime import date, datetime
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
from jsonschema import Draft202012Validator, FormatChecker, SchemaError
|
|
16
|
+
|
|
17
|
+
from .models import Schema
|
|
18
|
+
|
|
19
|
+
type Parser = Callable[[str], Any]
|
|
20
|
+
"""Turns a prompt answer into a value, raises `ValueError` with a readable message."""
|
|
21
|
+
|
|
22
|
+
type PromptValidator = Callable[[str], bool | str]
|
|
23
|
+
"""Returns `True` for a valid answer, else the error message."""
|
|
24
|
+
|
|
25
|
+
type Constraint = tuple[str, str, Parser, PromptValidator]
|
|
26
|
+
"""A schema keyword, its question, the answer parser and the answer validator."""
|
|
27
|
+
|
|
28
|
+
# -----------------------
|
|
29
|
+
# parsers
|
|
30
|
+
# -----------------------
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def to_int(text: str) -> int:
|
|
34
|
+
"""Parse an integer: `"42"`."""
|
|
35
|
+
try:
|
|
36
|
+
return int(text.strip())
|
|
37
|
+
except ValueError:
|
|
38
|
+
message = f"'{text}' is not an integer"
|
|
39
|
+
raise ValueError(message) from None
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def to_number(text: str) -> int | float:
|
|
43
|
+
"""Parse a finite number, keeping integers as `int`: `"42"`, `"4.2"`."""
|
|
44
|
+
try:
|
|
45
|
+
return to_int(text)
|
|
46
|
+
except ValueError:
|
|
47
|
+
pass
|
|
48
|
+
|
|
49
|
+
try:
|
|
50
|
+
number = float(text.strip())
|
|
51
|
+
except ValueError:
|
|
52
|
+
message = f"'{text}' is not a number"
|
|
53
|
+
raise ValueError(message) from None
|
|
54
|
+
|
|
55
|
+
if not math.isfinite(number):
|
|
56
|
+
message = f"'{text}' is not a finite number"
|
|
57
|
+
raise ValueError(message)
|
|
58
|
+
|
|
59
|
+
return number
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def to_non_negative_int(text: str) -> int:
|
|
63
|
+
"""Parse an integer that is 0 or more, like a length."""
|
|
64
|
+
number = to_int(text)
|
|
65
|
+
|
|
66
|
+
if number < 0:
|
|
67
|
+
message = f"'{text}' must be 0 or more"
|
|
68
|
+
raise ValueError(message)
|
|
69
|
+
|
|
70
|
+
return number
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def to_positive_number(text: str) -> int | float:
|
|
74
|
+
"""Parse a number greater than 0, like `multipleOf`."""
|
|
75
|
+
number = to_number(text)
|
|
76
|
+
|
|
77
|
+
if number <= 0:
|
|
78
|
+
message = f"'{text}' must be greater than 0"
|
|
79
|
+
raise ValueError(message)
|
|
80
|
+
|
|
81
|
+
return number
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def to_regex(text: str) -> str:
|
|
85
|
+
"""Check `text` compiles as a regular expression and return it unchanged."""
|
|
86
|
+
try:
|
|
87
|
+
re.compile(text)
|
|
88
|
+
except re.error as e:
|
|
89
|
+
message = f"Invalid regular expression: {e}"
|
|
90
|
+
raise ValueError(message) from None
|
|
91
|
+
|
|
92
|
+
return text
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def to_choices(text: str) -> list[str]:
|
|
96
|
+
"""Parse comma separated choices: `"red, green"` -> `["red", "green"]`."""
|
|
97
|
+
choices = [choice.strip() for choice in text.split(",") if choice.strip()]
|
|
98
|
+
|
|
99
|
+
if not choices:
|
|
100
|
+
message = "Enter at least one choice"
|
|
101
|
+
raise ValueError(message)
|
|
102
|
+
|
|
103
|
+
if len(set(choices)) != len(choices):
|
|
104
|
+
message = "Choices must be unique"
|
|
105
|
+
raise ValueError(message)
|
|
106
|
+
|
|
107
|
+
return choices
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def to_date(text: str) -> str:
|
|
111
|
+
"""Parse an ISO date, returned as an ISO string: `"2026-11-04"`."""
|
|
112
|
+
return date.fromisoformat(text.strip()).isoformat()
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def to_datetime(text: str) -> str:
|
|
116
|
+
"""Parse an ISO datetime, returned as an RFC 3339 string.
|
|
117
|
+
|
|
118
|
+
A datetime without offset is taken as local time, RFC 3339 requires the offset.
|
|
119
|
+
"""
|
|
120
|
+
value = datetime.fromisoformat(text.strip())
|
|
121
|
+
|
|
122
|
+
return (value if value.tzinfo else value.astimezone()).isoformat()
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def default_parser(schema: Schema) -> Parser:
|
|
126
|
+
"""Build a parser for a default value of `schema`.
|
|
127
|
+
|
|
128
|
+
The answer is read as JSON (`42`, `true`, `["a"]`), else as text, the first matching `schema`
|
|
129
|
+
is kept: `42` is a number for an integer and the text `"42"` for a string.
|
|
130
|
+
"""
|
|
131
|
+
|
|
132
|
+
def parse(text: str) -> Any:
|
|
133
|
+
candidates: list[Any] = [text.strip()]
|
|
134
|
+
|
|
135
|
+
with contextlib.suppress(json.JSONDecodeError):
|
|
136
|
+
candidates.insert(0, json.loads(text))
|
|
137
|
+
|
|
138
|
+
errors = [check_value(schema, candidate) for candidate in candidates]
|
|
139
|
+
|
|
140
|
+
for candidate, error in zip(candidates, errors, strict=True):
|
|
141
|
+
if error is None:
|
|
142
|
+
return candidate
|
|
143
|
+
|
|
144
|
+
raise ValueError(errors[0])
|
|
145
|
+
|
|
146
|
+
return parse
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
# -----------------------
|
|
150
|
+
# prompt validators
|
|
151
|
+
# -----------------------
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def validator(parse: Parser) -> PromptValidator:
|
|
155
|
+
"""Build a prompt validator accepting the answers `parse` accepts."""
|
|
156
|
+
|
|
157
|
+
def validate(text: str) -> bool | str:
|
|
158
|
+
try:
|
|
159
|
+
parse(text)
|
|
160
|
+
except ValueError as e:
|
|
161
|
+
return str(e)
|
|
162
|
+
|
|
163
|
+
return True
|
|
164
|
+
|
|
165
|
+
return validate
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def optional(validate: PromptValidator) -> PromptValidator:
|
|
169
|
+
"""Accept an empty answer (to skip), else run `validate`."""
|
|
170
|
+
return lambda text: not text.strip() or validate(text)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def is_choice(existing: Collection[str]) -> PromptValidator:
|
|
174
|
+
"""Accept a new enum choice, or an empty answer to finish once one choice exists."""
|
|
175
|
+
|
|
176
|
+
def validate(text: str) -> bool | str:
|
|
177
|
+
choice = text.strip()
|
|
178
|
+
|
|
179
|
+
if not choice:
|
|
180
|
+
return bool(existing) or "Add at least one choice"
|
|
181
|
+
|
|
182
|
+
return choice not in existing or f"Choice '{choice}' already exists"
|
|
183
|
+
|
|
184
|
+
return validate
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def is_property_name(existing: Collection[str]) -> PromptValidator:
|
|
188
|
+
"""Accept a new property name, or an empty answer to finish once one property exists."""
|
|
189
|
+
|
|
190
|
+
def validate(text: str) -> bool | str:
|
|
191
|
+
name = text.strip()
|
|
192
|
+
|
|
193
|
+
if not name:
|
|
194
|
+
return bool(existing) or "Add at least one property"
|
|
195
|
+
|
|
196
|
+
return name not in existing or f"Property '{name}' already exists"
|
|
197
|
+
|
|
198
|
+
return validate
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
# -----------------------
|
|
202
|
+
# constraints
|
|
203
|
+
# -----------------------
|
|
204
|
+
|
|
205
|
+
STRING_CONSTRAINTS: list[Constraint] = [
|
|
206
|
+
("minLength", "Minimum length", to_non_negative_int, validator(to_non_negative_int)),
|
|
207
|
+
("maxLength", "Maximum length", to_non_negative_int, validator(to_non_negative_int)),
|
|
208
|
+
("pattern", "Regular expression pattern", to_regex, validator(to_regex)),
|
|
209
|
+
(
|
|
210
|
+
"not",
|
|
211
|
+
"Blocked values, comma separated",
|
|
212
|
+
lambda text: {"enum": to_choices(text)},
|
|
213
|
+
validator(to_choices),
|
|
214
|
+
),
|
|
215
|
+
]
|
|
216
|
+
"""The validators asked for a string, mapped to JSON Schema keywords."""
|
|
217
|
+
|
|
218
|
+
NUMBER_CONSTRAINTS: list[Constraint] = [
|
|
219
|
+
("exclusiveMinimum", "Greater than", to_number, validator(to_number)),
|
|
220
|
+
("minimum", "Greater than or equal to", to_number, validator(to_number)),
|
|
221
|
+
("exclusiveMaximum", "Less than", to_number, validator(to_number)),
|
|
222
|
+
("maximum", "Less than or equal to", to_number, validator(to_number)),
|
|
223
|
+
("multipleOf", "Multiple of", to_positive_number, validator(to_positive_number)),
|
|
224
|
+
]
|
|
225
|
+
"""The validators asked for an integer or a number, mapped to JSON Schema keywords."""
|
|
226
|
+
|
|
227
|
+
ARRAY_CONSTRAINTS: list[Constraint] = [
|
|
228
|
+
("minItems", "Minimum items", to_non_negative_int, validator(to_non_negative_int)),
|
|
229
|
+
("maxItems", "Maximum items", to_non_negative_int, validator(to_non_negative_int)),
|
|
230
|
+
]
|
|
231
|
+
"""The validators asked for an array, mapped to JSON Schema keywords."""
|
|
232
|
+
|
|
233
|
+
_BOUNDS = [
|
|
234
|
+
# (lower keyword, upper keyword, can they be equal)
|
|
235
|
+
("minLength", "maxLength", True),
|
|
236
|
+
("minItems", "maxItems", True),
|
|
237
|
+
("minimum", "maximum", True),
|
|
238
|
+
("minimum", "exclusiveMaximum", False),
|
|
239
|
+
("exclusiveMinimum", "maximum", False),
|
|
240
|
+
("exclusiveMinimum", "exclusiveMaximum", False),
|
|
241
|
+
]
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def check_bounds(schema: Schema) -> str | None:
|
|
245
|
+
"""Return why no value can satisfy the bounds of `schema`, or `None` when they can."""
|
|
246
|
+
for low, high, equal_ok in _BOUNDS:
|
|
247
|
+
if low not in schema or high not in schema:
|
|
248
|
+
continue
|
|
249
|
+
|
|
250
|
+
if schema[low] > schema[high] or (not equal_ok and schema[low] == schema[high]):
|
|
251
|
+
return f"{low} ({schema[low]}) must be less than {high} ({schema[high]})"
|
|
252
|
+
|
|
253
|
+
return None
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def within_bounds(schema: Schema, constraint: Constraint) -> PromptValidator:
|
|
257
|
+
"""Run the `constraint` validator, then reject an answer conflicting with `schema` bounds.
|
|
258
|
+
|
|
259
|
+
So the user stays on the conflicting answer instead of starting the constraints over.
|
|
260
|
+
"""
|
|
261
|
+
keyword, _, parse, validate = constraint
|
|
262
|
+
|
|
263
|
+
def validate_bounds(text: str) -> bool | str:
|
|
264
|
+
result = validate(text)
|
|
265
|
+
|
|
266
|
+
if result is not True:
|
|
267
|
+
return result
|
|
268
|
+
|
|
269
|
+
return check_bounds({**schema, keyword: parse(text)}) or True
|
|
270
|
+
|
|
271
|
+
return validate_bounds
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
# -----------------------
|
|
275
|
+
# json schema
|
|
276
|
+
# -----------------------
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def check_schema(schema: Any) -> str | None:
|
|
280
|
+
"""Return why `schema` is not a valid JSON Schema object, or `None` when it is."""
|
|
281
|
+
if not isinstance(schema, dict):
|
|
282
|
+
return "A schema must be a JSON object"
|
|
283
|
+
|
|
284
|
+
try:
|
|
285
|
+
Draft202012Validator.check_schema(schema)
|
|
286
|
+
except SchemaError as e:
|
|
287
|
+
return e.message
|
|
288
|
+
|
|
289
|
+
return None
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def check_value(schema: Schema, value: Any) -> str | None:
|
|
293
|
+
"""Return why `value` does not match `schema`, or `None` when it does.
|
|
294
|
+
|
|
295
|
+
Formats are checked when jsonschema supports them without extras (date, email, uuid, ...).
|
|
296
|
+
"""
|
|
297
|
+
validator_ = Draft202012Validator(schema, format_checker=FormatChecker())
|
|
298
|
+
error = next(validator_.iter_errors(value), None)
|
|
299
|
+
|
|
300
|
+
return None if error is None else error.message
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: schemify-cli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Build JSON Schemas interactively, and fill JSON data from them, from the command line.
|
|
5
|
+
Author: aaltatan
|
|
6
|
+
Author-email: aaltatan <a.altatan@gmail.com>
|
|
7
|
+
Requires-Dist: jsonschema>=4.26.0
|
|
8
|
+
Requires-Dist: num2words>=0.5.14
|
|
9
|
+
Requires-Dist: questionary>=2.1.1
|
|
10
|
+
Requires-Dist: rich>=15.0.0
|
|
11
|
+
Requires-Dist: typer>=0.27.3
|
|
12
|
+
Requires-Dist: typer-di>=0.1.5
|
|
13
|
+
Requires-Dist: typer-swissknife>=0.1.1
|
|
14
|
+
Requires-Python: >=3.12
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
|
|
17
|
+
# schemify-cli
|
|
18
|
+
|
|
19
|
+
Build JSON Schemas interactively, and fill JSON data from them, from the command line.
|
|
20
|
+
|
|
21
|
+
Source: [github.com/aaltatan/schemify-cli](https://github.com/aaltatan/schemify-cli)
|
|
22
|
+
|
|
23
|
+
A web version runs at [schemify.surge.sh](https://schemify.surge.sh)
|
|
24
|
+
([source](https://github.com/aaltatan/schemify)): the same questions as fields, with the
|
|
25
|
+
schema updating as you type.
|
|
26
|
+
|
|
27
|
+
## Install
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
uv tool install schemify
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Usage
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
schemify generate -t Person -o person.schema.json # ask for the properties, write the schema
|
|
37
|
+
schemify fill person.schema.json -o person.json # ask for one object matching it
|
|
38
|
+
schemify list person.schema.json -f python # ask for a list of them
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
| Command | Alias | Does |
|
|
42
|
+
|---|---|---|
|
|
43
|
+
| `generate` | `g` | Asks for the properties of an object and builds its JSON Schema (Draft 2020-12) |
|
|
44
|
+
| `fill` | `f` | Asks for data matching a schema file, checked against it |
|
|
45
|
+
| `list` | `l` | Asks for a list of items, each matching the schema |
|
|
46
|
+
|
|
47
|
+
- `-o, --output` saves to a file (asks before overwriting); without it the result is printed.
|
|
48
|
+
- `-f, --format` picks the output of `fill` and `list`: `json`, `python` or `javascript`.
|
|
49
|
+
|
|
50
|
+
Types: `string`, `integer`, `number`, `boolean`, `date`, `datetime`, `enum`, `array`, `object`,
|
|
51
|
+
each with its constraints (length, pattern, format, range, items, ...) and an optional default.
|
|
52
|
+
|
|
53
|
+
## Computed properties
|
|
54
|
+
|
|
55
|
+
A property with `x-compute` is not asked in `fill`, it is computed from the other values:
|
|
56
|
+
|
|
57
|
+
```json
|
|
58
|
+
"full_name": {"type": "string", "x-compute": {"fn": "join", "from": ["first", "middle?", "last"]}}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
- `fn`: the computer, `from`: source paths (a list, or an `{alias: path}` object for `template`
|
|
62
|
+
and `expr`), `options`: extra arguments, `default`: the value when a source is missing.
|
|
63
|
+
- Paths are relative to the owning object (`address.city`, `items[0]`), or to the root
|
|
64
|
+
(`$.company`). `items[*].price` gives every price; a trailing `?` makes a source optional.
|
|
65
|
+
|
|
66
|
+
Built-in computers:
|
|
67
|
+
|
|
68
|
+
- **Text**: join, concat, template, upper, lower, title, capitalize, trim, replace, slice,
|
|
69
|
+
initials, slug, length, pad, mask
|
|
70
|
+
- **Numbers**: sum, subtract, multiply, divide, average, min, max, abs, round, percent,
|
|
71
|
+
percent_of, expr
|
|
72
|
+
- **Dates**: today, now, add_days, add_months, add_years, days_between, age, year, month, day,
|
|
73
|
+
weekday, format_date, start_of_month, end_of_month
|
|
74
|
+
- **Number to words**: words
|
|
75
|
+
- **Logic, lookup and misc**: coalesce, if_else, equals, lookup, constant, count, count_if,
|
|
76
|
+
unique, uuid
|
|
77
|
+
|
|
78
|
+
## Reusing it in a typer app
|
|
79
|
+
|
|
80
|
+
`schemify.cli.dependencies.schema` exposes typer-di dependencies that load a schema file and ask
|
|
81
|
+
for its data: `FilledDataArgumentDI` and `FilledListArgumentDI` take the path as an argument,
|
|
82
|
+
`FilledDataOptionDI` and `FilledListOptionDI` as `--js`.
|
|
83
|
+
|
|
84
|
+
## Development
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
uv sync
|
|
88
|
+
uv run pytest
|
|
89
|
+
uv run ruff check . && uv run mypy src
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
After changing `generators.py`, `validators.py` or `computers.py`, run `pnpm sync && pnpm check`
|
|
93
|
+
in [schemify-web](https://github.com/aaltatan/schemify-web) so it stays in step.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
schemify/__init__.py,sha256=bVb4Qylc0TYeBR3jhRz5rp0EZe5gCwbRfYToXAMpB_M,564
|
|
2
|
+
schemify/cli/__init__.py,sha256=aayFp21WiVMIDHa1Z84eqw15ZLtteL24vuQR6a1W9Mg,43
|
|
3
|
+
schemify/cli/dependencies/__init__.py,sha256=Nq8g_PGFq5RBHU1ELk7p1PKVVe9Xjj5AVL3dKAJqZAs,134
|
|
4
|
+
schemify/cli/dependencies/console.py,sha256=Ai0VzaU7cL6kK8YjqaPW2YWFeO2FbWazBeFj4vpuIGE,265
|
|
5
|
+
schemify/cli/dependencies/schema.py,sha256=RoNJJLYTR3VoM99FsaENYp09OgRERQGtAq5XgovKQAs,3700
|
|
6
|
+
schemify/cli/main.py,sha256=zN-LH-ci0zqRITOrJtBVtpQoF3rt_m9e0yXaZ_PL-Ns,3766
|
|
7
|
+
schemify/computers.py,sha256=cRbk4DVeXpMB3zWQXbqpSivm-7HieXGLBLoj_Ija_hA,20732
|
|
8
|
+
schemify/fillers.py,sha256=avd50hSzFA86M2a30m3HZm31fpxxV43_kxPA3T1zDnU,8306
|
|
9
|
+
schemify/formatters.py,sha256=RF7NlpOrxTr5TS4cx0aq9c4kB1GNmNLwV6rhXD4Gdiw,1903
|
|
10
|
+
schemify/generators.py,sha256=dnP8f0mpP-6Bb0FBghFI_81q8In6uywPbd8nmXrfGFk,10421
|
|
11
|
+
schemify/models.py,sha256=V9Ob9vTF4U5AsaFHOtl3HOUDpBowBUQ2h-WZo2irO-g,160
|
|
12
|
+
schemify/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
13
|
+
schemify/registry.py,sha256=gLneezkhx6OjnybD35R0DL_KWGfXMFnqK-tuo1SlmCo,1928
|
|
14
|
+
schemify/validators.py,sha256=2CVLgwwl_Ofm1wSaONA7UcWiJJ8LU-5yJy5jEHCw-O0,9079
|
|
15
|
+
schemify_cli-0.1.0.dist-info/WHEEL,sha256=si2p8vW03RnhyBnhWl4tj-ObJa78SpE-c9HsEtblGR0,80
|
|
16
|
+
schemify_cli-0.1.0.dist-info/entry_points.txt,sha256=4uAUa_6hC_cz6oMOXTN-djmSHdPh9Uc9-rq1qd9j_ic,53
|
|
17
|
+
schemify_cli-0.1.0.dist-info/METADATA,sha256=fUOJhRqrfKlJ-s7shE6V_LUdok3Ks8cBVpywJq45DGQ,3565
|
|
18
|
+
schemify_cli-0.1.0.dist-info/RECORD,,
|