kpubdata 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
kpubdata/__init__.py ADDED
@@ -0,0 +1,52 @@
1
+ """KPubData — Korean public data access framework for Python 3.10+."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from kpubdata.client import Client
6
+ from kpubdata.core.capability import Operation, PaginationMode, QuerySupport
7
+ from kpubdata.core.models import DatasetRef, FieldDescriptor, Query, RecordBatch, SchemaDescriptor
8
+ from kpubdata.core.representation import Representation
9
+ from kpubdata.exceptions import (
10
+ AuthError,
11
+ ConfigError,
12
+ DatasetNotFoundError,
13
+ InvalidRequestError,
14
+ ParseError,
15
+ ProviderNotRegisteredError,
16
+ ProviderResponseError,
17
+ PublicDataError,
18
+ RateLimitError,
19
+ ServiceUnavailableError,
20
+ TransportError,
21
+ TransportTimeoutError,
22
+ UnsupportedCapabilityError,
23
+ )
24
+
25
+ __all__ = [
26
+ "__version__",
27
+ "Client",
28
+ "DatasetRef",
29
+ "Query",
30
+ "RecordBatch",
31
+ "SchemaDescriptor",
32
+ "FieldDescriptor",
33
+ "Operation",
34
+ "PaginationMode",
35
+ "QuerySupport",
36
+ "Representation",
37
+ "PublicDataError",
38
+ "ConfigError",
39
+ "AuthError",
40
+ "TransportError",
41
+ "TransportTimeoutError",
42
+ "RateLimitError",
43
+ "ServiceUnavailableError",
44
+ "ParseError",
45
+ "InvalidRequestError",
46
+ "ProviderResponseError",
47
+ "UnsupportedCapabilityError",
48
+ "DatasetNotFoundError",
49
+ "ProviderNotRegisteredError",
50
+ ]
51
+
52
+ __version__ = "0.1.0a0"
kpubdata/catalog.py ADDED
@@ -0,0 +1,101 @@
1
+ """Catalog — dataset discovery, search, and resolution."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import builtins
6
+ from typing import cast
7
+
8
+ from kpubdata.core.models import DatasetRef
9
+ from kpubdata.core.protocol import ProviderAdapter
10
+ from kpubdata.exceptions import DatasetNotFoundError, ProviderNotRegisteredError
11
+ from kpubdata.registry import ProviderRegistry
12
+
13
+
14
+ class Catalog:
15
+ """Provides dataset discovery across registered providers."""
16
+
17
+ def __init__(self, registry: ProviderRegistry) -> None:
18
+ """Initialize catalog bound to a provider registry."""
19
+
20
+ self._registry = registry
21
+
22
+ def list(self, *, provider: str | None = None) -> builtins.list[DatasetRef]:
23
+ """Return discoverable datasets, optionally filtered by provider.
24
+
25
+ Raises:
26
+ ProviderNotRegisteredError: If ``provider`` is given but unknown.
27
+ """
28
+
29
+ if provider is not None:
30
+ adapter = self._get_adapter(provider)
31
+ return adapter.list_datasets()
32
+
33
+ datasets: builtins.list[DatasetRef] = []
34
+ for provider_name in self._registry:
35
+ adapter = self._get_adapter(provider_name)
36
+ datasets.extend(adapter.list_datasets())
37
+ return datasets
38
+
39
+ def search(self, text: str, *, provider: str | None = None) -> builtins.list[DatasetRef]:
40
+ """Search datasets by delegating to each adapter's search logic.
41
+
42
+ Each adapter implements its own ``search_datasets(text)`` method,
43
+ allowing provider-specific search semantics.
44
+
45
+ Raises:
46
+ ProviderNotRegisteredError: If ``provider`` is given but unknown.
47
+ """
48
+
49
+ if provider is not None:
50
+ adapter = self._get_adapter(provider)
51
+ return adapter.search_datasets(text)
52
+
53
+ results: builtins.list[DatasetRef] = []
54
+ for provider_name in self._registry:
55
+ adapter = self._get_adapter(provider_name)
56
+ results.extend(adapter.search_datasets(text))
57
+ return results
58
+
59
+ def resolve(self, dataset_id: str) -> tuple[ProviderAdapter, DatasetRef]:
60
+ """Resolve ``provider.dataset_key`` into an adapter and dataset ref.
61
+
62
+ Raises:
63
+ DatasetNotFoundError: If the dataset id is malformed or not found.
64
+ ProviderNotRegisteredError: If the provider is not registered.
65
+ """
66
+
67
+ provider_name, dataset_key = self._split_dataset_id(dataset_id)
68
+ adapter = self._get_adapter(provider_name)
69
+
70
+ try:
71
+ return adapter, adapter.get_dataset(dataset_key)
72
+ except DatasetNotFoundError:
73
+ raise
74
+ except Exception as exc:
75
+ raise DatasetNotFoundError(
76
+ f"Dataset not found: {dataset_id}",
77
+ provider=provider_name,
78
+ dataset_id=dataset_id,
79
+ ) from exc
80
+
81
+ def _get_adapter(self, provider: str) -> ProviderAdapter:
82
+ """Fetch provider adapter from registry or raise canonical error."""
83
+
84
+ try:
85
+ return cast(ProviderAdapter, self._registry.get(provider))
86
+ except ProviderNotRegisteredError:
87
+ raise
88
+
89
+ @staticmethod
90
+ def _split_dataset_id(dataset_id: str) -> tuple[str, str]:
91
+ """Split canonical dataset id into ``(provider, dataset_key)``."""
92
+
93
+ parts = dataset_id.split(".", 1)
94
+ if len(parts) != 2 or not parts[0] or not parts[1]:
95
+ raise DatasetNotFoundError(
96
+ f"Invalid dataset id format: {dataset_id}", dataset_id=dataset_id
97
+ )
98
+ return parts[0], parts[1]
99
+
100
+
101
+ __all__ = ["Catalog"]
kpubdata/client.py ADDED
@@ -0,0 +1,179 @@
1
+ """Client — the top-level entry point for KPubData."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Callable
6
+ from typing import cast
7
+
8
+ from typing_extensions import override
9
+
10
+ from kpubdata.catalog import Catalog
11
+ from kpubdata.config import KPubDataConfig
12
+ from kpubdata.core.dataset import Dataset
13
+ from kpubdata.core.protocol import ProviderAdapter
14
+ from kpubdata.registry import ProviderRegistry
15
+ from kpubdata.transport.http import HttpTransport, TransportConfig, TransportRequirements
16
+
17
+ _BUILTIN_PROVIDERS: tuple[tuple[str, str, str], ...] = (
18
+ ("datago", "kpubdata.providers.datago", "DataGoAdapter"),
19
+ ("bok", "kpubdata.providers.bok", "BokAdapter"),
20
+ ("kosis", "kpubdata.providers.kosis", "KosisAdapter"),
21
+ ("lofin", "kpubdata.providers.lofin", "LofinAdapter"),
22
+ )
23
+
24
+
25
+ class Client:
26
+ """Top-level entry point for dataset discovery and bound operations."""
27
+
28
+ def __init__(
29
+ self,
30
+ *,
31
+ provider_keys: dict[str, str] | None = None,
32
+ timeout: float = 30.0,
33
+ max_retries: int = 3,
34
+ **extra: object,
35
+ ) -> None:
36
+ """Initialize a client with explicit provider and transport configuration.
37
+
38
+ Use ``provider_keys`` to supply credentials directly, and configure
39
+ transport behavior with ``timeout`` and ``max_retries``.
40
+ Built-in providers (datago, bok, kosis, lofin) are lazily registered by default.
41
+ """
42
+
43
+ self._config: KPubDataConfig = KPubDataConfig(
44
+ provider_keys=provider_keys or {},
45
+ timeout=timeout,
46
+ max_retries=max_retries,
47
+ extra=dict(extra),
48
+ )
49
+ self._registry: ProviderRegistry = ProviderRegistry()
50
+ self._transport_config: TransportConfig = TransportConfig(
51
+ timeout=self._config.timeout,
52
+ max_retries=self._config.max_retries,
53
+ )
54
+ self._transport: HttpTransport = HttpTransport(self._transport_config)
55
+ self._provider_transports: list[HttpTransport] = []
56
+ self._register_builtin_providers()
57
+ self._catalog: Catalog = Catalog(self._registry)
58
+
59
+ @classmethod
60
+ def from_env(cls, **overrides: object) -> Client:
61
+ """Create a client from environment variables and explicit overrides."""
62
+
63
+ config = KPubDataConfig.from_env(**overrides)
64
+ return cls(
65
+ provider_keys=config.provider_keys,
66
+ timeout=config.timeout,
67
+ max_retries=config.max_retries,
68
+ **config.extra,
69
+ )
70
+
71
+ def __enter__(self) -> Client:
72
+ """Enter context manager and initialize transport client."""
73
+
74
+ _ = self._transport.__enter__()
75
+ return self
76
+
77
+ def __exit__(self, *exc: object) -> None:
78
+ """Exit context manager and close transport resources."""
79
+
80
+ self.close()
81
+
82
+ def close(self) -> None:
83
+ """Close underlying transport resources for this client."""
84
+
85
+ self._transport.close()
86
+ for provider_transport in self._provider_transports:
87
+ provider_transport.close()
88
+ self._provider_transports.clear()
89
+
90
+ @property
91
+ def datasets(self) -> Catalog:
92
+ """Return catalog interface for discovery, search, and resolution."""
93
+
94
+ return self._catalog
95
+
96
+ def dataset(self, dataset_id: str) -> Dataset:
97
+ """Bind and return a dataset object by canonical identifier.
98
+
99
+ Raises:
100
+ DatasetNotFoundError: If the dataset id is invalid or unknown.
101
+ ProviderNotRegisteredError: If the provider is not registered.
102
+ """
103
+
104
+ adapter, ref = self._catalog.resolve(dataset_id)
105
+ return Dataset(ref=ref, adapter=adapter)
106
+
107
+ def register_provider(self, adapter: object) -> None:
108
+ """Register a provider adapter in this client's registry.
109
+
110
+ Raises:
111
+ TypeError: If the adapter does not satisfy the required protocol.
112
+ ValueError: If the provider is already registered.
113
+ """
114
+
115
+ self._registry.register(adapter)
116
+
117
+ def _register_builtin_providers(self) -> None:
118
+ config = self._config
119
+ transport = self._transport
120
+ transport_config = self._transport_config
121
+ provider_transports = self._provider_transports
122
+
123
+ for provider_name, module_path, class_name in _BUILTIN_PROVIDERS:
124
+
125
+ def _make_factory(
126
+ mod: str,
127
+ cls: str,
128
+ cfg: KPubDataConfig,
129
+ tpt: HttpTransport,
130
+ base_transport_config: TransportConfig,
131
+ owned_transports: list[HttpTransport],
132
+ ) -> Callable[[], ProviderAdapter]:
133
+ def _factory() -> ProviderAdapter:
134
+ import importlib
135
+
136
+ module = importlib.import_module(mod)
137
+ adapter_cls = cast(Callable[..., ProviderAdapter], getattr(module, cls))
138
+ adapter = adapter_cls(config=cfg, transport=tpt)
139
+ requirements = _get_transport_requirements(adapter)
140
+ if requirements is None:
141
+ return adapter
142
+
143
+ custom_transport = HttpTransport.with_requirements(
144
+ base_transport_config,
145
+ requirements,
146
+ )
147
+ owned_transports.append(custom_transport)
148
+ return adapter_cls(config=cfg, transport=custom_transport)
149
+
150
+ return _factory
151
+
152
+ self._registry.register_lazy(
153
+ provider_name,
154
+ _make_factory(
155
+ module_path,
156
+ class_name,
157
+ config,
158
+ transport,
159
+ transport_config,
160
+ provider_transports,
161
+ ),
162
+ skip_if_exists=True,
163
+ )
164
+
165
+ @override
166
+ def __repr__(self) -> str:
167
+ """Return concise representation with known providers."""
168
+
169
+ return f"Client(providers=[{', '.join(self._registry)}])"
170
+
171
+
172
+ __all__ = ["Client"]
173
+
174
+
175
+ def _get_transport_requirements(adapter: ProviderAdapter) -> TransportRequirements | None:
176
+ requirements = getattr(adapter, "transport_requirements", None)
177
+ if requirements is None:
178
+ return None
179
+ return cast(TransportRequirements | None, requirements)
kpubdata/config.py ADDED
@@ -0,0 +1,119 @@
1
+ """Configuration management — explicit construction and environment-based loading.
2
+
3
+ Key lookup order for provider keys:
4
+ 1. Explicit `provider_keys` dict passed to constructor
5
+ 2. Environment variable: KPUBDATA_{PROVIDER}_API_KEY (uppercased)
6
+ 3. Environment variable: {PROVIDER}_API_KEY (uppercased, fallback)
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import os
12
+ import re
13
+ from dataclasses import dataclass, field
14
+ from typing import Any
15
+
16
+ from kpubdata.exceptions import ConfigError
17
+
18
+ _ENV_KEY_PATTERN = re.compile(r"^KPUBDATA_([A-Z0-9_]+)_API_KEY$")
19
+
20
+
21
+ @dataclass
22
+ class KPubDataConfig:
23
+ """Framework configuration."""
24
+
25
+ provider_keys: dict[str, str] = field(default_factory=dict)
26
+ timeout: float = 30.0
27
+ max_retries: int = 3
28
+ extra: dict[str, object] = field(default_factory=dict)
29
+
30
+ def __repr__(self) -> str:
31
+ """Return concise debug representation without exposing secrets."""
32
+ providers = sorted(self.provider_keys.keys())
33
+ return (
34
+ "KPubDataConfig("
35
+ f"providers={providers}, "
36
+ f"timeout={self.timeout}, "
37
+ f"max_retries={self.max_retries}, "
38
+ f"extra_keys={sorted(self.extra.keys())}"
39
+ ")"
40
+ )
41
+
42
+ def get_provider_key(self, provider: str) -> str | None:
43
+ """Look up API key for a provider following documented precedence."""
44
+ normalized_provider = _normalize_provider_name(provider)
45
+
46
+ explicit = _get_explicit_key(self.provider_keys, normalized_provider)
47
+ if explicit:
48
+ return explicit
49
+
50
+ provider_token = _provider_env_token(normalized_provider)
51
+ kpub_var = f"KPUBDATA_{provider_token}_API_KEY"
52
+ value = os.environ.get(kpub_var)
53
+ if value:
54
+ return value
55
+
56
+ fallback_var = f"{provider_token}_API_KEY"
57
+ fallback_value = os.environ.get(fallback_var)
58
+ if fallback_value:
59
+ return fallback_value
60
+
61
+ return None
62
+
63
+ def require_provider_key(self, provider: str) -> str:
64
+ """Like get_provider_key but raises ConfigError if missing."""
65
+ key = self.get_provider_key(provider)
66
+ if key is not None:
67
+ return key
68
+ raise ConfigError(f"Missing provider API key for '{provider}'")
69
+
70
+ @classmethod
71
+ def from_env(cls, **overrides: Any) -> KPubDataConfig:
72
+ """Build config from environment variables.
73
+
74
+ Scans for KPUBDATA_*_API_KEY patterns.
75
+ Overrides can be passed as kwargs.
76
+ """
77
+ scanned_keys: dict[str, str] = {}
78
+ for env_name, env_value in os.environ.items():
79
+ match = _ENV_KEY_PATTERN.match(env_name)
80
+ if match is None:
81
+ continue
82
+ if not env_value:
83
+ continue
84
+ provider_name = match.group(1).lower()
85
+ scanned_keys[provider_name] = env_value
86
+
87
+ provider_overrides_raw = overrides.pop("provider_keys", None)
88
+ provider_overrides: dict[str, str] = {}
89
+ if isinstance(provider_overrides_raw, dict):
90
+ for key, value in provider_overrides_raw.items():
91
+ if isinstance(key, str) and isinstance(value, str) and value:
92
+ provider_overrides[_normalize_provider_name(key)] = value
93
+
94
+ merged_provider_keys = scanned_keys.copy()
95
+ merged_provider_keys.update(provider_overrides)
96
+
97
+ return cls(provider_keys=merged_provider_keys, **overrides)
98
+
99
+
100
+ def _normalize_provider_name(provider: str) -> str:
101
+ return provider.strip().lower()
102
+
103
+
104
+ def _provider_env_token(provider: str) -> str:
105
+ token = re.sub(r"[^A-Za-z0-9]", "_", provider)
106
+ return token.upper()
107
+
108
+
109
+ def _get_explicit_key(provider_keys: dict[str, str], provider: str) -> str | None:
110
+ if provider in provider_keys and provider_keys[provider]:
111
+ return provider_keys[provider]
112
+
113
+ for name, value in provider_keys.items():
114
+ if name.lower() == provider and value:
115
+ return value
116
+ return None
117
+
118
+
119
+ __all__ = ["KPubDataConfig"]
@@ -0,0 +1,25 @@
1
+ """Core canonical models and capability metadata for KPubData."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from kpubdata.core.capability import Operation, PaginationMode, QuerySupport
6
+ from kpubdata.core.models import (
7
+ DatasetRef,
8
+ FieldDescriptor,
9
+ Query,
10
+ RecordBatch,
11
+ SchemaDescriptor,
12
+ )
13
+ from kpubdata.core.representation import Representation
14
+
15
+ __all__ = [
16
+ "DatasetRef",
17
+ "FieldDescriptor",
18
+ "Operation",
19
+ "PaginationMode",
20
+ "Query",
21
+ "QuerySupport",
22
+ "RecordBatch",
23
+ "Representation",
24
+ "SchemaDescriptor",
25
+ ]
@@ -0,0 +1,56 @@
1
+ """Capability metadata describing dataset operations and query support."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Callable
6
+ from dataclasses import dataclass as _stdlib_dataclass
7
+ from enum import Enum
8
+ from typing import TypeVar
9
+
10
+ from typing_extensions import dataclass_transform
11
+
12
+ _T = TypeVar("_T")
13
+
14
+
15
+ @dataclass_transform()
16
+ def _dataclass(
17
+ *,
18
+ slots: bool = False,
19
+ frozen: bool = False,
20
+ ) -> Callable[[type[_T]], type[_T]]:
21
+ def _decorate(cls: type[_T]) -> type[_T]:
22
+ return _stdlib_dataclass(slots=slots, frozen=frozen)(cls) # pyright: ignore[reportCallIssue]
23
+
24
+ return _decorate
25
+
26
+
27
+ class Operation(str, Enum):
28
+ """Major operations a dataset can support."""
29
+
30
+ LIST = "list"
31
+ GET = "get"
32
+ SCHEMA = "schema"
33
+ RAW = "raw"
34
+ DOWNLOAD = "download"
35
+
36
+
37
+ class PaginationMode(str, Enum):
38
+ """How a dataset supports pagination."""
39
+
40
+ OFFSET = "offset"
41
+ CURSOR = "cursor"
42
+ NONE = "none"
43
+
44
+
45
+ @_dataclass(slots=True, frozen=True)
46
+ class QuerySupport:
47
+ """Structured metadata about the list-query features a dataset supports."""
48
+
49
+ pagination: PaginationMode = PaginationMode.NONE
50
+ filterable_fields: frozenset[str] = frozenset()
51
+ sortable_fields: frozenset[str] = frozenset()
52
+ time_range: bool = False
53
+ max_page_size: int | None = None
54
+
55
+
56
+ __all__ = ["Operation", "PaginationMode", "QuerySupport"]
@@ -0,0 +1,153 @@
1
+ """Bound Dataset — the user-facing object for dataset operations."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from kpubdata.core.capability import Operation
6
+ from kpubdata.core.models import DatasetRef, Query, RecordBatch, SchemaDescriptor
7
+ from kpubdata.core.protocol import ProviderAdapter
8
+ from kpubdata.exceptions import UnsupportedCapabilityError
9
+
10
+ _CANONICAL_QUERY_KEYS = frozenset(
11
+ {"page", "page_size", "cursor", "start_date", "end_date", "fields", "sort"}
12
+ )
13
+
14
+
15
+ class Dataset:
16
+ """Bound dataset that routes operations to a provider adapter."""
17
+
18
+ def __init__(self, ref: DatasetRef, adapter: ProviderAdapter) -> None:
19
+ """Initialize a dataset bound to its canonical ref and adapter."""
20
+
21
+ self._ref = ref
22
+ self._adapter = adapter
23
+
24
+ @property
25
+ def ref(self) -> DatasetRef:
26
+ """Return immutable canonical dataset reference."""
27
+
28
+ return self._ref
29
+
30
+ @property
31
+ def id(self) -> str:
32
+ """Return canonical dataset identifier."""
33
+
34
+ return self._ref.id
35
+
36
+ @property
37
+ def name(self) -> str:
38
+ """Return human-readable dataset name."""
39
+
40
+ return self._ref.name
41
+
42
+ @property
43
+ def provider(self) -> str:
44
+ """Return provider identifier serving this dataset."""
45
+
46
+ return self._ref.provider
47
+
48
+ @property
49
+ def operations(self) -> frozenset[Operation]:
50
+ """Return declared operation capabilities for this dataset."""
51
+
52
+ return self._ref.operations
53
+
54
+ def list(self, **kwargs: object) -> RecordBatch:
55
+ """Query records from this dataset using canonical list semantics.
56
+
57
+ Canonical query parameters (``page``, ``page_size``, ``cursor``,
58
+ ``start_date``, ``end_date``, ``fields``, ``sort``) are extracted
59
+ into the corresponding ``Query`` fields. Remaining kwargs are
60
+ passed as provider-specific ``filters``.
61
+
62
+ Raises:
63
+ UnsupportedCapabilityError: If this dataset does not support ``list``.
64
+ """
65
+
66
+ if Operation.LIST not in self._ref.operations:
67
+ raise UnsupportedCapabilityError(
68
+ f"Dataset does not support list: {self._ref.id}",
69
+ provider=self._ref.provider,
70
+ dataset_id=self._ref.id,
71
+ operation=Operation.LIST.value,
72
+ )
73
+
74
+ page: int | None = None
75
+ page_size: int | None = None
76
+ cursor: str | None = None
77
+ start_date: str | None = None
78
+ end_date: str | None = None
79
+ fields_list: list[str] | None = None
80
+ sort_list: list[str] | None = None
81
+ filters: dict[str, object] = {}
82
+
83
+ for key, value in kwargs.items():
84
+ if key == "page" and isinstance(value, int):
85
+ page = value
86
+ elif key == "page_size" and isinstance(value, int):
87
+ page_size = value
88
+ elif key == "cursor" and isinstance(value, str):
89
+ cursor = value
90
+ elif key == "start_date" and isinstance(value, str):
91
+ start_date = value
92
+ elif key == "end_date" and isinstance(value, str):
93
+ end_date = value
94
+ elif key == "fields" and isinstance(value, list):
95
+ fields_list = value
96
+ elif key == "sort" and isinstance(value, list):
97
+ sort_list = value
98
+ else:
99
+ filters[key] = value
100
+
101
+ query = Query(
102
+ filters=filters,
103
+ page=page,
104
+ page_size=page_size,
105
+ cursor=cursor,
106
+ start_date=start_date,
107
+ end_date=end_date,
108
+ fields=fields_list,
109
+ sort=sort_list,
110
+ )
111
+ return self._adapter.query_records(self._ref, query)
112
+
113
+ def get(self, **key: object) -> dict[str, object] | None:
114
+ """Return a single record matching the provided key fields.
115
+
116
+ Return ``None`` when no matching record is found.
117
+
118
+ Raises:
119
+ UnsupportedCapabilityError: If this dataset does not support ``get``.
120
+ """
121
+
122
+ if Operation.GET not in self._ref.operations:
123
+ raise UnsupportedCapabilityError(
124
+ f"Dataset does not support get: {self._ref.id}",
125
+ provider=self._ref.provider,
126
+ dataset_id=self._ref.id,
127
+ operation=Operation.GET.value,
128
+ )
129
+ key_payload: dict[str, object] = {k: v for k, v in key.items()}
130
+ return self._adapter.get_record(self._ref, key_payload)
131
+
132
+ def schema(self) -> SchemaDescriptor | None:
133
+ """Return canonical schema metadata when the provider exposes it."""
134
+
135
+ return self._adapter.get_schema(self._ref)
136
+
137
+ def call_raw(self, operation: str, **params: object) -> object:
138
+ """Execute a provider-native operation without canonical normalization.
139
+
140
+ Use this escape hatch for provider features not represented in the
141
+ canonical model.
142
+ """
143
+
144
+ payload: dict[str, object] = {k: v for k, v in params.items()}
145
+ return self._adapter.call_raw(self._ref, operation, payload)
146
+
147
+ def __repr__(self) -> str:
148
+ """Return concise debug representation."""
149
+
150
+ return f"Dataset({self._ref.id!r})"
151
+
152
+
153
+ __all__ = ["Dataset"]