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.
@@ -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,3 @@
1
+ # ai37-a2ui-catalog
2
+
3
+ Python validation package for the AI37 A2UI catalog.
@@ -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()