Skip to main content

a context-aware si-unit registry manager built on pint and pydantic

Project description

kanne

codecov PyPI version Maintainer PyPI pyversions PyPI status PyPI download day

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_server splits each value into {magnitude, unit} columns so SQL can SUM()/AVG()/range-query the number while keeping the original unit, and the GraphQL API serves it back either way — duration as 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 a pint.Quantity or another dimension instance is also accepted.
  • Still dimensionful at runtimeDuration("5 ms") + Duration("1 s")1005 ms; Duration("1000 ms") == Duration("1 s")True. Wrong dimensions raise.
  • Context-aware registryKanne() sets the active Pint registry on a contextvar for the with block (defaults to a shared global; degC & friends work out of the box). Mixing registries raises rather than silently misbehaving.

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

kanne-2.2.1.tar.gz (13.6 kB view details)

Uploaded Source

Built Distribution

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

kanne-2.2.1-py3-none-any.whl (15.1 kB view details)

Uploaded Python 3

File details

Details for the file kanne-2.2.1.tar.gz.

File metadata

  • Download URL: kanne-2.2.1.tar.gz
  • Upload date:
  • Size: 13.6 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.11.23 {"installer":{"name":"uv","version":"0.11.23","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

Hashes for kanne-2.2.1.tar.gz
Algorithm Hash digest
SHA256 2ddbed6eab12638754662e55a8da6c213db9deb47c955753672472fbd2cb323a
MD5 e69137a668ecb832d0522fbbf6df9102
BLAKE2b-256 b70333f91ad7df8670130e7d5693653404d39c5492ec329406cefa0c9360f3d8

See more details on using hashes here.

File details

Details for the file kanne-2.2.1-py3-none-any.whl.

File metadata

  • Download URL: kanne-2.2.1-py3-none-any.whl
  • Upload date:
  • Size: 15.1 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.11.23 {"installer":{"name":"uv","version":"0.11.23","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

Hashes for kanne-2.2.1-py3-none-any.whl
Algorithm Hash digest
SHA256 783e8c1ab373abf85fa3d0c232b4b4216b52435a057b50b039b996d247fa232d
MD5 015a5a0c4b46a3503838e320d6144c09
BLAKE2b-256 79307ddda89e5882d1b121472ccbcadcc27536b0943f8f192bea6f0b729d5b2d

See more details on using hashes here.

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