Published: Jul 10, 2025 by Isaac Johnson
First, I should point out that if you like watching things instead of reading, I did create a YouTube video that covers how to setup and use Gemini CLI already. In that video I do some updates to a Python project and show the results.
However, here we will start again fresh and show how we can install Gemini CLI and use it as a code writing tool (very similar to Claude Code but faster and cheaper).
Getting started
In the video I updated PyBsPoster to add system diagrams and update the Readme.
How about this time we update python-kasa which was a fork of the original python-kasa which basically Dockerized it
I like Flask, I do, but I have a new favourite FastAPI
Let’s do two things - let’s sync up my repo, then update to FastAPI.
Now that we have a plan, let’s start by adding the gemini
cli.
Unfortunately, i didn’t actually fork in Github, just make my own forked copy. So fork synching won’t work
builder@DESKTOP-QADGF36:~/Workspaces/ijohnson-python-kasa$ gh repo sync python-kasa/python-kasa -b master
HTTP 404: Not Found (https://api.github.com/repos/python-kasa/python-kasa/merge-upstream)
Here is how you can pull in changes should this ever come up for you (by adding another origin)
builder@DESKTOP-QADGF36:~/Workspaces/ijohnson-python-kasa$ git remote add origin2 https://github.com/python-kasa/python-kasa.git
builder@DESKTOP-QADGF36:~/Workspaces/ijohnson-python-kasa$ git pull origin2 master
remote: Enumerating objects: 7850, done.
remote: Counting objects: 100% (1257/1257), done.
remote: Compressing objects: 100% (293/293), done.
remote: Total 7850 (delta 1163), reused 968 (delta 964), pack-reused 6593 (from 2)
Receiving objects: 100% (7850/7850), 3.82 MiB | 17.63 MiB/s, done.
Resolving deltas: 100% (6181/6181), completed with 92 local objects.
From https://github.com/python-kasa/python-kasa
* branch master -> FETCH_HEAD
* [new branch] master -> origin2/master
Removing tox.ini
Removing poetry.lock
Removing kasa/tests/test_smartdevice.py
Removing kasa/tests/test_readme_examples.py
... snip ...
I see they are at Python 3.11 or higher from the uv.lock and my app right now is pinned at 3.8 in the Dockerfile.
Good use of CLIs i think.
First, I’ll set my Google Project (needed later) and add the latest gemini-cli with npm install -g @google/gemini-cli
$ export GOOGLE_CLOUD_PROJECT="myanthosproject2"
$ nvm use lts/iron
Now using node v20.19.3 (npm v10.8.2)
$ npm install -g @google/gemini-cli
changed 432 packages in 17s
123 packages are looking for funding
run `npm fund` for details
The first time you run gemini
you may have to auth, but that has already happened for me so we are right into the prompt
I’ll try and update
I’m not certain this is going to work.
builder@DESKTOP-QADGF36:~/Workspaces/ijohnson-python-kasa$ docker build -t kasarest:test01 .
[+] Building 10.0s (10/10) FINISHED docker:default
=> [internal] load build definition from Dockerfile 0.1s
=> => transferring dockerfile: 501B 0.0s
=> [internal] load metadata for docker.io/library/python:3.11-slim 0.6s
=> [auth] library/python:pull token for registry-1.docker.io 0.0s
=> [internal] load .dockerignore 0.0s
=> => transferring context: 69B 0.0s
=> CACHED [1/5] FROM docker.io/library/python:3.11-slim@sha256:139020233cc412efe4c8135b0efe1c7569dc8b28ddd88bddb109b764 0.0s
=> => resolve docker.io/library/python:3.11-slim@sha256:139020233cc412efe4c8135b0efe1c7569dc8b28ddd88bddb109b764f8977e3 0.0s
=> [internal] load build context 0.0s
=> => transferring context: 11.32kB 0.0s
=> [2/5] RUN pip install -U pip pipenv 8.1s
=> [3/5] COPY cashman-flask-project/ /app/ 0.0s
=> [4/5] WORKDIR /app 0.0s
=> ERROR [5/5] RUN pipenv install --system --deploy 1.0s
------
> [5/5] RUN pipenv install --system --deploy:
0.932 Your Pipfile.lock
0.932 (601e48664aae47c8c903f9f070ec23d766dc916b445c91361ab8090b3c62cf9f) is out of
0.932 date. Expected:
0.932 (8b67abd7c8e0a6d8a775e5d2d9fe8b7590a95e48d6678472af2e0cd7070587b4).
0.933 Usage: pipenv install [OPTIONS] [PACKAGES]...
0.933
0.934 ERROR:: Aborting deploy
------
Dockerfile:16
--------------------
14 |
15 | # Install dependencies
16 | >>> RUN pipenv install --system --deploy
17 |
18 | # Expose the port and start the application
--------------------
ERROR: failed to solve: process "/bin/sh -c pipenv install --system --deploy" did not complete successfully: exit code: 2
Let’s go about it a bit different.
We can see our python code is really just about triggering some bash scripts that invoke the KASA cli
Let me make a much bigger ask that is more exacting:
This repo has a python app that is built and run with uv. The kasa binary is invoked with a –host parameter and state like on, off, and state.
You can see we wrote another python app in /cashman-flask-project that uses shell scripts of /cashman-flask-project/swap.sh and /cashman-flask-project/some.sh to invoke the kasa binary. The /cashman-flask-project/bootstrap.sh is the ENTRYPOINT in the Dockerfile.
I would like to use FastAPI documented in https://fastapi.tiangolo.com/. The goal would be to compile the kasa app with uv but then expose a RESTFUL service similar to the flask apps /cashman-flask-project/restapi/index.py with an /on /off /swap and /health interface. Ignore the /testshell1 /testshell2 /testshell3 /testshell4 routes as they are not used.
I gave it some wide latitude for change
which used a lot more tokens
But docker did build without error
So far so good
I see GET is not allowed
which matches the code
@app.post("/on")
def turn_on(devip: str, type: str = "plug"):
return {"output": run_kasa_command(["--host", devip, "--type", type, "on"])}
My old Flask based version allowed GET and POST.
I tried with POST and it worked great
builder@DESKTOP-QADGF36:~$ curl -X POST http://localhost:8000/on?devip=192.168.1.3
{"output":"Turning on BlueBulb\n"}builder@DESKTOP-QADGF36:~$ curl -X POST http://localhost:8000/off?devip=192.168.1.3
{"output":"Turning off BlueBulb\n"}builder@DESKTOP-QADGF36:~$
I realize why apikey
is missing here. I never merged my branch
So this new code is amazing, but lacks “apikey”
from fastapi import FastAPI, HTTPException
import subprocess
app = FastAPI()
def run_kasa_command(command: list[str]):
try:
process = subprocess.run(
["/usr/local/bin/kasa"] + command,
capture_output=True,
text=True,
check=True,
)
return process.stdout
except subprocess.CalledProcessError as e:
raise HTTPException(status_code=500, detail=f"Kasa command failed: {e.stderr}")
@app.get("/health")
def health_check():
return {"status": "ok"}
@app.post("/on")
def turn_on(devip: str, type: str = "plug"):
return {"output": run_kasa_command(["--host", devip, "--type", type, "on"])}
@app.post("/off")
def turn_off(devip: str, type: str = "plug"):
return {"output": run_kasa_command(["--host", devip, "--type", type, "off"])}
@app.post("/swap")
def swap(devip: str, type: str = "plug"):
state_output = run_kasa_command(["--host", devip, "--type", type, "state"])
if "Device state: OFF" in state_output:
return {"output": run_kasa_command(["--host", devip, "--type", type, "on"])}
else:
return {"output": run_kasa_command(["--host", devip, "--type", type, "off"])}
I committed the code to a new branch, but I’ll wait on merging it as these 3 years of updates account for 757 commits ahead of master
let’s try asking Gemini to help
we saw it have some issues fetching from github (perhaps i should have used a raw URL) but it did work just fine.
What is so awesome about FastAPI is the created Swagger
as well as redoc
Comparing to Claude
Let’s install or update Claude Code
builder@DESKTOP-QADGF36:~/Workspaces/ijohnson-python-kasa$ claude .
claude: command not found
builder@DESKTOP-QADGF36:~/Workspaces/ijohnson-python-kasa$ npm install -g @anthropic-ai/claude-code
added 3 packages in 6s
2 packages are looking for funding
run `npm fund` for details
Let’s ask it to create an updated README to show how to build and run the docker container
I have a new updated Dockerfile that uses FastAPI which is documented at https://fastapi.tiangolo.com/. Our app now has OpenAPI (Swagger) documentation on the /docs endpoint. Please update the README.md with build and run instructions for docker. Include notes on passing the API_KEY environment variable for authentication.
It worked. But also cost me 14c to do it
I noticed I do not have a working Github build to push to Dockerhub. But based on some repo variables, I know I must have at one time.
Let’s use Gemini again for this!
we can see how quick it sorts that out
I’ll commit and push
builder@DESKTOP-QADGF36:~/Workspaces/ijohnson-python-kasa$ git add Dockerfile
builder@DESKTOP-QADGF36:~/Workspaces/ijohnson-python-kasa$ git add README.md
builder@DESKTOP-QADGF36:~/Workspaces/ijohnson-python-kasa$ git add kasa_fastapi/main.py
builder@DESKTOP-QADGF36:~/Workspaces/ijohnson-python-kasa$ git commit -m "add API KEY, update docs, add new CICD"
[new-fastapi 09de404] add API KEY, update docs, add new CICD
4 files changed, 110 insertions(+), 5 deletions(-)
create mode 100644 .github/workflows/cicd.yaml
builder@DESKTOP-QADGF36:~/Workspaces/ijohnson-python-kasa$ git push
Enumerating objects: 16, done.
Counting objects: 100% (16/16), done.
Delta compression using up to 16 threads
Compressing objects: 100% (8/8), done.
Writing objects: 100% (9/9), 2.08 KiB | 2.08 MiB/s, done.
Total 9 (delta 6), reused 0 (delta 0)
remote: Resolving deltas: 100% (6/6), completed with 6 local objects.
To https://github.com/idjohnson/python-kasa
3e94f39..09de404 new-fastapi -> new-fastapi
I knew it would get ugly so I skipped the checks and just merged the PR
When the new workflow failed to fire, i realized there was a branch mismatch and I needed to add “master” to the list
builder@DESKTOP-QADGF36:~/Workspaces/ijohnson-python-kasa$ vi .github/workflows/cicd.yaml
builder@DESKTOP-QADGF36:~/Workspaces/ijohnson-python-kasa$ git diff .github/workflows/cicd.yaml
diff --git a/.github/workflows/cicd.yaml b/.github/workflows/cicd.yaml
index bb4f188..101500d 100644
--- a/.github/workflows/cicd.yaml
+++ b/.github/workflows/cicd.yaml
@@ -2,7 +2,7 @@ name: CI/CD
on:
push:
- branches: [ "main" ]
+ branches: [ "main", "master" ]
jobs:
build-and-push:
builder@DESKTOP-QADGF36:~/Workspaces/ijohnson-python-kasa$ git add .github/workflows/cicd.yaml
builder@DESKTOP-QADGF36:~/Workspaces/ijohnson-python-kasa$ git commit -m "change on master"
[master 8a032e4] change on master
1 file changed, 1 insertion(+), 1 deletion(-)
builder@DESKTOP-QADGF36:~/Workspaces/ijohnson-python-kasa$ git push
Enumerating objects: 9, done.
Counting objects: 100% (9/9), done.
Delta compression using up to 16 threads
Compressing objects: 100% (5/5), done.
Writing objects: 100% (5/5), 418 bytes | 418.00 KiB/s, done.
Total 5 (delta 4), reused 0 (delta 0)
remote: Resolving deltas: 100% (4/4), completed with 4 local objects.
remote: Bypassed rule violations for refs/heads/master:
remote:
remote: - Changes must be made through a pull request.
remote:
To https://github.com/idjohnson/python-kasa
ebc79f1..8a032e4 master -> master
Now I have a build
And we can see a new image for kasarest on Dockerhub
There is a helm chart, though stuck in the unmerged PR
I’ll checkout that branch, then pull and update my existing values just to take in the new tag
builder@DESKTOP-QADGF36:~/Workspaces/ijohnson-python-kasa$ git checkout feature/updates-for-variables
Switched to branch 'feature/updates-for-variables'
Your branch is up to date with 'origin/feature/updates-for-variables'.
builder@DESKTOP-QADGF36:~/Workspaces/ijohnson-python-kasa$ helm get values pykasa -o yaml
apikey: xxxxxnot-the-real-keyxxxxxx
image:
repository: idjohnson/kasarest
tag: 1.1.5
imagePullSecrets: null
builder@DESKTOP-QADGF36:~/Workspaces/ijohnson-python-kasa$ helm get values pykasa -o yaml > pykasa.values.yaml
builder@DESKTOP-QADGF36:~/Workspaces/ijohnson-python-kasa$ helm get values pykasa -o yaml > pykasa.values.yaml.old
builder@DESKTOP-QADGF36:~/Workspaces/ijohnson-python-kasa$ vi pykasa.values.yaml
builder@DESKTOP-QADGF36:~/Workspaces/ijohnson-python-kasa$ diff pykasa.values.yaml pykasa.values.yaml.old
4c4
< tag: 2.0.0
---
> tag: 1.1.5
I can now upgrade using helm
$ kubectl get po -A | grep kasa
default pykasa-59f76bb6d5-djv9z 1/1 Running 2 (19d ago) 494d
$ helm upgrade pykasa -f ./pykasa.values.yaml ./pykasa
Release "pykasa" has been upgraded. Happy Helming!
NAME: pykasa
LAST DEPLOYED: Wed Jul 9 19:57:58 2025
NAMESPACE: default
STATUS: deployed
REVISION: 2
NOTES:
1. Get the application URL by running these commands:
https://kasarest.freshbrewed.science/
HOWEVER, it fails to work
builder@DESKTOP-QADGF36:~/Workspaces/ijohnson-python-kasa$ kubectl get po -A | grep kasa
default pykasa-57fdc74dc6-fjnlc 0/1 Running 1 (19s ago) 60s
default pykasa-59f76bb6d5-djv9z 1/1 Running 2 (19d ago) 494d
Why? because the old container was exposed on 5000 not 8000
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal Scheduled 118s default-scheduler Successfully assigned default/pykasa-57fdc74dc6-fjnlc to builder-hp-elitebook-745-g5
Normal Pulling 117s kubelet Pulling image "idjohnson/kasarest:2.0.0"
Normal Pulled 108s kubelet Successfully pulled image "idjohnson/kasarest:2.0.0" in 8.7s (8.7s including waiting)
Normal Created 77s (x2 over 108s) kubelet Created container: pykasa
Normal Pulled 77s kubelet Container image "idjohnson/kasarest:2.0.0" already present on machine
Normal Started 76s (x2 over 108s) kubelet Started container pykasa
Warning Unhealthy 57s (x10 over 106s) kubelet Readiness probe failed: Get "http://10.42.0.211:5000/": dial tcp 10.42.0.211:5000: connect: connection refused
Warning Unhealthy 47s (x6 over 97s) kubelet Liveness probe failed: Get "http://10.42.0.211:5000/": dial tcp 10.42.0.211:5000: connect: connection refused
Normal Killing 47s (x2 over 77s) kubelet Container pykasa failed liveness probe, will be restarted
builder@DESKTOP-QADGF36:~/Workspaces/ijohnson-python-kasa$
I’m going to tweak the chart a bit, including setting the default service port to 8000 now
builder@DESKTOP-QADGF36:~/Workspaces/ijohnson-python-kasa$ helm upgrade pykasa -f ./pykasa.values.yaml ./pykasa
Release "pykasa" has been upgraded. Happy Helming!
NAME: pykasa
LAST DEPLOYED: Wed Jul 9 20:02:45 2025
NAMESPACE: default
STATUS: deployed
REVISION: 3
NOTES:
1. Get the application URL by running these commands:
https://kasarest.freshbrewed.science/
builder@DESKTOP-QADGF36:~/Workspaces/ijohnson-python-kasa$ git diff pykasa
diff --git a/pykasa/Chart.yaml b/pykasa/Chart.yaml
index 18de48a..cadd5fc 100644
--- a/pykasa/Chart.yaml
+++ b/pykasa/Chart.yaml
@@ -15,10 +15,10 @@ type: application
# This is the chart version. This version number should be incremented each time you make changes
# to the chart and its templates, including the app version.
# Versions are expected to follow Semantic Versioning (https://semver.org/)
-version: 0.1.0
+version: 0.1.1
# This is the version number of the application being deployed. This version number should be
# incremented each time you make changes to the application. Versions are not expected to
# follow Semantic Versioning. They should reflect the version the application is using.
# It is recommended to use it with quotes.
-appVersion: "1.16.0"
+appVersion: "2.0.0"
diff --git a/pykasa/templates/deployment.yaml b/pykasa/templates/deployment.yaml
index d91d9b3..3a638eb 100644
--- a/pykasa/templates/deployment.yaml
+++ b/pykasa/templates/deployment.yaml
@@ -34,17 +34,17 @@ spec:
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
ports:
- - name: http
+ - name: httpkasa
containerPort: {{ .Values.service.port }}
protocol: TCP
livenessProbe:
httpGet:
path: /
- port: http
+ port: httpkasa
readinessProbe:
httpGet:
path: /
- port: http
+ port: httpkasa
env:
- name: PORT
value: "{{ .Values.service.port }}"
diff --git a/pykasa/templates/service.yaml b/pykasa/templates/service.yaml
index e009ccd..5573ba2 100644
--- a/pykasa/templates/service.yaml
+++ b/pykasa/templates/service.yaml
@@ -10,6 +10,6 @@ spec:
- port: {{ .Values.service.port }}
targetPort: {{ .Values.service.port }}
protocol: TCP
- name: http
+ name: httpkasa
selector:
{{- include "pykasa.selectorLabels" . | nindent 4 }}
diff --git a/pykasa/values.yaml b/pykasa/values.yaml
index 7a1f344..00a23e1 100644
--- a/pykasa/values.yaml
+++ b/pykasa/values.yaml
@@ -43,7 +43,7 @@ securityContext: {}
service:
type: ClusterIP
- port: 5000
+ port: 8000
ingress:
enabled: true
I next saw it was failing on 404 which is likely because I did HTTP probe checks not TCP
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal Scheduled 2m25s default-scheduler Successfully assigned default/pykasa-6bc847d77b-52qxh to builder-hp-elitebook-745-g5
Normal Pulled 114s (x2 over 2m23s) kubelet Container image "idjohnson/kasarest:2.0.0" already present on machine
Normal Created 114s (x2 over 2m23s) kubelet Created container: pykasa
Normal Started 113s (x2 over 2m22s) kubelet Started container pykasa
Warning Unhealthy 110s (x5 over 2m21s) kubelet Readiness probe failed: Get "http://10.42.0.212:8000/": dial tcp 10.42.0.212:8000: connect: connection refused
Warning Unhealthy 94s (x6 over 2m14s) kubelet Readiness probe failed: HTTP probe failed with statuscode: 404
Warning Unhealthy 84s (x6 over 2m14s) kubelet Liveness probe failed: HTTP probe failed with statuscode: 404
Normal Killing 84s (x2 over 114s) kubelet Container pykasa failed liveness probe, will be restarted
I updated the probes in the deployment
livenessProbe:
httpGet:
path: /health
port: httpkasa
readinessProbe:
httpGet:
path: /health
port: httpkasa
and tried again
$ kubectl get po -A | grep kasa
default pykasa-7dbb5bc7cb-p8vvd 1/1 Running 0 51s
And all we need to do to see it is live is hit the Swagger/OpenAPI endpoint of https://kasarest.freshbrewed.science/docs
Customization
Let’s talk a moment about some of the customization we can do. I really didn’t cover it in my YouTube video.
Here we can see I have added a GEMINI.md that really instructs Gemini to be a total arse to me. Be the jerk - I demand it.
builder@DESKTOP-QADGF36:~/Workspaces/pybsposter$ cat .gemini/GEMINI.md
# Project: My Awesome TypeScript Library
## General Instructions:
- Respond as an angry developer.
- You are mean.
- Add insults.
- When generating new code, create detailed comments
- All code should be compatible with Python 3.11+.
## Coding Style:
- Use 3 spaces for indentation.
- Private class members should be prefixed with an underscore (`_`).
- Always use strict equality (`===` and `!==`).
## Specific Component: `SYSTEM.md`
- This file handles contains MermaidJS documentation about components and data flows
- When adding new API call functions, ensure the MermaidJS diagrams and documentation is updated.
## Regarding Dependencies:
- Always update requirements.txt with new libraries
- Any new non-python libraries that are added should be updated in the Dockerfile
- If a new dependency is required, please state the reason.
which really just gives me chuckles
You can, of course, do much more with it like adding your own rules or files to add to the context. I just wanted to make it a surly jerk.
Summary
I can already tell you I won’t be charged and even if I was, it would still be under $2 according to Pro 2.5 pricing
Frankly, I find Gemini CLI amazing. It’s faster than Claude Code and so far has worked even better.
I’ll keep checking my Cloud billing, but so far, this is my new favourite in AI Coding assistants.
(Update: Checking the next day there were no charges)