Source code for geobox.relationship

from typing import List, Dict, Optional, TYPE_CHECKING, Union
from dataclasses import dataclass

from geobox.exception import NotFoundError, ValidationError
from geobox.utils import clean_data

from .base import Base
from .enums import RelationshipCardinality

if TYPE_CHECKING:
    from . import GeoboxClient
    from .table import Table, TableField
    from .vectorlayer import VectorLayer
    from .field import Field



[docs] @dataclass(frozen=True) class RelationshipEndpoint: """ Represents one endpoint (source or target) of a relationship Args: table (Table | VectorLayer): The source or target table or vector layer field (TableField | Field | str): The field name or object on the source/target entity fk_field (TableField | Field | str, optional): The foreign key field name or object on the relation table. (Required for Many-to-Many relationships) """ table: Union['Table', 'VectorLayer'] field: Union['TableField', 'Field', str] fk_field: Optional[Union['TableField', 'Field', str]] = None
[docs] class Relationship(Base): BASE_ENDPOINT = 'relationships/'
[docs] def __init__(self, api: 'GeoboxClient', uuid: str, data: Optional[Dict] = {}, ): """ Initialize a relationship instance. Args: api (GeoboxClient): The GeoboxClient instance for making requests. uuid (str): The unique identifier for the relationship. data (Dict): The response data of the table. """ super().__init__(api=api, uuid=uuid, data=data)
[docs] def __repr__(self) -> str: """ Return a string representation of the Relationship. Returns: str: The string representation of the Relationship. """ return f"Relationship(uuid={self.uuid}, name={self.data.get('relation_name') or self.data.get('name')}, cardinality={self.data.get('relation_cardinality') or self.data.get('cardinality')})"
@property def cardinality(self) -> RelationshipCardinality: """ Return: RelationshipCardinality: the relationship cardinality """ return RelationshipCardinality(self.relation_cardinality)
[docs] @classmethod def get_relationships( cls, api: 'GeoboxClient', **kwargs, ) -> Union[List['Relationship'], int]: """ Get a list of relationships with optional filtering and pagination. Args: api (GeoboxClient): The GeoboxClient instance for making requests. Keyword Args: q (str): query filter based on OGC CQL standard. e.g. "field1 LIKE '%GIS%' AND created_at > '2021-01-01'" 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 search_fields (str): comma separated list of fields for searching 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. return_count (bool): Whether to return total count. default: False. skip (int): Number of items to skip. default: 0 limit (int): Number of items to return. default: 10 user_id (int): Specific user. privileges required shared (bool): Whether to return shared tables. default: False Returns: List[Relationship] | int: A list of relationship instances or the total number of relationships. Example: >>> from geobox import GeoboxClient >>> from geobox.table import Relatinship >>> client = GeoboxClient() >>> relationships = client.get_relationships(q="name LIKE '%My relationship%'") or >>> relationships = Table.get_relationships(client, q="name LIKE '%My relationship%'") """ params = { 'f': 'json', 'q': kwargs.get('q'), 'search': kwargs.get('search'), 'search_fields': kwargs.get('search_fields'), 'order_by': kwargs.get('order_by'), 'return_count': kwargs.get('return_count', False), 'skip': kwargs.get('skip', 0), 'limit': kwargs.get('limit', 10), 'user_id': kwargs.get('user_id'), 'shared': kwargs.get('shared', False), } return super()._get_list(api, cls.BASE_ENDPOINT, params, factory_func=lambda api, item: Relationship(api, item['uuid'], item))
[docs] @classmethod def create_relationship( cls, api: 'GeoboxClient', name: str, cardinality: RelationshipCardinality, *, source: 'RelationshipEndpoint', target: 'RelationshipEndpoint', relation_table: Optional['Table'] = None, display_name: Optional[str] = None, description: Optional[str] = None, user_id: Optional[int] = None, ) -> 'Relationship': """ Create a new Relationship Args: api (GeoboxClient): The GeoboxClient instance for making requests. name (str): name of the relationship cardinality (RelationshipCardinality): One to One, One to Many, or Many to Many Keyword Args: source (RelationshipEndpoint): Definition of the source side of the relationship, including the table (or layer), field, and foreign-key field target (RelationshipEndpoint): Definition of the target side of the relationship, including the table (or layer), field, and foreign-key field relation_table (Table, optional): The table that stores the relationship metadata or join records. (Required for Many-to-Many relationships) display_name (str, optional): Human-readable name for the relationship description (str, optional): the description of the relationship user_id (int, optional): Specific user. privileges required. Returns: Relationship: a relationship instance Example: >>> from geobox import GeoboxClient >>> from geobox.table import Relationship, RelationshipEndpoint, RelationshipCardinality >>> client = GeoboxClient() >>> source = RelationshipEndpoint( ... table=client.get_table_by_name('owner'), ... field="name", # on source table ... fk_field="book_name", # on relation table ... ) >>> target = RelationshipEndpoint( ... table=client.get_table_by_name('parcel'), ... field="name", # on target table ... fk_field="author_name", # on relation table ... ) >>> relationship = client.create_relationship( ... name="owner_parcel", ... cardinality=RelationshipCardinality.ManytoMany, ... source=source, ... target=target, ... relation_table=client.get_table_by_name('owner_parcel'), ... ) or >>> relationship = Relationsh.create_relationship( ... client, ... name="owner_parcel", ... cardinality=RelationshipCardinality.ManytoMany, ... source=source, ... target=target, ... relation_table=client.get_table_by_name('owner_parcel'), ... ) """ data = { "relation_name": name, "relation_display_name": display_name, "relation_description": description, "relation_cardinality": cardinality.value, "relation_table_id": relation_table.id if relation_table else None, "relation_table_search": f"{relation_table.__class__.__name__}/{relation_table.name}" if relation_table else None, "source_field_name": source.field if type(source.field) == str else source.field.name, "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, "source_id": source.table.id, "source_search": f"{source.table.__class__.__name__}/{source.table.name}", "source_type": str(source.table.__class__.__name__), "target_field_name": target.field if type(target.field) == str else target.field.name, "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, "target_id": target.table.id, "target_search": f"{target.table.__class__.__name__}/{target.table.name}", "target_type": str(target.table.__class__.__name__), "user_id": user_id, } return super()._create(api, cls.BASE_ENDPOINT, data, factory_func=lambda api, item: Relationship(api, item['uuid'], item))
[docs] @classmethod def get_relationship( cls, api: 'GeoboxClient', uuid: str, user_id: Optional[int] = None, ) -> 'Relationship': """ Get a relationship by UUID. Args: api (GeoboxClient): The GeoboxClient instance for making requests. uuid (str): The UUID of the relationship to get. user_id (int, optional): Specific user. privileges required. Returns: Relationship: The Relationship object. Raises: NotFoundError: If the Relationship with the specified UUID is not found. Example: >>> from geobox import GeoboxClient >>> from geobox.table import Relationship >>> client = GeoboxClient() >>> relationship = client.get_relationship(uuid="12345678-1234-5678-1234-567812345678") or >>> relationship = Relationship.get_relationship(client, uuid="12345678-1234-5678-1234-567812345678") """ params = { 'f': 'json', 'user_id': user_id, } return super()._get_detail( api, cls.BASE_ENDPOINT, uuid, params, factory_func=lambda api, item: Relationship(api, item['uuid'], item) )
[docs] @classmethod def get_relationship_by_name( cls, api: 'GeoboxClient', name: str, user_id: Optional[int] = None, ) -> Union['Relationship', None]: """ Get a relationship by name Args: api (GeoboxClient): The GeoboxClient instance for making requests. name (str): the name of the relationship to get user_id (int, optional): specific user. privileges required. Returns: Relationship | None: returns the relationship if a relationship matches the given name, else None Example: >>> from geobox import GeoboxClient >>> from geobox.relationship import Relationship >>> client = GeoboxClient() >>> relationship = client.get_relationship_by_name(name='test') or >>> relationship = Relationship.get_relationship_by_name(client, name='test') """ relationships = cls.get_relationships(api, q=f"name = '{name}'", user_id=user_id) if relationships and relationships[0].relation_name == name: return relationships[0] else: return None
[docs] def update(self, *, name: Optional[str] = None, cardinality: Optional[RelationshipCardinality] = None, source: Optional['RelationshipEndpoint'] = None, target: Optional['RelationshipEndpoint'] = None, relation_table: Optional['Table'] = None, display_name: Optional[str] = None, description: Optional[str] = None, ) -> Dict: """ Update the relationship. Keyword Args: name (str): The name of the relationship. cardinality (RelationshipCardinality): One to One, One to Many, or Many to Many source (RelationshipEndpoint): Definition of the source side of the relationship, including the table (or layer), field, and foreign-key field target (RelationshipEndpoint): Definition of the target side of the relationship, including the table (or layer), field, and foreign-key field relation_table (Table, optional): The table that stores the relationship metadata or join records. (Required for Many-to-Many relationships) display_name (str, optional): Human-readable name for the relationship description (str, optional): the description of the relationship Returns: Dict: The updated relationship data. Raises: ValidationError: If the relationship data is invalid. Example: >>> from geobox import GeoboxClient >>> from geobox.table import Relationship >>> client = GeoboxClient() >>> relationship = Relationship.get_relationship(client, uuid="12345678-1234-5678-1234-567812345678") >>> relationship.update(display_name="New Display Name") """ # Validate relationships if cardinality == RelationshipCardinality.ManytoMany and not relation_table: raise ValidationError("relation_table is required for Many-to-Many relationships") # Helper to extract field name def get_field_name(field) -> Optional[str]: if field is None: return None return field if isinstance(field, str) else field.name # Helper to get table info def get_table_info(table) -> Dict[str, str]: if table is None: return {} return { "id": table.id, "search": f"{table.__class__.__name__}/{table.name}", "type": table.__class__.__name__ } # Build payload data = { "relation_name": name, "relation_display_name": display_name, "relation_description": description, "relation_cardinality": cardinality.value if cardinality else None, } # Add relation table info if present if relation_table: data.update({ "relation_table_id": relation_table.id, "relation_table_search": f"{relation_table.__class__.__name__}/{relation_table.name}" }) # Add source info if source: source_table_info = get_table_info(source.table) data.update({ "source_field_name": get_field_name(source.field), "source_id": source_table_info.get("id"), "source_search": source_table_info.get("search"), "source_type": source_table_info.get("type"), }) if relation_table: data["source_fk_name"] = get_field_name(source.fk_field) # Add target info if target: target_table_info = get_table_info(target.table) data.update({ "target_field_name": get_field_name(target.field), "target_id": target_table_info.get("id"), "target_search": target_table_info.get("search"), "target_type": target_table_info.get("type"), }) if relation_table: data["target_fk_name"] = get_field_name(target.fk_field) return super()._update(self.endpoint, data)
[docs] def delete(self) -> None: """ Delete the Relationship Returns: None Example: >>> from geobox import GeoboxClient >>> from geobox.table import Relationship >>> client = GeoboxClient() >>> relationship = Relationship.get_relationship(client, uuid="12345678-1234-5678-1234-567812345678") >>> relationship.delete() """ super()._delete(self.endpoint)
[docs] def get_source(self) -> Union['Table', 'VectorLayer']: """ Get the source table or layer Returns: Table | VectorLayer: the source table or layer Raises: NotFoundError: if the table or layer has been deleted Example: >>> from geobox import GeoboxClient >>> client = GeoboxClient() >>> relationship = client.get_relationship(uuid="12345678-1234-5678-1234-567812345678") >>> source = relationship.get_source() """ try: result = [] if self.source_type == 'Table': result = self.api.get_tables( q=f"id = {self.source_id}" ) elif self.source_type == 'VectorLayer': result = self.api.get_vector( q=f"id = {self.source_id}" ) source = next(source for source in result if source.id == self.source_id) return source except StopIteration: raise NotFoundError("Source dataset not found!")
[docs] def get_target(self) -> Union['Table', 'VectorLayer']: """ Get the target table or layer Returns: Table | VectorLayer: the target table or layer Raises: NotFoundError: if the table or layer has been deleted Example: >>> from geobox import GeoboxClient >>> client = GeoboxClient() >>> relationship = client.get_relationship(uuid="12345678-1234-5678-1234-567812345678") >>> target_table = relationship.get_target() """ try: result = [] if self.target_type == 'Table': result = self.api.get_tables( q=f"id = {self.target_id}" ) elif self.target_type == 'VectorLayer': result = self.api.get_vectors( q=f"id = {self.target_id}" ) target = next(target for target in result if target.id == self.target_id) return target except StopIteration: raise NotFoundError("Table not found!")
[docs] def get_relation_table(self) -> 'Table': """ Get the relation table Returns: Table: the relation table Raises: ValueError: If the relationship is not Many-to-Many and thus has no relation table NotFoundError: if the table has been deleted Example: >>> from geobox import GeoboxClient >>> client = GeoboxClient() >>> relationship = client.get_relationship(uuid="12345678-1234-5678-1234-567812345678") >>> relation_table = relationship.get_relation_table() """ try: if not self.relation_table_id: raise ValueError( "Relationship is not Many-to-Many. " "Relation tables are only used for Many-to-Many cardinality." ) result = self.api.get_tables( q=f"id = {self.relation_table_id}" ) table = next(table for table in result if table.id == self.relation_table_id) return table except StopIteration: raise NotFoundError("Table not found!")
[docs] def associate_records( self, source_id: int, *, target_ids: Optional[List[int]] = None, q: Optional[str] = None, ) -> Dict: """ Create relationships between the source record and target records Args: source_id (int): the id of feature/row in the source layer/table target_ids (List[int], optional): a list of target record ids to be associated with the current record 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 Returns: Dict: the record association result Example: >>> from geobox import GeoboxClient >>> client = GeoboxClient() >>> relationship = client.get_relationship(uuid="12345678-1234-5678-1234-567812345678") >>> result = relationship.associate_records( ... source_id=1, ... target_ids=[1, 2, 3], ... q="name LIKE '%_school'", ... ) """ data = { 'source_id': source_id, 'target_ids': ', '.join([str(i) for i in target_ids]), 'q': q, } endpoint = f"{self.endpoint}associateRecords/" return self.api.post( endpoint, clean_data(data), is_json=False, )
[docs] def disassociate_records( self, source_id: int, *, target_ids: Optional[List[int]] = None, q: Optional[str] = None, ) -> Dict: """ Remove relationships between the source record and target records Args: source_id (int): the id of feature/row in the source layer/table target_ids (List[int], optional): a list of target record ids to be disassociated with the current record 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 Returns: Dict: the record disassociation result Example: >>> from geobox import GeoboxClient >>> client = GeoboxClient() >>> relationship = client.get_relationship(uuid="12345678-1234-5678-1234-567812345678") >>> result = relationship.disassociate_records( ... source_id=1, ... target_ids=[1, 2, 3], ... q="name LIKE '%_school'", ... ) """ data = { 'source_id': source_id, 'target_ids': ', '.join([str(i) for i in target_ids]), 'q': q, } endpoint = f"{self.endpoint}disassociateRecords/" return self.api.post( endpoint, clean_data(data), is_json=False, )