Skip to content

Commit 73fa98e

Browse files
fbruecksvdimchenko
andauthored
feat: Add flag to not use alias for Pydantic’s json schema generation (#9)
* feat: Add flag to not use alias for json schema generation --------- Co-authored-by: Serhii Dimchenko <[email protected]>
1 parent 7363b35 commit 73fa98e

File tree

8 files changed

+149
-7
lines changed

8 files changed

+149
-7
lines changed

.gitignore

+3
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,6 @@ __pycache__/
1212

1313
# Poetry
1414
poetry.lock
15+
16+
# Temporary
17+
tests/tmp

README.md

+51
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,16 @@
11
# JSON Schema to AWS Glue schema converter
22

3+
<!-- TOC -->
4+
* [JSON Schema to AWS Glue schema converter](#json-schema-to-aws-glue-schema-converter)
5+
* [Installation](#installation)
6+
* [What?](#what)
7+
* [Why?](#why)
8+
* [Example](#example)
9+
* [Override the type for the AWS Glue Schema](#override-the-type-for-the-aws-glue-schema)
10+
* [How it works?](#how-it-works)
11+
* [Future work](#future-work)
12+
<!-- TOC -->
13+
314
## Installation
415

516
```bash
@@ -100,6 +111,46 @@ Alternatively you can run CLI with `-o` flag to set output file location:
100111
pydantic-glue -f example.py -c Foo -o example.json -l
101112
```
102113

114+
If your Pydantic models use field aliases, but you prefer to display the field names in the JSON schema,
115+
you can enable this behavior by using the `--schema-by-name` flag.
116+
117+
Here you can find the details regarding [pydantic aliases](https://docs.pydantic.dev/latest/concepts/alias/).
118+
119+
The following model will be converted differently with `--schema-by-name` argument.
120+
121+
```python
122+
from pydantic import BaseModel, Field
123+
124+
class A(BaseModel):
125+
hey: str = Field(alias="h")
126+
ho: str
127+
```
128+
129+
```bash
130+
pydantic-glue -f tests/data/input.py -c A
131+
132+
2025-02-01 00:08:45,046 - INFO - Generated file content:
133+
{
134+
"//": "Generated by pydantic-glue at 2025-01-31 23:08:45.046012+00:00. DO NOT EDIT",
135+
"columns": {
136+
"h": "string",
137+
"ho": "string"
138+
}
139+
}
140+
```
141+
142+
```bash
143+
pydantic-glue -f tests/data/input.py -c A --schema-by-name
144+
2025-02-01 00:09:18,381 - INFO - Generated file content:
145+
{
146+
"//": "Generated by pydantic-glue at 2025-01-31 23:09:18.380586+00:00. DO NOT EDIT",
147+
"columns": {
148+
"hey": "string",
149+
"ho": "string"
150+
}
151+
}
152+
```
153+
103154
## Override the type for the AWS Glue Schema
104155

105156
Wherever there is a `type` key in the input JSON Schema, an additional key `glue_type` may be

pydantic_glue/cli.py

+34-7
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import os
44
import sys
55
from argparse import ArgumentParser
6+
from dataclasses import dataclass
67
from datetime import datetime, timezone
78
from pathlib import Path
89

@@ -12,24 +13,50 @@
1213
log = logging.getLogger(__name__)
1314

1415

15-
def cli() -> None:
16+
@dataclass
17+
class Arguments:
18+
module_file: str
19+
class_name: str
20+
output_file: str
21+
log_result: bool
22+
json_schema_by_alias: bool
23+
24+
25+
def parse_args(argv: list[str]) -> Arguments:
1626
parser = ArgumentParser()
1727
parser.add_argument("-f", dest="source_file", required=True, type=str, help="Path to the python file")
1828
parser.add_argument("-c", dest="class_name", required=True, type=str, help="Python class name")
1929
parser.add_argument("-o", dest="output_file", required=False, type=str, help="Path to the output file")
2030
parser.add_argument(
2131
"-l", dest="log_result", action="store_true", default=False, help="Flag if need to print result in log"
2232
)
23-
args = parser.parse_args(sys.argv[1:])
33+
parser.add_argument(
34+
"--schema-by-name",
35+
dest="json_schema_by_alias",
36+
action="store_false",
37+
default=True,
38+
help="Flag to not use name for json schema generation",
39+
)
40+
args = parser.parse_args(argv)
41+
42+
return Arguments(
43+
module_file=args.source_file.removesuffix(".py"),
44+
class_name=args.class_name,
45+
output_file=args.output_file,
46+
log_result=args.log_result,
47+
json_schema_by_alias=args.json_schema_by_alias,
48+
)
2449

25-
module_file = args.source_file.removesuffix(".py")
2650

27-
sys.path.append(os.path.dirname(module_file))
51+
def cli() -> None:
52+
args = parse_args(sys.argv[1:])
53+
sys.path.append(os.path.dirname(args.module_file))
54+
imported = __import__(os.path.basename(args.module_file))
2855

29-
imported = __import__(os.path.basename(module_file))
30-
imported = getattr(imported, args.class_name)
31-
input_schema = json.dumps(imported.model_json_schema())
56+
model = getattr(imported, args.class_name)
57+
input_schema = json.dumps(model.model_json_schema(by_alias=args.json_schema_by_alias))
3258
converted = convert(input_schema)
59+
3360
output = json.dumps(
3461
{
3562
"//": f"Generated by pydantic-glue at {datetime.now(tz=timezone.utc)}. DO NOT EDIT",

pyproject.toml

+1
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ flake8 = "^7.0.0"
2828
black = "^24.4.2"
2929
isort = "^5.13.2"
3030
pre-commit = "^3.7.0"
31+
cli-test-helpers = "^4.1.0"
3132

3233
[tool.autoflake]
3334
recursive = true

tests/data/expected.json

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"//": "Generated by pydantic-glue at 2025-01-31 22:51:59.027907+00:00. DO NOT EDIT",
3+
"columns": {
4+
"h": "string",
5+
"ho": "string"
6+
}
7+
}

tests/data/expected_by_name.json

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"//": "Generated by pydantic-glue at 2025-01-31 22:51:59.027907+00:00. DO NOT EDIT",
3+
"columns": {
4+
"hey": "string",
5+
"ho": "string"
6+
}
7+
}

tests/data/input.py

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
from pydantic import BaseModel, Field
2+
3+
4+
class A(BaseModel):
5+
hey: str = Field(alias="h")
6+
ho: str

tests/unit/test_cli.py

+40
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import json
2+
from pathlib import Path
3+
from unittest import TestCase
4+
5+
from cli_test_helpers import shell
6+
from pydantic_glue.cli import parse_args
7+
8+
9+
def test_parse_args():
10+
11+
args = parse_args(["-f", "foo/bar/file.py", "-c", "Test"])
12+
assert args.module_file == "foo/bar/file"
13+
assert args.class_name == "Test"
14+
assert args.output_file is None
15+
assert args.log_result is False
16+
assert args.json_schema_by_alias is True
17+
18+
19+
def test_parse_args_schema_by_name():
20+
21+
args = parse_args(["-f", "foo/bar/file.py", "-c", "Test", "--schema-by-name"])
22+
assert args.module_file == "foo/bar/file"
23+
assert args.class_name == "Test"
24+
assert args.output_file is None
25+
assert args.log_result is False
26+
assert args.json_schema_by_alias is False
27+
28+
29+
def test_cli():
30+
shell("pydantic-glue -f tests/data/input.py -c A -o tests/tmp/actual.json")
31+
actual = json.loads(Path("tests/tmp/actual.json").read_text())
32+
expected = json.loads(Path("tests/data/expected.json").read_text())
33+
TestCase().assertDictEqual(actual["columns"], expected["columns"])
34+
35+
36+
def test_cli_schema_by_name():
37+
shell("pydantic-glue -f tests/data/input.py -c A -o tests/tmp/actual_by_name.json --schema-by-name")
38+
actual = json.loads(Path("tests/tmp/actual_by_name.json").read_text())
39+
expected = json.loads(Path("tests/data/expected_by_name.json").read_text())
40+
TestCase().assertDictEqual(actual["columns"], expected["columns"])

0 commit comments

Comments
 (0)