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.
Files changed (72) hide show
  1. nicegui_rdm-0.1.0/.gitignore +54 -0
  2. nicegui_rdm-0.1.0/LICENSE +21 -0
  3. nicegui_rdm-0.1.0/PKG-INFO +170 -0
  4. nicegui_rdm-0.1.0/README.md +143 -0
  5. nicegui_rdm-0.1.0/check_styles.py +196 -0
  6. nicegui_rdm-0.1.0/keyboard_output.log +6 -0
  7. nicegui_rdm-0.1.0/pyproject.toml +60 -0
  8. nicegui_rdm-0.1.0/requirements-test.txt +4 -0
  9. nicegui_rdm-0.1.0/requirements.txt +3 -0
  10. nicegui_rdm-0.1.0/src/ng_rdm/__init__.py +37 -0
  11. nicegui_rdm-0.1.0/src/ng_rdm/components/API.md +593 -0
  12. nicegui_rdm-0.1.0/src/ng_rdm/components/__init__.py +138 -0
  13. nicegui_rdm-0.1.0/src/ng_rdm/components/base.py +431 -0
  14. nicegui_rdm-0.1.0/src/ng_rdm/components/fields.py +51 -0
  15. nicegui_rdm-0.1.0/src/ng_rdm/components/i18n.py +64 -0
  16. nicegui_rdm-0.1.0/src/ng_rdm/components/ng_rdm.css +1047 -0
  17. nicegui_rdm-0.1.0/src/ng_rdm/components/protocol.py +65 -0
  18. nicegui_rdm-0.1.0/src/ng_rdm/components/widgets/__init__.py +35 -0
  19. nicegui_rdm-0.1.0/src/ng_rdm/components/widgets/action_button_table.py +107 -0
  20. nicegui_rdm-0.1.0/src/ng_rdm/components/widgets/button.py +72 -0
  21. nicegui_rdm-0.1.0/src/ng_rdm/components/widgets/detail_card.py +91 -0
  22. nicegui_rdm-0.1.0/src/ng_rdm/components/widgets/dialog.py +145 -0
  23. nicegui_rdm-0.1.0/src/ng_rdm/components/widgets/edit_card.py +82 -0
  24. nicegui_rdm-0.1.0/src/ng_rdm/components/widgets/edit_dialog.py +115 -0
  25. nicegui_rdm-0.1.0/src/ng_rdm/components/widgets/layout.py +106 -0
  26. nicegui_rdm-0.1.0/src/ng_rdm/components/widgets/list_table.py +96 -0
  27. nicegui_rdm-0.1.0/src/ng_rdm/components/widgets/selection_table.py +153 -0
  28. nicegui_rdm-0.1.0/src/ng_rdm/components/widgets/tabs.py +53 -0
  29. nicegui_rdm-0.1.0/src/ng_rdm/components/widgets/view_stack.py +113 -0
  30. nicegui_rdm-0.1.0/src/ng_rdm/components/widgets/wizard.py +156 -0
  31. nicegui_rdm-0.1.0/src/ng_rdm/debug/__init__.py +12 -0
  32. nicegui_rdm-0.1.0/src/ng_rdm/debug/event_log.py +128 -0
  33. nicegui_rdm-0.1.0/src/ng_rdm/debug/page.py +175 -0
  34. nicegui_rdm-0.1.0/src/ng_rdm/examples/README.md +34 -0
  35. nicegui_rdm-0.1.0/src/ng_rdm/examples/catalog.py +645 -0
  36. nicegui_rdm-0.1.0/src/ng_rdm/examples/custom_datasource.py +152 -0
  37. nicegui_rdm-0.1.0/src/ng_rdm/examples/examples.css +60 -0
  38. nicegui_rdm-0.1.0/src/ng_rdm/examples/master_detail.py +272 -0
  39. nicegui_rdm-0.1.0/src/ng_rdm/examples/topic_filtering.py +201 -0
  40. nicegui_rdm-0.1.0/src/ng_rdm/examples/vanilla_store.py +166 -0
  41. nicegui_rdm-0.1.0/src/ng_rdm/models/__init__.py +8 -0
  42. nicegui_rdm-0.1.0/src/ng_rdm/models/qmodel.py +127 -0
  43. nicegui_rdm-0.1.0/src/ng_rdm/models/types.py +12 -0
  44. nicegui_rdm-0.1.0/src/ng_rdm/page.py +15 -0
  45. nicegui_rdm-0.1.0/src/ng_rdm/store/__init__.py +25 -0
  46. nicegui_rdm-0.1.0/src/ng_rdm/store/base.py +257 -0
  47. nicegui_rdm-0.1.0/src/ng_rdm/store/dict_store.py +70 -0
  48. nicegui_rdm-0.1.0/src/ng_rdm/store/multitenancy.py +71 -0
  49. nicegui_rdm-0.1.0/src/ng_rdm/store/notifier.py +201 -0
  50. nicegui_rdm-0.1.0/src/ng_rdm/store/orm.py +150 -0
  51. nicegui_rdm-0.1.0/src/ng_rdm/utils/__init__.py +7 -0
  52. nicegui_rdm-0.1.0/src/ng_rdm/utils/helpers.py +217 -0
  53. nicegui_rdm-0.1.0/src/ng_rdm/utils/logging.py +85 -0
  54. nicegui_rdm-0.1.0/tests/__init__.py +0 -0
  55. nicegui_rdm-0.1.0/tests/components/__init__.py +1 -0
  56. nicegui_rdm-0.1.0/tests/components/conftest.py +89 -0
  57. nicegui_rdm-0.1.0/tests/components/test_base.py +143 -0
  58. nicegui_rdm-0.1.0/tests/components/test_detail_card.py +128 -0
  59. nicegui_rdm-0.1.0/tests/components/test_dialog.py +72 -0
  60. nicegui_rdm-0.1.0/tests/components/test_edit.py +258 -0
  61. nicegui_rdm-0.1.0/tests/components/test_i18n.py +90 -0
  62. nicegui_rdm-0.1.0/tests/components/test_layout.py +145 -0
  63. nicegui_rdm-0.1.0/tests/components/test_navigation.py +213 -0
  64. nicegui_rdm-0.1.0/tests/components/test_poc.py +293 -0
  65. nicegui_rdm-0.1.0/tests/components/test_tables.py +460 -0
  66. nicegui_rdm-0.1.0/tests/components/test_wizard.py +126 -0
  67. nicegui_rdm-0.1.0/tests/conftest.py +100 -0
  68. nicegui_rdm-0.1.0/tests/test_multitenancy.py +109 -0
  69. nicegui_rdm-0.1.0/tests/test_notifier.py +322 -0
  70. nicegui_rdm-0.1.0/tests/test_orm.py +287 -0
  71. nicegui_rdm-0.1.0/tests/test_store.py +315 -0
  72. 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 &lsquo;client-side&rsquo; with NiceGUI bindings etc.
39
+
40
+ 2. Secondly, I've always been fighting Quasar's **&ldquo;composite&rdquo; 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 &ndash; 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 &lsquo;client-side&rsquo; with NiceGUI bindings etc.
12
+
13
+ 2. Secondly, I've always been fighting Quasar's **&ldquo;composite&rdquo; 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 &ndash; 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
+ ]
@@ -0,0 +1,4 @@
1
+ pytest>=8.0
2
+ pytest-asyncio>=0.23
3
+ pytest-cov>=5.0
4
+ httpx
@@ -0,0 +1,3 @@
1
+ nicegui>=3.0,<4.0
2
+ tortoise-orm>=1.0.0,<2.0.0
3
+ pytz