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.
Files changed (68) hide show
  1. msra_codegen/README.md +23 -0
  2. msra_codegen/__init__.py +6 -0
  3. msra_codegen/__main__.py +5 -0
  4. msra_codegen/bridge.py +29 -0
  5. msra_codegen/cli.py +105 -0
  6. msra_codegen/codegen_context.py +1690 -0
  7. msra_codegen/config.toml +164 -0
  8. msra_codegen/core_naming.py +155 -0
  9. msra_codegen/docs_generator.py +346 -0
  10. msra_codegen/file_utils.py +8 -0
  11. msra_codegen/funcresult.py +156 -0
  12. msra_codegen/generator.py +6 -0
  13. msra_codegen/generator_config.py +35 -0
  14. msra_codegen/github_workflows.py +129 -0
  15. msra_codegen/gitignore.py +31 -0
  16. msra_codegen/issue_templates.py +100 -0
  17. msra_codegen/logo_assets.py +99 -0
  18. msra_codegen/msra_serializer.py +205 -0
  19. msra_codegen/node_export.js +296 -0
  20. msra_codegen/package_metadata.py +306 -0
  21. msra_codegen/package_writer.py +175 -0
  22. msra_codegen/project_model.py +490 -0
  23. msra_codegen/python_formatting.py +88 -0
  24. msra_codegen/python_render.py +242 -0
  25. msra_codegen/readme_pipeline.py +519 -0
  26. msra_codegen/requirements.txt +5 -0
  27. msra_codegen/template_engine.py +26 -0
  28. msra_codegen/templates/Makefile.tpl +44 -0
  29. msra_codegen/templates/README.md.tpl +55 -0
  30. msra_codegen/templates/abstraction/__init__.py.tpl +188 -0
  31. msra_codegen/templates/abstraction/regexes.py.tpl +25 -0
  32. msra_codegen/templates/docs/requirements.txt.tpl +3 -0
  33. msra_codegen/templates/docs/source/Makefile.tpl +20 -0
  34. msra_codegen/templates/docs/source/api.rst.tpl +9 -0
  35. msra_codegen/templates/docs/source/conf.py.tpl +88 -0
  36. msra_codegen/templates/docs/source/index.rst.tpl +14 -0
  37. msra_codegen/templates/docs/source/module.rst.tpl +34 -0
  38. msra_codegen/templates/docs/source/quick_start.rst.tpl +19 -0
  39. msra_codegen/templates/endpoints_init.py.tpl +15 -0
  40. msra_codegen/templates/example.py.tpl +1 -0
  41. msra_codegen/templates/function.py.tpl +364 -0
  42. msra_codegen/templates/github/issue_templates/bug_report.yml.tpl +55 -0
  43. msra_codegen/templates/github/issue_templates/config.yml.tpl +8 -0
  44. msra_codegen/templates/github/issue_templates/documentation_issue.yml.tpl +33 -0
  45. msra_codegen/templates/github/issue_templates/feature_request.yml.tpl +36 -0
  46. msra_codegen/templates/github/workflows/publish.yml.tpl +100 -0
  47. msra_codegen/templates/github/workflows/source-sync.yml.tpl +177 -0
  48. msra_codegen/templates/github/workflows/tests.yml.tpl +69 -0
  49. msra_codegen/templates/gitignore.tpl +3 -0
  50. msra_codegen/templates/group.py.tpl +56 -0
  51. msra_codegen/templates/group_init.py.tpl +14 -0
  52. msra_codegen/templates/init.py.tpl +4 -0
  53. msra_codegen/templates/licenses/GPL-3.0-or-later.txt.tpl +674 -0
  54. msra_codegen/templates/licenses/MIT.txt.tpl +21 -0
  55. msra_codegen/templates/manager.py.tpl +257 -0
  56. msra_codegen/templates/pyproject.toml.tpl +38 -0
  57. msra_codegen/templates/tests/api_test.py.tpl +49 -0
  58. msra_codegen/templates/tests/conftest.py.tpl +21 -0
  59. msra_codegen/templates/variable.py.tpl +54 -0
  60. msra_codegen/tests_generator.py +988 -0
  61. msra_codegen/typespec.py +275 -0
  62. msra_codegen/validation.py +118 -0
  63. msra_codegen-0.1.0.dist-info/METADATA +47 -0
  64. msra_codegen-0.1.0.dist-info/RECORD +68 -0
  65. msra_codegen-0.1.0.dist-info/WHEEL +5 -0
  66. msra_codegen-0.1.0.dist-info/entry_points.txt +2 -0
  67. msra_codegen-0.1.0.dist-info/licenses/LICENSE +674 -0
  68. msra_codegen-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,164 @@
1
+ [python]
2
+ releases_api_url = "https://www.python.org/api/v2/downloads/release/"
3
+ releases_api_timeout_seconds = 15.0
4
+
5
+ [runtime_dependencies]
6
+ base = [
7
+ "camoufox[geoip]",
8
+ "human_requests",
9
+ "Pillow",
10
+ "rich",
11
+ ]
12
+ direct = [
13
+ "aiohttp",
14
+ "aiohttp-retry",
15
+ ]
16
+
17
+ [development_dependencies]
18
+ base = [
19
+ "pytest",
20
+ "pytest-anyio",
21
+ "pytest-jsonschema-snapshot",
22
+ "ruff",
23
+ "mypy",
24
+ ]
25
+
26
+ [gitignore]
27
+ patterns = [
28
+ "# Python bytecode",
29
+ "__pycache__/",
30
+ "*.py[cod]",
31
+ "*.pyd",
32
+ "# Virtual environments",
33
+ ".venv/",
34
+ "venv/",
35
+ "env/",
36
+ "# Test, lint, and coverage caches",
37
+ ".pytest_cache/",
38
+ ".mypy_cache/",
39
+ ".ruff_cache/",
40
+ ".coverage",
41
+ "coverage.xml",
42
+ "coverage.svg",
43
+ "htmlcov/",
44
+ "# Build artifacts",
45
+ "build/",
46
+ "dist/",
47
+ "*.egg-info/",
48
+ "# Generated project noise",
49
+ "merged.msra",
50
+ "error.log",
51
+ "screenshot.png",
52
+ "docs/_build/",
53
+ ]
54
+
55
+ [classifiers]
56
+ static = [
57
+ "Operating System :: Microsoft :: Windows",
58
+ "Operating System :: POSIX :: Linux",
59
+ "Intended Audience :: Developers",
60
+ "Intended Audience :: Information Technology",
61
+ "Topic :: Software Development :: Libraries :: Python Modules",
62
+ "Topic :: Internet",
63
+ "Topic :: Utilities",
64
+ ]
65
+
66
+ [licenses]
67
+ default = "MIT"
68
+ aliases = { "GPL-3.0" = "GPL-3.0-or-later", "GPL-3.0+" = "GPL-3.0-or-later" }
69
+
70
+ [readme]
71
+ principle_text = "Библиотека полностью повторяет сетевую работу обычного пользователя на сайте."
72
+
73
+ [[readme.badges]]
74
+ label = "Ruff"
75
+ badge_url = "https://img.shields.io/badge/linting-Ruff-blue?logo=ruff&logoColor=white"
76
+ url = "https://github.com/astral-sh/ruff"
77
+
78
+ [[readme.socials]]
79
+ key = "discord"
80
+ label = "Discord"
81
+ badge_url = "https://img.shields.io/discord/792572437292253224?label=Discord&labelColor=%232c2f33&color=%237289da"
82
+
83
+ [[readme.socials]]
84
+ key = "telegram"
85
+ label = "Telegram"
86
+ badge_url = "https://img.shields.io/badge/Telegram-24A1DE"
87
+
88
+ [github.workflows.tests]
89
+ name = "tests"
90
+ schedule_cron = "17 3 * * *"
91
+ runner = ["self-hosted"]
92
+ python_version = "3.12"
93
+ checkout_action = "actions/checkout@v4"
94
+ setup_python_action = "actions/setup-python@v5"
95
+ install_commands = [
96
+ "python -m venv venv",
97
+ "venv/bin/python -m pip install -U pip",
98
+ "venv/bin/python -m pip install -r requirements-dev.txt",
99
+ ]
100
+ run_command_shell = "bash -e -lc"
101
+ headed_run_command_shell = "xvfb-run -a bash -e -lc"
102
+ run_commands = [
103
+ "set -o pipefail",
104
+ "venv/bin/python -m pytest --jsss-ci-cd --save-original --tb=short 2>&1 | tee error.log",
105
+ ]
106
+ report_playwright_failure_action = "Miskler/human-requests-bot@v11"
107
+ report_schema_action = "Miskler/pytest-jsonschema-snapshot-bot@v14"
108
+ github_token_secret = "GITHUB_TOKEN"
109
+ log_path = "error.log"
110
+ screenshot_path = "screenshot.png"
111
+
112
+ [github.workflows.publish]
113
+ name = "publish"
114
+ python_version = "3.12"
115
+ branch = "main"
116
+ checkout_action = "actions/checkout@v4"
117
+ setup_python_action = "actions/setup-python@v5"
118
+ setup_python_cache = "pip"
119
+ upload_pages_action = "actions/upload-pages-artifact@v3"
120
+ deploy_pages_action = "actions/deploy-pages@v4"
121
+ pypi_action = "pypa/gh-action-pypi-publish@release/v1"
122
+ target_options = ["docs", "package", "all"]
123
+ pages_environment_name = "github-pages"
124
+ pypi_environment_name = "pypi"
125
+ github_token_secret = "GITHUB_TOKEN"
126
+
127
+ [github.workflows.source_sync]
128
+ name = "source-sync"
129
+ source_branch = "source"
130
+ target_branch = "main"
131
+ logic_repository = "Open-Inflation/engine-reverse-ide"
132
+ logic_ref = "main"
133
+ checkout_action = "actions/checkout@v4"
134
+ setup_python_action = "actions/setup-python@v5"
135
+ python_version = "3.12"
136
+ generator_requirements_path = "msra_codegen/requirements.txt"
137
+ repo_token_secret = "SOURCE_SYNC_TOKEN"
138
+ commit_user_name = "msra-sync-bot"
139
+ commit_user_email = "msra-sync-bot@users.noreply.github.com"
140
+
141
+ [[validation.checks]]
142
+ name = "python-syntax"
143
+ argv = ["{python_executable}", "-m", "compileall", "-q"]
144
+ targets = ["{package_name}", "tests", "example.py", "docs/source/conf.py"]
145
+
146
+ [ruff]
147
+ line_length = 200
148
+
149
+ [ruff.lint]
150
+ select = ["E", "F", "I", "UP", "B", "SIM"]
151
+ ignore = ["RUF001", "RUF002", "RUF003"]
152
+
153
+ [mypy]
154
+ ignore_missing_imports = true
155
+
156
+ [[validation.checks]]
157
+ name = "ruff"
158
+ argv = ["{python_executable}", "-m", "ruff", "check"]
159
+ targets = ["{package_name}", "tests", "example.py", "docs/source/conf.py"]
160
+
161
+ [[validation.checks]]
162
+ name = "mypy"
163
+ argv = ["{python_executable}", "-m", "mypy"]
164
+ targets = ["{package_name}"]
@@ -0,0 +1,155 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+ from pathlib import Path
5
+ from typing import Any
6
+
7
+ from .python_render import get_plain_value
8
+
9
+
10
+ def snake_case(text: str) -> str:
11
+ text = re.sub(r"(?<!^)(?=[A-Z])", "_", text).replace("-", "_")
12
+ text = re.sub(r"[^A-Za-z0-9_]+", "_", text)
13
+ return text.strip("_").lower() or "generated"
14
+
15
+
16
+ def pascal_case(text: str) -> str:
17
+ parts = re.split(r"[^A-Za-z0-9]+|_", text)
18
+ cleaned = []
19
+ for part in parts:
20
+ if not part:
21
+ continue
22
+ cleaned.append(part[:1].upper() + part[1:].lower())
23
+ return "".join(cleaned) or "Generated"
24
+
25
+
26
+ def root_client_class_name(project: dict[str, Any]) -> str:
27
+ name = str(project["app"]["name"])
28
+ if re.fullmatch(r"[A-Za-z_][A-Za-z0-9_]*", name):
29
+ return name
30
+ return pascal_case(name)
31
+
32
+
33
+ def normalize_script_path(script_path: str) -> str:
34
+ normalized = script_path.strip().replace("\\", "/")
35
+ while normalized.startswith("./"):
36
+ normalized = normalized[2:]
37
+ normalized = normalized.strip("/")
38
+ if normalized.startswith("pipelines/"):
39
+ normalized = normalized[len("pipelines/"):]
40
+ return normalized
41
+
42
+
43
+ def normalize_abstraction_path(abstraction_path: str) -> str:
44
+ normalized = abstraction_path.strip().replace("\\", "/")
45
+ while normalized.startswith("./"):
46
+ normalized = normalized[2:]
47
+ return normalized.strip("/")
48
+
49
+
50
+ def abstraction_module_name_from_path(abstraction_path: str) -> str:
51
+ normalized = normalize_abstraction_path(abstraction_path)
52
+ if normalized.endswith(".py"):
53
+ normalized = normalized[:-3]
54
+ normalized = normalized.strip("/")
55
+ if not normalized:
56
+ return "generated"
57
+ return Path(normalized).name
58
+
59
+
60
+ def module_file_name_for_group(path: list[str]) -> str:
61
+ if not path:
62
+ return "generated.py"
63
+ return f"{snake_case(path[-1])}.py"
64
+
65
+
66
+ def package_dir_for_group(path: list[str]) -> Path:
67
+ return Path(*[snake_case(segment) for segment in path]) if path else Path()
68
+
69
+
70
+ def module_import_name_for_group(group_node: dict[str, Any]) -> str:
71
+ path = group_node.get("path", [])
72
+ if not path:
73
+ return "generated"
74
+ if group_node.get("children"):
75
+ return snake_case(path[-1])
76
+ return module_file_name_for_group(path)[:-3]
77
+
78
+
79
+ def module_package_depth_for_group(group_node: dict[str, Any]) -> int:
80
+ path = group_node.get("path", [])
81
+ return len(path) + (1 if group_node.get("children") else 0)
82
+
83
+
84
+ def module_output_dir_for_group(group_node: dict[str, Any], endpoints_root: Path) -> Path:
85
+ path = group_node.get("path", [])
86
+ if group_node.get("children"):
87
+ return endpoints_root / package_dir_for_group(path)
88
+ if path:
89
+ return endpoints_root / package_dir_for_group(path[:-1])
90
+ return endpoints_root
91
+
92
+
93
+ def base_class_name_for_group(path: list[str]) -> str:
94
+ if not path:
95
+ return "GeneratedGroup"
96
+ return pascal_case(path[-1])
97
+
98
+
99
+ def class_name_for_group(path: list[str]) -> str:
100
+ return f"Class{base_class_name_for_group(path)}"
101
+
102
+
103
+ def field_name_for_group(path: list[str]) -> str:
104
+ if not path:
105
+ return "Group"
106
+ return str(path[-1])
107
+
108
+
109
+ def group_public_import_path(package_name: str, group_path: list[str]) -> str:
110
+ segments = [snake_case(segment) for segment in group_path]
111
+ if not segments:
112
+ return f"{package_name}.endpoints"
113
+ return ".".join([package_name, "endpoints", *segments])
114
+
115
+
116
+ def group_path_from_expr(expr: Any) -> str:
117
+ plain = get_plain_value(expr)
118
+ if isinstance(plain, dict) and plain.get("kind") == "ref":
119
+ parts = [part["value"] for part in plain.get("parts", []) if part.get("kind") == "name"]
120
+ if not parts:
121
+ return ""
122
+ if parts[0] == "GROUPS":
123
+ parts = parts[1:]
124
+ return ".".join(parts)
125
+ if isinstance(plain, str):
126
+ return plain.strip()
127
+ if isinstance(plain, list):
128
+ return ".".join(str(part).strip() for part in plain if str(part).strip())
129
+ return str(plain).strip() if plain is not None else ""
130
+
131
+
132
+ def script_module_name_from_path(script_path: str) -> str:
133
+ normalized = normalize_script_path(script_path)
134
+ if normalized.endswith(".py"):
135
+ normalized = normalized[:-3]
136
+ normalized = normalized.strip("/")
137
+ return f"pipelines.{normalized.replace('/', '.')}" if normalized else "pipelines.warmup"
138
+
139
+
140
+ def parse_script_reference(expr: Any) -> dict[str, str] | None:
141
+ plain = get_plain_value(expr)
142
+ if not isinstance(plain, str):
143
+ return None
144
+ script_path, separator, function_name = plain.rpartition(":")
145
+ if not separator:
146
+ return None
147
+ script_path = script_path.strip()
148
+ function_name = function_name.strip()
149
+ if not script_path or not function_name:
150
+ return None
151
+ return {
152
+ "path": script_path,
153
+ "module": script_module_name_from_path(script_path),
154
+ "function": function_name,
155
+ }
@@ -0,0 +1,346 @@
1
+ from __future__ import annotations
2
+
3
+ import shutil
4
+ import textwrap
5
+ from datetime import date
6
+ from pathlib import Path
7
+ from typing import Any
8
+ from urllib.parse import quote
9
+
10
+ from .codegen_context import build_group_context
11
+ from .core_naming import group_public_import_path, root_client_class_name
12
+ from .file_utils import write_text
13
+ from .generator_config import config_section
14
+ from .logo_assets import build_logo_assets
15
+ from .python_formatting import format_python_source
16
+ from .project_model import top_level_groups
17
+ from .readme_pipeline import build_readme_pipeline_code, build_readme_pipeline_note
18
+ from .template_engine import render_template
19
+
20
+
21
+ def generate_docs_project(
22
+ project: dict[str, Any],
23
+ output_dir: Path,
24
+ package_name: str,
25
+ group_tree: dict[str, Any],
26
+ *,
27
+ tests_context: dict[str, Any] | None = None,
28
+ ) -> None:
29
+ docs_root = output_dir / "docs"
30
+ if docs_root.exists():
31
+ shutil.rmtree(docs_root)
32
+
33
+ docs_source_root = docs_root / "source"
34
+ api_root = docs_source_root / "_api"
35
+ static_root = docs_source_root / "_static"
36
+ templates_root = docs_source_root / "_templates"
37
+ api_root.mkdir(parents=True, exist_ok=True)
38
+ static_root.mkdir(parents=True, exist_ok=True)
39
+ templates_root.mkdir(parents=True, exist_ok=True)
40
+
41
+ project_source_root = Path(project["source_path"]).resolve().parent
42
+ logo_context = build_logo_assets(project, project_source_root, static_root)
43
+ context = build_docs_project_context(
44
+ project,
45
+ package_name,
46
+ group_tree,
47
+ tests_context=tests_context,
48
+ logo_context=logo_context,
49
+ )
50
+
51
+ write_text(
52
+ output_dir / "README.md",
53
+ render_template("README.md.tpl", context),
54
+ )
55
+ write_text(
56
+ output_dir / "example.py",
57
+ render_template("example.py.tpl", context),
58
+ )
59
+ stale_examples_dir = output_dir / "examples"
60
+ if stale_examples_dir.exists():
61
+ shutil.rmtree(stale_examples_dir)
62
+ write_text(
63
+ docs_root / "requirements.txt",
64
+ render_template("docs/requirements.txt.tpl", context),
65
+ )
66
+ write_text(
67
+ docs_source_root / "Makefile",
68
+ render_template("docs/source/Makefile.tpl", context),
69
+ )
70
+ write_text(
71
+ docs_source_root / "conf.py",
72
+ render_template("docs/source/conf.py.tpl", context),
73
+ )
74
+ write_text(
75
+ docs_source_root / "index.rst",
76
+ render_template("docs/source/index.rst.tpl", context),
77
+ )
78
+ write_text(
79
+ docs_source_root / "quick_start.rst",
80
+ render_template("docs/source/quick_start.rst.tpl", context),
81
+ )
82
+ write_text(
83
+ docs_source_root / "api.rst",
84
+ render_template("docs/source/api.rst.tpl", context),
85
+ )
86
+ write_text(
87
+ api_root / f"{package_name}.manager.rst",
88
+ render_template("docs/source/module.rst.tpl", context["manager_module"]),
89
+ )
90
+ write_text(
91
+ api_root / f"{package_name}.endpoints.rst",
92
+ render_template("docs/source/module.rst.tpl", context["endpoints_module"]),
93
+ )
94
+
95
+ for group_node in top_level_groups(group_tree):
96
+ write_group_docs(group_node, project, package_name, api_root)
97
+
98
+
99
+ def write_group_docs(
100
+ group_node: dict[str, Any],
101
+ project: dict[str, Any],
102
+ package_name: str,
103
+ api_root: Path,
104
+ ) -> None:
105
+ context = build_group_docs_context(group_node, project, package_name)
106
+ write_text(
107
+ api_root / f"{context['import_path']}.rst",
108
+ render_template("docs/source/module.rst.tpl", context),
109
+ )
110
+ for child_node in group_node.get("children", {}).values():
111
+ write_group_docs(child_node, project, package_name, api_root)
112
+
113
+
114
+ def build_docs_project_context(
115
+ project: dict[str, Any],
116
+ package_name: str,
117
+ group_tree: dict[str, Any],
118
+ *,
119
+ tests_context: dict[str, Any] | None = None,
120
+ logo_context: dict[str, Any] | None = None,
121
+ ) -> dict[str, Any]:
122
+ app = project["app"]
123
+ docs_descriptions = config_section("docs", "descriptions")
124
+ client_class_name = root_client_class_name(project)
125
+ top_groups = [
126
+ build_group_docs_context(group_node, project, package_name)
127
+ for group_node in top_level_groups(group_tree)
128
+ ]
129
+ project_title = str(app["name"] or package_name)
130
+ index_title = f"{project_title} documentation"
131
+ pipeline_script_code = format_python_source(
132
+ build_readme_pipeline_code(project, package_name, client_class_name),
133
+ )
134
+ return {
135
+ "title": index_title,
136
+ "title_underline": "=" * len(index_title),
137
+ "project_name": project_title,
138
+ "project_version": str(app["version"]),
139
+ "package_name": package_name,
140
+ "logo": logo_context,
141
+ "browser": str(app.get("browser", "")),
142
+ "client_class_name": client_class_name,
143
+ "current_year": str(date.today().year),
144
+ "manager_module": build_module_page_context(
145
+ title=f"{package_name}.manager",
146
+ import_path=f"{package_name}.manager",
147
+ description=str(docs_descriptions.get("manager") or ""),
148
+ class_names=[client_class_name, "Warmup"],
149
+ child_pages=[],
150
+ ),
151
+ "endpoints_module": build_module_page_context(
152
+ title=f"{package_name}.endpoints",
153
+ import_path=f"{package_name}.endpoints",
154
+ description=str(docs_descriptions.get("endpoints") or ""),
155
+ class_names=[group["class_name"] for group in top_groups],
156
+ child_pages=[group["import_path"] for group in top_groups],
157
+ ),
158
+ "api_docnames": [f"_api/{package_name}.endpoints", f"_api/{package_name}.manager"],
159
+ "top_groups": top_groups,
160
+ "quick_start": {
161
+ "title": "Quick Start",
162
+ "title_underline": "=" * len("Quick Start"),
163
+ "package_name": package_name,
164
+ "client_class_name": client_class_name,
165
+ "requires_camoufox": str(app.get("browser", "")) == "camoufox",
166
+ "top_groups": top_groups,
167
+ },
168
+ "tests": build_readme_tests_context(project, package_name, tests_context),
169
+ "readme": build_readme_context(
170
+ project,
171
+ package_name,
172
+ pipeline_script_code,
173
+ ),
174
+ "pipeline_script_code": pipeline_script_code,
175
+ "pipeline_script_code_rst": textwrap.indent(pipeline_script_code, " "),
176
+ "pipeline_note": build_readme_pipeline_note(project),
177
+ }
178
+
179
+
180
+ def build_readme_context(
181
+ project: dict[str, Any],
182
+ package_name: str,
183
+ pipeline_script_code: str,
184
+ ) -> dict[str, Any]:
185
+ app = project["app"]
186
+ readme_config = config_section("readme")
187
+ package_owner = str(app.get("package_owner") or package_name).strip() or package_name
188
+ package_owner_lower = package_owner.lower()
189
+ package_name_slug = package_name.replace("_", "-")
190
+ repo_url = f"https://github.com/{package_owner}/{package_name}"
191
+ docs_url = f"https://{package_owner_lower}.github.io/{package_name}/quick_start"
192
+ workflow_url = f"{repo_url}/actions/workflows/tests.yml"
193
+ workflow_runs_url = f"https://api.github.com/repos/{package_owner}/{package_name}/actions/workflows/tests.yml/runs?per_page=1&status=completed"
194
+ display_title = str(app.get("name") or package_name).strip() or package_name
195
+ description = str(app.get("description") or "").strip()
196
+ badges = build_readme_badges()
197
+ socials = build_readme_social_links(app.get("social"))
198
+ return {
199
+ "title": display_title,
200
+ "project_line": display_title,
201
+ "description": description,
202
+ "package_owner": package_owner,
203
+ "package_owner_lower": package_owner_lower,
204
+ "package_name": package_name,
205
+ "package_name_slug": package_name_slug,
206
+ "repo_url": repo_url,
207
+ "issues_url": f"{repo_url}/issues",
208
+ "docs_url": docs_url,
209
+ "workflow_url": workflow_url,
210
+ "workflow_badge_url": f"{workflow_url}/badge.svg",
211
+ "workflow_last_run_badge_url": (
212
+ "https://img.shields.io/badge/dynamic/json?label=Tests%20last%20run"
213
+ "&query=%24.workflow_runs%5B0%5D.updated_at"
214
+ f"&url={quote(workflow_runs_url, safe='')}"
215
+ "&logo=githubactions&cacheSeconds=300"
216
+ ),
217
+ "pypi_project_url": f"https://pypi.org/project/{package_name_slug}/",
218
+ "pypi_python_badge_url": f"https://img.shields.io/pypi/pyversions/{package_name}",
219
+ "pypi_version_badge_url": f"https://img.shields.io/pypi/v/{package_name}?color=blue",
220
+ "pypi_downloads_badge_url": f"https://img.shields.io/pypi/dm/{package_name}?label=PyPi%20downloads",
221
+ "license_badge_url": f"https://img.shields.io/github/license/{package_owner}/{package_name}",
222
+ "license_url": f"{repo_url}/blob/main/LICENSE",
223
+ "badges": badges,
224
+ "socials": socials,
225
+ "principle_text": str(readme_config.get("principle_text", "Библиотека полностью повторяет сетевую работу обычного пользователя на сайте.")),
226
+ "pipeline_script_code": pipeline_script_code,
227
+ }
228
+
229
+
230
+ def build_readme_tests_context(
231
+ project: dict[str, Any],
232
+ package_name: str,
233
+ tests_context: dict[str, Any] | None,
234
+ ) -> dict[str, Any]:
235
+ client_class_name = root_client_class_name(project)
236
+ has_autotests = False
237
+ if isinstance(tests_context, dict):
238
+ api_test = tests_context.get("api_test")
239
+ if isinstance(api_test, dict):
240
+ has_autotests = any(bool(api_test.get(key)) for key in ("hooks", "providers", "manual_tests", "data_cases"))
241
+ return {
242
+ "has_autotests": has_autotests,
243
+ "autotest_start_class": f"{package_name}.{client_class_name}",
244
+ }
245
+
246
+
247
+ def build_readme_social_links(social: Any) -> list[dict[str, Any]]:
248
+ if not isinstance(social, dict):
249
+ return []
250
+ readme_socials = config_section("readme").get("socials", [])
251
+ links: list[dict[str, Any]] = []
252
+ if not isinstance(readme_socials, list):
253
+ return links
254
+ for item in readme_socials:
255
+ if not isinstance(item, dict):
256
+ continue
257
+ key = str(item.get("key") or "").strip()
258
+ if not key:
259
+ continue
260
+ url = str(social.get(key) or "").strip()
261
+ if not url:
262
+ continue
263
+ badge_url = str(item.get("badge_url", "")).strip()
264
+ if not badge_url:
265
+ continue
266
+ links.append(
267
+ {
268
+ "key": key,
269
+ "label": str(item.get("label", key.title())),
270
+ "badge_url": badge_url,
271
+ "url": url,
272
+ }
273
+ )
274
+ return links
275
+
276
+
277
+ def build_readme_badges() -> list[dict[str, Any]]:
278
+ readme_badges = config_section("readme").get("badges", [])
279
+ if not isinstance(readme_badges, list):
280
+ return []
281
+ badges: list[dict[str, Any]] = []
282
+ for item in readme_badges:
283
+ if not isinstance(item, dict):
284
+ continue
285
+ label = str(item.get("label", "")).strip()
286
+ badge_url = str(item.get("badge_url", "")).strip()
287
+ url = str(item.get("url", "")).strip()
288
+ if not label or not badge_url or not url:
289
+ continue
290
+ badges.append(
291
+ {
292
+ "label": label,
293
+ "badge_url": badge_url,
294
+ "url": url,
295
+ }
296
+ )
297
+ return badges
298
+
299
+
300
+ def build_group_docs_context(
301
+ group_node: dict[str, Any],
302
+ project: dict[str, Any],
303
+ package_name: str,
304
+ ) -> dict[str, Any]:
305
+ group_context = build_group_context(group_node, project, package_name)
306
+ child_nodes = list(group_node.get("children", {}).values())
307
+ child_class_names = [child["class_name"] for child in group_context["child_imports"]]
308
+ class_names = [group_context["class_name"], *child_class_names]
309
+ import_path = group_public_import_path(package_name, group_node.get("path", []))
310
+ context = build_module_page_context(
311
+ title=import_path,
312
+ import_path=import_path,
313
+ description=group_context["description"],
314
+ class_names=list(dict.fromkeys(class_names)),
315
+ child_pages=[group_public_import_path(package_name, child["path"]) for child in child_nodes],
316
+ )
317
+ context.update(
318
+ {
319
+ "class_name": group_context["class_name"],
320
+ "child_class_names": list(dict.fromkeys(child_class_names)),
321
+ "field_name": group_node["path"][-1] if group_node.get("path") else "Group",
322
+ }
323
+ )
324
+ return context
325
+
326
+
327
+ def build_module_page_context(
328
+ *,
329
+ title: str,
330
+ import_path: str,
331
+ description: str,
332
+ class_names: list[str],
333
+ child_pages: list[str],
334
+ ) -> dict[str, Any]:
335
+ class_names = list(dict.fromkeys(class_names))
336
+ child_pages = list(dict.fromkeys(child_pages))
337
+ return {
338
+ "title": title,
339
+ "title_underline": "=" * len(title),
340
+ "import_path": import_path,
341
+ "description": description,
342
+ "class_names": class_names,
343
+ "class_refs": [f"{import_path}.{name}" for name in class_names],
344
+ "child_pages": child_pages,
345
+ "child_docnames": child_pages,
346
+ }
@@ -0,0 +1,8 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+
5
+
6
+ def write_text(path: Path, content: str) -> None:
7
+ path.parent.mkdir(parents=True, exist_ok=True)
8
+ path.write_text(content, encoding="utf-8")