sacs 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.
- sacs-0.1.0/LICENSE +21 -0
- sacs-0.1.0/PKG-INFO +223 -0
- sacs-0.1.0/README.md +181 -0
- sacs-0.1.0/pyproject.toml +67 -0
- sacs-0.1.0/sacs/__init__.py +12 -0
- sacs-0.1.0/sacs/alembic.ini +40 -0
- sacs-0.1.0/sacs/app.py +97 -0
- sacs-0.1.0/sacs/audit.py +55 -0
- sacs-0.1.0/sacs/cli.py +791 -0
- sacs-0.1.0/sacs/config.py +111 -0
- sacs-0.1.0/sacs/db.py +116 -0
- sacs-0.1.0/sacs/deps.py +101 -0
- sacs-0.1.0/sacs/events.py +233 -0
- sacs-0.1.0/sacs/migrations/env.py +56 -0
- sacs-0.1.0/sacs/migrations/script.py.mako +24 -0
- sacs-0.1.0/sacs/migrations/versions/ade8928a2588_initial_schema.py +420 -0
- sacs-0.1.0/sacs/models.py +461 -0
- sacs-0.1.0/sacs/rbac.py +106 -0
- sacs-0.1.0/sacs/routers/__init__.py +10 -0
- sacs-0.1.0/sacs/routers/academics.py +199 -0
- sacs-0.1.0/sacs/routers/auth.py +161 -0
- sacs-0.1.0/sacs/routers/bell.py +208 -0
- sacs-0.1.0/sacs/routers/platform.py +250 -0
- sacs-0.1.0/sacs/routers/realtime.py +78 -0
- sacs-0.1.0/sacs/routers/requests.py +158 -0
- sacs-0.1.0/sacs/routers/schools.py +407 -0
- sacs-0.1.0/sacs/routers/timetable.py +237 -0
- sacs-0.1.0/sacs/scheduler.py +200 -0
- sacs-0.1.0/sacs/security.py +99 -0
- sacs-0.1.0/sacs/seed.py +307 -0
- sacs-0.1.0/sacs/workflow.py +386 -0
- sacs-0.1.0/sacs.egg-info/PKG-INFO +223 -0
- sacs-0.1.0/sacs.egg-info/SOURCES.txt +44 -0
- sacs-0.1.0/sacs.egg-info/dependency_links.txt +1 -0
- sacs-0.1.0/sacs.egg-info/entry_points.txt +2 -0
- sacs-0.1.0/sacs.egg-info/requires.txt +20 -0
- sacs-0.1.0/sacs.egg-info/top_level.txt +1 -0
- sacs-0.1.0/setup.cfg +4 -0
- sacs-0.1.0/tests/test_auth.py +67 -0
- sacs-0.1.0/tests/test_bell.py +113 -0
- sacs-0.1.0/tests/test_cli.py +52 -0
- sacs-0.1.0/tests/test_membership.py +139 -0
- sacs-0.1.0/tests/test_packaging.py +81 -0
- sacs-0.1.0/tests/test_realtime.py +70 -0
- sacs-0.1.0/tests/test_tenant_isolation.py +84 -0
- sacs-0.1.0/tests/test_workflow.py +108 -0
sacs-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ivanbright
|
|
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.
|
sacs-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: sacs
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: SACS - Smart Academic Coordination System: a real-time multi-tenant school coordination platform with a one-command live demo.
|
|
5
|
+
Author: ivanbright
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/ivanbright/sacs
|
|
8
|
+
Project-URL: Repository, https://github.com/ivanbright/sacs
|
|
9
|
+
Project-URL: Issues, https://github.com/ivanbright/sacs/issues
|
|
10
|
+
Project-URL: Changelog, https://github.com/ivanbright/sacs/blob/main/CHANGELOG.md
|
|
11
|
+
Keywords: education,school,timetable,multi-tenant,fastapi,realtime
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Environment :: Console
|
|
14
|
+
Classifier: Intended Audience :: Education
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Topic :: Education
|
|
22
|
+
Requires-Python: >=3.11
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
License-File: LICENSE
|
|
25
|
+
Requires-Dist: fastapi>=0.110
|
|
26
|
+
Requires-Dist: uvicorn[standard]>=0.27
|
|
27
|
+
Requires-Dist: sqlalchemy>=2.0
|
|
28
|
+
Requires-Dist: alembic>=1.13
|
|
29
|
+
Requires-Dist: typer>=0.12
|
|
30
|
+
Requires-Dist: rich>=13.0
|
|
31
|
+
Requires-Dist: httpx>=0.27
|
|
32
|
+
Requires-Dist: email-validator>=2.0
|
|
33
|
+
Requires-Dist: tzdata>=2024.1; platform_system != "Linux"
|
|
34
|
+
Provides-Extra: dev
|
|
35
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
36
|
+
Requires-Dist: httpx>=0.27; extra == "dev"
|
|
37
|
+
Requires-Dist: build>=1.0; extra == "dev"
|
|
38
|
+
Requires-Dist: twine>=5.0; extra == "dev"
|
|
39
|
+
Provides-Extra: postgres
|
|
40
|
+
Requires-Dist: psycopg[binary]>=3.1; extra == "postgres"
|
|
41
|
+
Dynamic: license-file
|
|
42
|
+
|
|
43
|
+
# SACS — Smart Academic Coordination System
|
|
44
|
+
|
|
45
|
+
A real-time, multi-tenant school coordination platform that runs entirely from the terminal.
|
|
46
|
+
SACS coordinates **schools** (tenants), **staff**, **timetables**, **lesson sessions**, a
|
|
47
|
+
**lesson-completion approval workflow**, and a deterministic **software bell scheduler**,
|
|
48
|
+
streaming every event to authenticated workspaces over WebSockets.
|
|
49
|
+
|
|
50
|
+
There is no browser frontend: the whole product is exercised through the `sacs` CLI, which
|
|
51
|
+
talks to one shared FastAPI backend exactly like a real client would.
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## Quick start
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
pip install sacs # then:
|
|
59
|
+
sacs demo # migrate, seed, start the server, open the live dashboard
|
|
60
|
+
sacs status # show whether the demonstration is running
|
|
61
|
+
sacs demo reset # destroy ONLY the local demo database (requires confirmation)
|
|
62
|
+
sacs demo stop # stop the running demonstration
|
|
63
|
+
sacs check # verify dependencies and running environment
|
|
64
|
+
sacs version
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
`sacs demo` is the product. It:
|
|
68
|
+
|
|
69
|
+
1. migrates the database to the latest schema (Alembic) and seeds two schools;
|
|
70
|
+
2. starts the shared SACS service on a local port;
|
|
71
|
+
3. authenticates **five independently authorized identities** against the real API;
|
|
72
|
+
4. shows a live multi-workspace dashboard (Rich) with keyboard commands for the teacher
|
|
73
|
+
submit → DOS review → head decide workflow, bell operations, and school administration.
|
|
74
|
+
|
|
75
|
+
The demonstration persists between restarts (`sacs demo` again keeps your data; use
|
|
76
|
+
`sacs demo reset` for a clean slate).
|
|
77
|
+
|
|
78
|
+
### Demo identities
|
|
79
|
+
|
|
80
|
+
| Workspace | Email | Role | What they can do |
|
|
81
|
+
| ---------- | ------------------------------ | ------------- | -------------------------------------------------- |
|
|
82
|
+
| Platform | `platform@demo.sacs` | platform admin | create/suspend/reactivate schools, health, audit |
|
|
83
|
+
| Head | `head.northstar@demo.sacs` | school head | final approve/reject, staff & academics, bells |
|
|
84
|
+
| DOS | `dos.northstar@demo.sacs` | director of studies | review, forward, return requests; timetable |
|
|
85
|
+
| Teacher | `teacher.northstar@demo.sacs` | teacher | submit completion requests for own lessons |
|
|
86
|
+
| Bell | `bell.northstar@demo.sacs` | bell monitor | monitor bell events in real time |
|
|
87
|
+
|
|
88
|
+
(Pair for `riverside-institute`: `head.riverside@demo.sacs`, `dos.riverside@demo.sacs`,
|
|
89
|
+
`teacher.riverside@demo.sacs`, `bell.riverside@demo.sacs`.)
|
|
90
|
+
|
|
91
|
+
All demo accounts use the password **`SacsDemo123!`**.
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
## The lesson-completion workflow
|
|
96
|
+
|
|
97
|
+
A teacher submits a completion request for one of their published lessons. The request then
|
|
98
|
+
moves through a strictly enforced state machine — invalid transitions are rejected
|
|
99
|
+
server-side:
|
|
100
|
+
|
|
101
|
+
```
|
|
102
|
+
submitted ──review──> under_dos_review
|
|
103
|
+
submitted ──forward─> forwarded_to_head
|
|
104
|
+
submitted ──return──> returned_for_correction
|
|
105
|
+
under_dos_review ──forward─> forwarded_to_head
|
|
106
|
+
under_dos_review ──return──> returned_for_correction
|
|
107
|
+
forwarded_to_head ──approve─> approved
|
|
108
|
+
forwarded_to_head ──reject──> rejected
|
|
109
|
+
returned_for_correction ──submit─> submitted
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Every transition records an audit action and the requester-facing history. A secondary-level
|
|
113
|
+
request may not be final-approved: `request.decide` (head) and `request.review` (DOS) are
|
|
114
|
+
deliberately disjoint.
|
|
115
|
+
|
|
116
|
+
## Roles and permissions
|
|
117
|
+
|
|
118
|
+
| Permission | school_head | dos | teacher | bell_monitor |
|
|
119
|
+
| ---------------------- | :-: | :-: | :-: | :-: |
|
|
120
|
+
| `school.view` | ✓ | ✓ | ✓ | ✓ |
|
|
121
|
+
| `school.settings.manage` | ✓ | | | |
|
|
122
|
+
| `staff.manage` | ✓ | | | |
|
|
123
|
+
| `academics.manage` | ✓ | ✓ | | |
|
|
124
|
+
| `timetable.manage` | ✓ | ✓ | | |
|
|
125
|
+
| `timetable.publish` | ✓ | | | |
|
|
126
|
+
| `request.submit` | | | ✓ | |
|
|
127
|
+
| `request.review` | | ✓ | | |
|
|
128
|
+
| `request.decide` | ✓ | | | |
|
|
129
|
+
| `bell.view` | ✓ | ✓ | ✓ | ✓ |
|
|
130
|
+
| `bell.manage` | ✓ | | | |
|
|
131
|
+
| `audit.view.school` | ✓ | | | |
|
|
132
|
+
|
|
133
|
+
Platform permissions (`platform.schools.manage`, `platform.schools.suspend`,
|
|
134
|
+
`platform.health.view`, `platform.audit.view`) are exclusive to platform administrators and
|
|
135
|
+
never grant academic approval authority.
|
|
136
|
+
|
|
137
|
+
## Bell scheduler
|
|
138
|
+
|
|
139
|
+
Each school runs `BellSchedule`s with per-period times evaluated in the school's own time
|
|
140
|
+
zone (`Europe/London`, `America/New_York`, …). A background loop ticks once per second; each
|
|
141
|
+
tick records every due-but-unrecorded occurrence with outcome:
|
|
142
|
+
|
|
143
|
+
- **fired** — executed inside the grace window (`SACS_BELL_GRACE_SECONDS`, default 90);
|
|
144
|
+
- **missed** — the window elapsed while the scheduler was offline;
|
|
145
|
+
- **skipped** — the schedule was paused;
|
|
146
|
+
- occurrences are keyed `schedule_id + day + period + (start|end)` with a unique constraint,
|
|
147
|
+
so restarts and overlapping ticks can never double-fire the school bell.
|
|
148
|
+
|
|
149
|
+
### Deterministic test clock
|
|
150
|
+
|
|
151
|
+
In non-production environments the clock can be driven for testing and demonstrations:
|
|
152
|
+
|
|
153
|
+
```
|
|
154
|
+
POST /api/test/clock/set { "now_utc": "2026-03-02T10:00:30+00:00" }
|
|
155
|
+
POST /api/test/bell/tick
|
|
156
|
+
POST /api/test/clock/clear
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
The test clock is hard-disabled in production unless explicitly enabled.
|
|
160
|
+
|
|
161
|
+
## Real-time events
|
|
162
|
+
|
|
163
|
+
Every state change is written to a transactional **outbox** in the same database commit as
|
|
164
|
+
the change itself, then drained and streamed to the exact recipients allowed server-side by
|
|
165
|
+
the event audience rule (`platform` | school roles | specific users). This guarantees a
|
|
166
|
+
persisted change never silently loses its notification, and gives tests a synchronous
|
|
167
|
+
delivery path.
|
|
168
|
+
|
|
169
|
+
## Architecture
|
|
170
|
+
|
|
171
|
+
- **API**: FastAPI + Pydantic v2, packaged with uvicorn.
|
|
172
|
+
- **Persistence**: SQLAlchemy 2.0 (typed `Mapped` models); schema only ever created through
|
|
173
|
+
Alembic migrations — no implicit `create_all`.
|
|
174
|
+
- **Auth**: opaque hashed session tokens, PBKDF2 password hashing, per-school memberships.
|
|
175
|
+
- **Tenant isolation**: every school-scoped endpoint resolves the caller's membership and
|
|
176
|
+
re-checks the requested `school_id` server-side; a suspended school blocks all its members.
|
|
177
|
+
- **Time handling**: all timestamps are UTC; per-school time zones applied only for bell
|
|
178
|
+
scheduling and local display (`tzdata` included on Windows).
|
|
179
|
+
|
|
180
|
+
## Configuration
|
|
181
|
+
|
|
182
|
+
Everything is configurable through environment variables (see `sacs/config.py`):
|
|
183
|
+
|
|
184
|
+
| Variable | Default |
|
|
185
|
+
| ------------------------- | ----------------------------------------- |
|
|
186
|
+
| `SACS_HOME` | `~/.sacs` (demo database lives here) |
|
|
187
|
+
| `SACS_DATABASE_URL` | `sqlite:///<SACS_HOME>/sacs.db` |
|
|
188
|
+
| `SACS_ENV` | `demo` (`demo`/`dev`/`prod`) |
|
|
189
|
+
| `SACS_HOST` / `SACS_PORT` | `127.0.0.1` / `0` (auto-pick a free port) |
|
|
190
|
+
| `SACS_ENABLE_TEST_CLOCK` | `true` (`false` forced in `prod`) |
|
|
191
|
+
| `SACS_SESSION_TTL_HOURS` | `12` |
|
|
192
|
+
| `SACS_BELL_GRACE_SECONDS` | `90` |
|
|
193
|
+
|
|
194
|
+
Deployments can point `SACS_DATABASE_URL` at PostgreSQL (see the `postgres` extra) and run
|
|
195
|
+
`uvicorn sacs.app:create_app --factory`.
|
|
196
|
+
|
|
197
|
+
## Development
|
|
198
|
+
|
|
199
|
+
```bash
|
|
200
|
+
pip install -e ".[dev]"
|
|
201
|
+
pytest # full test suite
|
|
202
|
+
pytest tests/test_packaging.py # build wheel/sdist and inspect artifacts
|
|
203
|
+
python -m build --no-isolation --outdir dist
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
## Repository layout
|
|
207
|
+
|
|
208
|
+
```
|
|
209
|
+
sacs/
|
|
210
|
+
cli.py Typer CLI + the demo dashboard (five workspaces)
|
|
211
|
+
app.py FastAPI application and lifespan tasks
|
|
212
|
+
models.py SQLAlchemy models (19 tables)
|
|
213
|
+
db.py engines, sessions, Alembic wiring
|
|
214
|
+
security.py password + session token handling
|
|
215
|
+
rbac.py permission catalog for roles and platform
|
|
216
|
+
workflow.py the completion-request state machine
|
|
217
|
+
scheduler.py deterministic clock + bell loop and outcomes
|
|
218
|
+
events.py connection manager, transactional outbox, dispatcher
|
|
219
|
+
seed.py idempotent demo seed (two schools, five identities)
|
|
220
|
+
migrations/ Alembic baseline migration
|
|
221
|
+
routers/ platform, schools, academics, timetable, requests, bell, realtime, auth
|
|
222
|
+
tests/ unit + API + isolation + scheduler + packaging tests
|
|
223
|
+
```
|
sacs-0.1.0/README.md
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
# SACS — Smart Academic Coordination System
|
|
2
|
+
|
|
3
|
+
A real-time, multi-tenant school coordination platform that runs entirely from the terminal.
|
|
4
|
+
SACS coordinates **schools** (tenants), **staff**, **timetables**, **lesson sessions**, a
|
|
5
|
+
**lesson-completion approval workflow**, and a deterministic **software bell scheduler**,
|
|
6
|
+
streaming every event to authenticated workspaces over WebSockets.
|
|
7
|
+
|
|
8
|
+
There is no browser frontend: the whole product is exercised through the `sacs` CLI, which
|
|
9
|
+
talks to one shared FastAPI backend exactly like a real client would.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Quick start
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pip install sacs # then:
|
|
17
|
+
sacs demo # migrate, seed, start the server, open the live dashboard
|
|
18
|
+
sacs status # show whether the demonstration is running
|
|
19
|
+
sacs demo reset # destroy ONLY the local demo database (requires confirmation)
|
|
20
|
+
sacs demo stop # stop the running demonstration
|
|
21
|
+
sacs check # verify dependencies and running environment
|
|
22
|
+
sacs version
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
`sacs demo` is the product. It:
|
|
26
|
+
|
|
27
|
+
1. migrates the database to the latest schema (Alembic) and seeds two schools;
|
|
28
|
+
2. starts the shared SACS service on a local port;
|
|
29
|
+
3. authenticates **five independently authorized identities** against the real API;
|
|
30
|
+
4. shows a live multi-workspace dashboard (Rich) with keyboard commands for the teacher
|
|
31
|
+
submit → DOS review → head decide workflow, bell operations, and school administration.
|
|
32
|
+
|
|
33
|
+
The demonstration persists between restarts (`sacs demo` again keeps your data; use
|
|
34
|
+
`sacs demo reset` for a clean slate).
|
|
35
|
+
|
|
36
|
+
### Demo identities
|
|
37
|
+
|
|
38
|
+
| Workspace | Email | Role | What they can do |
|
|
39
|
+
| ---------- | ------------------------------ | ------------- | -------------------------------------------------- |
|
|
40
|
+
| Platform | `platform@demo.sacs` | platform admin | create/suspend/reactivate schools, health, audit |
|
|
41
|
+
| Head | `head.northstar@demo.sacs` | school head | final approve/reject, staff & academics, bells |
|
|
42
|
+
| DOS | `dos.northstar@demo.sacs` | director of studies | review, forward, return requests; timetable |
|
|
43
|
+
| Teacher | `teacher.northstar@demo.sacs` | teacher | submit completion requests for own lessons |
|
|
44
|
+
| Bell | `bell.northstar@demo.sacs` | bell monitor | monitor bell events in real time |
|
|
45
|
+
|
|
46
|
+
(Pair for `riverside-institute`: `head.riverside@demo.sacs`, `dos.riverside@demo.sacs`,
|
|
47
|
+
`teacher.riverside@demo.sacs`, `bell.riverside@demo.sacs`.)
|
|
48
|
+
|
|
49
|
+
All demo accounts use the password **`SacsDemo123!`**.
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## The lesson-completion workflow
|
|
54
|
+
|
|
55
|
+
A teacher submits a completion request for one of their published lessons. The request then
|
|
56
|
+
moves through a strictly enforced state machine — invalid transitions are rejected
|
|
57
|
+
server-side:
|
|
58
|
+
|
|
59
|
+
```
|
|
60
|
+
submitted ──review──> under_dos_review
|
|
61
|
+
submitted ──forward─> forwarded_to_head
|
|
62
|
+
submitted ──return──> returned_for_correction
|
|
63
|
+
under_dos_review ──forward─> forwarded_to_head
|
|
64
|
+
under_dos_review ──return──> returned_for_correction
|
|
65
|
+
forwarded_to_head ──approve─> approved
|
|
66
|
+
forwarded_to_head ──reject──> rejected
|
|
67
|
+
returned_for_correction ──submit─> submitted
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Every transition records an audit action and the requester-facing history. A secondary-level
|
|
71
|
+
request may not be final-approved: `request.decide` (head) and `request.review` (DOS) are
|
|
72
|
+
deliberately disjoint.
|
|
73
|
+
|
|
74
|
+
## Roles and permissions
|
|
75
|
+
|
|
76
|
+
| Permission | school_head | dos | teacher | bell_monitor |
|
|
77
|
+
| ---------------------- | :-: | :-: | :-: | :-: |
|
|
78
|
+
| `school.view` | ✓ | ✓ | ✓ | ✓ |
|
|
79
|
+
| `school.settings.manage` | ✓ | | | |
|
|
80
|
+
| `staff.manage` | ✓ | | | |
|
|
81
|
+
| `academics.manage` | ✓ | ✓ | | |
|
|
82
|
+
| `timetable.manage` | ✓ | ✓ | | |
|
|
83
|
+
| `timetable.publish` | ✓ | | | |
|
|
84
|
+
| `request.submit` | | | ✓ | |
|
|
85
|
+
| `request.review` | | ✓ | | |
|
|
86
|
+
| `request.decide` | ✓ | | | |
|
|
87
|
+
| `bell.view` | ✓ | ✓ | ✓ | ✓ |
|
|
88
|
+
| `bell.manage` | ✓ | | | |
|
|
89
|
+
| `audit.view.school` | ✓ | | | |
|
|
90
|
+
|
|
91
|
+
Platform permissions (`platform.schools.manage`, `platform.schools.suspend`,
|
|
92
|
+
`platform.health.view`, `platform.audit.view`) are exclusive to platform administrators and
|
|
93
|
+
never grant academic approval authority.
|
|
94
|
+
|
|
95
|
+
## Bell scheduler
|
|
96
|
+
|
|
97
|
+
Each school runs `BellSchedule`s with per-period times evaluated in the school's own time
|
|
98
|
+
zone (`Europe/London`, `America/New_York`, …). A background loop ticks once per second; each
|
|
99
|
+
tick records every due-but-unrecorded occurrence with outcome:
|
|
100
|
+
|
|
101
|
+
- **fired** — executed inside the grace window (`SACS_BELL_GRACE_SECONDS`, default 90);
|
|
102
|
+
- **missed** — the window elapsed while the scheduler was offline;
|
|
103
|
+
- **skipped** — the schedule was paused;
|
|
104
|
+
- occurrences are keyed `schedule_id + day + period + (start|end)` with a unique constraint,
|
|
105
|
+
so restarts and overlapping ticks can never double-fire the school bell.
|
|
106
|
+
|
|
107
|
+
### Deterministic test clock
|
|
108
|
+
|
|
109
|
+
In non-production environments the clock can be driven for testing and demonstrations:
|
|
110
|
+
|
|
111
|
+
```
|
|
112
|
+
POST /api/test/clock/set { "now_utc": "2026-03-02T10:00:30+00:00" }
|
|
113
|
+
POST /api/test/bell/tick
|
|
114
|
+
POST /api/test/clock/clear
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
The test clock is hard-disabled in production unless explicitly enabled.
|
|
118
|
+
|
|
119
|
+
## Real-time events
|
|
120
|
+
|
|
121
|
+
Every state change is written to a transactional **outbox** in the same database commit as
|
|
122
|
+
the change itself, then drained and streamed to the exact recipients allowed server-side by
|
|
123
|
+
the event audience rule (`platform` | school roles | specific users). This guarantees a
|
|
124
|
+
persisted change never silently loses its notification, and gives tests a synchronous
|
|
125
|
+
delivery path.
|
|
126
|
+
|
|
127
|
+
## Architecture
|
|
128
|
+
|
|
129
|
+
- **API**: FastAPI + Pydantic v2, packaged with uvicorn.
|
|
130
|
+
- **Persistence**: SQLAlchemy 2.0 (typed `Mapped` models); schema only ever created through
|
|
131
|
+
Alembic migrations — no implicit `create_all`.
|
|
132
|
+
- **Auth**: opaque hashed session tokens, PBKDF2 password hashing, per-school memberships.
|
|
133
|
+
- **Tenant isolation**: every school-scoped endpoint resolves the caller's membership and
|
|
134
|
+
re-checks the requested `school_id` server-side; a suspended school blocks all its members.
|
|
135
|
+
- **Time handling**: all timestamps are UTC; per-school time zones applied only for bell
|
|
136
|
+
scheduling and local display (`tzdata` included on Windows).
|
|
137
|
+
|
|
138
|
+
## Configuration
|
|
139
|
+
|
|
140
|
+
Everything is configurable through environment variables (see `sacs/config.py`):
|
|
141
|
+
|
|
142
|
+
| Variable | Default |
|
|
143
|
+
| ------------------------- | ----------------------------------------- |
|
|
144
|
+
| `SACS_HOME` | `~/.sacs` (demo database lives here) |
|
|
145
|
+
| `SACS_DATABASE_URL` | `sqlite:///<SACS_HOME>/sacs.db` |
|
|
146
|
+
| `SACS_ENV` | `demo` (`demo`/`dev`/`prod`) |
|
|
147
|
+
| `SACS_HOST` / `SACS_PORT` | `127.0.0.1` / `0` (auto-pick a free port) |
|
|
148
|
+
| `SACS_ENABLE_TEST_CLOCK` | `true` (`false` forced in `prod`) |
|
|
149
|
+
| `SACS_SESSION_TTL_HOURS` | `12` |
|
|
150
|
+
| `SACS_BELL_GRACE_SECONDS` | `90` |
|
|
151
|
+
|
|
152
|
+
Deployments can point `SACS_DATABASE_URL` at PostgreSQL (see the `postgres` extra) and run
|
|
153
|
+
`uvicorn sacs.app:create_app --factory`.
|
|
154
|
+
|
|
155
|
+
## Development
|
|
156
|
+
|
|
157
|
+
```bash
|
|
158
|
+
pip install -e ".[dev]"
|
|
159
|
+
pytest # full test suite
|
|
160
|
+
pytest tests/test_packaging.py # build wheel/sdist and inspect artifacts
|
|
161
|
+
python -m build --no-isolation --outdir dist
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
## Repository layout
|
|
165
|
+
|
|
166
|
+
```
|
|
167
|
+
sacs/
|
|
168
|
+
cli.py Typer CLI + the demo dashboard (five workspaces)
|
|
169
|
+
app.py FastAPI application and lifespan tasks
|
|
170
|
+
models.py SQLAlchemy models (19 tables)
|
|
171
|
+
db.py engines, sessions, Alembic wiring
|
|
172
|
+
security.py password + session token handling
|
|
173
|
+
rbac.py permission catalog for roles and platform
|
|
174
|
+
workflow.py the completion-request state machine
|
|
175
|
+
scheduler.py deterministic clock + bell loop and outcomes
|
|
176
|
+
events.py connection manager, transactional outbox, dispatcher
|
|
177
|
+
seed.py idempotent demo seed (two schools, five identities)
|
|
178
|
+
migrations/ Alembic baseline migration
|
|
179
|
+
routers/ platform, schools, academics, timetable, requests, bell, realtime, auth
|
|
180
|
+
tests/ unit + API + isolation + scheduler + packaging tests
|
|
181
|
+
```
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "sacs"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "SACS - Smart Academic Coordination System: a real-time multi-tenant school coordination platform with a one-command live demo."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.11"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [{ name = "ivanbright" }]
|
|
13
|
+
keywords = ["education", "school", "timetable", "multi-tenant", "fastapi", "realtime"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 3 - Alpha",
|
|
16
|
+
"Environment :: Console",
|
|
17
|
+
"Intended Audience :: Education",
|
|
18
|
+
"License :: OSI Approved :: MIT License",
|
|
19
|
+
"Operating System :: OS Independent",
|
|
20
|
+
"Programming Language :: Python :: 3",
|
|
21
|
+
"Programming Language :: Python :: 3.11",
|
|
22
|
+
"Programming Language :: Python :: 3.12",
|
|
23
|
+
"Programming Language :: Python :: 3.13",
|
|
24
|
+
"Topic :: Education",
|
|
25
|
+
]
|
|
26
|
+
dependencies = [
|
|
27
|
+
"fastapi>=0.110",
|
|
28
|
+
"uvicorn[standard]>=0.27",
|
|
29
|
+
"sqlalchemy>=2.0",
|
|
30
|
+
"alembic>=1.13",
|
|
31
|
+
"typer>=0.12",
|
|
32
|
+
"rich>=13.0",
|
|
33
|
+
"httpx>=0.27",
|
|
34
|
+
"email-validator>=2.0",
|
|
35
|
+
"tzdata>=2024.1; platform_system != 'Linux'",
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
[project.optional-dependencies]
|
|
39
|
+
dev = ["pytest>=8.0", "httpx>=0.27", "build>=1.0", "twine>=5.0"]
|
|
40
|
+
postgres = ["psycopg[binary]>=3.1"]
|
|
41
|
+
|
|
42
|
+
[project.urls]
|
|
43
|
+
Homepage = "https://github.com/ivanbright/sacs"
|
|
44
|
+
Repository = "https://github.com/ivanbright/sacs"
|
|
45
|
+
Issues = "https://github.com/ivanbright/sacs/issues"
|
|
46
|
+
Changelog = "https://github.com/ivanbright/sacs/blob/main/CHANGELOG.md"
|
|
47
|
+
|
|
48
|
+
[project.scripts]
|
|
49
|
+
sacs = "sacs.cli:main"
|
|
50
|
+
|
|
51
|
+
[tool.setuptools]
|
|
52
|
+
include-package-data = true
|
|
53
|
+
|
|
54
|
+
[tool.setuptools.packages.find]
|
|
55
|
+
include = ["sacs", "sacs.*"]
|
|
56
|
+
|
|
57
|
+
[tool.setuptools.package-data]
|
|
58
|
+
sacs = ["migrations/*", "migrations/versions/*", "alembic.ini"]
|
|
59
|
+
|
|
60
|
+
[tool.pytest.ini_options]
|
|
61
|
+
testpaths = ["tests"]
|
|
62
|
+
addopts = "-q"
|
|
63
|
+
filterwarnings = ["ignore::DeprecationWarning"]
|
|
64
|
+
|
|
65
|
+
[tool.ruff]
|
|
66
|
+
line-length = 100
|
|
67
|
+
target-version = "py311"
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""SACS - Smart Academic Coordination System.
|
|
2
|
+
|
|
3
|
+
A real-time, multi-tenant school coordination platform. The package ships a
|
|
4
|
+
Python CLI (``sacs``) that launches a complete local demonstration with five
|
|
5
|
+
simultaneously usable browser workspaces backed by one shared service.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
__version__ = "0.1.0"
|
|
11
|
+
|
|
12
|
+
__all__ = ["__version__"]
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
[alembic]
|
|
2
|
+
script_location = sacs/migrations
|
|
3
|
+
prepend_sys_path = .
|
|
4
|
+
version_path_separator = os
|
|
5
|
+
# Overridden programmatically by sacs.db; present for manual `alembic` use.
|
|
6
|
+
sqlalchemy.url = sqlite:///./_migrate_tmp.db
|
|
7
|
+
|
|
8
|
+
[loggers]
|
|
9
|
+
keys = root,sqlalchemy,alembic
|
|
10
|
+
|
|
11
|
+
[handlers]
|
|
12
|
+
keys = console
|
|
13
|
+
|
|
14
|
+
[formatters]
|
|
15
|
+
keys = generic
|
|
16
|
+
|
|
17
|
+
[logger_root]
|
|
18
|
+
level = WARNING
|
|
19
|
+
handlers = console
|
|
20
|
+
qualname =
|
|
21
|
+
|
|
22
|
+
[logger_sqlalchemy]
|
|
23
|
+
level = WARNING
|
|
24
|
+
handlers =
|
|
25
|
+
qualname = sqlalchemy.engine
|
|
26
|
+
|
|
27
|
+
[logger_alembic]
|
|
28
|
+
level = INFO
|
|
29
|
+
handlers =
|
|
30
|
+
qualname = alembic
|
|
31
|
+
|
|
32
|
+
[handler_console]
|
|
33
|
+
class = StreamHandler
|
|
34
|
+
args = (sys.stderr,)
|
|
35
|
+
level = NOTSET
|
|
36
|
+
formatter = generic
|
|
37
|
+
|
|
38
|
+
[formatter_generic]
|
|
39
|
+
format = %(levelname)-5.5s [%(name)s] %(message)s
|
|
40
|
+
datefmt = %H:%M:%S
|
sacs-0.1.0/sacs/app.py
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"""FastAPI application factory for SACS."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
from contextlib import asynccontextmanager
|
|
7
|
+
|
|
8
|
+
from fastapi import FastAPI
|
|
9
|
+
from fastapi.middleware.cors import CORSMiddleware
|
|
10
|
+
|
|
11
|
+
from . import __version__
|
|
12
|
+
from .config import get_settings
|
|
13
|
+
from .db import get_sessionmaker
|
|
14
|
+
from .events import background_dispatcher
|
|
15
|
+
from .scheduler import bell_loop
|
|
16
|
+
|
|
17
|
+
from .routers import auth as auth_router
|
|
18
|
+
from .routers import academics as academics_router
|
|
19
|
+
from .routers import bell as bell_router
|
|
20
|
+
from .routers import platform as platform_router
|
|
21
|
+
from .routers import realtime as realtime_router
|
|
22
|
+
from .routers import requests as requests_router
|
|
23
|
+
from .routers import schools as schools_router
|
|
24
|
+
from .routers import timetable as timetable_router
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def cors_urls() -> list[str]:
|
|
28
|
+
settings = get_settings()
|
|
29
|
+
origins = list(settings.cors_origins)
|
|
30
|
+
if settings.env in ("demo", "dev"):
|
|
31
|
+
origins = ["*"]
|
|
32
|
+
return origins
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def create_app() -> FastAPI:
|
|
36
|
+
settings = get_settings()
|
|
37
|
+
|
|
38
|
+
@asynccontextmanager
|
|
39
|
+
async def lifespan(app: FastAPI):
|
|
40
|
+
session_factory = get_sessionmaker()
|
|
41
|
+
tasks = [
|
|
42
|
+
bell_loop(session_factory),
|
|
43
|
+
background_dispatcher(session_factory),
|
|
44
|
+
]
|
|
45
|
+
for task in tasks:
|
|
46
|
+
app.state.tasks.append(asyncio.create_task(task))
|
|
47
|
+
yield
|
|
48
|
+
for task in app.state.tasks:
|
|
49
|
+
task.cancel()
|
|
50
|
+
await asyncio.gather(*app.state.tasks, return_exceptions=True)
|
|
51
|
+
|
|
52
|
+
app = FastAPI(
|
|
53
|
+
title="SACS - Smart Academic Coordination System",
|
|
54
|
+
version=__version__,
|
|
55
|
+
description="Multi-tenant academic coordination platform: one shared backend, "
|
|
56
|
+
"isolated school tenants, real-time delivery, and a software bell scheduler.",
|
|
57
|
+
lifespan=lifespan,
|
|
58
|
+
)
|
|
59
|
+
app.state.tasks = []
|
|
60
|
+
|
|
61
|
+
app.add_middleware(
|
|
62
|
+
CORSMiddleware,
|
|
63
|
+
allow_origins=cors_urls(),
|
|
64
|
+
allow_credentials=False,
|
|
65
|
+
allow_methods=["*"],
|
|
66
|
+
allow_headers=["*"],
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
app.include_router(schools_router.accept_router)
|
|
70
|
+
app.include_router(auth_router.router)
|
|
71
|
+
app.include_router(platform_router.router)
|
|
72
|
+
app.include_router(schools_router.router)
|
|
73
|
+
app.include_router(academics_router.router)
|
|
74
|
+
app.include_router(timetable_router.router)
|
|
75
|
+
app.include_router(requests_router.router)
|
|
76
|
+
app.include_router(bell_router.router)
|
|
77
|
+
app.include_router(bell_router.test_router)
|
|
78
|
+
app.include_router(realtime_router.router)
|
|
79
|
+
|
|
80
|
+
@app.get("/api/health", tags=["system"])
|
|
81
|
+
def public_health() -> dict:
|
|
82
|
+
return {"status": "ok", "service": "sacs", "version": __version__}
|
|
83
|
+
|
|
84
|
+
@app.get("/", include_in_schema=False)
|
|
85
|
+
def root() -> dict:
|
|
86
|
+
return {
|
|
87
|
+
"service": "sacs",
|
|
88
|
+
"version": __version__,
|
|
89
|
+
"docs": "/docs",
|
|
90
|
+
"health": "/api/health",
|
|
91
|
+
"note": "No browser frontend is shipped; the SACS CLI is the interface.",
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return app
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
__all__ = ["create_app"]
|