Skip to main content

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-DD filters
  • Automatic request validation via Pydantic/SQLModel schemas
  • Auto-generated OpenAPI docs at /docs and /redoc
  • SQLite database auto-created on startup via a FastAPI lifespan handler
  • Typer CLI (with a project sub-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:

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 returns WWW-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 for fastapi.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):

  1. Register the project name on PyPI.
  2. In the project's "Publishing" settings, add a Trusted Publisher with:
    • Owner: rodrigokimura
    • Repository: systema2
    • Workflow filename: release.yml
    • Environment name: pypi
  3. 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

systema2-0.1.0.tar.gz (75.1 kB view details)

Uploaded Source

Built Distribution

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

systema2-0.1.0-py3-none-any.whl (35.3 kB view details)

Uploaded Python 3

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

Hashes for systema2-0.1.0.tar.gz
Algorithm Hash digest
SHA256 c0824d8061c10c5d8ba29c6b60dc55e95b730932e389f6b2aed53958badb6b37
MD5 3147a64d31d1af186fc8df6ea03e9971
BLAKE2b-256 adf254ed2ba2db18e1b965bb67a3cf35a6fa4dbfe93d9d4d3a01702eefb07cb8

See more details on using hashes here.

Provenance

The following attestation bundles were made for systema2-0.1.0.tar.gz:

Publisher: release.yml on rodrigokimura/systema2

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

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

Hashes for systema2-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 580727aaefa4c2d1bf0079a1669fca1c37d7a2a45eceb4d4e594d3bf588c467a
MD5 dc27dc2a4d7bdc1b8a7e30872ac22900
BLAKE2b-256 2327129540cf3f5a7f6cd79e2054e05e3d5d6c6b2b64c299220b33d5386bf4d9

See more details on using hashes here.

Provenance

The following attestation bundles were made for systema2-0.1.0-py3-none-any.whl:

Publisher: release.yml on rodrigokimura/systema2

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

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