pptx-cli 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.
- pptx_cli/__init__.py +5 -0
- pptx_cli/__main__.py +4 -0
- pptx_cli/cli.py +372 -0
- pptx_cli/commands/__init__.py +1 -0
- pptx_cli/commands/compose.py +73 -0
- pptx_cli/commands/guide.py +157 -0
- pptx_cli/commands/init.py +52 -0
- pptx_cli/commands/inspect.py +80 -0
- pptx_cli/commands/manifest_ops.py +23 -0
- pptx_cli/commands/validate.py +13 -0
- pptx_cli/commands/wrapper.py +191 -0
- pptx_cli/core/__init__.py +1 -0
- pptx_cli/core/composition.py +280 -0
- pptx_cli/core/ids.py +24 -0
- pptx_cli/core/io.py +60 -0
- pptx_cli/core/manifest_store.py +65 -0
- pptx_cli/core/runtime.py +30 -0
- pptx_cli/core/template.py +595 -0
- pptx_cli/core/validation.py +215 -0
- pptx_cli/core/versioning.py +47 -0
- pptx_cli/models/__init__.py +1 -0
- pptx_cli/models/envelope.py +50 -0
- pptx_cli/models/manifest.py +175 -0
- pptx_cli-1.0.0.dist-info/METADATA +505 -0
- pptx_cli-1.0.0.dist-info/RECORD +28 -0
- pptx_cli-1.0.0.dist-info/WHEEL +4 -0
- pptx_cli-1.0.0.dist-info/entry_points.txt +2 -0
- pptx_cli-1.0.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from pptx import Presentation
|
|
6
|
+
|
|
7
|
+
from pptx_cli.models.manifest import (
|
|
8
|
+
ManifestDiffResult,
|
|
9
|
+
ManifestDocument,
|
|
10
|
+
ValidationIssue,
|
|
11
|
+
ValidationResult,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ValidationError(ValueError):
|
|
16
|
+
code: str
|
|
17
|
+
|
|
18
|
+
def __init__(self, code: str, message: str) -> None:
|
|
19
|
+
super().__init__(message)
|
|
20
|
+
self.code = code
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def validate_deck(
|
|
24
|
+
manifest_dir: Path,
|
|
25
|
+
manifest: ManifestDocument,
|
|
26
|
+
deck_path: Path,
|
|
27
|
+
*,
|
|
28
|
+
strict: bool,
|
|
29
|
+
) -> ValidationResult:
|
|
30
|
+
if not deck_path.exists():
|
|
31
|
+
raise ValidationError("ERR_IO_NOT_FOUND", f"Deck not found: {deck_path}")
|
|
32
|
+
|
|
33
|
+
prs = Presentation(str(deck_path))
|
|
34
|
+
issues: list[ValidationIssue] = []
|
|
35
|
+
expected_size = manifest.presentation.get("page_size", {})
|
|
36
|
+
actual_width = int(prs.slide_width or 0)
|
|
37
|
+
actual_height = int(prs.slide_height or 0)
|
|
38
|
+
if actual_width != expected_size.get("width_emu") or actual_height != expected_size.get(
|
|
39
|
+
"height_emu"
|
|
40
|
+
):
|
|
41
|
+
issues.append(
|
|
42
|
+
ValidationIssue(
|
|
43
|
+
code="ERR_VALIDATION_PAGE_SIZE",
|
|
44
|
+
severity="error",
|
|
45
|
+
message="Deck page size does not match the manifest",
|
|
46
|
+
details={
|
|
47
|
+
"expected": expected_size,
|
|
48
|
+
"actual": {"width_emu": actual_width, "height_emu": actual_height},
|
|
49
|
+
},
|
|
50
|
+
)
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
layouts_by_source_name = {layout.source_layout_name: layout for layout in manifest.layouts}
|
|
54
|
+
checked_layouts: set[str] = set()
|
|
55
|
+
for slide_index, slide in enumerate(prs.slides, start=1):
|
|
56
|
+
source_layout = getattr(slide, "slide_layout", None)
|
|
57
|
+
layout_name = getattr(source_layout, "name", None)
|
|
58
|
+
if layout_name not in layouts_by_source_name:
|
|
59
|
+
issues.append(
|
|
60
|
+
ValidationIssue(
|
|
61
|
+
code="ERR_VALIDATION_LAYOUT_UNKNOWN",
|
|
62
|
+
severity="error",
|
|
63
|
+
message="Slide uses a layout that is not present in the manifest",
|
|
64
|
+
details={"slide_index": slide_index, "layout_name": layout_name},
|
|
65
|
+
)
|
|
66
|
+
)
|
|
67
|
+
continue
|
|
68
|
+
|
|
69
|
+
layout = layouts_by_source_name[layout_name]
|
|
70
|
+
checked_layouts.add(layout.id)
|
|
71
|
+
expected_idxs = {placeholder.placeholder_idx for placeholder in layout.placeholders}
|
|
72
|
+
actual_idxs = {shape.placeholder_format.idx for shape in slide.placeholders}
|
|
73
|
+
missing_idxs = sorted(expected_idxs - actual_idxs)
|
|
74
|
+
if missing_idxs:
|
|
75
|
+
issues.append(
|
|
76
|
+
ValidationIssue(
|
|
77
|
+
code="ERR_VALIDATION_PLACEHOLDER_MISSING",
|
|
78
|
+
severity="error",
|
|
79
|
+
message="Slide is missing expected placeholders for its layout",
|
|
80
|
+
details={
|
|
81
|
+
"slide_index": slide_index,
|
|
82
|
+
"layout_id": layout.id,
|
|
83
|
+
"placeholder_idxs": missing_idxs,
|
|
84
|
+
},
|
|
85
|
+
)
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
if manifest.compatibility_report.findings:
|
|
89
|
+
for finding in manifest.compatibility_report.findings:
|
|
90
|
+
if finding.severity == "warning":
|
|
91
|
+
issues.append(
|
|
92
|
+
ValidationIssue(
|
|
93
|
+
code=finding.code,
|
|
94
|
+
severity="error" if strict else "warning",
|
|
95
|
+
message=f"Manifest compatibility warning: {finding.message}",
|
|
96
|
+
details=finding.details,
|
|
97
|
+
)
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
has_errors = any(issue.severity == "error" for issue in issues)
|
|
101
|
+
return ValidationResult(
|
|
102
|
+
manifest_path=str(manifest_dir),
|
|
103
|
+
deck_path=str(deck_path),
|
|
104
|
+
ok=not has_errors,
|
|
105
|
+
issues=issues,
|
|
106
|
+
checked_slides=len(prs.slides),
|
|
107
|
+
checked_layouts=len(checked_layouts),
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def diff_manifests(left: ManifestDocument, right: ManifestDocument) -> ManifestDiffResult:
|
|
112
|
+
result = ManifestDiffResult()
|
|
113
|
+
left_layouts = {layout.id: layout for layout in left.layouts}
|
|
114
|
+
right_layouts = {layout.id: layout for layout in right.layouts}
|
|
115
|
+
|
|
116
|
+
removed_layouts = sorted(set(left_layouts) - set(right_layouts))
|
|
117
|
+
added_layouts = sorted(set(right_layouts) - set(left_layouts))
|
|
118
|
+
|
|
119
|
+
for layout_id in removed_layouts:
|
|
120
|
+
result.breaking_changes.append({"type": "layout.removed", "layout_id": layout_id})
|
|
121
|
+
for layout_id in added_layouts:
|
|
122
|
+
result.additive_changes.append({"type": "layout.added", "layout_id": layout_id})
|
|
123
|
+
|
|
124
|
+
shared_layouts = sorted(set(left_layouts) & set(right_layouts))
|
|
125
|
+
for layout_id in shared_layouts:
|
|
126
|
+
left_layout = left_layouts[layout_id]
|
|
127
|
+
right_layout = right_layouts[layout_id]
|
|
128
|
+
if left_layout.source_layout_name != right_layout.source_layout_name:
|
|
129
|
+
result.breaking_changes.append(
|
|
130
|
+
{
|
|
131
|
+
"type": "layout.renamed",
|
|
132
|
+
"layout_id": layout_id,
|
|
133
|
+
"before": left_layout.source_layout_name,
|
|
134
|
+
"after": right_layout.source_layout_name,
|
|
135
|
+
}
|
|
136
|
+
)
|
|
137
|
+
left_placeholders = {
|
|
138
|
+
placeholder.logical_name: placeholder for placeholder in left_layout.placeholders
|
|
139
|
+
}
|
|
140
|
+
right_placeholders = {
|
|
141
|
+
placeholder.logical_name: placeholder for placeholder in right_layout.placeholders
|
|
142
|
+
}
|
|
143
|
+
removed_placeholders = sorted(set(left_placeholders) - set(right_placeholders))
|
|
144
|
+
added_placeholders = sorted(set(right_placeholders) - set(left_placeholders))
|
|
145
|
+
for placeholder_name in removed_placeholders:
|
|
146
|
+
result.breaking_changes.append(
|
|
147
|
+
{
|
|
148
|
+
"type": "placeholder.removed",
|
|
149
|
+
"layout_id": layout_id,
|
|
150
|
+
"placeholder": placeholder_name,
|
|
151
|
+
}
|
|
152
|
+
)
|
|
153
|
+
for placeholder_name in added_placeholders:
|
|
154
|
+
result.additive_changes.append(
|
|
155
|
+
{
|
|
156
|
+
"type": "placeholder.added",
|
|
157
|
+
"layout_id": layout_id,
|
|
158
|
+
"placeholder": placeholder_name,
|
|
159
|
+
}
|
|
160
|
+
)
|
|
161
|
+
for placeholder_name in sorted(set(left_placeholders) & set(right_placeholders)):
|
|
162
|
+
left_placeholder = left_placeholders[placeholder_name]
|
|
163
|
+
right_placeholder = right_placeholders[placeholder_name]
|
|
164
|
+
left_geometry = (
|
|
165
|
+
left_placeholder.left_emu,
|
|
166
|
+
left_placeholder.top_emu,
|
|
167
|
+
left_placeholder.width_emu,
|
|
168
|
+
left_placeholder.height_emu,
|
|
169
|
+
)
|
|
170
|
+
right_geometry = (
|
|
171
|
+
right_placeholder.left_emu,
|
|
172
|
+
right_placeholder.top_emu,
|
|
173
|
+
right_placeholder.width_emu,
|
|
174
|
+
right_placeholder.height_emu,
|
|
175
|
+
)
|
|
176
|
+
if left_geometry != right_geometry:
|
|
177
|
+
result.breaking_changes.append(
|
|
178
|
+
{
|
|
179
|
+
"type": "placeholder.geometry_changed",
|
|
180
|
+
"layout_id": layout_id,
|
|
181
|
+
"placeholder": placeholder_name,
|
|
182
|
+
"before": left_geometry,
|
|
183
|
+
"after": right_geometry,
|
|
184
|
+
}
|
|
185
|
+
)
|
|
186
|
+
if (
|
|
187
|
+
left_placeholder.supported_content_types
|
|
188
|
+
!= right_placeholder.supported_content_types
|
|
189
|
+
):
|
|
190
|
+
result.breaking_changes.append(
|
|
191
|
+
{
|
|
192
|
+
"type": "placeholder.content_types_changed",
|
|
193
|
+
"layout_id": layout_id,
|
|
194
|
+
"placeholder": placeholder_name,
|
|
195
|
+
"before": left_placeholder.supported_content_types,
|
|
196
|
+
"after": right_placeholder.supported_content_types,
|
|
197
|
+
}
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
left_theme = left.presentation.get("theme", {})
|
|
201
|
+
right_theme = right.presentation.get("theme", {})
|
|
202
|
+
if left_theme != right_theme:
|
|
203
|
+
result.breaking_changes.append(
|
|
204
|
+
{
|
|
205
|
+
"type": "theme.changed",
|
|
206
|
+
"before": left_theme,
|
|
207
|
+
"after": right_theme,
|
|
208
|
+
}
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
if not result.breaking_changes and not result.additive_changes:
|
|
212
|
+
result.unchanged.append("layouts")
|
|
213
|
+
result.unchanged.append("theme")
|
|
214
|
+
|
|
215
|
+
return result
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
_VERSION_RE = re.compile(r'__version__\s*=\s*["\'](?P<version>\d+\.\d+\.\d+)["\']')
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass(frozen=True, slots=True)
|
|
11
|
+
class SemVer:
|
|
12
|
+
major: int
|
|
13
|
+
minor: int
|
|
14
|
+
patch: int
|
|
15
|
+
|
|
16
|
+
@classmethod
|
|
17
|
+
def parse(cls, value: str) -> SemVer:
|
|
18
|
+
parts = value.split(".")
|
|
19
|
+
if len(parts) != 3 or not all(part.isdigit() for part in parts):
|
|
20
|
+
raise ValueError(f"Invalid semantic version: {value}")
|
|
21
|
+
return cls(*(int(part) for part in parts))
|
|
22
|
+
|
|
23
|
+
def bump(self, part: str) -> SemVer:
|
|
24
|
+
if part == "major":
|
|
25
|
+
return SemVer(self.major + 1, 0, 0)
|
|
26
|
+
if part == "minor":
|
|
27
|
+
return SemVer(self.major, self.minor + 1, 0)
|
|
28
|
+
if part == "patch":
|
|
29
|
+
return SemVer(self.major, self.minor, self.patch + 1)
|
|
30
|
+
raise ValueError(f"Unsupported version part: {part}")
|
|
31
|
+
|
|
32
|
+
def __str__(self) -> str:
|
|
33
|
+
return f"{self.major}.{self.minor}.{self.patch}"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def read_version_from_init(init_file: Path) -> SemVer:
|
|
37
|
+
contents = init_file.read_text(encoding="utf-8")
|
|
38
|
+
match = _VERSION_RE.search(contents)
|
|
39
|
+
if match is None:
|
|
40
|
+
raise ValueError(f"Could not find __version__ in {init_file}")
|
|
41
|
+
return SemVer.parse(match.group("version"))
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def write_version_to_init(init_file: Path, version: SemVer) -> None:
|
|
45
|
+
contents = init_file.read_text(encoding="utf-8")
|
|
46
|
+
updated = _VERSION_RE.sub(f'__version__ = "{version}"', contents, count=1)
|
|
47
|
+
init_file.write_text(updated, encoding="utf-8")
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Typed models for CLI contracts."""
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Literal
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel, Field
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class CliMessage(BaseModel):
|
|
9
|
+
code: str
|
|
10
|
+
message: str
|
|
11
|
+
retryable: bool = False
|
|
12
|
+
retry_after_ms: int | None = None
|
|
13
|
+
suggested_action: str | None = None
|
|
14
|
+
details: dict[str, Any] = Field(default_factory=dict)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class Metrics(BaseModel):
|
|
18
|
+
duration_ms: int
|
|
19
|
+
operations_executed: int | None = None
|
|
20
|
+
bytes_written: int | None = None
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class Envelope(BaseModel):
|
|
24
|
+
schema_version: str = "1.0"
|
|
25
|
+
request_id: str
|
|
26
|
+
ok: bool
|
|
27
|
+
command: str
|
|
28
|
+
result: dict[str, Any] | list[Any] | str | int | float | bool | None
|
|
29
|
+
warnings: list[CliMessage] = Field(default_factory=list)
|
|
30
|
+
errors: list[CliMessage] = Field(default_factory=list)
|
|
31
|
+
metrics: Metrics
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class GuideCommand(BaseModel):
|
|
35
|
+
id: str
|
|
36
|
+
summary: str
|
|
37
|
+
mutates: bool
|
|
38
|
+
input_schema: dict[str, Any] | None = None
|
|
39
|
+
output_schema: dict[str, Any] | None = None
|
|
40
|
+
examples: list[str] = Field(default_factory=list)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class GuideDocument(BaseModel):
|
|
44
|
+
schema_version: Literal["1.0"] = "1.0"
|
|
45
|
+
compatibility: dict[str, str]
|
|
46
|
+
commands: list[GuideCommand]
|
|
47
|
+
exit_codes: dict[str, int]
|
|
48
|
+
error_codes: dict[str, dict[str, Any]] = Field(default_factory=dict)
|
|
49
|
+
identifier_conventions: dict[str, str] = Field(default_factory=dict)
|
|
50
|
+
concurrency: dict[str, Any] = Field(default_factory=dict)
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from typing import Any, Literal
|
|
5
|
+
|
|
6
|
+
from pydantic import BaseModel, Field
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class PageSize(BaseModel):
|
|
10
|
+
width_emu: int
|
|
11
|
+
height_emu: int
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ThemeModel(BaseModel):
|
|
15
|
+
name: str | None = None
|
|
16
|
+
colors: dict[str, str] = Field(default_factory=dict)
|
|
17
|
+
fonts: dict[str, str] = Field(default_factory=dict)
|
|
18
|
+
effects: dict[str, str] = Field(default_factory=dict)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class AssetRef(BaseModel):
|
|
22
|
+
id: str
|
|
23
|
+
kind: Literal["image", "media", "embedded", "template", "theme", "xml"]
|
|
24
|
+
path: str
|
|
25
|
+
source_path: str | None = None
|
|
26
|
+
sha256: str
|
|
27
|
+
size_bytes: int
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class ProtectedElement(BaseModel):
|
|
31
|
+
element_id: str
|
|
32
|
+
element_type: str
|
|
33
|
+
name: str
|
|
34
|
+
left_emu: int
|
|
35
|
+
top_emu: int
|
|
36
|
+
width_emu: int
|
|
37
|
+
height_emu: int
|
|
38
|
+
lock_policy: Literal["locked"] = "locked"
|
|
39
|
+
asset_ref: str | None = None
|
|
40
|
+
fingerprint: str
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class PlaceholderContract(BaseModel):
|
|
44
|
+
logical_name: str
|
|
45
|
+
source_name: str
|
|
46
|
+
placeholder_idx: int
|
|
47
|
+
placeholder_type: str
|
|
48
|
+
guidance_text: str | None = None
|
|
49
|
+
guidance_lines: list[str] = Field(default_factory=list)
|
|
50
|
+
supported_content_types: list[str]
|
|
51
|
+
left_emu: int
|
|
52
|
+
top_emu: int
|
|
53
|
+
width_emu: int
|
|
54
|
+
height_emu: int
|
|
55
|
+
required: bool = False
|
|
56
|
+
overflow_policy: Literal["fit", "warn", "truncate"] = "warn"
|
|
57
|
+
text_defaults: dict[str, Any] = Field(default_factory=dict)
|
|
58
|
+
inheritance_chain: list[str] = Field(default_factory=list)
|
|
59
|
+
allowed_formatting_overrides: list[str] = Field(default_factory=list)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class LayoutContract(BaseModel):
|
|
63
|
+
id: str
|
|
64
|
+
name: str
|
|
65
|
+
aliases: list[str] = Field(default_factory=list)
|
|
66
|
+
source_master_id: str
|
|
67
|
+
source_layout_index: int
|
|
68
|
+
source_layout_name: str
|
|
69
|
+
description: str | None = None
|
|
70
|
+
preview_path: str
|
|
71
|
+
placeholders: list[PlaceholderContract] = Field(default_factory=list)
|
|
72
|
+
protected_static_elements: list[ProtectedElement] = Field(default_factory=list)
|
|
73
|
+
validation_rules: dict[str, Any] = Field(default_factory=dict)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class MasterContract(BaseModel):
|
|
77
|
+
id: str
|
|
78
|
+
name: str
|
|
79
|
+
layout_ids: list[str] = Field(default_factory=list)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class TemplateInfo(BaseModel):
|
|
83
|
+
name: str
|
|
84
|
+
source_file: str
|
|
85
|
+
source_hash: str
|
|
86
|
+
extracted_at: datetime
|
|
87
|
+
stored_template_path: str
|
|
88
|
+
locale: str | None = None
|
|
89
|
+
owner: str | None = None
|
|
90
|
+
version: str | None = None
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class CompatibilityFinding(BaseModel):
|
|
94
|
+
code: str
|
|
95
|
+
severity: Literal["info", "warning", "error"]
|
|
96
|
+
message: str
|
|
97
|
+
details: dict[str, Any] = Field(default_factory=dict)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class CompatibilityReport(BaseModel):
|
|
101
|
+
status: Literal["ok", "warn", "error"] = "ok"
|
|
102
|
+
findings: list[CompatibilityFinding] = Field(default_factory=list)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class ManifestDocument(BaseModel):
|
|
106
|
+
manifest_version: Literal[1] = 1
|
|
107
|
+
template: TemplateInfo
|
|
108
|
+
presentation: dict[str, Any]
|
|
109
|
+
masters: list[MasterContract]
|
|
110
|
+
layouts: list[LayoutContract]
|
|
111
|
+
assets: list[AssetRef] = Field(default_factory=list)
|
|
112
|
+
rules: dict[str, Any] = Field(default_factory=dict)
|
|
113
|
+
capabilities: dict[str, Any] = Field(default_factory=dict)
|
|
114
|
+
compatibility_report: CompatibilityReport = Field(default_factory=CompatibilityReport)
|
|
115
|
+
fingerprints: dict[str, str] = Field(default_factory=dict)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class LayoutAnnotation(BaseModel):
|
|
119
|
+
layout_id: str
|
|
120
|
+
aliases: list[str] = Field(default_factory=list)
|
|
121
|
+
semantic_tags: list[str] = Field(default_factory=list)
|
|
122
|
+
usage_notes: str | None = None
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
class TemplateAnnotations(BaseModel):
|
|
126
|
+
semantic_tags: list[str] = Field(default_factory=list)
|
|
127
|
+
operator_notes: str | None = None
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
class AnnotationsDocument(BaseModel):
|
|
131
|
+
template_annotations: TemplateAnnotations = Field(default_factory=TemplateAnnotations)
|
|
132
|
+
layouts: list[LayoutAnnotation] = Field(default_factory=list)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
class InitReport(BaseModel):
|
|
136
|
+
template: str
|
|
137
|
+
output_dir: str
|
|
138
|
+
manifest_path: str
|
|
139
|
+
findings: list[CompatibilityFinding] = Field(default_factory=list)
|
|
140
|
+
assets_copied: int = 0
|
|
141
|
+
layout_count: int = 0
|
|
142
|
+
placeholder_count: int = 0
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
class SlideSpec(BaseModel):
|
|
146
|
+
layout: str
|
|
147
|
+
content: dict[str, Any] = Field(default_factory=dict)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
class DeckSpec(BaseModel):
|
|
151
|
+
manifest: str | None = None
|
|
152
|
+
metadata: dict[str, Any] = Field(default_factory=dict)
|
|
153
|
+
slides: list[SlideSpec]
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
class ValidationIssue(BaseModel):
|
|
157
|
+
code: str
|
|
158
|
+
severity: Literal["warning", "error"]
|
|
159
|
+
message: str
|
|
160
|
+
details: dict[str, Any] = Field(default_factory=dict)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
class ValidationResult(BaseModel):
|
|
164
|
+
manifest_path: str
|
|
165
|
+
deck_path: str
|
|
166
|
+
ok: bool
|
|
167
|
+
issues: list[ValidationIssue] = Field(default_factory=list)
|
|
168
|
+
checked_slides: int = 0
|
|
169
|
+
checked_layouts: int = 0
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
class ManifestDiffResult(BaseModel):
|
|
173
|
+
breaking_changes: list[dict[str, Any]] = Field(default_factory=list)
|
|
174
|
+
additive_changes: list[dict[str, Any]] = Field(default_factory=list)
|
|
175
|
+
unchanged: list[str] = Field(default_factory=list)
|