modelgen 0.0.1__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,7 @@
1
+ Metadata-Version: 2.4
2
+ Name: modelgen
3
+ Version: 0.0.1
4
+ Summary: Add your description here
5
+ Requires-Python: >=3.12
6
+ Description-Content-Type: text/markdown
7
+ Requires-Dist: datamodel-code-generator>=0.33.0
File without changes
@@ -0,0 +1,27 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "modelgen"
7
+ version = "0.0.1"
8
+ description = "Add your description here"
9
+ readme = "README.md"
10
+ requires-python = ">=3.12"
11
+ dependencies = [
12
+ "datamodel-code-generator>=0.33.0",
13
+ ]
14
+
15
+ [dependency-groups]
16
+ dev = [
17
+ "bandit>=1.8.6",
18
+ "mypy>=1.18.1",
19
+ "nox>=2025.5.1",
20
+ "pydantic>=2.11.7",
21
+ "pytest>=8.4.2",
22
+ "pytest-cov>=7.0.0",
23
+ "ruff>=0.13.0",
24
+ ]
25
+
26
+ [tool.ruff.lint]
27
+ select = ["I"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,3 @@
1
+ from .code_generator import CodeGenerator
2
+
3
+ __all__ = ["CodeGenerator"]
@@ -0,0 +1,113 @@
1
+ import ast
2
+ import os
3
+ import re
4
+ import subprocess # nosec B404
5
+ from copy import copy
6
+ from pathlib import Path
7
+ from typing import List
8
+
9
+
10
+ class CodeGenerator:
11
+ output_dir: str
12
+ _source_code: str
13
+ _imports: list[ast.Import | ast.ImportFrom]
14
+ _classes: list[ast.ClassDef]
15
+
16
+ def __init__(self, openapi_file_name: str, output_dir: str):
17
+ self.output_dir = output_dir
18
+ temporary_filename: str = "temporary_model.py"
19
+ temporary_filepath = os.path.join(output_dir, temporary_filename)
20
+ self._generate_temporary_file(temporary_filepath, openapi_file_name)
21
+ source_code = self._import_temporary_file(temporary_filepath)
22
+ self._imports = self._extract_imports(source_code)
23
+ self._classes = self._extract_classes(source_code)
24
+ self._source_code = source_code
25
+ os.remove(temporary_filepath)
26
+
27
+ def filter_import_node(
28
+ self, import_node: ast.Import | ast.ImportFrom, used_imports: set[str]
29
+ ):
30
+ new_node = copy(import_node)
31
+ new_node.names = []
32
+ if names := [
33
+ name
34
+ for name in import_node.names
35
+ if (name.asname if name.asname else name.name) in used_imports
36
+ ]:
37
+ new_node.names = names
38
+
39
+ return new_node if new_node.names else None
40
+
41
+ def execute(self):
42
+ os.makedirs(self.output_dir, exist_ok=True)
43
+
44
+ for class_node in self._classes:
45
+ used_imports = self._get_imports_for_class(class_node)
46
+ import_nodes = [
47
+ self.filter_import_node(import_node, used_imports)
48
+ for import_node in self._imports
49
+ ]
50
+ class_source_code = ast.get_source_segment(self._source_code, class_node)
51
+ output_path = os.path.join(
52
+ self.output_dir, f"{self._convert_to_snake_case(class_node.name)}.py"
53
+ )
54
+
55
+ if not class_source_code:
56
+ continue
57
+
58
+ with open(output_path, "w", encoding="utf-8") as f:
59
+ f.write("\n".join([ast.unparse(node) for node in import_nodes if node]))
60
+ f.write("\n\n\n")
61
+ f.write(class_source_code)
62
+ f.write("\n")
63
+
64
+ def _get_imports_for_class(self, class_node: ast.ClassDef):
65
+ used_names = set()
66
+ for node in ast.walk(class_node):
67
+ if isinstance(node, ast.Name):
68
+ used_names.add(node.id)
69
+ return used_names
70
+
71
+ def _generate_temporary_file(self, temporary_filename: str, openapi_file_name: str):
72
+ openapi_file_path = Path(openapi_file_name).resolve(strict=True)
73
+ temporary_file_path = Path(temporary_filename).resolve()
74
+ subprocess.run(
75
+ [
76
+ "datamodel-codegen",
77
+ "--input",
78
+ openapi_file_path,
79
+ "--input-file-type",
80
+ "openapi",
81
+ "--output",
82
+ temporary_file_path,
83
+ "--use-union-operator",
84
+ "--use-default-kwarg",
85
+ "--use-field-description",
86
+ "--use-double-quotes",
87
+ ],
88
+ check=True,
89
+ ) # nosec B603, B607
90
+
91
+ @staticmethod
92
+ def _import_temporary_file(temporary_filename: str):
93
+ with open(temporary_filename, "r", encoding="utf-8") as f:
94
+ return f.read()
95
+
96
+ @staticmethod
97
+ def _extract_imports(source_code: str) -> List[ast.Import | ast.ImportFrom]:
98
+ """ソースコードからimport文を抽出し、そのテキストリストを返す"""
99
+ tree = ast.parse(source_code)
100
+ return [
101
+ node for node in tree.body if isinstance(node, (ast.Import, ast.ImportFrom))
102
+ ]
103
+
104
+ @staticmethod
105
+ def _extract_classes(source_code: str) -> List[ast.ClassDef]:
106
+ """ソースコードからトップレベルのクラス定義のASTノードを抽出する"""
107
+ tree = ast.parse(source_code)
108
+ return [node for node in tree.body if isinstance(node, ast.ClassDef)]
109
+
110
+ def _convert_to_snake_case(self, string: str):
111
+ s1 = re.sub("(.)([A-Z][a-z]+)", r"\1_\2", string)
112
+ s2 = re.sub("([a-z0-9])([A-Z])", r"\1_\2", s1)
113
+ return s2.lower()
@@ -0,0 +1,7 @@
1
+ Metadata-Version: 2.4
2
+ Name: modelgen
3
+ Version: 0.0.1
4
+ Summary: Add your description here
5
+ Requires-Python: >=3.12
6
+ Description-Content-Type: text/markdown
7
+ Requires-Dist: datamodel-code-generator>=0.33.0
@@ -0,0 +1,10 @@
1
+ README.md
2
+ pyproject.toml
3
+ src/__init__.py
4
+ src/code_generator.py
5
+ src/modelgen.egg-info/PKG-INFO
6
+ src/modelgen.egg-info/SOURCES.txt
7
+ src/modelgen.egg-info/dependency_links.txt
8
+ src/modelgen.egg-info/requires.txt
9
+ src/modelgen.egg-info/top_level.txt
10
+ tests/test_code_generator.py
@@ -0,0 +1 @@
1
+ datamodel-code-generator>=0.33.0
@@ -0,0 +1,2 @@
1
+ __init__
2
+ code_generator
@@ -0,0 +1,32 @@
1
+ import os
2
+
3
+ from src.code_generator import CodeGenerator
4
+
5
+
6
+ class TestCodeGenerator:
7
+ def test_init(self):
8
+ # Arrange
9
+ openapi_file_name = "tests/sample.yaml"
10
+ output_dir = "tests/sample_dir/"
11
+
12
+ # Act
13
+ code_generator = CodeGenerator(openapi_file_name, output_dir)
14
+
15
+ # Assert
16
+ assert len(code_generator._imports) == 3
17
+ assert len(code_generator._classes) == 3
18
+
19
+ def test_execute(self):
20
+ # Arrange
21
+ openapi_file_name = "tests/sample.yaml"
22
+ output_dir = "tests/sample_dir/"
23
+
24
+ # Act
25
+ CodeGenerator(openapi_file_name, output_dir).execute()
26
+
27
+ # Assert
28
+ with os.scandir(output_dir) as entries:
29
+ files = [entry.name for entry in entries if entry.is_file()]
30
+ assert "user.py" in files
31
+ assert "user_create.py" in files
32
+ assert "user_update.py" in files