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.
- geobox/aio/api.py +153 -7
- geobox/aio/apikey.py +0 -26
- geobox/aio/attachment.py +5 -31
- geobox/aio/basemap.py +2 -30
- geobox/aio/dashboard.py +0 -26
- geobox/aio/feature.py +172 -17
- geobox/aio/field.py +0 -27
- geobox/aio/file.py +0 -26
- geobox/aio/layout.py +0 -26
- geobox/aio/log.py +0 -27
- geobox/aio/map.py +0 -26
- geobox/aio/model3d.py +0 -26
- geobox/aio/mosaic.py +0 -26
- geobox/aio/plan.py +0 -26
- geobox/aio/query.py +0 -26
- geobox/aio/raster.py +0 -26
- geobox/aio/scene.py +1 -26
- geobox/aio/settings.py +0 -28
- geobox/aio/table.py +644 -55
- geobox/aio/task.py +0 -27
- geobox/aio/tile3d.py +0 -26
- geobox/aio/tileset.py +1 -26
- geobox/aio/usage.py +0 -32
- geobox/aio/user.py +0 -59
- geobox/aio/vector_tool.py +49 -0
- geobox/aio/vectorlayer.py +8 -34
- geobox/aio/version.py +1 -25
- geobox/aio/view.py +5 -35
- geobox/aio/workflow.py +0 -26
- geobox/api.py +152 -7
- geobox/apikey.py +0 -26
- geobox/attachment.py +0 -26
- geobox/basemap.py +0 -28
- geobox/dashboard.py +0 -26
- geobox/enums.py +11 -1
- geobox/feature.py +170 -15
- geobox/field.py +0 -28
- geobox/file.py +0 -26
- geobox/layout.py +0 -26
- geobox/log.py +0 -26
- geobox/map.py +0 -26
- geobox/model3d.py +0 -26
- geobox/mosaic.py +0 -26
- geobox/plan.py +1 -26
- geobox/query.py +1 -26
- geobox/raster.py +1 -31
- geobox/scene.py +1 -27
- geobox/settings.py +1 -29
- geobox/table.py +640 -55
- geobox/task.py +2 -29
- geobox/tile3d.py +0 -26
- geobox/tileset.py +1 -26
- geobox/usage.py +2 -33
- geobox/user.py +1 -59
- geobox/vector_tool.py +49 -0
- geobox/vectorlayer.py +9 -36
- geobox/version.py +1 -26
- geobox/view.py +4 -34
- geobox/workflow.py +1 -26
- {geobox-2.3.0.dist-info → geobox-2.4.0.dist-info}/METADATA +1 -1
- geobox-2.4.0.dist-info/RECORD +74 -0
- {geobox-2.3.0.dist-info → geobox-2.4.0.dist-info}/WHEEL +1 -1
- geobox-2.3.0.dist-info/RECORD +0 -74
- {geobox-2.3.0.dist-info → geobox-2.4.0.dist-info}/licenses/LICENSE +0 -0
- {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 .
|
|
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 .
|
|
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
|
|
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
|
-
|
|
284
|
+
Create relationships between the source record and target records
|
|
159
285
|
|
|
160
286
|
Args:
|
|
161
|
-
|
|
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
|
-
|
|
292
|
+
Dict: the record association result
|
|
165
293
|
|
|
166
294
|
Example:
|
|
167
|
-
>>> from geobox import
|
|
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
|
-
>>>
|
|
173
|
-
|
|
299
|
+
>>> row.associate_with(
|
|
300
|
+
... relationship_uuid="12345678-1234-5678-1234-567812345678",
|
|
301
|
+
... target_ids=[1, 2, 3],
|
|
302
|
+
... )
|
|
174
303
|
"""
|
|
175
|
-
|
|
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
|
-
|
|
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
|
-
|
|
551
|
+
Initialize a relationship instance.
|
|
379
552
|
|
|
380
553
|
Args:
|
|
381
|
-
|
|
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
|
-
|
|
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
|
|
388
|
-
>>> from geobox.
|
|
598
|
+
>>> from geobox import GeoboxClient
|
|
599
|
+
>>> from geobox.table import Relatinship
|
|
389
600
|
>>> client = GeoboxClient()
|
|
390
|
-
>>>
|
|
391
|
-
|
|
392
|
-
>>>
|
|
393
|
-
|
|
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
|
-
|
|
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
|
-
|
|
398
|
-
|
|
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.
|
|
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
|
-
|
|
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)
|