Skip to content

Core Module

gigaspatial.core

http

AuthConfig

Bases: BaseModel

Authentication configuration for a REST API client.

Source code in gigaspatial/core/http/auth.py
class AuthConfig(BaseModel):
    """Authentication configuration for a REST API client."""

    auth_type: AuthType = Field(AuthType.NONE, description="Authentication method")
    api_key: Optional[SecretStr] = Field(None, description="API key or Bearer token")
    api_key_header: str = Field("X-Api-Key", description="Header name for API key auth")
    api_key_param: str = Field("apikey", description="Query param name for API key auth")
    username: Optional[str] = Field(None, description="Username for Basic auth")
    password: Optional[SecretStr] = Field(None, description="Password for Basic auth")

    class Config:
        frozen = True

    def build(self) -> Tuple[dict[str, str], dict[str, str], Any]:
        """
        Resolve auth config into (headers, query_params, httpx_auth).

        Returns
        -------
        Tuple[dict, dict, Any]
            headers, query_params, and httpx auth object (or None).
        """
        headers: dict[str, str] = {}
        params: dict[str, str] = {}
        auth: Any = None

        if self.auth_type == AuthType.BEARER:
            headers["Authorization"] = f"Bearer {self.api_key.get_secret_value()}"
        elif self.auth_type == AuthType.API_KEY_HEADER:
            headers[self.api_key_header] = self.api_key.get_secret_value()
        elif self.auth_type == AuthType.API_KEY_QUERY:
            params[self.api_key_param] = self.api_key.get_secret_value()
        elif self.auth_type == AuthType.BASIC:
            auth = (self.username, self.password.get_secret_value())

        return headers, params, auth
build()

Resolve auth config into (headers, query_params, httpx_auth).

Returns

Tuple[dict, dict, Any] headers, query_params, and httpx auth object (or None).

Source code in gigaspatial/core/http/auth.py
def build(self) -> Tuple[dict[str, str], dict[str, str], Any]:
    """
    Resolve auth config into (headers, query_params, httpx_auth).

    Returns
    -------
    Tuple[dict, dict, Any]
        headers, query_params, and httpx auth object (or None).
    """
    headers: dict[str, str] = {}
    params: dict[str, str] = {}
    auth: Any = None

    if self.auth_type == AuthType.BEARER:
        headers["Authorization"] = f"Bearer {self.api_key.get_secret_value()}"
    elif self.auth_type == AuthType.API_KEY_HEADER:
        headers[self.api_key_header] = self.api_key.get_secret_value()
    elif self.auth_type == AuthType.API_KEY_QUERY:
        params[self.api_key_param] = self.api_key.get_secret_value()
    elif self.auth_type == AuthType.BASIC:
        auth = (self.username, self.password.get_secret_value())

    return headers, params, auth

BasePaginationStrategy

Bases: ABC

Abstract base for pagination strategies.

Subclass this to implement cursor-based, offset/limit, page-number, or Link-header pagination.

Source code in gigaspatial/core/http/pagination.py
class BasePaginationStrategy(ABC):
    """
    Abstract base for pagination strategies.

    Subclass this to implement cursor-based, offset/limit,
    page-number, or Link-header pagination.
    """

    @abstractmethod
    def next_request(
        self,
        response: httpx.Response,
        current_params: dict[str, Any],
    ) -> Optional[dict[str, Any]]:
        """
        Return updated params for the next page, or None if exhausted.

        Parameters
        ----------
        response : httpx.Response
            The most recent response from the API.
        current_params : dict
            The params used for the current request.

        Returns
        -------
        Optional[dict]
            Params for the next request, or None to stop pagination.
        """
        ...

    @abstractmethod
    def extract_records(self, response: httpx.Response) -> list[dict]:
        """Extract the list of records from a response."""
        ...
extract_records(response) abstractmethod

Extract the list of records from a response.

Source code in gigaspatial/core/http/pagination.py
@abstractmethod
def extract_records(self, response: httpx.Response) -> list[dict]:
    """Extract the list of records from a response."""
    ...
next_request(response, current_params) abstractmethod

Return updated params for the next page, or None if exhausted.

Parameters

response : httpx.Response The most recent response from the API. current_params : dict The params used for the current request.

Returns

Optional[dict] Params for the next request, or None to stop pagination.

Source code in gigaspatial/core/http/pagination.py
@abstractmethod
def next_request(
    self,
    response: httpx.Response,
    current_params: dict[str, Any],
) -> Optional[dict[str, Any]]:
    """
    Return updated params for the next page, or None if exhausted.

    Parameters
    ----------
    response : httpx.Response
        The most recent response from the API.
    current_params : dict
        The params used for the current request.

    Returns
    -------
    Optional[dict]
        Params for the next request, or None to stop pagination.
    """
    ...

BaseRestApiClient

Bases: ABC

Abstract base class for REST API clients in GigaSpatial.

Composes authentication (AuthConfig), retry logic, and pagination (BasePaginationStrategy) into a reusable context-managed client. Subclasses define the pagination strategy and any endpoint-specific methods.

Parameters

config : RestApiClientConfig Pydantic configuration for the client.

Examples

config = RestApiClientConfig( ... base_url="https://api.example.com", ... auth=AuthConfig(auth_type=AuthType.BEARER, api_key="secret"), ... ) with MyApiClient(config) as client: ... for page in client.paginate("/schools", params={"country": "KE"}): ... process(page)

Source code in gigaspatial/core/http/client.py
class BaseRestApiClient(ABC):
    """
    Abstract base class for REST API clients in GigaSpatial.

    Composes authentication (AuthConfig), retry logic, and pagination
    (BasePaginationStrategy) into a reusable context-managed client.
    Subclasses define the pagination strategy and any endpoint-specific methods.

    Parameters
    ----------
    config : RestApiClientConfig
        Pydantic configuration for the client.

    Examples
    --------
    >>> config = RestApiClientConfig(
    ...     base_url="https://api.example.com",
    ...     auth=AuthConfig(auth_type=AuthType.BEARER, api_key="secret"),
    ... )
    >>> with MyApiClient(config) as client:
    ...     for page in client.paginate("/schools", params={"country": "KE"}):
    ...         process(page)
    """

    def __init__(self, config: RestApiClientConfig) -> None:
        self.config = config
        self._client: Optional[httpx.Client] = None

    # ------------------------------------------------------------------
    # Pagination strategy — subclasses define this
    # ------------------------------------------------------------------

    @property
    @abstractmethod
    def pagination_strategy(self) -> BasePaginationStrategy:
        """Return the pagination strategy for this API."""
        ...

    # ------------------------------------------------------------------
    # Session lifecycle
    # ------------------------------------------------------------------

    def _build_client(self) -> httpx.Client:
        auth_headers, auth_params, httpx_auth = self.config.auth.build()
        headers = {**self.config.default_headers, **auth_headers}
        return httpx.Client(
            base_url=self.config.base_url,
            headers=headers,
            params=auth_params,
            auth=httpx_auth,
            timeout=self.config.timeout,
        )

    def __enter__(self) -> "BaseRestApiClient":
        self._client = self._build_client()
        return self

    def __exit__(self, *args: Any) -> None:
        if self._client:
            self._client.close()
            self._client = None

    # ------------------------------------------------------------------
    # Core request with retry + rate-limit handling
    # ------------------------------------------------------------------

    def request(self, method: str, endpoint: str, **kwargs: Any) -> httpx.Response:
        """
        Send an HTTP request with automatic retries and backoff.

        Parameters
        ----------
        method : str
            HTTP method (GET, POST, etc.).
        endpoint : str
            API endpoint path relative to base_url.
        **kwargs
            Passed directly to httpx.Client.request().

        Returns
        -------
        httpx.Response

        Raises
        ------
        httpx.HTTPStatusError
            If the request fails after all retries.
        """
        assert self._client is not None, "Client not started — use as a context manager."
        delay = 1.0

        for attempt in range(1, self.config.max_retries + 1):
            response = self._client.request(method, endpoint, **kwargs)

            if response.status_code == 429:
                wait = float(response.headers.get("Retry-After", delay))
                logger.warning("Rate limited. Waiting %.1fs (attempt %d/%d)", wait, attempt, self.config.max_retries)
                time.sleep(wait)
                delay *= self.config.retry_backoff
                continue

            if response.status_code >= 500 and attempt < self.config.max_retries:
                logger.warning("Server error %d. Retrying in %.1fs (attempt %d/%d)", response.status_code, delay, attempt, self.config.max_retries)
                time.sleep(delay)
                delay *= self.config.retry_backoff
                continue

            response.raise_for_status()
            return response

        response.raise_for_status()
        return response

    def get(self, endpoint: str, **kwargs: Any) -> httpx.Response:
        """Convenience wrapper for GET requests."""
        return self.request("GET", endpoint, **kwargs)

    def post(self, endpoint: str, **kwargs: Any) -> httpx.Response:
        """Convenience wrapper for POST requests."""
        return self.request("POST", endpoint, **kwargs)

    # ------------------------------------------------------------------
    # Pagination
    # ------------------------------------------------------------------

    def paginate(
        self,
        endpoint: str,
        params: Optional[dict[str, Any]] = None,
    ) -> Generator[list[dict], None, None]:
        """
        Yield pages of records using the configured pagination strategy.

        Parameters
        ----------
        endpoint : str
            API endpoint to paginate.
        params : dict, optional
            Initial query parameters.

        Yields
        ------
        list[dict]
            One page of records per iteration.
        """
        current_params = params or {}
        strategy = self.pagination_strategy

        while True:
            response = self.get(endpoint, params=current_params)
            records = strategy.extract_records(response)
            if not records:
                break
            yield records
            next_params = strategy.next_request(response, current_params)
            if next_params is None:
                break
            current_params = next_params
pagination_strategy: BasePaginationStrategy abstractmethod property

Return the pagination strategy for this API.

get(endpoint, **kwargs)

Convenience wrapper for GET requests.

Source code in gigaspatial/core/http/client.py
def get(self, endpoint: str, **kwargs: Any) -> httpx.Response:
    """Convenience wrapper for GET requests."""
    return self.request("GET", endpoint, **kwargs)
paginate(endpoint, params=None)

Yield pages of records using the configured pagination strategy.

Parameters

endpoint : str API endpoint to paginate. params : dict, optional Initial query parameters.

Yields

list[dict] One page of records per iteration.

Source code in gigaspatial/core/http/client.py
def paginate(
    self,
    endpoint: str,
    params: Optional[dict[str, Any]] = None,
) -> Generator[list[dict], None, None]:
    """
    Yield pages of records using the configured pagination strategy.

    Parameters
    ----------
    endpoint : str
        API endpoint to paginate.
    params : dict, optional
        Initial query parameters.

    Yields
    ------
    list[dict]
        One page of records per iteration.
    """
    current_params = params or {}
    strategy = self.pagination_strategy

    while True:
        response = self.get(endpoint, params=current_params)
        records = strategy.extract_records(response)
        if not records:
            break
        yield records
        next_params = strategy.next_request(response, current_params)
        if next_params is None:
            break
        current_params = next_params
post(endpoint, **kwargs)

Convenience wrapper for POST requests.

Source code in gigaspatial/core/http/client.py
def post(self, endpoint: str, **kwargs: Any) -> httpx.Response:
    """Convenience wrapper for POST requests."""
    return self.request("POST", endpoint, **kwargs)
request(method, endpoint, **kwargs)

Send an HTTP request with automatic retries and backoff.

Parameters

method : str HTTP method (GET, POST, etc.). endpoint : str API endpoint path relative to base_url. **kwargs Passed directly to httpx.Client.request().

Returns

httpx.Response

Raises

httpx.HTTPStatusError If the request fails after all retries.

Source code in gigaspatial/core/http/client.py
def request(self, method: str, endpoint: str, **kwargs: Any) -> httpx.Response:
    """
    Send an HTTP request with automatic retries and backoff.

    Parameters
    ----------
    method : str
        HTTP method (GET, POST, etc.).
    endpoint : str
        API endpoint path relative to base_url.
    **kwargs
        Passed directly to httpx.Client.request().

    Returns
    -------
    httpx.Response

    Raises
    ------
    httpx.HTTPStatusError
        If the request fails after all retries.
    """
    assert self._client is not None, "Client not started — use as a context manager."
    delay = 1.0

    for attempt in range(1, self.config.max_retries + 1):
        response = self._client.request(method, endpoint, **kwargs)

        if response.status_code == 429:
            wait = float(response.headers.get("Retry-After", delay))
            logger.warning("Rate limited. Waiting %.1fs (attempt %d/%d)", wait, attempt, self.config.max_retries)
            time.sleep(wait)
            delay *= self.config.retry_backoff
            continue

        if response.status_code >= 500 and attempt < self.config.max_retries:
            logger.warning("Server error %d. Retrying in %.1fs (attempt %d/%d)", response.status_code, delay, attempt, self.config.max_retries)
            time.sleep(delay)
            delay *= self.config.retry_backoff
            continue

        response.raise_for_status()
        return response

    response.raise_for_status()
    return response

CursorPagination

Bases: BasePaginationStrategy

Cursor-based pagination (e.g. GitHub, Mapbox APIs).

Parameters

cursor_response_key : str JSON key in the response body containing the next cursor. cursor_param : str Query parameter name to pass the cursor on the next request. records_key : str JSON key containing the list of records.

Source code in gigaspatial/core/http/pagination.py
class CursorPagination(BasePaginationStrategy):
    """
    Cursor-based pagination (e.g. GitHub, Mapbox APIs).

    Parameters
    ----------
    cursor_response_key : str
        JSON key in the response body containing the next cursor.
    cursor_param : str
        Query parameter name to pass the cursor on the next request.
    records_key : str
        JSON key containing the list of records.
    """

    def __init__(
        self,
        cursor_response_key: str = "next_cursor",
        cursor_param: str = "cursor",
        records_key: str = "results",
    ) -> None:
        self.cursor_response_key = cursor_response_key
        self.cursor_param = cursor_param
        self.records_key = records_key

    def next_request(
        self,
        response: httpx.Response,
        current_params: dict[str, Any],
    ) -> Optional[dict[str, Any]]:
        cursor = response.json().get(self.cursor_response_key)
        if not cursor:
            return None
        return {**current_params, self.cursor_param: cursor}

    def extract_records(self, response: httpx.Response) -> list[dict]:
        return response.json().get(self.records_key, [])

OffsetPagination

Bases: BasePaginationStrategy

Standard offset/limit pagination.

Parameters

page_size : int Number of records per page. offset_param : str Query parameter name for the offset. limit_param : str Query parameter name for the limit/page size. records_key : str JSON key containing the list of records.

Source code in gigaspatial/core/http/pagination.py
class OffsetPagination(BasePaginationStrategy):
    """
    Standard offset/limit pagination.

    Parameters
    ----------
    page_size : int
        Number of records per page.
    offset_param : str
        Query parameter name for the offset.
    limit_param : str
        Query parameter name for the limit/page size.
    records_key : str
        JSON key containing the list of records.
    """

    def __init__(
        self,
        page_size: int = 100,
        offset_param: str = "offset",
        limit_param: str = "limit",
        records_key: str = "results",
    ) -> None:
        self.page_size = page_size
        self.offset_param = offset_param
        self.limit_param = limit_param
        self.records_key = records_key

    def next_request(
        self,
        response: httpx.Response,
        current_params: dict[str, Any],
    ) -> Optional[dict[str, Any]]:
        records = self.extract_records(response)
        if len(records) < self.page_size:
            return None  # last page
        next_offset = current_params.get(self.offset_param, 0) + self.page_size
        return {**current_params, self.offset_param: next_offset, self.limit_param: self.page_size}

    def extract_records(self, response: httpx.Response) -> list[dict]:
        return response.json().get(self.records_key, [])

PageNumberPagination

Bases: BasePaginationStrategy

Page-number pagination (page=1, page=2, ...).

Parameters

page_size : int Number of records per page. page_param : str Query parameter name for the page number. size_param : str Query parameter name for the page size. records_key : str JSON key containing the list of records.

Source code in gigaspatial/core/http/pagination.py
class PageNumberPagination(BasePaginationStrategy):
    """
    Page-number pagination (page=1, page=2, ...).

    Parameters
    ----------
    page_size : int
        Number of records per page.
    page_param : str
        Query parameter name for the page number.
    size_param : str
        Query parameter name for the page size.
    records_key : str
        JSON key containing the list of records.
    """

    def __init__(
        self,
        page_size: int = 1000,
        page_param: str = "page",
        size_param: str = "size",
        records_key: str = "data",
    ) -> None:
        self.page_size = page_size
        self.page_param = page_param
        self.size_param = size_param
        self.records_key = records_key

    def next_request(
        self,
        response: httpx.Response,
        current_params: dict[str, Any],
    ) -> Optional[dict[str, Any]]:
        records = self.extract_records(response)
        if len(records) < self.page_size:
            return None  # partial page — last page reached
        next_page = current_params.get(self.page_param, 1) + 1
        return {**current_params, self.page_param: next_page}

    def extract_records(self, response: httpx.Response) -> list[dict]:
        return response.json().get(self.records_key, [])

RestApiClientConfig

Bases: BaseModel

Top-level configuration for a REST API client.

Source code in gigaspatial/core/http/client.py
class RestApiClientConfig(BaseModel):
    """Top-level configuration for a REST API client."""

    base_url: str = Field(..., description="Base URL of the REST API")
    auth: AuthConfig = Field(default_factory=AuthConfig, description="Authentication config")
    timeout: float = Field(30.0, description="Request timeout in seconds")
    max_retries: int = Field(3, description="Max retries on transient errors")
    retry_backoff: float = Field(1.5, description="Exponential backoff multiplier")
    default_headers: dict[str, str] = Field(default_factory=dict)

    class Config:
        frozen = True

auth

AuthConfig

Bases: BaseModel

Authentication configuration for a REST API client.

Source code in gigaspatial/core/http/auth.py
class AuthConfig(BaseModel):
    """Authentication configuration for a REST API client."""

    auth_type: AuthType = Field(AuthType.NONE, description="Authentication method")
    api_key: Optional[SecretStr] = Field(None, description="API key or Bearer token")
    api_key_header: str = Field("X-Api-Key", description="Header name for API key auth")
    api_key_param: str = Field("apikey", description="Query param name for API key auth")
    username: Optional[str] = Field(None, description="Username for Basic auth")
    password: Optional[SecretStr] = Field(None, description="Password for Basic auth")

    class Config:
        frozen = True

    def build(self) -> Tuple[dict[str, str], dict[str, str], Any]:
        """
        Resolve auth config into (headers, query_params, httpx_auth).

        Returns
        -------
        Tuple[dict, dict, Any]
            headers, query_params, and httpx auth object (or None).
        """
        headers: dict[str, str] = {}
        params: dict[str, str] = {}
        auth: Any = None

        if self.auth_type == AuthType.BEARER:
            headers["Authorization"] = f"Bearer {self.api_key.get_secret_value()}"
        elif self.auth_type == AuthType.API_KEY_HEADER:
            headers[self.api_key_header] = self.api_key.get_secret_value()
        elif self.auth_type == AuthType.API_KEY_QUERY:
            params[self.api_key_param] = self.api_key.get_secret_value()
        elif self.auth_type == AuthType.BASIC:
            auth = (self.username, self.password.get_secret_value())

        return headers, params, auth
build()

Resolve auth config into (headers, query_params, httpx_auth).

Returns

Tuple[dict, dict, Any] headers, query_params, and httpx auth object (or None).

Source code in gigaspatial/core/http/auth.py
def build(self) -> Tuple[dict[str, str], dict[str, str], Any]:
    """
    Resolve auth config into (headers, query_params, httpx_auth).

    Returns
    -------
    Tuple[dict, dict, Any]
        headers, query_params, and httpx auth object (or None).
    """
    headers: dict[str, str] = {}
    params: dict[str, str] = {}
    auth: Any = None

    if self.auth_type == AuthType.BEARER:
        headers["Authorization"] = f"Bearer {self.api_key.get_secret_value()}"
    elif self.auth_type == AuthType.API_KEY_HEADER:
        headers[self.api_key_header] = self.api_key.get_secret_value()
    elif self.auth_type == AuthType.API_KEY_QUERY:
        params[self.api_key_param] = self.api_key.get_secret_value()
    elif self.auth_type == AuthType.BASIC:
        auth = (self.username, self.password.get_secret_value())

    return headers, params, auth

client

BaseRestApiClient

Bases: ABC

Abstract base class for REST API clients in GigaSpatial.

Composes authentication (AuthConfig), retry logic, and pagination (BasePaginationStrategy) into a reusable context-managed client. Subclasses define the pagination strategy and any endpoint-specific methods.

Parameters

config : RestApiClientConfig Pydantic configuration for the client.

Examples

config = RestApiClientConfig( ... base_url="https://api.example.com", ... auth=AuthConfig(auth_type=AuthType.BEARER, api_key="secret"), ... ) with MyApiClient(config) as client: ... for page in client.paginate("/schools", params={"country": "KE"}): ... process(page)

Source code in gigaspatial/core/http/client.py
class BaseRestApiClient(ABC):
    """
    Abstract base class for REST API clients in GigaSpatial.

    Composes authentication (AuthConfig), retry logic, and pagination
    (BasePaginationStrategy) into a reusable context-managed client.
    Subclasses define the pagination strategy and any endpoint-specific methods.

    Parameters
    ----------
    config : RestApiClientConfig
        Pydantic configuration for the client.

    Examples
    --------
    >>> config = RestApiClientConfig(
    ...     base_url="https://api.example.com",
    ...     auth=AuthConfig(auth_type=AuthType.BEARER, api_key="secret"),
    ... )
    >>> with MyApiClient(config) as client:
    ...     for page in client.paginate("/schools", params={"country": "KE"}):
    ...         process(page)
    """

    def __init__(self, config: RestApiClientConfig) -> None:
        self.config = config
        self._client: Optional[httpx.Client] = None

    # ------------------------------------------------------------------
    # Pagination strategy — subclasses define this
    # ------------------------------------------------------------------

    @property
    @abstractmethod
    def pagination_strategy(self) -> BasePaginationStrategy:
        """Return the pagination strategy for this API."""
        ...

    # ------------------------------------------------------------------
    # Session lifecycle
    # ------------------------------------------------------------------

    def _build_client(self) -> httpx.Client:
        auth_headers, auth_params, httpx_auth = self.config.auth.build()
        headers = {**self.config.default_headers, **auth_headers}
        return httpx.Client(
            base_url=self.config.base_url,
            headers=headers,
            params=auth_params,
            auth=httpx_auth,
            timeout=self.config.timeout,
        )

    def __enter__(self) -> "BaseRestApiClient":
        self._client = self._build_client()
        return self

    def __exit__(self, *args: Any) -> None:
        if self._client:
            self._client.close()
            self._client = None

    # ------------------------------------------------------------------
    # Core request with retry + rate-limit handling
    # ------------------------------------------------------------------

    def request(self, method: str, endpoint: str, **kwargs: Any) -> httpx.Response:
        """
        Send an HTTP request with automatic retries and backoff.

        Parameters
        ----------
        method : str
            HTTP method (GET, POST, etc.).
        endpoint : str
            API endpoint path relative to base_url.
        **kwargs
            Passed directly to httpx.Client.request().

        Returns
        -------
        httpx.Response

        Raises
        ------
        httpx.HTTPStatusError
            If the request fails after all retries.
        """
        assert self._client is not None, "Client not started — use as a context manager."
        delay = 1.0

        for attempt in range(1, self.config.max_retries + 1):
            response = self._client.request(method, endpoint, **kwargs)

            if response.status_code == 429:
                wait = float(response.headers.get("Retry-After", delay))
                logger.warning("Rate limited. Waiting %.1fs (attempt %d/%d)", wait, attempt, self.config.max_retries)
                time.sleep(wait)
                delay *= self.config.retry_backoff
                continue

            if response.status_code >= 500 and attempt < self.config.max_retries:
                logger.warning("Server error %d. Retrying in %.1fs (attempt %d/%d)", response.status_code, delay, attempt, self.config.max_retries)
                time.sleep(delay)
                delay *= self.config.retry_backoff
                continue

            response.raise_for_status()
            return response

        response.raise_for_status()
        return response

    def get(self, endpoint: str, **kwargs: Any) -> httpx.Response:
        """Convenience wrapper for GET requests."""
        return self.request("GET", endpoint, **kwargs)

    def post(self, endpoint: str, **kwargs: Any) -> httpx.Response:
        """Convenience wrapper for POST requests."""
        return self.request("POST", endpoint, **kwargs)

    # ------------------------------------------------------------------
    # Pagination
    # ------------------------------------------------------------------

    def paginate(
        self,
        endpoint: str,
        params: Optional[dict[str, Any]] = None,
    ) -> Generator[list[dict], None, None]:
        """
        Yield pages of records using the configured pagination strategy.

        Parameters
        ----------
        endpoint : str
            API endpoint to paginate.
        params : dict, optional
            Initial query parameters.

        Yields
        ------
        list[dict]
            One page of records per iteration.
        """
        current_params = params or {}
        strategy = self.pagination_strategy

        while True:
            response = self.get(endpoint, params=current_params)
            records = strategy.extract_records(response)
            if not records:
                break
            yield records
            next_params = strategy.next_request(response, current_params)
            if next_params is None:
                break
            current_params = next_params
pagination_strategy: BasePaginationStrategy abstractmethod property

Return the pagination strategy for this API.

get(endpoint, **kwargs)

Convenience wrapper for GET requests.

Source code in gigaspatial/core/http/client.py
def get(self, endpoint: str, **kwargs: Any) -> httpx.Response:
    """Convenience wrapper for GET requests."""
    return self.request("GET", endpoint, **kwargs)
paginate(endpoint, params=None)

Yield pages of records using the configured pagination strategy.

Parameters

endpoint : str API endpoint to paginate. params : dict, optional Initial query parameters.

Yields

list[dict] One page of records per iteration.

Source code in gigaspatial/core/http/client.py
def paginate(
    self,
    endpoint: str,
    params: Optional[dict[str, Any]] = None,
) -> Generator[list[dict], None, None]:
    """
    Yield pages of records using the configured pagination strategy.

    Parameters
    ----------
    endpoint : str
        API endpoint to paginate.
    params : dict, optional
        Initial query parameters.

    Yields
    ------
    list[dict]
        One page of records per iteration.
    """
    current_params = params or {}
    strategy = self.pagination_strategy

    while True:
        response = self.get(endpoint, params=current_params)
        records = strategy.extract_records(response)
        if not records:
            break
        yield records
        next_params = strategy.next_request(response, current_params)
        if next_params is None:
            break
        current_params = next_params
post(endpoint, **kwargs)

Convenience wrapper for POST requests.

Source code in gigaspatial/core/http/client.py
def post(self, endpoint: str, **kwargs: Any) -> httpx.Response:
    """Convenience wrapper for POST requests."""
    return self.request("POST", endpoint, **kwargs)
request(method, endpoint, **kwargs)

Send an HTTP request with automatic retries and backoff.

Parameters

method : str HTTP method (GET, POST, etc.). endpoint : str API endpoint path relative to base_url. **kwargs Passed directly to httpx.Client.request().

Returns

httpx.Response

Raises

httpx.HTTPStatusError If the request fails after all retries.

Source code in gigaspatial/core/http/client.py
def request(self, method: str, endpoint: str, **kwargs: Any) -> httpx.Response:
    """
    Send an HTTP request with automatic retries and backoff.

    Parameters
    ----------
    method : str
        HTTP method (GET, POST, etc.).
    endpoint : str
        API endpoint path relative to base_url.
    **kwargs
        Passed directly to httpx.Client.request().

    Returns
    -------
    httpx.Response

    Raises
    ------
    httpx.HTTPStatusError
        If the request fails after all retries.
    """
    assert self._client is not None, "Client not started — use as a context manager."
    delay = 1.0

    for attempt in range(1, self.config.max_retries + 1):
        response = self._client.request(method, endpoint, **kwargs)

        if response.status_code == 429:
            wait = float(response.headers.get("Retry-After", delay))
            logger.warning("Rate limited. Waiting %.1fs (attempt %d/%d)", wait, attempt, self.config.max_retries)
            time.sleep(wait)
            delay *= self.config.retry_backoff
            continue

        if response.status_code >= 500 and attempt < self.config.max_retries:
            logger.warning("Server error %d. Retrying in %.1fs (attempt %d/%d)", response.status_code, delay, attempt, self.config.max_retries)
            time.sleep(delay)
            delay *= self.config.retry_backoff
            continue

        response.raise_for_status()
        return response

    response.raise_for_status()
    return response
RestApiClientConfig

Bases: BaseModel

Top-level configuration for a REST API client.

Source code in gigaspatial/core/http/client.py
class RestApiClientConfig(BaseModel):
    """Top-level configuration for a REST API client."""

    base_url: str = Field(..., description="Base URL of the REST API")
    auth: AuthConfig = Field(default_factory=AuthConfig, description="Authentication config")
    timeout: float = Field(30.0, description="Request timeout in seconds")
    max_retries: int = Field(3, description="Max retries on transient errors")
    retry_backoff: float = Field(1.5, description="Exponential backoff multiplier")
    default_headers: dict[str, str] = Field(default_factory=dict)

    class Config:
        frozen = True

pagination

BasePaginationStrategy

Bases: ABC

Abstract base for pagination strategies.

Subclass this to implement cursor-based, offset/limit, page-number, or Link-header pagination.

Source code in gigaspatial/core/http/pagination.py
class BasePaginationStrategy(ABC):
    """
    Abstract base for pagination strategies.

    Subclass this to implement cursor-based, offset/limit,
    page-number, or Link-header pagination.
    """

    @abstractmethod
    def next_request(
        self,
        response: httpx.Response,
        current_params: dict[str, Any],
    ) -> Optional[dict[str, Any]]:
        """
        Return updated params for the next page, or None if exhausted.

        Parameters
        ----------
        response : httpx.Response
            The most recent response from the API.
        current_params : dict
            The params used for the current request.

        Returns
        -------
        Optional[dict]
            Params for the next request, or None to stop pagination.
        """
        ...

    @abstractmethod
    def extract_records(self, response: httpx.Response) -> list[dict]:
        """Extract the list of records from a response."""
        ...
extract_records(response) abstractmethod

Extract the list of records from a response.

Source code in gigaspatial/core/http/pagination.py
@abstractmethod
def extract_records(self, response: httpx.Response) -> list[dict]:
    """Extract the list of records from a response."""
    ...
next_request(response, current_params) abstractmethod

Return updated params for the next page, or None if exhausted.

Parameters

response : httpx.Response The most recent response from the API. current_params : dict The params used for the current request.

Returns

Optional[dict] Params for the next request, or None to stop pagination.

Source code in gigaspatial/core/http/pagination.py
@abstractmethod
def next_request(
    self,
    response: httpx.Response,
    current_params: dict[str, Any],
) -> Optional[dict[str, Any]]:
    """
    Return updated params for the next page, or None if exhausted.

    Parameters
    ----------
    response : httpx.Response
        The most recent response from the API.
    current_params : dict
        The params used for the current request.

    Returns
    -------
    Optional[dict]
        Params for the next request, or None to stop pagination.
    """
    ...
CursorPagination

Bases: BasePaginationStrategy

Cursor-based pagination (e.g. GitHub, Mapbox APIs).

Parameters

cursor_response_key : str JSON key in the response body containing the next cursor. cursor_param : str Query parameter name to pass the cursor on the next request. records_key : str JSON key containing the list of records.

Source code in gigaspatial/core/http/pagination.py
class CursorPagination(BasePaginationStrategy):
    """
    Cursor-based pagination (e.g. GitHub, Mapbox APIs).

    Parameters
    ----------
    cursor_response_key : str
        JSON key in the response body containing the next cursor.
    cursor_param : str
        Query parameter name to pass the cursor on the next request.
    records_key : str
        JSON key containing the list of records.
    """

    def __init__(
        self,
        cursor_response_key: str = "next_cursor",
        cursor_param: str = "cursor",
        records_key: str = "results",
    ) -> None:
        self.cursor_response_key = cursor_response_key
        self.cursor_param = cursor_param
        self.records_key = records_key

    def next_request(
        self,
        response: httpx.Response,
        current_params: dict[str, Any],
    ) -> Optional[dict[str, Any]]:
        cursor = response.json().get(self.cursor_response_key)
        if not cursor:
            return None
        return {**current_params, self.cursor_param: cursor}

    def extract_records(self, response: httpx.Response) -> list[dict]:
        return response.json().get(self.records_key, [])
OffsetPagination

Bases: BasePaginationStrategy

Standard offset/limit pagination.

Parameters

page_size : int Number of records per page. offset_param : str Query parameter name for the offset. limit_param : str Query parameter name for the limit/page size. records_key : str JSON key containing the list of records.

Source code in gigaspatial/core/http/pagination.py
class OffsetPagination(BasePaginationStrategy):
    """
    Standard offset/limit pagination.

    Parameters
    ----------
    page_size : int
        Number of records per page.
    offset_param : str
        Query parameter name for the offset.
    limit_param : str
        Query parameter name for the limit/page size.
    records_key : str
        JSON key containing the list of records.
    """

    def __init__(
        self,
        page_size: int = 100,
        offset_param: str = "offset",
        limit_param: str = "limit",
        records_key: str = "results",
    ) -> None:
        self.page_size = page_size
        self.offset_param = offset_param
        self.limit_param = limit_param
        self.records_key = records_key

    def next_request(
        self,
        response: httpx.Response,
        current_params: dict[str, Any],
    ) -> Optional[dict[str, Any]]:
        records = self.extract_records(response)
        if len(records) < self.page_size:
            return None  # last page
        next_offset = current_params.get(self.offset_param, 0) + self.page_size
        return {**current_params, self.offset_param: next_offset, self.limit_param: self.page_size}

    def extract_records(self, response: httpx.Response) -> list[dict]:
        return response.json().get(self.records_key, [])
PageNumberPagination

Bases: BasePaginationStrategy

Page-number pagination (page=1, page=2, ...).

Parameters

page_size : int Number of records per page. page_param : str Query parameter name for the page number. size_param : str Query parameter name for the page size. records_key : str JSON key containing the list of records.

Source code in gigaspatial/core/http/pagination.py
class PageNumberPagination(BasePaginationStrategy):
    """
    Page-number pagination (page=1, page=2, ...).

    Parameters
    ----------
    page_size : int
        Number of records per page.
    page_param : str
        Query parameter name for the page number.
    size_param : str
        Query parameter name for the page size.
    records_key : str
        JSON key containing the list of records.
    """

    def __init__(
        self,
        page_size: int = 1000,
        page_param: str = "page",
        size_param: str = "size",
        records_key: str = "data",
    ) -> None:
        self.page_size = page_size
        self.page_param = page_param
        self.size_param = size_param
        self.records_key = records_key

    def next_request(
        self,
        response: httpx.Response,
        current_params: dict[str, Any],
    ) -> Optional[dict[str, Any]]:
        records = self.extract_records(response)
        if len(records) < self.page_size:
            return None  # partial page — last page reached
        next_page = current_params.get(self.page_param, 1) + 1
        return {**current_params, self.page_param: next_page}

    def extract_records(self, response: httpx.Response) -> list[dict]:
        return response.json().get(self.records_key, [])

io

DataStore

Bases: ABC

Abstract base class defining the interface for data store implementations. This class serves as a parent for both local and cloud-based storage solutions.

Source code in gigaspatial/core/io/data_store.py
class DataStore(ABC):
    """
    Abstract base class defining the interface for data store implementations.
    This class serves as a parent for both local and cloud-based storage solutions.
    """

    @abstractmethod
    def read_file(self, path: str) -> Any:
        """
        Read contents of a file from the data store.

        Args:
            path: Path to the file to read

        Returns:
            Contents of the file

        Raises:
            IOError: If file cannot be read
        """
        pass

    @abstractmethod
    def write_file(self, path: str, data: Any) -> None:
        """
        Write data to a file in the data store.

        Args:
            path: Path where to write the file
            data: Data to write to the file

        Raises:
            IOError: If file cannot be written
        """
        pass

    @abstractmethod
    def file_exists(self, path: str) -> bool:
        """
        Check if a file exists in the data store.

        Args:
            path: Path to check

        Returns:
            True if file exists, False otherwise
        """
        pass

    @abstractmethod
    def list_files(self, path: str) -> List[str]:
        """
        List all files in a directory.

        Args:
            path: Directory path to list

        Returns:
            List of file paths in the directory
        """
        pass

    @abstractmethod
    def walk(self, top: str) -> Generator:
        """
        Walk through directory tree, similar to os.walk().

        Args:
            top: Starting directory for the walk

        Returns:
            Generator yielding tuples of (dirpath, dirnames, filenames)
        """
        pass

    @abstractmethod
    def open(self, file: str, mode: str = "r") -> Union[str, bytes]:
        """
        Context manager for file operations.

        Args:
            file: Path to the file
            mode: File mode ('r', 'w', 'rb', 'wb')

        Yields:
            File-like object

        Raises:
            IOError: If file cannot be opened
        """
        pass

    @abstractmethod
    def is_file(self, path: str) -> bool:
        """
        Check if path points to a file.

        Args:
            path: Path to check

        Returns:
            True if path is a file, False otherwise
        """
        pass

    @abstractmethod
    def is_dir(self, path: str) -> bool:
        """
        Check if path points to a directory.

        Args:
            path: Path to check

        Returns:
            True if path is a directory, False otherwise
        """
        pass

    @abstractmethod
    def remove(self, path: str) -> None:
        """
        Remove a file.

        Args:
            path: Path to the file to remove

        Raises:
            IOError: If file cannot be removed
        """
        pass

    @abstractmethod
    def rmdir(self, dir: str) -> None:
        """
        Remove a directory and all its contents.

        Args:
            dir: Path to the directory to remove

        Raises:
            IOError: If directory cannot be removed
        """
        pass
file_exists(path) abstractmethod

Check if a file exists in the data store.

Parameters:

Name Type Description Default
path str

Path to check

required

Returns:

Type Description
bool

True if file exists, False otherwise

Source code in gigaspatial/core/io/data_store.py
@abstractmethod
def file_exists(self, path: str) -> bool:
    """
    Check if a file exists in the data store.

    Args:
        path: Path to check

    Returns:
        True if file exists, False otherwise
    """
    pass
is_dir(path) abstractmethod

Check if path points to a directory.

Parameters:

Name Type Description Default
path str

Path to check

required

Returns:

Type Description
bool

True if path is a directory, False otherwise

Source code in gigaspatial/core/io/data_store.py
@abstractmethod
def is_dir(self, path: str) -> bool:
    """
    Check if path points to a directory.

    Args:
        path: Path to check

    Returns:
        True if path is a directory, False otherwise
    """
    pass
is_file(path) abstractmethod

Check if path points to a file.

Parameters:

Name Type Description Default
path str

Path to check

required

Returns:

Type Description
bool

True if path is a file, False otherwise

Source code in gigaspatial/core/io/data_store.py
@abstractmethod
def is_file(self, path: str) -> bool:
    """
    Check if path points to a file.

    Args:
        path: Path to check

    Returns:
        True if path is a file, False otherwise
    """
    pass
list_files(path) abstractmethod

List all files in a directory.

Parameters:

Name Type Description Default
path str

Directory path to list

required

Returns:

Type Description
List[str]

List of file paths in the directory

Source code in gigaspatial/core/io/data_store.py
@abstractmethod
def list_files(self, path: str) -> List[str]:
    """
    List all files in a directory.

    Args:
        path: Directory path to list

    Returns:
        List of file paths in the directory
    """
    pass
open(file, mode='r') abstractmethod

Context manager for file operations.

Parameters:

Name Type Description Default
file str

Path to the file

required
mode str

File mode ('r', 'w', 'rb', 'wb')

'r'

Yields:

Type Description
Union[str, bytes]

File-like object

Raises:

Type Description
IOError

If file cannot be opened

Source code in gigaspatial/core/io/data_store.py
@abstractmethod
def open(self, file: str, mode: str = "r") -> Union[str, bytes]:
    """
    Context manager for file operations.

    Args:
        file: Path to the file
        mode: File mode ('r', 'w', 'rb', 'wb')

    Yields:
        File-like object

    Raises:
        IOError: If file cannot be opened
    """
    pass
read_file(path) abstractmethod

Read contents of a file from the data store.

Parameters:

Name Type Description Default
path str

Path to the file to read

required

Returns:

Type Description
Any

Contents of the file

Raises:

Type Description
IOError

If file cannot be read

Source code in gigaspatial/core/io/data_store.py
@abstractmethod
def read_file(self, path: str) -> Any:
    """
    Read contents of a file from the data store.

    Args:
        path: Path to the file to read

    Returns:
        Contents of the file

    Raises:
        IOError: If file cannot be read
    """
    pass
remove(path) abstractmethod

Remove a file.

Parameters:

Name Type Description Default
path str

Path to the file to remove

required

Raises:

Type Description
IOError

If file cannot be removed

Source code in gigaspatial/core/io/data_store.py
@abstractmethod
def remove(self, path: str) -> None:
    """
    Remove a file.

    Args:
        path: Path to the file to remove

    Raises:
        IOError: If file cannot be removed
    """
    pass
rmdir(dir) abstractmethod

Remove a directory and all its contents.

Parameters:

Name Type Description Default
dir str

Path to the directory to remove

required

Raises:

Type Description
IOError

If directory cannot be removed

Source code in gigaspatial/core/io/data_store.py
@abstractmethod
def rmdir(self, dir: str) -> None:
    """
    Remove a directory and all its contents.

    Args:
        dir: Path to the directory to remove

    Raises:
        IOError: If directory cannot be removed
    """
    pass
walk(top) abstractmethod

Walk through directory tree, similar to os.walk().

Parameters:

Name Type Description Default
top str

Starting directory for the walk

required

Returns:

Type Description
Generator

Generator yielding tuples of (dirpath, dirnames, filenames)

Source code in gigaspatial/core/io/data_store.py
@abstractmethod
def walk(self, top: str) -> Generator:
    """
    Walk through directory tree, similar to os.walk().

    Args:
        top: Starting directory for the walk

    Returns:
        Generator yielding tuples of (dirpath, dirnames, filenames)
    """
    pass
write_file(path, data) abstractmethod

Write data to a file in the data store.

Parameters:

Name Type Description Default
path str

Path where to write the file

required
data Any

Data to write to the file

required

Raises:

Type Description
IOError

If file cannot be written

Source code in gigaspatial/core/io/data_store.py
@abstractmethod
def write_file(self, path: str, data: Any) -> None:
    """
    Write data to a file in the data store.

    Args:
        path: Path where to write the file
        data: Data to write to the file

    Raises:
        IOError: If file cannot be written
    """
    pass

read_dataset(path, data_store=None, compression=None, validate_crs=True, **kwargs)

Read data from various file formats stored in local or cloud-based storage.

Supports plain and compressed formats. For compound extensions such as .csv.gz or .geojson.gz, the inner format is detected automatically.

Parameters:

Name Type Description Default
path str

Path to the file in data storage.

required
data_store DataStore

DataStore instance for accessing storage. If None, local storage is used.

None
compression str

Explicit compression type ("gzip", "bz2", "xz"). If None, inferred from the file extension.

None
validate_crs bool

If True, emit a UserWarning when a GeoDataFrame has no CRS.

True
**kwargs

Additional arguments forwarded to the underlying reader function (e.g., sep, encoding, driver).

{}

Returns:

Type Description
Union[DataFrame, GeoDataFrame]

The data read from the file as a DataFrame or GeoDataFrame.

Raises:

Type Description
FileNotFoundError

If the file does not exist in the storage backend.

ValueError

If the file type is unsupported or the file cannot be parsed.

RuntimeError

For unexpected errors during the read process.

Source code in gigaspatial/core/io/readers.py
def read_dataset(
    path: str,
    data_store: DataStore = None,
    compression: str = None,
    validate_crs: bool = True,
    **kwargs,
) -> Union[pd.DataFrame, gpd.GeoDataFrame]:
    """
    Read data from various file formats stored in local or cloud-based storage.

    Supports plain and compressed formats. For compound extensions such as
    ``.csv.gz`` or ``.geojson.gz``, the inner format is detected automatically.

    Args:
        path: Path to the file in data storage.
        data_store: DataStore instance for accessing storage. If None, local storage is used.
        compression: Explicit compression type ("gzip", "bz2", "xz").
            If None, inferred from the file extension.
        validate_crs: If True, emit a UserWarning when a GeoDataFrame has no CRS.
        **kwargs: Additional arguments forwarded to the underlying reader function
            (e.g., `sep`, `encoding`, `driver`).

    Returns:
        The data read from the file as a DataFrame or GeoDataFrame.

    Raises:
        FileNotFoundError: If the file does not exist in the storage backend.
        ValueError: If the file type is unsupported or the file cannot be parsed.
        RuntimeError: For unexpected errors during the read process.
    """
    try:
        # ------------------------------------------------------------------ #
        # 1. Store check
        # ------------------------------------------------------------------ #
        data_store = data_store or LocalDataStore()

        # ------------------------------------------------------------------ #
        # 2. Existence check
        # ------------------------------------------------------------------ #
        if not data_store.file_exists(path):
            storage_name = _storage_display_name(data_store)
            raise FileNotFoundError(f"File '{path}' not found in {storage_name}.")

        path_obj = Path(path)
        suffixes = path_obj.suffixes
        file_extension = suffixes[-1].lower() if suffixes else ""

        # ------------------------------------------------------------------ #
        # 2. Compressed file handling (.gz / .bz2 / .xz)
        # FIX: .zip is no longer in COMPRESSION_FORMATS; it is handled as a
        #      geo container below. This removes the double-routing bug.
        # ------------------------------------------------------------------ #
        inferred_compression = COMPRESSION_FORMATS.get(file_extension)
        active_compression = compression or inferred_compression

        if active_compression and file_extension in COMPRESSION_FORMATS:
            if len(suffixes) > 1:
                inner_ext = suffixes[-2].lower()

                if inner_ext == ".tar":
                    raise ValueError(
                        "Tar archives (.tar.gz) are not directly supported."
                    )

                # Compressed tabular file (e.g. .csv.gz, .json.gz)
                if inner_ext in PANDAS_READERS:
                    try:
                        with data_store.open(path, "rb") as f:
                            return PANDAS_READERS[inner_ext](
                                f, compression=active_compression, **kwargs
                            )
                    except Exception as e:
                        raise ValueError(
                            f"Error reading compressed tabular file '{path}': {e}"
                        ) from e

                # Compressed geo file (e.g. .geojson.gz, .fgb.gz)
                if inner_ext in GEO_READERS and active_compression == "gzip":
                    try:
                        with data_store.open(path, "rb") as f:
                            decompressed = gzip.decompress(f.read())
                        result = GEO_READERS[inner_ext](
                            io.BytesIO(decompressed), **kwargs
                        )
                        return _maybe_warn_crs(result, path, validate_crs)
                    except Exception as e:
                        raise ValueError(
                            f"Error reading compressed geo file '{path}': {e}"
                        ) from e

                if inner_ext in GEO_READERS and active_compression != "gzip":
                    raise ValueError(
                        f"Compression '{active_compression}' is not supported "
                        f"for geo formats. Only gzip is supported."
                    )

            else:
                # Bare .gz with no inner extension hint - try JSON lines then CSV
                # FIX: delegates to read_gzipped_json_or_csv instead of silently
                #      assuming CSV, which could return garbage for JSON payloads.
                result = read_gzipped_json_or_csv(path, data_store)
                if result is None:
                    raise ValueError(
                        f"Could not parse '{path}' as JSON lines or CSV. "
                        f"Use a compound extension (e.g. '.json.gz') to specify "
                        f"the inner format explicitly."
                    )
                return result

        # ------------------------------------------------------------------ #
        # 3. ZIP container - always geo (shapefile zip or zipped vector)
        # FIX: .zip is no longer ambiguously caught by COMPRESSION_FORMATS.
        # ------------------------------------------------------------------ #
        if file_extension == ".zip":
            try:
                with data_store.open(path, "rb") as f:
                    result = gpd.read_file(f, **kwargs)
                return _maybe_warn_crs(result, path, validate_crs)
            except Exception as e:
                raise ValueError(
                    f"Error reading ZIP file '{path}' as geospatial data: {e}"
                ) from e

        # ------------------------------------------------------------------ #
        # 4. Shapefile - requires sidecar files
        # FIX: inner `suffixes` variable renamed to `sidecar_extensions` to
        #      avoid shadowing the outer `suffixes = path_obj.suffixes`.
        # ------------------------------------------------------------------ #
        if file_extension == ".shp":
            base_path = Path(path)
            with tempfile.TemporaryDirectory() as tmp_dir:
                local_shp_path = None
                for ext in SHAPEFILE_SIDECAR_EXTENSIONS:
                    part_path = str(base_path.with_suffix(ext))
                    if data_store.file_exists(part_path):
                        local_part = os.path.join(
                            tmp_dir, base_path.with_suffix(ext).name
                        )
                        content = data_store.read_file(part_path)
                        with open(local_part, "wb") as lf:
                            lf.write(content)
                        if ext == ".shp":
                            local_shp_path = local_part

                if local_shp_path is None:
                    raise FileNotFoundError(
                        f"Main .shp file could not be retrieved for path: {path}"
                    )
                result = gpd.read_file(local_shp_path, **kwargs)
            return _maybe_warn_crs(result, path, validate_crs)

        # ------------------------------------------------------------------ #
        # 5. Tabular formats (CSV, Excel)
        # ------------------------------------------------------------------ #
        if file_extension in PANDAS_READERS:
            mode = "rb" if file_extension in BINARY_FORMATS else "r"
            try:
                with data_store.open(path, mode) as f:
                    return PANDAS_READERS[file_extension](f, **kwargs)
            except Exception as e:
                raise ValueError(f"Error reading '{path}' with pandas: {e}") from e

        # ------------------------------------------------------------------ #
        # 6. Geospatial formats
        # ------------------------------------------------------------------ #
        if file_extension in GEO_READERS:
            try:
                with data_store.open(path, "rb") as f:
                    result = GEO_READERS[file_extension](f, **kwargs)
                return _maybe_warn_crs(result, path, validate_crs)
            except Exception as e:
                # FIX: For parquet, fall back to pandas if geopandas fails
                # (non-spatial parquet is common in GigaSpatial pipelines).
                if file_extension == ".parquet":
                    try:
                        with data_store.open(path, "rb") as f:
                            return pd.read_parquet(f, **kwargs)
                    except Exception as e2:
                        raise ValueError(
                            f"Failed to read parquet '{path}' with geopandas "
                            f"({e}) and pandas ({e2})."
                        ) from e2
                raise ValueError(f"Error reading '{path}' with geopandas: {e}") from e

        # ------------------------------------------------------------------ #
        # 7. Unsupported format
        # FIX: added missing newline between formats and compressions in message
        # ------------------------------------------------------------------ #
        supported_formats = sorted(set(PANDAS_READERS) | set(GEO_READERS))
        supported_compressions = sorted(COMPRESSION_FORMATS)
        raise ValueError(
            f"Unsupported file type: '{file_extension}'\n"
            f"Supported formats:      {', '.join(supported_formats)}\n"
            f"Supported compressions: {', '.join(supported_compressions)}"
        )

    except (FileNotFoundError, ValueError):
        raise
    except Exception as e:
        raise RuntimeError(f"Unexpected error reading dataset '{path}': {e}") from e

read_datasets(paths, data_store=None, **kwargs)

Read multiple datasets from data storage at once.

Parameters:

Name Type Description Default
paths

List of file paths to read.

required
data_store DataStore

DataStore instance. If None, local storage is used.

None
**kwargs

Additional arguments passed to read_dataset for each file.

{}

Returns:

Type Description

Dictionary mapping paths to their corresponding DataFrames/GeoDataFrames.

Source code in gigaspatial/core/io/readers.py
def read_datasets(paths, data_store: DataStore = None, **kwargs):
    """
    Read multiple datasets from data storage at once.

    Args:
        paths: List of file paths to read.
        data_store: DataStore instance. If None, local storage is used.
        **kwargs: Additional arguments passed to `read_dataset` for each file.

    Returns:
        Dictionary mapping paths to their corresponding DataFrames/GeoDataFrames.
    """
    results = {}
    errors = {}

    data_store = data_store or LocalDataStore()

    if data_store is None:
        data_store = LocalDataStore()

    for path in paths:
        try:
            results[path] = read_dataset(path, data_store, **kwargs)
        except Exception as e:
            errors[path] = str(e)

    if errors:
        error_msg = "\n".join(f"- {path}: {error}" for path, error in errors.items())
        raise ValueError(f"Errors reading datasets:\n{error_msg}")

    return results

read_gzipped_json_or_csv(file_path, data_store=None)

Read a gzipped file, attempting JSON (lines=True) first, then CSV.

Parameters:

Name Type Description Default
file_path str

Path to the .gz file in the DataStore.

required
data_store DataStore

DataStore instance. If None, local storage is used.

None

Returns:

Type Description
Union[DataFrame, None]

Parsed DataFrame, or None if parsing fails for both formats.

Source code in gigaspatial/core/io/readers.py
def read_gzipped_json_or_csv(
    file_path: str, data_store: DataStore = None
) -> Union[pd.DataFrame, None]:
    """
    Read a gzipped file, attempting JSON (lines=True) first, then CSV.

    Args:
        file_path: Path to the .gz file in the DataStore.
        data_store: DataStore instance. If None, local storage is used.

    Returns:
        Parsed DataFrame, or None if parsing fails for both formats.
    """
    data_store = data_store or LocalDataStore()

    with data_store.open(file_path, "rb") as f:
        text = gzip.GzipFile(fileobj=f).read().decode("utf-8")

    try:
        return pd.read_json(io.StringIO(text), lines=True)
    except (json.JSONDecodeError, ValueError):
        pass

    try:
        return pd.read_csv(io.StringIO(text))
    except pd.errors.ParserError:
        logger.error("Could not parse '%s' as JSON lines or CSV.", file_path)
        return None

read_json(path, data_store=None)

Read a JSON file from a DataStore.

Parameters:

Name Type Description Default
path str

Path to the JSON file.

required
data_store DataStore

DataStore instance for accessing storage. If None, local storage is used.

None

Returns:

Type Description
dict

Parsed JSON content as a dictionary.

Source code in gigaspatial/core/io/readers.py
def read_json(path: str, data_store: DataStore = None) -> dict:
    """
    Read a JSON file from a DataStore.

    Args:
        path: Path to the JSON file.
        data_store: DataStore instance for accessing storage. If None, local storage is used.

    Returns:
        Parsed JSON content as a dictionary.
    """
    data_store = data_store or LocalDataStore()

    with data_store.open(path, "r") as f:
        return json.load(f)

read_kmz(file_obj, **kwargs)

Read a KMZ file and return a GeoDataFrame.

Parameters:

Name Type Description Default
file_obj

A file-like object pointing to a KMZ archive.

required
**kwargs

Additional keyword arguments passed to geopandas.read_file.

{}

Returns:

Type Description
GeoDataFrame

The geospatial data extracted from the KMZ.

Raises:

Type Description
ValueError

If no KML file is found in the archive or if content is empty.

RuntimeError

For unexpected errors during KMZ processing.

Source code in gigaspatial/core/io/readers.py
def read_kmz(file_obj, **kwargs) -> gpd.GeoDataFrame:
    """
    Read a KMZ file and return a GeoDataFrame.

    Args:
        file_obj: A file-like object pointing to a KMZ archive.
        **kwargs: Additional keyword arguments passed to `geopandas.read_file`.

    Returns:
        The geospatial data extracted from the KMZ.

    Raises:
        ValueError: If no KML file is found in the archive or if content is empty.
        RuntimeError: For unexpected errors during KMZ processing.
    """
    try:
        with zipfile.ZipFile(file_obj) as kmz:
            kml_filename = next(
                (name for name in kmz.namelist() if name.endswith(".kml")), None
            )
            if kml_filename is None:
                raise ValueError("No KML file found in the KMZ archive.")

            kml_content = io.BytesIO(kmz.read(kml_filename))
            gdf = gpd.read_file(kml_content, **kwargs)

            if gdf.empty:
                raise ValueError(
                    "The KML file is empty or does not contain valid geospatial data."
                )
        return gdf

    except zipfile.BadZipFile:
        raise ValueError("The provided file is not a valid KMZ file.")
    except ValueError:
        raise
    except Exception as e:
        raise RuntimeError(f"An error occurred reading KMZ: {e}") from e

write_dataset(data, data_store=None, path=None, **kwargs)

Write DataFrame, GeoDataFrame, or a generic object to various file formats.

Supported formats include .csv, .xlsx, .json, .parquet for DataFrames, and .geojson, .gpkg, .parquet for GeoDataFrames.

Parameters:

Name Type Description Default
data

The data to write. Can be pd.DataFrame, gpd.GeoDataFrame, or a JSON-serializable object.

required
data_store DataStore

Instance of DataStore for accessing data storage. If None, local storage is used.

None
path

Path where the file will be written.

None
**kwargs

Additional arguments passed to the specific writer function (e.g., index=False).

{}

Raises:

Type Description
ValueError

If the file type is unsupported or if there's an error writing the file.

TypeError

If input data type is incompatible with the file extension.

RuntimeError

For unexpected errors during the write process.

Source code in gigaspatial/core/io/writers.py
def write_dataset(data, data_store: DataStore = None, path=None, **kwargs):
    """
    Write DataFrame, GeoDataFrame, or a generic object to various file formats.

    Supported formats include .csv, .xlsx, .json, .parquet for DataFrames,
    and .geojson, .gpkg, .parquet for GeoDataFrames.

    Args:
        data: The data to write. Can be pd.DataFrame, gpd.GeoDataFrame, or a JSON-serializable object.
        data_store: Instance of DataStore for accessing data storage. If None, local storage is used.
        path: Path where the file will be written.
        **kwargs: Additional arguments passed to the specific writer function (e.g., index=False).

    Raises:
        ValueError: If the file type is unsupported or if there's an error writing the file.
        TypeError: If input data type is incompatible with the file extension.
        RuntimeError: For unexpected errors during the write process.
    """
    data_store = data_store or LocalDataStore()

    # Define supported file formats and their writers
    BINARY_FORMATS = {".shp", ".zip", ".parquet", ".gpkg", ".xlsx", ".xls"}

    PANDAS_WRITERS = {
        ".csv": lambda df, buf, **kw: df.to_csv(buf, **kw),
        ".xlsx": lambda df, buf, **kw: df.to_excel(buf, engine="openpyxl", **kw),
        ".json": lambda df, buf, **kw: df.to_json(buf, **kw),
        ".parquet": lambda df, buf, **kw: df.to_parquet(buf, **kw),
    }

    GEO_WRITERS = {
        ".geojson": lambda gdf, buf, **kw: gdf.to_file(buf, driver="GeoJSON", **kw),
        ".gpkg": lambda gdf, buf, **kw: gdf.to_file(buf, driver="GPKG", **kw),
        ".parquet": lambda gdf, buf, **kw: gdf.to_parquet(buf, **kw),
    }

    try:
        # Get file suffix and ensure it's lowercase
        suffix = Path(path).suffix.lower()

        # 1. Handle generic JSON data
        is_dataframe_like = isinstance(data, (pd.DataFrame, gpd.GeoDataFrame))
        if not is_dataframe_like:
            if suffix == ".json":
                try:
                    # Pass generic data directly to the write_json function
                    write_json(data, data_store, path, **kwargs)
                    return  # Successfully wrote JSON, so exit
                except Exception as e:
                    raise ValueError(f"Error writing generic JSON data: {str(e)}")
            else:
                # Raise an error if it's not a DataFrame/GeoDataFrame and not a .json file
                raise TypeError(
                    "Input data must be a pandas DataFrame or GeoDataFrame, "
                    "or a generic object destined for a '.json' file."
                )

        # 2. Handle DataFrame/GeoDataFrame
        # Determine if we need binary mode based on file type
        mode = "wb" if suffix in BINARY_FORMATS else "w"

        # Handle different data types and formats
        if isinstance(data, gpd.GeoDataFrame):
            if suffix not in GEO_WRITERS:
                supported_formats = sorted(GEO_WRITERS.keys())
                raise ValueError(
                    f"Unsupported file type for GeoDataFrame: {suffix}\n"
                    f"Supported formats: {', '.join(supported_formats)}"
                )

            try:
                # Write to BytesIO buffer first
                buffer = io.BytesIO()
                GEO_WRITERS[suffix](data, buffer, **kwargs)
                buffer.seek(0)

                # Then write buffer contents to data_store
                with data_store.open(path, "wb") as f:
                    f.write(buffer.getvalue())

                # with data_store.open(path, "wb") as f:
                #    GEO_WRITERS[suffix](data, f, **kwargs)
            except Exception as e:
                raise ValueError(f"Error writing GeoDataFrame: {str(e)}")

        else:  # pandas DataFrame
            if suffix not in PANDAS_WRITERS:
                supported_formats = sorted(PANDAS_WRITERS.keys())
                raise ValueError(
                    f"Unsupported file type for DataFrame: {suffix}\n"
                    f"Supported formats: {', '.join(supported_formats)}"
                )

            try:
                with data_store.open(path, mode) as f:
                    PANDAS_WRITERS[suffix](data, f, **kwargs)
            except Exception as e:
                raise ValueError(f"Error writing DataFrame: {str(e)}")

    except Exception as e:
        if isinstance(e, (TypeError, ValueError)):
            raise
        raise RuntimeError(f"Unexpected error writing dataset: {str(e)}")

write_datasets(data_dict, data_store=None, **kwargs)

Write multiple datasets to data storage at once.

Parameters:

Name Type Description Default
data_dict

Dictionary mapping paths (str) to data objects.

required
data_store DataStore

DataStore instance. If None, local storage is used.

None
**kwargs

Additional arguments passed to write_dataset for each item.

{}

Raises:

Type Description
ValueError

If one or more datasets fail to write, containing details of all errors.

Source code in gigaspatial/core/io/writers.py
def write_datasets(data_dict, data_store: DataStore = None, **kwargs):
    """
    Write multiple datasets to data storage at once.

    Args:
        data_dict: Dictionary mapping paths (str) to data objects.
        data_store: DataStore instance. If None, local storage is used.
        **kwargs: Additional arguments passed to write_dataset for each item.

    Raises:
        ValueError: If one or more datasets fail to write, containing details of all errors.
    """
    errors = {}
    data_store = data_store or LocalDataStore()

    for path, data in data_dict.items():
        try:
            write_dataset(data, data_store, path, **kwargs)
        except Exception as e:
            errors[path] = str(e)

    if errors:
        error_msg = "\n".join(f"- {path}: {error}" for path, error in errors.items())
        raise ValueError(f"Errors writing datasets:\n{error_msg}")

write_json(data, data_store=None, path=None, **kwargs)

Write data to a JSON file in the data store.

Parameters:

Name Type Description Default
data

Object to serialize to JSON.

required
data_store DataStore

DataStore instance to use for writing. If None, local storage is used.

None
path

Destination path in the data store.

None
**kwargs

Additional arguments passed to json.dump.

{}
Source code in gigaspatial/core/io/writers.py
def write_json(data, data_store: DataStore = None, path=None, **kwargs):
    """
    Write data to a JSON file in the data store.

    Args:
        data: Object to serialize to JSON.
        data_store: DataStore instance to use for writing. If None, local storage is used.
        path: Destination path in the data store.
        **kwargs: Additional arguments passed to json.dump.
    """
    data_store = data_store or LocalDataStore()
    with data_store.open(path, "w") as f:
        json.dump(data, f, **kwargs)

adls_data_store

Module for Azure Data Lake Storage (ADLS) DataStore implementation. Provides a consistent interface for interacting with blobs in Azure storage.

ADLSDataStore

Bases: DataStore

An implementation of DataStore for Azure Data Lake Storage.

Source code in gigaspatial/core/io/adls_data_store.py
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
class ADLSDataStore(DataStore):
    """
    An implementation of DataStore for Azure Data Lake Storage.
    """

    def __init__(
        self,
        container: str = config.ADLS_CONTAINER_NAME,
        connection_string: str = config.ADLS_CONNECTION_STRING,
        account_url: str = config.ADLS_ACCOUNT_URL,
        sas_token: str = config.ADLS_SAS_TOKEN,
    ):
        """
        Initialize the ADLS data store.

        Args:
            container: The name of the container in ADLS to interact with.
            connection_string: Azure storage connection string.
            account_url: Azure storage account URL.
            sas_token: SAS token for authentication if connection_string is not provided.

        Raises:
            ImportError: If 'azure-storage-blob' is not installed.
            ValueError: If authentication credentials are missing.
        """
        if not _HAS_AZURE:
            raise ImportError(
                "ADLSDataStore requires 'azure-storage-blob'. "
                "Install it with: pip install 'giga-spatial[azure]'"
            )
        if connection_string:
            self.blob_service_client = BlobServiceClient.from_connection_string(
                connection_string
            )
        elif account_url and sas_token:
            self.blob_service_client = BlobServiceClient(
                account_url=account_url, credential=sas_token
            )
        else:
            raise ValueError(
                "Either connection_string or account_url and sas_token must be provided."
            )

        self.container_client = self.blob_service_client.get_container_client(
            container=container
        )
        self.container = container

    def _to_blob_key(self, path: Pathish, *, ensure_dir: bool = False) -> str:
        """
        Convert an input path (str or PathLike) to a normalized ADLS blob key.

        Rules:
        - Accepts str, pathlib.Path, PurePath, etc.
        - Converts Windows backslashes to '/' via PurePosixPath.
        - Strips any leading '/'.
        - Optionally enforces a trailing '/' for directory prefixes.
        """
        # Convert any PathLike to str in OS-native form first
        raw = os.fspath(path)

        # Normalize to POSIX-style using pathlib semantics
        # This handles backslashes and redundant separators.
        posix = PurePosixPath(raw).as_posix()

        # Strip leading slash (Azure blob keys don’t need it)
        if posix.startswith("/"):
            posix = posix.lstrip("/")

        if ensure_dir and posix and not posix.endswith("/"):
            posix = posix + "/"

        return posix

    def read_file(
        self, path: Pathish, encoding: Optional[str] = None
    ) -> Union[str, bytes]:
        """
        Read file contents from blob storage.

        Args:
            path: Path to the file in blob storage.
            encoding: Optional string encoding (e.g., 'utf-8'). If None, returns bytes.

        Returns:
            File contents as a string if encoding is provided, otherwise bytes.

        Raises:
            IOError: If there's an error downloading the blob.
        """
        try:
            blob_key = self._to_blob_key(path)
            blob_client = self.container_client.get_blob_client(blob_key)
            blob_data = blob_client.download_blob().readall()

            # If no encoding specified, return raw bytes
            if encoding is None:
                return blob_data

            # If encoding is specified, decode the bytes
            return blob_data.decode(encoding)

        except Exception as e:
            raise IOError(f"Error reading file {path}: {e}")

    def write_file(self, path: Pathish, data) -> None:
        """
        Write data (string or bytes) to a blob.

        Args:
            path: Destination path in blob storage.
            data: Data to write (str or bytes).

        Raises:
            ValueError: If data type is not supported.
        """
        blob_key = self._to_blob_key(path)
        blob_client = self.blob_service_client.get_blob_client(
            container=self.container, blob=blob_key, snapshot=None
        )

        if isinstance(data, str):
            binary_data = data.encode()
        elif isinstance(data, bytes):
            binary_data = data
        else:
            raise ValueError(f'Unsupported data type. Only "bytes" or "string" accepted')

        blob_client.upload_blob(binary_data, overwrite=True)

    def upload_file(self, file_path: str, blob_path: Pathish):
        """Uploads a single file to Azure Blob Storage."""
        try:
            blob_key = self._to_blob_key(blob_path)
            blob_client = self.container_client.get_blob_client(blob_key)
            with open(file_path, "rb") as data:
                blob_client.upload_blob(data, overwrite=True)
            print(f"Uploaded {file_path} to {blob_path}")
        except Exception as e:
            print(f"Failed to upload {file_path}: {e}")

    def upload_directory(self, dir_path: str, blob_dir_path: Pathish):
        """Uploads all files from a directory to Azure Blob Storage."""
        blob_dir_key = self._to_blob_key(blob_dir_path, ensure_dir=True)
        for root, dirs, files in os.walk(dir_path):
            for file in files:
                local_file_path = os.path.join(root, file)
                relative_path = os.path.relpath(local_file_path, dir_path)
                # Construct blob path and normalize
                blob_file_path = PurePosixPath(blob_dir_key) / relative_path
                blob_file_key = self._to_blob_key(blob_file_path)

                self.upload_file(local_file_path, blob_file_key)

    def download_directory(self, blob_dir_path: Pathish, local_dir_path: str):
        """Downloads all files from a directory in Azure Blob Storage to a local directory."""
        try:
            blob_dir_key = self._to_blob_key(blob_dir_path, ensure_dir=True)

            # Ensure the local directory exists
            os.makedirs(local_dir_path, exist_ok=True)

            # List all files in the blob directory
            blob_items = self.container_client.list_blobs(name_starts_with=blob_dir_key)

            for blob_item in blob_items:
                # Get the relative path of the blob file
                relative_path = os.path.relpath(blob_item.name, blob_dir_key)
                # Construct the local file path
                local_file_path = os.path.join(local_dir_path, relative_path)
                # Create directories if needed
                os.makedirs(os.path.dirname(local_file_path), exist_ok=True)

                # Download the blob to the local file
                blob_client = self.container_client.get_blob_client(blob_item.name)
                with open(local_file_path, "wb") as file:
                    file.write(blob_client.download_blob().readall())

            print(f"Downloaded directory {blob_dir_key} to {local_dir_path}")
        except Exception as e:
            print(f"Failed to download directory {blob_dir_path}: {e}")

    def copy_directory(self, source_dir: Pathish, destination_dir: Pathish):
        """
        Copies all files from a source directory to a destination directory.

        Args:
            source_dir: The source directory path in blob storage.
            destination_dir: The destination directory path in blob storage.
        """
        try:
            # Ensure source directory path ends with a trailing slash
            source_dir = self._to_blob_key(source_dir, ensure_dir=True)
            destination_dir = self._to_blob_key(destination_dir, ensure_dir=True)

            # List all blobs in the source directory
            source_blobs = self.container_client.list_blobs(name_starts_with=source_dir)

            for blob in source_blobs:
                # Get the relative path of the blob
                relative_path = os.path.relpath(blob.name, source_dir)

                # Construct the new blob path
                new_blob_path = os.path.join(destination_dir, relative_path).replace(
                    "\\", "/"
                )

                # Use copy_file method to copy each file
                self.copy_file(blob.name, new_blob_path, overwrite=True)

            print(f"Copied directory from {source_dir} to {destination_dir}")
        except Exception as e:
            print(f"Failed to copy directory {source_dir}: {e}")

    def copy_file(
        self, source_path: Pathish, destination_path: Pathish, overwrite: bool = False
    ):
        """
        Copies a single file from source to destination.

        Args:
            source_path: The source file path in blob storage.
            destination_path: The destination file path in blob storage.
            overwrite: If True, overwrite the destination file if it already exists.

        Raises:
            FileNotFoundError: If source file is missing.
            FileExistsError: If destination exists and overwrite=False.
        """
        try:
            if not self.file_exists(source_path):
                raise FileNotFoundError(f"Source file not found: {source_path}")

            if self.file_exists(destination_path) and not overwrite:
                raise FileExistsError(
                    f"Destination file already exists and overwrite is False: {destination_path}"
                )

            source_path = self._to_blob_key(source_path)
            destination_path = self._to_blob_key(destination_path)

            # Create source and destination blob clients
            source_blob_client = self.container_client.get_blob_client(source_path)
            destination_blob_client = self.container_client.get_blob_client(
                destination_path
            )

            # Start the server-side copy operation
            destination_blob_client.start_copy_from_url(source_blob_client.url)

            print(f"Copied file from {source_path} to {destination_path}")
        except Exception as e:
            print(f"Failed to copy file {source_path}: {e}")
            raise

    def exists(self, path: Pathish) -> bool:
        """Checks if a blob exists at the prefix."""
        blob_key = self._to_blob_key(path)
        blob_client = self.blob_service_client.get_blob_client(
            container=self.container, blob=blob_key, snapshot=None
        )
        return blob_client.exists()

    def file_exists(self, path: Pathish) -> bool:
        """Checks if a blob is a file and exists."""
        return self.exists(path) and not self.is_dir(path)

    def file_size(self, path: Pathish) -> float:
        blob_key = self._to_blob_key(path)
        blob_client = self.blob_service_client.get_blob_client(
            container=self.container, blob=blob_key, snapshot=None
        )
        properties = blob_client.get_blob_properties()

        # The size is in bytes, convert it to kilobytes
        size_in_bytes = properties.size
        size_in_kb = size_in_bytes / 1024.0
        return size_in_kb

    def list_files(self, dir_path: Pathish) -> list:
        """
        List all files in a directory.

        For large directories (100K+ files), consider using
        list_files_iter() to avoid memory overhead.

        Performance improvements in this version:
        - Uses list_blob_names() internally (8-14x faster)
        - Lower memory usage (strings vs objects)

        Args:
            dir_path: Directory path

        Returns:
            List of file paths
        """
        prefix = self._to_blob_key(dir_path, ensure_dir=True)
        return list(self.container_client.list_blob_names(name_starts_with=prefix))

    def list_files_iter(
        self, dir_path: Pathish, results_per_page: Optional[int] = None
    ) -> Iterator[str]:
        """
        Iterate over files with lazy evaluation (memory efficient).

        Recommended for large directories. Returns generator that yields
        file paths one at a time.

        Args:
            dir_path: Directory path
            results_per_page: Number of results to request per page.

        Yields:
            File paths

        Example:
            for file in store.list_files_iter('data/'):
                if file.endswith('.png'):
                    process(file)
                    break  # Early exit possible
        """
        prefix = self._to_blob_key(dir_path, ensure_dir=True)
        return self.container_client.list_blob_names(
            name_starts_with=prefix, results_per_page=results_per_page
        )

    def has_files_with_extension(self, dir_path: Pathish, extension: str) -> bool:
        """
        Check if ANY file with given extension exists (early exit).

        Much faster than filtering list_files() result when you only
        need to know if at least one file exists.

        Args:
            dir_path: Directory path
            extension: File extension (e.g., '.png' or 'png')

        Returns:
            True if at least one file with extension exists

        Example:
            if store.has_files_with_extension('images/', '.png'):
                print("Folder contains PNG images")

        Performance:
            - Returns immediately on first match
            - Best case: <1s (if match in first page)
            - Worst case: ~30-60s (no matches, must check all)
        """
        if not extension.startswith("."):
            extension = "." + extension

        for blob_name in self.list_files_iter(dir_path):
            if blob_name.endswith(extension):
                return True  # Early exit

        return False

    def count_files(self, dir_path: Pathish) -> int:
        """
        Count total files in a directory (memory efficient).

        More memory-efficient than len(list_files()) for large directories.

        Args:
            dir_path: Directory path

        Returns:
            Number of files

        Example:
            count = store.count_files('data/')
            print(f"Total files: {count}")

        Performance:
            - Must iterate all files (no shortcuts)
            - Memory: ~2MB (one page at a time)
            - Time: ~30-60s for 1M files (Azure rate limit)
        """
        return sum(1 for _ in self.list_files_iter(dir_path))

    def count_files_with_extension(self, dir_path: Pathish, extension: str) -> int:
        """
        Count files with specific extension (memory efficient).

        Args:
            dir_path: Directory path
            extension: File extension (e.g., '.png' or 'png')

        Returns:
            Number of files with given extension

        Example:
            png_count = store.count_files_with_extension('images/', '.png')
            print(f"Found {png_count} PNG files")
        """
        if not extension.startswith("."):
            extension = "." + extension

        return sum(
            1
            for blob_name in self.list_files_iter(dir_path)
            if blob_name.endswith(extension)
        )

    def walk(self, top: Pathish):
        """
        Walk through directory tree yielding (dirpath, dirnames, filenames).

        Optimized to use list_files_iter() for better performance.
        """
        for blob_name in self.list_files_iter(top):
            dirpath, filename = os.path.split(blob_name)
            yield (dirpath, [], [filename])

    def list_directories(self, path: Pathish) -> list:
        """
        List only directory names using Azure's hierarchical listing.
        """
        prefix = self._to_blob_key(path, ensure_dir=True)
        directories = set()

        # Use walk_blobs with delimiter - Azure returns directories directly!
        blob_hierarchy = self.container_client.walk_blobs(
            name_starts_with=prefix, delimiter="/"
        )

        for item in blob_hierarchy:
            # Items with 'prefix' attribute are directories
            if hasattr(item, "prefix"):
                dir_path = item.prefix
                # Extract just the directory name
                relative_path = dir_path[len(prefix) :].rstrip("/")
                if relative_path:
                    directories.add(relative_path)

        return sorted(list(directories))

    @contextlib.contextmanager
    def open(self, path: Pathish, mode: str = "r"):
        """
        Context manager for file operations with enhanced mode support.

        :param path: File path in blob storage
        :param mode: File open mode (r, rb, w, wb)
        """
        blob_key = self._to_blob_key(path)

        if mode == "w":
            file = io.StringIO()
            yield file
            self.write_file(blob_key, file.getvalue())

        elif mode == "wb":
            file = io.BytesIO()
            yield file
            self.write_file(blob_key, file.getvalue())

        elif mode == "r":
            data = self.read_file(blob_key, encoding="UTF-8")
            file = io.StringIO(data)
            yield file

        elif mode == "rb":
            data = self.read_file(blob_key)
            file = io.BytesIO(data)
            yield file

    def get_file_metadata(self, path: Pathish) -> dict:
        """
        Retrieve comprehensive file metadata.

        Args:
            path: File path in blob storage.

        Returns:
            Dictionary containing metadata like 'size_bytes', 'last_modified', 'etag'.
        """
        blob_key = self._to_blob_key(path)
        blob_client = self.container_client.get_blob_client(blob_key)
        properties = blob_client.get_blob_properties()

        return {
            "name": blob_key,
            "size_bytes": properties.size,
            "content_type": properties.content_settings.content_type,
            "last_modified": properties.last_modified,
            "etag": properties.etag,
        }

    def is_file(self, path: Pathish) -> bool:
        """Checks if path is a file."""
        return self.file_exists(path)

    def is_dir(self, path: Pathish) -> bool:
        """Checks if path is a directory."""
        dir_key = self._to_blob_key(path, ensure_dir=True)

        iterator = self.list_files_iter(dir_key, results_per_page=2)
        try:
            first_blob = next(iterator)
        except StopIteration:
            return False

        try:
            next(iterator)
            return True
        except StopIteration:
            return first_blob != self._to_blob_key(path)

    def rmdir(self, dir: Pathish) -> None:
        """Removes a directory."""
        # Normalize directory path to ensure it targets all children
        dir_key = self._to_blob_key(dir, ensure_dir=True)

        # Azure Blob batch delete has a hard limit on number of sub-requests
        # per batch (currently 256). Delete in chunks to avoid
        # ExceedsMaxBatchRequestCount errors.
        blobs = list(self.list_files(dir_key))
        if not blobs:
            return

        BATCH_LIMIT = 256
        for start_idx in range(0, len(blobs), BATCH_LIMIT):
            batch = blobs[start_idx : start_idx + BATCH_LIMIT]
            self.container_client.delete_blobs(*batch)

    def mkdir(self, path: Pathish, exist_ok: bool = False) -> None:
        """
        Create a virtual directory in ADLS.

        Directories are conceptual in blob storage and represented by a placeholder blob.

        Args:
            path: Path of the directory to create.
            exist_ok: If False, raise an error if the directory already exists.

        Raises:
            FileExistsError: If directory exists and exist_ok=False.
        """
        dir_key = self._to_blob_key(path, ensure_dir=True)

        existing_blobs = list(self.list_files(dir_key))

        if existing_blobs and not exist_ok:
            raise FileExistsError(f"Directory {path} already exists")

        # Create a placeholder blob to represent the directory
        placeholder_blob_path = PurePosixPath(dir_key) / ".placeholder"
        placeholder_blob_key = self._to_blob_key(placeholder_blob_path)

        # Only create placeholder if it doesn't already exist
        if not self.file_exists(placeholder_blob_key):
            placeholder_content = (
                b"This is a placeholder blob to represent a directory."
            )
            blob_client = self.blob_service_client.get_blob_client(
                container=self.container, blob=placeholder_blob_key
            )
            blob_client.upload_blob(placeholder_content, overwrite=True)

    def remove(self, path: Pathish) -> None:
        """Removes a file."""
        blob_key = self._to_blob_key(path)
        blob_client = self.blob_service_client.get_blob_client(
            container=self.container, blob=blob_key, snapshot=None
        )
        if blob_client.exists():
            blob_client.delete_blob()

    def rename(
        self,
        source_path: Pathish,
        destination_path: Pathish,
        overwrite: bool = False,
        delete_source: bool = True,
        wait: bool = True,
        timeout_seconds: int = 300,
        poll_interval_seconds: int = 1,
    ) -> None:
        """
        Rename (move) a single file by copying to the new path and deleting the source.

        :param source_path: Existing blob path
        :param destination_path: Target blob path
        :param overwrite: Overwrite destination if it already exists
        :param delete_source: Delete original after successful copy
        :param wait: Wait for the copy operation to complete
        :param timeout_seconds: Max time to wait for copy to succeed
        :param poll_interval_seconds: Polling interval while waiting
        """
        source_key = self._to_blob_key(source_path)
        destination_key = self._to_blob_key(destination_path)

        if not self.file_exists(source_key):
            raise FileNotFoundError(f"Source file not found: {source_key}")

        if self.file_exists(destination_key) and not overwrite:
            raise FileExistsError(
                f"Destination already exists and overwrite is False: {destination_key}"
            )

        # Use copy_file method to copy the file
        self.copy_file(source_key, destination_key, overwrite=overwrite)

        if wait:
            # Wait for copy to complete if requested
            dest_client = self.container_client.get_blob_client(destination_key)
            deadline = time.time() + timeout_seconds
            while True:
                props = dest_client.get_blob_properties()
                status = getattr(props.copy, "status", None)
                if status == "success":
                    break
                if status in {"aborted", "failed"}:
                    raise IOError(
                        f"Copy failed with status {status} from {source_key} to {destination_key}"
                    )
                if time.time() > deadline:
                    raise TimeoutError(
                        f"Timed out waiting for copy to complete for {destination_key}"
                    )
                time.sleep(poll_interval_seconds)

        if delete_source:
            self.remove(source_key)

    def _normalize_path(self, path: str) -> str:
        """
        Normalize a path for Azure Blob Storage.

        DEPRECATED: Use _to_blob_key() instead for better type support.
        This method is kept for backward compatibility.

        Ensures the path:
        - Ends with '/' if it's a directory
        - Doesn't start with '/' (Azure doesn't use leading slashes)
        - Uses forward slashes

        Args:
            path: Path to normalize

        Returns:
            Normalized path

        Examples:
            _normalize_path('data') -> 'data/'
            _normalize_path('data/') -> 'data/'
            _normalize_path('/data/') -> 'data/'
            _normalize_path('data\\\\subdir') -> 'data/subdir/'
        """
        return self._to_blob_key(path, ensure_dir=True)
__init__(container=config.ADLS_CONTAINER_NAME, connection_string=config.ADLS_CONNECTION_STRING, account_url=config.ADLS_ACCOUNT_URL, sas_token=config.ADLS_SAS_TOKEN)

Initialize the ADLS data store.

Parameters:

Name Type Description Default
container str

The name of the container in ADLS to interact with.

ADLS_CONTAINER_NAME
connection_string str

Azure storage connection string.

ADLS_CONNECTION_STRING
account_url str

Azure storage account URL.

ADLS_ACCOUNT_URL
sas_token str

SAS token for authentication if connection_string is not provided.

ADLS_SAS_TOKEN

Raises:

Type Description
ImportError

If 'azure-storage-blob' is not installed.

ValueError

If authentication credentials are missing.

Source code in gigaspatial/core/io/adls_data_store.py
def __init__(
    self,
    container: str = config.ADLS_CONTAINER_NAME,
    connection_string: str = config.ADLS_CONNECTION_STRING,
    account_url: str = config.ADLS_ACCOUNT_URL,
    sas_token: str = config.ADLS_SAS_TOKEN,
):
    """
    Initialize the ADLS data store.

    Args:
        container: The name of the container in ADLS to interact with.
        connection_string: Azure storage connection string.
        account_url: Azure storage account URL.
        sas_token: SAS token for authentication if connection_string is not provided.

    Raises:
        ImportError: If 'azure-storage-blob' is not installed.
        ValueError: If authentication credentials are missing.
    """
    if not _HAS_AZURE:
        raise ImportError(
            "ADLSDataStore requires 'azure-storage-blob'. "
            "Install it with: pip install 'giga-spatial[azure]'"
        )
    if connection_string:
        self.blob_service_client = BlobServiceClient.from_connection_string(
            connection_string
        )
    elif account_url and sas_token:
        self.blob_service_client = BlobServiceClient(
            account_url=account_url, credential=sas_token
        )
    else:
        raise ValueError(
            "Either connection_string or account_url and sas_token must be provided."
        )

    self.container_client = self.blob_service_client.get_container_client(
        container=container
    )
    self.container = container
copy_directory(source_dir, destination_dir)

Copies all files from a source directory to a destination directory.

Parameters:

Name Type Description Default
source_dir Pathish

The source directory path in blob storage.

required
destination_dir Pathish

The destination directory path in blob storage.

required
Source code in gigaspatial/core/io/adls_data_store.py
def copy_directory(self, source_dir: Pathish, destination_dir: Pathish):
    """
    Copies all files from a source directory to a destination directory.

    Args:
        source_dir: The source directory path in blob storage.
        destination_dir: The destination directory path in blob storage.
    """
    try:
        # Ensure source directory path ends with a trailing slash
        source_dir = self._to_blob_key(source_dir, ensure_dir=True)
        destination_dir = self._to_blob_key(destination_dir, ensure_dir=True)

        # List all blobs in the source directory
        source_blobs = self.container_client.list_blobs(name_starts_with=source_dir)

        for blob in source_blobs:
            # Get the relative path of the blob
            relative_path = os.path.relpath(blob.name, source_dir)

            # Construct the new blob path
            new_blob_path = os.path.join(destination_dir, relative_path).replace(
                "\\", "/"
            )

            # Use copy_file method to copy each file
            self.copy_file(blob.name, new_blob_path, overwrite=True)

        print(f"Copied directory from {source_dir} to {destination_dir}")
    except Exception as e:
        print(f"Failed to copy directory {source_dir}: {e}")
copy_file(source_path, destination_path, overwrite=False)

Copies a single file from source to destination.

Parameters:

Name Type Description Default
source_path Pathish

The source file path in blob storage.

required
destination_path Pathish

The destination file path in blob storage.

required
overwrite bool

If True, overwrite the destination file if it already exists.

False

Raises:

Type Description
FileNotFoundError

If source file is missing.

FileExistsError

If destination exists and overwrite=False.

Source code in gigaspatial/core/io/adls_data_store.py
def copy_file(
    self, source_path: Pathish, destination_path: Pathish, overwrite: bool = False
):
    """
    Copies a single file from source to destination.

    Args:
        source_path: The source file path in blob storage.
        destination_path: The destination file path in blob storage.
        overwrite: If True, overwrite the destination file if it already exists.

    Raises:
        FileNotFoundError: If source file is missing.
        FileExistsError: If destination exists and overwrite=False.
    """
    try:
        if not self.file_exists(source_path):
            raise FileNotFoundError(f"Source file not found: {source_path}")

        if self.file_exists(destination_path) and not overwrite:
            raise FileExistsError(
                f"Destination file already exists and overwrite is False: {destination_path}"
            )

        source_path = self._to_blob_key(source_path)
        destination_path = self._to_blob_key(destination_path)

        # Create source and destination blob clients
        source_blob_client = self.container_client.get_blob_client(source_path)
        destination_blob_client = self.container_client.get_blob_client(
            destination_path
        )

        # Start the server-side copy operation
        destination_blob_client.start_copy_from_url(source_blob_client.url)

        print(f"Copied file from {source_path} to {destination_path}")
    except Exception as e:
        print(f"Failed to copy file {source_path}: {e}")
        raise
count_files(dir_path)

Count total files in a directory (memory efficient).

More memory-efficient than len(list_files()) for large directories.

Parameters:

Name Type Description Default
dir_path Pathish

Directory path

required

Returns:

Type Description
int

Number of files

Example

count = store.count_files('data/') print(f"Total files: {count}")

Performance
  • Must iterate all files (no shortcuts)
  • Memory: ~2MB (one page at a time)
  • Time: ~30-60s for 1M files (Azure rate limit)
Source code in gigaspatial/core/io/adls_data_store.py
def count_files(self, dir_path: Pathish) -> int:
    """
    Count total files in a directory (memory efficient).

    More memory-efficient than len(list_files()) for large directories.

    Args:
        dir_path: Directory path

    Returns:
        Number of files

    Example:
        count = store.count_files('data/')
        print(f"Total files: {count}")

    Performance:
        - Must iterate all files (no shortcuts)
        - Memory: ~2MB (one page at a time)
        - Time: ~30-60s for 1M files (Azure rate limit)
    """
    return sum(1 for _ in self.list_files_iter(dir_path))
count_files_with_extension(dir_path, extension)

Count files with specific extension (memory efficient).

Parameters:

Name Type Description Default
dir_path Pathish

Directory path

required
extension str

File extension (e.g., '.png' or 'png')

required

Returns:

Type Description
int

Number of files with given extension

Example

png_count = store.count_files_with_extension('images/', '.png') print(f"Found {png_count} PNG files")

Source code in gigaspatial/core/io/adls_data_store.py
def count_files_with_extension(self, dir_path: Pathish, extension: str) -> int:
    """
    Count files with specific extension (memory efficient).

    Args:
        dir_path: Directory path
        extension: File extension (e.g., '.png' or 'png')

    Returns:
        Number of files with given extension

    Example:
        png_count = store.count_files_with_extension('images/', '.png')
        print(f"Found {png_count} PNG files")
    """
    if not extension.startswith("."):
        extension = "." + extension

    return sum(
        1
        for blob_name in self.list_files_iter(dir_path)
        if blob_name.endswith(extension)
    )
download_directory(blob_dir_path, local_dir_path)

Downloads all files from a directory in Azure Blob Storage to a local directory.

Source code in gigaspatial/core/io/adls_data_store.py
def download_directory(self, blob_dir_path: Pathish, local_dir_path: str):
    """Downloads all files from a directory in Azure Blob Storage to a local directory."""
    try:
        blob_dir_key = self._to_blob_key(blob_dir_path, ensure_dir=True)

        # Ensure the local directory exists
        os.makedirs(local_dir_path, exist_ok=True)

        # List all files in the blob directory
        blob_items = self.container_client.list_blobs(name_starts_with=blob_dir_key)

        for blob_item in blob_items:
            # Get the relative path of the blob file
            relative_path = os.path.relpath(blob_item.name, blob_dir_key)
            # Construct the local file path
            local_file_path = os.path.join(local_dir_path, relative_path)
            # Create directories if needed
            os.makedirs(os.path.dirname(local_file_path), exist_ok=True)

            # Download the blob to the local file
            blob_client = self.container_client.get_blob_client(blob_item.name)
            with open(local_file_path, "wb") as file:
                file.write(blob_client.download_blob().readall())

        print(f"Downloaded directory {blob_dir_key} to {local_dir_path}")
    except Exception as e:
        print(f"Failed to download directory {blob_dir_path}: {e}")
exists(path)

Checks if a blob exists at the prefix.

Source code in gigaspatial/core/io/adls_data_store.py
def exists(self, path: Pathish) -> bool:
    """Checks if a blob exists at the prefix."""
    blob_key = self._to_blob_key(path)
    blob_client = self.blob_service_client.get_blob_client(
        container=self.container, blob=blob_key, snapshot=None
    )
    return blob_client.exists()
file_exists(path)

Checks if a blob is a file and exists.

Source code in gigaspatial/core/io/adls_data_store.py
def file_exists(self, path: Pathish) -> bool:
    """Checks if a blob is a file and exists."""
    return self.exists(path) and not self.is_dir(path)
get_file_metadata(path)

Retrieve comprehensive file metadata.

Parameters:

Name Type Description Default
path Pathish

File path in blob storage.

required

Returns:

Type Description
dict

Dictionary containing metadata like 'size_bytes', 'last_modified', 'etag'.

Source code in gigaspatial/core/io/adls_data_store.py
def get_file_metadata(self, path: Pathish) -> dict:
    """
    Retrieve comprehensive file metadata.

    Args:
        path: File path in blob storage.

    Returns:
        Dictionary containing metadata like 'size_bytes', 'last_modified', 'etag'.
    """
    blob_key = self._to_blob_key(path)
    blob_client = self.container_client.get_blob_client(blob_key)
    properties = blob_client.get_blob_properties()

    return {
        "name": blob_key,
        "size_bytes": properties.size,
        "content_type": properties.content_settings.content_type,
        "last_modified": properties.last_modified,
        "etag": properties.etag,
    }
has_files_with_extension(dir_path, extension)

Check if ANY file with given extension exists (early exit).

Much faster than filtering list_files() result when you only need to know if at least one file exists.

Parameters:

Name Type Description Default
dir_path Pathish

Directory path

required
extension str

File extension (e.g., '.png' or 'png')

required

Returns:

Type Description
bool

True if at least one file with extension exists

Example

if store.has_files_with_extension('images/', '.png'): print("Folder contains PNG images")

Performance
  • Returns immediately on first match
  • Best case: <1s (if match in first page)
  • Worst case: ~30-60s (no matches, must check all)
Source code in gigaspatial/core/io/adls_data_store.py
def has_files_with_extension(self, dir_path: Pathish, extension: str) -> bool:
    """
    Check if ANY file with given extension exists (early exit).

    Much faster than filtering list_files() result when you only
    need to know if at least one file exists.

    Args:
        dir_path: Directory path
        extension: File extension (e.g., '.png' or 'png')

    Returns:
        True if at least one file with extension exists

    Example:
        if store.has_files_with_extension('images/', '.png'):
            print("Folder contains PNG images")

    Performance:
        - Returns immediately on first match
        - Best case: <1s (if match in first page)
        - Worst case: ~30-60s (no matches, must check all)
    """
    if not extension.startswith("."):
        extension = "." + extension

    for blob_name in self.list_files_iter(dir_path):
        if blob_name.endswith(extension):
            return True  # Early exit

    return False
is_dir(path)

Checks if path is a directory.

Source code in gigaspatial/core/io/adls_data_store.py
def is_dir(self, path: Pathish) -> bool:
    """Checks if path is a directory."""
    dir_key = self._to_blob_key(path, ensure_dir=True)

    iterator = self.list_files_iter(dir_key, results_per_page=2)
    try:
        first_blob = next(iterator)
    except StopIteration:
        return False

    try:
        next(iterator)
        return True
    except StopIteration:
        return first_blob != self._to_blob_key(path)
is_file(path)

Checks if path is a file.

Source code in gigaspatial/core/io/adls_data_store.py
def is_file(self, path: Pathish) -> bool:
    """Checks if path is a file."""
    return self.file_exists(path)
list_directories(path)

List only directory names using Azure's hierarchical listing.

Source code in gigaspatial/core/io/adls_data_store.py
def list_directories(self, path: Pathish) -> list:
    """
    List only directory names using Azure's hierarchical listing.
    """
    prefix = self._to_blob_key(path, ensure_dir=True)
    directories = set()

    # Use walk_blobs with delimiter - Azure returns directories directly!
    blob_hierarchy = self.container_client.walk_blobs(
        name_starts_with=prefix, delimiter="/"
    )

    for item in blob_hierarchy:
        # Items with 'prefix' attribute are directories
        if hasattr(item, "prefix"):
            dir_path = item.prefix
            # Extract just the directory name
            relative_path = dir_path[len(prefix) :].rstrip("/")
            if relative_path:
                directories.add(relative_path)

    return sorted(list(directories))
list_files(dir_path)

List all files in a directory.

For large directories (100K+ files), consider using list_files_iter() to avoid memory overhead.

Performance improvements in this version: - Uses list_blob_names() internally (8-14x faster) - Lower memory usage (strings vs objects)

Parameters:

Name Type Description Default
dir_path Pathish

Directory path

required

Returns:

Type Description
list

List of file paths

Source code in gigaspatial/core/io/adls_data_store.py
def list_files(self, dir_path: Pathish) -> list:
    """
    List all files in a directory.

    For large directories (100K+ files), consider using
    list_files_iter() to avoid memory overhead.

    Performance improvements in this version:
    - Uses list_blob_names() internally (8-14x faster)
    - Lower memory usage (strings vs objects)

    Args:
        dir_path: Directory path

    Returns:
        List of file paths
    """
    prefix = self._to_blob_key(dir_path, ensure_dir=True)
    return list(self.container_client.list_blob_names(name_starts_with=prefix))
list_files_iter(dir_path, results_per_page=None)

Iterate over files with lazy evaluation (memory efficient).

Recommended for large directories. Returns generator that yields file paths one at a time.

Parameters:

Name Type Description Default
dir_path Pathish

Directory path

required
results_per_page Optional[int]

Number of results to request per page.

None

Yields:

Type Description
str

File paths

Example

for file in store.list_files_iter('data/'): if file.endswith('.png'): process(file) break # Early exit possible

Source code in gigaspatial/core/io/adls_data_store.py
def list_files_iter(
    self, dir_path: Pathish, results_per_page: Optional[int] = None
) -> Iterator[str]:
    """
    Iterate over files with lazy evaluation (memory efficient).

    Recommended for large directories. Returns generator that yields
    file paths one at a time.

    Args:
        dir_path: Directory path
        results_per_page: Number of results to request per page.

    Yields:
        File paths

    Example:
        for file in store.list_files_iter('data/'):
            if file.endswith('.png'):
                process(file)
                break  # Early exit possible
    """
    prefix = self._to_blob_key(dir_path, ensure_dir=True)
    return self.container_client.list_blob_names(
        name_starts_with=prefix, results_per_page=results_per_page
    )
mkdir(path, exist_ok=False)

Create a virtual directory in ADLS.

Directories are conceptual in blob storage and represented by a placeholder blob.

Parameters:

Name Type Description Default
path Pathish

Path of the directory to create.

required
exist_ok bool

If False, raise an error if the directory already exists.

False

Raises:

Type Description
FileExistsError

If directory exists and exist_ok=False.

Source code in gigaspatial/core/io/adls_data_store.py
def mkdir(self, path: Pathish, exist_ok: bool = False) -> None:
    """
    Create a virtual directory in ADLS.

    Directories are conceptual in blob storage and represented by a placeholder blob.

    Args:
        path: Path of the directory to create.
        exist_ok: If False, raise an error if the directory already exists.

    Raises:
        FileExistsError: If directory exists and exist_ok=False.
    """
    dir_key = self._to_blob_key(path, ensure_dir=True)

    existing_blobs = list(self.list_files(dir_key))

    if existing_blobs and not exist_ok:
        raise FileExistsError(f"Directory {path} already exists")

    # Create a placeholder blob to represent the directory
    placeholder_blob_path = PurePosixPath(dir_key) / ".placeholder"
    placeholder_blob_key = self._to_blob_key(placeholder_blob_path)

    # Only create placeholder if it doesn't already exist
    if not self.file_exists(placeholder_blob_key):
        placeholder_content = (
            b"This is a placeholder blob to represent a directory."
        )
        blob_client = self.blob_service_client.get_blob_client(
            container=self.container, blob=placeholder_blob_key
        )
        blob_client.upload_blob(placeholder_content, overwrite=True)
open(path, mode='r')

Context manager for file operations with enhanced mode support.

:param path: File path in blob storage :param mode: File open mode (r, rb, w, wb)

Source code in gigaspatial/core/io/adls_data_store.py
@contextlib.contextmanager
def open(self, path: Pathish, mode: str = "r"):
    """
    Context manager for file operations with enhanced mode support.

    :param path: File path in blob storage
    :param mode: File open mode (r, rb, w, wb)
    """
    blob_key = self._to_blob_key(path)

    if mode == "w":
        file = io.StringIO()
        yield file
        self.write_file(blob_key, file.getvalue())

    elif mode == "wb":
        file = io.BytesIO()
        yield file
        self.write_file(blob_key, file.getvalue())

    elif mode == "r":
        data = self.read_file(blob_key, encoding="UTF-8")
        file = io.StringIO(data)
        yield file

    elif mode == "rb":
        data = self.read_file(blob_key)
        file = io.BytesIO(data)
        yield file
read_file(path, encoding=None)

Read file contents from blob storage.

Parameters:

Name Type Description Default
path Pathish

Path to the file in blob storage.

required
encoding Optional[str]

Optional string encoding (e.g., 'utf-8'). If None, returns bytes.

None

Returns:

Type Description
Union[str, bytes]

File contents as a string if encoding is provided, otherwise bytes.

Raises:

Type Description
IOError

If there's an error downloading the blob.

Source code in gigaspatial/core/io/adls_data_store.py
def read_file(
    self, path: Pathish, encoding: Optional[str] = None
) -> Union[str, bytes]:
    """
    Read file contents from blob storage.

    Args:
        path: Path to the file in blob storage.
        encoding: Optional string encoding (e.g., 'utf-8'). If None, returns bytes.

    Returns:
        File contents as a string if encoding is provided, otherwise bytes.

    Raises:
        IOError: If there's an error downloading the blob.
    """
    try:
        blob_key = self._to_blob_key(path)
        blob_client = self.container_client.get_blob_client(blob_key)
        blob_data = blob_client.download_blob().readall()

        # If no encoding specified, return raw bytes
        if encoding is None:
            return blob_data

        # If encoding is specified, decode the bytes
        return blob_data.decode(encoding)

    except Exception as e:
        raise IOError(f"Error reading file {path}: {e}")
remove(path)

Removes a file.

Source code in gigaspatial/core/io/adls_data_store.py
def remove(self, path: Pathish) -> None:
    """Removes a file."""
    blob_key = self._to_blob_key(path)
    blob_client = self.blob_service_client.get_blob_client(
        container=self.container, blob=blob_key, snapshot=None
    )
    if blob_client.exists():
        blob_client.delete_blob()
rename(source_path, destination_path, overwrite=False, delete_source=True, wait=True, timeout_seconds=300, poll_interval_seconds=1)

Rename (move) a single file by copying to the new path and deleting the source.

:param source_path: Existing blob path :param destination_path: Target blob path :param overwrite: Overwrite destination if it already exists :param delete_source: Delete original after successful copy :param wait: Wait for the copy operation to complete :param timeout_seconds: Max time to wait for copy to succeed :param poll_interval_seconds: Polling interval while waiting

Source code in gigaspatial/core/io/adls_data_store.py
def rename(
    self,
    source_path: Pathish,
    destination_path: Pathish,
    overwrite: bool = False,
    delete_source: bool = True,
    wait: bool = True,
    timeout_seconds: int = 300,
    poll_interval_seconds: int = 1,
) -> None:
    """
    Rename (move) a single file by copying to the new path and deleting the source.

    :param source_path: Existing blob path
    :param destination_path: Target blob path
    :param overwrite: Overwrite destination if it already exists
    :param delete_source: Delete original after successful copy
    :param wait: Wait for the copy operation to complete
    :param timeout_seconds: Max time to wait for copy to succeed
    :param poll_interval_seconds: Polling interval while waiting
    """
    source_key = self._to_blob_key(source_path)
    destination_key = self._to_blob_key(destination_path)

    if not self.file_exists(source_key):
        raise FileNotFoundError(f"Source file not found: {source_key}")

    if self.file_exists(destination_key) and not overwrite:
        raise FileExistsError(
            f"Destination already exists and overwrite is False: {destination_key}"
        )

    # Use copy_file method to copy the file
    self.copy_file(source_key, destination_key, overwrite=overwrite)

    if wait:
        # Wait for copy to complete if requested
        dest_client = self.container_client.get_blob_client(destination_key)
        deadline = time.time() + timeout_seconds
        while True:
            props = dest_client.get_blob_properties()
            status = getattr(props.copy, "status", None)
            if status == "success":
                break
            if status in {"aborted", "failed"}:
                raise IOError(
                    f"Copy failed with status {status} from {source_key} to {destination_key}"
                )
            if time.time() > deadline:
                raise TimeoutError(
                    f"Timed out waiting for copy to complete for {destination_key}"
                )
            time.sleep(poll_interval_seconds)

    if delete_source:
        self.remove(source_key)
rmdir(dir)

Removes a directory.

Source code in gigaspatial/core/io/adls_data_store.py
def rmdir(self, dir: Pathish) -> None:
    """Removes a directory."""
    # Normalize directory path to ensure it targets all children
    dir_key = self._to_blob_key(dir, ensure_dir=True)

    # Azure Blob batch delete has a hard limit on number of sub-requests
    # per batch (currently 256). Delete in chunks to avoid
    # ExceedsMaxBatchRequestCount errors.
    blobs = list(self.list_files(dir_key))
    if not blobs:
        return

    BATCH_LIMIT = 256
    for start_idx in range(0, len(blobs), BATCH_LIMIT):
        batch = blobs[start_idx : start_idx + BATCH_LIMIT]
        self.container_client.delete_blobs(*batch)
upload_directory(dir_path, blob_dir_path)

Uploads all files from a directory to Azure Blob Storage.

Source code in gigaspatial/core/io/adls_data_store.py
def upload_directory(self, dir_path: str, blob_dir_path: Pathish):
    """Uploads all files from a directory to Azure Blob Storage."""
    blob_dir_key = self._to_blob_key(blob_dir_path, ensure_dir=True)
    for root, dirs, files in os.walk(dir_path):
        for file in files:
            local_file_path = os.path.join(root, file)
            relative_path = os.path.relpath(local_file_path, dir_path)
            # Construct blob path and normalize
            blob_file_path = PurePosixPath(blob_dir_key) / relative_path
            blob_file_key = self._to_blob_key(blob_file_path)

            self.upload_file(local_file_path, blob_file_key)
upload_file(file_path, blob_path)

Uploads a single file to Azure Blob Storage.

Source code in gigaspatial/core/io/adls_data_store.py
def upload_file(self, file_path: str, blob_path: Pathish):
    """Uploads a single file to Azure Blob Storage."""
    try:
        blob_key = self._to_blob_key(blob_path)
        blob_client = self.container_client.get_blob_client(blob_key)
        with open(file_path, "rb") as data:
            blob_client.upload_blob(data, overwrite=True)
        print(f"Uploaded {file_path} to {blob_path}")
    except Exception as e:
        print(f"Failed to upload {file_path}: {e}")
walk(top)

Walk through directory tree yielding (dirpath, dirnames, filenames).

Optimized to use list_files_iter() for better performance.

Source code in gigaspatial/core/io/adls_data_store.py
def walk(self, top: Pathish):
    """
    Walk through directory tree yielding (dirpath, dirnames, filenames).

    Optimized to use list_files_iter() for better performance.
    """
    for blob_name in self.list_files_iter(top):
        dirpath, filename = os.path.split(blob_name)
        yield (dirpath, [], [filename])
write_file(path, data)

Write data (string or bytes) to a blob.

Parameters:

Name Type Description Default
path Pathish

Destination path in blob storage.

required
data

Data to write (str or bytes).

required

Raises:

Type Description
ValueError

If data type is not supported.

Source code in gigaspatial/core/io/adls_data_store.py
def write_file(self, path: Pathish, data) -> None:
    """
    Write data (string or bytes) to a blob.

    Args:
        path: Destination path in blob storage.
        data: Data to write (str or bytes).

    Raises:
        ValueError: If data type is not supported.
    """
    blob_key = self._to_blob_key(path)
    blob_client = self.blob_service_client.get_blob_client(
        container=self.container, blob=blob_key, snapshot=None
    )

    if isinstance(data, str):
        binary_data = data.encode()
    elif isinstance(data, bytes):
        binary_data = data
    else:
        raise ValueError(f'Unsupported data type. Only "bytes" or "string" accepted')

    blob_client.upload_blob(binary_data, overwrite=True)

bigquery_client

BigQueryClient

A generalizable Google BigQuery client for GigaSpatial.

Provides generic, dataset-agnostic methods for schema inspection and query execution. Dataset-specific handlers (e.g. MLabHandler) should compose this client rather than reimplement it.

Parameters

config : BigQueryClientConfig, optional Validated configuration object. If not provided, a default config is constructed from global_config.

Examples

client = BigQueryClient() # uses global_config defaults client.list_datasets() df = client.query_to_dataframe("SELECT * FROM project.dataset.table LIMIT 10")

custom = BigQueryClient(BigQueryClientConfig(project="my-gcp-project"))

Source code in gigaspatial/core/io/bigquery_client.py
class BigQueryClient:
    """
    A generalizable Google BigQuery client for GigaSpatial.

    Provides generic, dataset-agnostic methods for schema inspection and
    query execution. Dataset-specific handlers (e.g. MLabHandler) should
    compose this client rather than reimplement it.

    Parameters
    ----------
    config : BigQueryClientConfig, optional
        Validated configuration object. If not provided, a default config
        is constructed from global_config.

    Examples
    --------
    >>> client = BigQueryClient()  # uses global_config defaults
    >>> client.list_datasets()
    >>> df = client.query_to_dataframe("SELECT * FROM `project.dataset.table` LIMIT 10")

    >>> custom = BigQueryClient(BigQueryClientConfig(project="my-gcp-project"))
    """

    def __init__(self, config: Optional[BigQueryClientConfig] = None) -> None:
        if not _HAS_BQ:
            raise ImportError(
                "BigQueryClient requires 'google-cloud-bigquery' and 'db-dtypes'. "
                "Install them with: pip install 'giga-spatial[bq]'"
            )
        self.config = config or BigQueryClientConfig()
        self._credentials = self._resolve_credentials()
        self.client = bigquery.Client(
            project=self.config.project,
            credentials=self._credentials,
        )
        self.storage_client = (
            bigquery_storage.BigQueryReadClient(credentials=self._credentials)
            if self.config.use_bq_storage
            else None
        )
        logger.info(
            "BigQueryClient initialized for project '%s' (service_account=%s)",
            self.config.project,
            self.config.service_account or "ADC",
        )

    def _resolve_credentials(self):
        """
        Resolve credentials using the following priority:

        1. Service account key file (``config.service_account_key_path``)
        2. Application Default Credentials (ADC)

        Returns
        -------
        google.auth.credentials.Credentials
        """
        if self.config.service_account_key_path:
            logger.debug(
                "Using service account key: %s",
                self.config.service_account_key_path,
            )
            return service_account.Credentials.from_service_account_file(
                self.config.service_account_key_path,
                scopes=BIGQUERY_SCOPES,
            )
        logger.debug("Falling back to Application Default Credentials (ADC).")
        credentials, _ = google.auth.default(scopes=BIGQUERY_SCOPES)
        return credentials

    def list_datasets(self, project_id: Optional[str] = None) -> list[str]:
        """
        List all available datasets in the configured project.

        Parameters
        ----------
        project_id : str, optional
            The BigQuery project ID. Defaults to ``config.project``.

        Returns
        -------
        list[str]
            Dataset IDs within the given project.
        """
        target_project = project_id or self.config.project
        datasets = self.client.list_datasets(target_project)
        return [ds.dataset_id for ds in datasets]

    def list_tables(self, dataset_id: str) -> list[str]:
        """
        List all tables within a dataset.

        Parameters
        ----------
        dataset_id : str
            The BigQuery dataset ID.

        Returns
        -------
        list[str]
            Table IDs within the given dataset.
        """
        tables = self.client.list_tables(dataset_id)
        return [t.table_id for t in tables]

    def get_table_schema(self, dataset_id: str, table_id: str) -> list[dict]:
        """
        Retrieve the schema for a specific table.

        Parameters
        ----------
        dataset_id : str
            The BigQuery dataset ID.
        table_id : str
            The BigQuery table ID.

        Returns
        -------
        list[dict]
            List of field descriptors with keys:
            ``name``, ``type``, ``mode``, ``description``.
        """
        table_ref = f"{dataset_id}.{table_id}"
        table = self.client.get_table(table_ref)
        return [
            {
                "name": field.name,
                "type": field.field_type,
                "mode": field.mode,
                "description": field.description,
            }
            for field in table.schema
        ]

    def query(self, sql: str, **kwargs) -> "bigquery.table.RowIterator":
        """
        Execute a SQL query and return a raw result iterator.

        Parameters
        ----------
        sql : str
            Standard SQL query string.
        **kwargs
            Additional keyword arguments forwarded to ``bigquery.Client.query()``.

        Returns
        -------
        google.cloud.bigquery.table.RowIterator
        """
        logger.debug("Executing BigQuery query: %.200s", sql)
        return self.client.query(sql, **kwargs).result()

    def query_to_dataframe(
        self,
        sql: str,
        dtypes: Optional[dict] = None,
        max_gb_allowed: Optional[int] = 10,  # Safety rail
        is_ci: bool = False,
        **kwargs,
    ) -> pd.DataFrame:
        """
        Execute a SQL query and return results as a pandas DataFrame.

        Parameters
        ----------
        sql : str
            Standard SQL query string.
        dtypes : dict, optional
            Column dtype overrides passed to ``to_dataframe()``.
        max_gb_allowed : int, optional
            Maximum number of gigabytes to bill for the query. Defaults to 10.
        is_ci : bool, optional
            Whether the query is running in a CI environment. Defaults to False.
        **kwargs
            Additional keyword arguments forwarded to ``bigquery.Client.query()``.

        Returns
        -------
        pd.DataFrame
            DataFrame containing the query results.
        """

        job_config = bigquery.QueryJobConfig(
            maximum_bytes_billed=max_gb_allowed * (1024**3) if max_gb_allowed else None
        )

        # Merge any extra kwargs into job_config if needed
        job = self.client.query(sql, job_config=job_config, **kwargs)

        # Dataset rows can be complex; using the storage client
        return job.result().to_dataframe(
            bqstorage_client=self.storage_client,
            progress_bar_type="tqdm" if not is_ci else None,
            dtypes=dtypes,
        )

    def get_query_cost_estimate(self, sql: str) -> float:
        """
        Estimate the cost of a BigQuery query.

        Parameters
        ----------
        sql : str
            Standard SQL query string.

        Returns
        -------
        float
            Estimated cost in USD based on on-demand pricing
            (${_BQ_COST_PER_TIB_USD}/TiB). For flat-rate pricing this
            will not reflect actual cost.
        """
        job_config = bigquery.QueryJobConfig(dry_run=True, use_query_cache=False)
        query_job = self.client.query(sql, job_config=job_config)
        return (query_job.total_bytes_processed / (1024**4)) * _BQ_COST_PER_TIB_USD
get_query_cost_estimate(sql)

Estimate the cost of a BigQuery query.

Parameters

sql : str Standard SQL query string.

Returns

float Estimated cost in USD based on on-demand pricing (${_BQ_COST_PER_TIB_USD}/TiB). For flat-rate pricing this will not reflect actual cost.

Source code in gigaspatial/core/io/bigquery_client.py
def get_query_cost_estimate(self, sql: str) -> float:
    """
    Estimate the cost of a BigQuery query.

    Parameters
    ----------
    sql : str
        Standard SQL query string.

    Returns
    -------
    float
        Estimated cost in USD based on on-demand pricing
        (${_BQ_COST_PER_TIB_USD}/TiB). For flat-rate pricing this
        will not reflect actual cost.
    """
    job_config = bigquery.QueryJobConfig(dry_run=True, use_query_cache=False)
    query_job = self.client.query(sql, job_config=job_config)
    return (query_job.total_bytes_processed / (1024**4)) * _BQ_COST_PER_TIB_USD
get_table_schema(dataset_id, table_id)

Retrieve the schema for a specific table.

Parameters

dataset_id : str The BigQuery dataset ID. table_id : str The BigQuery table ID.

Returns

list[dict] List of field descriptors with keys: name, type, mode, description.

Source code in gigaspatial/core/io/bigquery_client.py
def get_table_schema(self, dataset_id: str, table_id: str) -> list[dict]:
    """
    Retrieve the schema for a specific table.

    Parameters
    ----------
    dataset_id : str
        The BigQuery dataset ID.
    table_id : str
        The BigQuery table ID.

    Returns
    -------
    list[dict]
        List of field descriptors with keys:
        ``name``, ``type``, ``mode``, ``description``.
    """
    table_ref = f"{dataset_id}.{table_id}"
    table = self.client.get_table(table_ref)
    return [
        {
            "name": field.name,
            "type": field.field_type,
            "mode": field.mode,
            "description": field.description,
        }
        for field in table.schema
    ]
list_datasets(project_id=None)

List all available datasets in the configured project.

Parameters

project_id : str, optional The BigQuery project ID. Defaults to config.project.

Returns

list[str] Dataset IDs within the given project.

Source code in gigaspatial/core/io/bigquery_client.py
def list_datasets(self, project_id: Optional[str] = None) -> list[str]:
    """
    List all available datasets in the configured project.

    Parameters
    ----------
    project_id : str, optional
        The BigQuery project ID. Defaults to ``config.project``.

    Returns
    -------
    list[str]
        Dataset IDs within the given project.
    """
    target_project = project_id or self.config.project
    datasets = self.client.list_datasets(target_project)
    return [ds.dataset_id for ds in datasets]
list_tables(dataset_id)

List all tables within a dataset.

Parameters

dataset_id : str The BigQuery dataset ID.

Returns

list[str] Table IDs within the given dataset.

Source code in gigaspatial/core/io/bigquery_client.py
def list_tables(self, dataset_id: str) -> list[str]:
    """
    List all tables within a dataset.

    Parameters
    ----------
    dataset_id : str
        The BigQuery dataset ID.

    Returns
    -------
    list[str]
        Table IDs within the given dataset.
    """
    tables = self.client.list_tables(dataset_id)
    return [t.table_id for t in tables]
query(sql, **kwargs)

Execute a SQL query and return a raw result iterator.

Parameters

sql : str Standard SQL query string. **kwargs Additional keyword arguments forwarded to bigquery.Client.query().

Returns

google.cloud.bigquery.table.RowIterator

Source code in gigaspatial/core/io/bigquery_client.py
def query(self, sql: str, **kwargs) -> "bigquery.table.RowIterator":
    """
    Execute a SQL query and return a raw result iterator.

    Parameters
    ----------
    sql : str
        Standard SQL query string.
    **kwargs
        Additional keyword arguments forwarded to ``bigquery.Client.query()``.

    Returns
    -------
    google.cloud.bigquery.table.RowIterator
    """
    logger.debug("Executing BigQuery query: %.200s", sql)
    return self.client.query(sql, **kwargs).result()
query_to_dataframe(sql, dtypes=None, max_gb_allowed=10, is_ci=False, **kwargs)

Execute a SQL query and return results as a pandas DataFrame.

Parameters

sql : str Standard SQL query string. dtypes : dict, optional Column dtype overrides passed to to_dataframe(). max_gb_allowed : int, optional Maximum number of gigabytes to bill for the query. Defaults to 10. is_ci : bool, optional Whether the query is running in a CI environment. Defaults to False. **kwargs Additional keyword arguments forwarded to bigquery.Client.query().

Returns

pd.DataFrame DataFrame containing the query results.

Source code in gigaspatial/core/io/bigquery_client.py
def query_to_dataframe(
    self,
    sql: str,
    dtypes: Optional[dict] = None,
    max_gb_allowed: Optional[int] = 10,  # Safety rail
    is_ci: bool = False,
    **kwargs,
) -> pd.DataFrame:
    """
    Execute a SQL query and return results as a pandas DataFrame.

    Parameters
    ----------
    sql : str
        Standard SQL query string.
    dtypes : dict, optional
        Column dtype overrides passed to ``to_dataframe()``.
    max_gb_allowed : int, optional
        Maximum number of gigabytes to bill for the query. Defaults to 10.
    is_ci : bool, optional
        Whether the query is running in a CI environment. Defaults to False.
    **kwargs
        Additional keyword arguments forwarded to ``bigquery.Client.query()``.

    Returns
    -------
    pd.DataFrame
        DataFrame containing the query results.
    """

    job_config = bigquery.QueryJobConfig(
        maximum_bytes_billed=max_gb_allowed * (1024**3) if max_gb_allowed else None
    )

    # Merge any extra kwargs into job_config if needed
    job = self.client.query(sql, job_config=job_config, **kwargs)

    # Dataset rows can be complex; using the storage client
    return job.result().to_dataframe(
        bqstorage_client=self.storage_client,
        progress_bar_type="tqdm" if not is_ci else None,
        dtypes=dtypes,
    )
BigQueryClientConfig

Bases: BaseModel

Configuration for authenticating and initializing a BigQuery client.

Defaults are resolved from GigaSpatial's global_config, mirroring the pattern used by the Google Earth Engine profiler.

Credential resolution priority: 1. Service account key file (service_account_key_path) 2. Application Default Credentials (ADC) — for GCP-hosted environments

Parameters

project : str, optional GCP project ID to bill queries against. Defaults to global_config.GOOGLE_CLOUD_PROJECT. service_account : str, optional Service account email address. Defaults to global_config.GOOGLE_SERVICE_ACCOUNT. service_account_key_path : str, optional Path to the service account JSON key file. Defaults to global_config.GOOGLE_SERVICE_ACCOUNT_KEY_PATH. use_bq_storage : bool Enable BigQuery Storage API for faster query_to_dataframe reads. Defaults to False.

Source code in gigaspatial/core/io/bigquery_client.py
class BigQueryClientConfig(BaseModel):
    """
    Configuration for authenticating and initializing a BigQuery client.

    Defaults are resolved from GigaSpatial's global_config, mirroring the
    pattern used by the Google Earth Engine profiler.

    Credential resolution priority:
    1. Service account key file (``service_account_key_path``)
    2. Application Default Credentials (ADC) — for GCP-hosted environments

    Parameters
    ----------
    project : str, optional
        GCP project ID to bill queries against.
        Defaults to ``global_config.GOOGLE_CLOUD_PROJECT``.
    service_account : str, optional
        Service account email address.
        Defaults to ``global_config.GOOGLE_SERVICE_ACCOUNT``.
    service_account_key_path : str, optional
        Path to the service account JSON key file.
        Defaults to ``global_config.GOOGLE_SERVICE_ACCOUNT_KEY_PATH``.
    use_bq_storage : bool
        Enable BigQuery Storage API for faster ``query_to_dataframe`` reads.
        Defaults to ``False``.
    """

    project: Optional[str] = global_config.GOOGLE_CLOUD_PROJECT
    service_account: Optional[str] = global_config.GOOGLE_SERVICE_ACCOUNT
    service_account_key_path: Optional[str] = (
        global_config.GOOGLE_SERVICE_ACCOUNT_KEY_PATH
    )
    use_bq_storage: bool = False

    model_config = {"arbitrary_types_allowed": True}

    @model_validator(mode="after")
    def validate_project(self) -> "BigQueryClientConfig":
        if not self.project:
            raise ValueError(
                "`project` must be set explicitly or via "
                "GOOGLE_CLOUD_PROJECT in global_config."
            )
        return self

data_store

Module for defining the abstract DataStore interface. This interface provides a consistent API for reading and writing files across different storage backends like local disk, ADLS, or Snowflake.

DataStore

Bases: ABC

Abstract base class defining the interface for data store implementations. This class serves as a parent for both local and cloud-based storage solutions.

Source code in gigaspatial/core/io/data_store.py
class DataStore(ABC):
    """
    Abstract base class defining the interface for data store implementations.
    This class serves as a parent for both local and cloud-based storage solutions.
    """

    @abstractmethod
    def read_file(self, path: str) -> Any:
        """
        Read contents of a file from the data store.

        Args:
            path: Path to the file to read

        Returns:
            Contents of the file

        Raises:
            IOError: If file cannot be read
        """
        pass

    @abstractmethod
    def write_file(self, path: str, data: Any) -> None:
        """
        Write data to a file in the data store.

        Args:
            path: Path where to write the file
            data: Data to write to the file

        Raises:
            IOError: If file cannot be written
        """
        pass

    @abstractmethod
    def file_exists(self, path: str) -> bool:
        """
        Check if a file exists in the data store.

        Args:
            path: Path to check

        Returns:
            True if file exists, False otherwise
        """
        pass

    @abstractmethod
    def list_files(self, path: str) -> List[str]:
        """
        List all files in a directory.

        Args:
            path: Directory path to list

        Returns:
            List of file paths in the directory
        """
        pass

    @abstractmethod
    def walk(self, top: str) -> Generator:
        """
        Walk through directory tree, similar to os.walk().

        Args:
            top: Starting directory for the walk

        Returns:
            Generator yielding tuples of (dirpath, dirnames, filenames)
        """
        pass

    @abstractmethod
    def open(self, file: str, mode: str = "r") -> Union[str, bytes]:
        """
        Context manager for file operations.

        Args:
            file: Path to the file
            mode: File mode ('r', 'w', 'rb', 'wb')

        Yields:
            File-like object

        Raises:
            IOError: If file cannot be opened
        """
        pass

    @abstractmethod
    def is_file(self, path: str) -> bool:
        """
        Check if path points to a file.

        Args:
            path: Path to check

        Returns:
            True if path is a file, False otherwise
        """
        pass

    @abstractmethod
    def is_dir(self, path: str) -> bool:
        """
        Check if path points to a directory.

        Args:
            path: Path to check

        Returns:
            True if path is a directory, False otherwise
        """
        pass

    @abstractmethod
    def remove(self, path: str) -> None:
        """
        Remove a file.

        Args:
            path: Path to the file to remove

        Raises:
            IOError: If file cannot be removed
        """
        pass

    @abstractmethod
    def rmdir(self, dir: str) -> None:
        """
        Remove a directory and all its contents.

        Args:
            dir: Path to the directory to remove

        Raises:
            IOError: If directory cannot be removed
        """
        pass
file_exists(path) abstractmethod

Check if a file exists in the data store.

Parameters:

Name Type Description Default
path str

Path to check

required

Returns:

Type Description
bool

True if file exists, False otherwise

Source code in gigaspatial/core/io/data_store.py
@abstractmethod
def file_exists(self, path: str) -> bool:
    """
    Check if a file exists in the data store.

    Args:
        path: Path to check

    Returns:
        True if file exists, False otherwise
    """
    pass
is_dir(path) abstractmethod

Check if path points to a directory.

Parameters:

Name Type Description Default
path str

Path to check

required

Returns:

Type Description
bool

True if path is a directory, False otherwise

Source code in gigaspatial/core/io/data_store.py
@abstractmethod
def is_dir(self, path: str) -> bool:
    """
    Check if path points to a directory.

    Args:
        path: Path to check

    Returns:
        True if path is a directory, False otherwise
    """
    pass
is_file(path) abstractmethod

Check if path points to a file.

Parameters:

Name Type Description Default
path str

Path to check

required

Returns:

Type Description
bool

True if path is a file, False otherwise

Source code in gigaspatial/core/io/data_store.py
@abstractmethod
def is_file(self, path: str) -> bool:
    """
    Check if path points to a file.

    Args:
        path: Path to check

    Returns:
        True if path is a file, False otherwise
    """
    pass
list_files(path) abstractmethod

List all files in a directory.

Parameters:

Name Type Description Default
path str

Directory path to list

required

Returns:

Type Description
List[str]

List of file paths in the directory

Source code in gigaspatial/core/io/data_store.py
@abstractmethod
def list_files(self, path: str) -> List[str]:
    """
    List all files in a directory.

    Args:
        path: Directory path to list

    Returns:
        List of file paths in the directory
    """
    pass
open(file, mode='r') abstractmethod

Context manager for file operations.

Parameters:

Name Type Description Default
file str

Path to the file

required
mode str

File mode ('r', 'w', 'rb', 'wb')

'r'

Yields:

Type Description
Union[str, bytes]

File-like object

Raises:

Type Description
IOError

If file cannot be opened

Source code in gigaspatial/core/io/data_store.py
@abstractmethod
def open(self, file: str, mode: str = "r") -> Union[str, bytes]:
    """
    Context manager for file operations.

    Args:
        file: Path to the file
        mode: File mode ('r', 'w', 'rb', 'wb')

    Yields:
        File-like object

    Raises:
        IOError: If file cannot be opened
    """
    pass
read_file(path) abstractmethod

Read contents of a file from the data store.

Parameters:

Name Type Description Default
path str

Path to the file to read

required

Returns:

Type Description
Any

Contents of the file

Raises:

Type Description
IOError

If file cannot be read

Source code in gigaspatial/core/io/data_store.py
@abstractmethod
def read_file(self, path: str) -> Any:
    """
    Read contents of a file from the data store.

    Args:
        path: Path to the file to read

    Returns:
        Contents of the file

    Raises:
        IOError: If file cannot be read
    """
    pass
remove(path) abstractmethod

Remove a file.

Parameters:

Name Type Description Default
path str

Path to the file to remove

required

Raises:

Type Description
IOError

If file cannot be removed

Source code in gigaspatial/core/io/data_store.py
@abstractmethod
def remove(self, path: str) -> None:
    """
    Remove a file.

    Args:
        path: Path to the file to remove

    Raises:
        IOError: If file cannot be removed
    """
    pass
rmdir(dir) abstractmethod

Remove a directory and all its contents.

Parameters:

Name Type Description Default
dir str

Path to the directory to remove

required

Raises:

Type Description
IOError

If directory cannot be removed

Source code in gigaspatial/core/io/data_store.py
@abstractmethod
def rmdir(self, dir: str) -> None:
    """
    Remove a directory and all its contents.

    Args:
        dir: Path to the directory to remove

    Raises:
        IOError: If directory cannot be removed
    """
    pass
walk(top) abstractmethod

Walk through directory tree, similar to os.walk().

Parameters:

Name Type Description Default
top str

Starting directory for the walk

required

Returns:

Type Description
Generator

Generator yielding tuples of (dirpath, dirnames, filenames)

Source code in gigaspatial/core/io/data_store.py
@abstractmethod
def walk(self, top: str) -> Generator:
    """
    Walk through directory tree, similar to os.walk().

    Args:
        top: Starting directory for the walk

    Returns:
        Generator yielding tuples of (dirpath, dirnames, filenames)
    """
    pass
write_file(path, data) abstractmethod

Write data to a file in the data store.

Parameters:

Name Type Description Default
path str

Path where to write the file

required
data Any

Data to write to the file

required

Raises:

Type Description
IOError

If file cannot be written

Source code in gigaspatial/core/io/data_store.py
@abstractmethod
def write_file(self, path: str, data: Any) -> None:
    """
    Write data to a file in the data store.

    Args:
        path: Path where to write the file
        data: Data to write to the file

    Raises:
        IOError: If file cannot be written
    """
    pass

database

Module for database connectivity and operations. Provides a unified interface (DBConnection) for interacting with PostgreSQL and Trino databases.

DBConnection

A unified database connection class supporting both Trino and PostgreSQL.

Source code in gigaspatial/core/io/database.py
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
class DBConnection:
    """
    A unified database connection class supporting both Trino and PostgreSQL.
    """

    DB_CONFIG = global_config.DB_CONFIG or {}

    def __init__(
        self,
        db_type: Literal["postgresql", "trino"] = DB_CONFIG.get(
            "db_type", "postgresql"
        ),
        host: Optional[str] = DB_CONFIG.get("host", None),
        port: Union[int, str] = DB_CONFIG.get("port", None),  # type: ignore
        user: Optional[str] = DB_CONFIG.get("user", None),
        password: Optional[str] = DB_CONFIG.get("password", None),
        catalog: Optional[str] = DB_CONFIG.get("catalog", None),  # For Trino
        database: Optional[str] = DB_CONFIG.get("database", None),  # For PostgreSQL
        schema: str = DB_CONFIG.get("schema", "public"),  # Default for PostgreSQL
        http_scheme: str = DB_CONFIG.get("http_scheme", "https"),  # For Trino
        sslmode: str = DB_CONFIG.get("sslmode", "require"),  # For PostgreSQL
        **kwargs,
    ):
        """
        Initialize a database connection.

        Args:
            db_type: Database type ("trino" or "postgresql").
            host: Server hostname or IP address.
            port: Database port number.
            user: Username for authentication.
            password: Password for authentication.
            catalog: Catalog name (Trino only).
            database: Database name (PostgreSQL only).
            schema: Default schema to use.
            http_scheme: HTTP scheme for Trino ("http" or "https").
            sslmode: SSL mode for PostgreSQL (e.g., "require").
            **kwargs: Additional connection parameters for SQLAlchemy.

        Raises:
            ImportError: If 'sqlalchemy' is not installed.
            ValueError: If db_type is unsupported.
        """
        if not _HAS_SQLALCHEMY:
            raise ImportError(
                "DBConnection requires 'sqlalchemy'. "
                "Install it with: pip install 'giga-spatial[db]'"
            )
        self.db_type = db_type.lower()
        self.host = host
        self.port = str(port) if port else None
        self.user = user
        self.password = quote_plus(password) if password else None
        self.default_schema = schema

        if self.db_type == "trino":
            self.catalog = catalog
            self.http_scheme = http_scheme
            self.engine = self._create_trino_engine(**kwargs)
        elif self.db_type == "postgresql":
            self.database = database
            self.sslmode = sslmode
            self.engine = self._create_postgresql_engine(**kwargs)
        else:
            raise ValueError(f"Unsupported database type: {db_type}")

        self._add_event_listener()

    def _create_trino_engine(self, **kwargs) -> "Engine":
        """
        Create a Trino SQLAlchemy engine.

        Args:
            **kwargs: Additional arguments for create_engine.

        Returns:
            SQLAlchemy Engine.

        Raises:
            ImportError: If 'sqlalchemy-trino' is not installed.
        """
        try:
            import trino
        except ImportError:
            raise ImportError(
                "Trino support requires 'sqlalchemy-trino'. "
                "Install it with: pip install 'giga-spatial[db]'"
            )
        self._connection_string = (
            f"trino://{self.user}:{self.password}@{self.host}:{self.port}/"
            f"{self.catalog}/{self.default_schema}"
        )
        return create_engine(
            self._connection_string,
            connect_args={"http_scheme": self.http_scheme},
            **kwargs,
        )

    def _create_postgresql_engine(self, **kwargs) -> "Engine":
        """
        Create a PostgreSQL SQLAlchemy engine.

        Args:
            **kwargs: Additional arguments for create_engine.

        Returns:
            SQLAlchemy Engine.
        """
        self._connection_string = (
            f"postgresql://{self.user}:{self.password}@{self.host}:{self.port}/"
            f"{self.database}?sslmode={self.sslmode}"
        )
        return create_engine(self._connection_string, **kwargs)

    def _add_event_listener(self):
        """Add event listeners for schema setting."""
        if self.db_type == "trino":

            @event.listens_for(self.engine, "connect", insert=True)
            def set_current_schema(dbapi_connection, connection_record):
                cursor_obj = dbapi_connection.cursor()
                try:
                    cursor_obj.execute(f"USE {self.default_schema}")
                except Exception as e:
                    warnings.warn(f"Could not set schema to {self.default_schema}: {e}")
                finally:
                    cursor_obj.close()

    def get_connection_string(self) -> str:
        """
        Returns the connection string used to create the engine.

        Returns:
            The raw connection string.
        """
        return self._connection_string

    def _parse_schema_table(
        self, table_name: str, schema: Optional[str] = None
    ) -> tuple[Optional[str], str]:
        """
        Parse a table name that might contain schema or catalog prefixes.

        Args:
            table_name: The table name (e.g., 'table', 'schema.table', or 'catalog.schema.table').
            schema: Optional explicit schema.

        Returns:
            A tuple of (schema, table_name).
        """
        if "." in table_name:
            parts = table_name.split(".")
            if len(parts) == 3:
                # For Trino and others, we join catalog and schema
                schema = f"{parts[0]}.{parts[1]}"
                table_name = parts[2]
            elif len(parts) == 2:
                schema, table_name = parts

        schema = schema or self.default_schema
        return schema, table_name

    def get_schema_names(self) -> List[str]:
        """
        Get list of all schema names in the database.

        Returns:
            List of schema names.
        """
        inspector = inspect(self.engine)
        return inspector.get_schema_names()

    def get_table_names(self, schema: Optional[str] = None) -> List[str]:
        """
        Get list of table names in a schema.

        Args:
            schema: Schema name. Defaults to default_schema.

        Returns:
            List of table names.
        """
        schema = schema or self.default_schema
        inspector = inspect(self.engine)
        return inspector.get_table_names(schema=schema)

    def get_view_names(self, schema: Optional[str] = None) -> List[str]:
        """
        Get list of view names in a schema.

        Args:
            schema: Schema name. Defaults to default_schema.

        Returns:
            List of view names.
        """
        schema = schema or self.default_schema
        inspector = inspect(self.engine)
        return inspector.get_view_names(schema=schema)

    def get_column_names(
        self, table_name: str, schema: Optional[str] = None
    ) -> List[str]:
        """
        Get column names for a specific table.

        Args:
            table_name: Name of the table.
            schema: Schema name.

        Returns:
            List of column names.
        """

        columns = self.get_table_info(table_name, schema=schema)
        return [col["name"] for col in columns]

    def get_table_info(
        self, table_name: str, schema: Optional[str] = None
    ) -> List[Dict]:
        """
        Get detailed column information for a table.

        Args:
            table_name: Name of the table.
            schema: Schema name.

        Returns:
            List of column metadata dictionaries.
        """
        orig_name = table_name
        schema, table_name = self._parse_schema_table(table_name, schema)
        try:
            inspector = inspect(self.engine)
            return inspector.get_columns(table_name, schema=schema)
        except Exception:
            # Fallback for Trino cross-catalog issues or other reflection failures
            if self.db_type == "trino":
                # Reconstruct path for DESCRIBE
                path = (
                    orig_name
                    if "." in orig_name
                    else f"{schema}.{table_name}"
                    if schema
                    else table_name
                )
                try:
                    # DESCRIBE returns Column, Type, Extra, Comment
                    df = self.read_sql_to_dataframe(f"DESCRIBE {path}")
                    return [
                        {
                            "name": row["Column"],
                            "type": row["Type"],
                            "nullable": True,
                            "default": None,
                            "autoincrement": False,
                            "comment": row.get("Comment"),
                        }
                        for _, row in df.iterrows()
                    ]
                except Exception:
                    pass
            raise

    def get_primary_keys(
        self, table_name: str, schema: Optional[str] = None
    ) -> List[str]:
        """
        Get primary key columns for a table.

        Args:
            table_name: Name of the table.
            schema: Schema name.

        Returns:
            List of primary key column names.
        """
        schema, table_name = self._parse_schema_table(table_name, schema)

        inspector = inspect(self.engine)
        try:
            return inspector.get_pk_constraint(table_name, schema=schema)[
                "constrained_columns"
            ]
        except:
            return []  # Some databases may not support PK constraints

    def table_exists(self, table_name: str, schema: Optional[str] = None) -> bool:
        """
        Check if a table exists in the database.

        Args:
            table_name: Name of the table.
            schema: Schema name.

        Returns:
            True if table exists, False otherwise.
        """
        try:
            columns = self.get_table_info(table_name, schema=schema)
            return len(columns) > 0
        except Exception:
            return False

    # PostgreSQL-specific methods
    def get_extensions(self) -> List[str]:
        """
        Get list of installed PostgreSQL extensions.

        Returns:
            List of extension names.

        Raises:
            NotImplementedError: If database type is not PostgreSQL.
        """
        if self.db_type != "postgresql":
            raise NotImplementedError(
                "This method is only available for PostgreSQL connections"
            )

        with self.engine.connect() as conn:
            result = conn.execute("SELECT extname FROM pg_extension")
            return [row[0] for row in result]

    def execute_query(
        self, query: str, fetch_results: bool = True, params: Optional[Dict] = None
    ) -> Union[List[tuple], None]:
        """
        Executes a SQL query.

        Args:
            query: SQL query string to execute.
            fetch_results: Whether to fetch and return results.
            params: Optional dictionary of parameters for the query.

        Returns:
            List of result tuples if fetch_results is True, else None.

        Raises:
            SQLAlchemyError: If query execution fails.
        """
        try:
            with self.engine.connect() as connection:
                stmt = text(query)
                result = (
                    connection.execute(stmt, params)
                    if params
                    else connection.execute(stmt)
                )

                if fetch_results and result.returns_rows:
                    return result.fetchall()
                return None
        except SQLAlchemyError as e:
            print(f"Error executing query: {e}")
            raise

    def test_connection(self) -> bool:
        """
        Tests the database connection.

        Returns:
            True if connection successful, False otherwise.
        """
        test_query = (
            "SELECT 1"
            if self.db_type == "postgresql"
            else "SELECT 1 AS connection_test"
        )

        try:
            print(
                f"Attempting to connect to {self.db_type} at {self.host}:{self.port}..."
            )
            with self.engine.connect() as conn:
                conn.execute(text(test_query))
            print(f"Successfully connected to {self.db_type.upper()}.")
            return True
        except Exception as e:
            print(f"Failed to connect to {self.db_type.upper()}: {e}")
            return False

    def read_sql_to_dataframe(
        self, query: str, params: Optional[Dict] = None
    ) -> pd.DataFrame:
        """
        Executes query and returns results as pandas DataFrame.

        Args:
            query: SQL query string to execute.
            params: Optional parameters for the query.

        Returns:
            pandas DataFrame containing the query results.

        Raises:
            SQLAlchemyError: If query execution fails.
        """
        try:
            with self.engine.connect() as connection:
                return pd.read_sql_query(text(query), connection, params=params)
        except SQLAlchemyError as e:
            print(f"Error reading SQL to DataFrame: {e}")
            raise

    def read_sql_to_dask_dataframe(
        self,
        table_name: str,
        index_col: str,
        columns: Optional[List[str]] = None,
        limit: Optional[int] = None,
        **kwargs,
    ) -> "dd.DataFrame":
        """
        Reads data into a Dask DataFrame.

        Args:
            table_name: Table name (schema.table or just table).
            index_col: Column to use as index for partitioning.
            columns: List of columns to select.
            limit: Maximum rows to return.
            **kwargs: Additional arguments for dd.read_sql_query.

        Returns:
            Dask DataFrame with results.

        Raises:
            ImportError: If 'dask' is not installed.
            ValueError: If reading fails.
        """
        if not _HAS_DASK:
            raise ImportError(
                "read_sql_to_dask_dataframe requires 'dask'. "
                "Install it with: pip install 'giga-spatial[db]'"
            )
        try:
            connection_string = self.get_connection_string()

            # Handle schema.table format
            schema, table = self._parse_schema_table(table_name)

            metadata = MetaData()
            table_obj = Table(table, metadata, schema=schema, autoload_with=self.engine)

            # Build query
            query = (
                select(*[table_obj.c[col] for col in columns])
                if columns
                else select(table_obj)
            )
            if limit:
                query = query.limit(limit)

            return dd.read_sql_query(
                sql=query, con=connection_string, index_col=index_col, **kwargs
            )
        except Exception as e:
            print(f"Error reading SQL to Dask DataFrame: {e}")
            raise ValueError(f"Failed to read SQL to Dask DataFrame: {e}") from e
__init__(db_type=DB_CONFIG.get('db_type', 'postgresql'), host=DB_CONFIG.get('host', None), port=DB_CONFIG.get('port', None), user=DB_CONFIG.get('user', None), password=DB_CONFIG.get('password', None), catalog=DB_CONFIG.get('catalog', None), database=DB_CONFIG.get('database', None), schema=DB_CONFIG.get('schema', 'public'), http_scheme=DB_CONFIG.get('http_scheme', 'https'), sslmode=DB_CONFIG.get('sslmode', 'require'), **kwargs)

Initialize a database connection.

Parameters:

Name Type Description Default
db_type Literal['postgresql', 'trino']

Database type ("trino" or "postgresql").

get('db_type', 'postgresql')
host Optional[str]

Server hostname or IP address.

get('host', None)
port Union[int, str]

Database port number.

get('port', None)
user Optional[str]

Username for authentication.

get('user', None)
password Optional[str]

Password for authentication.

get('password', None)
catalog Optional[str]

Catalog name (Trino only).

get('catalog', None)
database Optional[str]

Database name (PostgreSQL only).

get('database', None)
schema str

Default schema to use.

get('schema', 'public')
http_scheme str

HTTP scheme for Trino ("http" or "https").

get('http_scheme', 'https')
sslmode str

SSL mode for PostgreSQL (e.g., "require").

get('sslmode', 'require')
**kwargs

Additional connection parameters for SQLAlchemy.

{}

Raises:

Type Description
ImportError

If 'sqlalchemy' is not installed.

ValueError

If db_type is unsupported.

Source code in gigaspatial/core/io/database.py
def __init__(
    self,
    db_type: Literal["postgresql", "trino"] = DB_CONFIG.get(
        "db_type", "postgresql"
    ),
    host: Optional[str] = DB_CONFIG.get("host", None),
    port: Union[int, str] = DB_CONFIG.get("port", None),  # type: ignore
    user: Optional[str] = DB_CONFIG.get("user", None),
    password: Optional[str] = DB_CONFIG.get("password", None),
    catalog: Optional[str] = DB_CONFIG.get("catalog", None),  # For Trino
    database: Optional[str] = DB_CONFIG.get("database", None),  # For PostgreSQL
    schema: str = DB_CONFIG.get("schema", "public"),  # Default for PostgreSQL
    http_scheme: str = DB_CONFIG.get("http_scheme", "https"),  # For Trino
    sslmode: str = DB_CONFIG.get("sslmode", "require"),  # For PostgreSQL
    **kwargs,
):
    """
    Initialize a database connection.

    Args:
        db_type: Database type ("trino" or "postgresql").
        host: Server hostname or IP address.
        port: Database port number.
        user: Username for authentication.
        password: Password for authentication.
        catalog: Catalog name (Trino only).
        database: Database name (PostgreSQL only).
        schema: Default schema to use.
        http_scheme: HTTP scheme for Trino ("http" or "https").
        sslmode: SSL mode for PostgreSQL (e.g., "require").
        **kwargs: Additional connection parameters for SQLAlchemy.

    Raises:
        ImportError: If 'sqlalchemy' is not installed.
        ValueError: If db_type is unsupported.
    """
    if not _HAS_SQLALCHEMY:
        raise ImportError(
            "DBConnection requires 'sqlalchemy'. "
            "Install it with: pip install 'giga-spatial[db]'"
        )
    self.db_type = db_type.lower()
    self.host = host
    self.port = str(port) if port else None
    self.user = user
    self.password = quote_plus(password) if password else None
    self.default_schema = schema

    if self.db_type == "trino":
        self.catalog = catalog
        self.http_scheme = http_scheme
        self.engine = self._create_trino_engine(**kwargs)
    elif self.db_type == "postgresql":
        self.database = database
        self.sslmode = sslmode
        self.engine = self._create_postgresql_engine(**kwargs)
    else:
        raise ValueError(f"Unsupported database type: {db_type}")

    self._add_event_listener()
execute_query(query, fetch_results=True, params=None)

Executes a SQL query.

Parameters:

Name Type Description Default
query str

SQL query string to execute.

required
fetch_results bool

Whether to fetch and return results.

True
params Optional[Dict]

Optional dictionary of parameters for the query.

None

Returns:

Type Description
Union[List[tuple], None]

List of result tuples if fetch_results is True, else None.

Raises:

Type Description
SQLAlchemyError

If query execution fails.

Source code in gigaspatial/core/io/database.py
def execute_query(
    self, query: str, fetch_results: bool = True, params: Optional[Dict] = None
) -> Union[List[tuple], None]:
    """
    Executes a SQL query.

    Args:
        query: SQL query string to execute.
        fetch_results: Whether to fetch and return results.
        params: Optional dictionary of parameters for the query.

    Returns:
        List of result tuples if fetch_results is True, else None.

    Raises:
        SQLAlchemyError: If query execution fails.
    """
    try:
        with self.engine.connect() as connection:
            stmt = text(query)
            result = (
                connection.execute(stmt, params)
                if params
                else connection.execute(stmt)
            )

            if fetch_results and result.returns_rows:
                return result.fetchall()
            return None
    except SQLAlchemyError as e:
        print(f"Error executing query: {e}")
        raise
get_column_names(table_name, schema=None)

Get column names for a specific table.

Parameters:

Name Type Description Default
table_name str

Name of the table.

required
schema Optional[str]

Schema name.

None

Returns:

Type Description
List[str]

List of column names.

Source code in gigaspatial/core/io/database.py
def get_column_names(
    self, table_name: str, schema: Optional[str] = None
) -> List[str]:
    """
    Get column names for a specific table.

    Args:
        table_name: Name of the table.
        schema: Schema name.

    Returns:
        List of column names.
    """

    columns = self.get_table_info(table_name, schema=schema)
    return [col["name"] for col in columns]
get_connection_string()

Returns the connection string used to create the engine.

Returns:

Type Description
str

The raw connection string.

Source code in gigaspatial/core/io/database.py
def get_connection_string(self) -> str:
    """
    Returns the connection string used to create the engine.

    Returns:
        The raw connection string.
    """
    return self._connection_string
get_extensions()

Get list of installed PostgreSQL extensions.

Returns:

Type Description
List[str]

List of extension names.

Raises:

Type Description
NotImplementedError

If database type is not PostgreSQL.

Source code in gigaspatial/core/io/database.py
def get_extensions(self) -> List[str]:
    """
    Get list of installed PostgreSQL extensions.

    Returns:
        List of extension names.

    Raises:
        NotImplementedError: If database type is not PostgreSQL.
    """
    if self.db_type != "postgresql":
        raise NotImplementedError(
            "This method is only available for PostgreSQL connections"
        )

    with self.engine.connect() as conn:
        result = conn.execute("SELECT extname FROM pg_extension")
        return [row[0] for row in result]
get_primary_keys(table_name, schema=None)

Get primary key columns for a table.

Parameters:

Name Type Description Default
table_name str

Name of the table.

required
schema Optional[str]

Schema name.

None

Returns:

Type Description
List[str]

List of primary key column names.

Source code in gigaspatial/core/io/database.py
def get_primary_keys(
    self, table_name: str, schema: Optional[str] = None
) -> List[str]:
    """
    Get primary key columns for a table.

    Args:
        table_name: Name of the table.
        schema: Schema name.

    Returns:
        List of primary key column names.
    """
    schema, table_name = self._parse_schema_table(table_name, schema)

    inspector = inspect(self.engine)
    try:
        return inspector.get_pk_constraint(table_name, schema=schema)[
            "constrained_columns"
        ]
    except:
        return []  # Some databases may not support PK constraints
get_schema_names()

Get list of all schema names in the database.

Returns:

Type Description
List[str]

List of schema names.

Source code in gigaspatial/core/io/database.py
def get_schema_names(self) -> List[str]:
    """
    Get list of all schema names in the database.

    Returns:
        List of schema names.
    """
    inspector = inspect(self.engine)
    return inspector.get_schema_names()
get_table_info(table_name, schema=None)

Get detailed column information for a table.

Parameters:

Name Type Description Default
table_name str

Name of the table.

required
schema Optional[str]

Schema name.

None

Returns:

Type Description
List[Dict]

List of column metadata dictionaries.

Source code in gigaspatial/core/io/database.py
def get_table_info(
    self, table_name: str, schema: Optional[str] = None
) -> List[Dict]:
    """
    Get detailed column information for a table.

    Args:
        table_name: Name of the table.
        schema: Schema name.

    Returns:
        List of column metadata dictionaries.
    """
    orig_name = table_name
    schema, table_name = self._parse_schema_table(table_name, schema)
    try:
        inspector = inspect(self.engine)
        return inspector.get_columns(table_name, schema=schema)
    except Exception:
        # Fallback for Trino cross-catalog issues or other reflection failures
        if self.db_type == "trino":
            # Reconstruct path for DESCRIBE
            path = (
                orig_name
                if "." in orig_name
                else f"{schema}.{table_name}"
                if schema
                else table_name
            )
            try:
                # DESCRIBE returns Column, Type, Extra, Comment
                df = self.read_sql_to_dataframe(f"DESCRIBE {path}")
                return [
                    {
                        "name": row["Column"],
                        "type": row["Type"],
                        "nullable": True,
                        "default": None,
                        "autoincrement": False,
                        "comment": row.get("Comment"),
                    }
                    for _, row in df.iterrows()
                ]
            except Exception:
                pass
        raise
get_table_names(schema=None)

Get list of table names in a schema.

Parameters:

Name Type Description Default
schema Optional[str]

Schema name. Defaults to default_schema.

None

Returns:

Type Description
List[str]

List of table names.

Source code in gigaspatial/core/io/database.py
def get_table_names(self, schema: Optional[str] = None) -> List[str]:
    """
    Get list of table names in a schema.

    Args:
        schema: Schema name. Defaults to default_schema.

    Returns:
        List of table names.
    """
    schema = schema or self.default_schema
    inspector = inspect(self.engine)
    return inspector.get_table_names(schema=schema)
get_view_names(schema=None)

Get list of view names in a schema.

Parameters:

Name Type Description Default
schema Optional[str]

Schema name. Defaults to default_schema.

None

Returns:

Type Description
List[str]

List of view names.

Source code in gigaspatial/core/io/database.py
def get_view_names(self, schema: Optional[str] = None) -> List[str]:
    """
    Get list of view names in a schema.

    Args:
        schema: Schema name. Defaults to default_schema.

    Returns:
        List of view names.
    """
    schema = schema or self.default_schema
    inspector = inspect(self.engine)
    return inspector.get_view_names(schema=schema)
read_sql_to_dask_dataframe(table_name, index_col, columns=None, limit=None, **kwargs)

Reads data into a Dask DataFrame.

Parameters:

Name Type Description Default
table_name str

Table name (schema.table or just table).

required
index_col str

Column to use as index for partitioning.

required
columns Optional[List[str]]

List of columns to select.

None
limit Optional[int]

Maximum rows to return.

None
**kwargs

Additional arguments for dd.read_sql_query.

{}

Returns:

Type Description
DataFrame

Dask DataFrame with results.

Raises:

Type Description
ImportError

If 'dask' is not installed.

ValueError

If reading fails.

Source code in gigaspatial/core/io/database.py
def read_sql_to_dask_dataframe(
    self,
    table_name: str,
    index_col: str,
    columns: Optional[List[str]] = None,
    limit: Optional[int] = None,
    **kwargs,
) -> "dd.DataFrame":
    """
    Reads data into a Dask DataFrame.

    Args:
        table_name: Table name (schema.table or just table).
        index_col: Column to use as index for partitioning.
        columns: List of columns to select.
        limit: Maximum rows to return.
        **kwargs: Additional arguments for dd.read_sql_query.

    Returns:
        Dask DataFrame with results.

    Raises:
        ImportError: If 'dask' is not installed.
        ValueError: If reading fails.
    """
    if not _HAS_DASK:
        raise ImportError(
            "read_sql_to_dask_dataframe requires 'dask'. "
            "Install it with: pip install 'giga-spatial[db]'"
        )
    try:
        connection_string = self.get_connection_string()

        # Handle schema.table format
        schema, table = self._parse_schema_table(table_name)

        metadata = MetaData()
        table_obj = Table(table, metadata, schema=schema, autoload_with=self.engine)

        # Build query
        query = (
            select(*[table_obj.c[col] for col in columns])
            if columns
            else select(table_obj)
        )
        if limit:
            query = query.limit(limit)

        return dd.read_sql_query(
            sql=query, con=connection_string, index_col=index_col, **kwargs
        )
    except Exception as e:
        print(f"Error reading SQL to Dask DataFrame: {e}")
        raise ValueError(f"Failed to read SQL to Dask DataFrame: {e}") from e
read_sql_to_dataframe(query, params=None)

Executes query and returns results as pandas DataFrame.

Parameters:

Name Type Description Default
query str

SQL query string to execute.

required
params Optional[Dict]

Optional parameters for the query.

None

Returns:

Type Description
DataFrame

pandas DataFrame containing the query results.

Raises:

Type Description
SQLAlchemyError

If query execution fails.

Source code in gigaspatial/core/io/database.py
def read_sql_to_dataframe(
    self, query: str, params: Optional[Dict] = None
) -> pd.DataFrame:
    """
    Executes query and returns results as pandas DataFrame.

    Args:
        query: SQL query string to execute.
        params: Optional parameters for the query.

    Returns:
        pandas DataFrame containing the query results.

    Raises:
        SQLAlchemyError: If query execution fails.
    """
    try:
        with self.engine.connect() as connection:
            return pd.read_sql_query(text(query), connection, params=params)
    except SQLAlchemyError as e:
        print(f"Error reading SQL to DataFrame: {e}")
        raise
table_exists(table_name, schema=None)

Check if a table exists in the database.

Parameters:

Name Type Description Default
table_name str

Name of the table.

required
schema Optional[str]

Schema name.

None

Returns:

Type Description
bool

True if table exists, False otherwise.

Source code in gigaspatial/core/io/database.py
def table_exists(self, table_name: str, schema: Optional[str] = None) -> bool:
    """
    Check if a table exists in the database.

    Args:
        table_name: Name of the table.
        schema: Schema name.

    Returns:
        True if table exists, False otherwise.
    """
    try:
        columns = self.get_table_info(table_name, schema=schema)
        return len(columns) > 0
    except Exception:
        return False
test_connection()

Tests the database connection.

Returns:

Type Description
bool

True if connection successful, False otherwise.

Source code in gigaspatial/core/io/database.py
def test_connection(self) -> bool:
    """
    Tests the database connection.

    Returns:
        True if connection successful, False otherwise.
    """
    test_query = (
        "SELECT 1"
        if self.db_type == "postgresql"
        else "SELECT 1 AS connection_test"
    )

    try:
        print(
            f"Attempting to connect to {self.db_type} at {self.host}:{self.port}..."
        )
        with self.engine.connect() as conn:
            conn.execute(text(test_query))
        print(f"Successfully connected to {self.db_type.upper()}.")
        return True
    except Exception as e:
        print(f"Failed to connect to {self.db_type.upper()}: {e}")
        return False

delta_sharing_data_store

DeltaSharingConfig

Bases: BaseModel

Configuration for Delta Sharing connection.

Attributes:

Name Type Description
profile_file Optional[Path]

Path to the delta-sharing profile file (JSON format).

share_name Optional[str]

Name of the share to access.

schema_name Optional[str]

Name of the schema within the share.

enable_cache bool

Whether to cache loaded tables in memory.

Source code in gigaspatial/core/io/delta_sharing_data_store.py
class DeltaSharingConfig(BaseModel):
    """Configuration for Delta Sharing connection.

    Attributes:
        profile_file: Path to the delta-sharing profile file (JSON format).
        share_name: Name of the share to access.
        schema_name: Name of the schema within the share.
        enable_cache: Whether to cache loaded tables in memory.
    """

    model_config = ConfigDict(frozen=True, validate_assignment=True)

    # All optional so can be composed from global_config + overrides
    profile_file: Optional[Path] = Field(
        default=None, description="Path to Delta Sharing profile configuration file"
    )
    share_name: Optional[str] = Field(
        default=None, description="Share name in Delta Sharing catalog"
    )
    schema_name: Optional[str] = Field(
        default=None, description="Schema name within the share"
    )
    enable_cache: bool = Field(
        default=True, description="Enable in-memory caching of loaded tables"
    )

    @field_validator("profile_file", mode="before")
    @classmethod
    def validate_profile_path(cls, v: Optional[Union[str, Path]]) -> Optional[Path]:
        """Ensure profile file exists and is valid if provided."""
        if v is None:
            return None
        path = Path(v)
        if not path.exists():
            raise ValueError(f"Profile file not found: {path}")
        if not path.is_file():
            raise ValueError(f"Profile path is not a file: {path}")
        return path
validate_profile_path(v) classmethod

Ensure profile file exists and is valid if provided.

Source code in gigaspatial/core/io/delta_sharing_data_store.py
@field_validator("profile_file", mode="before")
@classmethod
def validate_profile_path(cls, v: Optional[Union[str, Path]]) -> Optional[Path]:
    """Ensure profile file exists and is valid if provided."""
    if v is None:
        return None
    path = Path(v)
    if not path.exists():
        raise ValueError(f"Profile file not found: {path}")
    if not path.is_file():
        raise ValueError(f"Profile path is not a file: {path}")
    return path
DeltaSharingDataStore

General-purpose Delta Sharing data accessor.

Source code in gigaspatial/core/io/delta_sharing_data_store.py
class DeltaSharingDataStore:
    """General-purpose Delta Sharing data accessor."""

    def __init__(
        self,
        *,
        # Global config values with individual override capability
        profile_file: Optional[Union[str, Path]] = global_config.API_PROFILE_FILE_PATH,
        share_name: Optional[str] = global_config.API_SHARE_NAME,
        schema_name: Optional[str] = global_config.API_SCHEMA_NAME,
        enable_cache: Optional[bool] = None,
    ):
        if not _HAS_DELTA_SHARING:
            raise ImportError(
                "DeltaSharingDataStore requires 'delta-sharing'. "
                "Install it with: pip install 'giga-spatial[delta]'"
            )
        """
        Initialize with selective overrides of global config.

        Priority order (highest to lowest):
        1. Explicit kwargs
        2. config.* values (from env vars)
        3. Validation errors if required fields missing

        Examples:
            # Use all global config defaults
            store = DeltaSharingDataStore()

            # Use global share/profile but override schema
            store = DeltaSharingDataStore(schema_name="my-schema")

            # Fully explicit
            store = DeltaSharingDataStore(
                profile_file="/path/to/profile.json",
                share_name="gold",
                schema_name="school-master"
            )
        """
        # Validate required fields
        missing = []
        if profile_file is None:
            missing.append("profile_file")
        if share_name is None:
            missing.append("share_name")
        if schema_name is None:
            missing.append("schema_name")

        if missing:
            raise ValueError(
                f"Missing required config: {', '.join(missing)}. "
                "Set env vars (API_PROFILE_FILE_PATH, API_SHARE_NAME, API_SCHEMA_NAME) "
                "or pass them explicitly."
            )

        # Create validated config
        self.config = DeltaSharingConfig(
            profile_file=Path(profile_file),
            share_name=share_name,
            schema_name=schema_name,
            enable_cache=enable_cache if enable_cache is not None else True,
        )

        self._client: Optional["delta_sharing.SharingClient"] = None
        self._cache: Dict[str, pd.DataFrame] = {}

        LOGGER.info(
            "Initialized DeltaSharingDataStore with "
            f"share={self.config.share_name}, schema={self.config.schema_name}, "
            f"profile={self.config.profile_file}"
        )

    @property
    def client(self) -> "delta_sharing.SharingClient":
        """Lazy-load Delta Sharing client."""
        if self._client is None:
            self._client = delta_sharing.SharingClient(str(self.config.profile_file))
            LOGGER.debug(f"Created SharingClient from {self.config.profile_file}")
        return self._client

    def list_tables(
        self,
        schema_filter: Optional[str] = None,
        sort: bool = True,
    ) -> List[str]:
        """List all available tables in the configured schema."""
        schema = schema_filter or self.config.schema_name
        if schema is None:
            raise RuntimeError(
                "Schema name is not configured. "
                "Set 'schema_name' via env/global_config or pass it explicitly."
            )

        # 1) Try list_all_tables first
        all_tables = list(self.client.list_all_tables())
        if all_tables:
            table_names = [t.name for t in all_tables if t.schema == schema]
        else:
            # 2) Fallback: enumerate from configured share + schema
            LOGGER.debug(
                "list_all_tables() returned empty; falling back to "
                "share/schema enumeration"
            )
            # Find matching share
            shares = list(self.client.list_shares())
            try:
                share = next(s for s in shares if s.name == self.config.share_name)
            except StopIteration:
                LOGGER.warning(
                    "Configured share '%s' not found in list_shares()",
                    self.config.share_name,
                )
                return []

            # Find matching schema within that share
            schemas = list(self.client.list_schemas(share))
            try:
                schema_obj = next(s for s in schemas if s.name == schema)
            except StopIteration:
                LOGGER.warning(
                    "Configured schema '%s' not found in share '%s'",
                    schema,
                    self.config.share_name,
                )
                return []

            # List tables for that share+schema
            tables = list(self.client.list_tables(schema_obj))
            table_names = [t.name for t in tables]

        if sort:
            table_names.sort()
        return table_names

    def load_table(
        self,
        table_name: str,
        filters: Optional[Dict[str, Any]] = None,
        use_cache: Optional[bool] = None,
    ) -> pd.DataFrame:
        """Load a table from Delta Sharing with optional filtering."""
        if self.config.share_name is None or self.config.schema_name is None:
            raise RuntimeError(
                "share_name and schema_name must be configured before loading tables."
            )

        effective_cache = (
            use_cache if use_cache is not None else self.config.enable_cache
        )

        # Cache
        if effective_cache and table_name in self._cache:
            df = self._cache[table_name]
        else:
            table_url = (
                f"{self.config.profile_file}#"
                f"{self.config.share_name}."
                f"{self.config.schema_name}."
                f"{table_name}"
            )
            df = delta_sharing.load_as_pandas(table_url)
            if effective_cache:
                self._cache[table_name] = df

        if filters:
            for column, value in filters.items():
                if column not in df.columns:
                    raise ValueError(
                        f"Filter column '{column}' not found in table '{table_name}'"
                    )
                df = df[df[column] == value]

        return df

    def load_multiple_tables(
        self,
        table_names: List[str],
        filters: Optional[Dict[str, Any]] = None,
    ) -> pd.DataFrame:
        """Load and concatenate multiple tables."""
        dfs = [self.load_table(name, filters=filters) for name in table_names]
        return pd.concat(dfs, ignore_index=True)

    def get_table_metadata(self, table_name: str) -> Dict[str, Any]:
        """Retrieve metadata for a table."""
        df = self.load_table(table_name)
        return {
            "table_name": table_name,
            "columns": df.columns.tolist(),
            "data_types": {k: str(v) for k, v in df.dtypes.to_dict().items()},
            "num_records": len(df),
            "memory_usage_mb": df.memory_usage(deep=True).sum() / 1024**2,
        }

    def clear_cache(self, table_name: Optional[str] = None) -> None:
        """Clear cached data."""
        if table_name is None:
            self._cache.clear()
        else:
            self._cache.pop(table_name, None)

    def get_cached_tables(self) -> List[str]:
        """Get list of currently cached table names."""
        return list(self._cache.keys())

    @property
    def cache_size_mb(self) -> float:
        """Total memory used by cache in megabytes."""
        total_bytes = sum(
            df.memory_usage(deep=True).sum() for df in self._cache.values()
        )
        return total_bytes / 1024**2
cache_size_mb: float property

Total memory used by cache in megabytes.

client: delta_sharing.SharingClient property

Lazy-load Delta Sharing client.

clear_cache(table_name=None)

Clear cached data.

Source code in gigaspatial/core/io/delta_sharing_data_store.py
def clear_cache(self, table_name: Optional[str] = None) -> None:
    """Clear cached data."""
    if table_name is None:
        self._cache.clear()
    else:
        self._cache.pop(table_name, None)
get_cached_tables()

Get list of currently cached table names.

Source code in gigaspatial/core/io/delta_sharing_data_store.py
def get_cached_tables(self) -> List[str]:
    """Get list of currently cached table names."""
    return list(self._cache.keys())
get_table_metadata(table_name)

Retrieve metadata for a table.

Source code in gigaspatial/core/io/delta_sharing_data_store.py
def get_table_metadata(self, table_name: str) -> Dict[str, Any]:
    """Retrieve metadata for a table."""
    df = self.load_table(table_name)
    return {
        "table_name": table_name,
        "columns": df.columns.tolist(),
        "data_types": {k: str(v) for k, v in df.dtypes.to_dict().items()},
        "num_records": len(df),
        "memory_usage_mb": df.memory_usage(deep=True).sum() / 1024**2,
    }
list_tables(schema_filter=None, sort=True)

List all available tables in the configured schema.

Source code in gigaspatial/core/io/delta_sharing_data_store.py
def list_tables(
    self,
    schema_filter: Optional[str] = None,
    sort: bool = True,
) -> List[str]:
    """List all available tables in the configured schema."""
    schema = schema_filter or self.config.schema_name
    if schema is None:
        raise RuntimeError(
            "Schema name is not configured. "
            "Set 'schema_name' via env/global_config or pass it explicitly."
        )

    # 1) Try list_all_tables first
    all_tables = list(self.client.list_all_tables())
    if all_tables:
        table_names = [t.name for t in all_tables if t.schema == schema]
    else:
        # 2) Fallback: enumerate from configured share + schema
        LOGGER.debug(
            "list_all_tables() returned empty; falling back to "
            "share/schema enumeration"
        )
        # Find matching share
        shares = list(self.client.list_shares())
        try:
            share = next(s for s in shares if s.name == self.config.share_name)
        except StopIteration:
            LOGGER.warning(
                "Configured share '%s' not found in list_shares()",
                self.config.share_name,
            )
            return []

        # Find matching schema within that share
        schemas = list(self.client.list_schemas(share))
        try:
            schema_obj = next(s for s in schemas if s.name == schema)
        except StopIteration:
            LOGGER.warning(
                "Configured schema '%s' not found in share '%s'",
                schema,
                self.config.share_name,
            )
            return []

        # List tables for that share+schema
        tables = list(self.client.list_tables(schema_obj))
        table_names = [t.name for t in tables]

    if sort:
        table_names.sort()
    return table_names
load_multiple_tables(table_names, filters=None)

Load and concatenate multiple tables.

Source code in gigaspatial/core/io/delta_sharing_data_store.py
def load_multiple_tables(
    self,
    table_names: List[str],
    filters: Optional[Dict[str, Any]] = None,
) -> pd.DataFrame:
    """Load and concatenate multiple tables."""
    dfs = [self.load_table(name, filters=filters) for name in table_names]
    return pd.concat(dfs, ignore_index=True)
load_table(table_name, filters=None, use_cache=None)

Load a table from Delta Sharing with optional filtering.

Source code in gigaspatial/core/io/delta_sharing_data_store.py
def load_table(
    self,
    table_name: str,
    filters: Optional[Dict[str, Any]] = None,
    use_cache: Optional[bool] = None,
) -> pd.DataFrame:
    """Load a table from Delta Sharing with optional filtering."""
    if self.config.share_name is None or self.config.schema_name is None:
        raise RuntimeError(
            "share_name and schema_name must be configured before loading tables."
        )

    effective_cache = (
        use_cache if use_cache is not None else self.config.enable_cache
    )

    # Cache
    if effective_cache and table_name in self._cache:
        df = self._cache[table_name]
    else:
        table_url = (
            f"{self.config.profile_file}#"
            f"{self.config.share_name}."
            f"{self.config.schema_name}."
            f"{table_name}"
        )
        df = delta_sharing.load_as_pandas(table_url)
        if effective_cache:
            self._cache[table_name] = df

    if filters:
        for column, value in filters.items():
            if column not in df.columns:
                raise ValueError(
                    f"Filter column '{column}' not found in table '{table_name}'"
                )
            df = df[df[column] == value]

    return df

local_data_store

Module for local filesystem DataStore implementation. Provides access to local files and directories using a consistent DataStore interface.

LocalDataStore

Bases: DataStore

Implementation for local filesystem storage.

Source code in gigaspatial/core/io/local_data_store.py
class LocalDataStore(DataStore):
    """Implementation for local filesystem storage."""

    def __init__(self, base_path: Union[str, Path] = ""):
        """
        Initialize the local data store.

        Args:
            base_path: Base directory for relative paths. Defaults to current directory.
        """
        super().__init__()
        self.base_path = Path(base_path).resolve()

    def _resolve_path(self, path: Pathish) -> Path:
        path_obj = Path(path)

        # If absolute, return as-is
        if path_obj.is_absolute():
            return path_obj.resolve()

        # Otherwise, resolve relative to base_path
        return (self.base_path / path_obj).resolve()

    def read_file(self, path: str) -> bytes:
        """
        Read contents of a file as bytes.

        Args:
            path: Path to the file.

        Returns:
            File contents in bytes.
        """
        full_path = self._resolve_path(path)
        with open(full_path, "rb") as f:
            return f.read()

    def write_file(self, path: str, data: Union[bytes, str]) -> None:
        """
        Write data (string or bytes) to a file.
        Automatically creates parent directories if they don't exist.

        Args:
            path: Path where to write.
            data: Data to write (str or bytes).
        """
        full_path = self._resolve_path(path)
        self.mkdir(str(full_path.parent), exist_ok=True)

        if isinstance(data, str):
            mode = "w"
            encoding = "utf-8"
        else:
            mode = "wb"
            encoding = None

        with open(full_path, mode, encoding=encoding) as f:
            f.write(data)

    def file_exists(self, path: str) -> bool:
        """Checks if file exists at path."""
        return self._resolve_path(path).is_file()

    def list_files(self, path: str) -> List[str]:
        """
        List all files in a directory, returning relative paths from base_path.

        Args:
            path: Directory to list.

        Returns:
            List of relative file paths.
        """
        full_path = self._resolve_path(path)
        return [
            str(f.relative_to(self.base_path))
            for f in full_path.iterdir()
            if f.is_file()
        ]

    def walk(self, top: str) -> Generator[Tuple[str, List[str], List[str]], None, None]:
        """
        Walk through directory tree.

        Args:
            top: Starting directory.

        Yields:
            Tuple of (relative_root, dirnames, filenames).
        """
        full_path = self._resolve_path(top)
        for root, dirs, files in os.walk(full_path):
            rel_root = str(Path(root).relative_to(self.base_path))
            yield rel_root, dirs, files

    def list_directories(self, path: str) -> List[str]:
        """
        List immediate subdirectories in path.

        Args:
            path: Directory to list.

        Returns:
            List of subdirectory names.
        """
        full_path = self._resolve_path(path)

        if not full_path.exists():
            return []

        if not full_path.is_dir():
            return []

        return [d.name for d in full_path.iterdir() if d.is_dir()]

    def open(self, path: str, mode: str = "r") -> IO:
        """
        Open a file-like object using the local filesystem.

        Args:
            path: File path.
            mode: Open mode.

        Returns:
            File descriptor.
        """
        full_path = self._resolve_path(path)
        self.mkdir(str(full_path.parent), exist_ok=True)
        return open(full_path, mode)

    def is_file(self, path: str) -> bool:
        """Checks if path is a file."""
        return self._resolve_path(path).is_file()

    def is_dir(self, path: str) -> bool:
        """Checks if path is a directory."""
        return self._resolve_path(path).is_dir()

    def remove(self, path: str) -> None:
        """Removes a file."""
        full_path = self._resolve_path(path)
        if full_path.is_file():
            os.remove(full_path)

    def copy_file(self, src: str, dst: str) -> None:
        """Copy a file from src to dst."""
        src_path = self._resolve_path(src)
        dst_path = self._resolve_path(dst)
        self.mkdir(str(dst_path.parent), exist_ok=True)
        shutil.copy2(src_path, dst_path)

    def rmdir(self, directory: str) -> None:
        """Removes a directory."""
        full_path = self._resolve_path(directory)
        if full_path.is_dir():
            os.rmdir(full_path)

    def mkdir(self, path: str, exist_ok: bool = False) -> None:
        """Creates a directory and its parents."""
        full_path = self._resolve_path(path)
        full_path.mkdir(parents=True, exist_ok=exist_ok)

    def exists(self, path: str) -> bool:
        """Checks if path exists."""
        return self._resolve_path(path).exists()
__init__(base_path='')

Initialize the local data store.

Parameters:

Name Type Description Default
base_path Union[str, Path]

Base directory for relative paths. Defaults to current directory.

''
Source code in gigaspatial/core/io/local_data_store.py
def __init__(self, base_path: Union[str, Path] = ""):
    """
    Initialize the local data store.

    Args:
        base_path: Base directory for relative paths. Defaults to current directory.
    """
    super().__init__()
    self.base_path = Path(base_path).resolve()
copy_file(src, dst)

Copy a file from src to dst.

Source code in gigaspatial/core/io/local_data_store.py
def copy_file(self, src: str, dst: str) -> None:
    """Copy a file from src to dst."""
    src_path = self._resolve_path(src)
    dst_path = self._resolve_path(dst)
    self.mkdir(str(dst_path.parent), exist_ok=True)
    shutil.copy2(src_path, dst_path)
exists(path)

Checks if path exists.

Source code in gigaspatial/core/io/local_data_store.py
def exists(self, path: str) -> bool:
    """Checks if path exists."""
    return self._resolve_path(path).exists()
file_exists(path)

Checks if file exists at path.

Source code in gigaspatial/core/io/local_data_store.py
def file_exists(self, path: str) -> bool:
    """Checks if file exists at path."""
    return self._resolve_path(path).is_file()
is_dir(path)

Checks if path is a directory.

Source code in gigaspatial/core/io/local_data_store.py
def is_dir(self, path: str) -> bool:
    """Checks if path is a directory."""
    return self._resolve_path(path).is_dir()
is_file(path)

Checks if path is a file.

Source code in gigaspatial/core/io/local_data_store.py
def is_file(self, path: str) -> bool:
    """Checks if path is a file."""
    return self._resolve_path(path).is_file()
list_directories(path)

List immediate subdirectories in path.

Parameters:

Name Type Description Default
path str

Directory to list.

required

Returns:

Type Description
List[str]

List of subdirectory names.

Source code in gigaspatial/core/io/local_data_store.py
def list_directories(self, path: str) -> List[str]:
    """
    List immediate subdirectories in path.

    Args:
        path: Directory to list.

    Returns:
        List of subdirectory names.
    """
    full_path = self._resolve_path(path)

    if not full_path.exists():
        return []

    if not full_path.is_dir():
        return []

    return [d.name for d in full_path.iterdir() if d.is_dir()]
list_files(path)

List all files in a directory, returning relative paths from base_path.

Parameters:

Name Type Description Default
path str

Directory to list.

required

Returns:

Type Description
List[str]

List of relative file paths.

Source code in gigaspatial/core/io/local_data_store.py
def list_files(self, path: str) -> List[str]:
    """
    List all files in a directory, returning relative paths from base_path.

    Args:
        path: Directory to list.

    Returns:
        List of relative file paths.
    """
    full_path = self._resolve_path(path)
    return [
        str(f.relative_to(self.base_path))
        for f in full_path.iterdir()
        if f.is_file()
    ]
mkdir(path, exist_ok=False)

Creates a directory and its parents.

Source code in gigaspatial/core/io/local_data_store.py
def mkdir(self, path: str, exist_ok: bool = False) -> None:
    """Creates a directory and its parents."""
    full_path = self._resolve_path(path)
    full_path.mkdir(parents=True, exist_ok=exist_ok)
open(path, mode='r')

Open a file-like object using the local filesystem.

Parameters:

Name Type Description Default
path str

File path.

required
mode str

Open mode.

'r'

Returns:

Type Description
IO

File descriptor.

Source code in gigaspatial/core/io/local_data_store.py
def open(self, path: str, mode: str = "r") -> IO:
    """
    Open a file-like object using the local filesystem.

    Args:
        path: File path.
        mode: Open mode.

    Returns:
        File descriptor.
    """
    full_path = self._resolve_path(path)
    self.mkdir(str(full_path.parent), exist_ok=True)
    return open(full_path, mode)
read_file(path)

Read contents of a file as bytes.

Parameters:

Name Type Description Default
path str

Path to the file.

required

Returns:

Type Description
bytes

File contents in bytes.

Source code in gigaspatial/core/io/local_data_store.py
def read_file(self, path: str) -> bytes:
    """
    Read contents of a file as bytes.

    Args:
        path: Path to the file.

    Returns:
        File contents in bytes.
    """
    full_path = self._resolve_path(path)
    with open(full_path, "rb") as f:
        return f.read()
remove(path)

Removes a file.

Source code in gigaspatial/core/io/local_data_store.py
def remove(self, path: str) -> None:
    """Removes a file."""
    full_path = self._resolve_path(path)
    if full_path.is_file():
        os.remove(full_path)
rmdir(directory)

Removes a directory.

Source code in gigaspatial/core/io/local_data_store.py
def rmdir(self, directory: str) -> None:
    """Removes a directory."""
    full_path = self._resolve_path(directory)
    if full_path.is_dir():
        os.rmdir(full_path)
walk(top)

Walk through directory tree.

Parameters:

Name Type Description Default
top str

Starting directory.

required

Yields:

Type Description
Tuple[str, List[str], List[str]]

Tuple of (relative_root, dirnames, filenames).

Source code in gigaspatial/core/io/local_data_store.py
def walk(self, top: str) -> Generator[Tuple[str, List[str], List[str]], None, None]:
    """
    Walk through directory tree.

    Args:
        top: Starting directory.

    Yields:
        Tuple of (relative_root, dirnames, filenames).
    """
    full_path = self._resolve_path(top)
    for root, dirs, files in os.walk(full_path):
        rel_root = str(Path(root).relative_to(self.base_path))
        yield rel_root, dirs, files
write_file(path, data)

Write data (string or bytes) to a file. Automatically creates parent directories if they don't exist.

Parameters:

Name Type Description Default
path str

Path where to write.

required
data Union[bytes, str]

Data to write (str or bytes).

required
Source code in gigaspatial/core/io/local_data_store.py
def write_file(self, path: str, data: Union[bytes, str]) -> None:
    """
    Write data (string or bytes) to a file.
    Automatically creates parent directories if they don't exist.

    Args:
        path: Path where to write.
        data: Data to write (str or bytes).
    """
    full_path = self._resolve_path(path)
    self.mkdir(str(full_path.parent), exist_ok=True)

    if isinstance(data, str):
        mode = "w"
        encoding = "utf-8"
    else:
        mode = "wb"
        encoding = None

    with open(full_path, mode, encoding=encoding) as f:
        f.write(data)

readers

Module for reading geospatial and tabular datasets from a DataStore. Supports various formats including CSV, Excel, KML, KMZ, GeoJSON, Parquet, and Shapefiles, with automatic compression detection (gzip, bz2, xz).

read_dataset(path, data_store=None, compression=None, validate_crs=True, **kwargs)

Read data from various file formats stored in local or cloud-based storage.

Supports plain and compressed formats. For compound extensions such as .csv.gz or .geojson.gz, the inner format is detected automatically.

Parameters:

Name Type Description Default
path str

Path to the file in data storage.

required
data_store DataStore

DataStore instance for accessing storage. If None, local storage is used.

None
compression str

Explicit compression type ("gzip", "bz2", "xz"). If None, inferred from the file extension.

None
validate_crs bool

If True, emit a UserWarning when a GeoDataFrame has no CRS.

True
**kwargs

Additional arguments forwarded to the underlying reader function (e.g., sep, encoding, driver).

{}

Returns:

Type Description
Union[DataFrame, GeoDataFrame]

The data read from the file as a DataFrame or GeoDataFrame.

Raises:

Type Description
FileNotFoundError

If the file does not exist in the storage backend.

ValueError

If the file type is unsupported or the file cannot be parsed.

RuntimeError

For unexpected errors during the read process.

Source code in gigaspatial/core/io/readers.py
def read_dataset(
    path: str,
    data_store: DataStore = None,
    compression: str = None,
    validate_crs: bool = True,
    **kwargs,
) -> Union[pd.DataFrame, gpd.GeoDataFrame]:
    """
    Read data from various file formats stored in local or cloud-based storage.

    Supports plain and compressed formats. For compound extensions such as
    ``.csv.gz`` or ``.geojson.gz``, the inner format is detected automatically.

    Args:
        path: Path to the file in data storage.
        data_store: DataStore instance for accessing storage. If None, local storage is used.
        compression: Explicit compression type ("gzip", "bz2", "xz").
            If None, inferred from the file extension.
        validate_crs: If True, emit a UserWarning when a GeoDataFrame has no CRS.
        **kwargs: Additional arguments forwarded to the underlying reader function
            (e.g., `sep`, `encoding`, `driver`).

    Returns:
        The data read from the file as a DataFrame or GeoDataFrame.

    Raises:
        FileNotFoundError: If the file does not exist in the storage backend.
        ValueError: If the file type is unsupported or the file cannot be parsed.
        RuntimeError: For unexpected errors during the read process.
    """
    try:
        # ------------------------------------------------------------------ #
        # 1. Store check
        # ------------------------------------------------------------------ #
        data_store = data_store or LocalDataStore()

        # ------------------------------------------------------------------ #
        # 2. Existence check
        # ------------------------------------------------------------------ #
        if not data_store.file_exists(path):
            storage_name = _storage_display_name(data_store)
            raise FileNotFoundError(f"File '{path}' not found in {storage_name}.")

        path_obj = Path(path)
        suffixes = path_obj.suffixes
        file_extension = suffixes[-1].lower() if suffixes else ""

        # ------------------------------------------------------------------ #
        # 2. Compressed file handling (.gz / .bz2 / .xz)
        # FIX: .zip is no longer in COMPRESSION_FORMATS; it is handled as a
        #      geo container below. This removes the double-routing bug.
        # ------------------------------------------------------------------ #
        inferred_compression = COMPRESSION_FORMATS.get(file_extension)
        active_compression = compression or inferred_compression

        if active_compression and file_extension in COMPRESSION_FORMATS:
            if len(suffixes) > 1:
                inner_ext = suffixes[-2].lower()

                if inner_ext == ".tar":
                    raise ValueError(
                        "Tar archives (.tar.gz) are not directly supported."
                    )

                # Compressed tabular file (e.g. .csv.gz, .json.gz)
                if inner_ext in PANDAS_READERS:
                    try:
                        with data_store.open(path, "rb") as f:
                            return PANDAS_READERS[inner_ext](
                                f, compression=active_compression, **kwargs
                            )
                    except Exception as e:
                        raise ValueError(
                            f"Error reading compressed tabular file '{path}': {e}"
                        ) from e

                # Compressed geo file (e.g. .geojson.gz, .fgb.gz)
                if inner_ext in GEO_READERS and active_compression == "gzip":
                    try:
                        with data_store.open(path, "rb") as f:
                            decompressed = gzip.decompress(f.read())
                        result = GEO_READERS[inner_ext](
                            io.BytesIO(decompressed), **kwargs
                        )
                        return _maybe_warn_crs(result, path, validate_crs)
                    except Exception as e:
                        raise ValueError(
                            f"Error reading compressed geo file '{path}': {e}"
                        ) from e

                if inner_ext in GEO_READERS and active_compression != "gzip":
                    raise ValueError(
                        f"Compression '{active_compression}' is not supported "
                        f"for geo formats. Only gzip is supported."
                    )

            else:
                # Bare .gz with no inner extension hint - try JSON lines then CSV
                # FIX: delegates to read_gzipped_json_or_csv instead of silently
                #      assuming CSV, which could return garbage for JSON payloads.
                result = read_gzipped_json_or_csv(path, data_store)
                if result is None:
                    raise ValueError(
                        f"Could not parse '{path}' as JSON lines or CSV. "
                        f"Use a compound extension (e.g. '.json.gz') to specify "
                        f"the inner format explicitly."
                    )
                return result

        # ------------------------------------------------------------------ #
        # 3. ZIP container - always geo (shapefile zip or zipped vector)
        # FIX: .zip is no longer ambiguously caught by COMPRESSION_FORMATS.
        # ------------------------------------------------------------------ #
        if file_extension == ".zip":
            try:
                with data_store.open(path, "rb") as f:
                    result = gpd.read_file(f, **kwargs)
                return _maybe_warn_crs(result, path, validate_crs)
            except Exception as e:
                raise ValueError(
                    f"Error reading ZIP file '{path}' as geospatial data: {e}"
                ) from e

        # ------------------------------------------------------------------ #
        # 4. Shapefile - requires sidecar files
        # FIX: inner `suffixes` variable renamed to `sidecar_extensions` to
        #      avoid shadowing the outer `suffixes = path_obj.suffixes`.
        # ------------------------------------------------------------------ #
        if file_extension == ".shp":
            base_path = Path(path)
            with tempfile.TemporaryDirectory() as tmp_dir:
                local_shp_path = None
                for ext in SHAPEFILE_SIDECAR_EXTENSIONS:
                    part_path = str(base_path.with_suffix(ext))
                    if data_store.file_exists(part_path):
                        local_part = os.path.join(
                            tmp_dir, base_path.with_suffix(ext).name
                        )
                        content = data_store.read_file(part_path)
                        with open(local_part, "wb") as lf:
                            lf.write(content)
                        if ext == ".shp":
                            local_shp_path = local_part

                if local_shp_path is None:
                    raise FileNotFoundError(
                        f"Main .shp file could not be retrieved for path: {path}"
                    )
                result = gpd.read_file(local_shp_path, **kwargs)
            return _maybe_warn_crs(result, path, validate_crs)

        # ------------------------------------------------------------------ #
        # 5. Tabular formats (CSV, Excel)
        # ------------------------------------------------------------------ #
        if file_extension in PANDAS_READERS:
            mode = "rb" if file_extension in BINARY_FORMATS else "r"
            try:
                with data_store.open(path, mode) as f:
                    return PANDAS_READERS[file_extension](f, **kwargs)
            except Exception as e:
                raise ValueError(f"Error reading '{path}' with pandas: {e}") from e

        # ------------------------------------------------------------------ #
        # 6. Geospatial formats
        # ------------------------------------------------------------------ #
        if file_extension in GEO_READERS:
            try:
                with data_store.open(path, "rb") as f:
                    result = GEO_READERS[file_extension](f, **kwargs)
                return _maybe_warn_crs(result, path, validate_crs)
            except Exception as e:
                # FIX: For parquet, fall back to pandas if geopandas fails
                # (non-spatial parquet is common in GigaSpatial pipelines).
                if file_extension == ".parquet":
                    try:
                        with data_store.open(path, "rb") as f:
                            return pd.read_parquet(f, **kwargs)
                    except Exception as e2:
                        raise ValueError(
                            f"Failed to read parquet '{path}' with geopandas "
                            f"({e}) and pandas ({e2})."
                        ) from e2
                raise ValueError(f"Error reading '{path}' with geopandas: {e}") from e

        # ------------------------------------------------------------------ #
        # 7. Unsupported format
        # FIX: added missing newline between formats and compressions in message
        # ------------------------------------------------------------------ #
        supported_formats = sorted(set(PANDAS_READERS) | set(GEO_READERS))
        supported_compressions = sorted(COMPRESSION_FORMATS)
        raise ValueError(
            f"Unsupported file type: '{file_extension}'\n"
            f"Supported formats:      {', '.join(supported_formats)}\n"
            f"Supported compressions: {', '.join(supported_compressions)}"
        )

    except (FileNotFoundError, ValueError):
        raise
    except Exception as e:
        raise RuntimeError(f"Unexpected error reading dataset '{path}': {e}") from e
read_datasets(paths, data_store=None, **kwargs)

Read multiple datasets from data storage at once.

Parameters:

Name Type Description Default
paths

List of file paths to read.

required
data_store DataStore

DataStore instance. If None, local storage is used.

None
**kwargs

Additional arguments passed to read_dataset for each file.

{}

Returns:

Type Description

Dictionary mapping paths to their corresponding DataFrames/GeoDataFrames.

Source code in gigaspatial/core/io/readers.py
def read_datasets(paths, data_store: DataStore = None, **kwargs):
    """
    Read multiple datasets from data storage at once.

    Args:
        paths: List of file paths to read.
        data_store: DataStore instance. If None, local storage is used.
        **kwargs: Additional arguments passed to `read_dataset` for each file.

    Returns:
        Dictionary mapping paths to their corresponding DataFrames/GeoDataFrames.
    """
    results = {}
    errors = {}

    data_store = data_store or LocalDataStore()

    if data_store is None:
        data_store = LocalDataStore()

    for path in paths:
        try:
            results[path] = read_dataset(path, data_store, **kwargs)
        except Exception as e:
            errors[path] = str(e)

    if errors:
        error_msg = "\n".join(f"- {path}: {error}" for path, error in errors.items())
        raise ValueError(f"Errors reading datasets:\n{error_msg}")

    return results
read_gzipped_json_or_csv(file_path, data_store=None)

Read a gzipped file, attempting JSON (lines=True) first, then CSV.

Parameters:

Name Type Description Default
file_path str

Path to the .gz file in the DataStore.

required
data_store DataStore

DataStore instance. If None, local storage is used.

None

Returns:

Type Description
Union[DataFrame, None]

Parsed DataFrame, or None if parsing fails for both formats.

Source code in gigaspatial/core/io/readers.py
def read_gzipped_json_or_csv(
    file_path: str, data_store: DataStore = None
) -> Union[pd.DataFrame, None]:
    """
    Read a gzipped file, attempting JSON (lines=True) first, then CSV.

    Args:
        file_path: Path to the .gz file in the DataStore.
        data_store: DataStore instance. If None, local storage is used.

    Returns:
        Parsed DataFrame, or None if parsing fails for both formats.
    """
    data_store = data_store or LocalDataStore()

    with data_store.open(file_path, "rb") as f:
        text = gzip.GzipFile(fileobj=f).read().decode("utf-8")

    try:
        return pd.read_json(io.StringIO(text), lines=True)
    except (json.JSONDecodeError, ValueError):
        pass

    try:
        return pd.read_csv(io.StringIO(text))
    except pd.errors.ParserError:
        logger.error("Could not parse '%s' as JSON lines or CSV.", file_path)
        return None
read_json(path, data_store=None)

Read a JSON file from a DataStore.

Parameters:

Name Type Description Default
path str

Path to the JSON file.

required
data_store DataStore

DataStore instance for accessing storage. If None, local storage is used.

None

Returns:

Type Description
dict

Parsed JSON content as a dictionary.

Source code in gigaspatial/core/io/readers.py
def read_json(path: str, data_store: DataStore = None) -> dict:
    """
    Read a JSON file from a DataStore.

    Args:
        path: Path to the JSON file.
        data_store: DataStore instance for accessing storage. If None, local storage is used.

    Returns:
        Parsed JSON content as a dictionary.
    """
    data_store = data_store or LocalDataStore()

    with data_store.open(path, "r") as f:
        return json.load(f)
read_kmz(file_obj, **kwargs)

Read a KMZ file and return a GeoDataFrame.

Parameters:

Name Type Description Default
file_obj

A file-like object pointing to a KMZ archive.

required
**kwargs

Additional keyword arguments passed to geopandas.read_file.

{}

Returns:

Type Description
GeoDataFrame

The geospatial data extracted from the KMZ.

Raises:

Type Description
ValueError

If no KML file is found in the archive or if content is empty.

RuntimeError

For unexpected errors during KMZ processing.

Source code in gigaspatial/core/io/readers.py
def read_kmz(file_obj, **kwargs) -> gpd.GeoDataFrame:
    """
    Read a KMZ file and return a GeoDataFrame.

    Args:
        file_obj: A file-like object pointing to a KMZ archive.
        **kwargs: Additional keyword arguments passed to `geopandas.read_file`.

    Returns:
        The geospatial data extracted from the KMZ.

    Raises:
        ValueError: If no KML file is found in the archive or if content is empty.
        RuntimeError: For unexpected errors during KMZ processing.
    """
    try:
        with zipfile.ZipFile(file_obj) as kmz:
            kml_filename = next(
                (name for name in kmz.namelist() if name.endswith(".kml")), None
            )
            if kml_filename is None:
                raise ValueError("No KML file found in the KMZ archive.")

            kml_content = io.BytesIO(kmz.read(kml_filename))
            gdf = gpd.read_file(kml_content, **kwargs)

            if gdf.empty:
                raise ValueError(
                    "The KML file is empty or does not contain valid geospatial data."
                )
        return gdf

    except zipfile.BadZipFile:
        raise ValueError("The provided file is not a valid KMZ file.")
    except ValueError:
        raise
    except Exception as e:
        raise RuntimeError(f"An error occurred reading KMZ: {e}") from e

snowflake_data_store

Module for Snowflake internal stage DataStore implementation. Provides access to files stored in Snowflake stages using the DataStore interface.

SnowflakeDataStore

Bases: DataStore

An implementation of DataStore for Snowflake internal stages. Uses Snowflake stages for file storage and retrieval.

Source code in gigaspatial/core/io/snowflake_data_store.py
  30
  31
  32
  33
  34
  35
  36
  37
  38
  39
  40
  41
  42
  43
  44
  45
  46
  47
  48
  49
  50
  51
  52
  53
  54
  55
  56
  57
  58
  59
  60
  61
  62
  63
  64
  65
  66
  67
  68
  69
  70
  71
  72
  73
  74
  75
  76
  77
  78
  79
  80
  81
  82
  83
  84
  85
  86
  87
  88
  89
  90
  91
  92
  93
  94
  95
  96
  97
  98
  99
 100
 101
 102
 103
 104
 105
 106
 107
 108
 109
 110
 111
 112
 113
 114
 115
 116
 117
 118
 119
 120
 121
 122
 123
 124
 125
 126
 127
 128
 129
 130
 131
 132
 133
 134
 135
 136
 137
 138
 139
 140
 141
 142
 143
 144
 145
 146
 147
 148
 149
 150
 151
 152
 153
 154
 155
 156
 157
 158
 159
 160
 161
 162
 163
 164
 165
 166
 167
 168
 169
 170
 171
 172
 173
 174
 175
 176
 177
 178
 179
 180
 181
 182
 183
 184
 185
 186
 187
 188
 189
 190
 191
 192
 193
 194
 195
 196
 197
 198
 199
 200
 201
 202
 203
 204
 205
 206
 207
 208
 209
 210
 211
 212
 213
 214
 215
 216
 217
 218
 219
 220
 221
 222
 223
 224
 225
 226
 227
 228
 229
 230
 231
 232
 233
 234
 235
 236
 237
 238
 239
 240
 241
 242
 243
 244
 245
 246
 247
 248
 249
 250
 251
 252
 253
 254
 255
 256
 257
 258
 259
 260
 261
 262
 263
 264
 265
 266
 267
 268
 269
 270
 271
 272
 273
 274
 275
 276
 277
 278
 279
 280
 281
 282
 283
 284
 285
 286
 287
 288
 289
 290
 291
 292
 293
 294
 295
 296
 297
 298
 299
 300
 301
 302
 303
 304
 305
 306
 307
 308
 309
 310
 311
 312
 313
 314
 315
 316
 317
 318
 319
 320
 321
 322
 323
 324
 325
 326
 327
 328
 329
 330
 331
 332
 333
 334
 335
 336
 337
 338
 339
 340
 341
 342
 343
 344
 345
 346
 347
 348
 349
 350
 351
 352
 353
 354
 355
 356
 357
 358
 359
 360
 361
 362
 363
 364
 365
 366
 367
 368
 369
 370
 371
 372
 373
 374
 375
 376
 377
 378
 379
 380
 381
 382
 383
 384
 385
 386
 387
 388
 389
 390
 391
 392
 393
 394
 395
 396
 397
 398
 399
 400
 401
 402
 403
 404
 405
 406
 407
 408
 409
 410
 411
 412
 413
 414
 415
 416
 417
 418
 419
 420
 421
 422
 423
 424
 425
 426
 427
 428
 429
 430
 431
 432
 433
 434
 435
 436
 437
 438
 439
 440
 441
 442
 443
 444
 445
 446
 447
 448
 449
 450
 451
 452
 453
 454
 455
 456
 457
 458
 459
 460
 461
 462
 463
 464
 465
 466
 467
 468
 469
 470
 471
 472
 473
 474
 475
 476
 477
 478
 479
 480
 481
 482
 483
 484
 485
 486
 487
 488
 489
 490
 491
 492
 493
 494
 495
 496
 497
 498
 499
 500
 501
 502
 503
 504
 505
 506
 507
 508
 509
 510
 511
 512
 513
 514
 515
 516
 517
 518
 519
 520
 521
 522
 523
 524
 525
 526
 527
 528
 529
 530
 531
 532
 533
 534
 535
 536
 537
 538
 539
 540
 541
 542
 543
 544
 545
 546
 547
 548
 549
 550
 551
 552
 553
 554
 555
 556
 557
 558
 559
 560
 561
 562
 563
 564
 565
 566
 567
 568
 569
 570
 571
 572
 573
 574
 575
 576
 577
 578
 579
 580
 581
 582
 583
 584
 585
 586
 587
 588
 589
 590
 591
 592
 593
 594
 595
 596
 597
 598
 599
 600
 601
 602
 603
 604
 605
 606
 607
 608
 609
 610
 611
 612
 613
 614
 615
 616
 617
 618
 619
 620
 621
 622
 623
 624
 625
 626
 627
 628
 629
 630
 631
 632
 633
 634
 635
 636
 637
 638
 639
 640
 641
 642
 643
 644
 645
 646
 647
 648
 649
 650
 651
 652
 653
 654
 655
 656
 657
 658
 659
 660
 661
 662
 663
 664
 665
 666
 667
 668
 669
 670
 671
 672
 673
 674
 675
 676
 677
 678
 679
 680
 681
 682
 683
 684
 685
 686
 687
 688
 689
 690
 691
 692
 693
 694
 695
 696
 697
 698
 699
 700
 701
 702
 703
 704
 705
 706
 707
 708
 709
 710
 711
 712
 713
 714
 715
 716
 717
 718
 719
 720
 721
 722
 723
 724
 725
 726
 727
 728
 729
 730
 731
 732
 733
 734
 735
 736
 737
 738
 739
 740
 741
 742
 743
 744
 745
 746
 747
 748
 749
 750
 751
 752
 753
 754
 755
 756
 757
 758
 759
 760
 761
 762
 763
 764
 765
 766
 767
 768
 769
 770
 771
 772
 773
 774
 775
 776
 777
 778
 779
 780
 781
 782
 783
 784
 785
 786
 787
 788
 789
 790
 791
 792
 793
 794
 795
 796
 797
 798
 799
 800
 801
 802
 803
 804
 805
 806
 807
 808
 809
 810
 811
 812
 813
 814
 815
 816
 817
 818
 819
 820
 821
 822
 823
 824
 825
 826
 827
 828
 829
 830
 831
 832
 833
 834
 835
 836
 837
 838
 839
 840
 841
 842
 843
 844
 845
 846
 847
 848
 849
 850
 851
 852
 853
 854
 855
 856
 857
 858
 859
 860
 861
 862
 863
 864
 865
 866
 867
 868
 869
 870
 871
 872
 873
 874
 875
 876
 877
 878
 879
 880
 881
 882
 883
 884
 885
 886
 887
 888
 889
 890
 891
 892
 893
 894
 895
 896
 897
 898
 899
 900
 901
 902
 903
 904
 905
 906
 907
 908
 909
 910
 911
 912
 913
 914
 915
 916
 917
 918
 919
 920
 921
 922
 923
 924
 925
 926
 927
 928
 929
 930
 931
 932
 933
 934
 935
 936
 937
 938
 939
 940
 941
 942
 943
 944
 945
 946
 947
 948
 949
 950
 951
 952
 953
 954
 955
 956
 957
 958
 959
 960
 961
 962
 963
 964
 965
 966
 967
 968
 969
 970
 971
 972
 973
 974
 975
 976
 977
 978
 979
 980
 981
 982
 983
 984
 985
 986
 987
 988
 989
 990
 991
 992
 993
 994
 995
 996
 997
 998
 999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
class SnowflakeDataStore(DataStore):
    """
    An implementation of DataStore for Snowflake internal stages.
    Uses Snowflake stages for file storage and retrieval.
    """

    def __init__(
        self,
        account: str = config.SNOWFLAKE_ACCOUNT,
        user: str = config.SNOWFLAKE_USER,
        password: str = config.SNOWFLAKE_PASSWORD,
        warehouse: str = config.SNOWFLAKE_WAREHOUSE,
        database: str = config.SNOWFLAKE_DATABASE,
        schema: str = config.SNOWFLAKE_SCHEMA,
        stage_name: str = config.SNOWFLAKE_STAGE_NAME,
    ):
        """
        Initialize the Snowflake data store.

        Args:
            account: Snowflake account identifier.
            user: Snowflake username.
            password: Snowflake password.
            warehouse: Snowflake warehouse name.
            database: Snowflake database name.
            schema: Snowflake schema name.
            stage_name: Name of the Snowflake stage to use for storage.

        Note:
            Connection is created lazily to support multiprocessing. This allows
            the DataStore to be pickled and sent to worker processes.

        Raises:
            ImportError: If 'snowflake-connector-python' is not installed.
            ValueError: If connection parameters are missing.
        """
        if not _HAS_SNOWFLAKE:
            raise ImportError(
                "SnowflakeDataStore requires 'snowflake-connector-python'. "
                "Install it with: pip install 'giga-spatial[snowflake]'"
            )
        # Check if running in SPCS mode (user/password not required)
        import os

        spcs_run = os.getenv("SPCS_RUN", "false").lower() == "true"

        if spcs_run:
            # SPCS mode: user/password not required, but other params are
            if not all([account, warehouse, database, schema, stage_name]):
                raise ValueError(
                    "Snowflake connection parameters (account, warehouse, "
                    "database, schema, stage_name) must be provided via config or constructor. "
                    "User/password not required in SPCS mode."
                )
        else:
            # Non-SPCS mode: all parameters including user/password required
            if not all(
                [account, user, password, warehouse, database, schema, stage_name]
            ):
                raise ValueError(
                    "Snowflake connection parameters (account, user, password, warehouse, "
                    "database, schema, stage_name) must be provided via config or constructor."
                )

        self.account = account
        self.user = user
        self.password = password
        self.warehouse = warehouse
        self.database = database
        self.schema = schema
        self.stage_name = stage_name

        # Connection created lazily (not in __init__) to support multiprocessing
        # When pickled and sent to a worker process, only parameters are serialized
        # Each process creates its own connection on first use
        self._connection = None
        self._lock = None  # Lock created lazily to avoid pickling issues

        self.logger = config.get_logger(self.__class__.__name__)

        # Temporary directory for file operations
        self._temp_dir = tempfile.mkdtemp()

    def _get_lock(self):
        """Get or create the lock for thread safety (created per process)."""
        if self._lock is None:
            self._lock = threading.Lock()
        return self._lock

    def _get_connection(self):
        """
        Get or create the Snowflake connection (lazy initialization).

        This method ensures the connection is created on first use, which allows
        the DataStore to be pickled and sent to worker processes. Each process
        creates its own connection.
        """
        if self._connection is None:
            with self._get_lock():
                # Double-check pattern for thread safety within a process
                if self._connection is None:
                    self._connection = self._create_connection()
        return self._connection

    @property
    def connection(self):
        """
        Property accessor for connection (for backward compatibility).

        This allows existing code that accesses self.connection to continue working,
        while using lazy initialization internally.
        """
        return self._get_connection()

    def _create_connection(self):
        """Create and return a Snowflake connection."""
        spcs_run = os.getenv("SPCS_RUN", "false").lower() == "true"

        if spcs_run:
            # SPCS mode: use snowflake_utils which handles OAuth authentication
            # Import here to avoid circular dependencies
            try:
                from snowflake_utils import get_snowflake_connection

                connection = get_snowflake_connection()
            except ImportError:
                # Fallback: try to create connection with OAuth token
                token_path = os.getenv("SPCS_TOKEN_PATH", "/snowflake/session/token")
                try:
                    with open(token_path, "r") as f:
                        token = f.read().strip()

                    conn_params = {
                        "host": os.getenv("SNOWFLAKE_HOST"),
                        "port": os.getenv("SNOWFLAKE_PORT"),
                        "protocol": "https",
                        "account": self.account,
                        "authenticator": "oauth",
                        "token": token,
                        "warehouse": self.warehouse,
                        "database": self.database,
                        "schema": self.schema,
                        "client_session_keep_alive": True,
                    }
                    # Remove None values
                    conn_params = {
                        k: v for k, v in conn_params.items() if v is not None
                    }
                    connection = snowflake.connector.connect(**conn_params)
                except Exception as e:
                    raise ValueError(f"Failed to create SPCS connection: {str(e)}")
        else:
            # Non-SPCS mode: use password authentication
            conn_params = {
                "account": self.account,
                "user": self.user,
                "password": self.password,
                "warehouse": self.warehouse,
                "database": self.database,
                "schema": self.schema,
            }
            connection = snowflake.connector.connect(**conn_params)

        # Explicitly set the database and schema context
        # This ensures the session knows which database/schema to use
        # (Even though connection params may include them, explicitly setting ensures consistency)
        cursor = connection.cursor()
        try:
            # Use database first
            cursor.execute(f'USE DATABASE "{self.database}"')
            # Then use schema (don't need to specify database again)
            cursor.execute(f'USE SCHEMA "{self.schema}"')
            cursor.close()
        except Exception as e:
            cursor.close()
            connection.close()
            error_msg = (
                f"Failed to set database/schema context: {e}\n"
                f"Make sure the database '{self.database}' and schema '{self.schema}' exist.\n"
                f"You may need to run the setup_snowflake_test.sql script first.\n"
                f"Current config - Database: {self.database}, Schema: {self.schema}, Stage: {self.stage_name}"
            )
            raise IOError(error_msg)

        return connection

    def _ensure_connection(self):
        """Ensure the connection is active, reconnect if needed."""
        try:
            self._get_connection().cursor().execute("SELECT 1")
        except Exception:
            # Reset connection and create a new one
            if self._connection:
                try:
                    self._connection.close()
                except:
                    pass
            self._connection = None
            self._get_connection()

    def _get_stage_path(self, path: str) -> str:
        """Convert a file path to a Snowflake stage path."""
        # Remove leading/trailing slashes and normalize
        path = path.strip("/")
        # Stage paths use forward slashes and @stage_name/ prefix
        return f"@{self.stage_name}/{path}"

    def _normalize_path(self, path: str) -> str:
        """Normalize path for Snowflake stage operations."""
        return path.strip("/").replace("\\", "/")

    def read_file(self, path: str, encoding: Optional[str] = None) -> Union[str, bytes]:
        """
        Read file contents from Snowflake stage.

        Args:
            path: Path to the file in the stage.
            encoding: Optional string encoding. If None, returns bytes.

        Returns:
            File contents as a string if encoding is provided, otherwise bytes.

        Raises:
            FileNotFoundError: If file is not found in the stage.
            IOError: If there's an error during download or read.
        """
        self._ensure_connection()
        cursor = self._get_connection().cursor(DictCursor)

        try:
            normalized_path = self._normalize_path(path)
            stage_path = self._get_stage_path(normalized_path)

            # Create temporary directory for download
            temp_download_dir = os.path.join(self._temp_dir, "downloads")
            os.makedirs(temp_download_dir, exist_ok=True)

            # Download file from stage using GET command
            # GET command: GET <stage_path> file://<local_path>
            temp_dir_normalized = temp_download_dir.replace("\\", "/")
            if not temp_dir_normalized.endswith("/"):
                temp_dir_normalized += "/"

            get_command = f"GET {stage_path} 'file://{temp_dir_normalized}'"
            cursor.execute(get_command)

            # Find the downloaded file (Snowflake may add prefixes/suffixes or preserve structure)
            downloaded_files = []
            for root, dirs, files in os.walk(temp_download_dir):
                for f in files:
                    file_path = os.path.join(root, f)
                    # Check if this file matches our expected filename
                    if os.path.basename(
                        normalized_path
                    ) in f or normalized_path.endswith(f):
                        downloaded_files.append(file_path)

            if not downloaded_files:
                raise FileNotFoundError(f"File not found in stage: {path}")

            # Read the first matching file
            downloaded_path = downloaded_files[0]
            with open(downloaded_path, "rb") as f:
                data = f.read()

            # Clean up
            os.remove(downloaded_path)
            # Clean up empty directories
            try:
                if os.path.exists(temp_download_dir) and not os.listdir(
                    temp_download_dir
                ):
                    os.rmdir(temp_download_dir)
            except OSError:
                pass

            # Decode if encoding is specified
            if encoding:
                return data.decode(encoding)
            return data

        except Exception as e:
            raise IOError(f"Error reading file {path} from Snowflake stage: {e}")
        finally:
            cursor.close()

    def write_file(self, path: str, data: Union[bytes, str]) -> None:
        """
        Write data (string or bytes) to a Snowflake stage.

        Args:
            path: Destination path in the stage.
            data: Data to write (str or bytes).

        Raises:
            ValueError: If data type is unsupported.
            IOError: If there's an error during the upload process.
        """
        self._ensure_connection()
        cursor = self._get_connection().cursor()

        try:
            # Convert to bytes if string
            if isinstance(data, str):
                binary_data = data.encode("utf-8")
            elif isinstance(data, bytes):
                binary_data = data
            else:
                raise ValueError(
                    'Unsupported data type. Only "bytes" or "string" accepted'
                )

            normalized_path = self._normalize_path(path)

            # Write to temporary file first
            # Use the full path structure for the temp file to preserve directory structure
            temp_file_path = os.path.join(self._temp_dir, normalized_path)
            os.makedirs(os.path.dirname(temp_file_path), exist_ok=True)

            with open(temp_file_path, "wb") as f:
                f.write(binary_data)

            # Upload to stage using PUT command
            # Snowflake PUT requires the local file path and the target stage path
            # Convert Windows paths to Unix-style for Snowflake
            temp_file_normalized = os.path.abspath(temp_file_path).replace("\\", "/")

            # PUT command: PUT 'file://<absolute_local_path>' @stage_name/<path>
            # The file will be stored at the specified path in the stage
            stage_target = f"@{self.stage_name}/"
            if "/" in normalized_path:
                # Include directory structure in stage path
                dir_path = os.path.dirname(normalized_path)
                stage_target = f"@{self.stage_name}/{dir_path}/"

            # Snowflake PUT syntax: PUT 'file://<path>' @stage/path
            put_command = f"PUT 'file://{temp_file_normalized}' {stage_target} OVERWRITE=TRUE AUTO_COMPRESS=FALSE"
            cursor.execute(put_command)

            # Clean up temp file
            if os.path.exists(temp_file_path):
                os.remove(temp_file_path)
                # Clean up empty directories if they were created
                try:
                    temp_dir = os.path.dirname(temp_file_path)
                    if temp_dir != self._temp_dir and os.path.exists(temp_dir):
                        os.rmdir(temp_dir)
                except OSError:
                    pass  # Directory not empty or other error, ignore

        except Exception as e:
            raise IOError(f"Error writing file {path} to Snowflake stage: {e}")
        finally:
            cursor.close()

    def upload_file(self, file_path: str, stage_path: str):
        """
        Uploads a single file from local filesystem to Snowflake stage.

        Args:
            file_path: Local file path.
            stage_path: Destination path in the stage.

        Raises:
            FileNotFoundError: If local file is missing.
        """
        try:
            if not os.path.exists(file_path):
                raise FileNotFoundError(f"Local file not found: {file_path}")

            # Read the file
            with open(file_path, "rb") as f:
                data = f.read()

            # Write to stage using write_file
            self.write_file(stage_path, data)
            self.logger.info(f"Uploaded {file_path} to {stage_path}")
        except Exception as e:
            self.logger.error(f"Failed to upload {file_path}: {e}")
            raise

    def upload_directory(self, dir_path: str, stage_dir_path: str):
        """
        Uploads all files from a local directory to Snowflake stage.

        Args:
            dir_path: Local directory path.
            stage_dir_path: Destination directory path in the stage.

        Raises:
            NotADirectoryError: If dir_path is not a directory.
        """
        if not os.path.isdir(dir_path):
            raise NotADirectoryError(f"Local directory not found: {dir_path}")

        for root, dirs, files in os.walk(dir_path):
            for file in files:
                local_file_path = os.path.join(root, file)
                relative_path = os.path.relpath(local_file_path, dir_path)
                # Normalize path separators for stage
                stage_file_path = os.path.join(stage_dir_path, relative_path).replace(
                    "\\", "/"
                )

                self.upload_file(local_file_path, stage_file_path)

    def download_directory(self, stage_dir_path: str, local_dir_path: str):
        """
        Downloads all files from a Snowflake stage directory to a local directory.

        Args:
            stage_dir_path: Source directory path in the stage.
            local_dir_path: Destination local directory path.
        """
        try:
            # Ensure the local directory exists
            os.makedirs(local_dir_path, exist_ok=True)

            # List all files in the stage directory
            files = self.list_files(stage_dir_path)

            for file_path in files:
                # Get the relative path from the stage directory
                if stage_dir_path:
                    if file_path.startswith(stage_dir_path):
                        relative_path = file_path[len(stage_dir_path) :].lstrip("/")
                    else:
                        # If file_path doesn't start with stage_dir_path, use it as is
                        relative_path = os.path.basename(file_path)
                else:
                    relative_path = file_path

                # Construct the local file path
                local_file_path = os.path.join(local_dir_path, relative_path)
                # Create directories if needed
                os.makedirs(os.path.dirname(local_file_path), exist_ok=True)

                # Download the file
                data = self.read_file(file_path)
                with open(local_file_path, "wb") as f:
                    if isinstance(data, str):
                        f.write(data.encode("utf-8"))
                    else:
                        f.write(data)

            self.logger.info(
                f"Downloaded directory {stage_dir_path} to {local_dir_path}"
            )
        except Exception as e:
            self.logger.error(f"Failed to download directory {stage_dir_path}: {e}")
            raise

    def copy_directory(self, source_dir: str, destination_dir: str):
        """
        Copies all files from source to destination directory within the stage.

        Args:
            source_dir: Source directory path in the stage.
            destination_dir: Destination directory path in the stage.
        """
        try:
            # Normalize directory paths
            source_dir = source_dir.rstrip("/")
            destination_dir = destination_dir.rstrip("/")

            # List all files in the source directory
            files = self.list_files(source_dir)

            for file_path in files:
                # Get relative path from source directory
                if source_dir:
                    if file_path.startswith(source_dir):
                        relative_path = file_path[len(source_dir) :].lstrip("/")
                    else:
                        relative_path = os.path.basename(file_path)
                else:
                    relative_path = file_path

                # Construct the destination file path
                if destination_dir:
                    dest_file_path = f"{destination_dir}/{relative_path}".replace(
                        "//", "/"
                    )
                else:
                    dest_file_path = relative_path

                # Copy each file
                self.copy_file(file_path, dest_file_path, overwrite=True)

            self.logger.info(f"Copied directory from {source_dir} to {destination_dir}")
        except Exception as e:
            self.logger.error(f"Failed to copy directory {source_dir}: {e}")
            raise

    def copy_file(
        self, source_path: str, destination_path: str, overwrite: bool = False
    ):
        """
        Copies a single file within the Snowflake stage.

        Args:
            source_path: Source file path in the stage.
            destination_path: Destination file path in the stage.
            overwrite: If True, overwrite the destination file if it already exists.

        Raises:
            FileNotFoundError: If source file is missing.
            FileExistsError: If destination exists and overwrite=False.
        """
        try:
            if not self.file_exists(source_path):
                raise FileNotFoundError(f"Source file not found: {source_path}")

            if self.file_exists(destination_path) and not overwrite:
                raise FileExistsError(
                    f"Destination file already exists and overwrite is False: {destination_path}"
                )

            # Read from source and write to destination
            data = self.read_file(source_path)
            self.write_file(destination_path, data)

            self.logger.info(f"Copied file from {source_path} to {destination_path}")
        except Exception as e:
            self.logger.error(f"Failed to copy file {source_path}: {e}")
            raise

    def exists(self, path: str) -> bool:
        """Check if a path exists (file or directory)."""
        return self.file_exists(path) or self.is_dir(path)

    def file_exists(self, path: str) -> bool:
        """
        Check if a file exists in the Snowflake stage.

        Args:
            path: Path to check in the stage.

        Returns:
            True if file exists, False otherwise.
        """
        self._ensure_connection()
        cursor = self._get_connection().cursor(DictCursor)

        try:
            normalized_path = self._normalize_path(path)
            stage_path = self._get_stage_path(normalized_path)

            # List files in stage with the given path pattern
            list_command = f"LIST {stage_path}"
            cursor.execute(list_command)
            results = cursor.fetchall()

            # Check if exact file exists
            for result in results:
                if (
                    result["name"].endswith(normalized_path)
                    or result["name"] == stage_path
                ):
                    return True

            return False

        except Exception as e:
            self.logger.warning(f"Error checking file existence {path}: {e}")
            return False
        finally:
            cursor.close()

    def file_size(self, path: str) -> float:
        """
        Get the size of a file in kilobytes.

        Args:
            path: File path in the stage.

        Returns:
            File size in kilobytes.

        Raises:
            FileNotFoundError: If file is not found.
        """
        self._ensure_connection()
        cursor = self._get_connection().cursor(DictCursor)

        try:
            normalized_path = self._normalize_path(path)
            stage_path = self._get_stage_path(normalized_path)

            # LIST command returns file metadata including size
            list_command = f"LIST {stage_path}"
            cursor.execute(list_command)
            results = cursor.fetchall()

            # Find the matching file and get its size
            for result in results:
                file_path = result["name"]
                if normalized_path in file_path.lower() or file_path.endswith(
                    normalized_path
                ):
                    # Size is in bytes, convert to kilobytes
                    size_bytes = result.get("size", 0)
                    size_kb = size_bytes / 1024.0
                    return size_kb

            raise FileNotFoundError(f"File not found: {path}")
        except Exception as e:
            self.logger.error(f"Error getting file size for {path}: {e}")
            raise
        finally:
            cursor.close()

    def list_files(self, path: str) -> List[str]:
        """
        List all files in a directory within the Snowflake stage.

        Args:
            path: Directory path to list.

        Returns:
            List of relative file paths from the stage root.
        """
        self._ensure_connection()
        cursor = self._get_connection().cursor(DictCursor)

        try:
            normalized_path = self._normalize_path(path)
            stage_path = self._get_stage_path(normalized_path)

            # List files in stage
            list_command = f"LIST {stage_path}"
            cursor.execute(list_command)
            results = cursor.fetchall()

            # Extract file paths relative to the base stage path
            files = []
            for result in results:
                file_path = result["name"]
                # Snowflake LIST returns names in lowercase without @ symbol
                # Remove stage prefix to get relative path
                # Check both @stage_name/ and lowercase stage_name/ formats
                stage_prefixes = [
                    f"@{self.stage_name}/",
                    f"{self.stage_name.lower()}/",
                    f"@{self.stage_name.lower()}/",
                ]

                for prefix in stage_prefixes:
                    if file_path.startswith(prefix):
                        relative_path = file_path[len(prefix) :]
                        files.append(relative_path)
                        break
                else:
                    # If no prefix matches, try to extract path after stage name
                    # Sometimes stage name might be in different case
                    stage_name_lower = self.stage_name.lower()
                    if stage_name_lower in file_path.lower():
                        # Find the position after the stage name
                        idx = file_path.lower().find(stage_name_lower)
                        if idx != -1:
                            # Get everything after stage name and '/'
                            after_stage = file_path[
                                idx + len(stage_name_lower) :
                            ].lstrip("/")
                            if after_stage.startswith(normalized_path):
                                relative_path = after_stage
                                files.append(relative_path)

            return files

        except Exception as e:
            self.logger.warning(f"Error listing files in {path}: {e}")
            return []
        finally:
            cursor.close()

    def walk(self, top: str) -> Generator[Tuple[str, List[str], List[str]], None, None]:
        """
        Walk through directory tree in Snowflake stage.

        Args:
            top: Starting directory for the walk.

        Yields:
            Tuples of (dirpath, dirnames, filenames).
        """
        try:
            normalized_top = self._normalize_path(top)

            # Use list_files to get all files (it handles path parsing correctly)
            all_files = self.list_files(normalized_top)

            # Organize into directory structure
            dirs = {}

            for file_path in all_files:
                # Ensure we're working with paths relative to the top
                if normalized_top and not file_path.startswith(normalized_top):
                    continue

                # Get relative path from top
                if normalized_top and file_path.startswith(normalized_top):
                    relative_path = file_path[len(normalized_top) :].lstrip("/")
                else:
                    relative_path = file_path

                if not relative_path:
                    continue

                # Get directory and filename
                if "/" in relative_path:
                    dir_path, filename = os.path.split(relative_path)
                    full_dir_path = (
                        f"{normalized_top}/{dir_path}" if normalized_top else dir_path
                    )
                    if full_dir_path not in dirs:
                        dirs[full_dir_path] = []
                    dirs[full_dir_path].append(filename)
                else:
                    # File in root of the top directory
                    if normalized_top not in dirs:
                        dirs[normalized_top] = []
                    dirs[normalized_top].append(relative_path)

            # Yield results in os.walk format
            for dir_path, files in dirs.items():
                # Extract subdirectories (simplified - Snowflake stages are flat)
                subdirs = []
                yield (dir_path, subdirs, files)

        except Exception as e:
            self.logger.warning(f"Error walking directory {top}: {e}")
            yield (top, [], [])

    def list_directories(self, path: str) -> List[str]:
        """
        List immediate directory names from a given path in the stage.

        Args:
            path: Directory path to list.

        Returns:
            List of subdirectory names.
        """
        normalized_path = self._normalize_path(path)
        files = self.list_files(normalized_path)

        directories = set()

        for file_path in files:
            # Get relative path from the search path
            if normalized_path:
                if file_path.startswith(normalized_path):
                    relative_path = file_path[len(normalized_path) :].lstrip("/")
                else:
                    continue
            else:
                relative_path = file_path

            # Skip if empty
            if not relative_path:
                continue

            # If there's a "/" in the relative path, it means there's a subdirectory
            if "/" in relative_path:
                # Get the first directory name
                dir_name = relative_path.split("/")[0]
                directories.add(dir_name)

        return sorted(list(directories))

    @contextlib.contextmanager
    def open(self, path: str, mode: str = "r"):
        """
        Context manager for opening files in Snowflake stage.

        Args:
            path: File path in stage.
            mode: File mode ('r', 'rb', 'w', 'wb').

        Returns:
            Context manager yielding a file-like object.

        Raises:
            ValueError: If mode is unsupported.
        """
        if mode == "w":
            file = io.StringIO()
            yield file
            self.write_file(path, file.getvalue())

        elif mode == "wb":
            file = io.BytesIO()
            yield file
            self.write_file(path, file.getvalue())

        elif mode == "r":
            data = self.read_file(path, encoding="UTF-8")
            file = io.StringIO(data)
            yield file

        elif mode == "rb":
            data = self.read_file(path)
            file = io.BytesIO(data)
            yield file

        else:
            raise ValueError(f"Unsupported mode: {mode}")

    def get_file_metadata(self, path: str) -> dict:
        """
        Retrieve comprehensive file metadata from Snowflake stage.

        Args:
            path: File path in the stage.

        Returns:
            Dictionary with keys: 'name', 'size_bytes', 'last_modified', 'md5'.

        Raises:
            FileNotFoundError: If file is not found.
        """
        self._ensure_connection()
        cursor = self._get_connection().cursor(DictCursor)

        try:
            normalized_path = self._normalize_path(path)
            stage_path = self._get_stage_path(normalized_path)

            # LIST command returns file metadata
            list_command = f"LIST {stage_path}"
            cursor.execute(list_command)
            results = cursor.fetchall()

            # Find the matching file
            for result in results:
                file_path = result["name"]
                if normalized_path in file_path.lower() or file_path.endswith(
                    normalized_path
                ):
                    return {
                        "name": path,
                        "size_bytes": result.get("size", 0),
                        "last_modified": result.get("last_modified"),
                        "md5": result.get("md5"),
                    }

            raise FileNotFoundError(f"File not found: {path}")
        except Exception as e:
            self.logger.error(f"Error getting file metadata for {path}: {e}")
            raise
        finally:
            cursor.close()

    def is_file(self, path: str) -> bool:
        """Check if path points to a file."""
        return self.file_exists(path)

    def is_dir(self, path: str) -> bool:
        """Check if path points to a directory."""
        # First check if it's actually a file (exact match)
        if self.file_exists(path):
            return False

        # In Snowflake stages, directories are conceptual
        # Check if there are files with this path prefix
        normalized_path = self._normalize_path(path)
        files = self.list_files(normalized_path)

        # Filter out files that are exact matches (they're files, not directories)
        exact_match = any(f == normalized_path or f == path for f in files)
        if exact_match:
            return False

        return len(files) > 0

    def rmdir(self, dir: str) -> None:
        """
        Remove a directory and all its contents from the Snowflake stage.

        Args:
            dir: Path to the directory to remove in stage.

        Raises:
            IOError: If removal fails.
        """
        self._ensure_connection()
        cursor = self._get_connection().cursor()

        try:
            normalized_dir = self._normalize_path(dir)
            stage_path = self._get_stage_path(normalized_dir)

            # Remove all files in the directory
            remove_command = f"REMOVE {stage_path}"
            cursor.execute(remove_command)

        except Exception as e:
            raise IOError(f"Error removing directory {dir}: {e}")
        finally:
            cursor.close()

    def mkdir(self, path: str, exist_ok: bool = False) -> None:
        """
        Create a directory in Snowflake stage.
        Directories are created implicitly on upload; this creates a placeholder.

        Args:
            path: Path of the directory to create.
            exist_ok: If False, raise error if directory exists.

        Raises:
            FileExistsError: If directory exists and exist_ok=False.
        """
        # Check if directory already exists
        if self.is_dir(path) and not exist_ok:
            raise FileExistsError(f"Directory {path} already exists")

        # Create a placeholder file to ensure directory exists
        placeholder_path = os.path.join(path, ".placeholder").replace("\\", "/")
        if not self.file_exists(placeholder_path):
            self.write_file(placeholder_path, b"Placeholder file for directory")

    def remove(self, path: str) -> None:
        """
        Remove a file from the Snowflake stage.

        Args:
            path: Path to the file to remove in stage.

        Raises:
            IOError: If removal fails.
        """
        self._ensure_connection()
        cursor = self._get_connection().cursor()

        try:
            normalized_path = self._normalize_path(path)
            stage_path = self._get_stage_path(normalized_path)

            remove_command = f"REMOVE {stage_path}"
            cursor.execute(remove_command)

        except Exception as e:
            raise IOError(f"Error removing file {path}: {e}")
        finally:
            cursor.close()

    def rename(
        self,
        source_path: str,
        destination_path: str,
        overwrite: bool = False,
        delete_source: bool = True,
    ) -> None:
        """
        Rename (move) a single file by copying to the new path and deleting source.

        Args:
            source_path: Existing file path in the stage.
            destination_path: Target file path in the stage.
            overwrite: Overwrite destination if it exists.
            delete_source: Delete original after successful copy.

        Raises:
            FileNotFoundError: If source is missing.
            FileExistsError: If destination exists and overwrite=False.
        """
        if not self.file_exists(source_path):
            raise FileNotFoundError(f"Source file not found: {source_path}")

        if self.file_exists(destination_path) and not overwrite:
            raise FileExistsError(
                f"Destination already exists and overwrite is False: {destination_path}"
            )

        # Copy file to new location
        self.copy_file(source_path, destination_path, overwrite=overwrite)

        # Delete source if requested
        if delete_source:
            self.remove(source_path)

    def close(self):
        """Close the Snowflake connection."""
        if self._connection:
            self._connection.close()
            self._connection = None

    def __getstate__(self):
        """
        Custom pickling: exclude connection and lock to allow multiprocessing.

        When pickled, only store connection parameters (all picklable).
        The connection and lock will be recreated in the worker process.
        """
        state = self.__dict__.copy()
        # Remove connection and lock (they can't be pickled)
        state["_connection"] = None
        state["_lock"] = None
        return state

    def __setstate__(self, state):
        """
        Custom unpickling: restore state without connection.

        Connection will be created lazily on first use in the worker process.
        """
        self.__dict__.update(state)
        # Ensure connection and lock are None (will be created on first use)
        self._connection = None
        self._lock = None

    def __enter__(self):
        """Context manager entry."""
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        """Context manager exit."""
        self.close()
connection property

Property accessor for connection (for backward compatibility).

This allows existing code that accesses self.connection to continue working, while using lazy initialization internally.

__enter__()

Context manager entry.

Source code in gigaspatial/core/io/snowflake_data_store.py
def __enter__(self):
    """Context manager entry."""
    return self
__exit__(exc_type, exc_val, exc_tb)

Context manager exit.

Source code in gigaspatial/core/io/snowflake_data_store.py
def __exit__(self, exc_type, exc_val, exc_tb):
    """Context manager exit."""
    self.close()
__getstate__()

Custom pickling: exclude connection and lock to allow multiprocessing.

When pickled, only store connection parameters (all picklable). The connection and lock will be recreated in the worker process.

Source code in gigaspatial/core/io/snowflake_data_store.py
def __getstate__(self):
    """
    Custom pickling: exclude connection and lock to allow multiprocessing.

    When pickled, only store connection parameters (all picklable).
    The connection and lock will be recreated in the worker process.
    """
    state = self.__dict__.copy()
    # Remove connection and lock (they can't be pickled)
    state["_connection"] = None
    state["_lock"] = None
    return state
__init__(account=config.SNOWFLAKE_ACCOUNT, user=config.SNOWFLAKE_USER, password=config.SNOWFLAKE_PASSWORD, warehouse=config.SNOWFLAKE_WAREHOUSE, database=config.SNOWFLAKE_DATABASE, schema=config.SNOWFLAKE_SCHEMA, stage_name=config.SNOWFLAKE_STAGE_NAME)

Initialize the Snowflake data store.

Parameters:

Name Type Description Default
account str

Snowflake account identifier.

SNOWFLAKE_ACCOUNT
user str

Snowflake username.

SNOWFLAKE_USER
password str

Snowflake password.

SNOWFLAKE_PASSWORD
warehouse str

Snowflake warehouse name.

SNOWFLAKE_WAREHOUSE
database str

Snowflake database name.

SNOWFLAKE_DATABASE
schema str

Snowflake schema name.

SNOWFLAKE_SCHEMA
stage_name str

Name of the Snowflake stage to use for storage.

SNOWFLAKE_STAGE_NAME
Note

Connection is created lazily to support multiprocessing. This allows the DataStore to be pickled and sent to worker processes.

Raises:

Type Description
ImportError

If 'snowflake-connector-python' is not installed.

ValueError

If connection parameters are missing.

Source code in gigaspatial/core/io/snowflake_data_store.py
def __init__(
    self,
    account: str = config.SNOWFLAKE_ACCOUNT,
    user: str = config.SNOWFLAKE_USER,
    password: str = config.SNOWFLAKE_PASSWORD,
    warehouse: str = config.SNOWFLAKE_WAREHOUSE,
    database: str = config.SNOWFLAKE_DATABASE,
    schema: str = config.SNOWFLAKE_SCHEMA,
    stage_name: str = config.SNOWFLAKE_STAGE_NAME,
):
    """
    Initialize the Snowflake data store.

    Args:
        account: Snowflake account identifier.
        user: Snowflake username.
        password: Snowflake password.
        warehouse: Snowflake warehouse name.
        database: Snowflake database name.
        schema: Snowflake schema name.
        stage_name: Name of the Snowflake stage to use for storage.

    Note:
        Connection is created lazily to support multiprocessing. This allows
        the DataStore to be pickled and sent to worker processes.

    Raises:
        ImportError: If 'snowflake-connector-python' is not installed.
        ValueError: If connection parameters are missing.
    """
    if not _HAS_SNOWFLAKE:
        raise ImportError(
            "SnowflakeDataStore requires 'snowflake-connector-python'. "
            "Install it with: pip install 'giga-spatial[snowflake]'"
        )
    # Check if running in SPCS mode (user/password not required)
    import os

    spcs_run = os.getenv("SPCS_RUN", "false").lower() == "true"

    if spcs_run:
        # SPCS mode: user/password not required, but other params are
        if not all([account, warehouse, database, schema, stage_name]):
            raise ValueError(
                "Snowflake connection parameters (account, warehouse, "
                "database, schema, stage_name) must be provided via config or constructor. "
                "User/password not required in SPCS mode."
            )
    else:
        # Non-SPCS mode: all parameters including user/password required
        if not all(
            [account, user, password, warehouse, database, schema, stage_name]
        ):
            raise ValueError(
                "Snowflake connection parameters (account, user, password, warehouse, "
                "database, schema, stage_name) must be provided via config or constructor."
            )

    self.account = account
    self.user = user
    self.password = password
    self.warehouse = warehouse
    self.database = database
    self.schema = schema
    self.stage_name = stage_name

    # Connection created lazily (not in __init__) to support multiprocessing
    # When pickled and sent to a worker process, only parameters are serialized
    # Each process creates its own connection on first use
    self._connection = None
    self._lock = None  # Lock created lazily to avoid pickling issues

    self.logger = config.get_logger(self.__class__.__name__)

    # Temporary directory for file operations
    self._temp_dir = tempfile.mkdtemp()
__setstate__(state)

Custom unpickling: restore state without connection.

Connection will be created lazily on first use in the worker process.

Source code in gigaspatial/core/io/snowflake_data_store.py
def __setstate__(self, state):
    """
    Custom unpickling: restore state without connection.

    Connection will be created lazily on first use in the worker process.
    """
    self.__dict__.update(state)
    # Ensure connection and lock are None (will be created on first use)
    self._connection = None
    self._lock = None
close()

Close the Snowflake connection.

Source code in gigaspatial/core/io/snowflake_data_store.py
def close(self):
    """Close the Snowflake connection."""
    if self._connection:
        self._connection.close()
        self._connection = None
copy_directory(source_dir, destination_dir)

Copies all files from source to destination directory within the stage.

Parameters:

Name Type Description Default
source_dir str

Source directory path in the stage.

required
destination_dir str

Destination directory path in the stage.

required
Source code in gigaspatial/core/io/snowflake_data_store.py
def copy_directory(self, source_dir: str, destination_dir: str):
    """
    Copies all files from source to destination directory within the stage.

    Args:
        source_dir: Source directory path in the stage.
        destination_dir: Destination directory path in the stage.
    """
    try:
        # Normalize directory paths
        source_dir = source_dir.rstrip("/")
        destination_dir = destination_dir.rstrip("/")

        # List all files in the source directory
        files = self.list_files(source_dir)

        for file_path in files:
            # Get relative path from source directory
            if source_dir:
                if file_path.startswith(source_dir):
                    relative_path = file_path[len(source_dir) :].lstrip("/")
                else:
                    relative_path = os.path.basename(file_path)
            else:
                relative_path = file_path

            # Construct the destination file path
            if destination_dir:
                dest_file_path = f"{destination_dir}/{relative_path}".replace(
                    "//", "/"
                )
            else:
                dest_file_path = relative_path

            # Copy each file
            self.copy_file(file_path, dest_file_path, overwrite=True)

        self.logger.info(f"Copied directory from {source_dir} to {destination_dir}")
    except Exception as e:
        self.logger.error(f"Failed to copy directory {source_dir}: {e}")
        raise
copy_file(source_path, destination_path, overwrite=False)

Copies a single file within the Snowflake stage.

Parameters:

Name Type Description Default
source_path str

Source file path in the stage.

required
destination_path str

Destination file path in the stage.

required
overwrite bool

If True, overwrite the destination file if it already exists.

False

Raises:

Type Description
FileNotFoundError

If source file is missing.

FileExistsError

If destination exists and overwrite=False.

Source code in gigaspatial/core/io/snowflake_data_store.py
def copy_file(
    self, source_path: str, destination_path: str, overwrite: bool = False
):
    """
    Copies a single file within the Snowflake stage.

    Args:
        source_path: Source file path in the stage.
        destination_path: Destination file path in the stage.
        overwrite: If True, overwrite the destination file if it already exists.

    Raises:
        FileNotFoundError: If source file is missing.
        FileExistsError: If destination exists and overwrite=False.
    """
    try:
        if not self.file_exists(source_path):
            raise FileNotFoundError(f"Source file not found: {source_path}")

        if self.file_exists(destination_path) and not overwrite:
            raise FileExistsError(
                f"Destination file already exists and overwrite is False: {destination_path}"
            )

        # Read from source and write to destination
        data = self.read_file(source_path)
        self.write_file(destination_path, data)

        self.logger.info(f"Copied file from {source_path} to {destination_path}")
    except Exception as e:
        self.logger.error(f"Failed to copy file {source_path}: {e}")
        raise
download_directory(stage_dir_path, local_dir_path)

Downloads all files from a Snowflake stage directory to a local directory.

Parameters:

Name Type Description Default
stage_dir_path str

Source directory path in the stage.

required
local_dir_path str

Destination local directory path.

required
Source code in gigaspatial/core/io/snowflake_data_store.py
def download_directory(self, stage_dir_path: str, local_dir_path: str):
    """
    Downloads all files from a Snowflake stage directory to a local directory.

    Args:
        stage_dir_path: Source directory path in the stage.
        local_dir_path: Destination local directory path.
    """
    try:
        # Ensure the local directory exists
        os.makedirs(local_dir_path, exist_ok=True)

        # List all files in the stage directory
        files = self.list_files(stage_dir_path)

        for file_path in files:
            # Get the relative path from the stage directory
            if stage_dir_path:
                if file_path.startswith(stage_dir_path):
                    relative_path = file_path[len(stage_dir_path) :].lstrip("/")
                else:
                    # If file_path doesn't start with stage_dir_path, use it as is
                    relative_path = os.path.basename(file_path)
            else:
                relative_path = file_path

            # Construct the local file path
            local_file_path = os.path.join(local_dir_path, relative_path)
            # Create directories if needed
            os.makedirs(os.path.dirname(local_file_path), exist_ok=True)

            # Download the file
            data = self.read_file(file_path)
            with open(local_file_path, "wb") as f:
                if isinstance(data, str):
                    f.write(data.encode("utf-8"))
                else:
                    f.write(data)

        self.logger.info(
            f"Downloaded directory {stage_dir_path} to {local_dir_path}"
        )
    except Exception as e:
        self.logger.error(f"Failed to download directory {stage_dir_path}: {e}")
        raise
exists(path)

Check if a path exists (file or directory).

Source code in gigaspatial/core/io/snowflake_data_store.py
def exists(self, path: str) -> bool:
    """Check if a path exists (file or directory)."""
    return self.file_exists(path) or self.is_dir(path)
file_exists(path)

Check if a file exists in the Snowflake stage.

Parameters:

Name Type Description Default
path str

Path to check in the stage.

required

Returns:

Type Description
bool

True if file exists, False otherwise.

Source code in gigaspatial/core/io/snowflake_data_store.py
def file_exists(self, path: str) -> bool:
    """
    Check if a file exists in the Snowflake stage.

    Args:
        path: Path to check in the stage.

    Returns:
        True if file exists, False otherwise.
    """
    self._ensure_connection()
    cursor = self._get_connection().cursor(DictCursor)

    try:
        normalized_path = self._normalize_path(path)
        stage_path = self._get_stage_path(normalized_path)

        # List files in stage with the given path pattern
        list_command = f"LIST {stage_path}"
        cursor.execute(list_command)
        results = cursor.fetchall()

        # Check if exact file exists
        for result in results:
            if (
                result["name"].endswith(normalized_path)
                or result["name"] == stage_path
            ):
                return True

        return False

    except Exception as e:
        self.logger.warning(f"Error checking file existence {path}: {e}")
        return False
    finally:
        cursor.close()
file_size(path)

Get the size of a file in kilobytes.

Parameters:

Name Type Description Default
path str

File path in the stage.

required

Returns:

Type Description
float

File size in kilobytes.

Raises:

Type Description
FileNotFoundError

If file is not found.

Source code in gigaspatial/core/io/snowflake_data_store.py
def file_size(self, path: str) -> float:
    """
    Get the size of a file in kilobytes.

    Args:
        path: File path in the stage.

    Returns:
        File size in kilobytes.

    Raises:
        FileNotFoundError: If file is not found.
    """
    self._ensure_connection()
    cursor = self._get_connection().cursor(DictCursor)

    try:
        normalized_path = self._normalize_path(path)
        stage_path = self._get_stage_path(normalized_path)

        # LIST command returns file metadata including size
        list_command = f"LIST {stage_path}"
        cursor.execute(list_command)
        results = cursor.fetchall()

        # Find the matching file and get its size
        for result in results:
            file_path = result["name"]
            if normalized_path in file_path.lower() or file_path.endswith(
                normalized_path
            ):
                # Size is in bytes, convert to kilobytes
                size_bytes = result.get("size", 0)
                size_kb = size_bytes / 1024.0
                return size_kb

        raise FileNotFoundError(f"File not found: {path}")
    except Exception as e:
        self.logger.error(f"Error getting file size for {path}: {e}")
        raise
    finally:
        cursor.close()
get_file_metadata(path)

Retrieve comprehensive file metadata from Snowflake stage.

Parameters:

Name Type Description Default
path str

File path in the stage.

required

Returns:

Type Description
dict

Dictionary with keys: 'name', 'size_bytes', 'last_modified', 'md5'.

Raises:

Type Description
FileNotFoundError

If file is not found.

Source code in gigaspatial/core/io/snowflake_data_store.py
def get_file_metadata(self, path: str) -> dict:
    """
    Retrieve comprehensive file metadata from Snowflake stage.

    Args:
        path: File path in the stage.

    Returns:
        Dictionary with keys: 'name', 'size_bytes', 'last_modified', 'md5'.

    Raises:
        FileNotFoundError: If file is not found.
    """
    self._ensure_connection()
    cursor = self._get_connection().cursor(DictCursor)

    try:
        normalized_path = self._normalize_path(path)
        stage_path = self._get_stage_path(normalized_path)

        # LIST command returns file metadata
        list_command = f"LIST {stage_path}"
        cursor.execute(list_command)
        results = cursor.fetchall()

        # Find the matching file
        for result in results:
            file_path = result["name"]
            if normalized_path in file_path.lower() or file_path.endswith(
                normalized_path
            ):
                return {
                    "name": path,
                    "size_bytes": result.get("size", 0),
                    "last_modified": result.get("last_modified"),
                    "md5": result.get("md5"),
                }

        raise FileNotFoundError(f"File not found: {path}")
    except Exception as e:
        self.logger.error(f"Error getting file metadata for {path}: {e}")
        raise
    finally:
        cursor.close()
is_dir(path)

Check if path points to a directory.

Source code in gigaspatial/core/io/snowflake_data_store.py
def is_dir(self, path: str) -> bool:
    """Check if path points to a directory."""
    # First check if it's actually a file (exact match)
    if self.file_exists(path):
        return False

    # In Snowflake stages, directories are conceptual
    # Check if there are files with this path prefix
    normalized_path = self._normalize_path(path)
    files = self.list_files(normalized_path)

    # Filter out files that are exact matches (they're files, not directories)
    exact_match = any(f == normalized_path or f == path for f in files)
    if exact_match:
        return False

    return len(files) > 0
is_file(path)

Check if path points to a file.

Source code in gigaspatial/core/io/snowflake_data_store.py
def is_file(self, path: str) -> bool:
    """Check if path points to a file."""
    return self.file_exists(path)
list_directories(path)

List immediate directory names from a given path in the stage.

Parameters:

Name Type Description Default
path str

Directory path to list.

required

Returns:

Type Description
List[str]

List of subdirectory names.

Source code in gigaspatial/core/io/snowflake_data_store.py
def list_directories(self, path: str) -> List[str]:
    """
    List immediate directory names from a given path in the stage.

    Args:
        path: Directory path to list.

    Returns:
        List of subdirectory names.
    """
    normalized_path = self._normalize_path(path)
    files = self.list_files(normalized_path)

    directories = set()

    for file_path in files:
        # Get relative path from the search path
        if normalized_path:
            if file_path.startswith(normalized_path):
                relative_path = file_path[len(normalized_path) :].lstrip("/")
            else:
                continue
        else:
            relative_path = file_path

        # Skip if empty
        if not relative_path:
            continue

        # If there's a "/" in the relative path, it means there's a subdirectory
        if "/" in relative_path:
            # Get the first directory name
            dir_name = relative_path.split("/")[0]
            directories.add(dir_name)

    return sorted(list(directories))
list_files(path)

List all files in a directory within the Snowflake stage.

Parameters:

Name Type Description Default
path str

Directory path to list.

required

Returns:

Type Description
List[str]

List of relative file paths from the stage root.

Source code in gigaspatial/core/io/snowflake_data_store.py
def list_files(self, path: str) -> List[str]:
    """
    List all files in a directory within the Snowflake stage.

    Args:
        path: Directory path to list.

    Returns:
        List of relative file paths from the stage root.
    """
    self._ensure_connection()
    cursor = self._get_connection().cursor(DictCursor)

    try:
        normalized_path = self._normalize_path(path)
        stage_path = self._get_stage_path(normalized_path)

        # List files in stage
        list_command = f"LIST {stage_path}"
        cursor.execute(list_command)
        results = cursor.fetchall()

        # Extract file paths relative to the base stage path
        files = []
        for result in results:
            file_path = result["name"]
            # Snowflake LIST returns names in lowercase without @ symbol
            # Remove stage prefix to get relative path
            # Check both @stage_name/ and lowercase stage_name/ formats
            stage_prefixes = [
                f"@{self.stage_name}/",
                f"{self.stage_name.lower()}/",
                f"@{self.stage_name.lower()}/",
            ]

            for prefix in stage_prefixes:
                if file_path.startswith(prefix):
                    relative_path = file_path[len(prefix) :]
                    files.append(relative_path)
                    break
            else:
                # If no prefix matches, try to extract path after stage name
                # Sometimes stage name might be in different case
                stage_name_lower = self.stage_name.lower()
                if stage_name_lower in file_path.lower():
                    # Find the position after the stage name
                    idx = file_path.lower().find(stage_name_lower)
                    if idx != -1:
                        # Get everything after stage name and '/'
                        after_stage = file_path[
                            idx + len(stage_name_lower) :
                        ].lstrip("/")
                        if after_stage.startswith(normalized_path):
                            relative_path = after_stage
                            files.append(relative_path)

        return files

    except Exception as e:
        self.logger.warning(f"Error listing files in {path}: {e}")
        return []
    finally:
        cursor.close()
mkdir(path, exist_ok=False)

Create a directory in Snowflake stage. Directories are created implicitly on upload; this creates a placeholder.

Parameters:

Name Type Description Default
path str

Path of the directory to create.

required
exist_ok bool

If False, raise error if directory exists.

False

Raises:

Type Description
FileExistsError

If directory exists and exist_ok=False.

Source code in gigaspatial/core/io/snowflake_data_store.py
def mkdir(self, path: str, exist_ok: bool = False) -> None:
    """
    Create a directory in Snowflake stage.
    Directories are created implicitly on upload; this creates a placeholder.

    Args:
        path: Path of the directory to create.
        exist_ok: If False, raise error if directory exists.

    Raises:
        FileExistsError: If directory exists and exist_ok=False.
    """
    # Check if directory already exists
    if self.is_dir(path) and not exist_ok:
        raise FileExistsError(f"Directory {path} already exists")

    # Create a placeholder file to ensure directory exists
    placeholder_path = os.path.join(path, ".placeholder").replace("\\", "/")
    if not self.file_exists(placeholder_path):
        self.write_file(placeholder_path, b"Placeholder file for directory")
open(path, mode='r')

Context manager for opening files in Snowflake stage.

Parameters:

Name Type Description Default
path str

File path in stage.

required
mode str

File mode ('r', 'rb', 'w', 'wb').

'r'

Returns:

Type Description

Context manager yielding a file-like object.

Raises:

Type Description
ValueError

If mode is unsupported.

Source code in gigaspatial/core/io/snowflake_data_store.py
@contextlib.contextmanager
def open(self, path: str, mode: str = "r"):
    """
    Context manager for opening files in Snowflake stage.

    Args:
        path: File path in stage.
        mode: File mode ('r', 'rb', 'w', 'wb').

    Returns:
        Context manager yielding a file-like object.

    Raises:
        ValueError: If mode is unsupported.
    """
    if mode == "w":
        file = io.StringIO()
        yield file
        self.write_file(path, file.getvalue())

    elif mode == "wb":
        file = io.BytesIO()
        yield file
        self.write_file(path, file.getvalue())

    elif mode == "r":
        data = self.read_file(path, encoding="UTF-8")
        file = io.StringIO(data)
        yield file

    elif mode == "rb":
        data = self.read_file(path)
        file = io.BytesIO(data)
        yield file

    else:
        raise ValueError(f"Unsupported mode: {mode}")
read_file(path, encoding=None)

Read file contents from Snowflake stage.

Parameters:

Name Type Description Default
path str

Path to the file in the stage.

required
encoding Optional[str]

Optional string encoding. If None, returns bytes.

None

Returns:

Type Description
Union[str, bytes]

File contents as a string if encoding is provided, otherwise bytes.

Raises:

Type Description
FileNotFoundError

If file is not found in the stage.

IOError

If there's an error during download or read.

Source code in gigaspatial/core/io/snowflake_data_store.py
def read_file(self, path: str, encoding: Optional[str] = None) -> Union[str, bytes]:
    """
    Read file contents from Snowflake stage.

    Args:
        path: Path to the file in the stage.
        encoding: Optional string encoding. If None, returns bytes.

    Returns:
        File contents as a string if encoding is provided, otherwise bytes.

    Raises:
        FileNotFoundError: If file is not found in the stage.
        IOError: If there's an error during download or read.
    """
    self._ensure_connection()
    cursor = self._get_connection().cursor(DictCursor)

    try:
        normalized_path = self._normalize_path(path)
        stage_path = self._get_stage_path(normalized_path)

        # Create temporary directory for download
        temp_download_dir = os.path.join(self._temp_dir, "downloads")
        os.makedirs(temp_download_dir, exist_ok=True)

        # Download file from stage using GET command
        # GET command: GET <stage_path> file://<local_path>
        temp_dir_normalized = temp_download_dir.replace("\\", "/")
        if not temp_dir_normalized.endswith("/"):
            temp_dir_normalized += "/"

        get_command = f"GET {stage_path} 'file://{temp_dir_normalized}'"
        cursor.execute(get_command)

        # Find the downloaded file (Snowflake may add prefixes/suffixes or preserve structure)
        downloaded_files = []
        for root, dirs, files in os.walk(temp_download_dir):
            for f in files:
                file_path = os.path.join(root, f)
                # Check if this file matches our expected filename
                if os.path.basename(
                    normalized_path
                ) in f or normalized_path.endswith(f):
                    downloaded_files.append(file_path)

        if not downloaded_files:
            raise FileNotFoundError(f"File not found in stage: {path}")

        # Read the first matching file
        downloaded_path = downloaded_files[0]
        with open(downloaded_path, "rb") as f:
            data = f.read()

        # Clean up
        os.remove(downloaded_path)
        # Clean up empty directories
        try:
            if os.path.exists(temp_download_dir) and not os.listdir(
                temp_download_dir
            ):
                os.rmdir(temp_download_dir)
        except OSError:
            pass

        # Decode if encoding is specified
        if encoding:
            return data.decode(encoding)
        return data

    except Exception as e:
        raise IOError(f"Error reading file {path} from Snowflake stage: {e}")
    finally:
        cursor.close()
remove(path)

Remove a file from the Snowflake stage.

Parameters:

Name Type Description Default
path str

Path to the file to remove in stage.

required

Raises:

Type Description
IOError

If removal fails.

Source code in gigaspatial/core/io/snowflake_data_store.py
def remove(self, path: str) -> None:
    """
    Remove a file from the Snowflake stage.

    Args:
        path: Path to the file to remove in stage.

    Raises:
        IOError: If removal fails.
    """
    self._ensure_connection()
    cursor = self._get_connection().cursor()

    try:
        normalized_path = self._normalize_path(path)
        stage_path = self._get_stage_path(normalized_path)

        remove_command = f"REMOVE {stage_path}"
        cursor.execute(remove_command)

    except Exception as e:
        raise IOError(f"Error removing file {path}: {e}")
    finally:
        cursor.close()
rename(source_path, destination_path, overwrite=False, delete_source=True)

Rename (move) a single file by copying to the new path and deleting source.

Parameters:

Name Type Description Default
source_path str

Existing file path in the stage.

required
destination_path str

Target file path in the stage.

required
overwrite bool

Overwrite destination if it exists.

False
delete_source bool

Delete original after successful copy.

True

Raises:

Type Description
FileNotFoundError

If source is missing.

FileExistsError

If destination exists and overwrite=False.

Source code in gigaspatial/core/io/snowflake_data_store.py
def rename(
    self,
    source_path: str,
    destination_path: str,
    overwrite: bool = False,
    delete_source: bool = True,
) -> None:
    """
    Rename (move) a single file by copying to the new path and deleting source.

    Args:
        source_path: Existing file path in the stage.
        destination_path: Target file path in the stage.
        overwrite: Overwrite destination if it exists.
        delete_source: Delete original after successful copy.

    Raises:
        FileNotFoundError: If source is missing.
        FileExistsError: If destination exists and overwrite=False.
    """
    if not self.file_exists(source_path):
        raise FileNotFoundError(f"Source file not found: {source_path}")

    if self.file_exists(destination_path) and not overwrite:
        raise FileExistsError(
            f"Destination already exists and overwrite is False: {destination_path}"
        )

    # Copy file to new location
    self.copy_file(source_path, destination_path, overwrite=overwrite)

    # Delete source if requested
    if delete_source:
        self.remove(source_path)
rmdir(dir)

Remove a directory and all its contents from the Snowflake stage.

Parameters:

Name Type Description Default
dir str

Path to the directory to remove in stage.

required

Raises:

Type Description
IOError

If removal fails.

Source code in gigaspatial/core/io/snowflake_data_store.py
def rmdir(self, dir: str) -> None:
    """
    Remove a directory and all its contents from the Snowflake stage.

    Args:
        dir: Path to the directory to remove in stage.

    Raises:
        IOError: If removal fails.
    """
    self._ensure_connection()
    cursor = self._get_connection().cursor()

    try:
        normalized_dir = self._normalize_path(dir)
        stage_path = self._get_stage_path(normalized_dir)

        # Remove all files in the directory
        remove_command = f"REMOVE {stage_path}"
        cursor.execute(remove_command)

    except Exception as e:
        raise IOError(f"Error removing directory {dir}: {e}")
    finally:
        cursor.close()
upload_directory(dir_path, stage_dir_path)

Uploads all files from a local directory to Snowflake stage.

Parameters:

Name Type Description Default
dir_path str

Local directory path.

required
stage_dir_path str

Destination directory path in the stage.

required

Raises:

Type Description
NotADirectoryError

If dir_path is not a directory.

Source code in gigaspatial/core/io/snowflake_data_store.py
def upload_directory(self, dir_path: str, stage_dir_path: str):
    """
    Uploads all files from a local directory to Snowflake stage.

    Args:
        dir_path: Local directory path.
        stage_dir_path: Destination directory path in the stage.

    Raises:
        NotADirectoryError: If dir_path is not a directory.
    """
    if not os.path.isdir(dir_path):
        raise NotADirectoryError(f"Local directory not found: {dir_path}")

    for root, dirs, files in os.walk(dir_path):
        for file in files:
            local_file_path = os.path.join(root, file)
            relative_path = os.path.relpath(local_file_path, dir_path)
            # Normalize path separators for stage
            stage_file_path = os.path.join(stage_dir_path, relative_path).replace(
                "\\", "/"
            )

            self.upload_file(local_file_path, stage_file_path)
upload_file(file_path, stage_path)

Uploads a single file from local filesystem to Snowflake stage.

Parameters:

Name Type Description Default
file_path str

Local file path.

required
stage_path str

Destination path in the stage.

required

Raises:

Type Description
FileNotFoundError

If local file is missing.

Source code in gigaspatial/core/io/snowflake_data_store.py
def upload_file(self, file_path: str, stage_path: str):
    """
    Uploads a single file from local filesystem to Snowflake stage.

    Args:
        file_path: Local file path.
        stage_path: Destination path in the stage.

    Raises:
        FileNotFoundError: If local file is missing.
    """
    try:
        if not os.path.exists(file_path):
            raise FileNotFoundError(f"Local file not found: {file_path}")

        # Read the file
        with open(file_path, "rb") as f:
            data = f.read()

        # Write to stage using write_file
        self.write_file(stage_path, data)
        self.logger.info(f"Uploaded {file_path} to {stage_path}")
    except Exception as e:
        self.logger.error(f"Failed to upload {file_path}: {e}")
        raise
walk(top)

Walk through directory tree in Snowflake stage.

Parameters:

Name Type Description Default
top str

Starting directory for the walk.

required

Yields:

Type Description
Tuple[str, List[str], List[str]]

Tuples of (dirpath, dirnames, filenames).

Source code in gigaspatial/core/io/snowflake_data_store.py
def walk(self, top: str) -> Generator[Tuple[str, List[str], List[str]], None, None]:
    """
    Walk through directory tree in Snowflake stage.

    Args:
        top: Starting directory for the walk.

    Yields:
        Tuples of (dirpath, dirnames, filenames).
    """
    try:
        normalized_top = self._normalize_path(top)

        # Use list_files to get all files (it handles path parsing correctly)
        all_files = self.list_files(normalized_top)

        # Organize into directory structure
        dirs = {}

        for file_path in all_files:
            # Ensure we're working with paths relative to the top
            if normalized_top and not file_path.startswith(normalized_top):
                continue

            # Get relative path from top
            if normalized_top and file_path.startswith(normalized_top):
                relative_path = file_path[len(normalized_top) :].lstrip("/")
            else:
                relative_path = file_path

            if not relative_path:
                continue

            # Get directory and filename
            if "/" in relative_path:
                dir_path, filename = os.path.split(relative_path)
                full_dir_path = (
                    f"{normalized_top}/{dir_path}" if normalized_top else dir_path
                )
                if full_dir_path not in dirs:
                    dirs[full_dir_path] = []
                dirs[full_dir_path].append(filename)
            else:
                # File in root of the top directory
                if normalized_top not in dirs:
                    dirs[normalized_top] = []
                dirs[normalized_top].append(relative_path)

        # Yield results in os.walk format
        for dir_path, files in dirs.items():
            # Extract subdirectories (simplified - Snowflake stages are flat)
            subdirs = []
            yield (dir_path, subdirs, files)

    except Exception as e:
        self.logger.warning(f"Error walking directory {top}: {e}")
        yield (top, [], [])
write_file(path, data)

Write data (string or bytes) to a Snowflake stage.

Parameters:

Name Type Description Default
path str

Destination path in the stage.

required
data Union[bytes, str]

Data to write (str or bytes).

required

Raises:

Type Description
ValueError

If data type is unsupported.

IOError

If there's an error during the upload process.

Source code in gigaspatial/core/io/snowflake_data_store.py
def write_file(self, path: str, data: Union[bytes, str]) -> None:
    """
    Write data (string or bytes) to a Snowflake stage.

    Args:
        path: Destination path in the stage.
        data: Data to write (str or bytes).

    Raises:
        ValueError: If data type is unsupported.
        IOError: If there's an error during the upload process.
    """
    self._ensure_connection()
    cursor = self._get_connection().cursor()

    try:
        # Convert to bytes if string
        if isinstance(data, str):
            binary_data = data.encode("utf-8")
        elif isinstance(data, bytes):
            binary_data = data
        else:
            raise ValueError(
                'Unsupported data type. Only "bytes" or "string" accepted'
            )

        normalized_path = self._normalize_path(path)

        # Write to temporary file first
        # Use the full path structure for the temp file to preserve directory structure
        temp_file_path = os.path.join(self._temp_dir, normalized_path)
        os.makedirs(os.path.dirname(temp_file_path), exist_ok=True)

        with open(temp_file_path, "wb") as f:
            f.write(binary_data)

        # Upload to stage using PUT command
        # Snowflake PUT requires the local file path and the target stage path
        # Convert Windows paths to Unix-style for Snowflake
        temp_file_normalized = os.path.abspath(temp_file_path).replace("\\", "/")

        # PUT command: PUT 'file://<absolute_local_path>' @stage_name/<path>
        # The file will be stored at the specified path in the stage
        stage_target = f"@{self.stage_name}/"
        if "/" in normalized_path:
            # Include directory structure in stage path
            dir_path = os.path.dirname(normalized_path)
            stage_target = f"@{self.stage_name}/{dir_path}/"

        # Snowflake PUT syntax: PUT 'file://<path>' @stage/path
        put_command = f"PUT 'file://{temp_file_normalized}' {stage_target} OVERWRITE=TRUE AUTO_COMPRESS=FALSE"
        cursor.execute(put_command)

        # Clean up temp file
        if os.path.exists(temp_file_path):
            os.remove(temp_file_path)
            # Clean up empty directories if they were created
            try:
                temp_dir = os.path.dirname(temp_file_path)
                if temp_dir != self._temp_dir and os.path.exists(temp_dir):
                    os.rmdir(temp_dir)
            except OSError:
                pass  # Directory not empty or other error, ignore

    except Exception as e:
        raise IOError(f"Error writing file {path} to Snowflake stage: {e}")
    finally:
        cursor.close()

writers

Module for writing datasets (DataFrames, GeoDataFrames, JSON) to a DataStore. Provides high-level utilities for serializing geospatial and tabular data.

write_dataset(data, data_store=None, path=None, **kwargs)

Write DataFrame, GeoDataFrame, or a generic object to various file formats.

Supported formats include .csv, .xlsx, .json, .parquet for DataFrames, and .geojson, .gpkg, .parquet for GeoDataFrames.

Parameters:

Name Type Description Default
data

The data to write. Can be pd.DataFrame, gpd.GeoDataFrame, or a JSON-serializable object.

required
data_store DataStore

Instance of DataStore for accessing data storage. If None, local storage is used.

None
path

Path where the file will be written.

None
**kwargs

Additional arguments passed to the specific writer function (e.g., index=False).

{}

Raises:

Type Description
ValueError

If the file type is unsupported or if there's an error writing the file.

TypeError

If input data type is incompatible with the file extension.

RuntimeError

For unexpected errors during the write process.

Source code in gigaspatial/core/io/writers.py
def write_dataset(data, data_store: DataStore = None, path=None, **kwargs):
    """
    Write DataFrame, GeoDataFrame, or a generic object to various file formats.

    Supported formats include .csv, .xlsx, .json, .parquet for DataFrames,
    and .geojson, .gpkg, .parquet for GeoDataFrames.

    Args:
        data: The data to write. Can be pd.DataFrame, gpd.GeoDataFrame, or a JSON-serializable object.
        data_store: Instance of DataStore for accessing data storage. If None, local storage is used.
        path: Path where the file will be written.
        **kwargs: Additional arguments passed to the specific writer function (e.g., index=False).

    Raises:
        ValueError: If the file type is unsupported or if there's an error writing the file.
        TypeError: If input data type is incompatible with the file extension.
        RuntimeError: For unexpected errors during the write process.
    """
    data_store = data_store or LocalDataStore()

    # Define supported file formats and their writers
    BINARY_FORMATS = {".shp", ".zip", ".parquet", ".gpkg", ".xlsx", ".xls"}

    PANDAS_WRITERS = {
        ".csv": lambda df, buf, **kw: df.to_csv(buf, **kw),
        ".xlsx": lambda df, buf, **kw: df.to_excel(buf, engine="openpyxl", **kw),
        ".json": lambda df, buf, **kw: df.to_json(buf, **kw),
        ".parquet": lambda df, buf, **kw: df.to_parquet(buf, **kw),
    }

    GEO_WRITERS = {
        ".geojson": lambda gdf, buf, **kw: gdf.to_file(buf, driver="GeoJSON", **kw),
        ".gpkg": lambda gdf, buf, **kw: gdf.to_file(buf, driver="GPKG", **kw),
        ".parquet": lambda gdf, buf, **kw: gdf.to_parquet(buf, **kw),
    }

    try:
        # Get file suffix and ensure it's lowercase
        suffix = Path(path).suffix.lower()

        # 1. Handle generic JSON data
        is_dataframe_like = isinstance(data, (pd.DataFrame, gpd.GeoDataFrame))
        if not is_dataframe_like:
            if suffix == ".json":
                try:
                    # Pass generic data directly to the write_json function
                    write_json(data, data_store, path, **kwargs)
                    return  # Successfully wrote JSON, so exit
                except Exception as e:
                    raise ValueError(f"Error writing generic JSON data: {str(e)}")
            else:
                # Raise an error if it's not a DataFrame/GeoDataFrame and not a .json file
                raise TypeError(
                    "Input data must be a pandas DataFrame or GeoDataFrame, "
                    "or a generic object destined for a '.json' file."
                )

        # 2. Handle DataFrame/GeoDataFrame
        # Determine if we need binary mode based on file type
        mode = "wb" if suffix in BINARY_FORMATS else "w"

        # Handle different data types and formats
        if isinstance(data, gpd.GeoDataFrame):
            if suffix not in GEO_WRITERS:
                supported_formats = sorted(GEO_WRITERS.keys())
                raise ValueError(
                    f"Unsupported file type for GeoDataFrame: {suffix}\n"
                    f"Supported formats: {', '.join(supported_formats)}"
                )

            try:
                # Write to BytesIO buffer first
                buffer = io.BytesIO()
                GEO_WRITERS[suffix](data, buffer, **kwargs)
                buffer.seek(0)

                # Then write buffer contents to data_store
                with data_store.open(path, "wb") as f:
                    f.write(buffer.getvalue())

                # with data_store.open(path, "wb") as f:
                #    GEO_WRITERS[suffix](data, f, **kwargs)
            except Exception as e:
                raise ValueError(f"Error writing GeoDataFrame: {str(e)}")

        else:  # pandas DataFrame
            if suffix not in PANDAS_WRITERS:
                supported_formats = sorted(PANDAS_WRITERS.keys())
                raise ValueError(
                    f"Unsupported file type for DataFrame: {suffix}\n"
                    f"Supported formats: {', '.join(supported_formats)}"
                )

            try:
                with data_store.open(path, mode) as f:
                    PANDAS_WRITERS[suffix](data, f, **kwargs)
            except Exception as e:
                raise ValueError(f"Error writing DataFrame: {str(e)}")

    except Exception as e:
        if isinstance(e, (TypeError, ValueError)):
            raise
        raise RuntimeError(f"Unexpected error writing dataset: {str(e)}")
write_datasets(data_dict, data_store=None, **kwargs)

Write multiple datasets to data storage at once.

Parameters:

Name Type Description Default
data_dict

Dictionary mapping paths (str) to data objects.

required
data_store DataStore

DataStore instance. If None, local storage is used.

None
**kwargs

Additional arguments passed to write_dataset for each item.

{}

Raises:

Type Description
ValueError

If one or more datasets fail to write, containing details of all errors.

Source code in gigaspatial/core/io/writers.py
def write_datasets(data_dict, data_store: DataStore = None, **kwargs):
    """
    Write multiple datasets to data storage at once.

    Args:
        data_dict: Dictionary mapping paths (str) to data objects.
        data_store: DataStore instance. If None, local storage is used.
        **kwargs: Additional arguments passed to write_dataset for each item.

    Raises:
        ValueError: If one or more datasets fail to write, containing details of all errors.
    """
    errors = {}
    data_store = data_store or LocalDataStore()

    for path, data in data_dict.items():
        try:
            write_dataset(data, data_store, path, **kwargs)
        except Exception as e:
            errors[path] = str(e)

    if errors:
        error_msg = "\n".join(f"- {path}: {error}" for path, error in errors.items())
        raise ValueError(f"Errors writing datasets:\n{error_msg}")
write_json(data, data_store=None, path=None, **kwargs)

Write data to a JSON file in the data store.

Parameters:

Name Type Description Default
data

Object to serialize to JSON.

required
data_store DataStore

DataStore instance to use for writing. If None, local storage is used.

None
path

Destination path in the data store.

None
**kwargs

Additional arguments passed to json.dump.

{}
Source code in gigaspatial/core/io/writers.py
def write_json(data, data_store: DataStore = None, path=None, **kwargs):
    """
    Write data to a JSON file in the data store.

    Args:
        data: Object to serialize to JSON.
        data_store: DataStore instance to use for writing. If None, local storage is used.
        path: Destination path in the data store.
        **kwargs: Additional arguments passed to json.dump.
    """
    data_store = data_store or LocalDataStore()
    with data_store.open(path, "w") as f:
        json.dump(data, f, **kwargs)

schemas

admin_boundary

Module for administrative boundary schema and processing. Defines the AdminBoundary entity, representing hierarchical divisions like countries, states, and districts with polygon geometries.

AdminBoundary

Bases: GigaGeoEntity

Represents an administrative boundary at any hierarchical level.

Inherits polygon geometry from GigaGeoEntity. Used for spatial joins, country-level filtering, and administrative enrichment workflows.

Source code in gigaspatial/core/schemas/admin_boundary.py
class AdminBoundary(GigaGeoEntity):
    """
    Represents an administrative boundary at any hierarchical level.

    Inherits polygon geometry from GigaGeoEntity. Used for spatial joins,
    country-level filtering, and administrative enrichment workflows.
    """

    model_config = GEO_ENUM_ENTITY_CONFIG

    # Identity
    boundary_id: str = Field(
        ..., max_length=100, description="Unique identifier for the boundary"
    )
    admin_level: AdminLevel = Field(
        ..., description="Hierarchical administrative level (0=country, 1=state, ...)"
    )

    # Names
    name: str = Field(..., max_length=200, description="Primary name in local language")
    name_en: Optional[str] = Field(None, max_length=200, description="Name in English")
    name_int: Optional[str] = Field(
        None, max_length=200, description="International or standardized name"
    )

    # Codes and references
    iso_code: Optional[str] = Field(
        None, max_length=20, description="ISO or local administrative code"
    )
    country_iso: Optional[str] = Field(
        None, max_length=3, description="ISO 3166-1 alpha-3 country code"
    )
    parent_id: Optional[str] = Field(
        None,
        max_length=100,
        description="boundary_id of the parent administrative unit",
    )

    # Attributes
    population: Optional[int] = Field(
        None, ge=0, description="Population count within the boundary"
    )
    population_year: Optional[int] = Field(
        None, ge=1900, le=2100, description="Reference year for population data"
    )
    area_km2: Optional[float] = Field(
        None,
        ge=0,
        description="Area of the boundary in square kilometres as reported by source",
    )
    bbox: Optional[List[float]] = Field(
        None,
        min_length=4,
        max_length=4,
        description="Bounding box [min_lon, min_lat, max_lon, max_lat]",
    )

    @property
    def id(self) -> str:
        """Map `id` to `boundary_id`."""
        return self.boundary_id
id: str property

Map id to boundary_id.

AdminBoundaryProcessor

Bases: EntityProcessor

Cleaning and normalization for AdminBoundary entities.

Source code in gigaspatial/core/schemas/admin_boundary.py
class AdminBoundaryProcessor(EntityProcessor):
    """Cleaning and normalization for AdminBoundary entities."""

    NUMERIC_COLUMNS: ClassVar[List[str]] = [
        "population",
        "population_year",
        "area_km2",
        "admin_level",
    ]

    LOWERCASE_COLUMNS: ClassVar[List[str]] = []

    verbose: bool = False

    def process(self, df: pd.DataFrame, **kwargs) -> pd.DataFrame:
        """
        Execute the full processing pipeline for admin boundaries.

        Args:
            df: Raw admin boundary DataFrame.
            **kwargs: Additional processing arguments.

        Returns:
            Processed and normalized DataFrame.
        """
        df = super().process(df, **kwargs)
        df = self._normalize_country_iso(df)
        df = self._parse_geometry(df)
        return df

    def _normalize_country_iso(self, df: pd.DataFrame) -> pd.DataFrame:
        """
        Uppercase country ISO codes for consistency.

        Args:
            df: DataFrame to normalize.

        Returns:
            DataFrame with normalized country_iso column.
        """
        if "country_iso" in df.columns:
            df["country_iso"] = df["country_iso"].str.upper()
        return df
process(df, **kwargs)

Execute the full processing pipeline for admin boundaries.

Parameters:

Name Type Description Default
df DataFrame

Raw admin boundary DataFrame.

required
**kwargs

Additional processing arguments.

{}

Returns:

Type Description
DataFrame

Processed and normalized DataFrame.

Source code in gigaspatial/core/schemas/admin_boundary.py
def process(self, df: pd.DataFrame, **kwargs) -> pd.DataFrame:
    """
    Execute the full processing pipeline for admin boundaries.

    Args:
        df: Raw admin boundary DataFrame.
        **kwargs: Additional processing arguments.

    Returns:
        Processed and normalized DataFrame.
    """
    df = super().process(df, **kwargs)
    df = self._normalize_country_iso(df)
    df = self._parse_geometry(df)
    return df
AdminBoundaryTable

Bases: EntityTable[AdminBoundary]

Container for AdminBoundary entities with hierarchy and spatial operations.

Source code in gigaspatial/core/schemas/admin_boundary.py
class AdminBoundaryTable(EntityTable[AdminBoundary]):
    """Container for AdminBoundary entities with hierarchy and spatial operations."""

    @classmethod
    def from_file(
        cls,
        file_path: Union[str, Path],
        data_store: Optional[DataStore] = None,
        clean: bool = True,
        processor: Optional[
            Union[Type[EntityProcessor], EntityProcessor]
        ] = AdminBoundaryProcessor,
        **kwargs,
    ) -> "AdminBoundaryTable":
        """
        Create an AdminBoundaryTable from a file.

        Args:
            file_path: Path to the dataset file.
            data_store: DataStore instance for file access. Defaults to LocalDataStore.
            clean: Whether to apply the processor before validation. Defaults to True.
            processor: Optional EntityProcessor subclass or instance. Defaults to AdminBoundaryProcessor.
            **kwargs: Additional arguments forwarded to read_dataset and the processor.

        Returns:
            AdminBoundaryTable with validated AdminBoundary entities.

        Raises:
            FileNotFoundError: If the file does not exist.
            ValueError: If the file cannot be read or parsed.
        """
        return super().from_file(
            file_path=file_path,
            entity_class=AdminBoundary,
            data_store=data_store,
            clean=clean,
            processor=processor,
            **kwargs,
        )

    @classmethod
    def from_dataframe(
        cls,
        df: pd.DataFrame,
        entity_class: Type[AdminBoundary] = AdminBoundary,
        clean: bool = False,
        processor: Optional[
            Union[Type[EntityProcessor], EntityProcessor]
        ] = AdminBoundaryProcessor,
        **kwargs,
    ) -> "AdminBoundaryTable":
        """
        Create an AdminBoundaryTable from an existing DataFrame.

        Args:
            df: DataFrame containing admin boundary data.
            entity_class: Entity class to validate against. Defaults to AdminBoundary.
            clean: Whether to apply AdminBoundaryProcessor before validation.
            processor: Optional EntityProcessor subclass or instance. Defaults to AdminBoundaryProcessor.
            **kwargs: Additional arguments passed to the processor.

        Returns:
            AdminBoundaryTable with validated AdminBoundary entities.
        """
        return super().from_dataframe(
            df=df,
            entity_class=entity_class,
            clean=clean,
            processor=processor,
            **kwargs,
        )

    # ------------------------------------------------------------------
    # Filters
    # ------------------------------------------------------------------

    def filter_by_admin_level(self, level: AdminLevel) -> "AdminBoundaryTable":
        """Filter boundaries by administrative level."""
        return self.__class__(
            entities=[e for e in self.entities if e.admin_level == level.value]
        )

    def filter_by_country(self, country_iso: str) -> "AdminBoundaryTable":
        """Filter boundaries by ISO 3166-1 alpha-3 country code."""
        return self.__class__(
            entities=[e for e in self.entities if e.country_iso == country_iso.upper()]
        )

    def filter_by_parent(self, parent_id: str) -> "AdminBoundaryTable":
        """Return all direct children of a given boundary."""
        return self.__class__(
            entities=[e for e in self.entities if e.parent_id == parent_id]
        )

    def filter_contains_point(self, lat: float, lon: float) -> "AdminBoundaryTable":
        """Return all boundaries whose geometry contains the given point."""
        point = Point(lon, lat)
        return self.__class__(
            entities=[e for e in self.entities if e.geometry.contains(point)]
        )

    # ------------------------------------------------------------------
    # Hierarchy
    # ------------------------------------------------------------------

    def get_children(self, boundary_id: str) -> "AdminBoundaryTable":
        """Return all direct children of a boundary by its ID."""
        return self.filter_by_parent(boundary_id)

    def get_parent(self, boundary_id: str) -> Optional[AdminBoundary]:
        """Return the parent boundary of a given boundary, if present in the table."""
        entity = self.get_by_id(boundary_id)
        if entity is None or entity.parent_id is None:
            return None
        return self.get_by_id(entity.parent_id)

    def get_by_id(self, boundary_id: str) -> Optional[AdminBoundary]:
        """Return a boundary by its ID, or None if not found."""
        for e in self.entities:
            if e.boundary_id == boundary_id:
                return e
        return None

    def to_hierarchy_dict(self) -> Dict[str, List[str]]:
        """
        Build a parent → children mapping of boundary IDs.

        Returns:
            Dict mapping parent boundary_id → list of child boundary_ids.
        """
        hierarchy: Dict[str, List[str]] = {}
        for e in self.entities:
            if e.parent_id:
                hierarchy.setdefault(e.parent_id, []).append(e.boundary_id)
        return hierarchy

    # ------------------------------------------------------------------
    # Aggregations
    # ------------------------------------------------------------------

    def get_countries(self) -> Set[str]:
        """
        Return the set of all unique country ISO codes present in the table.

        Returns:
            Set of unique country ISO-3 strings.
        """
        return {e.country_iso for e in self.entities if e.country_iso is not None}

    def get_admin_levels(self) -> Set[int]:
        """
        Return the set of all unique admin levels present in the table.

        Returns:
            Set of unique administrative level integers.
        """
        return {e.admin_level for e in self.entities}
filter_by_admin_level(level)

Filter boundaries by administrative level.

Source code in gigaspatial/core/schemas/admin_boundary.py
def filter_by_admin_level(self, level: AdminLevel) -> "AdminBoundaryTable":
    """Filter boundaries by administrative level."""
    return self.__class__(
        entities=[e for e in self.entities if e.admin_level == level.value]
    )
filter_by_country(country_iso)

Filter boundaries by ISO 3166-1 alpha-3 country code.

Source code in gigaspatial/core/schemas/admin_boundary.py
def filter_by_country(self, country_iso: str) -> "AdminBoundaryTable":
    """Filter boundaries by ISO 3166-1 alpha-3 country code."""
    return self.__class__(
        entities=[e for e in self.entities if e.country_iso == country_iso.upper()]
    )
filter_by_parent(parent_id)

Return all direct children of a given boundary.

Source code in gigaspatial/core/schemas/admin_boundary.py
def filter_by_parent(self, parent_id: str) -> "AdminBoundaryTable":
    """Return all direct children of a given boundary."""
    return self.__class__(
        entities=[e for e in self.entities if e.parent_id == parent_id]
    )
filter_contains_point(lat, lon)

Return all boundaries whose geometry contains the given point.

Source code in gigaspatial/core/schemas/admin_boundary.py
def filter_contains_point(self, lat: float, lon: float) -> "AdminBoundaryTable":
    """Return all boundaries whose geometry contains the given point."""
    point = Point(lon, lat)
    return self.__class__(
        entities=[e for e in self.entities if e.geometry.contains(point)]
    )
from_dataframe(df, entity_class=AdminBoundary, clean=False, processor=AdminBoundaryProcessor, **kwargs) classmethod

Create an AdminBoundaryTable from an existing DataFrame.

Parameters:

Name Type Description Default
df DataFrame

DataFrame containing admin boundary data.

required
entity_class Type[AdminBoundary]

Entity class to validate against. Defaults to AdminBoundary.

AdminBoundary
clean bool

Whether to apply AdminBoundaryProcessor before validation.

False
processor Optional[Union[Type[EntityProcessor], EntityProcessor]]

Optional EntityProcessor subclass or instance. Defaults to AdminBoundaryProcessor.

AdminBoundaryProcessor
**kwargs

Additional arguments passed to the processor.

{}

Returns:

Type Description
'AdminBoundaryTable'

AdminBoundaryTable with validated AdminBoundary entities.

Source code in gigaspatial/core/schemas/admin_boundary.py
@classmethod
def from_dataframe(
    cls,
    df: pd.DataFrame,
    entity_class: Type[AdminBoundary] = AdminBoundary,
    clean: bool = False,
    processor: Optional[
        Union[Type[EntityProcessor], EntityProcessor]
    ] = AdminBoundaryProcessor,
    **kwargs,
) -> "AdminBoundaryTable":
    """
    Create an AdminBoundaryTable from an existing DataFrame.

    Args:
        df: DataFrame containing admin boundary data.
        entity_class: Entity class to validate against. Defaults to AdminBoundary.
        clean: Whether to apply AdminBoundaryProcessor before validation.
        processor: Optional EntityProcessor subclass or instance. Defaults to AdminBoundaryProcessor.
        **kwargs: Additional arguments passed to the processor.

    Returns:
        AdminBoundaryTable with validated AdminBoundary entities.
    """
    return super().from_dataframe(
        df=df,
        entity_class=entity_class,
        clean=clean,
        processor=processor,
        **kwargs,
    )
from_file(file_path, data_store=None, clean=True, processor=AdminBoundaryProcessor, **kwargs) classmethod

Create an AdminBoundaryTable from a file.

Parameters:

Name Type Description Default
file_path Union[str, Path]

Path to the dataset file.

required
data_store Optional[DataStore]

DataStore instance for file access. Defaults to LocalDataStore.

None
clean bool

Whether to apply the processor before validation. Defaults to True.

True
processor Optional[Union[Type[EntityProcessor], EntityProcessor]]

Optional EntityProcessor subclass or instance. Defaults to AdminBoundaryProcessor.

AdminBoundaryProcessor
**kwargs

Additional arguments forwarded to read_dataset and the processor.

{}

Returns:

Type Description
'AdminBoundaryTable'

AdminBoundaryTable with validated AdminBoundary entities.

Raises:

Type Description
FileNotFoundError

If the file does not exist.

ValueError

If the file cannot be read or parsed.

Source code in gigaspatial/core/schemas/admin_boundary.py
@classmethod
def from_file(
    cls,
    file_path: Union[str, Path],
    data_store: Optional[DataStore] = None,
    clean: bool = True,
    processor: Optional[
        Union[Type[EntityProcessor], EntityProcessor]
    ] = AdminBoundaryProcessor,
    **kwargs,
) -> "AdminBoundaryTable":
    """
    Create an AdminBoundaryTable from a file.

    Args:
        file_path: Path to the dataset file.
        data_store: DataStore instance for file access. Defaults to LocalDataStore.
        clean: Whether to apply the processor before validation. Defaults to True.
        processor: Optional EntityProcessor subclass or instance. Defaults to AdminBoundaryProcessor.
        **kwargs: Additional arguments forwarded to read_dataset and the processor.

    Returns:
        AdminBoundaryTable with validated AdminBoundary entities.

    Raises:
        FileNotFoundError: If the file does not exist.
        ValueError: If the file cannot be read or parsed.
    """
    return super().from_file(
        file_path=file_path,
        entity_class=AdminBoundary,
        data_store=data_store,
        clean=clean,
        processor=processor,
        **kwargs,
    )
get_admin_levels()

Return the set of all unique admin levels present in the table.

Returns:

Type Description
Set[int]

Set of unique administrative level integers.

Source code in gigaspatial/core/schemas/admin_boundary.py
def get_admin_levels(self) -> Set[int]:
    """
    Return the set of all unique admin levels present in the table.

    Returns:
        Set of unique administrative level integers.
    """
    return {e.admin_level for e in self.entities}
get_by_id(boundary_id)

Return a boundary by its ID, or None if not found.

Source code in gigaspatial/core/schemas/admin_boundary.py
def get_by_id(self, boundary_id: str) -> Optional[AdminBoundary]:
    """Return a boundary by its ID, or None if not found."""
    for e in self.entities:
        if e.boundary_id == boundary_id:
            return e
    return None
get_children(boundary_id)

Return all direct children of a boundary by its ID.

Source code in gigaspatial/core/schemas/admin_boundary.py
def get_children(self, boundary_id: str) -> "AdminBoundaryTable":
    """Return all direct children of a boundary by its ID."""
    return self.filter_by_parent(boundary_id)
get_countries()

Return the set of all unique country ISO codes present in the table.

Returns:

Type Description
Set[str]

Set of unique country ISO-3 strings.

Source code in gigaspatial/core/schemas/admin_boundary.py
def get_countries(self) -> Set[str]:
    """
    Return the set of all unique country ISO codes present in the table.

    Returns:
        Set of unique country ISO-3 strings.
    """
    return {e.country_iso for e in self.entities if e.country_iso is not None}
get_parent(boundary_id)

Return the parent boundary of a given boundary, if present in the table.

Source code in gigaspatial/core/schemas/admin_boundary.py
def get_parent(self, boundary_id: str) -> Optional[AdminBoundary]:
    """Return the parent boundary of a given boundary, if present in the table."""
    entity = self.get_by_id(boundary_id)
    if entity is None or entity.parent_id is None:
        return None
    return self.get_by_id(entity.parent_id)
to_hierarchy_dict()

Build a parent → children mapping of boundary IDs.

Returns:

Type Description
Dict[str, List[str]]

Dict mapping parent boundary_id → list of child boundary_ids.

Source code in gigaspatial/core/schemas/admin_boundary.py
def to_hierarchy_dict(self) -> Dict[str, List[str]]:
    """
    Build a parent → children mapping of boundary IDs.

    Returns:
        Dict mapping parent boundary_id → list of child boundary_ids.
    """
    hierarchy: Dict[str, List[str]] = {}
    for e in self.entities:
        if e.parent_id:
            hierarchy.setdefault(e.parent_id, []).append(e.boundary_id)
    return hierarchy
AdminLevel

Bases: int, Enum

Hierarchical administrative division levels.

Source code in gigaspatial/core/schemas/admin_boundary.py
class AdminLevel(int, Enum):
    """Hierarchical administrative division levels."""

    COUNTRY = 0
    STATE = 1  # state / province / region
    DISTRICT = 2  # district / county / department
    SUBDISTRICT = 3  # sub-district / municipality
    WARD = 4  # ward / village / locality

building_footprint

Module for building footprint schema and processing. Defines the BuildingFootprint entity, representing physical building structures with polygon geometries.

BuildingCondition

Bases: str, Enum

Physical condition of a building structure.

Source code in gigaspatial/core/schemas/building_footprint.py
class BuildingCondition(str, Enum):
    """Physical condition of a building structure."""

    GOOD = "good"
    FAIR = "fair"
    POOR = "poor"
    RUIN = "ruin"
BuildingFootprint

Bases: GigaGeoEntity

Represents a building footprint as a polygon geometry.

Used for infrastructure proximity analysis, population estimation, and spatial enrichment workflows.

Source code in gigaspatial/core/schemas/building_footprint.py
class BuildingFootprint(GigaGeoEntity):
    """
    Represents a building footprint as a polygon geometry.

    Used for infrastructure proximity analysis, population estimation,
    and spatial enrichment workflows.
    """

    model_config = GEO_ENTITY_CONFIG

    # Identity
    footprint_id: str = Field(
        ..., max_length=100, description="Unique identifier for the building footprint"
    )
    footprint_id_source: Optional[str] = Field(
        None, max_length=100, description="Original identifier in the source dataset"
    )

    # Classification
    building_type: Optional[BuildingType] = Field(
        None, description="Functional classification of the building"
    )
    building_condition: Optional[BuildingCondition] = Field(
        None, description="Physical condition of the building structure"
    )
    building_material: Optional[BuildingMaterial] = Field(
        None, description="Primary construction material of the building"
    )

    # Physical attributes
    height_meters: Optional[float] = Field(
        None, ge=0, description="Height of the building in meters"
    )
    num_floors: Optional[int] = Field(
        None, ge=1, description="Number of floors in the building"
    )
    area_sq_m: Optional[float] = Field(
        None, ge=0, description="Footprint area in square metres as reported by source"
    )
    roof_type: Optional[str] = Field(
        None,
        max_length=50,
        description="Type of roof structure (e.g. flat, gabled, hipped)",
    )

    # Provenance
    source: Optional[str] = Field(
        None,
        max_length=100,
        description="Data source (e.g. OSM, Google, Microsoft, DigitizeAfrica)",
    )
    capture_date: Optional[str] = Field(
        None,
        description="ISO 8601 date when the footprint was captured or last updated",
    )
    confidence: Optional[float] = Field(
        None,
        ge=0,
        le=1,
        description="Model confidence score for AI-derived footprints (0–1)",
    )

    @property
    def id(self) -> str:
        """Map `id` to `footprint_id`."""
        return self.footprint_id
id: str property

Map id to footprint_id.

BuildingFootprintProcessor

Bases: EntityProcessor

Cleaning and normalization for BuildingFootprint entities.

Source code in gigaspatial/core/schemas/building_footprint.py
class BuildingFootprintProcessor(EntityProcessor):
    """Cleaning and normalization for BuildingFootprint entities."""

    # BuildingFootprint has no lat/lon (GigaGeoEntity)
    NUMERIC_COLUMNS: ClassVar[list[str]] = [
        "height_meters",
        "num_floors",
        "area_sq_m",
        "confidence",
    ]

    LOWERCASE_COLUMNS: ClassVar[list[str]] = [
        "building_type",
        "building_condition",
        "building_material",
    ]

    BUILDING_TYPE_ALIAS_MAP: ClassVar[dict[str, str]] = BUILDING_TYPE_ALIAS_MAP

    def process(self, df: pd.DataFrame, **kwargs) -> pd.DataFrame:
        """
        Execute the full processing pipeline for building footprints.

        Args:
            df: Raw building footprint DataFrame.
            **kwargs: Additional processing arguments.

        Returns:
            Processed and normalized DataFrame.
        """
        df = super().process(df, **kwargs)
        df = self._normalize_building_type(df)
        df = self._parse_geometry(df)
        return df

    def _normalize_building_type(self, df: pd.DataFrame) -> pd.DataFrame:
        """
        Normalize building_type values to canonical BuildingType enums.

        Args:
            df: DataFrame to normalize.

        Returns:
            DataFrame with normalized building_type column.
        """
        return self._normalize_enum_column(
            df,
            column="building_type",
            alias_map=self.BUILDING_TYPE_ALIAS_MAP,
            valid_values={bt.value for bt in BuildingType},
            required=False,
        )
process(df, **kwargs)

Execute the full processing pipeline for building footprints.

Parameters:

Name Type Description Default
df DataFrame

Raw building footprint DataFrame.

required
**kwargs

Additional processing arguments.

{}

Returns:

Type Description
DataFrame

Processed and normalized DataFrame.

Source code in gigaspatial/core/schemas/building_footprint.py
def process(self, df: pd.DataFrame, **kwargs) -> pd.DataFrame:
    """
    Execute the full processing pipeline for building footprints.

    Args:
        df: Raw building footprint DataFrame.
        **kwargs: Additional processing arguments.

    Returns:
        Processed and normalized DataFrame.
    """
    df = super().process(df, **kwargs)
    df = self._normalize_building_type(df)
    df = self._parse_geometry(df)
    return df
BuildingFootprintTable

Bases: EntityTable[BuildingFootprint]

Container for BuildingFootprint entities with footprint-specific operations.

Source code in gigaspatial/core/schemas/building_footprint.py
class BuildingFootprintTable(EntityTable[BuildingFootprint]):
    """Container for BuildingFootprint entities with footprint-specific operations."""

    @classmethod
    def from_file(
        cls,
        file_path: Union[str, Path],
        data_store: Optional[DataStore] = None,
        clean: bool = True,
        processor: Optional[
            Union[Type[EntityProcessor], EntityProcessor]
        ] = BuildingFootprintProcessor,
        **kwargs,
    ) -> "BuildingFootprintTable":
        """
        Create a BuildingFootprintTable from a file.

        Args:
            file_path: Path to the dataset file.
            data_store: DataStore instance for file access. Defaults to LocalDataStore.
            clean: Whether to apply the processor before validation. Defaults to True.
            processor: Optional EntityProcessor subclass or instance. Defaults to BuildingFootprintProcessor.
            **kwargs: Additional arguments forwarded to read_dataset and the processor.

        Returns:
            BuildingFootprintTable with validated BuildingFootprint entities.

        Raises:
            FileNotFoundError: If the file does not exist.
            ValueError: If the file cannot be read or parsed.
        """
        return super().from_file(
            file_path=file_path,
            entity_class=BuildingFootprint,
            data_store=data_store,
            clean=clean,
            processor=processor,
            **kwargs,
        )

    @classmethod
    def from_dataframe(
        cls,
        df: pd.DataFrame,
        entity_class: Type[BuildingFootprint] = BuildingFootprint,
        clean: bool = False,
        processor: Optional[
            Union[Type[EntityProcessor], EntityProcessor]
        ] = BuildingFootprintProcessor,
        **kwargs,
    ) -> "BuildingFootprintTable":
        """
        Create a BuildingFootprintTable from an existing DataFrame.

        Args:
            df: DataFrame containing building footprint data.
            entity_class: Entity class to validate against. Defaults to BuildingFootprint.
            clean: Whether to apply BuildingFootprintProcessor before validation.
            processor: Optional EntityProcessor subclass or instance. Defaults to BuildingFootprintProcessor.
            **kwargs: Additional arguments passed to the processor.

        Returns:
            BuildingFootprintTable with validated BuildingFootprint entities.
        """
        return super().from_dataframe(
            df=df,
            entity_class=entity_class,
            clean=clean,
            processor=processor,
            **kwargs,
        )

    # ------------------------------------------------------------------
    # Filters
    # ------------------------------------------------------------------

    def filter_by_building_type(
        self, building_type: BuildingType
    ) -> "BuildingFootprintTable":
        """Filter footprints by building functional type."""
        return self.__class__(
            entities=[
                e for e in self.entities if e.building_type == building_type.value
            ]
        )

    def filter_by_building_types(
        self, building_types: set[BuildingType]
    ) -> "BuildingFootprintTable":
        """Filter footprints matching any of the given building types."""
        values = {bt.value for bt in building_types}
        return self.__class__(
            entities=[e for e in self.entities if e.building_type in values]
        )

    def filter_by_condition(
        self, condition: BuildingCondition
    ) -> "BuildingFootprintTable":
        """Filter footprints by physical condition."""
        return self.__class__(
            entities=[
                e for e in self.entities if e.building_condition == condition.value
            ]
        )

    def filter_by_min_confidence(
        self, min_confidence: float
    ) -> "BuildingFootprintTable":
        """Filter AI-derived footprints by minimum model confidence score."""
        return self.__class__(
            entities=[
                e
                for e in self.entities
                if e.confidence is not None and e.confidence >= min_confidence
            ]
        )

    def filter_by_min_area(self, min_area_sq_m: float) -> "BuildingFootprintTable":
        """Filter footprints by minimum reported area in square metres."""
        return self.__class__(
            entities=[
                e
                for e in self.entities
                if e.area_sq_m is not None and e.area_sq_m >= min_area_sq_m
            ]
        )

    def filter_contains_point(self, lat: float, lon: float) -> "BuildingFootprintTable":
        """Return all footprints whose geometry contains the given point."""
        from shapely.geometry import Point

        point = Point(lon, lat)
        return self.__class__(
            entities=[e for e in self.entities if e.geometry.contains(point)]
        )

    # ------------------------------------------------------------------
    # Aggregations
    # ------------------------------------------------------------------

    def get_building_types(self) -> set[str]:
        """
        Return the set of all unique building types present in the table.

        Returns:
            Set of building type strings.
        """
        return {e.building_type for e in self.entities if e.building_type is not None}

    def get_sources(self) -> set[str]:
        """
        Return the set of all unique data sources present in the table.

        Returns:
            Set of source name strings.
        """
        return {e.source for e in self.entities if e.source is not None}

    def total_footprint_area_sq_m(self) -> float:
        """
        Compute total building footprint area in square metres from actual geometries.

        Uses UTM projection for accuracy. Does not dissolve overlapping footprints
        since buildings should not overlap in a clean dataset.

        Returns:
            Total footprint area in square metres.
        """
        import geopandas as gpd
        from gigaspatial.processing.geo import estimate_utm_crs_with_fallback

        gdf = self.to_geodataframe()
        utm_crs = estimate_utm_crs_with_fallback(gdf)
        return gdf.to_crs(utm_crs).geometry.area.sum()

    def get_footprint_summary(self) -> pd.DataFrame:
        """
        Return a summary DataFrame of footprint counts and area by building type and source.

        Returns:
            DataFrame with columns: building_type, source, count, total_area_sq_m.
        """
        import geopandas as gpd
        from gigaspatial.processing.geo import estimate_utm_crs_with_fallback

        gdf = self.to_geodataframe()
        utm_crs = estimate_utm_crs_with_fallback(gdf)
        gdf["area_sq_m"] = gdf.to_crs(utm_crs).geometry.area

        return (
            gdf.groupby(["building_type", "source"])
            .agg(
                count=("footprint_id", "count"),
                total_area_sq_m=("area_sq_m", "sum"),
            )
            .reset_index()
        )
filter_by_building_type(building_type)

Filter footprints by building functional type.

Source code in gigaspatial/core/schemas/building_footprint.py
def filter_by_building_type(
    self, building_type: BuildingType
) -> "BuildingFootprintTable":
    """Filter footprints by building functional type."""
    return self.__class__(
        entities=[
            e for e in self.entities if e.building_type == building_type.value
        ]
    )
filter_by_building_types(building_types)

Filter footprints matching any of the given building types.

Source code in gigaspatial/core/schemas/building_footprint.py
def filter_by_building_types(
    self, building_types: set[BuildingType]
) -> "BuildingFootprintTable":
    """Filter footprints matching any of the given building types."""
    values = {bt.value for bt in building_types}
    return self.__class__(
        entities=[e for e in self.entities if e.building_type in values]
    )
filter_by_condition(condition)

Filter footprints by physical condition.

Source code in gigaspatial/core/schemas/building_footprint.py
def filter_by_condition(
    self, condition: BuildingCondition
) -> "BuildingFootprintTable":
    """Filter footprints by physical condition."""
    return self.__class__(
        entities=[
            e for e in self.entities if e.building_condition == condition.value
        ]
    )
filter_by_min_area(min_area_sq_m)

Filter footprints by minimum reported area in square metres.

Source code in gigaspatial/core/schemas/building_footprint.py
def filter_by_min_area(self, min_area_sq_m: float) -> "BuildingFootprintTable":
    """Filter footprints by minimum reported area in square metres."""
    return self.__class__(
        entities=[
            e
            for e in self.entities
            if e.area_sq_m is not None and e.area_sq_m >= min_area_sq_m
        ]
    )
filter_by_min_confidence(min_confidence)

Filter AI-derived footprints by minimum model confidence score.

Source code in gigaspatial/core/schemas/building_footprint.py
def filter_by_min_confidence(
    self, min_confidence: float
) -> "BuildingFootprintTable":
    """Filter AI-derived footprints by minimum model confidence score."""
    return self.__class__(
        entities=[
            e
            for e in self.entities
            if e.confidence is not None and e.confidence >= min_confidence
        ]
    )
filter_contains_point(lat, lon)

Return all footprints whose geometry contains the given point.

Source code in gigaspatial/core/schemas/building_footprint.py
def filter_contains_point(self, lat: float, lon: float) -> "BuildingFootprintTable":
    """Return all footprints whose geometry contains the given point."""
    from shapely.geometry import Point

    point = Point(lon, lat)
    return self.__class__(
        entities=[e for e in self.entities if e.geometry.contains(point)]
    )
from_dataframe(df, entity_class=BuildingFootprint, clean=False, processor=BuildingFootprintProcessor, **kwargs) classmethod

Create a BuildingFootprintTable from an existing DataFrame.

Parameters:

Name Type Description Default
df DataFrame

DataFrame containing building footprint data.

required
entity_class Type[BuildingFootprint]

Entity class to validate against. Defaults to BuildingFootprint.

BuildingFootprint
clean bool

Whether to apply BuildingFootprintProcessor before validation.

False
processor Optional[Union[Type[EntityProcessor], EntityProcessor]]

Optional EntityProcessor subclass or instance. Defaults to BuildingFootprintProcessor.

BuildingFootprintProcessor
**kwargs

Additional arguments passed to the processor.

{}

Returns:

Type Description
BuildingFootprintTable

BuildingFootprintTable with validated BuildingFootprint entities.

Source code in gigaspatial/core/schemas/building_footprint.py
@classmethod
def from_dataframe(
    cls,
    df: pd.DataFrame,
    entity_class: Type[BuildingFootprint] = BuildingFootprint,
    clean: bool = False,
    processor: Optional[
        Union[Type[EntityProcessor], EntityProcessor]
    ] = BuildingFootprintProcessor,
    **kwargs,
) -> "BuildingFootprintTable":
    """
    Create a BuildingFootprintTable from an existing DataFrame.

    Args:
        df: DataFrame containing building footprint data.
        entity_class: Entity class to validate against. Defaults to BuildingFootprint.
        clean: Whether to apply BuildingFootprintProcessor before validation.
        processor: Optional EntityProcessor subclass or instance. Defaults to BuildingFootprintProcessor.
        **kwargs: Additional arguments passed to the processor.

    Returns:
        BuildingFootprintTable with validated BuildingFootprint entities.
    """
    return super().from_dataframe(
        df=df,
        entity_class=entity_class,
        clean=clean,
        processor=processor,
        **kwargs,
    )
from_file(file_path, data_store=None, clean=True, processor=BuildingFootprintProcessor, **kwargs) classmethod

Create a BuildingFootprintTable from a file.

Parameters:

Name Type Description Default
file_path Union[str, Path]

Path to the dataset file.

required
data_store Optional[DataStore]

DataStore instance for file access. Defaults to LocalDataStore.

None
clean bool

Whether to apply the processor before validation. Defaults to True.

True
processor Optional[Union[Type[EntityProcessor], EntityProcessor]]

Optional EntityProcessor subclass or instance. Defaults to BuildingFootprintProcessor.

BuildingFootprintProcessor
**kwargs

Additional arguments forwarded to read_dataset and the processor.

{}

Returns:

Type Description
BuildingFootprintTable

BuildingFootprintTable with validated BuildingFootprint entities.

Raises:

Type Description
FileNotFoundError

If the file does not exist.

ValueError

If the file cannot be read or parsed.

Source code in gigaspatial/core/schemas/building_footprint.py
@classmethod
def from_file(
    cls,
    file_path: Union[str, Path],
    data_store: Optional[DataStore] = None,
    clean: bool = True,
    processor: Optional[
        Union[Type[EntityProcessor], EntityProcessor]
    ] = BuildingFootprintProcessor,
    **kwargs,
) -> "BuildingFootprintTable":
    """
    Create a BuildingFootprintTable from a file.

    Args:
        file_path: Path to the dataset file.
        data_store: DataStore instance for file access. Defaults to LocalDataStore.
        clean: Whether to apply the processor before validation. Defaults to True.
        processor: Optional EntityProcessor subclass or instance. Defaults to BuildingFootprintProcessor.
        **kwargs: Additional arguments forwarded to read_dataset and the processor.

    Returns:
        BuildingFootprintTable with validated BuildingFootprint entities.

    Raises:
        FileNotFoundError: If the file does not exist.
        ValueError: If the file cannot be read or parsed.
    """
    return super().from_file(
        file_path=file_path,
        entity_class=BuildingFootprint,
        data_store=data_store,
        clean=clean,
        processor=processor,
        **kwargs,
    )
get_building_types()

Return the set of all unique building types present in the table.

Returns:

Type Description
set[str]

Set of building type strings.

Source code in gigaspatial/core/schemas/building_footprint.py
def get_building_types(self) -> set[str]:
    """
    Return the set of all unique building types present in the table.

    Returns:
        Set of building type strings.
    """
    return {e.building_type for e in self.entities if e.building_type is not None}
get_footprint_summary()

Return a summary DataFrame of footprint counts and area by building type and source.

Returns:

Type Description
DataFrame

DataFrame with columns: building_type, source, count, total_area_sq_m.

Source code in gigaspatial/core/schemas/building_footprint.py
def get_footprint_summary(self) -> pd.DataFrame:
    """
    Return a summary DataFrame of footprint counts and area by building type and source.

    Returns:
        DataFrame with columns: building_type, source, count, total_area_sq_m.
    """
    import geopandas as gpd
    from gigaspatial.processing.geo import estimate_utm_crs_with_fallback

    gdf = self.to_geodataframe()
    utm_crs = estimate_utm_crs_with_fallback(gdf)
    gdf["area_sq_m"] = gdf.to_crs(utm_crs).geometry.area

    return (
        gdf.groupby(["building_type", "source"])
        .agg(
            count=("footprint_id", "count"),
            total_area_sq_m=("area_sq_m", "sum"),
        )
        .reset_index()
    )
get_sources()

Return the set of all unique data sources present in the table.

Returns:

Type Description
set[str]

Set of source name strings.

Source code in gigaspatial/core/schemas/building_footprint.py
def get_sources(self) -> set[str]:
    """
    Return the set of all unique data sources present in the table.

    Returns:
        Set of source name strings.
    """
    return {e.source for e in self.entities if e.source is not None}
total_footprint_area_sq_m()

Compute total building footprint area in square metres from actual geometries.

Uses UTM projection for accuracy. Does not dissolve overlapping footprints since buildings should not overlap in a clean dataset.

Returns:

Type Description
float

Total footprint area in square metres.

Source code in gigaspatial/core/schemas/building_footprint.py
def total_footprint_area_sq_m(self) -> float:
    """
    Compute total building footprint area in square metres from actual geometries.

    Uses UTM projection for accuracy. Does not dissolve overlapping footprints
    since buildings should not overlap in a clean dataset.

    Returns:
        Total footprint area in square metres.
    """
    import geopandas as gpd
    from gigaspatial.processing.geo import estimate_utm_crs_with_fallback

    gdf = self.to_geodataframe()
    utm_crs = estimate_utm_crs_with_fallback(gdf)
    return gdf.to_crs(utm_crs).geometry.area.sum()
BuildingMaterial

Bases: str, Enum

Primary construction material of the building.

Source code in gigaspatial/core/schemas/building_footprint.py
class BuildingMaterial(str, Enum):
    """Primary construction material of the building."""

    CONCRETE = "concrete"
    BRICK = "brick"
    WOOD = "wood"
    METAL = "metal"
    MUD = "mud"
    STONE = "stone"
    MIXED = "mixed"
BuildingType

Bases: str, Enum

Functional classification of a building.

Source code in gigaspatial/core/schemas/building_footprint.py
class BuildingType(str, Enum):
    """Functional classification of a building."""

    RESIDENTIAL = "residential"
    COMMERCIAL = "commercial"
    INDUSTRIAL = "industrial"
    EDUCATIONAL = "educational"
    HEALTHCARE = "healthcare"
    RELIGIOUS = "religious"
    GOVERNMENTAL = "governmental"
    TRANSPORTATION = "transportation"
    AGRICULTURAL = "agricultural"
    MIXED = "mixed"
    UNKNOWN = "unknown"

entity

Base entity schemas and table containers. Provides the foundation for all geospatial and network entities in Giga Spatial, including point-based (GigaEntity) and geometry-based (GigaGeoEntity) records, along with generic table containers (EntityTable).

BaseGigaEntity

Bases: BaseModel, ABC

Base class for all Giga entities with common fields.

Source code in gigaspatial/core/schemas/entity.py
class BaseGigaEntity(BaseModel, ABC):
    """Base class for all Giga entities with common fields."""

    model_config = BASE_ENTITY_CONFIG

    source: Optional[str] = Field(None, max_length=100, description="Source reference")
    source_detail: Optional[str] = Field(
        None, description="Detailed source information"
    )

    @property
    @abstractmethod
    def id(self) -> str:
        """Abstract property that must be implemented by subclasses."""
        raise NotImplementedError("Subclasses must implement id property")
id: str abstractmethod property

Abstract property that must be implemented by subclasses.

EntityTable

Bases: BaseModel, Generic[E]

Source code in gigaspatial/core/schemas/entity.py
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
class EntityTable(BaseModel, Generic[E]):
    model_config = ConfigDict(arbitrary_types_allowed=True)

    entities: List[E] = Field(default_factory=list)
    entity_class: Optional[Type[E]] = Field(default=None, exclude=True)
    _cached_kdtree: Optional[cKDTree] = PrivateAttr(
        default=None
    )  # Internal cache for the KDTree

    @model_validator(mode="after")
    def _infer_entity_class(self) -> "EntityTable":
        """
        Infer entity_class from entities list if not explicitly provided.

        Returns:
            Table with potentially updated entity_class.
        """
        if self.entity_class is None and self.entities:
            self.entity_class = type(self.entities[0])
        return self

    @property
    def is_point_entity(self) -> bool:
        """
        Whether entities carry lat/lon point location.

        Returns:
            True if entities are GigaEntity but not GigaGeoEntity.
        """
        if self.entity_class is None:
            return False
        return issubclass(self.entity_class, GigaEntity) and not issubclass(
            self.entity_class, GigaGeoEntity
        )

    @property
    def is_geo_entity(self) -> bool:
        """
        Whether entities carry complex polygon/multipolygon geometry.

        Returns:
            True if entities are GigaGeoEntity.
        """
        if self.entity_class is None:
            return False
        return issubclass(self.entity_class, GigaGeoEntity)

    @classmethod
    def from_file(
        cls,
        file_path: Union[str, Path],
        entity_class: Type[E],
        data_store: Optional[DataStore] = None,
        clean: bool = True,
        processor: Optional[Union[Type[EntityProcessor], EntityProcessor]] = None,
        **kwargs,
    ) -> "EntityTable[E]":
        """
        Create an EntityTable from a file, with optional pre-validation cleaning.

        Args:
            file_path: Path to the dataset file.
            entity_class: The Pydantic entity class to validate each row against.
            data_store: DataStore instance for file access. Defaults to LocalDataStore.
            clean: Whether to apply an EntityProcessor before validation. Defaults to True.
            processor: Optional EntityProcessor subclass or instance to apply before validation.
            **kwargs: Additional arguments forwarded to read_dataset and the processor.

        Returns:
            EntityTable instance containing successfully validated entities.

        Raises:
            FileNotFoundError: If the file does not exist in the data store.
            ValueError: If the file cannot be read or parsed.
        """

        df = read_dataset(file_path, data_store=data_store, **kwargs)
        logger.debug("Loaded %d rows from %s", len(df), file_path)

        try:
            return cls.from_dataframe(
                df,
                entity_class,
                clean=clean,
                processor=processor,
                **kwargs,
            )
        except Exception as e:
            raise ValueError(f"Error reading or processing the file: {e}")

    @classmethod
    def from_dataframe(
        cls,
        df: pd.DataFrame,
        entity_class: Type[E],
        clean: bool = False,
        processor: Optional[Union[Type[EntityProcessor], EntityProcessor]] = None,
        **kwargs,
    ) -> "EntityTable[E]":
        """
        Create an EntityTable from an existing DataFrame.

        Args:
            df: DataFrame containing entity data.
            entity_class: The Pydantic entity class to validate each row against.
            clean: Whether to apply an EntityProcessor before validation.
            processor: Optional processor instance or class to use if clean=True.
                Defaults to the base EntityProcessor if None.
            **kwargs: Additional arguments passed to the processor.

        Returns:
            EntityTable instance. Rows that fail validation are skipped with a warning.
        """
        if clean:
            if processor is None:
                processor = EntityProcessor()
            elif isinstance(processor, type):
                processor = processor()
            df = processor.process(df, **kwargs)
            logger.debug(
                "Cleaned data has %d rows after %s.",
                len(df),
                processor.__class__.__name__,
            )

        entities: List[E] = []
        failed_rows: List[dict] = []
        errors: List[str] = []

        tqdm_stream = config.get_tqdm_logger_stream(logger)
        # Convert NaN to None so Pydantic handles missing Optional fields correctly.
        # Otherwise, np.nan triggers numeric validations (like ge=0) and fails.
        records = cast(
            List[Dict[str, Any]], df.replace({np.nan: None}).to_dict(orient="records")
        )

        for row in tqdm(
            records,
            file=tqdm_stream,
            desc=f"Validating {entity_class.__name__} entities",
            total=len(df),
        ):
            try:
                entities.append(entity_class(**row))
            except Exception as e:
                failed_rows.append(row)
                errors.append(str(e))

        if errors:
            logger.warning(
                "%d of %d rows failed validation and were skipped. First 5 errors: %s",
                len(errors),
                len(df),
                errors[:5],
            )

        logger.debug("EntityTable created with %d valid entities.", len(entities))
        return cls(entities=entities, entity_class=entity_class)

    @classmethod
    def from_files(
        cls,
        file_paths: List[Union[str, Path]],
        entity_class: Type[E],
        data_store: Optional[DataStore] = None,
        clean: bool = True,
        processor: Optional[Union[Type[EntityProcessor], EntityProcessor]] = None,
        **kwargs,
    ) -> "EntityTable[E]":
        """
        Load and merge multiple source files into a single EntityTable.

        Each file is independently cleaned and validated before merging.
        Failed rows in individual files are skipped and logged but do not
        abort the remaining files.

        Args:
            file_paths: List of paths to source files.
            entity_class: The Pydantic entity class to validate each row against.
            data_store: DataStore instance for file access. Defaults to LocalDataStore.
            clean: Whether to apply an EntityProcessor before validation. Defaults to True.
            processor: Optional EntityProcessor subclass or instance applied to each file.
            **kwargs: Additional arguments forwarded to read_dataset and the processor.

        Returns:
            Merged EntityTable instance.

        Raises:
            ValueError: If file_paths is empty.
        """
        if not file_paths:
            raise ValueError("file_paths must not be empty.")

        tables = []
        for path in file_paths:
            try:
                table = cls.from_file(
                    file_path=path,
                    entity_class=entity_class,
                    data_store=data_store,
                    clean=clean,
                    processor=processor,
                    **kwargs,
                )
                tables.append(table)
                logger.debug("Loaded %d entities from %s.", len(table), path)
            except Exception as e:
                logger.warning("Failed to load file '%s': %s. Skipping.", path, e)

        if not tables:
            logger.warning("No files were successfully loaded.")
            return cls(entities=[], entity_class=entity_class)

        return cls.merge(tables)

    @classmethod
    def merge(
        cls,
        tables: List["EntityTable[E]"],
        deduplicate_by_id: bool = True,
    ) -> "EntityTable[E]":
        """
        Merge multiple EntityTable instances into one.

        Args:
            tables: List of EntityTable instances to merge.
            deduplicate_by_id: If True, deduplicate merged entities by their
                `id` property, keeping the first occurrence. Defaults to True.

        Returns:
            Merged EntityTable instance.

        Raises:
            ValueError: If tables list is empty.
        """
        if not tables:
            raise ValueError("Cannot merge an empty list of tables.")

        entity_class = next(
            (t.entity_class for t in tables if t.entity_class is not None), None
        )

        all_entities: List[E] = []
        for table in tables:
            all_entities.extend(table.entities)

        if deduplicate_by_id:
            seen_ids: set = set()
            unique_entities: List[E] = []
            for entity in all_entities:
                if entity.id not in seen_ids:
                    seen_ids.add(entity.id)
                    unique_entities.append(entity)

            duplicates_removed = len(all_entities) - len(unique_entities)
            if duplicates_removed:
                logger.info(
                    "Merge removed %d duplicate entities by id.", duplicates_removed
                )
            all_entities = unique_entities

        logger.info(
            "Merged %d tables into %d entities.", len(tables), len(all_entities)
        )
        return cls(entities=all_entities, entity_class=entity_class)

    def to_dataframe(self) -> pd.DataFrame:
        """
        Convert the entity table to a pandas DataFrame.

        Returns:
            A DataFrame where each row corresponds to an entity model dump.
        """
        return pd.DataFrame([e.model_dump() for e in self.entities])

    def to_geodataframe(self) -> gpd.GeoDataFrame:
        """
        Convert the entity table to a GeoDataFrame.

        Returns:
            GeoDataFrame with point or polygon geometry.

        Raises:
            ValueError: If entities have no associated geometry fields.
        """
        if not self.is_point_entity and not self.is_geo_entity:
            raise ValueError("Cannot create GeoDataFrame: entities have no geometry.")

        if self.is_point_entity:
            df = self.to_dataframe()
            return gpd.GeoDataFrame(
                df,
                geometry=gpd.points_from_xy(df["longitude"], df["latitude"]),
                crs="EPSG:4326",
            )
        else:
            # GigaGeoEntity — geometry column already present in model_dump()
            df = self.to_dataframe()
            return gpd.GeoDataFrame(df, geometry="geometry", crs="EPSG:4326")

    def to_geoms(self) -> List:
        """
        Return a list of Shapely geometry objects for all entities.

        - ``is_point_entity``: constructs ``Point(lon, lat)`` per entity,
        skipping rows where either coordinate is ``None``.
        - ``is_geo_entity`` (``GigaGeoEntity``): returns each entity's
        ``geometry`` field directly, skipping ``None`` values.
        - Neither: returns an empty list.

        Returns:
            List of Shapely ``BaseGeometry`` objects, never ``None``.
        """
        if self.is_point_entity:
            return [
                Point(e.longitude, e.latitude)
                for e in self.entities
                if e.longitude is not None and e.latitude is not None
            ]

        if self.is_geo_entity:
            return [e.geometry for e in self.entities if e.geometry is not None]

        logger.debug(
            "%s is neither a point nor a geo entity; to_geoms returns [].",
            self.__class__.__name__,
        )
        return []

    def to_coordinate_vector(self) -> np.ndarray:
        """
        Transform the entity table into a numpy array of (lat, lon) coordinates.

        Returns:
            Numpy array of shape (N, 2). Empty array if not a point entity.
        """
        if not self.is_point_entity or not self.entities:
            return np.zeros((0, 2))
        return np.array([[e.latitude, e.longitude] for e in self.entities])

    def get_lat_array(self) -> np.ndarray:
        """
        Get an array of latitude values.

        Returns:
            Numpy array of floats.
        """
        if not self.is_point_entity:
            return np.array([])
        return np.array([e.latitude for e in self.entities])

    def get_lon_array(self) -> np.ndarray:
        """
        Get an array of longitude values.

        Returns:
            Numpy array of floats.
        """
        if not self.is_point_entity:
            return np.array([])
        return np.array([e.longitude for e in self.entities])

    def filter_by_admin1(self, admin1_id: str) -> "EntityTable[E]":
        """
        Filter entities by primary administrative division ID.

        Args:
            admin1_id: Identifier for the division.

        Returns:
            New EntityTable with filtered entities.
        """
        return self.__class__(
            entities=[e for e in self.entities if e.admin1_id == admin1_id]
        )

    def filter_by_admin2(self, admin2_id: str) -> "EntityTable[E]":
        """
        Filter entities by secondary administrative division ID.

        Args:
            admin2_id: Identifier for the division.

        Returns:
            New EntityTable with filtered entities.
        """
        return self.__class__(
            entities=[e for e in self.entities if e.admin2_id == admin2_id]
        )

    def filter_by_polygon(self, polygon: Polygon) -> "EntityTable[E]":
        """
        Filter entities whose location falls within a polygon.

        Args:
            polygon: Shapely Polygon to filter by.

        Returns:
            New EntityTable with entities inside the polygon.
        """
        if not self.is_point_entity:
            return self.__class__(entities=[])
        return self.__class__(
            entities=[
                e
                for e in self.entities
                if polygon.contains(Point(e.longitude, e.latitude))
            ]
        )

    def filter_by_bounds(
        self, min_lat: float, max_lat: float, min_lon: float, max_lon: float
    ) -> "EntityTable[E]":
        """
        Filter entities whose coordinates fall within a bounding box.

        Args:
            min_lat: Minimum latitude.
            max_lat: Maximum latitude.
            min_lon: Minimum longitude.
            max_lon: Maximum longitude.

        Returns:
            New EntityTable with entities within bounds.
        """
        if not self.is_point_entity:
            return self.__class__(entities=[])
        return self.__class__(
            entities=[
                e
                for e in self.entities
                if min_lat <= e.latitude <= max_lat
                and min_lon <= e.longitude <= max_lon
            ]
        )

    def get_nearest_neighbors(
        self, lat: float, lon: float, k: int = 5
    ) -> "EntityTable[E]":
        """
        Find k nearest neighbors to a given set of coordinates.

        Uses an internal KDTree cache to accelerate repeated lookups.

        Args:
            lat: Query latitude.
            lon: Query longitude.
            k: Number of neighbors to return. Defaults to 5.

        Returns:
            New EntityTable containing the k nearest entities.
        """
        if not self.is_point_entity:
            return self.__class__(entities=[])

        if not self._cached_kdtree:
            self._build_kdtree()

        if not self._cached_kdtree:
            return self.__class__(entities=[])

        _, indices = self._cached_kdtree.query([[lat, lon]], k=k)
        return self.__class__(entities=[self.entities[i] for i in indices[0]])

    def to_distance_graph(
        self,
        distance_threshold: float,
        max_k: int = 100,
        return_dataframe: bool = False,
        verbose: bool = True,
    ) -> Union[nx.Graph, Tuple[nx.Graph, pd.DataFrame]]:
        """
        Build a spatial distance graph of entities within this table.

        Computes pairwise distances between all entities and returns a NetworkX
        graph where edges represent pairs within the given distance threshold.
        Self-matches are automatically excluded.

        Args:
            distance_threshold: Maximum distance in meters for two entities to be connected.
            max_k: Maximum number of neighbors to consider per entity. Default is 100.
            return_dataframe: If True, also return the matches DataFrame alongside the graph.
            verbose: If True, log graph construction statistics.

        Returns:
            NetworkX Graph, or tuple of (Graph, DataFrame) if return_dataframe=True.

        Raises:
            ValueError: If entities do not have location data.
        """
        if not self.is_point_entity:
            raise ValueError(
                "Cannot build distance graph: entities do not have location data."
            )
        return build_distance_graph(
            left_df=self.to_geodataframe(),
            right_df=self.to_geodataframe(),
            distance_threshold=distance_threshold,
            max_k=max_k,
            return_dataframe=return_dataframe,
            verbose=verbose,
            exclude_same_index=True,
        )

    def to_distance_graph_with(
        self,
        other: "EntityTable",
        distance_threshold: float,
        max_k: int = 100,
        return_dataframe: bool = False,
        verbose: bool = True,
    ) -> Union[nx.Graph, Tuple[nx.Graph, pd.DataFrame]]:
        """
        Build a spatial distance graph between this table and another EntityTable.

        Matches entities in this table (left) against entities in another table (right)
        and returns a bipartite-style graph where edges connect entities within the
        given distance threshold.

        Args:
            other: The right-hand EntityTable to match against.
            distance_threshold: Maximum distance in meters for two entities to be connected.
            max_k: Maximum number of neighbors to consider per entity. Default is 100.
            return_dataframe: If True, also return the matches DataFrame alongside the graph.
            verbose: If True, log graph construction statistics.

        Returns:
            NetworkX Graph, or tuple of (Graph, DataFrame) if return_dataframe=True.

        Raises:
            ValueError: If either table's entities do not have location data.
        """
        if not self.is_point_entity:
            raise ValueError(
                "Cannot build distance graph: this table's entities do not have location data."
            )
        if not other.is_point_entity:
            raise ValueError(
                "Cannot build distance graph: other table's entities do not have location data."
            )
        return build_distance_graph(
            left_df=self.to_geodataframe(),
            right_df=other.to_geodataframe(),
            distance_threshold=distance_threshold,
            max_k=max_k,
            return_dataframe=return_dataframe,
            verbose=verbose,
            exclude_same_index=False,
        )

    def _build_kdtree(self):
        """Build and cache the KDTree from entity point coordinates."""
        if not self.is_point_entity:
            self._cached_kdtree = None
            return
        coords = self.to_coordinate_vector()
        if coords.size > 0:
            logger.debug("Building KDTree for %d entities.", len(self.entities))
            self._cached_kdtree = cKDTree(coords)
        else:
            logger.warning("EntityTable is empty, skipping KDTree build.")

    @staticmethod
    def _enum_value(value: object) -> object:
        """Return a primitive enum value when value is an enum instance."""
        return getattr(value, "value", value)

    def clear_cache(self):
        """Clear the internal KDTree spatial index cache."""
        self._cached_kdtree = None

    def to_file(
        self,
        file_path: Union[str, Path],
        data_store: Optional[DataStore] = None,
        as_geodataframe: bool = False,
        **kwargs,
    ) -> None:
        """
        Save the entity table to a file.

        Args:
            file_path: Destination file path.
            data_store: DataStore to use for writing. Defaults to LocalDataStore.
            as_geodataframe: If True, uses GeoDataFrame for spatial formats (e.g. GeoJSON).
            **kwargs: Additional arguments for writers.write_dataset.

        Raises:
            ValueError: If the table is empty.
        """
        if not self.entities:
            raise ValueError("Cannot write to file: no entities available.")
        data_store = data_store or LocalDataStore()
        df = self.to_geodataframe() if as_geodataframe else self.to_dataframe()
        write_dataset(df, data_store, file_path, **kwargs)

    def __len__(self) -> int:
        return len(self.entities)

    def __iter__(self):
        return iter(self.entities)

    def __repr__(self) -> str:
        entity_type = (
            self.entities[0].__class__.__name__ if self.entities else "unknown"
        )
        return f"{self.__class__.__name__}(n={len(self.entities)}, type={entity_type})"

    def __getitem__(self, index: int) -> E:
        return self.entities[index]
is_geo_entity: bool property

Whether entities carry complex polygon/multipolygon geometry.

Returns:

Type Description
bool

True if entities are GigaGeoEntity.

is_point_entity: bool property

Whether entities carry lat/lon point location.

Returns:

Type Description
bool

True if entities are GigaEntity but not GigaGeoEntity.

clear_cache()

Clear the internal KDTree spatial index cache.

Source code in gigaspatial/core/schemas/entity.py
def clear_cache(self):
    """Clear the internal KDTree spatial index cache."""
    self._cached_kdtree = None
filter_by_admin1(admin1_id)

Filter entities by primary administrative division ID.

Parameters:

Name Type Description Default
admin1_id str

Identifier for the division.

required

Returns:

Type Description
'EntityTable[E]'

New EntityTable with filtered entities.

Source code in gigaspatial/core/schemas/entity.py
def filter_by_admin1(self, admin1_id: str) -> "EntityTable[E]":
    """
    Filter entities by primary administrative division ID.

    Args:
        admin1_id: Identifier for the division.

    Returns:
        New EntityTable with filtered entities.
    """
    return self.__class__(
        entities=[e for e in self.entities if e.admin1_id == admin1_id]
    )
filter_by_admin2(admin2_id)

Filter entities by secondary administrative division ID.

Parameters:

Name Type Description Default
admin2_id str

Identifier for the division.

required

Returns:

Type Description
'EntityTable[E]'

New EntityTable with filtered entities.

Source code in gigaspatial/core/schemas/entity.py
def filter_by_admin2(self, admin2_id: str) -> "EntityTable[E]":
    """
    Filter entities by secondary administrative division ID.

    Args:
        admin2_id: Identifier for the division.

    Returns:
        New EntityTable with filtered entities.
    """
    return self.__class__(
        entities=[e for e in self.entities if e.admin2_id == admin2_id]
    )
filter_by_bounds(min_lat, max_lat, min_lon, max_lon)

Filter entities whose coordinates fall within a bounding box.

Parameters:

Name Type Description Default
min_lat float

Minimum latitude.

required
max_lat float

Maximum latitude.

required
min_lon float

Minimum longitude.

required
max_lon float

Maximum longitude.

required

Returns:

Type Description
'EntityTable[E]'

New EntityTable with entities within bounds.

Source code in gigaspatial/core/schemas/entity.py
def filter_by_bounds(
    self, min_lat: float, max_lat: float, min_lon: float, max_lon: float
) -> "EntityTable[E]":
    """
    Filter entities whose coordinates fall within a bounding box.

    Args:
        min_lat: Minimum latitude.
        max_lat: Maximum latitude.
        min_lon: Minimum longitude.
        max_lon: Maximum longitude.

    Returns:
        New EntityTable with entities within bounds.
    """
    if not self.is_point_entity:
        return self.__class__(entities=[])
    return self.__class__(
        entities=[
            e
            for e in self.entities
            if min_lat <= e.latitude <= max_lat
            and min_lon <= e.longitude <= max_lon
        ]
    )
filter_by_polygon(polygon)

Filter entities whose location falls within a polygon.

Parameters:

Name Type Description Default
polygon Polygon

Shapely Polygon to filter by.

required

Returns:

Type Description
'EntityTable[E]'

New EntityTable with entities inside the polygon.

Source code in gigaspatial/core/schemas/entity.py
def filter_by_polygon(self, polygon: Polygon) -> "EntityTable[E]":
    """
    Filter entities whose location falls within a polygon.

    Args:
        polygon: Shapely Polygon to filter by.

    Returns:
        New EntityTable with entities inside the polygon.
    """
    if not self.is_point_entity:
        return self.__class__(entities=[])
    return self.__class__(
        entities=[
            e
            for e in self.entities
            if polygon.contains(Point(e.longitude, e.latitude))
        ]
    )
from_dataframe(df, entity_class, clean=False, processor=None, **kwargs) classmethod

Create an EntityTable from an existing DataFrame.

Parameters:

Name Type Description Default
df DataFrame

DataFrame containing entity data.

required
entity_class Type[E]

The Pydantic entity class to validate each row against.

required
clean bool

Whether to apply an EntityProcessor before validation.

False
processor Optional[Union[Type[EntityProcessor], EntityProcessor]]

Optional processor instance or class to use if clean=True. Defaults to the base EntityProcessor if None.

None
**kwargs

Additional arguments passed to the processor.

{}

Returns:

Type Description
'EntityTable[E]'

EntityTable instance. Rows that fail validation are skipped with a warning.

Source code in gigaspatial/core/schemas/entity.py
@classmethod
def from_dataframe(
    cls,
    df: pd.DataFrame,
    entity_class: Type[E],
    clean: bool = False,
    processor: Optional[Union[Type[EntityProcessor], EntityProcessor]] = None,
    **kwargs,
) -> "EntityTable[E]":
    """
    Create an EntityTable from an existing DataFrame.

    Args:
        df: DataFrame containing entity data.
        entity_class: The Pydantic entity class to validate each row against.
        clean: Whether to apply an EntityProcessor before validation.
        processor: Optional processor instance or class to use if clean=True.
            Defaults to the base EntityProcessor if None.
        **kwargs: Additional arguments passed to the processor.

    Returns:
        EntityTable instance. Rows that fail validation are skipped with a warning.
    """
    if clean:
        if processor is None:
            processor = EntityProcessor()
        elif isinstance(processor, type):
            processor = processor()
        df = processor.process(df, **kwargs)
        logger.debug(
            "Cleaned data has %d rows after %s.",
            len(df),
            processor.__class__.__name__,
        )

    entities: List[E] = []
    failed_rows: List[dict] = []
    errors: List[str] = []

    tqdm_stream = config.get_tqdm_logger_stream(logger)
    # Convert NaN to None so Pydantic handles missing Optional fields correctly.
    # Otherwise, np.nan triggers numeric validations (like ge=0) and fails.
    records = cast(
        List[Dict[str, Any]], df.replace({np.nan: None}).to_dict(orient="records")
    )

    for row in tqdm(
        records,
        file=tqdm_stream,
        desc=f"Validating {entity_class.__name__} entities",
        total=len(df),
    ):
        try:
            entities.append(entity_class(**row))
        except Exception as e:
            failed_rows.append(row)
            errors.append(str(e))

    if errors:
        logger.warning(
            "%d of %d rows failed validation and were skipped. First 5 errors: %s",
            len(errors),
            len(df),
            errors[:5],
        )

    logger.debug("EntityTable created with %d valid entities.", len(entities))
    return cls(entities=entities, entity_class=entity_class)
from_file(file_path, entity_class, data_store=None, clean=True, processor=None, **kwargs) classmethod

Create an EntityTable from a file, with optional pre-validation cleaning.

Parameters:

Name Type Description Default
file_path Union[str, Path]

Path to the dataset file.

required
entity_class Type[E]

The Pydantic entity class to validate each row against.

required
data_store Optional[DataStore]

DataStore instance for file access. Defaults to LocalDataStore.

None
clean bool

Whether to apply an EntityProcessor before validation. Defaults to True.

True
processor Optional[Union[Type[EntityProcessor], EntityProcessor]]

Optional EntityProcessor subclass or instance to apply before validation.

None
**kwargs

Additional arguments forwarded to read_dataset and the processor.

{}

Returns:

Type Description
'EntityTable[E]'

EntityTable instance containing successfully validated entities.

Raises:

Type Description
FileNotFoundError

If the file does not exist in the data store.

ValueError

If the file cannot be read or parsed.

Source code in gigaspatial/core/schemas/entity.py
@classmethod
def from_file(
    cls,
    file_path: Union[str, Path],
    entity_class: Type[E],
    data_store: Optional[DataStore] = None,
    clean: bool = True,
    processor: Optional[Union[Type[EntityProcessor], EntityProcessor]] = None,
    **kwargs,
) -> "EntityTable[E]":
    """
    Create an EntityTable from a file, with optional pre-validation cleaning.

    Args:
        file_path: Path to the dataset file.
        entity_class: The Pydantic entity class to validate each row against.
        data_store: DataStore instance for file access. Defaults to LocalDataStore.
        clean: Whether to apply an EntityProcessor before validation. Defaults to True.
        processor: Optional EntityProcessor subclass or instance to apply before validation.
        **kwargs: Additional arguments forwarded to read_dataset and the processor.

    Returns:
        EntityTable instance containing successfully validated entities.

    Raises:
        FileNotFoundError: If the file does not exist in the data store.
        ValueError: If the file cannot be read or parsed.
    """

    df = read_dataset(file_path, data_store=data_store, **kwargs)
    logger.debug("Loaded %d rows from %s", len(df), file_path)

    try:
        return cls.from_dataframe(
            df,
            entity_class,
            clean=clean,
            processor=processor,
            **kwargs,
        )
    except Exception as e:
        raise ValueError(f"Error reading or processing the file: {e}")
from_files(file_paths, entity_class, data_store=None, clean=True, processor=None, **kwargs) classmethod

Load and merge multiple source files into a single EntityTable.

Each file is independently cleaned and validated before merging. Failed rows in individual files are skipped and logged but do not abort the remaining files.

Parameters:

Name Type Description Default
file_paths List[Union[str, Path]]

List of paths to source files.

required
entity_class Type[E]

The Pydantic entity class to validate each row against.

required
data_store Optional[DataStore]

DataStore instance for file access. Defaults to LocalDataStore.

None
clean bool

Whether to apply an EntityProcessor before validation. Defaults to True.

True
processor Optional[Union[Type[EntityProcessor], EntityProcessor]]

Optional EntityProcessor subclass or instance applied to each file.

None
**kwargs

Additional arguments forwarded to read_dataset and the processor.

{}

Returns:

Type Description
'EntityTable[E]'

Merged EntityTable instance.

Raises:

Type Description
ValueError

If file_paths is empty.

Source code in gigaspatial/core/schemas/entity.py
@classmethod
def from_files(
    cls,
    file_paths: List[Union[str, Path]],
    entity_class: Type[E],
    data_store: Optional[DataStore] = None,
    clean: bool = True,
    processor: Optional[Union[Type[EntityProcessor], EntityProcessor]] = None,
    **kwargs,
) -> "EntityTable[E]":
    """
    Load and merge multiple source files into a single EntityTable.

    Each file is independently cleaned and validated before merging.
    Failed rows in individual files are skipped and logged but do not
    abort the remaining files.

    Args:
        file_paths: List of paths to source files.
        entity_class: The Pydantic entity class to validate each row against.
        data_store: DataStore instance for file access. Defaults to LocalDataStore.
        clean: Whether to apply an EntityProcessor before validation. Defaults to True.
        processor: Optional EntityProcessor subclass or instance applied to each file.
        **kwargs: Additional arguments forwarded to read_dataset and the processor.

    Returns:
        Merged EntityTable instance.

    Raises:
        ValueError: If file_paths is empty.
    """
    if not file_paths:
        raise ValueError("file_paths must not be empty.")

    tables = []
    for path in file_paths:
        try:
            table = cls.from_file(
                file_path=path,
                entity_class=entity_class,
                data_store=data_store,
                clean=clean,
                processor=processor,
                **kwargs,
            )
            tables.append(table)
            logger.debug("Loaded %d entities from %s.", len(table), path)
        except Exception as e:
            logger.warning("Failed to load file '%s': %s. Skipping.", path, e)

    if not tables:
        logger.warning("No files were successfully loaded.")
        return cls(entities=[], entity_class=entity_class)

    return cls.merge(tables)
get_lat_array()

Get an array of latitude values.

Returns:

Type Description
ndarray

Numpy array of floats.

Source code in gigaspatial/core/schemas/entity.py
def get_lat_array(self) -> np.ndarray:
    """
    Get an array of latitude values.

    Returns:
        Numpy array of floats.
    """
    if not self.is_point_entity:
        return np.array([])
    return np.array([e.latitude for e in self.entities])
get_lon_array()

Get an array of longitude values.

Returns:

Type Description
ndarray

Numpy array of floats.

Source code in gigaspatial/core/schemas/entity.py
def get_lon_array(self) -> np.ndarray:
    """
    Get an array of longitude values.

    Returns:
        Numpy array of floats.
    """
    if not self.is_point_entity:
        return np.array([])
    return np.array([e.longitude for e in self.entities])
get_nearest_neighbors(lat, lon, k=5)

Find k nearest neighbors to a given set of coordinates.

Uses an internal KDTree cache to accelerate repeated lookups.

Parameters:

Name Type Description Default
lat float

Query latitude.

required
lon float

Query longitude.

required
k int

Number of neighbors to return. Defaults to 5.

5

Returns:

Type Description
'EntityTable[E]'

New EntityTable containing the k nearest entities.

Source code in gigaspatial/core/schemas/entity.py
def get_nearest_neighbors(
    self, lat: float, lon: float, k: int = 5
) -> "EntityTable[E]":
    """
    Find k nearest neighbors to a given set of coordinates.

    Uses an internal KDTree cache to accelerate repeated lookups.

    Args:
        lat: Query latitude.
        lon: Query longitude.
        k: Number of neighbors to return. Defaults to 5.

    Returns:
        New EntityTable containing the k nearest entities.
    """
    if not self.is_point_entity:
        return self.__class__(entities=[])

    if not self._cached_kdtree:
        self._build_kdtree()

    if not self._cached_kdtree:
        return self.__class__(entities=[])

    _, indices = self._cached_kdtree.query([[lat, lon]], k=k)
    return self.__class__(entities=[self.entities[i] for i in indices[0]])
merge(tables, deduplicate_by_id=True) classmethod

Merge multiple EntityTable instances into one.

Parameters:

Name Type Description Default
tables List['EntityTable[E]']

List of EntityTable instances to merge.

required
deduplicate_by_id bool

If True, deduplicate merged entities by their id property, keeping the first occurrence. Defaults to True.

True

Returns:

Type Description
'EntityTable[E]'

Merged EntityTable instance.

Raises:

Type Description
ValueError

If tables list is empty.

Source code in gigaspatial/core/schemas/entity.py
@classmethod
def merge(
    cls,
    tables: List["EntityTable[E]"],
    deduplicate_by_id: bool = True,
) -> "EntityTable[E]":
    """
    Merge multiple EntityTable instances into one.

    Args:
        tables: List of EntityTable instances to merge.
        deduplicate_by_id: If True, deduplicate merged entities by their
            `id` property, keeping the first occurrence. Defaults to True.

    Returns:
        Merged EntityTable instance.

    Raises:
        ValueError: If tables list is empty.
    """
    if not tables:
        raise ValueError("Cannot merge an empty list of tables.")

    entity_class = next(
        (t.entity_class for t in tables if t.entity_class is not None), None
    )

    all_entities: List[E] = []
    for table in tables:
        all_entities.extend(table.entities)

    if deduplicate_by_id:
        seen_ids: set = set()
        unique_entities: List[E] = []
        for entity in all_entities:
            if entity.id not in seen_ids:
                seen_ids.add(entity.id)
                unique_entities.append(entity)

        duplicates_removed = len(all_entities) - len(unique_entities)
        if duplicates_removed:
            logger.info(
                "Merge removed %d duplicate entities by id.", duplicates_removed
            )
        all_entities = unique_entities

    logger.info(
        "Merged %d tables into %d entities.", len(tables), len(all_entities)
    )
    return cls(entities=all_entities, entity_class=entity_class)
to_coordinate_vector()

Transform the entity table into a numpy array of (lat, lon) coordinates.

Returns:

Type Description
ndarray

Numpy array of shape (N, 2). Empty array if not a point entity.

Source code in gigaspatial/core/schemas/entity.py
def to_coordinate_vector(self) -> np.ndarray:
    """
    Transform the entity table into a numpy array of (lat, lon) coordinates.

    Returns:
        Numpy array of shape (N, 2). Empty array if not a point entity.
    """
    if not self.is_point_entity or not self.entities:
        return np.zeros((0, 2))
    return np.array([[e.latitude, e.longitude] for e in self.entities])
to_dataframe()

Convert the entity table to a pandas DataFrame.

Returns:

Type Description
DataFrame

A DataFrame where each row corresponds to an entity model dump.

Source code in gigaspatial/core/schemas/entity.py
def to_dataframe(self) -> pd.DataFrame:
    """
    Convert the entity table to a pandas DataFrame.

    Returns:
        A DataFrame where each row corresponds to an entity model dump.
    """
    return pd.DataFrame([e.model_dump() for e in self.entities])
to_distance_graph(distance_threshold, max_k=100, return_dataframe=False, verbose=True)

Build a spatial distance graph of entities within this table.

Computes pairwise distances between all entities and returns a NetworkX graph where edges represent pairs within the given distance threshold. Self-matches are automatically excluded.

Parameters:

Name Type Description Default
distance_threshold float

Maximum distance in meters for two entities to be connected.

required
max_k int

Maximum number of neighbors to consider per entity. Default is 100.

100
return_dataframe bool

If True, also return the matches DataFrame alongside the graph.

False
verbose bool

If True, log graph construction statistics.

True

Returns:

Type Description
Union[Graph, Tuple[Graph, DataFrame]]

NetworkX Graph, or tuple of (Graph, DataFrame) if return_dataframe=True.

Raises:

Type Description
ValueError

If entities do not have location data.

Source code in gigaspatial/core/schemas/entity.py
def to_distance_graph(
    self,
    distance_threshold: float,
    max_k: int = 100,
    return_dataframe: bool = False,
    verbose: bool = True,
) -> Union[nx.Graph, Tuple[nx.Graph, pd.DataFrame]]:
    """
    Build a spatial distance graph of entities within this table.

    Computes pairwise distances between all entities and returns a NetworkX
    graph where edges represent pairs within the given distance threshold.
    Self-matches are automatically excluded.

    Args:
        distance_threshold: Maximum distance in meters for two entities to be connected.
        max_k: Maximum number of neighbors to consider per entity. Default is 100.
        return_dataframe: If True, also return the matches DataFrame alongside the graph.
        verbose: If True, log graph construction statistics.

    Returns:
        NetworkX Graph, or tuple of (Graph, DataFrame) if return_dataframe=True.

    Raises:
        ValueError: If entities do not have location data.
    """
    if not self.is_point_entity:
        raise ValueError(
            "Cannot build distance graph: entities do not have location data."
        )
    return build_distance_graph(
        left_df=self.to_geodataframe(),
        right_df=self.to_geodataframe(),
        distance_threshold=distance_threshold,
        max_k=max_k,
        return_dataframe=return_dataframe,
        verbose=verbose,
        exclude_same_index=True,
    )
to_distance_graph_with(other, distance_threshold, max_k=100, return_dataframe=False, verbose=True)

Build a spatial distance graph between this table and another EntityTable.

Matches entities in this table (left) against entities in another table (right) and returns a bipartite-style graph where edges connect entities within the given distance threshold.

Parameters:

Name Type Description Default
other 'EntityTable'

The right-hand EntityTable to match against.

required
distance_threshold float

Maximum distance in meters for two entities to be connected.

required
max_k int

Maximum number of neighbors to consider per entity. Default is 100.

100
return_dataframe bool

If True, also return the matches DataFrame alongside the graph.

False
verbose bool

If True, log graph construction statistics.

True

Returns:

Type Description
Union[Graph, Tuple[Graph, DataFrame]]

NetworkX Graph, or tuple of (Graph, DataFrame) if return_dataframe=True.

Raises:

Type Description
ValueError

If either table's entities do not have location data.

Source code in gigaspatial/core/schemas/entity.py
def to_distance_graph_with(
    self,
    other: "EntityTable",
    distance_threshold: float,
    max_k: int = 100,
    return_dataframe: bool = False,
    verbose: bool = True,
) -> Union[nx.Graph, Tuple[nx.Graph, pd.DataFrame]]:
    """
    Build a spatial distance graph between this table and another EntityTable.

    Matches entities in this table (left) against entities in another table (right)
    and returns a bipartite-style graph where edges connect entities within the
    given distance threshold.

    Args:
        other: The right-hand EntityTable to match against.
        distance_threshold: Maximum distance in meters for two entities to be connected.
        max_k: Maximum number of neighbors to consider per entity. Default is 100.
        return_dataframe: If True, also return the matches DataFrame alongside the graph.
        verbose: If True, log graph construction statistics.

    Returns:
        NetworkX Graph, or tuple of (Graph, DataFrame) if return_dataframe=True.

    Raises:
        ValueError: If either table's entities do not have location data.
    """
    if not self.is_point_entity:
        raise ValueError(
            "Cannot build distance graph: this table's entities do not have location data."
        )
    if not other.is_point_entity:
        raise ValueError(
            "Cannot build distance graph: other table's entities do not have location data."
        )
    return build_distance_graph(
        left_df=self.to_geodataframe(),
        right_df=other.to_geodataframe(),
        distance_threshold=distance_threshold,
        max_k=max_k,
        return_dataframe=return_dataframe,
        verbose=verbose,
        exclude_same_index=False,
    )
to_file(file_path, data_store=None, as_geodataframe=False, **kwargs)

Save the entity table to a file.

Parameters:

Name Type Description Default
file_path Union[str, Path]

Destination file path.

required
data_store Optional[DataStore]

DataStore to use for writing. Defaults to LocalDataStore.

None
as_geodataframe bool

If True, uses GeoDataFrame for spatial formats (e.g. GeoJSON).

False
**kwargs

Additional arguments for writers.write_dataset.

{}

Raises:

Type Description
ValueError

If the table is empty.

Source code in gigaspatial/core/schemas/entity.py
def to_file(
    self,
    file_path: Union[str, Path],
    data_store: Optional[DataStore] = None,
    as_geodataframe: bool = False,
    **kwargs,
) -> None:
    """
    Save the entity table to a file.

    Args:
        file_path: Destination file path.
        data_store: DataStore to use for writing. Defaults to LocalDataStore.
        as_geodataframe: If True, uses GeoDataFrame for spatial formats (e.g. GeoJSON).
        **kwargs: Additional arguments for writers.write_dataset.

    Raises:
        ValueError: If the table is empty.
    """
    if not self.entities:
        raise ValueError("Cannot write to file: no entities available.")
    data_store = data_store or LocalDataStore()
    df = self.to_geodataframe() if as_geodataframe else self.to_dataframe()
    write_dataset(df, data_store, file_path, **kwargs)
to_geodataframe()

Convert the entity table to a GeoDataFrame.

Returns:

Type Description
GeoDataFrame

GeoDataFrame with point or polygon geometry.

Raises:

Type Description
ValueError

If entities have no associated geometry fields.

Source code in gigaspatial/core/schemas/entity.py
def to_geodataframe(self) -> gpd.GeoDataFrame:
    """
    Convert the entity table to a GeoDataFrame.

    Returns:
        GeoDataFrame with point or polygon geometry.

    Raises:
        ValueError: If entities have no associated geometry fields.
    """
    if not self.is_point_entity and not self.is_geo_entity:
        raise ValueError("Cannot create GeoDataFrame: entities have no geometry.")

    if self.is_point_entity:
        df = self.to_dataframe()
        return gpd.GeoDataFrame(
            df,
            geometry=gpd.points_from_xy(df["longitude"], df["latitude"]),
            crs="EPSG:4326",
        )
    else:
        # GigaGeoEntity — geometry column already present in model_dump()
        df = self.to_dataframe()
        return gpd.GeoDataFrame(df, geometry="geometry", crs="EPSG:4326")
to_geoms()

Return a list of Shapely geometry objects for all entities.

  • is_point_entity: constructs Point(lon, lat) per entity, skipping rows where either coordinate is None.
  • is_geo_entity (GigaGeoEntity): returns each entity's geometry field directly, skipping None values.
  • Neither: returns an empty list.

Returns:

Type Description
List

List of Shapely BaseGeometry objects, never None.

Source code in gigaspatial/core/schemas/entity.py
def to_geoms(self) -> List:
    """
    Return a list of Shapely geometry objects for all entities.

    - ``is_point_entity``: constructs ``Point(lon, lat)`` per entity,
    skipping rows where either coordinate is ``None``.
    - ``is_geo_entity`` (``GigaGeoEntity``): returns each entity's
    ``geometry`` field directly, skipping ``None`` values.
    - Neither: returns an empty list.

    Returns:
        List of Shapely ``BaseGeometry`` objects, never ``None``.
    """
    if self.is_point_entity:
        return [
            Point(e.longitude, e.latitude)
            for e in self.entities
            if e.longitude is not None and e.latitude is not None
        ]

    if self.is_geo_entity:
        return [e.geometry for e in self.entities if e.geometry is not None]

    logger.debug(
        "%s is neither a point nor a geo entity; to_geoms returns [].",
        self.__class__.__name__,
    )
    return []
GigaEntity

Bases: BaseGigaEntity

Entity with location data.

Source code in gigaspatial/core/schemas/entity.py
class GigaEntity(BaseGigaEntity):
    """Entity with location data."""

    latitude: float = Field(
        ..., ge=-90, le=90, description="Latitude coordinate of the entity"
    )
    longitude: float = Field(
        ..., ge=-180, le=180, description="Longitude coordinate of the entity"
    )
    admin1: Optional[str] = Field(
        None, max_length=100, description="Primary administrative division name"
    )
    admin1_id: Optional[str] = Field(
        None,
        max_length=50,
        description="Unique identifier for the primary administrative division",
    )
    admin2: Optional[str] = Field(
        None, max_length=100, description="Secondary administrative division name"
    )
    admin2_id: Optional[str] = Field(
        None,
        max_length=50,
        description="Unique identifier for the secondary administrative division",
    )

    @field_validator("admin1", "admin2", mode="before")
    @classmethod
    def normalize_admin(cls, v: Optional[str]) -> Optional[str]:
        """
        Normalize administrative division names.

        Converts "unknown", "n/a" variations to None, and title-cases valid names.
        """
        if v is None or str(v).strip().lower() in ("unknown", "n/a", "none", ""):
            return None
        return str(v).strip().title()

    @model_validator(mode="after")
    def check_null_island(self) -> "GigaEntity":
        """Validate that coordinates are not (0.0, 0.0)."""
        if self.latitude == 0.0 and self.longitude == 0.0:
            raise ValueError("Null island coordinates (0.0, 0.0) are not valid.")
        return self
check_null_island()

Validate that coordinates are not (0.0, 0.0).

Source code in gigaspatial/core/schemas/entity.py
@model_validator(mode="after")
def check_null_island(self) -> "GigaEntity":
    """Validate that coordinates are not (0.0, 0.0)."""
    if self.latitude == 0.0 and self.longitude == 0.0:
        raise ValueError("Null island coordinates (0.0, 0.0) are not valid.")
    return self
normalize_admin(v) classmethod

Normalize administrative division names.

Converts "unknown", "n/a" variations to None, and title-cases valid names.

Source code in gigaspatial/core/schemas/entity.py
@field_validator("admin1", "admin2", mode="before")
@classmethod
def normalize_admin(cls, v: Optional[str]) -> Optional[str]:
    """
    Normalize administrative division names.

    Converts "unknown", "n/a" variations to None, and title-cases valid names.
    """
    if v is None or str(v).strip().lower() in ("unknown", "n/a", "none", ""):
        return None
    return str(v).strip().title()
GigaEntityNoLocation

Bases: BaseGigaEntity

Entity without location data.

Source code in gigaspatial/core/schemas/entity.py
class GigaEntityNoLocation(BaseGigaEntity):
    """Entity without location data."""

    pass
GigaGeoEntity

Bases: BaseGigaEntity

Entity with polygon or multipolygon geometry.

Used for spatially bounded records such as coverage areas, administrative boundaries, and building footprints where a point representation is insufficient.

Source code in gigaspatial/core/schemas/entity.py
class GigaGeoEntity(BaseGigaEntity):
    """
    Entity with polygon or multipolygon geometry.

    Used for spatially bounded records such as coverage areas,
    administrative boundaries, and building footprints where a
    point representation is insufficient.
    """

    model_config = GEO_ENTITY_CONFIG

    geometry: GeometryType = Field(
        ..., description="Polygon or MultiPolygon geometry of the entity"
    )

    @field_validator("geometry", mode="before")
    @classmethod
    def parse_geometry(cls, v: Any) -> GeometryType:
        """
        Accept Shapely geometries, WKT strings, or WKB bytes.

        Args:
            v: Raw geometry data.

        Returns:
            Parsed Polygon or MultiPolygon.

        Raises:
            ValueError: If parsing fails or geometry type is invalid.
        """
        if isinstance(v, (Polygon, MultiPolygon)):
            return v

        try:
            if isinstance(v, str):
                parsed = wkt.loads(v)
            elif isinstance(v, (bytes, bytearray)):
                parsed = wkb.loads(v)
            else:
                raise ValueError(
                    f"Cannot parse geometry from type {type(v).__name__}. "
                    "Expected Shapely geometry, WKT string, or WKB bytes."
                )
        except Exception as e:
            raise ValueError(f"Failed to parse geometry: {e}") from e

        if not isinstance(parsed, (Polygon, MultiPolygon)):
            raise ValueError(
                f"Expected Polygon or MultiPolygon geometry, got {type(parsed).__name__}. "
                "If you have Point or LineString geometries, use GigaEntity instead."
            )
        return parsed

    @property
    def centroid(self) -> tuple[float, float]:
        """
        Return (latitude, longitude) of the geometry centroid.

        Returns:
            Tuple of (lat, lon).
        """
        c = self.geometry.centroid
        return c.y, c.x

    @property
    def area_sq_km(self) -> float:
        """
        Approximate area in square kilometres.

        Calculated using the centroid UTM projection.

        Returns:
            Area in square km.
        """
        from gigaspatial.processing.geo import estimate_utm_crs_with_fallback
        import geopandas as gpd

        gdf = gpd.GeoDataFrame(geometry=[self.geometry], crs="EPSG:4326")
        utm_crs = estimate_utm_crs_with_fallback(gdf)
        return gdf.to_crs(utm_crs).geometry.area.iloc[0] / 1e6
area_sq_km: float property

Approximate area in square kilometres.

Calculated using the centroid UTM projection.

Returns:

Type Description
float

Area in square km.

centroid: tuple[float, float] property

Return (latitude, longitude) of the geometry centroid.

Returns:

Type Description
tuple[float, float]

Tuple of (lat, lon).

parse_geometry(v) classmethod

Accept Shapely geometries, WKT strings, or WKB bytes.

Parameters:

Name Type Description Default
v Any

Raw geometry data.

required

Returns:

Type Description
GeometryType

Parsed Polygon or MultiPolygon.

Raises:

Type Description
ValueError

If parsing fails or geometry type is invalid.

Source code in gigaspatial/core/schemas/entity.py
@field_validator("geometry", mode="before")
@classmethod
def parse_geometry(cls, v: Any) -> GeometryType:
    """
    Accept Shapely geometries, WKT strings, or WKB bytes.

    Args:
        v: Raw geometry data.

    Returns:
        Parsed Polygon or MultiPolygon.

    Raises:
        ValueError: If parsing fails or geometry type is invalid.
    """
    if isinstance(v, (Polygon, MultiPolygon)):
        return v

    try:
        if isinstance(v, str):
            parsed = wkt.loads(v)
        elif isinstance(v, (bytes, bytearray)):
            parsed = wkb.loads(v)
        else:
            raise ValueError(
                f"Cannot parse geometry from type {type(v).__name__}. "
                "Expected Shapely geometry, WKT string, or WKB bytes."
            )
    except Exception as e:
        raise ValueError(f"Failed to parse geometry: {e}") from e

    if not isinstance(parsed, (Polygon, MultiPolygon)):
        raise ValueError(
            f"Expected Polygon or MultiPolygon geometry, got {type(parsed).__name__}. "
            "If you have Point or LineString geometries, use GigaEntity instead."
        )
    return parsed

mobile_coverage

Module for mobile coverage schema and processing. Defines the MobileCoverage entity, representing cellular coverage areas (measured or modeled) with polygon geometries.

CoverageType

Bases: str, Enum

Whether coverage data is empirically measured, propagation-modeled, or operator-declared.

Source code in gigaspatial/core/schemas/mobile_coverage.py
class CoverageType(str, Enum):
    """Whether coverage data is empirically measured, propagation-modeled, or operator-declared."""

    MEASURED = "measured"
    MODELED = "modeled"
    PREDICTED = "predicted"
MobileCoverage

Bases: GigaGeoEntity

Represents a cellular coverage area as a polygon or multipolygon geometry.

Source code in gigaspatial/core/schemas/mobile_coverage.py
class MobileCoverage(GigaGeoEntity):
    """Represents a cellular coverage area as a polygon or multipolygon geometry."""

    model_config = GEO_ENUM_ENTITY_CONFIG

    # Identity
    mobile_coverage_id: str = Field(
        ..., max_length=50, description="Unique identifier for the mobile coverage area"
    )

    # Classification
    radio_type: RadioType = Field(
        ..., description="Mobile network generation technology"
    )
    coverage_type: Optional[CoverageType] = Field(
        None, description="Whether coverage is measured, modeled, or operator-predicted"
    )
    signal_strength: Optional[SignalStrength] = Field(
        None, description="Categorical signal strength classification"
    )
    operator_name: Optional[str] = Field(
        None, max_length=100, description="Name of the mobile network operator"
    )

    # RF metrics
    average_rssi_dbm: Optional[float] = Field(
        None, ge=-120, le=0, description="Average RSSI in dBm"
    )
    average_rsrp_dbm: Optional[float] = Field(
        None, ge=-140, le=-44, description="Average RSRP in dBm"
    )
    average_sinr_db: Optional[float] = Field(
        None, ge=-20, le=30, description="Average SINR in dB"
    )

    # Provenance
    reported_area_km2: Optional[float] = Field(
        None,
        ge=0,
        description="Coverage area as reported by the source in km². "
        "May differ from area computed from the actual geometry.",
    )
    related_wireless_sites: Optional[List[str]] = Field(
        None, description="Wireless site IDs contributing to this coverage area"
    )
    measurement_date: Optional[str] = Field(
        None, description="ISO 8601 date when coverage data was collected or modeled"
    )
    data_confidence: Optional[DataConfidence] = Field(
        None, description="Level of confidence in the data source"
    )

    @property
    def id(self) -> str:
        """Map `id` to `mobile_coverage_id`."""
        return self.mobile_coverage_id
id: str property

Map id to mobile_coverage_id.

MobileCoverageProcessor

Bases: EntityProcessor

Cleaning and normalization for MobileCoverage entities.

Source code in gigaspatial/core/schemas/mobile_coverage.py
class MobileCoverageProcessor(EntityProcessor):
    """Cleaning and normalization for MobileCoverage entities."""

    # MobileCoverage has no lat/lon (GigaGeoEntity), so base numeric
    # columns are excluded — only RF metrics and reported area apply
    NUMERIC_COLUMNS: ClassVar[List[str]] = [
        "average_rssi_dbm",
        "average_rsrp_dbm",
        "average_sinr_db",
        "reported_area_km2",
    ]

    LOWERCASE_COLUMNS: ClassVar[List[str]] = [
        "radio_type",
        "signal_strength",
        "coverage_type",
        "data_confidence",
    ]

    RADIO_ALIAS_MAP: ClassVar[Dict[str, str]] = RADIO_ALIAS_MAP

    SIGNAL_STRENGTH_ALIAS_MAP: ClassVar[Dict[str, str]] = SIGNAL_STRENGTH_ALIAS_MAP

    COVERAGE_TYPE_ALIAS_MAP: ClassVar[Dict[str, str]] = COVERAGE_TYPE_ALIAS_MAP

    def process(self, df: pd.DataFrame, **kwargs) -> pd.DataFrame:
        """
        Execute the full processing pipeline for mobile coverage.

        Args:
            df: Raw mobile coverage DataFrame.
            **kwargs: Additional processing arguments.

        Returns:
            Processed and normalized DataFrame.
        """
        df = super().process(df, **kwargs)
        df = self._normalize_radio_type(df)
        df = self._normalize_signal_strength(df)
        df = self._normalize_coverage_type(df)
        df = self._parse_geometry(df)
        return df

    def _normalize_radio_type(self, df: pd.DataFrame) -> pd.DataFrame:
        """
        Normalize radio_type values to canonical RadioType enums.

        Args:
            df: DataFrame to normalize.

        Returns:
            DataFrame with normalized radio_type column.
        """
        return self._normalize_enum_column(
            df,
            "radio_type",
            self.RADIO_ALIAS_MAP,
            {rt.value for rt in RadioType},
            required=True,
        )

    def _normalize_signal_strength(self, df: pd.DataFrame) -> pd.DataFrame:
        """
        Normalize signal_strength values to canonical SignalStrength enums.

        Args:
            df: DataFrame to normalize.

        Returns:
            DataFrame with normalized signal_strength column.
        """
        return self._normalize_enum_column(
            df,
            "signal_strength",
            self.SIGNAL_STRENGTH_ALIAS_MAP,
            {s.value for s in SignalStrength},
        )

    def _normalize_coverage_type(self, df: pd.DataFrame) -> pd.DataFrame:
        """
        Normalize coverage_type values to canonical CoverageType enums.

        Args:
            df: DataFrame to normalize.

        Returns:
            DataFrame with normalized coverage_type column.
        """
        return self._normalize_enum_column(
            df,
            "coverage_type",
            self.COVERAGE_TYPE_ALIAS_MAP,
            {ct.value for ct in CoverageType},
        )
process(df, **kwargs)

Execute the full processing pipeline for mobile coverage.

Parameters:

Name Type Description Default
df DataFrame

Raw mobile coverage DataFrame.

required
**kwargs

Additional processing arguments.

{}

Returns:

Type Description
DataFrame

Processed and normalized DataFrame.

Source code in gigaspatial/core/schemas/mobile_coverage.py
def process(self, df: pd.DataFrame, **kwargs) -> pd.DataFrame:
    """
    Execute the full processing pipeline for mobile coverage.

    Args:
        df: Raw mobile coverage DataFrame.
        **kwargs: Additional processing arguments.

    Returns:
        Processed and normalized DataFrame.
    """
    df = super().process(df, **kwargs)
    df = self._normalize_radio_type(df)
    df = self._normalize_signal_strength(df)
    df = self._normalize_coverage_type(df)
    df = self._parse_geometry(df)
    return df
MobileCoverageTable

Bases: EntityTable[MobileCoverage]

Container for MobileCoverage entities with coverage-specific spatial operations.

Source code in gigaspatial/core/schemas/mobile_coverage.py
class MobileCoverageTable(EntityTable[MobileCoverage]):
    """Container for MobileCoverage entities with coverage-specific spatial operations."""

    @classmethod
    def from_file(
        cls,
        file_path: Union[str, Path],
        data_store: Optional[DataStore] = None,
        clean: bool = True,
        processor: Optional[
            Union[Type[EntityProcessor], EntityProcessor]
        ] = MobileCoverageProcessor,
        **kwargs,
    ) -> "MobileCoverageTable":
        """
        Create a MobileCoverageTable from a file.

        Args:
            file_path: Path to the dataset file.
            data_store: DataStore instance for file access. Defaults to LocalDataStore.
            clean: Whether to apply the processor before validation. Defaults to True.
            processor: Optional EntityProcessor subclass or instance. Defaults to MobileCoverageProcessor.
            **kwargs: Additional arguments forwarded to read_dataset and the processor.

        Returns:
            MobileCoverageTable with validated MobileCoverage entities.

        Raises:
            FileNotFoundError: If the file does not exist.
            ValueError: If the file cannot be read or parsed.
        """
        return super().from_file(
            file_path=file_path,
            entity_class=MobileCoverage,
            data_store=data_store,
            clean=clean,
            processor=processor,
            **kwargs,
        )

    @classmethod
    def from_dataframe(
        cls,
        df: pd.DataFrame,
        entity_class: Type[MobileCoverage] = MobileCoverage,
        clean: bool = False,
        processor: Optional[
            Union[Type[EntityProcessor], EntityProcessor]
        ] = MobileCoverageProcessor,
        **kwargs,
    ) -> "MobileCoverageTable":
        """
        Create a MobileCoverageTable from an existing DataFrame.

        Args:
            df: DataFrame containing mobile coverage data.
            entity_class: Entity class to validate against. Defaults to MobileCoverage.
            clean: Whether to apply MobileCoverageProcessor before validation.
                Defaults to False since DataFrames passed directly are assumed pre-cleaned.
            processor: Optional EntityProcessor subclass or instance. Defaults to MobileCoverageProcessor.
            **kwargs: Additional arguments passed to the processor.

        Returns:
            MobileCoverageTable with validated MobileCoverage entities.
        """
        return super().from_dataframe(
            df=df,
            entity_class=entity_class,
            clean=clean,
            processor=processor,
            **kwargs,
        )

    # ------------------------------------------------------------------
    # Filters
    # ------------------------------------------------------------------

    def filter_by_radio_type(self, radio_type: RadioType) -> "MobileCoverageTable":
        """Filter coverage areas by radio technology."""
        return self.__class__(
            entities=[e for e in self.entities if e.radio_type == radio_type.value]
        )

    def filter_by_radio_types(
        self, radio_types: Set[RadioType]
    ) -> "MobileCoverageTable":
        """Filter coverage areas matching any of the given radio technologies."""
        values = {rt.value for rt in radio_types}
        return self.__class__(
            entities=[e for e in self.entities if e.radio_type in values]
        )

    def filter_by_operator(self, operator_name: str) -> "MobileCoverageTable":
        """Filter coverage areas by mobile network operator."""
        return self.__class__(
            entities=[e for e in self.entities if e.operator_name == operator_name]
        )

    def filter_by_signal_strength(
        self, signal_strength: SignalStrength
    ) -> "MobileCoverageTable":
        """Filter coverage areas by signal strength category."""
        return self.__class__(
            entities=[
                e for e in self.entities if e.signal_strength == signal_strength.value
            ]
        )

    def filter_by_coverage_type(
        self, coverage_type: CoverageType
    ) -> "MobileCoverageTable":
        """Filter coverage areas by measurement methodology."""
        return self.__class__(
            entities=[
                e for e in self.entities if e.coverage_type == coverage_type.value
            ]
        )

    def filter_by_measurement_date(
        self,
        since: Optional[str] = None,
        until: Optional[str] = None,
    ) -> "MobileCoverageTable":
        """
        Filter coverage areas by measurement date range.

        Args:
            since: ISO 8601 date string. Include records on or after this date.
            until: ISO 8601 date string. Include records on or before this date.
        """
        entities = self.entities
        if since:
            entities = [
                e
                for e in entities
                if e.measurement_date is not None and e.measurement_date >= since
            ]
        if until:
            entities = [
                e
                for e in entities
                if e.measurement_date is not None and e.measurement_date <= until
            ]
        return self.__class__(entities=entities)

    # ------------------------------------------------------------------
    # Spatial operations
    # ------------------------------------------------------------------

    def covers_point(self, lat: float, lon: float) -> "MobileCoverageTable":
        """
        Return all coverage areas whose geometry contains the given point.

        Args:
            lat: Latitude of the point.
            lon: Longitude of the point.

        Returns:
            MobileCoverageTable of coverage polygons that contain the point.
        """
        point = Point(lon, lat)
        return self.__class__(
            entities=[e for e in self.entities if e.geometry.contains(point)]
        )

    def covers_point_by_radio_type(
        self, lat: float, lon: float
    ) -> dict[str, "MobileCoverageTable"]:
        """
        For a given point, return coverage areas grouped by radio type.

        Useful for determining which technologies cover a specific location.

        Args:
            lat: Latitude of the point.
            lon: Longitude of the point.

        Returns:
            Dict mapping radio_type value → MobileCoverageTable.
        """
        covering = self.covers_point(lat, lon)
        groups: dict[str, list[MobileCoverage]] = {}
        for entity in covering.entities:
            groups.setdefault(entity.radio_type, []).append(entity)
        return {
            radio_type: self.__class__(entities=entities)
            for radio_type, entities in groups.items()
        }

    def dissolve_by_operator(self) -> gpd.GeoDataFrame:
        """
        Dissolve coverage geometries by operator and radio type.

        Returns a GeoDataFrame with merged polygons per operator/radio_type pair,
        useful for computing total coverage area without double-counting overlaps.

        Returns:
            GeoDataFrame with columns: operator_name, radio_type, geometry.
        """
        gdf = self.to_geodataframe()
        return gdf.dissolve(by=["operator_name", "radio_type"]).reset_index()[
            ["operator_name", "radio_type", "geometry"]
        ]

    def total_coverage_area_km2(self, dissolve: bool = True) -> float:
        """
        Compute total geographic area covered in square kilometres.

        Args:
            dissolve: If True, dissolve overlapping polygons before computing area
                to avoid double-counting. Defaults to True.

        Returns:
            Total coverage area in km².
        """
        gdf = self.to_geodataframe()

        from gigaspatial.processing.geo import estimate_utm_crs_with_fallback

        utm_crs = estimate_utm_crs_with_fallback(gdf)
        gdf = gdf.to_crs(utm_crs)

        if dissolve:
            gdf = gdf.dissolve().reset_index()

        return gdf.geometry.area.sum() / 1e6

    # ------------------------------------------------------------------
    # Aggregations
    # ------------------------------------------------------------------

    def get_operators(self) -> Set[str]:
        """
        Return the set of all unique operator names present in the table.

        Returns:
            Set of operator name strings.
        """
        return {e.operator_name for e in self.entities if e.operator_name is not None}

    def get_radio_types(self) -> Set[str]:
        """
        Return the set of all unique radio types present in the table.

        Returns:
            Set of radio type strings.
        """
        return {e.radio_type for e in self.entities if e.radio_type is not None}

    def get_coverage_summary(self) -> pd.DataFrame:
        """
        Return a summary DataFrame of coverage counts and area by operator and radio type.

        Returns:
            DataFrame with columns: operator_name, radio_type, polygon_count, total_area_km2.
        """
        gdf = self.to_geodataframe()

        from gigaspatial.processing.geo import estimate_utm_crs_with_fallback

        utm_crs = estimate_utm_crs_with_fallback(gdf)
        gdf["area_km2"] = gdf.to_crs(utm_crs).geometry.area / 1e6

        return (
            gdf.groupby(["operator_name", "radio_type"])
            .agg(
                polygon_count=("mobile_coverage_id", "count"),
                total_area_km2=("area_km2", "sum"),
            )
            .reset_index()
        )
covers_point(lat, lon)

Return all coverage areas whose geometry contains the given point.

Parameters:

Name Type Description Default
lat float

Latitude of the point.

required
lon float

Longitude of the point.

required

Returns:

Type Description
MobileCoverageTable

MobileCoverageTable of coverage polygons that contain the point.

Source code in gigaspatial/core/schemas/mobile_coverage.py
def covers_point(self, lat: float, lon: float) -> "MobileCoverageTable":
    """
    Return all coverage areas whose geometry contains the given point.

    Args:
        lat: Latitude of the point.
        lon: Longitude of the point.

    Returns:
        MobileCoverageTable of coverage polygons that contain the point.
    """
    point = Point(lon, lat)
    return self.__class__(
        entities=[e for e in self.entities if e.geometry.contains(point)]
    )
covers_point_by_radio_type(lat, lon)

For a given point, return coverage areas grouped by radio type.

Useful for determining which technologies cover a specific location.

Parameters:

Name Type Description Default
lat float

Latitude of the point.

required
lon float

Longitude of the point.

required

Returns:

Type Description
dict[str, MobileCoverageTable]

Dict mapping radio_type value → MobileCoverageTable.

Source code in gigaspatial/core/schemas/mobile_coverage.py
def covers_point_by_radio_type(
    self, lat: float, lon: float
) -> dict[str, "MobileCoverageTable"]:
    """
    For a given point, return coverage areas grouped by radio type.

    Useful for determining which technologies cover a specific location.

    Args:
        lat: Latitude of the point.
        lon: Longitude of the point.

    Returns:
        Dict mapping radio_type value → MobileCoverageTable.
    """
    covering = self.covers_point(lat, lon)
    groups: dict[str, list[MobileCoverage]] = {}
    for entity in covering.entities:
        groups.setdefault(entity.radio_type, []).append(entity)
    return {
        radio_type: self.__class__(entities=entities)
        for radio_type, entities in groups.items()
    }
dissolve_by_operator()

Dissolve coverage geometries by operator and radio type.

Returns a GeoDataFrame with merged polygons per operator/radio_type pair, useful for computing total coverage area without double-counting overlaps.

Returns:

Type Description
GeoDataFrame

GeoDataFrame with columns: operator_name, radio_type, geometry.

Source code in gigaspatial/core/schemas/mobile_coverage.py
def dissolve_by_operator(self) -> gpd.GeoDataFrame:
    """
    Dissolve coverage geometries by operator and radio type.

    Returns a GeoDataFrame with merged polygons per operator/radio_type pair,
    useful for computing total coverage area without double-counting overlaps.

    Returns:
        GeoDataFrame with columns: operator_name, radio_type, geometry.
    """
    gdf = self.to_geodataframe()
    return gdf.dissolve(by=["operator_name", "radio_type"]).reset_index()[
        ["operator_name", "radio_type", "geometry"]
    ]
filter_by_coverage_type(coverage_type)

Filter coverage areas by measurement methodology.

Source code in gigaspatial/core/schemas/mobile_coverage.py
def filter_by_coverage_type(
    self, coverage_type: CoverageType
) -> "MobileCoverageTable":
    """Filter coverage areas by measurement methodology."""
    return self.__class__(
        entities=[
            e for e in self.entities if e.coverage_type == coverage_type.value
        ]
    )
filter_by_measurement_date(since=None, until=None)

Filter coverage areas by measurement date range.

Parameters:

Name Type Description Default
since Optional[str]

ISO 8601 date string. Include records on or after this date.

None
until Optional[str]

ISO 8601 date string. Include records on or before this date.

None
Source code in gigaspatial/core/schemas/mobile_coverage.py
def filter_by_measurement_date(
    self,
    since: Optional[str] = None,
    until: Optional[str] = None,
) -> "MobileCoverageTable":
    """
    Filter coverage areas by measurement date range.

    Args:
        since: ISO 8601 date string. Include records on or after this date.
        until: ISO 8601 date string. Include records on or before this date.
    """
    entities = self.entities
    if since:
        entities = [
            e
            for e in entities
            if e.measurement_date is not None and e.measurement_date >= since
        ]
    if until:
        entities = [
            e
            for e in entities
            if e.measurement_date is not None and e.measurement_date <= until
        ]
    return self.__class__(entities=entities)
filter_by_operator(operator_name)

Filter coverage areas by mobile network operator.

Source code in gigaspatial/core/schemas/mobile_coverage.py
def filter_by_operator(self, operator_name: str) -> "MobileCoverageTable":
    """Filter coverage areas by mobile network operator."""
    return self.__class__(
        entities=[e for e in self.entities if e.operator_name == operator_name]
    )
filter_by_radio_type(radio_type)

Filter coverage areas by radio technology.

Source code in gigaspatial/core/schemas/mobile_coverage.py
def filter_by_radio_type(self, radio_type: RadioType) -> "MobileCoverageTable":
    """Filter coverage areas by radio technology."""
    return self.__class__(
        entities=[e for e in self.entities if e.radio_type == radio_type.value]
    )
filter_by_radio_types(radio_types)

Filter coverage areas matching any of the given radio technologies.

Source code in gigaspatial/core/schemas/mobile_coverage.py
def filter_by_radio_types(
    self, radio_types: Set[RadioType]
) -> "MobileCoverageTable":
    """Filter coverage areas matching any of the given radio technologies."""
    values = {rt.value for rt in radio_types}
    return self.__class__(
        entities=[e for e in self.entities if e.radio_type in values]
    )
filter_by_signal_strength(signal_strength)

Filter coverage areas by signal strength category.

Source code in gigaspatial/core/schemas/mobile_coverage.py
def filter_by_signal_strength(
    self, signal_strength: SignalStrength
) -> "MobileCoverageTable":
    """Filter coverage areas by signal strength category."""
    return self.__class__(
        entities=[
            e for e in self.entities if e.signal_strength == signal_strength.value
        ]
    )
from_dataframe(df, entity_class=MobileCoverage, clean=False, processor=MobileCoverageProcessor, **kwargs) classmethod

Create a MobileCoverageTable from an existing DataFrame.

Parameters:

Name Type Description Default
df DataFrame

DataFrame containing mobile coverage data.

required
entity_class Type[MobileCoverage]

Entity class to validate against. Defaults to MobileCoverage.

MobileCoverage
clean bool

Whether to apply MobileCoverageProcessor before validation. Defaults to False since DataFrames passed directly are assumed pre-cleaned.

False
processor Optional[Union[Type[EntityProcessor], EntityProcessor]]

Optional EntityProcessor subclass or instance. Defaults to MobileCoverageProcessor.

MobileCoverageProcessor
**kwargs

Additional arguments passed to the processor.

{}

Returns:

Type Description
MobileCoverageTable

MobileCoverageTable with validated MobileCoverage entities.

Source code in gigaspatial/core/schemas/mobile_coverage.py
@classmethod
def from_dataframe(
    cls,
    df: pd.DataFrame,
    entity_class: Type[MobileCoverage] = MobileCoverage,
    clean: bool = False,
    processor: Optional[
        Union[Type[EntityProcessor], EntityProcessor]
    ] = MobileCoverageProcessor,
    **kwargs,
) -> "MobileCoverageTable":
    """
    Create a MobileCoverageTable from an existing DataFrame.

    Args:
        df: DataFrame containing mobile coverage data.
        entity_class: Entity class to validate against. Defaults to MobileCoverage.
        clean: Whether to apply MobileCoverageProcessor before validation.
            Defaults to False since DataFrames passed directly are assumed pre-cleaned.
        processor: Optional EntityProcessor subclass or instance. Defaults to MobileCoverageProcessor.
        **kwargs: Additional arguments passed to the processor.

    Returns:
        MobileCoverageTable with validated MobileCoverage entities.
    """
    return super().from_dataframe(
        df=df,
        entity_class=entity_class,
        clean=clean,
        processor=processor,
        **kwargs,
    )
from_file(file_path, data_store=None, clean=True, processor=MobileCoverageProcessor, **kwargs) classmethod

Create a MobileCoverageTable from a file.

Parameters:

Name Type Description Default
file_path Union[str, Path]

Path to the dataset file.

required
data_store Optional[DataStore]

DataStore instance for file access. Defaults to LocalDataStore.

None
clean bool

Whether to apply the processor before validation. Defaults to True.

True
processor Optional[Union[Type[EntityProcessor], EntityProcessor]]

Optional EntityProcessor subclass or instance. Defaults to MobileCoverageProcessor.

MobileCoverageProcessor
**kwargs

Additional arguments forwarded to read_dataset and the processor.

{}

Returns:

Type Description
MobileCoverageTable

MobileCoverageTable with validated MobileCoverage entities.

Raises:

Type Description
FileNotFoundError

If the file does not exist.

ValueError

If the file cannot be read or parsed.

Source code in gigaspatial/core/schemas/mobile_coverage.py
@classmethod
def from_file(
    cls,
    file_path: Union[str, Path],
    data_store: Optional[DataStore] = None,
    clean: bool = True,
    processor: Optional[
        Union[Type[EntityProcessor], EntityProcessor]
    ] = MobileCoverageProcessor,
    **kwargs,
) -> "MobileCoverageTable":
    """
    Create a MobileCoverageTable from a file.

    Args:
        file_path: Path to the dataset file.
        data_store: DataStore instance for file access. Defaults to LocalDataStore.
        clean: Whether to apply the processor before validation. Defaults to True.
        processor: Optional EntityProcessor subclass or instance. Defaults to MobileCoverageProcessor.
        **kwargs: Additional arguments forwarded to read_dataset and the processor.

    Returns:
        MobileCoverageTable with validated MobileCoverage entities.

    Raises:
        FileNotFoundError: If the file does not exist.
        ValueError: If the file cannot be read or parsed.
    """
    return super().from_file(
        file_path=file_path,
        entity_class=MobileCoverage,
        data_store=data_store,
        clean=clean,
        processor=processor,
        **kwargs,
    )
get_coverage_summary()

Return a summary DataFrame of coverage counts and area by operator and radio type.

Returns:

Type Description
DataFrame

DataFrame with columns: operator_name, radio_type, polygon_count, total_area_km2.

Source code in gigaspatial/core/schemas/mobile_coverage.py
def get_coverage_summary(self) -> pd.DataFrame:
    """
    Return a summary DataFrame of coverage counts and area by operator and radio type.

    Returns:
        DataFrame with columns: operator_name, radio_type, polygon_count, total_area_km2.
    """
    gdf = self.to_geodataframe()

    from gigaspatial.processing.geo import estimate_utm_crs_with_fallback

    utm_crs = estimate_utm_crs_with_fallback(gdf)
    gdf["area_km2"] = gdf.to_crs(utm_crs).geometry.area / 1e6

    return (
        gdf.groupby(["operator_name", "radio_type"])
        .agg(
            polygon_count=("mobile_coverage_id", "count"),
            total_area_km2=("area_km2", "sum"),
        )
        .reset_index()
    )
get_operators()

Return the set of all unique operator names present in the table.

Returns:

Type Description
Set[str]

Set of operator name strings.

Source code in gigaspatial/core/schemas/mobile_coverage.py
def get_operators(self) -> Set[str]:
    """
    Return the set of all unique operator names present in the table.

    Returns:
        Set of operator name strings.
    """
    return {e.operator_name for e in self.entities if e.operator_name is not None}
get_radio_types()

Return the set of all unique radio types present in the table.

Returns:

Type Description
Set[str]

Set of radio type strings.

Source code in gigaspatial/core/schemas/mobile_coverage.py
def get_radio_types(self) -> Set[str]:
    """
    Return the set of all unique radio types present in the table.

    Returns:
        Set of radio type strings.
    """
    return {e.radio_type for e in self.entities if e.radio_type is not None}
total_coverage_area_km2(dissolve=True)

Compute total geographic area covered in square kilometres.

Parameters:

Name Type Description Default
dissolve bool

If True, dissolve overlapping polygons before computing area to avoid double-counting. Defaults to True.

True

Returns:

Type Description
float

Total coverage area in km².

Source code in gigaspatial/core/schemas/mobile_coverage.py
def total_coverage_area_km2(self, dissolve: bool = True) -> float:
    """
    Compute total geographic area covered in square kilometres.

    Args:
        dissolve: If True, dissolve overlapping polygons before computing area
            to avoid double-counting. Defaults to True.

    Returns:
        Total coverage area in km².
    """
    gdf = self.to_geodataframe()

    from gigaspatial.processing.geo import estimate_utm_crs_with_fallback

    utm_crs = estimate_utm_crs_with_fallback(gdf)
    gdf = gdf.to_crs(utm_crs)

    if dissolve:
        gdf = gdf.dissolve().reset_index()

    return gdf.geometry.area.sum() / 1e6
SignalStrength

Bases: str, Enum

Enum for signal strength categories.

Source code in gigaspatial/core/schemas/mobile_coverage.py
class SignalStrength(str, Enum):
    """Enum for signal strength categories."""

    WEAK = "weak"
    VARIABLE = "variable"
    STRONG = "strong"

shared

InfrastructureStatus

Bases: str, Enum

Enum for infrastructure operational status.

Source code in gigaspatial/core/schemas/shared.py
class InfrastructureStatus(str, Enum):
    """Enum for infrastructure operational status."""

    PROPOSED = "proposed"
    PLANNED = "planned"
    UNDER_CONSTRUCTION = "underconstruction"
    OPERATIONAL = "operational"
    DECOMMISSIONED = "decommissioned"
    INACTIVE = "inactive"
RadioType

Bases: str, Enum

Enum for different radio technology types.

Source code in gigaspatial/core/schemas/shared.py
class RadioType(str, Enum):
    """Enum for different radio technology types."""

    TWO_G = "2G"
    THREE_G = "3G"
    FOUR_G = "4G"
    FIVE_G = "5G"
SpectrumType

Bases: str, Enum

Enum for spectrum access and licensing categories.

Source code in gigaspatial/core/schemas/shared.py
class SpectrumType(str, Enum):
    """Enum for spectrum access and licensing categories."""

    LICENSED = "licensed"
    SHARED = "shared"
    UNLICENSED = "unlicensed"
WirelessAccessServiceType

Bases: str, Enum

Access-service roles supported by a wireless site.

Source code in gigaspatial/core/schemas/shared.py
class WirelessAccessServiceType(str, Enum):
    """Access-service roles supported by a wireless site."""

    MOBILE = "mobile"
    FIXED_WIRELESS = "fixed_wireless"
    MIXED = "mixed"
    UNKNOWN = "unknown"
WirelessAccessTechnology

Bases: str, Enum

Technologies used to provide wireless access services.

Source code in gigaspatial/core/schemas/shared.py
class WirelessAccessTechnology(str, Enum):
    """Technologies used to provide wireless access services."""

    GSM = "2g"
    UMTS = "3g"
    LTE = "4g"
    NR = "5g"
    WIFI = "wifi"
    WIMAX = "wimax"
    PROPRIETARY = "proprietary"
    OTHER = "other"
enum_value(value)

Return a canonical string value from an enum or string.

Source code in gigaspatial/core/schemas/shared.py
def enum_value(value: EnumValue) -> str:
    """Return a canonical string value from an enum or string."""
    return value.value if isinstance(value, Enum) else value

transmission_node

Module for transmission node schema and processing. Defines the TransmissionNode entity, representing physical network infrastructure nodes like backbone cores, metro sites, and aggregation points.

BackhaulTechnology

Bases: str, Enum

Enum for backhaul technologies.

Source code in gigaspatial/core/schemas/transmission_node.py
class BackhaulTechnology(str, Enum):
    """Enum for backhaul technologies."""

    DWDM = "dwdm"
    SDH = "sdh"
    TDM = "tdm"
    SONET = "sonet"
    CARRIER_ETHERNET = "carrier_ethernet"
    MPLS = "mpls"
    IP_OPTICAL = "ip_optical"
    PACKET_MICROWAVE = "packet_microwave"
NodeType

Bases: str, Enum

Enum for node types.

Source code in gigaspatial/core/schemas/transmission_node.py
class NodeType(str, Enum):
    """Enum for node types."""

    BACKBONE = "backbone"  # Core/national network, highest capacity
    METRO = "metro"  # Metropolitan/regional network
    AGGREGATION = "aggregation"  # Aggregating traffic from access nodes
    ACCESS = "access"  # Direct connection to cell sites
TransmissionMedium

Bases: str, Enum

Enum for transmission medium types.

Source code in gigaspatial/core/schemas/transmission_node.py
class TransmissionMedium(str, Enum):
    """Enum for transmission medium types."""

    FIBER = "fiber"
    MICROWAVE = "microwave"
    COPPER = "copper"
    COAXIAL = "coaxial"
    SATELLITE = "satellite"  # VSAT backhaul
    FREE_SPACE_OPTICAL = "fso"  # FSO links
TransmissionNode

Bases: GigaEntity

Represents a physical or logical node in the transport network.

The entity captures sites and logical aggregation points that switch, aggregate, or transport traffic between access networks and upstream backbone or metro networks.

Source code in gigaspatial/core/schemas/transmission_node.py
class TransmissionNode(GigaEntity):
    """
    Represents a physical or logical node in the transport network.

    The entity captures sites and logical aggregation points that switch,
    aggregate, or transport traffic between access networks and upstream
    backbone or metro networks.
    """

    model_config = ENUM_ENTITY_CONFIG

    # Identity
    transmission_node_id: str = Field(
        ..., max_length=50, description="Unique identifier for the transmission node"
    )
    transmission_node_id_source: Optional[str] = Field(
        None, max_length=50, description="Original node identifier in source system"
    )
    node_name: Optional[str] = Field(
        None, max_length=100, description="Common/operational name of the node"
    )

    # Classification
    node_type: Optional[NodeType] = Field(
        None, description="Hierarchical role of the node in the network"
    )
    node_status: Optional[InfrastructureStatus] = Field(
        None, description="Current operational status of the node"
    )
    transmission_medium: Optional[TransmissionMedium] = Field(
        None, description="Physical transmission medium used"
    )
    backhaul_technologies: Optional[List[BackhaulTechnology]] = Field(
        None, description="Backhaul technologies in use at this node"
    )
    access_technologies: Optional[List[str]] = Field(
        None, description="Access technologies available at this node"
    )
    is_logical_node: Optional[bool] = Field(
        None, description="True if this site hosts active transmission equipment"
    )

    # Ownership
    physical_infrastructure_provider: Optional[str] = Field(
        None,
        max_length=100,
        description="Organization that owns, hosts, or provides the physical site or "
        "passive infrastructure supporting this transmission node",
    )
    network_providers: Optional[List[str]] = Field(
        None,
        description="Mobile network operators, internet service providers, or other "
        "network operators using or operating active equipment at this node",
    )

    # Capacity
    equipped_capacity_access_mbps: Optional[float] = Field(
        None, ge=0, description="Current equipped access capacity in Mbps"
    )
    potential_capacity_access_mbps: Optional[float] = Field(
        None, ge=0, description="Maximum potential access capacity in Mbps"
    )
    equipped_capacity_backhaul_mbps: Optional[float] = Field(
        None, ge=0, description="Current equipped backhaul capacity in Mbps"
    )
    potential_capacity_backhaul_mbps: Optional[float] = Field(
        None, ge=0, description="Maximum potential backhaul capacity in Mbps"
    )

    # Physical
    elevation_meters: Optional[float] = Field(
        None, description="Elevation above sea level in meters"
    )
    power_source: Optional[PowerSource] = Field(
        None, description="Type of power source used"
    )
    data_confidence: Optional[DataConfidence] = Field(
        None, description="Level of confidence in the data"
    )

    # Temporal
    installation_date: Optional[str] = Field(
        None, description="ISO 8601 date when the node became operational"
    )
    decommission_date: Optional[str] = Field(
        None, description="ISO 8601 date when the node was decommissioned"
    )

    # Topology
    connected_node_ids: Optional[List[str]] = Field(
        None, description="IDs of directly connected transmission nodes"
    )

    @property
    def id(self) -> str:
        """Alias for transmission_node_id."""
        return self.transmission_node_id
id: str property

Alias for transmission_node_id.

TransmissionNodeProcessor

Bases: EntityProcessor

Processor for cleaning and normalizing transmission node data.

Source code in gigaspatial/core/schemas/transmission_node.py
class TransmissionNodeProcessor(EntityProcessor):
    """Processor for cleaning and normalizing transmission node data."""

    NUMERIC_COLUMNS: ClassVar[List[str]] = [
        *EntityProcessor.NUMERIC_COLUMNS,
        "equipped_capacity_access_mbps",
        "potential_capacity_access_mbps",
        "equipped_capacity_backhaul_mbps",
        "potential_capacity_backhaul_mbps",
        "elevation_meters",
    ]

    LOWERCASE_COLUMNS: ClassVar[List[str]] = [
        "transmission_medium",
        "node_type",
        "node_status",
        "power_source",
    ]

    def process(self, df: pd.DataFrame, **kwargs) -> pd.DataFrame:
        """
        Execute the full processing pipeline for transmission nodes.

        Args:
            df: Raw transmission node DataFrame.
            **kwargs: Additional processing arguments.

        Returns:
            Processed and normalized DataFrame.
        """
        df = super().process(df, **kwargs)
        df = self._normalize_transmission_medium(df)
        df = self._split_list_columns(df)
        return df

    def _normalize_transmission_medium(self, df: pd.DataFrame) -> pd.DataFrame:
        """
        Normalize transmission_medium values to canonical enums.

        Args:
            df: DataFrame to normalize.

        Returns:
            DataFrame with normalized transmission_medium column.
        """
        return self._normalize_enum_column(
            df,
            column="transmission_medium",
            alias_map=BACKHAUL_ALIAS_MAP,
            valid_values={tm.value for tm in TransmissionMedium},
            required=False,
        )

    def _split_list_columns(self, df: pd.DataFrame) -> pd.DataFrame:
        """
        Split comma-separated string columns into Python lists.

        Args:
            df: DataFrame to process.

        Returns:
            DataFrame with list columns.
        """
        for col in (
            "network_providers",
            "access_technologies",
            "backhaul_technologies",
        ):
            if col in df.columns:
                df[col] = df[col].apply(
                    lambda v: (
                        [x.strip() for x in v.split(",")] if isinstance(v, str) else v
                    )
                )
        return df
process(df, **kwargs)

Execute the full processing pipeline for transmission nodes.

Parameters:

Name Type Description Default
df DataFrame

Raw transmission node DataFrame.

required
**kwargs

Additional processing arguments.

{}

Returns:

Type Description
DataFrame

Processed and normalized DataFrame.

Source code in gigaspatial/core/schemas/transmission_node.py
def process(self, df: pd.DataFrame, **kwargs) -> pd.DataFrame:
    """
    Execute the full processing pipeline for transmission nodes.

    Args:
        df: Raw transmission node DataFrame.
        **kwargs: Additional processing arguments.

    Returns:
        Processed and normalized DataFrame.
    """
    df = super().process(df, **kwargs)
    df = self._normalize_transmission_medium(df)
    df = self._split_list_columns(df)
    return df
TransmissionNodeTable

Bases: EntityTable[TransmissionNode]

Container for TransmissionNode entities with network-specific operations.

Source code in gigaspatial/core/schemas/transmission_node.py
class TransmissionNodeTable(EntityTable[TransmissionNode]):
    """Container for TransmissionNode entities with network-specific operations."""

    @classmethod
    def from_file(
        cls,
        file_path: Union[str, Path],
        data_store: Optional[DataStore] = None,
        clean: bool = True,
        processor: Optional[
            Union[Type[EntityProcessor], EntityProcessor]
        ] = TransmissionNodeProcessor,
        **kwargs,
    ) -> "TransmissionNodeTable":
        """
        Create a TransmissionNodeTable from a file.

        Args:
            file_path: Path to the dataset file.
            data_store: DataStore instance for file access. Defaults to LocalDataStore.
            clean: Whether to apply the processor before validation. Defaults to True.
            processor: Optional EntityProcessor subclass or instance. Defaults to TransmissionNodeProcessor.
            **kwargs: Additional arguments forwarded to read_dataset and the processor.

        Returns:
            TransmissionNodeTable instance with validated TransmissionNode entities.

        Raises:
            FileNotFoundError: If the file does not exist.
            ValueError: If the file cannot be read or parsed.
        """
        return super().from_file(
            file_path=file_path,
            entity_class=TransmissionNode,
            data_store=data_store,
            clean=clean,
            processor=processor,
            **kwargs,
        )

    @classmethod
    def from_dataframe(
        cls,
        df: pd.DataFrame,
        entity_class: Type[TransmissionNode] = TransmissionNode,
        clean: bool = False,
        processor: Optional[
            Union[Type[EntityProcessor], EntityProcessor]
        ] = TransmissionNodeProcessor,
        **kwargs,
    ) -> "TransmissionNodeTable":
        """
        Create a TransmissionNodeTable from an existing DataFrame.

        Args:
            df: DataFrame containing transmission node data.
            entity_class: Entity class to validate against. Defaults to TransmissionNode.
            clean: Whether to apply TransmissionNodeProcessor before validation.
                Defaults to False since DataFrames passed directly are assumed pre-cleaned.
            processor: Optional EntityProcessor subclass or instance. Defaults to TransmissionNodeProcessor.
            **kwargs: Additional arguments passed to the processor.

        Returns:
            TransmissionNodeTable instance with validated TransmissionNode entities.
        """
        return super().from_dataframe(
            df=df,
            entity_class=entity_class,
            clean=clean,
            processor=processor,
            **kwargs,
        )

    @classmethod
    def from_files(
        cls,
        file_paths: List[Union[str, Path]],
        data_store: Optional[DataStore] = None,
        clean: bool = True,
        processor: Optional[
            Union[Type[EntityProcessor], EntityProcessor]
        ] = TransmissionNodeProcessor,
        **kwargs,
    ) -> "TransmissionNodeTable":
        """
        Load and merge multiple TransmissionNode source files into a single TransmissionNodeTable.

        Args:
            file_paths: List of paths to source files.
            data_store: DataStore instance for file access. Defaults to LocalDataStore.
            clean: Whether to apply the processor before validation. Defaults to True.
            processor: Optional EntityProcessor subclass or instance. Defaults to TransmissionNodeProcessor.
            **kwargs: Additional arguments forwarded to read_dataset and the processor.

        Returns:
            TransmissionNodeTable with merged and validated TransmissionNode entities.
        """
        return super().from_files(
            file_paths=file_paths,
            entity_class=TransmissionNode,
            data_store=data_store,
            clean=clean,
            processor=processor,
            **kwargs,
        )

    # ------------------------------------------------------------------
    # Filters
    # ------------------------------------------------------------------

    def filter_by_node_type(
        self, node_type: Union[NodeType, str]
    ) -> "TransmissionNodeTable":
        """
        Filter nodes by their hierarchical type in the network.

        Args:
            node_type: A ``NodeType`` enum member or its string value
                (e.g. ``"backbone"``, ``"metro"``).
        """
        value = (
            NodeType(node_type).value if isinstance(node_type, str) else node_type.value
        )
        return self.__class__(
            entities=[e for e in self.entities if e.node_type == value]
        )

    def filter_by_node_types(
        self, node_types: Set[Union[NodeType, str]]
    ) -> "TransmissionNodeTable":
        """
        Filter nodes matching any of the given node types.

        Args:
            node_types: A set of ``NodeType`` enum members or string values.
        """
        values = {
            NodeType(nt).value if isinstance(nt, str) else nt.value for nt in node_types
        }
        return self.__class__(
            entities=[e for e in self.entities if e.node_type in values]
        )

    def filter_by_status(
        self, status: Union[InfrastructureStatus, str]
    ) -> "TransmissionNodeTable":
        """
        Filter nodes by operational status.

        Args:
            status: A ``NodeStatus`` enum member or its string value
                (e.g. ``"operational"``, ``"planned"``).
        """
        value = (
            InfrastructureStatus(status).value
            if isinstance(status, str)
            else status.value
        )
        return self.__class__(
            entities=[e for e in self.entities if e.node_status == value]
        )

    def filter_operational(self) -> "TransmissionNodeTable":
        """Return only operational nodes."""
        return self.filter_by_status(InfrastructureStatus.OPERATIONAL)

    def filter_by_medium(
        self, medium: Union[TransmissionMedium, str]
    ) -> "TransmissionNodeTable":
        """
        Filter nodes by transmission medium.

        Args:
            medium: A ``TransmissionMedium`` enum member or its string value
                (e.g. ``"fiber"``, ``"microwave"``, ``"satellite"``).
        """
        value = (
            TransmissionMedium(medium).value
            if isinstance(medium, str)
            else medium.value
        )
        return self.__class__(
            entities=[e for e in self.entities if e.transmission_medium == value]
        )

    def filter_by_provider(self, provider: str) -> "TransmissionNodeTable":
        """Filter nodes by physical infrastructure provider."""
        return self.__class__(
            entities=[
                e
                for e in self.entities
                if e.physical_infrastructure_provider == provider
            ]
        )

    def filter_by_backhaul_technology(
        self, technology: Union[BackhaulTechnology, str]
    ) -> "TransmissionNodeTable":
        """
        Filter nodes that use a specific backhaul technology.

        Args:
            technology: A ``BackhaulTechnology`` enum member or its string value
                (e.g. ``"dwdm"``, ``"mpls"``).
        """
        value = (
            BackhaulTechnology(technology).value
            if isinstance(technology, str)
            else technology.value
        )
        return self.__class__(
            entities=[
                e
                for e in self.entities
                if e.backhaul_technologies and value in e.backhaul_technologies
            ]
        )

    def filter_by_min_capacity(
        self,
        min_mbps: float,
        capacity_type: str = "equipped_access",
    ) -> "TransmissionNodeTable":
        """
        Filter nodes by minimum capacity threshold.

        Args:
            min_mbps: Minimum capacity in Mbps.
            capacity_type: One of 'equipped_access', 'potential_access',
                'equipped_backhaul', 'potential_backhaul'.
        """
        field_map = {
            "equipped_access": "equipped_capacity_access_mbps",
            "potential_access": "potential_capacity_access_mbps",
            "equipped_backhaul": "equipped_capacity_backhaul_mbps",
            "potential_backhaul": "potential_capacity_backhaul_mbps",
        }
        if capacity_type not in field_map:
            raise ValueError(
                f"Invalid capacity_type '{capacity_type}'. "
                f"Must be one of: {list(field_map.keys())}"
            )
        field = field_map[capacity_type]
        return self.__class__(
            entities=[
                e
                for e in self.entities
                if getattr(e, field) is not None and getattr(e, field) >= min_mbps
            ]
        )

    # ------------------------------------------------------------------
    # Aggregations
    # ------------------------------------------------------------------

    def get_node_types(self) -> Set[str]:
        """
        Return the set of all unique node types present in the table.

        Returns:
            Set of unique node type strings.
        """
        return {e.node_type for e in self.entities if e.node_type is not None}

    def get_providers(self) -> Set[str]:
        """
        Return all unique physical infrastructure providers.

        Returns:
            Set of unique provider name strings.
        """
        return {
            e.physical_infrastructure_provider
            for e in self.entities
            if e.physical_infrastructure_provider is not None
        }

    def get_backbone_nodes(self) -> "TransmissionNodeTable":
        """
        Filter to only backbone (core network) nodes.

        Returns:
            TransmissionNodeTable with only backbone nodes.
        """
        return self.filter_by_node_type(NodeType.BACKBONE)

    def get_access_nodes(self) -> "TransmissionNodeTable":
        """
        Filter to only access-layer nodes.

        Returns:
            TransmissionNodeTable with only access nodes.
        """
        return self.filter_by_node_type(NodeType.ACCESS)

    # ------------------------------------------------------------------
    # Topology
    # ------------------------------------------------------------------

    def to_topology_graph(self) -> nx.Graph:
        """
        Build a NetworkX graph from the connected_node_ids field on each node.

        Uses the explicitly declared node connections rather than spatial proximity.
        Use to_distance_graph() instead for proximity-based graph construction.

        Returns:
            NetworkX Graph where nodes are transmission_node_ids and edges
            represent declared connections between nodes.
        """
        G = nx.Graph()

        for entity in self.entities:
            G.add_node(
                entity.transmission_node_id,
                node_type=entity.node_type,
                node_status=entity.node_status,
                transmission_medium=entity.transmission_medium,
                equipped_capacity_backhaul_mbps=entity.equipped_capacity_backhaul_mbps,
            )

        for entity in self.entities:
            if entity.connected_node_ids:
                for connected_id in entity.connected_node_ids:
                    if G.has_node(connected_id):
                        G.add_edge(entity.transmission_node_id, connected_id)
                    else:
                        logger.warning(
                            "Node '%s' references unknown connected node '%s'. "
                            "Edge skipped.",
                            entity.transmission_node_id,
                            connected_id,
                        )

        logger.debug(
            "Topology graph built: %d nodes, %d edges.",
            G.number_of_nodes(),
            G.number_of_edges(),
        )
        return G
filter_by_backhaul_technology(technology)

Filter nodes that use a specific backhaul technology.

Parameters:

Name Type Description Default
technology Union[BackhaulTechnology, str]

A BackhaulTechnology enum member or its string value (e.g. "dwdm", "mpls").

required
Source code in gigaspatial/core/schemas/transmission_node.py
def filter_by_backhaul_technology(
    self, technology: Union[BackhaulTechnology, str]
) -> "TransmissionNodeTable":
    """
    Filter nodes that use a specific backhaul technology.

    Args:
        technology: A ``BackhaulTechnology`` enum member or its string value
            (e.g. ``"dwdm"``, ``"mpls"``).
    """
    value = (
        BackhaulTechnology(technology).value
        if isinstance(technology, str)
        else technology.value
    )
    return self.__class__(
        entities=[
            e
            for e in self.entities
            if e.backhaul_technologies and value in e.backhaul_technologies
        ]
    )
filter_by_medium(medium)

Filter nodes by transmission medium.

Parameters:

Name Type Description Default
medium Union[TransmissionMedium, str]

A TransmissionMedium enum member or its string value (e.g. "fiber", "microwave", "satellite").

required
Source code in gigaspatial/core/schemas/transmission_node.py
def filter_by_medium(
    self, medium: Union[TransmissionMedium, str]
) -> "TransmissionNodeTable":
    """
    Filter nodes by transmission medium.

    Args:
        medium: A ``TransmissionMedium`` enum member or its string value
            (e.g. ``"fiber"``, ``"microwave"``, ``"satellite"``).
    """
    value = (
        TransmissionMedium(medium).value
        if isinstance(medium, str)
        else medium.value
    )
    return self.__class__(
        entities=[e for e in self.entities if e.transmission_medium == value]
    )
filter_by_min_capacity(min_mbps, capacity_type='equipped_access')

Filter nodes by minimum capacity threshold.

Parameters:

Name Type Description Default
min_mbps float

Minimum capacity in Mbps.

required
capacity_type str

One of 'equipped_access', 'potential_access', 'equipped_backhaul', 'potential_backhaul'.

'equipped_access'
Source code in gigaspatial/core/schemas/transmission_node.py
def filter_by_min_capacity(
    self,
    min_mbps: float,
    capacity_type: str = "equipped_access",
) -> "TransmissionNodeTable":
    """
    Filter nodes by minimum capacity threshold.

    Args:
        min_mbps: Minimum capacity in Mbps.
        capacity_type: One of 'equipped_access', 'potential_access',
            'equipped_backhaul', 'potential_backhaul'.
    """
    field_map = {
        "equipped_access": "equipped_capacity_access_mbps",
        "potential_access": "potential_capacity_access_mbps",
        "equipped_backhaul": "equipped_capacity_backhaul_mbps",
        "potential_backhaul": "potential_capacity_backhaul_mbps",
    }
    if capacity_type not in field_map:
        raise ValueError(
            f"Invalid capacity_type '{capacity_type}'. "
            f"Must be one of: {list(field_map.keys())}"
        )
    field = field_map[capacity_type]
    return self.__class__(
        entities=[
            e
            for e in self.entities
            if getattr(e, field) is not None and getattr(e, field) >= min_mbps
        ]
    )
filter_by_node_type(node_type)

Filter nodes by their hierarchical type in the network.

Parameters:

Name Type Description Default
node_type Union[NodeType, str]

A NodeType enum member or its string value (e.g. "backbone", "metro").

required
Source code in gigaspatial/core/schemas/transmission_node.py
def filter_by_node_type(
    self, node_type: Union[NodeType, str]
) -> "TransmissionNodeTable":
    """
    Filter nodes by their hierarchical type in the network.

    Args:
        node_type: A ``NodeType`` enum member or its string value
            (e.g. ``"backbone"``, ``"metro"``).
    """
    value = (
        NodeType(node_type).value if isinstance(node_type, str) else node_type.value
    )
    return self.__class__(
        entities=[e for e in self.entities if e.node_type == value]
    )
filter_by_node_types(node_types)

Filter nodes matching any of the given node types.

Parameters:

Name Type Description Default
node_types Set[Union[NodeType, str]]

A set of NodeType enum members or string values.

required
Source code in gigaspatial/core/schemas/transmission_node.py
def filter_by_node_types(
    self, node_types: Set[Union[NodeType, str]]
) -> "TransmissionNodeTable":
    """
    Filter nodes matching any of the given node types.

    Args:
        node_types: A set of ``NodeType`` enum members or string values.
    """
    values = {
        NodeType(nt).value if isinstance(nt, str) else nt.value for nt in node_types
    }
    return self.__class__(
        entities=[e for e in self.entities if e.node_type in values]
    )
filter_by_provider(provider)

Filter nodes by physical infrastructure provider.

Source code in gigaspatial/core/schemas/transmission_node.py
def filter_by_provider(self, provider: str) -> "TransmissionNodeTable":
    """Filter nodes by physical infrastructure provider."""
    return self.__class__(
        entities=[
            e
            for e in self.entities
            if e.physical_infrastructure_provider == provider
        ]
    )
filter_by_status(status)

Filter nodes by operational status.

Parameters:

Name Type Description Default
status Union[InfrastructureStatus, str]

A NodeStatus enum member or its string value (e.g. "operational", "planned").

required
Source code in gigaspatial/core/schemas/transmission_node.py
def filter_by_status(
    self, status: Union[InfrastructureStatus, str]
) -> "TransmissionNodeTable":
    """
    Filter nodes by operational status.

    Args:
        status: A ``NodeStatus`` enum member or its string value
            (e.g. ``"operational"``, ``"planned"``).
    """
    value = (
        InfrastructureStatus(status).value
        if isinstance(status, str)
        else status.value
    )
    return self.__class__(
        entities=[e for e in self.entities if e.node_status == value]
    )
filter_operational()

Return only operational nodes.

Source code in gigaspatial/core/schemas/transmission_node.py
def filter_operational(self) -> "TransmissionNodeTable":
    """Return only operational nodes."""
    return self.filter_by_status(InfrastructureStatus.OPERATIONAL)
from_dataframe(df, entity_class=TransmissionNode, clean=False, processor=TransmissionNodeProcessor, **kwargs) classmethod

Create a TransmissionNodeTable from an existing DataFrame.

Parameters:

Name Type Description Default
df DataFrame

DataFrame containing transmission node data.

required
entity_class Type[TransmissionNode]

Entity class to validate against. Defaults to TransmissionNode.

TransmissionNode
clean bool

Whether to apply TransmissionNodeProcessor before validation. Defaults to False since DataFrames passed directly are assumed pre-cleaned.

False
processor Optional[Union[Type[EntityProcessor], EntityProcessor]]

Optional EntityProcessor subclass or instance. Defaults to TransmissionNodeProcessor.

TransmissionNodeProcessor
**kwargs

Additional arguments passed to the processor.

{}

Returns:

Type Description
TransmissionNodeTable

TransmissionNodeTable instance with validated TransmissionNode entities.

Source code in gigaspatial/core/schemas/transmission_node.py
@classmethod
def from_dataframe(
    cls,
    df: pd.DataFrame,
    entity_class: Type[TransmissionNode] = TransmissionNode,
    clean: bool = False,
    processor: Optional[
        Union[Type[EntityProcessor], EntityProcessor]
    ] = TransmissionNodeProcessor,
    **kwargs,
) -> "TransmissionNodeTable":
    """
    Create a TransmissionNodeTable from an existing DataFrame.

    Args:
        df: DataFrame containing transmission node data.
        entity_class: Entity class to validate against. Defaults to TransmissionNode.
        clean: Whether to apply TransmissionNodeProcessor before validation.
            Defaults to False since DataFrames passed directly are assumed pre-cleaned.
        processor: Optional EntityProcessor subclass or instance. Defaults to TransmissionNodeProcessor.
        **kwargs: Additional arguments passed to the processor.

    Returns:
        TransmissionNodeTable instance with validated TransmissionNode entities.
    """
    return super().from_dataframe(
        df=df,
        entity_class=entity_class,
        clean=clean,
        processor=processor,
        **kwargs,
    )
from_file(file_path, data_store=None, clean=True, processor=TransmissionNodeProcessor, **kwargs) classmethod

Create a TransmissionNodeTable from a file.

Parameters:

Name Type Description Default
file_path Union[str, Path]

Path to the dataset file.

required
data_store Optional[DataStore]

DataStore instance for file access. Defaults to LocalDataStore.

None
clean bool

Whether to apply the processor before validation. Defaults to True.

True
processor Optional[Union[Type[EntityProcessor], EntityProcessor]]

Optional EntityProcessor subclass or instance. Defaults to TransmissionNodeProcessor.

TransmissionNodeProcessor
**kwargs

Additional arguments forwarded to read_dataset and the processor.

{}

Returns:

Type Description
TransmissionNodeTable

TransmissionNodeTable instance with validated TransmissionNode entities.

Raises:

Type Description
FileNotFoundError

If the file does not exist.

ValueError

If the file cannot be read or parsed.

Source code in gigaspatial/core/schemas/transmission_node.py
@classmethod
def from_file(
    cls,
    file_path: Union[str, Path],
    data_store: Optional[DataStore] = None,
    clean: bool = True,
    processor: Optional[
        Union[Type[EntityProcessor], EntityProcessor]
    ] = TransmissionNodeProcessor,
    **kwargs,
) -> "TransmissionNodeTable":
    """
    Create a TransmissionNodeTable from a file.

    Args:
        file_path: Path to the dataset file.
        data_store: DataStore instance for file access. Defaults to LocalDataStore.
        clean: Whether to apply the processor before validation. Defaults to True.
        processor: Optional EntityProcessor subclass or instance. Defaults to TransmissionNodeProcessor.
        **kwargs: Additional arguments forwarded to read_dataset and the processor.

    Returns:
        TransmissionNodeTable instance with validated TransmissionNode entities.

    Raises:
        FileNotFoundError: If the file does not exist.
        ValueError: If the file cannot be read or parsed.
    """
    return super().from_file(
        file_path=file_path,
        entity_class=TransmissionNode,
        data_store=data_store,
        clean=clean,
        processor=processor,
        **kwargs,
    )
from_files(file_paths, data_store=None, clean=True, processor=TransmissionNodeProcessor, **kwargs) classmethod

Load and merge multiple TransmissionNode source files into a single TransmissionNodeTable.

Parameters:

Name Type Description Default
file_paths List[Union[str, Path]]

List of paths to source files.

required
data_store Optional[DataStore]

DataStore instance for file access. Defaults to LocalDataStore.

None
clean bool

Whether to apply the processor before validation. Defaults to True.

True
processor Optional[Union[Type[EntityProcessor], EntityProcessor]]

Optional EntityProcessor subclass or instance. Defaults to TransmissionNodeProcessor.

TransmissionNodeProcessor
**kwargs

Additional arguments forwarded to read_dataset and the processor.

{}

Returns:

Type Description
TransmissionNodeTable

TransmissionNodeTable with merged and validated TransmissionNode entities.

Source code in gigaspatial/core/schemas/transmission_node.py
@classmethod
def from_files(
    cls,
    file_paths: List[Union[str, Path]],
    data_store: Optional[DataStore] = None,
    clean: bool = True,
    processor: Optional[
        Union[Type[EntityProcessor], EntityProcessor]
    ] = TransmissionNodeProcessor,
    **kwargs,
) -> "TransmissionNodeTable":
    """
    Load and merge multiple TransmissionNode source files into a single TransmissionNodeTable.

    Args:
        file_paths: List of paths to source files.
        data_store: DataStore instance for file access. Defaults to LocalDataStore.
        clean: Whether to apply the processor before validation. Defaults to True.
        processor: Optional EntityProcessor subclass or instance. Defaults to TransmissionNodeProcessor.
        **kwargs: Additional arguments forwarded to read_dataset and the processor.

    Returns:
        TransmissionNodeTable with merged and validated TransmissionNode entities.
    """
    return super().from_files(
        file_paths=file_paths,
        entity_class=TransmissionNode,
        data_store=data_store,
        clean=clean,
        processor=processor,
        **kwargs,
    )
get_access_nodes()

Filter to only access-layer nodes.

Returns:

Type Description
TransmissionNodeTable

TransmissionNodeTable with only access nodes.

Source code in gigaspatial/core/schemas/transmission_node.py
def get_access_nodes(self) -> "TransmissionNodeTable":
    """
    Filter to only access-layer nodes.

    Returns:
        TransmissionNodeTable with only access nodes.
    """
    return self.filter_by_node_type(NodeType.ACCESS)
get_backbone_nodes()

Filter to only backbone (core network) nodes.

Returns:

Type Description
TransmissionNodeTable

TransmissionNodeTable with only backbone nodes.

Source code in gigaspatial/core/schemas/transmission_node.py
def get_backbone_nodes(self) -> "TransmissionNodeTable":
    """
    Filter to only backbone (core network) nodes.

    Returns:
        TransmissionNodeTable with only backbone nodes.
    """
    return self.filter_by_node_type(NodeType.BACKBONE)
get_node_types()

Return the set of all unique node types present in the table.

Returns:

Type Description
Set[str]

Set of unique node type strings.

Source code in gigaspatial/core/schemas/transmission_node.py
def get_node_types(self) -> Set[str]:
    """
    Return the set of all unique node types present in the table.

    Returns:
        Set of unique node type strings.
    """
    return {e.node_type for e in self.entities if e.node_type is not None}
get_providers()

Return all unique physical infrastructure providers.

Returns:

Type Description
Set[str]

Set of unique provider name strings.

Source code in gigaspatial/core/schemas/transmission_node.py
def get_providers(self) -> Set[str]:
    """
    Return all unique physical infrastructure providers.

    Returns:
        Set of unique provider name strings.
    """
    return {
        e.physical_infrastructure_provider
        for e in self.entities
        if e.physical_infrastructure_provider is not None
    }
to_topology_graph()

Build a NetworkX graph from the connected_node_ids field on each node.

Uses the explicitly declared node connections rather than spatial proximity. Use to_distance_graph() instead for proximity-based graph construction.

Returns:

Type Description
Graph

NetworkX Graph where nodes are transmission_node_ids and edges

Graph

represent declared connections between nodes.

Source code in gigaspatial/core/schemas/transmission_node.py
def to_topology_graph(self) -> nx.Graph:
    """
    Build a NetworkX graph from the connected_node_ids field on each node.

    Uses the explicitly declared node connections rather than spatial proximity.
    Use to_distance_graph() instead for proximity-based graph construction.

    Returns:
        NetworkX Graph where nodes are transmission_node_ids and edges
        represent declared connections between nodes.
    """
    G = nx.Graph()

    for entity in self.entities:
        G.add_node(
            entity.transmission_node_id,
            node_type=entity.node_type,
            node_status=entity.node_status,
            transmission_medium=entity.transmission_medium,
            equipped_capacity_backhaul_mbps=entity.equipped_capacity_backhaul_mbps,
        )

    for entity in self.entities:
        if entity.connected_node_ids:
            for connected_id in entity.connected_node_ids:
                if G.has_node(connected_id):
                    G.add_edge(entity.transmission_node_id, connected_id)
                else:
                    logger.warning(
                        "Node '%s' references unknown connected node '%s'. "
                        "Edge skipped.",
                        entity.transmission_node_id,
                        connected_id,
                    )

    logger.debug(
        "Topology graph built: %d nodes, %d edges.",
        G.number_of_nodes(),
        G.number_of_edges(),
    )
    return G

wireless_access_service

Module for wireless access service schema and processing.

Defines the WirelessAccessService entity, representing a wireless access capability delivered from a WirelessSite. The schema supports mobile, fixed-wireless, and mixed access services without requiring detailed cell-, sector-, or radio-installation-level engineering data.

The module includes
  • WirelessAccessService: Pydantic entity representing an access capability.
  • WirelessAccessServiceProcessor: Cleaning and normalization pipeline.
  • WirelessAccessServiceTable: Typed entity-table container and filters.

A WirelessAccessService can represent a reported service deployment, a site-level access role, or an aggregation of lower-level source records.

WirelessAccessService

Bases: GigaEntityNoLocation

Represents an access capability delivered from a WirelessSite.

The entity supports mobile and fixed-wireless access without requiring cell-, sector-, or radio-installation-level engineering data. It can represent a reported access deployment, a service role, or a meaningful aggregation of detailed source records at one wireless site.

Source code in gigaspatial/core/schemas/wireless_access_service.py
class WirelessAccessService(GigaEntityNoLocation):
    """
    Represents an access capability delivered from a WirelessSite.

    The entity supports mobile and fixed-wireless access without requiring
    cell-, sector-, or radio-installation-level engineering data. It can
    represent a reported access deployment, a service role, or a meaningful
    aggregation of detailed source records at one wireless site.
    """

    model_config = ENUM_ENTITY_CONFIG

    wireless_access_service_id: str = Field(
        ...,
        max_length=50,
        description="Unique identifier for the wireless access service record",
    )
    wireless_site_id: str = Field(
        ...,
        max_length=50,
        description="Identifier of the WirelessSite hosting this access capability",
    )

    access_service_type: WirelessAccessServiceType = Field(
        ...,
        description="Primary access role delivered from the site",
    )
    service_status: Optional[InfrastructureStatus] = Field(
        None,
        description="Operational status of this access capability",
    )

    access_technologies: Optional[Set[WirelessAccessTechnology]] = Field(
        None,
        description=(
            "Technologies used by this access capability, such as 4G, 5G, Wi-Fi, "
            "WiMAX, or a proprietary system"
        ),
    )
    frequency_bands_mhz: Optional[Set[float]] = Field(
        None,
        description=(
            "Reported frequency bands used by this service in MHz; a summary "
            "rather than a per-sector or per-channel allocation"
        ),
    )
    spectrum_type: Optional[SpectrumType] = Field(
        None,
        description="Spectrum licensing category used by the access capability",
    )

    network_provider: Optional[str] = Field(
        None,
        max_length=100,
        description=(
            "Mobile network operator, ISP, or other provider operating this "
            "specific access capability"
        ),
    )

    equipped_capacity_mbps: Optional[float] = Field(
        None,
        ge=0,
        description="Installed aggregate access capacity in Mbps",
    )
    potential_capacity_mbps: Optional[float] = Field(
        None,
        ge=0,
        description=(
            "Maximum plausible aggregate capacity after documented upgrades or "
            "configuration changes, in Mbps"
        ),
    )
    available_capacity_mbps: Optional[float] = Field(
        None,
        ge=0,
        description=(
            "Capacity currently unallocated for additional connections, in Mbps; "
            "valid only with capacity_observed_at"
        ),
    )
    capacity_observed_at: Optional[str] = Field(
        None,
        description="ISO 8601 timestamp when available capacity was measured",
    )

    sector_count: Optional[int] = Field(
        None,
        ge=1,
        description=(
            "Known number of sectors supporting this access capability; "
            "optional site/service summary, not an individual-sector record"
        ),
    )
    antenna_height_agl_meters: Optional[float] = Field(
        None,
        ge=0,
        description=(
            "Known antenna height above ground level (AGL) for this access "
            "capability, in meters; may be a representative or maximum value "
            "when multiple antennas are used."
        ),
    )

    data_confidence: Optional[DataConfidence] = Field(
        None,
        description="Confidence assigned to the source or observed service data",
    )
    last_verified_date: Optional[str] = Field(
        None,
        description="ISO 8601 date the access capability was last verified",
    )

    @property
    def id(self) -> str:
        """Alias for wireless_access_service_id."""
        return self.wireless_access_service_id
id: str property

Alias for wireless_access_service_id.

WirelessAccessServiceProcessor

Bases: EntityProcessor

Processor for cleaning and normalizing WirelessAccessService data.

Source code in gigaspatial/core/schemas/wireless_access_service.py
class WirelessAccessServiceProcessor(EntityProcessor):
    """Processor for cleaning and normalizing WirelessAccessService data."""

    NUMERIC_COLUMNS: ClassVar[List[str]] = [
        *EntityProcessor.NUMERIC_COLUMNS,
        "equipped_capacity_mbps",
        "potential_capacity_mbps",
        "available_capacity_mbps",
        "sector_count",
        "antenna_height_agl_meters",
    ]

    LOWERCASE_COLUMNS: ClassVar[List[str]] = [
        "access_service_type",
        "service_status",
        "spectrum_type",
        "data_confidence",
    ]

    COLUMN_ALIASES: ClassVar[Dict[str, str]] = {
        "id": "wireless_access_service_id",
        "service_id": "wireless_access_service_id",
        "access_service_id": "wireless_access_service_id",
        "cell_id": "wireless_access_service_id",
        "wireless_site": "wireless_site_id",
        "site_id": "wireless_site_id",
        "cell_tower_id": "wireless_site_id",
        "tower_id": "wireless_site_id",
        "fixed_wireless_site_id": "wireless_site_id",
        "service_type": "access_service_type",
        "access_type": "access_service_type",
        "technology": "access_technologies",
        "technologies": "access_technologies",
        # Source values such as "4G" and "5G" are normalized to detailed
        # WirelessAccessTechnology values where known; raw generation is not retained.
        "radio_type": "access_technologies",
        "radio_types": "access_technologies",
        "radio_technology": "access_technologies",
        "radio_technologies": "access_technologies",
        "frequency_band_mhz": "frequency_bands_mhz",
        "frequency_mhz": "frequency_bands_mhz",
        "band_mhz": "frequency_bands_mhz",
        "frequency_band": "frequency_bands_mhz",
        "frequency_bands": "frequency_bands_mhz",
        "band": "frequency_bands_mhz",
        "operator": "network_provider",
        "operator_name": "network_provider",
        "provider": "network_provider",
        "network_operator": "network_provider",
        "capacity_mbps": "equipped_capacity_mbps",
        "installed_capacity_mbps": "equipped_capacity_mbps",
        "available_capacity": "available_capacity_mbps",
        "antenna_height": "antenna_height_agl_meters",
        "antenna_height_agl": "antenna_height_agl_meters",
        "is_active": "service_status",
        "status": "service_status",
        "potential_capacity": "potential_capacity_mbps",
        "potential_capacity_mbps": "potential_capacity_mbps",
        "available_capacity_mbps": "available_capacity_mbps",
        "capacity_observed_date": "capacity_observed_at",
        "capacity_observed_at": "capacity_observed_at",
        "last_verified": "last_verified_date",
        "verification_date": "last_verified_date",
    }

    ACCESS_SERVICE_TYPE_ALIAS_MAP: ClassVar[Dict[str, str]] = {
        "mobile": WirelessAccessServiceType.MOBILE.value,
        "mobile cellular": WirelessAccessServiceType.MOBILE.value,
        "cellular": WirelessAccessServiceType.MOBILE.value,
        "mobile broadband": WirelessAccessServiceType.MOBILE.value,
        "fwa": WirelessAccessServiceType.FIXED_WIRELESS.value,
        "fixed wireless": WirelessAccessServiceType.FIXED_WIRELESS.value,
        "fixed_wireless": WirelessAccessServiceType.FIXED_WIRELESS.value,
        "wireless broadband": WirelessAccessServiceType.FIXED_WIRELESS.value,
        "mixed": WirelessAccessServiceType.MIXED.value,
        "mobile and fixed wireless": WirelessAccessServiceType.MIXED.value,
        "unknown": WirelessAccessServiceType.UNKNOWN.value,
    }

    STATUS_ALIAS_MAP: ClassVar[Dict[str, str]] = {
        "active": InfrastructureStatus.OPERATIONAL.value,
        "in service": InfrastructureStatus.OPERATIONAL.value,
        "live": InfrastructureStatus.OPERATIONAL.value,
        "under construction": InfrastructureStatus.UNDER_CONSTRUCTION.value,
        "under_construction": InfrastructureStatus.UNDER_CONSTRUCTION.value,
        "retired": InfrastructureStatus.DECOMMISSIONED.value,
        "decommissioned": InfrastructureStatus.DECOMMISSIONED.value,
        "inactive": InfrastructureStatus.INACTIVE.value,
    }

    SPECTRUM_TYPE_ALIAS_MAP: ClassVar[Dict[str, str]] = {
        "licensed": SpectrumType.LICENSED.value,
        "licenced": SpectrumType.LICENSED.value,
        "license": SpectrumType.LICENSED.value,
        "shared": SpectrumType.SHARED.value,
        "shared spectrum": SpectrumType.SHARED.value,
        "unlicensed": SpectrumType.UNLICENSED.value,
        "unlicenced": SpectrumType.UNLICENSED.value,
        "un-licensed": SpectrumType.UNLICENSED.value,
        "licence exempt": SpectrumType.UNLICENSED.value,
        "license exempt": SpectrumType.UNLICENSED.value,
    }

    def process(self, df: pd.DataFrame, **kwargs) -> pd.DataFrame:
        """
        Execute the complete WirelessAccessService normalization pipeline.

        Args:
            df: Raw wireless access service DataFrame.
            **kwargs: Additional processing arguments.

        Returns:
            Processed and normalized DataFrame.
        """
        df = super().process(df, **kwargs)
        df = self._normalize_status_from_boolean(df)
        df = self._normalize_enum_columns(df)
        df = self._normalize_access_technologies(df)
        df = self._normalize_frequency_bands_mhz(df)
        df = self._normalize_sector_count(df)
        df = self._coerce_invalid_nonnegative_values(df)
        return df

    def _normalize_enum_columns(self, df: pd.DataFrame) -> pd.DataFrame:
        """
        Normalize scalar enum-backed fields to canonical values.

        Args:
            df: DataFrame to process.

        Returns:
            DataFrame with normalized enum values.
        """
        enum_columns = (
            (
                "access_service_type",
                self.ACCESS_SERVICE_TYPE_ALIAS_MAP,
                {item.value for item in WirelessAccessServiceType},
                True,
            ),
            (
                "service_status",
                self.STATUS_ALIAS_MAP,
                {item.value for item in InfrastructureStatus},
                False,
            ),
            (
                "spectrum_type",
                self.SPECTRUM_TYPE_ALIAS_MAP,
                {item.value for item in SpectrumType},
                False,
            ),
        )

        for column, alias_map, valid_values, required in enum_columns:
            df = self._normalize_enum_column(
                df,
                column=column,
                alias_map=alias_map,
                valid_values=valid_values,
                required=required,
            )

        return df

    def _normalize_access_technologies(
        self,
        df: pd.DataFrame,
    ) -> pd.DataFrame:
        """Normalize access technologies to canonical enum values."""
        return self._normalize_enum_collection_column(
            df=df,
            column="access_technologies",
            alias_map=WIRELESS_ACCESS_TECHNOLOGY_ALIAS_MAP,
            valid_values={technology.value for technology in WirelessAccessTechnology},
        )

    def _normalize_frequency_bands_mhz(self, df: pd.DataFrame) -> pd.DataFrame:
        """
        Convert frequency-band values to lists of valid MHz measurements.

        Args:
            df: DataFrame to process.

        Returns:
            DataFrame with normalized frequency_bands_mhz values.
        """
        if "frequency_bands_mhz" not in df.columns:
            return df

        def normalize(value: object) -> Optional[List[float]]:
            if value is None or (isinstance(value, float) and pd.isna(value)):
                return None

            if isinstance(value, str):
                values = (
                    value.replace(";", ",")
                    .replace("|", ",")
                    .replace("/", ",")
                    .split(",")
                )
            elif isinstance(value, (list, tuple, set)):
                values = value
            else:
                values = [value]

            normalized: List[float] = []
            for item in values:
                try:
                    numeric_value = float(item)
                except (TypeError, ValueError):
                    logger.warning(
                        "Invalid frequency-band value '%s'; value omitted.",
                        item,
                    )
                    continue

                if numeric_value <= 0:
                    logger.warning(
                        "Non-positive frequency-band value '%s'; value omitted.",
                        item,
                    )
                    continue

                normalized.append(numeric_value)

            return list(dict.fromkeys(normalized)) or None

        df["frequency_bands_mhz"] = df["frequency_bands_mhz"].apply(normalize)
        return df

    def _normalize_status_from_boolean(self, df: pd.DataFrame) -> pd.DataFrame:
        """
        Convert legacy is_active values into service_status when needed.

        This supports old Cell-like input datasets after column alias mapping.

        Args:
            df: DataFrame to process.

        Returns:
            DataFrame with normalized service_status values.
        """
        if "service_status" not in df.columns:
            return df

        active_status = InfrastructureStatus.OPERATIONAL.value
        inactive_status = InfrastructureStatus.INACTIVE.value

        boolean_map = {
            True: active_status,
            False: inactive_status,
            "true": active_status,
            "false": inactive_status,
            "yes": active_status,
            "no": inactive_status,
            "1": active_status,
            "0": inactive_status,
            1: active_status,
            0: inactive_status,
        }

        df["service_status"] = df["service_status"].apply(
            lambda value: boolean_map.get(value, value)
        )
        return df

    def _normalize_sector_count(self, df: pd.DataFrame) -> pd.DataFrame:
        """Retain only positive whole-number sector counts."""
        if "sector_count" not in df.columns:
            return df

        values = pd.to_numeric(df["sector_count"], errors="coerce")
        valid = values.notna() & (values >= 1) & (values % 1 == 0)
        df["sector_count"] = values.where(valid).astype("Int64")
        return df

    def _coerce_invalid_nonnegative_values(
        self,
        df: pd.DataFrame,
    ) -> pd.DataFrame:
        """
        Replace invalid negative values in constrained numeric fields with null.

        Args:
            df: DataFrame to process.

        Returns:
            DataFrame with invalid values coerced to null.
        """
        fields = (
            "equipped_capacity_mbps",
            "potential_capacity_mbps",
            "available_capacity_mbps",
            "sector_count",
            "antenna_height_agl_meters",
        )

        for column in fields:
            if column in df.columns:
                df.loc[df[column] < 0, column] = None

        return df
process(df, **kwargs)

Execute the complete WirelessAccessService normalization pipeline.

Parameters:

Name Type Description Default
df DataFrame

Raw wireless access service DataFrame.

required
**kwargs

Additional processing arguments.

{}

Returns:

Type Description
DataFrame

Processed and normalized DataFrame.

Source code in gigaspatial/core/schemas/wireless_access_service.py
def process(self, df: pd.DataFrame, **kwargs) -> pd.DataFrame:
    """
    Execute the complete WirelessAccessService normalization pipeline.

    Args:
        df: Raw wireless access service DataFrame.
        **kwargs: Additional processing arguments.

    Returns:
        Processed and normalized DataFrame.
    """
    df = super().process(df, **kwargs)
    df = self._normalize_status_from_boolean(df)
    df = self._normalize_enum_columns(df)
    df = self._normalize_access_technologies(df)
    df = self._normalize_frequency_bands_mhz(df)
    df = self._normalize_sector_count(df)
    df = self._coerce_invalid_nonnegative_values(df)
    return df
WirelessAccessServiceTable

Bases: EntityTable[WirelessAccessService]

Container for WirelessAccessService entities and service-level operations.

Source code in gigaspatial/core/schemas/wireless_access_service.py
class WirelessAccessServiceTable(EntityTable[WirelessAccessService]):
    """Container for WirelessAccessService entities and service-level operations."""

    @classmethod
    def from_file(
        cls,
        file_path: Union[str, Path],
        data_store: Optional[DataStore] = None,
        clean: bool = True,
        processor: Optional[
            Union[Type[EntityProcessor], EntityProcessor]
        ] = WirelessAccessServiceProcessor,
        **kwargs,
    ) -> "WirelessAccessServiceTable":
        """
        Create a WirelessAccessServiceTable from a source file.

        Args:
            file_path: Path to the source dataset.
            data_store: Optional DataStore used to access the file.
            clean: Whether to process records before validation.
            processor: Processor class or instance used for normalization.
            **kwargs: Additional loading or processing options.

        Returns:
            Validated WirelessAccessServiceTable.
        """
        return super().from_file(
            file_path=file_path,
            entity_class=WirelessAccessService,
            data_store=data_store,
            clean=clean,
            processor=processor,
            **kwargs,
        )

    @classmethod
    def from_dataframe(
        cls,
        df: pd.DataFrame,
        entity_class: Type[WirelessAccessService] = WirelessAccessService,
        clean: bool = False,
        processor: Optional[
            Union[Type[EntityProcessor], EntityProcessor]
        ] = WirelessAccessServiceProcessor,
        **kwargs,
    ) -> "WirelessAccessServiceTable":
        """
        Create a WirelessAccessServiceTable from an existing DataFrame.

        Args:
            df: DataFrame containing service records.
            entity_class: Entity schema used for validation.
            clean: Whether to process records before validation.
            processor: Processor class or instance used for normalization.
            **kwargs: Additional processing options.

        Returns:
            Validated WirelessAccessServiceTable.
        """
        return super().from_dataframe(
            df=df,
            entity_class=entity_class,
            clean=clean,
            processor=processor,
            **kwargs,
        )

    @classmethod
    def from_files(
        cls,
        file_paths: List[Union[str, Path]],
        data_store: Optional[DataStore] = None,
        clean: bool = True,
        processor: Optional[
            Union[Type[EntityProcessor], EntityProcessor]
        ] = WirelessAccessServiceProcessor,
        **kwargs,
    ) -> "WirelessAccessServiceTable":
        """
        Load and combine several source files into one table.

        Args:
            file_paths: Paths to source datasets.
            data_store: Optional DataStore used to access files.
            clean: Whether to process records before validation.
            processor: Processor class or instance used for normalization.
            **kwargs: Additional loading or processing options.

        Returns:
            Merged and validated WirelessAccessServiceTable.
        """
        return super().from_files(
            file_paths=file_paths,
            entity_class=WirelessAccessService,
            data_store=data_store,
            clean=clean,
            processor=processor,
            **kwargs,
        )

    def filter_by_wireless_site(
        self,
        wireless_site_id: str,
    ) -> "WirelessAccessServiceTable":
        """
        Return access services hosted by a given wireless site.

        Args:
            wireless_site_id: Canonical WirelessSite identifier.

        Returns:
            Table containing services at the requested site.
        """
        return self.__class__(
            entities=[
                entity
                for entity in self.entities
                if entity.wireless_site_id == wireless_site_id
            ]
        )

    def filter_by_service_type(
        self,
        service_type: Union[WirelessAccessServiceType, str],
    ) -> "WirelessAccessServiceTable":
        """
        Filter access services by service role.

        Args:
            service_type: Access-service enum or its canonical string value.

        Returns:
            Table containing matching services.
        """
        value = (
            WirelessAccessServiceType(service_type).value
            if isinstance(service_type, str)
            else service_type.value
        )
        return self.__class__(
            entities=[
                entity
                for entity in self.entities
                if entity.access_service_type == value
            ]
        )

    def filter_by_status(
        self,
        status: Union[InfrastructureStatus, str],
    ) -> "WirelessAccessServiceTable":
        """
        Filter access services by operational status.

        Args:
            status: InfrastructureStatus enum or canonical string value.

        Returns:
            Table containing matching services.
        """
        value = (
            InfrastructureStatus(status).value
            if isinstance(status, str)
            else status.value
        )
        return self.__class__(
            entities=[
                entity for entity in self.entities if entity.service_status == value
            ]
        )

    def filter_operational(self) -> "WirelessAccessServiceTable":
        """
        Return only operational access services.

        Returns:
            Table containing operational services.
        """
        return self.filter_by_status(InfrastructureStatus.OPERATIONAL)

    def filter_by_technology(
        self,
        technology: Union[WirelessAccessTechnology, str],
    ) -> "WirelessAccessServiceTable":
        """
        Filter services that use a requested access technology.

        Args:
            technology: Access technology enum or canonical string value.

        Returns:
            Table containing matching services.
        """
        value = (
            WirelessAccessTechnology(technology).value
            if isinstance(technology, str)
            else technology.value
        )
        return self.__class__(
            entities=[
                entity
                for entity in self.entities
                if entity.access_technologies and value in entity.access_technologies
            ]
        )

    def filter_by_provider(
        self,
        provider: str,
    ) -> "WirelessAccessServiceTable":
        """
        Filter services by the operating network provider.

        Args:
            provider: Exact provider name to match.

        Returns:
            Table containing matching services.
        """
        return self.__class__(
            entities=[
                entity
                for entity in self.entities
                if entity.network_provider == provider
            ]
        )

    def filter_by_min_capacity(
        self,
        min_mbps: float,
        capacity_type: str = "equipped",
    ) -> "WirelessAccessServiceTable":
        """
        Filter services by a capacity threshold.

        Args:
            min_mbps: Minimum capacity in Mbps.
            capacity_type: One of ``equipped``, ``potential``, or ``available``.

        Returns:
            Table containing matching services.

        Raises:
            ValueError: If min_mbps is negative or capacity_type is unsupported.
        """
        if min_mbps < 0:
            raise ValueError("min_mbps must be greater than or equal to zero.")

        field_map = {
            "equipped": "equipped_capacity_mbps",
            "potential": "potential_capacity_mbps",
            "available": "available_capacity_mbps",
        }

        if capacity_type not in field_map:
            raise ValueError(
                f"Unsupported capacity_type '{capacity_type}'. "
                f"Expected one of: {list(field_map)}."
            )

        field_name = field_map[capacity_type]
        return self.__class__(
            entities=[
                entity
                for entity in self.entities
                if getattr(entity, field_name) is not None
                and getattr(entity, field_name) >= min_mbps
            ]
        )

    def get_wireless_site_ids(self) -> Set[str]:
        """
        Return the wireless-site identifiers referenced by this table.

        Returns:
            Set of WirelessSite identifiers.
        """
        return {entity.wireless_site_id for entity in self.entities}

    def get_network_providers(self) -> Set[str]:
        """
        Return the service operators represented in the table.

        Returns:
            Set of non-null network-provider names.
        """
        return {
            entity.network_provider
            for entity in self.entities
            if entity.network_provider is not None
        }

    def get_access_technologies(self) -> Set[str]:
        """
        Return the access technologies represented in the table.

        Returns:
            Set of canonical WirelessAccessTechnology values.
        """
        return {
            technology
            for entity in self.entities
            if entity.access_technologies
            for technology in entity.access_technologies
        }

    def group_by_wireless_site(self) -> Dict[str, "WirelessAccessServiceTable"]:
        """
        Group access services by their parent WirelessSite identifier.

        Returns:
            Mapping from wireless-site ID to its service table.
        """
        groups: Dict[str, List[WirelessAccessService]] = {}

        for entity in self.entities:
            groups.setdefault(entity.wireless_site_id, []).append(entity)

        return {
            wireless_site_id: self.__class__(entities=entities)
            for wireless_site_id, entities in groups.items()
        }
filter_by_min_capacity(min_mbps, capacity_type='equipped')

Filter services by a capacity threshold.

Parameters:

Name Type Description Default
min_mbps float

Minimum capacity in Mbps.

required
capacity_type str

One of equipped, potential, or available.

'equipped'

Returns:

Type Description
WirelessAccessServiceTable

Table containing matching services.

Raises:

Type Description
ValueError

If min_mbps is negative or capacity_type is unsupported.

Source code in gigaspatial/core/schemas/wireless_access_service.py
def filter_by_min_capacity(
    self,
    min_mbps: float,
    capacity_type: str = "equipped",
) -> "WirelessAccessServiceTable":
    """
    Filter services by a capacity threshold.

    Args:
        min_mbps: Minimum capacity in Mbps.
        capacity_type: One of ``equipped``, ``potential``, or ``available``.

    Returns:
        Table containing matching services.

    Raises:
        ValueError: If min_mbps is negative or capacity_type is unsupported.
    """
    if min_mbps < 0:
        raise ValueError("min_mbps must be greater than or equal to zero.")

    field_map = {
        "equipped": "equipped_capacity_mbps",
        "potential": "potential_capacity_mbps",
        "available": "available_capacity_mbps",
    }

    if capacity_type not in field_map:
        raise ValueError(
            f"Unsupported capacity_type '{capacity_type}'. "
            f"Expected one of: {list(field_map)}."
        )

    field_name = field_map[capacity_type]
    return self.__class__(
        entities=[
            entity
            for entity in self.entities
            if getattr(entity, field_name) is not None
            and getattr(entity, field_name) >= min_mbps
        ]
    )
filter_by_provider(provider)

Filter services by the operating network provider.

Parameters:

Name Type Description Default
provider str

Exact provider name to match.

required

Returns:

Type Description
WirelessAccessServiceTable

Table containing matching services.

Source code in gigaspatial/core/schemas/wireless_access_service.py
def filter_by_provider(
    self,
    provider: str,
) -> "WirelessAccessServiceTable":
    """
    Filter services by the operating network provider.

    Args:
        provider: Exact provider name to match.

    Returns:
        Table containing matching services.
    """
    return self.__class__(
        entities=[
            entity
            for entity in self.entities
            if entity.network_provider == provider
        ]
    )
filter_by_service_type(service_type)

Filter access services by service role.

Parameters:

Name Type Description Default
service_type Union[WirelessAccessServiceType, str]

Access-service enum or its canonical string value.

required

Returns:

Type Description
WirelessAccessServiceTable

Table containing matching services.

Source code in gigaspatial/core/schemas/wireless_access_service.py
def filter_by_service_type(
    self,
    service_type: Union[WirelessAccessServiceType, str],
) -> "WirelessAccessServiceTable":
    """
    Filter access services by service role.

    Args:
        service_type: Access-service enum or its canonical string value.

    Returns:
        Table containing matching services.
    """
    value = (
        WirelessAccessServiceType(service_type).value
        if isinstance(service_type, str)
        else service_type.value
    )
    return self.__class__(
        entities=[
            entity
            for entity in self.entities
            if entity.access_service_type == value
        ]
    )
filter_by_status(status)

Filter access services by operational status.

Parameters:

Name Type Description Default
status Union[InfrastructureStatus, str]

InfrastructureStatus enum or canonical string value.

required

Returns:

Type Description
WirelessAccessServiceTable

Table containing matching services.

Source code in gigaspatial/core/schemas/wireless_access_service.py
def filter_by_status(
    self,
    status: Union[InfrastructureStatus, str],
) -> "WirelessAccessServiceTable":
    """
    Filter access services by operational status.

    Args:
        status: InfrastructureStatus enum or canonical string value.

    Returns:
        Table containing matching services.
    """
    value = (
        InfrastructureStatus(status).value
        if isinstance(status, str)
        else status.value
    )
    return self.__class__(
        entities=[
            entity for entity in self.entities if entity.service_status == value
        ]
    )
filter_by_technology(technology)

Filter services that use a requested access technology.

Parameters:

Name Type Description Default
technology Union[WirelessAccessTechnology, str]

Access technology enum or canonical string value.

required

Returns:

Type Description
WirelessAccessServiceTable

Table containing matching services.

Source code in gigaspatial/core/schemas/wireless_access_service.py
def filter_by_technology(
    self,
    technology: Union[WirelessAccessTechnology, str],
) -> "WirelessAccessServiceTable":
    """
    Filter services that use a requested access technology.

    Args:
        technology: Access technology enum or canonical string value.

    Returns:
        Table containing matching services.
    """
    value = (
        WirelessAccessTechnology(technology).value
        if isinstance(technology, str)
        else technology.value
    )
    return self.__class__(
        entities=[
            entity
            for entity in self.entities
            if entity.access_technologies and value in entity.access_technologies
        ]
    )
filter_by_wireless_site(wireless_site_id)

Return access services hosted by a given wireless site.

Parameters:

Name Type Description Default
wireless_site_id str

Canonical WirelessSite identifier.

required

Returns:

Type Description
WirelessAccessServiceTable

Table containing services at the requested site.

Source code in gigaspatial/core/schemas/wireless_access_service.py
def filter_by_wireless_site(
    self,
    wireless_site_id: str,
) -> "WirelessAccessServiceTable":
    """
    Return access services hosted by a given wireless site.

    Args:
        wireless_site_id: Canonical WirelessSite identifier.

    Returns:
        Table containing services at the requested site.
    """
    return self.__class__(
        entities=[
            entity
            for entity in self.entities
            if entity.wireless_site_id == wireless_site_id
        ]
    )
filter_operational()

Return only operational access services.

Returns:

Type Description
WirelessAccessServiceTable

Table containing operational services.

Source code in gigaspatial/core/schemas/wireless_access_service.py
def filter_operational(self) -> "WirelessAccessServiceTable":
    """
    Return only operational access services.

    Returns:
        Table containing operational services.
    """
    return self.filter_by_status(InfrastructureStatus.OPERATIONAL)
from_dataframe(df, entity_class=WirelessAccessService, clean=False, processor=WirelessAccessServiceProcessor, **kwargs) classmethod

Create a WirelessAccessServiceTable from an existing DataFrame.

Parameters:

Name Type Description Default
df DataFrame

DataFrame containing service records.

required
entity_class Type[WirelessAccessService]

Entity schema used for validation.

WirelessAccessService
clean bool

Whether to process records before validation.

False
processor Optional[Union[Type[EntityProcessor], EntityProcessor]]

Processor class or instance used for normalization.

WirelessAccessServiceProcessor
**kwargs

Additional processing options.

{}

Returns:

Type Description
WirelessAccessServiceTable

Validated WirelessAccessServiceTable.

Source code in gigaspatial/core/schemas/wireless_access_service.py
@classmethod
def from_dataframe(
    cls,
    df: pd.DataFrame,
    entity_class: Type[WirelessAccessService] = WirelessAccessService,
    clean: bool = False,
    processor: Optional[
        Union[Type[EntityProcessor], EntityProcessor]
    ] = WirelessAccessServiceProcessor,
    **kwargs,
) -> "WirelessAccessServiceTable":
    """
    Create a WirelessAccessServiceTable from an existing DataFrame.

    Args:
        df: DataFrame containing service records.
        entity_class: Entity schema used for validation.
        clean: Whether to process records before validation.
        processor: Processor class or instance used for normalization.
        **kwargs: Additional processing options.

    Returns:
        Validated WirelessAccessServiceTable.
    """
    return super().from_dataframe(
        df=df,
        entity_class=entity_class,
        clean=clean,
        processor=processor,
        **kwargs,
    )
from_file(file_path, data_store=None, clean=True, processor=WirelessAccessServiceProcessor, **kwargs) classmethod

Create a WirelessAccessServiceTable from a source file.

Parameters:

Name Type Description Default
file_path Union[str, Path]

Path to the source dataset.

required
data_store Optional[DataStore]

Optional DataStore used to access the file.

None
clean bool

Whether to process records before validation.

True
processor Optional[Union[Type[EntityProcessor], EntityProcessor]]

Processor class or instance used for normalization.

WirelessAccessServiceProcessor
**kwargs

Additional loading or processing options.

{}

Returns:

Type Description
WirelessAccessServiceTable

Validated WirelessAccessServiceTable.

Source code in gigaspatial/core/schemas/wireless_access_service.py
@classmethod
def from_file(
    cls,
    file_path: Union[str, Path],
    data_store: Optional[DataStore] = None,
    clean: bool = True,
    processor: Optional[
        Union[Type[EntityProcessor], EntityProcessor]
    ] = WirelessAccessServiceProcessor,
    **kwargs,
) -> "WirelessAccessServiceTable":
    """
    Create a WirelessAccessServiceTable from a source file.

    Args:
        file_path: Path to the source dataset.
        data_store: Optional DataStore used to access the file.
        clean: Whether to process records before validation.
        processor: Processor class or instance used for normalization.
        **kwargs: Additional loading or processing options.

    Returns:
        Validated WirelessAccessServiceTable.
    """
    return super().from_file(
        file_path=file_path,
        entity_class=WirelessAccessService,
        data_store=data_store,
        clean=clean,
        processor=processor,
        **kwargs,
    )
from_files(file_paths, data_store=None, clean=True, processor=WirelessAccessServiceProcessor, **kwargs) classmethod

Load and combine several source files into one table.

Parameters:

Name Type Description Default
file_paths List[Union[str, Path]]

Paths to source datasets.

required
data_store Optional[DataStore]

Optional DataStore used to access files.

None
clean bool

Whether to process records before validation.

True
processor Optional[Union[Type[EntityProcessor], EntityProcessor]]

Processor class or instance used for normalization.

WirelessAccessServiceProcessor
**kwargs

Additional loading or processing options.

{}

Returns:

Type Description
WirelessAccessServiceTable

Merged and validated WirelessAccessServiceTable.

Source code in gigaspatial/core/schemas/wireless_access_service.py
@classmethod
def from_files(
    cls,
    file_paths: List[Union[str, Path]],
    data_store: Optional[DataStore] = None,
    clean: bool = True,
    processor: Optional[
        Union[Type[EntityProcessor], EntityProcessor]
    ] = WirelessAccessServiceProcessor,
    **kwargs,
) -> "WirelessAccessServiceTable":
    """
    Load and combine several source files into one table.

    Args:
        file_paths: Paths to source datasets.
        data_store: Optional DataStore used to access files.
        clean: Whether to process records before validation.
        processor: Processor class or instance used for normalization.
        **kwargs: Additional loading or processing options.

    Returns:
        Merged and validated WirelessAccessServiceTable.
    """
    return super().from_files(
        file_paths=file_paths,
        entity_class=WirelessAccessService,
        data_store=data_store,
        clean=clean,
        processor=processor,
        **kwargs,
    )
get_access_technologies()

Return the access technologies represented in the table.

Returns:

Type Description
Set[str]

Set of canonical WirelessAccessTechnology values.

Source code in gigaspatial/core/schemas/wireless_access_service.py
def get_access_technologies(self) -> Set[str]:
    """
    Return the access technologies represented in the table.

    Returns:
        Set of canonical WirelessAccessTechnology values.
    """
    return {
        technology
        for entity in self.entities
        if entity.access_technologies
        for technology in entity.access_technologies
    }
get_network_providers()

Return the service operators represented in the table.

Returns:

Type Description
Set[str]

Set of non-null network-provider names.

Source code in gigaspatial/core/schemas/wireless_access_service.py
def get_network_providers(self) -> Set[str]:
    """
    Return the service operators represented in the table.

    Returns:
        Set of non-null network-provider names.
    """
    return {
        entity.network_provider
        for entity in self.entities
        if entity.network_provider is not None
    }
get_wireless_site_ids()

Return the wireless-site identifiers referenced by this table.

Returns:

Type Description
Set[str]

Set of WirelessSite identifiers.

Source code in gigaspatial/core/schemas/wireless_access_service.py
def get_wireless_site_ids(self) -> Set[str]:
    """
    Return the wireless-site identifiers referenced by this table.

    Returns:
        Set of WirelessSite identifiers.
    """
    return {entity.wireless_site_id for entity in self.entities}
group_by_wireless_site()

Group access services by their parent WirelessSite identifier.

Returns:

Type Description
Dict[str, WirelessAccessServiceTable]

Mapping from wireless-site ID to its service table.

Source code in gigaspatial/core/schemas/wireless_access_service.py
def group_by_wireless_site(self) -> Dict[str, "WirelessAccessServiceTable"]:
    """
    Group access services by their parent WirelessSite identifier.

    Returns:
        Mapping from wireless-site ID to its service table.
    """
    groups: Dict[str, List[WirelessAccessService]] = {}

    for entity in self.entities:
        groups.setdefault(entity.wireless_site_id, []).append(entity)

    return {
        wireless_site_id: self.__class__(entities=entities)
        for wireless_site_id, entities in groups.items()
    }

wireless_site

Module for wireless site schema and processing.

Defines the WirelessSite entity, representing a physical location that hosts one or more wireless installations. A wireless site can be a tower, rooftop, mast, pole, relay, hub, or customer-premises location. It may host mobile and fixed-wireless equipment concurrently.

This entity represents the shared physical-site inventory. Radio technologies, spectrum, antenna configuration, sectors, and access-service capacity belong to service- or installation-level schemas rather than this site-level schema.

BackhaulType

Bases: str, Enum

Enum for different backhaul types.

Source code in gigaspatial/core/schemas/wireless_site.py
class BackhaulType(str, Enum):
    """Enum for different backhaul types."""

    FIBER = "fiber"
    MICROWAVE = "microwave"
    CELLULAR = "cellular"
    SATELLITE = "satellite"
WirelessSite

Bases: GigaEntity

Represents a physical site that hosts one or more wireless installations.

A site may support mobile and fixed-wireless access simultaneously. The entity models shared physical infrastructure, power, providers, backhaul, and optional upstream transport relationships; it does not model a customer-specific connection or an individual radio installation.

Source code in gigaspatial/core/schemas/wireless_site.py
class WirelessSite(GigaEntity):
    """
    Represents a physical site that hosts one or more wireless installations.

    A site may support mobile and fixed-wireless access simultaneously. The
    entity models shared physical infrastructure, power, providers, backhaul,
    and optional upstream transport relationships; it does not model a
    customer-specific connection or an individual radio installation.
    """

    model_config = ENUM_ENTITY_CONFIG

    # Identity
    wireless_site_id: str = Field(
        ...,
        max_length=50,
        description="Unique identifier for the wireless site",
    )
    wireless_site_id_source: Optional[str] = Field(
        None,
        max_length=100,
        description="Original site identifier in the source system",
    )
    site_name: Optional[str] = Field(
        None,
        max_length=150,
        description="Common, operational, or published name of the site",
    )

    # Classification
    site_type: Optional[WirelessSiteType] = Field(
        None,
        description="Physical type of site hosting wireless installations",
    )
    site_status: Optional[InfrastructureStatus] = Field(
        None,
        description="Current operational status of the wireless site",
    )
    access_service_types: Optional[Set[WirelessAccessServiceType]] = Field(
        None,
        description=(
            "Access-service roles known to be supported from the site, aggregated "
            "from linked WirelessAccessService records or reported directly. Does "
            "not confirm serviceability at a particular point of interest."
        ),
    )
    radio_types: Optional[Set[RadioType]] = Field(
        None,
        description=(
            "Observed or reported mobile generations hosted at the site, aggregated "
            "from linked WirelessAccessService records or reported directly. This is "
            "a site-level summary, not a per-service configuration."
        ),
    )
    access_technologies: Optional[Set[WirelessAccessTechnology]] = Field(
        None,
        description=(
            "Observed or reported wireless access technologies hosted at the site, "
            "aggregated from linked WirelessAccessService records or reported "
            "directly. This is a site-level summary, not a per-service configuration."
        ),
    )

    # Physical characteristics
    structure_height_meters: Optional[float] = Field(
        None,
        ge=0,
        description=(
            "Height above ground of the supporting tower, mast, pole, or other "
            "freestanding structure, in meters; not applicable to rooftop-only sites."
        ),
    )

    highest_antenna_height_agl_meters: Optional[float] = Field(
        None,
        ge=0,
        description=(
            "Highest known hosted antenna height above ground level (AGL), in "
            "meters; a site-level summary across hosted installations."
        ),
    )
    elevation_meters: Optional[float] = Field(
        None,
        description="Site elevation above mean sea level in meters",
    )
    power_source: Optional[PowerSource] = Field(
        None,
        description="Primary power source serving the wireless site",
    )
    backup_power_hours: Optional[float] = Field(
        None,
        ge=0,
        description="Estimated backup power autonomy in hours",
    )

    # Transport and backhaul
    backhaul_type: Optional[BackhaulType] = Field(
        None,
        description="Primary known backhaul medium serving the site",
    )
    backhaul_capacity_mbps: Optional[float] = Field(
        None,
        ge=0,
        description="Known aggregate backhaul capacity serving the site in Mbps",
    )
    upstream_transmission_node_id: Optional[str] = Field(
        None,
        max_length=50,
        description=(
            "Identifier of the known upstream TransmissionNode providing transport "
            "or aggregation connectivity"
        ),
    )

    # Ownership and operations
    physical_infrastructure_provider: Optional[str] = Field(
        None,
        max_length=100,
        description=(
            "Organization that owns, hosts, or provides the passive physical "
            "infrastructure or site"
        ),
    )
    network_providers: Optional[List[str]] = Field(
        None,
        description=(
            "Network operators, mobile network operators, or internet service "
            "providers operating active equipment at the site"
        ),
    )

    # Data quality and lifecycle
    installation_date: Optional[str] = Field(
        None,
        description="ISO 8601 date when the site became operational",
    )
    decommission_date: Optional[str] = Field(
        None,
        description="ISO 8601 date when the site was decommissioned",
    )
    last_verified_date: Optional[str] = Field(
        None,
        description="ISO 8601 date when the site information was last verified",
    )
    data_confidence: Optional[DataConfidence] = Field(
        None,
        description="Confidence level assigned to the source or observed data",
    )

    @property
    def id(self) -> str:
        """Alias for wireless_site_id."""
        return self.wireless_site_id
id: str property

Alias for wireless_site_id.

WirelessSiteProcessor

Bases: EntityProcessor

Processor for cleaning and normalizing WirelessSite data.

The processor normalizes source aliases for site types, lifecycle status, power, backhaul, and provider lists before Pydantic validation.

Source code in gigaspatial/core/schemas/wireless_site.py
class WirelessSiteProcessor(EntityProcessor):
    """
    Processor for cleaning and normalizing WirelessSite data.

    The processor normalizes source aliases for site types, lifecycle status,
    power, backhaul, and provider lists before Pydantic validation.
    """

    NUMERIC_COLUMNS: ClassVar[List[str]] = [
        *EntityProcessor.NUMERIC_COLUMNS,
        "structure_height_meters",
        "highest_antenna_height_agl_meters",
        "elevation_meters",
        "backup_power_hours",
        "backhaul_capacity_mbps",
    ]

    LOWERCASE_COLUMNS: ClassVar[List[str]] = [
        "site_type",
        "site_status",
        "backhaul_type",
        "power_source",
        "data_confidence",
    ]

    COLUMN_ALIASES: ClassVar[Dict[str, str]] = {
        "id": "wireless_site_id",
        "site_id": "wireless_site_id",
        "tower_id": "wireless_site_id",
        "cell_tower_id": "wireless_site_id",
        "fixed_wireless_site_id": "wireless_site_id",
        "source_id": "wireless_site_id_source",
        "tower_id_source": "wireless_site_id_source",
        "cell_tower_id_source": "wireless_site_id_source",
        "fixed_wireless_site_id_source": "wireless_site_id_source",
        "site": "site_name",
        "tower_name": "site_name",
        "site_category": "site_type",
        "tower_type": "site_type",
        "operator": "network_providers",
        "operator_name": "network_providers",
        "network_operator": "network_providers",
        "provider": "network_providers",
        "backhaul": "backhaul_type",
        "backhaul_capacity": "backhaul_capacity_mbps",
        "transmission_node_id": "upstream_transmission_node_id",
        "height": "structure_height_meters",
        "tower_height": "structure_height_meters",
        "structure_height": "structure_height_meters",
        "max_antenna_height": "highest_antenna_height_agl_meters",
        "highest_antenna_height": "highest_antenna_height_agl_meters",
        "antenna_height": "highest_antenna_height_agl_meters",
        "antenna_height_agl": "highest_antenna_height_agl_meters",
        "service_type": "access_service_types",
        "service_types": "access_service_types",
        "access_service_type": "access_service_types",
        "access_service_types": "access_service_types",
        "technology": "access_technologies",
        "technologies": "access_technologies",
        "radio_technology": "access_technologies",
        "radio_technologies": "access_technologies",
        "radio_type": "radio_types",
        "radio_types": "radio_types",
        "generation": "radio_types",
        "network_generation": "radio_types",
    }

    SITE_TYPE_ALIAS_MAP: ClassVar[Dict[str, str]] = {
        "tower": "tower",
        "cell tower": "tower",
        "tower site": "tower",
        "macro tower": "tower",
        "rooftop": "rooftop",
        "roof top": "rooftop",
        "roof-top": "rooftop",
        "mast": "mast",
        "pole": "pole",
        "utility pole": "pole",
        "street pole": "pole",
        "relay": "relay",
        "repeater": "relay",
        "hub": "hub",
        "pop": "hub",
        "point of presence": "hub",
        "customer premises": "customer_premises",
        "customer premise": "customer_premises",
        "cpe": "customer_premises",
    }

    STATUS_ALIAS_MAP: ClassVar[Dict[str, str]] = {
        "under construction": "underconstruction",
        "under_construction": "underconstruction",
        "in service": "operational",
        "active": "operational",
        "retired": "decommissioned",
    }

    def process(self, df: pd.DataFrame, **kwargs) -> pd.DataFrame:
        """Execute the complete WirelessSite normalization pipeline."""
        df = super().process(df, **kwargs)
        df = self._normalize_enum_columns(df)
        df = self._normalize_provider_lists(df)
        df = self._normalize_access_service_types(df)
        df = self._normalize_radio_types(df)
        df = self._normalize_access_technologies(df)
        df = self._derive_radio_types(df)
        df = self._coerce_invalid_nonnegative_values(df)
        return df

    def _normalize_enum_columns(self, df: pd.DataFrame) -> pd.DataFrame:
        """
        Normalize enum-backed source fields to canonical values.

        Args:
            df: DataFrame to process.

        Returns:
            DataFrame with normalized enum-backed columns.
        """
        enum_columns = (
            (
                "site_type",
                self.SITE_TYPE_ALIAS_MAP,
                {site_type.value for site_type in WirelessSiteType},
            ),
            (
                "site_status",
                self.STATUS_ALIAS_MAP,
                {status.value for status in InfrastructureStatus},
            ),
            (
                "backhaul_type",
                BACKHAUL_ALIAS_MAP,
                {backhaul_type.value for backhaul_type in BackhaulType},
            ),
        )

        for column, alias_map, valid_values in enum_columns:
            df = self._normalize_enum_column(
                df,
                column=column,
                alias_map=alias_map,
                valid_values=valid_values,
                required=False,
            )

        return df

    def _normalize_provider_lists(self, df: pd.DataFrame) -> pd.DataFrame:
        """
        Convert provider fields into de-duplicated lists.

        Args:
            df: DataFrame to process.

        Returns:
            DataFrame with normalized network_providers values.
        """
        if "network_providers" not in df.columns:
            return df

        def normalize(value: object) -> Optional[List[str]]:
            if value is None or (isinstance(value, float) and pd.isna(value)):
                return None

            if isinstance(value, str):
                values = value.replace(";", ",").split(",")
            elif isinstance(value, (list, tuple, set)):
                values = value
            else:
                return None

            normalized = [
                str(item).strip()
                for item in values
                if item is not None and str(item).strip()
            ]
            return list(dict.fromkeys(normalized)) or None

        df["network_providers"] = df["network_providers"].apply(normalize)
        return df

    def _coerce_invalid_nonnegative_values(
        self,
        df: pd.DataFrame,
    ) -> pd.DataFrame:
        """
        Replace negative values for nonnegative fields with null values.

        This prevents an otherwise valid record from failing Pydantic validation
        because of one invalid source measurement.

        Args:
            df: DataFrame to process.

        Returns:
            DataFrame with invalid negative values coerced to null.
        """
        fields = (
            "structure_height_meters",
            "highest_antenna_height_agl_meters",
            "backup_power_hours",
            "backhaul_capacity_mbps",
        )

        for column in fields:
            if column in df.columns:
                df.loc[df[column] < 0, column] = None

        return df

    def _normalize_radio_types(self, df: pd.DataFrame) -> pd.DataFrame:
        """Normalize reported 2G–5G values to canonical RadioType values."""
        return self._normalize_enum_collection_column(
            df=df,
            column="radio_types",
            alias_map=RADIO_ALIAS_MAP,
            valid_values={radio_type.value for radio_type in RadioType},
        )

    def _derive_radio_types(self, df: pd.DataFrame) -> pd.DataFrame:
        """
        Derive and merge 2G–5G summaries from detailed access technologies.

        Existing normalized radio_types are preserved. Wi-Fi, WiMAX, proprietary,
        and other technologies do not map to RadioType and are excluded.
        """
        if "access_technologies" not in df.columns:
            return df

        def derive(technologies: object) -> List[str]:
            if not technologies:
                return []

            derived: List[str] = []

            for technology in technologies:
                technology_enum = WirelessAccessTechnology(technology)
                radio_type = WIRELESS_ACCESS_TECHNOLOGY_TO_RADIO_TYPE.get(
                    technology_enum
                )
                if radio_type is not None:
                    derived.append(radio_type.value)

            return list(dict.fromkeys(derived))

        derived_radio_types = df["access_technologies"].apply(derive)

        if "radio_types" not in df.columns:
            df["radio_types"] = derived_radio_types.apply(lambda values: values or None)
            return df

        def merge(
            reported: object,
            derived: List[str],
        ) -> Optional[List[str]]:
            """Merge reported and derived radio types without duplicate values."""
            if reported is None or (isinstance(reported, float) and pd.isna(reported)):
                reported_values: List[str] = []
            else:
                reported_values = list(reported)

            values = [*reported_values, *derived]
            return list(dict.fromkeys(values)) or None

        df["radio_types"] = [
            merge(reported, derived)
            for reported, derived in zip(df["radio_types"], derived_radio_types)
        ]
        return df

    def _normalize_access_service_types(
        self,
        df: pd.DataFrame,
    ) -> pd.DataFrame:
        """Normalize site-level wireless access-service roles."""
        return self._normalize_enum_collection_column(
            df=df,
            column="access_service_types",
            alias_map=ACCESS_SERVICE_TYPE_ALIAS_MAP,
            valid_values={
                service_type.value for service_type in WirelessAccessServiceType
            },
        )

    def _normalize_access_technologies(
        self,
        df: pd.DataFrame,
    ) -> pd.DataFrame:
        """Normalize site-level detailed wireless access technologies."""
        return self._normalize_enum_collection_column(
            df=df,
            column="access_technologies",
            alias_map=WIRELESS_ACCESS_TECHNOLOGY_ALIAS_MAP,
            valid_values={technology.value for technology in WirelessAccessTechnology},
        )
process(df, **kwargs)

Execute the complete WirelessSite normalization pipeline.

Source code in gigaspatial/core/schemas/wireless_site.py
def process(self, df: pd.DataFrame, **kwargs) -> pd.DataFrame:
    """Execute the complete WirelessSite normalization pipeline."""
    df = super().process(df, **kwargs)
    df = self._normalize_enum_columns(df)
    df = self._normalize_provider_lists(df)
    df = self._normalize_access_service_types(df)
    df = self._normalize_radio_types(df)
    df = self._normalize_access_technologies(df)
    df = self._derive_radio_types(df)
    df = self._coerce_invalid_nonnegative_values(df)
    return df
WirelessSiteTable

Bases: EntityTable[WirelessSite]

Container for WirelessSite entities and site-level operations.

Source code in gigaspatial/core/schemas/wireless_site.py
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
class WirelessSiteTable(EntityTable[WirelessSite]):
    """Container for WirelessSite entities and site-level operations."""

    @classmethod
    def from_file(
        cls,
        file_path: Union[str, Path],
        data_store: Optional[DataStore] = None,
        clean: bool = True,
        processor: Optional[
            Union[Type[EntityProcessor], EntityProcessor]
        ] = WirelessSiteProcessor,
        **kwargs,
    ) -> "WirelessSiteTable":
        """
        Create a WirelessSiteTable from a file.

        Args:
            file_path: Path to the source dataset.
            data_store: Optional data-store implementation.
            clean: Whether to run the configured processor before validation.
            processor: Processor class or instance to use.
            **kwargs: Additional loading and processing options.

        Returns:
            Validated WirelessSiteTable.
        """
        return super().from_file(
            file_path=file_path,
            entity_class=WirelessSite,
            data_store=data_store,
            clean=clean,
            processor=processor,
            **kwargs,
        )

    @classmethod
    def from_dataframe(
        cls,
        df: pd.DataFrame,
        entity_class: Type[WirelessSite] = WirelessSite,
        clean: bool = False,
        processor: Optional[
            Union[Type[EntityProcessor], EntityProcessor]
        ] = WirelessSiteProcessor,
        **kwargs,
    ) -> "WirelessSiteTable":
        """
        Create a WirelessSiteTable from an existing DataFrame.

        Args:
            df: DataFrame containing wireless-site records.
            entity_class: Entity schema used to validate the records.
            clean: Whether to run the configured processor before validation.
            processor: Processor class or instance to use.
            **kwargs: Additional processing options.

        Returns:
            Validated WirelessSiteTable.
        """
        return super().from_dataframe(
            df=df,
            entity_class=entity_class,
            clean=clean,
            processor=processor,
            **kwargs,
        )

    @classmethod
    def from_files(
        cls,
        file_paths: List[Union[str, Path]],
        data_store: Optional[DataStore] = None,
        clean: bool = True,
        processor: Optional[
            Union[Type[EntityProcessor], EntityProcessor]
        ] = WirelessSiteProcessor,
        **kwargs,
    ) -> "WirelessSiteTable":
        """
        Load, process, and merge several source files into one table.

        Args:
            file_paths: Paths to the source datasets.
            data_store: Optional data-store implementation.
            clean: Whether to run the configured processor before validation.
            processor: Processor class or instance to use.
            **kwargs: Additional loading and processing options.

        Returns:
            Merged and validated WirelessSiteTable.
        """
        return super().from_files(
            file_paths=file_paths,
            entity_class=WirelessSite,
            data_store=data_store,
            clean=clean,
            processor=processor,
            **kwargs,
        )

    def filter_by_site_type(
        self,
        site_type: Union[WirelessSiteType, str],
    ) -> "WirelessSiteTable":
        """
        Filter sites by physical type.

        Args:
            site_type: WirelessSiteType or its canonical string value.

        Returns:
            Table containing matching sites.
        """
        value = (
            WirelessSiteType(site_type).value
            if isinstance(site_type, str)
            else site_type.value
        )
        return self.__class__(
            entities=[entity for entity in self.entities if entity.site_type == value]
        )

    def filter_by_status(
        self,
        status: Union[InfrastructureStatus, str],
    ) -> "WirelessSiteTable":
        """
        Filter sites by operational status.

        Args:
            status: InfrastructureStatus or its canonical string value.

        Returns:
            Table containing matching sites.
        """
        value = (
            InfrastructureStatus(status).value
            if isinstance(status, str)
            else status.value
        )
        return self.__class__(
            entities=[entity for entity in self.entities if entity.site_status == value]
        )

    def filter_operational(self) -> "WirelessSiteTable":
        """
        Return operational sites only.

        Returns:
            Table containing operational sites.
        """
        return self.filter_by_status(InfrastructureStatus.OPERATIONAL)

    def filter_by_backhaul_type(
        self,
        backhaul_type: Union[BackhaulType, str],
    ) -> "WirelessSiteTable":
        """
        Filter sites by primary backhaul medium.

        Args:
            backhaul_type: BackhaulType or canonical string value.

        Returns:
            Table containing matching sites.
        """
        value = (
            BackhaulType(backhaul_type).value
            if isinstance(backhaul_type, str)
            else backhaul_type.value
        )
        return self.__class__(
            entities=[
                entity for entity in self.entities if entity.backhaul_type == value
            ]
        )

    def filter_by_network_provider(self, provider: str) -> "WirelessSiteTable":
        """
        Filter sites hosting a given active network provider.

        Args:
            provider: Exact provider name to match.

        Returns:
            Table containing sites where the provider operates equipment.
        """
        return self.__class__(
            entities=[
                entity
                for entity in self.entities
                if entity.network_providers and provider in entity.network_providers
            ]
        )

    def filter_by_access_service_type(
        self,
        service_type: Union[WirelessAccessServiceType, str],
    ) -> "WirelessSiteTable":
        """Return sites supporting a specified wireless access-service role."""
        value = (
            WirelessAccessServiceType(service_type).value
            if isinstance(service_type, str)
            else service_type.value
        )
        return self.__class__(
            entities=[
                entity
                for entity in self.entities
                if entity.access_service_types and value in entity.access_service_types
            ]
        )

    def filter_by_access_technology(
        self,
        technology: Union[WirelessAccessTechnology, str],
    ) -> "WirelessSiteTable":
        """Return sites hosting a specified wireless access technology."""
        value = (
            WirelessAccessTechnology(technology).value
            if isinstance(technology, str)
            else technology.value
        )
        return self.__class__(
            entities=[
                entity
                for entity in self.entities
                if entity.access_technologies and value in entity.access_technologies
            ]
        )

    def filter_by_radio_type(
        self,
        radio_type: Union[RadioType, str],
    ) -> "WirelessSiteTable":
        """Return sites supporting a specified mobile radio generation."""
        value = (
            RadioType(radio_type).value
            if isinstance(radio_type, str)
            else radio_type.value
        )
        return self.__class__(
            entities=[
                entity
                for entity in self.entities
                if entity.radio_types and value in entity.radio_types
            ]
        )

    def filter_by_infrastructure_provider(self, provider: str) -> "WirelessSiteTable":
        """
        Filter sites by passive infrastructure provider.

        Args:
            provider: Exact provider name to match.

        Returns:
            Table containing sites with the matching infrastructure provider.
        """
        return self.__class__(
            entities=[
                entity
                for entity in self.entities
                if entity.physical_infrastructure_provider == provider
            ]
        )

    def filter_by_min_backhaul_capacity(
        self,
        min_mbps: float,
    ) -> "WirelessSiteTable":
        """
        Filter sites with known backhaul capacity at or above a threshold.

        Args:
            min_mbps: Minimum known aggregate backhaul capacity in Mbps.

        Returns:
            Table containing matching sites.

        Raises:
            ValueError: If min_mbps is negative.
        """
        if min_mbps < 0:
            raise ValueError("min_mbps must be greater than or equal to zero.")

        return self.__class__(
            entities=[
                entity
                for entity in self.entities
                if entity.backhaul_capacity_mbps is not None
                and entity.backhaul_capacity_mbps >= min_mbps
            ]
        )

    def get_site_types(self) -> Set[str]:
        """
        Return the canonical site types represented in the table.

        Returns:
            Set of site-type values.
        """
        return {
            entity.site_type for entity in self.entities if entity.site_type is not None
        }

    def get_network_providers(self) -> Set[str]:
        """
        Return all active network providers represented in the table.

        Returns:
            Set of provider names.
        """
        return {
            provider
            for entity in self.entities
            if entity.network_providers
            for provider in entity.network_providers
        }

    def get_infrastructure_providers(self) -> Set[str]:
        """
        Return all passive infrastructure providers represented in the table.

        Returns:
            Set of provider names.
        """
        return {
            entity.physical_infrastructure_provider
            for entity in self.entities
            if entity.physical_infrastructure_provider is not None
        }

    def get_upstream_transmission_node_ids(self) -> Set[str]:
        """
        Return referenced upstream transmission-node identifiers.

        Returns:
            Set of non-null TransmissionNode identifiers.
        """
        return {
            entity.upstream_transmission_node_id
            for entity in self.entities
            if entity.upstream_transmission_node_id is not None
        }

    def enrich_from_wireless_access_services(
        self,
        service_table: "WirelessAccessServiceTable",
    ) -> "WirelessSiteTable":
        """
        Enrich site-level service summaries from linked wireless access services.

        Existing values reported directly on WirelessSite entities are preserved and
        unioned with values derived from WirelessAccessService records. Services
        whose wireless_site_id is absent from this table are ignored.

        Args:
            service_table: Validated wireless access services to aggregate by site.

        Returns:
            New WirelessSiteTable with enriched service, technology, and radio-type
            summary fields.
        """
        summaries: Dict[str, Dict[str, Set[str]]] = {}

        for service in service_table.entities:
            summary = summaries.setdefault(
                service.wireless_site_id,
                {
                    "access_service_types": set(),
                    "access_technologies": set(),
                    "radio_types": set(),
                },
            )

            summary["access_service_types"].add(enum_value(service.access_service_type))

            for technology in service.access_technologies or set():
                technology_value = enum_value(technology)
                summary["access_technologies"].add(technology_value)

                radio_type = WIRELESS_ACCESS_TECHNOLOGY_TO_RADIO_TYPE.get(
                    WirelessAccessTechnology(technology_value)
                )
                if radio_type is not None:
                    summary["radio_types"].add(radio_type.value)

        enriched_entities: List[WirelessSite] = []

        for site in self.entities:
            summary = summaries.get(site.wireless_site_id)

            if summary is None:
                enriched_entities.append(site)
                continue

            access_service_types = (site.access_service_types or set()) | summary[
                "access_service_types"
            ]
            access_technologies = (site.access_technologies or set()) | summary[
                "access_technologies"
            ]
            radio_types = (site.radio_types or set()) | summary["radio_types"]

            enriched_entities.append(
                WirelessSite.model_validate(
                    {
                        **site.model_dump(),
                        "access_service_types": access_service_types or None,
                        "access_technologies": access_technologies or None,
                        "radio_types": radio_types or None,
                    }
                )
            )

        return self.__class__(entities=enriched_entities)
enrich_from_wireless_access_services(service_table)

Enrich site-level service summaries from linked wireless access services.

Existing values reported directly on WirelessSite entities are preserved and unioned with values derived from WirelessAccessService records. Services whose wireless_site_id is absent from this table are ignored.

Parameters:

Name Type Description Default
service_table 'WirelessAccessServiceTable'

Validated wireless access services to aggregate by site.

required

Returns:

Type Description
'WirelessSiteTable'

New WirelessSiteTable with enriched service, technology, and radio-type

'WirelessSiteTable'

summary fields.

Source code in gigaspatial/core/schemas/wireless_site.py
def enrich_from_wireless_access_services(
    self,
    service_table: "WirelessAccessServiceTable",
) -> "WirelessSiteTable":
    """
    Enrich site-level service summaries from linked wireless access services.

    Existing values reported directly on WirelessSite entities are preserved and
    unioned with values derived from WirelessAccessService records. Services
    whose wireless_site_id is absent from this table are ignored.

    Args:
        service_table: Validated wireless access services to aggregate by site.

    Returns:
        New WirelessSiteTable with enriched service, technology, and radio-type
        summary fields.
    """
    summaries: Dict[str, Dict[str, Set[str]]] = {}

    for service in service_table.entities:
        summary = summaries.setdefault(
            service.wireless_site_id,
            {
                "access_service_types": set(),
                "access_technologies": set(),
                "radio_types": set(),
            },
        )

        summary["access_service_types"].add(enum_value(service.access_service_type))

        for technology in service.access_technologies or set():
            technology_value = enum_value(technology)
            summary["access_technologies"].add(technology_value)

            radio_type = WIRELESS_ACCESS_TECHNOLOGY_TO_RADIO_TYPE.get(
                WirelessAccessTechnology(technology_value)
            )
            if radio_type is not None:
                summary["radio_types"].add(radio_type.value)

    enriched_entities: List[WirelessSite] = []

    for site in self.entities:
        summary = summaries.get(site.wireless_site_id)

        if summary is None:
            enriched_entities.append(site)
            continue

        access_service_types = (site.access_service_types or set()) | summary[
            "access_service_types"
        ]
        access_technologies = (site.access_technologies or set()) | summary[
            "access_technologies"
        ]
        radio_types = (site.radio_types or set()) | summary["radio_types"]

        enriched_entities.append(
            WirelessSite.model_validate(
                {
                    **site.model_dump(),
                    "access_service_types": access_service_types or None,
                    "access_technologies": access_technologies or None,
                    "radio_types": radio_types or None,
                }
            )
        )

    return self.__class__(entities=enriched_entities)
filter_by_access_service_type(service_type)

Return sites supporting a specified wireless access-service role.

Source code in gigaspatial/core/schemas/wireless_site.py
def filter_by_access_service_type(
    self,
    service_type: Union[WirelessAccessServiceType, str],
) -> "WirelessSiteTable":
    """Return sites supporting a specified wireless access-service role."""
    value = (
        WirelessAccessServiceType(service_type).value
        if isinstance(service_type, str)
        else service_type.value
    )
    return self.__class__(
        entities=[
            entity
            for entity in self.entities
            if entity.access_service_types and value in entity.access_service_types
        ]
    )
filter_by_access_technology(technology)

Return sites hosting a specified wireless access technology.

Source code in gigaspatial/core/schemas/wireless_site.py
def filter_by_access_technology(
    self,
    technology: Union[WirelessAccessTechnology, str],
) -> "WirelessSiteTable":
    """Return sites hosting a specified wireless access technology."""
    value = (
        WirelessAccessTechnology(technology).value
        if isinstance(technology, str)
        else technology.value
    )
    return self.__class__(
        entities=[
            entity
            for entity in self.entities
            if entity.access_technologies and value in entity.access_technologies
        ]
    )
filter_by_backhaul_type(backhaul_type)

Filter sites by primary backhaul medium.

Parameters:

Name Type Description Default
backhaul_type Union[BackhaulType, str]

BackhaulType or canonical string value.

required

Returns:

Type Description
'WirelessSiteTable'

Table containing matching sites.

Source code in gigaspatial/core/schemas/wireless_site.py
def filter_by_backhaul_type(
    self,
    backhaul_type: Union[BackhaulType, str],
) -> "WirelessSiteTable":
    """
    Filter sites by primary backhaul medium.

    Args:
        backhaul_type: BackhaulType or canonical string value.

    Returns:
        Table containing matching sites.
    """
    value = (
        BackhaulType(backhaul_type).value
        if isinstance(backhaul_type, str)
        else backhaul_type.value
    )
    return self.__class__(
        entities=[
            entity for entity in self.entities if entity.backhaul_type == value
        ]
    )
filter_by_infrastructure_provider(provider)

Filter sites by passive infrastructure provider.

Parameters:

Name Type Description Default
provider str

Exact provider name to match.

required

Returns:

Type Description
'WirelessSiteTable'

Table containing sites with the matching infrastructure provider.

Source code in gigaspatial/core/schemas/wireless_site.py
def filter_by_infrastructure_provider(self, provider: str) -> "WirelessSiteTable":
    """
    Filter sites by passive infrastructure provider.

    Args:
        provider: Exact provider name to match.

    Returns:
        Table containing sites with the matching infrastructure provider.
    """
    return self.__class__(
        entities=[
            entity
            for entity in self.entities
            if entity.physical_infrastructure_provider == provider
        ]
    )
filter_by_min_backhaul_capacity(min_mbps)

Filter sites with known backhaul capacity at or above a threshold.

Parameters:

Name Type Description Default
min_mbps float

Minimum known aggregate backhaul capacity in Mbps.

required

Returns:

Type Description
'WirelessSiteTable'

Table containing matching sites.

Raises:

Type Description
ValueError

If min_mbps is negative.

Source code in gigaspatial/core/schemas/wireless_site.py
def filter_by_min_backhaul_capacity(
    self,
    min_mbps: float,
) -> "WirelessSiteTable":
    """
    Filter sites with known backhaul capacity at or above a threshold.

    Args:
        min_mbps: Minimum known aggregate backhaul capacity in Mbps.

    Returns:
        Table containing matching sites.

    Raises:
        ValueError: If min_mbps is negative.
    """
    if min_mbps < 0:
        raise ValueError("min_mbps must be greater than or equal to zero.")

    return self.__class__(
        entities=[
            entity
            for entity in self.entities
            if entity.backhaul_capacity_mbps is not None
            and entity.backhaul_capacity_mbps >= min_mbps
        ]
    )
filter_by_network_provider(provider)

Filter sites hosting a given active network provider.

Parameters:

Name Type Description Default
provider str

Exact provider name to match.

required

Returns:

Type Description
'WirelessSiteTable'

Table containing sites where the provider operates equipment.

Source code in gigaspatial/core/schemas/wireless_site.py
def filter_by_network_provider(self, provider: str) -> "WirelessSiteTable":
    """
    Filter sites hosting a given active network provider.

    Args:
        provider: Exact provider name to match.

    Returns:
        Table containing sites where the provider operates equipment.
    """
    return self.__class__(
        entities=[
            entity
            for entity in self.entities
            if entity.network_providers and provider in entity.network_providers
        ]
    )
filter_by_radio_type(radio_type)

Return sites supporting a specified mobile radio generation.

Source code in gigaspatial/core/schemas/wireless_site.py
def filter_by_radio_type(
    self,
    radio_type: Union[RadioType, str],
) -> "WirelessSiteTable":
    """Return sites supporting a specified mobile radio generation."""
    value = (
        RadioType(radio_type).value
        if isinstance(radio_type, str)
        else radio_type.value
    )
    return self.__class__(
        entities=[
            entity
            for entity in self.entities
            if entity.radio_types and value in entity.radio_types
        ]
    )
filter_by_site_type(site_type)

Filter sites by physical type.

Parameters:

Name Type Description Default
site_type Union[WirelessSiteType, str]

WirelessSiteType or its canonical string value.

required

Returns:

Type Description
'WirelessSiteTable'

Table containing matching sites.

Source code in gigaspatial/core/schemas/wireless_site.py
def filter_by_site_type(
    self,
    site_type: Union[WirelessSiteType, str],
) -> "WirelessSiteTable":
    """
    Filter sites by physical type.

    Args:
        site_type: WirelessSiteType or its canonical string value.

    Returns:
        Table containing matching sites.
    """
    value = (
        WirelessSiteType(site_type).value
        if isinstance(site_type, str)
        else site_type.value
    )
    return self.__class__(
        entities=[entity for entity in self.entities if entity.site_type == value]
    )
filter_by_status(status)

Filter sites by operational status.

Parameters:

Name Type Description Default
status Union[InfrastructureStatus, str]

InfrastructureStatus or its canonical string value.

required

Returns:

Type Description
'WirelessSiteTable'

Table containing matching sites.

Source code in gigaspatial/core/schemas/wireless_site.py
def filter_by_status(
    self,
    status: Union[InfrastructureStatus, str],
) -> "WirelessSiteTable":
    """
    Filter sites by operational status.

    Args:
        status: InfrastructureStatus or its canonical string value.

    Returns:
        Table containing matching sites.
    """
    value = (
        InfrastructureStatus(status).value
        if isinstance(status, str)
        else status.value
    )
    return self.__class__(
        entities=[entity for entity in self.entities if entity.site_status == value]
    )
filter_operational()

Return operational sites only.

Returns:

Type Description
'WirelessSiteTable'

Table containing operational sites.

Source code in gigaspatial/core/schemas/wireless_site.py
def filter_operational(self) -> "WirelessSiteTable":
    """
    Return operational sites only.

    Returns:
        Table containing operational sites.
    """
    return self.filter_by_status(InfrastructureStatus.OPERATIONAL)
from_dataframe(df, entity_class=WirelessSite, clean=False, processor=WirelessSiteProcessor, **kwargs) classmethod

Create a WirelessSiteTable from an existing DataFrame.

Parameters:

Name Type Description Default
df DataFrame

DataFrame containing wireless-site records.

required
entity_class Type[WirelessSite]

Entity schema used to validate the records.

WirelessSite
clean bool

Whether to run the configured processor before validation.

False
processor Optional[Union[Type[EntityProcessor], EntityProcessor]]

Processor class or instance to use.

WirelessSiteProcessor
**kwargs

Additional processing options.

{}

Returns:

Type Description
'WirelessSiteTable'

Validated WirelessSiteTable.

Source code in gigaspatial/core/schemas/wireless_site.py
@classmethod
def from_dataframe(
    cls,
    df: pd.DataFrame,
    entity_class: Type[WirelessSite] = WirelessSite,
    clean: bool = False,
    processor: Optional[
        Union[Type[EntityProcessor], EntityProcessor]
    ] = WirelessSiteProcessor,
    **kwargs,
) -> "WirelessSiteTable":
    """
    Create a WirelessSiteTable from an existing DataFrame.

    Args:
        df: DataFrame containing wireless-site records.
        entity_class: Entity schema used to validate the records.
        clean: Whether to run the configured processor before validation.
        processor: Processor class or instance to use.
        **kwargs: Additional processing options.

    Returns:
        Validated WirelessSiteTable.
    """
    return super().from_dataframe(
        df=df,
        entity_class=entity_class,
        clean=clean,
        processor=processor,
        **kwargs,
    )
from_file(file_path, data_store=None, clean=True, processor=WirelessSiteProcessor, **kwargs) classmethod

Create a WirelessSiteTable from a file.

Parameters:

Name Type Description Default
file_path Union[str, Path]

Path to the source dataset.

required
data_store Optional[DataStore]

Optional data-store implementation.

None
clean bool

Whether to run the configured processor before validation.

True
processor Optional[Union[Type[EntityProcessor], EntityProcessor]]

Processor class or instance to use.

WirelessSiteProcessor
**kwargs

Additional loading and processing options.

{}

Returns:

Type Description
'WirelessSiteTable'

Validated WirelessSiteTable.

Source code in gigaspatial/core/schemas/wireless_site.py
@classmethod
def from_file(
    cls,
    file_path: Union[str, Path],
    data_store: Optional[DataStore] = None,
    clean: bool = True,
    processor: Optional[
        Union[Type[EntityProcessor], EntityProcessor]
    ] = WirelessSiteProcessor,
    **kwargs,
) -> "WirelessSiteTable":
    """
    Create a WirelessSiteTable from a file.

    Args:
        file_path: Path to the source dataset.
        data_store: Optional data-store implementation.
        clean: Whether to run the configured processor before validation.
        processor: Processor class or instance to use.
        **kwargs: Additional loading and processing options.

    Returns:
        Validated WirelessSiteTable.
    """
    return super().from_file(
        file_path=file_path,
        entity_class=WirelessSite,
        data_store=data_store,
        clean=clean,
        processor=processor,
        **kwargs,
    )
from_files(file_paths, data_store=None, clean=True, processor=WirelessSiteProcessor, **kwargs) classmethod

Load, process, and merge several source files into one table.

Parameters:

Name Type Description Default
file_paths List[Union[str, Path]]

Paths to the source datasets.

required
data_store Optional[DataStore]

Optional data-store implementation.

None
clean bool

Whether to run the configured processor before validation.

True
processor Optional[Union[Type[EntityProcessor], EntityProcessor]]

Processor class or instance to use.

WirelessSiteProcessor
**kwargs

Additional loading and processing options.

{}

Returns:

Type Description
'WirelessSiteTable'

Merged and validated WirelessSiteTable.

Source code in gigaspatial/core/schemas/wireless_site.py
@classmethod
def from_files(
    cls,
    file_paths: List[Union[str, Path]],
    data_store: Optional[DataStore] = None,
    clean: bool = True,
    processor: Optional[
        Union[Type[EntityProcessor], EntityProcessor]
    ] = WirelessSiteProcessor,
    **kwargs,
) -> "WirelessSiteTable":
    """
    Load, process, and merge several source files into one table.

    Args:
        file_paths: Paths to the source datasets.
        data_store: Optional data-store implementation.
        clean: Whether to run the configured processor before validation.
        processor: Processor class or instance to use.
        **kwargs: Additional loading and processing options.

    Returns:
        Merged and validated WirelessSiteTable.
    """
    return super().from_files(
        file_paths=file_paths,
        entity_class=WirelessSite,
        data_store=data_store,
        clean=clean,
        processor=processor,
        **kwargs,
    )
get_infrastructure_providers()

Return all passive infrastructure providers represented in the table.

Returns:

Type Description
Set[str]

Set of provider names.

Source code in gigaspatial/core/schemas/wireless_site.py
def get_infrastructure_providers(self) -> Set[str]:
    """
    Return all passive infrastructure providers represented in the table.

    Returns:
        Set of provider names.
    """
    return {
        entity.physical_infrastructure_provider
        for entity in self.entities
        if entity.physical_infrastructure_provider is not None
    }
get_network_providers()

Return all active network providers represented in the table.

Returns:

Type Description
Set[str]

Set of provider names.

Source code in gigaspatial/core/schemas/wireless_site.py
def get_network_providers(self) -> Set[str]:
    """
    Return all active network providers represented in the table.

    Returns:
        Set of provider names.
    """
    return {
        provider
        for entity in self.entities
        if entity.network_providers
        for provider in entity.network_providers
    }
get_site_types()

Return the canonical site types represented in the table.

Returns:

Type Description
Set[str]

Set of site-type values.

Source code in gigaspatial/core/schemas/wireless_site.py
def get_site_types(self) -> Set[str]:
    """
    Return the canonical site types represented in the table.

    Returns:
        Set of site-type values.
    """
    return {
        entity.site_type for entity in self.entities if entity.site_type is not None
    }
get_upstream_transmission_node_ids()

Return referenced upstream transmission-node identifiers.

Returns:

Type Description
Set[str]

Set of non-null TransmissionNode identifiers.

Source code in gigaspatial/core/schemas/wireless_site.py
def get_upstream_transmission_node_ids(self) -> Set[str]:
    """
    Return referenced upstream transmission-node identifiers.

    Returns:
        Set of non-null TransmissionNode identifiers.
    """
    return {
        entity.upstream_transmission_node_id
        for entity in self.entities
        if entity.upstream_transmission_node_id is not None
    }
WirelessSiteType

Bases: str, Enum

Enum for physical wireless site types.

Source code in gigaspatial/core/schemas/wireless_site.py
class WirelessSiteType(str, Enum):
    """Enum for physical wireless site types."""

    TOWER = "tower"
    ROOFTOP = "rooftop"
    MAST = "mast"
    POLE = "pole"
    RELAY = "relay"
    HUB = "hub"
    CUSTOMER_PREMISES = "customer_premises"