Metadata-Version: 2.4
Name: baltimore-camden
Version: 0.9.2
Summary: Python toolkit for the Baltimore City Mayor's Office of Performance and Innovation (OPI).
Keywords: baltimore,civic-tech,microsoft-graph,sharepoint,open-data
Author: Baltimore City Office of Performance and Innovation
License-Expression: MIT
License-File: LICENSE
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: Science/Research
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Scientific/Engineering :: GIS
Classifier: Typing :: Typed
Requires-Dist: httpx>=0.28
Requires-Dist: msal>=1.37
Requires-Dist: pandas>=3.0
Requires-Dist: pyarrow>=25.0.0
Requires-Dist: pydantic>=2.13
Requires-Dist: pydantic-settings>=2.14
Requires-Dist: baltimore-camden[db,excel,geo,postgres] ; extra == 'all'
Requires-Dist: pyodbc>=5.3 ; extra == 'db'
Requires-Dist: sqlalchemy>=2.0 ; extra == 'db'
Requires-Dist: openpyxl>=3.1 ; extra == 'excel'
Requires-Dist: geopandas>=1.1 ; extra == 'geo'
Requires-Dist: psycopg[binary]>=3.3.4 ; extra == 'postgres'
Requires-Dist: sqlalchemy>=2.0 ; extra == 'postgres'
Requires-Python: >=3.13
Project-URL: Homepage, https://github.com/city-of-baltimore/camden
Project-URL: Issues, https://github.com/city-of-baltimore/camden/issues
Provides-Extra: all
Provides-Extra: db
Provides-Extra: excel
Provides-Extra: geo
Provides-Extra: postgres
Description-Content-Type: text/markdown

# Camden

**Platform baseline:** `baltimore-patapsco==0.12.1`

**Baltimore's civic data SDK.**

> _Named for Camden Yards, the Inner Harbor rail-and-ballpark hub where Baltimore's traffic gathers and arrives — just as this SDK gathers the city's data and delivers it with provenance._

[![CI](https://github.com/city-of-baltimore/camden/actions/workflows/ci.yml/badge.svg)](https://github.com/city-of-baltimore/camden/actions/workflows/ci.yml)
[![Python 3.13+](https://img.shields.io/badge/python-3.13%2B-blue.svg)](https://www.python.org/downloads/)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
[![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
[![uv](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/uv/main/assets/badge/v0.json)](https://github.com/astral-sh/uv)
[![Checked with mypy](https://img.shields.io/badge/mypy-checked-blue)](https://mypy-lang.org/)

Camden gives analysts and data staff in the Baltimore City Mayor's Office of Performance and Innovation (OPI) one consistent, safe, repeatable way to do the things we do every week: pull SharePoint lists into pandas, query city databases, fetch Open Baltimore layers, geocode addresses, send report emails, and post to Teams.

## Part of the Baltimore civic foundations

Camden is one of three independently versioned foundations OPI maintains for Baltimore civic software:

- **[camden](https://github.com/city-of-baltimore/camden)** (this repo) — Python **data** SDK (`baltimore-camden`): Graph/SharePoint, Open Baltimore, city databases, geocoding, provenance, delivery.
- **[patapsco](https://github.com/city-of-baltimore/patapsco)** — **platform / integration point** (`baltimore-patapsco` + `@city-of-baltimore/patapsco`): request IDs, safe error envelopes, observability, health/readiness, typed frontend client, and app scaffold. Authentication remains app-local today.
- **[bromo / Baltimore Design System](https://github.com/city-of-baltimore/bromo)** — TypeScript **presentation** layer (`@city-of-baltimore/bromo`): tokens, theme, shell, accessible components.

How the pieces fit together: **[patapsco/docs/use/foundations.md](https://github.com/city-of-baltimore/patapsco/blob/main/docs/use/foundations.md)** is the canonical architecture overview. The shared naming rule (`baltimore-<name>` on PyPI, `@city-of-baltimore/<name>` on npm) is [ADR-0005](docs/adr/0005-civic-packages-share-the-baltimore-namespace.md).

## Contents

- [Part of the Baltimore civic foundations](#part-of-the-baltimore-civic-foundations)
- [Philosophy](#philosophy)
- [What Camden supports](#what-camden-supports)
- [Installation](#installation)
- [Configuration](#configuration)
- [Quickstart](#quickstart)
- [Coming from r4OPI](#coming-from-r4opi)
- [Development](#development)
- [Roadmap](#roadmap)
- [Contributing](#contributing)
- [License](#license)

## Philosophy

**Camden exists to lower the ramp**: reusable helpers, copy-pasteable recipes, and shared standards, so each analyst doesn't hand-roll their own auth, connection strings, and API calls.

- **Python is the preferred default** for new shared, repeatable, production-facing, integrated, or automated workflows.
- **R remains welcome** for statistical analysis, exploratory work, and existing R/Shiny assets.

Design principles: plain-English function names, predictable return types (pandas in, pandas out), small composable functions, no magic, no hard-coded credentials, and everything configurable without editing code.

New to the toolkit? Start with the prose-first
[Camden overview](docs/onboarding.md), which explains the people it serves,
how data moves through it, its safety model, and where product responsibility
begins and ends. The
[product requirements](docs/product/product-requirements.md) and
[user stories](docs/product/user-stories.md) define Camden's users, complete
capability scope, acceptance boundaries, failure behavior, and verification
evidence. The
[technical specification](docs/product/technical-spec.md) is the current
system specification that maps those outcomes to Camden's architecture,
boundaries, and verification model. The evidence-mapped
[service standards](docs/service-standards.md) state what consuming teams can
rely on, how each promise is proven, and what Camden deliberately does not
promise. The [documentation map](docs/documentation-inventory.md) explains
which page owns each kind of guidance and when it must change.

## What Camden supports

| Area | Module | What you get |
| --- | --- | --- |
| Microsoft Graph auth | `camden.graph` | `GraphAuth` (app-only or signed-in) and a hardened `GraphClient` — retries, paging, and `$batch` built in |
| SharePoint & OneDrive | `camden.sharepoint` | Lists ↔ DataFrames with `where=` filters (non-indexed columns fall back gracefully), display-name labels, choice validation, person lookups, `describe()` diagnostics; schema admin (`create_list`/`delete_list`, typed column builders, `add_column`/`delete_column`); document libraries by path (`ls`, `mkdir`, `versions`, recursive download, upload, delete); file transfer by pasted link; extension-aware `read_sharepoint()` / `write_sharepoint()` |
| Email | `camden.mail` | `send_mail()` through Graph (attachments, HTML, cc/bcc) — no ad hoc SMTP |
| Teams | `camden.teams` | Post to channels by team/channel *name*, list team members/rosters, or use a secret-bearing Workflows webhook from unattended jobs |
| Planner | `camden.planner` | Plans/buckets/tasks as DataFrames; create and update tasks (buckets by name, assignees by email) with Planner's If-Match etag dance handled for you |
| Notifications | `camden.notify` | Provider-neutral `send()` with Graph-mail, SendGrid, and Teams-webhook transports; sent/skipped/failed statuses instead of exceptions; `"all"` or `"failover"` strategies |
| City databases | `camden.db` | Named registry (SQL Server + Postgres/PostGIS via `kind`), `read_sql()`/`read_table()` → DataFrames, `write_sql()` bulk writes, `read_postgis()` → GeoDataFrames, SQLAlchemy URLs, `sql_in()` |
| Artifact publication | `camden.artifacts` | Storage-neutral immutable generations plus one manifest-last commit marker for related object sets; strict parsing and payload size/SHA-256 verification detect mixed, corrupt, or mismatched generations relative to that manifest, but do not authenticate the manifest itself |
| Open Baltimore | `camden.opendata` | `search_datasets()` finds datasets; `get_layer()`/`get_table()`/`download_layer()`/`iter_layer_rows()` fetch them with stable ID ordering, duplicate guards, retries, and validated response shapes; provenance built in: `describe_layer()`, one-snapshot `assert_fields()` schema guards, `where_since()` incremental clauses, and an integrity-checked `manifest_for()` commit marker for every cached file |
| Census/ACS | `camden.census` | `acs()` Baltimore City estimates at county/tract/block-group level, friendly variable names, numeric columns, join-ready `geoid` |
| Geocoding | `camden.geocoding` | City ESRI locator with Census fallback, address scrubbing; batch `geocode_frame()` dedupes, rate-limits, and logs progress |
| Spatial recipes | `camden.geo` | `spatial_join()` (which neighborhood is each point in — accepts plain lon/lat DataFrames), `buffer_feet()` (no accidental degree buffers), `points_from_frame()`, `reproject()` with named CRS constants |
| Excel reports | `camden.excel` | `write_excel()`: branded headers, frozen rows, autofilter, currency/percent/date formats — one call from DataFrame to presentable workbook |
| Report templates | `camden.reporting` | `html_report()` (branded email-ready HTML from DataFrames + text) and `excel_report()` (About sheet with title/timestamp/run id + one sheet per section) |
| Validation & diffs | `camden.validate` | `check_frame()` — required columns/dtypes, nulls, uniqueness, allowed values, all problems in one teaching error; `diff_frames()` — keyed add/update/delete plans that power list sync |
| Job runtime | `camden.runtime` | `configure_logging()` (UTC, run-id correlation, quiet deps), `log_duration()` timing on success and failure, `safe_summary()` config logging with secrets redacted |
| City utilities | `camden.dates`, `camden.formats`, `camden.branding` | Fiscal years, dollar formatting, CCF→gallons, OPI report and plotting palette |

**What Camden is not:** a stats library, a dashboarding framework, or a place for one-off analysis code. If a helper is only useful to one project, it belongs in that project.

## Installation

Camden supports Python 3.13 and 3.14. The repository also pins the family-wide
Node.js 26 tooling baseline in `.nvmrc` (the version in
`patapsco/contracts/platform-bom.toml`); Camden itself ships no JavaScript
runtime or npm package.

Releases are published to PyPI as `baltimore-camden` from a matching `vX.Y.Z`
GitHub release through OIDC trusted publishing. Consumer applications pin a
released version rather than a moving Git branch.

The PyPI project and trusted-publisher binding are active; releases publish
through the workflow without a stored registry token.

**Current source candidate:** `0.9.2`

**Published consumer baseline:** `0.9.0`

Version 0.9.0 is the published, registry-backed consumer baseline. Version
0.9.2 is an unreleased source candidate for the current quality-convergence
work. Consumers should continue installing 0.9.0: a source checkout is useful
for development and review, but it is not registry-backed release evidence.

```bash
# with uv (preferred)
uv add "baltimore-camden[all]==0.9.0"

# with pip
pip install "baltimore-camden[all]==0.9.0"
```

Extras keep the core light — install only what you need:

| Extra | Adds | Needed for |
| --- | --- | --- |
| *(none)* | httpx, msal, pandas, pyarrow, pydantic | Graph, SharePoint, mail, Teams, Open Baltimore tables/Parquet, geocoding, utilities |
| `[db]` | pyodbc, SQLAlchemy | `camden.db` SQL Server profiles |
| `[postgres]` | psycopg, SQLAlchemy | `camden.db` Postgres/PostGIS profiles |
| `[excel]` | openpyxl | `camden.excel` styled workbooks, `.xlsx` writes in `write_sharepoint` |
| `[geo]` | geopandas | `camden.opendata` layers, spatial file reading/writing |
| `[all]` | everything above | — |

## Configuration

Camden reads `CAMDEN_*` environment variables (or a local `.env` file — see
[.env.example](.env.example)). Safe, non-secret defaults are provided where a
shared default makes sense. Features that call protected systems still require
their documented credentials or connection profile. The legacy Graph variables
`opi_graph_id` / `opi_graph_secret` remain supported.

| Variable | Default | Purpose |
| --- | --- | --- |
| `CAMDEN_CLIENT_ID` | legacy `opi_graph_id` | Entra app registration ID |
| `CAMDEN_CLIENT_SECRET` | legacy `opi_graph_secret` | Client secret (app-only auth) |
| `CAMDEN_TENANT` | `bmore.onmicrosoft.com` | Entra tenant |
| `CAMDEN_DEFAULT_SITE_URL` | OPI SharePoint site | Site used when none is given |
| `CAMDEN_MAIL_SENDER` | — | From-mailbox for app-only email |
| `CAMDEN_SENDGRID_API_KEY` | — | Enables the SendGrid notification transport |
| `CAMDEN_CENSUS_API_KEY` | — | Census API key for `camden.census` (free signup) |
| `CAMDEN_SQL_DRIVER` | auto-detected | ODBC driver name |
| `CAMDEN_DATABASES_FILE` | — | TOML file extending the database registry (`~` expands) |
| `CAMDEN_CACHE_DIR` | `~/.cache/camden` | Where cached open-data pulls live (`~` expands) |
| `CAMDEN_TIMEOUT` | `30` | Timeout for Camden-owned HTTP requests and database connection attempts; excludes MSAL and caller-injected resources |
| `CAMDEN_GRAPH_MAX_RETRIES` | `4` | Retry budget for transient Graph failures |
| `CAMDEN_GRAPH_BASE_URL` | Graph v1.0 | Microsoft Graph endpoint (rarely changed) |
| `CAMDEN_GRAPH_SCOPES` | `.default` | OAuth scopes requested for Graph tokens |

Keep credentials out of code. Camden's environment-backed `Settings` models
secret-bearing credential fields as secret values so their ordinary
representation does not reveal the configured value. Source and delivery
functions expose the
`settings=` or `client=` injection documented in their signatures when a job
needs an explicit override. Inspect resolved configuration with
`camden.get_settings()`; when diagnostics are needed, import and log only
`safe_summary()` from `baltimore.camden.runtime`.

## Quickstart

```python
from baltimore import camden
from baltimore.camden.graph import GraphAuth, GraphClient

# SharePoint lists <-> pandas
site = camden.get_site()                        # the OPI site by default
updates = site.get_list("Book of Business - Weekly Updates")
on_track = updates.to_df(where="fields/Status eq 'On track'", labels="display")
updates.choices("Status")                       # valid options before you write
updates.append(new_rows_df)                     # person columns: use site.user_lookup_id(email)

# Sync a tracker safely: dry-run the plan, then apply. Deletes are opt-in.
# `columns=` protects comparisons and existing-row updates; project weekly_df
# to owned fields first because new rows include all its non-reserved columns.
print(updates.diff(weekly_df, key="Title").summary())   # add 3, update 2, delete 0, unchanged 41
updates.sync(weekly_df, key="Title")

# Paste a link, get data — and write it back the same way
df = camden.read_sharepoint("https://bmore.sharepoint.com/:x:/r/sites/opi/...xlsx")
camden.write_sharepoint(df, "weekly.xlsx", folder_link)   # DataFrame -> xlsx -> SharePoint

# Document libraries, by path
docs = site.drive()                             # the default "Documents" library
docs.ls("General", recursive=True)              # name/path/kind/size/modified
docs.download_dir("General/Reports", "reports/")
docs.upload("weekly.xlsx", "General/Reports")

# City databases -> DataFrames (parameterize with ?, never f-strings)
permits = camden.read_sql(
    "SELECT * FROM dbo.Permits WHERE issued_date >= ?", "citistat", params=["2026-01-01"]
)
neighborhoods = camden.read_table("dbo.Neighborhoods", "citistat")
camden.write_sql(
    camden.check_frame(scores_df, columns={"score": "number"}, no_nulls=["agency"]),
    "dbo.Scores", "opi-sandbox", if_exists="append",
)
conn = camden.connect("citistat")               # raw connection when you need one

# Open Baltimore: search the catalog, then pull any dataset by name, page URL, or id
camden.search_datasets("parking fines")         # title, dataset_id, snippet, modified
hoods = camden.get_shape("neighborhoods")       # GeoDataFrame, EPSG:4326
vacants = camden.get_layer(                     # paste a dataset page straight from the catalog
    "https://data.baltimorecity.gov/datasets/8ed6565f93a04455ae6bd5bcb7d272db_19/explore"
)
camden.describe_layer("real_property")          # provenance: rows, last edit, owner
camden.assert_fields("vbns", {"NoticeDate": "Date"})   # fail fast on schema drift
camden.layer_fields("real_property")            # inspect the schema before filtering
camden.layer_count("real_property")             # size up a big pull first (~238k)
fines = camden.get_table(                       # JSON rows only — no geometry, much faster
    "d2a2330d6a374ad39a24a0d7f7b58f19_0",       # parking fines: ~11M rows
    where="ViolFine > 500",
    fields=["Citation", "Description", "ViolFine", "ViolDate"],
)
camden.download_layer(                          # page-written Parquet; no full DataFrame
    "d2a2330d6a374ad39a24a0d7f7b58f19_0", "fines.parquet", where="ViolDate >= DATE '2026-01-01'"
)  # duplicate detection still retains every object ID; memory grows with rows
fines = camden.cached_table(                    # or cache it: downloads at most once per max_age
    "d2a2330d6a374ad39a24a0d7f7b58f19_0",
    where=camden.where_since("ViolDate", "2026-01-01"),  # incremental pulls
    max_age="6h",
)  # cache_layer(...) returns the Parquet path instead; every download writes a
   # manifest commit marker — camden.manifest_for(path) verifies the file hash and
   # says exactly what query produced it — and iter_layer_rows() streams row-by-row

# Geocoding + spatial tagging
located = camden.geocode_frame(df["address"], fallback=True, rate_limit_per_second=5)
tagged = camden.spatial_join(located, hoods, keep=["Name"])   # neighborhood per address

# Styled Excel in one call (branded header, filters, number formats)
camden.write_excel(summary_df, "weekly.xlsx", currency=["TotalFines"], percent=["ShareResolved"])

# Email a report; post to Teams as a signed-in user
camden.send_mail(to="team@baltimorecity.gov", subject="Weekly VBN report",
                 body="Attached.", attachments=["vbns.xlsx"])
with GraphClient(auth=GraphAuth(flow="device_code")) as delegated_graph:
    camden.send_channel_message(
        "OPI",
        "Book of Business",
        "Reporting period is <b>open</b>.",
        client=delegated_graph,
    )

# City utilities
camden.fiscal_year_label("2026-07-01")          # "FY27"
camden.week_bounds("2026-07-08")                # Fri-Thu reporting week: (Jul 3, Jul 9)
camden.dollars(121324520.45)                    # "$121,324,520.45"
```

> **Teams note:** Graph only allows channel messages from *signed-in users*
> (delegated auth), and `send_channel_message()` does not choose or validate that
> flow for you. Pass an explicitly delegated client as shown above. For
> unattended or scheduled jobs, use `camden.teams.send_webhook()` with a
> channel's Workflows URL. That URL is itself bearer-like authority; store and
> redact it as a credential.

## Coming from r4OPI

| r4OPI | Camden |
| --- | --- |
| `sharepointLogin()` | `GraphAuth()` (automatic inside every client) |
| `sharepointConnect()` | `camden.get_site()` |
| `sharedWithMe(url)` | `camden.sharepoint.download_file(url)` — direct API, no browser |
| `sqlConnection("citistat")` | `camden.connect("citistat")` |
| `ordTbl()` | `camden.db.ordered_select(conn, table)` / `ordered_columns()` |
| `getGeoShapeFromOB("neighborhoods")` | `camden.get_shape("neighborhoods")` |
| `getOBdatasets("vbns")` | `camden.get_dataset("vbns")` |
| `cityGeocoder(addr)` / `censusGeocoder(addr)` | `camden.geocode(addr)` / `camden.geocode(addr, source="census")`; batches: `camden.geocode_frame(addresses)` |
| `scrubAddress(addr)` | `camden.scrub_address(addr)` |
| `whatFY(d)` / `whatFY(d, abbrv=TRUE)` | `camden.fiscal_year(d)` / `camden.fiscal_year_label(d)` |
| `dollarWithCents(x)` | `camden.dollars(x)` |
| `ccfToGallons(x)` | `camden.ccf_to_gallons(x)` |
| `vectorToSql(v)` | `camden.sql_in(v)` |
| `opiColors()` | `camden.opi_colors()` / `camden.opi_palette()` |

New in Camden (no r4OPI equivalent): `send_mail`, `send_channel_message`, `send_webhook`, `upload_file`, `read_sharepoint`, list `update`/`delete`, SharePoint URL parsing, `shared_with_me()` listing.

## Development

One command contract for humans, CI, and agents — [Taskfile.yml](Taskfile.yml) ([install go-task](https://taskfile.dev/installation/)):

```bash
git clone https://github.com/city-of-baltimore/camden && cd camden
task setup      # locked Python + documentation tooling, and verified pre-push hook
task test       # fast offline unit tests + docstring examples
task typecheck  # mypy only
task build      # inspect and clean-install the wheel and sdist
task prepush    # complete pre-push gate: static checks, coverage, package install, docs
task check      # alias for prepush
task validate   # locked setup + prepush + foundation currency
task fmt        # auto-format and fix lint findings
task ci         # lean hosted gate: locked setup, static checks, lean-CI policy check
task docs-serve # live-preview the documentation site (mkdocs-material + mkdocstrings)
task docs:verify # verify already-built docs: local assets, navigation, themes, accessibility
task docs:setup # provision the pinned docs browser once before its first verification
task hooks:install # reinstall only the verified pre-push hook
task integration # no Graph/delivery credentials required; no remote provider mutation
```

Three tiers with the same meaning as every repo in the family — `ci` is static
checks only, `prepush` adds tests/coverage/builds, `validate` is the release
gate. See
[patapsco/docs/reference/app-consistency-standard.md](https://github.com/city-of-baltimore/patapsco/blob/main/docs/reference/app-consistency-standard.md)
section 4; Patapsco's published `platform-check` (run by `task ci` as
`task ci:policy`) enforces the boundary mechanically. Because tests live
pre-push, installing the pre-push hook is not optional: `task setup` installs
it, or `task hooks:install` on its own.

Documentation development also needs Node 26 and npm 11. The private npm
project bundles released Bromo tokens and self-hosted fonts for MkDocs; it is
separate from the Python SDK. `task docs` builds the site, and the local gate
verifies it before inspecting the Python archives. See the
[documentation maintenance guide](CONTRIBUTING.md#documentation-design) for
adapter ownership, upgrades, and browser evidence.

Unit tests run offline with no credentials. `task integration` needs no Graph or
delivery credentials: it reads small public Open Baltimore and geocoder samples
and verifies one provenance-marked local artifact. It makes no remote provider
mutation or delivery, although it writes temporary files and its evidence
bundle locally. Protected Graph and SharePoint reads use
`task integration:tenant`; scratch writes and deliveries have separate named
profiles whose commands carry the invocation-only `--live-writes` or
`--live-sends` consent flags. Those named tasks are the supported interface;
the explicit flags in the direct command line are the enforcement boundary.
During a live run the harness rejects
`PYTEST_ADDOPTS`, `PYTEST_DEBUG`, `PYTEST_PLUGINS`, any configured `addopts`
beyond Camden's reviewed `-ra` baseline, anything other than file-descriptor
capture, passed-test output, live or file logging, pytest debug/pastebin export,
warning-policy overrides, explicit pytest plugin changes, debugger modes, and
selectors that could reduce a profile. Those ambient diagnostics could expose
signed URLs before report redaction. No live-harness or Pytest configuration
variable can grant side-effect authority. Protected verification still
requires a trusted runner, process environment, certificate store, network
path, and proxy configuration.

Every active run writes redacted JSON and Markdown evidence below
`build/live-verification/`. A required check that is missing, skipped, or
duplicated is non-green. A write run is also non-green when cleanup fails or
could have left residue. The prose-first
[live-verification guide](docs/live-verification.md) explains the
profiles, safe scratch-target setup, GitHub environment, evidence meanings, and
operator response.

See [AGENTS.md](AGENTS.md) for architecture, conventions, and testing patterns, and [CONTRIBUTING.md](CONTRIBUTING.md) for the pull-request workflow.

## Roadmap

Current state, completed release history, external permission follow-up, and the
two-project data-foundation backlog live in [ROADMAP.md](ROADMAP.md). Execution
is tracked with GitHub [milestones](https://github.com/city-of-baltimore/camden/milestones)
and issues.

## Contributing

Contributions are welcome — new helpers, recipes, docs, and bug reports. Branch, open a PR to `main`, and CI must pass. If you have an idea but aren't sure where it fits, open an issue and ask.

## License

Distributed under the MIT License. See [LICENSE](LICENSE) for more information.

## Maintainers

Mayor's Office of Performance and Innovation (OPI).
