Skip to content

Commit 3a3f69f

Browse files
authored
feat(py): generate schemas.py from genkit-schema.json using datamodel-codegen #1807 (#1808)
ISSUE: #1807 CHANGELOG: - [ ] Add a `bin/generate_schema_types` script that generates the Pydantic `schemas.py` module. - [ ] Not updating the pre-commit hooks since the generated file requires manual patching. - [ ] Remove timestamp to ensure we do not treat a file with identical content differently preventing the hassles of updating this file per commit.
1 parent 889e309 commit 3a3f69f

File tree

10 files changed

+566
-216
lines changed

10 files changed

+566
-216
lines changed

py/__init__.py

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# Copyright 2025 Google LLC
2+
# SPDX-License-Identifier: Apache-2.0
3+

py/bin/generate_schema_types

+35
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
#!/usr/bin/env bash
2+
#
3+
# Copyright 2025 Google LLC
4+
# SPDX-License-Identifier: Apache-2.0
5+
6+
set -euo pipefail
7+
8+
TOP_DIR=$(git rev-parse --show-toplevel)
9+
SCHEMA_FILE="$TOP_DIR/py/packages/genkit/src/genkit/core/schemas.py"
10+
11+
# Generate types using configuration from pyproject.toml
12+
uv run --directory "$TOP_DIR/py" datamodel-codegen
13+
14+
# This isn't causing runtime errors at the moment so letting it be.
15+
#sed -i '' '/^class Model(RootModel\[Any\]):$/,/^ root: Any$/d' "$SCHEMA_FILE"
16+
17+
# Sanitize the generated schema.
18+
python3 "${TOP_DIR}/py/bin/sanitize_schemas.py" "$SCHEMA_FILE"
19+
20+
# Add a generated by `generate_schema_types` comment.
21+
sed -i '' '1i\
22+
# DO NOT EDIT: Generated by `generate_schema_types` from `genkit-schemas.json`.
23+
' "$SCHEMA_FILE"
24+
25+
# Add license header.
26+
addlicense \
27+
-c "Google LLC" \
28+
-s=only \
29+
"$SCHEMA_FILE"
30+
31+
# Checks and formatting.
32+
uv run --directory "$TOP_DIR/py" \
33+
ruff check --fix "$SCHEMA_FILE"
34+
uv run --directory "$TOP_DIR/py" \
35+
ruff format "$SCHEMA_FILE"

py/bin/sanitize_schemas.py

+85
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
#!/usr/bin/env python3
2+
#
3+
# Copyright 2025 Google LLC
4+
# SPDX-License-Identifier: Apache-2.0
5+
6+
import ast
7+
import sys
8+
9+
10+
class ModelConfigRemover(ast.NodeTransformer):
11+
def __init__(self) -> None:
12+
self.modified = False
13+
14+
def is_rootmodel_class(self, node: ast.ClassDef) -> bool:
15+
"""Check if the class inherits from RootModel."""
16+
for base in node.bases:
17+
if isinstance(base, ast.Name) and base.id == 'RootModel':
18+
return True
19+
elif isinstance(base, ast.Subscript):
20+
if (
21+
isinstance(base.value, ast.Name)
22+
and base.value.id == 'RootModel'
23+
):
24+
return True
25+
return False
26+
27+
def visit_ClassDef(self, node: ast.ClassDef) -> ast.ClassDef:
28+
"""Visit class definitions and remove model_config if class inherits from RootModel."""
29+
if self.is_rootmodel_class(node):
30+
# Filter out model_config assignments
31+
new_body = []
32+
for item in node.body:
33+
if isinstance(item, ast.Assign):
34+
targets = item.targets
35+
if len(targets) == 1 and isinstance(targets[0], ast.Name):
36+
if targets[0].id != 'model_config':
37+
new_body.append(item)
38+
else:
39+
new_body.append(item)
40+
41+
if len(new_body) != len(node.body):
42+
self.modified = True
43+
44+
node.body = new_body
45+
46+
return node
47+
48+
49+
def process_file(filename: str) -> None:
50+
"""Process a Python file to remove model_config from RootModel classes."""
51+
with open(filename, 'r') as f:
52+
source = f.read()
53+
54+
tree = ast.parse(source)
55+
56+
transformer = ModelConfigRemover()
57+
modified_tree = transformer.visit(tree)
58+
59+
if transformer.modified:
60+
ast.fix_missing_locations(modified_tree)
61+
modified_source = ast.unparse(modified_tree)
62+
with open(filename, 'w') as f:
63+
f.write(modified_source)
64+
print(
65+
f'Modified {filename}: Removed model_config from RootModel classes'
66+
)
67+
else:
68+
print(f'No modifications needed in {filename}')
69+
70+
71+
def main() -> None:
72+
if len(sys.argv) != 2:
73+
print('Usage: python script.py <filename>')
74+
sys.exit(1)
75+
76+
filename = sys.argv[1]
77+
try:
78+
process_file(filename)
79+
except Exception as e:
80+
print(f'Error processing {filename}: {str(e)}')
81+
sys.exit(1)
82+
83+
84+
if __name__ == '__main__':
85+
main()

py/captainhook.json

+6
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@
3333
{
3434
"run": "pnpm i --frozen-lockfile"
3535
},
36+
{
37+
"run": "py/bin/generate_schema_types"
38+
},
3639
{
3740
"run": "py/bin/fmt"
3841
},
@@ -81,6 +84,9 @@
8184
{
8285
"run": "pnpm i --frozen-lockfile"
8386
},
87+
{
88+
"run": "py/bin/generate_schema_types"
89+
},
8490
{
8591
"run": "py/bin/fmt"
8692
},

0 commit comments

Comments
 (0)