pulp-engine 0.85.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.
- pulp_engine/__init__.py +48 -0
- pulp_engine/_http.py +230 -0
- pulp_engine/client.py +96 -0
- pulp_engine/errors.py +142 -0
- pulp_engine/pagination.py +51 -0
- pulp_engine/py.typed +0 -0
- pulp_engine/resources/__init__.py +25 -0
- pulp_engine/resources/admin.py +71 -0
- pulp_engine/resources/assets.py +68 -0
- pulp_engine/resources/audit_events.py +62 -0
- pulp_engine/resources/auth.py +26 -0
- pulp_engine/resources/batch.py +146 -0
- pulp_engine/resources/health.py +23 -0
- pulp_engine/resources/pdf_transform.py +89 -0
- pulp_engine/resources/render.py +309 -0
- pulp_engine/resources/schedules.py +95 -0
- pulp_engine/resources/templates.py +161 -0
- pulp_engine/types.py +394 -0
- pulp_engine-0.85.0.dist-info/METADATA +217 -0
- pulp_engine-0.85.0.dist-info/RECORD +21 -0
- pulp_engine-0.85.0.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
"""Render resource — PDF, HTML, CSV, XLSX, DOCX, PPTX."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any, Literal
|
|
9
|
+
|
|
10
|
+
from .._http import HttpClient
|
|
11
|
+
from ..types import DryRunResult
|
|
12
|
+
|
|
13
|
+
# Format selector for RenderResource.dry_run() — selects which per-format
|
|
14
|
+
# route to hit. The result shape is identical regardless of format; the
|
|
15
|
+
# selector only affects which route is queried (matters when different
|
|
16
|
+
# formats have different rate limits or feature gates server-side).
|
|
17
|
+
DryRunFormat = Literal["pdf", "html", "csv", "xlsx", "docx", "pptx"]
|
|
18
|
+
|
|
19
|
+
_DRY_RUN_PATHS: dict[str, str] = {
|
|
20
|
+
"pdf": "/render/pdf",
|
|
21
|
+
"html": "/render/html",
|
|
22
|
+
"csv": "/render/csv",
|
|
23
|
+
"xlsx": "/render/xlsx",
|
|
24
|
+
"docx": "/render/docx",
|
|
25
|
+
"pptx": "/render/pptx",
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass
|
|
30
|
+
class BinaryResult:
|
|
31
|
+
"""Raw binary render result with response metadata.
|
|
32
|
+
|
|
33
|
+
Provides a ``save()`` convenience method for writing the bytes to a file.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
data: bytes
|
|
37
|
+
content_type: str | None
|
|
38
|
+
content_disposition: str | None
|
|
39
|
+
|
|
40
|
+
def save(self, path: str | Path) -> Path:
|
|
41
|
+
"""Write the binary data to ``path`` and return the resolved path."""
|
|
42
|
+
out = Path(path)
|
|
43
|
+
out.parent.mkdir(parents=True, exist_ok=True)
|
|
44
|
+
out.write_bytes(self.data)
|
|
45
|
+
return out
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass
|
|
49
|
+
class PptxResult(BinaryResult):
|
|
50
|
+
"""PPTX render result with structured warning count.
|
|
51
|
+
|
|
52
|
+
``warning_count`` is parsed from the ``X-Render-Warnings: count=N`` header.
|
|
53
|
+
Common warning sources: known-dimension overflow, mixed-mark bullet collapse
|
|
54
|
+
in richText, totalPages marker drop in header/footer.
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
warning_count: int = 0
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _build_render_body(
|
|
61
|
+
template: str,
|
|
62
|
+
data: dict[str, Any],
|
|
63
|
+
*,
|
|
64
|
+
version: str | None,
|
|
65
|
+
options: dict[str, Any] | None,
|
|
66
|
+
) -> dict[str, Any]:
|
|
67
|
+
body: dict[str, Any] = {"template": template, "data": data}
|
|
68
|
+
if version is not None:
|
|
69
|
+
body["version"] = version
|
|
70
|
+
if options is not None:
|
|
71
|
+
body["options"] = options
|
|
72
|
+
return body
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _parse_warning_count(header_value: str | None) -> int:
|
|
76
|
+
if not header_value:
|
|
77
|
+
return 0
|
|
78
|
+
match = re.search(r"count=(\d+)", header_value)
|
|
79
|
+
if not match:
|
|
80
|
+
return 0
|
|
81
|
+
try:
|
|
82
|
+
return int(match.group(1))
|
|
83
|
+
except ValueError:
|
|
84
|
+
return 0
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class RenderResource:
|
|
88
|
+
"""Render templates to various output formats."""
|
|
89
|
+
|
|
90
|
+
def __init__(self, http: HttpClient) -> None:
|
|
91
|
+
self._http = http
|
|
92
|
+
|
|
93
|
+
def pdf(
|
|
94
|
+
self,
|
|
95
|
+
template: str,
|
|
96
|
+
data: dict[str, Any],
|
|
97
|
+
*,
|
|
98
|
+
version: str | None = None,
|
|
99
|
+
options: dict[str, Any] | None = None,
|
|
100
|
+
) -> BinaryResult:
|
|
101
|
+
"""Render a template to PDF. Returns raw binary."""
|
|
102
|
+
body = _build_render_body(template, data, version=version, options=options)
|
|
103
|
+
content, headers = self._http.request_bytes("POST", "/render/pdf", json=body)
|
|
104
|
+
return BinaryResult(
|
|
105
|
+
data=content,
|
|
106
|
+
content_type=headers.get("content-type"),
|
|
107
|
+
content_disposition=headers.get("content-disposition"),
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
def pdf_stream(
|
|
111
|
+
self,
|
|
112
|
+
template: str,
|
|
113
|
+
data: dict[str, Any],
|
|
114
|
+
*,
|
|
115
|
+
version: str | None = None,
|
|
116
|
+
options: dict[str, Any] | None = None,
|
|
117
|
+
stream: Literal["auto", "force", "off"] = "auto",
|
|
118
|
+
) -> Any:
|
|
119
|
+
"""Render a template to PDF and stream the response body.
|
|
120
|
+
|
|
121
|
+
Returns a context manager yielding an ``httpx.Response``; iterate
|
|
122
|
+
``response.iter_bytes()`` inside the ``with`` block to consume chunks
|
|
123
|
+
as they arrive.
|
|
124
|
+
|
|
125
|
+
``stream="force"`` passes ``?stream=true`` so incompatible post-render
|
|
126
|
+
hook registrations surface as a 400 error up front instead of silently
|
|
127
|
+
falling back to buffered. ``stream="off"`` passes ``?stream=false`` to
|
|
128
|
+
force a buffered response (useful behind proxies that mangle chunked
|
|
129
|
+
encoding). ``stream="auto"`` (default) lets the server choose based on
|
|
130
|
+
whether hooks are registered.
|
|
131
|
+
|
|
132
|
+
Error contract: the streamed body may be truncated on mid-stream
|
|
133
|
+
failure. ``httpx`` raises a transport error when that happens; clients
|
|
134
|
+
must treat the bytes received before the error as possibly-incomplete
|
|
135
|
+
rather than relying on PDF EOF-sniffing.
|
|
136
|
+
|
|
137
|
+
Example::
|
|
138
|
+
|
|
139
|
+
with client.render.pdf_stream("big-report", data) as response:
|
|
140
|
+
with open("out.pdf", "wb") as f:
|
|
141
|
+
for chunk in response.iter_bytes():
|
|
142
|
+
f.write(chunk)
|
|
143
|
+
"""
|
|
144
|
+
body = _build_render_body(template, data, version=version, options=options)
|
|
145
|
+
params: dict[str, Any] = {}
|
|
146
|
+
if stream == "force":
|
|
147
|
+
params["stream"] = "true"
|
|
148
|
+
elif stream == "off":
|
|
149
|
+
params["stream"] = "false"
|
|
150
|
+
return self._http.stream_bytes("POST", "/render/pdf", json=body, params=params or None)
|
|
151
|
+
|
|
152
|
+
def html(
|
|
153
|
+
self,
|
|
154
|
+
template: str,
|
|
155
|
+
data: dict[str, Any],
|
|
156
|
+
*,
|
|
157
|
+
version: str | None = None,
|
|
158
|
+
options: dict[str, Any] | None = None,
|
|
159
|
+
) -> str:
|
|
160
|
+
"""Render a template to HTML. Returns the HTML string."""
|
|
161
|
+
body = _build_render_body(template, data, version=version, options=options)
|
|
162
|
+
return self._http.request_text("POST", "/render/html", json=body)
|
|
163
|
+
|
|
164
|
+
def csv(
|
|
165
|
+
self,
|
|
166
|
+
template: str,
|
|
167
|
+
data: dict[str, Any],
|
|
168
|
+
*,
|
|
169
|
+
version: str | None = None,
|
|
170
|
+
options: dict[str, Any] | None = None,
|
|
171
|
+
) -> BinaryResult:
|
|
172
|
+
"""Render a template to CSV. Returns raw binary."""
|
|
173
|
+
body = _build_render_body(template, data, version=version, options=options)
|
|
174
|
+
content, headers = self._http.request_bytes("POST", "/render/csv", json=body)
|
|
175
|
+
return BinaryResult(
|
|
176
|
+
data=content,
|
|
177
|
+
content_type=headers.get("content-type"),
|
|
178
|
+
content_disposition=headers.get("content-disposition"),
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
def xlsx(
|
|
182
|
+
self,
|
|
183
|
+
template: str,
|
|
184
|
+
data: dict[str, Any],
|
|
185
|
+
*,
|
|
186
|
+
version: str | None = None,
|
|
187
|
+
options: dict[str, Any] | None = None,
|
|
188
|
+
) -> BinaryResult:
|
|
189
|
+
"""Render a template to XLSX. Returns raw binary."""
|
|
190
|
+
body = _build_render_body(template, data, version=version, options=options)
|
|
191
|
+
content, headers = self._http.request_bytes("POST", "/render/xlsx", json=body)
|
|
192
|
+
return BinaryResult(
|
|
193
|
+
data=content,
|
|
194
|
+
content_type=headers.get("content-type"),
|
|
195
|
+
content_disposition=headers.get("content-disposition"),
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
def docx(
|
|
199
|
+
self,
|
|
200
|
+
template: str,
|
|
201
|
+
data: dict[str, Any],
|
|
202
|
+
*,
|
|
203
|
+
version: str | None = None,
|
|
204
|
+
options: dict[str, Any] | None = None,
|
|
205
|
+
) -> BinaryResult:
|
|
206
|
+
"""Render a template to DOCX. Returns raw binary."""
|
|
207
|
+
body = _build_render_body(template, data, version=version, options=options)
|
|
208
|
+
content, headers = self._http.request_bytes("POST", "/render/docx", json=body)
|
|
209
|
+
return BinaryResult(
|
|
210
|
+
data=content,
|
|
211
|
+
content_type=headers.get("content-type"),
|
|
212
|
+
content_disposition=headers.get("content-disposition"),
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
def pptx(
|
|
216
|
+
self,
|
|
217
|
+
template: str,
|
|
218
|
+
data: dict[str, Any],
|
|
219
|
+
*,
|
|
220
|
+
version: str | None = None,
|
|
221
|
+
options: dict[str, Any] | None = None,
|
|
222
|
+
) -> PptxResult:
|
|
223
|
+
"""Render a template to PPTX. Returns raw binary plus warning count.
|
|
224
|
+
|
|
225
|
+
Beta in v0.60.0. The route is gated server-side by ``PPTX_ENABLED`` —
|
|
226
|
+
when disabled, calls return 404.
|
|
227
|
+
"""
|
|
228
|
+
body = _build_render_body(template, data, version=version, options=options)
|
|
229
|
+
content, headers = self._http.request_bytes("POST", "/render/pptx", json=body)
|
|
230
|
+
return PptxResult(
|
|
231
|
+
data=content,
|
|
232
|
+
content_type=headers.get("content-type"),
|
|
233
|
+
content_disposition=headers.get("content-disposition"),
|
|
234
|
+
warning_count=_parse_warning_count(headers.get("x-render-warnings")),
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
def dry_run(
|
|
238
|
+
self,
|
|
239
|
+
template: str,
|
|
240
|
+
data: dict[str, Any],
|
|
241
|
+
*,
|
|
242
|
+
format: DryRunFormat = "pdf",
|
|
243
|
+
version: str | None = None,
|
|
244
|
+
options: dict[str, Any] | None = None,
|
|
245
|
+
) -> DryRunResult:
|
|
246
|
+
"""Dry-run a render: validate input data and exercise all template
|
|
247
|
+
expressions via a trial HTML render, then return structured results
|
|
248
|
+
without producing any binary output. Skips Chromium / DOCX / PPTX
|
|
249
|
+
entirely. Useful for CI/CD pre-flight checks.
|
|
250
|
+
|
|
251
|
+
Hits the same per-format route as the normal render method
|
|
252
|
+
(``/render/``, ``/render/html``, etc.) with ``dryRun: true`` in the
|
|
253
|
+
body. The route returns JSON regardless of its normal content type.
|
|
254
|
+
|
|
255
|
+
Unlike the normal render methods, this method does NOT raise on
|
|
256
|
+
template author errors (validation failures, undefined variables,
|
|
257
|
+
etc.) — those are surfaced in the result so callers can display them
|
|
258
|
+
to users. The only way this method raises is if the request itself
|
|
259
|
+
is malformed (network error, 4xx auth error, etc.) — in which case
|
|
260
|
+
:class:`PulpEngineError` is raised as usual.
|
|
261
|
+
|
|
262
|
+
Args:
|
|
263
|
+
template: Template key to dry-run.
|
|
264
|
+
data: Input data payload.
|
|
265
|
+
format: Which format route to hit. Defaults to ``"pdf"``. The
|
|
266
|
+
resulting :class:`DryRunResult` shape is identical regardless
|
|
267
|
+
of format — the format selector only affects which route is
|
|
268
|
+
queried (matters when different formats have different rate
|
|
269
|
+
limits or feature gates server-side).
|
|
270
|
+
version: Optional exact saved template version to dry-run.
|
|
271
|
+
options: Optional render options (paperSize, orientation).
|
|
272
|
+
|
|
273
|
+
Returns:
|
|
274
|
+
A :class:`DryRunResult` with ``valid``, ``validation``,
|
|
275
|
+
``expressions``, ``field_mappings_applied``, and
|
|
276
|
+
``data_sources_resolved`` fields.
|
|
277
|
+
|
|
278
|
+
Example::
|
|
279
|
+
|
|
280
|
+
result = client.render.dry_run("invoice", {"amount": 100})
|
|
281
|
+
if not result.valid:
|
|
282
|
+
for err in result.expressions.errors:
|
|
283
|
+
print(err.message, err.location.node_path if err.location else "")
|
|
284
|
+
"""
|
|
285
|
+
path = _DRY_RUN_PATHS[format]
|
|
286
|
+
body: dict[str, Any] = {"template": template, "data": data, "dryRun": True}
|
|
287
|
+
if version is not None:
|
|
288
|
+
body["version"] = version
|
|
289
|
+
if options is not None:
|
|
290
|
+
body["options"] = options
|
|
291
|
+
response = self._http.request_json("POST", path, json=body)
|
|
292
|
+
return DryRunResult.model_validate(response)
|
|
293
|
+
|
|
294
|
+
def validate(
|
|
295
|
+
self,
|
|
296
|
+
template: str,
|
|
297
|
+
data: dict[str, Any],
|
|
298
|
+
*,
|
|
299
|
+
version: str | None = None,
|
|
300
|
+
) -> dict[str, Any]:
|
|
301
|
+
"""Validate a template for publication readiness.
|
|
302
|
+
|
|
303
|
+
Returns a dict with ``valid: bool`` and ``issues: list``.
|
|
304
|
+
"""
|
|
305
|
+
body: dict[str, Any] = {"template": template, "data": data}
|
|
306
|
+
if version is not None:
|
|
307
|
+
body["version"] = version
|
|
308
|
+
result = self._http.request_json("POST", "/render/validate", json=body)
|
|
309
|
+
return result # type: ignore[no-any-return]
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""Schedules resource — scheduled render jobs and execution history."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from .._http import HttpClient
|
|
8
|
+
from ..pagination import PaginatedResult
|
|
9
|
+
from ..types import Schedule, ScheduleExecution
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class SchedulesResource:
|
|
13
|
+
"""Scheduled render jobs and execution history."""
|
|
14
|
+
|
|
15
|
+
def __init__(self, http: HttpClient) -> None:
|
|
16
|
+
self._http = http
|
|
17
|
+
|
|
18
|
+
def list(
|
|
19
|
+
self,
|
|
20
|
+
*,
|
|
21
|
+
limit: int | None = None,
|
|
22
|
+
offset: int | None = None,
|
|
23
|
+
enabled: bool | None = None,
|
|
24
|
+
) -> PaginatedResult[Schedule]:
|
|
25
|
+
"""List schedule definitions (paginated). Admin only."""
|
|
26
|
+
body = self._http.request_json(
|
|
27
|
+
"GET",
|
|
28
|
+
"/schedules/",
|
|
29
|
+
params={
|
|
30
|
+
"limit": limit,
|
|
31
|
+
"offset": offset,
|
|
32
|
+
"enabled": "true" if enabled is True else "false" if enabled is False else None,
|
|
33
|
+
},
|
|
34
|
+
)
|
|
35
|
+
return PaginatedResult(
|
|
36
|
+
items=[Schedule.model_validate(item) for item in body["items"]],
|
|
37
|
+
total=body["total"],
|
|
38
|
+
limit=body["limit"],
|
|
39
|
+
offset=body["offset"],
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
def get(self, id: str) -> Schedule:
|
|
43
|
+
"""Get a schedule by ID. Admin only."""
|
|
44
|
+
body = self._http.request_json("GET", f"/schedules/{id}")
|
|
45
|
+
return Schedule.model_validate(body)
|
|
46
|
+
|
|
47
|
+
def create(self, schedule: dict[str, Any]) -> Schedule:
|
|
48
|
+
"""Create a new schedule. Admin only."""
|
|
49
|
+
body = self._http.request_json("POST", "/schedules/", json=schedule)
|
|
50
|
+
return Schedule.model_validate(body)
|
|
51
|
+
|
|
52
|
+
def update(self, id: str, schedule: dict[str, Any]) -> Schedule:
|
|
53
|
+
"""Full-replace a schedule (PUT). Admin only."""
|
|
54
|
+
body = self._http.request_json("PUT", f"/schedules/{id}", json=schedule)
|
|
55
|
+
return Schedule.model_validate(body)
|
|
56
|
+
|
|
57
|
+
def patch(self, id: str, patch: dict[str, Any]) -> Schedule:
|
|
58
|
+
"""Partially update a schedule (PATCH). Admin only."""
|
|
59
|
+
body = self._http.request_json("PATCH", f"/schedules/{id}", json=patch)
|
|
60
|
+
return Schedule.model_validate(body)
|
|
61
|
+
|
|
62
|
+
def delete(self, id: str) -> None:
|
|
63
|
+
"""Delete a schedule. Admin only."""
|
|
64
|
+
self._http.request_json("DELETE", f"/schedules/{id}")
|
|
65
|
+
|
|
66
|
+
def trigger(self, id: str) -> ScheduleExecution:
|
|
67
|
+
"""Manually trigger a schedule execution. Admin only."""
|
|
68
|
+
body = self._http.request_json("POST", f"/schedules/{id}/trigger")
|
|
69
|
+
return ScheduleExecution.model_validate(body)
|
|
70
|
+
|
|
71
|
+
def list_executions(
|
|
72
|
+
self,
|
|
73
|
+
id: str,
|
|
74
|
+
*,
|
|
75
|
+
limit: int | None = None,
|
|
76
|
+
offset: int | None = None,
|
|
77
|
+
status: str | None = None,
|
|
78
|
+
) -> PaginatedResult[ScheduleExecution]:
|
|
79
|
+
"""List execution history for a schedule (paginated). Admin only."""
|
|
80
|
+
body = self._http.request_json(
|
|
81
|
+
"GET",
|
|
82
|
+
f"/schedules/{id}/executions",
|
|
83
|
+
params={"limit": limit, "offset": offset, "status": status},
|
|
84
|
+
)
|
|
85
|
+
return PaginatedResult(
|
|
86
|
+
items=[ScheduleExecution.model_validate(item) for item in body["items"]],
|
|
87
|
+
total=body["total"],
|
|
88
|
+
limit=body["limit"],
|
|
89
|
+
offset=body["offset"],
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
def get_execution(self, id: str, exec_id: str) -> ScheduleExecution:
|
|
93
|
+
"""Get a specific execution. Admin only."""
|
|
94
|
+
body = self._http.request_json("GET", f"/schedules/{id}/executions/{exec_id}")
|
|
95
|
+
return ScheduleExecution.model_validate(body)
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
"""Templates resource — CRUD, versioning, schema, validation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from .._http import HttpClient
|
|
8
|
+
from ..pagination import PaginatedResult
|
|
9
|
+
from ..types import (
|
|
10
|
+
TemplateDiff,
|
|
11
|
+
TemplateSummary,
|
|
12
|
+
TemplateWithDefinition,
|
|
13
|
+
ValidationResult,
|
|
14
|
+
VersionSummary,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class TemplatesResource:
|
|
19
|
+
"""Template CRUD and version management."""
|
|
20
|
+
|
|
21
|
+
def __init__(self, http: HttpClient) -> None:
|
|
22
|
+
self._http = http
|
|
23
|
+
|
|
24
|
+
def list(
|
|
25
|
+
self,
|
|
26
|
+
*,
|
|
27
|
+
limit: int | None = None,
|
|
28
|
+
offset: int | None = None,
|
|
29
|
+
) -> PaginatedResult[TemplateSummary]:
|
|
30
|
+
"""List templates (paginated)."""
|
|
31
|
+
body = self._http.request_json(
|
|
32
|
+
"GET",
|
|
33
|
+
"/templates/",
|
|
34
|
+
params={"limit": limit, "offset": offset},
|
|
35
|
+
)
|
|
36
|
+
return PaginatedResult(
|
|
37
|
+
items=[TemplateSummary.model_validate(item) for item in body["items"]],
|
|
38
|
+
total=body["total"],
|
|
39
|
+
limit=body["limit"],
|
|
40
|
+
offset=body["offset"],
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
def get(self, key: str) -> TemplateWithDefinition:
|
|
44
|
+
"""Get a template with its full definition."""
|
|
45
|
+
body = self._http.request_json("GET", f"/templates/{key}")
|
|
46
|
+
return TemplateWithDefinition.model_validate(body)
|
|
47
|
+
|
|
48
|
+
def create(self, definition: dict[str, Any]) -> TemplateSummary:
|
|
49
|
+
"""Create a new template."""
|
|
50
|
+
body = self._http.request_json("POST", "/templates/", json=definition)
|
|
51
|
+
return TemplateSummary.model_validate(body)
|
|
52
|
+
|
|
53
|
+
def update(
|
|
54
|
+
self,
|
|
55
|
+
key: str,
|
|
56
|
+
version: str,
|
|
57
|
+
definition: dict[str, Any],
|
|
58
|
+
) -> TemplateWithDefinition:
|
|
59
|
+
"""Update a template.
|
|
60
|
+
|
|
61
|
+
Requires the current version for optimistic concurrency (sent as
|
|
62
|
+
``If-Match`` header). The server returns 412 Precondition Failed if
|
|
63
|
+
another writer has updated the template since the version was fetched.
|
|
64
|
+
"""
|
|
65
|
+
body = self._http.request_json(
|
|
66
|
+
"PUT",
|
|
67
|
+
f"/templates/{key}",
|
|
68
|
+
json=definition,
|
|
69
|
+
headers={"If-Match": f'"{version}"'},
|
|
70
|
+
)
|
|
71
|
+
return TemplateWithDefinition.model_validate(body)
|
|
72
|
+
|
|
73
|
+
def delete(self, key: str, version: str) -> None:
|
|
74
|
+
"""Delete a template (admin only). Requires the current version."""
|
|
75
|
+
self._http.request_json(
|
|
76
|
+
"DELETE",
|
|
77
|
+
f"/templates/{key}",
|
|
78
|
+
headers={"If-Match": f'"{version}"'},
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
def schema(self, key: str) -> dict[str, Any]:
|
|
82
|
+
"""Get the JSON Schema describing the template's expected input data."""
|
|
83
|
+
body = self._http.request_json("GET", f"/templates/{key}/schema")
|
|
84
|
+
return body # type: ignore[no-any-return]
|
|
85
|
+
|
|
86
|
+
def sample(self, key: str) -> dict[str, Any]:
|
|
87
|
+
"""Get auto-generated sample data for a template."""
|
|
88
|
+
body = self._http.request_json("GET", f"/templates/{key}/sample")
|
|
89
|
+
return body # type: ignore[no-any-return]
|
|
90
|
+
|
|
91
|
+
def validate(self, key: str, input_data: dict[str, Any]) -> ValidationResult:
|
|
92
|
+
"""Validate input data against the template's schema."""
|
|
93
|
+
body = self._http.request_json(
|
|
94
|
+
"POST",
|
|
95
|
+
f"/templates/{key}/validate",
|
|
96
|
+
json=input_data,
|
|
97
|
+
)
|
|
98
|
+
return ValidationResult.model_validate(body)
|
|
99
|
+
|
|
100
|
+
def versions(
|
|
101
|
+
self,
|
|
102
|
+
key: str,
|
|
103
|
+
*,
|
|
104
|
+
limit: int | None = None,
|
|
105
|
+
offset: int | None = None,
|
|
106
|
+
) -> PaginatedResult[VersionSummary]:
|
|
107
|
+
"""List version history (paginated)."""
|
|
108
|
+
body = self._http.request_json(
|
|
109
|
+
"GET",
|
|
110
|
+
f"/templates/{key}/versions",
|
|
111
|
+
params={"limit": limit, "offset": offset},
|
|
112
|
+
)
|
|
113
|
+
return PaginatedResult(
|
|
114
|
+
items=[VersionSummary.model_validate(item) for item in body["items"]],
|
|
115
|
+
total=body["total"],
|
|
116
|
+
limit=body["limit"],
|
|
117
|
+
offset=body["offset"],
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
def get_version(self, key: str, version: str) -> TemplateWithDefinition:
|
|
121
|
+
"""Get a specific historical version."""
|
|
122
|
+
body = self._http.request_json("GET", f"/templates/{key}/versions/{version}")
|
|
123
|
+
return TemplateWithDefinition.model_validate(body)
|
|
124
|
+
|
|
125
|
+
def restore(
|
|
126
|
+
self,
|
|
127
|
+
key: str,
|
|
128
|
+
version: str,
|
|
129
|
+
current_version: str,
|
|
130
|
+
) -> TemplateWithDefinition:
|
|
131
|
+
"""Restore a historical version as the current version (admin only)."""
|
|
132
|
+
body = self._http.request_json(
|
|
133
|
+
"POST",
|
|
134
|
+
f"/templates/{key}/versions/{version}/restore",
|
|
135
|
+
headers={"If-Match": f'"{current_version}"'},
|
|
136
|
+
)
|
|
137
|
+
return TemplateWithDefinition.model_validate(body)
|
|
138
|
+
|
|
139
|
+
def diff(
|
|
140
|
+
self,
|
|
141
|
+
key: str,
|
|
142
|
+
*,
|
|
143
|
+
before_version: str,
|
|
144
|
+
after_version: str,
|
|
145
|
+
) -> TemplateDiff:
|
|
146
|
+
"""Diff two stored versions of the same template key.
|
|
147
|
+
|
|
148
|
+
Returns the raw ``TemplateDiff`` envelope from
|
|
149
|
+
``@pulp-engine/template-diff``. Both versions must exist in the
|
|
150
|
+
resolved tenant; otherwise the server returns 404 (raised as
|
|
151
|
+
:class:`PulpEngineError`).
|
|
152
|
+
"""
|
|
153
|
+
body = self._http.request_json(
|
|
154
|
+
"POST",
|
|
155
|
+
f"/templates/{key}/diff",
|
|
156
|
+
json={
|
|
157
|
+
"beforeVersion": before_version,
|
|
158
|
+
"afterVersion": after_version,
|
|
159
|
+
},
|
|
160
|
+
)
|
|
161
|
+
return TemplateDiff.model_validate(body)
|