msra-codegen 0.1.0__py3-none-any.whl
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.
- msra_codegen/README.md +23 -0
- msra_codegen/__init__.py +6 -0
- msra_codegen/__main__.py +5 -0
- msra_codegen/bridge.py +29 -0
- msra_codegen/cli.py +105 -0
- msra_codegen/codegen_context.py +1690 -0
- msra_codegen/config.toml +164 -0
- msra_codegen/core_naming.py +155 -0
- msra_codegen/docs_generator.py +346 -0
- msra_codegen/file_utils.py +8 -0
- msra_codegen/funcresult.py +156 -0
- msra_codegen/generator.py +6 -0
- msra_codegen/generator_config.py +35 -0
- msra_codegen/github_workflows.py +129 -0
- msra_codegen/gitignore.py +31 -0
- msra_codegen/issue_templates.py +100 -0
- msra_codegen/logo_assets.py +99 -0
- msra_codegen/msra_serializer.py +205 -0
- msra_codegen/node_export.js +296 -0
- msra_codegen/package_metadata.py +306 -0
- msra_codegen/package_writer.py +175 -0
- msra_codegen/project_model.py +490 -0
- msra_codegen/python_formatting.py +88 -0
- msra_codegen/python_render.py +242 -0
- msra_codegen/readme_pipeline.py +519 -0
- msra_codegen/requirements.txt +5 -0
- msra_codegen/template_engine.py +26 -0
- msra_codegen/templates/Makefile.tpl +44 -0
- msra_codegen/templates/README.md.tpl +55 -0
- msra_codegen/templates/abstraction/__init__.py.tpl +188 -0
- msra_codegen/templates/abstraction/regexes.py.tpl +25 -0
- msra_codegen/templates/docs/requirements.txt.tpl +3 -0
- msra_codegen/templates/docs/source/Makefile.tpl +20 -0
- msra_codegen/templates/docs/source/api.rst.tpl +9 -0
- msra_codegen/templates/docs/source/conf.py.tpl +88 -0
- msra_codegen/templates/docs/source/index.rst.tpl +14 -0
- msra_codegen/templates/docs/source/module.rst.tpl +34 -0
- msra_codegen/templates/docs/source/quick_start.rst.tpl +19 -0
- msra_codegen/templates/endpoints_init.py.tpl +15 -0
- msra_codegen/templates/example.py.tpl +1 -0
- msra_codegen/templates/function.py.tpl +364 -0
- msra_codegen/templates/github/issue_templates/bug_report.yml.tpl +55 -0
- msra_codegen/templates/github/issue_templates/config.yml.tpl +8 -0
- msra_codegen/templates/github/issue_templates/documentation_issue.yml.tpl +33 -0
- msra_codegen/templates/github/issue_templates/feature_request.yml.tpl +36 -0
- msra_codegen/templates/github/workflows/publish.yml.tpl +100 -0
- msra_codegen/templates/github/workflows/source-sync.yml.tpl +177 -0
- msra_codegen/templates/github/workflows/tests.yml.tpl +69 -0
- msra_codegen/templates/gitignore.tpl +3 -0
- msra_codegen/templates/group.py.tpl +56 -0
- msra_codegen/templates/group_init.py.tpl +14 -0
- msra_codegen/templates/init.py.tpl +4 -0
- msra_codegen/templates/licenses/GPL-3.0-or-later.txt.tpl +674 -0
- msra_codegen/templates/licenses/MIT.txt.tpl +21 -0
- msra_codegen/templates/manager.py.tpl +257 -0
- msra_codegen/templates/pyproject.toml.tpl +38 -0
- msra_codegen/templates/tests/api_test.py.tpl +49 -0
- msra_codegen/templates/tests/conftest.py.tpl +21 -0
- msra_codegen/templates/variable.py.tpl +54 -0
- msra_codegen/tests_generator.py +988 -0
- msra_codegen/typespec.py +275 -0
- msra_codegen/validation.py +118 -0
- msra_codegen-0.1.0.dist-info/METADATA +47 -0
- msra_codegen-0.1.0.dist-info/RECORD +68 -0
- msra_codegen-0.1.0.dist-info/WHEEL +5 -0
- msra_codegen-0.1.0.dist-info/entry_points.txt +2 -0
- msra_codegen-0.1.0.dist-info/licenses/LICENSE +674 -0
- msra_codegen-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import re
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from functools import lru_cache
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any
|
|
9
|
+
from urllib.request import urlopen
|
|
10
|
+
|
|
11
|
+
from packaging.version import Version
|
|
12
|
+
from jinja2 import TemplateNotFound
|
|
13
|
+
|
|
14
|
+
from .core_naming import root_client_class_name
|
|
15
|
+
from .file_utils import write_text
|
|
16
|
+
from .generator_config import config_section
|
|
17
|
+
from .template_engine import render_template
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def render_pyproject(project: dict[str, Any], package_name: str) -> str:
|
|
21
|
+
client_class_name = root_client_class_name(project)
|
|
22
|
+
authors = project["app"].get("authors", [])
|
|
23
|
+
min_required_python = str(project["app"].get("min_required_python", "3.10") or "3.10").strip()
|
|
24
|
+
description = str(project["app"].get("description", "") or "").strip()
|
|
25
|
+
runtime_dependencies = collect_runtime_dependencies(project)
|
|
26
|
+
licenses_config = config_section("licenses")
|
|
27
|
+
return render_template(
|
|
28
|
+
"pyproject.toml.tpl",
|
|
29
|
+
{
|
|
30
|
+
"authors_block": render_authors_block(authors),
|
|
31
|
+
"description": json.dumps(description, ensure_ascii=False),
|
|
32
|
+
"license": project["app"].get("license", licenses_config.get("default", "MIT")),
|
|
33
|
+
"keywords_block": render_keywords_block(project["app"].get("keywords", [])),
|
|
34
|
+
"classifiers_block": render_classifiers_block(min_required_python),
|
|
35
|
+
"requires_python": f">={min_required_python}",
|
|
36
|
+
"dependencies_block": render_toml_string_list("dependencies", runtime_dependencies),
|
|
37
|
+
"package_name": package_name,
|
|
38
|
+
"autotest_start_class": f"{package_name}.{client_class_name}",
|
|
39
|
+
"mypy_block": render_mypy_block(),
|
|
40
|
+
"ruff_block": render_ruff_block(),
|
|
41
|
+
},
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def render_authors_block(authors: Any) -> str:
|
|
46
|
+
items: list[str] = []
|
|
47
|
+
if isinstance(authors, list):
|
|
48
|
+
for author in authors:
|
|
49
|
+
if not isinstance(author, dict):
|
|
50
|
+
continue
|
|
51
|
+
fields = [f'name = {json.dumps(str(author.get("name", "")))}']
|
|
52
|
+
email = str(author.get("email", "")).strip()
|
|
53
|
+
if email:
|
|
54
|
+
fields.append(f'email = {json.dumps(email)}')
|
|
55
|
+
items.append(" { " + ", ".join(fields) + " }")
|
|
56
|
+
if not items:
|
|
57
|
+
return "authors = []"
|
|
58
|
+
return "authors = [\n" + ",\n".join(items) + "\n]"
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def render_keywords_block(keywords: Any) -> str:
|
|
62
|
+
return render_toml_string_list("keywords", keywords)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def render_ruff_block() -> str:
|
|
66
|
+
ruff_config = config_section("ruff")
|
|
67
|
+
line_length = ruff_config.get("line_length")
|
|
68
|
+
if not isinstance(line_length, int):
|
|
69
|
+
raise RuntimeError("ruff.line_length must be an integer.")
|
|
70
|
+
|
|
71
|
+
lint_config = ruff_config.get("lint")
|
|
72
|
+
if not isinstance(lint_config, dict):
|
|
73
|
+
raise RuntimeError("ruff.lint must be a table.")
|
|
74
|
+
|
|
75
|
+
select = lint_config.get("select")
|
|
76
|
+
if not isinstance(select, list) or not select:
|
|
77
|
+
raise RuntimeError("ruff.lint.select must be a non-empty list.")
|
|
78
|
+
|
|
79
|
+
ignore = lint_config.get("ignore", [])
|
|
80
|
+
if not isinstance(ignore, list):
|
|
81
|
+
raise RuntimeError("ruff.lint.ignore must be a list.")
|
|
82
|
+
|
|
83
|
+
return "\n".join(
|
|
84
|
+
[
|
|
85
|
+
"[tool.ruff]",
|
|
86
|
+
f"line-length = {line_length}",
|
|
87
|
+
"",
|
|
88
|
+
"[tool.ruff.lint]",
|
|
89
|
+
render_toml_string_list("select", select),
|
|
90
|
+
render_toml_string_list("ignore", ignore),
|
|
91
|
+
]
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def render_mypy_block() -> str:
|
|
96
|
+
mypy_config = config_section("mypy")
|
|
97
|
+
if not isinstance(mypy_config, dict):
|
|
98
|
+
raise RuntimeError("mypy must be a table.")
|
|
99
|
+
|
|
100
|
+
items: list[str] = []
|
|
101
|
+
for key, value in mypy_config.items():
|
|
102
|
+
if isinstance(value, bool):
|
|
103
|
+
rendered_value = "true" if value else "false"
|
|
104
|
+
elif isinstance(value, (int, float)) and not isinstance(value, bool):
|
|
105
|
+
rendered_value = str(value)
|
|
106
|
+
elif isinstance(value, str):
|
|
107
|
+
rendered_value = json.dumps(value)
|
|
108
|
+
elif isinstance(value, list):
|
|
109
|
+
rendered_items: list[str] = []
|
|
110
|
+
for item in value:
|
|
111
|
+
if isinstance(item, bool):
|
|
112
|
+
rendered_items.append("true" if item else "false")
|
|
113
|
+
elif isinstance(item, (int, float)) and not isinstance(item, bool):
|
|
114
|
+
rendered_items.append(str(item))
|
|
115
|
+
elif isinstance(item, str):
|
|
116
|
+
rendered_items.append(json.dumps(item))
|
|
117
|
+
else:
|
|
118
|
+
raise RuntimeError(f"mypy.{key} contains an unsupported value type.")
|
|
119
|
+
rendered_value = "["
|
|
120
|
+
if rendered_items:
|
|
121
|
+
rendered_value += ", ".join(rendered_items)
|
|
122
|
+
rendered_value += "]"
|
|
123
|
+
else:
|
|
124
|
+
raise RuntimeError(f"mypy.{key} contains an unsupported value type.")
|
|
125
|
+
items.append(f"{key} = {rendered_value}")
|
|
126
|
+
|
|
127
|
+
if not items:
|
|
128
|
+
raise RuntimeError("mypy must not be empty.")
|
|
129
|
+
|
|
130
|
+
return "\n".join(["[tool.mypy]", *items])
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
@lru_cache(maxsize=1)
|
|
134
|
+
def latest_supported_python_minor() -> int:
|
|
135
|
+
families = load_python_release_families()
|
|
136
|
+
if len(families) < 2:
|
|
137
|
+
raise RuntimeError(
|
|
138
|
+
"Could not determine the penultimate Python 3 minor family from the python.org releases API."
|
|
139
|
+
)
|
|
140
|
+
return families[1][1]
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def load_python_release_families() -> list[tuple[int, int]]:
|
|
144
|
+
python_config = config_section("python")
|
|
145
|
+
try:
|
|
146
|
+
with urlopen(
|
|
147
|
+
str(python_config.get("releases_api_url", "https://www.python.org/api/v2/downloads/release/")),
|
|
148
|
+
timeout=float(python_config.get("releases_api_timeout_seconds", 15.0)),
|
|
149
|
+
) as response:
|
|
150
|
+
data = json.load(response)
|
|
151
|
+
except OSError as exc: # pragma: no cover - depends on network availability
|
|
152
|
+
raise RuntimeError(
|
|
153
|
+
"Failed to load python.org releases API."
|
|
154
|
+
) from exc
|
|
155
|
+
if not isinstance(data, list):
|
|
156
|
+
raise RuntimeError("Unexpected response format from python.org releases API.")
|
|
157
|
+
|
|
158
|
+
families = {
|
|
159
|
+
(version.major, version.minor)
|
|
160
|
+
for version in extract_python_release_versions(data)
|
|
161
|
+
}
|
|
162
|
+
return sorted(families, reverse=True)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def extract_python_release_versions(data: Any) -> list[Version]:
|
|
166
|
+
versions: list[Version] = []
|
|
167
|
+
for item in data:
|
|
168
|
+
if not isinstance(item, dict):
|
|
169
|
+
continue
|
|
170
|
+
name = str(item.get("name", ""))
|
|
171
|
+
if not name.startswith("Python "):
|
|
172
|
+
continue
|
|
173
|
+
raw_version = name.removeprefix("Python ").strip()
|
|
174
|
+
try:
|
|
175
|
+
version = Version(raw_version)
|
|
176
|
+
except Exception:
|
|
177
|
+
continue
|
|
178
|
+
if version.major != 3:
|
|
179
|
+
continue
|
|
180
|
+
if version.is_prerelease or version.is_devrelease:
|
|
181
|
+
continue
|
|
182
|
+
versions.append(version)
|
|
183
|
+
return versions
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def render_classifiers_block(min_required_python: str) -> str:
|
|
187
|
+
classifiers_config = config_section("classifiers")
|
|
188
|
+
match = re.fullmatch(r"(\d+)\.(\d+)", min_required_python.strip())
|
|
189
|
+
if not match:
|
|
190
|
+
version_labels = [f"Programming Language :: Python :: {min_required_python.strip()}"]
|
|
191
|
+
else:
|
|
192
|
+
major = int(match.group(1))
|
|
193
|
+
start_minor = int(match.group(2))
|
|
194
|
+
version_labels = [f"Programming Language :: Python :: {major}"]
|
|
195
|
+
if major == 3:
|
|
196
|
+
end_minor = max(start_minor, latest_supported_python_minor())
|
|
197
|
+
version_labels.extend(
|
|
198
|
+
f"Programming Language :: Python :: 3.{minor}"
|
|
199
|
+
for minor in range(start_minor, end_minor + 1)
|
|
200
|
+
)
|
|
201
|
+
else:
|
|
202
|
+
version_labels.append(f"Programming Language :: Python :: {major}.{start_minor}")
|
|
203
|
+
version_labels.extend(
|
|
204
|
+
[
|
|
205
|
+
*[str(item) for item in classifiers_config.get("static", []) if str(item).strip()],
|
|
206
|
+
]
|
|
207
|
+
)
|
|
208
|
+
return render_toml_string_list("classifiers", version_labels)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def render_toml_string_list(key: str, values: Any) -> str:
|
|
212
|
+
items: list[str] = []
|
|
213
|
+
if isinstance(values, list):
|
|
214
|
+
for value in values:
|
|
215
|
+
text = str(value).strip()
|
|
216
|
+
if not text:
|
|
217
|
+
continue
|
|
218
|
+
items.append(json.dumps(text))
|
|
219
|
+
if not items:
|
|
220
|
+
return f"{key} = []"
|
|
221
|
+
if len(items) == 1:
|
|
222
|
+
return f"{key} = [{items[0]}]"
|
|
223
|
+
return f"{key} = [\n " + ",\n ".join(items) + "\n]"
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def collect_runtime_dependencies(project: dict[str, Any]) -> list[str]:
|
|
227
|
+
runtime_config = config_section("runtime_dependencies")
|
|
228
|
+
dependencies = [str(item).strip() for item in runtime_config.get("base", []) if str(item).strip()]
|
|
229
|
+
if any(func.get("transport") == "direct" for func in project.get("functions", [])):
|
|
230
|
+
dependencies.extend(
|
|
231
|
+
str(item).strip() for item in runtime_config.get("direct", []) if str(item).strip()
|
|
232
|
+
)
|
|
233
|
+
if project.get("app", {}).get("abstractions"):
|
|
234
|
+
dependencies.append("pydantic")
|
|
235
|
+
return dependencies
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def collect_dev_dependencies() -> list[str]:
|
|
239
|
+
dev_config = config_section("development_dependencies")
|
|
240
|
+
return [str(item).strip() for item in dev_config.get("base", []) if str(item).strip()]
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def render_requirements_txt(project: dict[str, Any]) -> str:
|
|
244
|
+
return "\n".join(collect_runtime_dependencies(project)) + "\n"
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def render_requirements_dev_txt(project: dict[str, Any]) -> str:
|
|
248
|
+
lines = [
|
|
249
|
+
"-r requirements.txt",
|
|
250
|
+
"-r docs/requirements.txt",
|
|
251
|
+
*collect_dev_dependencies(),
|
|
252
|
+
]
|
|
253
|
+
return "\n".join(lines) + "\n"
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def write_root_license(output_dir: Path, project: dict[str, Any]) -> None:
|
|
257
|
+
licenses_config = config_section("licenses")
|
|
258
|
+
license_name = resolve_license_template_name(
|
|
259
|
+
str(project["app"].get("license", licenses_config.get("default", "MIT")) or "").strip()
|
|
260
|
+
or str(licenses_config.get("default", "MIT"))
|
|
261
|
+
)
|
|
262
|
+
authors = project["app"].get("authors", [])
|
|
263
|
+
license_text = render_license_text(license_name, authors)
|
|
264
|
+
write_text(output_dir / "LICENSE", license_text)
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def resolve_license_template_name(license_name: str) -> str:
|
|
268
|
+
normalized = license_name.strip()
|
|
269
|
+
aliases = config_section("licenses").get("aliases", {})
|
|
270
|
+
if isinstance(aliases, dict) and normalized in aliases:
|
|
271
|
+
return str(aliases[normalized]).strip()
|
|
272
|
+
return normalized
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def render_license_text(license_name: str, authors: Any) -> str:
|
|
276
|
+
context = {}
|
|
277
|
+
if license_name == "MIT":
|
|
278
|
+
context = {
|
|
279
|
+
"copyright_holders": format_copyright_holders(authors),
|
|
280
|
+
"year": datetime.now().year,
|
|
281
|
+
}
|
|
282
|
+
template_name = f"licenses/{license_name}.txt.tpl"
|
|
283
|
+
try:
|
|
284
|
+
return render_template(template_name, context)
|
|
285
|
+
except TemplateNotFound as exc: # pragma: no cover - guardrail for unsupported licenses
|
|
286
|
+
raise RuntimeError(
|
|
287
|
+
f'Missing local license template "{template_name}". Add it under msra_codegen/templates/licenses/.'
|
|
288
|
+
) from exc
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def format_copyright_holders(authors: Any) -> str:
|
|
292
|
+
names: list[str] = []
|
|
293
|
+
if isinstance(authors, list):
|
|
294
|
+
for author in authors:
|
|
295
|
+
if not isinstance(author, dict):
|
|
296
|
+
continue
|
|
297
|
+
name = str(author.get("name", "")).strip()
|
|
298
|
+
if name:
|
|
299
|
+
names.append(name)
|
|
300
|
+
if not names:
|
|
301
|
+
return "The authors"
|
|
302
|
+
if len(names) == 1:
|
|
303
|
+
return names[0]
|
|
304
|
+
if len(names) == 2:
|
|
305
|
+
return " and ".join(names)
|
|
306
|
+
return ", ".join(names[:-1]) + ", and " + names[-1]
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import shutil
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from .codegen_context import (
|
|
8
|
+
build_abstraction_package_context,
|
|
9
|
+
collect_abstraction_scripts,
|
|
10
|
+
collect_extractor_scripts,
|
|
11
|
+
collect_goto_pipeline_scripts,
|
|
12
|
+
collect_warmup_scripts,
|
|
13
|
+
render_endpoints_init,
|
|
14
|
+
render_init,
|
|
15
|
+
render_manager_template,
|
|
16
|
+
write_group_package,
|
|
17
|
+
)
|
|
18
|
+
from .gitignore import generate_gitignore_project
|
|
19
|
+
from .github_workflows import generate_github_workflows_project
|
|
20
|
+
from .issue_templates import generate_github_issue_templates_project
|
|
21
|
+
from .core_naming import abstraction_module_name_from_path, normalize_abstraction_path, normalize_script_path
|
|
22
|
+
from .file_utils import write_text
|
|
23
|
+
from .python_formatting import format_python_tree
|
|
24
|
+
from .package_metadata import render_pyproject, render_requirements_dev_txt, render_requirements_txt, write_root_license
|
|
25
|
+
from .project_model import build_group_tree, top_level_groups
|
|
26
|
+
from .tests_generator import build_tests_project_context, generate_tests_project
|
|
27
|
+
from .template_engine import render_template
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def ensure_package_inits(target_dir: Path, package_root: Path) -> None:
|
|
31
|
+
current = target_dir
|
|
32
|
+
while current != package_root and package_root in current.parents:
|
|
33
|
+
init_file = current / "__init__.py"
|
|
34
|
+
if not init_file.exists():
|
|
35
|
+
write_text(init_file, "")
|
|
36
|
+
current = current.parent
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def generate_project(
|
|
40
|
+
project: dict[str, Any],
|
|
41
|
+
output_dir: Path,
|
|
42
|
+
source_root: Path | None = None,
|
|
43
|
+
) -> None:
|
|
44
|
+
output_dir = output_dir.resolve()
|
|
45
|
+
source_root = source_root.resolve() if source_root is not None else Path(project["source_path"]).resolve().parent
|
|
46
|
+
package_name = str(project["app"].get("package_name") or "").strip()
|
|
47
|
+
if not package_name:
|
|
48
|
+
raise ValueError('app.package_name is required and must be set explicitly in the source MSRA file.')
|
|
49
|
+
group_tree = build_group_tree(project)
|
|
50
|
+
|
|
51
|
+
package_root = output_dir / package_name
|
|
52
|
+
abstraction_root = package_root / "abstraction"
|
|
53
|
+
endpoints_root = package_root / "endpoints"
|
|
54
|
+
pipelines_root = package_root / "pipelines"
|
|
55
|
+
legacy_postprocess_root = package_root / "postprocess"
|
|
56
|
+
extractors_root = package_root / "extractors"
|
|
57
|
+
package_root.mkdir(parents=True, exist_ok=True)
|
|
58
|
+
if abstraction_root.exists():
|
|
59
|
+
shutil.rmtree(abstraction_root)
|
|
60
|
+
abstraction_root.mkdir(parents=True, exist_ok=True)
|
|
61
|
+
if pipelines_root.exists():
|
|
62
|
+
shutil.rmtree(pipelines_root)
|
|
63
|
+
pipelines_root.mkdir(parents=True, exist_ok=True)
|
|
64
|
+
if legacy_postprocess_root.exists():
|
|
65
|
+
shutil.rmtree(legacy_postprocess_root)
|
|
66
|
+
if extractors_root.exists():
|
|
67
|
+
shutil.rmtree(extractors_root)
|
|
68
|
+
extractors_root.mkdir(parents=True, exist_ok=True)
|
|
69
|
+
for stale_pipeline_file in [package_root / "warmup.py", package_root / "goto_pipeline.py"]:
|
|
70
|
+
if stale_pipeline_file.exists():
|
|
71
|
+
stale_pipeline_file.unlink()
|
|
72
|
+
|
|
73
|
+
tests_context = build_tests_project_context(project, package_name)
|
|
74
|
+
|
|
75
|
+
write_text(
|
|
76
|
+
output_dir / "pyproject.toml",
|
|
77
|
+
render_pyproject(project, package_name),
|
|
78
|
+
)
|
|
79
|
+
write_text(
|
|
80
|
+
output_dir / "requirements.txt",
|
|
81
|
+
render_requirements_txt(project),
|
|
82
|
+
)
|
|
83
|
+
write_text(
|
|
84
|
+
output_dir / "requirements-dev.txt",
|
|
85
|
+
render_requirements_dev_txt(project),
|
|
86
|
+
)
|
|
87
|
+
write_text(package_root / "__init__.py", render_init(project, package_name))
|
|
88
|
+
stale_abstraction_file = package_root / "abstraction.py"
|
|
89
|
+
if stale_abstraction_file.exists():
|
|
90
|
+
stale_abstraction_file.unlink()
|
|
91
|
+
abstraction_context = build_abstraction_package_context(project)
|
|
92
|
+
copied_abstraction_modules: set[str] = set()
|
|
93
|
+
for abstraction_script in collect_abstraction_scripts(project):
|
|
94
|
+
normalized_abstraction = normalize_abstraction_path(abstraction_script)
|
|
95
|
+
source = Path(normalized_abstraction)
|
|
96
|
+
if not source.is_absolute():
|
|
97
|
+
source = source_root / source
|
|
98
|
+
module_name = abstraction_module_name_from_path(normalized_abstraction)
|
|
99
|
+
if module_name in {"__init__", "regexes"}:
|
|
100
|
+
raise ValueError(
|
|
101
|
+
f'Abstraction file "{abstraction_script}" resolves to reserved module name "{module_name}".'
|
|
102
|
+
)
|
|
103
|
+
if module_name in copied_abstraction_modules:
|
|
104
|
+
raise ValueError(f'Duplicate abstraction module name "{module_name}" derived from "{abstraction_script}".')
|
|
105
|
+
copied_abstraction_modules.add(module_name)
|
|
106
|
+
target = abstraction_root / f"{module_name}.py"
|
|
107
|
+
shutil.copyfile(source, target)
|
|
108
|
+
write_text(
|
|
109
|
+
abstraction_root / "__init__.py",
|
|
110
|
+
render_template("abstraction/__init__.py.tpl", abstraction_context),
|
|
111
|
+
)
|
|
112
|
+
write_text(
|
|
113
|
+
abstraction_root / "regexes.py",
|
|
114
|
+
render_template("abstraction/regexes.py.tpl", abstraction_context),
|
|
115
|
+
)
|
|
116
|
+
write_text(
|
|
117
|
+
package_root / "manager.py",
|
|
118
|
+
render_manager_template(
|
|
119
|
+
project,
|
|
120
|
+
package_name,
|
|
121
|
+
group_tree,
|
|
122
|
+
autotest_function_ids=tests_context["autotest_function_ids"],
|
|
123
|
+
),
|
|
124
|
+
)
|
|
125
|
+
if endpoints_root.exists():
|
|
126
|
+
shutil.rmtree(endpoints_root)
|
|
127
|
+
endpoints_root.mkdir(parents=True, exist_ok=True)
|
|
128
|
+
write_text(endpoints_root / "__init__.py", render_endpoints_init(project, package_name, group_tree))
|
|
129
|
+
for group_node in top_level_groups(group_tree):
|
|
130
|
+
write_group_package(
|
|
131
|
+
group_node,
|
|
132
|
+
project,
|
|
133
|
+
package_name,
|
|
134
|
+
endpoints_root,
|
|
135
|
+
autotest_function_ids=tests_context["autotest_function_ids"],
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
for script in dict.fromkeys(collect_warmup_scripts(project) + collect_goto_pipeline_scripts(project)):
|
|
139
|
+
source = source_root / script
|
|
140
|
+
target = pipelines_root / normalize_script_path(script)
|
|
141
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
142
|
+
ensure_package_inits(target.parent, package_root)
|
|
143
|
+
shutil.copyfile(source, target)
|
|
144
|
+
|
|
145
|
+
for script in dict.fromkeys(collect_extractor_scripts(project)):
|
|
146
|
+
source = source_root / script
|
|
147
|
+
target = package_root / normalize_script_path(script)
|
|
148
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
149
|
+
shutil.copyfile(source, target)
|
|
150
|
+
|
|
151
|
+
legacy_license_dir = output_dir / "LICENSES"
|
|
152
|
+
if legacy_license_dir.exists():
|
|
153
|
+
shutil.rmtree(legacy_license_dir)
|
|
154
|
+
write_root_license(output_dir, project)
|
|
155
|
+
generate_gitignore_project(output_dir)
|
|
156
|
+
generate_github_workflows_project(project, output_dir, package_name)
|
|
157
|
+
generate_github_issue_templates_project(project, output_dir)
|
|
158
|
+
|
|
159
|
+
from .docs_generator import generate_docs_project
|
|
160
|
+
|
|
161
|
+
generate_docs_project(
|
|
162
|
+
project,
|
|
163
|
+
output_dir,
|
|
164
|
+
package_name,
|
|
165
|
+
group_tree,
|
|
166
|
+
tests_context=tests_context,
|
|
167
|
+
)
|
|
168
|
+
generate_tests_project(
|
|
169
|
+
project,
|
|
170
|
+
output_dir,
|
|
171
|
+
package_name,
|
|
172
|
+
group_tree,
|
|
173
|
+
tests_context=tests_context,
|
|
174
|
+
)
|
|
175
|
+
format_python_tree(output_dir)
|