Metadata-Version: 2.4
Name: st-page-state
Version: 0.1.0
Summary: Declarative, typed, URL-synced state management for Streamlit
Project-URL: Homepage, https://github.com/ju-sants/st-page-state
Author-email: Juan <juan.bisposantos05@gmail.com>
License: MIT
License-File: LICENSE
Requires-Python: >=3.8
Requires-Dist: streamlit>=1.30
Description-Content-Type: text/markdown

# st-page-state

Declarative, typed, URL-aware state management for complex Streamlit apps.

**st-page-state** is an architectural pattern for Streamlit that replaces loose dictionary keys with strict, typed classes. It solves the friction of managing complex state, URL synchronization, and component communication in large-scale applications.

## ⚡ The Problem vs. The Solution

You have written code like this a thousand times. It is brittle, hard to type-hint, and syncing it with the URL requires boilerplate.

### ❌ Native Streamlit (The "Old" Way)

```python
# 1. Initialize logic scattered across the script
if "status" not in st.session_state:
    # 2. Manual URL parsing
    url_val = st.query_params.get("status", "pending")
    st.session_state["status"] = url_val

# 3. No type safety
status = st.session_state["status"] 

# 4. Manual URL updating on change
def update_status():
    st.query_params["status"] = st.session_state["status"]

st.selectbox("Status", ["pending", "active"], key="status", on_change=update_status)
```

### ✅ With st-page-state

```python
from st_page_state import PageState, StateVar

class FilterState(PageState):
    # Declarative, typed, and auto-synced
    status: str = StateVar(default="pending", url_key="status")

# Access it anywhere. It just works.
st.selectbox("Status", ["pending", "active"], **FilterState.bind("status"))
```

## 🚀 Why use this?

This library is designed for Streamlit power users and internal tooling teams who need maintainable codebases, not just quick scripts.

*   **Declarative Syntax:** Define state schemas like Pydantic models.
*   **Auto-Magic URL Sync:** Bidirectional synchronization. Change the state → updates URL. Change URL → updates state.
*   **Strict Typing:** Full support for type hints (`int`, `bool`, `datetime`, `list[str]`). Inputs from URLs are automatically cast to the correct type.
*   **Namespace Isolation:** Every `PageState` class has its own isolated namespace, preventing key collisions in large multi-page apps.
*   **Introspection:** Built-in methods to `.dump()` state for debugging or API payloads.

## 📦 Installation

```bash
pip install st-page-state
```

*(Requires Python 3.8+ and Streamlit >= 1.30)*

## 📖 Core Patterns

### 1. Complex Data Types & URLs

Handling lists or dates in query parameters usually requires manual parsing logic. `st-page-state` handles serialization automatically.

```python
class SearchState(PageState):
    # Automatically handles ?tags=python||api
    tags: list[str] = StateVar(default=[], url_key="tags")
    
    # Automatically handles ISO format dates
    start_date: datetime.date = StateVar(default=datetime.date.today(), url_key="start")
```

### 2. Value Mapping (Enums)

Decouple your internal logic (integers/IDs) from your public URLs (friendly strings).

```python
class TaskState(PageState):
    status: int = StateVar(
        default=0,
        url_key="status",
        value_map={
            "todo": "0",        # URL shows ?status=todo
            "in_progress": "1", # URL shows ?status=in_progress
            "done": "2"         # URL shows ?status=done
        }
    )

# Your code works with clean integers
if TaskState.status == 1:
    st.info("Task is in progress")
```

### 3. Lifecycle Hooks

Stop cluttering your UI code with side effects. Define `on_init` and `on_change` logic directly where the state lives.

```python
class AppState(PageState):
    theme: str = StateVar(default="light")

    @classmethod
    def on_init(cls):
        """Runs once when the session starts."""
        print("State initialized.")

    @classmethod
    def on_change(cls, field, old_value, new_value):
        """Runs whenever a specific field changes."""
        if field == "theme":
            print(f"Theme changed from {old_value} to {new_value}")
            # Trigger external analytics, logging, or database updates here
```

### 4. Widget Binding

The `.bind()` method returns the exact dictionary (`key` and `on_change`) Streamlit widgets expect.

```python
st.text_input("Search", **SearchState.bind("query"))
```

## 🛠️ Advanced Tooling

For complex apps, visibility into your state is critical.

```python
# Get a pure dictionary of current values (great for API payloads)
payload = UserState.dump() 

# Reset specific fields or the entire state to defaults
UserState.reset()
```

## 🤝 Contributing

We welcome contributions from the community. If you are solving complex state problems in Streamlit, we want to hear from you.

1.  Fork the repository
2.  Create your feature branch
3.  Install dev dependencies (`pip install -e .[dev]`)
4.  Run tests (`pytest`)
5.  Open a Pull Request

## 📄 License

Distributed under the MIT License. See `LICENSE` for more information.
