mateu-ui 3.0.0a412__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.
- mateu_ui-3.0.0a412/.gitignore +6 -0
- mateu_ui-3.0.0a412/DESIGN.md +107 -0
- mateu_ui-3.0.0a412/PKG-INFO +278 -0
- mateu_ui-3.0.0a412/README.md +236 -0
- mateu_ui-3.0.0a412/mateu_core/__init__.py +24 -0
- mateu_ui-3.0.0a412/mateu_core/_version.py +8 -0
- mateu_ui-3.0.0a412/mateu_core/action_guard.py +273 -0
- mateu_ui-3.0.0a412/mateu_core/action_registry.py +319 -0
- mateu_ui-3.0.0a412/mateu_core/capabilities.py +49 -0
- mateu_ui-3.0.0a412/mateu_core/component_registry.py +123 -0
- mateu_ui-3.0.0a412/mateu_core/dev_specs.py +223 -0
- mateu_ui-3.0.0a412/mateu_core/documents.py +203 -0
- mateu_ui-3.0.0a412/mateu_core/environments.py +176 -0
- mateu_ui-3.0.0a412/mateu_core/error_boundary.py +77 -0
- mateu_ui-3.0.0a412/mateu_core/errors.py +40 -0
- mateu_ui-3.0.0a412/mateu_core/export.py +56 -0
- mateu_ui-3.0.0a412/mateu_core/field_type_registry.py +223 -0
- mateu_ui-3.0.0a412/mateu_core/identity.py +92 -0
- mateu_ui-3.0.0a412/mateu_core/importwizard.py +389 -0
- mateu_ui-3.0.0a412/mateu_core/inference_switches.py +36 -0
- mateu_ui-3.0.0a412/mateu_core/islands.py +93 -0
- mateu_ui-3.0.0a412/mateu_core/labels_aside_inference.py +73 -0
- mateu_ui-3.0.0a412/mateu_core/layout_delta.py +196 -0
- mateu_ui-3.0.0a412/mateu_core/layout_inference.py +77 -0
- mateu_ui-3.0.0a412/mateu_core/mapper/__init__.py +368 -0
- mateu_ui-3.0.0a412/mateu_core/mapper/_base.py +16 -0
- mateu_ui-3.0.0a412/mateu_core/mapper/_common.py +436 -0
- mateu_ui-3.0.0a412/mateu_core/mapper/app.py +566 -0
- mateu_ui-3.0.0a412/mateu_core/mapper/archetypes.py +233 -0
- mateu_ui-3.0.0a412/mateu_core/mapper/components.py +1086 -0
- mateu_ui-3.0.0a412/mateu_core/mapper/crud.py +655 -0
- mateu_ui-3.0.0a412/mateu_core/mapper/decorations.py +153 -0
- mateu_ui-3.0.0a412/mateu_core/mapper/fields.py +714 -0
- mateu_ui-3.0.0a412/mateu_core/mapper/layout.py +624 -0
- mateu_ui-3.0.0a412/mateu_core/mapper/view.py +270 -0
- mateu_ui-3.0.0a412/mateu_core/mapper/wizard.py +262 -0
- mateu_ui-3.0.0a412/mateu_core/mcp.py +318 -0
- mateu_ui-3.0.0a412/mateu_core/menu_route_conformance.py +55 -0
- mateu_ui-3.0.0a412/mateu_core/mobile_manifest.py +94 -0
- mateu_ui-3.0.0a412/mateu_core/naming.py +58 -0
- mateu_ui-3.0.0a412/mateu_core/page_inference.py +63 -0
- mateu_ui-3.0.0a412/mateu_core/page_type_inference.py +72 -0
- mateu_ui-3.0.0a412/mateu_core/partial_registry.py +80 -0
- mateu_ui-3.0.0a412/mateu_core/py.typed +0 -0
- mateu_ui-3.0.0a412/mateu_core/reflection.py +103 -0
- mateu_ui-3.0.0a412/mateu_core/registry.py +210 -0
- mateu_ui-3.0.0a412/mateu_core/request_context.py +81 -0
- mateu_ui-3.0.0a412/mateu_core/rest_source_registry.py +280 -0
- mateu_ui-3.0.0a412/mateu_core/route_registry.py +545 -0
- mateu_ui-3.0.0a412/mateu_core/sample_sources.py +27 -0
- mateu_ui-3.0.0a412/mateu_core/sync_handler/__init__.py +273 -0
- mateu_ui-3.0.0a412/mateu_core/sync_handler/_base.py +16 -0
- mateu_ui-3.0.0a412/mateu_core/sync_handler/_common.py +161 -0
- mateu_ui-3.0.0a412/mateu_core/sync_handler/actions.py +175 -0
- mateu_ui-3.0.0a412/mateu_core/sync_handler/app.py +144 -0
- mateu_ui-3.0.0a412/mateu_core/sync_handler/contract.py +76 -0
- mateu_ui-3.0.0a412/mateu_core/sync_handler/crud.py +547 -0
- mateu_ui-3.0.0a412/mateu_core/sync_handler/dispatch.py +362 -0
- mateu_ui-3.0.0a412/mateu_core/sync_handler/grid_field.py +307 -0
- mateu_ui-3.0.0a412/mateu_core/sync_handler/helpers.py +261 -0
- mateu_ui-3.0.0a412/mateu_core/sync_handler/listing.py +270 -0
- mateu_ui-3.0.0a412/mateu_core/sync_handler/proxy.py +164 -0
- mateu_ui-3.0.0a412/mateu_core/sync_handler/render.py +64 -0
- mateu_ui-3.0.0a412/mateu_core/sync_handler/search.py +572 -0
- mateu_ui-3.0.0a412/mateu_core/sync_handler/wizard.py +161 -0
- mateu_ui-3.0.0a412/mateu_core/translations.py +247 -0
- mateu_ui-3.0.0a412/mateu_core/url_template.py +82 -0
- mateu_ui-3.0.0a412/mateu_core/validation.py +158 -0
- mateu_ui-3.0.0a412/mateu_core/yaml_access.py +210 -0
- mateu_ui-3.0.0a412/mateu_core/yaml_preview.py +215 -0
- mateu_ui-3.0.0a412/mateu_core/yaml_spec_loader.py +225 -0
- mateu_ui-3.0.0a412/mateu_dtos/__init__.py +210 -0
- mateu_ui-3.0.0a412/mateu_dtos/app.py +158 -0
- mateu_ui-3.0.0a412/mateu_dtos/base.py +20 -0
- mateu_ui-3.0.0a412/mateu_dtos/basic.py +129 -0
- mateu_ui-3.0.0a412/mateu_dtos/components.py +270 -0
- mateu_ui-3.0.0a412/mateu_dtos/dashboard.py +212 -0
- mateu_ui-3.0.0a412/mateu_dtos/envelope.py +137 -0
- mateu_ui-3.0.0a412/mateu_dtos/fields.py +207 -0
- mateu_ui-3.0.0a412/mateu_dtos/front_office.py +390 -0
- mateu_ui-3.0.0a412/mateu_dtos/layout.py +135 -0
- mateu_ui-3.0.0a412/mateu_dtos/overlays.py +89 -0
- mateu_ui-3.0.0a412/mateu_dtos/py.typed +0 -0
- mateu_ui-3.0.0a412/mateu_dtos/records.py +236 -0
- mateu_ui-3.0.0a412/mateu_dtos/ux.py +328 -0
- mateu_ui-3.0.0a412/mateu_fastapi/__init__.py +401 -0
- mateu_ui-3.0.0a412/mateu_fastapi/py.typed +0 -0
- mateu_ui-3.0.0a412/mateu_uidl/__init__.py +370 -0
- mateu_ui-3.0.0a412/mateu_uidl/action_catalog.py +51 -0
- mateu_ui-3.0.0a412/mateu_uidl/adapters.py +64 -0
- mateu_ui-3.0.0a412/mateu_uidl/archetype_pages.py +600 -0
- mateu_ui-3.0.0a412/mateu_uidl/archetypes.py +282 -0
- mateu_ui-3.0.0a412/mateu_uidl/business_components.py +46 -0
- mateu_ui-3.0.0a412/mateu_uidl/class_decorators.py +673 -0
- mateu_ui-3.0.0a412/mateu_uidl/components.py +1843 -0
- mateu_ui-3.0.0a412/mateu_uidl/constraints.py +100 -0
- mateu_ui-3.0.0a412/mateu_uidl/documents.py +81 -0
- mateu_ui-3.0.0a412/mateu_uidl/environments.py +35 -0
- mateu_ui-3.0.0a412/mateu_uidl/export.py +130 -0
- mateu_ui-3.0.0a412/mateu_uidl/field_types.py +116 -0
- mateu_ui-3.0.0a412/mateu_uidl/flow.py +107 -0
- mateu_ui-3.0.0a412/mateu_uidl/i18n.py +29 -0
- mateu_ui-3.0.0a412/mateu_uidl/listing.py +337 -0
- mateu_ui-3.0.0a412/mateu_uidl/live_header_values.py +62 -0
- mateu_ui-3.0.0a412/mateu_uidl/markers.py +327 -0
- mateu_ui-3.0.0a412/mateu_uidl/messages.py +82 -0
- mateu_ui-3.0.0a412/mateu_uidl/method_decorators.py +231 -0
- mateu_ui-3.0.0a412/mateu_uidl/patterns.py +208 -0
- mateu_ui-3.0.0a412/mateu_uidl/placement.py +107 -0
- mateu_ui-3.0.0a412/mateu_uidl/py.typed +0 -0
- mateu_ui-3.0.0a412/mateu_uidl/rest_sources.py +207 -0
- mateu_ui-3.0.0a412/mateu_uidl/security.py +135 -0
- mateu_ui-3.0.0a412/mateu_uidl/suppliers.py +238 -0
- mateu_ui-3.0.0a412/mateu_uidl/views.py +131 -0
- mateu_ui-3.0.0a412/pyproject.toml +92 -0
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# Mateu server-side for Python — design
|
|
2
|
+
|
|
3
|
+
> Status: **implemented & verified**. Forms + sections + field types + validation, `Crud[T]`
|
|
4
|
+
> (list / detail / edit / new / save / delete), the `@app` shell + menu navigation, wizards,
|
|
5
|
+
> page decorations, i18n, events, security scaffolding, the tail features (tabs, stereotypes,
|
|
6
|
+
> KPIs, FABs, shortcuts, compact, unsaved-changes guard), and the UX-pattern components
|
|
7
|
+
> (MetricCard/Scoreboard/DashboardPanel/DashboardLayout, FoldoutLayout, HeroSection, EmptyState,
|
|
8
|
+
> Skeleton, Gantt) with the Dashboard/Foldout/ItemOverview/Welcome declarative archetypes, and
|
|
9
|
+
> `@auto_layout` deterministic layout inference (fold-optionals, sections-as-tabs, small-enum
|
|
10
|
+
> radios — the port of the Java `LayoutInference` decision table; `TabLayout` metadata carries
|
|
11
|
+
> `groupRelationship`/`adaptable` on the wire like `TabLayoutDto`).
|
|
12
|
+
> The golden-JSON tests pass; the live `showcase` view is byte-identical to the C# reference.
|
|
13
|
+
> Since 2026-10-10 the port is a fully supported producer (PyPI `mateu-ui`): the shared wire-
|
|
14
|
+
> conformance corpus is a hard gate, and every server row of the parity matrix is ✅ (see
|
|
15
|
+
> `doc/src/content/docs/reference/parity.md`). The engine is split by concern: `mateu_core/mapper/`
|
|
16
|
+
> and `mateu_core/sync_handler/` are packages of mixins composed into `ReflectionMapper` /
|
|
17
|
+
> `SyncHandler`; `mateu_uidl` and `mateu_dtos` are modules re-exported from their package roots.
|
|
18
|
+
|
|
19
|
+
## 1. Guiding principle: the renderers are backend-agnostic
|
|
20
|
+
|
|
21
|
+
Every Mateu renderer speaks one protocol:
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
POST /{baseUrl}/mateu/v3/sync/{route}
|
|
25
|
+
request (RunActionRq): { route, consumedRoute, actionId, serverSideType,
|
|
26
|
+
initiatorComponentId, componentState, appState, parameters }
|
|
27
|
+
response (UIIncrement): { commands[], messages[], fragments[], banners[], appendBanners,
|
|
28
|
+
appData, appState }
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
So the Python task is **not** "rewrite the framework" — it is **"emit the same JSON"**. We validate
|
|
32
|
+
by pointing a real renderer (or the C# reference server) at the Python server and diffing the JSON for
|
|
33
|
+
the same view.
|
|
34
|
+
|
|
35
|
+
## 2. Stack (decided)
|
|
36
|
+
|
|
37
|
+
- **FastAPI** (ASP.NET-minimal-API analogue) for the single `sync` route family.
|
|
38
|
+
- **Pydantic v2** for the wire model, using **discriminated unions** (`Field(discriminator="type")`
|
|
39
|
+
+ a `Literal["…"]` `type` field per model) for the `type` discriminators — the Python analogue of
|
|
40
|
+
Java's `@JsonTypeInfo` and C#'s `[JsonPolymorphic]`/`[JsonDerivedType]`. camelCase via
|
|
41
|
+
`alias_generator=to_camel`; nulls kept; dump with `by_alias=True`.
|
|
42
|
+
- **Reflection** over user classes: type hints for fields, `Annotated[...]` metadata for field
|
|
43
|
+
modifiers, decorators for class/method features. Read via `inspect.get_annotations` (PEP 649-safe).
|
|
44
|
+
|
|
45
|
+
## 3. The Python idiom: `Annotated` + decorators
|
|
46
|
+
|
|
47
|
+
C# attributes don't exist on Python attributes, so:
|
|
48
|
+
|
|
49
|
+
- **Field modifiers** ride in `Annotated[T, Marker(...)]`: `Required()`, `Section("…")`, `Tab("…")`,
|
|
50
|
+
`Stereotype("…")`, `Multiline()`, `Password()`, `Money()`, `PlainText()`, `HeaderBadge(...)`,
|
|
51
|
+
`Step(n)`, `Label("…")`.
|
|
52
|
+
- **Class features** are decorators: `@ui`, `@title`, `@subtitle`, `@app`, `@compact`,
|
|
53
|
+
`@confirm_on_navigation_if_dirty`, `@plain_text`, `@emits`, `@subscribe_to`, `@secured`,
|
|
54
|
+
`@menu_group` (a menu folder's look: `display="cards"` opens it as a card panel — the analogue of
|
|
55
|
+
Java's `@Menu(display = cards)` on a group field).
|
|
56
|
+
- **Method features** are decorators: `@button`, `@menu_item`, `@kpi`, `@fab`, `@banner`,
|
|
57
|
+
`@shortcut`. Each stamps a `__mateu_*__` attribute the mapper reads. `@menu_item(group=,
|
|
58
|
+
description=, icon=, image=)` — a `/` in `group` nests folders, and description/icon/image are
|
|
59
|
+
the entry's card look (wire `MenuItem.description/icon/display/image`, mirroring
|
|
60
|
+
`MenuOptionDto`).
|
|
61
|
+
|
|
62
|
+
Validation reuses "a field has `Required()`" (the analogue of `@NotEmpty` / `[Required]`), enforced
|
|
63
|
+
server-side on CRUD save.
|
|
64
|
+
|
|
65
|
+
## 4. Solution layout
|
|
66
|
+
|
|
67
|
+
```
|
|
68
|
+
backend/python/
|
|
69
|
+
mateu_uidl/ ← public API: decorators + field markers + Message/Crud/Wizard/Translator
|
|
70
|
+
mateu_dtos/ ← the wire model (Pydantic): UIIncrement, Component + metadata subtypes
|
|
71
|
+
mateu_core/ ← the engine: naming, reflection, MateuRegistry, ReflectionMapper, SyncHandler
|
|
72
|
+
mateu_fastapi/ ← add_mateu(app, *sources): the POST /mateu/v3/sync/{route} endpoint
|
|
73
|
+
samples/demo/ ← a runnable FastAPI app (Person, Reservations, SignupWizard, Showcase, …)
|
|
74
|
+
tests/ ← golden-JSON tests (pytest) — same suite as the C# port
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## 5. Mapping pipeline (mateu_core)
|
|
78
|
+
|
|
79
|
+
```
|
|
80
|
+
HTTP sync ─▶ MateuRegistry.resolve(serverSideType|route) ─▶ instantiate the view type
|
|
81
|
+
─▶ (action?) bind componentState → set attributes → invoke the action method
|
|
82
|
+
─▶ ReflectionMapper.map_view(instance) ─▶ Component tree
|
|
83
|
+
─▶ wrap in UIFragment(targetComponentId="ux_main"/"crud") ─▶ UIIncrement
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
- **MateuRegistry** scans the given modules/classes once for `@app`/`@ui` and resolves a request by
|
|
87
|
+
`serverSideType` (a stable `module.qualname`), by route, or — for CRUD sub-routes — by the longest
|
|
88
|
+
route prefix.
|
|
89
|
+
- **ReflectionMapper** is the heart: class → App/Page shell; fields → FormField metadata (type
|
|
90
|
+
inferred from the hint, label from `Label`/humanized name, stereotypes, options from `Enum`);
|
|
91
|
+
methods → buttons / KPIs / FABs / banners; `Section`/`Tab` → cards / tab strips.
|
|
92
|
+
- **SyncHandler** dispatches: app shell, CRUD (list/detail/edit/new + search/create/save/delete),
|
|
93
|
+
wizards (back/next/finish), and plain views (render or run an action → `Message` → toast).
|
|
94
|
+
|
|
95
|
+
## 6. Validation strategy
|
|
96
|
+
|
|
97
|
+
- **Golden-JSON tests** (`tests/`) — for each sample view, assert the serialized `UIIncrement` has
|
|
98
|
+
the expected wire shape (ignoring volatile bits like the random SSC id). Same assertions as the C#
|
|
99
|
+
suite.
|
|
100
|
+
- **Live diff** — run the demo on `:8594` and diff its JSON against the C# reference on `:8593` for
|
|
101
|
+
the same view. The `showcase` view (tabs + stereotypes + KPIs + FABs + shortcut + compact + dirty
|
|
102
|
+
guard) is byte-identical apart from the (demo-level) `serverSideType` name.
|
|
103
|
+
|
|
104
|
+
## 7. Beyond the core
|
|
105
|
+
|
|
106
|
+
The remaining Java features (component adapters, federated microfrontends, framework adapters,
|
|
107
|
+
SSE/AI chat) follow the same pattern: add a metadata DTO, extend the mapper, add a golden test.
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: mateu-ui
|
|
3
|
+
Version: 3.0.0a412
|
|
4
|
+
Summary: Mateu server-side for Python — annotate Python classes; any Mateu renderer renders them.
|
|
5
|
+
Project-URL: Homepage, https://mateu.io
|
|
6
|
+
Project-URL: Documentation, https://mateu.io
|
|
7
|
+
Project-URL: Repository, https://github.com/miguelperezcolom/mateu
|
|
8
|
+
Project-URL: Issues, https://github.com/miguelperezcolom/mateu/issues
|
|
9
|
+
Project-URL: Source, https://github.com/miguelperezcolom/mateu/tree/master/backend/python
|
|
10
|
+
Author: Miguel Pérez Colom
|
|
11
|
+
License-Expression: Apache-2.0
|
|
12
|
+
Keywords: backend-driven-ui,crud,fastapi,forms,low-code,model-driven,ui
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: Framework :: FastAPI
|
|
15
|
+
Classifier: Framework :: Pydantic :: 2
|
|
16
|
+
Classifier: Intended Audience :: Developers
|
|
17
|
+
Classifier: Operating System :: OS Independent
|
|
18
|
+
Classifier: Programming Language :: Python :: 3
|
|
19
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
23
|
+
Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
|
|
24
|
+
Classifier: Topic :: Software Development :: User Interfaces
|
|
25
|
+
Classifier: Typing :: Typed
|
|
26
|
+
Requires-Python: >=3.11
|
|
27
|
+
Requires-Dist: fastapi>=0.110
|
|
28
|
+
Requires-Dist: pydantic<3,>=2.6
|
|
29
|
+
Requires-Dist: pyyaml>=6
|
|
30
|
+
Provides-Extra: all
|
|
31
|
+
Requires-Dist: uvicorn>=0.27; extra == 'all'
|
|
32
|
+
Provides-Extra: dev
|
|
33
|
+
Requires-Dist: build>=1.2; extra == 'dev'
|
|
34
|
+
Requires-Dist: coverage>=7.2; extra == 'dev'
|
|
35
|
+
Requires-Dist: httpx>=0.27; extra == 'dev'
|
|
36
|
+
Requires-Dist: mypy>=1.10; extra == 'dev'
|
|
37
|
+
Requires-Dist: pytest>=8; extra == 'dev'
|
|
38
|
+
Requires-Dist: types-pyyaml; extra == 'dev'
|
|
39
|
+
Provides-Extra: server
|
|
40
|
+
Requires-Dist: uvicorn>=0.27; extra == 'server'
|
|
41
|
+
Description-Content-Type: text/markdown
|
|
42
|
+
|
|
43
|
+
# Mateu server-side for Python
|
|
44
|
+
|
|
45
|
+
A Python implementation of Mateu's **server side**: annotate plain Python classes and serve the
|
|
46
|
+
`/mateu/v3/sync` API, so the **existing renderers** (web, React Native, IntelliJ plugin) render a Python backend
|
|
47
|
+
with **zero client changes**. See [`DESIGN.md`](DESIGN.md) for the plan and the [C# port](../dotnet)
|
|
48
|
+
for the sibling implementation.
|
|
49
|
+
|
|
50
|
+
> **The leverage:** the renderers are backend-agnostic — they POST `/mateu/v3/sync/{route}` and
|
|
51
|
+
> consume the `UIIncrement` JSON. So this is **not** a port of the ~865-file Java framework; it just
|
|
52
|
+
> **emits the same JSON**. Verified by diffing the live output against the C# reference: the
|
|
53
|
+
> `showcase` view is byte-identical (344/344 lines). 20 golden-JSON tests pass.
|
|
54
|
+
|
|
55
|
+
## What works
|
|
56
|
+
|
|
57
|
+
- **Forms** — `@ui` classes; public type-hinted fields become fields (type inferred: `str`,
|
|
58
|
+
`int`→integer, `float`/`Decimal`→number, `bool`→boolean, `date`/`datetime`→date, `Enum`→dropdown);
|
|
59
|
+
`Required()` → required (enforced server-side); `Section("…")` → cards; `@button` → action + toast.
|
|
60
|
+
- **CRUD** — `Crud[T]`: searchable listing (table on desktop, cards on mobile) **plus** full
|
|
61
|
+
detail / edit / new (routes `/x`, `/x/{id}`, `/x/{id}/edit`, `/x/new`), save with validation,
|
|
62
|
+
delete, back-to-list navigation.
|
|
63
|
+
- **Capability listings** — `Listing[Row]` + mixins `Searchable`/`Filterable[F]`/`Navigable`/
|
|
64
|
+
`Editable`/`Creatable`/`Deletable`: a listing that grows by declaring capabilities as base
|
|
65
|
+
classes (mirror of the Java model; `Crud[T]` = all of them at once).
|
|
66
|
+
- **App shell** — `@app` + a menu from `@menu_item` methods + route navigation.
|
|
67
|
+
- **Wizards** — `Wizard` base, `Step(n)` fields, progress + Back/Next, state round-trip.
|
|
68
|
+
- **Decorations** — `@subtitle`, `@banner`, `HeaderBadge`.
|
|
69
|
+
- **Tail features** — `Tab` → tab strip (`Tab("Name", open=True)` makes that tab the one selected on
|
|
70
|
+
first render instead of the first-declared one); field stereotypes (`Multiline`, `Password`, `Money`,
|
|
71
|
+
`PlainText`, `Stereotype`); `LinkTo(href, icon, title, target)` → a nav-link icon on the field
|
|
72
|
+
(href/title travel as raw `${...}` templates, interpolated client-side; implement `LinkSupplier`
|
|
73
|
+
on the view for programmatic links — it wins, `None` falls back to the marker); `@kpi` → KPI
|
|
74
|
+
cards; `@fab` → floating action buttons; `@shortcut`; `@compact`;
|
|
75
|
+
`@confirm_on_navigation_if_dirty`.
|
|
76
|
+
- **i18n / events / security** — `Translator`; `@emits` / `@subscribe_to`; `@secured`.
|
|
77
|
+
- **Layout inference** — `@auto_layout` (opt-in, `@auto_layout(False)` opts out) ports the Java
|
|
78
|
+
`LayoutInference` decision table (same constants/thresholds): heavy unstructured editable forms
|
|
79
|
+
fold optional fields into a collapsed "More options" accordion panel; heavy `@read_only` views
|
|
80
|
+
with ≥ 5 sections become an adaptable `TabLayout` (id `_tabs`, `groupRelationship="alternative"`);
|
|
81
|
+
enums with ≤ 4 members render as radio buttons (`UseRadioButtons()` forces radio always);
|
|
82
|
+
developer-declared tabs always carry `groupRelationship="alternative"` and are `adaptable` when
|
|
83
|
+
the class opted in. Explicit layout markers always win.
|
|
84
|
+
- **UX-pattern components** (`mateu_uidl.components`, same wire JSON as the Java
|
|
85
|
+
`MetricCardDto`…`GanttDto`) — `MetricCard` (+ `MetricTrend`), `Scoreboard`, `DashboardPanel`,
|
|
86
|
+
`DashboardLayout`, `FoldoutPanel`/`FoldoutLayout` (overview slotted `overview`, panel contents
|
|
87
|
+
slotted `panel-N`), `HeroSection`, `EmptyState`, `Skeleton` (+ `SkeletonVariant`), `Gantt` /
|
|
88
|
+
`GanttTask`, plus fluent `Text` and `Button`. `PlanningBoard` (tape chart: `PlanningResource`
|
|
89
|
+
rows × days, `PlanningBlock`s) also carries the OPERA Room Diary extras — `attribute_columns`
|
|
90
|
+
+ per-resource `attributes`/`icon`, block `icon`/`summary` (hover text), and
|
|
91
|
+
`resize_action_id` / `open_action_id` (double click) / `range_select_action_id` (drag across
|
|
92
|
+
empty cells) next to `move_action_id`/`select_action_id`. `Map` / `MapMarker` (street map: `position`
|
|
93
|
+
"lat, lon", `zoom`, `markers`; a marker click runs `marker_action_id` with `_markerId`).
|
|
94
|
+
- **Declarative archetypes** — subclass `Dashboard` / `Foldout` / `ItemOverview` / `Welcome` and
|
|
95
|
+
declare type-hinted fields holding components; `Panel(title, subtitle, col_span, row_span, icon,
|
|
96
|
+
open)` in `Annotated[...]` marks titled panels/tabs/tiles (the analogue of Java's `@Panel`).
|
|
97
|
+
Or subclass `ComponentTreeSupplier` and return any fluent component tree from `component()`.
|
|
98
|
+
|
|
99
|
+
- **Validation** — `Required()`, `Min`/`Max`/`Size`/`Pattern`, `@validation`, `ValidationSupplier`
|
|
100
|
+
→ the component's client-side `validations` (Java's conditions and messages), re-checked on save.
|
|
101
|
+
- **Grid fields** — a `list[Row]` field is a grid with a row editor (add / edit / save / prev /
|
|
102
|
+
next / remove / move), `InlineEditing()` for in-place cells; wide fields span the row.
|
|
103
|
+
- **Catalogues** — named REST sources (`@rest_source`, `sources.yaml`, `source=` on every surface;
|
|
104
|
+
`RestSourceSupplier` for runtime proxy views) and business components (`@business_component`,
|
|
105
|
+
`components.yaml`, `ComponentRef`).
|
|
106
|
+
- **Field types** — the app's vocabulary (`OrderStatus`, `Money`…) declared once in
|
|
107
|
+
`specs/ui/types.yaml` (or a `FieldTypeCatalogSupplier`; the file wins by id) and referenced by
|
|
108
|
+
`fieldType:` on a YAML `FormField`/`GridColumn` or `Annotated[..., FieldType("X")]` on a listing
|
|
109
|
+
row field: the type supplies defaults (incl. status `tones` on columns), the field's own win.
|
|
110
|
+
- **Sample data** — a source in `sources.yaml` may carry `sample:` (the response) or `sampleFile:`
|
|
111
|
+
(JSON/YAML, relative to `specs/ui`). It is used **only in sample mode**, an explicit opt-in:
|
|
112
|
+
set `MATEU_SOURCES_MOCK=true`. Then the app metadata says `mockSources: true` (the browser
|
|
113
|
+
answers its direct fetches from the samples), the samples travel in the catalogue, and the
|
|
114
|
+
proxied `__restfetch__` answers reads with the sample and writes with `{}` without calling the
|
|
115
|
+
endpoint. Without it, samples never reach the wire and endpoints are called for real.
|
|
116
|
+
- **Component adapters** (`ComponentAdapter` + `AdaptedView`), **embedded islands** (a field holding
|
|
117
|
+
a routed view), **`layoutDelta:`** pages, **group actions** (`@group_action`), **wizard
|
|
118
|
+
completion actions**, **listing exports** (CSV built in; Excel/PDF through your own `ListingExporter`), class/method-level **`@eyes_only`**.
|
|
119
|
+
|
|
120
|
+
The shared wire-conformance corpus is a hard gate here: see `tests/test_wire_conformance.py`.
|
|
121
|
+
|
|
122
|
+
## Projects
|
|
123
|
+
|
|
124
|
+
| Package | Role |
|
|
125
|
+
|---|---|
|
|
126
|
+
| `mateu_uidl` | Public API — decorators (`@ui`, `@app`, `@button`, …) + field markers + `Message`, `Crud`, `Wizard`, `Translator`. One module per concern (`markers`, `security`, `suppliers`, `class_decorators`, `method_decorators`, `listing`, `views`, `archetypes`, …), all re-exported from `mateu_uidl` |
|
|
127
|
+
| `mateu_dtos` | The wire model in Pydantic — `UIIncrement`, `Component` + metadata (polymorphism on `type`); split by concern (`app`, `layout`, `fields`, `components`, `envelope`, …), re-exported from `mateu_dtos` |
|
|
128
|
+
| `mateu_core` | The engine — `MateuRegistry`, `ReflectionMapper` (package `mapper/`: one mixin per concern — app, view, archetypes, components, crud, layout, fields…), `SyncHandler` (package `sync_handler/`: dispatch, wizard, listing, crud, search, proxy…) |
|
|
129
|
+
| `mateu_fastapi` | `add_mateu(app, …)` — the `POST /mateu/v3/sync/{route}` endpoint |
|
|
130
|
+
| `samples/demo` | A runnable FastAPI app |
|
|
131
|
+
| `tests` | Golden-JSON tests asserting wire compatibility with the Java/C# backends |
|
|
132
|
+
|
|
133
|
+
## Install
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
pip install "mateu-ui[server]" # the package + uvicorn
|
|
137
|
+
pip install "mateu-ui[all]" # the FastAPI server extras
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
The distribution is **`mateu-ui`** (the PyPI name `mateu` belongs to an unrelated project); the
|
|
141
|
+
import names are `mateu_uidl`, `mateu_dtos`, `mateu_core` and `mateu_fastapi`. Versions move in
|
|
142
|
+
lockstep with the Java artifacts: the `v3.0-alpha.N` release publishes `3.0.0aN`.
|
|
143
|
+
|
|
144
|
+
## Run
|
|
145
|
+
|
|
146
|
+
```bash
|
|
147
|
+
cd backend/python
|
|
148
|
+
python3 -m venv .venv && . .venv/bin/activate
|
|
149
|
+
pip install -e ".[dev,all]"
|
|
150
|
+
uvicorn samples.demo.main:app --host 0.0.0.0 --port 8594 # serves the sync API
|
|
151
|
+
pytest # golden tests
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
Point any Mateu renderer at it — e.g. set the Compose app's `mateu.baseUrl=http://localhost:8594`.
|
|
155
|
+
The server binds to `0.0.0.0`, so the iOS simulator (`localhost:8594`) and Android emulator
|
|
156
|
+
(`10.0.2.2:8594`) reach it too.
|
|
157
|
+
|
|
158
|
+
## Serving: `add_mateu`
|
|
159
|
+
|
|
160
|
+
```python
|
|
161
|
+
from fastapi import FastAPI
|
|
162
|
+
from mateu_fastapi import add_mateu
|
|
163
|
+
from mateu_core.identity import identity_from_claims
|
|
164
|
+
|
|
165
|
+
app = FastAPI()
|
|
166
|
+
add_mateu(
|
|
167
|
+
app, views,
|
|
168
|
+
cors_origins=["https://app.example.com"], # CORS is OFF unless you list origins
|
|
169
|
+
secrets_provider=lambda key: vault.read(key), # ${secret.KEY} in proxied REST sources
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
@app.middleware("http") # YOUR authentication, not Mateu's
|
|
173
|
+
async def authenticate(request, call_next):
|
|
174
|
+
claims = verify_with_your_idp(request.headers.get("authorization")) # e.g. PyJWT + JWKS
|
|
175
|
+
if claims is not None:
|
|
176
|
+
request.state.mateu_identity = identity_from_claims(claims)
|
|
177
|
+
return await call_next(request)
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
- **Identity.** `EyesOnly` / `ReadOnlyUnless` / `DisabledUnless` (and `disabled_unless`) match the
|
|
181
|
+
caller's `Identity(roles, groups, scopes, permissions)`. **Mateu does not authenticate**: the
|
|
182
|
+
default provider, `framework_identity_provider()`, takes the identity your app established for
|
|
183
|
+
the request — an `Identity` your dependency or middleware put in `request.state.mateu_identity`,
|
|
184
|
+
else Starlette's `AuthenticationMiddleware` (`request.user`'s `roles`/`groups`/`permissions` +
|
|
185
|
+
`request.auth.scopes`). A Bearer token is never decoded: verify it in your app and hand Mateu the
|
|
186
|
+
result (`identity_from_claims(claims)` maps Keycloak `realm_access`/`resource_access` roles +
|
|
187
|
+
`roles`, `groups`, `scope`/`scp`, `permissions`). No identity → every gate denies. A custom
|
|
188
|
+
`identity_provider` (parameterless, reading `mateu_core.request_context.current_request()`)
|
|
189
|
+
replaces the default.
|
|
190
|
+
- **Secrets.** `secrets_provider(key) -> str | None` resolves `${secret.KEY}`; unset → the
|
|
191
|
+
environment variable `MATEU_SECRET_<KEY>` (never an arbitrary one). Only the proxy channel (`__restfetch__`) ever sees them.
|
|
192
|
+
- **CORS (breaking).** `add_mateu` used to install `allow_origins=["*"]` by default. CORS is now
|
|
193
|
+
off unless `cors_origins=[...]` lists the allowed origins (`["*"]` still works if you mean it);
|
|
194
|
+
`cors=True` without origins raises. A renderer served by the same app needs no CORS.
|
|
195
|
+
- **Errors.** An unhandled exception answers an error toast, never a raw 500 — the same texts as
|
|
196
|
+
Java and .NET: "Something went wrong" / "An unexpected error occurred. Reference: <id>", the
|
|
197
|
+
reference being the request's correlation id (also in the `X-Mateu-Correlation-Id` header and in
|
|
198
|
+
the ERROR log line with the traceback, logger `mateu.errors`). `dev=True` (or `MATEU_DEV=true` /
|
|
199
|
+
`MATEU_ERRORS_DETAILED=true`) shows the exception instead; raise
|
|
200
|
+
`mateu_uidl.UserFacingException("…", title="…")` (alias `UserFacingError`) for a message written
|
|
201
|
+
for the user, always shown; a pydantic validation error shows its messages. A denied action
|
|
202
|
+
still answers 403.
|
|
203
|
+
- **Concurrency.** The handler runs in Starlette's threadpool, so a slow proxied upstream
|
|
204
|
+
(`proxy_timeout_seconds`, default 30) never blocks the event loop; per-request state (the
|
|
205
|
+
request, the matched route seed, the audience) lives in `ContextVar`s, never on the shared
|
|
206
|
+
handler.
|
|
207
|
+
- **Audience is a projection, not security.** `Audience()` / `@audience` read the client-controlled
|
|
208
|
+
`appState["audience"]`; any caller can send any value. Use it to tailor a screen to a persona, and
|
|
209
|
+
`EyesOnly` (identity) for anything that must stay hidden.
|
|
210
|
+
|
|
211
|
+
## Define views
|
|
212
|
+
|
|
213
|
+
```python
|
|
214
|
+
from typing import Annotated
|
|
215
|
+
from mateu_uidl import ui, title, section, button, Required, Section, Message, app, menu_item, menu_group, MenuDisplay, Crud
|
|
216
|
+
|
|
217
|
+
@ui("person")
|
|
218
|
+
@title("Person")
|
|
219
|
+
class Person:
|
|
220
|
+
name: Annotated[str | None, Required(), Section("Identity")] = None
|
|
221
|
+
age: int = 0
|
|
222
|
+
|
|
223
|
+
@button()
|
|
224
|
+
def save(self) -> Message:
|
|
225
|
+
return Message(f"Saved {self.name}")
|
|
226
|
+
|
|
227
|
+
@ui("reservations")
|
|
228
|
+
@title("Reservations")
|
|
229
|
+
class Reservations(Crud[Reservation]):
|
|
230
|
+
def fetch(self, search): ...
|
|
231
|
+
|
|
232
|
+
@app("My Python Mateu app")
|
|
233
|
+
class DemoApp:
|
|
234
|
+
@menu_item("Person")
|
|
235
|
+
def person(self) -> Person: return Person()
|
|
236
|
+
|
|
237
|
+
# A card menu (like the product dropdowns of a docs site): @menu_group(display="cards") opens a
|
|
238
|
+
# folder as a panel of cards; @menu_item(description=, icon=, image=) / @menu_group on a nested
|
|
239
|
+
# folder ("Bookings/Reservations") give each card its look, and a nested folder's entries are its
|
|
240
|
+
# actions. Code-authored menus (MenuSupplier / AppSupplier) set MenuItem.display/description/icon/image.
|
|
241
|
+
@app("PMS")
|
|
242
|
+
@menu_group("Bookings", display=MenuDisplay.cards)
|
|
243
|
+
@menu_group("Bookings/Reservations", description="Search, create and modify",
|
|
244
|
+
icon="vaadin:calendar", image="/img/res.png")
|
|
245
|
+
class PmsApp:
|
|
246
|
+
@menu_item("Search", group="Bookings/Reservations")
|
|
247
|
+
def search(self) -> Reservations: return Reservations()
|
|
248
|
+
|
|
249
|
+
@menu_item("Room diary", group="Bookings", description="Rooms by day")
|
|
250
|
+
def diary(self) -> Person: return Person()
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
Dashboards, foldouts and Gantt charts use the fluent components + archetypes:
|
|
254
|
+
|
|
255
|
+
```python
|
|
256
|
+
from datetime import date
|
|
257
|
+
from mateu_uidl import ui, title, Dashboard, Panel
|
|
258
|
+
from mateu_uidl.components import MetricCard, MetricTrend, Gantt, GanttTask
|
|
259
|
+
|
|
260
|
+
@ui("dashboard")
|
|
261
|
+
@title("Sales dashboard")
|
|
262
|
+
class SalesDashboard(Dashboard):
|
|
263
|
+
revenue: MetricCard = MetricCard(title="Revenue", value="1.2", unit="M€",
|
|
264
|
+
trend=MetricTrend.up, action_id="openRevenue")
|
|
265
|
+
incidents: MetricCard = MetricCard(title="Incidents", value="3", trend=MetricTrend.down)
|
|
266
|
+
plan: Annotated[Gantt, Panel("Rollout plan", col_span=2)] = Gantt(tasks=(
|
|
267
|
+
GanttTask(id="t1", title="Design", start=date(2026, 7, 1), end=date(2026, 7, 20),
|
|
268
|
+
progress=80.0),
|
|
269
|
+
))
|
|
270
|
+
|
|
271
|
+
def open_revenue(self): ... # runs when the Revenue tile is clicked
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
## Note for Python 3.14+
|
|
275
|
+
|
|
276
|
+
Under [PEP 649](https://peps.python.org/pep-0649/) annotations are evaluated lazily and are no longer
|
|
277
|
+
eagerly stored in `__dict__['__annotations__']`. The reflection layer reads them via
|
|
278
|
+
`inspect.get_annotations(...)`, so the field markers work on 3.11 through 3.14+.
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
# Mateu server-side for Python
|
|
2
|
+
|
|
3
|
+
A Python implementation of Mateu's **server side**: annotate plain Python classes and serve the
|
|
4
|
+
`/mateu/v3/sync` API, so the **existing renderers** (web, React Native, IntelliJ plugin) render a Python backend
|
|
5
|
+
with **zero client changes**. See [`DESIGN.md`](DESIGN.md) for the plan and the [C# port](../dotnet)
|
|
6
|
+
for the sibling implementation.
|
|
7
|
+
|
|
8
|
+
> **The leverage:** the renderers are backend-agnostic — they POST `/mateu/v3/sync/{route}` and
|
|
9
|
+
> consume the `UIIncrement` JSON. So this is **not** a port of the ~865-file Java framework; it just
|
|
10
|
+
> **emits the same JSON**. Verified by diffing the live output against the C# reference: the
|
|
11
|
+
> `showcase` view is byte-identical (344/344 lines). 20 golden-JSON tests pass.
|
|
12
|
+
|
|
13
|
+
## What works
|
|
14
|
+
|
|
15
|
+
- **Forms** — `@ui` classes; public type-hinted fields become fields (type inferred: `str`,
|
|
16
|
+
`int`→integer, `float`/`Decimal`→number, `bool`→boolean, `date`/`datetime`→date, `Enum`→dropdown);
|
|
17
|
+
`Required()` → required (enforced server-side); `Section("…")` → cards; `@button` → action + toast.
|
|
18
|
+
- **CRUD** — `Crud[T]`: searchable listing (table on desktop, cards on mobile) **plus** full
|
|
19
|
+
detail / edit / new (routes `/x`, `/x/{id}`, `/x/{id}/edit`, `/x/new`), save with validation,
|
|
20
|
+
delete, back-to-list navigation.
|
|
21
|
+
- **Capability listings** — `Listing[Row]` + mixins `Searchable`/`Filterable[F]`/`Navigable`/
|
|
22
|
+
`Editable`/`Creatable`/`Deletable`: a listing that grows by declaring capabilities as base
|
|
23
|
+
classes (mirror of the Java model; `Crud[T]` = all of them at once).
|
|
24
|
+
- **App shell** — `@app` + a menu from `@menu_item` methods + route navigation.
|
|
25
|
+
- **Wizards** — `Wizard` base, `Step(n)` fields, progress + Back/Next, state round-trip.
|
|
26
|
+
- **Decorations** — `@subtitle`, `@banner`, `HeaderBadge`.
|
|
27
|
+
- **Tail features** — `Tab` → tab strip (`Tab("Name", open=True)` makes that tab the one selected on
|
|
28
|
+
first render instead of the first-declared one); field stereotypes (`Multiline`, `Password`, `Money`,
|
|
29
|
+
`PlainText`, `Stereotype`); `LinkTo(href, icon, title, target)` → a nav-link icon on the field
|
|
30
|
+
(href/title travel as raw `${...}` templates, interpolated client-side; implement `LinkSupplier`
|
|
31
|
+
on the view for programmatic links — it wins, `None` falls back to the marker); `@kpi` → KPI
|
|
32
|
+
cards; `@fab` → floating action buttons; `@shortcut`; `@compact`;
|
|
33
|
+
`@confirm_on_navigation_if_dirty`.
|
|
34
|
+
- **i18n / events / security** — `Translator`; `@emits` / `@subscribe_to`; `@secured`.
|
|
35
|
+
- **Layout inference** — `@auto_layout` (opt-in, `@auto_layout(False)` opts out) ports the Java
|
|
36
|
+
`LayoutInference` decision table (same constants/thresholds): heavy unstructured editable forms
|
|
37
|
+
fold optional fields into a collapsed "More options" accordion panel; heavy `@read_only` views
|
|
38
|
+
with ≥ 5 sections become an adaptable `TabLayout` (id `_tabs`, `groupRelationship="alternative"`);
|
|
39
|
+
enums with ≤ 4 members render as radio buttons (`UseRadioButtons()` forces radio always);
|
|
40
|
+
developer-declared tabs always carry `groupRelationship="alternative"` and are `adaptable` when
|
|
41
|
+
the class opted in. Explicit layout markers always win.
|
|
42
|
+
- **UX-pattern components** (`mateu_uidl.components`, same wire JSON as the Java
|
|
43
|
+
`MetricCardDto`…`GanttDto`) — `MetricCard` (+ `MetricTrend`), `Scoreboard`, `DashboardPanel`,
|
|
44
|
+
`DashboardLayout`, `FoldoutPanel`/`FoldoutLayout` (overview slotted `overview`, panel contents
|
|
45
|
+
slotted `panel-N`), `HeroSection`, `EmptyState`, `Skeleton` (+ `SkeletonVariant`), `Gantt` /
|
|
46
|
+
`GanttTask`, plus fluent `Text` and `Button`. `PlanningBoard` (tape chart: `PlanningResource`
|
|
47
|
+
rows × days, `PlanningBlock`s) also carries the OPERA Room Diary extras — `attribute_columns`
|
|
48
|
+
+ per-resource `attributes`/`icon`, block `icon`/`summary` (hover text), and
|
|
49
|
+
`resize_action_id` / `open_action_id` (double click) / `range_select_action_id` (drag across
|
|
50
|
+
empty cells) next to `move_action_id`/`select_action_id`. `Map` / `MapMarker` (street map: `position`
|
|
51
|
+
"lat, lon", `zoom`, `markers`; a marker click runs `marker_action_id` with `_markerId`).
|
|
52
|
+
- **Declarative archetypes** — subclass `Dashboard` / `Foldout` / `ItemOverview` / `Welcome` and
|
|
53
|
+
declare type-hinted fields holding components; `Panel(title, subtitle, col_span, row_span, icon,
|
|
54
|
+
open)` in `Annotated[...]` marks titled panels/tabs/tiles (the analogue of Java's `@Panel`).
|
|
55
|
+
Or subclass `ComponentTreeSupplier` and return any fluent component tree from `component()`.
|
|
56
|
+
|
|
57
|
+
- **Validation** — `Required()`, `Min`/`Max`/`Size`/`Pattern`, `@validation`, `ValidationSupplier`
|
|
58
|
+
→ the component's client-side `validations` (Java's conditions and messages), re-checked on save.
|
|
59
|
+
- **Grid fields** — a `list[Row]` field is a grid with a row editor (add / edit / save / prev /
|
|
60
|
+
next / remove / move), `InlineEditing()` for in-place cells; wide fields span the row.
|
|
61
|
+
- **Catalogues** — named REST sources (`@rest_source`, `sources.yaml`, `source=` on every surface;
|
|
62
|
+
`RestSourceSupplier` for runtime proxy views) and business components (`@business_component`,
|
|
63
|
+
`components.yaml`, `ComponentRef`).
|
|
64
|
+
- **Field types** — the app's vocabulary (`OrderStatus`, `Money`…) declared once in
|
|
65
|
+
`specs/ui/types.yaml` (or a `FieldTypeCatalogSupplier`; the file wins by id) and referenced by
|
|
66
|
+
`fieldType:` on a YAML `FormField`/`GridColumn` or `Annotated[..., FieldType("X")]` on a listing
|
|
67
|
+
row field: the type supplies defaults (incl. status `tones` on columns), the field's own win.
|
|
68
|
+
- **Sample data** — a source in `sources.yaml` may carry `sample:` (the response) or `sampleFile:`
|
|
69
|
+
(JSON/YAML, relative to `specs/ui`). It is used **only in sample mode**, an explicit opt-in:
|
|
70
|
+
set `MATEU_SOURCES_MOCK=true`. Then the app metadata says `mockSources: true` (the browser
|
|
71
|
+
answers its direct fetches from the samples), the samples travel in the catalogue, and the
|
|
72
|
+
proxied `__restfetch__` answers reads with the sample and writes with `{}` without calling the
|
|
73
|
+
endpoint. Without it, samples never reach the wire and endpoints are called for real.
|
|
74
|
+
- **Component adapters** (`ComponentAdapter` + `AdaptedView`), **embedded islands** (a field holding
|
|
75
|
+
a routed view), **`layoutDelta:`** pages, **group actions** (`@group_action`), **wizard
|
|
76
|
+
completion actions**, **listing exports** (CSV built in; Excel/PDF through your own `ListingExporter`), class/method-level **`@eyes_only`**.
|
|
77
|
+
|
|
78
|
+
The shared wire-conformance corpus is a hard gate here: see `tests/test_wire_conformance.py`.
|
|
79
|
+
|
|
80
|
+
## Projects
|
|
81
|
+
|
|
82
|
+
| Package | Role |
|
|
83
|
+
|---|---|
|
|
84
|
+
| `mateu_uidl` | Public API — decorators (`@ui`, `@app`, `@button`, …) + field markers + `Message`, `Crud`, `Wizard`, `Translator`. One module per concern (`markers`, `security`, `suppliers`, `class_decorators`, `method_decorators`, `listing`, `views`, `archetypes`, …), all re-exported from `mateu_uidl` |
|
|
85
|
+
| `mateu_dtos` | The wire model in Pydantic — `UIIncrement`, `Component` + metadata (polymorphism on `type`); split by concern (`app`, `layout`, `fields`, `components`, `envelope`, …), re-exported from `mateu_dtos` |
|
|
86
|
+
| `mateu_core` | The engine — `MateuRegistry`, `ReflectionMapper` (package `mapper/`: one mixin per concern — app, view, archetypes, components, crud, layout, fields…), `SyncHandler` (package `sync_handler/`: dispatch, wizard, listing, crud, search, proxy…) |
|
|
87
|
+
| `mateu_fastapi` | `add_mateu(app, …)` — the `POST /mateu/v3/sync/{route}` endpoint |
|
|
88
|
+
| `samples/demo` | A runnable FastAPI app |
|
|
89
|
+
| `tests` | Golden-JSON tests asserting wire compatibility with the Java/C# backends |
|
|
90
|
+
|
|
91
|
+
## Install
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
pip install "mateu-ui[server]" # the package + uvicorn
|
|
95
|
+
pip install "mateu-ui[all]" # the FastAPI server extras
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
The distribution is **`mateu-ui`** (the PyPI name `mateu` belongs to an unrelated project); the
|
|
99
|
+
import names are `mateu_uidl`, `mateu_dtos`, `mateu_core` and `mateu_fastapi`. Versions move in
|
|
100
|
+
lockstep with the Java artifacts: the `v3.0-alpha.N` release publishes `3.0.0aN`.
|
|
101
|
+
|
|
102
|
+
## Run
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
cd backend/python
|
|
106
|
+
python3 -m venv .venv && . .venv/bin/activate
|
|
107
|
+
pip install -e ".[dev,all]"
|
|
108
|
+
uvicorn samples.demo.main:app --host 0.0.0.0 --port 8594 # serves the sync API
|
|
109
|
+
pytest # golden tests
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Point any Mateu renderer at it — e.g. set the Compose app's `mateu.baseUrl=http://localhost:8594`.
|
|
113
|
+
The server binds to `0.0.0.0`, so the iOS simulator (`localhost:8594`) and Android emulator
|
|
114
|
+
(`10.0.2.2:8594`) reach it too.
|
|
115
|
+
|
|
116
|
+
## Serving: `add_mateu`
|
|
117
|
+
|
|
118
|
+
```python
|
|
119
|
+
from fastapi import FastAPI
|
|
120
|
+
from mateu_fastapi import add_mateu
|
|
121
|
+
from mateu_core.identity import identity_from_claims
|
|
122
|
+
|
|
123
|
+
app = FastAPI()
|
|
124
|
+
add_mateu(
|
|
125
|
+
app, views,
|
|
126
|
+
cors_origins=["https://app.example.com"], # CORS is OFF unless you list origins
|
|
127
|
+
secrets_provider=lambda key: vault.read(key), # ${secret.KEY} in proxied REST sources
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
@app.middleware("http") # YOUR authentication, not Mateu's
|
|
131
|
+
async def authenticate(request, call_next):
|
|
132
|
+
claims = verify_with_your_idp(request.headers.get("authorization")) # e.g. PyJWT + JWKS
|
|
133
|
+
if claims is not None:
|
|
134
|
+
request.state.mateu_identity = identity_from_claims(claims)
|
|
135
|
+
return await call_next(request)
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
- **Identity.** `EyesOnly` / `ReadOnlyUnless` / `DisabledUnless` (and `disabled_unless`) match the
|
|
139
|
+
caller's `Identity(roles, groups, scopes, permissions)`. **Mateu does not authenticate**: the
|
|
140
|
+
default provider, `framework_identity_provider()`, takes the identity your app established for
|
|
141
|
+
the request — an `Identity` your dependency or middleware put in `request.state.mateu_identity`,
|
|
142
|
+
else Starlette's `AuthenticationMiddleware` (`request.user`'s `roles`/`groups`/`permissions` +
|
|
143
|
+
`request.auth.scopes`). A Bearer token is never decoded: verify it in your app and hand Mateu the
|
|
144
|
+
result (`identity_from_claims(claims)` maps Keycloak `realm_access`/`resource_access` roles +
|
|
145
|
+
`roles`, `groups`, `scope`/`scp`, `permissions`). No identity → every gate denies. A custom
|
|
146
|
+
`identity_provider` (parameterless, reading `mateu_core.request_context.current_request()`)
|
|
147
|
+
replaces the default.
|
|
148
|
+
- **Secrets.** `secrets_provider(key) -> str | None` resolves `${secret.KEY}`; unset → the
|
|
149
|
+
environment variable `MATEU_SECRET_<KEY>` (never an arbitrary one). Only the proxy channel (`__restfetch__`) ever sees them.
|
|
150
|
+
- **CORS (breaking).** `add_mateu` used to install `allow_origins=["*"]` by default. CORS is now
|
|
151
|
+
off unless `cors_origins=[...]` lists the allowed origins (`["*"]` still works if you mean it);
|
|
152
|
+
`cors=True` without origins raises. A renderer served by the same app needs no CORS.
|
|
153
|
+
- **Errors.** An unhandled exception answers an error toast, never a raw 500 — the same texts as
|
|
154
|
+
Java and .NET: "Something went wrong" / "An unexpected error occurred. Reference: <id>", the
|
|
155
|
+
reference being the request's correlation id (also in the `X-Mateu-Correlation-Id` header and in
|
|
156
|
+
the ERROR log line with the traceback, logger `mateu.errors`). `dev=True` (or `MATEU_DEV=true` /
|
|
157
|
+
`MATEU_ERRORS_DETAILED=true`) shows the exception instead; raise
|
|
158
|
+
`mateu_uidl.UserFacingException("…", title="…")` (alias `UserFacingError`) for a message written
|
|
159
|
+
for the user, always shown; a pydantic validation error shows its messages. A denied action
|
|
160
|
+
still answers 403.
|
|
161
|
+
- **Concurrency.** The handler runs in Starlette's threadpool, so a slow proxied upstream
|
|
162
|
+
(`proxy_timeout_seconds`, default 30) never blocks the event loop; per-request state (the
|
|
163
|
+
request, the matched route seed, the audience) lives in `ContextVar`s, never on the shared
|
|
164
|
+
handler.
|
|
165
|
+
- **Audience is a projection, not security.** `Audience()` / `@audience` read the client-controlled
|
|
166
|
+
`appState["audience"]`; any caller can send any value. Use it to tailor a screen to a persona, and
|
|
167
|
+
`EyesOnly` (identity) for anything that must stay hidden.
|
|
168
|
+
|
|
169
|
+
## Define views
|
|
170
|
+
|
|
171
|
+
```python
|
|
172
|
+
from typing import Annotated
|
|
173
|
+
from mateu_uidl import ui, title, section, button, Required, Section, Message, app, menu_item, menu_group, MenuDisplay, Crud
|
|
174
|
+
|
|
175
|
+
@ui("person")
|
|
176
|
+
@title("Person")
|
|
177
|
+
class Person:
|
|
178
|
+
name: Annotated[str | None, Required(), Section("Identity")] = None
|
|
179
|
+
age: int = 0
|
|
180
|
+
|
|
181
|
+
@button()
|
|
182
|
+
def save(self) -> Message:
|
|
183
|
+
return Message(f"Saved {self.name}")
|
|
184
|
+
|
|
185
|
+
@ui("reservations")
|
|
186
|
+
@title("Reservations")
|
|
187
|
+
class Reservations(Crud[Reservation]):
|
|
188
|
+
def fetch(self, search): ...
|
|
189
|
+
|
|
190
|
+
@app("My Python Mateu app")
|
|
191
|
+
class DemoApp:
|
|
192
|
+
@menu_item("Person")
|
|
193
|
+
def person(self) -> Person: return Person()
|
|
194
|
+
|
|
195
|
+
# A card menu (like the product dropdowns of a docs site): @menu_group(display="cards") opens a
|
|
196
|
+
# folder as a panel of cards; @menu_item(description=, icon=, image=) / @menu_group on a nested
|
|
197
|
+
# folder ("Bookings/Reservations") give each card its look, and a nested folder's entries are its
|
|
198
|
+
# actions. Code-authored menus (MenuSupplier / AppSupplier) set MenuItem.display/description/icon/image.
|
|
199
|
+
@app("PMS")
|
|
200
|
+
@menu_group("Bookings", display=MenuDisplay.cards)
|
|
201
|
+
@menu_group("Bookings/Reservations", description="Search, create and modify",
|
|
202
|
+
icon="vaadin:calendar", image="/img/res.png")
|
|
203
|
+
class PmsApp:
|
|
204
|
+
@menu_item("Search", group="Bookings/Reservations")
|
|
205
|
+
def search(self) -> Reservations: return Reservations()
|
|
206
|
+
|
|
207
|
+
@menu_item("Room diary", group="Bookings", description="Rooms by day")
|
|
208
|
+
def diary(self) -> Person: return Person()
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
Dashboards, foldouts and Gantt charts use the fluent components + archetypes:
|
|
212
|
+
|
|
213
|
+
```python
|
|
214
|
+
from datetime import date
|
|
215
|
+
from mateu_uidl import ui, title, Dashboard, Panel
|
|
216
|
+
from mateu_uidl.components import MetricCard, MetricTrend, Gantt, GanttTask
|
|
217
|
+
|
|
218
|
+
@ui("dashboard")
|
|
219
|
+
@title("Sales dashboard")
|
|
220
|
+
class SalesDashboard(Dashboard):
|
|
221
|
+
revenue: MetricCard = MetricCard(title="Revenue", value="1.2", unit="M€",
|
|
222
|
+
trend=MetricTrend.up, action_id="openRevenue")
|
|
223
|
+
incidents: MetricCard = MetricCard(title="Incidents", value="3", trend=MetricTrend.down)
|
|
224
|
+
plan: Annotated[Gantt, Panel("Rollout plan", col_span=2)] = Gantt(tasks=(
|
|
225
|
+
GanttTask(id="t1", title="Design", start=date(2026, 7, 1), end=date(2026, 7, 20),
|
|
226
|
+
progress=80.0),
|
|
227
|
+
))
|
|
228
|
+
|
|
229
|
+
def open_revenue(self): ... # runs when the Revenue tile is clicked
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
## Note for Python 3.14+
|
|
233
|
+
|
|
234
|
+
Under [PEP 649](https://peps.python.org/pep-0649/) annotations are evaluated lazily and are no longer
|
|
235
|
+
eagerly stored in `__dict__['__annotations__']`. The reflection layer reads them via
|
|
236
|
+
`inspect.get_annotations(...)`, so the field markers work on 3.11 through 3.14+.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""The Mateu engine: route registry, reflection mapper, and the sync use-case."""
|
|
2
|
+
|
|
3
|
+
from ._version import __version__
|
|
4
|
+
from .action_guard import MateuForbiddenException
|
|
5
|
+
from .importwizard import ColumnMapping, ImportWizard, RowIssue, detect_separator, parse_csv
|
|
6
|
+
from .mapper import ReflectionMapper
|
|
7
|
+
from .registry import MateuRegistry, normalize, type_name
|
|
8
|
+
from .sync_handler import RunActionRq, SyncHandler
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"ColumnMapping",
|
|
12
|
+
"ImportWizard",
|
|
13
|
+
"MateuForbiddenException",
|
|
14
|
+
"MateuRegistry",
|
|
15
|
+
"ReflectionMapper",
|
|
16
|
+
"RowIssue",
|
|
17
|
+
"RunActionRq",
|
|
18
|
+
"SyncHandler",
|
|
19
|
+
"detect_separator",
|
|
20
|
+
"normalize",
|
|
21
|
+
"parse_csv",
|
|
22
|
+
"type_name",
|
|
23
|
+
"__version__",
|
|
24
|
+
]
|