#SHEETSHOW!

Hex.pmDocsLicense

Google Sheets from Elixir, as values. A spreadsheet is a collection of cells; pure functions turn what you want into a plan, and one function at the edge of your application carries the plan out. On top of that sits a small database, an append-only log or a table of rows with ids, for the times a tab is the right place to keep something. The same code runs against .xlsx files, on disk or on a WebDAV server.

Install

def deps do
[{:sheetshow, "~> 0.1.0"}]
end

No dependencies. Elixir 1.18 or later, for the built-in JSON; everything else comes from OTP.

The first thing that works

costs = [%{item: "Rent", cost: 1000}, %{item: "Food", cost: 400}]
cells =
Sheetshow.stack([
Sheetshow.row(["Item", "Cost"], style: %{bold: true}),
Sheetshow.records(costs, [:item, :cost])
])
|> Sheetshow.put_sheet("Costs")
{:ok, account} = Sheetshow.ServiceAccount.from_file("service-account.json")
{:ok, workbook} = Sheetshow.connect(Sheetshow.Workbook.google(id, credentials: account))
cells
|> Sheetshow.plan!(existing_sheets: Sheetshow.Workbook.titles(workbook))
|> Sheetshow.run(workbook)

plan!/2 builds a value; run/2 is the only thing there that writes anything, and it is one request whatever the plan holds. Swap Workbook.google/2 for Workbook.xlsx("costs.xlsx", create: true) and none of the rest changes.

The two quick starts walk the whole library end to end (cells, the two reads, a log and a table) against a Google spreadsheet and against an .xlsx file, which needs no account and no network. Every snippet on both pages is run by the test suite.

What this is

Values until the edge. Cells, plans, schemas, credentials and tokens are structs and maps. Pure functions turn what you want into a plan; one function carries the plan out. You test spreadsheet logic by asserting on plans, with nothing mocked and nothing stubbed.

No runtime ownership. Sheetshow defines no GenServer, supervisor, registry, pool or application callback module, and reads no configuration. Token caching, retries, backoff and scheduling belong to your application, which is the only thing that knows what they should be.

No macros. A schema is a keyword list you pass to functions. A query is Enum. There is nothing to use.

A database, with the caveats printed. A plan is applied whole or not at all; nothing between a read and a write is, because the Sheets API has no conditional write, so the library makes re-reading cheap instead of pretending the gap is closed. Where a backend cannot keep a promise it says so, in Sheetshow.Backend.capabilities/1, rather than letting you find out later. What Sheetshow Can Promise is the whole argument.

What's in it

Cells and plans. Coordinates, inclusive ranges and A1 parsing; values including formulas, dates and times; styles; and layout functions that build rows, columns, tables and records and stack them next to and below each other. Sheetshow.plan/2 turns cells into ops, run/2 carries them out, and read_cells/2 and read_rows/2 are the two reads: cells with everything about them, or plain rows at about a twelfth the bytes.

A database on a tab.Sheetshow.Log is append-only: an update is the same id appended again, a delete is the same id with a flag, and fold/1 turns the history back into state. Because Google resolves an append server-side, several writers need no compare-and-swap between them, and a failed write is safe to retry. Sheetshow.Table is the mutable one, where row 7 is the record: read a snapshot, build changes that name rows by id, refresh/2 to find them again, then plan and run. Both share Sheetshow.Schema, which is a keyword list.

Backends. Google Sheets, an in-memory one that doubles as the test double, and .xlsx, over a local file or over WebDAV, where the store gives you the conditional write Google will not. Foreign workbooks survive a round trip: charts, pivot tables, merged cells, conditional formatting and macros are copied through untouched.

Credentials. Service accounts and user OAuth, both reduced to one token type that nothing downstream can tell apart. Sheetshow.OAuth builds the consent URL and reads the answer; running a loopback listener is your application's job, not the library's.

What it costs. Google allows 60 reads and 60 writes a minute per user, and one request is one unit however many ops it carries, so batching is the only lever, and you can count a cycle's requests at the call site. The numbers are in the guide.

Where to go next

Setting Up Google is the service account, the shared spreadsheet, and user sign-in when a robot will not do. The cookbook is recipes for the things people actually do, and Google Sheets instead of Postgres is the case for using a spreadsheet as the database, made properly, with the walls other people hit. What Sheetshow Can Promise is the reference for atomicity, the missing conditional write and the quota; the cheatsheet is every public function on one page; and usage-rules.md is the short set of rules that are not guessable from the names, for an agent's context as much as a person's.

Why this and not something else

google_api_sheets is Google's own client, generated from the API specification. It is complete and it is the right answer when you want the Sheets API itself: everything is there, and so is every concept Google has, expressed as generated structs.

elixir_google_spreadsheets is the established convenience layer. It supervises a process per spreadsheet and reads and writes a row at a time, GSS.Spreadsheet.read_row(pid, 1), which is a comfortable shape until the quota notices, since a row is a request.

Sheetshow is for the case where the spreadsheet is part of your application's design rather than an export target: where you want the write to be a value you can test, the request count to be something you decide rather than discover, and a tab to be usable as a small database without giving up on knowing what it does and does not guarantee. It owns no processes and takes no dependencies, and the same cells go to an .xlsx file when the spreadsheet stops being the point.

If you want the full API surface, take Google's client. If you want a row at a time and don't mind the process, take GSS. This is the third option.

Development

mix check # format, compile with warnings as errors, offline tests
mix check.all # also the integration tests against a real spreadsheet
mix docs # the documentation, as it appears on HexDocs
mix run dev/cheatsheet.exs --write # regenerate guides/cheatsheet.cheatmd

The guides are run, not read: every elixir block in guides/ is executed by the tests, in order, and its pattern matches are the assertions. The cheatsheet is generated from the compiled modules, and a test fails when it drifts.

ex_doc is the only entry in deps, and it is only: :dev, runtime: false: nothing ships with Sheetshow, and nothing is loaded at runtime.

How this was built

Claude (Anthropic) wrote the overwhelming majority of the code, the tests and the documentation. The maintainer set the scope, made the design calls, ran every gate and reviewed the result. That division is worth stating plainly rather than leaving to be guessed at.

License

Apache License 2.0. See LICENSE.

%Sheetshow{acid: :free, gsm: 80, sheets: 500, last_write: :wins}