python-hwpx-automation 6.0.3__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.
- hwpx_automation/__init__.py +61 -0
- hwpx_automation/__init__.pyi +27 -0
- hwpx_automation/__main__.py +8 -0
- hwpx_automation/agent_document.py +392 -0
- hwpx_automation/api.py +136 -0
- hwpx_automation/blind_eval.py +407 -0
- hwpx_automation/capabilities.py +110 -0
- hwpx_automation/compat.py +48 -0
- hwpx_automation/configuration.py +60 -0
- hwpx_automation/core/__init__.py +2 -0
- hwpx_automation/core/content.py +762 -0
- hwpx_automation/core/context.py +111 -0
- hwpx_automation/core/diff.py +53 -0
- hwpx_automation/core/document.py +37 -0
- hwpx_automation/core/formatting.py +513 -0
- hwpx_automation/core/handles.py +24 -0
- hwpx_automation/core/locations.py +205 -0
- hwpx_automation/core/locator.py +162 -0
- hwpx_automation/core/plan.py +680 -0
- hwpx_automation/core/resources.py +42 -0
- hwpx_automation/core/search.py +296 -0
- hwpx_automation/core/transactions.py +434 -0
- hwpx_automation/core/txn.py +48 -0
- hwpx_automation/document_state.py +95 -0
- hwpx_automation/errors.py +174 -0
- hwpx_automation/execution_lock.py +15 -0
- hwpx_automation/fastmcp_adapter.py +672 -0
- hwpx_automation/form_fill.py +1177 -0
- hwpx_automation/form_output_models.py +223 -0
- hwpx_automation/handlers/__init__.py +2 -0
- hwpx_automation/handlers/_shared.py +377 -0
- hwpx_automation/handlers/agent_document.py +257 -0
- hwpx_automation/handlers/authoring.py +750 -0
- hwpx_automation/handlers/content_edit.py +1078 -0
- hwpx_automation/handlers/form_fill.py +607 -0
- hwpx_automation/handlers/layout_style.py +660 -0
- hwpx_automation/handlers/quality_render.py +566 -0
- hwpx_automation/handlers/read_export.py +1295 -0
- hwpx_automation/handlers/specialized.py +624 -0
- hwpx_automation/handlers/tracked_changes.py +589 -0
- hwpx_automation/handlers/workflow.py +105 -0
- hwpx_automation/hwp_converter.py +227 -0
- hwpx_automation/hwp_support.py +94 -0
- hwpx_automation/hwpx_ops.py +1439 -0
- hwpx_automation/identity.json +263 -0
- hwpx_automation/identity.py +18 -0
- hwpx_automation/ingest_adapters.py +85 -0
- hwpx_automation/markdown_plan.py +216 -0
- hwpx_automation/mcp_cli.py +29 -0
- hwpx_automation/metadata/tools_meta.py +40 -0
- hwpx_automation/mixed_form.py +3007 -0
- hwpx_automation/mutation_models.py +401 -0
- hwpx_automation/network_policy.py +232 -0
- hwpx_automation/office/__init__.py +14 -0
- hwpx_automation/office/agent/__init__.py +125 -0
- hwpx_automation/office/agent/_batch_verification.py +383 -0
- hwpx_automation/office/agent/blueprint/__init__.py +58 -0
- hwpx_automation/office/agent/blueprint/bundle.py +282 -0
- hwpx_automation/office/agent/blueprint/catalog.py +136 -0
- hwpx_automation/office/agent/blueprint/dump.py +520 -0
- hwpx_automation/office/agent/blueprint/mapping.py +312 -0
- hwpx_automation/office/agent/blueprint/model.py +722 -0
- hwpx_automation/office/agent/blueprint/native.py +621 -0
- hwpx_automation/office/agent/blueprint/replay.py +622 -0
- hwpx_automation/office/agent/catalog.py +252 -0
- hwpx_automation/office/agent/cli.py +647 -0
- hwpx_automation/office/agent/commands.py +1383 -0
- hwpx_automation/office/agent/document.py +801 -0
- hwpx_automation/office/agent/form_plan.py +1760 -0
- hwpx_automation/office/agent/model.py +808 -0
- hwpx_automation/office/agent/path.py +155 -0
- hwpx_automation/office/agent/query.py +230 -0
- hwpx_automation/office/agent/story.py +207 -0
- hwpx_automation/office/authoring/__init__.py +3542 -0
- hwpx_automation/office/authoring/advanced_generators.py +154 -0
- hwpx_automation/office/authoring/builder/__init__.py +52 -0
- hwpx_automation/office/authoring/builder/core.py +996 -0
- hwpx_automation/office/authoring/builder/report.py +195 -0
- hwpx_automation/office/authoring/design/__init__.py +30 -0
- hwpx_automation/office/authoring/design/_support.py +144 -0
- hwpx_automation/office/authoring/design/composer.py +282 -0
- hwpx_automation/office/authoring/design/harvest.py +305 -0
- hwpx_automation/office/authoring/design/plan.py +69 -0
- hwpx_automation/office/authoring/design/profile.py +88 -0
- hwpx_automation/office/authoring/design/profiles/application_form/fragments/body.xml +1 -0
- hwpx_automation/office/authoring/design/profiles/application_form/fragments/heading.xml +1 -0
- hwpx_automation/office/authoring/design/profiles/application_form/fragments/info_table.xml +1 -0
- hwpx_automation/office/authoring/design/profiles/application_form/fragments/title.xml +1 -0
- hwpx_automation/office/authoring/design/profiles/application_form/profile.json +25 -0
- hwpx_automation/office/authoring/design/profiles/application_form/template.hwpx +0 -0
- hwpx_automation/office/authoring/design/profiles/home_notice/fragments/body.xml +1 -0
- hwpx_automation/office/authoring/design/profiles/home_notice/fragments/heading.xml +1 -0
- hwpx_automation/office/authoring/design/profiles/home_notice/fragments/title.xml +1 -0
- hwpx_automation/office/authoring/design/profiles/home_notice/profile.json +24 -0
- hwpx_automation/office/authoring/design/profiles/home_notice/template.hwpx +0 -0
- hwpx_automation/office/authoring/design/profiles/official_notice/fragments/body.xml +1 -0
- hwpx_automation/office/authoring/design/profiles/official_notice/fragments/heading.xml +1 -0
- hwpx_automation/office/authoring/design/profiles/official_notice/fragments/info_table.xml +1 -0
- hwpx_automation/office/authoring/design/profiles/official_notice/fragments/title.xml +1 -0
- hwpx_automation/office/authoring/design/profiles/official_notice/profile.json +25 -0
- hwpx_automation/office/authoring/design/profiles/official_notice/template.hwpx +0 -0
- hwpx_automation/office/authoring/design/profiles/report/fragments/body.xml +1 -0
- hwpx_automation/office/authoring/design/profiles/report/fragments/heading.xml +1 -0
- hwpx_automation/office/authoring/design/profiles/report/fragments/info_table.xml +1 -0
- hwpx_automation/office/authoring/design/profiles/report/fragments/title.xml +1 -0
- hwpx_automation/office/authoring/design/profiles/report/profile.json +25 -0
- hwpx_automation/office/authoring/design/profiles/report/template.hwpx +0 -0
- hwpx_automation/office/authoring/design/validator.py +107 -0
- hwpx_automation/office/authoring/presets/__init__.py +22 -0
- hwpx_automation/office/authoring/presets/proposal.py +538 -0
- hwpx_automation/office/authoring/report_parser.py +141 -0
- hwpx_automation/office/authoring/style_profile.py +437 -0
- hwpx_automation/office/authoring/template_analyzer.py +657 -0
- hwpx_automation/office/compliance/__init__.py +38 -0
- hwpx_automation/office/compliance/official_lint.py +478 -0
- hwpx_automation/office/compliance/pii.py +388 -0
- hwpx_automation/office/document_ops/__init__.py +13 -0
- hwpx_automation/office/document_ops/comparison.py +62 -0
- hwpx_automation/office/document_ops/mail_merge.py +73 -0
- hwpx_automation/office/document_ops/redline.py +35 -0
- hwpx_automation/office/evalplan/__init__.py +36 -0
- hwpx_automation/office/evalplan/runtime.py +2762 -0
- hwpx_automation/office/exam/__init__.py +44 -0
- hwpx_automation/office/exam/compose.py +282 -0
- hwpx_automation/office/exam/ir.py +44 -0
- hwpx_automation/office/exam/measure.py +163 -0
- hwpx_automation/office/exam/parser.py +150 -0
- hwpx_automation/office/exam/profile.py +123 -0
- hwpx_automation/office/form_fill/__init__.py +66 -0
- hwpx_automation/office/form_fill/classification.py +108 -0
- hwpx_automation/office/form_fill/fill_residue.py +242 -0
- hwpx_automation/office/form_fill/fit/__init__.py +36 -0
- hwpx_automation/office/form_fill/fit/apply.py +24 -0
- hwpx_automation/office/form_fill/fit/engine.py +24 -0
- hwpx_automation/office/form_fill/fit/measure.py +50 -0
- hwpx_automation/office/form_fill/fit/policy.py +28 -0
- hwpx_automation/office/form_fill/fit/report.py +28 -0
- hwpx_automation/office/form_fill/fit/seal.py +457 -0
- hwpx_automation/office/form_fill/fit/wordbox.py +1343 -0
- hwpx_automation/office/form_fill/guidance.py +704 -0
- hwpx_automation/office/form_fill/quality.py +961 -0
- hwpx_automation/office/form_fill/split_run.py +333 -0
- hwpx_automation/office/form_fill/template_formfit.py +656 -0
- hwpx_automation/office/house_style/__init__.py +196 -0
- hwpx_automation/office/house_style/composition.py +68 -0
- hwpx_automation/office/house_style/data/bank.json +625 -0
- hwpx_automation/office/house_style/data/genres.json +43 -0
- hwpx_automation/office/quality/__init__.py +14 -0
- hwpx_automation/office/quality/page_guard.py +277 -0
- hwpx_automation/office/rendering/__init__.py +145 -0
- hwpx_automation/office/rendering/_hancom_open_rate.ps1 +374 -0
- hwpx_automation/office/rendering/_refresh_hwpx_mac.applescript +162 -0
- hwpx_automation/office/rendering/_render_hwpx.ps1 +72 -0
- hwpx_automation/office/rendering/_render_hwpx_mac.applescript +249 -0
- hwpx_automation/office/rendering/block_splits.py +76 -0
- hwpx_automation/office/rendering/detectors.py +151 -0
- hwpx_automation/office/rendering/diff.py +153 -0
- hwpx_automation/office/rendering/fixture_corpus.py +215 -0
- hwpx_automation/office/rendering/oracle.py +909 -0
- hwpx_automation/office/rendering/page_qa.py +245 -0
- hwpx_automation/office/rendering/qa_contracts.py +293 -0
- hwpx_automation/office/rendering/qa_metrics.py +241 -0
- hwpx_automation/office/rendering/worker.py +290 -0
- hwpx_automation/office/utilities/__init__.py +12 -0
- hwpx_automation/office/utilities/table_compute.py +477 -0
- hwpx_automation/ops_services/__init__.py +1 -0
- hwpx_automation/ops_services/_border_fill.py +283 -0
- hwpx_automation/ops_services/composition.py +55 -0
- hwpx_automation/ops_services/content_layout.py +322 -0
- hwpx_automation/ops_services/context.py +213 -0
- hwpx_automation/ops_services/form_fields.py +557 -0
- hwpx_automation/ops_services/media.py +178 -0
- hwpx_automation/ops_services/memo_style.py +477 -0
- hwpx_automation/ops_services/package_validation.py +166 -0
- hwpx_automation/ops_services/planning.py +201 -0
- hwpx_automation/ops_services/preview_export.py +585 -0
- hwpx_automation/ops_services/read_query.py +601 -0
- hwpx_automation/ops_services/save_policy.py +604 -0
- hwpx_automation/ops_services/tables.py +539 -0
- hwpx_automation/ops_services/transactions.py +616 -0
- hwpx_automation/preview_output_models.py +69 -0
- hwpx_automation/public-modules.json +206 -0
- hwpx_automation/py.typed +1 -0
- hwpx_automation/quality.py +351 -0
- hwpx_automation/quality_generation.py +725 -0
- hwpx_automation/runtime.py +321 -0
- hwpx_automation/runtime_services.py +100 -0
- hwpx_automation/server.py +259 -0
- hwpx_automation/storage.py +747 -0
- hwpx_automation/tool_bindings.py +170 -0
- hwpx_automation/tool_contract.py +982 -0
- hwpx_automation/upstream.py +755 -0
- hwpx_automation/utils/__init__.py +2 -0
- hwpx_automation/utils/helpers.py +29 -0
- hwpx_automation/visual_qa.py +667 -0
- hwpx_automation/workflow/__init__.py +55 -0
- hwpx_automation/workflow/adapters.py +482 -0
- hwpx_automation/workflow/dispatcher.py +213 -0
- hwpx_automation/workflow/models.py +243 -0
- hwpx_automation/workflow/policy.py +198 -0
- hwpx_automation/workflow/render_contracts.py +173 -0
- hwpx_automation/workflow/render_metrics.py +196 -0
- hwpx_automation/workflow/render_queue.py +482 -0
- hwpx_automation/workflow/render_security.py +172 -0
- hwpx_automation/workflow/render_transport.py +369 -0
- hwpx_automation/workflow/rendering.py +206 -0
- hwpx_automation/workflow/service.py +758 -0
- hwpx_automation/workflow/state_machine.py +65 -0
- hwpx_automation/workflow/store.py +747 -0
- hwpx_automation/workspace.py +1694 -0
- python_hwpx_automation-6.0.3.dist-info/METADATA +279 -0
- python_hwpx_automation-6.0.3.dist-info/RECORD +217 -0
- python_hwpx_automation-6.0.3.dist-info/WHEEL +5 -0
- python_hwpx_automation-6.0.3.dist-info/entry_points.txt +3 -0
- python_hwpx_automation-6.0.3.dist-info/licenses/LICENSE +178 -0
- python_hwpx_automation-6.0.3.dist-info/licenses/NOTICE +14 -0
- python_hwpx_automation-6.0.3.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""Typed house-style data service owned by the automation application layer."""
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import copy
|
|
6
|
+
import json
|
|
7
|
+
from functools import lru_cache
|
|
8
|
+
from importlib import resources
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from pydantic import BaseModel, ConfigDict, Field, ValidationError, field_validator
|
|
12
|
+
|
|
13
|
+
from .composition import SectionChipPlan, compose_section_chip
|
|
14
|
+
|
|
15
|
+
BANK_SCHEMA_VERSION = "hwpx.house-style-bank/v0"
|
|
16
|
+
GENRES_SCHEMA_VERSION = "hwpx.house-style-genres/v0"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class HouseStyleDataError(ValueError):
|
|
20
|
+
"""The packaged bank or genre grammar violates its frozen schema."""
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class _FrozenModel(BaseModel):
|
|
24
|
+
model_config = ConfigDict(
|
|
25
|
+
extra="forbid",
|
|
26
|
+
frozen=True,
|
|
27
|
+
populate_by_name=True,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class HouseStyleBank(_FrozenModel):
|
|
32
|
+
schema_version: str = Field(alias="schemaVersion")
|
|
33
|
+
generated: str
|
|
34
|
+
status: str
|
|
35
|
+
research_doc: str = Field(alias="researchDoc")
|
|
36
|
+
usage: str = Field(alias="_usage")
|
|
37
|
+
font_safety: dict[str, Any] = Field(alias="fontSafety")
|
|
38
|
+
shared_rules: list[str] = Field(alias="sharedRules")
|
|
39
|
+
profiles: dict[str, dict[str, Any]]
|
|
40
|
+
|
|
41
|
+
@field_validator("schema_version")
|
|
42
|
+
@classmethod
|
|
43
|
+
def _schema_is_exact(cls, value: str) -> str:
|
|
44
|
+
if value != BANK_SCHEMA_VERSION:
|
|
45
|
+
raise ValueError(f"expected {BANK_SCHEMA_VERSION!r}, got {value!r}")
|
|
46
|
+
return value
|
|
47
|
+
|
|
48
|
+
@field_validator("profiles")
|
|
49
|
+
@classmethod
|
|
50
|
+
def _profiles_are_named_objects(
|
|
51
|
+
cls,
|
|
52
|
+
value: dict[str, dict[str, Any]],
|
|
53
|
+
) -> dict[str, dict[str, Any]]:
|
|
54
|
+
if not value or any(not name.strip() or not isinstance(profile, dict) for name, profile in value.items()):
|
|
55
|
+
raise ValueError("profiles must be a non-empty map of named objects")
|
|
56
|
+
return value
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class GenreDefinition(_FrozenModel):
|
|
60
|
+
label: str
|
|
61
|
+
when: str
|
|
62
|
+
typography: dict[str, Any]
|
|
63
|
+
grammar: dict[str, Any]
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class GenreCatalog(_FrozenModel):
|
|
67
|
+
schema_version: str = Field(alias="schemaVersion")
|
|
68
|
+
generated: str
|
|
69
|
+
usage: str = Field(alias="_usage")
|
|
70
|
+
genres: dict[str, GenreDefinition]
|
|
71
|
+
|
|
72
|
+
@field_validator("schema_version")
|
|
73
|
+
@classmethod
|
|
74
|
+
def _schema_is_exact(cls, value: str) -> str:
|
|
75
|
+
if value != GENRES_SCHEMA_VERSION:
|
|
76
|
+
raise ValueError(f"expected {GENRES_SCHEMA_VERSION!r}, got {value!r}")
|
|
77
|
+
return value
|
|
78
|
+
|
|
79
|
+
@field_validator("genres")
|
|
80
|
+
@classmethod
|
|
81
|
+
def _genres_are_nonempty(
|
|
82
|
+
cls,
|
|
83
|
+
value: dict[str, GenreDefinition],
|
|
84
|
+
) -> dict[str, GenreDefinition]:
|
|
85
|
+
if not value:
|
|
86
|
+
raise ValueError("genres must be a non-empty map")
|
|
87
|
+
return value
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def validate_bank(payload: dict[str, Any]) -> HouseStyleBank:
|
|
91
|
+
"""Validate a decoded bank payload or fail closed with one public error."""
|
|
92
|
+
|
|
93
|
+
try:
|
|
94
|
+
return HouseStyleBank.model_validate(payload)
|
|
95
|
+
except ValidationError as exc:
|
|
96
|
+
raise HouseStyleDataError(f"invalid house-style bank: {exc}") from exc
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def validate_genres(payload: dict[str, Any]) -> GenreCatalog:
|
|
100
|
+
"""Validate a decoded genre payload or fail closed with one public error."""
|
|
101
|
+
|
|
102
|
+
try:
|
|
103
|
+
return GenreCatalog.model_validate(payload)
|
|
104
|
+
except ValidationError as exc:
|
|
105
|
+
raise HouseStyleDataError(f"invalid house-style genres: {exc}") from exc
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _load_payload(filename: str) -> dict[str, Any]:
|
|
109
|
+
try:
|
|
110
|
+
text = (
|
|
111
|
+
resources.files("hwpx_automation.office.house_style")
|
|
112
|
+
.joinpath("data")
|
|
113
|
+
.joinpath(filename)
|
|
114
|
+
.read_text(encoding="utf-8")
|
|
115
|
+
)
|
|
116
|
+
payload = json.loads(text)
|
|
117
|
+
except (OSError, json.JSONDecodeError) as exc:
|
|
118
|
+
raise HouseStyleDataError(f"cannot load packaged house-style data {filename!r}") from exc
|
|
119
|
+
if not isinstance(payload, dict):
|
|
120
|
+
raise HouseStyleDataError(f"house-style data {filename!r} must be a JSON object")
|
|
121
|
+
return payload
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
@lru_cache(maxsize=1)
|
|
125
|
+
def load_bank() -> HouseStyleBank:
|
|
126
|
+
return validate_bank(_load_payload("bank.json"))
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
@lru_cache(maxsize=1)
|
|
130
|
+
def load_genres() -> GenreCatalog:
|
|
131
|
+
return validate_genres(_load_payload("genres.json"))
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def list_profiles() -> list[str]:
|
|
135
|
+
return sorted(load_bank().profiles)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def get_profile(name: str) -> dict[str, Any]:
|
|
139
|
+
profiles = load_bank().profiles
|
|
140
|
+
if name not in profiles:
|
|
141
|
+
raise KeyError(f"unknown house-style profile: {name!r} (have {sorted(profiles)})")
|
|
142
|
+
return copy.deepcopy(profiles[name])
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def list_genres() -> list[str]:
|
|
146
|
+
return sorted(load_genres().genres)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def get_genre(name: str) -> GenreDefinition:
|
|
150
|
+
genres = load_genres().genres
|
|
151
|
+
if name not in genres:
|
|
152
|
+
raise KeyError(f"unknown genre: {name!r} (have {sorted(genres)})")
|
|
153
|
+
return genres[name]
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def genre_typography(name: str) -> dict[str, Any]:
|
|
157
|
+
typography = get_genre(name).typography
|
|
158
|
+
base_name = typography.get("inherits")
|
|
159
|
+
roles: dict[str, Any] = {}
|
|
160
|
+
if base_name:
|
|
161
|
+
roles.update(get_profile(str(base_name)).get("roles", {}))
|
|
162
|
+
roles.update(copy.deepcopy(typography.get("roles", {})))
|
|
163
|
+
return {"inherits": base_name, "roles": roles}
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def font_policy() -> dict[str, Any]:
|
|
167
|
+
return copy.deepcopy(load_bank().font_safety)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def resolve_font(font: str, *, mode: str = "hancom-native") -> str:
|
|
171
|
+
if mode != "portable":
|
|
172
|
+
return font
|
|
173
|
+
return str(font_policy().get("portableMap", {}).get(font, font))
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
__all__ = [
|
|
177
|
+
"BANK_SCHEMA_VERSION",
|
|
178
|
+
"GENRES_SCHEMA_VERSION",
|
|
179
|
+
"GenreCatalog",
|
|
180
|
+
"GenreDefinition",
|
|
181
|
+
"HouseStyleBank",
|
|
182
|
+
"HouseStyleDataError",
|
|
183
|
+
"SectionChipPlan",
|
|
184
|
+
"compose_section_chip",
|
|
185
|
+
"font_policy",
|
|
186
|
+
"genre_typography",
|
|
187
|
+
"get_genre",
|
|
188
|
+
"get_profile",
|
|
189
|
+
"list_genres",
|
|
190
|
+
"list_profiles",
|
|
191
|
+
"load_bank",
|
|
192
|
+
"load_genres",
|
|
193
|
+
"resolve_font",
|
|
194
|
+
"validate_bank",
|
|
195
|
+
"validate_genres",
|
|
196
|
+
]
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""Typed application composition for an operating-plan section divider."""
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Literal
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel, ConfigDict, field_validator
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class SectionChipPlan(BaseModel):
|
|
11
|
+
"""Application-level section divider lowered to generic plan blocks."""
|
|
12
|
+
|
|
13
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
14
|
+
|
|
15
|
+
number: str
|
|
16
|
+
title: str
|
|
17
|
+
style: Literal["box", "inline"] = "box"
|
|
18
|
+
accent_color: str | None = None
|
|
19
|
+
|
|
20
|
+
@field_validator("number", "title")
|
|
21
|
+
@classmethod
|
|
22
|
+
def _nonempty(cls, value: str) -> str:
|
|
23
|
+
normalized = str(value).strip()
|
|
24
|
+
if not normalized:
|
|
25
|
+
raise ValueError("section chip number and title must be non-empty")
|
|
26
|
+
return normalized
|
|
27
|
+
|
|
28
|
+
def to_document_plan_block(self) -> dict[str, Any]:
|
|
29
|
+
metadata: dict[str, Any] = {
|
|
30
|
+
"generator": "section_chip",
|
|
31
|
+
"style": self.style,
|
|
32
|
+
"number": self.number,
|
|
33
|
+
}
|
|
34
|
+
if self.accent_color:
|
|
35
|
+
metadata["accentColor"] = self.accent_color
|
|
36
|
+
if self.style == "inline":
|
|
37
|
+
return {
|
|
38
|
+
"type": "heading",
|
|
39
|
+
"level": 1,
|
|
40
|
+
"text": f"{self.number} {self.title}",
|
|
41
|
+
"metadata": metadata,
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
"type": "table",
|
|
45
|
+
"rows": [[self.number, "", self.title]],
|
|
46
|
+
"columnWidths": [1, 0.4, 11],
|
|
47
|
+
"metadata": metadata,
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def compose_section_chip(
|
|
52
|
+
number: str,
|
|
53
|
+
title: str,
|
|
54
|
+
*,
|
|
55
|
+
style: Literal["box", "inline"] = "box",
|
|
56
|
+
accent_color: str | None = None,
|
|
57
|
+
) -> dict[str, Any]:
|
|
58
|
+
"""Compose a genre-specific divider using generic document-plan blocks."""
|
|
59
|
+
|
|
60
|
+
return SectionChipPlan(
|
|
61
|
+
number=number,
|
|
62
|
+
title=title,
|
|
63
|
+
style=style,
|
|
64
|
+
accent_color=accent_color,
|
|
65
|
+
).to_document_plan_block()
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
__all__ = ["SectionChipPlan", "compose_section_chip"]
|