inguitive
A pure Python web framework combining intuitive syntax with HTMX for partial page reloads and Tailwind CSS for styling.
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.
Features
- Reactive State Management: Components automatically re-render when state changes
- HTMX Integration: Out-of-band swaps for seamless partial page updates
- Component-Based: Composable UI components with clean Python syntax
- Dynamic Attributes: All component attributes can be static strings or callables
- Trigger Arguments: Pass data from components to handlers via
trigger_argsandget_trigger_args() - Type Safe: Full type hints throughout the codebase
- Tailwind CSS: First-class support for utility-first styling
Quick Start
Installation
pip install inguitive
Basic Example
from inguitive import Div, Button, Label, State, create_app
from inguitive.css import BUTTON_PRIMARY_CSS
# Create FastAPI app
app = create_app()
# Create reactive state
counter_state = State(0, "counter_state")
# Define a trigger function
@app.trigger_handler
def increment():
counter_state.set(counter_state.get() + 1)
# Define a component
def Counter():
return Div(
Label(text=lambda: f"Count: {counter_state.get()}", id="counter-label", listen_to="counter_state"),
Button("+1", trigger="increment", css=BUTTON_PRIMARY_CSS),
)
# Define a route function
@app.page("/")
def index():
return Counter()
Trigger Arguments
Pass data from a component to its handler using trigger_args on the component and get_trigger_args() in the handler:
from inguitive import Button, Div, State, Text, create_app, get_trigger_args
app = create_app()
selected_state = State("none", "selected_state")
@app.trigger_handler
def select_item():
item_id = get_trigger_args().get("id")
selected_state.set(item_id)
@app.page("/")
def index():
return Div(
Button("Select A", trigger="select_item", trigger_args={"id": "a"}),
Button("Select B", trigger="select_item", trigger_args={"id": "b"}),
Text(lambda: f"Selected: {selected_state.get()}", listen_to="selected_state"),
)
trigger_args are passed as URL query parameters; get_trigger_args() returns them as a dict[str, str] inside the handler.
Component Reference
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.
Base Components
| Component | Description | Key Parameters | Example |
|---|---|---|---|
Component |
Base class for all components | id, css, listen_to |
Custom component base |
TemplateComponent |
Render Jinja2 templates | template, context vars |
Custom HTML with templating |
Layout Components
| Component | Description | Key Parameters | Example |
|---|---|---|---|
Div |
Container div element | *children, id, css |
Div(Button("Click"), css="flex gap-2") |
Text |
Paragraph/text element | text, id, css |
Text("Hello", css="text-xl") |
Label |
Form label element | text, for_, id, css |
Label("Name:", for_="name") |
Form Components
| Component | Description | Key Parameters | Example |
|---|---|---|---|
Form |
Form container | *children, action, method |
Form(Input(...), Button(...)) |
Input |
Text input field | type, value, placeholder, listen_to |
Input(id="email", type="email") |
Textarea |
Multi-line text input | value, placeholder, rows |
Textarea(id="bio", rows=5) |
Select |
Dropdown select | options, value, listen_to |
Select(id="country", options=[...]) |
Checkbox |
Checkbox input | checked, id, listen_to |
Checkbox(id="agree", checked=True) |
Radio |
Radio button input | value, checked, name |
Radio(id="male", name="gender") |
Button |
Clickable button | *children, trigger, css |
Button("Click", trigger="action") |
Navigation Components
| Component | Description | Key Parameters | Example |
|---|---|---|---|
Link |
Semantic navigation link | *children, href, css |
Link("Home", href="/") |
Data Display Components
| Component | Description | Key Parameters | Example |
|---|---|---|---|
DataTable |
Tabular data display | data, columns, css |
DataTable(data=[{"name": "A"}]) |
Icon |
SVG icon component | svg, css |
Icon("<svg ...>...</svg>", css="w-6 h-6") |
Common Parameters (All Components)
| Parameter | Type | Description |
|---|---|---|
id |
str | None |
HTML id attribute. Required for state listening and OOB updates |
css |
str | Callable[[], str] | dict | None |
Tailwind CSS classes. For DataTable, can be a dict with keys: table, header, row, cell |
listen_to |
str | list[str] | None |
State name(s) to listen for changes. Triggers re-render when state updates |
trigger |
str | None |
Trigger name for HTMX POST actions (Button, Input, etc.) |
trigger_args |
dict[str, str] | None |
Query parameters to pass with trigger |
Navigation & Actions
Use Link for traditional navigation (SEO, bookmarking, new-tab support) and trigger for partial page updates:
Link(href="...") |
trigger="..." |
|
|---|---|---|
| Renders | <a href="..."> |
HTMX POST |
| URL changes | ✅ | ❌ |
| Open in new tab | ✅ | ❌ |
from inguitive import Link, Button
# Traditional navigation
Link("Home", href="/")
Link("Documentation", href="/docs", css="text-blue-500")
# Partial updates
Button("Save", trigger="save_form")
Button("Like", trigger="like_post", trigger_args={"id": "123"})
Project Structure
.
├── src/
│ └── inguitive/
│ ├── __init__.py # Public API
│ ├── components.py # Component classes
│ ├── state.py # Reactive state
│ ├── htmx.py # HTMX helpers
│ ├── fastapi.py # FastAPI integration
│ └── svg.py # SVG icon definitions
├── examples/
│ ├── counter_app.py # Per-session counter with theme toggle
│ ├── todo_app.py # CRUD with filtering and real-time count
│ ├── chat_app.py # Real-time chat
│ ├── navigation_demo.py # Link vs trigger patterns
│ ├── registration_form.py # Form handling
│ └── data_table_app.py # DataTable with sorting and filtering
├── tests/
│ └── test_*.py # Test files
├── pyproject.toml # Build configuration
└── README.md
Session Backends
inguitive uses session-scoped registries to isolate user state. Choose a backend based on your deployment needs:
| Backend | Use When | Persistence | Multi-Worker |
|---|---|---|---|
MemoryBackend |
Development, single worker | ❌ No (lost on restart) | ❌ No |
RedisBackend |
Production, multiple workers | ✅ Yes | ✅ Yes |
MemoryBackend (default) stores sessions in RAM - perfect for development. RedisBackend stores sessions in Redis for production deployments with multiple workers or persistent sessions.
from inguitive import create_app, MemoryBackend, RedisBackend
# Development: In-memory sessions (default, no config needed)
app = create_app()
# Or explicitly:
app = create_app(session_backend=MemoryBackend())
# Production: Redis-backed sessions for scaling
app = create_app(
session_backend=RedisBackend(
redis_url="redis://localhost:6379",
ttl_seconds=3600 # Session timeout: 1 hour
)
)
Requires pip install redis for RedisBackend.
Session Lifetime and Expiry
inguitive sessions are created automatically on first request and persist across page reloads. Each session has isolated component, state, and data registries.
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.
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.
Session Expiry:
- MemoryBackend: Sessions expire after
ttl_seconds(default: 3600 = 1 hour) of inactivity. Expired sessions are automatically cleaned up every N requests (configurable viasession_cleanup_interval). - RedisBackend: Sessions are stored in Redis with a TTL. Redis automatically expires keys after the configured
ttl_seconds, providing automatic cleanup.
Differences Between Backends:
| Aspect | MemoryBackend | RedisBackend |
|---|---|---|
| Persistence | Lost on process restart | Survives process restarts |
| Multi-worker | Not supported (shared memory) | Supported (Redis as shared store) |
| Cleanup | Manual/Periodic via cleanup_expired() |
Automatic via Redis TTL |
| Use Case | Development, testing | Production, scaling |
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.
Production Deployment
Before deploying your inguitive app to production, configure these security settings:
from inguitive import create_app, RedisBackend
app = create_app(
session_backend=RedisBackend(redis_url="redis://localhost:6379"),
session_cookie_secure=True, # Cookies only over HTTPS
session_cookie_httponly=True, # Prevent JavaScript access (default)
session_cookie_max_age=86400, # 24-hour session timeout
)
Checklist:
- ✅ Use
RedisBackend(notMemoryBackend) for persistence across workers - ✅ Set
session_cookie_secure=Truewhen using HTTPS - ✅ Verify
session_cookie_httponly=True(enabled by default) - ✅ Deploy with HTTPS (required for secure cookies)
Running the Demo
# From the repository root
uvicorn examples.counter_app:app --reload
# Then open http://localhost:8000
License
MIT License - see LICENSE for details.
Contact
- GitHub: j-strk
- Email: info@stork-software.de
- Issues: GitHub Issues
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file inguitive-0.2.0.tar.gz.
File metadata
- Download URL: inguitive-0.2.0.tar.gz
- Upload date:
- Size: 43.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/6.2.0 CPython/3.12.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b5b2206ec74ff2ea1ee0e57d3ad02316d3f0113f85a3ee7c0b40e85fbcc76f72
|
|
| MD5 |
11d3179a3002a4314686d0152b37eb74
|
|
| BLAKE2b-256 |
3f013f036d03bef950f9a8ff7d10e974eba172d899a16fcbcefadee224aa5152
|
File details
Details for the file inguitive-0.2.0-py3-none-any.whl.
File metadata
- Download URL: inguitive-0.2.0-py3-none-any.whl
- Upload date:
- Size: 27.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/6.2.0 CPython/3.12.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d448fcc374ef8485023a634e2957e4868064b796af0a778221b70d4c69b96b84
|
|
| MD5 |
0bfe03cd289ee1047a4c5f649f49507e
|
|
| BLAKE2b-256 |
6b0fe5936e9239d92b41be78b7c67b0bfbb3ff7c91d63cf6572396d65c027a9f
|