OS Apps: Tocry, FastAPI and Typer

Published: Jul 2, 2025 by Isaac Johnson

I saw a Marious Post on ToCry which is a lightweight Kanban open-source app. It is elegant and quick and easy to setup. We’ll not only set it up and run in Docker and Kubernetes, but we’ll build out a full REST API for it as well.

In doing that, We’ll explore FastAPI and Typer. Both of these python frameworks are by Sebastián Ramírez who has a whole slew of excellent Opensource tools to explore.

We’ll start with this rather oddly named To-Do task app called “ToCry”

ToDo (ToCry)

I saw this MariusHosting post on “ToCry”, a todo app.

From the Github page, it comes from the question every developer asks “Are you going ToDo or ToCry?”

He suggested using portainer

services:
  tocry:
    image: ghcr.io/ralsina/tocry:latest
    container_name: ToCry
    ports:
      - 3135:3000
    environment:
     TOCRY_AUTH_USER: marius
     TOCRY_AUTH_PASS: mariushosting
    volumes:
      - /volume1/docker/tocry:/data:rw
    restart: on-failure:5

I asked Co-Pilot for some help converting that to a Kubernetes manifest

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: tocry-data-pvc
spec:
  accessModes:
    - ReadWriteOnce
  storageClassName: local-path
  resources:
    requests:
      storage: 1Gi
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: tocry
spec:
  replicas: 1
  selector:
    matchLabels:
      app: tocry
  template:
    metadata:
      labels:
        app: tocry
    spec:
      containers:
        - name: tocry
          image: ghcr.io/ralsina/tocry:latest
          ports:
            - containerPort: 3000
          env:
            - name: TOCRY_AUTH_USER
              value: "your_username"
            - name: TOCRY_AUTH_PASS
              value: "your_password"
          volumeMounts:
            - name: data
              mountPath: /data
      volumes:
        - name: data
          persistentVolumeClaim:
            claimName: tocry-data-pvc
---
apiVersion: v1
kind: Service
metadata:
  name: tocry
spec:
  selector:
    app: tocry
  ports:
    - protocol: TCP
      port: 3000
      targetPort: 3000

I applied it

$ kubectl apply -f ./k8stocry.yaml
persistentvolumeclaim/tocry-data-pvc created
deployment.apps/tocry created
service/tocry created

I can now port-forward to test

$ kubectl port-forward svc/tocry 3000:3000
Forwarding from 127.0.0.1:3000 -> 3000
Forwarding from [::1]:3000 -> 3000

Once logged in, we can create our first board

/content/images/2025/07/tocry-01.png

We give it a name

/content/images/2025/07/tocry-02.png

Let’s add some swimlanes and a note:

I can mess with themes and light/dark mode but that is pretty much it

/content/images/2025/07/tocry-04.png

I could see from the network protocol it uses POST to add issues

/content/images/2025/07/tocry-05.png

Let’s try to use curl to do it

curl -X POST http://localhost:3000/boards/myfirstboard/note \
  -u your_username:your_password \
  -H "Content-Type: application/json" \
  -d '{
    "lane_name": "Todo",
    "note": {
      "title": "My Note Title",
      "tags": ["tag1", "tag2"],
      "content": "This is the note content"
    }
  }'

I can now test with a curl

$ curl -X POST http://localhost:3000/boards/myfirstboard/note -u your_username:your_password -H "Content-Type: application/json" -d '{ "lane_name": "inbox", "note": { "title": "My Note Title", "tags": ["tag2","tag3"], "content": "my first test" }}'
{"sepia_id":"bc2767f7-44ab-4cab-84f5-98cb3c0476ca","id":"e01eee3d-a9f7-4133-8279-ad01d75788d3","title":"My Note Title","tags":["tag2","tag3"],"content":"my first test","expanded":false}

and see it created

/content/images/2025/07/tocry-06.png

Similarily we can use curl to move issues

$ curl -u your_username:your_password -X PUT http://localhost:3000/boards/myfirstboard/note/e01eee3d-a9f7-4133-8279-ad01d75788d3 \
  -H "Content-Type: application/json" \
  -d '{
    "note": {
      "title": "My Note Title",
      "tags": ["tag1", "tag2"],
      "content": "This is the note content"
    },
    "lane_name": "blocked-review",
    "position": 0
  }'

and I can see it moved

/content/images/2025/07/tocry-07.png

Swagger

I wanted to add a Swagger API to this.

I used some Copilot with GPT-4.1

/content/images/2025/07/tocry-11.png

which added the swagger JSON

{
  "openapi": "3.0.0",
  "info": {
    "title": "ToCry Kanban API",
    "version": "1.0.0"
  },
  "paths": {
    "/boards/{board_name}/lanes": {
      "get": {
        "summary": "Get all lanes for a board",
        "parameters": [
          {
            "name": "board_name",
            "in": "path",
            "required": true,
            "schema": { "type": "string" }
          }
        ],
        "responses": {
          "200": {
            "description": "List of lanes",
            "content": {
              "application/json": {
                "schema": { "type": "array", "items": { "type": "object" } }
              }
            }
          }
        }
      }
    },
    "/boards/{board_name}/note": {
      "post": {
        "summary": "Add a note to a lane",
        "parameters": [
          {
            "name": "board_name",
            "in": "path",
            "required": true,
            "schema": { "type": "string" }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "lane_name": { "type": "string" },
                  "note": {
                    "type": "object",
                    "properties": {
                      "title": { "type": "string" },
                      "tags": { "type": "array", "items": { "type": "string" } },
                      "content": { "type": "string" }
                    },
                    "required": ["title"]
                  }
                },
                "required": ["lane_name", "note"]
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Note created",
            "content": {
              "application/json": {
                "schema": { "type": "object" }
              }
            }
          }
        }
      }
    }
  }
}

I tested with a POST

/content/images/2025/07/tocry-08.png

which made a Note

/content/images/2025/07/tocry-09.png

or get boards

/content/images/2025/07/tocry-10.png

I kept working the same query to add the rest of the possible RESTful interfaces

/content/images/2025/07/tocry-12.png

I also added handling of basic auth. I left in the code, but found because we have to auth to get to the API it is not required for local execute (but would if we wanted example code for local runs).

/content/images/2025/07/tocry-13.png

Then, after much testing and tweaking (often using Chromes network monitor to see the real REST call), I have a pretty good working Swagger page:

PRs and sharing

I now have some work that I might wish to share with others

builder@DESKTOP-QADGF36:~/Workspaces/tocry$ git status
On branch main
Your branch is up to date with 'origin/main'.

Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)
        modified:   src/tocry.cr

Untracked files:
  (use "git add <file>..." to include in what will be committed)
        src/assets/swagger.json
        src/endpoints/swagger.cr

no changes added to commit (use "git add" and/or "git commit -a")

I’ll fork to my own namespace

/content/images/2025/07/tocry-15.png

Then bring it down

builder@DESKTOP-QADGF36:~/Workspaces/tocry$ cd ..
builder@DESKTOP-QADGF36:~/Workspaces$ git clone https://github.com/idjohnson/tocry.git idjohnson-tocry
Cloning into 'idjohnson-tocry'...
remote: Enumerating objects: 1256, done.
remote: Counting objects: 100% (58/58), done.
remote: Compressing objects: 100% (45/45), done.
remote: Total 1256 (delta 13), reused 30 (delta 13), pack-reused 1198 (from 1)
Receiving objects: 100% (1256/1256), 856.30 KiB | 4.58 MiB/s, done.
Resolving deltas: 100% (877/877), done.

I’ll then copy over my changes

builder@DESKTOP-QADGF36:~/Workspaces$ cd idjohnson-tocry/
builder@DESKTOP-QADGF36:~/Workspaces/idjohnson-tocry$ cp ../tocry/src/assets/swagger.json ./src/assets/
builder@DESKTOP-QADGF36:~/Workspaces/idjohnson-tocry$ cp ../tocry/src/tocry.cr ./src/tocry.cr
builder@DESKTOP-QADGF36:~/Workspaces/idjohnson-tocry$ cp ../tocry/src/endpoints/swagger.cr ./src/endpoints/
builder@DESKTOP-QADGF36:~/Workspaces/idjohnson-tocry$ git status
On branch main
Your branch is up to date with 'origin/main'.

Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)
        modified:   src/tocry.cr

Untracked files:
  (use "git add <file>..." to include in what will be committed)
        src/assets/swagger.json
        src/endpoints/swagger.cr

no changes added to commit (use "git add" and/or "git commit -a")

I’ll commit my changes

builder@DESKTOP-QADGF36:~/Workspaces/idjohnson-tocry$ git add -A
builder@DESKTOP-QADGF36:~/Workspaces/idjohnson-tocry$ git commit -m "Offer an OpenAPI spec 3.0 Swagger Endpoint"
[main 3c423ae] Offer an OpenAPI spec 3.0 Swagger Endpoint
 3 files changed, 483 insertions(+), 63 deletions(-)
 create mode 100644 src/assets/swagger.json
 create mode 100644 src/endpoints/swagger.cr
builder@DESKTOP-QADGF36:~/Workspaces/idjohnson-tocry$ git push
Enumerating objects: 13, done.
Counting objects: 100% (13/13), done.
Delta compression using up to 16 threads
Compressing objects: 100% (8/8), done.
Writing objects: 100% (8/8), 2.53 KiB | 2.53 MiB/s, done.
Total 8 (delta 5), reused 0 (delta 0)
remote: Resolving deltas: 100% (5/5), completed with 5 local objects.
To https://github.com/idjohnson/tocry.git
   a577313..3c423ae  main -> main
builder@DESKTOP-QADGF36:~/Workspaces

Lastly, I can create a PR back to their repo

/content/images/2025/07/tocry-16.png

I can then offer this as a PR should they want it.

For those that might just want my latest local built container from above, I did push to Docker hub

builder@DESKTOP-QADGF36:~/Workspaces/idjohnson-tocry$ docker tag tocry:local8 idjohnson/tocry:latest
builder@DESKTOP-QADGF36:~/Workspaces/idjohnson-tocry$ docker push idjohnson/tocry:latest
The push refers to repository [docker.io/idjohnson/tocry]
dff5a8c8e280: Pushed
latest: digest: sha256:97970f13a929b83bf8e557b4bcac3f3ea7e03b8d00dfd00d5bea6c83b68b8146 size: 528

I wrote a basic docker usage and made sure to credit the original author here.

For reference, I tend to run using

$ docker run --restart unless-stopped --name tocry -p 3000:3000 -e TOCRY_AUTH_USER=admin -e TOCRY_AUTH_PASS=password -v /home/builder/Workspaces/tocry/data:/data tocry:local8

FastAPI

While doing some searching for OpenAPI swagger tips, I found FastAPI. This is a framework to build quick python-based API systems with Swagger and RESTful interfaces.

We’ll make a Python virtual env

builder@DESKTOP-QADGF36:~/Workspaces$ mkdir fastapiProject
builder@DESKTOP-QADGF36:~/Workspaces$ cd fastapiProject/
builder@DESKTOP-QADGF36:~/Workspaces/fastapiProject$ python -m venv .venv

Then activate it and update pip

builder@DESKTOP-QADGF36:~/Workspaces/fastapiProject$ source .venv/bin/activate
(.venv) builder@DESKTOP-QADGF36:~/Workspaces/fastapiProject$ python -m pip install --upgrade pip
Requirement already satisfied: pip in ./.venv/lib/python3.11/site-packages (23.3.1)
Collecting pip
  Downloading pip-25.1.1-py3-none-any.whl.metadata (3.6 kB)
Downloading pip-25.1.1-py3-none-any.whl (1.8 MB)
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 1.8/1.8 MB 16.0 MB/s eta 0:00:00
Installing collected packages: pip
  Attempting uninstall: pip
    Found existing installation: pip 23.3.1
    Uninstalling pip-23.3.1:
      Successfully uninstalled pip-23.3.1
Successfully installed pip-25.1.1
(.venv) builder@DESKTOP-QADGF36:~/Workspaces/fastapiProject$

I can now instll fastapi with pip install "fastapi[standard]"

(.venv) builder@DESKTOP-QADGF36:~/Workspaces/fastapiProject$ pip install "fastapi[standard]"
Collecting fastapi[standard]
  Downloading fastapi-0.115.14-py3-none-any.whl.metadata (27 kB)
Collecting starlette<0.47.0,>=0.40.0 (from fastapi[standard])
  Using cached starlette-0.46.2-py3-none-any.whl.metadata (6.2 kB)
Collecting pydantic!=1.8,!=1.8.1,!=2.0.0,!=2.0.1,!=2.1.0,<3.0.0,>=1.7.4 (from fastapi[standard])
  Downloading pydantic-2.11.7-py3-none-any.whl.metadata (67 kB)
Collecting typing-extensions>=4.8.0 (from fastapi[standard])
  Downloading typing_extensions-4.14.0-py3-none-any.whl.metadata (3.0 kB)
Collecting fastapi-cli>=0.0.5 (from fastapi-cli[standard]>=0.0.5; extra == "standard"->fastapi[standard])
  Downloading fastapi_cli-0.0.7-py3-none-any.whl.metadata (6.2 kB)
Collecting httpx>=0.23.0 (from fastapi[standard])
  Using cached httpx-0.28.1-py3-none-any.whl.metadata (7.1 kB)
Collecting jinja2>=3.1.5 (from fastapi[standard])
  Downloading jinja2-3.1.6-py3-none-any.whl.metadata (2.9 kB)
Collecting python-multipart>=0.0.18 (from fastapi[standard])
  Downloading python_multipart-0.0.20-py3-none-any.whl.metadata (1.8 kB)
Collecting email-validator>=2.0.0 (from fastapi[standard])
  Downloading email_validator-2.2.0-py3-none-any.whl.metadata (25 kB)
Collecting uvicorn>=0.12.0 (from uvicorn[standard]>=0.12.0; extra == "standard"->fastapi[standard])
  Downloading uvicorn-0.35.0-py3-none-any.whl.metadata (6.5 kB)
Collecting annotated-types>=0.6.0 (from pydantic!=1.8,!=1.8.1,!=2.0.0,!=2.0.1,!=2.1.0,<3.0.0,>=1.7.4->fastapi[standard])
  Using cached annotated_types-0.7.0-py3-none-any.whl.metadata (15 kB)
Collecting pydantic-core==2.33.2 (from pydantic!=1.8,!=1.8.1,!=2.0.0,!=2.0.1,!=2.1.0,<3.0.0,>=1.7.4->fastapi[standard])
  Downloading pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.metadata (6.8 kB)
Collecting typing-inspection>=0.4.0 (from pydantic!=1.8,!=1.8.1,!=2.0.0,!=2.0.1,!=2.1.0,<3.0.0,>=1.7.4->fastapi[standard])
  Using cached typing_inspection-0.4.1-py3-none-any.whl.metadata (2.6 kB)
Collecting anyio<5,>=3.6.2 (from starlette<0.47.0,>=0.40.0->fastapi[standard])
  Using cached anyio-4.9.0-py3-none-any.whl.metadata (4.7 kB)
Collecting idna>=2.8 (from anyio<5,>=3.6.2->starlette<0.47.0,>=0.40.0->fastapi[standard])
  Using cached idna-3.10-py3-none-any.whl.metadata (10 kB)
Collecting sniffio>=1.1 (from anyio<5,>=3.6.2->starlette<0.47.0,>=0.40.0->fastapi[standard])
  Using cached sniffio-1.3.1-py3-none-any.whl.metadata (3.9 kB)
Collecting dnspython>=2.0.0 (from email-validator>=2.0.0->fastapi[standard])
  Using cached dnspython-2.7.0-py3-none-any.whl.metadata (5.8 kB)
Collecting typer>=0.12.3 (from fastapi-cli>=0.0.5->fastapi-cli[standard]>=0.0.5; extra == "standard"->fastapi[standard])
  Downloading typer-0.16.0-py3-none-any.whl.metadata (15 kB)
Collecting rich-toolkit>=0.11.1 (from fastapi-cli>=0.0.5->fastapi-cli[standard]>=0.0.5; extra == "standard"->fastapi[standard])
  Downloading rich_toolkit-0.14.8-py3-none-any.whl.metadata (999 bytes)
Collecting certifi (from httpx>=0.23.0->fastapi[standard])
  Downloading certifi-2025.6.15-py3-none-any.whl.metadata (2.4 kB)
Collecting httpcore==1.* (from httpx>=0.23.0->fastapi[standard])
  Using cached httpcore-1.0.9-py3-none-any.whl.metadata (21 kB)
Collecting h11>=0.16 (from httpcore==1.*->httpx>=0.23.0->fastapi[standard])
  Using cached h11-0.16.0-py3-none-any.whl.metadata (8.3 kB)
Collecting MarkupSafe>=2.0 (from jinja2>=3.1.5->fastapi[standard])
  Downloading MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.metadata (4.0 kB)
Collecting click>=8.1.7 (from rich-toolkit>=0.11.1->fastapi-cli>=0.0.5->fastapi-cli[standard]>=0.0.5; extra == "standard"->fastapi[standard])
  Downloading click-8.2.1-py3-none-any.whl.metadata (2.5 kB)
Collecting rich>=13.7.1 (from rich-toolkit>=0.11.1->fastapi-cli>=0.0.5->fastapi-cli[standard]>=0.0.5; extra == "standard"->fastapi[standard])
  Downloading rich-14.0.0-py3-none-any.whl.metadata (18 kB)
Collecting markdown-it-py>=2.2.0 (from rich>=13.7.1->rich-toolkit>=0.11.1->fastapi-cli>=0.0.5->fastapi-cli[standard]>=0.0.5; extra == "standard"->fastapi[standard])
  Downloading markdown_it_py-3.0.0-py3-none-any.whl.metadata (6.9 kB)
Collecting pygments<3.0.0,>=2.13.0 (from rich>=13.7.1->rich-toolkit>=0.11.1->fastapi-cli>=0.0.5->fastapi-cli[standard]>=0.0.5; extra == "standard"->fastapi[standard])
  Downloading pygments-2.19.2-py3-none-any.whl.metadata (2.5 kB)
Collecting mdurl~=0.1 (from markdown-it-py>=2.2.0->rich>=13.7.1->rich-toolkit>=0.11.1->fastapi-cli>=0.0.5->fastapi-cli[standard]>=0.0.5; extra == "standard"->fastapi[standard])
  Downloading mdurl-0.1.2-py3-none-any.whl.metadata (1.6 kB)
Collecting shellingham>=1.3.0 (from typer>=0.12.3->fastapi-cli>=0.0.5->fastapi-cli[standard]>=0.0.5; extra == "standard"->fastapi[standard])
  Downloading shellingham-1.5.4-py2.py3-none-any.whl.metadata (3.5 kB)
Collecting httptools>=0.6.3 (from uvicorn[standard]>=0.12.0; extra == "standard"->fastapi[standard])
  Downloading httptools-0.6.4-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl.metadata (3.6 kB)
Collecting python-dotenv>=0.13 (from uvicorn[standard]>=0.12.0; extra == "standard"->fastapi[standard])
  Downloading python_dotenv-1.1.1-py3-none-any.whl.metadata (24 kB)
Collecting pyyaml>=5.1 (from uvicorn[standard]>=0.12.0; extra == "standard"->fastapi[standard])
  Using cached PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.metadata (2.1 kB)
Collecting uvloop>=0.15.1 (from uvicorn[standard]>=0.12.0; extra == "standard"->fastapi[standard])
  Downloading uvloop-0.21.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.metadata (4.9 kB)
Collecting watchfiles>=0.13 (from uvicorn[standard]>=0.12.0; extra == "standard"->fastapi[standard])
  Downloading watchfiles-1.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.metadata (4.9 kB)
Collecting websockets>=10.4 (from uvicorn[standard]>=0.12.0; extra == "standard"->fastapi[standard])
  Using cached websockets-15.0.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl.metadata (6.8 kB)
Downloading fastapi-0.115.14-py3-none-any.whl (95 kB)
Downloading pydantic-2.11.7-py3-none-any.whl (444 kB)
Downloading pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (2.0 MB)
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 2.0/2.0 MB 16.4 MB/s eta 0:00:00
Using cached starlette-0.46.2-py3-none-any.whl (72 kB)
Using cached anyio-4.9.0-py3-none-any.whl (100 kB)
Using cached annotated_types-0.7.0-py3-none-any.whl (13 kB)
Downloading email_validator-2.2.0-py3-none-any.whl (33 kB)
Using cached dnspython-2.7.0-py3-none-any.whl (313 kB)
Downloading fastapi_cli-0.0.7-py3-none-any.whl (10 kB)
Using cached httpx-0.28.1-py3-none-any.whl (73 kB)
Using cached httpcore-1.0.9-py3-none-any.whl (78 kB)
Using cached h11-0.16.0-py3-none-any.whl (37 kB)
Using cached idna-3.10-py3-none-any.whl (70 kB)
Downloading jinja2-3.1.6-py3-none-any.whl (134 kB)
Downloading MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (23 kB)
Downloading python_multipart-0.0.20-py3-none-any.whl (24 kB)
Downloading rich_toolkit-0.14.8-py3-none-any.whl (24 kB)
Downloading click-8.2.1-py3-none-any.whl (102 kB)
Downloading rich-14.0.0-py3-none-any.whl (243 kB)
Downloading pygments-2.19.2-py3-none-any.whl (1.2 MB)
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 1.2/1.2 MB 19.0 MB/s eta 0:00:00
Downloading markdown_it_py-3.0.0-py3-none-any.whl (87 kB)
Downloading mdurl-0.1.2-py3-none-any.whl (10.0 kB)
Using cached sniffio-1.3.1-py3-none-any.whl (10 kB)
Downloading typer-0.16.0-py3-none-any.whl (46 kB)
Downloading shellingham-1.5.4-py2.py3-none-any.whl (9.8 kB)
Downloading typing_extensions-4.14.0-py3-none-any.whl (43 kB)
Using cached typing_inspection-0.4.1-py3-none-any.whl (14 kB)
Downloading uvicorn-0.35.0-py3-none-any.whl (66 kB)
Downloading httptools-0.6.4-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl (459 kB)
Downloading python_dotenv-1.1.1-py3-none-any.whl (20 kB)
Using cached PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (762 kB)
Downloading uvloop-0.21.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (4.0 MB)
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 4.0/4.0 MB 16.1 MB/s eta 0:00:00
Downloading watchfiles-1.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (453 kB)
Using cached websockets-15.0.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl (182 kB)
Downloading certifi-2025.6.15-py3-none-any.whl (157 kB)
Installing collected packages: websockets, uvloop, typing-extensions, sniffio, shellingham, pyyaml, python-multipart, python-dotenv, pygments, mdurl, MarkupSafe, idna, httptools, h11, dnspython, click, certifi, annotated-types, uvicorn, typing-inspection, pydantic-core, markdown-it-py, jinja2, httpcore, email-validator, anyio, watchfiles, starlette, rich, pydantic, httpx, typer, rich-toolkit, fastapi, fastapi-cli
Successfully installed MarkupSafe-3.0.2 annotated-types-0.7.0 anyio-4.9.0 certifi-2025.6.15 click-8.2.1 dnspython-2.7.0 email-validator-2.2.0 fastapi-0.115.14 fastapi-cli-0.0.7 h11-0.16.0 httpcore-1.0.9 httptools-0.6.4 httpx-0.28.1 idna-3.10 jinja2-3.1.6 markdown-it-py-3.0.0 mdurl-0.1.2 pydantic-2.11.7 pydantic-core-2.33.2 pygments-2.19.2 python-dotenv-1.1.1 python-multipart-0.0.20 pyyaml-6.0.2 rich-14.0.0 rich-toolkit-0.14.8 shellingham-1.5.4 sniffio-1.3.1 starlette-0.46.2 typer-0.16.0 typing-extensions-4.14.0 typing-inspection-0.4.1 uvicorn-0.35.0 uvloop-0.21.0 watchfiles-1.1.0 websockets-15.0.1
(.venv) builder@DESKTOP-QADGF36:~/Workspaces/fastapiProject$

we can now make a small REST API demo

(.venv) builder@DESKTOP-QADGF36:~/Workspaces/fastapiProject$ vi main.py
(.venv) builder@DESKTOP-QADGF36:~/Workspaces/fastapiProject$ cat ./main.py
from typing import Union

from fastapi import FastAPI

app = FastAPI()

@app.get("/")
def read_root():
    return {"Hello": "Fresh"}


@app.get("/items/{item_id}")
def read_item(item_id: int, q: Union[str, None] = None):
    return {"item_id": item_id, "q": q}

I’ll now run it with fastapi dev main.py

/content/images/2025/07/fastapi-01.png

The front door works dandy

/content/images/2025/07/fastapi-02.png

The swagger was built for me

/content/images/2025/07/fastapi-03.png

And I can try it out and get a response

/content/images/2025/07/fastapi-04.png

and we can try some different values via the Swagger API

/content/images/2025/07/fastapi-05.png

If Swagger isn’t your thing, there is Redoc as well exposed on “/redoc”

/content/images/2025/07/fastapi-06.png

Though, unlike Swagger, Redoc doesn’t provide interactive “try it” buttons. It is just about good documentation

/content/images/2025/07/fastapi-07.png

Dockerfile

Checking the Python I’m using

(.venv) builder@DESKTOP-QADGF36:~/Workspaces/fastapiProject$ python --version
Python 3.11.6

Suggests this should be easy to build into a docker container.

FROM python:3.11-slim

WORKDIR /app

COPY main.py .

RUN pip install --upgrade pip && \
    pip install "fastapi[standard]"

EXPOSE 8000

CMD ["fastapi","dev", "main.py"]

I can build that

(.venv) builder@DESKTOP-QADGF36:~/Workspaces/fastapiProject$ docker build -t fastapi:test .
[+] Building 22.5s (10/10) FINISHED                                                                                        docker:default
 => [internal] load build definition from Dockerfile                                                                                 0.0s
 => => transferring dockerfile: 210B                                                                                                 0.0s
 => [internal] load metadata for docker.io/library/python:3.11-slim                                                                  1.1s
 => [auth] library/python:pull token for registry-1.docker.io                                                                        0.0s
 => [internal] load .dockerignore                                                                                                    0.0s
 => => transferring context: 2B                                                                                                      0.0s
 => [1/4] FROM docker.io/library/python:3.11-slim@sha256:747b7782488dbf0a7c247b4da006097b871ae3a3fcd5d943c21b376eab6a6ef4            7.1s
 => => resolve docker.io/library/python:3.11-slim@sha256:747b7782488dbf0a7c247b4da006097b871ae3a3fcd5d943c21b376eab6a6ef4            0.1s
 => => sha256:747b7782488dbf0a7c247b4da006097b871ae3a3fcd5d943c21b376eab6a6ef4 9.13kB / 9.13kB                                       0.0s
 => => sha256:153bae509ec02c9ac789e2e35f3cbe94be446b59c3afcfbbc88c96a344d2eb73 1.75kB / 1.75kB                                       0.0s
 => => sha256:0b14a859cdba15104c5f194ef813fcccbf2749d74bc7be4550c06a0fc0d482e6 5.37kB / 5.37kB                                       0.0s
 => => sha256:3da95a905ed546f99c4564407923a681757d89651a388ec3f1f5e9bf5ed0b39d 28.23MB / 28.23MB                                     1.3s
 => => sha256:483d0dd375188d7d3b2d66116d5974d2b67e6988c6146eb2c6a3e2bc33a92121 3.51MB / 3.51MB                                       0.5s
 => => sha256:02a5d22e0d6f85a6ac1c7fb356e9eed39a981decd1fac1205a31f31f4cb010f1 16.21MB / 16.21MB                                     1.1s
 => => sha256:471797cdda8c4cd4a9795c8cb56078e627b3fc7486fd8574804ec7d5f1676b9b 249B / 249B                                           0.7s
 => => extracting sha256:3da95a905ed546f99c4564407923a681757d89651a388ec3f1f5e9bf5ed0b39d                                            3.2s
 => => extracting sha256:483d0dd375188d7d3b2d66116d5974d2b67e6988c6146eb2c6a3e2bc33a92121                                            0.3s
 => => extracting sha256:02a5d22e0d6f85a6ac1c7fb356e9eed39a981decd1fac1205a31f31f4cb010f1                                            1.9s
 => => extracting sha256:471797cdda8c4cd4a9795c8cb56078e627b3fc7486fd8574804ec7d5f1676b9b                                            0.0s
 => [internal] load build context                                                                                                    0.0s
 => => transferring context: 297B                                                                                                    0.0s
 => [2/4] WORKDIR /app                                                                                                               0.4s
 => [3/4] COPY main.py .                                                                                                             0.0s
 => [4/4] RUN pip install --upgrade pip &&     pip install "fastapi[standard]"                                                      12.8s
 => exporting to image                                                                                                               0.9s
 => => exporting layers                                                                                                              0.9s
 => => writing image sha256:1400413b555875e39b1a16c30b1f9ba0e7c9de5ff4b53332e6f5f030a6f7f12f                                         0.0s
 => => naming to docker.io/library/fastapi:test                                                                                      0.0s
(.venv) builder@DESKTOP-QADGF36:~/Workspaces/fastapiProject$

I can now test

(.venv) builder@DESKTOP-QADGF36:~/Workspaces/fastapiProject$ docker run --rm -p 8000:8000 fastapi:test

   FastAPI   Starting development server 🚀

             Searching for package file structure from directories with
             __init__.py files
             Importing from /app

    module   🐍 main.py

      code   Importing the FastAPI app object from the module with the following
             code:

             from main import app

       app   Using import string: main:app

    server   Server started at http://127.0.0.1:8000
    server   Documentation at http://127.0.0.1:8000/docs

       tip   Running in development mode, for production use: fastapi run

             Logs:

      INFO   Will watch for changes in these directories: ['/app']
      INFO   Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
      INFO   Started reloader process [1] using WatchFiles
      INFO   Started server process [8]
      INFO   Waiting for application startup.
      INFO   Application startup complete.

However, that did not work

/content/images/2025/07/fastapi-08.png

I did fix pretty quick. Just use run instead of dev. for the CMD invokation.

That is:

FROM python:3.11-slim

WORKDIR /app

COPY main.py .

RUN pip install --upgrade pip && \
    pip install "fastapi[standard]"

EXPOSE 8000

CMD ["fastapi","run", "main.py"]

Bluesky Poster

I got excited to add FastAPI to my BlueSky poster app. I wrote that with Flask, so it should be straightforward to swap out Flask with FastAPI

/content/images/2025/07/fastapi-09.png

After some tweaks (like figuring out the proper way to use JSON payloads instead of QueryParameters), I got it to work

/content/images/2025/07/fastapi-10.png

And we can see the results in Bluesky

/content/images/2025/07/fastapi-11.png

And what a good time, while we are updating the chart, to reference Goldilocks to properly update the container specs

/content/images/2025/07/fastapi-12.png

And as added to the updated chart

/content/images/2025/07/fastapi-13.png

I’ll update and push to main

builder@DESKTOP-QADGF36:~/Workspaces/pybsposter$ git commit -m "new 0.1.0 version with FastAPI instead of Flask, new chart as well"
[main 81b382d] new 0.1.0 version with FastAPI instead of Flask, new chart as well
 6 files changed, 52 insertions(+), 25 deletions(-)
builder@DESKTOP-QADGF36:~/Workspaces/pybsposter$ git push
Enumerating objects: 21, done.
Counting objects: 100% (21/21), done.
Delta compression using up to 16 threads
Compressing objects: 100% (9/9), done.
Writing objects: 100% (11/11), 1.40 KiB | 719.00 KiB/s, done.
Total 11 (delta 7), reused 0 (delta 0)
remote: Resolving deltas: 100% (7/7), completed with 7 local objects.
To https://github.com/idjohnson/pybsposter.git
   e51072d..81b382d  main -> main

The automated CICD build completed

/content/images/2025/07/fastapi-14.png

So now I could upgrade my production cluster. It has been a while since I updated

$ helm list | grep pybsposter
pybsposter                      default         6               2025-02-06 08:14:25.12365721 -0600 CST  deployed        pybsposter-0.1.1                1.0
$ helm get values pybsposter
USER-SUPPLIED VALUES:
image:
  tag: 0.0.5
ingress:
  enabled: true

I did a quick dry-run first to ensure this would work (noting the image, chart version and memory/cpu setttings)

builder@DESKTOP-QADGF36:~/Workspaces/pybsposter$ helm upgrade --dry-run --debug pybsposter --set image.tag=0.1.2 --set ingress.enabled=true ./charts/pybsposter/
upgrade.go:164: 2025-07-01 12:21:30.519245608 -0500 CDT m=+0.266434903 [debug] preparing upgrade for pybsposter
upgrade.go:172: 2025-07-01 12:21:30.780895366 -0500 CDT m=+0.528084671 [debug] performing update for pybsposter
upgrade.go:366: 2025-07-01 12:21:30.887349751 -0500 CDT m=+0.634539056 [debug] dry run for pybsposter
Release "pybsposter" has been upgraded. Happy Helming!
NAME: pybsposter
LAST DEPLOYED: Tue Jul  1 12:21:30 2025
NAMESPACE: default
STATUS: pending-upgrade
REVISION: 7
TEST SUITE: None
USER-SUPPLIED VALUES:
image:
  tag: 0.1.2
ingress:
  enabled: true

COMPUTED VALUES:
image:
  pullPolicy: IfNotPresent
  repository: harbor.freshbrewed.science/library/pybsposter
  tag: 0.1.2
ingress:
  certManagerIssuer: gcpleprod2
  clientMaxBodySize: "0"
  connectTimeout: "3600"
  enabled: true
  host: bskyposter.steeped.space
  ingressClass: nginx
  proxyBodySize: "0"
  readTimeout: "3600"
  sendTimeout: "3600"
  sslRedirect: "true"
  tlsAcme: "true"
  tlsSecretName: pybspostergcp-tls
replicas: 1
resources:
  limits:
    cpu: 15m
    memory: 133Mi
  requests:
    cpu: 15m
    memory: 105Mi
service:
  port: 80
  targetPort: 8000
  type: ClusterIP

HOOKS:
MANIFEST:
---
# Source: pybsposter/templates/service.yaml
apiVersion: v1
kind: Service
metadata:
  name: pybsposter
  labels:
    helm.sh/chart: pybsposter-0.1.2
    app.kubernetes.io/name: pybsposter
    app.kubernetes.io/instance: pybsposter
    app.kubernetes.io/version: "2.0"
    app.kubernetes.io/managed-by: Helm
spec:
  selector:
    app.kubernetes.io/name: pybsposter
    app.kubernetes.io/instance: pybsposter
  type: ClusterIP
  ports:
    - protocol: TCP
      port: 80
      targetPort: 8000
---
# Source: pybsposter/templates/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: pybsposter
  labels:
    helm.sh/chart: pybsposter-0.1.2
    app.kubernetes.io/name: pybsposter
    app.kubernetes.io/instance: pybsposter
    app.kubernetes.io/version: "2.0"
    app.kubernetes.io/managed-by: Helm
spec:
  replicas: 1
  selector:
    matchLabels:
      app.kubernetes.io/name: pybsposter
      app.kubernetes.io/instance: pybsposter
  template:
    metadata:
      labels:
        app.kubernetes.io/name: pybsposter
        app.kubernetes.io/instance: pybsposter
    spec:
      containers:
        - name: pybsposter
          image: "harbor.freshbrewed.science/library/pybsposter:0.1.2"
          imagePullPolicy: IfNotPresent
          ports:
          - containerPort: 8000
          resources:
            limits:
              cpu: 15m
              memory: 133Mi
            requests:
              cpu: 15m
              memory: 105Mi
---
# Source: pybsposter/templates/ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: pybsposter
  labels:
    helm.sh/chart: pybsposter-0.1.2
    app.kubernetes.io/name: pybsposter
    app.kubernetes.io/instance: pybsposter
    app.kubernetes.io/version: "2.0"
    app.kubernetes.io/managed-by: Helm
  annotations:
    cert-manager.io/cluster-issuer: "gcpleprod2"
    ingress.kubernetes.io/proxy-body-size: "0"
    ingress.kubernetes.io/ssl-redirect: "true"
    kubernetes.io/ingress.class: "nginx"
    kubernetes.io/tls-acme: "true"
    nginx.ingress.kubernetes.io/proxy-body-size: "0"
    nginx.ingress.kubernetes.io/proxy-read-timeout: "3600"
    nginx.ingress.kubernetes.io/proxy-send-timeout: "3600"
    nginx.ingress.kubernetes.io/ssl-redirect: "true"
    nginx.org/client-max-body-size: "0"
    nginx.org/proxy-connect-timeout: "3600"
    nginx.org/proxy-read-timeout: "3600"
    nginx.org/websocket-services: pybsposter
spec:
  rules:
  - host: bskyposter.steeped.space
    http:
      paths:
      - backend:
          service:
            name: pybsposter
            port:
              number: 80
        path: /
        pathType: ImplementationSpecific
  tls:
  - hosts:
    - bskyposter.steeped.space
    secretName: pybspostergcp-tls

Looks good, so let’s upgrade

builder@DESKTOP-QADGF36:~/Workspaces/pybsposter$ helm upgrade pybsposter --set image.tag=0.1.2 --set ingress.enabled=true ./charts/pybsposter/
Release "pybsposter" has been upgraded. Happy Helming!
NAME: pybsposter
LAST DEPLOYED: Tue Jul  1 12:22:22 2025
NAMESPACE: default
STATUS: deployed
REVISION: 7
TEST SUITE: None

However, I saw an error

builder@DESKTOP-QADGF36:~/Workspaces/pybsposter$ kubectl get po -l app.kubernetes.io/instance=pybsposter
NAME                          READY   STATUS                 RESTARTS      AGE
pybsposter-697b79dc87-j46mj   0/1     CreateContainerError   0             69s
pybsposter-7d4dfbcddb-gz9kv   1/1     Running                1 (11d ago)   144d
builder@DESKTOP-QADGF36:~/Workspaces/pybsposter$ kubectl describe po pybsposter-697b79dc87-j46mj | tail -n 15
                             node.kubernetes.io/unreachable:NoExecute op=Exists for 300s
Events:
  Type     Reason     Age               From               Message
  ----     ------     ----              ----               -------
  Normal   Scheduled  77s               default-scheduler  Successfully assigned default/pybsposter-697b79dc87-j46mj to builder-hp-elitebook-745-g5
  Normal   Pulling    71s               kubelet            Pulling image "harbor.freshbrewed.science/library/pybsposter:0.1.2"
  Normal   Pulled     70s               kubelet            Successfully pulled image "harbor.freshbrewed.science/library/pybsposter:0.1.2" in 999ms (999ms including waiting)
  Warning  Failed     70s               kubelet            Error: failed to generate container "2d4d07a38ddd7c1835c76c58be29524754bedb6f26b54fdb95620a17022f4f0c" spec: failed to generate spec: no command specified
  Warning  Failed     70s               kubelet            Error: failed to generate container "8264aaf09d8da45aea2afe02fa18604b54cdf082e2d712b8fa02bfcd3b81eb8d" spec: failed to generate spec: no command specified
  Warning  Failed     58s               kubelet            Error: failed to generate container "c231f752ae7c76eb7bddab9dae9de68ae9516f9f5525d1fd54af079a5abb0425" spec: failed to generate spec: no command specified
  Warning  Failed     45s               kubelet            Error: failed to generate container "6e9c950585cd3974a5773aecb6bdf407744799ba2c1bf240b0f6d7f870fd4f3a" spec: failed to generate spec: no command specified
  Warning  Failed     30s               kubelet            Error: failed to generate container "b9afd7dda6313672d151c19d91113df43c5e0299318eda8865c4e1310ffb0c45" spec: failed to generate spec: no command specified
  Warning  Failed     19s               kubelet            Error: failed to generate container "50ce21ffd7ae3d11305b6ce44a0e02c2d811d29c7a41528dc5a03f05a703e216" spec: failed to generate spec: no command specified
  Normal   Pulled     4s (x6 over 70s)  kubelet            Container image "harbor.freshbrewed.science/library/pybsposter:0.1.2" already present on machine
  Warning  Failed     4s                kubelet            Error: failed to generate container "f75e52d2605ce34011ba36e3697bf2be704c7e962c9be848b1ef784425e39f1a" spec: failed to generate spec: no command specified

That is entirely my error as the container should have been 0.1.0 not 0.1.2 (that is the chart)

/content/images/2025/07/fastapi-15.png

I’ll fix that real quick by changing the image tag

$ helm upgrade pybsposter --set image.tag=0.1.0 --set ingress.enabled=true ./charts/pybsposter/
Release "pybsposter" has been upgraded. Happy Helming!
NAME: pybsposter
LAST DEPLOYED: Tue Jul  1 12:26:45 2025
NAMESPACE: default
STATUS: deployed
REVISION: 8
TEST SUITE: None

That looks much better

$ kubectl get po -l app.kubernetes.io/instance=pybsposter
NAME                          READY   STATUS        RESTARTS      AGE
pybsposter-69f8d7cc69-b2p7v   1/1     Running       0             21s
pybsposter-7d4dfbcddb-gz9kv   1/1     Terminating   1 (11d ago)   144d

However, while the pod is happy, the ingress is not

/content/images/2025/07/fastapi-16.png

I did some fighting, and it turns out Goldilocks was a bit too reduced on the container specs. It was failing to really load until I upped the values

resources:
  requests:
    cpu: 15m
    memory: 105Mi
  limits:
    cpu: 250m
    memory: 250Mi

Now I can see the Swagger

/content/images/2025/07/fastapi-17.png

as well as Redoc

/content/images/2025/07/fastapi-18.png

I then realized my existing workflows used capitalized fields like “UNSERNAME” and “PASSWORD” which failed.

I fixed the code


class SocialPost(BaseModel):
    username: str
    password: str
    text: str
    link: str | None = None

    class Config:
        allow_population_by_field_name = True
        alias_generator = lambda s: s.upper()
        populate_by_name = True

then pushed it up

builder@LuiGi:~/Workspaces/pybsposter$ git commit -m "update to accept uppercase as the old poster did"
[main 83fa4f2] update to accept uppercase as the old poster did
 2 files changed, 6 insertions(+), 1 deletion(-)
builder@LuiGi:~/Workspaces/pybsposter$ git push
Enumerating objects: 7, done.
Counting objects: 100% (7/7), done.
Delta compression using up to 16 threads
Compressing objects: 100% (4/4), done.
Writing objects: 100% (4/4), 485 bytes | 485.00 KiB/s, done.
Total 4 (delta 3), reused 0 (delta 0), pack-reused 0 (from 0)
remote: Resolving deltas: 100% (3/3), completed with 3 local objects.
To https://github.com/idjohnson/pybsposter.git
   192af1c..83fa4f2  main -> main

Which made a new 0.1.1 image

/content/images/2025/07/fastapi-19.png

I can nupgrade with helm

$ helm upgrade pybsposter --set image.tag=0.1.1 --set ingress.enabled=true ./charts/pybsposter/
Release "pybsposter" has been upgraded. Happy Helming!
NAME: pybsposter
LAST DEPLOYED: Tue Jul  1 17:52:33 2025
NAMESPACE: default
STATUS: deployed
REVISION: 11
TEST SUITE: None

and a quick test works

/content/images/2025/07/fastapi-20.png

Typer

They have another project for making CLI projects, Typer.

Our setup is similar to FastAPI

(.venv) builder@DESKTOP-QADGF36:~/Workspaces$ mkdir typerProject
(.venv) builder@DESKTOP-QADGF36:~/Workspaces$ cd typerProject/
(.venv) builder@DESKTOP-QADGF36:~/Workspaces/typerProject$ python -m venv .venv
(.venv) builder@DESKTOP-QADGF36:~/Workspaces/typerProject$ source .venv/bin/activate
(.venv) builder@DESKTOP-QADGF36:~/Workspaces/typerProject$ python -m pip install --upgrade pip
Requirement already satisfied: pip in ./.venv/lib/python3.11/site-packages (23.3.1)
Collecting pip
  Using cached pip-25.1.1-py3-none-any.whl.metadata (3.6 kB)
Using cached pip-25.1.1-py3-none-any.whl (1.8 MB)
Installing collected packages: pip
  Attempting uninstall: pip
    Found existing installation: pip 23.3.1
    Uninstalling pip-23.3.1:
      Successfully uninstalled pip-23.3.1
Successfully installed pip-25.1.1
(.venv) builder@DESKTOP-QADGF36:~/Workspaces/typerProject$ pip install typer
Collecting typer
  Using cached typer-0.16.0-py3-none-any.whl.metadata (15 kB)
Collecting click>=8.0.0 (from typer)
  Using cached click-8.2.1-py3-none-any.whl.metadata (2.5 kB)
Collecting typing-extensions>=3.7.4.3 (from typer)
  Using cached typing_extensions-4.14.0-py3-none-any.whl.metadata (3.0 kB)
Collecting shellingham>=1.3.0 (from typer)
  Using cached shellingham-1.5.4-py2.py3-none-any.whl.metadata (3.5 kB)
Collecting rich>=10.11.0 (from typer)
  Using cached rich-14.0.0-py3-none-any.whl.metadata (18 kB)
Collecting markdown-it-py>=2.2.0 (from rich>=10.11.0->typer)
  Using cached markdown_it_py-3.0.0-py3-none-any.whl.metadata (6.9 kB)
Collecting pygments<3.0.0,>=2.13.0 (from rich>=10.11.0->typer)
  Using cached pygments-2.19.2-py3-none-any.whl.metadata (2.5 kB)
Collecting mdurl~=0.1 (from markdown-it-py>=2.2.0->rich>=10.11.0->typer)
  Using cached mdurl-0.1.2-py3-none-any.whl.metadata (1.6 kB)
Using cached typer-0.16.0-py3-none-any.whl (46 kB)
Using cached click-8.2.1-py3-none-any.whl (102 kB)
Using cached rich-14.0.0-py3-none-any.whl (243 kB)
Using cached pygments-2.19.2-py3-none-any.whl (1.2 MB)
Using cached markdown_it_py-3.0.0-py3-none-any.whl (87 kB)
Using cached mdurl-0.1.2-py3-none-any.whl (10.0 kB)
Using cached shellingham-1.5.4-py2.py3-none-any.whl (9.8 kB)
Using cached typing_extensions-4.14.0-py3-none-any.whl (43 kB)
Installing collected packages: typing-extensions, shellingham, pygments, mdurl, click, markdown-it-py, rich, typer
Successfully installed click-8.2.1 markdown-it-py-3.0.0 mdurl-0.1.2 pygments-2.19.2 rich-14.0.0 shellingham-1.5.4 typer-0.16.0 typing-extensions-4.14.0

We’ll make the basic Hello World example

(.venv) builder@DESKTOP-QADGF36:~/Workspaces/typerProject$ vi main.py
(.venv) builder@DESKTOP-QADGF36:~/Workspaces/typerProject$ cat main.py
def main(name: str):
    print(f"Hello {name}")

We can now see an example error, help and actual usage:

/content/images/2025/07/typer-01.png

We can also just import typer to run in python

(.venv) builder@DESKTOP-QADGF36:~/Workspaces/typerProject$ cat main.py
import typer


def main(name: str):
    print(f"Greetings {name}")

def main(name: str, mood: str):
    print(f"Hi {name}, sounds like you are feeling {mood}")

if __name__ == "__main__":
    typer.run(main)

/content/images/2025/07/typer-02.png

you’ll note that mood was set to required. But we an use defaults as well to make those optional

(.venv) builder@DESKTOP-QADGF36:~/Workspaces/typerProject$ cat main.py
import typer

app = typer.Typer()

@app.command()
def main(name: str, mood: str = "jolly"):
    print(f"Hi {name}, sounds like you are feeling {mood}")

if __name__ == "__main__":
    typer.run(main)

/content/images/2025/07/typer-03.png

Summary

Today we looked into ToCry and set it up in Docker and Kubernetes. I really liked it and wanted to consider using it with a pipeline which necessitated API access. This prompted me to build out a Swagger (OpenAPI) backend and share it back with the author.

In doing that, I came across FastAPI which makes it easy to add automatically created Swagger (and Redoc) APIs around python apps. After doing the demo code, I pulled down my Bluesky Poster app (pybsposter) and moved it from Flask to FastAPI. This was pretty easy and now is running at https://bskyposter.steeped.space/ with a new swagger interface in /docs.

I also looked into some other OS tools from Sebastián Ramírez such as Typer which does for a CLI what FastAPI did for Web APIs.

opensource todo tocry fastapi typer

Have something to add? Feedback? You can use the feedback form

Isaac Johnson

Isaac Johnson

Cloud Solutions Architect

Isaac is a CSA and DevOps engineer who focuses on cloud migrations and devops processes. He also is a dad to three wonderful daughters (hence the references to Princess King sprinkled throughout the blog).

Theme built by C.S. Rhymes