flask-silo 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.
@@ -0,0 +1,31 @@
1
+ # Byte-compiled
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+
6
+ # Distribution
7
+ dist/
8
+ build/
9
+ *.egg-info/
10
+ *.egg
11
+
12
+ # Virtual environments
13
+ .venv/
14
+ venv/
15
+ env/
16
+
17
+ # IDE
18
+ .idea/
19
+ .vscode/
20
+ *.swp
21
+ *.swo
22
+
23
+ # Testing
24
+ .coverage
25
+ htmlcov/
26
+ .pytest_cache/
27
+ .mypy_cache/
28
+
29
+ # OS
30
+ .DS_Store
31
+ Thumbs.db
@@ -0,0 +1,41 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [0.1.0] — 2026-02-24
9
+
10
+ ### Added
11
+
12
+ - `SessionStore` — thread-safe session state manager with TTL enforcement
13
+ - Namespace-based state isolation
14
+ - Expired-SID tracking for the 410 Gone pattern
15
+ - Configurable busy-check predicates to prevent cleanup of active sessions
16
+ - Lifecycle callbacks (`on_create`, `on_expire`)
17
+ - Lazy namespace initialisation for late registrations
18
+ - Custom SID generator support
19
+ - `CleanupDaemon` — background daemon thread for periodic stale-session purging
20
+ - Interruptible sleep via `threading.Event`
21
+ - Idempotent start/stop
22
+ - `BackgroundTask` — threaded task runner with progress tracking
23
+ - Progress percentage, status messages, and log entries
24
+ - Auto-complete on return, explicit complete/fail methods
25
+ - State snapshots via `TaskState` dataclass
26
+ - Progress clamping and timestamp tracking
27
+ - `FileStore` — per-session file storage management
28
+ - Automatic directory creation
29
+ - Save from bytes or file-like objects
30
+ - Session-isolated file listing and lookup
31
+ - Individual and bulk cleanup
32
+ - Total disk usage introspection
33
+ - `Silo` — Flask extension tying everything together
34
+ - Header-based session ID (`X-Session-ID`) with query-param fallback
35
+ - `before_request` / `after_request` lifecycle hooks
36
+ - 410 Gone responses for expired sessions on data endpoints
37
+ - Flask factory pattern support (`init_app`)
38
+ - Integrated file-store cleanup on session expiry and reset
39
+ - Comprehensive test suite (60+ tests) covering all components
40
+ - Type annotations with PEP 561 `py.typed` marker
41
+ - Two example applications (counter API, data processor)
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026
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,351 @@
1
+ Metadata-Version: 2.4
2
+ Name: flask-silo
3
+ Version: 0.1.0
4
+ Summary: Session-isolated state management for Flask APIs — TTL-enforced, thread-safe, with background tasks and file storage.
5
+ Project-URL: Homepage, https://github.com/yourusername/flask-silo
6
+ Project-URL: Repository, https://github.com/yourusername/flask-silo
7
+ Project-URL: Documentation, https://github.com/yourusername/flask-silo#readme
8
+ Project-URL: Issues, https://github.com/yourusername/flask-silo/issues
9
+ Project-URL: Changelog, https://github.com/yourusername/flask-silo/blob/main/CHANGELOG.md
10
+ Author-email: Pasindu Suraweera <pssuraweera2003@gmail.com>
11
+ License-Expression: MIT
12
+ License-File: LICENSE
13
+ Keywords: api,background-tasks,flask,isolation,session,state-management,ttl,upload
14
+ Classifier: Development Status :: 4 - Beta
15
+ Classifier: Framework :: Flask
16
+ Classifier: Intended Audience :: Developers
17
+ Classifier: License :: OSI Approved :: MIT License
18
+ Classifier: Operating System :: OS Independent
19
+ Classifier: Programming Language :: Python :: 3
20
+ Classifier: Programming Language :: Python :: 3.10
21
+ Classifier: Programming Language :: Python :: 3.11
22
+ Classifier: Programming Language :: Python :: 3.12
23
+ Classifier: Programming Language :: Python :: 3.13
24
+ Classifier: Topic :: Internet :: WWW/HTTP :: Session
25
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
26
+ Classifier: Typing :: Typed
27
+ Requires-Python: >=3.10
28
+ Requires-Dist: flask>=2.0
29
+ Provides-Extra: dev
30
+ Requires-Dist: mypy>=1.0; extra == 'dev'
31
+ Requires-Dist: pytest-cov>=4.0; extra == 'dev'
32
+ Requires-Dist: pytest>=7.0; extra == 'dev'
33
+ Requires-Dist: ruff>=0.4; extra == 'dev'
34
+ Description-Content-Type: text/markdown
35
+
36
+ # Flask-Silo
37
+
38
+ **Session-isolated state management for Flask APIs.**
39
+
40
+ [![Python 3.10+](https://img.shields.io/badge/python-3.10%2B-blue.svg)](https://www.python.org/downloads/)
41
+ [![License: MIT](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE)
42
+ [![Tests](https://img.shields.io/badge/tests-60%2B%20passing-brightgreen.svg)]()
43
+ [![Typed](https://img.shields.io/badge/typing-PEP%20561-blueviolet.svg)]()
44
+
45
+ ---
46
+
47
+ **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.
48
+
49
+ ## The Problem
50
+
51
+ Flask apps handling stateful workflows (file upload → process → report) need per-client state isolation. Without it:
52
+
53
+ - User A's upload overwrites User B's data
54
+ - Background tasks corrupt each other's progress
55
+ - Session expiry leaves orphaned files on disk
56
+ - Module-level globals create race conditions
57
+
58
+ **Flask-Silo** solves all of this with a clean, typed API.
59
+
60
+ ## Features
61
+
62
+ | Feature | Description |
63
+ |---|---|
64
+ | **Session Isolation** | Each client gets independent state via `X-Session-ID` header |
65
+ | **TTL Enforcement** | Daemon thread automatically cleans up idle sessions |
66
+ | **410 Gone Pattern** | Expired clients get `410` on data endpoints (not a silent empty session) |
67
+ | **Background Tasks** | Thread-based runner with progress %, log entries, and completion status |
68
+ | **File Management** | Per-session upload directories with automatic cleanup on expiry |
69
+ | **Busy Protection** | Custom predicates prevent cleanup of sessions with running tasks |
70
+ | **Lifecycle Hooks** | `on_create` / `on_expire` callbacks for monitoring and side effects |
71
+ | **Factory Pattern** | Supports Flask's `init_app()` factory pattern |
72
+ | **Fully Typed** | PEP 561 compliant with comprehensive type annotations |
73
+
74
+ ## Quick Start
75
+
76
+ ### Installation
77
+
78
+ ```bash
79
+ pip install flask-silo
80
+ ```
81
+
82
+ ### Minimal Example
83
+
84
+ ```python
85
+ from flask import Flask, jsonify
86
+ from flask_silo import Silo
87
+
88
+ app = Flask(__name__)
89
+ silo = Silo(app, ttl=3600) # 1-hour sessions
90
+
91
+ # Register a namespace — each client gets their own copy
92
+ silo.register('counter', lambda: {'value': 0})
93
+
94
+ @app.route('/api/increment', methods=['POST'])
95
+ def increment():
96
+ state = silo.state('counter')
97
+ state['value'] += 1
98
+ return jsonify({'value': state['value'], 'sid': silo.sid})
99
+
100
+ @app.route('/api/count')
101
+ def count():
102
+ return jsonify({'value': silo.state('counter')['value']})
103
+ ```
104
+
105
+ ```bash
106
+ # Client A
107
+ curl -X POST http://localhost:5000/api/increment
108
+ # → {"sid": "a1b2c3...", "value": 1}
109
+
110
+ # Client B (different session)
111
+ curl http://localhost:5000/api/count
112
+ # → {"value": 0} ← isolated!
113
+ ```
114
+
115
+ ## Full-Featured Example
116
+
117
+ ```python
118
+ from flask import Flask, jsonify, request
119
+ from flask_silo import Silo, BackgroundTask
120
+
121
+ app = Flask(__name__)
122
+ silo = Silo(app, ttl=3600)
123
+
124
+ # Multiple namespaces per session
125
+ silo.register('processing', lambda: {
126
+ 'data': None,
127
+ 'results': None,
128
+ 'task': BackgroundTask('process'),
129
+ })
130
+
131
+ # Per-session file storage (auto-cleaned on expiry)
132
+ uploads = silo.add_file_store('uploads', './uploads')
133
+
134
+ # These endpoints return 410 if session expired
135
+ silo.add_data_endpoints('/api/results', '/api/export')
136
+
137
+ # Don't clean up sessions with running tasks
138
+ silo.store.set_busy_check(
139
+ lambda sid, s: s['processing']['task'].is_running
140
+ )
141
+
142
+ @app.route('/api/upload', methods=['POST'])
143
+ def upload():
144
+ f = request.files['file']
145
+ path = uploads.save(silo.sid, f.filename, f)
146
+ silo.state('processing')['data'] = path
147
+ return jsonify({'message': f'Uploaded {f.filename}'})
148
+
149
+ @app.route('/api/process', methods=['POST'])
150
+ def process():
151
+ state = silo.state('processing')
152
+
153
+ def work(task, filepath):
154
+ for i in range(10):
155
+ # ... do work ...
156
+ task.update(progress=(i+1)*10, message=f'Step {i+1}/10')
157
+ task.log(f'Completed step {i+1}')
158
+ task.complete('Done!')
159
+
160
+ state['task'].start(work, state['data'])
161
+ return jsonify({'status': 'started'})
162
+
163
+ @app.route('/api/progress')
164
+ def progress():
165
+ return jsonify(silo.state('processing')['task'].state.to_dict())
166
+
167
+ @app.route('/api/reset', methods=['POST'])
168
+ def reset():
169
+ silo.reset_current() # clears state + files
170
+ return jsonify({'message': 'Reset'})
171
+ ```
172
+
173
+ ## Architecture
174
+
175
+ ```
176
+ ┌─────────────────────────────────────────────────────────────┐
177
+ │ Flask App │
178
+ │ │
179
+ │ ┌───────────────────────────────────────────────────────┐ │
180
+ │ │ Silo (Extension) │ │
181
+ │ │ │ │
182
+ │ │ before_request ──→ Extract SID ──→ Load/Create State │ │
183
+ │ │ after_request ──→ Set X-Session-ID header │ │
184
+ │ │ │ │
185
+ │ │ ┌─────────────┐ ┌──────────────┐ ┌─────────────┐ │ │
186
+ │ │ │SessionStore │ │CleanupDaemon │ │ FileStore(s)│ │ │
187
+ │ │ │ │ │ │ │ │ │ │
188
+ │ │ │ _sessions{} │◄─│ cleanup() │ │ base_dir/ │ │ │
189
+ │ │ │ _expired{} │ │ every 60s │──│ {sid}/ │ │ │
190
+ │ │ │ _factories{}│ │ │ │ files... │ │ │
191
+ │ │ └─────────────┘ └──────────────┘ └─────────────┘ │ │
192
+ │ │ │ │
193
+ │ │ Session Dict: │ │
194
+ │ │ ┌──────────────────────────────────────────────────┐ │ │
195
+ │ │ │ { 'namespace_a': {...}, │ │ │
196
+ │ │ │ 'namespace_b': {..., task: BackgroundTask}, │ │ │
197
+ │ │ │ '_meta': {created_at, last_active, sid} } │ │ │
198
+ │ │ └──────────────────────────────────────────────────┘ │ │
199
+ │ └───────────────────────────────────────────────────────┘ │
200
+ └─────────────────────────────────────────────────────────────┘
201
+ ```
202
+
203
+ ## API Reference
204
+
205
+ ### `Silo` — Flask Extension
206
+
207
+ ```python
208
+ silo = Silo(
209
+ app=None, # Flask app (or use init_app)
210
+ ttl=3600, # Session lifetime (seconds)
211
+ cleanup_interval=60, # Cleanup frequency (seconds)
212
+ expired_retain=7200, # Remember expired SIDs for 410 (seconds)
213
+ header="X-Session-ID", # Header name for session ID
214
+ query_param="_sid", # Query param fallback (for download links)
215
+ min_sid_length=16, # Minimum SID length to accept
216
+ auto_cleanup=True, # Start cleanup daemon automatically
217
+ api_prefix="/api/", # URL prefix triggering session handling
218
+ )
219
+ ```
220
+
221
+ | Method | Description |
222
+ |---|---|
223
+ | `silo.register(name, factory)` | Register a state namespace |
224
+ | `silo.state(namespace)` | Get namespace state for current request |
225
+ | `silo.sid` | Current session ID (property) |
226
+ | `silo.add_file_store(name, dir)` | Add per-session file storage |
227
+ | `silo.file_store(name)` | Get a registered file store |
228
+ | `silo.add_data_endpoints(*paths)` | Mark endpoints for 410 on expiry |
229
+ | `silo.reset_current()` | Reset current session + cleanup files |
230
+ | `silo.init_app(app)` | Deferred initialisation (factory pattern) |
231
+ | `silo.stop()` | Stop cleanup daemon |
232
+
233
+ ### `SessionStore` — Core State Manager
234
+
235
+ ```python
236
+ store = SessionStore(ttl=3600, cleanup_interval=60, expired_retain=7200)
237
+ store.register_namespace('ns', lambda: {'key': 'value'})
238
+ ```
239
+
240
+ | Method | Description |
241
+ |---|---|
242
+ | `store.get(sid)` | Get/create full session dict |
243
+ | `store.get_namespace(sid, ns)` | Get specific namespace state |
244
+ | `store.touch(sid)` | Reset TTL timer |
245
+ | `store.exists(sid)` | Check if session is active |
246
+ | `store.is_expired(sid)` | Check if SID was recently expired |
247
+ | `store.cleanup()` | Run cleanup pass, returns expired SIDs |
248
+ | `store.reset(sid)` | Reset to fresh state |
249
+ | `store.destroy(sid)` | Remove without expiry tracking |
250
+ | `store.set_busy_check(fn)` | Set cleanup veto predicate |
251
+ | `store.on_create(callback)` | Register creation callback |
252
+ | `store.on_expire(callback)` | Register expiry callback |
253
+ | `store.active_count` | Number of active sessions |
254
+ | `store.expired_count` | Number of tracked expired SIDs |
255
+
256
+ ### `BackgroundTask` — Progress-Tracked Threading
257
+
258
+ ```python
259
+ task = BackgroundTask('classify')
260
+
261
+ def work(task, filepath):
262
+ task.update(progress=50, message='Halfway')
263
+ task.log('Processing batch 5/10')
264
+ task.complete('All done')
265
+
266
+ task.start(work, '/path/to/file')
267
+ ```
268
+
269
+ | Method / Property | Description |
270
+ |---|---|
271
+ | `task.start(target, *args, **kwargs)` | Run target in daemon thread |
272
+ | `task.update(progress, message)` | Update progress (0–100) |
273
+ | `task.log(message)` | Append log entry |
274
+ | `task.complete(message)` | Mark as successfully complete |
275
+ | `task.fail(error)` | Mark as failed |
276
+ | `task.reset()` | Reset for re-use |
277
+ | `task.state` | `TaskState` snapshot |
278
+ | `task.is_running` | Currently executing? |
279
+ | `task.is_complete` | Finished successfully? |
280
+ | `task.is_failed` | Failed with error? |
281
+
282
+ ### `FileStore` — Per-Session File Management
283
+
284
+ ```python
285
+ fs = FileStore('/tmp/uploads')
286
+ path = fs.save('sid-123', 'report.xlsx', file_obj)
287
+ fs.cleanup('sid-123')
288
+ ```
289
+
290
+ | Method | Description |
291
+ |---|---|
292
+ | `fs.session_dir(sid)` | Get/create session directory |
293
+ | `fs.save(sid, filename, data)` | Save file (bytes or file-like) |
294
+ | `fs.get_path(sid, filename)` | Get path or `None` |
295
+ | `fs.list_files(sid)` | List filenames in session dir |
296
+ | `fs.cleanup(sid)` | Remove session's files |
297
+ | `fs.cleanup_all()` | Remove all session dirs |
298
+ | `fs.total_size_bytes` | Total disk usage |
299
+
300
+ ## The 410 Gone Pattern
301
+
302
+ 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:
303
+
304
+ ```
305
+ Client Server
306
+ │ │
307
+ ├── Upload file ────────► │ ✓ Session created (SID: abc)
308
+ │ │
309
+ │ ... 1 hour passes ... │
310
+ │ │ ← Cleanup daemon expires SID abc
311
+ │ │
312
+ ├── GET /api/report ────► │ 410 Gone (SID abc was expired)
313
+ │ │
314
+ └── Upload file ────────► │ ✓ Session re-created (same SID)
315
+ ```
316
+
317
+ This enables clean frontend handling:
318
+
319
+ ```javascript
320
+ if (response.status === 410) {
321
+ clearSession();
322
+ showToast('Session expired — please re-upload');
323
+ redirect('/upload');
324
+ }
325
+ ```
326
+
327
+ ## Testing
328
+
329
+ ```bash
330
+ # Install dev dependencies
331
+ pip install -e ".[dev]"
332
+
333
+ # Run tests
334
+ pytest
335
+
336
+ # With coverage
337
+ pytest --cov=flask_silo --cov-report=html
338
+
339
+ # Type checking
340
+ mypy src/flask_silo
341
+ ```
342
+
343
+ ## How It Was Born
344
+
345
+ This library was extracted from a production **PH 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.
346
+
347
+ 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.
348
+
349
+ ## License
350
+
351
+ [MIT](LICENSE)