schemify-cli 0.1.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.
@@ -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,77 @@
1
+ # schemify-cli
2
+
3
+ Build JSON Schemas interactively, and fill JSON data from them, from the command line.
4
+
5
+ Source: [github.com/aaltatan/schemify-cli](https://github.com/aaltatan/schemify-cli)
6
+
7
+ A web version runs at [schemify.surge.sh](https://schemify.surge.sh)
8
+ ([source](https://github.com/aaltatan/schemify)): the same questions as fields, with the
9
+ schema updating as you type.
10
+
11
+ ## Install
12
+
13
+ ```bash
14
+ uv tool install schemify
15
+ ```
16
+
17
+ ## Usage
18
+
19
+ ```bash
20
+ schemify generate -t Person -o person.schema.json # ask for the properties, write the schema
21
+ schemify fill person.schema.json -o person.json # ask for one object matching it
22
+ schemify list person.schema.json -f python # ask for a list of them
23
+ ```
24
+
25
+ | Command | Alias | Does |
26
+ |---|---|---|
27
+ | `generate` | `g` | Asks for the properties of an object and builds its JSON Schema (Draft 2020-12) |
28
+ | `fill` | `f` | Asks for data matching a schema file, checked against it |
29
+ | `list` | `l` | Asks for a list of items, each matching the schema |
30
+
31
+ - `-o, --output` saves to a file (asks before overwriting); without it the result is printed.
32
+ - `-f, --format` picks the output of `fill` and `list`: `json`, `python` or `javascript`.
33
+
34
+ Types: `string`, `integer`, `number`, `boolean`, `date`, `datetime`, `enum`, `array`, `object`,
35
+ each with its constraints (length, pattern, format, range, items, ...) and an optional default.
36
+
37
+ ## Computed properties
38
+
39
+ A property with `x-compute` is not asked in `fill`, it is computed from the other values:
40
+
41
+ ```json
42
+ "full_name": {"type": "string", "x-compute": {"fn": "join", "from": ["first", "middle?", "last"]}}
43
+ ```
44
+
45
+ - `fn`: the computer, `from`: source paths (a list, or an `{alias: path}` object for `template`
46
+ and `expr`), `options`: extra arguments, `default`: the value when a source is missing.
47
+ - Paths are relative to the owning object (`address.city`, `items[0]`), or to the root
48
+ (`$.company`). `items[*].price` gives every price; a trailing `?` makes a source optional.
49
+
50
+ Built-in computers:
51
+
52
+ - **Text**: join, concat, template, upper, lower, title, capitalize, trim, replace, slice,
53
+ initials, slug, length, pad, mask
54
+ - **Numbers**: sum, subtract, multiply, divide, average, min, max, abs, round, percent,
55
+ percent_of, expr
56
+ - **Dates**: today, now, add_days, add_months, add_years, days_between, age, year, month, day,
57
+ weekday, format_date, start_of_month, end_of_month
58
+ - **Number to words**: words
59
+ - **Logic, lookup and misc**: coalesce, if_else, equals, lookup, constant, count, count_if,
60
+ unique, uuid
61
+
62
+ ## Reusing it in a typer app
63
+
64
+ `schemify.cli.dependencies.schema` exposes typer-di dependencies that load a schema file and ask
65
+ for its data: `FilledDataArgumentDI` and `FilledListArgumentDI` take the path as an argument,
66
+ `FilledDataOptionDI` and `FilledListOptionDI` as `--js`.
67
+
68
+ ## Development
69
+
70
+ ```bash
71
+ uv sync
72
+ uv run pytest
73
+ uv run ruff check . && uv run mypy src
74
+ ```
75
+
76
+ 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.
@@ -0,0 +1,47 @@
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
+ requires-python = ">=3.12"
7
+ dependencies = [
8
+ "jsonschema>=4.26.0",
9
+ "num2words>=0.5.14",
10
+ "questionary>=2.1.1",
11
+ "rich>=15.0.0",
12
+ "typer>=0.27.3",
13
+ "typer-di>=0.1.5",
14
+ "typer-swissknife>=0.1.1",
15
+ ]
16
+
17
+ [[project.authors]]
18
+ name = "aaltatan"
19
+ email = "a.altatan@gmail.com"
20
+
21
+ [project.scripts]
22
+ schemify = "schemify.cli.main:main"
23
+
24
+ [build-system]
25
+ requires = ["uv_build>=0.13.5,<0.14.0"]
26
+ build-backend = "uv_build"
27
+
28
+ [dependency-groups]
29
+ dev = [
30
+ "mypy>=2.4.1",
31
+ "pytest>=9.2.0",
32
+ "ruff>=0.17.3",
33
+ "types-jsonschema>=4.26.0.20261006",
34
+ ]
35
+
36
+ [tool.pytest.ini_options]
37
+ testpaths = ["tests"]
38
+
39
+ [[tool.mypy.overrides]]
40
+ module = [
41
+ "typer_swissknife",
42
+ "num2words",
43
+ ]
44
+ ignore_missing_imports = true
45
+
46
+ [tool.uv.build-backend]
47
+ module-name = "schemify"
@@ -0,0 +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"
@@ -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."""
@@ -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()