Skip to content

Commit ae8670a

Browse files
author
Nathaniel Henry
committed
Run the tutorial notebooks at build time when a key is present
1 parent e9ebbce commit ae8670a

12 files changed

Lines changed: 462 additions & 426 deletions

.github/workflows/docs.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,11 @@ jobs:
2323
with:
2424
python-version: "3.12"
2525
- run: pip install -e ".[docs]"
26+
# Set this repo secret to run the tutorial notebooks and render their output
27+
# and maps. Without it, the notebooks are shown but not executed.
2628
- run: sphinx-build -b html docs docs/_build/html -W --keep-going
29+
env:
30+
CLOSECITY_KEY: ${{ secrets.CLOSECITY_KEY }}
2731
- uses: actions/upload-pages-artifact@v3
2832
with:
2933
path: docs/_build/html

docs/conf.py

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
"""Sphinx configuration for the closecity Python documentation."""
22

33
import importlib.metadata
4+
import os
45

56
project = "closecity"
6-
author = "Close"
7-
copyright = "2026, Close" # noqa: A001
7+
author = "Nathaniel Henry"
8+
copyright = "2026, Henry Spatial Analysis" # noqa: A001
89
try:
910
release = importlib.metadata.version("closecity")
1011
except importlib.metadata.PackageNotFoundError:
@@ -16,9 +17,20 @@
1617
"sphinx.ext.viewcode",
1718
"sphinx.ext.intersphinx",
1819
"sphinx_copybutton",
20+
"myst_nb",
1921
]
2022

21-
# httpx is the only runtime dep; mock it so the docs build without installing it.
23+
# The getting-started and tutorial pages are executable notebooks. Run them when a
24+
# key is present (on the docs site), and show the code without running it otherwise.
25+
nb_execution_mode = "auto" if os.environ.get("CLOSECITY_KEY") else "off"
26+
nb_execution_timeout = 180
27+
nb_execution_raise_on_error = True
28+
29+
# When the notebooks are not executed (no key), myst-nb cannot infer a highlight
30+
# language for the code cells and warns once per cell. The cells still render with
31+
# python highlighting from the code-cell directive, so silence that one warning.
32+
suppress_warnings = ["myst-nb.lexer"]
33+
2234
autodoc_mock_imports = ["httpx"]
2335
autodoc_member_order = "bysource"
2436
# The SDK docstrings are plain reStructuredText prose with single-backtick

docs/getting_started.md

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
---
2+
jupytext:
3+
text_representation:
4+
extension: .md
5+
format_name: myst
6+
kernelspec:
7+
display_name: Python 3
8+
name: python3
9+
---
10+
11+
# Getting started
12+
13+
A short tour of the client. The tutorials go further.
14+
15+
```{code-cell} python
16+
:tags: [remove-cell]
17+
import os
18+
from closecity import Client
19+
close = Client(os.environ.get("CLOSECITY_KEY"))
20+
```
21+
22+
## Words you will see
23+
24+
A few terms come up throughout:
25+
26+
- **Census block.** The smallest area the Census Bureau publishes. Each one has a
27+
15-digit id, its **GEOID**.
28+
- **Destination type.** A category of place, such as grocery stores or libraries.
29+
Every type has a numeric id.
30+
- **Mode.** How someone travels: walk, bike, or transit.
31+
- **Isochrone.** The area reachable from a point within a time limit, as a polygon.
32+
- **Catchment.** The reverse: every block that can reach a given place.
33+
34+
## Build a client
35+
36+
You make every request through a client.
37+
38+
```python
39+
from closecity import Client
40+
41+
close = Client("ck_live_your_key") # use your own key here
42+
```
43+
44+
The catalog and lookup routes are free, so `Client()` with no key works for those.
45+
46+
```{code-cell} python
47+
close.modes().data["modes"]
48+
```
49+
50+
## Look things up instead of guessing
51+
52+
Read the numeric id for a category from the catalog, and turn a city name into a
53+
GEOID and a centre point.
54+
55+
```{code-cell} python
56+
types = close.destination_types().data["destination_types"]
57+
ids = {t["label"]: t["dest_type_id"] for t in types}
58+
grocery = ids["grocery_stores"]
59+
60+
providence = close.places("Providence").data["places"][0]
61+
providence["geoid"]
62+
```
63+
64+
## Make a call and map it
65+
66+
Feature methods return a GeoDataFrame, so you can map the result straight away.
67+
68+
```{code-cell} python
69+
groceries = close.pois_search(lat = providence["lat"], lon = providence["lon"],
70+
radius_m = 1500, type = grocery)
71+
groceries.plot(color = "#202a5b")
72+
```
73+
74+
## Turn spatial output off
75+
76+
Set the client's `spatial` flag to `False` to work with the raw data.
77+
78+
```{code-cell} python
79+
close.spatial = False
80+
summary = close.block_summary("440070036001010", mode = "walk")
81+
summary.results
82+
```
83+
84+
## Errors
85+
86+
Problem responses become typed exceptions.
87+
88+
```{code-cell} python
89+
from closecity import CloseAPIError
90+
91+
try:
92+
close.block_summary("000000000000000")
93+
except CloseAPIError as err:
94+
print(err.status, err.slug)
95+
```

docs/getting_started.rst

Lines changed: 0 additions & 108 deletions
This file was deleted.

docs/tutorials/amenity_basket.md

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
---
2+
jupytext:
3+
text_representation:
4+
extension: .md
5+
format_name: myst
6+
kernelspec:
7+
display_name: Python 3
8+
name: python3
9+
---
10+
11+
# The amenity basket
12+
13+
A city planner wants every resident to be able to walk to a basket of six everyday
14+
amenities: a grocery store, a library, a park, a frequent-transit stop, a restaurant,
15+
and a cafe. This tutorial measures how many residents already have that, and shows
16+
where the gaps are. The idea follows
17+
[this analysis](https://nathenry.com/writing/2023-02-07-seattle-walkability.html),
18+
here applied to Richmond, Virginia.
19+
20+
```{code-cell} python
21+
:tags: [remove-cell]
22+
import os
23+
from closecity import Client
24+
close = Client(os.environ.get("CLOSECITY_KEY"))
25+
```
26+
27+
## Set up
28+
29+
Read the six category ids from the free catalog, and turn the city name into a centre
30+
point.
31+
32+
```python
33+
from closecity import Client
34+
35+
close = Client("ck_live_your_key") # use your own key here
36+
```
37+
38+
```{code-cell} python
39+
types = close.destination_types().data["destination_types"]
40+
ids = {t["label"]: t["dest_type_id"] for t in types}
41+
basket = {
42+
"grocery": ids["grocery_stores"],
43+
"library": ids["libraries"],
44+
"park": ids["parks"],
45+
"transit": ids["frequent_transit"],
46+
"restaurant": ids["restaurants"],
47+
"cafe": ids["cafes"],
48+
}
49+
50+
city = close.places("Richmond").data["places"][0]
51+
```
52+
53+
## Pull the blocks, with population
54+
55+
One call gets the walk time from every block near downtown to each of the six
56+
categories, along with each block's population.
57+
58+
```{code-cell} python
59+
blocks = close.blocks_query(
60+
center = {"lon": city["lon"], "lat": city["lat"]}, radius_m = 2500,
61+
mode = "walk", type = list(basket.values()), include_population = True)
62+
63+
one_per_block = blocks.drop_duplicates("geoid")
64+
total_pop = one_per_block["population"].sum()
65+
```
66+
67+
## Coverage, one amenity at a time
68+
69+
For each amenity, a block counts as covered when it is within a 15-minute walk.
70+
71+
```{code-cell} python
72+
for name, type_id in basket.items():
73+
covered = set(blocks.loc[(blocks.dest_type_id == type_id) &
74+
(blocks.travel_time <= 15), "geoid"])
75+
pop = one_per_block.loc[one_per_block.geoid.isin(covered), "population"].sum()
76+
print(f"{name:11} {100 * pop / total_pop:3.0f}%")
77+
```
78+
79+
Map one amenity to see the pattern.
80+
81+
```{code-cell} python
82+
near_transit = set(blocks.loc[(blocks.dest_type_id == basket["transit"]) &
83+
(blocks.travel_time <= 15), "geoid"])
84+
one_per_block = one_per_block.assign(
85+
has_transit = one_per_block.geoid.isin(near_transit))
86+
one_per_block.plot(column = "has_transit", cmap = "Greens")
87+
```
88+
89+
## Who can reach all six
90+
91+
A block is fully covered only if all six amenities are within 15 minutes.
92+
93+
```{code-cell} python
94+
covered_all = set(one_per_block.geoid)
95+
for type_id in basket.values():
96+
covered = set(blocks.loc[(blocks.dest_type_id == type_id) &
97+
(blocks.travel_time <= 15), "geoid"])
98+
covered_all &= covered
99+
100+
basket_pop = one_per_block.loc[one_per_block.geoid.isin(covered_all),
101+
"population"].sum()
102+
print(f"All six amenities: {100 * basket_pop / total_pop:.0f}% of residents")
103+
104+
one_per_block = one_per_block.assign(
105+
full_basket = one_per_block.geoid.isin(covered_all))
106+
one_per_block.plot(column = "full_basket", cmap = "Oranges")
107+
```
108+
109+
## Which amenity to add first
110+
111+
Count how many not-yet-covered residents are missing each amenity. The amenity the
112+
most people lack is the one to add first.
113+
114+
```{code-cell} python
115+
uncovered = set(one_per_block.geoid) - covered_all
116+
for name, type_id in basket.items():
117+
covered = set(blocks.loc[(blocks.dest_type_id == type_id) &
118+
(blocks.travel_time <= 15), "geoid"])
119+
lacking = uncovered - covered
120+
pop = one_per_block.loc[one_per_block.geoid.isin(lacking), "population"].sum()
121+
print(f"{name:11} {pop:6.0f} residents would gain access")
122+
```
123+
124+
Map the uncovered blocks to see where new amenities would do the most good.
125+
126+
```{code-cell} python
127+
one_per_block = one_per_block.assign(
128+
uncovered = one_per_block.geoid.isin(uncovered))
129+
one_per_block.plot(column = "uncovered", cmap = "Blues")
130+
```

0 commit comments

Comments
 (0)