ai37-a2ui-catalog 0.3.0__tar.gz
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.
- ai37_a2ui_catalog-0.3.0/PKG-INFO +17 -0
- ai37_a2ui_catalog-0.3.0/README.md +3 -0
- ai37_a2ui_catalog-0.3.0/pyproject.toml +26 -0
- ai37_a2ui_catalog-0.3.0/src/ai37_a2ui_catalog/__init__.py +16 -0
- ai37_a2ui_catalog-0.3.0/src/ai37_a2ui_catalog/constants.py +12 -0
- ai37_a2ui_catalog-0.3.0/src/ai37_a2ui_catalog/models/__init__.py +35 -0
- ai37_a2ui_catalog-0.3.0/src/ai37_a2ui_catalog/models/choice_card.py +24 -0
- ai37_a2ui_catalog-0.3.0/src/ai37_a2ui_catalog/models/flex_table.py +72 -0
- ai37_a2ui_catalog-0.3.0/src/ai37_a2ui_catalog/models/form_card.py +31 -0
- ai37_a2ui_catalog-0.3.0/src/ai37_a2ui_catalog/models/latex_formula.py +13 -0
- ai37_a2ui_catalog-0.3.0/src/ai37_a2ui_catalog/models/shared.py +13 -0
- ai37_a2ui_catalog-0.3.0/src/ai37_a2ui_catalog/models/simple_table.py +33 -0
- ai37_a2ui_catalog-0.3.0/src/ai37_a2ui_catalog/validation.py +19 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ai37-a2ui-catalog
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: Pydantic models and validation helpers for the AI37 A2UI catalog
|
|
5
|
+
Author: AI37
|
|
6
|
+
Author-email: dev@ai37.ru
|
|
7
|
+
Requires-Python: >=3.13,<4.0
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
11
|
+
Requires-Dist: pydantic (>=2.11.7,<3.0.0)
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
|
|
14
|
+
# ai37-a2ui-catalog
|
|
15
|
+
|
|
16
|
+
Python validation package for the AI37 A2UI catalog.
|
|
17
|
+
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "ai37-a2ui-catalog"
|
|
3
|
+
version = "0.3.0"
|
|
4
|
+
description = "Pydantic models and validation helpers for the AI37 A2UI catalog"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
authors = [{ name = "AI37", email = "dev@ai37.ru" }]
|
|
7
|
+
requires-python = ">=3.13,<4.0"
|
|
8
|
+
dependencies = [
|
|
9
|
+
"pydantic>=2.11.7,<3.0.0"
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
[tool.poetry.dependencies]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
[tool.poetry]
|
|
16
|
+
packages = [{ include = "ai37_a2ui_catalog", from = "src" }]
|
|
17
|
+
|
|
18
|
+
[tool.poetry.group.dev.dependencies]
|
|
19
|
+
pytest = "^8.4.2"
|
|
20
|
+
|
|
21
|
+
[build-system]
|
|
22
|
+
requires = ["poetry-core>=2.0.0"]
|
|
23
|
+
build-backend = "poetry.core.masonry.api"
|
|
24
|
+
|
|
25
|
+
[tool.pytest.ini_options]
|
|
26
|
+
pythonpath = ["src"]
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from .constants import CATALOG_ID, CATALOG_VERSION
|
|
2
|
+
from .models import ChoiceCardProps, FlexTableProps, FormCardProps, LatexFormulaProps, SimpleTableProps
|
|
3
|
+
from .validation import get_component_schema, validate_component_payload, validate_component_payload_json
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
"CATALOG_ID",
|
|
7
|
+
"CATALOG_VERSION",
|
|
8
|
+
"ChoiceCardProps",
|
|
9
|
+
"FlexTableProps",
|
|
10
|
+
"FormCardProps",
|
|
11
|
+
"LatexFormulaProps",
|
|
12
|
+
"SimpleTableProps",
|
|
13
|
+
"get_component_schema",
|
|
14
|
+
"validate_component_payload",
|
|
15
|
+
"validate_component_payload_json",
|
|
16
|
+
]
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
CATALOG_SLUG = "ai37-a2ui"
|
|
4
|
+
CATALOG_VERSION = "v2"
|
|
5
|
+
# Хостинг каталога — GitHub Pages проекта AI-37/ai37-a2ui-catalog (см. constants.ts).
|
|
6
|
+
CATALOG_BASE_URL = (
|
|
7
|
+
f"https://ai-37.github.io/ai37-a2ui-catalog/a2ui/catalogs/{CATALOG_SLUG}/{CATALOG_VERSION}"
|
|
8
|
+
)
|
|
9
|
+
CATALOG_ID = f"{CATALOG_BASE_URL}/catalog.json"
|
|
10
|
+
# Id базового каталога A2UI (basicCatalog @a2ui) — для content-negotiation вывода
|
|
11
|
+
# (MIME application/vnd.a2ui+json → base). См. constants.ts.
|
|
12
|
+
A2UI_BASE_CATALOG_ID = "https://a2ui.org/specification/v0_9/catalogs/basic/catalog.json"
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
from .choice_card import ChoiceCardProps, ChoiceCardSubmit, ChoiceOption
|
|
2
|
+
from .flex_table import FlexTableCell, FlexTableProps, FlexTableRow
|
|
3
|
+
from .form_card import FormCardProps, FormCardSubmit, FormField, FormFieldType
|
|
4
|
+
from .latex_formula import LatexFormulaProps
|
|
5
|
+
from .shared import CellPrimitive, StrictModel, TextAlign
|
|
6
|
+
from .simple_table import SimpleTableColumn, SimpleTableProps, SimpleTableRow
|
|
7
|
+
|
|
8
|
+
COMPONENT_MODELS = {
|
|
9
|
+
"SimpleTable": SimpleTableProps,
|
|
10
|
+
"FlexTable": FlexTableProps,
|
|
11
|
+
"LatexFormula": LatexFormulaProps,
|
|
12
|
+
"ChoiceCard": ChoiceCardProps,
|
|
13
|
+
"FormCard": FormCardProps,
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
"COMPONENT_MODELS",
|
|
18
|
+
"CellPrimitive",
|
|
19
|
+
"ChoiceCardProps",
|
|
20
|
+
"ChoiceCardSubmit",
|
|
21
|
+
"ChoiceOption",
|
|
22
|
+
"FlexTableCell",
|
|
23
|
+
"FlexTableProps",
|
|
24
|
+
"FlexTableRow",
|
|
25
|
+
"FormCardProps",
|
|
26
|
+
"FormCardSubmit",
|
|
27
|
+
"FormField",
|
|
28
|
+
"FormFieldType",
|
|
29
|
+
"LatexFormulaProps",
|
|
30
|
+
"SimpleTableColumn",
|
|
31
|
+
"SimpleTableProps",
|
|
32
|
+
"SimpleTableRow",
|
|
33
|
+
"StrictModel",
|
|
34
|
+
"TextAlign",
|
|
35
|
+
]
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pydantic import Field
|
|
4
|
+
|
|
5
|
+
from .shared import StrictModel
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ChoiceOption(StrictModel):
|
|
9
|
+
label: str = Field(min_length=1, max_length=120)
|
|
10
|
+
value: str = Field(min_length=1, max_length=120)
|
|
11
|
+
description: str = Field(default=None, min_length=1, max_length=240)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ChoiceCardSubmit(StrictModel):
|
|
15
|
+
label: str = Field(min_length=1, max_length=80)
|
|
16
|
+
action: str = Field(min_length=1, max_length=120)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ChoiceCardProps(StrictModel):
|
|
20
|
+
title: str = Field(min_length=1, max_length=120)
|
|
21
|
+
description: str = Field(default=None, min_length=1, max_length=240)
|
|
22
|
+
multiple: bool = None
|
|
23
|
+
choices: list[ChoiceOption] = Field(min_length=1)
|
|
24
|
+
submit: ChoiceCardSubmit = None
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Literal
|
|
4
|
+
|
|
5
|
+
from pydantic import Field, model_validator
|
|
6
|
+
|
|
7
|
+
from .shared import StrictModel, TextAlign
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class FlexTableCell(StrictModel):
|
|
11
|
+
content: str = Field(min_length=1)
|
|
12
|
+
align: TextAlign = None
|
|
13
|
+
colSpan: int = Field(default=None, ge=1)
|
|
14
|
+
rowSpan: int = Field(default=None, ge=1)
|
|
15
|
+
emphasis: Literal["normal", "muted", "strong"] = None
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class FlexTableRow(StrictModel):
|
|
19
|
+
cells: list[FlexTableCell] = Field(min_length=1)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def validate_header_grid(rows: list[FlexTableRow]) -> int:
|
|
23
|
+
total_columns = sum(cell.colSpan or 1 for cell in rows[0].cells)
|
|
24
|
+
active_spans = [0] * total_columns
|
|
25
|
+
|
|
26
|
+
for row_index, row in enumerate(rows):
|
|
27
|
+
if row_index > 0:
|
|
28
|
+
active_spans = [max(0, span - 1) for span in active_spans]
|
|
29
|
+
|
|
30
|
+
column_index = 0
|
|
31
|
+
|
|
32
|
+
for cell in row.cells:
|
|
33
|
+
while column_index < total_columns and active_spans[column_index] > 0:
|
|
34
|
+
column_index += 1
|
|
35
|
+
|
|
36
|
+
col_span = cell.colSpan or 1
|
|
37
|
+
row_span = cell.rowSpan or 1
|
|
38
|
+
|
|
39
|
+
if column_index + col_span > total_columns:
|
|
40
|
+
raise ValueError(f"Header row {row_index} exceeds the available column span.")
|
|
41
|
+
|
|
42
|
+
if any(active_spans[column_index + offset] > 0 for offset in range(col_span)):
|
|
43
|
+
raise ValueError(f"Header row {row_index} overlaps an active rowSpan.")
|
|
44
|
+
|
|
45
|
+
for offset in range(col_span):
|
|
46
|
+
active_spans[column_index + offset] = row_span
|
|
47
|
+
|
|
48
|
+
column_index += col_span
|
|
49
|
+
|
|
50
|
+
if any(span == 0 for span in active_spans):
|
|
51
|
+
raise ValueError(f"Header row {row_index} does not fully cover the table width.")
|
|
52
|
+
|
|
53
|
+
return total_columns
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class FlexTableProps(StrictModel):
|
|
57
|
+
title: str = Field(default=None, min_length=1, max_length=120)
|
|
58
|
+
caption: str = Field(default=None, min_length=1, max_length=240)
|
|
59
|
+
dense: bool = None
|
|
60
|
+
headerRows: list[FlexTableRow] = Field(min_length=1)
|
|
61
|
+
bodyRows: list[FlexTableRow] = Field(min_length=1)
|
|
62
|
+
|
|
63
|
+
@model_validator(mode="after")
|
|
64
|
+
def validate_row_spans(self) -> "FlexTableProps":
|
|
65
|
+
expected = validate_header_grid(self.headerRows)
|
|
66
|
+
|
|
67
|
+
for index, row in enumerate(self.bodyRows):
|
|
68
|
+
width = sum(cell.colSpan or 1 for cell in row.cells)
|
|
69
|
+
if width != expected:
|
|
70
|
+
raise ValueError(f"Body row {index} has inconsistent total span.")
|
|
71
|
+
|
|
72
|
+
return self
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Literal
|
|
4
|
+
|
|
5
|
+
from pydantic import Field
|
|
6
|
+
|
|
7
|
+
from .shared import StrictModel
|
|
8
|
+
|
|
9
|
+
FormFieldType = Literal["text", "number", "select", "boolean"]
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class FormField(StrictModel):
|
|
13
|
+
name: str = Field(min_length=1, max_length=80)
|
|
14
|
+
label: str = Field(min_length=1, max_length=120)
|
|
15
|
+
type: FormFieldType
|
|
16
|
+
required: bool = None
|
|
17
|
+
options: list[str] = Field(default=None, min_length=1)
|
|
18
|
+
placeholder: str = Field(default=None, min_length=1, max_length=120)
|
|
19
|
+
defaultValue: str | int | float | bool = None
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class FormCardSubmit(StrictModel):
|
|
23
|
+
label: str = Field(min_length=1, max_length=80)
|
|
24
|
+
action: str = Field(min_length=1, max_length=120)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class FormCardProps(StrictModel):
|
|
28
|
+
title: str = Field(min_length=1, max_length=120)
|
|
29
|
+
description: str = Field(default=None, min_length=1, max_length=240)
|
|
30
|
+
fields: list[FormField] = Field(min_length=1)
|
|
31
|
+
submit: FormCardSubmit
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pydantic import Field
|
|
4
|
+
|
|
5
|
+
from .shared import StrictModel
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class LatexFormulaProps(StrictModel):
|
|
9
|
+
title: str = Field(default=None, min_length=1, max_length=120)
|
|
10
|
+
formula: str = Field(min_length=1)
|
|
11
|
+
displayMode: bool = None
|
|
12
|
+
annotation: str = Field(default=None, min_length=1, max_length=240)
|
|
13
|
+
bordered: bool = None
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Literal
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel, ConfigDict
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class StrictModel(BaseModel):
|
|
9
|
+
model_config = ConfigDict(extra="forbid")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
TextAlign = Literal["start", "center", "end"]
|
|
13
|
+
CellPrimitive = str | int | float | bool | None
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pydantic import Field, model_validator
|
|
4
|
+
|
|
5
|
+
from .shared import CellPrimitive, StrictModel, TextAlign
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class SimpleTableColumn(StrictModel):
|
|
9
|
+
key: str = Field(min_length=1)
|
|
10
|
+
header: str = Field(min_length=1)
|
|
11
|
+
align: TextAlign = None
|
|
12
|
+
width: str = Field(default=None, min_length=1)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class SimpleTableRow(StrictModel):
|
|
16
|
+
cells: list[CellPrimitive]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class SimpleTableProps(StrictModel):
|
|
20
|
+
title: str = Field(default=None, min_length=1, max_length=120)
|
|
21
|
+
caption: str = Field(default=None, min_length=1, max_length=240)
|
|
22
|
+
compact: bool = None
|
|
23
|
+
striped: bool = None
|
|
24
|
+
columns: list[SimpleTableColumn] = Field(min_length=1)
|
|
25
|
+
rows: list[SimpleTableRow]
|
|
26
|
+
|
|
27
|
+
@model_validator(mode="after")
|
|
28
|
+
def validate_row_lengths(self) -> "SimpleTableProps":
|
|
29
|
+
expected = len(self.columns)
|
|
30
|
+
for index, row in enumerate(self.rows):
|
|
31
|
+
if len(row.cells) != expected:
|
|
32
|
+
raise ValueError(f"Row {index} must contain exactly {expected} cells.")
|
|
33
|
+
return self
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from .models import COMPONENT_MODELS
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def validate_component_payload(component: str, payload: dict[str, Any]):
|
|
9
|
+
model = COMPONENT_MODELS[component]
|
|
10
|
+
return model.model_validate(payload)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def validate_component_payload_json(component: str, payload_json: str):
|
|
14
|
+
model = COMPONENT_MODELS[component]
|
|
15
|
+
return model.model_validate_json(payload_json)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def get_component_schema(component: str) -> dict[str, Any]:
|
|
19
|
+
return COMPONENT_MODELS[component].model_json_schema()
|