1- # closecity — Python client for the Close API
1+ # closecity
22
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 ) .
3+ Python client for the Close API. Get travel times from every US census block to
4+ nearby places, on foot, by bike, and by public transit. This is the data behind
5+ [ close.city ] ( https://close.city ) , read over the [ Close API] ( https://api.close.city ) .
66
77** Documentation:** https://henryspatialanalysis.github.io/closecity-python/
88
9+ ## Install
10+
911``` bash
1012pip install closecity
11- pip install " closecity[geo ]" # + GeoPandas output (to_geopandas)
13+ pip install " closecity[tiger ]" # to auto-download census-block boundaries
1214```
1315
14- ## Quickstart
16+ This pulls in ` httpx ` and ` geopandas ` , so feature methods return GeoDataFrames out
17+ of the box.
1518
16- ``` python
17- from closecity import Client
19+ ## A first call
1820
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:
21+ You make requests through a client. Feature results come back as GeoDataFrames, so
22+ you can map them right away.
3123
3224``` python
3325from closecity import Client
34- close = Client()
35- print (close.modes().data[" modes" ])
36- print (close.last_updated().data[" last_updated" ])
3726
38- # Resolve a city name to its census place GEOID + centroid:
39- print (close.places(" Providence" ).data[" places" ][0 ])
40- ```
27+ # The key (ck_live_ or ck_test_) comes from https://account.close.city
28+ close = Client(" ck_live_your_key" ) # use your own key here
4129
42- ## Pagination
30+ # Grocery stores within a 1.5 km walk of a point, as points:
31+ groceries = close.pois_search(lat = 41.823 , lon = - 71.412 , radius_m = 1500 , type = 30 )
32+ groceries.plot()
33+ ```
4334
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:
35+ Catalog and lookup routes are free and need no key:
4636
4737``` 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" )
38+ close = Client()
39+ close.modes().data[" modes" ] # walk, bike, transit
40+ close.places(" Providence" ).data[" places" ][0 ] # a city name to its GEOID and centre
5541```
5642
57- Cursors are opaque and signed — never construct or modify them.
43+ ## Words you will see
5844
59- ## Conditional requests (free revalidation)
45+ A few terms come up throughout the API:
6046
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:
47+ - ** Census block.** The smallest area the Census Bureau publishes. Each one has a
48+ 15-digit id called a ** GEOID** .
49+ - ** Destination type.** A category of place, such as grocery stores or libraries.
50+ Each type has a numeric id. Look them up with ` close.destination_types() ` .
51+ - ** Mode.** How someone travels: walk, bike, or transit.
52+ - ** Isochrone.** The area you can reach from a point within a time limit, as a
53+ polygon.
54+ - ** Catchment.** The reverse of an isochrone: every block that can reach a place.
6355
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- ```
56+ ## Spatial output, on or off
7057
71- ## Isochrones
58+ Feature methods return GeoDataFrames by default. Block routes join census-block
59+ boundaries for you, using ` pygris ` (the ` tiger ` extra) to download them once. To work
60+ with the raw data instead, build the client with ` spatial=False ` :
7261
7362``` 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" ])
63+ close = Client(" ck_live_your_key" , spatial = False ) # use your own key here
64+ for poi in close.pois_search(lat = 41.823 , lon = - 71.412 , radius_m = 1500 ):
65+ print (poi[" name" ])
7866```
7967
80- ## Errors
68+ ## Handling errors
8169
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 ` :
70+ Problem responses become typed exceptions. Catch a specific one, or the
71+ ` CloseAPIError ` base.
8572
8673``` python
87- from closecity import CloseAPIError, TokensExhaustedError, RateLimitedError
74+ from closecity import TokensExhaustedError, CloseAPIError
8875
8976try :
9077 close.block_summary(" 000000000000000" )
9178except TokensExhaustedError:
92- ... # buy more tokens at account.close.city
93- except RateLimitedError as err:
94- time.sleep(err.retry_after or 1 )
79+ ...
9580except 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()
81+ print (err.status, err.slug)
10882```
10983
11084## Reference
@@ -116,7 +90,7 @@ iso = close.isochrone(block = "440070036001010", minutes = 15).to_geopandas()
11690## Development
11791
11892``` bash
119- pip install -e ' .[dev,geo ]'
120- pytest # unit tests ( no network — httpx MockTransport)
93+ pip install -e ' .[dev]'
94+ pytest # unit tests, no network ( httpx MockTransport)
12195ruff check src tests
12296```
0 commit comments