Unofficial read-only Python clients for municipal government software platforms
Project description
libmuni
Unofficial read-only Python clients for municipal government software platforms. A municipality's public record is spread across several vendor platforms, and libmuni provides one small, typed, well-behaved client per platform. The clients share no cross-vendor data model — what unifies them is the jurisdiction rather than a schema.
The first platform is CivicClerk (libmuni.civicclerk), a meeting-portal
system used by many municipalities to publish meeting calendars, agendas,
minutes, and supporting documents. Those portals are backed by a public,
unauthenticated API at https://{tenant}.api.civicclerk.com/v1; the client
reads that data — meeting calendars, attendance details, structured agendas,
and document downloads — without reverse-engineering the API yourself.
Install
pip install libmuni
Requires Python 3.11+. The only runtime dependency is httpx.
Quickstart
The tenant name is the subdomain of a municipality's CivicClerk portal —
the exampletown in exampletown.portal.civicclerk.com.
import datetime as dt
from pathlib import Path
from libmuni.civicclerk import CivicClerkClient
with CivicClerkClient("exampletown") as client:
# Boards and committees
for category in client.get_event_categories():
print(category.id, category.name)
# This month's meetings for one board
events = list(
client.iter_events(
category_id=12,
from_date=dt.date.today(),
to_date=dt.date.today() + dt.timedelta(days=31),
)
)
for event in events:
print(event.starts_at, event.name)
print(" attend:", event.location, event.virtual_meeting_url)
print(" recording:", event.recording_url)
# The structured agenda of the next meeting that has one
event = next(e for e in events if e.agenda_id)
agenda = client.get_agenda(event.agenda_id)
for item in agenda.walk_items():
print((" " if item.is_section else " ") + (item.name or ""))
for attachment in item.attachments:
client.download_attachment(attachment, Path(attachment.file_name or "file.pdf"))
# Meeting-level documents (agenda PDF, packet, minutes)
for file in event.published_files:
client.download_file(file.file_id, Path(f"{file.file_id}.pdf"))
text = client.get_file_text(file.file_id) # best-effort, may be None
Every model keeps the raw API object in .raw, so fields the library
doesn't model yet are still reachable.
Command line
The package installs a muni-cli command that routes to a subcommand per
platform. muni-cli civicclerk demo exampletown narrates a tour of the whole
CivicClerk surface, and the other subcommands (categories, events,
agenda, text, download) mirror the client methods, with --json
exposing raw payloads.
Datetimes are local wall-clock time
The API stamps a Z (UTC) suffix on datetimes that are actually local
wall-clock times, and nothing in the payload declares the tenant's
timezone. The library therefore returns naive datetimes carrying the
local reading (a 7 pm meeting is 19:00, whatever the timezone). If you
need aware datetimes, attach the municipality's timezone yourself.
Errors
Every operational error subclasses CivicClerkError:
UnknownTenantError— the tenant subdomain doesn't exist (typo?)NotFoundError— an id-parameterized lookup (agenda, file) has no dataAPIStatusError— any other HTTP error statusNetworkError— DNS/connection/timeout failuresPayloadError— a response body the library can't use
Two Python built-ins stay outside that hierarchy by design: ValueError
for an invalid tenant or argument, and OSError for a failed local file
write.
Absent optional data is never an error: an event with no published
materials has an empty published_files, not an exception.
Documentation
The cheat sheet is the fastest way into the full documentation — the whole public surface, the traps, and what the library deliberately does not do.
Using an LLM or coding agent? Hand it llms-full.txt, which is the entire documentation set — signatures included — as a single file. An llms.txt index is served alongside it in the llms.txt format.
Versioning
Semantic versioning covers this library's surface only. The upstream CivicClerk API is undocumented and makes no compatibility promises; every model keeps the raw response payload reachable so upstream additions are usable immediately, and upstream drift is addressed in library releases.
Drift is detected by an opt-in live contract suite: set
CIVICCLERK_TEST_TENANT=<tenant> and run pytest -m live (deselected by
default; suitable for a scheduled CI job).
Development
uv sync # install with dev tools
uv run pytest # tests (live suite excluded)
uvx prek run --all-files # lint, format, type check (what CI enforces)
uv run --group docs mkdocs serve # preview the docs site locally
Documentation lives at
chapinb.com/libmuni.
Pushing a v* tag deploys that version's docs with a version selector
(GitHub Pages must be set to serve the gh-pages branch once).
Disclaimer
This project is not affiliated with, endorsed by, or supported by CivicPlus
or the CivicClerk product. "CivicClerk" is used only to describe the API this
library talks to. The library reads publicly available government records
from small municipal servers, and paces its requests politely by default
(at most ~1 request/second; see min_request_interval to tune or disable).
Any redistribution of downloaded documents is the responsibility of
downstream applications.
Acceptable use. The portals are governed by the platform vendor's and each jurisdiction's terms of use, which bind anyone accessing them and can change at any time — this library neither reproduces nor tracks them. They commonly restrict automated access and cap total requests over a window; the default pacing bounds the instantaneous rate but not total volume. Read the terms for the portals you query and keep your usage within them. See Acceptable use for details.
License
MIT
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
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 libmuni-0.1.0.tar.gz.
File metadata
- Download URL: libmuni-0.1.0.tar.gz
- Upload date:
- Size: 25.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
18d6fd5c787caaccbcce6aa163a7a02e58d2e4590d4ab95bc757074b07bf3938
|
|
| MD5 |
9051b02ad01724937e4ebb58c20d8593
|
|
| BLAKE2b-256 |
41fe3293985341058ef924f3f38a8292af394dac333530e37a200184748bafd6
|
Provenance
The following attestation bundles were made for libmuni-0.1.0.tar.gz:
Publisher:
release.yml on chapinb/libmuni
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
libmuni-0.1.0.tar.gz -
Subject digest:
18d6fd5c787caaccbcce6aa163a7a02e58d2e4590d4ab95bc757074b07bf3938 - Sigstore transparency entry: 2194703878
- Sigstore integration time:
-
Permalink:
chapinb/libmuni@176fadd3db80d30154f2617aefd505f1f7b7fbea -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/chapinb
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@176fadd3db80d30154f2617aefd505f1f7b7fbea -
Trigger Event:
push
-
Statement type:
File details
Details for the file libmuni-0.1.0-py3-none-any.whl.
File metadata
- Download URL: libmuni-0.1.0-py3-none-any.whl
- Upload date:
- Size: 30.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
21cf39a35ec2d69cb49f8eb18251e24d2e9ec46e465ccedaf43c94d3bcf414a2
|
|
| MD5 |
b435811696ba3a453197c62066b0d646
|
|
| BLAKE2b-256 |
f4679d51a8bc7376be0656356f40ede309f2528463d498cc5f737ee24d7a7d6a
|
Provenance
The following attestation bundles were made for libmuni-0.1.0-py3-none-any.whl:
Publisher:
release.yml on chapinb/libmuni
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
libmuni-0.1.0-py3-none-any.whl -
Subject digest:
21cf39a35ec2d69cb49f8eb18251e24d2e9ec46e465ccedaf43c94d3bcf414a2 - Sigstore transparency entry: 2194703890
- Sigstore integration time:
-
Permalink:
chapinb/libmuni@176fadd3db80d30154f2617aefd505f1f7b7fbea -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/chapinb
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@176fadd3db80d30154f2617aefd505f1f7b7fbea -
Trigger Event:
push
-
Statement type: