geobox 1.4.2__py3-none-any.whl → 2.0.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 (66) hide show
  1. geobox/__init__.py +2 -2
  2. geobox/aio/__init__.py +63 -0
  3. geobox/aio/api.py +2640 -0
  4. geobox/aio/apikey.py +263 -0
  5. geobox/aio/attachment.py +339 -0
  6. geobox/aio/base.py +262 -0
  7. geobox/aio/basemap.py +196 -0
  8. geobox/aio/dashboard.py +342 -0
  9. geobox/aio/feature.py +527 -0
  10. geobox/aio/field.py +321 -0
  11. geobox/aio/file.py +522 -0
  12. geobox/aio/layout.py +341 -0
  13. geobox/aio/log.py +145 -0
  14. geobox/aio/map.py +1034 -0
  15. geobox/aio/model3d.py +415 -0
  16. geobox/aio/mosaic.py +696 -0
  17. geobox/aio/plan.py +315 -0
  18. geobox/aio/query.py +693 -0
  19. geobox/aio/raster.py +869 -0
  20. geobox/aio/route.py +63 -0
  21. geobox/aio/scene.py +342 -0
  22. geobox/aio/settings.py +194 -0
  23. geobox/aio/task.py +402 -0
  24. geobox/aio/tile3d.py +339 -0
  25. geobox/aio/tileset.py +672 -0
  26. geobox/aio/usage.py +243 -0
  27. geobox/aio/user.py +507 -0
  28. geobox/aio/vectorlayer.py +1363 -0
  29. geobox/aio/version.py +273 -0
  30. geobox/aio/view.py +983 -0
  31. geobox/aio/workflow.py +341 -0
  32. geobox/api.py +14 -15
  33. geobox/apikey.py +28 -1
  34. geobox/attachment.py +27 -1
  35. geobox/base.py +4 -4
  36. geobox/basemap.py +30 -1
  37. geobox/dashboard.py +27 -0
  38. geobox/feature.py +33 -13
  39. geobox/field.py +33 -21
  40. geobox/file.py +40 -46
  41. geobox/layout.py +28 -1
  42. geobox/log.py +31 -7
  43. geobox/map.py +34 -2
  44. geobox/model3d.py +31 -37
  45. geobox/mosaic.py +28 -7
  46. geobox/plan.py +29 -3
  47. geobox/query.py +39 -14
  48. geobox/raster.py +26 -13
  49. geobox/scene.py +26 -0
  50. geobox/settings.py +30 -1
  51. geobox/task.py +28 -6
  52. geobox/tile3d.py +27 -1
  53. geobox/tileset.py +26 -5
  54. geobox/usage.py +32 -1
  55. geobox/user.py +62 -6
  56. geobox/utils.py +34 -0
  57. geobox/vectorlayer.py +40 -4
  58. geobox/version.py +25 -1
  59. geobox/view.py +37 -17
  60. geobox/workflow.py +27 -1
  61. {geobox-1.4.2.dist-info → geobox-2.0.1.dist-info}/METADATA +4 -1
  62. geobox-2.0.1.dist-info/RECORD +68 -0
  63. geobox-1.4.2.dist-info/RECORD +0 -38
  64. {geobox-1.4.2.dist-info → geobox-2.0.1.dist-info}/WHEEL +0 -0
  65. {geobox-1.4.2.dist-info → geobox-2.0.1.dist-info}/licenses/LICENSE +0 -0
  66. {geobox-1.4.2.dist-info → geobox-2.0.1.dist-info}/top_level.txt +0 -0
geobox/aio/feature.py ADDED
@@ -0,0 +1,527 @@
1
+ from urllib.parse import urljoin
2
+ from typing import Optional, List, Dict, Any, TYPE_CHECKING
3
+
4
+ from .base import AsyncBase
5
+ from ..enums import FeatureType
6
+
7
+ if TYPE_CHECKING:
8
+ from .vectorlayer import VectorLayer
9
+ from ..api import GeoboxClient as SyncGeoboxClient
10
+ from ..feature import Feature as SyncFeature
11
+
12
+
13
+ class Feature(AsyncBase):
14
+
15
+ BASE_SRID = 3857
16
+
17
+ def __init__(self,
18
+ layer: 'VectorLayer',
19
+ srid: Optional[int] = 3857,
20
+ data: Optional[Dict] = {}):
21
+ """
22
+ Constructs all the necessary attributes for the Feature object.
23
+
24
+ Args:
25
+ layer (VectorLayer): The vector layer this feature belongs to
26
+ srid (int, optional): The Spatial Reference System Identifier (default is 3857)
27
+ data (Dict, optional): The feature data contains the feature geometry and properties
28
+
29
+ Example:
30
+ >>> from geobox import GeoboxClient, Feature
31
+ >>> client = GeoboxClient()
32
+ >>> layer = client.get_vector(uuid="12345678-1234-5678-1234-567812345678")
33
+ >>> feature = Feature(layer=layer, srid=4326) # example srid set to 4326
34
+ >>> feature.save()
35
+ """
36
+ super().__init__(api=layer.api)
37
+ self.layer = layer
38
+ self._srid = srid
39
+ self.data = data or {}
40
+ self.original_geometry = self.data.get('geometry')
41
+ self.endpoint = urljoin(layer.endpoint, f'features/{self.data.get("id")}/') if self.data.get('id') else None
42
+
43
+
44
+ def __dir__(self) -> List[str]:
45
+ """
46
+ Return a list of available attributes for the Feature object.
47
+
48
+ This method extends the default dir() behavior to include:
49
+ - All keys from the feature data dictionary
50
+ - All keys from the geometry dictionary
51
+ - All keys from the properties dictionary
52
+
53
+ This allows for better IDE autocompletion and introspection of feature attributes.
54
+
55
+ Returns:
56
+ list: A list of attribute names available on this Feature object.
57
+ """
58
+ return super().__dir__() + list(self.data.keys()) + list(self.data.get('geometry').keys()) + list(self.data.get('properties').keys())
59
+
60
+
61
+ def __repr__(self) -> str:
62
+ """
63
+ Return a string representation of the Feature object.
64
+
65
+ Returns:
66
+ str: A string representation of the Feature object.
67
+ """
68
+ return f"Feature(id={self.id}, type={self.feature_type})"
69
+
70
+
71
+ def __getattr__(self, name: str) -> Any:
72
+ """
73
+ Get an attribute from the resource.
74
+
75
+ Args:
76
+ name (str): The name of the attribute
77
+ """
78
+ if name in self.data:
79
+ return self.data.get(name)
80
+ # elif name in self.data['geometry']:
81
+ # return self.data['geometry'].get(name)
82
+ elif name in self.data['properties']:
83
+ return self.data['properties'].get(name)
84
+
85
+ raise AttributeError(f"Feature has no attribute {name}")
86
+
87
+
88
+ @property
89
+ def srid(self) -> int:
90
+ """
91
+ Get the Spatial Reference System Identifier (SRID) of the feature.
92
+
93
+ Returns:
94
+ int: The SRID of the feature.
95
+
96
+ Example:
97
+ >>> from geobox.aio import AsyncGeoboxClient
98
+ >>> async with AsyncGeoboxClient() as client:
99
+ >>> layer = await client.get_vector(uuid="12345678-1234-5678-1234-567812345678")
100
+ >>> feature = await layer.get_feature(id=1)
101
+ >>> feature.srid # 3857
102
+ """
103
+ return self._srid
104
+
105
+
106
+ @property
107
+ def feature_type(self) -> 'FeatureType':
108
+ """
109
+ Get the type of the feature.
110
+
111
+ Returns:
112
+ FeatureType: The type of the feature.
113
+
114
+ Example:
115
+ >>> from geobox.aio import AsyncGeoboxClient
116
+ >>> async with AsyncGeoboxClient() as client:
117
+ >>> layer = await client.get_vector(uuid="12345678-1234-5678-1234-567812345678")
118
+ >>> feature = await layer.get_feature(id=1)
119
+ >>> feature.feature_type
120
+ """
121
+ return FeatureType(self.data.get('geometry').get('type')) if self.data.get('geometry') else None
122
+
123
+
124
+ @property
125
+ def coordinates(self) -> List[float]:
126
+ """
127
+ Get the coordinates of the ferepoature.
128
+
129
+ Returns:
130
+ list: The coordinates of the feature.
131
+
132
+ Example:
133
+ >>> from geobox.aio import AsyncGeoboxClient
134
+ >>> async with AsyncGeoboxClient() as client:
135
+ >>> layer = await client.get_vector(uuid="12345678-1234-5678-1234-567812345678")
136
+ >>> feature = await layer.get_feature(id=1)
137
+ >>> feature.coordinates
138
+ """
139
+ return self.data.get('geometry').get('coordinates') if self.data.get('geometry') else None
140
+
141
+
142
+ @coordinates.setter
143
+ def coordinates(self, value: List[float]) -> None:
144
+ """
145
+ Set the coordinates of the feature.
146
+
147
+ Args:
148
+ value (list): The coordinates to set.
149
+
150
+ Example:
151
+ >>> from geobox.aio import AsyncGeoboxClient
152
+ >>> async with AsyncGeoboxClient() as client:
153
+ >>> layer = await client.get_vector(uuid="12345678-1234-5678-1234-567812345678")
154
+ >>> feature = await layer.get_feature(id=1)
155
+ >>> feature.coordinates = [10, 20]
156
+ """
157
+ self.data['geometry']['coordinates'] = value
158
+
159
+
160
+ @property
161
+ async def length(self) -> float:
162
+ """
163
+ [async] Returns the length of thefeature geometry (geometry package extra is required!)
164
+
165
+ Returns:
166
+ float: the length of thefeature geometry
167
+
168
+ Example:
169
+ >>> from geobox.aio import AsyncGeoboxClient
170
+ >>> async with AsyncGeoboxClient() as client:
171
+ >>> layer = await client.get_vector(uuid="12345678-1234-5678-1234-567812345678")
172
+ >>> feature = await layer.get_feature(id=1)
173
+ >>> await feature.length
174
+ """
175
+ try:
176
+ return self.geom_length
177
+ except AttributeError:
178
+ endpoint = f'{self.endpoint}length'
179
+ return await self.api.get(endpoint)
180
+
181
+
182
+ @property
183
+ async def area(self) -> float:
184
+ """
185
+ [async] Returns the area of thefeature geometry (geometry package extra is required!)
186
+
187
+ Returns:
188
+ float: the area of thefeature geometry
189
+
190
+ Example:
191
+ >>> from geobox.aio import AsyncGeoboxClient
192
+ >>> async with AsyncGeoboxClient() as client:
193
+ >>> layer = await client.get_vector(uuid="12345678-1234-5678-1234-567812345678")
194
+ >>> feature = await layer.get_feature(id=1)
195
+ >>> await feature.area
196
+ """
197
+ try:
198
+ return self.geom_area
199
+ except AttributeError:
200
+ endpoint = f'{self.endpoint}area'
201
+ return await self.api.get(endpoint)
202
+
203
+
204
+ async def save(self) -> None:
205
+ """
206
+ [async] Save the feature. Creates a new feature if feature_id is None, updates existing feature otherwise.
207
+
208
+ Returns:
209
+ None
210
+
211
+ Example:
212
+ >>> from geobox.aio import AsyncGeoboxClient
213
+ >>> async with AsyncGeoboxClient() as client:
214
+ >>> layer = await client.get_vector(uuid="12345678-1234-5678-1234-567812345678")
215
+ >>> feature = await layer.get_feature(id=1)
216
+ >>> feature.properties['name'] = 'New Name'
217
+ >>> await feature.save()
218
+ """
219
+ data = self.data.copy()
220
+ try:
221
+ if self.id:
222
+ if self.srid != self.BASE_SRID:
223
+ self.data['geometry'] = self.original_geometry
224
+ await self.update(self.data)
225
+ self.coordinates = data['geometry']['coordinates']
226
+ except AttributeError:
227
+ endpoint = urljoin(self.layer.endpoint, 'features/')
228
+ response = await self.layer.api.post(endpoint, data)
229
+ self.endpoint = urljoin(self.layer.endpoint, f'features/{response["id"]}/')
230
+ self.data.update(response)
231
+
232
+
233
+ async def delete(self) -> None:
234
+ """
235
+ [async] Delete the feature.
236
+
237
+ Returns:
238
+ None
239
+
240
+ Example:
241
+ >>> from geobox.aio import AsyncGeoboxClient
242
+ >>> async with AsyncGeoboxClient() as client:
243
+ >>> layer = await client.get_vector(uuid="12345678-1234-5678-1234-567812345678")
244
+ >>> feature = await layer.get_feature(id=1)
245
+ >>> await feature.delete()
246
+ """
247
+ await super().delete(self.endpoint)
248
+
249
+
250
+ async def update(self, geojson: Dict) -> Dict:
251
+ """
252
+ [async] Update the feature data property.
253
+
254
+ Args:
255
+ geojson (Dict): The GeoJSON data for the feature
256
+
257
+ Returns:
258
+ Dict: The response from the API.
259
+
260
+ Example:
261
+ >>> from geobox.aio import AsyncGeoboxClient
262
+ >>> async with AsyncGeoboxClient() as client:
263
+ >>> layer = await client.get_vector(uuid="12345678-1234-5678-1234-567812345678")
264
+ >>> feature = await layer.get_feature(id=1)
265
+ >>> geojson = {
266
+ ... "geometry": {
267
+ ... "type": "Point",
268
+ ... "coordinates": [10, 20]
269
+ ... }
270
+ ... }
271
+ >>> await feature.update(geojson)
272
+ """
273
+ return await super()._update(self.endpoint, geojson, clean=False)
274
+
275
+
276
+ @classmethod
277
+ async def create_feature(cls, layer: 'VectorLayer', geojson: Dict) -> 'Feature':
278
+ """
279
+ [async] Create a new feature in the vector layer.
280
+
281
+ Args:
282
+ layer (VectorLayer): The vector layer to create the feature in
283
+ geojson (Dict): The GeoJSON data for the feature
284
+
285
+ Returns:
286
+ Feature: The created feature instance
287
+
288
+ Example:
289
+ >>> from geobox.aio import AsyncGeoboxClient
290
+ >>> async with AsyncGeoboxClient() as client:
291
+ >>> layer = await client.get_vector(uuid="12345678-1234-5678-1234-567812345678")
292
+ >>> geojson = {
293
+ ... "type": "Feature",
294
+ ... "geometry": {"type": "Point", "coordinates": [10, 20]},
295
+ ... "properties": {"name": "My Point"}
296
+ ... }
297
+ >>> feature = await Feature.create_feature(layer, geojson)
298
+ """
299
+ endpoint = urljoin(layer.endpoint, 'features/')
300
+ return await cls._create(layer.api, endpoint, geojson, factory_func=lambda api, item: Feature(layer, data=item))
301
+
302
+
303
+ @classmethod
304
+ async def get_feature(cls, layer: 'VectorLayer', feature_id: int, user_id: int = None) -> 'Feature':
305
+ """
306
+ [async] Get a feature by its ID.
307
+
308
+ Args:
309
+ layer (VectorLayer): The vector layer the feature belongs to
310
+ feature_id (int): The ID of the feature
311
+ user_id (int): specific user. privileges required.
312
+
313
+ Returns:
314
+ Feature: The retrieved feature instance
315
+
316
+ Example:
317
+ >>> from geobox.aio import AsyncGeoboxClient
318
+ >>> from geobox.aio.feature import Feature
319
+ >>> async with AsyncGeoboxClient() as client:
320
+ >>> layer = await client.get_vector(uuid="12345678-1234-5678-1234-567812345678")
321
+ >>> feature = await Feature.get_feature(layer, feature_id=1)
322
+ """
323
+ param = {
324
+ 'f': 'json',
325
+ 'user_id': user_id
326
+ }
327
+ endpoint = urljoin(layer.endpoint, f'features/')
328
+ return await cls._get_detail(layer.api, endpoint, uuid=feature_id, params=param, factory_func=lambda api, item: Feature(layer, data=item))
329
+
330
+
331
+ @property
332
+ def geometry(self) -> 'BaseGeometry':
333
+ """
334
+ Get the feature geometry as a Shapely geometry object.
335
+
336
+ Returns:
337
+ shapely.geometry.BaseGeometry: The Shapely geometry object representing the feature's geometry
338
+
339
+ Raises:
340
+ ValueError: If the geometry is not a dictionary
341
+ ValueError: If the geometry type is not present in the feature data
342
+ ValueError: If the geometry coordinates are not present in the feature data
343
+ ImportError: If shapely is not installed
344
+
345
+ Example:
346
+ >>> from geobox.aio import AsyncGeoboxClient
347
+ >>> from geobox.aio.feature import Feature
348
+ >>> async with AsyncGeoboxClient() as client:
349
+ >>> layer = await client.get_vector(uuid="12345678-1234-5678-1234-567812345678")
350
+ >>> feature = await layer.get_feature(id=1)
351
+ >>> feature.geometry
352
+ """
353
+ try:
354
+ from shapely.geometry import shape
355
+ except ImportError:
356
+ raise ImportError(
357
+ "The 'geometry' extra is required for this function. "
358
+ "Install it with: pip install geobox[geometry]"
359
+ )
360
+
361
+ if not self.data:
362
+ return None
363
+
364
+ elif not self.data.get('geometry'):
365
+ raise ValueError("Geometry is not present in the feature data")
366
+
367
+ elif not isinstance(self.data['geometry'], dict):
368
+ raise ValueError("Geometry is not a dictionary")
369
+
370
+ elif not self.data['geometry'].get('type'):
371
+ raise ValueError("Geometry type is not present in the feature data")
372
+
373
+ elif not self.data['geometry'].get('coordinates'):
374
+ raise ValueError("Geometry coordinates are not present in the feature data")
375
+
376
+ else:
377
+ return shape(self.data['geometry'])
378
+
379
+
380
+ @geometry.setter
381
+ def geometry(self, value: object) -> None:
382
+ """
383
+ Set the feature geometry.
384
+
385
+ Args:
386
+ value (object): The geometry to set.
387
+
388
+ Raises:
389
+ ValueError: If geometry type is not supported
390
+ ValueError: If the geometry has a different type than the layer type
391
+ ImportError: If shapely is not installed
392
+
393
+ Returns:
394
+ None
395
+
396
+ Example:
397
+ >>> from geobox.aio import AsyncGeoboxClient
398
+ >>> from geobox.aio.feature import Feature
399
+ >>> from shapely.affinity import translate
400
+ >>> async with AsyncGeoboxClient() as client:
401
+ >>> layer = await client.get_vector(uuid="12345678-1234-5678-1234-567812345678")
402
+ >>> feature = await layer.get_feature(id=1)
403
+ >>> geom = feature.geometry
404
+ >>> geom = translate(geom, 3.0, 0.5) # example change applied to the feature's geometry
405
+ >>> feature.geometry = geom
406
+ >>> await feature.save()
407
+ """
408
+ try:
409
+ from shapely.geometry import mapping, Point, MultiPoint, LineString, MultiLineString, Polygon, MultiPolygon
410
+ except ImportError:
411
+ raise ImportError(
412
+ "The 'geometry' extra is required for this function. "
413
+ "Install it with: pip install geobox[geometry]"
414
+ )
415
+
416
+ if not isinstance(value, (Point, MultiPoint, LineString, MultiLineString, Polygon, MultiPolygon)):
417
+ raise ValueError("Geometry must be a Shapely geometry object")
418
+
419
+ elif self.feature_type and value.geom_type != self.feature_type.value:
420
+ raise ValueError("Geometry must have the same type as the layer type")
421
+
422
+ else:
423
+ self.data['geometry'] = mapping(value)
424
+
425
+
426
+ def transform(self, out_srid: int) -> 'Feature':
427
+ """
428
+ Transform the feature geometry to a new SRID.
429
+
430
+ Args:
431
+ out_srid (int): The target SRID to transform the geometry to (e.g., 4326 for WGS84, 3857 for Web Mercator)
432
+
433
+ Returns:
434
+ Feature: A new Feature instance with transformed geometry.
435
+
436
+ Raises:
437
+ ValueError: If the feature has no geometry or if the transformation fails.
438
+ ImportError: If pyproj is not installed.
439
+
440
+ Example:
441
+ >>> from geobox.aio import AsyncGeoboxClient
442
+ >>> from geobox.aio.feature import Feature
443
+ >>> async with AsyncGeoboxClient() as client:
444
+ >>> layer = await client.get_vector(uuid="12345678-1234-5678-1234-567812345678")
445
+ >>> feature = await layer.get_feature(id=1, srid=3857)
446
+ >>> # Transform from Web Mercator (3857) to WGS84 (4326)
447
+ >>> transformed = feature.transform(out_srid=4326)
448
+ >>> transformed.srid # 4326
449
+ """
450
+ try:
451
+ from pyproj import Transformer
452
+ from shapely.geometry import Point, LineString, Polygon, MultiPoint, MultiLineString, MultiPolygon, mapping
453
+ except ImportError:
454
+ raise ImportError(
455
+ "The 'geometry' extra is required for this function. "
456
+ "Install it with: pip install geobox[geometry]"
457
+ )
458
+
459
+ if not self.data or not self.data.get('geometry'):
460
+ raise ValueError("Feature geometry is required for transformation")
461
+
462
+ # Get the current SRID from the feature or default to 3857 (Web Mercator)
463
+ current_srid = self.srid or 3857
464
+
465
+ # Create transformer
466
+ transformer = Transformer.from_crs(current_srid, out_srid, always_xy=True)
467
+
468
+ # Get the geometry
469
+ geom = self.geometry
470
+
471
+ # Transform coordinates based on geometry type
472
+ if geom.geom_type == 'Point':
473
+ x, y = geom.x, geom.y
474
+ new_x, new_y = transformer.transform(x, y)
475
+ new_geom = Point(new_x, new_y)
476
+ elif geom.geom_type == 'LineString':
477
+ coords = list(geom.coords)
478
+ new_coords = [transformer.transform(x, y) for x, y in coords]
479
+ new_geom = LineString(new_coords)
480
+ elif geom.geom_type == 'Polygon':
481
+ exterior = [transformer.transform(x, y) for x, y in geom.exterior.coords]
482
+ interiors = [[transformer.transform(x, y) for x, y in interior.coords] for interior in geom.interiors]
483
+ new_geom = Polygon(exterior, holes=interiors)
484
+ elif geom.geom_type == 'MultiPoint':
485
+ new_geoms = [Point(transformer.transform(point.x, point.y)) for point in geom.geoms]
486
+ new_geom = MultiPoint(new_geoms)
487
+ elif geom.geom_type == 'MultiLineString':
488
+ new_geoms = [LineString([transformer.transform(x, y) for x, y in line.coords]) for line in geom.geoms]
489
+ new_geom = MultiLineString(new_geoms)
490
+ elif geom.geom_type == 'MultiPolygon':
491
+ new_geoms = []
492
+ for poly in geom.geoms:
493
+ exterior = [transformer.transform(x, y) for x, y in poly.exterior.coords]
494
+ interiors = [[transformer.transform(x, y) for x, y in interior.coords] for interior in poly.interiors]
495
+ new_geoms.append(Polygon(exterior, holes=interiors))
496
+ new_geom = MultiPolygon(new_geoms)
497
+
498
+ # update the feature data
499
+ self.data['geometry'] = mapping(new_geom)
500
+ self._srid = out_srid
501
+
502
+ return self
503
+
504
+
505
+ def to_sync(self, sync_client: 'SyncGeoboxClient') -> 'SyncFeature':
506
+ """
507
+ [async] Switch to sync version of the feature instance to have access to the sync methods
508
+
509
+ Args:
510
+ sync_client (SyncGeoboxClient): The sync version of the GeoboxClient instance for making requests.
511
+
512
+ Returns:
513
+ geobox.feature.Feature: the sync instance of the feature.
514
+
515
+ Example:
516
+ >>> from geobox import Geoboxclient
517
+ >>> from geobox.aio import AsyncGeoboxClient
518
+ >>> client = GeoboxClient()
519
+ >>> async with AsyncGeoboxClient() as async_client:
520
+ >>> layer = await async_client.get_vector(uuid="12345678-1234-5678-1234-567812345678")
521
+ >>> feature = await layer.get_feature(id=1, srid=3857)
522
+ >>> sync_feature = feature.to_sync(client)
523
+ """
524
+ from ..feature import Feature as SyncFeature
525
+
526
+ sync_layer = self.layer.to_sync(sync_client=sync_client)
527
+ return SyncFeature(layer=sync_layer, srid=self.srid, data=self.data)