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.
- flask_silo-0.1.0/.gitignore +31 -0
- flask_silo-0.1.0/CHANGELOG.md +41 -0
- flask_silo-0.1.0/LICENSE +21 -0
- flask_silo-0.1.0/PKG-INFO +351 -0
- flask_silo-0.1.0/README.md +316 -0
- flask_silo-0.1.0/examples/basic_app.py +76 -0
- flask_silo-0.1.0/examples/data_processor.py +173 -0
- flask_silo-0.1.0/pyproject.toml +85 -0
- flask_silo-0.1.0/src/flask_silo/__init__.py +70 -0
- flask_silo-0.1.0/src/flask_silo/cleanup.py +98 -0
- flask_silo-0.1.0/src/flask_silo/errors.py +55 -0
- flask_silo-0.1.0/src/flask_silo/ext.py +307 -0
- flask_silo-0.1.0/src/flask_silo/files.py +159 -0
- flask_silo-0.1.0/src/flask_silo/py.typed +0 -0
- flask_silo-0.1.0/src/flask_silo/store.py +346 -0
- flask_silo-0.1.0/src/flask_silo/tasks.py +240 -0
- flask_silo-0.1.0/tests/__init__.py +0 -0
- flask_silo-0.1.0/tests/conftest.py +9 -0
- flask_silo-0.1.0/tests/test_cleanup.py +145 -0
- flask_silo-0.1.0/tests/test_files.py +85 -0
- flask_silo-0.1.0/tests/test_integration.py +266 -0
- flask_silo-0.1.0/tests/test_store.py +206 -0
- flask_silo-0.1.0/tests/test_tasks.py +187 -0
|
@@ -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)
|
flask_silo-0.1.0/LICENSE
ADDED
|
@@ -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
|
+
[](https://www.python.org/downloads/)
|
|
41
|
+
[](LICENSE)
|
|
42
|
+
[]()
|
|
43
|
+
[]()
|
|
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)
|