Skip to main content

Session-isolated state management for Flask APIs - TTL-enforced, thread-safe, with background tasks and file storage.

Project description

Flask-Silo

Session-isolated state management for Flask APIs.

Python 3.10+ License: MIT Tests Typed


Flask-Silo gives each API client its own isolated state - like giving every user their own private workspace on the server. Born from production data-processing pipelines where multiple users upload files, run background tasks, and generate reports simultaneously.

The Problem

Flask apps handling stateful workflows (file upload → process → report) need per-client state isolation. Without it:

  • User A's upload overwrites User B's data
  • Background tasks corrupt each other's progress
  • Session expiry leaves orphaned files on disk
  • Module-level globals create race conditions

Flask-Silo solves all of this with a clean, typed API.

Features

Feature Description
Session Isolation Each client gets independent state via X-Session-ID header
TTL Enforcement Daemon thread automatically cleans up idle sessions
410 Gone Pattern Expired clients get 410 on data endpoints (not a silent empty session)
Background Tasks Thread-based runner with progress %, log entries, and completion status
File Management Per-session upload directories with automatic cleanup on expiry
Busy Protection Custom predicates prevent cleanup of sessions with running tasks
Lifecycle Hooks on_create / on_expire callbacks for monitoring and side effects
Factory Pattern Supports Flask's init_app() factory pattern
Fully Typed PEP 561 compliant with comprehensive type annotations

Quick Start

Installation

pip install flask-silo

Minimal Example

from flask import Flask, jsonify
from flask_silo import Silo

app = Flask(__name__)
silo = Silo(app, ttl=3600)  # 1-hour sessions

# Register a namespace - each client gets their own copy
silo.register('counter', lambda: {'value': 0})

@app.route('/api/increment', methods=['POST'])
def increment():
    state = silo.state('counter')
    state['value'] += 1
    return jsonify({'value': state['value'], 'sid': silo.sid})

@app.route('/api/count')
def count():
    return jsonify({'value': silo.state('counter')['value']})
# Client A
curl -X POST http://localhost:5000/api/increment
# → {"sid": "a1b2c3...", "value": 1}

# Client B (different session)
curl http://localhost:5000/api/count
# → {"value": 0}  ← isolated!

Full-Featured Example

from flask import Flask, jsonify, request
from flask_silo import Silo, BackgroundTask

app = Flask(__name__)
silo = Silo(app, ttl=3600)

# Multiple namespaces per session
silo.register('processing', lambda: {
    'data': None,
    'results': None,
    'task': BackgroundTask('process'),
})

# Per-session file storage (auto-cleaned on expiry)
uploads = silo.add_file_store('uploads', './uploads')

# These endpoints return 410 if session expired
silo.add_data_endpoints('/api/results', '/api/export')

# Don't clean up sessions with running tasks
silo.store.set_busy_check(
    lambda sid, s: s['processing']['task'].is_running
)

@app.route('/api/upload', methods=['POST'])
def upload():
    f = request.files['file']
    path = uploads.save(silo.sid, f.filename, f)
    silo.state('processing')['data'] = path
    return jsonify({'message': f'Uploaded {f.filename}'})

@app.route('/api/process', methods=['POST'])
def process():
    state = silo.state('processing')

    def work(task, filepath):
        for i in range(10):
            # ... do work ...
            task.update(progress=(i+1)*10, message=f'Step {i+1}/10')
            task.log(f'Completed step {i+1}')
        task.complete('Done!')

    state['task'].start(work, state['data'])
    return jsonify({'status': 'started'})

@app.route('/api/progress')
def progress():
    return jsonify(silo.state('processing')['task'].state.to_dict())

@app.route('/api/reset', methods=['POST'])
def reset():
    silo.reset_current()  # clears state + files
    return jsonify({'message': 'Reset'})

Architecture

┌─────────────────────────────────────────────────────────────┐
│                        Flask App                            │
│                                                             │
│  ┌───────────────────────────────────────────────────────┐  │
│  │                    Silo (Extension)                    │  │
│  │                                                       │  │
│  │  before_request ──→ Extract SID ──→ Load/Create State │  │
│  │  after_request  ──→ Set X-Session-ID header           │  │
│  │                                                       │  │
│  │  ┌─────────────┐  ┌──────────────┐  ┌─────────────┐  │  │
│  │  │SessionStore │  │CleanupDaemon │  │ FileStore(s)│  │  │
│  │  │             │  │              │  │             │  │  │
│  │  │ _sessions{} │◄─│ cleanup()    │  │ base_dir/   │  │  │
│  │  │ _expired{}  │  │ every 60s    │──│  {sid}/     │  │  │
│  │  │ _factories{}│  │              │  │   files...  │  │  │
│  │  └─────────────┘  └──────────────┘  └─────────────┘  │  │
│  │                                                       │  │
│  │  Session Dict:                                        │  │
│  │  ┌──────────────────────────────────────────────────┐ │  │
│  │  │ { 'namespace_a': {...},                          │ │  │
│  │  │   'namespace_b': {..., task: BackgroundTask},    │ │  │
│  │  │   '_meta': {created_at, last_active, sid} }      │ │  │
│  │  └──────────────────────────────────────────────────┘ │  │
│  └───────────────────────────────────────────────────────┘  │
└─────────────────────────────────────────────────────────────┘

API Reference

Silo - Flask Extension

silo = Silo(
    app=None,              # Flask app (or use init_app)
    ttl=3600,              # Session lifetime (seconds)
    cleanup_interval=60,   # Cleanup frequency (seconds)
    expired_retain=7200,   # Remember expired SIDs for 410 (seconds)
    header="X-Session-ID", # Header name for session ID
    query_param="_sid",    # Query param fallback (for download links)
    min_sid_length=16,     # Minimum SID length to accept
    auto_cleanup=True,     # Start cleanup daemon automatically
    api_prefix="/api/",    # URL prefix triggering session handling
)
Method Description
silo.register(name, factory) Register a state namespace
silo.state(namespace) Get namespace state for current request
silo.sid Current session ID (property)
silo.add_file_store(name, dir) Add per-session file storage
silo.file_store(name) Get a registered file store
silo.add_data_endpoints(*paths) Mark endpoints for 410 on expiry
silo.reset_current() Reset current session + cleanup files
silo.init_app(app) Deferred initialisation (factory pattern)
silo.stop() Stop cleanup daemon

SessionStore - Core State Manager

store = SessionStore(ttl=3600, cleanup_interval=60, expired_retain=7200)
store.register_namespace('ns', lambda: {'key': 'value'})
Method Description
store.get(sid) Get/create full session dict
store.get_namespace(sid, ns) Get specific namespace state
store.touch(sid) Reset TTL timer
store.exists(sid) Check if session is active
store.is_expired(sid) Check if SID was recently expired
store.cleanup() Run cleanup pass, returns expired SIDs
store.reset(sid) Reset to fresh state
store.destroy(sid) Remove without expiry tracking
store.set_busy_check(fn) Set cleanup veto predicate
store.on_create(callback) Register creation callback
store.on_expire(callback) Register expiry callback
store.active_count Number of active sessions
store.expired_count Number of tracked expired SIDs

BackgroundTask - Progress-Tracked Threading

task = BackgroundTask('classify')

def work(task, filepath):
    task.update(progress=50, message='Halfway')
    task.log('Processing batch 5/10')
    task.complete('All done')

task.start(work, '/path/to/file')
Method / Property Description
task.start(target, *args, **kwargs) Run target in daemon thread
task.update(progress, message) Update progress (0–100)
task.log(message) Append log entry
task.complete(message) Mark as successfully complete
task.fail(error) Mark as failed
task.reset() Reset for re-use
task.state TaskState snapshot
task.is_running Currently executing?
task.is_complete Finished successfully?
task.is_failed Failed with error?

FileStore - Per-Session File Management

fs = FileStore('/tmp/uploads')
path = fs.save('sid-123', 'report.xlsx', file_obj)
fs.cleanup('sid-123')
Method Description
fs.session_dir(sid) Get/create session directory
fs.save(sid, filename, data) Save file (bytes or file-like)
fs.get_path(sid, filename) Get path or None
fs.list_files(sid) List filenames in session dir
fs.cleanup(sid) Remove session's files
fs.cleanup_all() Remove all session dirs
fs.total_size_bytes Total disk usage

The 410 Gone Pattern

When a session expires, instead of silently creating a new empty session, Flask-Silo tracks the old SID and returns 410 Gone on data-dependent endpoints:

Client                    Server
  │                         │
  ├── Upload file ────────► │  ✓ Session created (SID: abc)
  │                         │
  │   ... 1 hour passes ... │
  │                         │  ← Cleanup daemon expires SID abc
  │                         │
  ├── GET /api/report ────► │  410 Gone (SID abc was expired)
  │                         │
  └── Upload file ────────► │  ✓ Session re-created (same SID)

This enables clean frontend handling:

if (response.status === 410) {
  clearSession();
  showToast('Session expired - please re-upload');
  redirect('/upload');
}

Testing

# Install dev dependencies
pip install -e ".[dev]"

# Run tests
pytest

# With coverage
pytest --cov=flask_silo --cov-report=html

# Type checking
mypy src/flask_silo

How It Was Born

This library was extracted from a production QSR Analysis Hub - a Flask + Next.js application that analyses restaurant void bills, deleted items, and staff discounts. The server needed to handle multiple concurrent users, each uploading Excel files, running AI classification tasks, reviewing results, and exporting reports - all with complete session isolation.

The patterns that emerged (session stores, TTL cleanup daemons, 410 Gone for expired sessions, background task progress tracking, per-session file storage) proved generic enough to become a reusable library.

License

MIT

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

flask_silo-0.1.3.tar.gz (26.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

flask_silo-0.1.3-py3-none-any.whl (20.8 kB view details)

Uploaded Python 3

File details

Details for the file flask_silo-0.1.3.tar.gz.

File metadata

  • Download URL: flask_silo-0.1.3.tar.gz
  • Upload date:
  • Size: 26.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.10.11

File hashes

Hashes for flask_silo-0.1.3.tar.gz
Algorithm Hash digest
SHA256 c50529728571d50dbbab78a4384364128eed6364d4c3b506595dfac38c295055
MD5 4f1fe604d28a4226c38df1f0cf654197
BLAKE2b-256 7ce26682df7085fe19f3a840d71f5326fe5b0961ec130aeb670923acb249be3d

See more details on using hashes here.

File details

Details for the file flask_silo-0.1.3-py3-none-any.whl.

File metadata

  • Download URL: flask_silo-0.1.3-py3-none-any.whl
  • Upload date:
  • Size: 20.8 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.10.11

File hashes

Hashes for flask_silo-0.1.3-py3-none-any.whl
Algorithm Hash digest
SHA256 2f4eac0ae23fb8f325b38c77d95a2d08f67a94e3abf255dfba5cc978c8400454
MD5 a56cade69567cc79c9a28b8f92c3a93e
BLAKE2b-256 4090768ab2387d98728def0e3248ff72c56dd133b843b53de35b05d96e7c0e87

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page