bcdata 0.13.0__tar.gz → 0.14.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: bcdata
3
- Version: 0.13.0
3
+ Version: 0.14.0
4
4
  Summary: Download open data, monitor and report on changes
5
5
  Author-email: Simon Norris <snorris@hillcrestgeo.ca>
6
6
  Project-URL: Homepage, https://github.com/smnorris/bcdata
@@ -123,7 +123,8 @@ AERODROME_STATUS AIRCRAFT_ACCESS_IND AIRPORT_NAME
123
123
  ```
124
124
 
125
125
  ### CLI
126
- Commands available via the bcdata command line interface are documented with the `--help` option:
126
+
127
+ Commands available via the bcdata command line interface are documented with the --help option
127
128
 
128
129
  ```
129
130
 
@@ -136,7 +137,7 @@ Options:
136
137
  --help Show this message and exit.
137
138
 
138
139
  Commands:
139
- bc2pg Download a DataBC WFS layer to postgres
140
+ bc2pg Load a DataBC WFS layer to a postgres db
140
141
  cat Write DataBC features to stdout as GeoJSON feature objects.
141
142
  dem Dump BC DEM to TIFF
142
143
  dump Write DataBC features to stdout as GeoJSON feature collection.
@@ -151,7 +152,7 @@ $ bcdata bc2pg --help
151
152
 
152
153
  Usage: bcdata bc2pg [OPTIONS] DATASET
153
154
 
154
- Download a DataBC WFS layer to postgres
155
+ Load a DataBC WFS layer to a postgres db
155
156
 
156
157
  $ bcdata bc2pg whse_imagery_and_base_maps.gsr_airports_svw
157
158
 
@@ -190,16 +191,18 @@ Usage: bcdata cat [OPTIONS] DATASET
190
191
 
191
192
  Options:
192
193
  --query TEXT A valid CQL or ECQL query
194
+ -c, --count INTEGER Number of features to request and dump
193
195
  --bounds TEXT Bounds: "left bottom right top" or "[left,
194
196
  bottom, right, top]". Coordinates are BC
195
197
  Albers (default) or --bounds_crs
198
+ --bounds-crs, --bounds_crs TEXT
199
+ CRS of provided bounds
196
200
  --indent INTEGER Indentation level for JSON output
197
201
  --compact / --not-compact Use compact separators (',', ':').
198
202
  --dst-crs, --dst_crs TEXT Destination CRS
199
203
  -s, --sortby TEXT Name of sort field
200
- --bounds-crs, --bounds_crs TEXT
201
- CRS of provided bounds
202
204
  -l, --lowercase Write column/properties names as lowercase
205
+ -m, --promote-to-multi Promote features to multipart
203
206
  -v, --verbose Increase verbosity.
204
207
  -q, --quiet Decrease verbosity.
205
208
  --help Show this message and exit.
@@ -219,9 +222,8 @@ Options:
219
222
  --bounds TEXT Bounds: "left bottom right top" or "[left,
220
223
  bottom, right, top]". Coordinates are BC
221
224
  Albers (default) or --bounds_crs [required]
222
- --dst-crs, --dst_crs TEXT Destination CRS
223
- --bounds-crs, --bounds_crs TEXT
224
- CRS of provided bounds
225
+ --dst-crs TEXT CRS of output file
226
+ --bounds-crs TEXT CRS of provided bounds
225
227
  -r, --resolution INTEGER
226
228
  -a, --align Align provided bounds to provincial standard
227
229
  -i, --interpolation [nearest|bilinear|bicubic]
@@ -243,18 +245,20 @@ Usage: bcdata dump [OPTIONS] DATASET
243
245
  $ bcdata dump bc-airports --query "AIRPORT_NAME='Victoria Harbour (Shoal Point) Heliport'"
244
246
  $ bcdata dump bc-airports --bounds xmin ymin xmax ymax
245
247
 
246
- It can also be combined to read bounds of a feature dataset using Fiona:
248
+ It can also be combined to read bounds of a feature dataset using Fiona: 
247
249
  $ bcdata dump bc-airports --bounds $(fio info aoi.shp --bounds)
248
250
 
249
251
  Options:
250
252
  --query TEXT A valid CQL or ECQL query
251
- -o, --out_file TEXT Output file
253
+ -c, --count INTEGER Number of features to request and dump
252
254
  --bounds TEXT Bounds: "left bottom right top" or "[left,
253
255
  bottom, right, top]". Coordinates are BC
254
256
  Albers (default) or --bounds_crs
255
257
  --bounds-crs, --bounds_crs TEXT
256
258
  CRS of provided bounds
259
+ -s, --sortby TEXT Name of sort field
257
260
  -l, --lowercase Write column/properties names as lowercase
261
+ -m, --promote-to-multi Promote features to multipart
258
262
  -v, --verbose Increase verbosity.
259
263
  -q, --quiet Decrease verbosity.
260
264
  --help Show this message and exit.
@@ -291,6 +295,7 @@ Usage: bcdata list [OPTIONS]
291
295
  List DataBC layers available via WFS
292
296
 
293
297
  Options:
298
+ -r, --refresh Refresh the cached list
294
299
  --help Show this message and exit.
295
300
  ```
296
301
 
@@ -91,7 +91,8 @@ AERODROME_STATUS AIRCRAFT_ACCESS_IND AIRPORT_NAME
91
91
  ```
92
92
 
93
93
  ### CLI
94
- Commands available via the bcdata command line interface are documented with the `--help` option:
94
+
95
+ Commands available via the bcdata command line interface are documented with the --help option
95
96
 
96
97
  ```
97
98
 
@@ -104,7 +105,7 @@ Options:
104
105
  --help Show this message and exit.
105
106
 
106
107
  Commands:
107
- bc2pg Download a DataBC WFS layer to postgres
108
+ bc2pg Load a DataBC WFS layer to a postgres db
108
109
  cat Write DataBC features to stdout as GeoJSON feature objects.
109
110
  dem Dump BC DEM to TIFF
110
111
  dump Write DataBC features to stdout as GeoJSON feature collection.
@@ -119,7 +120,7 @@ $ bcdata bc2pg --help
119
120
 
120
121
  Usage: bcdata bc2pg [OPTIONS] DATASET
121
122
 
122
- Download a DataBC WFS layer to postgres
123
+ Load a DataBC WFS layer to a postgres db
123
124
 
124
125
  $ bcdata bc2pg whse_imagery_and_base_maps.gsr_airports_svw
125
126
 
@@ -158,16 +159,18 @@ Usage: bcdata cat [OPTIONS] DATASET
158
159
 
159
160
  Options:
160
161
  --query TEXT A valid CQL or ECQL query
162
+ -c, --count INTEGER Number of features to request and dump
161
163
  --bounds TEXT Bounds: "left bottom right top" or "[left,
162
164
  bottom, right, top]". Coordinates are BC
163
165
  Albers (default) or --bounds_crs
166
+ --bounds-crs, --bounds_crs TEXT
167
+ CRS of provided bounds
164
168
  --indent INTEGER Indentation level for JSON output
165
169
  --compact / --not-compact Use compact separators (',', ':').
166
170
  --dst-crs, --dst_crs TEXT Destination CRS
167
171
  -s, --sortby TEXT Name of sort field
168
- --bounds-crs, --bounds_crs TEXT
169
- CRS of provided bounds
170
172
  -l, --lowercase Write column/properties names as lowercase
173
+ -m, --promote-to-multi Promote features to multipart
171
174
  -v, --verbose Increase verbosity.
172
175
  -q, --quiet Decrease verbosity.
173
176
  --help Show this message and exit.
@@ -187,9 +190,8 @@ Options:
187
190
  --bounds TEXT Bounds: "left bottom right top" or "[left,
188
191
  bottom, right, top]". Coordinates are BC
189
192
  Albers (default) or --bounds_crs [required]
190
- --dst-crs, --dst_crs TEXT Destination CRS
191
- --bounds-crs, --bounds_crs TEXT
192
- CRS of provided bounds
193
+ --dst-crs TEXT CRS of output file
194
+ --bounds-crs TEXT CRS of provided bounds
193
195
  -r, --resolution INTEGER
194
196
  -a, --align Align provided bounds to provincial standard
195
197
  -i, --interpolation [nearest|bilinear|bicubic]
@@ -211,18 +213,20 @@ Usage: bcdata dump [OPTIONS] DATASET
211
213
  $ bcdata dump bc-airports --query "AIRPORT_NAME='Victoria Harbour (Shoal Point) Heliport'"
212
214
  $ bcdata dump bc-airports --bounds xmin ymin xmax ymax
213
215
 
214
- It can also be combined to read bounds of a feature dataset using Fiona:
216
+ It can also be combined to read bounds of a feature dataset using Fiona: 
215
217
  $ bcdata dump bc-airports --bounds $(fio info aoi.shp --bounds)
216
218
 
217
219
  Options:
218
220
  --query TEXT A valid CQL or ECQL query
219
- -o, --out_file TEXT Output file
221
+ -c, --count INTEGER Number of features to request and dump
220
222
  --bounds TEXT Bounds: "left bottom right top" or "[left,
221
223
  bottom, right, top]". Coordinates are BC
222
224
  Albers (default) or --bounds_crs
223
225
  --bounds-crs, --bounds_crs TEXT
224
226
  CRS of provided bounds
227
+ -s, --sortby TEXT Name of sort field
225
228
  -l, --lowercase Write column/properties names as lowercase
229
+ -m, --promote-to-multi Promote features to multipart
226
230
  -v, --verbose Increase verbosity.
227
231
  -q, --quiet Decrease verbosity.
228
232
  --help Show this message and exit.
@@ -259,6 +263,7 @@ Usage: bcdata list [OPTIONS]
259
263
  List DataBC layers available via WFS
260
264
 
261
265
  Options:
266
+ -r, --refresh Refresh the cached list
262
267
  --help Show this message and exit.
263
268
  ```
264
269
 
@@ -4,10 +4,8 @@ from .bc2pg import bc2pg as bc2pg
4
4
  from .bcdc import get_table_definition as get_table_definition
5
5
  from .bcdc import get_table_name as get_table_name
6
6
  from .wcs import get_dem as get_dem
7
- from .wfs import define_requests as define_requests
8
7
  from .wfs import get_count as get_count
9
8
  from .wfs import get_data as get_data
10
- from .wfs import get_features as get_features
11
9
  from .wfs import get_sortkey as get_sortkey
12
10
  from .wfs import list_tables as list_tables
13
11
  from .wfs import validate_name as validate_name
@@ -24,4 +22,4 @@ else:
24
22
  raise Exception(f"Failed to download primary key database at {PRIMARY_KEY_DB_URL}")
25
23
  primary_keys = {}
26
24
 
27
- __version__ = "0.13.0"
25
+ __version__ = "0.14.0"
@@ -73,14 +73,8 @@ def bc2pg( # noqa: C901
73
73
  WFS = BCWFS()
74
74
 
75
75
  # define requests
76
- urls = bcdata.define_requests(
77
- dataset,
78
- query=query,
79
- bounds=bounds,
80
- bounds_crs=bounds_crs,
81
- count=count,
82
- sortby=sortby,
83
- crs="epsg:3005",
76
+ urls = WFS.define_requests(
77
+ dataset, query=query, bounds=bounds, bounds_crs=bounds_crs, count=count, sortby=sortby
84
78
  )
85
79
 
86
80
  df = None # just for tracking if first download is done by geometry type check
@@ -101,7 +95,7 @@ def bc2pg( # noqa: C901
101
95
 
102
96
  # if geometry type is not provided, determine type by making the first request
103
97
  if not geometry_type:
104
- df = WFS.make_requests(dataset=dataset, urls=[urls[0]], as_gdf=True, crs="epsg:3005", lowercase=True)
98
+ df = WFS.request_features(url=urls[0], as_gdf=True, crs="epsg:3005", lowercase=True)
105
99
  geometry_type = df.geom_type.unique()[0] # keep only the first type
106
100
  if numpy.any(df.has_z.unique()[0]): # geopandas does not include Z in geom_type string
107
101
  geometry_type = geometry_type + "Z"
@@ -110,9 +104,8 @@ def bc2pg( # noqa: C901
110
104
  # (in case all entrys with geom are near the bottom)
111
105
  if not geometry_type:
112
106
  if not urls[-1] == urls[0]:
113
- df_temp = WFS.make_requests(
114
- dataset=dataset,
115
- urls=[urls[-1]],
107
+ df_temp = WFS.request_features(
108
+ url=urls[-1],
116
109
  as_gdf=True,
117
110
  crs="epsg:3005",
118
111
  lowercase=True,
@@ -165,7 +158,7 @@ def bc2pg( # noqa: C901
165
158
  for n, url in enumerate(urls):
166
159
  # if first url not downloaded above when checking geom type, do now
167
160
  if df is None:
168
- df = WFS.make_requests(dataset=dataset, urls=[url], as_gdf=True, crs="epsg:3005", lowercase=True)
161
+ df = WFS.request_features(url=url, as_gdf=True, crs="epsg:3005", lowercase=True)
169
162
  # tidy the resulting dataframe
170
163
  df = df.rename_geometry("geom")
171
164
  # lowercasify
@@ -6,12 +6,6 @@ import sys
6
6
 
7
7
  import click
8
8
  from cligj import compact_opt, indent_opt, quiet_opt, verbose_opt
9
- from shapely.geometry.linestring import LineString
10
- from shapely.geometry.multilinestring import MultiLineString
11
- from shapely.geometry.multipoint import MultiPoint
12
- from shapely.geometry.multipolygon import MultiPolygon
13
- from shapely.geometry.point import Point
14
- from shapely.geometry.polygon import Polygon
15
9
 
16
10
  import bcdata
17
11
  from bcdata.database import Database
@@ -27,6 +21,7 @@ def configure_logging(verbosity):
27
21
  def complete_dataset_names(ctx, param, incomplete):
28
22
  return [k for k in bcdata.list_tables() if k.startswith(incomplete)]
29
23
 
24
+
30
25
  # bounds handling direct from rasterio
31
26
  # https://github.com/mapbox/rasterio/blob/master/rasterio/rio/options.py
32
27
  # https://github.com/mapbox/rasterio/blob/master/rasterio/rio/clip.py
@@ -201,7 +196,13 @@ def dem(
201
196
  "--query",
202
197
  help="A valid CQL or ECQL query",
203
198
  )
204
- @click.option("--out_file", "-o", help="Output file")
199
+ @click.option(
200
+ "--count",
201
+ "-c",
202
+ default=None,
203
+ type=int,
204
+ help="Number of features to request and dump",
205
+ )
205
206
  @bounds_opt
206
207
  @click.option(
207
208
  "--bounds-crs",
@@ -209,17 +210,20 @@ def dem(
209
210
  help="CRS of provided bounds",
210
211
  default="EPSG:3005",
211
212
  )
213
+ @click.option("--sortby", "-s", help="Name of sort field")
214
+ @lowercase_opt
212
215
  @click.option(
213
- "--no-clean",
214
- "-nc",
215
- help="Do not do any data standardization",
216
+ "--promote-to-multi",
217
+ "-m",
218
+ help="Promote features to multipart",
216
219
  is_flag=True,
217
- default=True,
220
+ default=False,
218
221
  )
219
- @lowercase_opt
220
222
  @verbose_opt
221
223
  @quiet_opt
222
- def dump(dataset, query, out_file, bounds, bounds_crs, no_clean, lowercase, verbose, quiet):
224
+ def dump(
225
+ dataset, query, count, bounds, bounds_crs, sortby, lowercase, promote_to_multi, verbose, quiet
226
+ ):
223
227
  """Write DataBC features to stdout as GeoJSON feature collection.
224
228
 
225
229
  \b
@@ -235,19 +239,19 @@ def dump(dataset, query, out_file, bounds, bounds_crs, no_clean, lowercase, verb
235
239
  verbosity = verbose - quiet
236
240
  configure_logging(verbosity)
237
241
  table = bcdata.validate_name(dataset)
238
- if no_clean:
239
- clean = False
240
- else:
241
- clean = True
242
242
  data = bcdata.get_data(
243
- table, query=query, bounds=bounds, bounds_crs=bounds_crs, lowercase=lowercase, clean=clean
243
+ table,
244
+ query=query,
245
+ count=count,
246
+ bounds=bounds,
247
+ bounds_crs=bounds_crs,
248
+ sortby=sortby,
249
+ lowercase=lowercase,
250
+ promote_to_multi=promote_to_multi,
251
+ as_gdf=False,
244
252
  )
245
- if out_file:
246
- with open(out_file, "w") as sink:
247
- sink.write(json.dumps(data))
248
- else:
249
- sink = click.get_text_stream("stdout")
250
- sink.write(json.dumps(data))
253
+ sink = click.get_text_stream("stdout")
254
+ sink.write(json.dumps(data))
251
255
 
252
256
 
253
257
  @cli.command()
@@ -256,23 +260,38 @@ def dump(dataset, query, out_file, bounds, bounds_crs, no_clean, lowercase, verb
256
260
  "--query",
257
261
  help="A valid CQL or ECQL query",
258
262
  )
263
+ @click.option(
264
+ "--count",
265
+ "-c",
266
+ default=None,
267
+ type=int,
268
+ help="Number of features to request and dump",
269
+ )
259
270
  @bounds_opt
260
- @indent_opt
261
- @compact_opt
262
- @dst_crs_opt
263
- @click.option("--sortby", "-s", help="Name of sort field")
264
271
  @click.option(
265
272
  "--bounds-crs",
266
273
  "--bounds_crs",
267
274
  help="CRS of provided bounds",
268
275
  default="EPSG:3005",
269
276
  )
277
+ @indent_opt
278
+ @compact_opt
279
+ @dst_crs_opt
280
+ @click.option("--sortby", "-s", help="Name of sort field")
270
281
  @lowercase_opt
282
+ @click.option(
283
+ "--promote-to-multi",
284
+ "-m",
285
+ help="Promote features to multipart",
286
+ is_flag=True,
287
+ default=False,
288
+ )
271
289
  @verbose_opt
272
290
  @quiet_opt
273
291
  def cat(
274
292
  dataset,
275
293
  query,
294
+ count,
276
295
  bounds,
277
296
  bounds_crs,
278
297
  indent,
@@ -280,6 +299,7 @@ def cat(
280
299
  dst_crs,
281
300
  sortby,
282
301
  lowercase,
302
+ promote_to_multi,
283
303
  verbose,
284
304
  quiet,
285
305
  ):
@@ -295,23 +315,23 @@ def cat(
295
315
  if compact:
296
316
  dump_kwds["separators"] = (",", ":")
297
317
  table = bcdata.validate_name(dataset)
298
- for feat in bcdata.get_features(
318
+ WFS = bcdata.wfs.BCWFS()
319
+ for url in WFS.define_requests(
299
320
  table,
300
321
  query=query,
322
+ count=count,
301
323
  bounds=bounds,
302
324
  bounds_crs=bounds_crs,
303
- sortby=sortby,
304
- crs=dst_crs,
305
- lowercase=lowercase,
306
325
  ):
307
- click.echo(json.dumps(feat, **dump_kwds))
308
-
309
-
310
- @cli.command()
311
- @verbose_opt
312
- @quiet_opt
313
- def clear_cache(verbose, quiet):
314
- bcdata.clear_cache()
326
+ featurecollection = WFS.request_features(
327
+ url=url,
328
+ as_gdf=False,
329
+ lowercase=lowercase,
330
+ crs=dst_crs,
331
+ promote_to_multi=promote_to_multi,
332
+ )
333
+ for feat in featurecollection["features"]:
334
+ click.echo(json.dumps(feat, **dump_kwds))
315
335
 
316
336
 
317
337
  @cli.command()
@@ -10,6 +10,7 @@ from pathlib import Path
10
10
  from urllib.parse import urlencode
11
11
 
12
12
  import geopandas as gpd
13
+ import pandas as pd
13
14
  import requests
14
15
  import stamina
15
16
  from owslib.feature import schema as wfs_schema
@@ -30,26 +31,22 @@ if not sys.warnoptions:
30
31
  log = logging.getLogger(__name__)
31
32
 
32
33
 
33
- def ensure_single_geometry_type(df):
34
- """If mix of single/multi part geometries are present, promote all geometries to multipart"""
35
- geomtypes = sorted(
36
- [t.upper() for t in df.geometry.geom_type.dropna(axis=0, how="all").unique()], key=len
37
- )
38
- if len(geomtypes) > 1 and geomtypes[1] == "MULTI" + geomtypes[0]:
39
- df.geometry = [
40
- MultiPoint([feature]) if isinstance(feature, Point) else feature
41
- for feature in df.geometry
42
- ]
43
- df.geometry = [
44
- MultiLineString([feature]) if isinstance(feature, LineString) else feature
45
- for feature in df.geometry
46
- ]
47
- df.geometry = [
48
- MultiPolygon([feature]) if isinstance(feature, Polygon) else feature
49
- for feature in df.geometry
50
- ]
34
+ def promote_gdf_to_multi(df):
35
+ """Promote all features to multipart"""
36
+ df.geometry = [
37
+ MultiPoint([feature]) if isinstance(feature, Point) else feature for feature in df.geometry
38
+ ]
39
+ df.geometry = [
40
+ MultiLineString([feature]) if isinstance(feature, LineString) else feature
41
+ for feature in df.geometry
42
+ ]
43
+ df.geometry = [
44
+ MultiPolygon([feature]) if isinstance(feature, Polygon) else feature
45
+ for feature in df.geometry
46
+ ]
51
47
  return df
52
48
 
49
+
53
50
  class ServiceException(Exception):
54
51
  pass
55
52
 
@@ -158,7 +155,7 @@ class BCWFS(object):
158
155
 
159
156
  @stamina.retry(on=requests.HTTPError, timeout=60)
160
157
  def _request_features(self, url, silent=False):
161
- """Submit a getfeature request to DataBC WFS and return features"""
158
+ """Submit a getfeature request to DataBC WFS and return feature collection"""
162
159
  r = requests.get(url, headers=self.request_headers)
163
160
  if not silent:
164
161
  log.info(r.url)
@@ -176,6 +173,26 @@ class BCWFS(object):
176
173
  r.raise_for_status()
177
174
  return r.json()["features"]
178
175
 
176
+ @stamina.retry(on=requests.HTTPError, timeout=60)
177
+ def _request_featurecollection(self, url, silent=False):
178
+ """Submit a getfeature request to DataBC WFS and return feature collection"""
179
+ r = requests.get(url, headers=self.request_headers)
180
+ if not silent:
181
+ log.info(r.url)
182
+ else:
183
+ log.debug(r.url)
184
+ if r.status_code in [400, 401, 404]:
185
+ log.error(f"HTTP error {r.status_code}")
186
+ log.error(f"Response headers: {r.headers}")
187
+ log.error(f"Response text: {r.text}")
188
+ raise ServiceException(r.text) # presumed request error
189
+ elif r.status_code in [500, 502, 503, 504]: # presumed serivce error, retry
190
+ log.warning(f"HTTP error: {r.status_code}")
191
+ log.warning(f"Response headers: {r.headers}")
192
+ log.warning(f"Response text: {r.text}")
193
+ r.raise_for_status()
194
+ return r.json()
195
+
179
196
  def build_bounds_filter(self, query, bounds, bounds_crs, geom_column):
180
197
  """The bbox param shortcut is mutually exclusive with CQL_FILTER,
181
198
  combine query and bounds into a single CQL_FILTER expression
@@ -274,7 +291,6 @@ class BCWFS(object):
274
291
  self,
275
292
  dataset,
276
293
  query=None,
277
- crs="epsg:4326",
278
294
  bounds=None,
279
295
  bounds_crs="EPSG:3005",
280
296
  count=None,
@@ -342,7 +358,7 @@ class BCWFS(object):
342
358
  "request": "GetFeature",
343
359
  "typeName": table,
344
360
  "outputFormat": "json",
345
- "SRSNAME": crs,
361
+ "SRSNAME": "EPSG:4326",
346
362
  }
347
363
  if sortby:
348
364
  request["sortby"] = sortby.upper()
@@ -364,133 +380,79 @@ class BCWFS(object):
364
380
  urls.append(self.wfs_url + "?" + urlencode(request, doseq=True))
365
381
  return urls
366
382
 
367
- def make_requests(
368
- self, dataset, urls, as_gdf=False, crs="epsg4326", lowercase=False, silent=False, clean=True
383
+ def request_features(
384
+ self,
385
+ url,
386
+ as_gdf=False,
387
+ crs="EPSG:4326",
388
+ lowercase=False,
389
+ promote_to_multi=False,
369
390
  ):
370
- """turn urls into data"""
371
- # loop through urls
372
- results = []
373
- for url in urls:
374
- results.append(self._request_features(url, silent))
375
- outjson = dict(type="FeatureCollection", features=[])
376
- for result in results:
377
- outjson["features"] += result
378
-
379
- # if specified, lowercasify all properties
380
- if lowercase:
381
- for feature in outjson["features"]:
382
- feature["properties"] = {
383
- k.lower(): v for k, v in feature["properties"].items()
384
- }
385
-
386
- # load to geodataframe, standardize data slightly
387
- if len(outjson["features"]) > 0:
388
- gdf = gpd.GeoDataFrame.from_features(outjson)
389
- gdf.crs = crs
390
- # minor data cleaning as default
391
- if clean:
392
- if gdf.geometry.name != "geometry":
393
- gdf = gdf.rename_geometry("geometry")
394
- gdf = ensure_single_geometry_type(gdf)
395
- table_definition = bcdata.get_table_definition(dataset)
396
- column_names = [
397
- c["column_name"]
398
- for c in table_definition["schema"]
399
- if c["column_name"] not in ["FEATURE_AREA_SQM", "FEATURE_LENGTH_M"]
400
- and c["data_type"] in ["NUMBER", "VARCHAR2", "DATE"]
401
- ]
402
- if lowercase:
403
- column_names = [c.lower() for c in column_names]
404
- gdf = gdf[column_names + ["geometry"]]
391
+ """Make a request to WFS and return data as a FeatureCollection or a GeoDataFrame"""
392
+ # get the data
393
+ featurecollection = self._request_featurecollection(url)
394
+
395
+ # load to gdf for reprojection/minor data cleaning
396
+ if len(featurecollection["features"]) > 0:
397
+ gdf = gpd.GeoDataFrame.from_features(featurecollection)
398
+ gdf = gdf.set_crs("EPSG:4326")
399
+ if crs != "EPSG:4326":
400
+ gdf = gdf.to_crs(crs)
401
+ if gdf.geometry.name != "geometry":
402
+ gdf = gdf.rename_geometry("geometry")
403
+ if lowercase:
404
+ gdf.columns = [c.lower() for c in gdf.columns]
405
+ if promote_to_multi:
406
+ gdf = promote_gdf_to_multi(gdf)
405
407
  else:
406
408
  gdf = gpd.GeoDataFrame()
407
409
 
408
410
  if as_gdf:
409
411
  return gdf
410
-
411
412
  else:
412
413
  return json.loads(gdf.to_json())
413
414
 
414
- def get_data(
415
- self,
416
- dataset,
417
- query=None,
418
- crs="epsg:4326",
419
- bounds=None,
420
- bounds_crs="epsg:3005",
421
- count=None,
422
- sortby=None,
423
- as_gdf=False,
424
- lowercase=False,
425
- clean=True
426
- ):
427
- """Request features from DataBC WFS and return GeoJSON featurecollection or geodataframe"""
428
- dataset = self.validate_name(dataset)
429
- urls = self.define_requests(
430
- dataset,
431
- query=query,
432
- crs=crs,
433
- bounds=bounds,
434
- bounds_crs=bounds_crs,
435
- count=count,
436
- sortby=sortby,
437
- )
438
- return self.make_requests(dataset, urls, as_gdf=as_gdf, crs=crs, lowercase=lowercase, clean=clean)
439
-
440
- def get_features(
441
- self,
442
- dataset,
443
- query=None,
444
- crs="epsg:4326",
445
- bounds=None,
446
- bounds_crs="epsg:3005",
447
- count=None,
448
- sortby=None,
449
- lowercase=False,
450
- check_count=True,
451
- ):
452
- """Yield features from DataBC WFS"""
453
- urls = self.define_requests(
454
- dataset,
455
- query=query,
456
- crs=crs,
457
- bounds=bounds,
458
- bounds_crs=bounds_crs,
459
- count=count,
460
- sortby=sortby,
461
- check_count=check_count,
462
- )
463
- for url in urls:
464
- for feature in self._request_features(url):
465
- if lowercase:
466
- feature["properties"] = {k.lower(): v for k, v in feature["properties"].items()}
467
- yield feature
468
-
469
-
470
- # abstract away the WFS object
471
-
472
415
 
473
- def define_requests(
416
+ def get_data(
474
417
  dataset,
475
418
  query=None,
476
419
  crs="epsg:4326",
477
420
  bounds=None,
478
- bounds_crs="EPSG:3005",
421
+ bounds_crs="epsg:3005",
479
422
  count=None,
480
423
  sortby=None,
481
- check_count=True,
424
+ as_gdf=False,
425
+ lowercase=False,
426
+ promote_to_multi=False,
482
427
  ):
428
+ """Request features from DataBC WFS, returning GeoJSON featurecollection or geodataframe"""
483
429
  WFS = BCWFS()
484
- return WFS.define_requests(
485
- dataset,
430
+ table = WFS.validate_name(dataset)
431
+ urls = WFS.define_requests(
432
+ table,
486
433
  query=query,
487
- crs=crs,
488
434
  bounds=bounds,
489
435
  bounds_crs=bounds_crs,
490
436
  count=count,
491
437
  sortby=sortby,
492
- check_count=check_count,
493
438
  )
439
+ results = []
440
+ for url in urls:
441
+ results.append(
442
+ WFS.request_features(
443
+ url, crs=crs, as_gdf=True, lowercase=lowercase, promote_to_multi=promote_to_multi
444
+ )
445
+ )
446
+ if len(results) > 1:
447
+ gdf = pd.concat(results)
448
+ elif len(results) == 1:
449
+ gdf = results[0]
450
+ else:
451
+ gdf = gpd.GeoDataFrame()
452
+ if as_gdf:
453
+ return gdf
454
+ else:
455
+ return json.loads(gdf.to_json())
494
456
 
495
457
 
496
458
  def get_count(dataset, query=None, bounds=None, bounds_crs="EPSG:3005"):
@@ -506,58 +468,6 @@ def get_count(dataset, query=None, bounds=None, bounds_crs="EPSG:3005"):
506
468
  )
507
469
 
508
470
 
509
- def get_data(
510
- dataset,
511
- query=None,
512
- crs="epsg:4326",
513
- bounds=None,
514
- bounds_crs="epsg:3005",
515
- count=None,
516
- sortby=None,
517
- as_gdf=False,
518
- lowercase=False,
519
- clean=True
520
- ):
521
- WFS = BCWFS()
522
- return WFS.get_data(
523
- dataset,
524
- query=query,
525
- crs=crs,
526
- bounds=bounds,
527
- bounds_crs=bounds_crs,
528
- count=count,
529
- sortby=sortby,
530
- as_gdf=as_gdf,
531
- lowercase=lowercase,
532
- clean=clean
533
- )
534
-
535
-
536
- def get_features(
537
- dataset,
538
- query=None,
539
- crs="epsg:4326",
540
- bounds=None,
541
- bounds_crs="epsg:3005",
542
- count=None,
543
- sortby=None,
544
- lowercase=False,
545
- check_count=True,
546
- ):
547
- WFS = BCWFS()
548
- return WFS.get_features(
549
- dataset,
550
- query=query,
551
- crs=crs,
552
- bounds=bounds,
553
- bounds_crs=bounds_crs,
554
- count=count,
555
- sortby=sortby,
556
- lowercase=lowercase,
557
- check_count=check_count,
558
- )
559
-
560
-
561
471
  def get_sortkey(dataset):
562
472
  WFS = BCWFS()
563
473
  table = WFS.validate_name(dataset)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: bcdata
3
- Version: 0.13.0
3
+ Version: 0.14.0
4
4
  Summary: Download open data, monitor and report on changes
5
5
  Author-email: Simon Norris <snorris@hillcrestgeo.ca>
6
6
  Project-URL: Homepage, https://github.com/smnorris/bcdata
@@ -123,7 +123,8 @@ AERODROME_STATUS AIRCRAFT_ACCESS_IND AIRPORT_NAME
123
123
  ```
124
124
 
125
125
  ### CLI
126
- Commands available via the bcdata command line interface are documented with the `--help` option:
126
+
127
+ Commands available via the bcdata command line interface are documented with the --help option
127
128
 
128
129
  ```
129
130
 
@@ -136,7 +137,7 @@ Options:
136
137
  --help Show this message and exit.
137
138
 
138
139
  Commands:
139
- bc2pg Download a DataBC WFS layer to postgres
140
+ bc2pg Load a DataBC WFS layer to a postgres db
140
141
  cat Write DataBC features to stdout as GeoJSON feature objects.
141
142
  dem Dump BC DEM to TIFF
142
143
  dump Write DataBC features to stdout as GeoJSON feature collection.
@@ -151,7 +152,7 @@ $ bcdata bc2pg --help
151
152
 
152
153
  Usage: bcdata bc2pg [OPTIONS] DATASET
153
154
 
154
- Download a DataBC WFS layer to postgres
155
+ Load a DataBC WFS layer to a postgres db
155
156
 
156
157
  $ bcdata bc2pg whse_imagery_and_base_maps.gsr_airports_svw
157
158
 
@@ -190,16 +191,18 @@ Usage: bcdata cat [OPTIONS] DATASET
190
191
 
191
192
  Options:
192
193
  --query TEXT A valid CQL or ECQL query
194
+ -c, --count INTEGER Number of features to request and dump
193
195
  --bounds TEXT Bounds: "left bottom right top" or "[left,
194
196
  bottom, right, top]". Coordinates are BC
195
197
  Albers (default) or --bounds_crs
198
+ --bounds-crs, --bounds_crs TEXT
199
+ CRS of provided bounds
196
200
  --indent INTEGER Indentation level for JSON output
197
201
  --compact / --not-compact Use compact separators (',', ':').
198
202
  --dst-crs, --dst_crs TEXT Destination CRS
199
203
  -s, --sortby TEXT Name of sort field
200
- --bounds-crs, --bounds_crs TEXT
201
- CRS of provided bounds
202
204
  -l, --lowercase Write column/properties names as lowercase
205
+ -m, --promote-to-multi Promote features to multipart
203
206
  -v, --verbose Increase verbosity.
204
207
  -q, --quiet Decrease verbosity.
205
208
  --help Show this message and exit.
@@ -219,9 +222,8 @@ Options:
219
222
  --bounds TEXT Bounds: "left bottom right top" or "[left,
220
223
  bottom, right, top]". Coordinates are BC
221
224
  Albers (default) or --bounds_crs [required]
222
- --dst-crs, --dst_crs TEXT Destination CRS
223
- --bounds-crs, --bounds_crs TEXT
224
- CRS of provided bounds
225
+ --dst-crs TEXT CRS of output file
226
+ --bounds-crs TEXT CRS of provided bounds
225
227
  -r, --resolution INTEGER
226
228
  -a, --align Align provided bounds to provincial standard
227
229
  -i, --interpolation [nearest|bilinear|bicubic]
@@ -243,18 +245,20 @@ Usage: bcdata dump [OPTIONS] DATASET
243
245
  $ bcdata dump bc-airports --query "AIRPORT_NAME='Victoria Harbour (Shoal Point) Heliport'"
244
246
  $ bcdata dump bc-airports --bounds xmin ymin xmax ymax
245
247
 
246
- It can also be combined to read bounds of a feature dataset using Fiona:
248
+ It can also be combined to read bounds of a feature dataset using Fiona: 
247
249
  $ bcdata dump bc-airports --bounds $(fio info aoi.shp --bounds)
248
250
 
249
251
  Options:
250
252
  --query TEXT A valid CQL or ECQL query
251
- -o, --out_file TEXT Output file
253
+ -c, --count INTEGER Number of features to request and dump
252
254
  --bounds TEXT Bounds: "left bottom right top" or "[left,
253
255
  bottom, right, top]". Coordinates are BC
254
256
  Albers (default) or --bounds_crs
255
257
  --bounds-crs, --bounds_crs TEXT
256
258
  CRS of provided bounds
259
+ -s, --sortby TEXT Name of sort field
257
260
  -l, --lowercase Write column/properties names as lowercase
261
+ -m, --promote-to-multi Promote features to multipart
258
262
  -v, --verbose Increase verbosity.
259
263
  -q, --quiet Decrease verbosity.
260
264
  --help Show this message and exit.
@@ -291,6 +295,7 @@ Usage: bcdata list [OPTIONS]
291
295
  List DataBC layers available via WFS
292
296
 
293
297
  Options:
298
+ -r, --refresh Refresh the cached list
294
299
  --help Show this message and exit.
295
300
  ```
296
301
 
@@ -1,3 +1,4 @@
1
+ import json
1
2
  import os
2
3
 
3
4
  from click.testing import CliRunner
@@ -73,6 +74,14 @@ def test_cat_bounds_ll():
73
74
  assert len(result.output.split("\n")) == 4
74
75
 
75
76
 
77
+ def test_dump():
78
+ runner = CliRunner()
79
+ result = runner.invoke(cli, ["dump", AIRPORTS_TABLE, "--count", 1])
80
+ assert result.exit_code == 0
81
+ assert json.loads(result.output)["type"] == "FeatureCollection"
82
+ assert len(json.loads(result.output)["features"]) == 1
83
+
84
+
76
85
  def test_bc2pg():
77
86
  runner = CliRunner()
78
87
  result = runner.invoke(cli, ["bc2pg", AIRPORTS_TABLE, "--db_url", DB_URL])
@@ -1,5 +1,4 @@
1
1
  import pytest
2
- import json
3
2
  import requests
4
3
  import requests_mock
5
4
  import stamina
@@ -94,14 +93,7 @@ def test_get_data_lowercase():
94
93
 
95
94
  def test_get_data_crs():
96
95
  data = bcdata.get_data(AIRPORTS_TABLE, crs="EPSG:3005")
97
- assert (
98
- data["crs"]["properties"]["name"] == 'urn:ogc:def:crs:EPSG::3005'
99
- )
100
-
101
-
102
- def test_get_features():
103
- data = [f for f in bcdata.get_features(AIRPORTS_TABLE)]
104
- assert len(data) == 455
96
+ assert data["crs"]["properties"]["name"] == "urn:ogc:def:crs:EPSG::3005"
105
97
 
106
98
 
107
99
  def test_get_data_count():
@@ -147,20 +139,3 @@ def test_cql_bounds_filter():
147
139
  )
148
140
  assert len(data["features"]) == 1
149
141
  assert data["features"][0]["properties"]["AIRPORT_NAME"] == "Victoria International Airport"
150
-
151
-
152
- def test_clean():
153
- data = bcdata.get_data(
154
- AIRPORTS_TABLE,
155
- query="AIRPORT_NAME='Terrace (Northwest Regional) Airport'",
156
- )
157
- assert "SE_ANNO_CAD_DATA" not in data["features"][0]["properties"].keys()
158
-
159
-
160
- def test_no_clean():
161
- data = bcdata.get_data(
162
- AIRPORTS_TABLE,
163
- query="AIRPORT_NAME='Terrace (Northwest Regional) Airport'",
164
- clean=False
165
- )
166
- assert "SE_ANNO_CAD_DATA" in data["features"][0]["properties"].keys()
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes