Formualizer
The fastest open-source spreadsheet engine. Native speed, from Python.
Load Excel workbooks, change inputs, recalculate and read results—in-process, with no Excel installation or office-suite service. Formualizer runs the calculation in Rust and gives you a Python API. Built for financial models, data pipelines and AI agents.
- No compromise on speed. Copied formulas compute as families over Arrow columns; lookups reuse indexes; only affected cells recalculate. See the benchmarks and methodology.
- Excel-compatible. 400+ functions, dynamic arrays,
LET,LAMBDA, names and cross-sheet references. Unlike a file reader, Formualizer computes formulas rather than just returning cached values. - Built for agents. Inspect dependencies, track edits with undo/redo, inject a clock and random seed, and expose typed inputs and outputs through SheetPort.
- Portable. Native wheels for Linux, macOS and Windows, plus a separate Pyodide build. The same engine also ships for Rust and JavaScript.
Need CLI or MCP tools for an agent rather than an embedded library? Use agent-spreadsheet, built on Formualizer.
Installation
pip install formualizer
Prebuilt stable-ABI (abi3) wheels are published for Python 3.10 and newer on Linux (glibc and musl), macOS, and Windows. No Rust toolchain required.
Documentation
Full documentation at formualizer.dev:
- Python Quickstart
- Python API Reference
- Function Reference — 400+ built-in functions
- SheetPort Guide — spreadsheets as typed APIs
- Workbook Edits and Batching
Quick start
Rows and columns are 1-based: row 3, column 2 is B3.
Create and calculate a model
import formualizer as fz
wb = fz.Workbook()
s = wb.sheet("Sheet1")
s.set_value(1, 1, fz.LiteralValue.number(1000.0)) # A1: principal
s.set_value(2, 1, fz.LiteralValue.number(0.05)) # A2: annual rate
s.set_value(3, 1, fz.LiteralValue.number(12.0)) # A3: periods
s.set_formula(1, 2, "=PMT(A2/12, A3, -A1)")
print(wb.evaluate_cell("Sheet1", 1, 2)) # ~85.61
Load an XLSX and evaluate
import formualizer as fz
# Use a workbook with Assumptions and Summary sheets.
wb = fz.load_workbook("financial_model.xlsx", strategy="eager_all")
wb.set_value("Assumptions", 3, 2, 0.07) # B3: change an input
wb.evaluate_all() # calculate affected formulas
print(wb.get_value("Summary", 5, 2)) # B5: read an output
# Optional native read-only mapping. The underlying file must not be
# destructively modified or truncated while it is loading.
mapped = fz.load_workbook(
"financial_model.xlsx",
path_source=fz.XlsxPathSource.DIRECT_MMAP,
)
Load and save XLSX bytes
import formualizer as fz
payload = open("financial_model.xlsx", "rb").read()
wb = fz.load_workbook_bytes(payload)
print(wb.evaluate_cell("Summary", 1, 2))
out = wb.to_xlsx_bytes()
Native Python builds use calamine by default for both path-based and byte-oriented XLSX loading. Pyodide currently defaults to umya, which also remains available explicitly on native builds. XLSX byte export uses umya because Calamine is read-only.
Recalculate XLSX cached values (writeback)
import formualizer as fz
# in-place
summary = fz.recalculate_file("financial_model.xlsx")
print(summary["status"], summary["evaluated"], summary["errors"])
# write to a new file
summary = fz.recalculate_file(
"financial_model.xlsx", output="financial_model.recalc.xlsx"
)
Formula text is preserved. Cached-value typing follows the active
umya-spreadsheetimplementation.
Cache-only XLSX recalculation
result = fz.recalculate_xlsx_bytes(payload)
assert isinstance(result["bytes"], bytes)
print(result["summary"]["status"], result["cache_cells_changed"])
# The file API snapshots input and atomically replaces the destination on success.
result = fz.recalculate_xlsx_file("model.xlsx", output="model.recalc.xlsx")
These APIs use the shared cache-only Rust implementation, retaining formula text
and unrelated package members. Safe core resource limits apply;
error_location_limit= only caps retained error locations.
Parse and analyze formulas
from formualizer import parse
from formualizer.visitor import collect_references, collect_function_names
ast = parse("=SUMIFS(Revenue,Region,A1,Year,B1)")
print(ast.pretty()) # indented AST tree
print(ast.to_formula()) # canonical Excel string
print(collect_references(ast)) # [Revenue, Region, A1, Year, B1]
print(collect_function_names(ast)) # ['SUMIFS']
Key features
| Capability | Description |
|---|---|
| Tokenization | Break formulas into structured Token objects with byte spans and operator metadata |
| Parsing | Produce a rich AST with reference normalization, source tracking, and 64-bit structural fingerprints |
| 400+ built-in functions | Math, text, lookup (XLOOKUP, VLOOKUP), date/time, financial, statistics, database, engineering |
| Workbook evaluation | Set values and formulas, evaluate cells/ranges, load XLSX/CSV/JSON |
| XLSX cache writeback | recalculate_file(path, output=None) recalculates formulas and writes cached values back |
| Batch operations | set_values_batch / set_formulas_batch for efficient bulk updates |
| Undo / redo | Optional changelog with automatic action grouping — single edits are individually undoable |
| Evaluation planning | Inspect the dependency graph and evaluation schedule before computing |
| SheetPort | Treat spreadsheets as typed functions with YAML manifests, schema validation, and batch scenarios |
| Deterministic mode | Inject clock, timezone, and RNG seed for reproducible evaluation |
| Visitor utilities | walk_ast, collect_references, collect_function_names for ergonomic tree traversal |
| Rich errors | Typed TokenizerError / ParserError / ExcelEvaluationError with position info |
Workbook evaluation
import formualizer as fz
wb = fz.Workbook()
s = wb.sheet("Data")
# Set values and formulas
s.set_value(1, 1, fz.LiteralValue.number(100.0))
s.set_value(2, 1, fz.LiteralValue.number(200.0))
s.set_value(3, 1, fz.LiteralValue.number(300.0))
s.set_formula(4, 1, "=SUM(A1:A3)")
s.set_formula(4, 2, "=AVERAGE(A1:A3)")
print(wb.evaluate_cell("Data", 4, 1)) # 600.0
print(wb.evaluate_cell("Data", 4, 2)) # 200.0
Custom functions
Register workbook-local callbacks without forking Formualizer:
import formualizer as fz
wb = fz.Workbook(mode=fz.WorkbookMode.Ephemeral)
wb.add_sheet("Sheet1")
wb.register_function(
"py_add",
lambda a, b: a + b,
min_args=2,
max_args=2,
)
wb.set_formula("Sheet1", 1, 1, "=PY_ADD(20,22)")
print(wb.evaluate_cell("Sheet1", 1, 1)) # 42
print(wb.list_functions())
wb.unregister_function("py_add")
Key semantics:
- Names are case-insensitive and stored canonically (
py_add->PY_ADD). - Custom functions are workbook-local and take precedence over global built-ins.
- Built-in override is disabled by default; set
allow_override_builtin=Trueto opt in. - Args are passed by value; range inputs arrive as nested Python lists.
- Return Python primitives, datetime/date/time/timedelta, dict error objects, or nested lists for array spill output.
- Python callback exceptions are sanitized and mapped to
#VALUE!.
Runnable custom-function example (from a source checkout: python bindings/python/examples/custom_function_registration.py).
Batch operations
# Bulk-set values (auto-grouped as one undo step when changelog is enabled)
s.set_values_batch(
1,
1,
3,
2,
[
[fz.LiteralValue.number(10.0), fz.LiteralValue.number(20.0)],
[fz.LiteralValue.number(30.0), fz.LiteralValue.number(40.0)],
[fz.LiteralValue.number(50.0), fz.LiteralValue.number(60.0)],
],
)
Undo / redo
The changelog is opt-in. Once enabled, every edit is tracked:
wb.set_changelog_enabled(True)
s.set_value(1, 1, fz.LiteralValue.number(10.0))
s.set_value(1, 1, fz.LiteralValue.number(20.0))
wb.undo() # back to 10
wb.redo() # back to 20
# Batch methods are auto-grouped as one undo step.
# For manual grouping of multiple calls:
wb.begin_action("update prices")
s.set_value(1, 1, fz.LiteralValue.number(100.0))
s.set_value(2, 1, fz.LiteralValue.number(200.0))
wb.end_action()
wb.undo() # reverts both values at once
Evaluation planning
Inspect what the engine will compute before running:
plan = wb.get_eval_plan([("Sheet1", 1, 2)])
print(f"Vertices to evaluate: {plan.total_vertices_to_evaluate}")
print(f"Parallel layers: {plan.estimated_parallel_layers}")
for layer in plan.layers:
print(f" Layer: {layer.vertex_count} vertices, parallel={layer.parallel_eligible}")
# By default this will build deferred workbook graphs if needed.
# Disable that behavior if you want planning to fail instead of mutating workbook state.
wb.get_eval_plan([("Sheet1", 1, 2)], build_graph_if_needed=False)
SheetPort: spreadsheets as typed APIs
Define a YAML manifest to treat a spreadsheet as a typed function with validated inputs/outputs:
from formualizer import SheetPortSession, Workbook
manifest_yaml = """
spec: fio
spec_version: "0.3.0"
manifest:
id: pricing-model
name: Pricing Model
workbook:
uri: memory://pricing.xlsx
locale: en-US
date_system: 1900
ports:
- id: base_price
dir: in
shape: scalar
location: { a1: Inputs!A1 }
schema: { type: number }
- id: final_price
dir: out
shape: scalar
location: { a1: Outputs!A1 }
schema: { type: number }
"""
wb = Workbook()
wb.add_sheet("Inputs")
wb.add_sheet("Outputs")
wb.set_formula("Outputs", 1, 1, "=Inputs!A1*1.2")
session = SheetPortSession.from_manifest_yaml(manifest_yaml, wb)
session.write_inputs({"base_price": 100.0})
result = session.evaluate_once(freeze_volatile=True)
print(result["final_price"]) # 120.0
API reference
Top-level functions
tokenize(formula: str, dialect: FormulaDialect = None) -> Tokenizer
parse(formula: str, dialect: FormulaDialect = None) -> ASTNode
load_workbook(path: str, strategy: str = None, *, path_source: XlsxPathSource | None = None, span_evaluation: bool | None = None) -> Workbook
load_workbook_bytes(data: bytes, strategy: str = None, backend: str | None = None, *, span_evaluation: bool | None = None) -> Workbook
recalculate_file(path: str, output: str | None = None) -> dict
recalculate_xlsx_bytes(data: bytes, *, error_location_limit: int | None = None) -> dict
recalculate_xlsx_file(path: str, output: str | None = None, *, error_location_limit: int | None = None) -> dict
Core classes
Workbook— create, load, evaluate, undo/redo. Supportsfrom_path(),from_bytes(),load_path(), andto_xlsx_bytes().Sheet— per-sheet facade forset_value,set_formula,get_cell, batch operations.LiteralValue— typed values:.int(),.number(),.text(),.boolean(),.date(),.time(),.datetime(),.duration(),.empty(),.error(),.array().Tokenizer— iterable token sequence with.render()and.tokens.ASTNode—.pretty(),.to_formula(),.fingerprint(),.children(),.walk_refs().CellRef/RangeRef/TableRef/NamedRangeRef— typed references.SheetPortSession— bind manifests to workbooks, read/write typed ports, evaluate.EvaluationConfig— tune parallel evaluation, warmup, range limits, date systems.
Visitor helpers (formualizer.visitor)
walk_ast(node, visitor_fn) # DFS with VisitControl (CONTINUE/SKIP/STOP)
collect_references(node) # -> list[ReferenceLike]
collect_function_names(node) # -> list[str]
collect_nodes_by_type(node, "Function") # -> list[ASTNode]
Full type stubs are included in the package (.pyi files) for IDE autocompletion and mypy.
Building from source
Requires Rust 1.93.0 (the pinned release toolchain; edition 2024) and maturin:
pip install maturin
cd bindings/python
maturin develop # debug build
maturin develop --release # optimized build
Using in Pyodide (browser / WebAssembly)
Native wheels are published to PyPI. Pyodide wheels are built and smoke-tested in CI and release workflows, then uploaded only as the wheels-pyodide Actions artifact; they are not uploaded to PyPI or attached to GitHub Releases. Download and extract the artifact (or build locally), then host the compatible wheel at a browser-accessible URL with suitable CORS headers. An Actions artifact ZIP is not a wheel URL:
import micropip
wheel_url = "<your-downloadable-wheel-url>"
await micropip.install(wheel_url)
import formualizer as fz
wb = fz.Workbook()
wb.add_sheet("Sheet1")
wb.set_value("Sheet1", 1, 1, 20)
wb.set_value("Sheet1", 2, 1, 22)
wb.set_formula("Sheet1", 1, 2, "=SUM(A1:A2)")
wb.evaluate_cell("Sheet1", 1, 2) # -> 42.0
Tested Pyodide target: CI and release smoke tests use Pyodide 0.29.3 and the wheel's derived ABI (currently pyodide_2025_0). Rebuild and smoke-test a wheel when targeting another runtime; no persistent public wheel URL is promised.
Pyodide-specific behavior:
EvaluationConfig()andWorkbook()defaultenable_parallel = Falseonsys.platform == "emscripten"(Pyodide has no threads). You can still opt in, but it falls back to single-threaded execution.- Native XLSX byte loading (
Workbook.from_bytes,load_workbook_bytes) defaults tocalamine; Pyodide defaults toumya. XLSX byte export usesumyaon all platforms. - Python UDFs registered via
Workbook.register_functionwork identically to native; single-cell refs arrive as scalars (Excel-native semantics).
Building a Pyodide wheel from source
For local development or targeting a Pyodide version without a retained Actions artifact:
./scripts/build-pyodide-wheel.sh
./scripts/smoke-pyodide-wheel.sh dist/pyodide/*-pyodide_*_wasm32.whl
The build script defaults to xbuildenv Pyodide 0.29.3, derives Python, ABI, Emscripten, and Rust toolchain values from pyodide config, installs Pyodide's custom wasm-EH Rust sysroot over the stock rustup target, and retags the output wheel to the platform tag Pyodide's micropip expects. pyodide-cli and pyodide-build are resolved through uvx and are not pinned by the script.
Testing
pip install formualizer[dev]
pytest bindings/python/tests
ruff check bindings/python
mypy bindings/python/formualizer
Workspace layout
formualizer/
crates/ # Rust core (parse, eval, workbook, sheetport)
bindings/python/
formualizer/ # Python package (helpers, visitor, type stubs)
src/ # PyO3 bridge (Rust -> Python)
The Python wheel links directly against the Rust crates — there is no runtime FFI overhead beyond the initial C-to-Rust boundary.
License
Dual-licensed under MIT or Apache-2.0, at your option. Both license texts ship inside the package.
Metadata
Release files for formualizer 0.10.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| formualizer-0.10.1.tar.gz | 2.7 MB | Details |
Built distributions (wheels)
| File | Reset | |||
|---|---|---|---|---|
| formualizer-0.10.1-cp310-abi3-win_amd64.whl | CPython 3.10 | abi3 | Windows x86-64 | Details |
| formualizer-0.10.1-cp310-abi3-musllinux_1_2_x86_64.whl | CPython 3.10 | abi3 | Linux musl 1.2+ x86-64 | Details |
| formualizer-0.10.1-cp310-abi3-musllinux_1_2_aarch64.whl | CPython 3.10 | abi3 | Linux musl 1.2+ ARM64 | Details |
| formualizer-0.10.1-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl | CPython 3.10 | abi3 | Linux glibc 2.17+ x86-64 | Details |
| formualizer-0.10.1-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl | CPython 3.10 | abi3 | Linux glibc 2.17+ ARM64 | Details |
| formualizer-0.10.1-cp310-abi3-macosx_11_0_arm64.whl | CPython 3.10 | abi3 | macOS 11.0+ ARM64 | Details |
| formualizer-0.10.1-cp310-abi3-macosx_10_12_x86_64.whl | CPython 3.10 | abi3 | macOS 10.12+ x86-64 | Details |
Total release size: 58.5 MB
Release files / formualizer-0.10.1.tar.gz
| Download URL | formualizer-0.10.1.tar.gz |
|---|---|
| Size | 2.7 MB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
693b83845c45cd561d96e0938a6d9fd1370d6f493292070804ae1e6eb5e87fbd
|
|
BLAKE2b-256 checksum How to use checksums |
afaa490452119a849ec99352b63cb97a524ca3f49a46050ea90b7c7b5ff03e10
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.12.21 {"installer":{"name":"uv","version":"0.12.21","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}
|
Release files / formualizer-0.10.1-cp310-abi3-win_amd64.whl
| Download URL | formualizer-0.10.1-cp310-abi3-win_amd64.whl |
|---|---|
| Size | 7.7 MB |
| Tags | CPython 3.10 Windows x86-64 abi3 |
|
SHA-256 checksum How to use checksums |
8f5c58bd2c2c5baaf68223b3a2c06949f62b11b41255a6327881926b235bfcfc
|
|
BLAKE2b-256 checksum How to use checksums |
4ea59b653a2b583778113c9a16548429190cef216903b597830485e2244f1a01
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.12.21 {"installer":{"name":"uv","version":"0.12.21","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}
|
Release files / formualizer-0.10.1-cp310-abi3-musllinux_1_2_x86_64.whl
| Download URL | formualizer-0.10.1-cp310-abi3-musllinux_1_2_x86_64.whl |
|---|---|
| Size | 8.5 MB |
| Tags | CPython 3.10 Linux musl 1.2+ x86-64 abi3 |
|
SHA-256 checksum How to use checksums |
3db5b28a03c6a4ac6fff09a1aadacf0ca92176516def7535d588ce385dd4c2b1
|
|
BLAKE2b-256 checksum How to use checksums |
4520d9da9a87868fde2d7d8d995240dd3f606b9f6a7f7fb8e172684cedeeb7a2
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.12.21 {"installer":{"name":"uv","version":"0.12.21","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}
|
Release files / formualizer-0.10.1-cp310-abi3-musllinux_1_2_aarch64.whl
| Download URL | formualizer-0.10.1-cp310-abi3-musllinux_1_2_aarch64.whl |
|---|---|
| Size | 8.1 MB |
| Tags | CPython 3.10 Linux musl 1.2+ ARM64 abi3 |
|
SHA-256 checksum How to use checksums |
d1033164fa3e92fdc456cebf4a4c5b8977e7fec3a1d00820df7e2d3e9e50d6cc
|
|
BLAKE2b-256 checksum How to use checksums |
5a79ef4012f7ff40cddd3a9d8cd71a7b9696f0d778f94148aec9f278874bb7ff
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.12.21 {"installer":{"name":"uv","version":"0.12.21","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}
|
Release files / formualizer-0.10.1-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
| Download URL | formualizer-0.10.1-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl |
|---|---|
| Size | 8.3 MB |
| Tags | CPython 3.10 Linux glibc 2.17+ x86-64 abi3 |
|
SHA-256 checksum How to use checksums |
2608121c5861f791d35bc119d45d9bfc23f7fdd49c28e842959d5c60472a05eb
|
|
BLAKE2b-256 checksum How to use checksums |
b892db9bed23faf5dcf245ea689829da19d05de758a44719db8b89042b52f26e
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.12.21 {"installer":{"name":"uv","version":"0.12.21","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}
|
Release files / formualizer-0.10.1-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
| Download URL | formualizer-0.10.1-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl |
|---|---|
| Size | 7.9 MB |
| Tags | CPython 3.10 Linux glibc 2.17+ ARM64 abi3 |
|
SHA-256 checksum How to use checksums |
2f021207408d7ba32957ccde72bd66ac0d291af16e031a84cbaa00bdfc3da523
|
|
BLAKE2b-256 checksum How to use checksums |
511bbd4aa41ae8d6a966b524474a42e202351210f03d5a978dcd94a871fa328b
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.12.21 {"installer":{"name":"uv","version":"0.12.21","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}
|
Release files / formualizer-0.10.1-cp310-abi3-macosx_11_0_arm64.whl
| Download URL | formualizer-0.10.1-cp310-abi3-macosx_11_0_arm64.whl |
|---|---|
| Size | 7.5 MB |
| Tags | CPython 3.10 abi3 macOS 11.0+ ARM64 |
|
SHA-256 checksum How to use checksums |
f6df5b1af3af953d911650041849987c5a6380ef216a0f368fb635251c5e64b0
|
|
BLAKE2b-256 checksum How to use checksums |
f349e967dcd716dba251e71c96709d89fe3bfc91afaa5deeb42fb1df3f493048
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.12.21 {"installer":{"name":"uv","version":"0.12.21","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}
|
Release files / formualizer-0.10.1-cp310-abi3-macosx_10_12_x86_64.whl
| Download URL | formualizer-0.10.1-cp310-abi3-macosx_10_12_x86_64.whl |
|---|---|
| Size | 7.9 MB |
| Tags | CPython 3.10 abi3 macOS 10.12+ x86-64 |
|
SHA-256 checksum How to use checksums |
dd7592e99b62a8227e07445c1d5ed1946e584d670fa95e6c5f77481821e2c380
|
|
BLAKE2b-256 checksum How to use checksums |
0800643a43372cb273097871d3dfdc2179bbd869eb625b8c0e01c2b9834e8d67
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.12.21 {"installer":{"name":"uv","version":"0.12.21","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}
|