Static-typing helpers for manifest and view authoring.
These exist purely so IDE-side tools (Pylance/Pyright, mypy) can flag
typos, missing required keys, and shape mismatches at edit time. They
have no runtime effect — the loader still does duck-typed reads of
each module's globals. Apps that don't use a type checker can ignore
this module entirely.
Recommended usage in a data file:
from pyvelm.types import ListView, FormView
VIEWS: list[View] = [
ListView(
name="partner.list",
model="res.partner",
view_type="list",
arch={"fields": ["name", "code"]},
),
]
Or, for maximum ergonomics, use the builder helpers in pyvelm.builders:
from pyvelm.builders import list_view, form_view, section, notebook, page
VIEWS = [
list_view("partner.list", "res.partner", fields=["name", "code"]),
form_view("partner.form", "res.partner", sections=[
section("identity", "Identity", ["name", "code"]),
notebook("extra", pages=[
page("tags", "Tags", ["tag_ids"]),
]),
]),
]
In a manifest, annotate each global individually:
from pyvelm.types import Manifest
NAME: str = "partners"
VERSION: tuple[int, ...] = (0, 2, 0)
DEPENDS: list[str] = ["base"]
DATA: list[str] = ["views/partner.py"]
(There is no module-level "Manifest" assignment because the loader
reads individual attributes, not a single dict. The Manifest
TypedDict below exists for the convenience of tools that want to
validate a manifest as a whole — e.g. a future pyvelm lint.)
FieldRef
Bases: _FieldRefRequired
One field entry inside an arch list (list.fields,
form.sections[*].fields, kanban.card.fields, kanban.card.badges).
Authoring sugar: a bare string "name" is equivalent to
{"name": "name"}. The normalizer rewrites strings to dicts on
storage so inheritance has stable addresses.
name is the only required key. Everything else is optional
and surface only when the matching renderer / form-control honors
it (widget → widget registry, required → red * + the
server-side validator, etc.). App-specific attributes that aren't
in this list are still accepted by the loader at runtime; the
type checker just won't autocomplete them.
One2many on parent forms (see docs/one2many-forms.md):
widget — dialog (default) or inline / table; also the
default when edit_toggle is true
edit_toggle — show Dialog / Inline grid switch (needs columns
or list_view for both modes)
columns — inline sub-grid columns without a registered list view
list_view — name of a comodel list view (columns + sequence)
form_view — comodel form for row links and dialog create
Source code in pyvelm/types.py
| class FieldRef(_FieldRefRequired, total=False):
"""One field entry inside an arch list (list.fields,
form.sections[*].fields, kanban.card.fields, kanban.card.badges).
Authoring sugar: a bare string ``"name"`` is equivalent to
``{"name": "name"}``. The normalizer rewrites strings to dicts on
storage so inheritance has stable addresses.
``name`` is the only required key. Everything else is optional
and surface only when the matching renderer / form-control honors
it (``widget`` → widget registry, ``required`` → red ``*`` + the
server-side validator, etc.). App-specific attributes that aren't
in this list are still accepted by the loader at runtime; the
type checker just won't autocomplete them.
**One2many on parent forms** (see ``docs/one2many-forms.md``):
- ``widget`` — ``dialog`` (default) or ``inline`` / ``table``; also the
default when ``edit_toggle`` is true
- ``edit_toggle`` — show **Dialog / Inline grid** switch (needs ``columns``
or ``list_view`` for both modes)
- ``columns`` — inline sub-grid columns without a registered list view
- ``list_view`` — name of a comodel list view (columns + ``sequence``)
- ``form_view`` — comodel form for row links and dialog create
"""
edit_toggle: bool
widget: WidgetHint
label: str
readonly: bool | object
readonly_when: list
required: bool | object
required_when: list
visible: bool | object
visible_when: list
hidden: bool | object
visible_js: str
live: bool | int | str
depends_on: list[str]
options_domain: list | object
default: object
# Form-grid only: how many columns of the surrounding section this
# field's cell spans. ``"full"`` (or an integer >= the section's
# ``cols``) makes the cell occupy the full row. Ignored on list /
# kanban / graph / pivot.
colspan: Union[int, Literal["full"]]
# One2many / Many2many on parent forms: which comodel list view powers
# the embedded table (default: lowest ``ir.ui.view`` id for that model).
list_view: Union[str, tuple[str, str]]
# Which comodel form view opens for row links / dialog create (default:
# lowest-id form view on the comodel).
form_view: Union[str, tuple[str, str]]
# One2many only: table columns without registering a list view. Same
# entries as a list arch ``fields`` list (strings or ``field(...)`` dicts).
# Takes precedence over ``list_view`` for column layout.
columns: list[FieldRefLike]
|
ArchList
Bases: _ArchListRequired
Arch for view_type="list" views.
Only fields is required. Optional keys:
title — human-readable heading shown above the table.
form_view — "<name>" of a form view to link each row to.
detail_view — read-only detail view for row clicks (uses can_read).
record_href — URL pattern for row clicks; {id} is substituted.
create_href — URL for the list's New button (full navigation).
page_actions — toolbar buttons (same shape as form header_actions).
bulk_actions — bulk bar actions (default: delete when unlink allowed).
row_actions — per-row action buttons in the Actions column.
sequence — name of an integer field; when set the renderer
adds a drag handle and forces sort by that field.
domain — static domain ANDed with toolbar search / filter chips
(same semantics as graph/pivot views).
Source code in pyvelm/types.py
| class ArchList(_ArchListRequired, total=False):
"""Arch for ``view_type="list"`` views.
Only ``fields`` is required. Optional keys:
- ``title`` — human-readable heading shown above the table.
- ``form_view`` — ``"<name>"`` of a form view to link each row to.
- ``detail_view`` — read-only detail view for row clicks (uses ``can_read``).
- ``record_href`` — URL pattern for row clicks; ``{id}`` is substituted.
- ``create_href`` — URL for the list's New button (full navigation).
- ``page_actions`` — toolbar buttons (same shape as form ``header_actions``).
- ``bulk_actions`` — bulk bar actions (default: delete when unlink allowed).
- ``row_actions`` — per-row action buttons in the Actions column.
- ``sequence`` — name of an integer field; when set the renderer
adds a drag handle and forces sort by that field.
- ``domain`` — static domain ANDed with toolbar search / filter chips
(same semantics as graph/pivot views).
"""
title: str
form_view: str
detail_view: str
record_href: str
create_href: str
page_actions: list[ArchHeaderAction]
bulk_actions: list[ArchListBulkAction]
row_actions: list[ArchHeaderAction]
sequence: str
domain: list
|
ArchListBulkAction
Bases: TypedDict
One bulk action on a list view bulk bar.
Built-in action values: "unlink" (delete selected rows).
Custom URL actions are not supported yet — use row_actions /
page_actions for one-off endpoints.
Source code in pyvelm/types.py
| class ArchListBulkAction(TypedDict, total=False):
"""One bulk action on a list view bulk bar.
Built-in ``action`` values: ``"unlink"`` (delete selected rows).
Custom URL actions are not supported yet — use ``row_actions`` /
``page_actions`` for one-off endpoints.
"""
label: str
action: str
confirm: str
perm: str
|
ArchSection
Bases: _ArchSectionRequired
One section of a form view.
name, title and fields are required. cols overrides
the form-level column count for the fields rendered inside this
section (default: the form's cols, which defaults to 2).
Source code in pyvelm/types.py
| class ArchSection(_ArchSectionRequired, total=False):
"""One section of a form view.
``name``, ``title`` and ``fields`` are required. ``cols`` overrides
the form-level column count for the fields rendered inside this
section (default: the form's ``cols``, which defaults to 2).
"""
cols: int
|
ArchPage
Bases: _ArchPageRequired
One tab page inside a form notebook.
Same grid rules as ArchSection; title is the tab label.
Source code in pyvelm/types.py
| class ArchPage(_ArchPageRequired, total=False):
"""One tab page inside a form notebook.
Same grid rules as ``ArchSection``; ``title`` is the tab label.
"""
cols: int
|
ArchNotebook
Bases: _ArchNotebookRequired
Tabbed notebook block on a form (Odoo <notebook>).
Use pages instead of fields. Optional title renders as an
outer fieldset legend above the tab strip.
Source code in pyvelm/types.py
| class ArchNotebook(_ArchNotebookRequired, total=False):
"""Tabbed notebook block on a form (Odoo ``<notebook>``).
Use ``pages`` instead of ``fields``. Optional ``title`` renders as an
outer fieldset legend above the tab strip.
"""
title: str
|
Bases: TypedDict
One button rendered in the form's display-mode action toolbar.
Keys:
label — button text (required).
url — endpoint to hit; {id} is substituted with the
current record id at render time.
method — HTTP verb, default "POST".
confirm — optional confirmation prompt; when set, the button
asks before firing.
perm — CRUD permission (read / write / create /
unlink) required to see this action. When the
current user lacks it, the button is hidden instead
of rendered-then-denied. Omit for actions any reader
may run.
model — model the perm check targets; defaults to the
view's own model.
Source code in pyvelm/types.py
| class ArchHeaderAction(TypedDict, total=False):
"""One button rendered in the form's display-mode action toolbar.
Keys:
- ``label`` — button text (required).
- ``url`` — endpoint to hit; ``{id}`` is substituted with the
current record id at render time.
- ``method`` — HTTP verb, default ``"POST"``.
- ``confirm`` — optional confirmation prompt; when set, the button
asks before firing.
- ``perm`` — CRUD permission (``read`` / ``write`` / ``create`` /
``unlink``) required to *see* this action. When the
current user lacks it, the button is hidden instead
of rendered-then-denied. Omit for actions any reader
may run.
- ``model`` — model the ``perm`` check targets; defaults to the
view's own model.
"""
label: str
url: str
method: str
confirm: str
perm: str
model: str
policy: str
|
Bases: _ArchFormRequired
Arch for view_type="form" views.
Only sections is required. Each entry is either a flat
section(...) (fields) or a notebook(...) (pages).
Optional keys:
title — overrides the auto-generated page heading.
header_actions — list of buttons rendered next to Edit /
Delete in display mode (e.g. "Run Now" on a cron form).
cols — number of columns each section uses (default 2). Each
field(...) may set colspan to span multiple cells; a
section or notebook page may override cols for its own block.
Source code in pyvelm/types.py
| class ArchForm(_ArchFormRequired, total=False):
"""Arch for ``view_type="form"`` views.
Only ``sections`` is required. Each entry is either a flat
``section(...)`` (``fields``) or a ``notebook(...)`` (``pages``).
Optional keys:
- ``title`` — overrides the auto-generated page heading.
- ``header_actions`` — list of buttons rendered next to Edit /
Delete in display mode (e.g. "Run Now" on a cron form).
- ``cols`` — number of columns each section uses (default 2). Each
``field(...)`` may set ``colspan`` to span multiple cells; a
section or notebook page may override ``cols`` for its own block.
"""
title: str
header_actions: list[ArchHeaderAction]
cols: int
|
ArchDetail
Bases: _ArchFormRequired
Arch for view_type="detail" views — read-only record pages.
Same layout keys as :class:ArchForm. Optional form_view names
the editable form opened by the Edit toolbar link.
Source code in pyvelm/types.py
| class ArchDetail(_ArchFormRequired, total=False):
"""Arch for ``view_type="detail"`` views — read-only record pages.
Same layout keys as :class:`ArchForm`. Optional ``form_view`` names
the editable form opened by the **Edit** toolbar link.
"""
title: str
header_actions: list[ArchHeaderAction]
cols: int
form_view: str
|
ArchKanban
Bases: TypedDict
Arch for view_type="kanban" views. All keys are optional.
Source code in pyvelm/types.py
| class ArchKanban(TypedDict, total=False):
"""Arch for ``view_type="kanban"`` views. All keys are optional."""
title: str
card: ArchKanbanCard
group_by: str
sequence: str
form_view: str
|
ArchGraph
Bases: _ArchGraphRequired
Arch for view_type="graph" views.
Required keys are groupby and measure. Optional:
title — page heading (defaults to the model's plural form).
chart — "bar" | "line" | "pie" (default "bar").
stacked — bar charts only: stack measures (no-op until we
grow multi-measure bar support).
horizontal — bar charts only: render horizontally.
domain — extra static domain ANDed with the search filters.
Source code in pyvelm/types.py
| class ArchGraph(_ArchGraphRequired, total=False):
"""Arch for ``view_type="graph"`` views.
Required keys are ``groupby`` and ``measure``. Optional:
- ``title`` — page heading (defaults to the model's plural form).
- ``chart`` — ``"bar" | "line" | "pie"`` (default ``"bar"``).
- ``stacked`` — bar charts only: stack measures (no-op until we
grow multi-measure bar support).
- ``horizontal`` — bar charts only: render horizontally.
- ``domain`` — extra static domain ANDed with the search filters.
"""
title: str
chart: Literal["bar", "line", "pie"]
stacked: bool
horizontal: bool
domain: list
|
ArchPivot
Bases: _ArchPivotRequired
Arch for view_type="pivot" views.
Required: row_groupby, col_groupby, measures. Optional:
title — page heading.
domain — extra static domain ANDed with the search filters.
Source code in pyvelm/types.py
| class ArchPivot(_ArchPivotRequired, total=False):
"""Arch for ``view_type="pivot"`` views.
Required: ``row_groupby``, ``col_groupby``, ``measures``. Optional:
- ``title`` — page heading.
- ``domain`` — extra static domain ANDed with the search filters.
"""
title: str
domain: list
|
Bases: TypedDict
One tile on a view_type="dashboard" page.
Every widget carries type and id (a stable DOM key). Other
keys depend on the type — see the builder helpers in
pyvelm.builders.
Source code in pyvelm/types.py
| class DashboardWidget(TypedDict, total=False):
"""One tile on a ``view_type="dashboard"`` page.
Every widget carries ``type`` and ``id`` (a stable DOM key). Other
keys depend on the type — see the builder helpers in
``pyvelm.builders``.
"""
type: DashboardWidgetType
id: str
title: str
colspan: DashboardColspan
# chart
model: str
groupby: str
measure: str
chart: Literal["bar", "line", "pie"]
domain: list
view: ViewRef
# table
fields: list[FieldRefLike]
columns: list[str]
limit: int
order: str
more_href: str
# stat
href: str
# link
subtitle: str
description: str
url: str
|
ArchDashboard
Bases: _ArchDashboardRequired
Arch for view_type="dashboard" views.
Source code in pyvelm/types.py
| class ArchDashboard(_ArchDashboardRequired, total=False):
"""Arch for ``view_type="dashboard"`` views."""
title: str
subtitle: str
columns: int
|
ListView
Bases: _ListViewRequired
A view_type="list" view declaration.
Source code in pyvelm/types.py
| class ListView(_ListViewRequired, total=False):
"""A ``view_type="list"`` view declaration."""
priority: int
|
Bases: _FormViewRequired
A view_type="form" view declaration.
Source code in pyvelm/types.py
| class FormView(_FormViewRequired, total=False):
"""A ``view_type="form"`` view declaration."""
priority: int
|
DetailView
Bases: _DetailViewRequired
A view_type="detail" read-only record view declaration.
Source code in pyvelm/types.py
| class DetailView(_DetailViewRequired, total=False):
"""A ``view_type="detail"`` read-only record view declaration."""
priority: int
|
KanbanView
Bases: _KanbanViewRequired
A view_type="kanban" view declaration.
Source code in pyvelm/types.py
| class KanbanView(_KanbanViewRequired, total=False):
"""A ``view_type="kanban"`` view declaration."""
priority: int
|
GraphView
Bases: _GraphViewRequired
A view_type="graph" view declaration.
Source code in pyvelm/types.py
| class GraphView(_GraphViewRequired, total=False):
"""A ``view_type="graph"`` view declaration."""
priority: int
|
PivotView
Bases: _PivotViewRequired
A view_type="pivot" view declaration.
Source code in pyvelm/types.py
| class PivotView(_PivotViewRequired, total=False):
"""A ``view_type="pivot"`` view declaration."""
priority: int
|
DashboardView
Bases: _DashboardViewRequired
A view_type="dashboard" view declaration.
Source code in pyvelm/types.py
| class DashboardView(_DashboardViewRequired, total=False):
"""A ``view_type="dashboard"`` view declaration."""
priority: int
|
Operation
Bases: _OperationRequired
One inheritance operation against a parent view's arch.
target is a list of segments. Slice C of Stage 7 widens the
accepted segment types:
- str – dict-key or list-by-name lookup (shorthand for
{"name": "<str>"} on list-of-dicts parents)
- int – positional index on a list parent
- dict – predicate; first list entry where every key/value
in the dict matches the entry's attributes
- "**" – wildcard prefix, only valid as the first segment;
finds any descendant where the next segment would
succeed and anchors the rest of the lookup there
value is required for every op except remove. The type
checker can't easily express "required for op != remove" so
value stays optional here; apply_operations raises at
install time when it's missing.
Source code in pyvelm/types.py
| class Operation(_OperationRequired, total=False):
"""One inheritance operation against a parent view's arch.
``target`` is a list of segments. Slice C of Stage 7 widens the
accepted segment types:
- ``str`` – dict-key or list-by-``name`` lookup (shorthand for
``{"name": "<str>"}`` on list-of-dicts parents)
- ``int`` – positional index on a list parent
- ``dict`` – predicate; first list entry where every key/value
in the dict matches the entry's attributes
- ``"**"`` – wildcard prefix, only valid as the first segment;
finds any descendant where the next segment would
succeed and anchors the rest of the lookup there
``value`` is required for every op except ``remove``. The type
checker can't easily express "required for op != remove" so
``value`` stays optional here; ``apply_operations`` raises at
install time when it's missing.
"""
value: Any
|
ViewInherit
Bases: _ViewInheritRequired
An extension view that patches another via operations.
Source code in pyvelm/types.py
| class ViewInherit(_ViewInheritRequired, total=False):
"""An extension view that patches another via ``operations``."""
priority: int
|
Bases: _MenuRequired
One entry in a module's MENUS list.
Top-level groups have an icon (Heroicons name like "home" or
legacy inline SVG) and no parent or
href. Leaf items have a parent ("<module>.<group_name>",
e.g. "partners.business" for group business in module
partners) and an href (typically /web/views/<module>/<view>).
Prefer :class:~pyvelm.builders.Menus so parent and href are
derived from short group names and view names. Nested trees use
:class:~pyvelm.builders.MenuBranch (m.group(...).children([...])).
Source code in pyvelm/types.py
| class Menu(_MenuRequired, total=False):
"""One entry in a module's ``MENUS`` list.
Top-level groups have an ``icon`` (Heroicons name like ``"home"`` or
legacy inline SVG) and no ``parent`` or
``href``. Leaf items have a ``parent`` (``"<module>.<group_name>"``,
e.g. ``"partners.business"`` for group ``business`` in module
``partners``) and an ``href`` (typically ``/web/views/<module>/<view>``).
Prefer :class:`~pyvelm.builders.Menus` so ``parent`` and ``href`` are
derived from short group names and view names. Nested trees use
:class:`~pyvelm.builders.MenuBranch` (``m.group(...).children([...])``).
"""
icon: str
href: str
parent: str # fully qualified: "<module>.<menu_name>"
sequence: int
access_model: str # gate visibility on this model …
access_perm: str # … with this perm (default "read")
access_policy: str # optional policy method name for recordless gating
|
Manifest
Bases: _ManifestRequired
Shape of a __pyvelm__.py manifest's module-level globals.
The loader reads these as individual attributes, so a manifest
declares them at module scope rather than building a dict named
Manifest. This TypedDict exists for tooling that wants to
validate a manifest as a single shape.
Only NAME and VERSION are strictly required by the loader;
everything else (the install hook, model package, dependencies,
catalog metadata) has sensible defaults.
Source code in pyvelm/types.py
| class Manifest(_ManifestRequired, total=False):
"""Shape of a ``__pyvelm__.py`` manifest's module-level globals.
The loader reads these as individual attributes, so a manifest
declares them at module scope rather than building a dict named
Manifest. This TypedDict exists for tooling that wants to
validate a manifest as a single shape.
Only ``NAME`` and ``VERSION`` are strictly required by the loader;
everything else (the install hook, model package, dependencies,
catalog metadata) has sensible defaults.
"""
DEPENDS: list[str]
DATA: list[str]
MODELS_PACKAGE: str
MIGRATIONS_PACKAGE: str
INSTALL_HOOK: str # dotted reference, e.g. "pkg.mod:fn"
SYNC_HOOK: str # runs on Apps Sync (installed, same version)
WEB_ROUTES: str # dotted ``register_routes(app)`` hook
COMMANDS: list[str] # dotted Command classes, e.g. "pkg.cmd:MyCommand"
# Apps catalog metadata — purely informational, drives /web/apps.
DISPLAY_NAME: str # human label; NAME is the technical id
SUMMARY: str
DESCRIPTION: str
CATEGORY: str
AUTHOR: str
ICON: str # raw inline SVG markup
# Optional Apps catalog visibility gate (UI only):
CATALOG_ACCESS_MODEL: str
CATALOG_ACCESS_PERM: str
CATALOG_ACCESS_POLICY: str
|