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 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
+ ]
@@ -0,0 +1 @@
1
+ """The schemify command line interface."""
@@ -0,0 +1,4 @@
1
+ """Typer dependencies shared by the commands, injected with typer-di.
2
+
3
+ They are importable by other typer-di apps, like templify.
4
+ """
@@ -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()