Source code for shapelib.core.cloud.cloud_path

"""Value Object for parsing and representing cloud/local file paths."""

from __future__ import annotations

from dataclasses import dataclass
from pathlib import Path
from typing import Final
from urllib.parse import urlparse


[docs] @dataclass(frozen=True) class CloudPath: """ Immutable Value Object representing a file path (local or remote). This class parses URLs and local paths to extract scheme, host, path, and file extension. It handles query strings in URLs (e.g., AWS presigned URLs) correctly by extracting the extension from the path component only. Parameters ---------- raw_path : str The original path or URL string. Attributes ---------- raw_path : str The original path or URL as provided. scheme : str The URL scheme (e.g., 'https', 's3', 'gs', 'file', or empty for local). host : str The host/bucket name for remote URLs, empty for local paths. path : str The path component (without query string). extension : str The file extension (e.g., '.fgb', '.shp'). query : str The query string component (for presigned URLs). Examples -------- Parse a presigned S3 URL: >>> cloud_path = CloudPath.from_string( ... "https://bucket.s3.amazonaws.com/file.fgb?X-Amz-Signature=..." ... ) >>> cloud_path.extension '.fgb' >>> cloud_path.is_remote True Parse a local path: >>> local_path = CloudPath.from_string("/home/user/data.shp") >>> local_path.is_remote False Big-O ----- O(n) where n is the length of the path string. """ raw_path: str scheme: str host: str path: str extension: str query: str _REMOTE_SCHEMES: Final[frozenset[str]] = frozenset({"http", "https", "s3", "gs", "az"})
[docs] @classmethod def from_string(cls, path: str | Path) -> CloudPath: """ Create a CloudPath from a string or Path object. Parameters ---------- path : str | Path The path or URL to parse. Returns ------- CloudPath A new CloudPath instance with parsed components. Raises ------ ValueError If the path is empty or None. """ if not path: raise ValueError("Path cannot be empty or None.") raw_path = str(path) parsed = urlparse(raw_path) scheme = parsed.scheme.lower() host = parsed.netloc url_path = parsed.path query = parsed.query extension = cls._extract_extension(url_path, raw_path) return cls( raw_path=raw_path, scheme=scheme, host=host, path=url_path, extension=extension, query=query, )
@classmethod def _extract_extension(cls, url_path: str, raw_path: str) -> str: """Extract file extension from path, handling URLs correctly.""" if url_path: return Path(url_path).suffix.lower() return Path(raw_path).suffix.lower() @property def is_remote(self) -> bool: """Check if this path represents a remote location.""" return self.scheme in self._REMOTE_SCHEMES @property def is_s3_protocol(self) -> bool: """Check if this path uses the S3 protocol (s3://).""" return self.scheme == "s3" @property def is_https(self) -> bool: """Check if this path uses HTTP/HTTPS protocol.""" return self.scheme in {"http", "https"} @property def full_url(self) -> str: """Reconstruct the full URL including query string.""" if self.query: return f"{self.scheme}://{self.host}{self.path}?{self.query}" if self.is_remote: return f"{self.scheme}://{self.host}{self.path}" return self.raw_path @property def s3_bucket(self) -> str | None: """Extract S3 bucket name from path.""" if self.is_s3_protocol: return self.host if self.is_https and ".s3." in self.host: return self.host.split(".s3.")[0] return None @property def s3_key(self) -> str | None: """Extract S3 object key from path.""" if self.is_s3_protocol or (self.is_https and ".s3." in self.host): return self.path.lstrip("/") return None @property def vsicurl_path(self) -> str: """ Build the GDAL virtual filesystem path for streaming. Returns ------- str The /vsicurl/ or /vsis3/ path for GDAL. """ if self.is_s3_protocol: return f"/vsis3/{self.s3_bucket}/{self.s3_key}" return f"/vsicurl/{self.full_url}" def __str__(self) -> str: """Return the original raw path string.""" return self.raw_path