Skip to content

Commit 31a2600

Browse files
committed
CCM-12896: Generate Pydantic models programmatically
1 parent 607d6a5 commit 31a2600

File tree

5 files changed

+80
-66
lines changed

5 files changed

+80
-66
lines changed

src/python-schema-generator/src/file_utils.py

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,17 +24,29 @@ def list_json_schemas(schema_dir: str) -> list[str]:
2424
return [f.name for f in flattened_schema_files]
2525

2626

27-
def load_json_schema(schema_path: str) -> dict[str, Any]:
28-
"""Load a JSON schema from file.
27+
def load_json_schema(schema_path: str) -> str:
28+
"""Load a JSON schema from file as a string.
2929
3030
Args:
3131
schema_path: Path to the JSON schema file
3232
3333
Returns:
34-
Loaded schema as dictionary
34+
Loaded schema as string
3535
"""
3636
with open(schema_path, encoding="utf-8") as f:
37-
return json.load(f)
37+
return f.read()
38+
39+
40+
def parse_json_schema(schema: str) -> dict[str, Any]:
41+
"""Parse a JSON schema from a string.
42+
43+
Args:
44+
schema: JSON schema as a string
45+
46+
Returns:
47+
Schema as dictionary
48+
"""
49+
return json.loads(schema)
3850

3951

4052
def write_init_file(output_dir: str, model_names: list[str]) -> None:

src/python-schema-generator/src/generate_models.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from file_utils import (
1414
list_json_schemas,
1515
load_json_schema,
16+
parse_json_schema,
1617
model_name_to_module_name,
1718
write_init_file,
1819
)
@@ -62,14 +63,15 @@ def main() -> int:
6263
generated_models = []
6364
for schema_filename in schema_filenames:
6465
schema_path = str(Path(args.input_dir) / schema_filename)
65-
schema = load_json_schema(schema_path)
66+
string_schema = load_json_schema(schema_path)
67+
schema = parse_json_schema(string_schema)
6668

6769
model_name = extract_model_name(schema)
6870
output_filename = model_name_to_module_name(model_name) + ".py"
69-
output_file_path = str(Path(args.output_dir) / output_filename)
71+
output_file_path = Path(args.output_dir) / output_filename
7072

7173
generate_pydantic_model(
72-
schema_path, output_file_path, model_name
74+
string_schema, output_file_path, model_name
7375
)
7476

7577
generated_models.append(model_name)
Lines changed: 11 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
11
"""Model generator using datamodel-code-generator."""
22

3-
import subprocess
4-
import sys
53
from pathlib import Path
4+
from datamodel_code_generator import DataModelType, InputFileType, generate
65

76

87

98
def generate_pydantic_model(
10-
schema_path: str, output_file: str, class_name: str
9+
schema: str, output_file_path: Path, class_name: str
1110
) -> None:
1211
"""Generate a Pydantic model from a JSON schema.
1312
@@ -19,32 +18,18 @@ def generate_pydantic_model(
1918
Raises:
2019
RuntimeError: If model generation fails
2120
"""
22-
datamodel_cmd = str(Path(sys.executable).parent / "datamodel-codegen")
23-
cmd = [
24-
datamodel_cmd,
25-
"--input",
26-
schema_path,
27-
"--output",
28-
output_file,
29-
"--class-name",
30-
class_name,
31-
"--input-file-type",
32-
"jsonschema",
33-
"--output-model-type",
34-
"pydantic_v2.BaseModel",
35-
"--use-schema-description",
36-
"--custom-file-header",
37-
'''"""Generated Pydantic model for NHS Notify Digital Letters events.
21+
22+
generate(
23+
schema,
24+
input_file_type=InputFileType.JsonSchema,
25+
output=output_file_path,
26+
output_model_type=DataModelType.PydanticV2BaseModel,
27+
class_name=class_name,
28+
use_schema_description=True,
29+
custom_file_header='''"""Generated Pydantic model for NHS Notify Digital Letters events.
3830
3931
This file is auto-generated. Do not edit manually.
4032
"""
4133
4234
'''
43-
]
44-
result = subprocess.run(
45-
cmd, capture_output=True, text=True, check=False, encoding="utf-8"
4635
)
47-
48-
if result.returncode != 0:
49-
error_msg = f"Failed to generate model: {result.stderr}"
50-
raise RuntimeError(error_msg)

src/python-schema-generator/tests/test_file_utils.py

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from src.file_utils import (
88
list_json_schemas,
99
load_json_schema,
10+
parse_json_schema,
1011
write_init_file,
1112
model_name_to_module_name
1213
)
@@ -51,21 +52,31 @@ class TestLoadJsonSchema:
5152
def test_loads_valid_json_schema(self, tmp_path):
5253
"""Test loading a valid JSON schema."""
5354
schema_file = tmp_path / "test.schema.json"
54-
schema_content = {"title": "TestSchema", "type": "object"}
55-
schema_file.write_text(json.dumps(schema_content))
55+
schema_content = json.dumps({"title": "TestSchema", "type": "object"})
56+
schema_file.write_text(schema_content)
5657

5758
result = load_json_schema(str(schema_file))
5859

5960
assert result == schema_content
6061

61-
def test_raises_error_for_invalid_json(self, tmp_path):
62+
63+
class TestParseJsonSchema:
64+
"""Tests for parse_json_schema function."""
65+
66+
def test_parses_valid_json_schema(self):
67+
"""Test loading a valid JSON schema."""
68+
schema_content = { "title": "TestSchema", "type": "object" }
69+
70+
result = parse_json_schema(json.dumps(schema_content))
71+
72+
assert result == schema_content
73+
74+
def test_raises_error_for_invalid_json(self):
6275
"""Test that it raises error for invalid JSON."""
63-
schema_file = tmp_path / "invalid.json"
64-
schema_file.write_text("not valid json")
76+
invalid_json = "not valid json"
6577

6678
with pytest.raises(json.JSONDecodeError):
67-
load_json_schema(str(schema_file))
68-
79+
parse_json_schema(invalid_json)
6980

7081
class TestWriteInitFile:
7182
"""Tests for write_init_file function."""
Lines changed: 30 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,49 @@
11
"""Tests for model_generator module."""
22

3-
from unittest.mock import MagicMock, patch
3+
from pathlib import Path
4+
from unittest.mock import patch
45

56
import pytest
7+
from datamodel_code_generator import DataModelType, InputFileType
68

79
from src.model_generator import generate_pydantic_model
810

911

1012
class TestGeneratePydanticModel:
1113
"""Tests for generate_pydantic_model function."""
1214

13-
@patch("src.model_generator.subprocess.run")
14-
def test_calls_datamodel_codegen_with_expected_arguments(self, mock_run):
15+
@patch("src.model_generator.generate")
16+
def test_calls_datamodel_codegen_with_expected_arguments(self, mock_generate):
1517
"""Test successful model generation."""
1618
# Arrange
17-
schema_path = "test_model.json"
18-
output_file = "test_model.py"
19-
mock_run.return_value = MagicMock(returncode=0, stderr="")
19+
schema = '{"type": "object", "properties": {"name": {"type": "string"}}}'
20+
output_file = Path("test_model.py")
2021

2122
# Act
22-
generate_pydantic_model(schema_path, output_file, "TestModel")
23+
generate_pydantic_model(schema, output_file, "TestModel")
2324

2425
# Assert
25-
assert mock_run.called
26-
cmd_args = mock_run.call_args[0][0]
27-
assert "datamodel-codegen" in cmd_args[0] # First arg is the executable
28-
assert "--input" in cmd_args[1]
29-
assert schema_path in cmd_args[2]
30-
assert "--output" in cmd_args[3]
31-
assert output_file in cmd_args[4]
32-
assert "--class-name" in cmd_args[5]
33-
assert "TestModel" in cmd_args[6]
34-
35-
@patch("src.model_generator.subprocess.run")
36-
def test_raises_error_on_generation_failure(self, mock_run):
37-
"""Test that it raises error when generation fails."""
38-
schema_path = "test_model.json"
39-
output_file = "test_model.py"
40-
mock_run.return_value = MagicMock(
41-
returncode=1, stderr="Error: Invalid schema"
26+
mock_generate.assert_called_once_with(
27+
schema,
28+
input_file_type=InputFileType.JsonSchema,
29+
output=output_file,
30+
output_model_type=DataModelType.PydanticV2BaseModel,
31+
class_name="TestModel",
32+
use_schema_description=True,
33+
custom_file_header='''"""Generated Pydantic model for NHS Notify Digital Letters events.
34+
35+
This file is auto-generated. Do not edit manually.
36+
"""
37+
38+
'''
4239
)
4340

44-
with pytest.raises(RuntimeError, match="Failed to generate model"):
45-
generate_pydantic_model(schema_path, output_file, "TestModel")
41+
@patch("src.model_generator.generate")
42+
def test_raises_error_on_generation_failure(self, mock_generate):
43+
"""Test that it raises error when generation fails."""
44+
schema = '{"type": "object", "properties": {"name": {"type": "string"}}}'
45+
output_file = Path("test_model.py")
46+
mock_generate.side_effect = RuntimeError("Invalid schema")
47+
48+
with pytest.raises(RuntimeError, match="Invalid schema"):
49+
generate_pydantic_model(schema, output_file, "TestModel")

0 commit comments

Comments
 (0)