Skip to content

Commit 8d16b3a

Browse files
Merge pull request #1 from Geocodio/sync/upstream-10.2.0
chore: sync fork with upstream batch-machine 10.2.0
2 parents 2d31d44 + 9962aa5 commit 8d16b3a

19 files changed

Lines changed: 990 additions & 41 deletions

CHANGELOG

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,14 @@
1+
2026-08-22 v10.2.0
2+
- Fix lat/lon axis flip for shapefile sources with an explicit `srs` tag https://github.com/openaddresses/batch-machine/pull/113
3+
- Extract nested zip files regardless of the conform `file` filter, cap zip entry size and nesting depth as a zip bomb guard, and skip recursing into a nested zip once the filter is already satisfied https://github.com/openaddresses/batch-machine/pull/112
4+
- Add KML support to the conform pipeline https://github.com/openaddresses/batch-machine/pull/111
5+
- Linearize curve geometries (e.g. `MULTISURFACE`) before exporting to WKT, fixing GDB parcel/building sources that error out https://github.com/openaddresses/batch-machine/pull/110
6+
- Fix `regexp` function's `replace` mode returning the unmatched field unchanged instead of an empty string https://github.com/openaddresses/batch-machine/pull/109
7+
- Collect the union of feature property keys before writing `geojson_source_to_csv` output, fixing a crash on GeoJSON sources with non-uniform feature properties https://github.com/openaddresses/batch-machine/pull/108
8+
9+
2026-08-13 v10.1.0
10+
- Support custom HTTP request headers for a source via `request.headers`, including on the file-extension pre-flight request (needed for downloads gated on Referer/etc.)
11+
112
2026-03-27 v10.0.0
213
- Upgrade base Docker image from GDAL 3.7.1 to 3.11.0 https://github.com/openaddresses/batch-machine/pull/99
314
- Update SSL CA certificates to fix download failures https://github.com/openaddresses/batch-machine/pull/98

Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
FROM ghcr.io/osgeo/gdal:alpine-normal-3.11.0
1+
FROM ghcr.io/osgeo/gdal:alpine-normal-3.11.0@sha256:edf2793e0f1ceb74ab12a1d85bd3404b541a113720fbc1e032613e7df2774f7c
22

33
RUN apk add --no-cache nodejs yarn git python3-dev py3-pip \
44
make sqlite-dev zlib-dev geos-dev \

README.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ Supported layer types are `addresses`, `parcels`, `buildings`, and `centerlines`
6363

6464
Review https://github.com/openaddresses/openaddresses/blob/master/CONTRIBUTING.md for input json syntax.
6565

66-
Supported conform formats include `shapefile`, `geojson`, `csv`, `xml`, `gdb`, and `gpkg`.
66+
Supported conform formats include `shapefile`, `geojson`, `csv`, `xml`, `gdb`, `gpkg`, and `kml` (2D Point placemarks with simple `ExtendedData` attributes are verified; other geometry types and `Schema`-typed attributes go through the same GDAL driver but are untested; altitude/3D coordinates and KMZ are not supported).
6767

6868
## Geocodio fork notes
6969

@@ -77,3 +77,9 @@ diverge from upstream and must be preserved across upstream merges:
7777
conformed output row, so named-but-unnumbered properties stay individually
7878
addressable. It is a geocodio-specific addition and is not part of the
7979
upstream OpenAddresses schema.
80+
- **Non-finite geometry is never written out** — ESRI services report a null
81+
point geometry as the string `"NaN"`, which reached the output as
82+
`"coordinates": [NaN, NaN]`. Invalid GeoJSON per RFC 7946, and unparseable by
83+
strict decoders. `openaddr/cache.py` now skips those features on download and
84+
`openaddr/conform.py` treats any non-finite WKT coordinate as no geometry,
85+
with `allow_nan=False` on the output writer as a backstop.

openaddr/VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
10.0.0
1+
10.2.0

openaddr/__init__.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,8 +75,10 @@ def cache(source_config, destdir, extras):
7575
source_urls = [source_urls]
7676

7777
protocol_string = source_config.data_source.get('protocol')
78+
request_settings = source_config.data_source.get('request') or {}
79+
source_headers = request_settings.get('headers') or {}
7880

79-
task = DownloadTask.from_protocol_string(protocol_string, source_config)
81+
task = DownloadTask.from_protocol_string(protocol_string, source_config, headers=source_headers)
8082
downloaded_files = task.download(source_urls, workdir, source_config)
8183

8284
# FIXME: I wrote the download stuff to assume multiple files because
@@ -128,6 +130,10 @@ def conform(source_config, destdir, extras, disable_centroids=False):
128130
if not isinstance(source_urls, list):
129131
source_urls = [source_urls]
130132

133+
# source_config.data_source['request'] is intentionally not passed here:
134+
# this re-downloads from the OA-owned cache artifact (S3), not the
135+
# contributor's original host, so contributor-supplied headers don't
136+
# apply.
131137
task1 = URLDownloadTask(source_config.data_source_name)
132138
downloaded_path = task1.download(source_urls, workdir, source_config)
133139
_L.info("Downloaded to %s", downloaded_path)

openaddr/cache.py

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,13 @@ def traverse(item):
4747
else:
4848
yield item
4949

50+
def coordinate_is_usable(value):
51+
"Test a single coordinate for a finite number; ESRI reports null geometry as the string \"NaN\""
52+
try:
53+
return math.isfinite(float(value))
54+
except (TypeError, ValueError):
55+
return False
56+
5057
def request(method, url, **kwargs):
5158
if urlparse(url).scheme == 'ftp':
5259
if method != 'GET':
@@ -130,22 +137,23 @@ def __init__(self, source_prefix, params={}, headers={}):
130137

131138

132139
@classmethod
133-
def from_protocol_string(clz, protocol_string, source_prefix=None):
140+
def from_protocol_string(clz, protocol_string, source_prefix=None, headers=None):
141+
headers = headers or {}
134142
if protocol_string.lower() == 'http':
135-
return URLDownloadTask(source_prefix)
143+
return URLDownloadTask(source_prefix, headers=headers)
136144
elif protocol_string.lower() == 'file':
137-
return URLDownloadTask(source_prefix)
145+
return URLDownloadTask(source_prefix, headers=headers)
138146
elif protocol_string.lower() == 'ftp':
139-
return URLDownloadTask(source_prefix)
147+
return URLDownloadTask(source_prefix, headers=headers)
140148
elif protocol_string.lower() == 'esri':
141-
return EsriRestDownloadTask(source_prefix)
149+
return EsriRestDownloadTask(source_prefix, headers=headers)
142150
else:
143151
raise KeyError("I don't know how to extract for protocol {}".format(protocol_string))
144152

145153
def download(self, source_urls, workdir, source_config):
146154
raise NotImplementedError()
147155

148-
def guess_url_file_extension(url):
156+
def guess_url_file_extension(url, headers=None):
149157
''' Get a filename extension for a URL using various hints.
150158
'''
151159
scheme, _, path, _, query, _ = urlparse(url)
@@ -172,7 +180,7 @@ def guess_url_file_extension(url):
172180
# Get a dictionary of headers and a few bytes of content from the URL.
173181
#
174182
if scheme in ('http', 'https'):
175-
response = request('GET', url, stream=True)
183+
response = request('GET', url, headers=headers or {}, stream=True)
176184
handle, file = mkstemp()
177185

178186
for chunk in response.iter_content(chunk_size=8192):
@@ -256,7 +264,7 @@ def get_file_path(self, url, dir_path):
256264
hash = sha1((host + path_base).encode('utf-8'))
257265
name_base = u'{}-{}'.format(self.source_prefix, hash.hexdigest()[:8])
258266

259-
path_ext = guess_url_file_extension(url)
267+
path_ext = guess_url_file_extension(url, self.headers)
260268
_L.debug(u'Guessed {}{} for {}'.format(name_base, path_ext, url))
261269

262270
return os.path.join(dir_path, name_base + path_ext)
@@ -391,7 +399,7 @@ def download(self, source_urls, workdir, source_config):
391399
_L.debug("File exists %s", file_path)
392400
continue
393401

394-
downloader = EsriDumper(source_url, parent_logger=_L, timeout=300)
402+
downloader = EsriDumper(source_url, parent_logger=_L, timeout=300, extra_headers=self.headers)
395403

396404
metadata = downloader.get_metadata()
397405

@@ -439,8 +447,8 @@ def download(self, source_urls, workdir, source_config):
439447

440448
if not geom:
441449
raise TypeError("No geometry parsed")
442-
if any((isinstance(g, float) and math.isnan(g)) for g in traverse(geom)):
443-
raise TypeError("Geometry has NaN coordinates")
450+
if any(not coordinate_is_usable(c) for c in traverse(geom.get('coordinates'))):
451+
raise TypeError("Geometry has non-finite coordinates")
444452

445453
shp = shape(geom)
446454
row[GEOM_FIELDNAME] = shp.wkt

0 commit comments

Comments
 (0)