tooaio-scripts 1.0.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.
- src/__init__.py +0 -0
- src/build_release_git_remote.py +200 -0
- src/openapi_doc.py +12 -0
- src/openapi_to_pdf.py +742 -0
- src/pydantic_schema_doc.py +333 -0
- tooaio_scripts-1.0.0.dist-info/METADATA +15 -0
- tooaio_scripts-1.0.0.dist-info/RECORD +9 -0
- tooaio_scripts-1.0.0.dist-info/WHEEL +4 -0
- tooaio_scripts-1.0.0.dist-info/licenses/LICENSE +24 -0
src/__init__.py
ADDED
|
File without changes
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
import subprocess
|
|
3
|
+
import shutil
|
|
4
|
+
from collections.abc import Callable
|
|
5
|
+
from git import Repo, rmtree
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ReleaseSafeUtils:
|
|
9
|
+
def __init__(self, release_path: Path) -> None:
|
|
10
|
+
self.release_path = release_path
|
|
11
|
+
|
|
12
|
+
def __check_path_in_release(self, path: Path) -> None:
|
|
13
|
+
if not path.resolve().is_relative_to(self.release_path.resolve()):
|
|
14
|
+
err_msg = f"Путь {path.resolve()} должен быть внутри папки релиза {self.release_path.resolve()}"
|
|
15
|
+
raise Exception(err_msg)
|
|
16
|
+
|
|
17
|
+
def __check_path_not_nested(self, *, not_nested: Path, in_path: Path) -> None:
|
|
18
|
+
if not_nested.resolve().is_relative_to(in_path.resolve()):
|
|
19
|
+
err_msg = f"Путь {not_nested.resolve()} не должен быть внутри папки {in_path.resolve()}"
|
|
20
|
+
raise Exception(err_msg)
|
|
21
|
+
|
|
22
|
+
def mkdir_safe(self, path: Path, *, exist_ok: bool = False) -> None:
|
|
23
|
+
self.__check_path_in_release(path)
|
|
24
|
+
path.mkdir(exist_ok=exist_ok)
|
|
25
|
+
|
|
26
|
+
def rmtree_safe(self, path: Path) -> None:
|
|
27
|
+
self.__check_path_in_release(path)
|
|
28
|
+
rmtree(path.resolve())
|
|
29
|
+
|
|
30
|
+
def copytree_safe(
|
|
31
|
+
self, src: Path, dst: Path, *, ignore: Callable[[str, list[str]], list[str]] | None = None
|
|
32
|
+
) -> None:
|
|
33
|
+
self.__check_path_in_release(dst)
|
|
34
|
+
self.__check_path_not_nested(not_nested=dst, in_path=src)
|
|
35
|
+
shutil.copytree(src.resolve(), dst.resolve(), ignore=ignore)
|
|
36
|
+
|
|
37
|
+
def copyfile_safe(self, src: Path, dst: Path) -> None:
|
|
38
|
+
self.__check_path_in_release(dst)
|
|
39
|
+
shutil.copyfile(src.resolve(), dst.resolve())
|
|
40
|
+
|
|
41
|
+
def copy_repo_files_safe(self, src_repo: Repo, dst_path: Path) -> None:
|
|
42
|
+
dst_path = dst_path.resolve()
|
|
43
|
+
|
|
44
|
+
if dst_path.exists():
|
|
45
|
+
self.rmtree_safe(dst_path)
|
|
46
|
+
|
|
47
|
+
dst_repo = src_repo.clone(dst_path)
|
|
48
|
+
self.rmtree_safe(Path(dst_repo.git_dir).resolve())
|
|
49
|
+
|
|
50
|
+
def zip_dir_safe(self, *, src_dir: Path, dst_zipfile: Path) -> None:
|
|
51
|
+
self.__check_path_not_nested(not_nested=dst_zipfile, in_path=src_dir)
|
|
52
|
+
res_file = shutil.make_archive(
|
|
53
|
+
base_name=str(dst_zipfile.with_suffix("")),
|
|
54
|
+
format="zip",
|
|
55
|
+
root_dir=str(src_dir),
|
|
56
|
+
)
|
|
57
|
+
print(res_file)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def build_release_git_remote(
|
|
61
|
+
*,
|
|
62
|
+
dev_repo_path: Path,
|
|
63
|
+
release_repo_url: str,
|
|
64
|
+
release_path: Path,
|
|
65
|
+
release_dev_repo_path: Path,
|
|
66
|
+
func_before_dev_repo_clone: Callable[[ReleaseSafeUtils], None] | None = None,
|
|
67
|
+
func_after_dev_repo_clone: Callable[[ReleaseSafeUtils], None] | None = None,
|
|
68
|
+
) -> ReleaseSafeUtils:
|
|
69
|
+
"""
|
|
70
|
+
Создание релиза в формате удаленного репозитория,
|
|
71
|
+
содержащего копию исходного кода из локального репозитория разработчика
|
|
72
|
+
|
|
73
|
+
Удаленный репозиторий - репозиторий на стороне заказчика (например, в среде разработки заказчика)
|
|
74
|
+
|
|
75
|
+
Удаленный репозиторий отличается от локального тем, что кроме исходного кода содержит файлы,\
|
|
76
|
+
специфичные для среды заказчика. Например: Dockerfile, предопределенные файлы конфигурации
|
|
77
|
+
|
|
78
|
+
Аргументы:
|
|
79
|
+
dev_repo_path - путь к локальному репозиторию разработчика, например "./"
|
|
80
|
+
|
|
81
|
+
release_repo_url - url удаленного репозитория
|
|
82
|
+
|
|
83
|
+
release_path - путь к локальной папке, в которой должен быть создан релиз, например "./.release"
|
|
84
|
+
|
|
85
|
+
release_dev_repo_path - путь к папке, в которой должна находится копия исходного кода
|
|
86
|
+
из локального репозитозитория резработчика, например "./release/src"
|
|
87
|
+
Путь, указанный в release_dev_repo_path, должен находится внути пути release_path
|
|
88
|
+
|
|
89
|
+
func_before_dev_repo_clone - функция, выполняющаяся перед копированием исходного кода
|
|
90
|
+
из локального репозитория разработчика в релиз.
|
|
91
|
+
Используется, например, для автообновления документации перед релизом
|
|
92
|
+
|
|
93
|
+
func_after_dev_repo_clone - функция, выполняющаяся после копирования исходного кода
|
|
94
|
+
из локального репозитория разработчика в релиз.
|
|
95
|
+
Используется, например, для добавления доп. артефактов в релиз
|
|
96
|
+
"""
|
|
97
|
+
|
|
98
|
+
dev_repo_path = dev_repo_path.resolve()
|
|
99
|
+
release_path = release_path.resolve()
|
|
100
|
+
release_dev_repo_path = release_dev_repo_path.resolve()
|
|
101
|
+
release_safe_utils = ReleaseSafeUtils(release_path=release_path)
|
|
102
|
+
uv_path = shutil.which("uv")
|
|
103
|
+
|
|
104
|
+
########################################
|
|
105
|
+
if release_path == dev_repo_path:
|
|
106
|
+
err_msg = f"Путь в аргументе release_path ({release_path}) не может совпадать с dev_repo_path ({dev_repo_path})"
|
|
107
|
+
raise Exception(err_msg)
|
|
108
|
+
|
|
109
|
+
if dev_repo_path.is_relative_to(release_path):
|
|
110
|
+
err_msg = f"Путь в аргументе dev_repo_path ({dev_repo_path}) не может быть внутри release_path ({release_path})"
|
|
111
|
+
raise Exception(err_msg)
|
|
112
|
+
|
|
113
|
+
if not release_path.is_relative_to(dev_repo_path):
|
|
114
|
+
err_msg = f"Путь в аргументе release_path ({release_path}) должен быть внутри dev_repo_path ({dev_repo_path})"
|
|
115
|
+
raise Exception(err_msg)
|
|
116
|
+
|
|
117
|
+
if not release_dev_repo_path.is_relative_to(release_path):
|
|
118
|
+
err_msg = (
|
|
119
|
+
f"Путь в аргументе release_dev_repo_path ({release_dev_repo_path})"
|
|
120
|
+
f" должен быть внутри release_path ({release_path})"
|
|
121
|
+
)
|
|
122
|
+
raise Exception(err_msg)
|
|
123
|
+
|
|
124
|
+
if not uv_path:
|
|
125
|
+
err_msg = "не найден uv"
|
|
126
|
+
raise Exception(err_msg)
|
|
127
|
+
|
|
128
|
+
########################################
|
|
129
|
+
Path.mkdir(release_path, exist_ok=True)
|
|
130
|
+
|
|
131
|
+
if not (release_path / ".git").exists():
|
|
132
|
+
print(f"Клонирование удаленного репозитория {release_repo_url} (clone --depth=1 --no-single-branch)")
|
|
133
|
+
Repo.clone_from(release_repo_url, release_path, allow_unsafe_protocols=True, depth=1, no_single_branch=True)
|
|
134
|
+
|
|
135
|
+
########################################
|
|
136
|
+
dev_repo = Repo(dev_repo_path)
|
|
137
|
+
release_repo = Repo(release_path)
|
|
138
|
+
|
|
139
|
+
print(f"Загрузка новых изменений и веток из удаленного репозитория {release_repo_url} (fetch --depth=1 )")
|
|
140
|
+
fetch_res = release_repo.remotes.origin.fetch(allow_unsafe_protocols=True, depth=1)
|
|
141
|
+
for fetch_info in fetch_res:
|
|
142
|
+
print(f"Загружена ветка {fetch_info.ref}")
|
|
143
|
+
|
|
144
|
+
if dev_repo.active_branch.name != release_repo.active_branch.name:
|
|
145
|
+
if dev_repo.active_branch.name in release_repo.remotes.origin.refs:
|
|
146
|
+
print(f"Переключение на удаленную ветку {dev_repo.active_branch}")
|
|
147
|
+
release_repo.git.switch(dev_repo.active_branch.name)
|
|
148
|
+
release_repo.active_branch.set_tracking_branch(
|
|
149
|
+
release_repo.remotes.origin.refs[dev_repo.active_branch.name]
|
|
150
|
+
)
|
|
151
|
+
elif dev_repo.active_branch.name in release_repo.heads:
|
|
152
|
+
print(f"Переключение на локальную ветку {dev_repo.active_branch}")
|
|
153
|
+
release_repo.git.switch(dev_repo.active_branch.name)
|
|
154
|
+
else:
|
|
155
|
+
print(f"Создание новой локальной ветки {dev_repo.active_branch}")
|
|
156
|
+
release_repo.git.switch("-c", dev_repo.active_branch.name)
|
|
157
|
+
|
|
158
|
+
if release_repo.active_branch.is_remote():
|
|
159
|
+
print(f"Вытягивание изменений из удаленного репозитория {release_repo_url} (pull --ff-only --depth 1)")
|
|
160
|
+
pull_res = release_repo.remotes.origin.pull(allow_unsafe_protocols=True, ff_only=True, depth=1)
|
|
161
|
+
for pull_info in pull_res:
|
|
162
|
+
print(f"Обновлена ветка {pull_info.ref}")
|
|
163
|
+
|
|
164
|
+
########################################
|
|
165
|
+
print(f"Создание requirements.txt в {release_path}")
|
|
166
|
+
print(
|
|
167
|
+
subprocess.check_output( # noqa S603
|
|
168
|
+
[
|
|
169
|
+
uv_path,
|
|
170
|
+
"export",
|
|
171
|
+
"--format",
|
|
172
|
+
"requirements.txt",
|
|
173
|
+
"-o",
|
|
174
|
+
release_path / "requirements.txt",
|
|
175
|
+
"--no-dev",
|
|
176
|
+
"--quiet",
|
|
177
|
+
"--frozen",
|
|
178
|
+
"--no-hashes",
|
|
179
|
+
"--no-editable",
|
|
180
|
+
"--no-emit-local",
|
|
181
|
+
],
|
|
182
|
+
text=True,
|
|
183
|
+
cwd=Path.cwd(),
|
|
184
|
+
)
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
########################################
|
|
188
|
+
if func_before_dev_repo_clone:
|
|
189
|
+
func_before_dev_repo_clone(release_safe_utils)
|
|
190
|
+
########################################
|
|
191
|
+
|
|
192
|
+
print(f"Копирование файлов локального репозитория в {release_dev_repo_path}")
|
|
193
|
+
release_safe_utils.copy_repo_files_safe(dev_repo, release_dev_repo_path)
|
|
194
|
+
|
|
195
|
+
########################################
|
|
196
|
+
if func_after_dev_repo_clone:
|
|
197
|
+
func_after_dev_repo_clone(release_safe_utils)
|
|
198
|
+
########################################
|
|
199
|
+
|
|
200
|
+
return release_safe_utils
|
src/openapi_doc.py
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
from .openapi_to_pdf import build_pdf
|
|
3
|
+
import json
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def openapi_to_json(openapi_dict: dict[str, Any], output_path: str) -> None:
|
|
7
|
+
with open(output_path, "w", encoding="utf-8") as f:
|
|
8
|
+
json.dump(openapi_dict, f, ensure_ascii=False, indent=2)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def openapi_to_pdf(openapi_dict: dict[str, Any], output_path: str) -> None:
|
|
12
|
+
build_pdf(openapi_dict, output_path)
|
src/openapi_to_pdf.py
ADDED
|
@@ -0,0 +1,742 @@
|
|
|
1
|
+
from typing import Any, Self
|
|
2
|
+
|
|
3
|
+
from reportlab.lib.pagesizes import A4
|
|
4
|
+
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
|
|
5
|
+
from reportlab.lib.units import mm
|
|
6
|
+
from reportlab.lib.colors import Color, HexColor, white, black
|
|
7
|
+
from reportlab.lib.enums import TA_CENTER, TA_JUSTIFY
|
|
8
|
+
|
|
9
|
+
from reportlab.platypus import (
|
|
10
|
+
Flowable,
|
|
11
|
+
SimpleDocTemplate,
|
|
12
|
+
Paragraph,
|
|
13
|
+
Spacer,
|
|
14
|
+
Table,
|
|
15
|
+
TableStyle,
|
|
16
|
+
PageBreak,
|
|
17
|
+
# HRFlowable,
|
|
18
|
+
XPreformatted,
|
|
19
|
+
KeepTogether,
|
|
20
|
+
IndexingFlowable,
|
|
21
|
+
)
|
|
22
|
+
from reportlab.platypus.flowables import KeepTogetherSplitAtTop
|
|
23
|
+
from reportlab.pdfbase import pdfmetrics
|
|
24
|
+
from reportlab.pdfbase.ttfonts import TTFont
|
|
25
|
+
import os
|
|
26
|
+
import glob
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def openapi_i18n(term: str) -> str:
|
|
30
|
+
voc = {
|
|
31
|
+
"Successful Response": "Успешный ответ",
|
|
32
|
+
"Validation Error": "Ошибка валидации",
|
|
33
|
+
}
|
|
34
|
+
return voc.get(term.strip(), term)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def create_default_font() -> str:
|
|
38
|
+
script_folder = os.path.dirname(__file__)
|
|
39
|
+
font_dirs = [script_folder]
|
|
40
|
+
found = {}
|
|
41
|
+
for fd in font_dirs:
|
|
42
|
+
for f in glob.glob(os.path.join(fd, "../assets/**/*.ttf"), recursive=True):
|
|
43
|
+
name = os.path.basename(f).lower()
|
|
44
|
+
found[name] = f
|
|
45
|
+
|
|
46
|
+
regular = found.get("KurintoSans-Rg.ttf".lower())
|
|
47
|
+
bold = found.get("KurintoSans-Bd.ttf".lower())
|
|
48
|
+
italic = found.get("KurintoSansSC-It.ttf".lower())
|
|
49
|
+
bolditalic = found.get("KurintoSans-BdIt.ttf".lower())
|
|
50
|
+
|
|
51
|
+
if regular:
|
|
52
|
+
pdfmetrics.registerFont(TTFont("DefaultFont", regular))
|
|
53
|
+
pdfmetrics.registerFont(TTFont("DefaultFont-Bold", bold or regular))
|
|
54
|
+
pdfmetrics.registerFont(TTFont("DefaultFont-Italic", italic or regular))
|
|
55
|
+
pdfmetrics.registerFont(TTFont("DefaultFont-BoldItalic", bolditalic or regular))
|
|
56
|
+
from reportlab.lib.fonts import addMapping
|
|
57
|
+
|
|
58
|
+
addMapping("DefaultFont", 0, 0, "DefaultFont")
|
|
59
|
+
addMapping("DefaultFont-Bold", 1, 0, "DefaultFont-Bold")
|
|
60
|
+
addMapping("DefaultFont-Italic", 0, 1, "DefaultFont-Italic")
|
|
61
|
+
addMapping("DefaultFont-BoldItalic", 1, 1, "DefaultFont-BoldItalic")
|
|
62
|
+
return "DefaultFont"
|
|
63
|
+
|
|
64
|
+
return "Helvetica"
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
DEFAULT_FONT = create_default_font()
|
|
68
|
+
|
|
69
|
+
# --- Colors ---
|
|
70
|
+
PRIMARY = HexColor("#1a3a5c")
|
|
71
|
+
ACCENT = HexColor("#2980b9")
|
|
72
|
+
LIGHT_BG = HexColor("#f0f4f8")
|
|
73
|
+
BORDER = HexColor("#bdc3c7")
|
|
74
|
+
METHOD_POST = HexColor("#49cc90")
|
|
75
|
+
METHOD_POST_BG = HexColor("#e8f8f0")
|
|
76
|
+
TYPE_COLOR = HexColor("#e67e22")
|
|
77
|
+
REQUIRED_COLOR = HexColor("#e74c3c")
|
|
78
|
+
GRAY_TEXT = HexColor("#7f8c8d")
|
|
79
|
+
LINK_TEXT = HexColor("#2980b9")
|
|
80
|
+
|
|
81
|
+
# --- Styles ---
|
|
82
|
+
styles = getSampleStyleSheet()
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def make_style(name: str, parent: str = "Normal", **kwargs: Any) -> ParagraphStyle: # noqa ANN401
|
|
86
|
+
base = styles[parent]
|
|
87
|
+
return ParagraphStyle(
|
|
88
|
+
name,
|
|
89
|
+
parent=base,
|
|
90
|
+
fontName=kwargs.get("fontName", DEFAULT_FONT),
|
|
91
|
+
**{k: v for k, v in kwargs.items() if k != "fontName"},
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
title_style = make_style(
|
|
96
|
+
"DocTitle",
|
|
97
|
+
fontSize=26,
|
|
98
|
+
leading=32,
|
|
99
|
+
textColor=PRIMARY,
|
|
100
|
+
fontName=f"{DEFAULT_FONT}-Bold" if DEFAULT_FONT != "Helvetica" else "Helvetica-Bold",
|
|
101
|
+
alignment=TA_CENTER,
|
|
102
|
+
spaceAfter=6,
|
|
103
|
+
)
|
|
104
|
+
subtitle_style = make_style(
|
|
105
|
+
"DocSubtitle", fontSize=12, leading=16, textColor=GRAY_TEXT, alignment=TA_CENTER, spaceAfter=30
|
|
106
|
+
)
|
|
107
|
+
h1_style = make_style(
|
|
108
|
+
"H1Custom",
|
|
109
|
+
fontSize=18,
|
|
110
|
+
leading=22,
|
|
111
|
+
textColor=PRIMARY,
|
|
112
|
+
fontName=f"{DEFAULT_FONT}-Bold" if DEFAULT_FONT != "Helvetica" else "Helvetica-Bold",
|
|
113
|
+
spaceBefore=20,
|
|
114
|
+
spaceAfter=10,
|
|
115
|
+
)
|
|
116
|
+
h2_style = make_style(
|
|
117
|
+
"H2Custom",
|
|
118
|
+
fontSize=14,
|
|
119
|
+
leading=18,
|
|
120
|
+
textColor=ACCENT,
|
|
121
|
+
fontName=f"{DEFAULT_FONT}-Bold" if DEFAULT_FONT != "Helvetica" else "Helvetica-Bold",
|
|
122
|
+
spaceBefore=14,
|
|
123
|
+
spaceAfter=6,
|
|
124
|
+
)
|
|
125
|
+
h3_style = make_style(
|
|
126
|
+
"H3Custom",
|
|
127
|
+
fontSize=11,
|
|
128
|
+
leading=14,
|
|
129
|
+
textColor=PRIMARY,
|
|
130
|
+
fontName=f"{DEFAULT_FONT}-Bold" if DEFAULT_FONT != "Helvetica" else "Helvetica-Bold",
|
|
131
|
+
spaceBefore=10,
|
|
132
|
+
spaceAfter=4,
|
|
133
|
+
)
|
|
134
|
+
body_style = make_style("BodyCustom", fontSize=9.5, leading=13, textColor=black, spaceAfter=4, alignment=TA_JUSTIFY)
|
|
135
|
+
small_style = make_style("SmallCustom", fontSize=8, leading=10, textColor=GRAY_TEXT)
|
|
136
|
+
code_style = make_style(
|
|
137
|
+
"CodeCustom",
|
|
138
|
+
fontSize=8.5,
|
|
139
|
+
leading=11,
|
|
140
|
+
textColor=HexColor("#2c3e50"),
|
|
141
|
+
backColor=LIGHT_BG,
|
|
142
|
+
leftIndent=10,
|
|
143
|
+
rightIndent=10,
|
|
144
|
+
spaceBefore=4,
|
|
145
|
+
spaceAfter=4,
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
link_style = make_style("LinkCustom", fontSize=8, leading=10, textColor=GRAY_TEXT)
|
|
149
|
+
|
|
150
|
+
bold_font = f"{DEFAULT_FONT}-Bold" if DEFAULT_FONT != "Helvetica" else "Helvetica-Bold"
|
|
151
|
+
|
|
152
|
+
BOLD = bold_font
|
|
153
|
+
REGULAR = DEFAULT_FONT
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
class DocSummaryFlowable(IndexingFlowable):
|
|
157
|
+
def __init__(self) -> None:
|
|
158
|
+
super().__init__()
|
|
159
|
+
self.page_count = -1
|
|
160
|
+
self._page_count = -1
|
|
161
|
+
self.target = self.__class__.__qualname__
|
|
162
|
+
|
|
163
|
+
def isSatisfied(self) -> int:
|
|
164
|
+
return self._page_count > -1
|
|
165
|
+
|
|
166
|
+
def notify(self, kind: str, stuff: Any) -> None: # noqa ANN401
|
|
167
|
+
if kind != self.target:
|
|
168
|
+
return
|
|
169
|
+
self.page_count = stuff.get("page_count", 0)
|
|
170
|
+
|
|
171
|
+
def beforeBuild(self) -> None:
|
|
172
|
+
self._page_count = self.page_count
|
|
173
|
+
|
|
174
|
+
def draw(self) -> None:
|
|
175
|
+
return
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
class CustomDocTemplateHeaderFooter:
|
|
179
|
+
def __init__(self, header_left: str, footer_left: str) -> None:
|
|
180
|
+
self.header_left = header_left
|
|
181
|
+
self.footer_left = footer_left
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
class CustomDocTemplate(SimpleDocTemplate):
|
|
185
|
+
def __init__(
|
|
186
|
+
self,
|
|
187
|
+
*args: Any, # noqa ANN401
|
|
188
|
+
header_footer_tmpl: CustomDocTemplateHeaderFooter,
|
|
189
|
+
**kwargs: Any, # noqa ANN401
|
|
190
|
+
) -> None:
|
|
191
|
+
super().__init__(*args, **kwargs)
|
|
192
|
+
self.header_footer_tmpl = header_footer_tmpl
|
|
193
|
+
self.doc_summary = DocSummaryFlowable()
|
|
194
|
+
|
|
195
|
+
def afterPage(self) -> None:
|
|
196
|
+
if self.page > self.doc_summary.page_count:
|
|
197
|
+
self.notify(self.doc_summary.target, {"page_count": self.page})
|
|
198
|
+
|
|
199
|
+
self.draw_header_footer()
|
|
200
|
+
|
|
201
|
+
def draw_header_footer(self) -> None:
|
|
202
|
+
doc = self
|
|
203
|
+
canvas = self.canv
|
|
204
|
+
canvas.saveState()
|
|
205
|
+
canvas.setStrokeColor(PRIMARY)
|
|
206
|
+
|
|
207
|
+
# Header
|
|
208
|
+
canvas.setLineWidth(0.5)
|
|
209
|
+
canvas.line(20 * mm, A4[1] - 15 * mm, A4[0] - 20 * mm, A4[1] - 15 * mm)
|
|
210
|
+
canvas.setFont(REGULAR, 7)
|
|
211
|
+
canvas.setFillColor(GRAY_TEXT)
|
|
212
|
+
canvas.drawString(20 * mm, A4[1] - 13 * mm, self.header_footer_tmpl.header_left or "")
|
|
213
|
+
|
|
214
|
+
# Footer
|
|
215
|
+
canvas.line(20 * mm, 15 * mm, A4[0] - 20 * mm, 15 * mm)
|
|
216
|
+
canvas.drawRightString(A4[0] - 20 * mm, 10 * mm, f"Страница {doc.page} из {doc.doc_summary.page_count}")
|
|
217
|
+
canvas.drawString(20 * mm, 10 * mm, self.header_footer_tmpl.footer_left or "")
|
|
218
|
+
canvas.restoreState()
|
|
219
|
+
|
|
220
|
+
def customBuild(self, story: list[Flowable]) -> None:
|
|
221
|
+
story.append(self.doc_summary)
|
|
222
|
+
super().multiBuild(story)
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
class BookmarkFlowable(Flowable):
|
|
226
|
+
title: str
|
|
227
|
+
key: str
|
|
228
|
+
level: int
|
|
229
|
+
|
|
230
|
+
def __init__(self, title: str, key: str, level: int) -> None:
|
|
231
|
+
super().__init__()
|
|
232
|
+
self.title = title
|
|
233
|
+
self.key = key
|
|
234
|
+
self.level = level
|
|
235
|
+
|
|
236
|
+
def draw(self) -> None:
|
|
237
|
+
curMatrixField = "_currentMatrix"
|
|
238
|
+
curMatrix = (getattr(self.canv, curMatrixField) or (0,)) + (0,) * 16
|
|
239
|
+
self.canv.bookmarkPage(self.key, fit="XYZ", left=0, top=curMatrix[5], zoom=None)
|
|
240
|
+
self.canv.addOutlineEntry(self.title, self.key, level=self.level, closed=False)
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
class TOC_ITEM:
|
|
244
|
+
num: int
|
|
245
|
+
title: str
|
|
246
|
+
level: int
|
|
247
|
+
bookmark: BookmarkFlowable
|
|
248
|
+
parent_item: Self | None
|
|
249
|
+
nested_toc: list[Self]
|
|
250
|
+
full_num: str
|
|
251
|
+
|
|
252
|
+
def __init__(self, parent_item: Self | None, title: str) -> None:
|
|
253
|
+
self.parent_item = parent_item
|
|
254
|
+
self.title = title
|
|
255
|
+
prev_item: Self | None = (
|
|
256
|
+
(self.parent_item.nested_toc)[-1] if self.parent_item and self.parent_item.nested_toc else None
|
|
257
|
+
)
|
|
258
|
+
self.nested_toc = []
|
|
259
|
+
self.num = (prev_item.num if prev_item else 0) + 1
|
|
260
|
+
|
|
261
|
+
if self.parent_item:
|
|
262
|
+
self.parent_item.nested_toc += [self]
|
|
263
|
+
|
|
264
|
+
self.level = 0
|
|
265
|
+
self.full_num = str(self.num)
|
|
266
|
+
self.key = str(self.num)
|
|
267
|
+
item = self.parent_item
|
|
268
|
+
while item and item.parent_item:
|
|
269
|
+
self.level += 1
|
|
270
|
+
self.full_num = str(item.num) + "." + self.full_num
|
|
271
|
+
self.key = str(item.num) + "_" + self.key
|
|
272
|
+
item = item.parent_item
|
|
273
|
+
|
|
274
|
+
self.key = "section_" + self.key
|
|
275
|
+
|
|
276
|
+
self.bookmark = BookmarkFlowable(title=self.title, key=self.key, level=self.level)
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def add_to_toc(story: list[Flowable], parent_item: TOC_ITEM, title: str) -> TOC_ITEM:
|
|
280
|
+
toc_item = TOC_ITEM(parent_item=parent_item, title=title)
|
|
281
|
+
story.append(toc_item.bookmark)
|
|
282
|
+
return toc_item
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def bold(text: str) -> str:
|
|
286
|
+
return f'<font name="{BOLD}">{text}</font>'
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def colored(text: str, color: Color) -> str:
|
|
290
|
+
return f'<font color="{color}">{text}</font>'
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def method_badge(method: str) -> str:
|
|
294
|
+
return f'<font name="{BOLD}" color="#ffffff" backColor="{METHOD_POST}"> {method.upper()} </font>'
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def bookmark_link(key: str, content: str) -> str:
|
|
298
|
+
return f'<a href="#{key}">{content}</a>'
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def model_link_name(model_name: str) -> str:
|
|
302
|
+
return f'<a name="models_{model_name}"/>' if model_name else ""
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def model_link_href(model_ref: str) -> str:
|
|
306
|
+
model_name = (model_ref or "").split("/")[-1]
|
|
307
|
+
return f'<a href="#models_{model_name}">{model_name}</a>' if model_ref else ""
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def section_toc(doc_toc: TOC_ITEM) -> list[Flowable]:
|
|
311
|
+
story: list[Flowable] = []
|
|
312
|
+
story.append(PageBreak())
|
|
313
|
+
story.append(Paragraph("Содержание", h1_style))
|
|
314
|
+
story.append(Spacer(1, 10))
|
|
315
|
+
|
|
316
|
+
def toc_walker(parent_item: TOC_ITEM, level: int) -> None:
|
|
317
|
+
indent = 20 * level
|
|
318
|
+
style = make_style(f"TOC{level}", fontSize=10, leading=16, leftIndent=indent)
|
|
319
|
+
for toc_item in parent_item.nested_toc:
|
|
320
|
+
story.append(Paragraph(f"{bold(toc_item.full_num)} {bookmark_link(toc_item.key, toc_item.title)}", style))
|
|
321
|
+
toc_walker(toc_item, level + 1)
|
|
322
|
+
|
|
323
|
+
toc_walker(parent_item=doc_toc, level=0)
|
|
324
|
+
|
|
325
|
+
return story
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def section_title(openapi_info: dict[str, Any]) -> list[Flowable]:
|
|
329
|
+
story: list[Flowable] = []
|
|
330
|
+
story.append(Spacer(1, 60))
|
|
331
|
+
story.append(Paragraph(openapi_info.get("title", ""), title_style))
|
|
332
|
+
# story.append(Paragraph("Документация REST API v1.0.0", subtitle_style))
|
|
333
|
+
# story.append(Spacer(1, 20))
|
|
334
|
+
# story.append(HRFlowable(width="60%", thickness=1, color=ACCENT, spaceAfter=20, spaceBefore=10))
|
|
335
|
+
story.append(
|
|
336
|
+
Paragraph(
|
|
337
|
+
openapi_info.get("description", ""),
|
|
338
|
+
make_style("CenterBody", fontSize=11, leading=15, alignment=TA_CENTER, textColor=PRIMARY),
|
|
339
|
+
)
|
|
340
|
+
)
|
|
341
|
+
story.append(Spacer(1, 10))
|
|
342
|
+
story.append(
|
|
343
|
+
Paragraph(
|
|
344
|
+
f"Версия API: {openapi_info.get('version', 'н/у')}",
|
|
345
|
+
make_style("CenterBody2", fontSize=11, leading=15, alignment=TA_CENTER, textColor=PRIMARY),
|
|
346
|
+
)
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
# story.append(Spacer(1, 40))
|
|
350
|
+
# info_data = [
|
|
351
|
+
# #[Paragraph(bold("Параметр"), small_style), Paragraph(bold("Значение"), small_style)],
|
|
352
|
+
# [],
|
|
353
|
+
# ["Версия API", openapi_info.get("version", "н/у")],
|
|
354
|
+
# # ["Дата создания", "JSON (application/json)"],
|
|
355
|
+
# # ["Дата изменения", "POST"],
|
|
356
|
+
# ]
|
|
357
|
+
# info_table = Table(info_data, colWidths=[150, 300], rowHeights=[0, None])
|
|
358
|
+
# info_table.setStyle(
|
|
359
|
+
# TableStyle(
|
|
360
|
+
# [
|
|
361
|
+
# ("BACKGROUND", (0, 0), (-1, 0), PRIMARY),
|
|
362
|
+
# ("TEXTCOLOR", (0, 0), (-1, 0), white),
|
|
363
|
+
# ("FONTNAME", (0, 0), (-1, -1), REGULAR),
|
|
364
|
+
# ("FONTSIZE", (0, 0), (-1, -1), 9),
|
|
365
|
+
# #("GRID", (0, 0), (-1, -1), 0.5, BORDER),
|
|
366
|
+
# ("ROWBACKGROUNDS", (0, 1), (-1, -1), [white, LIGHT_BG]),
|
|
367
|
+
# ("TOPPADDING", (0, 0), (-1, -1), 5),
|
|
368
|
+
# ("BOTTOMPADDING", (0, 0), (-1, -1), 5),
|
|
369
|
+
# ("LEFTPADDING", (0, 0), (-1, -1), 8),
|
|
370
|
+
# ('ALIGN', (0, 0), (0, -1), 'RIGHT'),
|
|
371
|
+
# ('ALIGN', (1, 0), (-1, -1), 'LEFT'),
|
|
372
|
+
# ]
|
|
373
|
+
# )
|
|
374
|
+
# )
|
|
375
|
+
# story.append(info_table)
|
|
376
|
+
return story
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
def section_overview(parent_toc: TOC_ITEM) -> list[Flowable]:
|
|
380
|
+
return []
|
|
381
|
+
# story: list[Flowable] = []
|
|
382
|
+
# story.append(PageBreak())
|
|
383
|
+
# section_toc = add_to_toc(story, parent_toc, "Обзор API")
|
|
384
|
+
|
|
385
|
+
# story.append(Paragraph(f"{section_toc.full_num} {section_toc.title}", h1_style))
|
|
386
|
+
# story.append(
|
|
387
|
+
# Paragraph(
|
|
388
|
+
# "Данный API обеспечивает автоматизированный обмен данными между системами "
|
|
389
|
+
# "НХТК и НОВАТЭК. Интеграция реализована в формате REST API с передачей данных "
|
|
390
|
+
# "в формате JSON. Все операции используют метод POST.",
|
|
391
|
+
# body_style,
|
|
392
|
+
# )
|
|
393
|
+
# )
|
|
394
|
+
# story.append(Spacer(1, 6))
|
|
395
|
+
# story.append(Paragraph(bold("Основные направления обмена:"), body_style))
|
|
396
|
+
# flows = [
|
|
397
|
+
# ("НОВАТЭК → НХТК", "Передача поручений экспедитору"),
|
|
398
|
+
# ("НХТК → НОВАТЭК", "Паспорта качества, экспедиторские расписки, приемо-сдаточные акты"),
|
|
399
|
+
# ]
|
|
400
|
+
# flow_data = [
|
|
401
|
+
# [Paragraph(bold("Направление"), small_style), Paragraph(bold("Данные"), small_style)],
|
|
402
|
+
# ]
|
|
403
|
+
# for direction, data in flows:
|
|
404
|
+
# flow_data.append(
|
|
405
|
+
# [
|
|
406
|
+
# Paragraph(direction, small_style),
|
|
407
|
+
# Paragraph(data, small_style),
|
|
408
|
+
# ]
|
|
409
|
+
# )
|
|
410
|
+
# ft = Table(flow_data, colWidths=[150, 305])
|
|
411
|
+
# ft.setStyle(
|
|
412
|
+
# TableStyle(
|
|
413
|
+
# [
|
|
414
|
+
# ("BACKGROUND", (0, 0), (-1, 0), ACCENT),
|
|
415
|
+
# ("TEXTCOLOR", (0, 0), (-1, 0), white),
|
|
416
|
+
# ("FONTNAME", (0, 0), (-1, -1), REGULAR),
|
|
417
|
+
# ("FONTSIZE", (0, 0), (-1, -1), 9),
|
|
418
|
+
# ("GRID", (0, 0), (-1, -1), 0.5, BORDER),
|
|
419
|
+
# ("ROWBACKGROUNDS", (0, 1), (-1, -1), [white, LIGHT_BG]),
|
|
420
|
+
# ("TOPPADDING", (0, 0), (-1, -1), 5),
|
|
421
|
+
# ("BOTTOMPADDING", (0, 0), (-1, -1), 5),
|
|
422
|
+
# ("LEFTPADDING", (0, 0), (-1, -1), 8),
|
|
423
|
+
# ]
|
|
424
|
+
# )
|
|
425
|
+
# )
|
|
426
|
+
# story.append(ft)
|
|
427
|
+
# story.append(Spacer(1, 10))
|
|
428
|
+
# return story
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
def section_operations(
|
|
432
|
+
parent_toc: TOC_ITEM, openapi_endpoints: dict[str, Any], openapi_schemas: dict[str, Any]
|
|
433
|
+
) -> list[Flowable]:
|
|
434
|
+
|
|
435
|
+
story: list[Flowable] = []
|
|
436
|
+
story.append(PageBreak())
|
|
437
|
+
|
|
438
|
+
section_toc = add_to_toc(story, parent_toc, title="Операции")
|
|
439
|
+
story.append(Paragraph(f"{section_toc.full_num} {section_toc.title}", h1_style))
|
|
440
|
+
|
|
441
|
+
operations = []
|
|
442
|
+
for endpoint_path, endpoint in openapi_endpoints.items():
|
|
443
|
+
for method, operation in endpoint.items():
|
|
444
|
+
operations.append(
|
|
445
|
+
{
|
|
446
|
+
"path": endpoint_path,
|
|
447
|
+
"method": method.upper(),
|
|
448
|
+
"summary": operation.get("summary", ""),
|
|
449
|
+
"description": operation.get("description", ""),
|
|
450
|
+
"request_body": operation.get("requestBody"),
|
|
451
|
+
"responses": operation.get("responses"),
|
|
452
|
+
}
|
|
453
|
+
)
|
|
454
|
+
|
|
455
|
+
for op in operations:
|
|
456
|
+
story.append(Spacer(1, 8))
|
|
457
|
+
|
|
458
|
+
op_title = op.get("summary", "") or op.get("path", "")
|
|
459
|
+
|
|
460
|
+
op_story_begin: list[Flowable] = []
|
|
461
|
+
op_toc = add_to_toc(op_story_begin, section_toc, op_title)
|
|
462
|
+
|
|
463
|
+
op_story_begin.append(
|
|
464
|
+
Paragraph(f'{op_toc.full_num} <font name="{BOLD}" color="{PRIMARY}">{op_title}</font>', h2_style)
|
|
465
|
+
)
|
|
466
|
+
op_story_begin.append(Paragraph(op.get("description", ""), body_style))
|
|
467
|
+
|
|
468
|
+
op_story_begin.append(Spacer(1, 4))
|
|
469
|
+
|
|
470
|
+
op_story_begin.append(Paragraph(f"Путь: {bold(op['path'])}", body_style))
|
|
471
|
+
op_story_begin.append(Paragraph(f"Метод запроса: {method_badge(op['method'])}", body_style))
|
|
472
|
+
|
|
473
|
+
story.append(KeepTogether(op_story_begin))
|
|
474
|
+
|
|
475
|
+
if (op.get("request_body") or {}).get("content"):
|
|
476
|
+
request_body = op["request_body"]["content"]
|
|
477
|
+
for media_type, media_dict in request_body.items():
|
|
478
|
+
if media_type:
|
|
479
|
+
story.append(Paragraph(f"Формат данных запроса: {bold(media_type)}", body_style))
|
|
480
|
+
schema = media_dict.get("schema", {})
|
|
481
|
+
model_ref = schema.get("$ref", "")
|
|
482
|
+
if model_ref:
|
|
483
|
+
model_link = model_link_href(model_ref)
|
|
484
|
+
story.append(Paragraph(f"Контракт данных запроса: {colored(model_link, LINK_TEXT)}", body_style))
|
|
485
|
+
|
|
486
|
+
##############################################
|
|
487
|
+
story.append(Spacer(1, 6))
|
|
488
|
+
responses: dict[str, Any] = op.get("responses", {})
|
|
489
|
+
story.append(Paragraph(bold("Ответы:"), body_style))
|
|
490
|
+
|
|
491
|
+
for status_code, response in responses.items():
|
|
492
|
+
story.append(Paragraph(f"{bold(openapi_i18n(response.get('description', '')))}", body_style))
|
|
493
|
+
story.append(Paragraph(f"Код статуса: {bold(status_code)}", body_style))
|
|
494
|
+
|
|
495
|
+
if response.get("content"):
|
|
496
|
+
response_body = response["content"]
|
|
497
|
+
for media_type, media_dict in response_body.items():
|
|
498
|
+
if media_type:
|
|
499
|
+
story.append(Paragraph(f"Формат данных ответа: {bold(media_type)}", body_style))
|
|
500
|
+
schema = media_dict.get("schema", {})
|
|
501
|
+
model_ref = schema.get("$ref", "")
|
|
502
|
+
if model_ref:
|
|
503
|
+
model_link = model_link_href(model_ref)
|
|
504
|
+
story.append(Paragraph(f"Контракт данных ответа: {colored(model_link, LINK_TEXT)}", body_style))
|
|
505
|
+
|
|
506
|
+
additional_props = schema.get("additionalProperties", "")
|
|
507
|
+
if not model_ref and additional_props:
|
|
508
|
+
story.append(
|
|
509
|
+
Paragraph(
|
|
510
|
+
f"Контракт данных ответа: {colored(additional_props.get('type', ''), TYPE_COLOR)}",
|
|
511
|
+
body_style,
|
|
512
|
+
)
|
|
513
|
+
)
|
|
514
|
+
|
|
515
|
+
story.append(Spacer(1, 6))
|
|
516
|
+
|
|
517
|
+
story.append(Spacer(1, 6))
|
|
518
|
+
# story.append(HRFlowable(width="100%", thickness=0.5, color=BORDER, spaceAfter=6))
|
|
519
|
+
|
|
520
|
+
return story
|
|
521
|
+
|
|
522
|
+
|
|
523
|
+
def section_models(parent_toc: TOC_ITEM, openapi_schemas: dict[str, Any]) -> list[Flowable]:
|
|
524
|
+
|
|
525
|
+
def build_field_type_span(type_schema: dict[str, Any], level: int = 0) -> str:
|
|
526
|
+
res = []
|
|
527
|
+
type_name = type_schema.get("type", "")
|
|
528
|
+
type_fullname = type_name
|
|
529
|
+
|
|
530
|
+
if type_schema.get("type_union"):
|
|
531
|
+
type_fullname = ""
|
|
532
|
+
type_union = type_schema["type_union"]
|
|
533
|
+
for ti, t in enumerate(type_union, start=1):
|
|
534
|
+
t_span = ""
|
|
535
|
+
t_span += build_field_type_span(t, level + 1 if len(type_union) > 1 else level)
|
|
536
|
+
if ti < len(type_union):
|
|
537
|
+
t_span += ", "
|
|
538
|
+
res.append(t_span)
|
|
539
|
+
|
|
540
|
+
if type_schema.get("fmt"):
|
|
541
|
+
type_fullname += f""" ({type_schema["fmt"]})"""
|
|
542
|
+
|
|
543
|
+
if type_fullname:
|
|
544
|
+
res.append(colored(type_fullname, TYPE_COLOR))
|
|
545
|
+
|
|
546
|
+
if type_schema.get("enum"):
|
|
547
|
+
res.append(f"[{', '.join(str(e) for e in type_schema['enum'])}]")
|
|
548
|
+
if type_schema.get("const"):
|
|
549
|
+
res.append(f"(константа: {type_schema['const']})")
|
|
550
|
+
|
|
551
|
+
ref = type_schema.get("$ref", "")
|
|
552
|
+
model_link = model_link_href(ref)
|
|
553
|
+
if model_link:
|
|
554
|
+
res.append(model_link)
|
|
555
|
+
|
|
556
|
+
return "<br/>".join(res)
|
|
557
|
+
|
|
558
|
+
def build_field_table(fields: list[dict[str, Any]]) -> Table:
|
|
559
|
+
header = [
|
|
560
|
+
Paragraph(bold("Поле"), small_style),
|
|
561
|
+
Paragraph(bold("Тип"), small_style),
|
|
562
|
+
Paragraph(bold("Обяз."), small_style),
|
|
563
|
+
Paragraph(bold("Описание"), small_style),
|
|
564
|
+
Paragraph(bold("Пример"), small_style),
|
|
565
|
+
]
|
|
566
|
+
data = [header]
|
|
567
|
+
for f in fields:
|
|
568
|
+
name = f["name"]
|
|
569
|
+
required = bool(f.get("required"))
|
|
570
|
+
desc = f.get("description", "")
|
|
571
|
+
example = str(f.get("example", ""))
|
|
572
|
+
|
|
573
|
+
type_schema = f.get("type_schema", {"type": "string"})
|
|
574
|
+
|
|
575
|
+
data.append(
|
|
576
|
+
[
|
|
577
|
+
XPreformatted(name, small_style),
|
|
578
|
+
Paragraph(build_field_type_span(type_schema), small_style),
|
|
579
|
+
Paragraph(colored("Да", REQUIRED_COLOR) if required else "Нет", small_style),
|
|
580
|
+
Paragraph(desc, small_style),
|
|
581
|
+
Paragraph(f'<font name="{REGULAR}" size="7">{example}</font>', small_style),
|
|
582
|
+
]
|
|
583
|
+
)
|
|
584
|
+
|
|
585
|
+
col_widths: list[float | None] = [None, None, 30, None, None]
|
|
586
|
+
for r in data:
|
|
587
|
+
w = r[0].minWidth() + 10
|
|
588
|
+
col_widths[0] = max(col_widths[0] or 0, w)
|
|
589
|
+
|
|
590
|
+
w = r[1].minWidth() + 10
|
|
591
|
+
col_widths[1] = max(col_widths[1] or 0, w)
|
|
592
|
+
|
|
593
|
+
t = Table(data, colWidths=col_widths, repeatRows=1)
|
|
594
|
+
t.setStyle(
|
|
595
|
+
TableStyle(
|
|
596
|
+
[
|
|
597
|
+
("BACKGROUND", (0, 0), (-1, 0), PRIMARY),
|
|
598
|
+
("TEXTCOLOR", (0, 0), (-1, 0), white),
|
|
599
|
+
("FONTNAME", (0, 0), (-1, -1), REGULAR),
|
|
600
|
+
("FONTSIZE", (0, 0), (-1, -1), 8),
|
|
601
|
+
("LEADING", (0, 0), (-1, -1), 10),
|
|
602
|
+
("ALIGN", (2, 0), (2, -1), "CENTER"),
|
|
603
|
+
("VALIGN", (0, 0), (-1, -1), "TOP"),
|
|
604
|
+
("GRID", (0, 0), (-1, -1), 0.5, BORDER),
|
|
605
|
+
("ROWBACKGROUNDS", (0, 1), (-1, -1), [white, LIGHT_BG]),
|
|
606
|
+
("TOPPADDING", (0, 0), (-1, -1), 3),
|
|
607
|
+
("BOTTOMPADDING", (0, 0), (-1, -1), 3),
|
|
608
|
+
("LEFTPADDING", (0, 0), (-1, -1), 4),
|
|
609
|
+
("RIGHTPADDING", (0, 0), (-1, -1), 4),
|
|
610
|
+
]
|
|
611
|
+
)
|
|
612
|
+
)
|
|
613
|
+
return t
|
|
614
|
+
|
|
615
|
+
def parse_prop_type(obj_schema: dict[str, Any]) -> dict[str, Any]:
|
|
616
|
+
|
|
617
|
+
ftype = obj_schema.get("type", "")
|
|
618
|
+
|
|
619
|
+
ftype_union = []
|
|
620
|
+
if "anyOf" in obj_schema:
|
|
621
|
+
ftype = "anyOf"
|
|
622
|
+
types = obj_schema["anyOf"]
|
|
623
|
+
for t in types:
|
|
624
|
+
if t.get("type") == "null" and len(types) > 1:
|
|
625
|
+
continue
|
|
626
|
+
ftype_union.append(parse_prop_type(t))
|
|
627
|
+
|
|
628
|
+
fref = ""
|
|
629
|
+
if ftype == "array" and "items" in obj_schema and "$ref" in obj_schema["items"]:
|
|
630
|
+
fref = obj_schema["items"]["$ref"]
|
|
631
|
+
if not fref and "$ref" in obj_schema:
|
|
632
|
+
fref = obj_schema["$ref"]
|
|
633
|
+
|
|
634
|
+
fmt = obj_schema.get("format", "")
|
|
635
|
+
pattern = str(obj_schema.get("pattern") or "")
|
|
636
|
+
|
|
637
|
+
enum = []
|
|
638
|
+
if "enum" in obj_schema:
|
|
639
|
+
enum = [str(e) for e in obj_schema["enum"]]
|
|
640
|
+
|
|
641
|
+
const = str(obj_schema.get("const") or "")
|
|
642
|
+
|
|
643
|
+
return {
|
|
644
|
+
"type": ftype,
|
|
645
|
+
"type_union": ftype_union,
|
|
646
|
+
"$ref": fref,
|
|
647
|
+
"fmt": fmt,
|
|
648
|
+
"pattern": pattern,
|
|
649
|
+
"enum": enum,
|
|
650
|
+
"const": const,
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
def parse_schema_fields(schema: dict[str, Any]) -> list[dict[str, Any]]:
|
|
654
|
+
fields = []
|
|
655
|
+
props = schema.get("properties", {})
|
|
656
|
+
req_set = set(schema.get("required", []))
|
|
657
|
+
for name, prop in props.items():
|
|
658
|
+
type_schema = parse_prop_type(prop)
|
|
659
|
+
examples = prop.get("examples", [])
|
|
660
|
+
example = examples[0] if examples else ""
|
|
661
|
+
|
|
662
|
+
fields.append(
|
|
663
|
+
{
|
|
664
|
+
"name": name,
|
|
665
|
+
"type_schema": type_schema,
|
|
666
|
+
"required": name in req_set,
|
|
667
|
+
"description": prop.get("description", ""),
|
|
668
|
+
"example": example,
|
|
669
|
+
}
|
|
670
|
+
)
|
|
671
|
+
return fields
|
|
672
|
+
|
|
673
|
+
def build_models() -> list[Flowable]:
|
|
674
|
+
story: list[Flowable] = []
|
|
675
|
+
story.append(PageBreak())
|
|
676
|
+
|
|
677
|
+
section_toc = add_to_toc(story, parent_toc, title="Контракты данных")
|
|
678
|
+
story.append(Paragraph(f"{section_toc.full_num} {section_toc.title}", h1_style))
|
|
679
|
+
|
|
680
|
+
schemas = openapi_schemas
|
|
681
|
+
|
|
682
|
+
for schema_name, schema in schemas.items():
|
|
683
|
+
title = (schema_name + " — " + schema.get("description", "")).rstrip(" —")
|
|
684
|
+
|
|
685
|
+
schema_toc = add_to_toc(story, section_toc, title)
|
|
686
|
+
|
|
687
|
+
schema_story: list[Flowable] = []
|
|
688
|
+
schema_story.append(Spacer(1, 8))
|
|
689
|
+
schema_story.append(Paragraph(f"{model_link_name(schema_name)}{schema_toc.full_num}. {title}", h2_style))
|
|
690
|
+
schema_story.append(Spacer(1, 4))
|
|
691
|
+
|
|
692
|
+
fields = parse_schema_fields(schema)
|
|
693
|
+
if fields:
|
|
694
|
+
schema_story.append(build_field_table(fields))
|
|
695
|
+
schema_story.append(Spacer(1, 6))
|
|
696
|
+
|
|
697
|
+
story.append(KeepTogetherSplitAtTop(schema_story))
|
|
698
|
+
|
|
699
|
+
return story
|
|
700
|
+
|
|
701
|
+
return build_models()
|
|
702
|
+
|
|
703
|
+
|
|
704
|
+
def build_pdf(openapi_dict: dict[str, Any], output_path: str) -> None:
|
|
705
|
+
openapi_info = openapi_dict.get("info", {})
|
|
706
|
+
openapi_endpoints = openapi_dict.get("paths", {})
|
|
707
|
+
openapi_schemas = openapi_dict.get("components", {}).get("schemas", {})
|
|
708
|
+
|
|
709
|
+
# output_path = output_path + "/" + openapi_info.get("title", "") + " v" + openapi_info.get("version", "") + ".pdf"
|
|
710
|
+
header_left = openapi_info.get("title", "") + " | API v" + openapi_info.get("version", "")
|
|
711
|
+
footer_left = ""
|
|
712
|
+
|
|
713
|
+
header_footer_tmpl = CustomDocTemplateHeaderFooter(header_left=header_left, footer_left=footer_left)
|
|
714
|
+
|
|
715
|
+
doc = CustomDocTemplate(
|
|
716
|
+
output_path,
|
|
717
|
+
header_footer_tmpl=header_footer_tmpl,
|
|
718
|
+
pagesize=A4,
|
|
719
|
+
leftMargin=20 * mm,
|
|
720
|
+
rightMargin=20 * mm,
|
|
721
|
+
topMargin=22 * mm,
|
|
722
|
+
bottomMargin=22 * mm,
|
|
723
|
+
)
|
|
724
|
+
|
|
725
|
+
doc_toc = TOC_ITEM(None, "")
|
|
726
|
+
|
|
727
|
+
sections: list[list[Flowable]] = []
|
|
728
|
+
sections.append(section_title(openapi_info=openapi_info))
|
|
729
|
+
sections.append(section_overview(parent_toc=doc_toc))
|
|
730
|
+
sections.append(
|
|
731
|
+
section_operations(parent_toc=doc_toc, openapi_endpoints=openapi_endpoints, openapi_schemas=openapi_schemas)
|
|
732
|
+
)
|
|
733
|
+
|
|
734
|
+
sections.append(section_models(parent_toc=doc_toc, openapi_schemas=openapi_schemas))
|
|
735
|
+
|
|
736
|
+
sections.insert(1, section_toc(doc_toc=doc_toc))
|
|
737
|
+
|
|
738
|
+
story: list[Flowable] = []
|
|
739
|
+
for s in sections:
|
|
740
|
+
story += s
|
|
741
|
+
|
|
742
|
+
doc.customBuild(story)
|
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
from collections.abc import Iterable
|
|
2
|
+
from typing import Any, get_args, get_origin, Literal
|
|
3
|
+
from typing_extensions import TypeIs
|
|
4
|
+
from typing import TextIO
|
|
5
|
+
from enum import StrEnum, Enum
|
|
6
|
+
import re
|
|
7
|
+
import pydantic
|
|
8
|
+
import pydantic_settings
|
|
9
|
+
from pydantic_core import PydanticSerializationError
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _str_simple(value: Any, indent: int = 4, outer_quotes: str = "'") -> str: # noqa ANN401
|
|
13
|
+
if not outer_quotes:
|
|
14
|
+
outer_quotes = "'"
|
|
15
|
+
|
|
16
|
+
if value is None:
|
|
17
|
+
return ""
|
|
18
|
+
|
|
19
|
+
if isinstance(value, type):
|
|
20
|
+
return ""
|
|
21
|
+
|
|
22
|
+
if isinstance(value, str):
|
|
23
|
+
strval = value.strip()
|
|
24
|
+
if not outer_quotes:
|
|
25
|
+
strval = f'"{strval}"' if "'" in strval else f"'{strval}'"
|
|
26
|
+
else:
|
|
27
|
+
strval_stripq = strval.strip(outer_quotes)
|
|
28
|
+
strval = (
|
|
29
|
+
outer_quotes + re.sub(r"([^\\])" + outer_quotes, r"\1\\" + outer_quotes, strval_stripq) + outer_quotes
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
return strval
|
|
33
|
+
|
|
34
|
+
try:
|
|
35
|
+
adapter = pydantic.TypeAdapter(Any, config=pydantic.ConfigDict(arbitrary_types_allowed=True))
|
|
36
|
+
return adapter.dump_json(value, indent=indent).decode()
|
|
37
|
+
except PydanticSerializationError:
|
|
38
|
+
return ""
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _str_simple_list(items: Iterable[Any], outer_qoutes: str = "'") -> list[str]:
|
|
42
|
+
return [item for item in [_str_simple(item, outer_quotes=outer_qoutes) for item in items] if item]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _is_enum(value: type) -> TypeIs[type[Enum]]:
|
|
46
|
+
return issubclass(value, (Enum, StrEnum))
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _is_literal(value: object) -> bool:
|
|
50
|
+
return get_origin(value) is Literal
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _str_list_enumerated_type(value: Any) -> list[str]: # noqa ANN401
|
|
54
|
+
if _is_enum(value):
|
|
55
|
+
return _str_simple_list([item.value for item in value.__members__.values()])
|
|
56
|
+
elif _is_literal(value):
|
|
57
|
+
return _str_simple_list(get_args(value))
|
|
58
|
+
else:
|
|
59
|
+
return []
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def build_env_example(
|
|
63
|
+
model: type[pydantic_settings.BaseSettings | pydantic.BaseModel], file: TextIO | None = None
|
|
64
|
+
) -> None:
|
|
65
|
+
output_line_length = 80
|
|
66
|
+
|
|
67
|
+
if model.__doc__:
|
|
68
|
+
print(f"""# {"\n# ".join(model.__doc__.strip().splitlines())}""", file=file)
|
|
69
|
+
print("", file=file)
|
|
70
|
+
|
|
71
|
+
fields_required: list[str] = []
|
|
72
|
+
fields_notrequired: list[str] = []
|
|
73
|
+
for field_name, field_info in model.model_fields.items():
|
|
74
|
+
field_description = field_info.description
|
|
75
|
+
if field_description:
|
|
76
|
+
print(f"# {'\n# '.join(field_description.strip().splitlines())}", file=file)
|
|
77
|
+
|
|
78
|
+
field_annotation = field_info.annotation
|
|
79
|
+
if field_annotation:
|
|
80
|
+
vals = _str_list_enumerated_type(field_annotation)
|
|
81
|
+
if vals:
|
|
82
|
+
print("# Возможные значения: ", file=file)
|
|
83
|
+
vals_str = f"# {', '.join(vals)}"
|
|
84
|
+
if len(vals_str) > output_line_length:
|
|
85
|
+
vals_str = f"# {'\n# '.join(vals)}"
|
|
86
|
+
print(vals_str, file=file)
|
|
87
|
+
|
|
88
|
+
field_value = _str_simple(field_info.default)
|
|
89
|
+
|
|
90
|
+
example_values = []
|
|
91
|
+
for example_value in field_info.examples or []:
|
|
92
|
+
example_value = _str_simple(example_value)
|
|
93
|
+
if not example_value:
|
|
94
|
+
continue
|
|
95
|
+
if not field_value:
|
|
96
|
+
field_value = example_value
|
|
97
|
+
example_values.append(example_value)
|
|
98
|
+
if len(example_values) == 1:
|
|
99
|
+
example_values.remove(field_value)
|
|
100
|
+
|
|
101
|
+
if example_values:
|
|
102
|
+
print("# Примеры значений", file=file)
|
|
103
|
+
for example_value in example_values:
|
|
104
|
+
print(f"# {example_value}", file=file)
|
|
105
|
+
|
|
106
|
+
print(f"{field_name} = {field_value}", file=file)
|
|
107
|
+
|
|
108
|
+
if field_info.is_required():
|
|
109
|
+
fields_required.append(field_name)
|
|
110
|
+
else:
|
|
111
|
+
fields_notrequired.append(field_name)
|
|
112
|
+
|
|
113
|
+
print("", file=file)
|
|
114
|
+
|
|
115
|
+
if not fields_required:
|
|
116
|
+
print("# Обязательные параметры отсутствуют", file=file)
|
|
117
|
+
else:
|
|
118
|
+
print("# Обязательные параметры: ", file=file)
|
|
119
|
+
for field_name in fields_required:
|
|
120
|
+
print(f"# {field_name}", file=file)
|
|
121
|
+
print("", file=file)
|
|
122
|
+
|
|
123
|
+
if not fields_notrequired:
|
|
124
|
+
print("# Необязательные параметры отсутствуют", file=file)
|
|
125
|
+
else:
|
|
126
|
+
print("# Необязательные параметры: ", file=file)
|
|
127
|
+
for field_name in fields_notrequired:
|
|
128
|
+
print(f"# {field_name}", file=file)
|
|
129
|
+
print("", file=file)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
class JSONSchemaNodeType(StrEnum):
|
|
133
|
+
ROOT = "root"
|
|
134
|
+
REF = "ref"
|
|
135
|
+
TYPE_UNION = "anyOf"
|
|
136
|
+
ARRAY = "array"
|
|
137
|
+
OBJECT = "object_body"
|
|
138
|
+
OBJECT_PROPERTY = "object_property"
|
|
139
|
+
ARRAY_VALUE = "array_value"
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def build_json_example(
|
|
143
|
+
model: type[pydantic_settings.BaseSettings | pydantic.BaseModel], file: TextIO | None = None
|
|
144
|
+
) -> None:
|
|
145
|
+
schema_dict = model.model_json_schema()
|
|
146
|
+
defs = schema_dict.pop("$defs", {})
|
|
147
|
+
|
|
148
|
+
print("// Описание схемы в формате JSON5, при копировании в JSON удалите комметарии", file=file)
|
|
149
|
+
print("", file=file)
|
|
150
|
+
|
|
151
|
+
def schema_to_json5(
|
|
152
|
+
schema: dict[str, Any] | list[Any], indent_level: int, node_type: JSONSchemaNodeType
|
|
153
|
+
) -> tuple[str, str]:
|
|
154
|
+
spacing = " " * indent_level
|
|
155
|
+
spacing_commets = " " * max(indent_level, 0)
|
|
156
|
+
lines_before = []
|
|
157
|
+
lines_after = []
|
|
158
|
+
|
|
159
|
+
if isinstance(schema, dict):
|
|
160
|
+
if "$ref" in schema:
|
|
161
|
+
ref_name = schema["$ref"].split("/")[-1]
|
|
162
|
+
ref_dict = defs[ref_name]
|
|
163
|
+
parent_has_description = False
|
|
164
|
+
if node_type == JSONSchemaNodeType.ARRAY:
|
|
165
|
+
parent_has_description = True
|
|
166
|
+
elif "description" in schema:
|
|
167
|
+
desc_lines = (schema["description"] or "").strip().splitlines()
|
|
168
|
+
parent_has_description = bool(desc_lines)
|
|
169
|
+
for desc_line in desc_lines:
|
|
170
|
+
lines_before.append(f"{spacing_commets}// {desc_line}")
|
|
171
|
+
|
|
172
|
+
save_ref_description = ""
|
|
173
|
+
if parent_has_description and "description" in ref_dict:
|
|
174
|
+
save_ref_description = ref_dict.pop("description", "")
|
|
175
|
+
|
|
176
|
+
str_before, str_after = schema_to_json5(ref_dict, indent_level, JSONSchemaNodeType.REF)
|
|
177
|
+
|
|
178
|
+
if save_ref_description:
|
|
179
|
+
ref_dict["description"] = save_ref_description
|
|
180
|
+
|
|
181
|
+
if str_before:
|
|
182
|
+
lines_before.append(f"{str_before}")
|
|
183
|
+
if str_after:
|
|
184
|
+
lines_after.append(f"{str_after}")
|
|
185
|
+
return "\n".join(lines_before), "\n".join(lines_after)
|
|
186
|
+
|
|
187
|
+
is_type = "type" in schema
|
|
188
|
+
type_name = schema["type"] if is_type else ""
|
|
189
|
+
is_object = type_name == "object"
|
|
190
|
+
is_array = type_name == "array"
|
|
191
|
+
|
|
192
|
+
if is_object:
|
|
193
|
+
lines_after.append(spacing + "{")
|
|
194
|
+
|
|
195
|
+
if "description" in schema:
|
|
196
|
+
desc_lines = (schema["description"] or "").strip().splitlines()
|
|
197
|
+
for desc_line in desc_lines:
|
|
198
|
+
lines_before.append(f"{spacing_commets}// {desc_line}")
|
|
199
|
+
|
|
200
|
+
if is_type and "enum" in schema and isinstance(schema["enum"], list):
|
|
201
|
+
enum_values = schema["enum"]
|
|
202
|
+
lines_before.append(
|
|
203
|
+
f"{spacing}// Возможные значения: " + ", ".join(_str_simple_list(enum_values, outer_qoutes='"'))
|
|
204
|
+
)
|
|
205
|
+
return "\n".join(lines_before), "\n".join(lines_after)
|
|
206
|
+
|
|
207
|
+
is_type_union = "anyOf" in schema and isinstance(schema["anyOf"], list)
|
|
208
|
+
if is_type_union:
|
|
209
|
+
type_list = schema["anyOf"]
|
|
210
|
+
type_list_null_val = {"type": "null"}
|
|
211
|
+
type_list_has_null = False
|
|
212
|
+
if type_list_null_val in type_list:
|
|
213
|
+
type_list_has_null = True
|
|
214
|
+
type_list.pop(type_list.index(type_list_null_val))
|
|
215
|
+
|
|
216
|
+
if type_list_has_null:
|
|
217
|
+
lines_before.append(f"{spacing}// Необязательное")
|
|
218
|
+
if len(type_list) > 1:
|
|
219
|
+
lines_after.append(f"{spacing}// Возможные значения")
|
|
220
|
+
else:
|
|
221
|
+
type_list = type_list[0]
|
|
222
|
+
|
|
223
|
+
str_before, str_after = schema_to_json5(type_list, indent_level, JSONSchemaNodeType.TYPE_UNION)
|
|
224
|
+
|
|
225
|
+
if type_list_has_null:
|
|
226
|
+
type_list = schema["anyOf"]
|
|
227
|
+
type_list.append(type_list_null_val)
|
|
228
|
+
|
|
229
|
+
if str_before:
|
|
230
|
+
lines_after.append(f"{str_before}")
|
|
231
|
+
if str_after:
|
|
232
|
+
lines_after.append(f"{str_after}")
|
|
233
|
+
return "\n".join(lines_before), "\n".join(lines_after)
|
|
234
|
+
|
|
235
|
+
if is_array and "items" in schema:
|
|
236
|
+
str_before, str_after = schema_to_json5(schema["items"], indent_level + 1, JSONSchemaNodeType.ARRAY)
|
|
237
|
+
|
|
238
|
+
lines_after.append(f"{spacing}[")
|
|
239
|
+
if str_before:
|
|
240
|
+
lines_after.append(f"{str_before}")
|
|
241
|
+
if str_after:
|
|
242
|
+
lines_after.append(f"{str_after},")
|
|
243
|
+
lines_after.append(f"{spacing}]")
|
|
244
|
+
return "\n".join(lines_before), "\n".join(lines_after)
|
|
245
|
+
|
|
246
|
+
if not is_type and node_type != JSONSchemaNodeType.OBJECT:
|
|
247
|
+
return "", ""
|
|
248
|
+
|
|
249
|
+
items = list(schema.items())
|
|
250
|
+
|
|
251
|
+
for i, (key, value) in enumerate(items):
|
|
252
|
+
if isinstance(value, dict):
|
|
253
|
+
if is_object and key == "properties":
|
|
254
|
+
str_before, str_after = schema_to_json5(value, indent_level + 1, JSONSchemaNodeType.OBJECT)
|
|
255
|
+
if str_before:
|
|
256
|
+
lines_after.append(str_before)
|
|
257
|
+
if str_after:
|
|
258
|
+
lines_after.append(str_after)
|
|
259
|
+
|
|
260
|
+
elif node_type == JSONSchemaNodeType.OBJECT:
|
|
261
|
+
str_before, str_after = schema_to_json5(value, indent_level, JSONSchemaNodeType.OBJECT_PROPERTY)
|
|
262
|
+
if str_before:
|
|
263
|
+
lines_after.append(str_before)
|
|
264
|
+
|
|
265
|
+
property_type_name = value.get("type", "")
|
|
266
|
+
|
|
267
|
+
# if "type" in value:
|
|
268
|
+
# lines_after.append(f"{spacing_commets}// тип: {_str_simple(value["type"])}")
|
|
269
|
+
|
|
270
|
+
default_str = ""
|
|
271
|
+
if "default" in value:
|
|
272
|
+
default_val = value["default"]
|
|
273
|
+
default_str = _str_simple(default_val, outer_quotes='"')
|
|
274
|
+
|
|
275
|
+
if default_str and isinstance(default_val, (str, int, bool)):
|
|
276
|
+
default_str = spacing + f"\n{spacing}".join(default_str.splitlines())
|
|
277
|
+
else:
|
|
278
|
+
default_str = ""
|
|
279
|
+
|
|
280
|
+
if not default_str:
|
|
281
|
+
if property_type_name == "boolean":
|
|
282
|
+
default_str = "false"
|
|
283
|
+
elif property_type_name in ("integer", "number"):
|
|
284
|
+
default_str = "0"
|
|
285
|
+
elif property_type_name == "string":
|
|
286
|
+
default_str = '""'
|
|
287
|
+
elif property_type_name == "array":
|
|
288
|
+
default_str = "[]"
|
|
289
|
+
elif not property_type_name:
|
|
290
|
+
default_str = ""
|
|
291
|
+
else:
|
|
292
|
+
default_str = "null"
|
|
293
|
+
|
|
294
|
+
if property_type_name == "array" and "items" in value and "$ref" in value["items"]:
|
|
295
|
+
default_str = ""
|
|
296
|
+
|
|
297
|
+
if default_str:
|
|
298
|
+
# default_str = default_str.strip("'")
|
|
299
|
+
str_after = f"{default_str}\n" + str_after
|
|
300
|
+
|
|
301
|
+
lines_after.append(f'{spacing}"{key}": {str_after.strip()}')
|
|
302
|
+
|
|
303
|
+
if i < len(items) - 1 and lines_after:
|
|
304
|
+
lines_after[-1] += ","
|
|
305
|
+
|
|
306
|
+
elif is_type and not is_object and type_name == "null":
|
|
307
|
+
lines_after.append(f"{spacing}{type_name}")
|
|
308
|
+
return "\n".join(lines_before), "\n".join(lines_after)
|
|
309
|
+
|
|
310
|
+
if is_object:
|
|
311
|
+
lines_after.append(spacing + "}")
|
|
312
|
+
|
|
313
|
+
return "\n".join(lines_before), "\n".join(lines_after)
|
|
314
|
+
|
|
315
|
+
elif isinstance(schema, list):
|
|
316
|
+
lines_after.append(f"{spacing}[")
|
|
317
|
+
for i, item in enumerate(schema):
|
|
318
|
+
str_before, str_after = schema_to_json5(item, indent_level + 1, JSONSchemaNodeType.ARRAY_VALUE)
|
|
319
|
+
if str_before:
|
|
320
|
+
lines_after.append(f"{str_before}")
|
|
321
|
+
if str_after:
|
|
322
|
+
lines_after.append(f"{str_after}")
|
|
323
|
+
if i < len(schema) - 1 and lines_after:
|
|
324
|
+
lines_after[-1] += ","
|
|
325
|
+
lines_after.append(f"{spacing}]")
|
|
326
|
+
return "\n".join(lines_before), "\n".join(lines_after)
|
|
327
|
+
|
|
328
|
+
else:
|
|
329
|
+
return "", _str_simple(schema, outer_quotes='"')
|
|
330
|
+
|
|
331
|
+
str_before, str_after = schema_to_json5(schema_dict, 0, JSONSchemaNodeType.ROOT)
|
|
332
|
+
print(str_before, file=file)
|
|
333
|
+
print(str_after, file=file)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: tooaio-scripts
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Скрипты общего назначения: сборка документации, сборка приложений
|
|
5
|
+
License-File: LICENSE
|
|
6
|
+
Requires-Python: >=3.12
|
|
7
|
+
Requires-Dist: gitpython>=3.1.62
|
|
8
|
+
Requires-Dist: pydantic-settings>=2.12.1
|
|
9
|
+
Requires-Dist: pydantic>=2.12.5
|
|
10
|
+
Requires-Dist: reportlab>=5.0.0
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
|
|
13
|
+
# Скрипты общего назначения
|
|
14
|
+
* сборка документации;
|
|
15
|
+
* сборка приложений;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
src/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
src/build_release_git_remote.py,sha256=KSfbD-9JXG0HRPQrIP313S0euPTvffqeNeR4vWCE8jQ,9683
|
|
3
|
+
src/openapi_doc.py,sha256=4G_4FG97hooM5UngiVZ_FKp6fO8pGpwhYyYJsX7I8eA,392
|
|
4
|
+
src/openapi_to_pdf.py,sha256=ZuMc52fogZxkAMJVThGQyjP9posVVvefSbU77ANE4zo,26702
|
|
5
|
+
src/pydantic_schema_doc.py,sha256=mpLKEUVFsqEKyzw-qRhJl5dDaHwlwc2LbFIzsnaZQC8,13385
|
|
6
|
+
tooaio_scripts-1.0.0.dist-info/METADATA,sha256=Uy8umVHg-Jm7dDffiqiZPWSjZkwfMXQaWJQCA7Hnbi4,548
|
|
7
|
+
tooaio_scripts-1.0.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
|
|
8
|
+
tooaio_scripts-1.0.0.dist-info/licenses/LICENSE,sha256=awOCsWJ58m_2kBQwBUGWejVqZm6wuRtCL2hi9rfa0X4,1211
|
|
9
|
+
tooaio_scripts-1.0.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
This is free and unencumbered software released into the public domain.
|
|
2
|
+
|
|
3
|
+
Anyone is free to copy, modify, publish, use, compile, sell, or
|
|
4
|
+
distribute this software, either in source code form or as a compiled
|
|
5
|
+
binary, for any purpose, commercial or non-commercial, and by any
|
|
6
|
+
means.
|
|
7
|
+
|
|
8
|
+
In jurisdictions that recognize copyright laws, the author or authors
|
|
9
|
+
of this software dedicate any and all copyright interest in the
|
|
10
|
+
software to the public domain. We make this dedication for the benefit
|
|
11
|
+
of the public at large and to the detriment of our heirs and
|
|
12
|
+
successors. We intend this dedication to be an overt act of
|
|
13
|
+
relinquishment in perpetuity of all present and future rights to this
|
|
14
|
+
software under copyright law.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
|
19
|
+
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
|
|
20
|
+
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
|
|
21
|
+
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
|
22
|
+
OTHER DEALINGS IN THE SOFTWARE.
|
|
23
|
+
|
|
24
|
+
For more information, please refer to <https://unlicense.org>
|