nicegui-rdm 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- nicegui_rdm-0.1.0/.gitignore +54 -0
- nicegui_rdm-0.1.0/LICENSE +21 -0
- nicegui_rdm-0.1.0/PKG-INFO +170 -0
- nicegui_rdm-0.1.0/README.md +143 -0
- nicegui_rdm-0.1.0/check_styles.py +196 -0
- nicegui_rdm-0.1.0/keyboard_output.log +6 -0
- nicegui_rdm-0.1.0/pyproject.toml +60 -0
- nicegui_rdm-0.1.0/requirements-test.txt +4 -0
- nicegui_rdm-0.1.0/requirements.txt +3 -0
- nicegui_rdm-0.1.0/src/ng_rdm/__init__.py +37 -0
- nicegui_rdm-0.1.0/src/ng_rdm/components/API.md +593 -0
- nicegui_rdm-0.1.0/src/ng_rdm/components/__init__.py +138 -0
- nicegui_rdm-0.1.0/src/ng_rdm/components/base.py +431 -0
- nicegui_rdm-0.1.0/src/ng_rdm/components/fields.py +51 -0
- nicegui_rdm-0.1.0/src/ng_rdm/components/i18n.py +64 -0
- nicegui_rdm-0.1.0/src/ng_rdm/components/ng_rdm.css +1047 -0
- nicegui_rdm-0.1.0/src/ng_rdm/components/protocol.py +65 -0
- nicegui_rdm-0.1.0/src/ng_rdm/components/widgets/__init__.py +35 -0
- nicegui_rdm-0.1.0/src/ng_rdm/components/widgets/action_button_table.py +107 -0
- nicegui_rdm-0.1.0/src/ng_rdm/components/widgets/button.py +72 -0
- nicegui_rdm-0.1.0/src/ng_rdm/components/widgets/detail_card.py +91 -0
- nicegui_rdm-0.1.0/src/ng_rdm/components/widgets/dialog.py +145 -0
- nicegui_rdm-0.1.0/src/ng_rdm/components/widgets/edit_card.py +82 -0
- nicegui_rdm-0.1.0/src/ng_rdm/components/widgets/edit_dialog.py +115 -0
- nicegui_rdm-0.1.0/src/ng_rdm/components/widgets/layout.py +106 -0
- nicegui_rdm-0.1.0/src/ng_rdm/components/widgets/list_table.py +96 -0
- nicegui_rdm-0.1.0/src/ng_rdm/components/widgets/selection_table.py +153 -0
- nicegui_rdm-0.1.0/src/ng_rdm/components/widgets/tabs.py +53 -0
- nicegui_rdm-0.1.0/src/ng_rdm/components/widgets/view_stack.py +113 -0
- nicegui_rdm-0.1.0/src/ng_rdm/components/widgets/wizard.py +156 -0
- nicegui_rdm-0.1.0/src/ng_rdm/debug/__init__.py +12 -0
- nicegui_rdm-0.1.0/src/ng_rdm/debug/event_log.py +128 -0
- nicegui_rdm-0.1.0/src/ng_rdm/debug/page.py +175 -0
- nicegui_rdm-0.1.0/src/ng_rdm/examples/README.md +34 -0
- nicegui_rdm-0.1.0/src/ng_rdm/examples/catalog.py +645 -0
- nicegui_rdm-0.1.0/src/ng_rdm/examples/custom_datasource.py +152 -0
- nicegui_rdm-0.1.0/src/ng_rdm/examples/examples.css +60 -0
- nicegui_rdm-0.1.0/src/ng_rdm/examples/master_detail.py +272 -0
- nicegui_rdm-0.1.0/src/ng_rdm/examples/topic_filtering.py +201 -0
- nicegui_rdm-0.1.0/src/ng_rdm/examples/vanilla_store.py +166 -0
- nicegui_rdm-0.1.0/src/ng_rdm/models/__init__.py +8 -0
- nicegui_rdm-0.1.0/src/ng_rdm/models/qmodel.py +127 -0
- nicegui_rdm-0.1.0/src/ng_rdm/models/types.py +12 -0
- nicegui_rdm-0.1.0/src/ng_rdm/page.py +15 -0
- nicegui_rdm-0.1.0/src/ng_rdm/store/__init__.py +25 -0
- nicegui_rdm-0.1.0/src/ng_rdm/store/base.py +257 -0
- nicegui_rdm-0.1.0/src/ng_rdm/store/dict_store.py +70 -0
- nicegui_rdm-0.1.0/src/ng_rdm/store/multitenancy.py +71 -0
- nicegui_rdm-0.1.0/src/ng_rdm/store/notifier.py +201 -0
- nicegui_rdm-0.1.0/src/ng_rdm/store/orm.py +150 -0
- nicegui_rdm-0.1.0/src/ng_rdm/utils/__init__.py +7 -0
- nicegui_rdm-0.1.0/src/ng_rdm/utils/helpers.py +217 -0
- nicegui_rdm-0.1.0/src/ng_rdm/utils/logging.py +85 -0
- nicegui_rdm-0.1.0/tests/__init__.py +0 -0
- nicegui_rdm-0.1.0/tests/components/__init__.py +1 -0
- nicegui_rdm-0.1.0/tests/components/conftest.py +89 -0
- nicegui_rdm-0.1.0/tests/components/test_base.py +143 -0
- nicegui_rdm-0.1.0/tests/components/test_detail_card.py +128 -0
- nicegui_rdm-0.1.0/tests/components/test_dialog.py +72 -0
- nicegui_rdm-0.1.0/tests/components/test_edit.py +258 -0
- nicegui_rdm-0.1.0/tests/components/test_i18n.py +90 -0
- nicegui_rdm-0.1.0/tests/components/test_layout.py +145 -0
- nicegui_rdm-0.1.0/tests/components/test_navigation.py +213 -0
- nicegui_rdm-0.1.0/tests/components/test_poc.py +293 -0
- nicegui_rdm-0.1.0/tests/components/test_tables.py +460 -0
- nicegui_rdm-0.1.0/tests/components/test_wizard.py +126 -0
- nicegui_rdm-0.1.0/tests/conftest.py +100 -0
- nicegui_rdm-0.1.0/tests/test_multitenancy.py +109 -0
- nicegui_rdm-0.1.0/tests/test_notifier.py +322 -0
- nicegui_rdm-0.1.0/tests/test_orm.py +287 -0
- nicegui_rdm-0.1.0/tests/test_store.py +315 -0
- nicegui_rdm-0.1.0/tests/test_utils.py +214 -0
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
*.so
|
|
6
|
+
.Python
|
|
7
|
+
build/
|
|
8
|
+
develop-eggs/
|
|
9
|
+
dist/
|
|
10
|
+
downloads/
|
|
11
|
+
eggs/
|
|
12
|
+
.eggs/
|
|
13
|
+
lib/
|
|
14
|
+
lib64/
|
|
15
|
+
parts/
|
|
16
|
+
sdist/
|
|
17
|
+
var/
|
|
18
|
+
wheels/
|
|
19
|
+
*.egg-info/
|
|
20
|
+
.installed.cfg
|
|
21
|
+
*.egg
|
|
22
|
+
.nicegui/
|
|
23
|
+
|
|
24
|
+
# Virtual environments
|
|
25
|
+
venv/
|
|
26
|
+
env/
|
|
27
|
+
ENV/
|
|
28
|
+
|
|
29
|
+
# AI assistant working files
|
|
30
|
+
cline_docs/
|
|
31
|
+
.clinerules
|
|
32
|
+
CLAUDE.md
|
|
33
|
+
.claude/
|
|
34
|
+
|
|
35
|
+
# IDE files
|
|
36
|
+
.idea/
|
|
37
|
+
.vscode/
|
|
38
|
+
*.swp
|
|
39
|
+
*.swo
|
|
40
|
+
|
|
41
|
+
# OS specific files
|
|
42
|
+
.DS_Store
|
|
43
|
+
Thumbs.db
|
|
44
|
+
|
|
45
|
+
# Pytest
|
|
46
|
+
.pytest_cache/
|
|
47
|
+
.coverage
|
|
48
|
+
htmlcov/
|
|
49
|
+
|
|
50
|
+
# database files
|
|
51
|
+
*.db
|
|
52
|
+
*.sqlite3
|
|
53
|
+
*.sqlite3-shm
|
|
54
|
+
*.sqlite3-wal
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Peter Kleynjan
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: nicegui-rdm
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Reactive Data Management for NiceGUI with Tortoise ORM
|
|
5
|
+
Project-URL: Homepage, https://github.com/kleynjan/nicegui-rdm
|
|
6
|
+
Project-URL: Repository, https://github.com/kleynjan/nicegui-rdm
|
|
7
|
+
Author: Peter Kleynjan
|
|
8
|
+
License: MIT
|
|
9
|
+
Keywords: crud,data-management,nicegui,reactive,tortoise-orm
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
17
|
+
Requires-Python: >=3.12
|
|
18
|
+
Requires-Dist: nicegui<4.0,>=3.0
|
|
19
|
+
Requires-Dist: pytz
|
|
20
|
+
Requires-Dist: tortoise-orm<2.0.0,>=1.0.0
|
|
21
|
+
Provides-Extra: dev
|
|
22
|
+
Requires-Dist: httpx; extra == 'dev'
|
|
23
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
24
|
+
Requires-Dist: pytest-cov>=5.0; extra == 'dev'
|
|
25
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
|
|
28
|
+
# nicegui-rdm: Reactive Data Management
|
|
29
|
+
|
|
30
|
+
## Why: in a nutshell
|
|
31
|
+
|
|
32
|
+
ng_rdm offers a clean and modern set of (non-Quasar) tables with all the plumbing you need to build database-backed NiceGUI applications. Moreover, these tables can automatically refresh when back-end data is added or modified. Hence: **reactive** data management.
|
|
33
|
+
|
|
34
|
+
## Background (feel free to skip)
|
|
35
|
+
|
|
36
|
+
ng_rdm is based on two ideas:
|
|
37
|
+
|
|
38
|
+
1. For my own apps I needed to add **reactivity to database applications**: changes in data should be reflected in UI components, *without* the user having to refresh a page. Imagine a table showing items, counts, stock being updated in near real-time as data is changing. This is the core of the library, implemented in `models` and `store`. Note: this is similar to but *complementary* to the reactivity we can easily achieve ‘client-side’ with NiceGUI bindings etc.
|
|
39
|
+
|
|
40
|
+
2. Secondly, I've always been fighting Quasar's **“composite” UI components** such as tables, dialogs, cards, etc.: layer upon layer of div's and the most obnoxious CSS imaginable. Thanks to NiceGUI's websocket architecture we can move the logic for and behavior of those components from JavaScript/VueJS over to the Python side. In `components/widgets` you'll find tables that create clean html with semantic CSS selectors and that tie in to `store` observability – entirely in Python.
|
|
41
|
+
|
|
42
|
+
See below for a more detailed overview of the architecture.
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
## Installation
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
pip install nicegui-rdm
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Architecture
|
|
52
|
+
|
|
53
|
+
```
|
|
54
|
+
┌──────────────────────────────────────────────────────────┐
|
|
55
|
+
│ UI Components │
|
|
56
|
+
│ ActionButtonTable · ListTable · SelectionTable │
|
|
57
|
+
│ EditDialog · EditCard · DetailCard · ViewStack │
|
|
58
|
+
└──────────────┬─────────────────────────────────┬─────────┘
|
|
59
|
+
│ 1. user action ▲
|
|
60
|
+
▼ │ 6. notify_observers
|
|
61
|
+
┌──────────────┴─────────────────────────────────┴─────────┐
|
|
62
|
+
│ Store Layer │
|
|
63
|
+
│ Store (base) · DictStore · TortoiseStore │
|
|
64
|
+
│ MultitenantTortoiseStore · StoreRegistry │
|
|
65
|
+
│ CRUD · validation · observer pattern │
|
|
66
|
+
└──────────────┬─────────────────────────────────┬─────────┘
|
|
67
|
+
│ 2. validate & write ▲
|
|
68
|
+
▼ │ 5. return result
|
|
69
|
+
┌──────────────┴─────────────────────────────────┴─────────┐
|
|
70
|
+
│ Data Layer │
|
|
71
|
+
│ Tortoise ORM · QModel │
|
|
72
|
+
│ SQLite · PostgreSQL · MySQL │
|
|
73
|
+
└──────────────────────────────────────────────────────────┘
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
User actions flow **down** through the Store layer (which validates and normalizes) to the database. On success, the Store broadcasts a `StoreEvent` **up** to all subscribed UI components, which automatically rebuild via `@ui.refreshable_method`. This is the reactive loop that keeps tables and detail views in sync with the database without manual refresh.
|
|
77
|
+
|
|
78
|
+
## Quick Start
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
from nicegui import app, ui
|
|
82
|
+
from tortoise import fields
|
|
83
|
+
|
|
84
|
+
from ng_rdm import TortoiseStore, init_db, close_db, FieldSpec, Validator
|
|
85
|
+
from ng_rdm.models import QModel
|
|
86
|
+
from ng_rdm.components import (
|
|
87
|
+
rdm_init, Column, TableConfig, FormConfig,
|
|
88
|
+
ActionButtonTable, EditDialog,
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
# 1. Define a model with validation
|
|
92
|
+
class Task(QModel):
|
|
93
|
+
id = fields.IntField(pk=True)
|
|
94
|
+
name = fields.CharField(max_length=100)
|
|
95
|
+
|
|
96
|
+
field_specs = {
|
|
97
|
+
"name": FieldSpec(validators=[
|
|
98
|
+
Validator("Name is required", lambda v, _: bool(v and v.strip()))
|
|
99
|
+
])
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
# 2. Initialize database and create a store (module level)
|
|
103
|
+
init_db(app, "sqlite://tasks.db", modules={"models": [__name__]}, generate_schemas=True)
|
|
104
|
+
app.on_shutdown(close_db)
|
|
105
|
+
|
|
106
|
+
task_store = TortoiseStore(Task)
|
|
107
|
+
|
|
108
|
+
# 3. Build a page
|
|
109
|
+
@ui.page("/")
|
|
110
|
+
async def index():
|
|
111
|
+
rdm_init() # load CSS + Bootstrap Icons
|
|
112
|
+
|
|
113
|
+
columns = [Column("name", "Task name")]
|
|
114
|
+
table_config = TableConfig(columns=columns)
|
|
115
|
+
form_config = FormConfig(columns=columns, title_add="New Task", title_edit="Edit Task")
|
|
116
|
+
|
|
117
|
+
# EditDialog for add/edit; ActionButtonTable for display
|
|
118
|
+
dlg = EditDialog(data_source=task_store, config=form_config,
|
|
119
|
+
on_saved=lambda _: table.build.refresh())
|
|
120
|
+
table = ActionButtonTable(
|
|
121
|
+
data_source=task_store, config=table_config,
|
|
122
|
+
on_add=dlg.open_for_new, on_edit=dlg.open_for_edit,
|
|
123
|
+
)
|
|
124
|
+
await table.build()
|
|
125
|
+
|
|
126
|
+
ui.run()
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
## What's Included
|
|
130
|
+
|
|
131
|
+
**Tables** — `ActionButtonTable` (CRUD with per-row action buttons), `ListTable` (read-only clickable rows), `SelectionTable` (checkbox multi-select)
|
|
132
|
+
|
|
133
|
+
**Forms** — `EditDialog` (modal create/edit), `EditCard` (inline form)
|
|
134
|
+
|
|
135
|
+
**Navigation** — `ViewStack` (list/detail/edit flow), `Tabs` (tabbed content)
|
|
136
|
+
|
|
137
|
+
**Display** — `DetailCard` (read-only detail view), `Dialog` (modal overlay), `StepWizard` (multi-step form)
|
|
138
|
+
|
|
139
|
+
**Layout** — `Button`, `IconButton`, `Icon`, `Row`, `Col`, `Separator`
|
|
140
|
+
|
|
141
|
+
**Store layer** — `DictStore` (in-memory), `TortoiseStore` (ORM-backed), `MultitenantTortoiseStore` (tenant-scoped)
|
|
142
|
+
|
|
143
|
+
See [`components/API.md`](components/API.md) for the full component API reference.
|
|
144
|
+
|
|
145
|
+
## Examples
|
|
146
|
+
|
|
147
|
+
Run any example with `python -m ng_rdm.examples.<name>` and open http://localhost:8080.
|
|
148
|
+
|
|
149
|
+
| Example | Description |
|
|
150
|
+
|---------|-------------|
|
|
151
|
+
| `catalog` | Component catalog — showcases all widgets |
|
|
152
|
+
| `master_detail` | ViewStack master-detail navigation |
|
|
153
|
+
| `custom_datasource` | Custom `RdmDataSource` implementation |
|
|
154
|
+
| `vanilla_store` | Basic store usage without UI components |
|
|
155
|
+
| `topic_filtering` | Topic-based observer filtering |
|
|
156
|
+
|
|
157
|
+
## Some notes
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
## Requirements
|
|
162
|
+
|
|
163
|
+
- Python 3.12+
|
|
164
|
+
- NiceGUI >= 1.4.0
|
|
165
|
+
- Tortoise ORM >= 0.20.0
|
|
166
|
+
- pytz
|
|
167
|
+
|
|
168
|
+
## License
|
|
169
|
+
|
|
170
|
+
MIT
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
# nicegui-rdm: Reactive Data Management
|
|
2
|
+
|
|
3
|
+
## Why: in a nutshell
|
|
4
|
+
|
|
5
|
+
ng_rdm offers a clean and modern set of (non-Quasar) tables with all the plumbing you need to build database-backed NiceGUI applications. Moreover, these tables can automatically refresh when back-end data is added or modified. Hence: **reactive** data management.
|
|
6
|
+
|
|
7
|
+
## Background (feel free to skip)
|
|
8
|
+
|
|
9
|
+
ng_rdm is based on two ideas:
|
|
10
|
+
|
|
11
|
+
1. For my own apps I needed to add **reactivity to database applications**: changes in data should be reflected in UI components, *without* the user having to refresh a page. Imagine a table showing items, counts, stock being updated in near real-time as data is changing. This is the core of the library, implemented in `models` and `store`. Note: this is similar to but *complementary* to the reactivity we can easily achieve ‘client-side’ with NiceGUI bindings etc.
|
|
12
|
+
|
|
13
|
+
2. Secondly, I've always been fighting Quasar's **“composite” UI components** such as tables, dialogs, cards, etc.: layer upon layer of div's and the most obnoxious CSS imaginable. Thanks to NiceGUI's websocket architecture we can move the logic for and behavior of those components from JavaScript/VueJS over to the Python side. In `components/widgets` you'll find tables that create clean html with semantic CSS selectors and that tie in to `store` observability – entirely in Python.
|
|
14
|
+
|
|
15
|
+
See below for a more detailed overview of the architecture.
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
## Installation
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
pip install nicegui-rdm
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Architecture
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
┌──────────────────────────────────────────────────────────┐
|
|
28
|
+
│ UI Components │
|
|
29
|
+
│ ActionButtonTable · ListTable · SelectionTable │
|
|
30
|
+
│ EditDialog · EditCard · DetailCard · ViewStack │
|
|
31
|
+
└──────────────┬─────────────────────────────────┬─────────┘
|
|
32
|
+
│ 1. user action ▲
|
|
33
|
+
▼ │ 6. notify_observers
|
|
34
|
+
┌──────────────┴─────────────────────────────────┴─────────┐
|
|
35
|
+
│ Store Layer │
|
|
36
|
+
│ Store (base) · DictStore · TortoiseStore │
|
|
37
|
+
│ MultitenantTortoiseStore · StoreRegistry │
|
|
38
|
+
│ CRUD · validation · observer pattern │
|
|
39
|
+
└──────────────┬─────────────────────────────────┬─────────┘
|
|
40
|
+
│ 2. validate & write ▲
|
|
41
|
+
▼ │ 5. return result
|
|
42
|
+
┌──────────────┴─────────────────────────────────┴─────────┐
|
|
43
|
+
│ Data Layer │
|
|
44
|
+
│ Tortoise ORM · QModel │
|
|
45
|
+
│ SQLite · PostgreSQL · MySQL │
|
|
46
|
+
└──────────────────────────────────────────────────────────┘
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
User actions flow **down** through the Store layer (which validates and normalizes) to the database. On success, the Store broadcasts a `StoreEvent` **up** to all subscribed UI components, which automatically rebuild via `@ui.refreshable_method`. This is the reactive loop that keeps tables and detail views in sync with the database without manual refresh.
|
|
50
|
+
|
|
51
|
+
## Quick Start
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
from nicegui import app, ui
|
|
55
|
+
from tortoise import fields
|
|
56
|
+
|
|
57
|
+
from ng_rdm import TortoiseStore, init_db, close_db, FieldSpec, Validator
|
|
58
|
+
from ng_rdm.models import QModel
|
|
59
|
+
from ng_rdm.components import (
|
|
60
|
+
rdm_init, Column, TableConfig, FormConfig,
|
|
61
|
+
ActionButtonTable, EditDialog,
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
# 1. Define a model with validation
|
|
65
|
+
class Task(QModel):
|
|
66
|
+
id = fields.IntField(pk=True)
|
|
67
|
+
name = fields.CharField(max_length=100)
|
|
68
|
+
|
|
69
|
+
field_specs = {
|
|
70
|
+
"name": FieldSpec(validators=[
|
|
71
|
+
Validator("Name is required", lambda v, _: bool(v and v.strip()))
|
|
72
|
+
])
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
# 2. Initialize database and create a store (module level)
|
|
76
|
+
init_db(app, "sqlite://tasks.db", modules={"models": [__name__]}, generate_schemas=True)
|
|
77
|
+
app.on_shutdown(close_db)
|
|
78
|
+
|
|
79
|
+
task_store = TortoiseStore(Task)
|
|
80
|
+
|
|
81
|
+
# 3. Build a page
|
|
82
|
+
@ui.page("/")
|
|
83
|
+
async def index():
|
|
84
|
+
rdm_init() # load CSS + Bootstrap Icons
|
|
85
|
+
|
|
86
|
+
columns = [Column("name", "Task name")]
|
|
87
|
+
table_config = TableConfig(columns=columns)
|
|
88
|
+
form_config = FormConfig(columns=columns, title_add="New Task", title_edit="Edit Task")
|
|
89
|
+
|
|
90
|
+
# EditDialog for add/edit; ActionButtonTable for display
|
|
91
|
+
dlg = EditDialog(data_source=task_store, config=form_config,
|
|
92
|
+
on_saved=lambda _: table.build.refresh())
|
|
93
|
+
table = ActionButtonTable(
|
|
94
|
+
data_source=task_store, config=table_config,
|
|
95
|
+
on_add=dlg.open_for_new, on_edit=dlg.open_for_edit,
|
|
96
|
+
)
|
|
97
|
+
await table.build()
|
|
98
|
+
|
|
99
|
+
ui.run()
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## What's Included
|
|
103
|
+
|
|
104
|
+
**Tables** — `ActionButtonTable` (CRUD with per-row action buttons), `ListTable` (read-only clickable rows), `SelectionTable` (checkbox multi-select)
|
|
105
|
+
|
|
106
|
+
**Forms** — `EditDialog` (modal create/edit), `EditCard` (inline form)
|
|
107
|
+
|
|
108
|
+
**Navigation** — `ViewStack` (list/detail/edit flow), `Tabs` (tabbed content)
|
|
109
|
+
|
|
110
|
+
**Display** — `DetailCard` (read-only detail view), `Dialog` (modal overlay), `StepWizard` (multi-step form)
|
|
111
|
+
|
|
112
|
+
**Layout** — `Button`, `IconButton`, `Icon`, `Row`, `Col`, `Separator`
|
|
113
|
+
|
|
114
|
+
**Store layer** — `DictStore` (in-memory), `TortoiseStore` (ORM-backed), `MultitenantTortoiseStore` (tenant-scoped)
|
|
115
|
+
|
|
116
|
+
See [`components/API.md`](components/API.md) for the full component API reference.
|
|
117
|
+
|
|
118
|
+
## Examples
|
|
119
|
+
|
|
120
|
+
Run any example with `python -m ng_rdm.examples.<name>` and open http://localhost:8080.
|
|
121
|
+
|
|
122
|
+
| Example | Description |
|
|
123
|
+
|---------|-------------|
|
|
124
|
+
| `catalog` | Component catalog — showcases all widgets |
|
|
125
|
+
| `master_detail` | ViewStack master-detail navigation |
|
|
126
|
+
| `custom_datasource` | Custom `RdmDataSource` implementation |
|
|
127
|
+
| `vanilla_store` | Basic store usage without UI components |
|
|
128
|
+
| `topic_filtering` | Topic-based observer filtering |
|
|
129
|
+
|
|
130
|
+
## Some notes
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
## Requirements
|
|
135
|
+
|
|
136
|
+
- Python 3.12+
|
|
137
|
+
- NiceGUI >= 1.4.0
|
|
138
|
+
- Tortoise ORM >= 0.20.0
|
|
139
|
+
- pytz
|
|
140
|
+
|
|
141
|
+
## License
|
|
142
|
+
|
|
143
|
+
MIT
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CSS audit script for ng_rdm.css.
|
|
3
|
+
|
|
4
|
+
Extracts all .rdm-* class definitions from the CSS file and cross-references
|
|
5
|
+
them against usage in Python source files, reporting:
|
|
6
|
+
- Unused classes (defined in CSS, not referenced in Python)
|
|
7
|
+
- Missing classes (referenced in Python, not defined in CSS)
|
|
8
|
+
- Duplicate selectors (same class defined in multiple rule blocks)
|
|
9
|
+
|
|
10
|
+
Usage:
|
|
11
|
+
python check_styles.py
|
|
12
|
+
python check_styles.py --consumer /path/to/consumer/app
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import argparse
|
|
16
|
+
import re
|
|
17
|
+
import sys
|
|
18
|
+
from collections import defaultdict
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
|
|
21
|
+
CSS_FILE = Path('src/ng_rdm/components/ng_rdm.css')
|
|
22
|
+
SRC_DIR = Path('src/ng_rdm')
|
|
23
|
+
|
|
24
|
+
# Known color variants for dynamic f-string patterns in button.py
|
|
25
|
+
COLOR_VARIANTS = ['default', 'primary', 'secondary', 'success', 'warning', 'danger', 'text']
|
|
26
|
+
|
|
27
|
+
# F-string class templates: prefix -> list of suffixes
|
|
28
|
+
DYNAMIC_TEMPLATES: dict[str, list[str]] = {
|
|
29
|
+
'rdm-btn': COLOR_VARIANTS,
|
|
30
|
+
'rdm-btn-icon': COLOR_VARIANTS,
|
|
31
|
+
'rdm-icon': COLOR_VARIANTS,
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
# ---------------------------------------------------------------------------
|
|
36
|
+
# CSS parsing
|
|
37
|
+
# ---------------------------------------------------------------------------
|
|
38
|
+
|
|
39
|
+
def extract_css_classes(css_path: Path) -> dict[str, list[int]]:
|
|
40
|
+
"""Return {class_name: [line_numbers]} for all active .rdm-* class selectors."""
|
|
41
|
+
result: dict[str, list[int]] = defaultdict(list)
|
|
42
|
+
in_block_comment = False
|
|
43
|
+
|
|
44
|
+
for i, line in enumerate(css_path.read_text().splitlines(), 1):
|
|
45
|
+
stripped = line.strip()
|
|
46
|
+
|
|
47
|
+
# Track block comments /* ... */
|
|
48
|
+
if '/*' in stripped:
|
|
49
|
+
in_block_comment = True
|
|
50
|
+
if in_block_comment:
|
|
51
|
+
if '*/' in stripped:
|
|
52
|
+
in_block_comment = False
|
|
53
|
+
continue
|
|
54
|
+
|
|
55
|
+
for match in re.finditer(r'\.(rdm-[a-zA-Z0-9_-]+)', line):
|
|
56
|
+
result[match.group(1)].append(i)
|
|
57
|
+
|
|
58
|
+
return dict(result)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def find_duplicate_selectors(css_classes: dict[str, list[int]]) -> dict[str, list[int]]:
|
|
62
|
+
"""Return classes whose selector appears in more than one distinct rule block."""
|
|
63
|
+
return {cls: lines for cls, lines in css_classes.items() if len(set(lines)) > 1}
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
# ---------------------------------------------------------------------------
|
|
67
|
+
# Python scanning
|
|
68
|
+
# ---------------------------------------------------------------------------
|
|
69
|
+
|
|
70
|
+
def expand_dynamic_classes(template: str) -> list[str]:
|
|
71
|
+
"""Expand f-string template like 'rdm-btn-{color}' to concrete class names."""
|
|
72
|
+
for prefix, variants in DYNAMIC_TEMPLATES.items():
|
|
73
|
+
pattern = re.compile(re.escape(prefix) + r'-\{[^}]+\}')
|
|
74
|
+
if pattern.search(template):
|
|
75
|
+
return [f'{prefix}-{v}' for v in variants]
|
|
76
|
+
return []
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def scan_python_file(path: Path) -> set[str]:
|
|
80
|
+
"""Return all rdm-* class names referenced in a Python file."""
|
|
81
|
+
content = path.read_text()
|
|
82
|
+
found: set[str] = set()
|
|
83
|
+
|
|
84
|
+
# 1. F-string templates first: f'rdm-btn-{color}' etc.
|
|
85
|
+
# Process before literal scan to avoid double-counting truncated prefixes.
|
|
86
|
+
fstring_spans: list[tuple[int, int]] = []
|
|
87
|
+
for m in re.finditer(r'f["\']([^"\']*rdm-[^"\']*\{[^}]+\}[^"\']*)["\']', content):
|
|
88
|
+
fstring_spans.append((m.start(), m.end()))
|
|
89
|
+
expanded = expand_dynamic_classes(m.group(1))
|
|
90
|
+
found.update(expanded) # may be empty if template not recognised — that's fine
|
|
91
|
+
|
|
92
|
+
# 2. Literal strings outside f-string spans
|
|
93
|
+
for m in re.finditer(r'["\']([^"\']*rdm-[^"\']*)["\']', content):
|
|
94
|
+
# Skip if this match overlaps an already-processed f-string
|
|
95
|
+
if any(s <= m.start() <= e for s, e in fstring_spans):
|
|
96
|
+
continue
|
|
97
|
+
for cls in re.findall(r'rdm-[a-zA-Z0-9_-]+', m.group(1)):
|
|
98
|
+
if not cls.endswith('-'): # skip truncated f-string fragments
|
|
99
|
+
found.add(cls)
|
|
100
|
+
|
|
101
|
+
# 3. _css_class attribute assignments
|
|
102
|
+
for m in re.finditer(r'_css_class\s*=\s*["\']([^"\']+)["\']', content):
|
|
103
|
+
for cls in m.group(1).split():
|
|
104
|
+
if cls.startswith('rdm-'):
|
|
105
|
+
found.add(cls)
|
|
106
|
+
|
|
107
|
+
return found
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def scan_directory(directory: Path) -> dict[str, set[str]]:
|
|
111
|
+
"""Return {class_name: {file_paths}} for all rdm-* references in .py files."""
|
|
112
|
+
usage: dict[str, set[str]] = defaultdict(set)
|
|
113
|
+
root = Path.cwd().resolve()
|
|
114
|
+
for py_file in directory.resolve().rglob('*.py'):
|
|
115
|
+
label = str(py_file.relative_to(root)) if py_file.is_relative_to(root) else str(py_file)
|
|
116
|
+
for cls in scan_python_file(py_file):
|
|
117
|
+
usage[cls].add(label)
|
|
118
|
+
return dict(usage)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
# ---------------------------------------------------------------------------
|
|
122
|
+
# Reporting
|
|
123
|
+
# ---------------------------------------------------------------------------
|
|
124
|
+
|
|
125
|
+
def print_section(title: str, items: list[str]) -> None:
|
|
126
|
+
print(f'\n{"=" * 60}')
|
|
127
|
+
print(f' {title} ({len(items)})')
|
|
128
|
+
print('=' * 60)
|
|
129
|
+
for item in sorted(items):
|
|
130
|
+
print(f' {item}')
|
|
131
|
+
if not items:
|
|
132
|
+
print(' (none)')
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def main() -> None:
|
|
136
|
+
parser = argparse.ArgumentParser(description='Audit ng_rdm.css for unused/missing classes.')
|
|
137
|
+
parser.add_argument('--consumer', metavar='PATH', help='Also scan a consumer app directory')
|
|
138
|
+
args = parser.parse_args()
|
|
139
|
+
|
|
140
|
+
if not CSS_FILE.exists():
|
|
141
|
+
print(f'Error: {CSS_FILE} not found. Run from the project root.', file=sys.stderr)
|
|
142
|
+
sys.exit(1)
|
|
143
|
+
|
|
144
|
+
# --- Extract CSS definitions ---
|
|
145
|
+
css_classes = extract_css_classes(CSS_FILE)
|
|
146
|
+
print(f'CSS: {len(css_classes)} distinct rdm-* class names in {CSS_FILE}')
|
|
147
|
+
|
|
148
|
+
# --- Scan Python usage ---
|
|
149
|
+
usage = scan_directory(SRC_DIR)
|
|
150
|
+
if args.consumer:
|
|
151
|
+
consumer_path = Path(args.consumer)
|
|
152
|
+
if not consumer_path.exists():
|
|
153
|
+
print(f'Warning: consumer path {consumer_path} not found', file=sys.stderr)
|
|
154
|
+
else:
|
|
155
|
+
consumer_usage = scan_directory(consumer_path)
|
|
156
|
+
for cls, files in consumer_usage.items():
|
|
157
|
+
if cls not in usage:
|
|
158
|
+
usage[cls] = set()
|
|
159
|
+
usage[cls].update(files)
|
|
160
|
+
print(f'Consumer: also scanned {consumer_path}')
|
|
161
|
+
|
|
162
|
+
print(f'Python: {len(usage)} distinct rdm-* class names referenced')
|
|
163
|
+
|
|
164
|
+
# --- Cross-reference ---
|
|
165
|
+
defined = set(css_classes.keys())
|
|
166
|
+
referenced = set(usage.keys())
|
|
167
|
+
|
|
168
|
+
unused = defined - referenced
|
|
169
|
+
missing = referenced - defined
|
|
170
|
+
|
|
171
|
+
duplicates = find_duplicate_selectors(css_classes)
|
|
172
|
+
|
|
173
|
+
# --- Output ---
|
|
174
|
+
print_section('UNUSED (defined in CSS, not found in Python)', list(unused))
|
|
175
|
+
|
|
176
|
+
if missing:
|
|
177
|
+
print_section('MISSING from CSS (referenced in Python, not defined)', list(missing))
|
|
178
|
+
|
|
179
|
+
if duplicates:
|
|
180
|
+
print('\n' + '=' * 60)
|
|
181
|
+
print(f' DUPLICATE SELECTORS ({len(duplicates)})')
|
|
182
|
+
print('=' * 60)
|
|
183
|
+
for cls, lines in sorted(duplicates.items()):
|
|
184
|
+
print(f' .{cls} — lines: {lines}')
|
|
185
|
+
|
|
186
|
+
# Usage detail for referenced classes
|
|
187
|
+
print('\n' + '=' * 60)
|
|
188
|
+
print(' USAGE DETAIL (referenced classes with file counts)')
|
|
189
|
+
print('=' * 60)
|
|
190
|
+
for cls in sorted(referenced & defined):
|
|
191
|
+
files = usage[cls]
|
|
192
|
+
print(f' {cls:<45} {len(files)} file(s)')
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
if __name__ == '__main__':
|
|
196
|
+
main()
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
NiceGUI ready to go on http://localhost:8080, and http://192.168.178.229:8080
|
|
2
|
+
2026-04-06 18:02:52.725 osascript[53846:16634124] ApplePersistence=NO
|
|
3
|
+
WARNING: WatchFiles detected changes in 'test_keyboard2.py'. Reloading...
|
|
4
|
+
NiceGUI ready to go on http://localhost:8080, and http://192.168.178.229:8080
|
|
5
|
+
WARNING: WatchFiles detected changes in 'src/ng_rdm/components/widgets/dialog.py'. Reloading...
|
|
6
|
+
NiceGUI ready to go on http://localhost:8080, and http://192.168.178.229:8080
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling<1.27"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "nicegui-rdm"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Reactive Data Management for NiceGUI with Tortoise ORM"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.12"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "Peter Kleynjan" },
|
|
14
|
+
]
|
|
15
|
+
keywords = ["nicegui", "tortoise-orm", "reactive", "data-management", "crud"]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 3 - Alpha",
|
|
18
|
+
"Intended Audience :: Developers",
|
|
19
|
+
"License :: OSI Approved :: MIT License",
|
|
20
|
+
"Operating System :: OS Independent",
|
|
21
|
+
"Programming Language :: Python :: 3",
|
|
22
|
+
"Programming Language :: Python :: 3.12",
|
|
23
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
24
|
+
]
|
|
25
|
+
dependencies = [
|
|
26
|
+
"nicegui>=3.0,<4.0",
|
|
27
|
+
"tortoise-orm>=1.0.0,<2.0.0",
|
|
28
|
+
"pytz",
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
[project.optional-dependencies]
|
|
32
|
+
dev = [
|
|
33
|
+
"pytest>=8.0",
|
|
34
|
+
"pytest-asyncio>=0.23",
|
|
35
|
+
"pytest-cov>=5.0",
|
|
36
|
+
"httpx",
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
[project.urls]
|
|
40
|
+
Homepage = "https://github.com/kleynjan/nicegui-rdm"
|
|
41
|
+
Repository = "https://github.com/kleynjan/nicegui-rdm"
|
|
42
|
+
|
|
43
|
+
[tool.hatch.build.targets.wheel]
|
|
44
|
+
packages = ["src/ng_rdm"]
|
|
45
|
+
exclude = ["*.sqlite3"]
|
|
46
|
+
|
|
47
|
+
[tool.pytest.ini_options]
|
|
48
|
+
addopts = "-W ignore::DeprecationWarning -W ignore::PendingDeprecationWarning -W ignore::pytest.PytestDeprecationWarning -v"
|
|
49
|
+
asyncio_mode = "auto"
|
|
50
|
+
asyncio_default_fixture_loop_scope = "session"
|
|
51
|
+
asyncio_default_test_loop_scope = "session"
|
|
52
|
+
main_file = ""
|
|
53
|
+
testpaths = ["tests"]
|
|
54
|
+
python_files = "test_*.py"
|
|
55
|
+
python_classes = "Test*"
|
|
56
|
+
python_functions = "test_*"
|
|
57
|
+
markers = [
|
|
58
|
+
"slow: marks tests as slow (deselect with '-m \"not slow\"')",
|
|
59
|
+
"components: component/widget UI tests using NiceGUI User fixture",
|
|
60
|
+
]
|