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/api.py CHANGED
@@ -5,7 +5,7 @@ from urllib.parse import urljoin
5
5
  from typing import Dict, List, Optional, Union
6
6
  from datetime import datetime
7
7
 
8
- from geobox.enums import AnalysisDataType, AnalysisResampleMethod
8
+ from geobox.enums import AnalysisDataType, AnalysisResampleMethod, RelationshipCardinality
9
9
 
10
10
  from .exception import AuthenticationError, ApiRequestError, NotFoundError, ValidationError, ServerError, AuthorizationError
11
11
  from .vectorlayer import VectorLayer, LayerType
@@ -35,6 +35,7 @@ from .attachment import Attachment
35
35
  from .apikey import ApiKey
36
36
  from .log import Log
37
37
  from .usage import Usage, UsageScale, UsageParam
38
+ from .table import RelationshipEndpoint, Table, Relationship
38
39
 
39
40
  logger = logging.getLogger(__name__)
40
41
 
@@ -187,10 +188,10 @@ class GeoboxClient:
187
188
  >>> client = GeoboxClient(apikey="apikey")
188
189
  >>> client = GeoboxClient(access_token="access_token")
189
190
  """
190
- self.username = os.getenv('GEOBOX_USERNAME') if os.getenv('GEOBOX_USERNAME') else username
191
- self.password = os.getenv('GEOBOX_PASSWORD') if os.getenv('GEOBOX_PASSWORD') else password
192
- self.access_token = os.getenv('GEOBOX_ACCESS_TOKEN') if os.getenv('GEOBOX_ACCESS_TOKEN') else access_token
193
- self.apikey = os.getenv('GEOBOX_APIKEY') if os.getenv('GEOBOX_APIKEY') else apikey
191
+ self.username = username if username else os.getenv('GEOBOX_USERNAME')
192
+ self.password = password if password else os.getenv('GEOBOX_PASSWORD')
193
+ self.access_token = access_token if access_token else os.getenv('GEOBOX_ACCESS_TOKEN')
194
+ self.apikey = apikey if apikey else os.getenv('GEOBOX_APIKEY')
194
195
  self.verify = verify
195
196
  self.session = _RequestSession(access_token=self.access_token)
196
197
 
@@ -2638,3 +2639,262 @@ class GeoboxClient:
2638
2639
  >>> client.update_usage()
2639
2640
  """
2640
2641
  return Usage.update_usage(self, user_id=user_id)
2642
+
2643
+
2644
+
2645
+ def get_tables(self, **kwargs) -> Union[List['Table'], int]:
2646
+ """
2647
+ Get list of tables with optional filtering and pagination.
2648
+
2649
+ Keyword Args:
2650
+ include_settings (bool): Whether to include table settings. default: False
2651
+ temporary (bool): Whether to return temporary tables. default: False
2652
+ q (str): query filter based on OGC CQL standard. e.g. "field1 LIKE '%GIS%' AND created_at > '2021-01-01'"
2653
+ 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
2654
+ search_fields (str): comma separated list of fields for searching
2655
+ 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.
2656
+ return_count (bool): Whether to return total count. default: False.
2657
+ skip (int): Number of items to skip. default: 0
2658
+ limit (int): Number of items to return. default: 10
2659
+ user_id (int): Specific user. privileges required
2660
+ shared (bool): Whether to return shared tables. default: False
2661
+
2662
+ Returns:
2663
+ List[Table] | int: A list of table instances or the total number of tables.
2664
+
2665
+ Example:
2666
+ >>> from geobox import GeoboxClient
2667
+ >>> client = GeoboxClient()
2668
+ >>> tables = client.get_tables(q="name LIKE '%My table%'")
2669
+ """
2670
+ return Table.get_tables(self, **kwargs)
2671
+
2672
+
2673
+ def create_table(self,
2674
+ name: str,
2675
+ display_name: Optional[str] = None,
2676
+ description: Optional[str] = None,
2677
+ temporary: bool = False,
2678
+ fields: Optional[List[Dict]] = None,
2679
+ ) -> 'Table':
2680
+ """
2681
+ Create a new table.
2682
+
2683
+ Args:
2684
+ name (str): The name of the Table.
2685
+ display_name (str, optional): The display name of the table.
2686
+ description (str, optional): The description of the table.
2687
+ temporary (bool, optional): Whether to create a temporary tables. default: False
2688
+ fields (List[Dict], optional): raw table fields. you can use add_field method for simpler and safer field addition. required dictionary keys: name, datatype
2689
+
2690
+ Returns:
2691
+ Table: The newly created table instance.
2692
+
2693
+ Raises:
2694
+ ValidationError: If the table data is invalid.
2695
+
2696
+ Example:
2697
+ >>> from geobox import GeoboxClient
2698
+ >>> client = GeoboxClient()
2699
+ >>> table = client.create_table(name="my_table")
2700
+ """
2701
+ return Table.create_table(self,
2702
+ name=name,
2703
+ display_name=display_name,
2704
+ description=description,
2705
+ temporary=temporary,
2706
+ fields=fields,
2707
+ )
2708
+
2709
+
2710
+ def get_table(self,
2711
+ uuid: str,
2712
+ user_id: int = None,
2713
+ ) -> 'Table':
2714
+ """
2715
+ Get a table by UUID.
2716
+
2717
+ Args:
2718
+ uuid (str): The UUID of the table to get.
2719
+ user_id (int): Specific user. privileges required.
2720
+
2721
+ Returns:
2722
+ Table: The Table object.
2723
+
2724
+ Raises:
2725
+ NotFoundError: If the table with the specified UUID is not found.
2726
+
2727
+ Example:
2728
+ >>> from geobox import GeoboxClient
2729
+ >>> client = GeoboxClient()
2730
+ >>> table = client.get_table(uuid="12345678-1234-5678-1234-567812345678")
2731
+ """
2732
+ return Table.get_table(self, uuid, user_id)
2733
+
2734
+
2735
+ def get_table_by_name(self,
2736
+ name: str,
2737
+ user_id: int = None,
2738
+ ) -> Union['Table', None]:
2739
+ """
2740
+ Get a table by name
2741
+
2742
+ Args:
2743
+ name (str): the name of the table to get
2744
+ user_id (int, optional): specific user. privileges required.
2745
+
2746
+ Returns:
2747
+ Table | None: returns the table if a table matches the given name, else None
2748
+
2749
+ Example:
2750
+ >>> from geobox import GeoboxClient
2751
+ >>> client = GeoboxClient()
2752
+ >>> table = client.get_table_by_name(name='test')
2753
+
2754
+ """
2755
+ return Table.get_table_by_name(self, name, user_id)
2756
+
2757
+
2758
+ def get_relationships(
2759
+ self,
2760
+ **kwargs,
2761
+ ) -> Union[List['Relationship'], int]:
2762
+ """
2763
+ Get a list of relationships with optional filtering and pagination.
2764
+
2765
+ Keyword Args:
2766
+ q (str): query filter based on OGC CQL standard. e.g. "field1 LIKE '%GIS%' AND created_at > '2021-01-01'"
2767
+ 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
2768
+ search_fields (str): comma separated list of fields for searching
2769
+ 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.
2770
+ return_count (bool): Whether to return total count. default: False.
2771
+ skip (int): Number of items to skip. default: 0
2772
+ limit (int): Number of items to return. default: 10
2773
+ user_id (int): Specific user. privileges required
2774
+ shared (bool): Whether to return shared tables. default: False
2775
+
2776
+ Returns:
2777
+ List[Relationship] | int: A list of relationship instances or the total number of relationships.
2778
+
2779
+ Example:
2780
+ >>> from geobox import GeoboxClient
2781
+ >>> from geobox.table import Relatinship
2782
+ >>> client = GeoboxClient()
2783
+ >>> relationships = client.get_relationships(q="name LIKE '%My relationship%'")
2784
+ """
2785
+ return Relationship.get_relationships(self, **kwargs)
2786
+
2787
+
2788
+ def create_relationship(
2789
+ self,
2790
+ name: str,
2791
+ cardinality: 'RelationshipCardinality',
2792
+ *,
2793
+ source: 'RelationshipEndpoint',
2794
+ target: 'RelationshipEndpoint',
2795
+ relation_table: Optional['Table'] = None,
2796
+ display_name: Optional[str] = None,
2797
+ description: Optional[str] = None,
2798
+ user_id: Optional[int] = None,
2799
+ ) -> 'Relationship':
2800
+ """
2801
+ Create a new Relationship
2802
+
2803
+ Args:
2804
+ name (str): name of the relationship
2805
+ cardinality (RelationshipCardinality): One to One, One to Many, or Many to Many
2806
+
2807
+ Keyword Args:
2808
+ source (RelationshipEndpoint): Definition of the source side of the relationship, including the table (or layer), field, and foreign-key field
2809
+ target (RelationshipEndpoint): Definition of the target side of the relationship, including the table (or layer), field, and foreign-key field
2810
+ relation_table (Table, optional): The table that stores the relationship metadata or join records. (Required for Many-to-Many relationships)
2811
+ display_name (str, optional): Human-readable name for the relationship
2812
+ description (str, optional): the description of the relationship
2813
+ user_id (int, optional): Specific user. privileges required.
2814
+
2815
+ Returns:
2816
+ Relationship: a relationship instance
2817
+
2818
+ Example:
2819
+ >>> from geobox import GeoboxClient
2820
+ >>> from geobox.table import RelationshipEndpoint, RelationshipCardinality
2821
+ >>> client = GeoboxClient()
2822
+ >>> source = RelationshipEndpoint(
2823
+ ... table=client.get_table_by_name('owner'),
2824
+ ... field="name", # on source table
2825
+ ... fk_field="book_name", # on relation table
2826
+ ... )
2827
+ >>> target = RelationshipEndpoint(
2828
+ ... table=client.get_table_by_name('parcel'),
2829
+ ... field="name", # on target table
2830
+ ... fk_field="author_name", # on relation table
2831
+ ... )
2832
+ >>> relationship = client.create_relationship(
2833
+ ... name="owner_parcel",
2834
+ ... cardinality=RelationshipCardinality.ManytoMany,
2835
+ ... source=source,
2836
+ ... target=target,
2837
+ ... relation_table=client.get_table_by_name('owner_parcel'),
2838
+ ... )
2839
+ """
2840
+ return Relationship.create_relationship(
2841
+ self,
2842
+ name=name,
2843
+ cardinality=cardinality,
2844
+ source=source,
2845
+ target=target,
2846
+ relation_table=relation_table,
2847
+ display_name=display_name,
2848
+ description=description,
2849
+ user_id=user_id,
2850
+ )
2851
+
2852
+
2853
+ def get_relationship(
2854
+ self,
2855
+ uuid: str,
2856
+ user_id: Optional[int] = None,
2857
+ ) -> 'Relationship':
2858
+ """
2859
+ Get a relationship by UUID.
2860
+
2861
+ Args:
2862
+ uuid (str): The UUID of the relationship to get.
2863
+ user_id (int, optional): Specific user. privileges required.
2864
+
2865
+ Returns:
2866
+ Relationship: The Relationship object.
2867
+
2868
+ Raises:
2869
+ NotFoundError: If the Relationship with the specified UUID is not found.
2870
+
2871
+ Example:
2872
+ >>> from geobox import GeoboxClient
2873
+ >>> client = GeoboxClient()
2874
+ >>> relationship = client.get_relationship(uuid="12345678-1234-5678-1234-567812345678")
2875
+ """
2876
+ return Relationship.get_relationship(
2877
+ self,
2878
+ uuid=uuid,
2879
+ user_id=user_id,
2880
+ )
2881
+
2882
+
2883
+ def get_relationship_by_name(self, name: str, user_id: int = None) -> Union['Relationship', None]:
2884
+ """
2885
+ Get a relationship by name
2886
+
2887
+ Args:
2888
+ name (str): the name of the relationship to get
2889
+ user_id (int, optional): specific user. privileges required.
2890
+
2891
+ Returns:
2892
+ Relationship | None: returns the relationship if a relationship matches the given name, else None
2893
+
2894
+ Example:
2895
+ >>> from geobox import GeoboxClient
2896
+ >>> from geobox.relationship import Relationship
2897
+ >>> client = GeoboxClient()
2898
+ >>> relationship = client.get_relationship_by_name(name='test')
2899
+ """
2900
+ return Relationship.get_relationship_by_name(self, name=name, user_id=user_id)
geobox/apikey.py CHANGED
@@ -6,8 +6,6 @@ from .utils import clean_data
6
6
 
7
7
  if TYPE_CHECKING:
8
8
  from . import GeoboxClient
9
- from .aio import AsyncGeoboxClient
10
- from .aio.apikey import AsyncApiKey
11
9
 
12
10
 
13
11
  class ApiKey(Base):
@@ -237,27 +235,3 @@ class ApiKey(Base):
237
235
  endpoint = f"{self.endpoint}/grant"
238
236
  self.api.post(endpoint)
239
237
  self.data['revoked'] = False
240
-
241
-
242
- def to_async(self, async_client: 'AsyncGeoboxClient') -> 'AsyncApiKey':
243
- """
244
- Switch to async version of the apikey instance to have access to the async methods
245
-
246
- Args:
247
- async_client (AsyncGeoboxClient): The async version of the GeoboxClient instance for making requests.
248
-
249
- Returns:
250
- AsyncApiKey: the async instance of the apikey.
251
-
252
- Example:
253
- >>> from geobox import Geoboxclient
254
- >>> from geobox.aio import AsyncGeoboxClient
255
- >>> from geobox.apikey import ApiKey
256
- >>> client = GeoboxClient()
257
- >>> apikey = ApiKey.get_apikey(client, key_id=1)
258
- >>> async with AsyncGeoboxClient() as async_client:
259
- >>> async_apikey = apikey.to_async(async_client)
260
- """
261
- from .aio.apikey import AsyncApiKey
262
-
263
- return AsyncApiKey(api=async_client, key_id=self.key_id, data=self.data)
geobox/attachment.py CHANGED
@@ -11,8 +11,6 @@ from .file import File
11
11
  if TYPE_CHECKING:
12
12
  from . import GeoboxClient
13
13
  from .feature import Feature
14
- from .aio import AsyncGeoboxClient
15
- from .aio.attachment import AsyncAttachment
16
14
 
17
15
 
18
16
  class Attachment(Base):
@@ -311,27 +309,3 @@ class Attachment(Base):
311
309
  >>> attachment.thumbnail
312
310
  """
313
311
  return super()._thumbnail(format='')
314
-
315
-
316
- def to_async(self, async_client: 'AsyncGeoboxClient') -> 'AsyncAttachment':
317
- """
318
- Switch to async version of the attachment instance to have access to the async methods
319
-
320
- Args:
321
- async_client (AsyncGeoboxClient): The async version of the GeoboxClient instance for making requests.
322
-
323
- Returns:
324
- AsyncAttachment: the async instance of the attachment.
325
-
326
- Example:
327
- >>> from geobox import Geoboxclient
328
- >>> from geobox.aio import AsyncGeoboxClient
329
- >>> from geobox.attachment import Attachment
330
- >>> client = GeoboxClient()
331
- >>> attachment = Attachment.get_attachment(client, uuid="12345678-1234-5678-1234-567812345678")
332
- >>> async with AsyncGeoboxClient() as async_client:
333
- >>> async_attachment = attachment.to_async(async_client)
334
- """
335
- from .aio.attachment import AsyncAttachment
336
-
337
- return AsyncAttachment(api=async_client, attachment_id=self.attachment_id, data=self.data)
geobox/basemap.py CHANGED
@@ -7,8 +7,6 @@ from .utils import clean_data
7
7
 
8
8
  if TYPE_CHECKING:
9
9
  from . import GeoboxClient
10
- from .aio import AsyncGeoboxClient
11
- from .aio.basemap import AsyncBasemap
12
10
 
13
11
 
14
12
  class Basemap(Base):
@@ -167,29 +165,3 @@ class Basemap(Base):
167
165
  query_string = urlencode(param)
168
166
  endpoint = urljoin(cls.BASE_ENDPOINT, f"?{query_string}")
169
167
  api.get(endpoint)
170
-
171
-
172
- def to_async(self, async_client: 'AsyncGeoboxClient') -> 'AsyncBasemap':
173
- """
174
- Switch to async version of the basemap instance to have access to the async methods
175
-
176
- Args:
177
- async_client (AsyncGeoboxClient): The async version of the GeoboxClient instance for making requests.
178
-
179
- Returns:
180
- AsyncBasemap: the async instance of the basemap.
181
-
182
- Example:
183
- >>> from geobox import Geoboxclient
184
- >>> from geobox.aio import AsyncGeoboxClient
185
- >>> from geobox.basemap import Basemap
186
- >>> client = GeoboxClient()
187
- >>> basemap = Basemap.get_basemap(client, name='test')
188
- or
189
- >>> basemap = client.get_basemap(name='test')
190
- >>> async with AsyncGeoboxClient() as async_client:
191
- >>> async_basemap = basemap.to_async(async_client)
192
- """
193
- from .aio.basemap import AsyncBasemap
194
-
195
- return AsyncBasemap(api=async_client, data=self.data)
geobox/dashboard.py CHANGED
@@ -5,8 +5,6 @@ from .base import Base
5
5
  if TYPE_CHECKING:
6
6
  from . import GeoboxClient
7
7
  from .user import User
8
- from .aio import AsyncGeoboxClient
9
- from .aio.dashboard import AsyncDashboard
10
8
 
11
9
 
12
10
  class Dashboard(Base):
@@ -314,27 +312,3 @@ class Dashboard(Base):
314
312
  'limit': limit
315
313
  }
316
314
  return super()._get_shared_users(self.endpoint, params)
317
-
318
-
319
- def to_async(self, async_client: 'AsyncGeoboxClient') -> 'AsyncDashboard':
320
- """
321
- Switch to async version of the dashboard instance to have access to the async methods
322
-
323
- Args:
324
- async_client (AsyncGeoboxClient): The async version of the GeoboxClient instance for making requests.
325
-
326
- Returns:
327
- AsyncDashboard: the async instance of the dashboard.
328
-
329
- Example:
330
- >>> from geobox import Geoboxclient
331
- >>> from geobox.aio import AsyncGeoboxClient
332
- >>> from geobox.dashboard import Dashboard
333
- >>> client = GeoboxClient()
334
- >>> dashboard = Dashboard.get_dashboard(client, uuid="12345678-1234-5678-1234-567812345678")
335
- >>> async with AsyncGeoboxClient() as async_client:
336
- >>> async_dashboard = dashboard.to_async(async_client)
337
- """
338
- from .aio.dashboard import AsyncDashboard
339
-
340
- return AsyncDashboard(api=async_client, uuid=self.uuid, data=self.data)
geobox/enums.py CHANGED
@@ -228,6 +228,10 @@ class QueryParamType(Enum):
228
228
  """
229
229
  A layer parameter.
230
230
  """
231
+ TABLE = "Table"
232
+ """
233
+ A table parameter.
234
+ """
231
235
  ATTRIBUTE = "Attribute"
232
236
  """
233
237
  A Attribute parameter.
@@ -429,4 +433,14 @@ class SpatialPredicate(Enum):
429
433
  EQUAL = 'Equal'
430
434
  OVERLAP = 'Overlap'
431
435
  TOUCH = 'Touch'
432
- WITHIN = 'Within'
436
+ WITHIN = 'Within'
437
+
438
+
439
+ class TableExportFormat(Enum):
440
+ CSV = 'CSV'
441
+
442
+
443
+ class RelationshipCardinality(Enum):
444
+ OnetoOne = '1-1'
445
+ OnetoMany = '1-M'
446
+ ManytoMany = 'M-N'
geobox/feature.py CHANGED
@@ -1,13 +1,12 @@
1
1
  from urllib.parse import urljoin
2
- from typing import Optional, List, Dict, Any, TYPE_CHECKING
2
+ from typing import Optional, List, Dict, Any, TYPE_CHECKING, Union
3
3
 
4
4
  from .base import Base
5
5
  from .enums import FeatureType
6
6
 
7
7
  if TYPE_CHECKING:
8
8
  from .vectorlayer import VectorLayer
9
- from .aio import AsyncGeoboxClient
10
- from .aio.feature import AsyncFeature
9
+ from .table import Table, TableRow, Relationship
11
10
 
12
11
 
13
12
  class Feature(Base):
@@ -545,26 +544,182 @@ class Feature(Base):
545
544
  return self
546
545
 
547
546
 
548
- def to_async(self, async_client: 'AsyncGeoboxClient') -> 'AsyncFeature':
547
+ def _get_other_side_of_relationship(
548
+ self,
549
+ relationship: 'Relationship',
550
+ ) -> Union['Table', 'VectorLayer']:
549
551
  """
550
- Switch to async version of the feature instance to have access to the async methods
552
+ Determine which side of a relationship this table is on and return the opposite side.
553
+
554
+ Used internally to navigate bidirectional relationships.
555
+
556
+ Args:
557
+ relationship (Relationship): The relationship to examine.
558
+
559
+ Returns:
560
+ Table | VectorLayer: The endpoint (table or layer) on the opposite side
561
+ of the relationship from this table.
562
+
563
+ Raises:
564
+ ValueError: If this table is not part of the given relationship.
565
+
566
+ Note:
567
+ This method assumes the table is either the source or target,
568
+ not the relation table in Many-to-Many relationships.
569
+ """
570
+ if relationship.source_id == self.layer.id:
571
+ return relationship.get_target()
572
+
573
+ if relationship.target_id == self.layer.id:
574
+ return relationship.get_source()
551
575
 
576
+ raise ValueError("Relationship does not involve this table.")
577
+
578
+
579
+ def _fetch_related_from_target(
580
+ self,
581
+ target: Union['Table', 'VectorLayer'],
582
+ relationship_uuid: str,
583
+ ) -> Union[List['TableRow'], List['Feature']]:
584
+ """
585
+ Fetch related rows/features from a relationship target.
586
+
587
+ Internal helper that dispatches to the appropriate API method
588
+ based on target type.
589
+
552
590
  Args:
553
- async_client (AsyncGeoboxClient): The async version of the GeoboxClient instance for making requests.
591
+ target (Table | VectorLayer): The target endpoint (Table or VectorLayer) to query.
592
+ relationship_uuid (str): UUID of the relationship to traverse.
593
+
594
+ Raises:
595
+ TypeError: If target is not a Table or VectorLayer.
554
596
 
555
597
  Returns:
556
- AsyncFeature: the async instance of the feature.
598
+ List[TableRow] | List[Feature]: Related rows or features.
599
+ """
600
+ from .table import Table
601
+ from .vectorlayer import VectorLayer
602
+
603
+ if isinstance(target, Table):
604
+ return target.get_rows(
605
+ relationship_uuid=relationship_uuid,
606
+ related_record_id=self.id,
607
+ )
608
+
609
+ if isinstance(target, VectorLayer):
610
+ return target.get_features(
611
+ relationship_uuid=relationship_uuid,
612
+ related_record_id=self.id,
613
+ )
614
+
615
+ raise TypeError(f"Unsupported target type: {type(target)}")
616
+
617
+
618
+ def get_related_records(self,
619
+ relationship_uuid: str,
620
+ ) -> Union[List['TableRow'], List['Feature']]:
621
+ """
622
+ Get the related records on the *other side* of the relationship that are linked to this row
623
+
624
+ Args:
625
+ relationship_uuid (str): The uuid of relationship
626
+
627
+ Returns:
628
+ List[TableRow] | List[Feature]: a list of the related records
629
+
630
+ Raises:
631
+ ValueError:
632
+ If the given relationship does not involve the current table
633
+ (i.e., this row is neither the source nor the target of the relationship).
634
+
635
+ TypeError:
636
+ If the relationship target type is not supported for fetching
637
+ related records.
557
638
 
558
639
  Example:
559
- >>> from geobox import Geoboxclient
560
- >>> from geobox.aio import AsyncGeoboxClient
640
+ >>> from geobox import GeoboxClient
561
641
  >>> client = GeoboxClient()
562
642
  >>> layer = client.get_vector(uuid="12345678-1234-5678-1234-567812345678")
563
- >>> feature = layer.get_feature(id=1, srid=3857)
564
- >>> async with AsyncGeoboxClient() as async_client:
565
- >>> async_feature = feature.to_async(async_client)
643
+ >>> feature = layer.get_feature(feature_id=1)
644
+ >>> related_records = feature.get_related_records(relationship_uuid="12345678-1234-5678-1234-567812345678")
566
645
  """
567
- from .aio.feature import AsyncFeature
646
+ relationship = self.api.get_relationship(relationship_uuid)
647
+
648
+ other_side = self._get_other_side_of_relationship(relationship)
649
+
650
+ return self._fetch_related_from_target(
651
+ target=other_side,
652
+ relationship_uuid=relationship_uuid,
653
+ )
654
+
655
+
656
+ def associate_with(
657
+ self,
658
+ relationship_uuid: str,
659
+ *,
660
+ target_ids: Optional[List[int]] = None,
661
+ q: Optional[str] = None,
662
+ ) -> Dict:
663
+ """
664
+ Create relationships between the source record and target records
665
+
666
+ Args:
667
+ relationship_uuid (str): the relationship uuid
668
+ target_ids (List[int], optional): a list of target record ids to be associated with the current record
669
+ 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
670
+
671
+ Returns:
672
+ Dict: the record association result
568
673
 
569
- async_layer = self.layer.to_async(async_client=async_client)
570
- return AsyncFeature(layer=async_layer, srid=self.srid, data=self.data)
674
+ Example:
675
+ >>> from geobox import GeoboxClient
676
+ >>> client = GeoboxClient()
677
+ >>> layer = client.get_vector(uuid="12345678-1234-5678-1234-567812345678")
678
+ >>> feature = layer.get_feature(feature_id=1)
679
+ >>> feature.associate_with(
680
+ ... relationship_uuid="12345678-1234-5678-1234-567812345678",
681
+ ... target_ids=[1, 2, 3],
682
+ ... )
683
+ """
684
+ relationship = self.api.get_relationship(uuid=relationship_uuid)
685
+ return relationship.associate_records(
686
+ source_id=self.id,
687
+ target_ids=target_ids,
688
+ q=q,
689
+ )
690
+
691
+
692
+ def disassociate_with(
693
+ self,
694
+ relationship_uuid: str,
695
+ *,
696
+ target_ids: Optional[List[int]] = None,
697
+ q: Optional[str] = None,
698
+ ) -> Dict:
699
+ """
700
+ Remove relationships between the source record and target records
701
+
702
+ Args:
703
+ relationship_uuid (str): the relationship uuid
704
+ target_ids (List[int], optional): a list of target record ids to be disassociated with the current record
705
+ 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
706
+
707
+ Returns:
708
+ Dict: the record association result
709
+
710
+ Example:
711
+ >>> from geobox import GeoboxClient
712
+ >>> client = GeoboxClient()
713
+ >>> layer = client.get_vector(uuid="12345678-1234-5678-1234-567812345678")
714
+ >>> feature = layer.get_feature(feature_id=1)
715
+ >>> feature.disassociate_with(
716
+ ... relationship_uuid="12345678-1234-5678-1234-567812345678",
717
+ ... target_ids=[1, 2, 3],
718
+ ... )
719
+ """
720
+ relationship = self.api.get_relationship(uuid=relationship_uuid)
721
+ return relationship.disassociate_records(
722
+ source_id=self.id,
723
+ target_ids=target_ids,
724
+ q=q,
725
+ )