Skip to content

Commit b23732b

Browse files
author
Nathaniel Henry
committed
Add the closecity Python client with Sphinx docs and tutorials
0 parents  commit b23732b

26 files changed

Lines changed: 2019 additions & 0 deletions

.github/workflows/ci.yml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
8+
jobs:
9+
test:
10+
runs-on: ubuntu-latest
11+
steps:
12+
- uses: actions/checkout@v4
13+
- uses: actions/setup-python@v5
14+
with:
15+
python-version: "3.12"
16+
- run: pip install -e ".[dev,geo]"
17+
- run: ruff check src tests
18+
- run: pytest -q

.github/workflows/docs.yml

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
name: docs
2+
3+
on:
4+
push:
5+
branches: [main]
6+
workflow_dispatch:
7+
8+
permissions:
9+
contents: read
10+
pages: write
11+
id-token: write
12+
13+
concurrency:
14+
group: pages
15+
cancel-in-progress: true
16+
17+
jobs:
18+
build:
19+
runs-on: ubuntu-latest
20+
steps:
21+
- uses: actions/checkout@v4
22+
- uses: actions/setup-python@v5
23+
with:
24+
python-version: "3.12"
25+
- run: pip install -e ".[docs]"
26+
- run: sphinx-build -b html docs docs/_build/html -W --keep-going
27+
- uses: actions/upload-pages-artifact@v3
28+
with:
29+
path: docs/_build/html
30+
31+
deploy:
32+
needs: build
33+
runs-on: ubuntu-latest
34+
environment:
35+
name: github-pages
36+
url: ${{ steps.deployment.outputs.page_url }}
37+
steps:
38+
- id: deployment
39+
uses: actions/deploy-pages@v4

.github/workflows/publish.yml

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
name: publish
2+
3+
# Publishes to PyPI via OIDC trusted publishing when a GitHub Release is
4+
# published. Configure the trusted publisher on PyPI first (project "closecity",
5+
# workflow "publish.yml", environment "pypi").
6+
on:
7+
release:
8+
types: [published]
9+
10+
permissions:
11+
contents: read
12+
13+
jobs:
14+
build:
15+
runs-on: ubuntu-latest
16+
steps:
17+
- uses: actions/checkout@v4
18+
- uses: actions/setup-python@v5
19+
with:
20+
python-version: "3.12"
21+
- run: pip install build
22+
- run: python -m build
23+
- uses: actions/upload-artifact@v4
24+
with:
25+
name: dist
26+
path: dist/
27+
28+
pypi:
29+
needs: build
30+
runs-on: ubuntu-latest
31+
environment:
32+
name: pypi
33+
url: https://pypi.org/p/closecity
34+
permissions:
35+
id-token: write
36+
steps:
37+
- uses: actions/download-artifact@v4
38+
with:
39+
name: dist
40+
path: dist/
41+
- uses: pypa/gh-action-pypi-publish@release/v1

.gitignore

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# Python
2+
__pycache__/
3+
*.py[cod]
4+
*.egg-info/
5+
.eggs/
6+
build/
7+
dist/
8+
.venv/
9+
venv/
10+
11+
# Tooling caches
12+
.ruff_cache/
13+
.pytest_cache/
14+
.mypy_cache/
15+
16+
# Sphinx build output
17+
docs/_build/

CHANGELOG.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# Changelog
2+
3+
## 1.0.0
4+
5+
First public release of the `closecity` Python client for the Close API
6+
(api.close.city).
7+
8+
- A thin, typed `Client` over the metered, read-only public endpoints: catalog,
9+
block/point summaries and POIs, POI search/detail/catchment, areal block
10+
queries, and isochrones.
11+
- First-class metering (`tokens_charged` / `tokens_remaining`), ETag/304
12+
conditional requests, keyset pagination (`Paginator`), and typed RFC 9457
13+
errors.
14+
- `places()` place-name lookup (city/town → GEOID + centroid).
15+
- Opt-in spatial output: `Reply.to_geopandas()` / `Paginator.to_geopandas()`
16+
(POI points, isochrone polygons, and census-block joins) under the `geo` extra.

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2026 Close
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

README.md

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
# closecity — Python client for the Close API
2+
3+
Travel times from every US census block to nearby points of interest, by walking,
4+
biking, and public transit — the data behind [close.city](https://close.city),
5+
over the [Close API](https://api.close.city).
6+
7+
**Documentation:** https://henryspatialanalysis.github.io/closecity-python/
8+
9+
```bash
10+
pip install closecity
11+
pip install "closecity[geo]" # + GeoPandas output (to_geopandas)
12+
```
13+
14+
## Quickstart
15+
16+
```python
17+
from closecity import Client
18+
19+
# The API key (ck_live_… / ck_test_…) is created at https://account.close.city.
20+
with Client("ck_live_your_key_here") as close:
21+
# Fastest travel time to each category from a census block.
22+
summary = close.block_summary("410390020001010", mode = ["walk", "transit"])
23+
for row in summary.results:
24+
print(row["dest_type_id"], row["mode"], row["travel_time"])
25+
26+
# Metering is surfaced on every metered reply.
27+
print(summary.tokens_charged, "charged;", summary.tokens_remaining, "left")
28+
```
29+
30+
Free routes (catalog, place lookup, health) need no key:
31+
32+
```python
33+
from closecity import Client
34+
close = Client()
35+
print(close.modes().data["modes"])
36+
print(close.last_updated().data["last_updated"])
37+
38+
# Resolve a city name to its census place GEOID + centroid:
39+
print(close.places("Providence").data["places"][0])
40+
```
41+
42+
## Pagination
43+
44+
List endpoints return a `Paginator` that follows the opaque keyset cursor for you.
45+
Iterate it for every record, or use `.pages()` for per-page metadata:
46+
47+
```python
48+
# Every POI near a point, across all pages:
49+
for poi in close.pois_search(lat = 44.05, lon = -123.09, radius_m = 2000):
50+
print(poi["name"], poi["address"]["city"])
51+
52+
# Or page-by-page, watching the token balance drop:
53+
for page in close.block_pois("410390020001010", limit = 500).pages():
54+
print(len(page.results), "rows;", page.tokens_remaining, "tokens left")
55+
```
56+
57+
Cursors are opaque and signed — never construct or modify them.
58+
59+
## Conditional requests (free revalidation)
60+
61+
Metered `GET`s return an `ETag`. Send it back to revalidate for free — a `304`
62+
costs no tokens, even at a zero balance:
63+
64+
```python
65+
first = close.block_summary("410390020001010")
66+
again = close.block_summary("410390020001010", if_none_match = first.etag)
67+
if again.not_modified:
68+
... # your cached copy is still current; nothing was charged
69+
```
70+
71+
## Isochrones
72+
73+
```python
74+
iso = close.isochrone(block = "410390020001010", contours = [15, 30, 45],
75+
mode = "walk", direction = "to")
76+
for feature in iso.data["features"]:
77+
print(feature["properties"]["contour"], feature["properties"]["reachable_blocks"])
78+
```
79+
80+
## Errors
81+
82+
Errors are [RFC 9457](https://www.rfc-editor.org/rfc/rfc9457) `problem+json`,
83+
mapped to exceptions. Catch a precise class or the `CloseAPIError` base; the stable
84+
machine key is `err.slug`:
85+
86+
```python
87+
from closecity import CloseAPIError, TokensExhaustedError, RateLimitedError
88+
89+
try:
90+
close.block_summary("000000000000000")
91+
except TokensExhaustedError:
92+
... # buy more tokens at account.close.city
93+
except RateLimitedError as err:
94+
time.sleep(err.retry_after or 1)
95+
except CloseAPIError as err:
96+
print(err.slug, err.status, err.title, err.request_id)
97+
```
98+
99+
## Spatial output
100+
101+
With `pip install "closecity[geo]"`, turn any reply into a GeoDataFrame — POIs
102+
become points, isochrones become polygons, and block replies join census-block
103+
boundaries:
104+
105+
```python
106+
pts = close.pois_search(lat = 41.823, lon = -71.412, radius_m = 1500).to_geopandas()
107+
iso = close.isochrone(block = "440070036001010", minutes = 15).to_geopandas()
108+
```
109+
110+
## Reference
111+
112+
- Documentation: https://henryspatialanalysis.github.io/closecity-python/
113+
- Interactive API: https://api.close.city/docs
114+
- Machine-readable contract: https://api.close.city/openapi.json
115+
116+
## Development
117+
118+
```bash
119+
pip install -e '.[dev,geo]'
120+
pytest # unit tests (no network — httpx MockTransport)
121+
ruff check src tests
122+
```

docs/_static/close-logo.png

27.9 KB
Loading

docs/_static/custom.css

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
/* Close type system, mirroring docs.close.city: Figtree for body, Inconsolata
2+
for code. Loaded from Google Fonts (the same source the app and Starlight
3+
docs use). Falls back to system fonts if offline. */
4+
@import url("https://fonts.googleapis.com/css2?family=Figtree:ital,wght@0,400;0,500;0,600;0,700;1,400&family=Inconsolata:wght@400;600&display=swap");
5+
6+
:root {
7+
--font-stack: "Figtree", system-ui, -apple-system, "Helvetica", sans-serif;
8+
--font-stack--monospace: "Inconsolata", ui-monospace, "SFMono-Regular", monospace;
9+
}
10+
11+
/* The Close wordmark accent: the terminal yellow dot on the logo. */
12+
.sidebar-brand-text {
13+
font-weight: 700;
14+
}

docs/_static/favicon.ico

15 KB
Binary file not shown.

0 commit comments

Comments
 (0)