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