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/__init__.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""schemify: build JSON Schemas interactively, and fill JSON data from them."""
|
|
2
|
+
|
|
3
|
+
from .computers import ComputeError, compute, computers
|
|
4
|
+
from .fillers import InvalidDataError, ask_data, ask_list, fillers
|
|
5
|
+
from .formatters import formatters
|
|
6
|
+
from .generators import ask_schema, generators
|
|
7
|
+
from .registry import NotSupportedError
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"ComputeError",
|
|
11
|
+
"InvalidDataError",
|
|
12
|
+
"NotSupportedError",
|
|
13
|
+
"ask_data",
|
|
14
|
+
"ask_list",
|
|
15
|
+
"ask_schema",
|
|
16
|
+
"compute",
|
|
17
|
+
"computers",
|
|
18
|
+
"fillers",
|
|
19
|
+
"formatters",
|
|
20
|
+
"generators",
|
|
21
|
+
]
|
schemify/cli/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""The schemify command line interface."""
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"""The rich console used for command output."""
|
|
2
|
+
|
|
3
|
+
from typing import Annotated
|
|
4
|
+
|
|
5
|
+
from rich.console import Console
|
|
6
|
+
from typer_di import Depends
|
|
7
|
+
|
|
8
|
+
ConsoleDI = Annotated[Console, Depends(lambda: Console(color_system="truecolor"))]
|
|
9
|
+
"""Injects a truecolor rich console."""
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"""The schema file, and the data the user fills from it, as an option or an argument.
|
|
2
|
+
|
|
3
|
+
Other typer-di apps can reuse them, pick the version matching how the path is given:
|
|
4
|
+
|
|
5
|
+
```python
|
|
6
|
+
@app.command()
|
|
7
|
+
def fill_template(data: FilledDataOptionDI) -> None: ... # app --js person.json
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@app.command()
|
|
11
|
+
def fill_template(data: FilledDataArgumentDI) -> None: ... # app person.json
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@app.command()
|
|
15
|
+
def fill_many(items: FilledListArgumentDI) -> None: ... # app person.json, a list of people
|
|
16
|
+
```
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
import json
|
|
20
|
+
from collections.abc import Callable
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
from typing import Annotated, Any
|
|
23
|
+
|
|
24
|
+
import typer
|
|
25
|
+
from typer_di import Depends
|
|
26
|
+
|
|
27
|
+
from schemify.fillers import InvalidDataError, ask_data, ask_list
|
|
28
|
+
from schemify.models import Schema
|
|
29
|
+
from schemify.registry import NotSupportedError
|
|
30
|
+
from schemify.validators import check_schema
|
|
31
|
+
|
|
32
|
+
_PATH_HELP = "Path to the JSON Schema file"
|
|
33
|
+
|
|
34
|
+
# -----------------------
|
|
35
|
+
# schema
|
|
36
|
+
# -----------------------
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _load_schema(path: Path) -> Schema:
|
|
40
|
+
try:
|
|
41
|
+
schema = json.loads(path.read_text(encoding="utf-8"))
|
|
42
|
+
except (json.JSONDecodeError, UnicodeDecodeError) as e:
|
|
43
|
+
msg = f"Could not read schema file '{path.name}': {e}"
|
|
44
|
+
raise typer.BadParameter(msg) from e
|
|
45
|
+
|
|
46
|
+
error = check_schema(schema)
|
|
47
|
+
|
|
48
|
+
if error is not None:
|
|
49
|
+
msg = f"Invalid schema file '{path.name}': {error}"
|
|
50
|
+
raise typer.BadParameter(msg)
|
|
51
|
+
|
|
52
|
+
return schema
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _schema_from_option(
|
|
56
|
+
path: Annotated[
|
|
57
|
+
Path,
|
|
58
|
+
typer.Option(
|
|
59
|
+
"--js",
|
|
60
|
+
"--json-schema",
|
|
61
|
+
help=_PATH_HELP,
|
|
62
|
+
exists=True,
|
|
63
|
+
file_okay=True,
|
|
64
|
+
dir_okay=False,
|
|
65
|
+
resolve_path=True,
|
|
66
|
+
),
|
|
67
|
+
],
|
|
68
|
+
) -> Schema:
|
|
69
|
+
return _load_schema(path)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _schema_from_argument(
|
|
73
|
+
path: Annotated[
|
|
74
|
+
Path,
|
|
75
|
+
typer.Argument(
|
|
76
|
+
help=_PATH_HELP,
|
|
77
|
+
exists=True,
|
|
78
|
+
file_okay=True,
|
|
79
|
+
dir_okay=False,
|
|
80
|
+
resolve_path=True,
|
|
81
|
+
),
|
|
82
|
+
],
|
|
83
|
+
) -> Schema:
|
|
84
|
+
return _load_schema(path)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
# -----------------------
|
|
88
|
+
# filled data
|
|
89
|
+
# -----------------------
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _filled_data(
|
|
93
|
+
read_schema: Callable[..., Schema], ask: Callable[[Schema], Any] = ask_data
|
|
94
|
+
) -> Callable[..., Any]:
|
|
95
|
+
"""Build a dependency asking (with `ask`) for data matching the schema `read_schema` injects."""
|
|
96
|
+
|
|
97
|
+
def fill(schema: Annotated[Schema, Depends(read_schema)]) -> Any:
|
|
98
|
+
try:
|
|
99
|
+
return ask(schema)
|
|
100
|
+
except (NotSupportedError, InvalidDataError) as e:
|
|
101
|
+
raise typer.BadParameter(str(e)) from e
|
|
102
|
+
except KeyboardInterrupt:
|
|
103
|
+
raise typer.Abort from None
|
|
104
|
+
|
|
105
|
+
return fill
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
# -----------------------
|
|
109
|
+
# DI
|
|
110
|
+
# -----------------------
|
|
111
|
+
|
|
112
|
+
SchemaOptionDI = Annotated[Schema, Depends(_schema_from_option)]
|
|
113
|
+
"""Injects the valid JSON Schema read from the `--js` / `--json-schema` option."""
|
|
114
|
+
|
|
115
|
+
SchemaArgumentDI = Annotated[Schema, Depends(_schema_from_argument)]
|
|
116
|
+
"""Injects the valid JSON Schema read from the `PATH` argument."""
|
|
117
|
+
|
|
118
|
+
FilledDataOptionDI = Annotated[Any, Depends(_filled_data(_schema_from_option))]
|
|
119
|
+
"""Injects the data the user entered, matching the `--js` / `--json-schema` file."""
|
|
120
|
+
|
|
121
|
+
FilledDataArgumentDI = Annotated[Any, Depends(_filled_data(_schema_from_argument))]
|
|
122
|
+
"""Injects the data the user entered, matching the `PATH` argument file."""
|
|
123
|
+
|
|
124
|
+
FilledListOptionDI = Annotated[list[Any], Depends(_filled_data(_schema_from_option, ask_list))]
|
|
125
|
+
"""Injects the list of items the user entered, each matching the `--js` / `--json-schema` file."""
|
|
126
|
+
|
|
127
|
+
FilledListArgumentDI = Annotated[list[Any], Depends(_filled_data(_schema_from_argument, ask_list))]
|
|
128
|
+
"""Injects the list of items the user entered, each matching the `PATH` argument file."""
|
schemify/cli/main.py
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
"""The schemify command line application."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Annotated
|
|
5
|
+
|
|
6
|
+
import typer
|
|
7
|
+
from rich.syntax import Syntax
|
|
8
|
+
from typer_di import TyperDI
|
|
9
|
+
from typer_swissknife import command
|
|
10
|
+
|
|
11
|
+
from schemify.cli.dependencies.console import ConsoleDI
|
|
12
|
+
from schemify.cli.dependencies.schema import FilledDataArgumentDI, FilledListArgumentDI
|
|
13
|
+
from schemify.formatters import formatters, to_json
|
|
14
|
+
from schemify.generators import ask_schema
|
|
15
|
+
|
|
16
|
+
app = TyperDI(
|
|
17
|
+
name="schemify",
|
|
18
|
+
help="Build JSON Schemas interactively, and fill JSON data from them",
|
|
19
|
+
no_args_is_help=True,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _confirm_overwrite(output: Path | None) -> Path | None:
|
|
24
|
+
"""Ask before the long interactive session, not after it."""
|
|
25
|
+
if output is not None and output.exists():
|
|
26
|
+
typer.confirm(f"'{output.name}' already exists, overwrite it?", abort=True)
|
|
27
|
+
|
|
28
|
+
return output
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
OutputOption = Annotated[ # not a `type` alias, typer can not read through those
|
|
32
|
+
Path | None,
|
|
33
|
+
typer.Option(
|
|
34
|
+
"-o",
|
|
35
|
+
"--output",
|
|
36
|
+
help="Path to save the output to, printed when missing",
|
|
37
|
+
file_okay=True,
|
|
38
|
+
dir_okay=False,
|
|
39
|
+
resolve_path=True,
|
|
40
|
+
callback=_confirm_overwrite,
|
|
41
|
+
),
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _check_if_format_is_supported(name: str) -> str:
|
|
46
|
+
if name not in formatters:
|
|
47
|
+
supported = ", ".join(formatters.names)
|
|
48
|
+
message = f"Format '{name}' is not supported, supported: {supported}"
|
|
49
|
+
raise typer.BadParameter(message)
|
|
50
|
+
|
|
51
|
+
return name.lower()
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
FormatOption = Annotated[
|
|
55
|
+
str,
|
|
56
|
+
typer.Option(
|
|
57
|
+
"-f",
|
|
58
|
+
"--format",
|
|
59
|
+
help=f"Output format ({', '.join(formatters.names)})",
|
|
60
|
+
callback=_check_if_format_is_supported,
|
|
61
|
+
),
|
|
62
|
+
]
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _output(console: ConsoleDI, text: str, lexer: str, output: Path | None) -> None:
|
|
66
|
+
if output is None:
|
|
67
|
+
console.print(Syntax(text, lexer, background_color="default"))
|
|
68
|
+
return
|
|
69
|
+
|
|
70
|
+
output.parent.mkdir(parents=True, exist_ok=True)
|
|
71
|
+
output.write_text(f"{text}\n", encoding="utf-8")
|
|
72
|
+
console.print(f"[green]Saved to:[/] {output}")
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@command(
|
|
76
|
+
app,
|
|
77
|
+
name="generate",
|
|
78
|
+
aliases=("g",),
|
|
79
|
+
help="Ask for the properties of an object and build its JSON Schema",
|
|
80
|
+
)
|
|
81
|
+
def generate_schema(
|
|
82
|
+
console: ConsoleDI,
|
|
83
|
+
output: OutputOption = None,
|
|
84
|
+
title: Annotated[
|
|
85
|
+
str | None,
|
|
86
|
+
typer.Option("-t", "--title", help="Title of the schema"),
|
|
87
|
+
] = None,
|
|
88
|
+
) -> None:
|
|
89
|
+
"""Ask for the properties of an object and build its JSON Schema."""
|
|
90
|
+
try:
|
|
91
|
+
schema = ask_schema(title)
|
|
92
|
+
except KeyboardInterrupt:
|
|
93
|
+
raise typer.Abort from None
|
|
94
|
+
|
|
95
|
+
_output(console, to_json(schema), "json", output)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@command(
|
|
99
|
+
app,
|
|
100
|
+
name="fill",
|
|
101
|
+
aliases=("f",),
|
|
102
|
+
no_args_is_help=True,
|
|
103
|
+
help="Ask for data matching a JSON Schema",
|
|
104
|
+
)
|
|
105
|
+
def fill_data(
|
|
106
|
+
console: ConsoleDI,
|
|
107
|
+
data: FilledDataArgumentDI,
|
|
108
|
+
output: OutputOption = None,
|
|
109
|
+
output_format: FormatOption = "json",
|
|
110
|
+
) -> None:
|
|
111
|
+
"""Ask for data matching a JSON Schema."""
|
|
112
|
+
_output(console, formatters[output_format](data), output_format, output)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
@command(
|
|
116
|
+
app,
|
|
117
|
+
name="list",
|
|
118
|
+
aliases=("l",),
|
|
119
|
+
no_args_is_help=True,
|
|
120
|
+
help="Ask for a list of items, each matching a JSON Schema",
|
|
121
|
+
)
|
|
122
|
+
def fill_list(
|
|
123
|
+
console: ConsoleDI,
|
|
124
|
+
items: FilledListArgumentDI,
|
|
125
|
+
output: OutputOption = None,
|
|
126
|
+
output_format: FormatOption = "json",
|
|
127
|
+
) -> None:
|
|
128
|
+
"""Ask for a list of items, each matching a JSON Schema."""
|
|
129
|
+
_output(console, formatters[output_format](items), output_format, output)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def main() -> None:
|
|
133
|
+
"""Run the schemify command line application."""
|
|
134
|
+
app()
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
if __name__ == "__main__":
|
|
138
|
+
main()
|