a context-aware si-unit registry manager built on pint and pydantic
Project description
kanne
Quick: is 0.0000001 one nanosecond? (...how many nanoseconds are in a second
again?) A bare number is a riddle. kanne makes you write "100 ns" and keeps it
that way — across Pydantic, JSON, GraphQL, the whole trip.
from kanne import Duration, Length
from kanne.kanne import Kanne
from pydantic import BaseModel
class Acquisition(BaseModel):
exposure: Duration # any time, your choice of unit
pixel_size: Length # any length
with Kanne():
a = Acquisition(exposure="100 ms", pixel_size="2.5 um")
a.model_dump() # {'exposure': '100 ms', 'pixel_size': '2.5 µm'}
a.exposure.to("s") # <Quantity(0.1, 'second')> — convert when *you* ask
a.exposure + a.pixel_size # 💥 can't add a time to a length
Acquisition(exposure=100, pixel_size="1 m") # 💥 100 what?
Acquisition(exposure="3 m", pixel_size="1 m") # 💥 a metre isn't a duration
Fields are typed by dimension (Duration, Length, ElectricPotential), not by a
fixed unit. They take any unit of the right dimension, keep the one you gave, and reject
bare numbers and nonsense at the door.
The point
One value, one self-describing token. No second _unit field to drift out of sync, no
"everyone just knows it's milliseconds" convention living in a docstring:
{ "exposure": 1500, "exposure_unit": "ms" } // 😬 two facts, can desync
{ "exposure": "1500 ms" } // 🎉 one fact, can't
And provenance: if the user typed 100 ms, it stays 100 ms — not 0.1 s, not
2.4999999999999996 from a round-trip through some base unit. The number you sent is
the number that arrives, bit for bit. Convert only when you mean to.
kanne is a wire protocol, not a database protocol. It moves quantities across boundaries intact. Storing them is the backend's job:
kanne_serversplits each value into{magnitude, unit}columns so SQL canSUM()/AVG()/range-query the number while keeping the original unit, and the GraphQL API serves it back either way —durationas the Pint string,durationAs("ms")as a plain number for clients that don't speak Pint. So: send the string, store the pair.
"Why not just store milliseconds as a float?"
Honestly? Often you should. A float is tiny, numeric, dependency-free, and a fixed unit
is a perfectly good contract — NEURON runs the world's biophysical models on exactly
that (dt, v, tstop = ms, mV) at huge scale. Inside a component that agrees on
the unit, the float wins, and that's literally how kanne_server stores things.
The float only cracks at the boundary, where the "it's in ms" convention has to travel out-of-band and isn't in the data:
- Wrong-unit bugs are silent. A reader assumes seconds, lands 1000× off, nothing complains. kanne raises instead.
- Your canonical unit ages badly. ms → µs spike timing, µm → nm optics, nM → pM
assays — every resolution bump means awkward sub-unit floats or a lockstep migration.
"25 ns"just... still works.
Float between your own functions; self-describing quantity between systems. kanne is only about the second half.
Install
uv add kanne
Built-in dimensions
Each is a real importable class (so codegen can name it), accepting any unit of its dimension:
| Type | e.g. | Type | e.g. |
|---|---|---|---|
Duration |
"5 ms" |
ElectricCurrent |
"5 pA" |
Frequency |
"1 kHz" |
ElectricPotential |
"-70 mV" |
Length |
"2.5 µm" |
ElectricCharge |
"2 nC" |
Area |
"4 µm**2" |
Capacitance |
"100 nF" |
Volume |
"5 µL" |
ElectricalConductance |
"2 µS" |
Velocity |
"3 µm/s" |
ElectricalResistance |
"100 MΩ" |
Mass |
"5 mg" |
Power |
"5 mW" |
Temperature |
"37 degC" |
Energy |
"5 mJ" |
AmountOfSubstance |
"5 mmol" |
Pressure |
"2 bar" |
Concentration |
"1 mM" |
Need another? One line:
from kanne import PintQuantity
class Acceleration(PintQuantity):
reference_unit = "meter / second ** 2" # declares the *dimension*, never a conversion target
Good to know
- The wire form is always a string — JSON schema is
string, JSON input must be a string, bare numbers are rejected. In Python apint.Quantityor another dimension instance is also accepted. - Still dimensionful at runtime —
Duration("5 ms") + Duration("1 s")→1005 ms;Duration("1000 ms") == Duration("1 s")→True. Wrong dimensions raise. - Context-aware registry —
Kanne()sets the active Pint registry on acontextvarfor thewithblock (defaults to a shared global;degC& friends work out of the box). Mixing registries raises rather than silently misbehaving.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file kanne-2.5.0.tar.gz.
File metadata
- Download URL: kanne-2.5.0.tar.gz
- Upload date:
- Size: 18.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: uv/0.11.26 {"installer":{"name":"uv","version":"0.11.26","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
303e40e8ae8bea726d1f96c988c3da266f2a682e149f740df62ef6a6879870ac
|
|
| MD5 |
933782d562b2d0d97885d3435227eb2e
|
|
| BLAKE2b-256 |
2360fc7a38bf1ac1831cb5075f7b8dbe7c9a86ded15e103f4274bc63e8815807
|
File details
Details for the file kanne-2.5.0-py3-none-any.whl.
File metadata
- Download URL: kanne-2.5.0-py3-none-any.whl
- Upload date:
- Size: 19.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: uv/0.11.26 {"installer":{"name":"uv","version":"0.11.26","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
94b96291c52929c425bfe91610e861be89199463467b6eef6bad36b9b5673bf0
|
|
| MD5 |
6e47f81bfb0de98cebe7a169a1ea5a5e
|
|
| BLAKE2b-256 |
df7103f17b644c4d69088937460702d087f23d6bc9c94258c0dcfc8cfd32ab12
|