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