geobox 2.3.0__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 +153 -7
  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 +644 -55
  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 +152 -7
  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 +11 -1
  36. geobox/feature.py +170 -15
  37. geobox/field.py +0 -28
  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 +640 -55
  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.3.0.dist-info → geobox-2.4.0.dist-info}/METADATA +1 -1
  61. geobox-2.4.0.dist-info/RECORD +74 -0
  62. {geobox-2.3.0.dist-info → geobox-2.4.0.dist-info}/WHEEL +1 -1
  63. geobox-2.3.0.dist-info/RECORD +0 -74
  64. {geobox-2.3.0.dist-info → geobox-2.4.0.dist-info}/licenses/LICENSE +0 -0
  65. {geobox-2.3.0.dist-info → geobox-2.4.0.dist-info}/top_level.txt +0 -0
geobox/table.py CHANGED
@@ -1,21 +1,35 @@
1
1
  from typing import List, Dict, Optional, TYPE_CHECKING, Union, Any
2
2
  from urllib.parse import urljoin
3
-
4
- from geobox.enums import TableExportFormat
3
+ from dataclasses import dataclass
5
4
 
6
5
  from .base import Base
7
6
  from .task import Task
8
- from .enums import FieldType
7
+ from .field import Field
8
+ from .vectorlayer import VectorLayer
9
+ from .enums import FieldType, TableExportFormat, RelationshipCardinality
9
10
  from .exception import NotFoundError
10
11
  from .utils import clean_data
11
12
 
12
13
  if TYPE_CHECKING:
13
14
  from . import GeoboxClient
14
15
  from .user import User
15
- from .aio import AsyncGeoboxClient
16
16
  from .file import File
17
- from .aio.table import Table as AsyncTable, AsyncTableRow, AsyncTableField
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
18
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
19
33
 
20
34
 
21
35
  class TableRow(Base):
@@ -153,30 +167,182 @@ class TableRow(Base):
153
167
  super()._delete(self.endpoint)
154
168
 
155
169
 
156
- def to_async(self, async_client: 'AsyncGeoboxClient') -> 'AsyncTableRow':
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:
157
283
  """
158
- Switch to async version of the table row instance to have access to the async methods
284
+ Create relationships between the source record and target records
159
285
 
160
286
  Args:
161
- async_client (AsyncGeoboxClient): The async version of the GeoboxClient instance for making requests.
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
162
290
 
163
291
  Returns:
164
- AsyncTableRow: the async instance of the TableRow.
292
+ Dict: the record association result
165
293
 
166
294
  Example:
167
- >>> from geobox import Geoboxclient
168
- >>> from geobox.aio import AsyncGeoboxClient
295
+ >>> from geobox import GeoboxClient
169
296
  >>> client = GeoboxClient()
170
297
  >>> table = client.get_table(uuid="12345678-1234-5678-1234-567812345678")
171
298
  >>> row = table.get_row(row_id=1)
172
- >>> async with AsyncGeoboxClient() as async_client:
173
- >>> async_row = row.to_async(async_client)
299
+ >>> row.associate_with(
300
+ ... relationship_uuid="12345678-1234-5678-1234-567812345678",
301
+ ... target_ids=[1, 2, 3],
302
+ ... )
174
303
  """
175
- from .aio.table import AsyncTableRow
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
+ )
176
310
 
177
- async_table = self.table.to_async(async_client=async_client)
178
- return AsyncTableRow(table=async_table, data=self.data)
179
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
+ )
180
346
 
181
347
 
182
348
  class TableField(Base):
@@ -373,31 +539,469 @@ class TableField(Base):
373
539
  return self.domain
374
540
 
375
541
 
376
- def to_async(self, async_client: 'AsyncGeoboxClient') -> 'AsyncTableField':
542
+ class Relationship(Base):
543
+ BASE_ENDPOINT = 'relationships/'
544
+
545
+ def __init__(self,
546
+ api: 'GeoboxClient',
547
+ uuid: str,
548
+ data: Optional[Dict] = {},
549
+ ):
377
550
  """
378
- Switch to async version of the field instance to have access to the async methods
551
+ Initialize a relationship instance.
379
552
 
380
553
  Args:
381
- async_client (AsyncGeoboxClient): The async version of the GeoboxClient instance for making requests.
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.
382
564
 
383
565
  Returns:
384
- AsyncField: the async instance of the field.
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.
385
596
 
386
597
  Example:
387
- >>> from geobox import Geoboxclient
388
- >>> from geobox.aio import AsyncGeoboxClient
598
+ >>> from geobox import GeoboxClient
599
+ >>> from geobox.table import Relatinship
389
600
  >>> client = GeoboxClient()
390
- >>> table = client.get_table(uuid="12345678-1234-5678-1234-567812345678")
391
- >>> field = table.get_field(name='test')
392
- >>> async with AsyncGeoboxClient() as async_client:
393
- >>> async_field = field.to_async(async_client)
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:
394
776
  """
395
- from .aio.table import AsyncTableField
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.
396
786
 
397
- async_table = self.table.to_async(async_client=async_client)
398
- return AsyncTableField(table=async_table, data_type=self.data_type, field_id=self.field_id, data=self.data)
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)
399
803
 
400
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
+
401
1005
 
402
1006
  class Table(Base):
403
1007
 
@@ -421,7 +1025,7 @@ class Table(Base):
421
1025
  @classmethod
422
1026
  def get_tables(cls, api: 'GeoboxClient', **kwargs) -> Union[List['Table'], int]:
423
1027
  """
424
- Get list of tables with optional filtering and pagination.
1028
+ Get a list of tables with optional filtering and pagination.
425
1029
 
426
1030
  Args:
427
1031
  api (GeoboxClient): The GeoboxClient instance for making requests.
@@ -590,7 +1194,7 @@ class Table(Base):
590
1194
  >>> from geobox.table import Table
591
1195
  >>> client = GeoboxClient()
592
1196
  >>> table = Table.get_table(client, uuid="12345678-1234-5678-1234-567812345678")
593
- >>> table.update_table(display_name="New Display Name")
1197
+ >>> table.update(display_name="New Display Name")
594
1198
  """
595
1199
  data = {
596
1200
  "name": kwargs.get('name'),
@@ -738,7 +1342,7 @@ class Table(Base):
738
1342
  data (Dict, optional): Additional field properties (display_name, description, etc.).
739
1343
 
740
1344
  Returns:
741
- Field: The newly created field instance.
1345
+ TableField: The newly created field instance.
742
1346
 
743
1347
  Raises:
744
1348
  ValidationError: If the field data is invalid.
@@ -815,6 +1419,8 @@ class Table(Base):
815
1419
  Query rows of a table
816
1420
 
817
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
818
1424
  q (str): Advanced filtering expression, e.g., 'status = "active" and age > 20'
819
1425
  search (str): Search term for keyword-based searching among fields/columns
820
1426
  search_fields (str): Comma separated column names to search in
@@ -842,6 +1448,8 @@ class Table(Base):
842
1448
  """
843
1449
  params = {
844
1450
  'f': 'json',
1451
+ 'relationship_uuid': kwargs.get('relationship_uuid'),
1452
+ 'related_record_id': kwargs.get('related_record_id'),
845
1453
  'q': kwargs.get('q'),
846
1454
  'search': kwargs.get('search'),
847
1455
  'search_fields': kwargs.get('search_fields'),
@@ -1109,26 +1717,3 @@ class Table(Base):
1109
1717
  'limit': limit
1110
1718
  }
1111
1719
  return super()._get_shared_users(self.endpoint, params)
1112
-
1113
-
1114
- def to_async(self, async_client: 'AsyncGeoboxClient') -> 'AsyncTable':
1115
- """
1116
- Switch to async version of the table instance to have access to the async methods
1117
-
1118
- Args:
1119
- async_client (AsyncGeoboxClient): The async version of the GeoboxClient instance for making requests.
1120
-
1121
- Returns:
1122
- AsyncTable: the async instance of the table.
1123
-
1124
- Example:
1125
- >>> from geobox import Geoboxclient
1126
- >>> from geobox.aio import AsyncGeoboxClient
1127
- >>> client = GeoboxClient()
1128
- >>> table = Table.get_table(client, uuid="12345678-1234-5678-1234-567812345678")
1129
- >>> async with AsyncGeoboxClient() as async_client:
1130
- >>> async_table = table.to_async(async_client)
1131
- """
1132
- from .aio.table import AsyncTable
1133
-
1134
- return AsyncTable(api=async_client, uuid=self.uuid, data=self.data)