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.
Files changed (31) hide show
  1. inguitive-0.1.0/LICENSE +21 -0
  2. inguitive-0.1.0/PKG-INFO +310 -0
  3. inguitive-0.1.0/README.md +274 -0
  4. inguitive-0.1.0/pyproject.toml +75 -0
  5. inguitive-0.1.0/setup.cfg +4 -0
  6. inguitive-0.1.0/src/inguitive/__init__.py +90 -0
  7. inguitive-0.1.0/src/inguitive/components.py +1055 -0
  8. inguitive-0.1.0/src/inguitive/css.py +16 -0
  9. inguitive-0.1.0/src/inguitive/fastapi.py +332 -0
  10. inguitive-0.1.0/src/inguitive/htmx.py +22 -0
  11. inguitive-0.1.0/src/inguitive/py.typed +0 -0
  12. inguitive-0.1.0/src/inguitive/session.py +280 -0
  13. inguitive-0.1.0/src/inguitive/state.py +100 -0
  14. inguitive-0.1.0/src/inguitive/svg.py +13 -0
  15. inguitive-0.1.0/src/inguitive/trigger.py +53 -0
  16. inguitive-0.1.0/src/inguitive/utils.py +27 -0
  17. inguitive-0.1.0/src/inguitive.egg-info/PKG-INFO +310 -0
  18. inguitive-0.1.0/src/inguitive.egg-info/SOURCES.txt +29 -0
  19. inguitive-0.1.0/src/inguitive.egg-info/dependency_links.txt +1 -0
  20. inguitive-0.1.0/src/inguitive.egg-info/requires.txt +12 -0
  21. inguitive-0.1.0/src/inguitive.egg-info/top_level.txt +1 -0
  22. inguitive-0.1.0/tests/test_async_handlers.py +223 -0
  23. inguitive-0.1.0/tests/test_components.py +366 -0
  24. inguitive-0.1.0/tests/test_decorators.py +250 -0
  25. inguitive-0.1.0/tests/test_form_data_injection.py +256 -0
  26. inguitive-0.1.0/tests/test_redis_backend.py +214 -0
  27. inguitive-0.1.0/tests/test_session_backends.py +424 -0
  28. inguitive-0.1.0/tests/test_session_isolation.py +124 -0
  29. inguitive-0.1.0/tests/test_state_isolation.py +135 -0
  30. inguitive-0.1.0/tests/test_trigger_args.py +262 -0
  31. inguitive-0.1.0/tests/test_utils.py +44 -0
@@ -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.
@@ -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"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+