acUIty-core 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- acuity/__init__.py +81 -0
- acuity/__main__.py +19 -0
- acuity/app/__init__.py +5 -0
- acuity/app/datalets/__init__.py +4 -0
- acuity/app/datalets/_chat_container.py +3 -0
- acuity/app/datalets/_empty.py +4 -0
- acuity/commands.py +43 -0
- acuity/datalets/_Mock.py +15 -0
- acuity/datalets/__init__.py +0 -0
- acuity/datalets/cards/_CardGrid.py +20 -0
- acuity/datalets/cards/__init__.py +6 -0
- acuity/datalets/cards/_model.py +18 -0
- acuity/datalets/form/_Form.py +143 -0
- acuity/datalets/form/__init__.py +10 -0
- acuity/datalets/form/_markers.py +82 -0
- acuity/datalets/form/_model.py +10 -0
- acuity/model/__init__.py +6 -0
- acuity/model/_agen_resp_chunk.py +7 -0
- acuity/model/_app.py +104 -0
- acuity/model/_datalet.py +77 -0
- acuity/model/_lucide.py +1671 -0
- acuity/model/_page.py +68 -0
- acuity/model/_panel.py +24 -0
- acuity/runtime/__init__.py +14 -0
- acuity/runtime/_ai_router.py +84 -0
- acuity/runtime/_datalet_router.py +108 -0
- acuity/runtime/_engine.py +144 -0
- acuity/runtime/_stream_router.py +60 -0
- acuity/runtime/storage/__init__.py +4 -0
- acuity/runtime/storage/_client.py +132 -0
- acuity/runtime/storage/_model.py +54 -0
- acuity/runtime/stream/__init__.py +1 -0
- acuity/runtime/stream/_stream_handler.py +40 -0
- acuity/runtime/templates/index.html +10 -0
- acuity/scripts/build_web_static.py +15 -0
- acuity_core-0.2.0.dist-info/METADATA +41 -0
- acuity_core-0.2.0.dist-info/RECORD +39 -0
- acuity_core-0.2.0.dist-info/WHEEL +4 -0
- acuity_core-0.2.0.dist-info/entry_points.txt +3 -0
acuity/__init__.py
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
## acuity
|
|
2
|
+
import asyncio
|
|
3
|
+
import logging
|
|
4
|
+
import os
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
from acuity.model import DataletType
|
|
8
|
+
from acuity.app import app
|
|
9
|
+
from acuity.model._lucide import LucideIconName
|
|
10
|
+
from acuity.datalets.form._Form import _Form
|
|
11
|
+
from acuity.datalets.cards._CardGrid import _CardGrid
|
|
12
|
+
from acuity.datalets._Mock import _Mock
|
|
13
|
+
from acuity.runtime.stream._stream_handler import channel, Channel
|
|
14
|
+
|
|
15
|
+
logging.basicConfig(
|
|
16
|
+
level=getattr(logging, os.environ.get("ACUITY_LOG_LEVEL", "INFO").upper(), logging.INFO),
|
|
17
|
+
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def title(value: str):
|
|
23
|
+
app.title = value
|
|
24
|
+
|
|
25
|
+
def description(value: str):
|
|
26
|
+
app.description = value
|
|
27
|
+
|
|
28
|
+
def hideBreadcrumb():
|
|
29
|
+
app.breadcrumb_visible = False
|
|
30
|
+
|
|
31
|
+
def logo(image_path: str):
|
|
32
|
+
app.setup_logo(image_path)
|
|
33
|
+
|
|
34
|
+
def load_image(image_name: str, image_path: str):
|
|
35
|
+
app.load_image(image_name, image_path)
|
|
36
|
+
|
|
37
|
+
def create_page(menu_title: str, route_path: str, icon: Optional[LucideIconName] = "Box"):
|
|
38
|
+
return app.new_page(menu_title, route_path, icon=icon)
|
|
39
|
+
|
|
40
|
+
async def push_event(channel_name: str, data: dict):
|
|
41
|
+
ch = channel(channel_name)
|
|
42
|
+
await ch.push(data)
|
|
43
|
+
|
|
44
|
+
def push_event_sync(channel_name: str, data: dict):
|
|
45
|
+
loop = asyncio.get_event_loop()
|
|
46
|
+
asyncio.run_coroutine_threadsafe(push_event(channel_name, data), loop)
|
|
47
|
+
|
|
48
|
+
def create_datalet(datalet_type: DataletType):
|
|
49
|
+
return app.new_datalet(datalet_type)
|
|
50
|
+
|
|
51
|
+
def new_mock():
|
|
52
|
+
mock = _Mock()
|
|
53
|
+
app._add_datalet(mock)
|
|
54
|
+
return mock
|
|
55
|
+
|
|
56
|
+
def new_form(model=None, **kwargs):
|
|
57
|
+
"""Create a form datalet; `model` is a pydantic class with Annotated form field markers."""
|
|
58
|
+
form = _Form(model, **kwargs)
|
|
59
|
+
app._add_datalet(form)
|
|
60
|
+
return form
|
|
61
|
+
|
|
62
|
+
def new_card_grid(**kwargs):
|
|
63
|
+
"""Create a card grid datalet; the `@dl.data()` handler returns a list of `CardItem`."""
|
|
64
|
+
grid = _CardGrid(**kwargs)
|
|
65
|
+
app._add_datalet(grid)
|
|
66
|
+
return grid
|
|
67
|
+
|
|
68
|
+
__all__ = [
|
|
69
|
+
"title",
|
|
70
|
+
"description",
|
|
71
|
+
"hideBreadcrumb",
|
|
72
|
+
"logo",
|
|
73
|
+
"load_image",
|
|
74
|
+
"create_page",
|
|
75
|
+
"push_event_sync",
|
|
76
|
+
"create_datalet",
|
|
77
|
+
"new_form",
|
|
78
|
+
"new_card_grid",
|
|
79
|
+
"new_mock",
|
|
80
|
+
"DataletType"
|
|
81
|
+
]
|
acuity/__main__.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# __main__.py
|
|
2
|
+
import click
|
|
3
|
+
|
|
4
|
+
from . import commands
|
|
5
|
+
|
|
6
|
+
@click.group()
|
|
7
|
+
def cli():
|
|
8
|
+
pass
|
|
9
|
+
|
|
10
|
+
cli.add_command(commands.run)
|
|
11
|
+
cli.add_command(commands.watch)
|
|
12
|
+
cli.add_command(commands.add)
|
|
13
|
+
cli.add_command(commands.sub)
|
|
14
|
+
cli.add_command(commands.mul)
|
|
15
|
+
cli.add_command(commands.div)
|
|
16
|
+
|
|
17
|
+
# enables module to be run as a script
|
|
18
|
+
if __name__ == "__main__":
|
|
19
|
+
cli()
|
acuity/app/__init__.py
ADDED
acuity/commands.py
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# commands.py
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
from acuity import runtime
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@click.command(help="Run acuity App")
|
|
8
|
+
@click.option("--port", default=8000, help="Port to run the app on.")
|
|
9
|
+
@click.argument("file_path", type=click.Path(exists=True))
|
|
10
|
+
def run(file_path, port):
|
|
11
|
+
runtime.run(file_path, port=port)
|
|
12
|
+
pass
|
|
13
|
+
|
|
14
|
+
@click.command(help="Watch acuity App")
|
|
15
|
+
@click.option("--port", default=8000, help="Port to watch the app on.")
|
|
16
|
+
@click.argument("file_path", type=click.Path(exists=True))
|
|
17
|
+
def watch(file_path, port):
|
|
18
|
+
runtime.watch(file_path, port=port)
|
|
19
|
+
pass
|
|
20
|
+
|
|
21
|
+
@click.command(help="Add two numbers.")
|
|
22
|
+
@click.argument("a", type=click.FLOAT)
|
|
23
|
+
@click.argument("b", type=click.FLOAT)
|
|
24
|
+
def add(a, b):
|
|
25
|
+
click.echo(a + b)
|
|
26
|
+
|
|
27
|
+
@click.command(help="Subtract two numbers.")
|
|
28
|
+
@click.argument("a", type=click.FLOAT)
|
|
29
|
+
@click.argument("b", type=click.FLOAT)
|
|
30
|
+
def sub(a, b):
|
|
31
|
+
click.echo(a - b)
|
|
32
|
+
|
|
33
|
+
@click.command(help="Multiply two numbers.")
|
|
34
|
+
@click.argument("a", type=click.FLOAT)
|
|
35
|
+
@click.argument("b", type=click.FLOAT)
|
|
36
|
+
def mul(a, b):
|
|
37
|
+
click.echo(a * b)
|
|
38
|
+
|
|
39
|
+
@click.command(help="Divide two numbers.")
|
|
40
|
+
@click.argument("a", type=click.FLOAT)
|
|
41
|
+
@click.argument("b", type=click.FLOAT)
|
|
42
|
+
def div(a, b):
|
|
43
|
+
click.echo(a / b)
|
acuity/datalets/_Mock.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from typing import Dict, Any
|
|
2
|
+
|
|
3
|
+
from acuity.model._datalet import _Datalet, _DataletType
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class _Mock(_Datalet):
|
|
7
|
+
|
|
8
|
+
def __init__(self):
|
|
9
|
+
super().__init__(_DataletType.MOCK)
|
|
10
|
+
|
|
11
|
+
def metadata(self) -> Dict[str, Any]:
|
|
12
|
+
return {
|
|
13
|
+
"name": "Mock",
|
|
14
|
+
"description": "A mock datalet for testing purposes.",
|
|
15
|
+
}
|
|
File without changes
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from typing import Any, Dict, List
|
|
2
|
+
from acuity.model._datalet import _Datalet, _DataletType
|
|
3
|
+
from acuity.datalets.cards._model import _CardItem
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class _CardGrid(_Datalet[List[_CardItem]]):
|
|
7
|
+
"""Python binder of the CardGrid datalet; the data handler returns the list of cards."""
|
|
8
|
+
|
|
9
|
+
title: str = ""
|
|
10
|
+
|
|
11
|
+
def __init__(self, title: str = ""):
|
|
12
|
+
super().__init__(_DataletType.CARD_GRID)
|
|
13
|
+
self.title = title
|
|
14
|
+
|
|
15
|
+
def with_title(self, title: str) -> "_CardGrid":
|
|
16
|
+
self.title = title
|
|
17
|
+
return self
|
|
18
|
+
|
|
19
|
+
def metadata(self) -> Dict[str, Any]:
|
|
20
|
+
return {"title": self.title}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from typing import List, Optional, Union
|
|
2
|
+
from pydantic import BaseModel, Field
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class _CardKpi(BaseModel):
|
|
6
|
+
label: str
|
|
7
|
+
value: Union[str, int, float]
|
|
8
|
+
trend: Optional[float]
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class _CardItem(BaseModel):
|
|
12
|
+
"""Card shown by the CardGrid datalet (mirrors CardItem in CardsContainer.svelte)."""
|
|
13
|
+
id: Optional[Union[str, int]] = None
|
|
14
|
+
title: str
|
|
15
|
+
description: Optional[str] = None
|
|
16
|
+
image: Optional[str] = None
|
|
17
|
+
kpis: List[_CardKpi] = Field(default_factory=list)
|
|
18
|
+
tags: List[str] = Field(default_factory=list)
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
from typing import Any, Dict, List, Optional, Type
|
|
2
|
+
from pydantic import BaseModel, Field
|
|
3
|
+
from pydantic_core import PydanticUndefined
|
|
4
|
+
from acuity.model._datalet import _Datalet, _DataletType
|
|
5
|
+
from acuity.datalets.form._markers import FormField, Group
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class _Form(_Datalet[BaseModel]):
|
|
9
|
+
"""Datalet describing a form; `to_json()` yields the same structure as
|
|
10
|
+
web/stories/bricks/organisms/forms/metadata.ts (fluent/functional builder)."""
|
|
11
|
+
|
|
12
|
+
title: str = ""
|
|
13
|
+
abstract: str = ""
|
|
14
|
+
lock_unlock: bool = False
|
|
15
|
+
tabs: List[Any] = Field(default_factory=list)
|
|
16
|
+
defaults: Dict[str, Any] = Field(default_factory=dict)
|
|
17
|
+
validation_schema: Dict[str, Any] = Field(default_factory=dict)
|
|
18
|
+
groups: List[Dict[str, Any]] = Field(default_factory=list)
|
|
19
|
+
submit_label: Optional[str] = None
|
|
20
|
+
cancel_label: Optional[str] = None
|
|
21
|
+
|
|
22
|
+
def __init__(self, model: Optional[Type[BaseModel]] = None, title: str = "", abstract: str = "",
|
|
23
|
+
lock_unlock: bool = False):
|
|
24
|
+
"""Create a form, optionally from a pydantic class whose fields are annotated with
|
|
25
|
+
`acuity.datalets._form` markers, e.g. `domain: Annotated[str, Select(group="target")]`.
|
|
26
|
+
Fields without a marker are ignored; field defaults become form defaults."""
|
|
27
|
+
super().__init__(_DataletType.FORM)
|
|
28
|
+
self.title = title
|
|
29
|
+
self.abstract = abstract
|
|
30
|
+
self.lock_unlock = lock_unlock
|
|
31
|
+
if model is not None:
|
|
32
|
+
self.from_model(model)
|
|
33
|
+
|
|
34
|
+
def from_model(self, model: Type[BaseModel]) -> "_Form":
|
|
35
|
+
for name, info in model.model_fields.items():
|
|
36
|
+
group = next((m for m in info.metadata if isinstance(m, Group)), None)
|
|
37
|
+
gtype = info.annotation
|
|
38
|
+
if group is not None and isinstance(gtype, type) and issubclass(gtype, BaseModel):
|
|
39
|
+
gname = group.name or name
|
|
40
|
+
self.add_group(gname, group.title, group.description)
|
|
41
|
+
self._from_fields(gtype, gname)
|
|
42
|
+
self._from_fields(model, None)
|
|
43
|
+
return self
|
|
44
|
+
|
|
45
|
+
def _from_fields(self, model: Type[BaseModel], group: Optional[str]) -> None:
|
|
46
|
+
for name, info in model.model_fields.items():
|
|
47
|
+
marker = next((m for m in info.metadata if isinstance(m, FormField)), None)
|
|
48
|
+
if marker is None:
|
|
49
|
+
continue
|
|
50
|
+
gname = group if group is not None else marker.group
|
|
51
|
+
self.add_field(
|
|
52
|
+
gname, name, type=marker.type, label=marker.label, placeholder=marker.placeholder,
|
|
53
|
+
cols=marker.cols, options=getattr(marker, "options", None), attributes=marker.attributes,
|
|
54
|
+
remote=marker.remote, depends_on=marker.depends_on, params=marker.params)
|
|
55
|
+
if info.default is not PydanticUndefined and info.default is not None:
|
|
56
|
+
self.with_defaults(gname, {name: info.default})
|
|
57
|
+
|
|
58
|
+
def with_title(self, title: str) -> "_Form":
|
|
59
|
+
self.title = title
|
|
60
|
+
return self
|
|
61
|
+
|
|
62
|
+
def with_abstract(self, abstract: str) -> "_Form":
|
|
63
|
+
self.abstract = abstract
|
|
64
|
+
return self
|
|
65
|
+
|
|
66
|
+
def with_lock_unlock(self, value: bool = True) -> "_Form":
|
|
67
|
+
self.lock_unlock = value
|
|
68
|
+
return self
|
|
69
|
+
|
|
70
|
+
def withSubmitLabel(self, label: str) -> "_Form":
|
|
71
|
+
self.submit_label = label
|
|
72
|
+
return self
|
|
73
|
+
|
|
74
|
+
def withCancelLabel(self, label: str) -> "_Form":
|
|
75
|
+
self.cancel_label = label
|
|
76
|
+
return self
|
|
77
|
+
|
|
78
|
+
def with_defaults(self, group: str, values: Dict[str, Any]) -> "_Form":
|
|
79
|
+
self.defaults.setdefault(group, {}).update(values)
|
|
80
|
+
return self
|
|
81
|
+
|
|
82
|
+
def with_validation_schema(self, schema: Dict[str, Any]) -> "_Form":
|
|
83
|
+
self.validation_schema = schema
|
|
84
|
+
return self
|
|
85
|
+
|
|
86
|
+
def add_group(self, name: str, title: Optional[str] = None, description: str = "",
|
|
87
|
+
fields: Optional[List[Dict[str, Any]]] = None) -> "_Form":
|
|
88
|
+
self.groups.append({
|
|
89
|
+
"name": name,
|
|
90
|
+
"title": title if title is not None else name,
|
|
91
|
+
"description": description,
|
|
92
|
+
"fields": list(fields or []),
|
|
93
|
+
})
|
|
94
|
+
return self
|
|
95
|
+
|
|
96
|
+
def add_field(self, group: str, name: str, type: str = "text", label: Optional[str] = None,
|
|
97
|
+
placeholder: str = "", cols: int = 2, options: Optional[List[Dict[str, Any]]] = None,
|
|
98
|
+
attributes: Optional[Dict[str, Any]] = None, remote: Optional[bool] = None,
|
|
99
|
+
depends_on: Optional[List[str]] = None, params: Optional[List[str]] = None) -> "_Form":
|
|
100
|
+
field: Dict[str, Any] = {
|
|
101
|
+
"name": name,
|
|
102
|
+
"type": type,
|
|
103
|
+
"label": label if label is not None else name,
|
|
104
|
+
"placeholder": placeholder,
|
|
105
|
+
"cols": cols,
|
|
106
|
+
}
|
|
107
|
+
if options is not None:
|
|
108
|
+
field["options"] = options
|
|
109
|
+
if attributes is not None:
|
|
110
|
+
field["attributes"] = attributes
|
|
111
|
+
if remote is not None:
|
|
112
|
+
field["remote"] = remote
|
|
113
|
+
if depends_on is not None:
|
|
114
|
+
field["dependsOn"] = depends_on
|
|
115
|
+
if params is not None:
|
|
116
|
+
field["params"] = params
|
|
117
|
+
|
|
118
|
+
target = next((g for g in self.groups if g["name"] == group), None)
|
|
119
|
+
if target is None:
|
|
120
|
+
self.add_group(group)
|
|
121
|
+
target = self.groups[-1]
|
|
122
|
+
target["fields"].append(field)
|
|
123
|
+
return self
|
|
124
|
+
|
|
125
|
+
def metadata(self) -> Dict[str, Any]:
|
|
126
|
+
"""Return the form metadata structure expected by the web Form brick."""
|
|
127
|
+
meta: Dict[str, Any] = {
|
|
128
|
+
"title": self.title,
|
|
129
|
+
"abstract": self.abstract,
|
|
130
|
+
"lockUnlock": self.lock_unlock,
|
|
131
|
+
"tabs": list(self.tabs),
|
|
132
|
+
"validationSchema": self.validation_schema,
|
|
133
|
+
"groups": self.groups,
|
|
134
|
+
}
|
|
135
|
+
actions: Dict[str, Any] = {}
|
|
136
|
+
if self.submit_label is not None:
|
|
137
|
+
actions["submitLabel"] = self.submit_label
|
|
138
|
+
if self.cancel_label is not None:
|
|
139
|
+
actions["cancelLabel"] = self.cancel_label
|
|
140
|
+
if actions:
|
|
141
|
+
meta["actions"] = actions
|
|
142
|
+
return meta
|
|
143
|
+
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
from acuity.datalets.form._markers import (
|
|
2
|
+
Group, FormField, Text, Email, Secret, Number, Date, DateTime, TextArea, Select, Combo, MultiCombo,
|
|
3
|
+
)
|
|
4
|
+
|
|
5
|
+
from acuity.datalets.form._model import (
|
|
6
|
+
_FormItemOption as FormItemOption,
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
__all__ = ["Group", "FormField", "Text", "Email", "Secret", "Number", "Date", "DateTime",
|
|
10
|
+
"TextArea", "Select", "Combo", "MultiCombo", "FormItemOption"]
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
from dataclasses import dataclass, field
|
|
2
|
+
from typing import Any, ClassVar, Dict, List, Optional
|
|
3
|
+
|
|
4
|
+
from acuity.datalets.form._model import _FormItemOption
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass(frozen=True)
|
|
8
|
+
class Group:
|
|
9
|
+
"""`Annotated` marker declaring a form group. The annotated type is a pydantic class whose
|
|
10
|
+
fields carry `FormField` markers, e.g. `user: Annotated[User, Group('user', 'User', 'who')]`.
|
|
11
|
+
`name` defaults to the attribute name and `title` to the name."""
|
|
12
|
+
name: Optional[str] = None
|
|
13
|
+
title: Optional[str] = None
|
|
14
|
+
description: str = ""
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass(frozen=True)
|
|
18
|
+
class FormField:
|
|
19
|
+
"""Base `Annotated` marker describing a form field (see metadata.ts)."""
|
|
20
|
+
type: ClassVar[str] = "text"
|
|
21
|
+
label: Optional[str] = None
|
|
22
|
+
placeholder: str = ""
|
|
23
|
+
cols: int = 2
|
|
24
|
+
attributes: Optional[Dict[str, Any]] = None
|
|
25
|
+
remote: Optional[bool] = None
|
|
26
|
+
depends_on: Optional[List[str]] = None
|
|
27
|
+
params: Optional[List[str]] = None
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass(frozen=True)
|
|
31
|
+
class Text(FormField):
|
|
32
|
+
type: ClassVar[str] = "text"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass(frozen=True)
|
|
36
|
+
class Email(FormField):
|
|
37
|
+
type: ClassVar[str] = "email"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass(frozen=True)
|
|
41
|
+
class Secret(FormField):
|
|
42
|
+
type: ClassVar[str] = "secret"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass(frozen=True)
|
|
46
|
+
class Number(FormField):
|
|
47
|
+
type: ClassVar[str] = "number"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@dataclass(frozen=True)
|
|
51
|
+
class Date(FormField):
|
|
52
|
+
type: ClassVar[str] = "date"
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@dataclass(frozen=True)
|
|
56
|
+
class DateTime(FormField):
|
|
57
|
+
type: ClassVar[str] = "datetime"
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@dataclass(frozen=True)
|
|
61
|
+
class TextArea(FormField):
|
|
62
|
+
type: ClassVar[str] = "textarea"
|
|
63
|
+
cols: int = 4
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@dataclass(frozen=True)
|
|
67
|
+
class Select(FormField):
|
|
68
|
+
type: ClassVar[str] = "select"
|
|
69
|
+
options: Optional[List[_FormItemOption]] = None
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@dataclass(frozen=True)
|
|
73
|
+
class Combo(Select):
|
|
74
|
+
type: ClassVar[str] = "combo"
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@dataclass(frozen=True)
|
|
78
|
+
class MultiCombo(Select):
|
|
79
|
+
type: ClassVar[str] = "multicombo"
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
FIELD_MARKERS = (Text, Email, Secret, Number, Date, DateTime, TextArea, Select, Combo, MultiCombo)
|
acuity/model/__init__.py
ADDED
acuity/model/_app.py
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
## acuity app properties modifiers
|
|
2
|
+
from enum import Enum
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel, PrivateAttr, computed_field
|
|
6
|
+
from acuity.model._lucide import LucideIconName
|
|
7
|
+
from acuity.model._datalet import _Datalet, _DataletType
|
|
8
|
+
from acuity.model._page import _Page
|
|
9
|
+
import uuid
|
|
10
|
+
|
|
11
|
+
class _ImageType(Enum):
|
|
12
|
+
LOGO = "logo"
|
|
13
|
+
FAVICON = "favicon"
|
|
14
|
+
BANNER = "banner"
|
|
15
|
+
GENERIC = "generic"
|
|
16
|
+
|
|
17
|
+
class _App(BaseModel):
|
|
18
|
+
"""App class represents the main application configuration and properties.
|
|
19
|
+
Handles core application settings and validates their values.
|
|
20
|
+
"""
|
|
21
|
+
title: str = "acUIty App"
|
|
22
|
+
description: str = "lorem ipsum"
|
|
23
|
+
breadcrumb_visible: bool = True
|
|
24
|
+
|
|
25
|
+
_datalets: dict[str, _Datalet] = {}
|
|
26
|
+
_pages: dict[str, _Page] = {}
|
|
27
|
+
_images: dict[_ImageType, Any] = {
|
|
28
|
+
_ImageType.LOGO: None,
|
|
29
|
+
_ImageType.FAVICON: None,
|
|
30
|
+
_ImageType.BANNER: None,
|
|
31
|
+
_ImageType.GENERIC: {}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
def datalet_by_id(self, datalet_id: str):
|
|
35
|
+
return self._datalets[datalet_id]
|
|
36
|
+
|
|
37
|
+
@computed_field
|
|
38
|
+
@property
|
|
39
|
+
def datalets(self) -> list[_Datalet]:
|
|
40
|
+
"""Returns the list of datalets from the internal datalets dictionary."""
|
|
41
|
+
return [datalet.model_dump() for datalet in self._datalets.values()]
|
|
42
|
+
|
|
43
|
+
@computed_field
|
|
44
|
+
@property
|
|
45
|
+
def pages(self) -> list[_Page]:
|
|
46
|
+
"""Returns the list of pages from the internal pages dictionary."""
|
|
47
|
+
return [page.model_dump(exclude={'panels'}) for page in self._pages.values()]
|
|
48
|
+
|
|
49
|
+
@computed_field
|
|
50
|
+
@property
|
|
51
|
+
def images(self) -> dict[str, str | dict[str, str]]:
|
|
52
|
+
"""Returns the icons dictionary with transformed image paths."""
|
|
53
|
+
result = {}
|
|
54
|
+
for key, value in self._images.items():
|
|
55
|
+
if key == _ImageType.LOGO:
|
|
56
|
+
result["logo"] = "/image/logo"
|
|
57
|
+
elif key == _ImageType.FAVICON:
|
|
58
|
+
result["favicon"] = "/image/favicon"
|
|
59
|
+
elif key == _ImageType.BANNER:
|
|
60
|
+
result["banner"] = "/image/banner"
|
|
61
|
+
elif key == _ImageType.GENERIC:
|
|
62
|
+
result["generic"] = {
|
|
63
|
+
image_name: f"/image/generic/{image_name}"
|
|
64
|
+
for image_name in value.keys()
|
|
65
|
+
}
|
|
66
|
+
return result
|
|
67
|
+
|
|
68
|
+
def new_page(self, title: str, route_path: str, icon: LucideIconName) -> _Page:
|
|
69
|
+
"""Create a new page with the given name and return it."""
|
|
70
|
+
|
|
71
|
+
page_id = f"page_{uuid.uuid4().hex[:8]}"
|
|
72
|
+
|
|
73
|
+
page = _Page(page_id=page_id, title=title, route_path=route_path, icon=icon)
|
|
74
|
+
self._pages[page_id] = page
|
|
75
|
+
return page
|
|
76
|
+
|
|
77
|
+
def _add_datalet(self, datalet: _Datalet) -> _Datalet:
|
|
78
|
+
datalet_id = datalet.id
|
|
79
|
+
self._datalets[datalet_id] = datalet
|
|
80
|
+
return datalet
|
|
81
|
+
|
|
82
|
+
def new_datalet(self, datalet_type: _DataletType) -> _Datalet:
|
|
83
|
+
"""Create a new datalet with the given type."""
|
|
84
|
+
|
|
85
|
+
datalet_id = f"datalet_{uuid.uuid4().hex[:8]}"
|
|
86
|
+
|
|
87
|
+
datalet = _Datalet(datalet_id=datalet_id, datalet_type=datalet_type)
|
|
88
|
+
self._datalets[datalet_id] = datalet
|
|
89
|
+
return datalet
|
|
90
|
+
|
|
91
|
+
def load_image(self, image_name: str, image_path: str):
|
|
92
|
+
self._images[_ImageType.GENERIC][image_name] = image_path
|
|
93
|
+
|
|
94
|
+
def setup_logo(self, image_path):
|
|
95
|
+
self._images[_ImageType.LOGO] = image_path
|
|
96
|
+
|
|
97
|
+
def image_registry(self) -> dict[str, str]:
|
|
98
|
+
return self._images[_ImageType.GENERIC]
|
|
99
|
+
|
|
100
|
+
def image_path(self, image_type: _ImageType) -> str:
|
|
101
|
+
if image_type == _ImageType.GENERIC:
|
|
102
|
+
raise ValueError(f"a single image path by image type {image_type} cannot be retrieved")
|
|
103
|
+
return self._images[image_type]
|
|
104
|
+
|
acuity/model/_datalet.py
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import uuid
|
|
2
|
+
from enum import Enum
|
|
3
|
+
from typing import Optional, Callable, Dict, Any, Generic, TypeVar
|
|
4
|
+
from abc import ABC, abstractmethod
|
|
5
|
+
import json
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel, PrivateAttr
|
|
8
|
+
|
|
9
|
+
T = TypeVar("T", bound=BaseModel)
|
|
10
|
+
|
|
11
|
+
class _DataletType(Enum):
|
|
12
|
+
MOCK = "Mock"
|
|
13
|
+
CHAT_CONTAINER = "ChatContainer"
|
|
14
|
+
FORM = "Form"
|
|
15
|
+
CARD_GRID = "CardGrid"
|
|
16
|
+
|
|
17
|
+
class _Datalet(BaseModel, ABC, Generic[T]):
|
|
18
|
+
"""Base datalet. Subclasses parametrize it with the data model type,
|
|
19
|
+
e.g. `class _Chart(_Datalet[ChartData])`."""
|
|
20
|
+
id: str
|
|
21
|
+
datalet_type: _DataletType
|
|
22
|
+
_data_handler: Optional[Callable[..., T]] = PrivateAttr(default=None)
|
|
23
|
+
_item_data_handlers: Dict[str, Callable[..., Any]] = PrivateAttr(default_factory=dict)
|
|
24
|
+
|
|
25
|
+
def __init__(self, datalet_type: _DataletType):
|
|
26
|
+
"""Initialize a new Datalet instance.
|
|
27
|
+
"""
|
|
28
|
+
datalet_id = f"datalet_{uuid.uuid4().hex[:8]}"
|
|
29
|
+
super().__init__(id=datalet_id, datalet_type=datalet_type)
|
|
30
|
+
|
|
31
|
+
@abstractmethod
|
|
32
|
+
def metadata(self) -> Dict[str, Any]:
|
|
33
|
+
return {}
|
|
34
|
+
|
|
35
|
+
def metadata_json(self, indent: int = 2) -> str:
|
|
36
|
+
return json.dumps(self.metadata(), indent=indent)
|
|
37
|
+
|
|
38
|
+
def data(self) -> Callable[[Callable[..., T]], Callable[..., T]]:
|
|
39
|
+
"""Decorator factory registering the decorated function as the data fetch
|
|
40
|
+
handler of this datalet. The function must return the datalet's data model `T`.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
def decorator(func: Callable[..., T]) -> Callable[..., T]:
|
|
44
|
+
self._data_handler = func
|
|
45
|
+
return func
|
|
46
|
+
|
|
47
|
+
return decorator
|
|
48
|
+
|
|
49
|
+
def item_data(self, field: str) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
|
|
50
|
+
"""Decorator factory registering the decorated function as the data item
|
|
51
|
+
handler of the field identified as `<group>.<name>`.
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
|
|
55
|
+
self._item_data_handlers[field] = func
|
|
56
|
+
return func
|
|
57
|
+
|
|
58
|
+
return decorator
|
|
59
|
+
|
|
60
|
+
def fetch_item_data(self, field: str) -> Callable[..., Any]:
|
|
61
|
+
"""Return the data item handler registered for the field identified as
|
|
62
|
+
`<group>.<name>`."""
|
|
63
|
+
handler = self._item_data_handlers.get(field)
|
|
64
|
+
if handler is None:
|
|
65
|
+
raise RuntimeError(f"No item data handler registered for {field} in {self.id}")
|
|
66
|
+
return handler
|
|
67
|
+
|
|
68
|
+
def fetch_data(self, *args: Any, **kwargs: Any) -> T:
|
|
69
|
+
"""Invoke the registered data handler."""
|
|
70
|
+
if self._data_handler is None:
|
|
71
|
+
raise RuntimeError(f"No data handler registered for {self.id}")
|
|
72
|
+
return self._data_handler(*args, **kwargs)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
|