inguitive 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.
- inguitive-0.1.0/LICENSE +21 -0
- inguitive-0.1.0/PKG-INFO +310 -0
- inguitive-0.1.0/README.md +274 -0
- inguitive-0.1.0/pyproject.toml +75 -0
- inguitive-0.1.0/setup.cfg +4 -0
- inguitive-0.1.0/src/inguitive/__init__.py +90 -0
- inguitive-0.1.0/src/inguitive/components.py +1055 -0
- inguitive-0.1.0/src/inguitive/css.py +16 -0
- inguitive-0.1.0/src/inguitive/fastapi.py +332 -0
- inguitive-0.1.0/src/inguitive/htmx.py +22 -0
- inguitive-0.1.0/src/inguitive/py.typed +0 -0
- inguitive-0.1.0/src/inguitive/session.py +280 -0
- inguitive-0.1.0/src/inguitive/state.py +100 -0
- inguitive-0.1.0/src/inguitive/svg.py +13 -0
- inguitive-0.1.0/src/inguitive/trigger.py +53 -0
- inguitive-0.1.0/src/inguitive/utils.py +27 -0
- inguitive-0.1.0/src/inguitive.egg-info/PKG-INFO +310 -0
- inguitive-0.1.0/src/inguitive.egg-info/SOURCES.txt +29 -0
- inguitive-0.1.0/src/inguitive.egg-info/dependency_links.txt +1 -0
- inguitive-0.1.0/src/inguitive.egg-info/requires.txt +12 -0
- inguitive-0.1.0/src/inguitive.egg-info/top_level.txt +1 -0
- inguitive-0.1.0/tests/test_async_handlers.py +223 -0
- inguitive-0.1.0/tests/test_components.py +366 -0
- inguitive-0.1.0/tests/test_decorators.py +250 -0
- inguitive-0.1.0/tests/test_form_data_injection.py +256 -0
- inguitive-0.1.0/tests/test_redis_backend.py +214 -0
- inguitive-0.1.0/tests/test_session_backends.py +424 -0
- inguitive-0.1.0/tests/test_session_isolation.py +124 -0
- inguitive-0.1.0/tests/test_state_isolation.py +135 -0
- inguitive-0.1.0/tests/test_trigger_args.py +262 -0
- inguitive-0.1.0/tests/test_utils.py +44 -0
inguitive-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Johannes
|
|
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.
|
inguitive-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: inguitive
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A pure Python web framework combining intuitive syntax with HTMX for partial page reloads and Tailwind CSS for styling
|
|
5
|
+
Author-email: Johannes Stork <info@stork-software.de>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/j-strk/inguitive
|
|
8
|
+
Project-URL: Documentation, https://github.com/j-strk/inguitive/blob/main/README.md
|
|
9
|
+
Project-URL: Repository, https://github.com/j-strk/inguitive
|
|
10
|
+
Project-URL: Issues, https://github.com/j-strk/inguitive/issues
|
|
11
|
+
Keywords: web,framework,htmx,tailwind,fastapi,python
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Framework :: FastAPI
|
|
19
|
+
Classifier: Topic :: Internet :: WWW/HTTP
|
|
20
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
21
|
+
Requires-Python: >=3.10
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
License-File: LICENSE
|
|
24
|
+
Requires-Dist: fastapi>=0.95.0
|
|
25
|
+
Requires-Dist: uvicorn[standard]>=0.21.0
|
|
26
|
+
Requires-Dist: jinja2>=3.1.0
|
|
27
|
+
Requires-Dist: python-multipart>=0.0.6
|
|
28
|
+
Provides-Extra: dev
|
|
29
|
+
Requires-Dist: pytest>=7.0.0; extra == "dev"
|
|
30
|
+
Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
|
|
31
|
+
Requires-Dist: black>=23.0.0; extra == "dev"
|
|
32
|
+
Requires-Dist: ruff>=0.1.0; extra == "dev"
|
|
33
|
+
Requires-Dist: mypy>=1.0.0; extra == "dev"
|
|
34
|
+
Requires-Dist: httpx>=0.24.0; extra == "dev"
|
|
35
|
+
Dynamic: license-file
|
|
36
|
+
|
|
37
|
+
# inguitive
|
|
38
|
+
|
|
39
|
+
A pure Python web framework combining intuitive syntax with **HTMX** for partial page reloads and **Tailwind CSS** for styling.
|
|
40
|
+
|
|
41
|
+
Unlike traditional request-response frameworks, inguitive provides reactive state management where components automatically re-render when state changes, eliminating the need for manual DOM manipulation or JavaScript. It is designed for Python developers who want to build interactive web applications using only Python, without sacrificing the dynamic feel of modern SPAs.
|
|
42
|
+
|
|
43
|
+
## Features
|
|
44
|
+
|
|
45
|
+
- **Reactive State Management**: Components automatically re-render when state changes
|
|
46
|
+
- **HTMX Integration**: Out-of-band swaps for seamless partial page updates
|
|
47
|
+
- **Component-Based**: Composable UI components with clean Python syntax
|
|
48
|
+
- **Dynamic Attributes**: All component attributes can be static strings or callables
|
|
49
|
+
- **Trigger Arguments**: Pass data from components to handlers via `trigger_args` and `get_trigger_args()`
|
|
50
|
+
- **Type Safe**: Full type hints throughout the codebase
|
|
51
|
+
- **Tailwind CSS**: First-class support for utility-first styling
|
|
52
|
+
|
|
53
|
+
## Quick Start
|
|
54
|
+
|
|
55
|
+
### Installation
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
pip install inguitive
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### Basic Example
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
from inguitive import Div, Button, Label, State, create_app
|
|
65
|
+
from inguitive.css import BUTTON_PRIMARY_CSS
|
|
66
|
+
|
|
67
|
+
# Create FastAPI app
|
|
68
|
+
app = create_app()
|
|
69
|
+
|
|
70
|
+
# Create reactive state
|
|
71
|
+
counter_state = State(0, "counter_state")
|
|
72
|
+
|
|
73
|
+
# Define a trigger function
|
|
74
|
+
@app.trigger_handler
|
|
75
|
+
def increment():
|
|
76
|
+
counter_state.set(counter_state.get() + 1)
|
|
77
|
+
|
|
78
|
+
# Define a component
|
|
79
|
+
def Counter():
|
|
80
|
+
return Div(
|
|
81
|
+
Label(text=lambda: f"Count: {counter_state.get()}", id="counter-label", listen_to="counter_state"),
|
|
82
|
+
Button("+1", trigger="increment", css=BUTTON_PRIMARY_CSS),
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
# Define a route function
|
|
86
|
+
@app.page("/")
|
|
87
|
+
def index():
|
|
88
|
+
return Counter()
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### Trigger Arguments
|
|
92
|
+
|
|
93
|
+
Pass data from a component to its handler using `trigger_args` on the component and `get_trigger_args()` in the handler:
|
|
94
|
+
|
|
95
|
+
```python
|
|
96
|
+
from inguitive import Button, Div, State, Text, create_app, get_trigger_args
|
|
97
|
+
|
|
98
|
+
app = create_app()
|
|
99
|
+
selected_state = State("none", "selected_state")
|
|
100
|
+
|
|
101
|
+
@app.trigger_handler
|
|
102
|
+
def select_item():
|
|
103
|
+
item_id = get_trigger_args().get("id")
|
|
104
|
+
selected_state.set(item_id)
|
|
105
|
+
|
|
106
|
+
@app.page("/")
|
|
107
|
+
def index():
|
|
108
|
+
return Div(
|
|
109
|
+
Button("Select A", trigger="select_item", trigger_args={"id": "a"}),
|
|
110
|
+
Button("Select B", trigger="select_item", trigger_args={"id": "b"}),
|
|
111
|
+
Text(lambda: f"Selected: {selected_state.get()}", listen_to="selected_state"),
|
|
112
|
+
)
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
`trigger_args` are passed as URL query parameters; `get_trigger_args()` returns them as a `dict[str, str]` inside the handler.
|
|
116
|
+
|
|
117
|
+
## Component Reference
|
|
118
|
+
|
|
119
|
+
inguitive provides a comprehensive set of components organized by category. All components support dynamic attributes via callables and can listen to state changes for automatic re-rendering.
|
|
120
|
+
|
|
121
|
+
### Base Components
|
|
122
|
+
|
|
123
|
+
| Component | Description | Key Parameters | Example |
|
|
124
|
+
|-----------|-------------|----------------|---------|
|
|
125
|
+
| **`Component`** | Base class for all components | `id`, `css`, `listen_to` | Custom component base |
|
|
126
|
+
| **`TemplateComponent`** | Render Jinja2 templates | `template`, context vars | Custom HTML with templating |
|
|
127
|
+
|
|
128
|
+
### Layout Components
|
|
129
|
+
|
|
130
|
+
| Component | Description | Key Parameters | Example |
|
|
131
|
+
|-----------|-------------|----------------|---------|
|
|
132
|
+
| **`Div`** | Container div element | `*children`, `id`, `css` | `Div(Button("Click"), css="flex gap-2")` |
|
|
133
|
+
| **`Text`** | Paragraph/text element | `text`, `id`, `css` | `Text("Hello", css="text-xl")` |
|
|
134
|
+
| **`Label`** | Form label element | `text`, `for_`, `id`, `css` | `Label("Name:", for_="name")` |
|
|
135
|
+
|
|
136
|
+
### Form Components
|
|
137
|
+
|
|
138
|
+
| Component | Description | Key Parameters | Example |
|
|
139
|
+
|-----------|-------------|----------------|---------|
|
|
140
|
+
| **`Form`** | Form container | `*children`, `action`, `method` | `Form(Input(...), Button(...))` |
|
|
141
|
+
| **`Input`** | Text input field | `type`, `value`, `placeholder`, `listen_to` | `Input(id="email", type="email")` |
|
|
142
|
+
| **`Textarea`** | Multi-line text input | `value`, `placeholder`, `rows` | `Textarea(id="bio", rows=5)` |
|
|
143
|
+
| **`Select`** | Dropdown select | `options`, `value`, `listen_to` | `Select(id="country", options=[...])` |
|
|
144
|
+
| **`Checkbox`** | Checkbox input | `checked`, `id`, `listen_to` | `Checkbox(id="agree", checked=True)` |
|
|
145
|
+
| **`Radio`** | Radio button input | `value`, `checked`, `name` | `Radio(id="male", name="gender")` |
|
|
146
|
+
| **`Button`** | Clickable button | `*children`, `trigger`, `css` | `Button("Click", trigger="action")` |
|
|
147
|
+
|
|
148
|
+
### Navigation Components
|
|
149
|
+
|
|
150
|
+
| Component | Description | Key Parameters | Example |
|
|
151
|
+
|-----------|-------------|----------------|---------|
|
|
152
|
+
| **`Link`** | Semantic navigation link | `*children`, `href`, `css` | `Link("Home", href="/")` |
|
|
153
|
+
|
|
154
|
+
### Data Display Components
|
|
155
|
+
|
|
156
|
+
| Component | Description | Key Parameters | Example |
|
|
157
|
+
|-----------|-------------|----------------|---------|
|
|
158
|
+
| **`DataTable`** | Tabular data display | `data`, `columns`, `css` | `DataTable(data=[{"name": "A"}])` |
|
|
159
|
+
| **`Icon`** | SVG icon component | `svg`, `css` | `Icon("<svg ...>...</svg>", css="w-6 h-6")` |
|
|
160
|
+
|
|
161
|
+
### Common Parameters (All Components)
|
|
162
|
+
|
|
163
|
+
| Parameter | Type | Description |
|
|
164
|
+
|-----------|------|-------------|
|
|
165
|
+
| `id` | `str \| None` | HTML id attribute. Required for state listening and OOB updates |
|
|
166
|
+
| `css` | `str \| Callable[[], str] \| dict \| None` | Tailwind CSS classes. For DataTable, can be a dict with keys: `table`, `header`, `row`, `cell` |
|
|
167
|
+
| `listen_to` | `str \| list[str] \| None` | State name(s) to listen for changes. Triggers re-render when state updates |
|
|
168
|
+
| `trigger` | `str \| None` | Trigger name for HTMX POST actions (Button, Input, etc.) |
|
|
169
|
+
| `trigger_args` | `dict[str, str] \| None` | Query parameters to pass with trigger |
|
|
170
|
+
|
|
171
|
+
## Navigation & Actions
|
|
172
|
+
|
|
173
|
+
Use `Link` for traditional navigation (SEO, bookmarking, new-tab support) and `trigger` for partial page updates:
|
|
174
|
+
|
|
175
|
+
| | `Link(href="...")` | `trigger="..."` |
|
|
176
|
+
|---|---|---|
|
|
177
|
+
| Renders | `<a href="...">` | HTMX POST |
|
|
178
|
+
| URL changes | ✅ | ❌ |
|
|
179
|
+
| Open in new tab | ✅ | ❌ |
|
|
180
|
+
|
|
181
|
+
```python
|
|
182
|
+
from inguitive import Link, Button
|
|
183
|
+
|
|
184
|
+
# Traditional navigation
|
|
185
|
+
Link("Home", href="/")
|
|
186
|
+
Link("Documentation", href="/docs", css="text-blue-500")
|
|
187
|
+
|
|
188
|
+
# Partial updates
|
|
189
|
+
Button("Save", trigger="save_form")
|
|
190
|
+
Button("Like", trigger="like_post", trigger_args={"id": "123"})
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
## Project Structure
|
|
194
|
+
|
|
195
|
+
```
|
|
196
|
+
.
|
|
197
|
+
├── src/
|
|
198
|
+
│ └── inguitive/
|
|
199
|
+
│ ├── __init__.py # Public API
|
|
200
|
+
│ ├── components.py # Component classes
|
|
201
|
+
│ ├── state.py # Reactive state
|
|
202
|
+
│ ├── htmx.py # HTMX helpers
|
|
203
|
+
│ ├── fastapi.py # FastAPI integration
|
|
204
|
+
│ └── svg.py # SVG icon definitions
|
|
205
|
+
├── examples/
|
|
206
|
+
│ ├── counter_app.py # Per-session counter with theme toggle
|
|
207
|
+
│ ├── todo_app.py # CRUD with filtering and real-time count
|
|
208
|
+
│ ├── chat_app.py # Real-time chat
|
|
209
|
+
│ ├── navigation_demo.py # Link vs trigger patterns
|
|
210
|
+
│ ├── registration_form.py # Form handling
|
|
211
|
+
│ └── data_table_app.py # DataTable with sorting and filtering
|
|
212
|
+
├── tests/
|
|
213
|
+
│ └── test_*.py # Test files
|
|
214
|
+
├── pyproject.toml # Build configuration
|
|
215
|
+
└── README.md
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
## Session Backends
|
|
219
|
+
|
|
220
|
+
inguitive uses session-scoped registries to isolate user state. Choose a backend based on your deployment needs:
|
|
221
|
+
|
|
222
|
+
| Backend | Use When | Persistence | Multi-Worker |
|
|
223
|
+
|---------|----------|-------------|--------------|
|
|
224
|
+
| **`MemoryBackend`** | Development, single worker | ❌ No (lost on restart) | ❌ No |
|
|
225
|
+
| **`RedisBackend`** | Production, multiple workers | ✅ Yes | ✅ Yes |
|
|
226
|
+
|
|
227
|
+
**MemoryBackend** (default) stores sessions in RAM - perfect for development. **RedisBackend** stores sessions in Redis for production deployments with multiple workers or persistent sessions.
|
|
228
|
+
|
|
229
|
+
```python
|
|
230
|
+
from inguitive import create_app, MemoryBackend, RedisBackend
|
|
231
|
+
|
|
232
|
+
# Development: In-memory sessions (default, no config needed)
|
|
233
|
+
app = create_app()
|
|
234
|
+
|
|
235
|
+
# Or explicitly:
|
|
236
|
+
app = create_app(session_backend=MemoryBackend())
|
|
237
|
+
|
|
238
|
+
# Production: Redis-backed sessions for scaling
|
|
239
|
+
app = create_app(
|
|
240
|
+
session_backend=RedisBackend(
|
|
241
|
+
redis_url="redis://localhost:6379",
|
|
242
|
+
ttl_seconds=3600 # Session timeout: 1 hour
|
|
243
|
+
)
|
|
244
|
+
)
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
Requires `pip install redis` for RedisBackend.
|
|
248
|
+
|
|
249
|
+
### Session Lifetime and Expiry
|
|
250
|
+
|
|
251
|
+
inguitive sessions are created automatically on first request and persist across page reloads. Each session has isolated component, state, and data registries.
|
|
252
|
+
|
|
253
|
+
**Session Creation:** A new session is created with a unique ID when a user first visits your application. The session ID is stored in a cookie.
|
|
254
|
+
|
|
255
|
+
**Session Persistence:** Sessions persist across page reloads and browser navigation within the same domain. The session cookie maintains the session ID, allowing the framework to restore the user's state.
|
|
256
|
+
|
|
257
|
+
**Session Expiry:**
|
|
258
|
+
- **MemoryBackend:** Sessions expire after `ttl_seconds` (default: 3600 = 1 hour) of inactivity. Expired sessions are automatically cleaned up every N requests (configurable via `session_cleanup_interval`).
|
|
259
|
+
- **RedisBackend:** Sessions are stored in Redis with a TTL. Redis automatically expires keys after the configured `ttl_seconds`, providing automatic cleanup.
|
|
260
|
+
|
|
261
|
+
**Differences Between Backends:**
|
|
262
|
+
|
|
263
|
+
| Aspect | MemoryBackend | RedisBackend |
|
|
264
|
+
|--------|---------------|--------------|
|
|
265
|
+
| Persistence | Lost on process restart | Survives process restarts |
|
|
266
|
+
| Multi-worker | Not supported (shared memory) | Supported (Redis as shared store) |
|
|
267
|
+
| Cleanup | Manual/Periodic via `cleanup_expired()` | Automatic via Redis TTL |
|
|
268
|
+
| Use Case | Development, testing | Production, scaling |
|
|
269
|
+
|
|
270
|
+
**Page Reload Behavior:** Session state is preserved across page reloads. Components listening to state will re-render with the current state values when the page loads.
|
|
271
|
+
|
|
272
|
+
## Production Deployment
|
|
273
|
+
|
|
274
|
+
Before deploying your inguitive app to production, configure these security settings:
|
|
275
|
+
|
|
276
|
+
```python
|
|
277
|
+
from inguitive import create_app, RedisBackend
|
|
278
|
+
|
|
279
|
+
app = create_app(
|
|
280
|
+
session_backend=RedisBackend(redis_url="redis://localhost:6379"),
|
|
281
|
+
session_cookie_secure=True, # Cookies only over HTTPS
|
|
282
|
+
session_cookie_httponly=True, # Prevent JavaScript access (default)
|
|
283
|
+
session_cookie_max_age=86400, # 24-hour session timeout
|
|
284
|
+
)
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
**Checklist:**
|
|
288
|
+
- ✅ Use `RedisBackend` (not `MemoryBackend`) for persistence across workers
|
|
289
|
+
- ✅ Set `session_cookie_secure=True` when using HTTPS
|
|
290
|
+
- ✅ Verify `session_cookie_httponly=True` (enabled by default)
|
|
291
|
+
- ✅ Deploy with HTTPS (required for secure cookies)
|
|
292
|
+
|
|
293
|
+
## Running the Demo
|
|
294
|
+
|
|
295
|
+
```bash
|
|
296
|
+
# From the repository root
|
|
297
|
+
uvicorn examples.counter_app:app --reload
|
|
298
|
+
|
|
299
|
+
# Then open http://localhost:8000
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
## License
|
|
303
|
+
|
|
304
|
+
MIT License - see [LICENSE](LICENSE) for details.
|
|
305
|
+
|
|
306
|
+
## Contact
|
|
307
|
+
|
|
308
|
+
- **GitHub**: [j-strk](https://github.com/j-strk)
|
|
309
|
+
- **Email**: info@stork-software.de
|
|
310
|
+
- **Issues**: [GitHub Issues](https://github.com/j-strk/inguitive/issues)
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
# inguitive
|
|
2
|
+
|
|
3
|
+
A pure Python web framework combining intuitive syntax with **HTMX** for partial page reloads and **Tailwind CSS** for styling.
|
|
4
|
+
|
|
5
|
+
Unlike traditional request-response frameworks, inguitive provides reactive state management where components automatically re-render when state changes, eliminating the need for manual DOM manipulation or JavaScript. It is designed for Python developers who want to build interactive web applications using only Python, without sacrificing the dynamic feel of modern SPAs.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- **Reactive State Management**: Components automatically re-render when state changes
|
|
10
|
+
- **HTMX Integration**: Out-of-band swaps for seamless partial page updates
|
|
11
|
+
- **Component-Based**: Composable UI components with clean Python syntax
|
|
12
|
+
- **Dynamic Attributes**: All component attributes can be static strings or callables
|
|
13
|
+
- **Trigger Arguments**: Pass data from components to handlers via `trigger_args` and `get_trigger_args()`
|
|
14
|
+
- **Type Safe**: Full type hints throughout the codebase
|
|
15
|
+
- **Tailwind CSS**: First-class support for utility-first styling
|
|
16
|
+
|
|
17
|
+
## Quick Start
|
|
18
|
+
|
|
19
|
+
### Installation
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
pip install inguitive
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
### Basic Example
|
|
26
|
+
|
|
27
|
+
```python
|
|
28
|
+
from inguitive import Div, Button, Label, State, create_app
|
|
29
|
+
from inguitive.css import BUTTON_PRIMARY_CSS
|
|
30
|
+
|
|
31
|
+
# Create FastAPI app
|
|
32
|
+
app = create_app()
|
|
33
|
+
|
|
34
|
+
# Create reactive state
|
|
35
|
+
counter_state = State(0, "counter_state")
|
|
36
|
+
|
|
37
|
+
# Define a trigger function
|
|
38
|
+
@app.trigger_handler
|
|
39
|
+
def increment():
|
|
40
|
+
counter_state.set(counter_state.get() + 1)
|
|
41
|
+
|
|
42
|
+
# Define a component
|
|
43
|
+
def Counter():
|
|
44
|
+
return Div(
|
|
45
|
+
Label(text=lambda: f"Count: {counter_state.get()}", id="counter-label", listen_to="counter_state"),
|
|
46
|
+
Button("+1", trigger="increment", css=BUTTON_PRIMARY_CSS),
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
# Define a route function
|
|
50
|
+
@app.page("/")
|
|
51
|
+
def index():
|
|
52
|
+
return Counter()
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### Trigger Arguments
|
|
56
|
+
|
|
57
|
+
Pass data from a component to its handler using `trigger_args` on the component and `get_trigger_args()` in the handler:
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
from inguitive import Button, Div, State, Text, create_app, get_trigger_args
|
|
61
|
+
|
|
62
|
+
app = create_app()
|
|
63
|
+
selected_state = State("none", "selected_state")
|
|
64
|
+
|
|
65
|
+
@app.trigger_handler
|
|
66
|
+
def select_item():
|
|
67
|
+
item_id = get_trigger_args().get("id")
|
|
68
|
+
selected_state.set(item_id)
|
|
69
|
+
|
|
70
|
+
@app.page("/")
|
|
71
|
+
def index():
|
|
72
|
+
return Div(
|
|
73
|
+
Button("Select A", trigger="select_item", trigger_args={"id": "a"}),
|
|
74
|
+
Button("Select B", trigger="select_item", trigger_args={"id": "b"}),
|
|
75
|
+
Text(lambda: f"Selected: {selected_state.get()}", listen_to="selected_state"),
|
|
76
|
+
)
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
`trigger_args` are passed as URL query parameters; `get_trigger_args()` returns them as a `dict[str, str]` inside the handler.
|
|
80
|
+
|
|
81
|
+
## Component Reference
|
|
82
|
+
|
|
83
|
+
inguitive provides a comprehensive set of components organized by category. All components support dynamic attributes via callables and can listen to state changes for automatic re-rendering.
|
|
84
|
+
|
|
85
|
+
### Base Components
|
|
86
|
+
|
|
87
|
+
| Component | Description | Key Parameters | Example |
|
|
88
|
+
|-----------|-------------|----------------|---------|
|
|
89
|
+
| **`Component`** | Base class for all components | `id`, `css`, `listen_to` | Custom component base |
|
|
90
|
+
| **`TemplateComponent`** | Render Jinja2 templates | `template`, context vars | Custom HTML with templating |
|
|
91
|
+
|
|
92
|
+
### Layout Components
|
|
93
|
+
|
|
94
|
+
| Component | Description | Key Parameters | Example |
|
|
95
|
+
|-----------|-------------|----------------|---------|
|
|
96
|
+
| **`Div`** | Container div element | `*children`, `id`, `css` | `Div(Button("Click"), css="flex gap-2")` |
|
|
97
|
+
| **`Text`** | Paragraph/text element | `text`, `id`, `css` | `Text("Hello", css="text-xl")` |
|
|
98
|
+
| **`Label`** | Form label element | `text`, `for_`, `id`, `css` | `Label("Name:", for_="name")` |
|
|
99
|
+
|
|
100
|
+
### Form Components
|
|
101
|
+
|
|
102
|
+
| Component | Description | Key Parameters | Example |
|
|
103
|
+
|-----------|-------------|----------------|---------|
|
|
104
|
+
| **`Form`** | Form container | `*children`, `action`, `method` | `Form(Input(...), Button(...))` |
|
|
105
|
+
| **`Input`** | Text input field | `type`, `value`, `placeholder`, `listen_to` | `Input(id="email", type="email")` |
|
|
106
|
+
| **`Textarea`** | Multi-line text input | `value`, `placeholder`, `rows` | `Textarea(id="bio", rows=5)` |
|
|
107
|
+
| **`Select`** | Dropdown select | `options`, `value`, `listen_to` | `Select(id="country", options=[...])` |
|
|
108
|
+
| **`Checkbox`** | Checkbox input | `checked`, `id`, `listen_to` | `Checkbox(id="agree", checked=True)` |
|
|
109
|
+
| **`Radio`** | Radio button input | `value`, `checked`, `name` | `Radio(id="male", name="gender")` |
|
|
110
|
+
| **`Button`** | Clickable button | `*children`, `trigger`, `css` | `Button("Click", trigger="action")` |
|
|
111
|
+
|
|
112
|
+
### Navigation Components
|
|
113
|
+
|
|
114
|
+
| Component | Description | Key Parameters | Example |
|
|
115
|
+
|-----------|-------------|----------------|---------|
|
|
116
|
+
| **`Link`** | Semantic navigation link | `*children`, `href`, `css` | `Link("Home", href="/")` |
|
|
117
|
+
|
|
118
|
+
### Data Display Components
|
|
119
|
+
|
|
120
|
+
| Component | Description | Key Parameters | Example |
|
|
121
|
+
|-----------|-------------|----------------|---------|
|
|
122
|
+
| **`DataTable`** | Tabular data display | `data`, `columns`, `css` | `DataTable(data=[{"name": "A"}])` |
|
|
123
|
+
| **`Icon`** | SVG icon component | `svg`, `css` | `Icon("<svg ...>...</svg>", css="w-6 h-6")` |
|
|
124
|
+
|
|
125
|
+
### Common Parameters (All Components)
|
|
126
|
+
|
|
127
|
+
| Parameter | Type | Description |
|
|
128
|
+
|-----------|------|-------------|
|
|
129
|
+
| `id` | `str \| None` | HTML id attribute. Required for state listening and OOB updates |
|
|
130
|
+
| `css` | `str \| Callable[[], str] \| dict \| None` | Tailwind CSS classes. For DataTable, can be a dict with keys: `table`, `header`, `row`, `cell` |
|
|
131
|
+
| `listen_to` | `str \| list[str] \| None` | State name(s) to listen for changes. Triggers re-render when state updates |
|
|
132
|
+
| `trigger` | `str \| None` | Trigger name for HTMX POST actions (Button, Input, etc.) |
|
|
133
|
+
| `trigger_args` | `dict[str, str] \| None` | Query parameters to pass with trigger |
|
|
134
|
+
|
|
135
|
+
## Navigation & Actions
|
|
136
|
+
|
|
137
|
+
Use `Link` for traditional navigation (SEO, bookmarking, new-tab support) and `trigger` for partial page updates:
|
|
138
|
+
|
|
139
|
+
| | `Link(href="...")` | `trigger="..."` |
|
|
140
|
+
|---|---|---|
|
|
141
|
+
| Renders | `<a href="...">` | HTMX POST |
|
|
142
|
+
| URL changes | ✅ | ❌ |
|
|
143
|
+
| Open in new tab | ✅ | ❌ |
|
|
144
|
+
|
|
145
|
+
```python
|
|
146
|
+
from inguitive import Link, Button
|
|
147
|
+
|
|
148
|
+
# Traditional navigation
|
|
149
|
+
Link("Home", href="/")
|
|
150
|
+
Link("Documentation", href="/docs", css="text-blue-500")
|
|
151
|
+
|
|
152
|
+
# Partial updates
|
|
153
|
+
Button("Save", trigger="save_form")
|
|
154
|
+
Button("Like", trigger="like_post", trigger_args={"id": "123"})
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
## Project Structure
|
|
158
|
+
|
|
159
|
+
```
|
|
160
|
+
.
|
|
161
|
+
├── src/
|
|
162
|
+
│ └── inguitive/
|
|
163
|
+
│ ├── __init__.py # Public API
|
|
164
|
+
│ ├── components.py # Component classes
|
|
165
|
+
│ ├── state.py # Reactive state
|
|
166
|
+
│ ├── htmx.py # HTMX helpers
|
|
167
|
+
│ ├── fastapi.py # FastAPI integration
|
|
168
|
+
│ └── svg.py # SVG icon definitions
|
|
169
|
+
├── examples/
|
|
170
|
+
│ ├── counter_app.py # Per-session counter with theme toggle
|
|
171
|
+
│ ├── todo_app.py # CRUD with filtering and real-time count
|
|
172
|
+
│ ├── chat_app.py # Real-time chat
|
|
173
|
+
│ ├── navigation_demo.py # Link vs trigger patterns
|
|
174
|
+
│ ├── registration_form.py # Form handling
|
|
175
|
+
│ └── data_table_app.py # DataTable with sorting and filtering
|
|
176
|
+
├── tests/
|
|
177
|
+
│ └── test_*.py # Test files
|
|
178
|
+
├── pyproject.toml # Build configuration
|
|
179
|
+
└── README.md
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
## Session Backends
|
|
183
|
+
|
|
184
|
+
inguitive uses session-scoped registries to isolate user state. Choose a backend based on your deployment needs:
|
|
185
|
+
|
|
186
|
+
| Backend | Use When | Persistence | Multi-Worker |
|
|
187
|
+
|---------|----------|-------------|--------------|
|
|
188
|
+
| **`MemoryBackend`** | Development, single worker | ❌ No (lost on restart) | ❌ No |
|
|
189
|
+
| **`RedisBackend`** | Production, multiple workers | ✅ Yes | ✅ Yes |
|
|
190
|
+
|
|
191
|
+
**MemoryBackend** (default) stores sessions in RAM - perfect for development. **RedisBackend** stores sessions in Redis for production deployments with multiple workers or persistent sessions.
|
|
192
|
+
|
|
193
|
+
```python
|
|
194
|
+
from inguitive import create_app, MemoryBackend, RedisBackend
|
|
195
|
+
|
|
196
|
+
# Development: In-memory sessions (default, no config needed)
|
|
197
|
+
app = create_app()
|
|
198
|
+
|
|
199
|
+
# Or explicitly:
|
|
200
|
+
app = create_app(session_backend=MemoryBackend())
|
|
201
|
+
|
|
202
|
+
# Production: Redis-backed sessions for scaling
|
|
203
|
+
app = create_app(
|
|
204
|
+
session_backend=RedisBackend(
|
|
205
|
+
redis_url="redis://localhost:6379",
|
|
206
|
+
ttl_seconds=3600 # Session timeout: 1 hour
|
|
207
|
+
)
|
|
208
|
+
)
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
Requires `pip install redis` for RedisBackend.
|
|
212
|
+
|
|
213
|
+
### Session Lifetime and Expiry
|
|
214
|
+
|
|
215
|
+
inguitive sessions are created automatically on first request and persist across page reloads. Each session has isolated component, state, and data registries.
|
|
216
|
+
|
|
217
|
+
**Session Creation:** A new session is created with a unique ID when a user first visits your application. The session ID is stored in a cookie.
|
|
218
|
+
|
|
219
|
+
**Session Persistence:** Sessions persist across page reloads and browser navigation within the same domain. The session cookie maintains the session ID, allowing the framework to restore the user's state.
|
|
220
|
+
|
|
221
|
+
**Session Expiry:**
|
|
222
|
+
- **MemoryBackend:** Sessions expire after `ttl_seconds` (default: 3600 = 1 hour) of inactivity. Expired sessions are automatically cleaned up every N requests (configurable via `session_cleanup_interval`).
|
|
223
|
+
- **RedisBackend:** Sessions are stored in Redis with a TTL. Redis automatically expires keys after the configured `ttl_seconds`, providing automatic cleanup.
|
|
224
|
+
|
|
225
|
+
**Differences Between Backends:**
|
|
226
|
+
|
|
227
|
+
| Aspect | MemoryBackend | RedisBackend |
|
|
228
|
+
|--------|---------------|--------------|
|
|
229
|
+
| Persistence | Lost on process restart | Survives process restarts |
|
|
230
|
+
| Multi-worker | Not supported (shared memory) | Supported (Redis as shared store) |
|
|
231
|
+
| Cleanup | Manual/Periodic via `cleanup_expired()` | Automatic via Redis TTL |
|
|
232
|
+
| Use Case | Development, testing | Production, scaling |
|
|
233
|
+
|
|
234
|
+
**Page Reload Behavior:** Session state is preserved across page reloads. Components listening to state will re-render with the current state values when the page loads.
|
|
235
|
+
|
|
236
|
+
## Production Deployment
|
|
237
|
+
|
|
238
|
+
Before deploying your inguitive app to production, configure these security settings:
|
|
239
|
+
|
|
240
|
+
```python
|
|
241
|
+
from inguitive import create_app, RedisBackend
|
|
242
|
+
|
|
243
|
+
app = create_app(
|
|
244
|
+
session_backend=RedisBackend(redis_url="redis://localhost:6379"),
|
|
245
|
+
session_cookie_secure=True, # Cookies only over HTTPS
|
|
246
|
+
session_cookie_httponly=True, # Prevent JavaScript access (default)
|
|
247
|
+
session_cookie_max_age=86400, # 24-hour session timeout
|
|
248
|
+
)
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
**Checklist:**
|
|
252
|
+
- ✅ Use `RedisBackend` (not `MemoryBackend`) for persistence across workers
|
|
253
|
+
- ✅ Set `session_cookie_secure=True` when using HTTPS
|
|
254
|
+
- ✅ Verify `session_cookie_httponly=True` (enabled by default)
|
|
255
|
+
- ✅ Deploy with HTTPS (required for secure cookies)
|
|
256
|
+
|
|
257
|
+
## Running the Demo
|
|
258
|
+
|
|
259
|
+
```bash
|
|
260
|
+
# From the repository root
|
|
261
|
+
uvicorn examples.counter_app:app --reload
|
|
262
|
+
|
|
263
|
+
# Then open http://localhost:8000
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
## License
|
|
267
|
+
|
|
268
|
+
MIT License - see [LICENSE](LICENSE) for details.
|
|
269
|
+
|
|
270
|
+
## Contact
|
|
271
|
+
|
|
272
|
+
- **GitHub**: [j-strk](https://github.com/j-strk)
|
|
273
|
+
- **Email**: info@stork-software.de
|
|
274
|
+
- **Issues**: [GitHub Issues](https://github.com/j-strk/inguitive/issues)
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "inguitive"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "A pure Python web framework combining intuitive syntax with HTMX for partial page reloads and Tailwind CSS for styling"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
requires-python = ">=3.10"
|
|
12
|
+
authors = [
|
|
13
|
+
{name = "Johannes Stork", email = "info@stork-software.de"}
|
|
14
|
+
]
|
|
15
|
+
keywords = ["web", "framework", "htmx", "tailwind", "fastapi", "python"]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 3 - Alpha",
|
|
18
|
+
"Intended Audience :: Developers",
|
|
19
|
+
"Programming Language :: Python :: 3",
|
|
20
|
+
"Programming Language :: Python :: 3.10",
|
|
21
|
+
"Programming Language :: Python :: 3.11",
|
|
22
|
+
"Programming Language :: Python :: 3.12",
|
|
23
|
+
"Framework :: FastAPI",
|
|
24
|
+
"Topic :: Internet :: WWW/HTTP",
|
|
25
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
dependencies = [
|
|
29
|
+
"fastapi>=0.95.0",
|
|
30
|
+
"uvicorn[standard]>=0.21.0",
|
|
31
|
+
"jinja2>=3.1.0",
|
|
32
|
+
"python-multipart>=0.0.6",
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
[project.optional-dependencies]
|
|
36
|
+
dev = [
|
|
37
|
+
"pytest>=7.0.0",
|
|
38
|
+
"pytest-cov>=4.0.0",
|
|
39
|
+
"black>=23.0.0",
|
|
40
|
+
"ruff>=0.1.0",
|
|
41
|
+
"mypy>=1.0.0",
|
|
42
|
+
"httpx>=0.24.0",
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
[project.urls]
|
|
46
|
+
Homepage = "https://github.com/j-strk/inguitive"
|
|
47
|
+
Documentation = "https://github.com/j-strk/inguitive/blob/main/README.md"
|
|
48
|
+
Repository = "https://github.com/j-strk/inguitive"
|
|
49
|
+
Issues = "https://github.com/j-strk/inguitive/issues"
|
|
50
|
+
|
|
51
|
+
[tool.setuptools.packages.find]
|
|
52
|
+
where = ["src"]
|
|
53
|
+
|
|
54
|
+
[tool.setuptools.package-data]
|
|
55
|
+
"*" = ["*.html", "*.svg", "py.typed"]
|
|
56
|
+
|
|
57
|
+
[tool.black]
|
|
58
|
+
line-length = 100
|
|
59
|
+
target-version = ["py310", "py311", "py312"]
|
|
60
|
+
|
|
61
|
+
[tool.ruff]
|
|
62
|
+
line-length = 100
|
|
63
|
+
select = ["E", "F", "I", "N", "W", "UP"]
|
|
64
|
+
ignore = ["E501"]
|
|
65
|
+
|
|
66
|
+
[tool.mypy]
|
|
67
|
+
python_version = "3.10"
|
|
68
|
+
warn_return_any = true
|
|
69
|
+
warn_unused_configs = true
|
|
70
|
+
ignore_missing_imports = true
|
|
71
|
+
|
|
72
|
+
[tool.pytest.ini_options]
|
|
73
|
+
python_files = ["test_*.py", "*_test.py"]
|
|
74
|
+
addopts = "-v --tb=short"
|
|
75
|
+
testpaths = ["tests"]
|