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
msra_codegen/config.toml
ADDED
|
@@ -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
|
+
}
|