Skip to main content

CodeAnim

中文说明 · English

Describe animation in Python code. One command renders it to a video file. No window, no interaction, no player — it just writes the file and exits.

codeanim examples/demo.py
# -> out.mp4

Zero-disk by default. Frames are piped straight into ffmpeg's stdin; no temporary files are written along the way. Disk usage equals the final video size, so 4K / high-quality / long clips are safe to run.


Contents


Install

Needs Python 3.8+, Pillow, and ffmpeg.

pip install codeanim-py      # "-py" suffix: `codeanim` was taken on PyPI
codeanim doctor              # checks the dependencies, tells you what to install

The import name is still codeanim — you never type the distribution name in your code:

from codeanim import Scene      # not codeanim_py

Language

Output is English by default. Add --ch for Chinese:

codeanim demo.py            # English help / logs / errors
codeanim demo.py --ch       # 全程中文
codeanim demo.py --zh       # same thing
CODEANIM_LANG=zh codeanim demo.py    # or via env var

This affects the CLI help, progress, warnings, and error messages. Docstrings and comments in the source stay Chinese.

Not installing is also fine (handy when working on the engine itself):

python -m pip install -r requirements.txt
python main.py doctor

Command line

codeanim <script.py> [options]
codeanim doctor
Option What it does Default
-o, --output Output path; extension picks the format out.mp4
--fps Frame rate 30
--quality low / medium / high / lossless high
--workers, -w Parallel processes CPU cores − 1
--snapshot <sec> Render a single PNG frame —
--frames <dir> Export a PNG sequence —
--gif Export GIF —
--keep-frames Keep intermediate frames (debug) off
--supersample <n> Supersampling factor 2
--mode <m> Frame storage: stream / ram / disk auto
--alpha Keep alpha channel (.mov / .webm) auto
--ffmpeg <path> Path to ffmpeg ffmpeg
--watch, -W Hot reload on file change off
--watch-at <sec> Preview time point for --watch 0
--inline Draw the frame in the terminal auto
--no-inline Don't draw in-terminal —
--preview <sec> Render one frame and open it —
--no-audio Ignore audio tracks off
--list-audio List the audio tracks and exit —
--layers Print the draw order and exit —
--ch / --zh / --en Output language en
--quiet, -q Only print errors off
--free-margin <size> Abort below this much free disk 256MB
--force Render even if disk-space check fails off
--no-disk-check Skip all disk checks off
codeanim demo.py --watch                       # edit with live preview
codeanim demo.py --watch --watch-at 2.5        # watch one specific frame
codeanim demo.py --preview 2.0                 # one frame, then open it
codeanim demo.py --snapshot 2.0                # one frame, don't open
codeanim demo.py --gif                         # GIF
codeanim demo.py --frames pngs/                # PNG sequence
codeanim demo.py -o out.mov --alpha            # transparent MOV
codeanim demo.py --layers                      # who covers whom
codeanim demo.py --list-audio                  # the audio timeline

Hot reload

The slow loop when writing animation is edit → run → wait → open the file. --watch removes everything in the middle:

codeanim examples/demo.py --watch
CodeAnim hot reload
────────────────────────────────────────────────────────
  entry    examples/demo.py
  watching 7 .py files
  preview  .codeanim/watch_preview.png
  t = 2.00s
────────────────────────────────────────────────────────
  Save a file and the preview refreshes. Ctrl-C to quit.

✓ updated  1280x720  8.60s  30 fps  11 primitives
   preview  .codeanim/watch_preview.png  (t=2.00s)
   took     814 ms

↻ changed: examples/demo.py
✓ updated  1280x720  8.60s  30 fps  11 primitives
   took     225 ms
  • Watches the entry script and every project .py it imports — editing a fragment file triggers it too
  • Renders one frame, not the whole video — typically 200–800 ms
  • Errors don't exit: syntax / runtime errors are printed, then it keeps waiting
  • Editor "save" bursts are debounced into a single re-render
  • Newly added .py files are picked up automatically
  • No third-party deps, plain mtime polling, cross-platform

In-terminal preview

Don't want to open an image? Draw it right in the terminal:

codeanim examples/demo.py --watch --inline
✓ updated  1280x720  8.60s  30 fps  11 primitives   took 1.46 s

  ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄
  ▄▄▄▄  ▄▄▄▄▄▄▄▄▄▄▄  ▄▄▄▄▄▄▄▄▄▄▄▄▄▄
  ▄▄▄  CodeAnim  animation in Python  ▄▄▄
  ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄
       ●                                ●
  t=4.00s   11 primitives   .codeanim/watch_preview.png

A terminal cell is roughly 1:2 (width:height), so the upper/lower half block ▄ lets one cell carry two vertical pixels and keeps the aspect ratio honest. An 80×24 terminal is about 80×48 colour cells — enough for layout, palette, and placement.

  • True colour (24-bit) first, then 256 colour, then greyscale characters
  • Repaints in place (ANSI cursor movement), no scrolling
  • Auto-detects terminal support; --no-inline disables, --inline forces
  • Also callable from code:
from codeanim import render_terminal_preview, preview_lines

print(render_terminal_preview(scene, at=2.0))        # a coloured string
lines = preview_lines(scene, at=2.0, max_cols=80)    # list of lines

Drawn primitives

Primitive Position anchor Main options
Circle centre r, fill, stroke, stroke_width
Rect top-left w, h, radius, fill, stroke
Line start point stroke, stroke_width
Text by anchor text, font_size, anchor, fill, fit_width
Image top-left source, w, h, crop, slice, tint, radius
Path top-left points, spline, closed, progress, arrow, dash
Digits by anchor value, decimals, thousands, formatter
Group centre rotation, scale, opacity

Origin is top-left, y grows downward.

Every primitive also accepts:

Option Meaning
cx / cy position by geometric centre (no manual offset math)
z layer number, higher draws on top
rotation / scale / opacity transforms, all animatable
shadow drop shadow (see below)
perspective 3D-ish tilt (see below)

Anything numeric or colour can be keyframed:

ball.cx.keys([(0, 200), (2, 1000)])                 # centre over time
ball.fill.keys([(0, "red"), (2, "#00aaff")])        # colour interpolation
ball.x.keys([(0, 0), (1, 500)], ease="outBack")     # ease per segment

25 built-in easing curves (linear, inOutCubic, outBack, inOutElastic, outBounce, …). ease= goes on the starting keyframe and describes the segment toward the next one.

Layer order

codeanim script.py --layers              # who covers whom
codeanim script.py --layers --layers-at 2.5
Draw order (bottom to top, 30 items)
────────────────────────────────────────────────────────────────────────
    #  z    type    name      position / size
────────────────────────────────────────────────────────────────────────
    1  -100 rect    <rect>    (0,0) 1280x720
    2  0    text    <text>    c(640,340) 'one command, one video'
    3  10   text    <text>    c(640,330) 'CodeAnim'
    4  20   digits  <digits>  c(1220,650) '0,000 frames'
────────────────────────────────────────────────────────────────────────
! Some primitives share the same z: 0(25), 10(3).
    Same z draws in insertion order -- later ones on top.
! 1 static primitive(s) get pre-composed into the bottom layer.

It warns about: all-equal z, duplicated z, static primitives (they get baked into a bottom layer), and decreasing z (usually group mixing).


Gradients / shadows / 3D tilt

from codeanim import Gradient, Shadow, Perspective

# gradient fill — just pass it as `fill=`
box = Rect(cx=640, cy=360, w=400, h=200,
           fill=Gradient.linear("#38bdf8", "#a855f7", angle=45))

Circle(cx=200, cy=200, r=80, fill=Gradient.radial("#fff", "#f00"))
Circle(cx=500, cy=200, r=80, fill=Gradient.conic(["#f00", "#0f0", "#00f"]))

# uneven stops
Gradient.linear([(0.0, "#000"), (0.3, "#38bdf8"), (1.0, "#fff")])

# drop shadow
card = Rect(cx=640, cy=360, w=300, h=180, fill="#1e293b",
            shadow=Shadow(blur=20, dy=8, alpha=0.5))

# 3D-ish tilt (quad warp, not real 3D — still pure & parallel)
panel = Rect(cx=640, cy=360, w=400, h=300, fill="#0ea5e9",
             perspective=Perspective(tilt_y=25, tilt_x=-10, distance=900))
Gradient
linear(*stops, angle=90) 0° = left→right, 90° = top→bottom
radial(*stops, center=(.5,.5), radius=.5) centre → edge
conic(*stops, angle=0) sweep around, good for rainbows / progress rings
Shadow
dx / dy offset in pixels
blur gaussian radius, 0 = hard edge
alpha opacity 0–1
spread grow the shape before blurring
color default black
Perspective
tilt_x pitch, in degrees
tilt_y yaw, in degrees
rotate in-plane rotation
distance camera distance; smaller = stronger

The output canvas is sized to the projected quad only. That's what makes the narrowing actually visible — if it kept the original size the content would just stretch back.


Particles

Particles are not a renderer special case. emit() bakes each particle's motion into keyframes, so scenes stay pure, seekable, and parallel-safe.

from codeanim import emit, emit_snow, emit_sparkle, emit_ripple

# one burst
emit(scene, count=120, at=(640, 360), t0=1.0,
     velocity=(200, 0), spread=260, gravity=500,
     life=1.8, r=(2, 6), fill=["#38bdf8", "#a855f7", "#f472b6"])

# a fountain
emit(scene, count=60, at=(640, 700), t0=0.0, t1=3.0,
     velocity=(0, -420), spread=90, gravity=380, life=2.0, r=4)

# one-liners
emit_snow(scene, count=80)        # snowfall with sway
emit_sparkle(scene, count=40)     # twinkle in place
emit_ripple(scene, count=3)       # expanding rings
emit option
count how many
at (x, y), or ((x1,y1),(x2,y2)) for a spawn rectangle
t0 / t1 start time / end time (t1 = continuous emission)
velocity + spread initial velocity and its random range
angle=(centre, cone) cone emission in degrees
gravity / drag acceleration / air resistance
life, r single value or (min, max)
fill a colour or a list to pick from
seed reproducible — same seed, same result every render
max_particles safety cap (default 600), exceeded = loud error

Text

from codeanim import Text

Text("Hello", cx=640, cy=360, font_size=48, fill="white", anchor="center")

# auto-shrink to fit a width (or height)
Text("A very long headline that must not overflow",
     cx=640, cy=120, font_size=96, fit_width=900, anchor="center")

# multi-line — "\n" splits, line_height scales the leading
Text("line one\nline two\nline three",
     cx=640, cy=360, font_size=36, line_height=1.4, anchor="center")

# change the copy in place, no rebuild
t = scene.add_text("old title", cx=640, cy=100)
t.set_text("new title")
t.content = "or assign it"

fit_width / fit_height binary-search the font size at render time; font_size itself is unchanged, so you can animate it independently and they compose.

anchor accepts center / left / right / top / bottom and the usual combinations (top_left, bottom_right, …).


Images & sprite animation

from codeanim import Image, load_image, load_or_placeholder

logo = scene.add_image("logo.png", cx=640, cy=360, w=200, radius=16)
logo.rotation.keys([(0, 0), (2, 360)])

scene.add_image("hero.png", cx=300, cy=200, w=240)       # h from aspect
tex = load_image("icon.png")                             # load once, reuse
scene.add_image(tex, cx=200, cy=500, w=80)
scene.add_image(tex, cx=320, cy=500, w=80, tint="#22d3ee")

scene.add_image(None, cx=100, cy=100, w=60, h=60)        # checkerboard stub
Option
w / h give one, the other follows the aspect ratio
crop=(x,y,w,h) cut a rect out of the source (sprite sheets)
slice= 9-slice: 12 or (l,t,r,b) — corners don't distort
radius= rounded corners
tint= + tint_amount= colourise, good for monochrome icons
fit= stretch (default) / contain / cover

Sprite animation

sheet = scene.add_image("run.png", sprite=(4, 3), w=96)
sheet.play(fps=12)                        # loop at 12 fps
sheet.show_frame(0, at=2.0)               # pin a frame

# reverse / manual stepping via keyframes
sheet.frame.keys([(0, 11), (2.6, 0)])

# or slice manually
scene.add_image("run.png", frames=[(0,0,48,48), (48,0,48,48)], w=48)

Audio

Two things only: import a file and say when it plays, or generate a sound in code. Deliberately plain — no node graph, no automation curves.

from codeanim import Audio, beep, chime, sweep, whoosh, arpeggio

# --- import ---
scene.add_audio(Audio("bgm.mp3", at=0.0, volume=0.35,
                      loop=True, fade_in=0.5, fade_out=2.0))
scene.add_audio(Audio("whoosh.wav", at=3.5))
scene.add_audio(Audio("speech.mp3", at=2.0, trim=(3.0, 8.0)))

# --- generate (no assets needed) ---
scene.add_audio(beep(880, duration=0.2, at=1.0))
scene.add_audio(chime(at=5.0))
scene.add_audio(whoosh(at=2.6))
scene.add_audio(sweep(200, 1600, duration=0.35, at=3.0))
scene.add_audio(arpeggio([523, 587, 659, 784], at=7.4))
Audio option
at when it starts, in absolute seconds (ms precision)
volume gain multiplier
loop loop to fill the rest
fade_in / fade_out seconds
trim=(start, end) take a slice of the source
speed playback rate (also shifts pitch)
volume_curve automation, see below
pan -1 left … 0 centre … +1 right
delay echo: seconds / (delay, decay) / (delay, decay, count)
reverb 0–1, larger = roomier
tone brightness, negative = duller

Music notation

Stop typing frequencies:

from codeanim import note, chord, scale, melody, parse_melody

note("A4")                     # -> 440.0
note("C#5") / note("Db5")      # sharps / flats
chord("A3", "m7")              # 19 chord types
scale("C4", "blues")           # 14 scales
parse_melody("1 2 3 4 5")      # numbered notation
parse_melody("1' 2'")          # up an octave
parse_melody("1, 2,")          # down an octave
parse_melody("1 2 3", solfege_key="G")   # in G

Write a melody:

scene.add_audio(melody("C4 D4 E4 F4 G4", note_dur=0.2, at=0.0))
scene.add_audio(melody("1 2 3 4 5 6 7 1'", note_dur=0.18))
scene.add_audio(melody([["C4","E4","G4"], ["F4","A4","C5"]], note_dur=0.8))
scene.add_audio(chord_sound("C4", "maj", duration=1.5, at=0.0))

Voices & envelopes

from codeanim import Tone, Envelope, VOICES

Tone(freq=note("C4"), duration=1.5, voice="epiano")
Tone(freq=note("C5"), duration=2.0, voice="bell", reverb=0.6)
Tone(freq=note("E4"), duration=1.0, voice="strings", vibrato=(5, 25))

# ADSR: attack / decay / sustain / release
Tone(freq=note("C4"), duration=1.5, voice="epiano",
     envelope=Envelope(attack=0.01, decay=0.25, sustain=0.45, release=0.5))
Tone(freq=440, duration=1.0, envelope=(0.01, 0.2, 0.5, 0.4))   # tuple works too
Voice Sounds like
sine square triangle saw basic waveforms
flute flute / wind
strings strings (rich harmonics)
epiano electric piano
bell bell
marimba marimba
bass bass
music_box music box

Or roll your own harmonic table:

Tone(freq=440, partials=[(1, 1.0), (2, 0.5), (3, 0.25), (5, 0.1)])
Tone(freq=440, detune=12)          # slight detune, warmer
Tone(freq=440, tremolo=(8, 0.6))   # amplitude wobble

Volume curves

Audio("bgm.mp3", at=0, volume_curve=[(0, 0), (3, 1), (6, 0)])
Audio("x.mp3", volume_curve=[0.0, 0.5, 1.0])        # one per second
Audio("x.mp3", at=1.0, volume_curve=[(1,0),(3,1)])  # absolute seconds OK

Groups & other effects

from codeanim import AudioGroup, noise, pulse

sfx = AudioGroup([whoosh(at=2.0), beep(880, at=3.0), pulse(4, at=5.0)],
                 name="sfx")
sfx.volume = 0.6        # whole-group gain
sfx.shift(0.5)          # shift everything later
sfx.mute() / sfx.unmute()
scene.add_audio(sfx)

scene.add_audio(noise(duration=3.0, volume=0.12, color="brown"))  # wind
scene.add_audio(pulse(8, interval=0.08))                          # typewriter

Everything is pure standard library — no numpy. Mixing is delegated to ffmpeg (adelay + amix + aecho + pan).

codeanim script.py --list-audio    # show the timeline (with note names)
codeanim script.py --no-audio      # drop audio, video only

GIF has no audio. With audio tracks present, exporting a GIF is a loud error suggesting .mp4 / .webm, not a silent drop.


Composing a scene

Groups

from codeanim import Group, Rect, Circle

card = Group(cx=640, cy=360)
card.add(Rect(cx=0, cy=0, w=300, h=200, fill="#1e293b"))
card.add(Circle(cx=0, cy=-60, r=30, fill="#38bdf8"))

card.rotation.keys([(0, 0), (2, 360)])
card.opacity.keys([(0, 0), (0.6, 1)])
scene.add(card)

Inside a group, (0, 0) is the group centre. Parent rotation / scale / opacity propagate to the whole subtree. Nesting works.

Timeline

from codeanim import seq, par

total = seq(scene, [
    lambda s: add_title(scene, start=s),
    lambda s: par(scene, [                     # these two run together
        lambda t: add_logo(scene, start=t),
        lambda t: add_counter(scene, start=t),
    ]),
    lambda s: add_outro(scene, start=s),
])
scene.duration = total

Or pin how long each clip should take:

seq(scene, [
    add_title,                    # whatever the clip returns
    (add_ball, 2.0),              # at least 2 seconds
    (add_logo, 1.5, 0.3),         # at least 1.5s, then 0.3s of silence
    {"clip": add_outro, "span": 2.0, "gap": 0.5},
])

seq(scene, [a, b, c], gap=0.5)    # uniform gap between clips

The requested span is a lower bound — a longer clip is never truncated:

clip returns you asked actual
0.5s 2.0 2.0s (padded)
3.0s 1.0 3.0s (clip wins)

Reusable fragments

my_project/
├── main.py          entry
├── title.py         title fragment
├── ball.py          ball fragment
└── logo.py          intro fragment
# title.py
from codeanim import Text

def add_title(scene, start=0.0, title="Hello"):
    t = Text(title, cx=scene.width/2, cy=scene.height/2,
             font_size=96, fill="white", anchor="center")
    t.opacity.keys([(start, 0), (start + 0.9, 1)])
    scene.add(t, z=10)
    return 1.5          # this clip takes 1.5s

Sibling .py files just import each other — the engine puts the script's directory on sys.path.


Disk safety

The easiest way to blow up a device with high-quality animation: 4K / 60fps / 60s of PNG frames is about 21 GB. Three layers of protection:

1. Zero-disk by default. stream mode pipes rendered frames straight into ffmpeg's stdin. No intermediate files at all — disk usage equals the final video.

2. If you do ask for disk, do the math first. Only --frames / --keep-frames use disk mode, and that path estimates space up front:

Not enough disk space for the intermediate PNG frames.

  frame size:  3840 x 2160
  frames:      3600
  needed:      15.30 GB
  available:   2.10 GB  (/mnt/data, 15% headroom reserved)

Three ways out (the first is recommended):
  1. Drop --frames / --keep-frames and use the default zero-disk streaming:
       codeanim your_script.py -o out.mp4
  2. Write to a roomier location:
       codeanim your_script.py --frames /other/drive/dir/
  3. Lower the resolution or frame rate.

3. A live red line during render. Streaming mode also checks free space every 40 frames and aborts below --free-margin. Temp files are cleaned up on success, error, and Ctrl-C.


Output formats

Format Use for How Alpha
MP4 (H.264) default, best compatibility -o out.mp4 —
WebM (VP9) small files -o out.webm yes*
MOV Apple ecosystem -o out.mov ProRes 4444
MKV generic container -o out.mkv —
GIF chat, docs --gif —
PNG sequence hand off to AE / Blender --frames dir/ yes
Single PNG check one moment --snapshot 2.0 yes

* WebM alpha depends on whether your libvpx was built with it. CodeAnim actually probes it (encodes a transparent sample and decodes it back), and tells you to use MOV instead of silently writing an opaque file.

Transparency

scene = Scene(1280, 720, background=None)   # None = transparent
codeanim overlay.py -o overlay.mov          # ProRes 4444

Drop it straight onto a Premiere / Final Cut timeline or a web <video>.


Design constraints

  1. No GUI. Never opens a window, never a player.
  2. Pure-function scene. f(t) is independently evaluable at any moment — so seeking, reversing, and multiprocess rendering all just work. There are dedicated regression tests for this.
  3. Scriptable. Fits in a Makefile, CI, GitHub Actions.
  4. Command line first. Every feature is reachable from a flag.
  5. Errors in plain language. Missing ffmpeg tells you how to install it; script errors show only your file and line; space / container problems give concrete fixes instead of a stack trace.

Testing

python tests/test_codeanim.py

527 assertions covering colour, easing, keyframes, purity, centre positioning, group transforms, gradients / shadows / perspective, particles, images, sprite animation, audio (import + generate + mixing + curves + voices + ADSR + pan/echo/reverb + groups), music theory, paths, Digits formatting, timelines, clip spans, layer order, sugar, hot reload, in-terminal preview, parameter validation, disk budgeting, encoder args, supersampling.


Examples

File Shows
examples/promo/ a full 19s 1080p promo, built with the engine itself
examples/demo.py title card: seq/par + Group + Digits + z layers
examples/chart.py bar chart + line + rolling numbers
examples/particles.py 300+ primitives, performance
examples/media.py images + sprite animation + audio
examples/showcase.py gradients / shadows / perspective / particles / text fit
examples/music_path.py music notation, voices, balls flying on beziers
examples/alpha_overlay.py transparent background for compositing

At a glance

Rendering

  • Zero-disk streaming (disk usage = final video size)
  • 2× supersampled anti-aliasing
  • Multiprocess parallel (frames are independent)
  • Static pre-composition + fast path
  • Automatic CJK font discovery (incl. .ttc)

Graphics

  • Primitives: Circle Rect Line Text Image Path Digits Group
  • Gradients: linear / radial / conic, uneven stops
  • Drop shadows with blur / spread / offset
  • 3D-ish perspective (quad warp)
  • Particles: burst, fountain, snow, sparkle, ripple — reproducible
  • Paths: bezier, Catmull-Rom splines, arcs, polygons, follow(), along()
  • 25 easing curves, colour interpolation

Audio

  • Import any format ffmpeg reads
  • Generate in code: beep chime sweep whoosh arpeggio melody chord_sound noise pulse
  • Music theory: note chord scale parse_melody
  • 11 voices + custom harmonic tables
  • ADSR envelopes, vibrato, tremolo, detune
  • Volume curves, pan, echo, reverb, brightness
  • Groups with batch gain / shift / mute
  • Pure standard library — no numpy

Workflow

  • --watch hot reload with live preview
  • --inline in-terminal rendering (half-block characters)
  • --preview render one frame and open it
  • --layers draw-order inspector
  • doctor dependency check
  • Disk budgeting and live guards
  • Errors show only your file and line
  • --ch for Chinese output (English by default)

Metadata

Release files for codeanim-py 0.4.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for codeanim-py 0.4.0
File Size Uploaded
codeanim_py-0.4.0.tar.gz 212.4 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for codeanim-py 0.4.0
File Interpreter ABI Platform
codeanim_py-0.4.0-py3-none-any.whl Python 3 none any Details

Total release size: 383.7 kB

Release files / codeanim_py-0.4.0.tar.gz

Download URL codeanim_py-0.4.0.tar.gz
Size 212.4 kB
Tags Source
SHA-256 checksum
How to use checksums
0d54e6b0fe8ece184a1eef91cd21f08aefa951a8cbd0ea93cbb29cef7f94cdb8
BLAKE2b-256 checksum
How to use checksums
3d37934ecebb2febd8b4585d919f16b5279c63a8f5ebffb0b23506cda8cfbb97
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.3

Release files / codeanim_py-0.4.0-py3-none-any.whl

Download URL codeanim_py-0.4.0-py3-none-any.whl
Size 171.4 kB
Tags Python 3
SHA-256 checksum
How to use checksums
f71fa13b04b900c9402bf89678df6c3348f7f4756d2fe0c5945a28be976f85d1
BLAKE2b-256 checksum
How to use checksums
27aa5b9e9c89642bb45bdbd6aac1fdecb031521753804652b37f5bf64367fd2d
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.3

Release history Release notifications | RSS feed

This release

0.4.0 This release

2 release files

0.3.0

2 release files

0.2.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page