django-gisserver 1.5.0__py3-none-any.whl → 2.1__py3-none-any.whl

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.
Files changed (77) hide show
  1. {django_gisserver-1.5.0.dist-info → django_gisserver-2.1.dist-info}/METADATA +34 -8
  2. django_gisserver-2.1.dist-info/RECORD +68 -0
  3. {django_gisserver-1.5.0.dist-info → django_gisserver-2.1.dist-info}/WHEEL +1 -1
  4. gisserver/__init__.py +1 -1
  5. gisserver/compat.py +23 -0
  6. gisserver/conf.py +7 -0
  7. gisserver/crs.py +401 -0
  8. gisserver/db.py +126 -51
  9. gisserver/exceptions.py +132 -4
  10. gisserver/extensions/__init__.py +4 -0
  11. gisserver/{parsers/fes20 → extensions}/functions.py +131 -31
  12. gisserver/extensions/queries.py +266 -0
  13. gisserver/features.py +253 -181
  14. gisserver/geometries.py +64 -311
  15. gisserver/management/__init__.py +0 -0
  16. gisserver/management/commands/__init__.py +0 -0
  17. gisserver/management/commands/loadgeojson.py +311 -0
  18. gisserver/operations/base.py +130 -312
  19. gisserver/operations/wfs20.py +399 -375
  20. gisserver/output/__init__.py +14 -49
  21. gisserver/output/base.py +198 -144
  22. gisserver/output/csv.py +78 -75
  23. gisserver/output/geojson.py +37 -37
  24. gisserver/output/gml32.py +287 -259
  25. gisserver/output/iters.py +207 -0
  26. gisserver/output/results.py +73 -61
  27. gisserver/output/stored.py +143 -0
  28. gisserver/output/utils.py +81 -169
  29. gisserver/output/xmlschema.py +85 -46
  30. gisserver/parsers/__init__.py +10 -10
  31. gisserver/parsers/ast.py +426 -0
  32. gisserver/parsers/fes20/__init__.py +89 -31
  33. gisserver/parsers/fes20/expressions.py +172 -58
  34. gisserver/parsers/fes20/filters.py +116 -45
  35. gisserver/parsers/fes20/identifiers.py +66 -28
  36. gisserver/parsers/fes20/lookups.py +146 -0
  37. gisserver/parsers/fes20/operators.py +417 -161
  38. gisserver/parsers/fes20/sorting.py +113 -34
  39. gisserver/parsers/gml/__init__.py +17 -25
  40. gisserver/parsers/gml/base.py +36 -15
  41. gisserver/parsers/gml/geometries.py +105 -44
  42. gisserver/parsers/ows/__init__.py +25 -0
  43. gisserver/parsers/ows/kvp.py +198 -0
  44. gisserver/parsers/ows/requests.py +160 -0
  45. gisserver/parsers/query.py +179 -0
  46. gisserver/parsers/values.py +87 -4
  47. gisserver/parsers/wfs20/__init__.py +39 -0
  48. gisserver/parsers/wfs20/adhoc.py +253 -0
  49. gisserver/parsers/wfs20/base.py +148 -0
  50. gisserver/parsers/wfs20/projection.py +103 -0
  51. gisserver/parsers/wfs20/requests.py +483 -0
  52. gisserver/parsers/wfs20/stored.py +193 -0
  53. gisserver/parsers/xml.py +261 -0
  54. gisserver/projection.py +367 -0
  55. gisserver/static/gisserver/index.css +20 -4
  56. gisserver/templates/gisserver/base.html +12 -0
  57. gisserver/templates/gisserver/index.html +9 -15
  58. gisserver/templates/gisserver/service_description.html +12 -6
  59. gisserver/templates/gisserver/wfs/2.0.0/get_capabilities.xml +9 -9
  60. gisserver/templates/gisserver/wfs/feature_field.html +3 -3
  61. gisserver/templates/gisserver/wfs/feature_type.html +35 -13
  62. gisserver/templatetags/gisserver_tags.py +20 -0
  63. gisserver/types.py +445 -313
  64. gisserver/views.py +227 -62
  65. django_gisserver-1.5.0.dist-info/RECORD +0 -54
  66. gisserver/parsers/base.py +0 -149
  67. gisserver/parsers/fes20/query.py +0 -285
  68. gisserver/parsers/tags.py +0 -102
  69. gisserver/queries/__init__.py +0 -37
  70. gisserver/queries/adhoc.py +0 -185
  71. gisserver/queries/base.py +0 -186
  72. gisserver/queries/projection.py +0 -240
  73. gisserver/queries/stored.py +0 -206
  74. gisserver/templates/gisserver/wfs/2.0.0/describe_stored_queries.xml +0 -20
  75. gisserver/templates/gisserver/wfs/2.0.0/list_stored_queries.xml +0 -14
  76. {django_gisserver-1.5.0.dist-info → django_gisserver-2.1.dist-info/licenses}/LICENSE +0 -0
  77. {django_gisserver-1.5.0.dist-info → django_gisserver-2.1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,311 @@
1
+ """Quick utility to import GeoJSON data in the Django project."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import operator
7
+ from argparse import ArgumentTypeError
8
+ from functools import reduce
9
+ from itertools import islice
10
+ from urllib.request import urlopen
11
+
12
+ from django.apps import apps
13
+ from django.contrib.gis.db.models import GeometryField
14
+ from django.contrib.gis.geos import GEOSGeometry
15
+ from django.core.exceptions import FieldDoesNotExist
16
+ from django.core.management import BaseCommand, CommandError, CommandParser
17
+ from django.db import DEFAULT_DB_ALIAS, connections, models, transaction
18
+
19
+ from gisserver.crs import CRS, CRS84
20
+
21
+
22
+ def _parse_model(value):
23
+ try:
24
+ return apps.get_model(value)
25
+ except LookupError as e:
26
+ raise ArgumentTypeError(str(e)) from e
27
+
28
+
29
+ def _parse_fields(value):
30
+ field_map = {}
31
+ for pair in value.split(","):
32
+ geojson_name, _, field_name = pair.strip().partition("=")
33
+ if not field_name:
34
+ raise ArgumentTypeError("Expect property=field,property2=field2 format")
35
+ field_map[geojson_name] = field_name
36
+ return field_map
37
+
38
+
39
+ class Command(BaseCommand):
40
+ """Quick command to import data in the WFS server."""
41
+
42
+ help = (
43
+ "Import GeoJSON data into a WFS feature collection. This can be done using:"
44
+ " manage.py loadgeojson --model=places.Province"
45
+ " -f name=name,prop2=field2 --geometry-field geometry provinces.json"
46
+ )
47
+
48
+ def add_arguments(self, parser: CommandParser):
49
+ parser.add_argument(
50
+ "--database",
51
+ default=DEFAULT_DB_ALIAS,
52
+ choices=tuple(connections),
53
+ help=(
54
+ "Nominates a specific database to load fixtures into. Defaults to the "
55
+ '"default" database.'
56
+ ),
57
+ )
58
+ parser.add_argument(
59
+ "-m",
60
+ "--model",
61
+ required=True,
62
+ metavar="MODEL",
63
+ type=_parse_model,
64
+ help="Django model, in the format: app_label.ModelName.",
65
+ )
66
+ parser.add_argument(
67
+ "-g",
68
+ "--geometry-field",
69
+ required=False,
70
+ metavar="NAME",
71
+ help="Name of the model's geometry field, auto-detected if not specified.",
72
+ )
73
+ parser.add_argument(
74
+ "-f",
75
+ "--field",
76
+ action="append",
77
+ dest="map_fields",
78
+ type=_parse_fields,
79
+ metavar="NAME=FIELD",
80
+ help=(
81
+ "Map GeoJSON properties to Django fields, in the format: property1=field1,property2=field2,... "
82
+ "By default, this autodetects common field names only. "
83
+ "Explicitly ignore fields by using -f property=,property2=field2."
84
+ ),
85
+ )
86
+ parser.add_argument(
87
+ "geojson-file",
88
+ help="GeoJSON file or URL to import. For debugging, it's better to download the file first.",
89
+ )
90
+
91
+ def handle(self, *args, **options):
92
+ # Get arguments
93
+ self.using = options["database"]
94
+ self.connection = connections[self.using]
95
+ model: type[models.Model] = options["model"]
96
+ main_geometry_field = self._get_geometry_field(model, options["geometry_field"])
97
+ field_map = self._parse_field_map(model, options["map_fields"])
98
+
99
+ # Read the file
100
+ geojson = self._load_geojson(options["geojson-file"])
101
+ if not geojson["features"]:
102
+ self.stdout.write(self.style.NOTICE("Empty GeoJSON data"))
103
+ return
104
+
105
+ # See if properties match the Django field names, use those too.
106
+ # (unless these are mapped already via the command line args).
107
+ field_map.update(self._get_auto_field_map(model, geojson["features"][0], field_map))
108
+
109
+ # See if a CRS is declared
110
+ self.crs = self._read_crs(geojson)
111
+
112
+ # Import in chunks
113
+ num_imported = 0
114
+ id_field = model._meta.pk.name
115
+ with transaction.atomic(using=self.using):
116
+ features = iter(self._read_geojson(geojson, model, main_geometry_field, field_map))
117
+ while batch := list(islice(features, 100)):
118
+ if id_field in batch[0]:
119
+ # The ID field is provided, allow "on conflict update..."
120
+ unique_fields = [id_field]
121
+ update_fields = list(batch[0].keys())
122
+ update_fields.remove(id_field)
123
+
124
+ model.objects.using(self.using).bulk_create(
125
+ [model(**values) for values in batch],
126
+ update_conflicts=True,
127
+ unique_fields=unique_fields,
128
+ update_fields=update_fields,
129
+ )
130
+ else:
131
+ model.objects.using(self.using).bulk_create(
132
+ [model(**values) for values in batch]
133
+ )
134
+
135
+ num_imported += len(batch)
136
+
137
+ self.stdout.write(f"Installed {num_imported} feature(s)")
138
+
139
+ def _get_geometry_field(self, model: type[models.Model], field_name: str | None):
140
+ """Find the geometry field name, or validate it."""
141
+ if field_name:
142
+ # Field is given in CLI args, validate it.
143
+ try:
144
+ field = model._meta.get_field(field_name)
145
+ except FieldDoesNotExist as e:
146
+ raise CommandError(f"Invalid value for --geometry-field: {e}") from e
147
+
148
+ if not isinstance(field, GeometryField):
149
+ raise CommandError("Invalid value for --geometry-field: not a GeometryField")
150
+ else:
151
+ # Field is not given, auto-detect
152
+ try:
153
+ field_name = next(
154
+ f.name for f in model._meta.fields if isinstance(f, GeometryField)
155
+ )
156
+ except StopIteration:
157
+ raise CommandError(
158
+ f"Model {model._meta.label} does not have any GeometryField member."
159
+ ) from None
160
+
161
+ return field_name
162
+
163
+ def _load_geojson(self, filename) -> dict:
164
+ """Parse and validate the GeoJSON data into Python dict data."""
165
+ try:
166
+ if "://" in filename:
167
+ with urlopen(filename, timeout=60) as response: # noqa: S310
168
+ geojson = json.load(response)
169
+ else:
170
+ with open(filename) as fh:
171
+ geojson = json.load(fh)
172
+ except OSError as e: # FileNotFoundError or HTTP errors
173
+ raise CommandError(str(e)) from e
174
+ except (ValueError, TypeError) as e:
175
+ raise CommandError(f"Unable to parse GeoJSON: {e}") from e
176
+
177
+ if (
178
+ not isinstance(geojson, dict)
179
+ or geojson.get("type") != "FeatureCollection"
180
+ or "features" not in geojson
181
+ ):
182
+ raise CommandError("Invalid GeoJSON data, expected FeatureCollection element.")
183
+
184
+ return geojson
185
+
186
+ def _read_crs(self, geojson: dict) -> CRS:
187
+ """Find the CRS that should be used for all geometry data."""
188
+ crs = geojson.get("crs")
189
+ if not crs:
190
+ return CRS84 # default for GeoJSON
191
+
192
+ try:
193
+ type = crs["type"]
194
+ if type == "name":
195
+ return CRS.from_string(crs["properties"]["name"])
196
+ else:
197
+ # type 'link' with subtype 'proj4/ogcwkt/esriwkt' are not handled here.
198
+ raise CommandError(f"CRS type {type} is not supported.")
199
+ except KeyError as e:
200
+ raise CommandError(f"CRS is invalid, missing '{e}' field") from e
201
+
202
+ def _parse_field_map(self, model, map_fields: list[dict]) -> dict:
203
+ """Validate that the provided field mapping points to model fields."""
204
+ if not map_fields:
205
+ return {}
206
+
207
+ field_map: dict = reduce(operator.or_, map_fields)
208
+ for field_name in field_map.values():
209
+ try:
210
+ model._meta.get_field(field_name)
211
+ except FieldDoesNotExist as e:
212
+ raise CommandError(f"Field '{field_name}' does not exist in Django model.") from e
213
+
214
+ return field_map
215
+
216
+ def _get_auto_field_map(self, model, feature: dict, field_map: dict):
217
+ """Autodetect which properties match model field names."""
218
+ auto_field_map = {}
219
+ for geojson_name in feature.get("properties", ()):
220
+ if geojson_name in field_map:
221
+ continue
222
+
223
+ try:
224
+ model._meta.get_field(geojson_name)
225
+ except FieldDoesNotExist:
226
+ msg = (
227
+ f"GeoJSON property '{geojson_name}' is not mapped nor exists in Django model, skipping."
228
+ if field_map
229
+ else f"GeoJSON property '{geojson_name}' does not exist in Django model, skipping."
230
+ )
231
+ self.stderr.write(self.style.WARNING(msg))
232
+ else:
233
+ self.stdout.write(
234
+ f"GeoJSON property '{geojson_name}' also exists as Django model field, using."
235
+ )
236
+ auto_field_map[geojson_name] = geojson_name
237
+
238
+ return auto_field_map
239
+
240
+ def _read_geojson(
241
+ self, geojson: dict, model: type[models.Model], main_geometry_field: str, field_map: dict
242
+ ):
243
+ """Convert the GeoJSON data to model field names."""
244
+ pk_field = model._meta.pk.name
245
+
246
+ for feature in geojson["features"]:
247
+ # Validate basic layout
248
+ try:
249
+ feature_type = feature["type"]
250
+ geometry_data = feature["geometry"]
251
+ except KeyError as e:
252
+ raise CommandError(f"Feature does not have required '{e}' element.") from e
253
+ if feature_type != "Feature":
254
+ raise CommandError(f"Expected 'Feature', not {feature_type}")
255
+
256
+ # Collect all fields
257
+ properties = feature.get("properties", {})
258
+ field_values = {
259
+ field_name: self._parse_value(
260
+ model._meta.get_field(field_name), properties.get(geojson_name)
261
+ )
262
+ for geojson_name, field_name in field_map.items()
263
+ if field_name
264
+ }
265
+
266
+ # Store the geometry, note GEOS only parses stringified JSON.
267
+ if geometry_data is None:
268
+ geometry = None
269
+ else:
270
+ try:
271
+ # NOTE: this looses the axis ordering information,
272
+ # and assumes x/y as the standard prescribes.
273
+ geometry = GEOSGeometry(json.dumps(geometry_data), srid=self.crs.srid)
274
+ except ValueError as e:
275
+ raise CommandError(
276
+ f"Unable to parse geometry data: {geometry_data!r}: {e}"
277
+ ) from e
278
+ geometry.srid = self.crs.srid # override default
279
+
280
+ field_values[main_geometry_field] = geometry
281
+
282
+ # Try to decode the identifier if it's present
283
+ if (
284
+ pk_field not in field_values
285
+ and (raw_value := feature.get("id")) is not None
286
+ and (id_value := self._parse_id(model._meta.pk, raw_value)) is not None
287
+ ):
288
+ field_values[pk_field] = id_value
289
+
290
+ # Pass as Django model fields
291
+ yield field_values
292
+
293
+ def _parse_id(self, pk_field: models.Field, id_value):
294
+ # Allow TypeName.id format, see if it parses
295
+ try:
296
+ return pk_field.get_db_prep_save(
297
+ id_value.rpartition(".")[2], connection=self.connection
298
+ )
299
+ except (ValueError, TypeError):
300
+ self.stderr.write(
301
+ self.style.WARNING(
302
+ f"Feature id value '{id_value}' can't be stored in the primary key field. ignoring."
303
+ )
304
+ )
305
+ return None
306
+
307
+ def _parse_value(self, field: models.Field, value):
308
+ try:
309
+ return field.get_db_prep_save(value, connection=self.connection)
310
+ except (ValueError, TypeError) as e:
311
+ raise CommandError(f"Can't parse {value!r} in model field '{field.name}': {e}.") from e