geobox 2.2.6__py3-none-any.whl → 2.4.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.
Files changed (65) hide show
  1. geobox/aio/api.py +265 -6
  2. geobox/aio/apikey.py +0 -26
  3. geobox/aio/attachment.py +5 -31
  4. geobox/aio/basemap.py +2 -30
  5. geobox/aio/dashboard.py +0 -26
  6. geobox/aio/feature.py +172 -17
  7. geobox/aio/field.py +0 -27
  8. geobox/aio/file.py +0 -26
  9. geobox/aio/layout.py +0 -26
  10. geobox/aio/log.py +0 -27
  11. geobox/aio/map.py +0 -26
  12. geobox/aio/model3d.py +0 -26
  13. geobox/aio/mosaic.py +0 -26
  14. geobox/aio/plan.py +0 -26
  15. geobox/aio/query.py +0 -26
  16. geobox/aio/raster.py +0 -26
  17. geobox/aio/scene.py +1 -26
  18. geobox/aio/settings.py +0 -28
  19. geobox/aio/table.py +1733 -0
  20. geobox/aio/task.py +0 -27
  21. geobox/aio/tile3d.py +0 -26
  22. geobox/aio/tileset.py +1 -26
  23. geobox/aio/usage.py +0 -32
  24. geobox/aio/user.py +0 -59
  25. geobox/aio/vector_tool.py +49 -0
  26. geobox/aio/vectorlayer.py +8 -34
  27. geobox/aio/version.py +1 -25
  28. geobox/aio/view.py +5 -35
  29. geobox/aio/workflow.py +0 -26
  30. geobox/api.py +265 -5
  31. geobox/apikey.py +0 -26
  32. geobox/attachment.py +0 -26
  33. geobox/basemap.py +0 -28
  34. geobox/dashboard.py +0 -26
  35. geobox/enums.py +15 -1
  36. geobox/feature.py +170 -15
  37. geobox/field.py +20 -37
  38. geobox/file.py +0 -26
  39. geobox/layout.py +0 -26
  40. geobox/log.py +0 -26
  41. geobox/map.py +0 -26
  42. geobox/model3d.py +0 -26
  43. geobox/mosaic.py +0 -26
  44. geobox/plan.py +1 -26
  45. geobox/query.py +1 -26
  46. geobox/raster.py +1 -31
  47. geobox/scene.py +1 -27
  48. geobox/settings.py +1 -29
  49. geobox/table.py +1719 -0
  50. geobox/task.py +2 -29
  51. geobox/tile3d.py +0 -26
  52. geobox/tileset.py +1 -26
  53. geobox/usage.py +2 -33
  54. geobox/user.py +1 -59
  55. geobox/vector_tool.py +49 -0
  56. geobox/vectorlayer.py +9 -36
  57. geobox/version.py +1 -26
  58. geobox/view.py +4 -34
  59. geobox/workflow.py +1 -26
  60. {geobox-2.2.6.dist-info → geobox-2.4.0.dist-info}/METADATA +2 -2
  61. geobox-2.4.0.dist-info/RECORD +74 -0
  62. {geobox-2.2.6.dist-info → geobox-2.4.0.dist-info}/WHEEL +1 -1
  63. geobox-2.2.6.dist-info/RECORD +0 -72
  64. {geobox-2.2.6.dist-info → geobox-2.4.0.dist-info}/licenses/LICENSE +0 -0
  65. {geobox-2.2.6.dist-info → geobox-2.4.0.dist-info}/top_level.txt +0 -0
geobox/aio/table.py ADDED
@@ -0,0 +1,1733 @@
1
+ from typing import List, Dict, Optional, TYPE_CHECKING, Union, Any
2
+ from urllib.parse import urljoin
3
+ from dataclasses import dataclass
4
+
5
+ from .base import AsyncBase
6
+ from .task import AsyncTask
7
+ from ..enums import FieldType, RelationshipCardinality, TableExportFormat
8
+ from ..exception import NotFoundError
9
+ from ..utils import clean_data
10
+
11
+ if TYPE_CHECKING:
12
+ from . import AsyncGeoboxClient
13
+ from .user import AsyncUser
14
+ from .file import AsyncFile
15
+ from ..table import Table, TableField
16
+ from .vectorlayer import AsyncVectorLayer
17
+ from .field import AsyncField
18
+ from .feature import AsyncFeature
19
+
20
+
21
+ @dataclass(frozen=True)
22
+ class RelationshipEndpoint:
23
+ """
24
+ Represents one endpoint (source or target) of a relationship
25
+
26
+ Args:
27
+ table (AsyncTable | AsyncVectorLayer): The source or target table or vector layer
28
+ field (AsyncTableField | AsyncField | str): The field name or object on the source/target entity
29
+ fk_field (AsyncTableField | AsyncField | str, optional): The foreign key field name or object on the relation table. (Required for Many-to-Many relationships)
30
+ """
31
+ table: Union['AsyncTable', 'AsyncVectorLayer']
32
+ field: Union['AsyncTableField', 'AsyncField', str]
33
+ fk_field: Optional[Union['AsyncTableField', 'AsyncField', str]] = None
34
+
35
+
36
+ class AsyncTableRow(AsyncBase):
37
+
38
+ def __init__(self,
39
+ table: 'AsyncTable',
40
+ data: Optional[Dict] = {},
41
+ ):
42
+ """
43
+ Constructs all the necessary attributes for the AsyncTableRow object.
44
+
45
+ Args:
46
+ table (AsyncTable): The table that the row belongs to.
47
+ data (Dict, optional): The data of the field.
48
+ """
49
+ super().__init__(api=table.api, data=data)
50
+ self.table = table
51
+ self.endpoint = urljoin(table.endpoint, f'rows/{self.id}/') if self.data.get('id') else None
52
+
53
+
54
+ def __repr__(self) -> str:
55
+ """
56
+ Return a string representation of the AsyncTableRow.
57
+
58
+ Returns:
59
+ str: The string representation of the AsyncTableRow.
60
+ """
61
+ return f"AsyncTableRow(id={self.id}, table_name={self.table.data.get('name', 'None')})"
62
+
63
+
64
+ @classmethod
65
+ async def create_row(cls, table: 'AsyncTable', **kwargs) -> 'AsyncTableRow':
66
+ """
67
+ [async] Create a new row in the table.
68
+
69
+ Each keyword argument represents a field value for the row, where:
70
+ - The keyword is the field name
71
+ - The value is the field value
72
+
73
+ Args:
74
+ table (AsyncTable): table instance
75
+
76
+ Keyword Args:
77
+ **kwargs: Arbitrary field values matching the table schema.
78
+
79
+ Returns:
80
+ AsyncTableRow: created table row instance
81
+
82
+ Example:
83
+ >>> from geobox.aio import AsyncGeoboxClient
84
+ >>> from geobox.aio.table import AsyncTable, AsyncTableRow
85
+ >>> async with AsyncGeoboxClient() as client:
86
+ >>> table = await client.get_table(uuid="12345678-1234-5678-1234-567812345678")
87
+ or
88
+ >>> table = await AsyncTable.get_table(client, uuid="12345678-1234-5678-1234-567812345678")
89
+ >>> row_data = {
90
+ ... 'field1': 'value1'
91
+ ... }
92
+ >>> row = await table.create_row(row_data)
93
+ or
94
+ >>> row = await AsyncTableRow.create_row(table, row_data)
95
+ """
96
+ endpoint = urljoin(table.endpoint, 'rows/')
97
+ return await cls._create(table.api, endpoint, kwargs, factory_func=lambda api, item: AsyncTableRow(table, data=item))
98
+
99
+
100
+ @classmethod
101
+ async def get_row(cls,
102
+ table: 'AsyncTable',
103
+ row_id: int,
104
+ user_id: Optional[int],
105
+ ) -> 'AsyncTableRow':
106
+ """
107
+ [async] Get a row by its id
108
+
109
+ Args:
110
+ table (AsyncTable): the table instance
111
+ row_id (int): the row id
112
+ user_id (int, optional): specific user. privileges required.
113
+
114
+ Returns:
115
+ TanbleRow: the table row instance
116
+
117
+ Example:
118
+ >>> from geobox.aio import AsyncGeoboxClient
119
+ >>> from geobox.aio.table import AsyncTable, AsyncTableRow
120
+ >>> async with AsyncGeoboxClient() as client:
121
+ >>> table = await client.get_table(uuid="12345678-1234-5678-1234-567812345678")
122
+ or
123
+ >>> table = await AsyncTable.get_table(client, uuid="12345678-1234-5678-1234-567812345678")
124
+
125
+ >>> row = await table.get_row(row_id=1)
126
+ or
127
+ >>> row = await AsyncTableRow.get_row(table, row_id=1)
128
+ """
129
+ param = {
130
+ 'f': 'json',
131
+ 'user_id': user_id
132
+ }
133
+ endpoint = urljoin(table.endpoint, f'rows/')
134
+ return await cls._get_detail(table.api, endpoint, uuid=row_id, params=param, factory_func=lambda api, item: AsyncTableRow(table, data=item))
135
+
136
+
137
+ async def update(self, **kwargs) -> Dict:
138
+ """
139
+ [async] Update a row
140
+
141
+ Keyword Args:
142
+ fields to update
143
+
144
+ Returns:
145
+ Dict: updated row data
146
+
147
+ Example:
148
+ >>> from geobox.aio import AsyncGeoboxClient
149
+ >>> async with AsyncGeoboxClient() as client:
150
+ >>> table = await client.get_table(uuid="12345678-1234-5678-1234-567812345678")
151
+ >>> row = await table.get_row(row_id=1)
152
+ >>> await row.update(field1='new_value')
153
+ """
154
+ await super()._update(self.endpoint, self.data, clean=False)
155
+ return self.data
156
+
157
+
158
+ async def delete(self) -> None:
159
+ """
160
+ [async] Delete a row
161
+
162
+ Returns:
163
+ None
164
+
165
+ Example:
166
+ >>> from geobox.aio import AsyncGeoboxClient
167
+ >>> async with AsyncGeoboxClient() as client:
168
+ >>> table = await client.get_table(uuid="12345678-1234-5678-1234-567812345678")
169
+ >>> row = await table.get_row(row_id=1)
170
+ >>> await row.delete()
171
+ """
172
+ await super()._delete(self.endpoint)
173
+
174
+
175
+ async def _get_other_side_of_relationship(
176
+ self,
177
+ relationship: 'AsyncRelationship',
178
+ ) -> Union['AsyncTable', 'AsyncVectorLayer']:
179
+ """
180
+ [async] Determine which side of a relationship this table is on and return the opposite side.
181
+
182
+ Used internally to navigate bidirectional relationships.
183
+
184
+ Args:
185
+ relationship (AsyncRelationship): The relationship to examine.
186
+
187
+ Returns:
188
+ AsyncTable | AsyncVectorLayer: The endpoint (table or layer) on the opposite side
189
+ of the relationship from this table.
190
+
191
+ Raises:
192
+ ValueError: If this table is not part of the given relationship.
193
+
194
+ Note:
195
+ This method assumes the table is either the source or target,
196
+ not the relation table in Many-to-Many relationships.
197
+ """
198
+ if relationship.source_id == self.table.id:
199
+ return await relationship.get_target()
200
+
201
+ if relationship.target_id == self.table.id:
202
+ return await relationship.get_source()
203
+
204
+ raise ValueError("Relationship does not involve this table.")
205
+
206
+
207
+ async def _fetch_related_from_target(
208
+ self,
209
+ target: Union['AsyncTable', 'AsyncVectorLayer'],
210
+ relationship_uuid: str,
211
+ ) -> Union[List['AsyncTableRow'], List['AsyncFeature']]:
212
+ """
213
+ [async] Fetch related rows/features from a relationship target.
214
+
215
+ Internal helper that dispatches to the appropriate API method
216
+ based on target type.
217
+
218
+ Args:
219
+ target (AsyncTable | AsyncVectorLayer): The target endpoint (Table or VectorLayer) to query.
220
+ relationship_uuid (str): UUID of the relationship to traverse.
221
+
222
+ Raises:
223
+ TypeError: If target is not a Table or VectorLayer.
224
+
225
+ Returns:
226
+ List[AsyncTableRow] | List[AsyncFeature]: Related rows or features.
227
+ """
228
+ from .table import AsyncTable
229
+ from .vectorlayer import AsyncVectorLayer
230
+
231
+ if isinstance(target, AsyncTable):
232
+ return await target.get_rows(
233
+ relationship_uuid=relationship_uuid,
234
+ related_record_id=self.id,
235
+ )
236
+
237
+ if isinstance(target, AsyncVectorLayer):
238
+ return await target.get_features(
239
+ relationship_uuid=relationship_uuid,
240
+ related_record_id=self.id,
241
+ )
242
+
243
+ raise TypeError(f"Unsupported target type: {type(target)}")
244
+
245
+
246
+ async def get_related_records(self,
247
+ relationship_uuid: str,
248
+ ) -> Union[List['AsyncTableRow'], List['AsyncFeature']]:
249
+ """
250
+ [async] Get the related records on the *other side* of the relationship that are linked to this row
251
+
252
+ Args:
253
+ relationship_uuid (str): The uuid of relationship
254
+
255
+ Returns:
256
+ List[AsyncTableRow] | List[AsyncFeature]: a list of the related records
257
+
258
+ Raises:
259
+ ValueError:
260
+ If the given relationship does not involve the current table
261
+ (i.e., this row is neither the source nor the target of the relationship).
262
+
263
+ TypeError:
264
+ If the relationship target type is not supported for fetching
265
+ related records.
266
+
267
+ Example:
268
+ >>> from geobox.aio import AsyncGeoboxClient
269
+ >>> async with AsyncGeoboxClient() as client:
270
+ >>> table = await client.get_table(uuid="12345678-1234-5678-1234-567812345678")
271
+ >>> row = await table.get_row(row_id=1)
272
+ >>> related_records = await row.get_related_records(relationship_uuid="12345678-1234-5678-1234-567812345678")
273
+ """
274
+ relationship = await self.api.get_relationship(relationship_uuid)
275
+
276
+ other_side = await self._get_other_side_of_relationship(relationship)
277
+
278
+ return await self._fetch_related_from_target(
279
+ target=other_side,
280
+ relationship_uuid=relationship_uuid,
281
+ )
282
+
283
+
284
+ async def associate_with(
285
+ self,
286
+ relationship_uuid: str,
287
+ *,
288
+ target_ids: Optional[List[int]] = None,
289
+ q: Optional[str] = None,
290
+ ) -> Dict:
291
+ """
292
+ [async] Create relationships between the source record and target records
293
+
294
+ Args:
295
+ relationship_uuid (str): the relationship uuid
296
+ target_ids (List[int], optional): a list of target record ids to be associated with the current record
297
+ q (str, optional): query filter on target layer or table to select which target features or rows that are going to be related to the current record
298
+
299
+ Returns:
300
+ Dict: the record association result
301
+
302
+ Example:
303
+ >>> from geobox.aio import AsyncGeoboxClient
304
+ >>> async with AsyncGeoboxClient() as client:
305
+ >>> table = await client.get_table(uuid="12345678-1234-5678-1234-567812345678")
306
+ >>> row = await table.get_row(row_id=1)
307
+ >>> await row.associate_with(
308
+ ... relationship_uuid="12345678-1234-5678-1234-567812345678",
309
+ ... target_ids=[1, 2, 3],
310
+ ... )
311
+ """
312
+ relationship = await self.api.get_relationship(uuid=relationship_uuid)
313
+ return await relationship.associate_records(
314
+ source_id=self.id,
315
+ target_ids=target_ids,
316
+ q=q,
317
+ )
318
+
319
+
320
+ async def disassociate_with(
321
+ self,
322
+ relationship_uuid: str,
323
+ *,
324
+ target_ids: Optional[List[int]] = None,
325
+ q: Optional[str] = None,
326
+ ) -> Dict:
327
+ """
328
+ [async] Remove relationships between the source record and target records
329
+
330
+ Args:
331
+ relationship_uuid (str): the relationship uuid
332
+ target_ids (List[int], optional): a list of target record ids to be disassociated with the current record
333
+ q (str, optional): query filter on target layer or table to select which target features or rows that are going to be related to the current
334
+
335
+ Returns:
336
+ Dict: the record disassociation result
337
+
338
+ Example:
339
+ >>> from geobox.aio import AsyncGeoboxClient
340
+ >>> async with AsyncGeoboxClient() as client:
341
+ >>> table = await client.get_table(uuid="12345678-1234-5678-1234-567812345678")
342
+ >>> row = await table.get_row(row_id=1)
343
+ >>> await row.disassociate_with(
344
+ ... relationship_uuid="12345678-1234-5678-1234-567812345678",
345
+ ... target_ids=[1, 2, 3],
346
+ ... )
347
+ """
348
+ relationship = await self.api.get_relationship(uuid=relationship_uuid)
349
+ return await relationship.disassociate_records(
350
+ source_id=self.id,
351
+ target_ids=target_ids,
352
+ q=q,
353
+ )
354
+
355
+
356
+ class AsyncTableField(AsyncBase):
357
+
358
+ def __init__(self,
359
+ table: 'AsyncTable',
360
+ data_type: 'FieldType',
361
+ field_id: int = None,
362
+ data: Optional[Dict] = {},
363
+ ):
364
+ """
365
+ Constructs all the necessary attributes for the Field object.
366
+
367
+ Args:
368
+ table (AsyncTable): The table that the field belongs to.
369
+ data_type (FieldType): type of the field
370
+ field_id (int): the id of the field
371
+ data (Dict, optional): The data of the field.
372
+ """
373
+ super().__init__(api=table.api, data=data)
374
+ self.table = table
375
+ self.field_id = field_id
376
+ if not isinstance(data_type, FieldType):
377
+ raise ValueError("data_type must be a FieldType instance")
378
+ self.data_type = data_type
379
+ self.endpoint = urljoin(table.endpoint, f'fields/{self.id}/') if self.data.get('id') else None
380
+
381
+
382
+ def __repr__(self) -> str:
383
+ """
384
+ Return a string representation of the field.
385
+
386
+ Returns:
387
+ str: The string representation of the field.
388
+ """
389
+ return f"AsyncTableField(id={self.id}, name={self.name}, data_type={self.data_type})"
390
+
391
+
392
+ def __getattr__(self, name: str) -> Any:
393
+ """
394
+ Get an attribute from the resource.
395
+
396
+ Args:
397
+ name (str): The name of the attribute
398
+ """
399
+ if name == 'datatype':
400
+ return FieldType(self.data['datatype'])
401
+ return super().__getattr__(name)
402
+
403
+
404
+ @property
405
+ def domain(self) -> Dict:
406
+ """
407
+ Domain property
408
+
409
+ Returns:
410
+ Dict: domain data
411
+ """
412
+ return self.data.get('domain')
413
+
414
+
415
+ @domain.setter
416
+ def domain(self, value: Dict) -> None:
417
+ """
418
+ Domain property setter
419
+
420
+ Returns:
421
+ None
422
+ """
423
+ self.data['domain'] = value
424
+
425
+
426
+ @classmethod
427
+ async def create_field(cls,
428
+ table: 'AsyncTable',
429
+ name: str,
430
+ data_type: 'FieldType',
431
+ data: Dict = {},
432
+ ) -> 'AsyncTableField':
433
+ """
434
+ [async] Create a new field
435
+
436
+ Args:
437
+ table (AsyncTable): field's table
438
+ name (str): name of the field
439
+ data_type (FieldType): type of the field
440
+ data (Dict, optional): the data of the field
441
+
442
+ Returns:
443
+ AsyncTableField: the created field object
444
+
445
+ Example:
446
+ >>> from geobox.aio import AsyncGeoboxClient
447
+ >>> from geobox.aio.table import AsyncTable, AsyncTableField
448
+ >>> async with AsyncGeoboxClient() as client:
449
+ >>> table = await client.get_table(uuid="12345678-1234-5678-1234-567812345678")
450
+
451
+ >>> field = await table.create_field(name='test', data_type=FieldType.Integer)
452
+ or
453
+ >>> field = await AsyncTableField.create_field(client, table=table, name='test', data_type=FieldType.Integer)
454
+ """
455
+ data.update({
456
+ "name": name,
457
+ "datatype": data_type.value
458
+ })
459
+ endpoint = urljoin(table.endpoint, 'fields/')
460
+ return await super()._create(table.api, endpoint, data, factory_func=lambda api, item: AsyncTableField(table, data_type, item['id'], item))
461
+
462
+
463
+ async def delete(self) -> None:
464
+ """
465
+ [async] Delete the field.
466
+
467
+ Returns:
468
+ None
469
+
470
+ Example:
471
+ >>> from geobox.aio import AsyncGeoboxClient
472
+ >>> from geobox.field import AsyncTableField
473
+ >>> async with AsyncGeoboxClient() as client:
474
+ >>> table = await client.get_table(uuid="12345678-1234-5678-1234-567812345678")
475
+ >>> field = await table.get_field(field_id=1)
476
+ >>> await field.delete()
477
+ """
478
+ await super()._delete(self.endpoint)
479
+ self.field_id = None
480
+
481
+
482
+ async def update(self, **kwargs) -> Dict:
483
+ """
484
+ [async] Update the field.
485
+
486
+ Keyword Args:
487
+ name (str): The name of the field.
488
+ display_name (str): The display name of the field.
489
+ description (str): The description of the field.
490
+ domain (Dict): the domain of the field
491
+ hyperlink (bool): the hyperlink field.
492
+
493
+ Returns:
494
+ Dict: The updated data.
495
+
496
+ Example:
497
+ >>> from geobox.aio import AsyncGeoboxClient
498
+ >>> from geobox.aio.table import AsyncTableField
499
+ >>> async with AsyncGeoboxClient() as client:
500
+ >>> table = await client.get_table(uuid="12345678-1234-5678-1234-567812345678")
501
+ >>> field = await table.get_field(field_id=1)
502
+ >>> await field.update(name="my_field", display_name="My Field", description="My Field Description")
503
+ """
504
+ data = {
505
+ "name": kwargs.get('name'),
506
+ "display_name": kwargs.get('display_name'),
507
+ "description": kwargs.get('description'),
508
+ "domain": kwargs.get('domain'),
509
+ "hyperlink": kwargs.get('hyperlink')
510
+ }
511
+ return await super()._update(self.endpoint, data)
512
+
513
+
514
+ async def update_domain(self,
515
+ range_domain: Dict = None,
516
+ list_domain: Dict = None,
517
+ ) -> Dict:
518
+ """
519
+ [async] Update field domian values
520
+
521
+ Args:
522
+ range_domain (Dict): a dictionary with min and max keys.
523
+ list_domain (Dict): a dictionary containing the domain codes and values.
524
+
525
+ Returns:
526
+ Dict: the updated field domain
527
+
528
+ Example:
529
+ >>> from geobox.aio import AsyncGeoboxClient
530
+ >>> async with AsyncGeoboxClient() as client:
531
+ >>> field = await client.get_table(uuid="12345678-1234-5678-1234-567812345678").get_fields()[0]
532
+ >>> range_d = {'min': 1, 'max': 10}
533
+ >>> await field.update_domain(range_domain = range_d)
534
+ or
535
+ >>> list_d = {'1': 'value1', '2': 'value2'}
536
+ >>> await field.update_domain(list_domain=list_d)
537
+ """
538
+ if not self.domain:
539
+ self.domain = {'min': None, 'max': None, 'items': {}}
540
+
541
+ if range_domain:
542
+ self.domain['min'] = range_domain['min']
543
+ self.domain['max'] = range_domain['max']
544
+
545
+ if list_domain:
546
+ self.domain['items'] = {**self.domain['items'], **list_domain}
547
+
548
+ await self.update(domain=self.domain)
549
+ return self.domain
550
+
551
+
552
+ class AsyncRelationship(AsyncBase):
553
+ BASE_ENDPOINT = 'relationships/'
554
+
555
+ def __init__(self,
556
+ api: 'AsyncGeoboxClient',
557
+ uuid: str,
558
+ data: Optional[Dict] = {},
559
+ ):
560
+ """
561
+ Initialize a relationship instance.
562
+
563
+ Args:
564
+ api (AsyncGeoboxClient): The GeoboxClient instance for making requests.
565
+ uuid (str): The unique identifier for the relationship.
566
+ data (Dict): The response data of the table.
567
+ """
568
+ super().__init__(api=api, uuid=uuid, data=data)
569
+
570
+
571
+ def __repr__(self) -> str:
572
+ """
573
+ Return a string representation of the AsyncRelationship.
574
+
575
+ Returns:
576
+ str: The string representation of the AsyncRelationship.
577
+ """
578
+ return f"AsyncRelationship(uuid={self.uuid}, name={self.relation_name}, cardinality={self.relation_cardinality})"
579
+
580
+
581
+ @classmethod
582
+ async def get_relationships(
583
+ cls,
584
+ api: 'AsyncGeoboxClient',
585
+ **kwargs,
586
+ ) -> Union[List['AsyncRelationship'], int]:
587
+ """
588
+ [async] Get a list of relationships with optional filtering and pagination.
589
+
590
+ Args:
591
+ api (AsyncGeoboxClient): The GeoboxClient instance for making requests.
592
+
593
+ Keyword Args:
594
+ q (str): query filter based on OGC CQL standard. e.g. "field1 LIKE '%GIS%' AND created_at > '2021-01-01'"
595
+ search (str): search term for keyword-based searching among search_fields or all textual fields if search_fields does not have value. NOTE: if q param is defined this param will be ignored
596
+ search_fields (str): comma separated list of fields for searching
597
+ order_by (str): comma separated list of fields for sorting results [field1 A|D, field2 A|D, …]. e.g. name A, type D. NOTE: "A" denotes ascending order and "D" denotes descending order.
598
+ return_count (bool): Whether to return total count. default: False.
599
+ skip (int): Number of items to skip. default: 0
600
+ limit (int): Number of items to return. default: 10
601
+ user_id (int): Specific user. privileges required
602
+ shared (bool): Whether to return shared tables. default: False
603
+
604
+ Returns:
605
+ List[AsyncRelationship] | int: A list of relationship instances or the total number of relationships.
606
+
607
+ Example:
608
+ >>> from geobox.aio import AsyncGeoboxClient
609
+ >>> from geobox.aio.table import AsyncRelatinship
610
+ >>> async with AsyncGeoboxClient() as client:
611
+ >>> relationships = await client.get_relationships(q="name LIKE '%My relationship%'")
612
+ or
613
+ >>> relationships = await Table.get_relationships(client, q="name LIKE '%My relationship%'")
614
+ """
615
+ params = {
616
+ 'f': 'json',
617
+ 'q': kwargs.get('q'),
618
+ 'search': kwargs.get('search'),
619
+ 'search_fields': kwargs.get('search_fields'),
620
+ 'order_by': kwargs.get('order_by'),
621
+ 'return_count': kwargs.get('return_count', False),
622
+ 'skip': kwargs.get('skip', 0),
623
+ 'limit': kwargs.get('limit', 10),
624
+ 'user_id': kwargs.get('user_id'),
625
+ 'shared': kwargs.get('shared', False),
626
+ }
627
+ return await super()._get_list(api, cls.BASE_ENDPOINT, params, factory_func=lambda api, item: AsyncRelationship(api, item['uuid'], item))
628
+
629
+
630
+ @classmethod
631
+ async def create_relationship(
632
+ cls,
633
+ api: 'AsyncGeoboxClient',
634
+ name: str,
635
+ cardinality: RelationshipCardinality,
636
+ *,
637
+ source: 'RelationshipEndpoint',
638
+ target: 'RelationshipEndpoint',
639
+ relation_table: Optional['AsyncTable'] = None,
640
+ display_name: Optional[str] = None,
641
+ description: Optional[str] = None,
642
+ user_id: Optional[int] = None,
643
+ ) -> 'AsyncRelationship':
644
+ """
645
+ [async] Create a new AsyncRelationship
646
+
647
+ Args:
648
+ api (AsyncGeoboxClient): The GeoboxClient instance for making requests.
649
+ name (str): name of the relationship
650
+ cardinality (RelationshipCardinality): One to One, One to Many, or Many to Many
651
+
652
+ Keyword Args:
653
+ source (RelationshipEndpoint): Definition of the source side of the relationship, including the table (or layer), field, and foreign-key field
654
+ target (RelationshipEndpoint): Definition of the target side of the relationship, including the table (or layer), field, and foreign-key field
655
+ relation_table (AsyncTable, optional): The table that stores the relationship metadata or join records. (Required for Many-to-Many relationships)
656
+ display_name (str, optional): Human-readable name for the relationship
657
+ description (str, optional): the description of the relationship
658
+ user_id (int, optional): Specific user. privileges required.
659
+
660
+ Returns:
661
+ AsyncRelationship: a relationship instance
662
+
663
+ Example:
664
+ >>> from geobox.aio import AsyncGeoboxClient
665
+ >>> from geobox.aio.table import AsyncRelationship, RelationshipEndpoint, RelationshipCardinality
666
+ >>> async with AsyncGeoboxClient() as client:
667
+ >>> source = RelationshipEndpoint(
668
+ ... table=client.get_table_by_name('owner'),
669
+ ... field="name", # on source table
670
+ ... fk_field="book_name", # on relation table
671
+ ... )
672
+ >>> target = RelationshipEndpoint(
673
+ ... table=client.get_table_by_name('parcel'),
674
+ ... field="name", # on target table
675
+ ... fk_field="author_name", # on relation table
676
+ ... )
677
+ >>> relationship = await client.create_relationship(
678
+ ... name="book_author",
679
+ ... cardinality=RelationshipCardinality.ManytoMany,
680
+ ... source=source,
681
+ ... target=target,
682
+ ... relation_table=client.get_table_by_name('owner_parcel'),
683
+ ... )
684
+ or
685
+ >>> relationship = await AsyncRelationship.create_relationship(
686
+ ... client,
687
+ ... name="owner_parcel",
688
+ ... cardinality=RelationshipCardinality.ManytoMany,
689
+ ... source=source,
690
+ ... target=target,
691
+ ... relation_table=client.get_table_by_name('owner_parcel'),
692
+ ... )
693
+ """
694
+ data = {
695
+ "relation_name": name,
696
+ "display_name": display_name,
697
+ "description": description,
698
+ "relation_cardinality": cardinality.value,
699
+ "relation_table_id": relation_table.id if relation_table else None,
700
+ "relation_table_search": f"{relation_table.__class__.__name__.split('Async')[-1]}/{relation_table.name}" if relation_table else None,
701
+ "source_field_name": source.field if type(source.field) == str else source.field.name,
702
+ "source_fk_name": (source.fk_field if not source.fk_field or type(source.fk_field) == str else source.fk_field.name) if relation_table else None,
703
+ "source_id": source.table.id,
704
+ "source_search": f"{source.table.__class__.__name__.split('Async')[-1]}/{source.table.name}",
705
+ "source_type": str(source.table.__class__.__name__.split('Async')[-1]),
706
+ "target_field_name": target.field if type(target.field) == str else target.field.name,
707
+ "target_fk_name": (target.fk_field if not target.fk_field or type(target.fk_field) == str else target.fk_field.name) if relation_table else None,
708
+ "target_id": target.table.id,
709
+ "target_search": f"{target.table.__class__.__name__.split('Async')[-1]}/{target.table.name}",
710
+ "target_type": str(target.table.__class__.__name__.split('Async')[-1]),
711
+ "user_id": user_id,
712
+ }
713
+ return await super()._create(api, cls.BASE_ENDPOINT, data, factory_func=lambda api, item: AsyncRelationship(api, item['uuid'], item))
714
+
715
+
716
+ @classmethod
717
+ async def get_relationship(
718
+ cls,
719
+ api: 'AsyncGeoboxClient',
720
+ uuid: str,
721
+ user_id: Optional[int] = None,
722
+ ) -> 'AsyncRelationship':
723
+ """
724
+ [async] Get a relationship by UUID.
725
+
726
+ Args:
727
+ api (AsyncGeoboxClient): The AsyncGeoboxClient instance for making requests.
728
+ uuid (str): The UUID of the relationship to get.
729
+ user_id (int, optional): Specific user. privileges required.
730
+
731
+ Returns:
732
+ AsyncRelationship: The AsyncRelationship object.
733
+
734
+ Raises:
735
+ NotFoundError: If the AsyncRelationship with the specified UUID is not found.
736
+
737
+ Example:
738
+ >>> from geobox.aio import AsyncGeoboxClient
739
+ >>> from geobox.aio.table import AsyncRelationship
740
+ >>> async with AsyncGeoboxClient() as client:
741
+ >>> relationship = await client.get_relationship(uuid="12345678-1234-5678-1234-567812345678")
742
+ or
743
+ >>> relationship = await AsyncRelationship.get_relationship(client, uuid="12345678-1234-5678-1234-567812345678")
744
+ """
745
+ params = {
746
+ 'f': 'json',
747
+ 'user_id': user_id,
748
+ }
749
+ return await super()._get_detail(api, cls.BASE_ENDPOINT, uuid, params, factory_func=lambda api, item: AsyncRelationship(api, item['uuid'], item))
750
+
751
+
752
+ @classmethod
753
+ async def get_relationship_by_name(
754
+ cls,
755
+ api: 'AsyncGeoboxClient',
756
+ name: str,
757
+ user_id: Optional[int] = None,
758
+ ) -> Union['AsyncRelationship', None]:
759
+ """
760
+ [async] Get a relationship by name
761
+
762
+ Args:
763
+ api (AsyncGeoboxClient): The GeoboxClient instance for making requests.
764
+ name (str): the name of the relationship to get
765
+ user_id (int, optional): specific user. privileges required.
766
+
767
+ Returns:
768
+ AsyncRelationship | None: returns the relationship if a relationship matches the given name, else None
769
+
770
+ Example:
771
+ >>> from geobox.aio import AsyncGeoboxClient
772
+ >>> from geobox.aio.table import AsyncRelationship
773
+ >>> async with AsyncGeoboxClient() as client:
774
+ >>> relationship = await client.get_relationship_by_name(name='test')
775
+ or
776
+ >>> relationship = await AsyncRelationship.get_relationship_by_name(client, name='test')
777
+ """
778
+ relationships = await cls.get_relationships(api, q=f"name = '{name}'", user_id=user_id)
779
+ if relationships and relationships[0].relation_name == name:
780
+ return relationships[0]
781
+ else:
782
+ return None
783
+
784
+
785
+ async def update(self, **kwargs) -> Dict:
786
+ """
787
+ [async] Update the relationship.
788
+
789
+ Keyword Args:
790
+ name (str): The name of the relationship.
791
+ display_name (str): The display name of the relationship.
792
+ description (str): The description of the relationship.
793
+
794
+ Returns:
795
+ Dict: The updated relationship data.
796
+
797
+ Raises:
798
+ ValidationError: If the relationship data is invalid.
799
+
800
+ Example:
801
+ >>> from geobox.aio import AsyncGeoboxClient
802
+ >>> from geobox.aio.table import AsyncRelationship
803
+ >>> async with AsyncGeoboxClient() as client:
804
+ >>> relationship = await AsyncRelationship.get_relationship(client, uuid="12345678-1234-5678-1234-567812345678")
805
+ >>> await relationship.update(display_name="New Display Name")
806
+ """
807
+ data = {
808
+ "name": kwargs.get('name'),
809
+ "display_name": kwargs.get('display_name'),
810
+ "description": kwargs.get('description'),
811
+ }
812
+ return await super()._update(self.endpoint, data)
813
+
814
+
815
+ async def delete(self) -> None:
816
+ """
817
+ [async] Delete the AsyncRelationship
818
+
819
+ Returns:
820
+ None
821
+
822
+ Example:
823
+ >>> from geobox.aio import AsyncGeoboxClient
824
+ >>> from geobox.aio.table import AsyncRelationship
825
+ >>> async with AsyncGeoboxClient() as client:
826
+ >>> relationship = await AsyncRelationship.get_relationship(client, uuid="12345678-1234-5678-1234-567812345678")
827
+ >>> await relationship.delete()
828
+ """
829
+ await super()._delete(self.endpoint)
830
+
831
+
832
+ async def get_source(self) -> Union['AsyncTable', 'AsyncVectorLayer']:
833
+ """
834
+ [async] Get the source table or layer
835
+
836
+ Returns:
837
+ AsyncTable | AsyncVectorLayer: the source table or layer
838
+
839
+ Raises:
840
+ NotFoundError: if the table or layer has been deleted
841
+
842
+ Example:
843
+ >>> from geobox.aio import AsyncGeoboxClient
844
+ >>> async with AsyncGeoboxClient() as client:
845
+ >>> relationship = await client.get_relationship(uuid="12345678-1234-5678-1234-567812345678")
846
+ >>> source_table = await relationship.get_source()
847
+ """
848
+ try:
849
+ result = []
850
+ if self.source_type == 'Table':
851
+ result = await self.api.get_tables(
852
+ q=f"id = {self.source_id}"
853
+ )
854
+ elif self.source_type == 'VectorLayer':
855
+ result = await self.api.get_vectors(
856
+ q=f"id = {self.source_id}"
857
+ )
858
+ source = next(source for source in result if source.id == self.source_id)
859
+ return source
860
+
861
+ except StopIteration:
862
+ raise NotFoundError("Table not found!")
863
+
864
+
865
+ async def get_target(self) -> Union['AsyncTable', 'AsyncVectorLayer']:
866
+ """
867
+ [async] Get the target table or layer
868
+
869
+ Returns:
870
+ AsyncTable: the target table or layer
871
+
872
+ Raises:
873
+ NotFoundError: if the table or layer has been deleted
874
+
875
+ Example:
876
+ >>> from geobox.aio import AsyncGeoboxClient
877
+ >>> async with AsyncGeoboxClient() as client:
878
+ >>> relationship = await client.get_relationship(uuid="12345678-1234-5678-1234-567812345678")
879
+ >>> target_table = await relationship.get_target()
880
+ """
881
+ try:
882
+ result = []
883
+ if self.target_type == 'Table':
884
+ result = await self.api.get_tables(
885
+ q=f"id = {self.target_id}"
886
+ )
887
+ elif self.target_type == 'VectorLayer':
888
+ result = await self.api.get_vectors(
889
+ q=f"id = {self.target_id}"
890
+ )
891
+ target = next(target for target in result if target.id == self.target_id)
892
+ return target
893
+
894
+ except StopIteration:
895
+ raise NotFoundError("Table not found!")
896
+
897
+
898
+ async def get_relation_table(self) -> 'AsyncTable':
899
+ """
900
+ [async] Get the relation table
901
+
902
+ Returns:
903
+ AsyncTable: the relation table
904
+
905
+ Raises:
906
+ ValueError: If the relationship is not Many-to-Many and thus has no relation table
907
+ NotFoundError: if the table has been deleted
908
+
909
+ Example:
910
+ >>> from geobox.aio import AsyncGeoboxClient
911
+ >>> async with AsyncGeoboxClient() as client:
912
+ >>> relationship = await client.get_relationship(uuid="12345678-1234-5678-1234-567812345678")
913
+ >>> relation_table = await relationship.get_relation_table()
914
+ """
915
+ try:
916
+ if not self.relation_table_id:
917
+ raise ValueError(
918
+ "Relationship is not Many-to-Many. "
919
+ "Relation tables are only used for Many-to-Many cardinality."
920
+ )
921
+
922
+ result = await self.api.get_tables(
923
+ q=f"id = {self.relation_table_id}"
924
+ )
925
+ table = next(table for table in result if table.id == self.relation_table_id)
926
+ return table
927
+
928
+ except StopIteration:
929
+ raise NotFoundError("Table not found!")
930
+
931
+
932
+ async def associate_records(
933
+ self,
934
+ source_id: int,
935
+ *,
936
+ target_ids: Optional[List[int]] = None,
937
+ q: Optional[str] = None,
938
+ ) -> Dict:
939
+ """
940
+ [async] Create relationships between the source record and target records
941
+
942
+ Args:
943
+ source_id (int): the id of feature/row in the source layer/table
944
+ target_ids (List[int], optional): a list of target record ids to be associated with the current record
945
+ q (str, optional): query filter on target layer or table to select which target features or rows that are going to be related to the current record
946
+
947
+ Returns:
948
+ Dict: the record association result
949
+
950
+ Example:
951
+ >>> from geobox.aio import AsyncGeoboxClient
952
+ >>> async with AsyncGeoboxClient() as client:
953
+ >>> relationship = await client.get_relationship(uuid="12345678-1234-5678-1234-567812345678")
954
+ >>> result = await relationship.associate_records(
955
+ ... source_id=1,
956
+ ... target_ids=[1, 2, 3],
957
+ ... q="name LIKE '%_school'",
958
+ ... )
959
+ """
960
+ data = {
961
+ 'source_id': source_id,
962
+ 'target_ids': ', '.join([str(i) for i in target_ids]),
963
+ 'q': q,
964
+ }
965
+
966
+ endpoint = f"{self.endpoint}associateRecords/"
967
+ return await self.api.post(
968
+ endpoint,
969
+ clean_data(data),
970
+ is_json=False,
971
+ )
972
+
973
+
974
+ async def disassociate_records(
975
+ self,
976
+ source_id: int,
977
+ *,
978
+ target_ids: Optional[List[int]] = None,
979
+ q: Optional[str] = None,
980
+ ) -> Dict:
981
+ """
982
+ [async] Remove relationships between the source record and target records
983
+
984
+ Args:
985
+ source_id (int): the id of feature/row in the source layer/table
986
+ target_ids (List[int], optional): a list of target record ids to be disassociated with the current record
987
+ q (str, optional): query filter on target layer or table to select which target features or rows that are going to be related to the current record
988
+
989
+ Returns:
990
+ Dict: the record disassociation result
991
+
992
+ Example:
993
+ >>> from geobox.aio import AsyncGeoboxClient
994
+ >>> async with AsyncGeoboxClient() as client:
995
+ >>> relationship = await client.get_relationship(uuid="12345678-1234-5678-1234-567812345678")
996
+ >>> result = await relationship.disassociate_records(
997
+ ... source_id=1,
998
+ ... target_ids=[1, 2, 3],
999
+ ... q="name LIKE '%_school'",
1000
+ ... )
1001
+ """
1002
+ data = {
1003
+ 'source_id': source_id,
1004
+ 'target_ids': ', '.join([str(i) for i in target_ids]),
1005
+ 'q': q,
1006
+ }
1007
+
1008
+ endpoint = f"{self.endpoint}disassociateRecords/"
1009
+ return await self.api.post(
1010
+ endpoint,
1011
+ clean_data(data),
1012
+ is_json=False,
1013
+ )
1014
+
1015
+
1016
+ class AsyncTable(AsyncBase):
1017
+
1018
+ BASE_ENDPOINT = 'tables/'
1019
+
1020
+ def __init__(self,
1021
+ api: 'AsyncGeoboxClient',
1022
+ uuid: str,
1023
+ data: Optional[Dict] = {}):
1024
+ """
1025
+ Initialize a table instance.
1026
+
1027
+ Args:
1028
+ api (AsyncGeoboxClient): The AsyncGeoboxClient instance for making requests.
1029
+ uuid (str): The unique identifier for the table.
1030
+ data (Dict): The response data of the table.
1031
+ """
1032
+ super().__init__(api, uuid=uuid, data=data)
1033
+
1034
+
1035
+ @classmethod
1036
+ async def get_tables(cls, api: 'AsyncGeoboxClient', **kwargs) -> Union[List['AsyncTable'], int]:
1037
+ """
1038
+ [async] Get a list of tables with optional filtering and pagination.
1039
+
1040
+ Args:
1041
+ api (AsyncGeoboxClient): The AsyncGeoboxClient instance for making requests.
1042
+
1043
+ Keyword Args:
1044
+ include_settings (bool): Whether to include table settings. default: False
1045
+ temporary (bool): Whether to return temporary tables. default: False
1046
+ q (str): query filter based on OGC CQL standard. e.g. "field1 LIKE '%GIS%' AND created_at > '2021-01-01'"
1047
+ search (str): search term for keyword-based searching among search_fields or all textual fields if search_fields does not have value. NOTE: if q param is defined this param will be ignored
1048
+ search_fields (str): comma separated list of fields for searching
1049
+ order_by (str): comma separated list of fields for sorting results [field1 A|D, field2 A|D, …]. e.g. name A, type D. NOTE: "A" denotes ascending order and "D" denotes descending order.
1050
+ return_count (bool): Whether to return total count. default: False.
1051
+ skip (int): Number of items to skip. default: 0
1052
+ limit (int): Number of items to return. default: 10
1053
+ user_id (int): Specific user. privileges required
1054
+ shared (bool): Whether to return shared tables. default: False
1055
+
1056
+ Returns:
1057
+ List[AsyncTable] | int: A list of table instances or the total number of tables.
1058
+
1059
+ Example:
1060
+ >>> from geobox.aio import AsyncGeoboxClient
1061
+ >>> from geobox.aio.table import AsyncTable
1062
+ >>> async with AsyncGeoboxClient() as client:
1063
+ >>> tables = await client.get_tables(q="name LIKE '%My table%'")
1064
+ or
1065
+ >>> tables = await AsyncTable.get_tables(client, q="name LIKE '%My table%'")
1066
+ """
1067
+ params = {
1068
+ 'f': 'json',
1069
+ 'include_settings': kwargs.get('include_settings', False),
1070
+ 'temporary': kwargs.get('temporary'),
1071
+ 'q': kwargs.get('q'),
1072
+ 'search': kwargs.get('search'),
1073
+ 'search_fields': kwargs.get('search_fields'),
1074
+ 'order_by': kwargs.get('order_by'),
1075
+ 'return_count': kwargs.get('return_count', False),
1076
+ 'skip': kwargs.get('skip', 0),
1077
+ 'limit': kwargs.get('limit', 10),
1078
+ 'user_id': kwargs.get('user_id'),
1079
+ 'shared': kwargs.get('shared', False)
1080
+ }
1081
+ return await super()._get_list(api, cls.BASE_ENDPOINT, params, factory_func=lambda api, item: AsyncTable(api, item['uuid'], item))
1082
+
1083
+
1084
+ @classmethod
1085
+ async def create_table(cls,
1086
+ api: 'AsyncGeoboxClient',
1087
+ name: str,
1088
+ display_name: Optional[str] = None,
1089
+ description: Optional[str] = None,
1090
+ temporary: bool = False,
1091
+ fields: Optional[List[Dict]] = None,
1092
+ ) -> 'AsyncTable':
1093
+ """
1094
+ [async] Create a new table.
1095
+
1096
+ Args:
1097
+ api (AsyncGeoboxClient): The AsyncGeoboxClient instance for making requests.
1098
+ name (str): The name of the AsyncTable.
1099
+ display_name (str, optional): The display name of the table.
1100
+ description (str, optional): The description of the table.
1101
+ temporary (bool, optional): Whether to create a temporary tables. default: False
1102
+ fields (List[Dict], optional): raw table fields. you can use create_field method for simpler and safer field addition. required dictionary keys: name, datatype
1103
+
1104
+ Returns:
1105
+ AsyncTable: The newly created table instance.
1106
+
1107
+ Raises:
1108
+ ValidationError: If the table data is invalid.
1109
+
1110
+ Example:
1111
+ >>> from geobox.aio import AsyncGeoboxClient
1112
+ >>> from geobox.aio.table import AsyncTable
1113
+ >>> async with AsyncGeoboxClient() as client:
1114
+ >>> table = await client.create_table(name="my_table")
1115
+ or
1116
+ >>> table = await AsyncTable.create_table(client, name="my_table")
1117
+ """
1118
+ data = {
1119
+ "name": name,
1120
+ "display_name": display_name,
1121
+ "description": description,
1122
+ "temporary": temporary,
1123
+ "fields": fields,
1124
+ }
1125
+ return await super()._create(api, cls.BASE_ENDPOINT, data, factory_func=lambda api, item: AsyncTable(api, item['uuid'], item))
1126
+
1127
+
1128
+ @classmethod
1129
+ async def get_table(cls, api: 'AsyncGeoboxClient', uuid: str, user_id: int = None) -> 'AsyncTable':
1130
+ """
1131
+ [async] Get a table by UUID.
1132
+
1133
+ Args:
1134
+ api (AsyncGeoboxClient): The GeoboxClient instance for making requests.
1135
+ uuid (str): The UUID of the table to get.
1136
+ user_id (int): Specific user. privileges required.
1137
+
1138
+ Returns:
1139
+ AsyncTable: The AsyncTable object.
1140
+
1141
+ Raises:
1142
+ NotFoundError: If the table with the specified UUID is not found.
1143
+
1144
+ Example:
1145
+ >>> from geobox.aio import AsyncGeoboxClient
1146
+ >>> from geobox.aio.table import AsyncTable
1147
+ >>> async with AsyncGeoboxClient() as client:
1148
+ >>> table = await client.get_table(uuid="12345678-1234-5678-1234-567812345678")
1149
+ or
1150
+ >>> table = await AsyncTable.get_table(client, uuid="12345678-1234-5678-1234-567812345678")
1151
+ """
1152
+ params = {
1153
+ 'f': 'json',
1154
+ 'user_id': user_id,
1155
+ }
1156
+ return await super()._get_detail(api, cls.BASE_ENDPOINT, uuid, params, factory_func=lambda api, item: AsyncTable(api, item['uuid'], item))
1157
+
1158
+
1159
+ @classmethod
1160
+ async def get_table_by_name(cls, api: 'AsyncGeoboxClient', name: str, user_id: int = None) -> Union['AsyncTable', None]:
1161
+ """
1162
+ [async] Get a table by name
1163
+
1164
+ Args:
1165
+ api (AsyncGeoboxClient): The GeoboxClient instance for making requests.
1166
+ name (str): the name of the table to get
1167
+ user_id (int, optional): specific user. privileges required.
1168
+
1169
+ Returns:
1170
+ AsyncTable | None: returns the table if a table matches the given name, else None
1171
+
1172
+ Example:
1173
+ >>> from geobox.aio import AsyncGeoboxClient
1174
+ >>> from geobox.aio.table import AsyncTable
1175
+ >>> async with AsyncGeoboxClient() as client:
1176
+ >>> table = await client.get_table_by_name(name='test')
1177
+ or
1178
+ >>> table = await AsyncTable.get_table_by_name(client, name='test')
1179
+ """
1180
+ tables = await cls.get_tables(api, q=f"name = '{name}'", user_id=user_id)
1181
+ if tables and tables[0].name == name:
1182
+ return tables[0]
1183
+ else:
1184
+ return None
1185
+
1186
+
1187
+ async def update(self, **kwargs) -> Dict:
1188
+ """
1189
+ [async] Update the table.
1190
+
1191
+ Keyword Args:
1192
+ name (str): The name of the table.
1193
+ display_name (str): The display name of the table.
1194
+ description (str): The description of the table.
1195
+
1196
+ Returns:
1197
+ Dict: The updated table data.
1198
+
1199
+ Raises:
1200
+ ValidationError: If the table data is invalid.
1201
+
1202
+ Example:
1203
+ >>> from geobox.aio import AsyncGeoboxClient
1204
+ >>> from geobox.aio.table import AsyncTable
1205
+ >>> async with AsyncGeoboxClient() as client:
1206
+ >>> table = await AsyncTable.get_table(client, uuid="12345678-1234-5678-1234-567812345678")
1207
+ >>> await table.update(display_name="New Display Name")
1208
+ """
1209
+ data = {
1210
+ "name": kwargs.get('name'),
1211
+ "display_name": kwargs.get('display_name'),
1212
+ "description": kwargs.get('description'),
1213
+ }
1214
+ return await super()._update(self.endpoint, data)
1215
+
1216
+
1217
+ async def delete(self) -> None:
1218
+ """
1219
+ [async] Delete the AsyncTable.
1220
+
1221
+ Returns:
1222
+ None
1223
+
1224
+ Example:
1225
+ >>> from geobox.aio import AsyncGeoboxClient
1226
+ >>> from geobox.aio.table import AsyncTable
1227
+ >>> async with AsyncGeoboxClient() as client:
1228
+ >>> table = await AsyncTable.get_table(client, uuid="12345678-1234-5678-1234-567812345678")
1229
+ >>> await table.delete()
1230
+ """
1231
+ await super()._delete(self.endpoint)
1232
+
1233
+
1234
+ @property
1235
+ async def settings(self) -> Dict:
1236
+ """
1237
+ [async] Get the table's settings.
1238
+
1239
+ Returns:
1240
+ Dict: The table settings.
1241
+
1242
+ Example:
1243
+ >>> from geobox.aio import AsyncGeoboxClient
1244
+ >>> from geobox.aio.table import AsyncTable
1245
+ >>> async with AsyncGeoboxClient() as client:
1246
+ >>> table = await AsyncTable.get_table(api=client, uuid="12345678-1234-5678-1234-567812345678")
1247
+ >>> setting = await table.settings
1248
+ """
1249
+ return await super()._get_settings(endpoint=self.endpoint)
1250
+
1251
+
1252
+ async def update_settings(self, settings: Dict) -> Dict:
1253
+ """
1254
+ [async] Update the settings
1255
+
1256
+ settings (Dict): settings dictionary
1257
+
1258
+ Returns:
1259
+ Dict: updated settings
1260
+
1261
+ Example:
1262
+ >>> from geobox.aio import AsyncGeoboxClient
1263
+ >>> async with AsyncGeoboxClient() as client:
1264
+ >>> table1 = await client.get_table(uuid="12345678-1234-5678-1234-567812345678")
1265
+ >>> table2 = await client.get_table(uuid="12345678-1234-5678-1234-567812345678")
1266
+ >>> await table1.update_settings(table2.settings)
1267
+ """
1268
+ return await super()._set_settings(self.endpoint, settings)
1269
+
1270
+
1271
+ async def get_fields(self) -> List['AsyncTableField']:
1272
+ """
1273
+ [async] Get all fields of the table.
1274
+
1275
+ Returns:
1276
+ List[AsyncTableField]: A list of Field instances representing the table's fields.
1277
+
1278
+ Example:
1279
+ >>> from geobox.aio import AsyncGeoboxClient
1280
+ >>> from geobox.aio.table import AsyncTable
1281
+ >>> async with AsyncGeoboxClient() as client:
1282
+ >>> table = await AsyncTable.get_table(api=client, uuid="12345678-1234-5678-1234-567812345678")
1283
+ >>> fields = await table.get_fields()
1284
+ """
1285
+ endpoint = urljoin(self.endpoint, 'fields/')
1286
+ return await super()._get_list(
1287
+ api=self.api,
1288
+ endpoint=endpoint,
1289
+ factory_func=lambda api, item: AsyncTableField(table=self, data_type=FieldType(item['datatype']), field_id=item['id'], data=item),
1290
+ )
1291
+
1292
+
1293
+ async def get_field(self, field_id: int) -> 'AsyncTableField':
1294
+ """
1295
+ [async] Get a specific field by ID.
1296
+
1297
+ Args:
1298
+ field_id (int, optional): The ID of the field to retrieve.
1299
+
1300
+ Returns:
1301
+ AsyncTableField: The requested field instance.
1302
+
1303
+ Raises:
1304
+ NotFoundError: If the field with the specified ID is not found.
1305
+
1306
+ Example:
1307
+ >>> from geobox.aio import AsyncGeoboxClient
1308
+ >>> from geobox.aio.table import AsyncTable
1309
+ >>> async with AsyncGeoboxClient() as client:
1310
+ >>> table = await AsyncTable.get_table(api=client, uuid="12345678-1234-5678-1234-567812345678")
1311
+ >>> field = await table.get_field(field_id=1)
1312
+ """
1313
+ field = next((f for f in await self.get_fields() if f.id == field_id), None)
1314
+ if not field:
1315
+ raise NotFoundError(f'Field with ID {field_id} not found in table {self.name}')
1316
+
1317
+ return field
1318
+
1319
+
1320
+ async def get_field_by_name(self, name: str) -> 'AsyncTableField':
1321
+ """
1322
+ [async] Get a specific field by name.
1323
+
1324
+ Args:
1325
+ name (str): The name of the field to retrieve.
1326
+
1327
+ Returns:
1328
+ AsyncTableField: The requested field instance.
1329
+
1330
+ Raises:
1331
+ NotFoundError: If the field with the specified name is not found.
1332
+
1333
+ Example:
1334
+ >>> from geobox.aio import AsyncGeoboxClient
1335
+ >>> from geobox.aio.table import AsyncTable
1336
+ >>> async with AsyncGeoboxClient() as client:
1337
+ >>> table = await AsyncTable.get_table(api=client, uuid="12345678-1234-5678-1234-567812345678")
1338
+ >>> field = await table.get_field_by_name(name='test')
1339
+ """
1340
+ field = next((f for f in await self.get_fields() if f.name == name), None)
1341
+ if not field:
1342
+ raise NotFoundError(f"Field with name '{name}' not found in table {self.name}")
1343
+
1344
+ return field
1345
+
1346
+
1347
+ async def add_field(self, name: str, data_type: 'FieldType', data: Dict = {}) -> 'AsyncTableField':
1348
+ """
1349
+ [async] Add a new field to the table.
1350
+
1351
+ Args:
1352
+ name (str): The name of the new field.
1353
+ data_type (FieldType): The data type of the new field.
1354
+ data (Dict, optional): Additional field properties (display_name, description, etc.).
1355
+
1356
+ Returns:
1357
+ AsyncTableField: The newly created field instance.
1358
+
1359
+ Raises:
1360
+ ValidationError: If the field data is invalid.
1361
+
1362
+ Example:
1363
+ >>> from geobox.aio import AsyncGeoboxClient
1364
+ >>> from geobox.aio.table import AsyncTable
1365
+ >>> async with AsyncGeoboxClient() as client:
1366
+ >>> table = await AsyncTable.get_table(api=client, uuid="12345678-1234-5678-1234-567812345678")
1367
+ >>> field = await table.add_field(name="new_field", data_type=FieldType.String)
1368
+ """
1369
+ return await AsyncTableField.create_field(self, name=name, data_type=data_type, data=data)
1370
+
1371
+
1372
+ async def calculate_field(self,
1373
+ target_field: str,
1374
+ expression: str,
1375
+ q: Optional[str] = None,
1376
+ search: Optional[str] = None,
1377
+ search_fields: Optional[str] = None,
1378
+ row_ids: Optional[str] = None,
1379
+ run_async: bool = True,
1380
+ user_id: Optional[int] = None,
1381
+ ) -> Union['AsyncTask', Dict]:
1382
+ """
1383
+ [async] Calculate values for a field based on an expression.
1384
+
1385
+ Args:
1386
+ target_field (str): The field to calculate values for.
1387
+ expression (str): The expression to use for calculation.
1388
+ q (str, optional): Query to filter features. default: None.
1389
+ search (str, optional): search term for keyword-based searching among search_fields or all textual fields if search_fields does not have value
1390
+ search_fields (str, optional): comma separated list of fields for searching
1391
+ row_ids (str, optional): List of specific row IDs to include. default: None
1392
+ run_async (bool, optional): Whether to run the calculation asynchronously. default: True.
1393
+ user_id (int, optional): Specific user. privileges required.
1394
+
1395
+ Returns:
1396
+ Task | Dict: The task instance of the calculation operation or the api response if the run_async=False.
1397
+
1398
+ Example:
1399
+ >>> from geobox.aio import AsyncGeoboxClient
1400
+ >>> from geobox.aio.table import AsyncTable
1401
+ >>> async with AsyncGeoboxClient() as client:
1402
+ >>> table = await AsyncTable.get_table(api=client, uuid="12345678-1234-5678-1234-567812345678")
1403
+ >>> task = await table.calculate_field(target_field="target_field",
1404
+ ... expression="expression",
1405
+ ... q="name like 'my_layer'",
1406
+ ... row_ids=[1, 2, 3],
1407
+ ... run_async=True)
1408
+ """
1409
+ data = clean_data({
1410
+ "target_field": target_field,
1411
+ "expression": expression,
1412
+ "q": q,
1413
+ "search": search,
1414
+ "search_fields": search_fields,
1415
+ "row_ids": row_ids,
1416
+ "run_async": run_async,
1417
+ "user_id": user_id
1418
+ })
1419
+
1420
+ endpoint = urljoin(self.endpoint, 'calculateField/')
1421
+ response = await self.api.post(endpoint, data, is_json=False)
1422
+ if run_async:
1423
+ task = await AsyncTask.get_task(self.api, response.get('task_id'))
1424
+ return task
1425
+
1426
+ return response
1427
+
1428
+
1429
+ async def get_rows(self, **kwargs) -> List['AsyncTableRow']:
1430
+ """
1431
+ [async] Query rows of a table
1432
+
1433
+ Keyword Args:
1434
+ relationship_uuid (str): The uuid of relationship
1435
+ related_record_id (int): This is the id of the feature/row that these rows are related to. This id belongs to the related layer/table not this table
1436
+ q (str): Advanced filtering expression, e.g., 'status = "active" and age > 20'
1437
+ search (str): Search term for keyword-based searching among fields/columns
1438
+ search_fields (str): Comma separated column names to search in
1439
+ row_ids (str): Comma separated list of row ids to filter for
1440
+ fields (str): Comma separated column names to include in results, or [ALL]
1441
+ exclude (str): Comma separated column names to exclude from result
1442
+ order_by (str): Comma separated list for ordering, e.g., 'name A, id D'
1443
+ skip (int): Number of records to skip for pagination. default: 0
1444
+ limit (int): Maximum number of records to return. default: 100
1445
+ return_count (bool): If true, returns only the count of matching rows
1446
+ user_id (int): Specific user. privileges required
1447
+
1448
+ Returns:
1449
+ List[AsyncTableRow]: list of table rows objects
1450
+
1451
+ Example:
1452
+ >>> from geobox.aio import AsyncGeoboxClient
1453
+ >>> from geobox.aio.table import AsyncTable
1454
+ >>> async with AsyncGeoboxClient() as client:
1455
+ >>> table = await client.get_table(uuid="12345678-1234-5678-1234-567812345678")
1456
+ or
1457
+ >>> table = await AsyncTable.get_table(client, uuid="12345678-1234-5678-1234-567812345678")
1458
+
1459
+ >>> rows = await table.get_rows()
1460
+ """
1461
+ params = {
1462
+ 'f': 'json',
1463
+ 'relationship_uuid': kwargs.get('relationship_uuid'),
1464
+ 'related_record_id': kwargs.get('related_record_id'),
1465
+ 'q': kwargs.get('q'),
1466
+ 'search': kwargs.get('search'),
1467
+ 'search_fields': kwargs.get('search_fields'),
1468
+ 'row_ids': kwargs.get('row_ids'),
1469
+ 'fields': kwargs.get('fields'),
1470
+ 'exclude': kwargs.get('exclude'),
1471
+ 'order_by': kwargs.get('order_by'),
1472
+ 'skip': kwargs.get('skip', 0),
1473
+ 'limit': kwargs.get('limit', 100),
1474
+ 'return_count': kwargs.get('return_count', False),
1475
+ 'user_id': kwargs.get('user_id'),
1476
+ }
1477
+
1478
+ endpoint = f'{self.endpoint}rows/'
1479
+
1480
+ return await super()._get_list(
1481
+ api=self.api,
1482
+ endpoint=endpoint,
1483
+ params=params,
1484
+ factory_func=lambda api, item: AsyncTableRow(self, item),
1485
+ )
1486
+
1487
+
1488
+ async def get_row(self,
1489
+ row_id: int,
1490
+ user_id: Optional[int] = None,
1491
+ ) -> 'AsyncTableRow':
1492
+ """
1493
+ [async] Get a row by its id
1494
+
1495
+ Args:
1496
+ row_id (int): the row id
1497
+ user_id (int, optional): specific user. privileges required.
1498
+
1499
+ Returns:
1500
+ TanbleRow: the table row instance
1501
+
1502
+ Example:
1503
+ >>> from geobox.aio import AsyncGeoboxClient
1504
+ >>> from geobox.aio.table import AsyncTable
1505
+ >>> async with AsyncGeoboxClient() as client:
1506
+ >>> table = await client.get_table(uuid="12345678-1234-5678-1234-567812345678")
1507
+ or
1508
+ >>> table = await AsyncTable.get_table(client, uuid="12345678-1234-5678-1234-567812345678")
1509
+
1510
+ >>> row = await table.get_row(row_id=1)
1511
+ """
1512
+ return await AsyncTableRow.get_row(self, row_id, user_id)
1513
+
1514
+
1515
+ async def create_row(self, **kwargs) -> 'AsyncTableRow':
1516
+ """
1517
+ [async] Create a new row in the table.
1518
+
1519
+ Each keyword argument represents a field value for the row, where:
1520
+ - The keyword is the field name
1521
+ - The value is the field value
1522
+
1523
+ Keyword Args:
1524
+ **kwargs: Arbitrary field values matching the table schema.
1525
+
1526
+ Returns:
1527
+ AsyncTableRow: created table row instance
1528
+
1529
+ Example:
1530
+ >>> from geobox.aio import AsyncGeoboxClient
1531
+ >>> from geobox.aio.table import AsyncTable
1532
+ >>> async with AsyncGeoboxClient() as client:
1533
+ >>> table = await client.get_table(uuid="12345678-1234-5678-1234-567812345678")
1534
+ or
1535
+ >>> table = await AsyncTable.get_table(client, uuid="12345678-1234-5678-1234-567812345678")
1536
+
1537
+ >>> row = await table.create_row(
1538
+ ... field1=value1
1539
+ ... )
1540
+ """
1541
+ return await AsyncTableRow.create_row(self, **kwargs)
1542
+
1543
+
1544
+ async def import_rows(self,
1545
+ file: 'AsyncFile',
1546
+ *,
1547
+ file_encoding: str = "utf-8",
1548
+ input_dataset: Optional[str] = None,
1549
+ delimiter: str = ',',
1550
+ has_header: bool = True,
1551
+ report_errors: bool = False,
1552
+ bulk_insert: bool = True,
1553
+ ) -> 'AsyncTask':
1554
+ """
1555
+ Import rows from a CSV file into a table
1556
+
1557
+ Args:
1558
+ file (AsyncFile): file object to import.
1559
+ file_encoding (str, optional): Character encoding of the input file. default: utf-8
1560
+ input_dataset (str, optional): Name of the dataset in the input file.
1561
+ delimiter (str, optional): the delimiter of the dataset. default: ,
1562
+ has_header (bool, optional): Whether the file has header or not. default: True
1563
+ report_errors (bool, optional): Whether to report import errors. default: False
1564
+ bulk_insert (bool, optional):
1565
+
1566
+ Returns:
1567
+ AsyncTask: The task instance of the import operation.
1568
+
1569
+ Raises:
1570
+ ValidationError: If the import parameters are invalid.
1571
+
1572
+ Example:
1573
+ >>> from geobox.aio import AsyncGeoboxClient
1574
+ >>> from geobox.aio.table import AsyncTable
1575
+ >>> async with AsyncGeoboxClient() as client:
1576
+ >>> table = await AsyncTable.get_table(api=client, uuid="12345678-1234-5678-1234-567812345678")
1577
+ >>> file = await client.get_file(uuid="12345678-1234-5678-1234-567812345678")
1578
+ >>> task = await table.import_rows(
1579
+ ... file=file,
1580
+ ... )
1581
+ """
1582
+ data = clean_data({
1583
+ "file_uuid": file.uuid,
1584
+ "file_encoding": file_encoding,
1585
+ "input_dataset": file.name if not input_dataset else input_dataset,
1586
+ "delimiter": delimiter,
1587
+ "has_header": has_header,
1588
+ "report_errors": report_errors,
1589
+ "bulk_insert": bulk_insert,
1590
+ })
1591
+
1592
+ endpoint = urljoin(self.endpoint, 'import-csv/')
1593
+ response = await self.api.post(endpoint, data, is_json=False)
1594
+ task = await AsyncTask.get_task(self.api, response.get('task_id'))
1595
+ return task
1596
+
1597
+
1598
+ async def export_rows(self,
1599
+ out_filename: str,
1600
+ *,
1601
+ out_format: 'TableExportFormat' = TableExportFormat.CSV,
1602
+ q: Optional[str] = None,
1603
+ search: Optional[str] = None,
1604
+ search_fields: Optional[str] = None,
1605
+ row_ids: Optional[str] = None,
1606
+ fields: Optional[str] = None,
1607
+ exclude: Optional[str] = None,
1608
+ order_by: Optional[str] = None,
1609
+ zipped: bool = False,
1610
+ run_async: bool = True,
1611
+ ) -> Union['AsyncTask', str]:
1612
+ """
1613
+ [async] Export rows of a table to a file
1614
+
1615
+ Args:
1616
+ out_filename (str): Name of the output file without the format (.csv)
1617
+ out_format (TableExportFormat, optional): Format of the output file
1618
+ q (str, optional): query filter based on OGC CQL standard. e.g. "field1 LIKE '%GIS%' AND created_at > '2021-01-01'"
1619
+ search (str, optional): search term for keyword-based searching among search_fields or all textual fields if search_fields does not have value
1620
+ search_fields (str, optional): comma separated list of fields for searching
1621
+ row_ids (str, optional): List of specific row IDs to include
1622
+ fields (str, optional): List of specific field names to include
1623
+ exclude (str, optional): List of specific field names to exclude
1624
+ order_by (str, optional): comma separated list of fields for sorting results [field1 A|D, field2 A|D, …]. e.g. name A, type D. NOTE: "A" denotes ascending order and "D" denotes descending order.
1625
+ zipped (str, optional): Whether to compress the output file
1626
+ run_async (bool, optional): Whether to run the export asynchronously. default: True
1627
+
1628
+ Returns:
1629
+ AsyncTask | Dict: The task instance of the export operation (run_async=True) or the export result (run_async=False)
1630
+
1631
+ Raises:
1632
+ ValidationError: If the export parameters are invalid.
1633
+
1634
+ Example:
1635
+ >>> from geobox.aio import AsyncGeoboxClient
1636
+ >>> from geobox.aio.table import AsyncTable
1637
+ >>> async with AsyncGeoboxClient() as client:
1638
+ >>> table = await AsyncTable.get_table(api=client, uuid="12345678-1234-5678-1234-567812345678")
1639
+ >>> file = await client.get_file(uuid="12345678-1234-5678-1234-567812345678")
1640
+ >>> task = await table.export_rows(
1641
+ ... file=file,
1642
+ ... )
1643
+ """
1644
+ data = clean_data({
1645
+ "out_filename": out_filename,
1646
+ "out_format": out_format.value if out_format else None,
1647
+ "q": q,
1648
+ "search": search,
1649
+ "search_fields": search_fields,
1650
+ "row_ids": row_ids,
1651
+ "fields": fields,
1652
+ "exclude": exclude,
1653
+ "order_by": order_by,
1654
+ "zipped": zipped,
1655
+ "run_async": run_async,
1656
+ })
1657
+
1658
+ endpoint = urljoin(self.endpoint, 'export/')
1659
+ response = await self.api.post(endpoint, data, is_json=False)
1660
+ if run_async:
1661
+ task = await AsyncTask.get_task(self.api, response.get('task_id'))
1662
+ return task
1663
+
1664
+ return response
1665
+
1666
+
1667
+ async def share(self, users: List['AsyncUser']) -> None:
1668
+ """
1669
+ [async] Shares the table with specified users.
1670
+
1671
+ Args:
1672
+ users (List[AsyncUser]): The list of user objects to share the table with.
1673
+
1674
+ Returns:
1675
+ None
1676
+
1677
+ Example:
1678
+ >>> from geobox.aio import AsyncGeoboxClient
1679
+ >>> from geobox.aio.table import AsyncTable
1680
+ >>> async with AsyncGeoboxClient() as client:
1681
+ >>> table = await AsyncTable.get_table(client, uuid="12345678-1234-5678-1234-567812345678")
1682
+ >>> users = await client.search_users(search='John')
1683
+ >>> await table.share(users=users)
1684
+ """
1685
+ await super()._share(self.endpoint, users)
1686
+
1687
+
1688
+ async def unshare(self, users: List['AsyncUser']) -> None:
1689
+ """
1690
+ [async] Unshares the table with specified users.
1691
+
1692
+ Args:
1693
+ users (List[User]): The list of user objects to unshare the table with.
1694
+
1695
+ Returns:
1696
+ None
1697
+
1698
+ Example:
1699
+ >>> from geobox.aio import AsyncGeoboxClient
1700
+ >>> from geobox.aio.table import AsyncTable
1701
+ >>> async with AsyncGeoboxClient() as client:
1702
+ >>> table = await AsyncTable.get_table(client, uuid="12345678-1234-5678-1234-567812345678")
1703
+ >>> users = await client.search_users(search='John')
1704
+ >>> await table.unshare(users=users)
1705
+ """
1706
+ await super()._unshare(self.endpoint, users)
1707
+
1708
+
1709
+ async def get_shared_users(self, search: str = None, skip: int = 0, limit: int = 10) -> List['AsyncUser']:
1710
+ """
1711
+ [async] Retrieves the list of users the table is shared with.
1712
+
1713
+ Args:
1714
+ search (str, optional): The search query.
1715
+ skip (int, optional): The number of users to skip.
1716
+ limit (int, optional): The maximum number of users to retrieve.
1717
+
1718
+ Returns:
1719
+ List[AsyncUser]: The list of shared users.
1720
+
1721
+ Example:
1722
+ >>> from geobox.aio import AsyncGeoboxClient
1723
+ >>> from geobox.aio.table import AsyncTable
1724
+ >>> async with AsyncGeoboxClient() as client:
1725
+ >>> table = await AsyncTable.get_table(client, uuid="12345678-1234-5678-1234-567812345678")
1726
+ >>> await table.get_shared_users(search='John', skip=0, limit=10)
1727
+ """
1728
+ params = {
1729
+ 'search': search,
1730
+ 'skip': skip,
1731
+ 'limit': limit
1732
+ }
1733
+ return await super()._get_shared_users(self.endpoint, params)