Metadata-Version: 2.4
Name: tiferet-streamlit
Version: 1.0.0a8
Summary: A Streamlit extension for the Tiferet Framework
License: MIT
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: tiferet>=2.0.3
Requires-Dist: streamlit>=1.30.0
Requires-Dist: toml>=0.10
Provides-Extra: test
Requires-Dist: pytest>=7.0; extra == "test"
Requires-Dist: pytest-cov; extra == "test"
Dynamic: license-file

# tiferet-streamlit

A Streamlit extension for the [Tiferet Framework](https://github.com/greatstrength/tiferet) — build multi-page Streamlit applications with Domain-Driven Design.

## Installation

```bash
pip install tiferet-streamlit
```

Requires `tiferet>=2.0.3` and `streamlit>=1.30.0`. `build_streamlit_app()` constructs the app via `tiferet.blueprints.app.build_app(...)`, yielding an `AppSessionContext`, and performs a runtime check on that constructed object, raising `INCOMPATIBLE_APP_CONTEXT` if it does not expose a `run(feature_id, headers, data)`-shaped callable — guarding against any future incompatible `tiferet` release.

## Quick Start

```python
import streamlit as st
from tiferet_streamlit import StreamlitApp, ViewContext

class HomeView(ViewContext):
    def init_state(self):
        self.session.set('count', 0)

    def render(self):
        count = self.session.get('count')
        st.title('Home')
        st.write(f'Count: {count}')
        if st.button('Increment'):
            self.session.set('count', count + 1)
            st.rerun()

StreamlitApp('my_interface', pages={'/': HomeView})
```

> The counter above wires its widget by hand. See [docs/guides/widgets.md](docs/guides/widgets.md) for `ViewContext` binding methods that sync a widget's value and dispatch for you.

## Core Concepts

### ViewContext

The code-behind for a Streamlit page. Manages state via `SessionCacheContext`, dispatches Tiferet features via `AppSessionContext`, and defines UI through `render()`.

- **`init_state()`** — Called once on first construction. Override to set initial state.
- **`dispatch(feature_id, headers=None, **data)`** — Execute a Tiferet feature.
- **`bind_widget`, `bind_widget_dispatch`, `bind_trigger`** — Bind a native Streamlit widget's value and dispatch on change; see [docs/guides/widgets.md](docs/guides/widgets.md).
- **`render()`** — Override to define Streamlit widgets.
- **`__call__()`** — Makes the view callable for `st.Page` composition.

### ViewComponent

A lightweight, prop-driven sub-component with parent `ViewContext` access.

```python
from tiferet_streamlit import ViewComponent

class Counter(ViewComponent):
    def render(self, label='Count', start=0):
        count = self.ctx.session.get('count') or start
        st.write(f'{label}: {count}')
```

### SessionCacheContext

Cache backed by `st.session_state` with namespace isolation for multi-page apps.

```python
from tiferet_streamlit import SessionCacheContext

cache = SessionCacheContext(namespace='my_view')
cache.set('key', 'value')
cache.get('key')  # 'value'
```

### Multi-Page Applications

Use `StreamlitApp` (or `build_streamlit_app`) to register multiple views with routes:

```python
StreamlitApp('my_interface', pages={
    '/': HomeView,
    '/about': AboutView,
    '/settings': SettingsView,
})
```

### Config-Driven Pages

Define pages as `Page` domain objects for YAML-driven configuration:

```python
from tiferet_streamlit import Page, StreamlitApp

pages = [
    Page(route='/', title='Home', icon='🏠',
         view_module_path='app.views.home', view_class_name='HomeView'),
    Page(route='/about', title='About', icon='ℹ️',
         view_module_path='app.views.about', view_class_name='AboutView'),
]

StreamlitApp('my_interface', page_configs=pages)
```

### ViewService-Backed Page Configuration

Source pages from a `ViewService` implementation (e.g. a YAML-backed repository registered in your app's DI configuration) instead of constructing `Page` objects in Python. `build_streamlit_app`/`StreamlitApp` never import `ViewService` directly — pass a `get_page_configs` handler that resolves one through `get_view_service`, the sole DI-mediated accessor:

```python
from tiferet_streamlit import StreamlitApp, get_view_service

StreamlitApp(
    'my_interface',
    get_page_configs=lambda app: get_view_service(app).list_pages(),
)
```

`get_view_service(app, service_id='view_service', flags=None)` resolves the dependency through the app's DI context and verifies it implements `ViewService`, raising a structured `INVALID_VIEW_SERVICE_ID` error otherwise.

### Config-Driven Theming

Declare a `Theme` as data and pass it to `StreamlitApp` to reach Streamlit's own native appearance controls, instead of hand-patching individual pages with one-off style tweaks:

```python
from tiferet_streamlit import Theme, StreamlitApp

theme = Theme(
    primary_color='#FF4B4B',
    background_color='#FFFFFF',
    text_color='#262730',
    custom_css='.stButton button { border-radius: 8px; }',
)

StreamlitApp('my_interface', pages={'/': HomeView}, theme=theme)
```

A declared `Theme` reaches two separate, independent paths:

- **Native `[theme]` fields** (`base`, `primary_color`, `background_color`, `secondary_background_color`, `text_color`, `font`) are merged into `.streamlit/config.toml`'s `[theme]` section on disk, preserving any unrelated settings already in that file. **Streamlit reads `config.toml` once at server startup, so this write takes effect on the *next* Streamlit process start — it does not re-theme the app that is currently running.**
- **`custom_css`** is injected via `st.markdown(..., unsafe_allow_html=True)` on every app run, so it takes effect immediately, including on the current rerun.

Omitting `theme` entirely leaves existing behavior unchanged: no `config.toml` write and no CSS injection.

### Feature Dispatch

Views dispatch Tiferet features for backend logic:

```python
class CalcView(ViewContext):
    def render(self):
        a = st.number_input('a')
        b = st.number_input('b')
        if st.button('Add'):
            result = self.dispatch('calc.add', a=a, b=b)
            st.write(f'Result: {result}')
```

> This example dispatches on every rerun rather than only on a real change. See [docs/guides/widgets.md](docs/guides/widgets.md) for the `bind_widget_dispatch` and `bind_trigger` methods that fix both hand-wired patterns above.

## API Reference

| Export | Module | Description |
|--------|--------|-------------|
| `build_streamlit_app` | `blueprints.streamlit` | Primary entry point blueprint function |
| `StreamlitApp` | `blueprints` | Alias for `build_streamlit_app` |
| `Page` | `domain.view` | Page configuration domain object |
| `Theme` | `domain.theme` | App appearance declared as data |
| `ViewService` | `interfaces.view` | Abstract service for page management |
| `get_view_service` | `contexts.di` | DI-mediated, verified accessor for a `ViewService` dependency |
| `SessionCacheContext` | `contexts.session` | Session-state-backed cache with namespacing |
| `ViewContext` | `contexts.view` | Page code-behind with lifecycle management and widget binding ([guide](docs/guides/widgets.md)) |
| `ViewComponent` | `contexts.view` | Prop-driven sub-component with delegated widget binding |
| `PageContext` | `contexts.page` | Multi-page navigation manager |

## Development

```bash
# Clone and set up
git clone https://github.com/greatstrength/tiferet-streamlit.git
cd tiferet-streamlit
python -m venv .venv
source .venv/bin/activate
pip install -e .[test]

# Run tests
pytest --verbose
```

## License

MIT
