schemify-cli 0.1.0__tar.gz → 0.2.0__tar.gz

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: schemify-cli
3
- Version: 0.1.0
3
+ Version: 0.2.0
4
4
  Summary: Build JSON Schemas interactively, and fill JSON data from them, from the command line.
5
5
  Author: aaltatan
6
6
  Author-email: aaltatan <a.altatan@gmail.com>
@@ -27,7 +27,7 @@ schema updating as you type.
27
27
  ## Install
28
28
 
29
29
  ```bash
30
- uv tool install schemify
30
+ uv tool install schemify-cli
31
31
  ```
32
32
 
33
33
  ## Usage
@@ -38,6 +38,19 @@ schemify fill person.schema.json -o person.json # ask for one object matchin
38
38
  schemify list person.schema.json -f python # ask for a list of them
39
39
  ```
40
40
 
41
+ <!-- absolute URLs so the GIFs also show on PyPI -->
42
+ `generate`: a `Person` with a `full_name` computed by joining `first` and `last`
43
+
44
+ ![schemify generate](https://raw.githubusercontent.com/aaltatan/schemify-cli/main/docs/demo/generate.gif)
45
+
46
+ `fill`: one object, `full_name` is computed, not asked
47
+
48
+ ![schemify fill](https://raw.githubusercontent.com/aaltatan/schemify-cli/main/docs/demo/fill.gif)
49
+
50
+ `list`: several objects, printed as Python
51
+
52
+ ![schemify list](https://raw.githubusercontent.com/aaltatan/schemify-cli/main/docs/demo/list.gif)
53
+
41
54
  | Command | Alias | Does |
42
55
  |---|---|---|
43
56
  | `generate` | `g` | Asks for the properties of an object and builds its JSON Schema (Draft 2020-12) |
@@ -79,7 +92,9 @@ Built-in computers:
79
92
 
80
93
  `schemify.cli.dependencies.schema` exposes typer-di dependencies that load a schema file and ask
81
94
  for its data: `FilledDataArgumentDI` and `FilledListArgumentDI` take the path as an argument,
82
- `FilledDataOptionDI` and `FilledListOptionDI` as `--js`.
95
+ `FilledDataOptionDI` and `FilledListOptionDI` as `--js`. When the path is optional or comes from
96
+ elsewhere, call `load_schema(path)` and `ask_or_abort(schema, ask_data)` directly: both report
97
+ failures as typer errors.
83
98
 
84
99
  ## Development
85
100
 
@@ -89,5 +104,13 @@ uv run pytest
89
104
  uv run ruff check . && uv run mypy src
90
105
  ```
91
106
 
107
+ The demo GIFs are recorded with [VHS](https://github.com/charmbracelet/vhs) (`scoop install vhs`
108
+ or `brew install vhs`) and PowerShell (`pwsh`), from the repository root, `generate` first since
109
+ the others use its schema:
110
+
111
+ ```bash
112
+ vhs docs/demo/generate.tape && vhs docs/demo/fill.tape && vhs docs/demo/list.tape
113
+ ```
114
+
92
115
  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.
116
+ in [the schemify editor](https://github.com/aaltatan/schemify) so it stays in step.
@@ -11,7 +11,7 @@ schema updating as you type.
11
11
  ## Install
12
12
 
13
13
  ```bash
14
- uv tool install schemify
14
+ uv tool install schemify-cli
15
15
  ```
16
16
 
17
17
  ## Usage
@@ -22,6 +22,19 @@ schemify fill person.schema.json -o person.json # ask for one object matchin
22
22
  schemify list person.schema.json -f python # ask for a list of them
23
23
  ```
24
24
 
25
+ <!-- absolute URLs so the GIFs also show on PyPI -->
26
+ `generate`: a `Person` with a `full_name` computed by joining `first` and `last`
27
+
28
+ ![schemify generate](https://raw.githubusercontent.com/aaltatan/schemify-cli/main/docs/demo/generate.gif)
29
+
30
+ `fill`: one object, `full_name` is computed, not asked
31
+
32
+ ![schemify fill](https://raw.githubusercontent.com/aaltatan/schemify-cli/main/docs/demo/fill.gif)
33
+
34
+ `list`: several objects, printed as Python
35
+
36
+ ![schemify list](https://raw.githubusercontent.com/aaltatan/schemify-cli/main/docs/demo/list.gif)
37
+
25
38
  | Command | Alias | Does |
26
39
  |---|---|---|
27
40
  | `generate` | `g` | Asks for the properties of an object and builds its JSON Schema (Draft 2020-12) |
@@ -63,7 +76,9 @@ Built-in computers:
63
76
 
64
77
  `schemify.cli.dependencies.schema` exposes typer-di dependencies that load a schema file and ask
65
78
  for its data: `FilledDataArgumentDI` and `FilledListArgumentDI` take the path as an argument,
66
- `FilledDataOptionDI` and `FilledListOptionDI` as `--js`.
79
+ `FilledDataOptionDI` and `FilledListOptionDI` as `--js`. When the path is optional or comes from
80
+ elsewhere, call `load_schema(path)` and `ask_or_abort(schema, ask_data)` directly: both report
81
+ failures as typer errors.
67
82
 
68
83
  ## Development
69
84
 
@@ -73,5 +88,13 @@ uv run pytest
73
88
  uv run ruff check . && uv run mypy src
74
89
  ```
75
90
 
91
+ The demo GIFs are recorded with [VHS](https://github.com/charmbracelet/vhs) (`scoop install vhs`
92
+ or `brew install vhs`) and PowerShell (`pwsh`), from the repository root, `generate` first since
93
+ the others use its schema:
94
+
95
+ ```bash
96
+ vhs docs/demo/generate.tape && vhs docs/demo/fill.tape && vhs docs/demo/list.tape
97
+ ```
98
+
76
99
  After changing `generators.py`, `validators.py` or `computers.py`, run `pnpm sync && pnpm check`
77
- in [schemify-web](https://github.com/aaltatan/schemify-web) so it stays in step.
100
+ in [the schemify editor](https://github.com/aaltatan/schemify) so it stays in step.
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "schemify-cli"
3
- version = "0.1.0"
3
+ version = "0.2.0"
4
4
  description = "Build JSON Schemas interactively, and fill JSON data from them, from the command line."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12"
@@ -1,43 +1,43 @@
1
- [project]
2
- name = "schemify-cli"
3
- version = "0.1.0"
4
- description = "Build JSON Schemas interactively, and fill JSON data from them, from the command line."
5
- readme = "README.md"
6
- authors = [
7
- { name = "aaltatan", email = "a.altatan@gmail.com" }
8
- ]
9
- requires-python = ">=3.12"
10
- dependencies = [
11
- "jsonschema>=4.26.0",
12
- "num2words>=0.5.14",
13
- "questionary>=2.1.1",
14
- "rich>=15.0.0",
15
- "typer>=0.27.3",
16
- "typer-di>=0.1.5",
17
- "typer-swissknife>=0.1.1",
18
- ]
19
-
20
- [project.scripts]
21
- schemify = "schemify.cli.main:main"
22
-
23
- [build-system]
24
- requires = ["uv_build>=0.13.5,<0.14.0"]
25
- build-backend = "uv_build"
26
-
27
- [dependency-groups]
28
- dev = [
29
- "mypy>=2.4.1",
30
- "pytest>=9.2.0",
31
- "ruff>=0.17.3",
32
- "types-jsonschema>=4.26.0.20261006",
33
- ]
34
-
35
- [tool.pytest.ini_options]
36
- testpaths = ["tests"]
37
-
38
- [[tool.mypy.overrides]]
39
- module = ["typer_swissknife", "num2words"]
40
- ignore_missing_imports = true
41
-
42
- [tool.uv.build-backend]
43
- module-name = "schemify"
1
+ [project]
2
+ name = "schemify-cli"
3
+ version = "0.2.0"
4
+ description = "Build JSON Schemas interactively, and fill JSON data from them, from the command line."
5
+ readme = "README.md"
6
+ authors = [
7
+ { name = "aaltatan", email = "a.altatan@gmail.com" }
8
+ ]
9
+ requires-python = ">=3.12"
10
+ dependencies = [
11
+ "jsonschema>=4.26.0",
12
+ "num2words>=0.5.14",
13
+ "questionary>=2.1.1",
14
+ "rich>=15.0.0",
15
+ "typer>=0.27.3",
16
+ "typer-di>=0.1.5",
17
+ "typer-swissknife>=0.1.1",
18
+ ]
19
+
20
+ [project.scripts]
21
+ schemify = "schemify.cli.main:main"
22
+
23
+ [build-system]
24
+ requires = ["uv_build>=0.13.5,<0.14.0"]
25
+ build-backend = "uv_build"
26
+
27
+ [dependency-groups]
28
+ dev = [
29
+ "mypy>=2.4.1",
30
+ "pytest>=9.2.0",
31
+ "ruff>=0.17.3",
32
+ "types-jsonschema>=4.26.0.20261006",
33
+ ]
34
+
35
+ [tool.pytest.ini_options]
36
+ testpaths = ["tests"]
37
+
38
+ [[tool.mypy.overrides]]
39
+ module = ["typer_swissknife", "num2words"]
40
+ ignore_missing_imports = true
41
+
42
+ [tool.uv.build-backend]
43
+ module-name = "schemify"
@@ -4,15 +4,20 @@ from .computers import ComputeError, compute, computers
4
4
  from .fillers import InvalidDataError, ask_data, ask_list, fillers
5
5
  from .formatters import formatters
6
6
  from .generators import ask_schema, generators
7
+ from .models import Schema
7
8
  from .registry import NotSupportedError
9
+ from .validators import check_schema, check_value
8
10
 
9
11
  __all__ = [
10
12
  "ComputeError",
11
13
  "InvalidDataError",
12
14
  "NotSupportedError",
15
+ "Schema",
13
16
  "ask_data",
14
17
  "ask_list",
15
18
  "ask_schema",
19
+ "check_schema",
20
+ "check_value",
16
21
  "compute",
17
22
  "computers",
18
23
  "fillers",
@@ -1 +1 @@
1
- """The schemify command line interface."""
1
+ """The schemify command line interface."""
@@ -1,4 +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
- """
1
+ """Typer dependencies shared by the commands, injected with typer-di.
2
+
3
+ They are importable by other typer-di apps, like templify.
4
+ """
@@ -1,9 +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."""
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."""
@@ -1,128 +1,145 @@
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."""
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
+ """Read a valid JSON Schema from `path`, a bad file is reported as a usage error.
41
+
42
+ Raises:
43
+ typer.BadParameter: The file is not JSON, or is not a valid JSON Schema.
44
+
45
+ """
46
+ try:
47
+ schema = json.loads(path.read_text(encoding="utf-8"))
48
+ except (json.JSONDecodeError, UnicodeDecodeError) as e:
49
+ msg = f"Could not read schema file '{path.name}': {e}"
50
+ raise typer.BadParameter(msg) from e
51
+
52
+ error = check_schema(schema)
53
+
54
+ if error is not None:
55
+ msg = f"Invalid schema file '{path.name}': {error}"
56
+ raise typer.BadParameter(msg)
57
+
58
+ return schema
59
+
60
+
61
+ def _schema_from_option(
62
+ path: Annotated[
63
+ Path,
64
+ typer.Option(
65
+ "--js",
66
+ "--json-schema",
67
+ help=_PATH_HELP,
68
+ exists=True,
69
+ file_okay=True,
70
+ dir_okay=False,
71
+ resolve_path=True,
72
+ ),
73
+ ],
74
+ ) -> Schema:
75
+ return load_schema(path)
76
+
77
+
78
+ def _schema_from_argument(
79
+ path: Annotated[
80
+ Path,
81
+ typer.Argument(
82
+ help=_PATH_HELP,
83
+ exists=True,
84
+ file_okay=True,
85
+ dir_okay=False,
86
+ resolve_path=True,
87
+ ),
88
+ ],
89
+ ) -> Schema:
90
+ return load_schema(path)
91
+
92
+
93
+ # -----------------------
94
+ # filled data
95
+ # -----------------------
96
+
97
+
98
+ def ask_or_abort[T](schema: Schema, ask: Callable[[Schema], T] = ask_data) -> T:
99
+ """Ask for data with `ask`, reporting failures the way a typer app expects.
100
+
101
+ Raises:
102
+ typer.BadParameter: The schema can not be filled, or the data does not match it.
103
+ typer.Abort: The user pressed Ctrl+C.
104
+
105
+ """
106
+ try:
107
+ return ask(schema)
108
+ except (NotSupportedError, InvalidDataError) as e:
109
+ raise typer.BadParameter(str(e)) from e
110
+ except KeyboardInterrupt:
111
+ raise typer.Abort from None
112
+
113
+
114
+ def _filled_data(
115
+ read_schema: Callable[..., Schema], ask: Callable[[Schema], Any] = ask_data
116
+ ) -> Callable[..., Any]:
117
+ """Build a dependency asking (with `ask`) for data matching the schema `read_schema` injects."""
118
+
119
+ def fill(schema: Annotated[Schema, Depends(read_schema)]) -> Any:
120
+ return ask_or_abort(schema, ask)
121
+
122
+ return fill
123
+
124
+
125
+ # -----------------------
126
+ # DI
127
+ # -----------------------
128
+
129
+ SchemaOptionDI = Annotated[Schema, Depends(_schema_from_option)]
130
+ """Injects the valid JSON Schema read from the `--js` / `--json-schema` option."""
131
+
132
+ SchemaArgumentDI = Annotated[Schema, Depends(_schema_from_argument)]
133
+ """Injects the valid JSON Schema read from the `PATH` argument."""
134
+
135
+ FilledDataOptionDI = Annotated[Any, Depends(_filled_data(_schema_from_option))]
136
+ """Injects the data the user entered, matching the `--js` / `--json-schema` file."""
137
+
138
+ FilledDataArgumentDI = Annotated[Any, Depends(_filled_data(_schema_from_argument))]
139
+ """Injects the data the user entered, matching the `PATH` argument file."""
140
+
141
+ FilledListOptionDI = Annotated[list[Any], Depends(_filled_data(_schema_from_option, ask_list))]
142
+ """Injects the list of items the user entered, each matching the `--js` / `--json-schema` file."""
143
+
144
+ FilledListArgumentDI = Annotated[list[Any], Depends(_filled_data(_schema_from_argument, ask_list))]
145
+ """Injects the list of items the user entered, each matching the `PATH` argument file."""