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.
- modelgen-0.0.1/PKG-INFO +7 -0
- modelgen-0.0.1/README.md +0 -0
- modelgen-0.0.1/pyproject.toml +27 -0
- modelgen-0.0.1/setup.cfg +4 -0
- modelgen-0.0.1/src/__init__.py +3 -0
- modelgen-0.0.1/src/code_generator.py +113 -0
- modelgen-0.0.1/src/modelgen.egg-info/PKG-INFO +7 -0
- modelgen-0.0.1/src/modelgen.egg-info/SOURCES.txt +10 -0
- modelgen-0.0.1/src/modelgen.egg-info/dependency_links.txt +1 -0
- modelgen-0.0.1/src/modelgen.egg-info/requires.txt +1 -0
- modelgen-0.0.1/src/modelgen.egg-info/top_level.txt +2 -0
- modelgen-0.0.1/tests/test_code_generator.py +32 -0
modelgen-0.0.1/PKG-INFO
ADDED
modelgen-0.0.1/README.md
ADDED
|
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"]
|
modelgen-0.0.1/setup.cfg
ADDED
|
@@ -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,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
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
datamodel-code-generator>=0.33.0
|
|
@@ -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
|