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/aio/table.py
CHANGED
|
@@ -1,20 +1,36 @@
|
|
|
1
|
-
from typing import List, Dict,
|
|
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 AsyncBase
|
|
7
6
|
from .task import AsyncTask
|
|
8
|
-
from ..enums import FieldType
|
|
7
|
+
from ..enums import FieldType, RelationshipCardinality, TableExportFormat
|
|
9
8
|
from ..exception import NotFoundError
|
|
10
9
|
from ..utils import clean_data
|
|
11
10
|
|
|
12
11
|
if TYPE_CHECKING:
|
|
13
12
|
from . import AsyncGeoboxClient
|
|
14
13
|
from .user import AsyncUser
|
|
15
|
-
from .. import GeoboxClient
|
|
16
14
|
from .file import AsyncFile
|
|
17
|
-
from ..table import Table, TableField
|
|
15
|
+
from ..table import Table, TableField
|
|
16
|
+
from .vectorlayer import AsyncVectorLayer
|
|
17
|
+
from .field import AsyncField
|
|
18
|
+
from .feature import AsyncFeature
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass(frozen=True)
|
|
22
|
+
class RelationshipEndpoint:
|
|
23
|
+
"""
|
|
24
|
+
Represents one endpoint (source or target) of a relationship
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
table (AsyncTable | AsyncVectorLayer): The source or target table or vector layer
|
|
28
|
+
field (AsyncTableField | AsyncField | str): The field name or object on the source/target entity
|
|
29
|
+
fk_field (AsyncTableField | AsyncField | str, optional): The foreign key field name or object on the relation table. (Required for Many-to-Many relationships)
|
|
30
|
+
"""
|
|
31
|
+
table: Union['AsyncTable', 'AsyncVectorLayer']
|
|
32
|
+
field: Union['AsyncTableField', 'AsyncField', str]
|
|
33
|
+
fk_field: Optional[Union['AsyncTableField', 'AsyncField', str]] = None
|
|
18
34
|
|
|
19
35
|
|
|
20
36
|
class AsyncTableRow(AsyncBase):
|
|
@@ -156,30 +172,185 @@ class AsyncTableRow(AsyncBase):
|
|
|
156
172
|
await super()._delete(self.endpoint)
|
|
157
173
|
|
|
158
174
|
|
|
159
|
-
def
|
|
175
|
+
async def _get_other_side_of_relationship(
|
|
176
|
+
self,
|
|
177
|
+
relationship: 'AsyncRelationship',
|
|
178
|
+
) -> Union['AsyncTable', 'AsyncVectorLayer']:
|
|
179
|
+
"""
|
|
180
|
+
[async] Determine which side of a relationship this table is on and return the opposite side.
|
|
181
|
+
|
|
182
|
+
Used internally to navigate bidirectional relationships.
|
|
183
|
+
|
|
184
|
+
Args:
|
|
185
|
+
relationship (AsyncRelationship): The relationship to examine.
|
|
186
|
+
|
|
187
|
+
Returns:
|
|
188
|
+
AsyncTable | AsyncVectorLayer: The endpoint (table or layer) on the opposite side
|
|
189
|
+
of the relationship from this table.
|
|
190
|
+
|
|
191
|
+
Raises:
|
|
192
|
+
ValueError: If this table is not part of the given relationship.
|
|
193
|
+
|
|
194
|
+
Note:
|
|
195
|
+
This method assumes the table is either the source or target,
|
|
196
|
+
not the relation table in Many-to-Many relationships.
|
|
197
|
+
"""
|
|
198
|
+
if relationship.source_id == self.table.id:
|
|
199
|
+
return await relationship.get_target()
|
|
200
|
+
|
|
201
|
+
if relationship.target_id == self.table.id:
|
|
202
|
+
return await relationship.get_source()
|
|
203
|
+
|
|
204
|
+
raise ValueError("Relationship does not involve this table.")
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
async def _fetch_related_from_target(
|
|
208
|
+
self,
|
|
209
|
+
target: Union['AsyncTable', 'AsyncVectorLayer'],
|
|
210
|
+
relationship_uuid: str,
|
|
211
|
+
) -> Union[List['AsyncTableRow'], List['AsyncFeature']]:
|
|
212
|
+
"""
|
|
213
|
+
[async] Fetch related rows/features from a relationship target.
|
|
214
|
+
|
|
215
|
+
Internal helper that dispatches to the appropriate API method
|
|
216
|
+
based on target type.
|
|
217
|
+
|
|
218
|
+
Args:
|
|
219
|
+
target (AsyncTable | AsyncVectorLayer): The target endpoint (Table or VectorLayer) to query.
|
|
220
|
+
relationship_uuid (str): UUID of the relationship to traverse.
|
|
221
|
+
|
|
222
|
+
Raises:
|
|
223
|
+
TypeError: If target is not a Table or VectorLayer.
|
|
224
|
+
|
|
225
|
+
Returns:
|
|
226
|
+
List[AsyncTableRow] | List[AsyncFeature]: Related rows or features.
|
|
227
|
+
"""
|
|
228
|
+
from .table import AsyncTable
|
|
229
|
+
from .vectorlayer import AsyncVectorLayer
|
|
230
|
+
|
|
231
|
+
if isinstance(target, AsyncTable):
|
|
232
|
+
return await target.get_rows(
|
|
233
|
+
relationship_uuid=relationship_uuid,
|
|
234
|
+
related_record_id=self.id,
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
if isinstance(target, AsyncVectorLayer):
|
|
238
|
+
return await target.get_features(
|
|
239
|
+
relationship_uuid=relationship_uuid,
|
|
240
|
+
related_record_id=self.id,
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
raise TypeError(f"Unsupported target type: {type(target)}")
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
async def get_related_records(self,
|
|
247
|
+
relationship_uuid: str,
|
|
248
|
+
) -> Union[List['AsyncTableRow'], List['AsyncFeature']]:
|
|
249
|
+
"""
|
|
250
|
+
[async] Get the related records on the *other side* of the relationship that are linked to this row
|
|
251
|
+
|
|
252
|
+
Args:
|
|
253
|
+
relationship_uuid (str): The uuid of relationship
|
|
254
|
+
|
|
255
|
+
Returns:
|
|
256
|
+
List[AsyncTableRow] | List[AsyncFeature]: a list of the related records
|
|
257
|
+
|
|
258
|
+
Raises:
|
|
259
|
+
ValueError:
|
|
260
|
+
If the given relationship does not involve the current table
|
|
261
|
+
(i.e., this row is neither the source nor the target of the relationship).
|
|
262
|
+
|
|
263
|
+
TypeError:
|
|
264
|
+
If the relationship target type is not supported for fetching
|
|
265
|
+
related records.
|
|
266
|
+
|
|
267
|
+
Example:
|
|
268
|
+
>>> from geobox.aio import AsyncGeoboxClient
|
|
269
|
+
>>> async with AsyncGeoboxClient() as client:
|
|
270
|
+
>>> table = await client.get_table(uuid="12345678-1234-5678-1234-567812345678")
|
|
271
|
+
>>> row = await table.get_row(row_id=1)
|
|
272
|
+
>>> related_records = await row.get_related_records(relationship_uuid="12345678-1234-5678-1234-567812345678")
|
|
273
|
+
"""
|
|
274
|
+
relationship = await self.api.get_relationship(relationship_uuid)
|
|
275
|
+
|
|
276
|
+
other_side = await self._get_other_side_of_relationship(relationship)
|
|
277
|
+
|
|
278
|
+
return await self._fetch_related_from_target(
|
|
279
|
+
target=other_side,
|
|
280
|
+
relationship_uuid=relationship_uuid,
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
async def associate_with(
|
|
285
|
+
self,
|
|
286
|
+
relationship_uuid: str,
|
|
287
|
+
*,
|
|
288
|
+
target_ids: Optional[List[int]] = None,
|
|
289
|
+
q: Optional[str] = None,
|
|
290
|
+
) -> Dict:
|
|
160
291
|
"""
|
|
161
|
-
|
|
292
|
+
[async] Create relationships between the source record and target records
|
|
162
293
|
|
|
163
294
|
Args:
|
|
164
|
-
|
|
295
|
+
relationship_uuid (str): the relationship uuid
|
|
296
|
+
target_ids (List[int], optional): a list of target record ids to be associated with the current record
|
|
297
|
+
q (str, optional): query filter on target layer or table to select which target features or rows that are going to be related to the current record
|
|
165
298
|
|
|
166
299
|
Returns:
|
|
167
|
-
|
|
300
|
+
Dict: the record association result
|
|
168
301
|
|
|
169
302
|
Example:
|
|
170
|
-
>>> from geobox import Geoboxclient
|
|
171
303
|
>>> from geobox.aio import AsyncGeoboxClient
|
|
172
304
|
>>> async with AsyncGeoboxClient() as client:
|
|
173
305
|
>>> table = await client.get_table(uuid="12345678-1234-5678-1234-567812345678")
|
|
174
306
|
>>> row = await table.get_row(row_id=1)
|
|
175
|
-
>>>
|
|
176
|
-
|
|
307
|
+
>>> await row.associate_with(
|
|
308
|
+
... relationship_uuid="12345678-1234-5678-1234-567812345678",
|
|
309
|
+
... target_ids=[1, 2, 3],
|
|
310
|
+
... )
|
|
311
|
+
"""
|
|
312
|
+
relationship = await self.api.get_relationship(uuid=relationship_uuid)
|
|
313
|
+
return await relationship.associate_records(
|
|
314
|
+
source_id=self.id,
|
|
315
|
+
target_ids=target_ids,
|
|
316
|
+
q=q,
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
async def disassociate_with(
|
|
321
|
+
self,
|
|
322
|
+
relationship_uuid: str,
|
|
323
|
+
*,
|
|
324
|
+
target_ids: Optional[List[int]] = None,
|
|
325
|
+
q: Optional[str] = None,
|
|
326
|
+
) -> Dict:
|
|
177
327
|
"""
|
|
178
|
-
|
|
328
|
+
[async] Remove relationships between the source record and target records
|
|
179
329
|
|
|
180
|
-
|
|
181
|
-
|
|
330
|
+
Args:
|
|
331
|
+
relationship_uuid (str): the relationship uuid
|
|
332
|
+
target_ids (List[int], optional): a list of target record ids to be disassociated with the current record
|
|
333
|
+
q (str, optional): query filter on target layer or table to select which target features or rows that are going to be related to the current
|
|
334
|
+
|
|
335
|
+
Returns:
|
|
336
|
+
Dict: the record disassociation result
|
|
182
337
|
|
|
338
|
+
Example:
|
|
339
|
+
>>> from geobox.aio import AsyncGeoboxClient
|
|
340
|
+
>>> async with AsyncGeoboxClient() as client:
|
|
341
|
+
>>> table = await client.get_table(uuid="12345678-1234-5678-1234-567812345678")
|
|
342
|
+
>>> row = await table.get_row(row_id=1)
|
|
343
|
+
>>> await row.disassociate_with(
|
|
344
|
+
... relationship_uuid="12345678-1234-5678-1234-567812345678",
|
|
345
|
+
... target_ids=[1, 2, 3],
|
|
346
|
+
... )
|
|
347
|
+
"""
|
|
348
|
+
relationship = await self.api.get_relationship(uuid=relationship_uuid)
|
|
349
|
+
return await relationship.disassociate_records(
|
|
350
|
+
source_id=self.id,
|
|
351
|
+
target_ids=target_ids,
|
|
352
|
+
q=q,
|
|
353
|
+
)
|
|
183
354
|
|
|
184
355
|
|
|
185
356
|
class AsyncTableField(AsyncBase):
|
|
@@ -378,30 +549,468 @@ class AsyncTableField(AsyncBase):
|
|
|
378
549
|
return self.domain
|
|
379
550
|
|
|
380
551
|
|
|
381
|
-
|
|
552
|
+
class AsyncRelationship(AsyncBase):
|
|
553
|
+
BASE_ENDPOINT = 'relationships/'
|
|
554
|
+
|
|
555
|
+
def __init__(self,
|
|
556
|
+
api: 'AsyncGeoboxClient',
|
|
557
|
+
uuid: str,
|
|
558
|
+
data: Optional[Dict] = {},
|
|
559
|
+
):
|
|
382
560
|
"""
|
|
383
|
-
|
|
561
|
+
Initialize a relationship instance.
|
|
384
562
|
|
|
385
563
|
Args:
|
|
386
|
-
|
|
564
|
+
api (AsyncGeoboxClient): The GeoboxClient instance for making requests.
|
|
565
|
+
uuid (str): The unique identifier for the relationship.
|
|
566
|
+
data (Dict): The response data of the table.
|
|
567
|
+
"""
|
|
568
|
+
super().__init__(api=api, uuid=uuid, data=data)
|
|
569
|
+
|
|
570
|
+
|
|
571
|
+
def __repr__(self) -> str:
|
|
572
|
+
"""
|
|
573
|
+
Return a string representation of the AsyncRelationship.
|
|
574
|
+
|
|
575
|
+
Returns:
|
|
576
|
+
str: The string representation of the AsyncRelationship.
|
|
577
|
+
"""
|
|
578
|
+
return f"AsyncRelationship(uuid={self.uuid}, name={self.relation_name}, cardinality={self.relation_cardinality})"
|
|
579
|
+
|
|
580
|
+
|
|
581
|
+
@classmethod
|
|
582
|
+
async def get_relationships(
|
|
583
|
+
cls,
|
|
584
|
+
api: 'AsyncGeoboxClient',
|
|
585
|
+
**kwargs,
|
|
586
|
+
) -> Union[List['AsyncRelationship'], int]:
|
|
587
|
+
"""
|
|
588
|
+
[async] Get a list of relationships with optional filtering and pagination.
|
|
589
|
+
|
|
590
|
+
Args:
|
|
591
|
+
api (AsyncGeoboxClient): The GeoboxClient instance for making requests.
|
|
592
|
+
|
|
593
|
+
Keyword Args:
|
|
594
|
+
q (str): query filter based on OGC CQL standard. e.g. "field1 LIKE '%GIS%' AND created_at > '2021-01-01'"
|
|
595
|
+
search (str): search term for keyword-based searching among search_fields or all textual fields if search_fields does not have value. NOTE: if q param is defined this param will be ignored
|
|
596
|
+
search_fields (str): comma separated list of fields for searching
|
|
597
|
+
order_by (str): comma separated list of fields for sorting results [field1 A|D, field2 A|D, …]. e.g. name A, type D. NOTE: "A" denotes ascending order and "D" denotes descending order.
|
|
598
|
+
return_count (bool): Whether to return total count. default: False.
|
|
599
|
+
skip (int): Number of items to skip. default: 0
|
|
600
|
+
limit (int): Number of items to return. default: 10
|
|
601
|
+
user_id (int): Specific user. privileges required
|
|
602
|
+
shared (bool): Whether to return shared tables. default: False
|
|
387
603
|
|
|
388
604
|
Returns:
|
|
389
|
-
|
|
605
|
+
List[AsyncRelationship] | int: A list of relationship instances or the total number of relationships.
|
|
390
606
|
|
|
391
607
|
Example:
|
|
392
|
-
>>> from geobox import Geoboxclient
|
|
393
608
|
>>> from geobox.aio import AsyncGeoboxClient
|
|
609
|
+
>>> from geobox.aio.table import AsyncRelatinship
|
|
394
610
|
>>> async with AsyncGeoboxClient() as client:
|
|
395
|
-
>>>
|
|
396
|
-
|
|
397
|
-
>>>
|
|
398
|
-
>>> sync_field = field.to_sync(sync_client)
|
|
611
|
+
>>> relationships = await client.get_relationships(q="name LIKE '%My relationship%'")
|
|
612
|
+
or
|
|
613
|
+
>>> relationships = await Table.get_relationships(client, q="name LIKE '%My relationship%'")
|
|
399
614
|
"""
|
|
400
|
-
|
|
615
|
+
params = {
|
|
616
|
+
'f': 'json',
|
|
617
|
+
'q': kwargs.get('q'),
|
|
618
|
+
'search': kwargs.get('search'),
|
|
619
|
+
'search_fields': kwargs.get('search_fields'),
|
|
620
|
+
'order_by': kwargs.get('order_by'),
|
|
621
|
+
'return_count': kwargs.get('return_count', False),
|
|
622
|
+
'skip': kwargs.get('skip', 0),
|
|
623
|
+
'limit': kwargs.get('limit', 10),
|
|
624
|
+
'user_id': kwargs.get('user_id'),
|
|
625
|
+
'shared': kwargs.get('shared', False),
|
|
626
|
+
}
|
|
627
|
+
return await super()._get_list(api, cls.BASE_ENDPOINT, params, factory_func=lambda api, item: AsyncRelationship(api, item['uuid'], item))
|
|
628
|
+
|
|
629
|
+
|
|
630
|
+
@classmethod
|
|
631
|
+
async def create_relationship(
|
|
632
|
+
cls,
|
|
633
|
+
api: 'AsyncGeoboxClient',
|
|
634
|
+
name: str,
|
|
635
|
+
cardinality: RelationshipCardinality,
|
|
636
|
+
*,
|
|
637
|
+
source: 'RelationshipEndpoint',
|
|
638
|
+
target: 'RelationshipEndpoint',
|
|
639
|
+
relation_table: Optional['AsyncTable'] = None,
|
|
640
|
+
display_name: Optional[str] = None,
|
|
641
|
+
description: Optional[str] = None,
|
|
642
|
+
user_id: Optional[int] = None,
|
|
643
|
+
) -> 'AsyncRelationship':
|
|
644
|
+
"""
|
|
645
|
+
[async] Create a new AsyncRelationship
|
|
401
646
|
|
|
402
|
-
|
|
403
|
-
|
|
647
|
+
Args:
|
|
648
|
+
api (AsyncGeoboxClient): The GeoboxClient instance for making requests.
|
|
649
|
+
name (str): name of the relationship
|
|
650
|
+
cardinality (RelationshipCardinality): One to One, One to Many, or Many to Many
|
|
651
|
+
|
|
652
|
+
Keyword Args:
|
|
653
|
+
source (RelationshipEndpoint): Definition of the source side of the relationship, including the table (or layer), field, and foreign-key field
|
|
654
|
+
target (RelationshipEndpoint): Definition of the target side of the relationship, including the table (or layer), field, and foreign-key field
|
|
655
|
+
relation_table (AsyncTable, optional): The table that stores the relationship metadata or join records. (Required for Many-to-Many relationships)
|
|
656
|
+
display_name (str, optional): Human-readable name for the relationship
|
|
657
|
+
description (str, optional): the description of the relationship
|
|
658
|
+
user_id (int, optional): Specific user. privileges required.
|
|
404
659
|
|
|
660
|
+
Returns:
|
|
661
|
+
AsyncRelationship: a relationship instance
|
|
662
|
+
|
|
663
|
+
Example:
|
|
664
|
+
>>> from geobox.aio import AsyncGeoboxClient
|
|
665
|
+
>>> from geobox.aio.table import AsyncRelationship, RelationshipEndpoint, RelationshipCardinality
|
|
666
|
+
>>> async with AsyncGeoboxClient() as client:
|
|
667
|
+
>>> source = RelationshipEndpoint(
|
|
668
|
+
... table=client.get_table_by_name('owner'),
|
|
669
|
+
... field="name", # on source table
|
|
670
|
+
... fk_field="book_name", # on relation table
|
|
671
|
+
... )
|
|
672
|
+
>>> target = RelationshipEndpoint(
|
|
673
|
+
... table=client.get_table_by_name('parcel'),
|
|
674
|
+
... field="name", # on target table
|
|
675
|
+
... fk_field="author_name", # on relation table
|
|
676
|
+
... )
|
|
677
|
+
>>> relationship = await client.create_relationship(
|
|
678
|
+
... name="book_author",
|
|
679
|
+
... cardinality=RelationshipCardinality.ManytoMany,
|
|
680
|
+
... source=source,
|
|
681
|
+
... target=target,
|
|
682
|
+
... relation_table=client.get_table_by_name('owner_parcel'),
|
|
683
|
+
... )
|
|
684
|
+
or
|
|
685
|
+
>>> relationship = await AsyncRelationship.create_relationship(
|
|
686
|
+
... client,
|
|
687
|
+
... name="owner_parcel",
|
|
688
|
+
... cardinality=RelationshipCardinality.ManytoMany,
|
|
689
|
+
... source=source,
|
|
690
|
+
... target=target,
|
|
691
|
+
... relation_table=client.get_table_by_name('owner_parcel'),
|
|
692
|
+
... )
|
|
693
|
+
"""
|
|
694
|
+
data = {
|
|
695
|
+
"relation_name": name,
|
|
696
|
+
"display_name": display_name,
|
|
697
|
+
"description": description,
|
|
698
|
+
"relation_cardinality": cardinality.value,
|
|
699
|
+
"relation_table_id": relation_table.id if relation_table else None,
|
|
700
|
+
"relation_table_search": f"{relation_table.__class__.__name__.split('Async')[-1]}/{relation_table.name}" if relation_table else None,
|
|
701
|
+
"source_field_name": source.field if type(source.field) == str else source.field.name,
|
|
702
|
+
"source_fk_name": (source.fk_field if not source.fk_field or type(source.fk_field) == str else source.fk_field.name) if relation_table else None,
|
|
703
|
+
"source_id": source.table.id,
|
|
704
|
+
"source_search": f"{source.table.__class__.__name__.split('Async')[-1]}/{source.table.name}",
|
|
705
|
+
"source_type": str(source.table.__class__.__name__.split('Async')[-1]),
|
|
706
|
+
"target_field_name": target.field if type(target.field) == str else target.field.name,
|
|
707
|
+
"target_fk_name": (target.fk_field if not target.fk_field or type(target.fk_field) == str else target.fk_field.name) if relation_table else None,
|
|
708
|
+
"target_id": target.table.id,
|
|
709
|
+
"target_search": f"{target.table.__class__.__name__.split('Async')[-1]}/{target.table.name}",
|
|
710
|
+
"target_type": str(target.table.__class__.__name__.split('Async')[-1]),
|
|
711
|
+
"user_id": user_id,
|
|
712
|
+
}
|
|
713
|
+
return await super()._create(api, cls.BASE_ENDPOINT, data, factory_func=lambda api, item: AsyncRelationship(api, item['uuid'], item))
|
|
714
|
+
|
|
715
|
+
|
|
716
|
+
@classmethod
|
|
717
|
+
async def get_relationship(
|
|
718
|
+
cls,
|
|
719
|
+
api: 'AsyncGeoboxClient',
|
|
720
|
+
uuid: str,
|
|
721
|
+
user_id: Optional[int] = None,
|
|
722
|
+
) -> 'AsyncRelationship':
|
|
723
|
+
"""
|
|
724
|
+
[async] Get a relationship by UUID.
|
|
725
|
+
|
|
726
|
+
Args:
|
|
727
|
+
api (AsyncGeoboxClient): The AsyncGeoboxClient instance for making requests.
|
|
728
|
+
uuid (str): The UUID of the relationship to get.
|
|
729
|
+
user_id (int, optional): Specific user. privileges required.
|
|
730
|
+
|
|
731
|
+
Returns:
|
|
732
|
+
AsyncRelationship: The AsyncRelationship object.
|
|
733
|
+
|
|
734
|
+
Raises:
|
|
735
|
+
NotFoundError: If the AsyncRelationship with the specified UUID is not found.
|
|
736
|
+
|
|
737
|
+
Example:
|
|
738
|
+
>>> from geobox.aio import AsyncGeoboxClient
|
|
739
|
+
>>> from geobox.aio.table import AsyncRelationship
|
|
740
|
+
>>> async with AsyncGeoboxClient() as client:
|
|
741
|
+
>>> relationship = await client.get_relationship(uuid="12345678-1234-5678-1234-567812345678")
|
|
742
|
+
or
|
|
743
|
+
>>> relationship = await AsyncRelationship.get_relationship(client, uuid="12345678-1234-5678-1234-567812345678")
|
|
744
|
+
"""
|
|
745
|
+
params = {
|
|
746
|
+
'f': 'json',
|
|
747
|
+
'user_id': user_id,
|
|
748
|
+
}
|
|
749
|
+
return await super()._get_detail(api, cls.BASE_ENDPOINT, uuid, params, factory_func=lambda api, item: AsyncRelationship(api, item['uuid'], item))
|
|
750
|
+
|
|
751
|
+
|
|
752
|
+
@classmethod
|
|
753
|
+
async def get_relationship_by_name(
|
|
754
|
+
cls,
|
|
755
|
+
api: 'AsyncGeoboxClient',
|
|
756
|
+
name: str,
|
|
757
|
+
user_id: Optional[int] = None,
|
|
758
|
+
) -> Union['AsyncRelationship', None]:
|
|
759
|
+
"""
|
|
760
|
+
[async] Get a relationship by name
|
|
761
|
+
|
|
762
|
+
Args:
|
|
763
|
+
api (AsyncGeoboxClient): The GeoboxClient instance for making requests.
|
|
764
|
+
name (str): the name of the relationship to get
|
|
765
|
+
user_id (int, optional): specific user. privileges required.
|
|
766
|
+
|
|
767
|
+
Returns:
|
|
768
|
+
AsyncRelationship | None: returns the relationship if a relationship matches the given name, else None
|
|
769
|
+
|
|
770
|
+
Example:
|
|
771
|
+
>>> from geobox.aio import AsyncGeoboxClient
|
|
772
|
+
>>> from geobox.aio.table import AsyncRelationship
|
|
773
|
+
>>> async with AsyncGeoboxClient() as client:
|
|
774
|
+
>>> relationship = await client.get_relationship_by_name(name='test')
|
|
775
|
+
or
|
|
776
|
+
>>> relationship = await AsyncRelationship.get_relationship_by_name(client, name='test')
|
|
777
|
+
"""
|
|
778
|
+
relationships = await cls.get_relationships(api, q=f"name = '{name}'", user_id=user_id)
|
|
779
|
+
if relationships and relationships[0].relation_name == name:
|
|
780
|
+
return relationships[0]
|
|
781
|
+
else:
|
|
782
|
+
return None
|
|
783
|
+
|
|
784
|
+
|
|
785
|
+
async def update(self, **kwargs) -> Dict:
|
|
786
|
+
"""
|
|
787
|
+
[async] Update the relationship.
|
|
788
|
+
|
|
789
|
+
Keyword Args:
|
|
790
|
+
name (str): The name of the relationship.
|
|
791
|
+
display_name (str): The display name of the relationship.
|
|
792
|
+
description (str): The description of the relationship.
|
|
793
|
+
|
|
794
|
+
Returns:
|
|
795
|
+
Dict: The updated relationship data.
|
|
796
|
+
|
|
797
|
+
Raises:
|
|
798
|
+
ValidationError: If the relationship data is invalid.
|
|
799
|
+
|
|
800
|
+
Example:
|
|
801
|
+
>>> from geobox.aio import AsyncGeoboxClient
|
|
802
|
+
>>> from geobox.aio.table import AsyncRelationship
|
|
803
|
+
>>> async with AsyncGeoboxClient() as client:
|
|
804
|
+
>>> relationship = await AsyncRelationship.get_relationship(client, uuid="12345678-1234-5678-1234-567812345678")
|
|
805
|
+
>>> await relationship.update(display_name="New Display Name")
|
|
806
|
+
"""
|
|
807
|
+
data = {
|
|
808
|
+
"name": kwargs.get('name'),
|
|
809
|
+
"display_name": kwargs.get('display_name'),
|
|
810
|
+
"description": kwargs.get('description'),
|
|
811
|
+
}
|
|
812
|
+
return await super()._update(self.endpoint, data)
|
|
813
|
+
|
|
814
|
+
|
|
815
|
+
async def delete(self) -> None:
|
|
816
|
+
"""
|
|
817
|
+
[async] Delete the AsyncRelationship
|
|
818
|
+
|
|
819
|
+
Returns:
|
|
820
|
+
None
|
|
821
|
+
|
|
822
|
+
Example:
|
|
823
|
+
>>> from geobox.aio import AsyncGeoboxClient
|
|
824
|
+
>>> from geobox.aio.table import AsyncRelationship
|
|
825
|
+
>>> async with AsyncGeoboxClient() as client:
|
|
826
|
+
>>> relationship = await AsyncRelationship.get_relationship(client, uuid="12345678-1234-5678-1234-567812345678")
|
|
827
|
+
>>> await relationship.delete()
|
|
828
|
+
"""
|
|
829
|
+
await super()._delete(self.endpoint)
|
|
830
|
+
|
|
831
|
+
|
|
832
|
+
async def get_source(self) -> Union['AsyncTable', 'AsyncVectorLayer']:
|
|
833
|
+
"""
|
|
834
|
+
[async] Get the source table or layer
|
|
835
|
+
|
|
836
|
+
Returns:
|
|
837
|
+
AsyncTable | AsyncVectorLayer: the source table or layer
|
|
838
|
+
|
|
839
|
+
Raises:
|
|
840
|
+
NotFoundError: if the table or layer has been deleted
|
|
841
|
+
|
|
842
|
+
Example:
|
|
843
|
+
>>> from geobox.aio import AsyncGeoboxClient
|
|
844
|
+
>>> async with AsyncGeoboxClient() as client:
|
|
845
|
+
>>> relationship = await client.get_relationship(uuid="12345678-1234-5678-1234-567812345678")
|
|
846
|
+
>>> source_table = await relationship.get_source()
|
|
847
|
+
"""
|
|
848
|
+
try:
|
|
849
|
+
result = []
|
|
850
|
+
if self.source_type == 'Table':
|
|
851
|
+
result = await self.api.get_tables(
|
|
852
|
+
q=f"id = {self.source_id}"
|
|
853
|
+
)
|
|
854
|
+
elif self.source_type == 'VectorLayer':
|
|
855
|
+
result = await self.api.get_vectors(
|
|
856
|
+
q=f"id = {self.source_id}"
|
|
857
|
+
)
|
|
858
|
+
source = next(source for source in result if source.id == self.source_id)
|
|
859
|
+
return source
|
|
860
|
+
|
|
861
|
+
except StopIteration:
|
|
862
|
+
raise NotFoundError("Table not found!")
|
|
863
|
+
|
|
864
|
+
|
|
865
|
+
async def get_target(self) -> Union['AsyncTable', 'AsyncVectorLayer']:
|
|
866
|
+
"""
|
|
867
|
+
[async] Get the target table or layer
|
|
868
|
+
|
|
869
|
+
Returns:
|
|
870
|
+
AsyncTable: the target table or layer
|
|
871
|
+
|
|
872
|
+
Raises:
|
|
873
|
+
NotFoundError: if the table or layer has been deleted
|
|
874
|
+
|
|
875
|
+
Example:
|
|
876
|
+
>>> from geobox.aio import AsyncGeoboxClient
|
|
877
|
+
>>> async with AsyncGeoboxClient() as client:
|
|
878
|
+
>>> relationship = await client.get_relationship(uuid="12345678-1234-5678-1234-567812345678")
|
|
879
|
+
>>> target_table = await relationship.get_target()
|
|
880
|
+
"""
|
|
881
|
+
try:
|
|
882
|
+
result = []
|
|
883
|
+
if self.target_type == 'Table':
|
|
884
|
+
result = await self.api.get_tables(
|
|
885
|
+
q=f"id = {self.target_id}"
|
|
886
|
+
)
|
|
887
|
+
elif self.target_type == 'VectorLayer':
|
|
888
|
+
result = await self.api.get_vectors(
|
|
889
|
+
q=f"id = {self.target_id}"
|
|
890
|
+
)
|
|
891
|
+
target = next(target for target in result if target.id == self.target_id)
|
|
892
|
+
return target
|
|
893
|
+
|
|
894
|
+
except StopIteration:
|
|
895
|
+
raise NotFoundError("Table not found!")
|
|
896
|
+
|
|
897
|
+
|
|
898
|
+
async def get_relation_table(self) -> 'AsyncTable':
|
|
899
|
+
"""
|
|
900
|
+
[async] Get the relation table
|
|
901
|
+
|
|
902
|
+
Returns:
|
|
903
|
+
AsyncTable: the relation table
|
|
904
|
+
|
|
905
|
+
Raises:
|
|
906
|
+
ValueError: If the relationship is not Many-to-Many and thus has no relation table
|
|
907
|
+
NotFoundError: if the table has been deleted
|
|
908
|
+
|
|
909
|
+
Example:
|
|
910
|
+
>>> from geobox.aio import AsyncGeoboxClient
|
|
911
|
+
>>> async with AsyncGeoboxClient() as client:
|
|
912
|
+
>>> relationship = await client.get_relationship(uuid="12345678-1234-5678-1234-567812345678")
|
|
913
|
+
>>> relation_table = await relationship.get_relation_table()
|
|
914
|
+
"""
|
|
915
|
+
try:
|
|
916
|
+
if not self.relation_table_id:
|
|
917
|
+
raise ValueError(
|
|
918
|
+
"Relationship is not Many-to-Many. "
|
|
919
|
+
"Relation tables are only used for Many-to-Many cardinality."
|
|
920
|
+
)
|
|
921
|
+
|
|
922
|
+
result = await self.api.get_tables(
|
|
923
|
+
q=f"id = {self.relation_table_id}"
|
|
924
|
+
)
|
|
925
|
+
table = next(table for table in result if table.id == self.relation_table_id)
|
|
926
|
+
return table
|
|
927
|
+
|
|
928
|
+
except StopIteration:
|
|
929
|
+
raise NotFoundError("Table not found!")
|
|
930
|
+
|
|
931
|
+
|
|
932
|
+
async def associate_records(
|
|
933
|
+
self,
|
|
934
|
+
source_id: int,
|
|
935
|
+
*,
|
|
936
|
+
target_ids: Optional[List[int]] = None,
|
|
937
|
+
q: Optional[str] = None,
|
|
938
|
+
) -> Dict:
|
|
939
|
+
"""
|
|
940
|
+
[async] Create relationships between the source record and target records
|
|
941
|
+
|
|
942
|
+
Args:
|
|
943
|
+
source_id (int): the id of feature/row in the source layer/table
|
|
944
|
+
target_ids (List[int], optional): a list of target record ids to be associated with the current record
|
|
945
|
+
q (str, optional): query filter on target layer or table to select which target features or rows that are going to be related to the current record
|
|
946
|
+
|
|
947
|
+
Returns:
|
|
948
|
+
Dict: the record association result
|
|
949
|
+
|
|
950
|
+
Example:
|
|
951
|
+
>>> from geobox.aio import AsyncGeoboxClient
|
|
952
|
+
>>> async with AsyncGeoboxClient() as client:
|
|
953
|
+
>>> relationship = await client.get_relationship(uuid="12345678-1234-5678-1234-567812345678")
|
|
954
|
+
>>> result = await relationship.associate_records(
|
|
955
|
+
... source_id=1,
|
|
956
|
+
... target_ids=[1, 2, 3],
|
|
957
|
+
... q="name LIKE '%_school'",
|
|
958
|
+
... )
|
|
959
|
+
"""
|
|
960
|
+
data = {
|
|
961
|
+
'source_id': source_id,
|
|
962
|
+
'target_ids': ', '.join([str(i) for i in target_ids]),
|
|
963
|
+
'q': q,
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
endpoint = f"{self.endpoint}associateRecords/"
|
|
967
|
+
return await self.api.post(
|
|
968
|
+
endpoint,
|
|
969
|
+
clean_data(data),
|
|
970
|
+
is_json=False,
|
|
971
|
+
)
|
|
972
|
+
|
|
973
|
+
|
|
974
|
+
async def disassociate_records(
|
|
975
|
+
self,
|
|
976
|
+
source_id: int,
|
|
977
|
+
*,
|
|
978
|
+
target_ids: Optional[List[int]] = None,
|
|
979
|
+
q: Optional[str] = None,
|
|
980
|
+
) -> Dict:
|
|
981
|
+
"""
|
|
982
|
+
[async] Remove relationships between the source record and target records
|
|
983
|
+
|
|
984
|
+
Args:
|
|
985
|
+
source_id (int): the id of feature/row in the source layer/table
|
|
986
|
+
target_ids (List[int], optional): a list of target record ids to be disassociated with the current record
|
|
987
|
+
q (str, optional): query filter on target layer or table to select which target features or rows that are going to be related to the current record
|
|
988
|
+
|
|
989
|
+
Returns:
|
|
990
|
+
Dict: the record disassociation result
|
|
991
|
+
|
|
992
|
+
Example:
|
|
993
|
+
>>> from geobox.aio import AsyncGeoboxClient
|
|
994
|
+
>>> async with AsyncGeoboxClient() as client:
|
|
995
|
+
>>> relationship = await client.get_relationship(uuid="12345678-1234-5678-1234-567812345678")
|
|
996
|
+
>>> result = await relationship.disassociate_records(
|
|
997
|
+
... source_id=1,
|
|
998
|
+
... target_ids=[1, 2, 3],
|
|
999
|
+
... q="name LIKE '%_school'",
|
|
1000
|
+
... )
|
|
1001
|
+
"""
|
|
1002
|
+
data = {
|
|
1003
|
+
'source_id': source_id,
|
|
1004
|
+
'target_ids': ', '.join([str(i) for i in target_ids]),
|
|
1005
|
+
'q': q,
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
endpoint = f"{self.endpoint}disassociateRecords/"
|
|
1009
|
+
return await self.api.post(
|
|
1010
|
+
endpoint,
|
|
1011
|
+
clean_data(data),
|
|
1012
|
+
is_json=False,
|
|
1013
|
+
)
|
|
405
1014
|
|
|
406
1015
|
|
|
407
1016
|
class AsyncTable(AsyncBase):
|
|
@@ -426,7 +1035,7 @@ class AsyncTable(AsyncBase):
|
|
|
426
1035
|
@classmethod
|
|
427
1036
|
async def get_tables(cls, api: 'AsyncGeoboxClient', **kwargs) -> Union[List['AsyncTable'], int]:
|
|
428
1037
|
"""
|
|
429
|
-
[async] Get list of tables with optional filtering and pagination.
|
|
1038
|
+
[async] Get a list of tables with optional filtering and pagination.
|
|
430
1039
|
|
|
431
1040
|
Args:
|
|
432
1041
|
api (AsyncGeoboxClient): The AsyncGeoboxClient instance for making requests.
|
|
@@ -595,7 +1204,7 @@ class AsyncTable(AsyncBase):
|
|
|
595
1204
|
>>> from geobox.aio.table import AsyncTable
|
|
596
1205
|
>>> async with AsyncGeoboxClient() as client:
|
|
597
1206
|
>>> table = await AsyncTable.get_table(client, uuid="12345678-1234-5678-1234-567812345678")
|
|
598
|
-
>>> await table.
|
|
1207
|
+
>>> await table.update(display_name="New Display Name")
|
|
599
1208
|
"""
|
|
600
1209
|
data = {
|
|
601
1210
|
"name": kwargs.get('name'),
|
|
@@ -745,7 +1354,7 @@ class AsyncTable(AsyncBase):
|
|
|
745
1354
|
data (Dict, optional): Additional field properties (display_name, description, etc.).
|
|
746
1355
|
|
|
747
1356
|
Returns:
|
|
748
|
-
|
|
1357
|
+
AsyncTableField: The newly created field instance.
|
|
749
1358
|
|
|
750
1359
|
Raises:
|
|
751
1360
|
ValidationError: If the field data is invalid.
|
|
@@ -822,6 +1431,8 @@ class AsyncTable(AsyncBase):
|
|
|
822
1431
|
[async] Query rows of a table
|
|
823
1432
|
|
|
824
1433
|
Keyword Args:
|
|
1434
|
+
relationship_uuid (str): The uuid of relationship
|
|
1435
|
+
related_record_id (int): This is the id of the feature/row that these rows are related to. This id belongs to the related layer/table not this table
|
|
825
1436
|
q (str): Advanced filtering expression, e.g., 'status = "active" and age > 20'
|
|
826
1437
|
search (str): Search term for keyword-based searching among fields/columns
|
|
827
1438
|
search_fields (str): Comma separated column names to search in
|
|
@@ -849,6 +1460,8 @@ class AsyncTable(AsyncBase):
|
|
|
849
1460
|
"""
|
|
850
1461
|
params = {
|
|
851
1462
|
'f': 'json',
|
|
1463
|
+
'relationship_uuid': kwargs.get('relationship_uuid'),
|
|
1464
|
+
'related_record_id': kwargs.get('related_record_id'),
|
|
852
1465
|
'q': kwargs.get('q'),
|
|
853
1466
|
'search': kwargs.get('search'),
|
|
854
1467
|
'search_fields': kwargs.get('search_fields'),
|
|
@@ -1118,27 +1731,3 @@ class AsyncTable(AsyncBase):
|
|
|
1118
1731
|
'limit': limit
|
|
1119
1732
|
}
|
|
1120
1733
|
return await super()._get_shared_users(self.endpoint, params)
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
def to_sync(self, sync_client: 'GeoboxClient') -> 'Table':
|
|
1124
|
-
"""
|
|
1125
|
-
Switch to sync version of the table instance to have access to the sync methods
|
|
1126
|
-
|
|
1127
|
-
Args:
|
|
1128
|
-
sync_client (GeoboxClient): The sync version of the GeoboxClient instance for making requests.
|
|
1129
|
-
|
|
1130
|
-
Returns:
|
|
1131
|
-
Table: the sync instance of the table.
|
|
1132
|
-
|
|
1133
|
-
Example:
|
|
1134
|
-
>>> from geobox import Geoboxclient
|
|
1135
|
-
>>> from geobox.aio import AsyncGeoboxClient
|
|
1136
|
-
>>> from geobox.aio.table import AsyncTable
|
|
1137
|
-
>>> async with AsyncGeoboxClient() as client:
|
|
1138
|
-
>>> table = await AsyncTable.get_table(client, uuid="12345678-1234-5678-1234-567812345678")
|
|
1139
|
-
>>> sync_client = GeoboxClient()
|
|
1140
|
-
>>> sync_table = table.to_async(sync_client)
|
|
1141
|
-
"""
|
|
1142
|
-
from ..table import Table
|
|
1143
|
-
|
|
1144
|
-
return Table(api=sync_client, uuid=self.uuid, data=self.data)
|