A small task manager with a FastAPI backend, Typer CLI, and Textual TUI. Runs in local, client, or server mode.
Project description
systema2
A small to-do REST API built with FastAPI and SQLModel, backed by SQLite. Managed with uv.
Features
- Tasks and Projects, with tasks optionally linked to a project
- Task priority (High / Medium / Low, default Medium)
- Optional task due date (ISO
YYYY-MM-DD); overdue tasks highlighted - CRUD endpoints for both; deleting a project unlinks its tasks
/tasks?project_id=N,?unassigned=true,?priority=H,?due_before=YYYY-MM-DDfilters- Automatic request validation via Pydantic/SQLModel schemas
- Auto-generated OpenAPI docs at
/docsand/redoc - SQLite database auto-created on startup via a FastAPI lifespan handler
- Typer CLI (with a
projectsub-app) and Textual TUI (projects sidebar + tasks pane) that share the same CRUD code - Three runtime modes (
local·client·server) switched via env var
Requirements
- Python >=3.14
- uv for dependency management
Setup
git clone <your-remote> systema2
cd systema2
uv sync # creates .venv and installs all deps from uv.lock
Running
CLI / TUI (local mode — default)
# Tasks
uv run systema2 create "buy milk" -d "2L, semi-skimmed"
uv run systema2 list
uv run systema2 update 1 --completed
uv run systema2 delete 1 --yes
# Projects
uv run systema2 project create "home" -d "Household chores"
uv run systema2 project list
uv run systema2 project show 1 --with-tasks
# Link a task to a project
uv run systema2 create "clean fridge" -p 1
uv run systema2 list -p 1 # filter by project
uv run systema2 list --unassigned # tasks with no project
uv run systema2 update 2 --clear-project
# Task priority (H/M/L, default M)
uv run systema2 create "ship it" -P H
uv run systema2 update 1 -P L
uv run systema2 list -P H # filter by priority
# Due date (YYYY-MM-DD, optional)
uv run systema2 create "review PR" -D 2030-05-15
uv run systema2 update 1 -D 2030-06-01
uv run systema2 update 1 --clear-due
uv run systema2 list --due-before 2030-12-31
# TUI
uv run systema2 tui
The TUI ships with the Tokyo Night colour theme (Textual built-in).
Press ctrl+p and pick another theme at runtime if you prefer a different
palette.
TUI keybindings (vim-style)
| Key | Action |
|---|---|
a |
add task |
i |
edit task (vim insert) |
x |
delete task (vim delete-char) |
space |
toggle task done / not done |
o |
open new project (vim open-line) |
I |
edit project |
X |
delete project |
j / k |
move cursor down / up |
g / G |
jump to first / last row |
ctrl+d / ctrl+u |
half-page down / up |
ctrl+w |
switch pane (tasks ↔ projects sidebar) |
r |
refresh |
q |
quit |
Inside modal dialogs the usual text-editing keys apply; ctrl+s saves
and escape cancels.
Server
SYSTEMA2_MODE=server uv run systema2 serve --host 127.0.0.1 --port 8000
# or equivalently:
uv run main.py
Interactive docs:
- Swagger UI: http://127.0.0.1:8000/docs
- ReDoc: http://127.0.0.1:8000/redoc
- OpenAPI: http://127.0.0.1:8000/openapi.json
Client mode (CLI/TUI over HTTP)
Point the CLI/TUI at a running server instead of the local SQLite file:
export SYSTEMA2_MODE=client
export SYSTEMA2_API_URL=http://127.0.0.1:8000
uv run systema2 list
uv run systema2 tui
Modes
SYSTEMA2_MODE |
CRUD backend | Uses DB? |
|---|---|---|
local (default) |
Direct SQLModel → SQLite | yes |
server |
Direct SQLModel → SQLite + serves API | yes |
client |
HTTP calls to SYSTEMA2_API_URL |
no |
Other env vars: SYSTEMA2_API_URL (default http://127.0.0.1:8000),
SYSTEMA2_HOST, SYSTEMA2_PORT, SYSTEMA2_API_KEY.
Authentication (client ↔ server)
When SYSTEMA2_API_KEY is set on the server, every request to
/tasks and /projects must present the shared secret. The /
health endpoint stays open so probes and reverse proxies still work.
Generate a strong shared secret (uses secrets.token_urlsafe, 256
bits of entropy by default):
uv run systema2 gen-api-key # prints the key on stdout
uv run systema2 gen-api-key --export # prints `export SYSTEMA2_API_KEY=...`
uv run systema2 gen-api-key --bytes 64 # more entropy
# Typical wiring:
export SYSTEMA2_API_KEY="$(uv run systema2 gen-api-key)"
Server:
export SYSTEMA2_MODE=server
export SYSTEMA2_API_KEY='s3cret'
uv run systema2 serve
Client (CLI / TUI): set the same value and the HttpTaskRepository
will forward it as X-API-Key on every call.
export SYSTEMA2_MODE=client
export SYSTEMA2_API_URL=http://127.0.0.1:8000
export SYSTEMA2_API_KEY='s3cret'
uv run systema2 list
Raw curl can use either header style:
curl -H 'X-API-Key: s3cret' http://127.0.0.1:8000/tasks
curl -H 'Authorization: Bearer s3cret' http://127.0.0.1:8000/tasks
Responses:
401 Missing API key— header absent (server also returnsWWW-Authenticate: X-API-Key).403 Invalid API key— header present but value does not match.
If SYSTEMA2_API_KEY is unset (or blank) on the server, the
endpoints are unauthenticated — convenient for local dev. The client
similarly sends no header when the variable is unset, so the default
experience is unchanged.
Project layout
systema2/
├── main.py # uvicorn entrypoint
├── pyproject.toml # project metadata & dependencies
├── uv.lock # pinned dependency lockfile
├── systema2/
│ ├── __init__.py
│ ├── app.py # FastAPI app, routes, lifespan
│ ├── database.py # engine, session dependency, init_db
│ └── models.py # Task + TaskCreate / TaskUpdate / TaskRead
└── systema2.db # SQLite database (auto-created, gitignored)
Data model
Project (table)
| Field | Type | Notes |
|---|---|---|
id |
int |
Primary key, auto-assigned |
name |
str |
Required, 1–200 chars, indexed |
description |
str | None |
Optional, up to 2000 chars |
created_at |
datetime (UTC) |
Set on creation |
updated_at |
datetime (UTC) |
Updated on every successful PATCH |
Task (table)
| Field | Type | Notes |
|---|---|---|
id |
int |
Primary key, auto-assigned |
title |
str |
Required, 1–200 chars, indexed |
description |
str | None |
Optional, up to 2000 chars |
completed |
bool |
Defaults to false |
priority |
"H"/"M"/"L" |
High / Medium / Low, default "M" |
due_date |
date | None |
ISO YYYY-MM-DD; overdue tasks are rendered in red |
project_id |
int | None |
FK to project.id; None = no project |
created_at |
datetime (UTC) |
Set on creation |
updated_at |
datetime (UTC) |
Updated on every successful PATCH |
Three schemas wrap each table at the API boundary
(TaskCreate/TaskUpdate/TaskRead and the analogous Project*).
Deleting a project does not delete its tasks; they are unlinked
(project_id set to NULL).
API reference
Base URL: http://127.0.0.1:8000
| Method | Path | Description | Success |
|---|---|---|---|
| GET | / |
Health / info | 200 |
| GET | /tasks |
List tasks (?project_id=N, ?unassigned=true) |
200 |
| GET | /tasks/{id} |
Get one task | 200 / 404 |
| POST | /tasks |
Create a task | 201 / 400 / 422 |
| PATCH | /tasks/{id} |
Partially update a task | 200 / 400 / 404 / 422 |
| DELETE | /tasks/{id} |
Delete a task | 204 / 404 |
| GET | /projects |
List all projects | 200 |
| GET | /projects/{id} |
Get one project | 200 / 404 |
| GET | /projects/{id}/tasks |
List tasks in a project | 200 / 404 |
| POST | /projects |
Create a project | 201 / 422 |
| PATCH | /projects/{id} |
Partially update a project | 200 / 404 / 422 |
| DELETE | /projects/{id} |
Delete a project (unlinks its tasks) | 204 / 404 |
POST/PATCH on /tasks with a non-existent project_id return 400
with {"detail": {"error_code": "project_not_found", "project_id": N}}.
Create a task
curl -X POST http://127.0.0.1:8000/tasks \
-H 'Content-Type: application/json' \
-d '{"title": "buy milk", "description": "2L, semi-skimmed"}'
{
"id": 1,
"title": "buy milk",
"description": "2L, semi-skimmed",
"completed": false,
"created_at": "2026-04-25T23:28:06.279831",
"updated_at": "2026-04-25T23:28:06.279843"
}
List tasks
curl http://127.0.0.1:8000/tasks
Get one task
curl http://127.0.0.1:8000/tasks/1
Returns 404 with {"detail": "Task not found"} if the id does not exist.
Update a task (partial)
Any subset of fields may be provided; omitted fields are untouched.
curl -X PATCH http://127.0.0.1:8000/tasks/1 \
-H 'Content-Type: application/json' \
-d '{"completed": true}'
Delete a task
curl -X DELETE http://127.0.0.1:8000/tasks/1
Responds with 204 No Content on success.
Development
Install dev dependencies (already in the lockfile, pulled in by uv sync):
httpx— used forfastapi.testclient.TestClient
Smoke-testing the API without a running server
from fastapi.testclient import TestClient
from systema2.app import app
with TestClient(app) as c: # context manager triggers startup/shutdown
r = c.post("/tasks", json={"title": "write docs"})
print(r.status_code, r.json())
Adding dependencies
uv add <package> # runtime dep
uv add --dev <package> # dev-only dep
Resetting the database
The SQLite file is created automatically on next startup:
rm -f systema2.db
Releases
Cutting a release is tag-driven. The release GitHub Actions workflow
runs the full test suite, builds sdist + wheel with uv build, and
publishes to PyPI using Trusted Publishing
(OIDC — no API tokens). It also attaches the built artifacts to a
GitHub Release.
To cut a new release:
# 1. Bump the version in pyproject.toml (must match the tag below).
# 2. Commit and push to master.
# 3. Tag and push:
git tag v0.2.0
git push origin v0.2.0
The workflow refuses to publish if the tag doesn't match
project.version in pyproject.toml.
One-time PyPI setup (per project, done via the PyPI UI):
- Register the project name on PyPI.
- In the project's "Publishing" settings, add a Trusted Publisher
with:
- Owner:
rodrigokimura - Repository:
systema2 - Workflow filename:
release.yml - Environment name:
pypi
- Owner:
- Create a matching GitHub environment named
pypi(Settings → Environments) with any required protection rules.
License
MIT — see LICENSE.
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
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file systema2-0.1.0.tar.gz.
File metadata
- Download URL: systema2-0.1.0.tar.gz
- Upload date:
- Size: 75.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c0824d8061c10c5d8ba29c6b60dc55e95b730932e389f6b2aed53958badb6b37
|
|
| MD5 |
3147a64d31d1af186fc8df6ea03e9971
|
|
| BLAKE2b-256 |
adf254ed2ba2db18e1b965bb67a3cf35a6fa4dbfe93d9d4d3a01702eefb07cb8
|
Provenance
The following attestation bundles were made for systema2-0.1.0.tar.gz:
Publisher:
release.yml on rodrigokimura/systema2
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
systema2-0.1.0.tar.gz -
Subject digest:
c0824d8061c10c5d8ba29c6b60dc55e95b730932e389f6b2aed53958badb6b37 - Sigstore transparency entry: 1403231300
- Sigstore integration time:
-
Permalink:
rodrigokimura/systema2@1076bf90bd486d066b51e74e5746d1c859541ee5 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/rodrigokimura
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@1076bf90bd486d066b51e74e5746d1c859541ee5 -
Trigger Event:
push
-
Statement type:
File details
Details for the file systema2-0.1.0-py3-none-any.whl.
File metadata
- Download URL: systema2-0.1.0-py3-none-any.whl
- Upload date:
- Size: 35.3 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
580727aaefa4c2d1bf0079a1669fca1c37d7a2a45eceb4d4e594d3bf588c467a
|
|
| MD5 |
dc27dc2a4d7bdc1b8a7e30872ac22900
|
|
| BLAKE2b-256 |
2327129540cf3f5a7f6cd79e2054e05e3d5d6c6b2b64c299220b33d5386bf4d9
|
Provenance
The following attestation bundles were made for systema2-0.1.0-py3-none-any.whl:
Publisher:
release.yml on rodrigokimura/systema2
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
systema2-0.1.0-py3-none-any.whl -
Subject digest:
580727aaefa4c2d1bf0079a1669fca1c37d7a2a45eceb4d4e594d3bf588c467a - Sigstore transparency entry: 1403231378
- Sigstore integration time:
-
Permalink:
rodrigokimura/systema2@1076bf90bd486d066b51e74e5746d1c859541ee5 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/rodrigokimura
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@1076bf90bd486d066b51e74e5746d1c859541ee5 -
Trigger Event:
push
-
Statement type: