Fields
Pydantic Docs · docs.pydantic.dev [1]
exclude=True and repr=False is a good pydantic combination for secret attributes such as user passwords, or hashed passwords. exclude keeps it out of model_dumps, and repr keeps it out of the logs.
from pydantic import BaseModel, Field
class User(BaseModel):
name: str = Field(repr=True)
age: int = Field(repr=False)
user = User(name='John', age=42)
print(user)
#> name='John'
Note
This post is a thought [2]. It’s a short note that I make
about someone else’s content online #thoughts
References:
[1]: https://docs.pydantic.dev/2.7/concepts/fields/#field-representation
[2]: /thoughts/
Posts tagged: python
All posts with the tag "python"
312 posts
latest post 2026-05-06
Publishing rhythm
Hatch v1.10.0 - Hatch
hatch.pypa.io [1]
Hatch be flyin.
This new release of hatch includes support for the new package installer uv which is just mind blowing fast compared to anything else we have in python right now.
[tool.hatch.envs.default]
installer = "uv"
The other features are cool too, check them out. I’ll probably be using the test runner, but I’ve been waiting for the uv support since uv launched.
Note
This post is a thought [2]. It’s a short note that I make
about someone else’s content online #thoughts
References:
[1]: https://hatch.pypa.io/latest/blog/2024/05/02/hatch-v1100/
[2]: /thoughts/
[1]
This is my go to rich response container for clis written in python. It creates a nice box around the content on the screen and provides some nice separation in the output. It can be overdone, but comes in clutch when looking for that print statement in a long output.
Note
This post is a thought [2]. It’s a short note that I make
about someone else’s content online #thoughts
References:
[1]: /static/https://rich.readthedocs.io/en/stable/reference/panel.html
[2]: /thoughts/
Handling Errors - FastAPI
FastAPI framework, high performance, easy to learn, fast to code, ready for production
fastapi.tiangolo.com [1]
This page shows how to customize your fastapi [2] errors. I found this very useful to setup common templates so that I can return the same 404’s both programatically and by default, so it all looks the same to the end user.
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
class UnicornException(Exception):
def __init__(self, name: str):
self.name = name
app = FastAPI()
@app.exception_handler(UnicornException)
async def unicorn_exception_handler(request: Request, exc: UnicornException):
return JSONResponse(
status_code=418,
content={"message": f"Oops! {exc.name} did something. There goes a rainbow..."},
)
@app.get("/unicorns/{name}")
async def read_unicorn(name: str):
if name == "yolo":
raise UnicornException(name=name)
return {"unicorn_name": name}
---
This post sat in draft for months. I stumbled upon it again and found great success returning good error messages based on user preferences. the default remains json, but if a user requests text/html it will be an html [3] response, and text for ...
To allow access only to the , you can pass add the Resource field to
the User Policy when you create a new token.
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"admin:*"
]
},
{
"Effect": "Allow",
"Action": [
"kms:*"
]
},
{
"Effect": "Allow",
"Action": [
"s3:*"
],
"Resource": [
"arn:aws:s3:::<bucket>",
"arn:aws:s3:::<bucket>/*"
]
}
]
}
You can inspect sqlite tables with the sqlite shell.
note that you get into the shell with sqlite3 database.db
.tables
I also learned that .tables, .index and .schema are helper functions that
query the sqlite_master table on the main database.
Here is an output from my redka database. The sqlite_master table contains all
the sqlite objects type, name, tbl_name, rootpage, and sql to create them.
❯ sqlite3 database.db "SELECT * from sqlite_master;"
table|rkey|rkey|2|CREATE TABLE rkey (
id integer primary key,
key text not null,
type integer not null,
version integer not null,
etime integer,
mtime integer not null
)
index|rkey_key_idx|rkey|3|CREATE UNIQUE INDEX rkey_key_idx on rkey (key)
index|rkey_etime_idx|rkey|4|CREATE INDEX rkey_etime_idx on rkey (etime)
where etime is not null
trigger|rkey_on_type_update|rkey|0|CREATE TRIGGER rkey_on_type_update
before update of type on rkey
for each row
when old.type is not new.type
begin
select raise(abort, 'key type mismatch');
end
table|rstring|rstring|5|CREATE TABLE rstring (
key_id integer not null,
value blob not null,
foreign key (key_id) references rkey (id)
on delete cascade
)
index|rstring_pk_idx|rstring|6|CREATE UN...
I recently had to update my copier-gallery command to trust my own templates
because some of them have shell scripts that run afterwards. Be warned that
this could be a dangerous feature to run on random templates you get off the
internet, but these are all mine, so if I wreck it its my own fault.
copier copy --trust <template> <destination>
All the the copier copy api can be found with help.
❯ copier copy --help
copier copy 8.3.0
Copy from a template source to a destination.
Usage:
copier copy [SWITCHES] template_src destination_path
Hidden-switches:
-h, --help Prints this help message and quits
--help-all Prints help messages of all sub-commands and quits
-v, --version Prints the program's version and quits
Switches:
-C, --no-cleanup On error, do not delete destination if it was
created by Copier.
--UNSAFE, --trust Allow templates with unsafe features (Jinja
extensions, migrations, tasks)
-a, --answers-file VALUE:str Update using this path (relative to
`destination_path`) to find the answers file
-d, --data VARIABLE=VALUE:str Make VARIABLE available as VALUE when rendering the
template; may be given multiple times
-f, --force Same as `--defaults --overwrite`...
Today I accidentally ran f2 in ipython to discover that it opens your $EDITOR!
I use this feature quite often in zsh, it is bound to <c-e> for me, and since
I have my environment variable EDITOR set to nvim it opens nvim when I hit
<c-e>. Today I discovered that Ipython has this bound to F2. If you know
how to set it to <c-e> let me know I’ve tried, a lot.
export EDITOR=nvim
ipython
<F2>
better yet add export EDITOR=nvim to your .zshrc
# ~/.zshrc
export EDITOR=nvim
I’ve really been enjoying using sqlmodel for my projects that need a database.
One thing that I definitely lacked on for too long was indexing my database. I
hit a point with one database where it was taking 7s for pretty simple
paginated queries to return 10 records.
For every field that you will be querying on, you can create an index, by
setting it equal to Field(index=True)
class Hero(SQLModel, table=True):
id: int | None = Field(default=None, primary_key=True)
name: str = Field(index=True)
secret_name: str
age: int | None = Field(default=None, index=True)
example courtesy of the docs
Note
primary keys are indexed by default.
The docs cover this pretty well, and in quite depth - Optimizing Queries [1]
References:
[1]: https://sqlmodel.tiangolo.com/tutorial/indexes/
Redirecting
15r10nk.github.io [1]
This is a cool snapshot testing tool that automatically creates, and updates test values for you.
Starting with some test code.
from inline_snapshot import snapshot
def something():
return 1548 * 18489
def test_something():
assert something() == snapshot()
now if I run pytest my tests will fail because my assert will fail, but if I run pytest --inline-snapshot=create it will fill out my snapshot values and the file will then look like this.
from inline_snapshot import snapshot
def something():
return 1548 * 18489
def test_something():
assert something() == snapshot(28620972)
Note
This post is a thought [2]. It’s a short note that I make
about someone else’s content online #thoughts
References:
[1]: https://15r10nk.github.io/inline-snapshot/
[2]: /thoughts/
inline-snapshot is a new tool that I am trying out for python testing. It
takes snapshots of your outputs and places them inline with the test.
Here is the most basic starter.
import inline_snapshot
def test_one():
assert 1 == snapshot()
Now when I run pytest my tests will fail because my assert has no value, but if I
run pytest --inline-snapshot=create it will fill out my snapshot values and the
file will then look like this.
import inline_snapshot
def test_one():
assert 1 == snapshot(1)
It also works with pydantic models.
class MyModel(BaseModel):
name: str
age: int
nickname: str | None = None
def test_my_model_instance():
assert MyModel(name="Waylon", age=1) == snapshot(MyModel(name="Waylon", age=1))
def test_my_model_fields():
me = MyModel(name="Waylon", age=1, nickname='Waylon')
assert me.name == snapshot("Waylon")
assert me.age == snapshot(1)
assert me.nickname == snapshot("Waylon")
Today I learned how to VACUUM a sqlite database and cut its size in about half.
It’s a database that I have had running for quite awhile and has some decent
traffic on it.
Why is it important to do a VACUUM? In short its becuase the file system gets
fragmented with as data is updated. On delete the files are removed from the
database and marked as available for reuse in the filesystem, but the space is
not reclaimed.
To VACUUM a database, run the following sql command. You can do it right form
the sqlite shell by running sqlite3.
You will need about double the current size of the database as free space to
do the VACUUM, you need space for a full copy, journaling or write ahead
logs, and the existing database.
VACUUM;
The docs are fantastic for vacuum [1].
References:
[1]: https://www.sqlite.org/lang_vacuum.html
Typer makes it easy to compose your cli applications, like you might with a web
router if you are more familiar with that. This allows you to build smaller
applications that compose into a larger application.
You will see similar patterns in the wild, namely the aws cli which always
has the aws <command> <subcommand> pattern.
Lets setup the cli app itself first. You can put it in project/cli/cli.py.
import typer
from project.cli.api import api_app
from project.cli.config import config_app
from project.cli.user import user_app
from project.cli.run import run_app
app = typer.Typer()
app.add_typer(api_app, name="api")
app.add_typer(config_app, name="config")
app.add_typer(user_app, name="user")
app.add_typer(run_app, name="run")
Creating an app that will become a command is the same as creating a regular
app in Typer. We need to create a callback that will become our command, and a
command that will become our subcommand in the parent app.
import typer
from rich.console import Console
from project.config import get_config
config_app = typer.Typer()
@config_app.callback()
def config():
"model cli"
@config_app.command()
def show(
):
project_config = get_config(env)
Cons...
One Day Build - Play Outside
Inspired by Adam Savage and his One Day builds on youtube. I often build
things, and want to make them generally useful for others and over configure
out of the gate. This project is purely for me inspired by a need I have.
- play-outside [1]
!How-To # [2]
This post will not directly show how to make a weather app, but document the
process that I went through to make mine. It will show the tools that I used
to make it, and the final result.
The Situation # [3]
It often goes in our house ask dad while he is busy and he will probably just
say yes without thinking much. This happens a lot when kids ask to go
outside. I think sure, go for it, you will figure it out. Then my wife walks
in and asks where they are, followed by, did you even check the weather, its
-11 degrees outside right now.
I need a tool for this decision making process
Lungs # [4]
You we have a family of not the most heathly lungs, we have my wife with lung
cancer, one lung missing, and kids with asthma. We nee...
I am working on a page for
htmx-patterns [1] and I ran into a
situation with lots of duplication. Especially when i am using tailwind I run
into situations where the duplication can get tedious to maintiain. The
solution I found is macros.
Now I can use the same code for all of my links, and call the macro to use it.
{% macro link(id, text, boosted=false) -%}
<a
class="
{% if id is none %}
pointer-events-none bg-terminal-950 text-terminal-900 ring-terminal-900
{% else %}
bg-terminal-950 hover:bg-terminal-900 hover:text-terminal-400 text-terminal-500 shadow-lg shadow-terminal-300/20 hover:shadow-terminal-300/30 ring-terminal-300
{% endif %}
cursor-pointer block text-center font-bold py-2 px-4 rounded w-full ring-1
"
{% if id is not none %}
href="{{ url_for('boosted', id=id) }}"
{% endif %}
{% if boosted %}
hx-boost="true"
{% endif %}>
{{ text }}
</a>
{%- endmacro %}
<h2 class='text-3xl font-light mt-0 max-w-xl text-center prose-xl mt-8 text-terminal-500'>
Boosted Links
</h2>
<div class='flex flex-row gap-4'>
{{ link(prev_id, 'Previous', boosted=True) }}
{{ link(next_id, 'Next', boosted=True) }}
</div>
<h2 class='text-3xl font-light mt-0 max-w-xl text-center...
jinja has a loop variable that is very handy to use with htmx [1]. Whether you
want to implement a click to load more or an infinite scroll this loop variable
is very handy.
{% for person in persons %}
<li
{% if loop.last %}
hx-get="{{ url_for('infinite', page=next_page) }}"
hx-trigger="intersect once"
hx-target="#persons"
hx-swap='beforeend'
hx-indicator="#persons-loading"
{% endif %}
{{ person.name.upper() }} -
{{ person.phone_number }}
</li>
{% endfor %}
Now for every chunk of contacts that we load we will trigger the infinite
scroll by loading more once the last one has intersected the screen.
References:
[1]: /htmx/
Out of the box FastAPI [1].">Starlette does not support url_for with query params. When
trying to use url_for with query params it throws the following error.
starlette.routing.NoMatchFound: No route exists for name "infinite" and params "page"
In my searching for this I found starlette issue #560 [2] quite helpful, but not complete, as it did not work for me.
import jinja2
if hasattr(jinja2, "pass_context"):
pass_context = jinja2.pass_context
else:
pass_context = jinja2.contextfunction
@pass_context
def url_for_query(context: dict, name: str, **params: dict) -> str:
request = context["request"]
url = str(request.url_for(name))
if params == {}:
return url
from urllib.parse import parse_qs, urlencode, urlparse, urlunparse
# Parse the URL
parsed_url = urlparse(url)
# Parse the query parameters
query_params = parse_qs(parsed_url.query)
# Update the query parameters with the new ones
query_params.update(params)
# Rebuild the query string
updated_query_string = urlencode(query_params, doseq=True)
# Rebuild the URL with the updated query string
updated_url = urlunparse(
(
parsed_url.scheme,
parsed_url.netloc,
parsed_url.path,
parsed_url.params,
updated_...
Kind (Kubernetes in Docker) is a tool that makes it easy to create and tear
down local clusters quickly. I like to use it to test out new workflows.
Argocd is a continuous delivery tool that makes it easy to setup gitops
workflows in kubernetes.
Here is how you can setup a new kind cluster and install argocd into it using
helm, the kubernetes package manager.
kind create cluster --name argocd
# your first time through you need to add the argocd repo
helm repo add argo https://argoproj.github.io/argo-helm
helm repo update
# install argocd into the cluster
helm install argo argo/argo-cd --namespace argocd --create-namespace
# deploy the app of apps
kubectl apply -f apps/apps.yaml
If you want to add repos and apps to your cluster you can use the argo cli to
do that, but first you will need forward the argocd port and login.
# Wait until Argo CD API server is available
echo "Waiting for Argo CD API server to be available..."
while ! kubectl wait --for=condition=available --timeout=60s deployment/argo-argocd-server -n argocd; do
echo "Waiting for Argo CD API server to be ready..."
sleep 10
done
kubectl port-forward svc/argo-argocd-server -n argocd 8080:443 &
argocd_admin_pa...
![[None]]
I’ve been using these decorators to modify the behavior of specific routes. It will do things like 404 admin only routes in a way that looks just like fastapi [1]’s default, or only allow certain roles into the route, or redirect unauthenticated users to login.
After listening to yesterday’s syntaxfm I’m now really thinking about middleware and the benefits it might have. middleware would make it easy to apply things like admin to an entire admin router, so you wont forget it on any one admin route. It will look cleaner as the admin checker is only applied once per router, not once per route.
import inspect
import time
from functools import wraps
from inspect import signature
from fastapi import Request
from fastapi.responses import FileResponse, JSONResponse, RedirectResponse
from starlette import status
from fokais.config import get_config
from fokais.models.user import Role
config = get_config()
admin_routes = []
authenticated_routes = []
not_cached_routes = []
cached_routes = []
def not_found(request):
hx_request_header = request.headers.get("hx-request")
user_agent = request.headers.get("user-agent", "").lower()
if "mozilla" in user_agent or "webkit" i...
![[None]]
jinja’s url_for in fastapi [1] does not account for https by default, there is
probably a better way, but this is a way that allows me to configure when I use
http vs https.
@pass_context
def https_url_for(context: dict, name: str, **path_params: Any) -> str:
"""
always convert http to https
"""
request = context["request"]
http_url = request.url_for(name, **path_params)
return str(http_url).replace("http", "https", 1)
def get_templates(config: BaseSettings) -> Jinja2Templates:
templates = Jinja2Templates(directory="templates")
templates.env.globals["https_url_for"] = https_url_for
## only use the default url_for for local development, for dev, qa, and prod use https
if os.environ.get("ENV") in ["dev", "qa", "prod"]:
templates.env.globals["url_for"] = https_url_for
console.print("Using HTTPS")
else:
console.print("Using HTTP")
return templates
Note
This post is a thought [2]. It’s a short note that I make
about someone else’s content online #thoughts
References:
[1]: /fastapi/
[2]: /thoughts/