gridcalc
A programmable spreadsheet for developers and technical users, with Excel-ish formulas, Python escape hatches, XLSX interop, goal seek, and LP/MIP solving. It ships two frontends over one engine: a curses terminal app, and a desktop window that renders the same workbook in a webview.
Inspired by Serge Zaitsev's kalk.
pip install gridcalc
gridcalc budget.json # terminal
gridcalc-web budget.json # desktop window (needs the [web] extra -- see below)
What it gives you
- Excel-compatible formulas (
=IF(A1>=B1, A1*0.05, 0)) and arrays (=SUM(A1:A10 * B1:B10)) without leaving the terminal. - Multi-sheet workbooks with cross-sheet refs (
=Sheet2!A1) and a proper dep graph. - Three formula modes per file: strict Excel, Excel-plus-
py.*, or full Pythonevalwith numpy/pandas. - xlsx interop via OpenXLSX C++ -- read every sheet's formulas + values; export with cached values.
- Linear, mixed-integer & quadratic programming built in via HiGHS --
:opt max B4 vars A4:A5 st D4:D6solves from cells in the sheet, with shadow prices, infeasibility diagnosis, and RHS sweeps. Models persist in the workbook. - Goal-seek --
:goal B10 = 100 by A1adjusts a variable so a formula hits a target value. - Vim-style command line (
:w,:e,:q,/search,y/p, visual selection, undo), with system-clipboard copy/paste -- yank pushes values as TSV, paste pulls TSV in from other apps. - A desktop app as well as a terminal one -- the same engine behind a
mouse-driven grid with menus, a Ctrl-K command palette, and solver results
painted onto the sheet. Most
:commands are a shared registry both frontends dispatch, so they behave identically in each. See Desktop app.
Try it on the provided examples:
gridcalc example_excel.json # sales report, named ranges, IF/MATCH
gridcalc example_hybrid.json # progressive tax via py.* + aggregations
gridcalc example.json # PYTHON: numpy/pandas, list-comprehensions
gridcalc example_multisheet.json # 3-sheet budget, cross-sheet formulas
gridcalc example_lp.json # LP/MIP demo -- type :opt to solve
gridcalc example_goal.json # goal-seek demo -- :goal B1 = 11 by A1
Install
Two options:
pip install gridcalc # core: zero third-party runtime deps
pip install 'gridcalc[extras]' # adds numpy, pandas, pygments
Or with uv: uv tool install 'gridcalc[extras]'.
The core install has zero third-party runtime dependencies on
Linux and macOS -- the full 300+ Excel function library (statistical
distributions, financial functions, LINEST/TREND regression, ...)
works on stdlib alone. (The 3.10 wheel pulls tomli for config-file
parsing; 3.11+ uses stdlib tomllib. On Windows only, windows-curses
is pulled in because curses is not in the Windows stdlib.)
The [extras] bundle enables, all at once:
np.array(...)in formulas; LAPACK-backed solvers; fasterLINEST.pd.DataFrame(...),:pd load/save, DataFrame cell display.- Pygments syntax-highlight in the load-time trust prompt.
Desktop app (web frontend)
An editable grid in a native desktop window, driven by the same engine as the
terminal app. The engine runs in-process on CPython -- keeping the C++
extensions -- and a pywebview window renders a
viewport over it, calling Python directly across the js_api bridge. There is
no server, no port, and no HTTP.
Both frontends read and write the same files, format cells through the same
code, and share most of the : command set through a
frontend-neutral registry -- so :sort in the
terminal and Sort rows in the palette are one implementation, not two that
drift. A conformance test fails if either frontend loses a shared command.
pip install 'gridcalc[web]'
gridcalc-web # demo workbook, or: gridcalc-web book.json
From a source checkout, build the UI bundle first:
make web-buildcompiles the React client intosrc/gridcalc/web/static/index.html, whichgridcalc-webloads. It is a build artifact and is not in git, so a checkout that has not run it exits with a message telling you to. Released wheels and sdists ship the bundle already built --make wheel/make sdistand every CI build job compile it first, and the release pipeline fails if a distribution is missing it.
Editing. In-cell and formula-bar editing, keyboard navigation, rectangular
selection by drag or shift-click, and clicking a row or column header to select
the whole line. Copy/cut/paste (formula references adjust, $ absolutes do
not), fill down and right, undo/redo, insert and delete rows and columns sized
to the selection. Sheets can be added, renamed, deleted, and reordered. Column
edges drag to resize and the width is saved with the workbook. A status bar
reports the selection's aggregates and an unsaved-changes marker; closing with
unsaved work asks first.
Formula point mode works as it does in the terminal: while typing a =
formula, clicking or dragging on the grid inserts the reference at the caret.
Ctrl-K opens a command palette over the whole command set, matched on
subsequences (fld finds Fill down). It is the home for commands no menu
would justify -- column width, named ranges, sort, formula mode, freeze panes,
manual recalculation -- and prompts for arguments the way the : line does.
Ctrl-F finds text or computed values, so a formula is findable by its
result as well as its source.
Optimization is where it goes past the terminal. Optimize reads a model
off a selected block or loads one saved in the workbook (the same models
:opt run executes), solves it, and then paints the result onto the sheet:
the objective, the decision cells, and each constraint marked binding or slack,
with shadow prices and RHS ranging in the hover text. A shadow price means
considerably more sitting on the constraint row it belongs to than in a column
of cell references. Goal Seek and a parametric Sweep -- the objective
plotted against a constraint's right-hand side, breakpoints marked -- round it
out.
Not there yet: the object editor for Vec/ndarray/DataFrame cells, code-block
editing (:e), pandas import/export, and :move/:replicate. Workbooks in
HYBRID or PYTHON mode load formulas only -- their code block is never
executed, so code-dependent cells show an error state until a trust flow
exists. See docs/web.md for the full parity table and
docs/gui.md for why this direction was chosen.
The [web] extra pulls a native webview stack, deliberately kept out of the
lean terminal build; the curses app has no such dependency.
Quick tour
Cells hold a number, a label (any non-=-prefixed string), or a formula
(prefixed with =). Arrow keys move; Enter commits and moves down;
Tab commits and moves right. The rest of this guide shows the terminal
frontend, but the model, formulas, file format, and most commands are the same
in the desktop app.
A B C
1 Revenue Cost Margin
2 1000 600 =(A2-B2)/A2*100 <- formula
3 1200 700 =(A3-B3)/A3*100
4 Total =SUM(B2:B3) =AVG(C2:C3)
Press : for the command line. The basics:
| Command | Purpose |
|---|---|
:w [file] |
save (extension .json or .xlsx) |
:o file |
open |
:q, :q! |
quit, force-quit |
:e |
edit the workbook's Python code block in $EDITOR |
u, Ctrl-R |
undo / redo |
v |
enter visual selection mode (then y yanks, p pastes) |
/text |
search (n/N to cycle matches) |
> |
go to a named cell (e.g. > AA10) |
A full command reference lives in the Reference section below.
Modes
Each workbook has one of three evaluation modes, controlling which formulas parse and what's reachable from them:
| Mode | Grammar | Python escape hatch | Sandbox | Use case |
|---|---|---|---|---|
EXCEL |
strict Excel | none | not needed (no eval) |
xlsx interop, untrusted files |
HYBRID |
Excel + py.* |
code-block functions reachable as py.foo(...) |
code blocks only | most new sheets |
PYTHON |
Python eval() |
full Python expressions | full AST sandbox | numpy/pandas-heavy work |
Switch with :mode <name> -- the change is refused if any current
formula doesn't parse in the target mode. Files without an explicit
mode field load as PYTHON (back-compat). :xlsx load switches to
EXCEL automatically.
Formulas
=A1 + B1 * 2 arithmetic, Excel precedence
=(A1 + A2) / 2 grouping
=2^10 exponent (PYTHON: ** also works)
=50% percent postfix -> 0.5
="hello " & A1 string concat
=IF(A1 > 0, "pos", "neg") conditionals
=IFERROR(B1/C1, 0) error catch -- #DIV/0!, #VALUE!, #N/A, ...
=SUM(A1:A10) range -> 1D array
=SUM(A1:A3 * B1:B3) element-wise array arithmetic
=LET(x, SUM(A1:A9), x/COUNT(A1:A9)) local bindings -- compute once, reuse
=FILTER(A1:A9, B1:B9 > 0) dynamic arrays: FILTER/SORT/UNIQUE
=SUM(revenue) named range
=py.margin(A1, B1) HYBRID: call a code-block function
Excel error values (#DIV/0!, #N/A, #NAME?, #REF!, #VALUE!,
#NUM!, #NULL!) propagate through arithmetic and are catchable with
IFERROR/IFNA.
Built-in functions (always available): SUM, AVG, MIN, MAX,
COUNT, ABS, SQRT, INT, plus everything in math (sin, cos,
log, pi, e, ...).
Excel-compatible library (auto-loaded in EXCEL/HYBRID): IF,
IFERROR, AND, OR, NOT, ROUND, AVERAGE, MEDIAN, SUMIF,
COUNTIF, AVERAGEIF, LET, VLOOKUP, HLOOKUP, XLOOKUP, XMATCH,
INDEX, MATCH, FILTER, SORT, UNIQUE, SEQUENCE, CONCATENATE,
LEFT, RIGHT, MID, LEN, TRIM, UPPER, LOWER, SUBSTITUTE,
and 280+ others. Dynamic-array functions return whole rows/columns and
compose (=INDEX(SORT(A1:B9), 1, 2)).
PYTHON-only extras: the math module, Python builtins (sum,
min, max, abs, len), list comprehensions, and -- when the
relevant extras are installed -- np.array(...), np.linalg, matrix
multiply (@), and pd.DataFrame(...).
Named ranges & custom functions
:name revenue A1:A12 Define a named range (workbook-global)
:names List
:unname revenue Remove
Used directly in formulas: =SUM(revenue), =MAX(revenue - costs).
Open the per-workbook Python code block with :e. Anything defined there
becomes callable from formulas:
def margin(rev, cost):
return (rev - cost) / rev * 100
In HYBRID: =py.margin(A1, B1). In PYTHON: =margin(A1, B1).
EXCEL mode forbids code blocks entirely.
Cell references
$A$1 fixes both; $A1 fixes the column; A$1 fixes the row.
References adjust automatically on insert/delete/replicate.
Multi-sheet workbooks
:sheets Interactive picker: list sheets, select one to switch
:sheet List sheets inline (active marked *)
:sheet Inputs Switch by name
:sheet 1 Switch by zero-based index
:sheet add Outputs Append (does not switch)
:sheet del Tmp Remove (refused if last sheet)
:sheet rename Old New Rename, rewriting `Old!` prefixes in formulas
:sheet move Inputs 0 Reorder
A workbook with more than one sheet shows a tab strip on the bottom line
(active tab highlighted, with an i/n position counter); single-sheet
workbooks leave that line clear. The status bar also prefixes the active
sheet name (Inputs!A1) whenever a workbook has multiple sheets.
Reference cells on other sheets with Sheet!cell:
=Sheet2!A1
=SUM(Sheet2!A1:A10)
=Sheet1!A1 + Sheet2!B1
The dep graph is keyed on (sheet, col, row) so cross-sheet recalc
works transparently. Cross-sheet ranges (Sheet1!A1:Sheet2!B5) are
not supported (Excel doesn't either).
Optimization
:opt solves linear and mixed-integer programs defined by cells in the
active sheet, via a vendored copy of HiGHS (MIT).
Models are workbook-persistent: define once, save the file, re-run
on reopen.
:opt Run the saved 'default' model
:opt max|min (with a visual selection) Infer the model from the selected block
:opt max|min <cell> vars <cells> st <cells> [bounds <spec>] [int <cells>] [bin <cells>]
Solve inline AND save as 'default'
:opt def <name> max|min <cell> ... Save under <name>; does not execute
:opt run [<name>] Execute a saved model
:opt sens [<name>] [into[!] <cell>] Sensitivity report -- paged, or written into cells
:opt sweep <cell> <lo>:<hi> [steps] [<name>] Re-solve across a range of RHS values
:opt list List saved models
:opt undef <name> Remove a saved model
The model is sheet-resident: an objective formula in one cell,
decision-variable cells holding values, and constraint cells holding
comparison formulas like =A1+A2<=10. The constraint cells keep
evaluating during recalc, so the sheet shows live feasibility
(TRUE/FALSE) before and after the solve.
A worked example (also at examples/example_lp.json):
| A | B | C | D | |
|---|---|---|---|---|
| 3 | Decision | Objective | Constraints | |
| 4 | 0 |
=3*A4+5*A5 |
=A4<=4 |
|
| 5 | 0 |
=2*A5<=12 |
||
| 6 | =3*A4+2*A5<=18 |
:opt max B4 vars A4:A5 st D4:D6
Status bar shows opt: OPTIMAL obj=36; A4 and A5 become 2.0
and 6.0; u rolls back.
Quadratic objectives
Objectives may contain squared decision variables and cross terms --
=(A1-3)*(A1-3), =A1^2 + A2^2, =A1*A2, =2*A1^2 + 3*A1 -- which
covers least-squares fitting, quadratic cost curves, target tracking,
and covariance-style objectives.
:opt min C1 vars A1:A2 st D1
opt: OPTIMAL obj=0 (quadratic)
Solved exactly as a QP; there is no approximation and no accuracy knob.
The objective must be convex for a minimisation (or concave for a maximisation). Otherwise the optimum sits at a corner of the feasible region, which is a different and much harder problem, and gridcalc refuses it rather than returning a plausible wrong answer:
opt: objective is not convex, so it has no interior minimum -- ...
Convexity is checked directly on the Hessian (symmetric elimination, no numpy required), so the message names the real problem rather than surfacing a solver failure.
Sensitivity analysis and infeasibility diagnosis are withheld for quadratic models: their duals do not carry the shadow-price reading the report describes. Same call as for MIPs. Integer variables cannot be combined with a quadratic objective.
Inferring the model from a selection
The layout above already says what the model is. Select the block with
v, then type :opt max (or min) and the components are read off it:
| Cell contents | Read as |
|---|---|
formula rooted in a comparison (=A4<=4) |
a constraint |
any other formula (=3*A4+5*A5) |
the objective |
| a plain number | a decision variable |
| labels, blanks | ignored |
For the sheet above, selecting A3:D6 and typing :opt max is
equivalent to :opt max B4 vars A4:A5 st D4:D6.
Exactly one non-comparison formula must be in the selection; more than one is ambiguous and reports the candidates so you can narrow it. Blanks are deliberately not treated as decision variables -- a selected rectangle is mostly whitespace, and promoting every gap to a variable would build a model you never described.
The inferred model is saved as default, so the block only has to be
selected once: plain :opt re-runs it afterwards, and :w persists it.
Clauses (any order after st):
bounds A1=lo:hi, B2=lo:hi-- per-variable bounds.lo/hiacceptinf,+inf,-inf. Default is[0, +inf).int <cells>-- decision variables are integer-valued (branch-and-bound).bin <cells>-- decision variables are binary ({0,1}); bounds clamped to[0,1].
Cell lists everywhere accept ranges (A1:A5), comma-separated refs
(A1,A3,B5), or a mix.
Saved models live under "models": {<name>: ...} in the JSON file
and round-trip verbatim (the spec strings the user typed are stored,
not pre-resolved coords).
Sensitivity analysis
:opt sens [<name>] solves the model and then opens a report answering
the question a bare optimum does not: what would change the answer?
Variable cells
cell value reduced obj coef coef from coef till
A4 2 0 3 0 7.5
A5 6 0 5 2 inf
Constraints (* = binding)
cell shadow rhs activity slack rhs from rhs till
D4 0 4 2 2 -inf inf
* D5 1.5 12 12 0 6 18
* D6 1 18 18 0 12 24
- shadow price -- objective gain per extra unit of right-hand side.
D5is worth 1.5 per unit andD6is worth 1, so buying more of theD5resource pays better.D4has slack and is worth nothing. - rhs from/till -- the range over which that shadow price holds. Past it the optimal basis changes and the price no longer applies.
- reduced cost -- for a variable stuck at a bound, how much the objective would move per unit if it were forced in. Zero for any variable already active.
- coef from/till -- how far an objective coefficient can move before the optimal mix changes.
*-- the constraint is binding (zero slack). Derived from slack rather than from a non-zero shadow price, since a degenerate optimum can bind at a price of zero.
Sensitivity is not reported for integer or binary models: a
branch-and-bound dual describes one LP relaxation rather than the
integer problem, so there is no valid shadow-price reading. :opt sens
on such a model still solves it and says why the report is absent.
Writing the report into cells
:opt sens into <cell> writes the report into the sheet instead of
paging it, anchored at the given cell:
:opt sens into F1
The numbers land as values, not text, so downstream formulas can reference them:
F13: =G7*100 -> 150 (G7 holds a shadow price of 1.5)
That is the reason to write into cells rather than read a report: the results become part of the sheet's own computation. Re-running the command refreshes the block in place.
Layout, anchored at the target cell -- a blank row separates the two tables, and positions are stable so formulas keep working across re-runs:
Variables value reduced obj coef coef from coef till
<one row per decision variable>
Constraints shadow rhs activity slack rhs from rhs till
<one row per constraint>
The write refuses to overwrite non-empty cells and names the first
one blocking it; use into! to overwrite anyway. The whole rectangle
belongs to the report, including the separator row, so stray values
cannot end up sitting inside it. The write is a single undo step.
Unbounded ranging values are written as infinities and display as
inf / -inf.
Parametric sweep
A shadow price answers what is the next unit worth. It cannot answer
how much more should I buy, because it stops being valid at the edge
of its rhs from/till range. :opt sweep re-solves across a range and
shows where the value changes:
:opt sweep D5 6:24 9
D5 right-hand side from 6 to 24 (* = marginal value changed)
rhs objective delta shadow status
6 27 -- 2.5
* 8 30 3 1.5
12 36 3 1.5
18 45 3 1.5
* 20 45 0 0
24 45 0 0
Read that as: capacity is worth 1.5 per unit up to 18, and nothing at all beyond it. Buy up to 18.
steps is the number of intervals (default 10), so the report has
steps + 1 rows spanning the range inclusive. The optional trailing
name selects a saved model other than default.
The sweep is read-only -- each point substitutes the right-hand side internally rather than editing the constraint formula, so the sheet is untouched and there is nothing to undo. Points where the model becomes infeasible or unbounded are kept in the series with their status, since learning that a level is unattainable answers the question too.
Available programmatically as opt.sweep(...), and the underlying
substitution as solve(rhs_override={cell: value}) for one-off what-if
questions.
Infeasibility diagnosis
An infeasible model reports which constraints contradict each other, not just that the model failed:
opt: INFEASIBLE conflict: D1, D2 (2 of 5 constraints)
The named cells are an irreducible conflicting set: together they are still infeasible, and dropping any one of them makes the model solvable again. Constraints that merely happen to be present are not listed, which is the whole point -- narrowing 30 constraints to the 2 that actually fight is the difference between a dead end and a fix.
Found by a deletion filter (one solve per constraint, on the failure path only), so a three-way conflict with no contradictory pair is reported correctly where a pairwise check would miss it. Variable bounds are held fixed rather than dropped, so a constraint that contradicts its variable's bounds is reported as the conflict.
This runs automatically on every infeasible :opt; there is no
separate command.
Unboundedness diagnosis
The mirror case. An unbounded model names the variable that can run away, rather than only reporting that no optimum exists:
opt: UNBOUNDED unbounded: A5 -- add an upper bound or a constraint
A variable is reported when the constraints permit it to move without limit in the direction that improves the objective -- established by re-solving over the same feasible region with that variable as the objective, so it is an exact answer rather than a large-number heuristic. Variables with no objective coefficient are never blamed: moving them cannot change the objective, so they are not the cause even when they are themselves unbounded.
Like the infeasibility case, this runs automatically.
Programmatic access:
from gridcalc.engine import Grid
from gridcalc.opt import solve
g = Grid()
g.jsonload("examples/example_lp.json")
g.recalc()
r = solve(g, objective_cell=(1, 3), decision_vars=[(0, 3), (0, 4)],
constraint_cells=[(3, 3), (3, 4), (3, 5)], maximize=True)
print(r.status_name, r.objective, r.values)
Goal-seek
For 1-D what-if ("what input makes this output equal X?"), use :goal:
:goal <formula_cell> = <target> by <var_cell> [in <lo>:<hi>]
:goal B10 = 100 by A1 auto-bracket from A1's current value
:goal B10 = 0 by A1 in -50:50 explicit search bracket
Uses bisection over Grid.recalc(); converges in milliseconds at
spreadsheet scale. The variable cell must hold a value (not a formula).
On success the variable cell is overwritten; u rolls back. Unlike
:opt, goal-seek isn't persisted -- the three args fit on one line, so
retyping is faster than naming.
Formatting
:f b Toggle bold (also Ctrl-B)
:f u Toggle underline (also Ctrl-U)
:f i Toggle italic
:f bi Combine: bold + italic
:f $ Dollar (2 decimal places)
:f % Percentage (value*100, 2 decimals)
:f I Integer (truncate)
:f * Bar chart (asterisks proportional to value)
:f L | R | G | D Left / right / general / use-global-format
:f ,.2f Any Python format spec: 1,234.50
:f .1% 15.7%
:f .2e 1.23e+04
:gf <fmt> sets the workbook-wide default format. :width <n> sets
column width (4-40). Labels longer than the column width spill into
adjacent empty cells, Excel-style.
Import / export
| Command | Reads | Writes | Notes |
|---|---|---|---|
:csv save/load |
CSV | CSV | Plain text, fast |
:xlsx save/load |
.xlsx formulas + values |
EXCEL-mode: formulas + cached values; other modes: values only | :xlsx load switches to EXCEL |
:pd save/load |
CSV/TSV/Excel/JSON/Parquet | same | Uses pandas; row 1 as headers |
:xlsx load translates Excel formulas into gridcalc's EXCEL grammar
and reads every sheet. INDIRECT and 3D ranges (Sheet1:Sheet3!A1:B2)
are deliberately unsupported -- they'd defeat the static dep graph.
Functions outside the auto-loaded library produce #NAME?.
File format
JSON, v2. v1 (single sheet, top-level cells) still loads.
{
"version": 2,
"mode": "HYBRID",
"active": "Inputs",
"code": "def margin(rev, cost):\n return (rev - cost) / rev * 100\n",
"names": { "revenue": "A1:A12", "costs": "B1:B12" },
"models": { "default": { "sense": "max", "objective": "B4",
"vars": "A4:A5", "constraints": "D4:D6" } },
"sheets": [
{ "name": "Inputs", "cells": [["Rev","Cost"],[1000,600],[1200,700]] },
{ "name": "Summary","cells": [["Total","=SUM(Inputs!A2:A3)"]] }
],
"format": { "width": 10 }
}
- mode:
"EXCEL"|"HYBRID"|"PYTHON". Absent →PYTHON. - sheets (v2): each is
{name, cells}with a 2Dcellsarray. - active (v2): name of the sheet to focus on load.
- names: workbook-global named ranges (sheet-relative when used).
- models: persisted LP/MIP definitions (see Optimization).
- code: per-workbook Python module string, editable via
:e.
Configuration
Optional gridcalc.toml (lookup: $PWD then $XDG_CONFIG_HOME/gridcalc/):
sandbox = true # AST validation of formulas + code blocks
width = 12 # default column width
format = "G" # default cell format
[keys.grid]
next_sheet = ["Tab", "F4"]
prev_sheet = ["S-Tab", "F3"]
cursor_left = ["Left", "h"]
cursor_down = ["Down", "j"]
cursor_up = ["Up", "k"]
cursor_right= ["Right", "l"]
Every TUI context (grid, entry, visual, cmdline, search) is
rebindable. User bindings fire before the hardcoded fallback chain,
so Tab → next_sheet replaces the default cursor-right meaning. See
docs/keybindings.md for the keyspec grammar
(Tab, S-Tab, C-x, C-Right, F3, ...) and rejected combinations.
Command reference
File :w [file] :wq :q :q! :o file :e
Edit :b :clear :dr :dc :ir :ic :m :r
:sort [col] [desc] yank/paste: y/p (syncs system clipboard)
undo/redo: u / Ctrl-R (aliases: Ctrl-Z / Ctrl-Y)
:recalc (or !) recompute every formula
Format :f <spec> :gf <spec> :width <n> Ctrl-B / Ctrl-U
Search /pattern n N
Sheets :sheets (picker) :sheet [name|N|add|del|rename|move]
Names :name <n> [range] :names :unname <n>
Modes :mode [excel|hybrid|python]
Import/export :csv save/load :xlsx save/load :pd save/load
Optimization :opt :opt def :opt run :opt sens :opt sweep
:opt list :opt undef
:goal <cell> = <target> by <cell> [in <lo>:<hi>]
View :view E :title <v|h|b|n> (aliases :tv/:th/:tb/:tn)
Limitations
INDIRECTis unsupported (would defeat the static dep graph).LAMBDAand its higher-order helpers (MAP,REDUCE,BYROW, ...) are unsupported;LETis supported. Dynamic-array results are packed into their origin cell rather than spilling into neighbours.- xlsx export of formulas is EXCEL-mode only -- PYTHON/HYBRID
syntax (
**, list comprehensions,py.*) isn't strict Excel. - 3D range refs (
Sheet1:Sheet3!A1:B2) are unsupported (returnsnan). Workaround: expand manually with+. - Cross-sheet ranges (
Sheet1!A1:Sheet2!B5) are rejected at parse time -- Excel doesn't support them either. - xlsx dates and styles aren't read or written; date serials arrive as floats.
Development
make build # rebuild the C++ extensions (_core, _opt)
make test # unit tests
make test-tty # PTY-driven curses integration tests (slow, requires xterm-256color)
make lint # ruff check
make typecheck # mypy
make qa # lint + typecheck + test + format (Python and TypeScript)
make web-build # compile the desktop app's React client into web/static/
make web-run # launch the desktop app (needs web-build first)
make web-dev # Vite dev server with HMR, against a mock bridge
make web-jstest # vitest suite for the client
make test-web # Playwright tests driving the built bundle in headless Chromium
make wheel # cpXX-cpXX wheel for current Python
make wheel-abi3 # single cp312-abi3 wheel (Python>=3.12)
make sdist # source distribution
make publish # upload to PyPI (after make check)
The abi3 build is gated on GRIDCALC_STABLE_ABI=ON (CMake) +
wheel.py-api=cp312 (scikit-build-core). Per-version wheels and the
abi3 wheel have separate CI workflows under .github/workflows/.
Prior Art
- sc-im: A ncurses spreadsheet program for terminal
- sheets: A terminal based spreadsheet tool
- rustxl: A fast, keyboard-driven spreadsheet with vim-style navigation and Excel-compatible formulas.
License
MIT
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distributions
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 gridcalc-0.3.2.tar.gz.
File metadata
- Download URL: gridcalc-0.3.2.tar.gz
- Upload date:
- Size: 2.8 MB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/6.2.0 CPython/3.13.2
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
3717381e14b77d8029bf81d8ba269f8eafd8417b4546033bdf1bac7f2e0de4ba
|
|
| MD5 |
7d732e2ebb6ada78f338fe04315fad3f
|
|
| BLAKE2b-256 |
fa6e9755de8aa378f568dd7d04a08b028f5775575390bf54b09c88f7da77da63
|
File details
Details for the file gridcalc-0.3.2-cp313-cp313-win_amd64.whl.
File metadata
- Download URL: gridcalc-0.3.2-cp313-cp313-win_amd64.whl
- Upload date:
- Size: 2.8 MB
- Tags: CPython 3.13, Windows x86-64
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/6.2.0 CPython/3.13.2
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
4abb63726207e0e56a4723f53ed6f8ba45ec4146d9f1026d83a7034cdc1c2bc1
|
|
| MD5 |
66c788cab5585b5959fde0241d733afe
|
|
| BLAKE2b-256 |
dffb80940a2a9fc263c29762a75ec2afcea4661aa622b4f9ba3c727830b80ba8
|
File details
Details for the file gridcalc-0.3.2-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl.
File metadata
- Download URL: gridcalc-0.3.2-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl
- Upload date:
- Size: 3.7 MB
- Tags: CPython 3.13, manylinux: glibc 2.27+ x86-64, manylinux: glibc 2.28+ x86-64
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/6.2.0 CPython/3.13.2
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f108a75d464416d4e2bbd554bea285e9b9e8958b214560fe7abeb5801cd8f6d1
|
|
| MD5 |
728d7de4428911b4eb0e019f43adbc55
|
|
| BLAKE2b-256 |
078e81c00882e714732a8ae2bf725f1695b10f95651c0a8c86370a00048f6f87
|
File details
Details for the file gridcalc-0.3.2-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl.
File metadata
- Download URL: gridcalc-0.3.2-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl
- Upload date:
- Size: 3.4 MB
- Tags: CPython 3.13, manylinux: glibc 2.26+ ARM64, manylinux: glibc 2.28+ ARM64
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/6.2.0 CPython/3.13.2
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
96fd24f27fa8ec84395ca3333b5de5aca1e136169dae99f60695a9e367dbd9d1
|
|
| MD5 |
f42b50353984e6686fd442f8999ce15e
|
|
| BLAKE2b-256 |
070b7ce79eef7d4e2c5f6034f8a57f082ace92b1821e502a942e41edc800f820
|
File details
Details for the file gridcalc-0.3.2-cp313-cp313-macosx_11_0_arm64.whl.
File metadata
- Download URL: gridcalc-0.3.2-cp313-cp313-macosx_11_0_arm64.whl
- Upload date:
- Size: 2.6 MB
- Tags: CPython 3.13, macOS 11.0+ ARM64
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/6.2.0 CPython/3.13.2
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
86cd05ed841fa331cbdce4f31af2de64992ea10099459125338b342120fe33bd
|
|
| MD5 |
8e467d07daca4708c9f8b6f2cfa22fa0
|
|
| BLAKE2b-256 |
8b39ebae4fc3a8654385febed954dad1bf14bfb07831b51be7e5645838d6a2d4
|
File details
Details for the file gridcalc-0.3.2-cp313-cp313-macosx_10_15_x86_64.whl.
File metadata
- Download URL: gridcalc-0.3.2-cp313-cp313-macosx_10_15_x86_64.whl
- Upload date:
- Size: 2.8 MB
- Tags: CPython 3.13, macOS 10.15+ x86-64
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/6.2.0 CPython/3.13.2
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
3e1649e5fdb7ee9a49378503fab7906ac1a9f92dd05db6fb2bb69a6a5c28a447
|
|
| MD5 |
4a9a45eea64dc1a65ae12955b6ac6ce9
|
|
| BLAKE2b-256 |
5dc89b9d22c0ee269ea1714beecd58f35112c81dd20e884d057a0077d5992f46
|
File details
Details for the file gridcalc-0.3.2-cp312-cp312-win_amd64.whl.
File metadata
- Download URL: gridcalc-0.3.2-cp312-cp312-win_amd64.whl
- Upload date:
- Size: 2.8 MB
- Tags: CPython 3.12, Windows x86-64
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/6.2.0 CPython/3.13.2
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a416adbb84c8064146d3e3c757b897fb4c4fb2c15a3b3f4bcd3e9366182f9155
|
|
| MD5 |
715ede7f481b08e1e85886c2c34db0d7
|
|
| BLAKE2b-256 |
6760469937609e928b18d70df44993903c725db4d9cb759f8702e2892e3b38fa
|
File details
Details for the file gridcalc-0.3.2-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl.
File metadata
- Download URL: gridcalc-0.3.2-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl
- Upload date:
- Size: 3.7 MB
- Tags: CPython 3.12, manylinux: glibc 2.27+ x86-64, manylinux: glibc 2.28+ x86-64
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/6.2.0 CPython/3.13.2
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d5af3a049af3eb3c4b5ccaa3a8c4f9833a41b4fd0314ebe340d7d4103c97f70e
|
|
| MD5 |
eadbcb3958437408ff993135df15a3b9
|
|
| BLAKE2b-256 |
01c4cfd465e8a0a8d220923612e2a28fd2561b242eedff6f634ea74545260c63
|
File details
Details for the file gridcalc-0.3.2-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl.
File metadata
- Download URL: gridcalc-0.3.2-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl
- Upload date:
- Size: 3.4 MB
- Tags: CPython 3.12, manylinux: glibc 2.26+ ARM64, manylinux: glibc 2.28+ ARM64
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/6.2.0 CPython/3.13.2
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
6ebd2f8f90cf4bb88e29f61c7ff4d20d368c90c05ba5a3c12c5990c7939d0cd3
|
|
| MD5 |
b7268b5a6e392d9b79283ed97b65f6c2
|
|
| BLAKE2b-256 |
8e8a1085dfcfbd07c02d48f6714d2a33b9e614f863cb027eb165d2bf32b4f6e5
|
File details
Details for the file gridcalc-0.3.2-cp312-cp312-macosx_11_0_arm64.whl.
File metadata
- Download URL: gridcalc-0.3.2-cp312-cp312-macosx_11_0_arm64.whl
- Upload date:
- Size: 2.6 MB
- Tags: CPython 3.12, macOS 11.0+ ARM64
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/6.2.0 CPython/3.13.2
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
0a9af457399406853bbcc5a3195c081cff0ee3601c570c5a17c84cf008fc69d4
|
|
| MD5 |
adc09b8770d5527fb8772fd9e6f2d13a
|
|
| BLAKE2b-256 |
a92590f8bbb09db4b2dae6d60b15b44ec8ef15ef7d36b798fb2dd7a6269f06e7
|
File details
Details for the file gridcalc-0.3.2-cp312-cp312-macosx_10_15_x86_64.whl.
File metadata
- Download URL: gridcalc-0.3.2-cp312-cp312-macosx_10_15_x86_64.whl
- Upload date:
- Size: 2.8 MB
- Tags: CPython 3.12, macOS 10.15+ x86-64
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/6.2.0 CPython/3.13.2
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f932c576928dd43d34ea662f6e90d825a7963f9c8c65827b868ace547a135366
|
|
| MD5 |
be270ea0ef8ce8724b5437c603516f15
|
|
| BLAKE2b-256 |
cd9f65efacab78297b89913c8946dbe6a67589470372ab473fee6e334486d8ed
|
File details
Details for the file gridcalc-0.3.2-cp312-abi3-win_amd64.whl.
File metadata
- Download URL: gridcalc-0.3.2-cp312-abi3-win_amd64.whl
- Upload date:
- Size: 2.8 MB
- Tags: CPython 3.12+, Windows x86-64
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/6.2.0 CPython/3.13.2
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
61b0ca8bce63bb2203b42eac3dfb87aec35f940726fbe411fad0005f48b8e01a
|
|
| MD5 |
60b40be86fad619f50e4fc707e110908
|
|
| BLAKE2b-256 |
0a0962a43ddbe7311177060b26c437651c7858b6b836abb362cd31294409a310
|
File details
Details for the file gridcalc-0.3.2-cp312-abi3-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl.
File metadata
- Download URL: gridcalc-0.3.2-cp312-abi3-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl
- Upload date:
- Size: 3.6 MB
- Tags: CPython 3.12+, manylinux: glibc 2.27+ x86-64, manylinux: glibc 2.28+ x86-64
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/6.2.0 CPython/3.13.2
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c0b6c84c46e9d27eef29e9d77b0e6b65802ac6428a964dc3d9491c69963b9b9e
|
|
| MD5 |
4bc3c2ae7de2e57bc28df8eecc7b9852
|
|
| BLAKE2b-256 |
457e6e4adad86e0322ab324cbd2b12bccd643c9b28082f99a04dd28b7985d087
|
File details
Details for the file gridcalc-0.3.2-cp312-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl.
File metadata
- Download URL: gridcalc-0.3.2-cp312-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl
- Upload date:
- Size: 3.4 MB
- Tags: CPython 3.12+, manylinux: glibc 2.26+ ARM64, manylinux: glibc 2.28+ ARM64
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/6.2.0 CPython/3.13.2
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
312a5856d51a7c3300644d5a9887707711e9b7b62714811d7e6d2fce2ff073e9
|
|
| MD5 |
fce5ae6714ccafc34dc30606bdb41624
|
|
| BLAKE2b-256 |
a5d224f69b80decfced4eca98dd29458a7b28045cbd05d5428719c751ccee2f0
|
File details
Details for the file gridcalc-0.3.2-cp312-abi3-macosx_11_0_arm64.whl.
File metadata
- Download URL: gridcalc-0.3.2-cp312-abi3-macosx_11_0_arm64.whl
- Upload date:
- Size: 2.6 MB
- Tags: CPython 3.12+, macOS 11.0+ ARM64
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/6.2.0 CPython/3.13.2
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
3f75a77a29f168cc2d30a349967ff80909d5ee99e5ec06aa8a6d3a2222761adb
|
|
| MD5 |
3391a18261c0729b34c31bc0a02a0521
|
|
| BLAKE2b-256 |
7efcc4e72011645778bd08e85150169b5b43e588e03739e49ebdec0e9c02b1a7
|
File details
Details for the file gridcalc-0.3.2-cp312-abi3-macosx_10_15_x86_64.whl.
File metadata
- Download URL: gridcalc-0.3.2-cp312-abi3-macosx_10_15_x86_64.whl
- Upload date:
- Size: 2.8 MB
- Tags: CPython 3.12+, macOS 10.15+ x86-64
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/6.2.0 CPython/3.13.2
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
da7a74d5ab38c79cbe9648b5637fb50d5ca30c32e278871d52148aa986d7b6ee
|
|
| MD5 |
93cba108d30c8518831b7f2c12614255
|
|
| BLAKE2b-256 |
39ede033749af383abc5ce9addb73b505f9ee2ef97106e4b51cdc3d0b8d45506
|
File details
Details for the file gridcalc-0.3.2-cp311-cp311-win_amd64.whl.
File metadata
- Download URL: gridcalc-0.3.2-cp311-cp311-win_amd64.whl
- Upload date:
- Size: 2.8 MB
- Tags: CPython 3.11, Windows x86-64
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/6.2.0 CPython/3.13.2
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
8715141b0f843b576d4469f2e0471a926a6da8b110b17dbf5a9e52d81722c5d2
|
|
| MD5 |
6547c2dd93e22ae245a8968f2b5c0157
|
|
| BLAKE2b-256 |
33152f55356d966ab2613d638b6161db8c76b0064dff2400f0324c7f35816778
|
File details
Details for the file gridcalc-0.3.2-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl.
File metadata
- Download URL: gridcalc-0.3.2-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl
- Upload date:
- Size: 3.7 MB
- Tags: CPython 3.11, manylinux: glibc 2.27+ x86-64, manylinux: glibc 2.28+ x86-64
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/6.2.0 CPython/3.13.2
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c28fedc31f1941f5d40f84a012d3135ca0fd325aaf1ba735cf156d4eb814dd0b
|
|
| MD5 |
e087ef94cfc0a3c9a7e71ae50de51a90
|
|
| BLAKE2b-256 |
1b73b2cca07698a10eac272987e24a3de8164596bb6ee004697dd2d9dda9a855
|
File details
Details for the file gridcalc-0.3.2-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl.
File metadata
- Download URL: gridcalc-0.3.2-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl
- Upload date:
- Size: 3.4 MB
- Tags: CPython 3.11, manylinux: glibc 2.26+ ARM64, manylinux: glibc 2.28+ ARM64
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/6.2.0 CPython/3.13.2
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
7ed023f9315a45305cda9cbc0e089b9069a52f16e570ff018915fcbded67efae
|
|
| MD5 |
fed85238213cc0287874e84923e22efd
|
|
| BLAKE2b-256 |
3fa9c458e527c4a47959cb7df5328549ee0be2fff58390999b0645351233ebef
|
File details
Details for the file gridcalc-0.3.2-cp311-cp311-macosx_11_0_arm64.whl.
File metadata
- Download URL: gridcalc-0.3.2-cp311-cp311-macosx_11_0_arm64.whl
- Upload date:
- Size: 2.6 MB
- Tags: CPython 3.11, macOS 11.0+ ARM64
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/6.2.0 CPython/3.13.2
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
94d20a518431b84f471c6b3cd672142b4d724331bd4f1a946a46a7b3467eacfc
|
|
| MD5 |
c0f39c1c546ad8ffac829416d40feddb
|
|
| BLAKE2b-256 |
7cbee3b1f9714ab92bff0529a2d1b9f9a4ac581c3788cfd24e82682020115125
|
File details
Details for the file gridcalc-0.3.2-cp311-cp311-macosx_10_15_x86_64.whl.
File metadata
- Download URL: gridcalc-0.3.2-cp311-cp311-macosx_10_15_x86_64.whl
- Upload date:
- Size: 2.8 MB
- Tags: CPython 3.11, macOS 10.15+ x86-64
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/6.2.0 CPython/3.13.2
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b72feced6cdc0873f164ce0bab68582df281bf8f2c4e4219d3d3d87a53ff75ca
|
|
| MD5 |
45725c0a05de3d6fae3e2086105d86b8
|
|
| BLAKE2b-256 |
19333087fedcfffc85fc3470c64c39179045e6c93e08cc597d310fa7466d26c6
|
File details
Details for the file gridcalc-0.3.2-cp310-cp310-win_amd64.whl.
File metadata
- Download URL: gridcalc-0.3.2-cp310-cp310-win_amd64.whl
- Upload date:
- Size: 2.8 MB
- Tags: CPython 3.10, Windows x86-64
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/6.2.0 CPython/3.13.2
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
4af47bc5d7f29441c9f8cb1b54379903433ce5ae6605cb64567febdca26337b1
|
|
| MD5 |
e66e59a8372ad7eb0a0b1b1c44328fe3
|
|
| BLAKE2b-256 |
76c2f26edcfdbe7422191bb40bd8c59e84538dfc03999c3ea4b236075f5c3612
|
File details
Details for the file gridcalc-0.3.2-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl.
File metadata
- Download URL: gridcalc-0.3.2-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl
- Upload date:
- Size: 3.7 MB
- Tags: CPython 3.10, manylinux: glibc 2.27+ x86-64, manylinux: glibc 2.28+ x86-64
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/6.2.0 CPython/3.13.2
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b9f26a3de3612310049f4559387c45623cb925f5fa72ce8e699b7446a1538a1f
|
|
| MD5 |
be0a42c3afae72c0ee82c73847074f04
|
|
| BLAKE2b-256 |
d59cb08168b3a502f6ae2566d6061b734359fb6c860d35927a0c7e1a60a4334a
|
File details
Details for the file gridcalc-0.3.2-cp310-cp310-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl.
File metadata
- Download URL: gridcalc-0.3.2-cp310-cp310-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl
- Upload date:
- Size: 3.4 MB
- Tags: CPython 3.10, manylinux: glibc 2.26+ ARM64, manylinux: glibc 2.28+ ARM64
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/6.2.0 CPython/3.13.2
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c37392fe33fb7d42b804df97afdfcd38b6ba854f2eac0fec4ffec61044644a2f
|
|
| MD5 |
9ddbf82647093a9c2593062b5c637076
|
|
| BLAKE2b-256 |
d8382f5b72f9ca1a7ce43dbc5d14af270fc37d4d9aba9bcef3a48515abfbdf27
|
File details
Details for the file gridcalc-0.3.2-cp310-cp310-macosx_11_0_arm64.whl.
File metadata
- Download URL: gridcalc-0.3.2-cp310-cp310-macosx_11_0_arm64.whl
- Upload date:
- Size: 2.6 MB
- Tags: CPython 3.10, macOS 11.0+ ARM64
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/6.2.0 CPython/3.13.2
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
2bef3ac00d286a835254d684c53b815dd79f2497ee1f98753afbf809e074d026
|
|
| MD5 |
842e76b74b6ae1fee98deadb66be029b
|
|
| BLAKE2b-256 |
f8713df7af5a171e90b6612f93d900daad94306d133d777e02f94700a1ab6b24
|
File details
Details for the file gridcalc-0.3.2-cp310-cp310-macosx_10_15_x86_64.whl.
File metadata
- Download URL: gridcalc-0.3.2-cp310-cp310-macosx_10_15_x86_64.whl
- Upload date:
- Size: 2.8 MB
- Tags: CPython 3.10, macOS 10.15+ x86-64
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/6.2.0 CPython/3.13.2
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
92b0c546c8b5726fbfb9178881a1f345c07fc3947d48ca22edbf8a6462abcafe
|
|
| MD5 |
f87a1ff491ebb219f3e88b06a95defcd
|
|
| BLAKE2b-256 |
78f9088310fda66526e4c0c5e431759b88ac5381d3b9689d87443c0c9c6d2968
|