chile-open-data-sdk 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.
- chile_open_data_sdk/__init__.py +8 -0
- chile_open_data_sdk/_internal/__init__.py +1 -0
- chile_open_data_sdk/_internal/responses.py +111 -0
- chile_open_data_sdk/client.py +142 -0
- chile_open_data_sdk/config.py +93 -0
- chile_open_data_sdk/errors.py +66 -0
- chile_open_data_sdk/py.typed +0 -0
- chile_open_data_sdk/types.py +5 -0
- chile_open_data_sdk-0.1.0.dist-info/METADATA +157 -0
- chile_open_data_sdk-0.1.0.dist-info/RECORD +12 -0
- chile_open_data_sdk-0.1.0.dist-info/WHEEL +4 -0
- chile_open_data_sdk-0.1.0.dist-info/licenses/LICENSE.md +9 -0
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"""Unofficial Chile-first CKAN Action API SDK."""
|
|
2
|
+
|
|
3
|
+
from chile_open_data_sdk.client import ChileOpenDataClient, CKANClient
|
|
4
|
+
from chile_open_data_sdk.config import ClientConfig
|
|
5
|
+
from chile_open_data_sdk.types import JSONValue
|
|
6
|
+
|
|
7
|
+
__version__ = "0.1.0"
|
|
8
|
+
__all__ = ["CKANClient", "ChileOpenDataClient", "ClientConfig", "JSONValue", "__version__"]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Private network-independent implementation helpers."""
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"""Parse CKAN envelopes and redact diagnostic payloads."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from typing import cast
|
|
5
|
+
|
|
6
|
+
import httpx
|
|
7
|
+
from pydantic import BaseModel, ConfigDict, JsonValue, StrictBool, ValidationError
|
|
8
|
+
|
|
9
|
+
from chile_open_data_sdk.errors import (
|
|
10
|
+
CKANAPIError,
|
|
11
|
+
CKANAuthenticationError,
|
|
12
|
+
CKANAuthorizationError,
|
|
13
|
+
CKANError,
|
|
14
|
+
CKANHTTPError,
|
|
15
|
+
CKANNotFoundError,
|
|
16
|
+
CKANProtocolError,
|
|
17
|
+
CKANRateLimitError,
|
|
18
|
+
CKANValidationError,
|
|
19
|
+
)
|
|
20
|
+
from chile_open_data_sdk.types import JSONValue
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class ActionResponse(BaseModel):
|
|
24
|
+
"""Internal envelope; extension metadata is tolerated."""
|
|
25
|
+
|
|
26
|
+
model_config = ConfigDict(extra="allow")
|
|
27
|
+
success: StrictBool
|
|
28
|
+
result: JsonValue = None
|
|
29
|
+
error: dict[str, JsonValue] | None = None
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def redact(value: JSONValue, token: str | None) -> JSONValue:
|
|
33
|
+
"""Remove configured tokens and values of credential-like diagnostic keys."""
|
|
34
|
+
if isinstance(value, str):
|
|
35
|
+
return value.replace(token, "[REDACTED]") if token else value
|
|
36
|
+
if isinstance(value, list):
|
|
37
|
+
return [redact(item, token) for item in value]
|
|
38
|
+
if isinstance(value, dict):
|
|
39
|
+
return {
|
|
40
|
+
str(redact(key, token)): (
|
|
41
|
+
"[REDACTED]"
|
|
42
|
+
if any(
|
|
43
|
+
part in key.lower()
|
|
44
|
+
for part in (
|
|
45
|
+
"token",
|
|
46
|
+
"authorization",
|
|
47
|
+
"password",
|
|
48
|
+
"secret",
|
|
49
|
+
"api_key",
|
|
50
|
+
"apikey",
|
|
51
|
+
)
|
|
52
|
+
)
|
|
53
|
+
else redact(item, token)
|
|
54
|
+
)
|
|
55
|
+
for key, item in value.items()
|
|
56
|
+
}
|
|
57
|
+
return value
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
_STATUS_ERRORS: dict[int, type[CKANError]] = {
|
|
61
|
+
400: CKANValidationError,
|
|
62
|
+
401: CKANAuthenticationError,
|
|
63
|
+
403: CKANAuthorizationError,
|
|
64
|
+
404: CKANNotFoundError,
|
|
65
|
+
409: CKANValidationError,
|
|
66
|
+
422: CKANValidationError,
|
|
67
|
+
429: CKANRateLimitError,
|
|
68
|
+
}
|
|
69
|
+
_TYPE_ERRORS: dict[str, type[CKANError]] = {
|
|
70
|
+
"Authentication Error": CKANAuthenticationError,
|
|
71
|
+
"Authorization Error": CKANAuthorizationError,
|
|
72
|
+
"Not Found Error": CKANNotFoundError,
|
|
73
|
+
"Validation Error": CKANValidationError,
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def parse_response(response: httpx.Response, action: str, token: str | None) -> JSONValue:
|
|
78
|
+
"""Return only the result; convert HTTP, CKAN, and malformed response failures."""
|
|
79
|
+
envelope = None
|
|
80
|
+
try:
|
|
81
|
+
envelope = ActionResponse.model_validate_json(response.content)
|
|
82
|
+
except (ValidationError, ValueError):
|
|
83
|
+
pass
|
|
84
|
+
status = response.status_code
|
|
85
|
+
if not response.is_success or (envelope is not None and not envelope.success):
|
|
86
|
+
details = redact(cast(JSONValue, envelope.error), token) if envelope else None
|
|
87
|
+
error_type = details.get("__type") if isinstance(details, dict) else None
|
|
88
|
+
error_type = error_type if isinstance(error_type, str) else None
|
|
89
|
+
error_class = _STATUS_ERRORS.get(status)
|
|
90
|
+
if error_class is None:
|
|
91
|
+
error_class = _TYPE_ERRORS.get(error_type or "")
|
|
92
|
+
if error_class is None:
|
|
93
|
+
error_class = CKANHTTPError if not response.is_success else CKANAPIError
|
|
94
|
+
raise error_class(
|
|
95
|
+
"CKAN action failed",
|
|
96
|
+
action=action,
|
|
97
|
+
status_code=status,
|
|
98
|
+
error_type=error_type,
|
|
99
|
+
details=details,
|
|
100
|
+
)
|
|
101
|
+
if envelope is None or "result" not in envelope.model_fields_set:
|
|
102
|
+
raise CKANProtocolError("Invalid CKAN response envelope", action=action, status_code=status)
|
|
103
|
+
# The JSON parser must reject non-standard NaN/Infinity accepted by some decoders.
|
|
104
|
+
result: JSONValue = envelope.result
|
|
105
|
+
try:
|
|
106
|
+
json.dumps(result, allow_nan=False)
|
|
107
|
+
except ValueError:
|
|
108
|
+
raise CKANProtocolError(
|
|
109
|
+
"Non-finite JSON result", action=action, status_code=status
|
|
110
|
+
) from None
|
|
111
|
+
return result
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
"""Synchronous CKAN clients and generic JSON Action API access."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import re
|
|
5
|
+
from collections.abc import Mapping
|
|
6
|
+
from types import TracebackType
|
|
7
|
+
from typing import Self
|
|
8
|
+
|
|
9
|
+
import httpx
|
|
10
|
+
|
|
11
|
+
from chile_open_data_sdk._internal.responses import parse_response, redact
|
|
12
|
+
from chile_open_data_sdk.config import ClientConfig
|
|
13
|
+
from chile_open_data_sdk.errors import CKANConnectionError, CKANError, CKANTimeoutError
|
|
14
|
+
from chile_open_data_sdk.types import JSONValue
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ActionService:
|
|
18
|
+
"""Generic CKAN JSON actions, with no automatic retries or redirects."""
|
|
19
|
+
|
|
20
|
+
def __init__(self, http: httpx.Client, config: ClientConfig) -> None:
|
|
21
|
+
self._http = http
|
|
22
|
+
self._config = config
|
|
23
|
+
|
|
24
|
+
def call(self, action: str, data: Mapping[str, JSONValue] | None = None) -> JSONValue:
|
|
25
|
+
"""POST action parameters as JSON and return the unwrapped result.
|
|
26
|
+
|
|
27
|
+
Action names must be single alphanumeric/underscore identifiers. Calls
|
|
28
|
+
may mutate server data: choose actions and credentials deliberately.
|
|
29
|
+
Unsupported payload values raise ValueError before any network request.
|
|
30
|
+
"""
|
|
31
|
+
if not re.fullmatch(r"[A-Za-z][A-Za-z0-9_]*", action):
|
|
32
|
+
raise ValueError("action must be a CKAN action identifier")
|
|
33
|
+
safe_action = str(redact(action, self._config.api_token))
|
|
34
|
+
if self._http.is_closed:
|
|
35
|
+
raise CKANError("Client is closed", action=safe_action)
|
|
36
|
+
try:
|
|
37
|
+
payload = json.dumps(dict(data) if data is not None else {}, allow_nan=False)
|
|
38
|
+
except (TypeError, ValueError):
|
|
39
|
+
raise ValueError("data must be a JSON-compatible mapping with finite numbers") from None
|
|
40
|
+
# Suppress raw HTTPX exception messages and chains, which can contain credentials.
|
|
41
|
+
failure: CKANError
|
|
42
|
+
try:
|
|
43
|
+
response = self._http.post(
|
|
44
|
+
self._config.action_url + "/" + action, content=payload.encode("utf-8")
|
|
45
|
+
)
|
|
46
|
+
except httpx.TimeoutException:
|
|
47
|
+
failure = CKANTimeoutError("CKAN request timed out", action=safe_action)
|
|
48
|
+
except httpx.RequestError:
|
|
49
|
+
failure = CKANConnectionError("CKAN request could not complete", action=safe_action)
|
|
50
|
+
else:
|
|
51
|
+
return parse_response(response, safe_action, self._config.api_token)
|
|
52
|
+
raise failure
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class CKANClient:
|
|
56
|
+
"""Reusable synchronous client for an explicitly configured CKAN site.
|
|
57
|
+
|
|
58
|
+
Supply either ``site_url`` (and optionally ``api_token``) or ``config``.
|
|
59
|
+
An injected HTTPX transport is owned and closed by this client.
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
def __init__(
|
|
63
|
+
self,
|
|
64
|
+
site_url: str | None = None,
|
|
65
|
+
*,
|
|
66
|
+
api_token: str | None = None,
|
|
67
|
+
config: ClientConfig | None = None,
|
|
68
|
+
transport: httpx.BaseTransport | None = None,
|
|
69
|
+
) -> None:
|
|
70
|
+
if config is not None and (site_url is not None or api_token is not None):
|
|
71
|
+
raise ValueError("Pass config or site_url/api_token, not both")
|
|
72
|
+
if config is None:
|
|
73
|
+
if site_url is None:
|
|
74
|
+
raise ValueError("CKANClient requires site_url or config")
|
|
75
|
+
config = ClientConfig(site_url=site_url, api_token=api_token)
|
|
76
|
+
self.config = config
|
|
77
|
+
headers = {
|
|
78
|
+
"User-Agent": config.user_agent,
|
|
79
|
+
"Accept": "application/json",
|
|
80
|
+
"Content-Type": "application/json",
|
|
81
|
+
}
|
|
82
|
+
if config.api_token is not None:
|
|
83
|
+
headers["Authorization"] = config.api_token
|
|
84
|
+
self._http = httpx.Client(
|
|
85
|
+
headers=headers,
|
|
86
|
+
timeout=httpx.Timeout(
|
|
87
|
+
connect=config.connect_timeout,
|
|
88
|
+
read=config.read_timeout,
|
|
89
|
+
write=config.write_timeout,
|
|
90
|
+
pool=config.pool_timeout,
|
|
91
|
+
),
|
|
92
|
+
limits=httpx.Limits(
|
|
93
|
+
max_connections=config.max_connections,
|
|
94
|
+
max_keepalive_connections=config.max_keepalive_connections,
|
|
95
|
+
),
|
|
96
|
+
verify=config.verify,
|
|
97
|
+
follow_redirects=False,
|
|
98
|
+
trust_env=False,
|
|
99
|
+
transport=transport,
|
|
100
|
+
)
|
|
101
|
+
self.actions = ActionService(self._http, config)
|
|
102
|
+
|
|
103
|
+
@property
|
|
104
|
+
def is_closed(self) -> bool:
|
|
105
|
+
"""Whether the owned HTTP client has been closed."""
|
|
106
|
+
return self._http.is_closed
|
|
107
|
+
|
|
108
|
+
def close(self) -> None:
|
|
109
|
+
"""Release the connection pool; safe to call more than once."""
|
|
110
|
+
self._http.close()
|
|
111
|
+
|
|
112
|
+
def __enter__(self) -> Self:
|
|
113
|
+
if self.is_closed:
|
|
114
|
+
raise CKANError("Client is closed", action="")
|
|
115
|
+
return self
|
|
116
|
+
|
|
117
|
+
def __exit__(
|
|
118
|
+
self,
|
|
119
|
+
exc_type: type[BaseException] | None,
|
|
120
|
+
exc: BaseException | None,
|
|
121
|
+
traceback: TracebackType | None,
|
|
122
|
+
) -> None:
|
|
123
|
+
self.close()
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
class ChileOpenDataClient(CKANClient):
|
|
127
|
+
"""CKAN client defaulting to https://datos.gob.cl/api/3/action."""
|
|
128
|
+
|
|
129
|
+
def __init__(
|
|
130
|
+
self,
|
|
131
|
+
site_url: str | None = None,
|
|
132
|
+
*,
|
|
133
|
+
api_token: str | None = None,
|
|
134
|
+
config: ClientConfig | None = None,
|
|
135
|
+
transport: httpx.BaseTransport | None = None,
|
|
136
|
+
) -> None:
|
|
137
|
+
super().__init__(
|
|
138
|
+
site_url="https://datos.gob.cl" if config is None and site_url is None else site_url,
|
|
139
|
+
api_token=api_token,
|
|
140
|
+
config=config,
|
|
141
|
+
transport=transport,
|
|
142
|
+
)
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"""Validated, immutable connection settings; constructors never read credentials implicitly."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
from math import isfinite
|
|
6
|
+
from typing import TypeGuard
|
|
7
|
+
from urllib.parse import urlsplit
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _is_connection_limit(value: object) -> TypeGuard[int]:
|
|
11
|
+
"""Narrow untrusted runtime input to an integer, excluding booleans."""
|
|
12
|
+
return isinstance(value, int) and not isinstance(value, bool)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _validate_tls_verification(value: object) -> None:
|
|
16
|
+
"""Reject untyped inputs that could accidentally disable TLS verification."""
|
|
17
|
+
if not isinstance(value, bool):
|
|
18
|
+
raise ValueError("verify must be a boolean")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass(frozen=True, slots=True)
|
|
22
|
+
class ClientConfig:
|
|
23
|
+
"""Configure a CKAN endpoint, timeouts in seconds, and connection pool limits.
|
|
24
|
+
|
|
25
|
+
``site_url`` can include a deployment prefix. TLS verification is enabled and
|
|
26
|
+
redirects are deliberately disabled. ``api_token`` is excluded from repr.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
site_url: str = "https://datos.gob.cl"
|
|
30
|
+
action_path: str = "/api/3/action"
|
|
31
|
+
api_token: str | None = field(default=None, repr=False)
|
|
32
|
+
user_agent: str = "chile-open-data-sdk/0.1.0"
|
|
33
|
+
connect_timeout: float = 5.0
|
|
34
|
+
read_timeout: float = 30.0
|
|
35
|
+
write_timeout: float = 30.0
|
|
36
|
+
pool_timeout: float = 5.0
|
|
37
|
+
max_connections: int = 100
|
|
38
|
+
max_keepalive_connections: int = 20
|
|
39
|
+
verify: bool = True
|
|
40
|
+
|
|
41
|
+
def __post_init__(self) -> None:
|
|
42
|
+
"""Reject ambiguous endpoints and invalid settings without echoing credentials."""
|
|
43
|
+
try:
|
|
44
|
+
url = urlsplit(self.site_url)
|
|
45
|
+
valid = (
|
|
46
|
+
url.scheme in {"http", "https"}
|
|
47
|
+
and bool(url.hostname)
|
|
48
|
+
and url.username is None
|
|
49
|
+
and url.password is None
|
|
50
|
+
and not url.query
|
|
51
|
+
and not url.fragment
|
|
52
|
+
and url.port != 0
|
|
53
|
+
and not any(c.isspace() or ord(c) < 32 for c in self.site_url)
|
|
54
|
+
and "\\" not in self.site_url
|
|
55
|
+
and all(p not in {".", ".."} for p in url.path.split("/"))
|
|
56
|
+
and "%" not in url.path
|
|
57
|
+
)
|
|
58
|
+
except ValueError:
|
|
59
|
+
valid = False
|
|
60
|
+
if not valid:
|
|
61
|
+
raise ValueError(
|
|
62
|
+
"site_url must be an HTTP(S) URL without credentials, query or fragment"
|
|
63
|
+
)
|
|
64
|
+
if not re.fullmatch(r"/(?:[A-Za-z0-9_-]+/)*[A-Za-z0-9_-]+/?", self.action_path):
|
|
65
|
+
raise ValueError("action_path must contain only absolute path segments")
|
|
66
|
+
object.__setattr__(self, "site_url", self.site_url.rstrip("/"))
|
|
67
|
+
object.__setattr__(self, "action_path", self.action_path.rstrip("/"))
|
|
68
|
+
for value in (
|
|
69
|
+
self.connect_timeout,
|
|
70
|
+
self.read_timeout,
|
|
71
|
+
self.write_timeout,
|
|
72
|
+
self.pool_timeout,
|
|
73
|
+
):
|
|
74
|
+
if isinstance(value, bool) or not isfinite(value) or value <= 0:
|
|
75
|
+
raise ValueError("Timeouts must be finite positive seconds")
|
|
76
|
+
if (
|
|
77
|
+
not _is_connection_limit(self.max_connections)
|
|
78
|
+
or self.max_connections < 1
|
|
79
|
+
or not _is_connection_limit(self.max_keepalive_connections)
|
|
80
|
+
or not 0 <= self.max_keepalive_connections <= self.max_connections
|
|
81
|
+
):
|
|
82
|
+
raise ValueError("Connection limits must be integers with 0 <= keepalive <= maximum")
|
|
83
|
+
for header_value in (self.user_agent, self.api_token):
|
|
84
|
+
if header_value is not None and (
|
|
85
|
+
not header_value or any(ord(c) < 32 or ord(c) > 126 for c in header_value)
|
|
86
|
+
):
|
|
87
|
+
raise ValueError("Header values must be nonempty printable ASCII")
|
|
88
|
+
_validate_tls_verification(self.verify)
|
|
89
|
+
|
|
90
|
+
@property
|
|
91
|
+
def action_url(self) -> str:
|
|
92
|
+
"""Return the normalized Action API base URL."""
|
|
93
|
+
return self.site_url + self.action_path
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""SDK exceptions with safe action context; raw requests and responses are not retained."""
|
|
2
|
+
|
|
3
|
+
from chile_open_data_sdk.types import JSONValue
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class ChileOpenDataError(Exception):
|
|
7
|
+
"""Base exception for SDK runtime failures."""
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class CKANError(ChileOpenDataError):
|
|
11
|
+
"""CKAN failure with sanitized diagnostic context."""
|
|
12
|
+
|
|
13
|
+
def __init__(
|
|
14
|
+
self,
|
|
15
|
+
message: str,
|
|
16
|
+
*,
|
|
17
|
+
action: str,
|
|
18
|
+
status_code: int | None = None,
|
|
19
|
+
error_type: str | None = None,
|
|
20
|
+
details: JSONValue = None,
|
|
21
|
+
) -> None:
|
|
22
|
+
super().__init__(message)
|
|
23
|
+
self.action = action
|
|
24
|
+
self.status_code = status_code
|
|
25
|
+
self.error_type = error_type
|
|
26
|
+
self.details = details
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class CKANAPIError(CKANError):
|
|
30
|
+
"""CKAN returned success=false."""
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class CKANProtocolError(CKANError):
|
|
34
|
+
"""The response was not a valid CKAN JSON envelope."""
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class CKANHTTPError(CKANError):
|
|
38
|
+
"""An HTTP response was unsuccessful, including redirects."""
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class CKANAuthenticationError(CKANAPIError, CKANHTTPError):
|
|
42
|
+
"""Authentication is required or failed."""
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class CKANAuthorizationError(CKANAPIError, CKANHTTPError):
|
|
46
|
+
"""The server denied permission."""
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class CKANNotFoundError(CKANAPIError, CKANHTTPError):
|
|
50
|
+
"""The requested action or entity was not found."""
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class CKANValidationError(CKANAPIError, CKANHTTPError):
|
|
54
|
+
"""The server rejected the action parameters."""
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class CKANRateLimitError(CKANAPIError, CKANHTTPError):
|
|
58
|
+
"""The server rate limited the request; it was not retried."""
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class CKANTimeoutError(CKANError):
|
|
62
|
+
"""A connect, read, write, or pool timeout occurred."""
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class CKANConnectionError(CKANError):
|
|
66
|
+
"""HTTPX could not complete the request."""
|
|
File without changes
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: chile-open-data-sdk
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: An unofficial, typed Python SDK for Chile's datos.gob.cl and CKAN Action API
|
|
5
|
+
Keywords: chile,open-data,ckan,sdk
|
|
6
|
+
Author: Eli-ezer Reuven Ramirez Ruiz
|
|
7
|
+
Author-email: Eli-ezer Reuven Ramirez Ruiz <ramirez.ruiz.eliezer.reuven@gmail.com>
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE.md
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
16
|
+
Classifier: Typing :: Typed
|
|
17
|
+
Requires-Dist: httpx>=0.28.1
|
|
18
|
+
Requires-Dist: pydantic>=2.12.0,<3
|
|
19
|
+
Requires-Python: >=3.11
|
|
20
|
+
Project-URL: Repository, https://github.com/ezer-mackenzie/chile-open-data-sdk
|
|
21
|
+
Project-URL: Issues, https://github.com/ezer-mackenzie/chile-open-data-sdk/issues
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
|
|
24
|
+
# Chile Open Data SDK
|
|
25
|
+
|
|
26
|
+
An unofficial, typed Python SDK for Chile's **datos.gob.cl**, with a reusable
|
|
27
|
+
CKAN Action API core. This community project is **not an official SDK of the
|
|
28
|
+
Government of Chile or the maintainers of datos.gob.cl**.
|
|
29
|
+
|
|
30
|
+
## v0.1.0 scope
|
|
31
|
+
|
|
32
|
+
This first release provides a synchronous client, validated configuration,
|
|
33
|
+
connection pooling, JSON action calls, token authentication, and structured errors.
|
|
34
|
+
It is an alpha foundation with a deliberately small public API.
|
|
35
|
+
|
|
36
|
+
Typed dataset services, DataStore wrappers, pagination, async clients, downloads,
|
|
37
|
+
and pandas/Polars integrations are planned for later milestones; they are not
|
|
38
|
+
available in v0.1.0. Generic actions can already query catalog and DataStore
|
|
39
|
+
endpoints when the server supports them.
|
|
40
|
+
|
|
41
|
+
## Installation
|
|
42
|
+
|
|
43
|
+
Python 3.11 or later is required. Before publication, install from a local checkout:
|
|
44
|
+
|
|
45
|
+
```console
|
|
46
|
+
uv sync --locked
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Or build and install the wheel with a standard Python installer:
|
|
50
|
+
|
|
51
|
+
```console
|
|
52
|
+
uv build
|
|
53
|
+
python -m pip install dist/chile_open_data_sdk-0.1.0-py3-none-any.whl
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
After v0.1.0 is published, installation will be:
|
|
57
|
+
|
|
58
|
+
```console
|
|
59
|
+
python -m pip install chile-open-data-sdk==0.1.0
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
The distribution is `chile-open-data-sdk`; the import is `chile_open_data_sdk`.
|
|
63
|
+
|
|
64
|
+
## Quick start
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
from chile_open_data_sdk import ChileOpenDataClient
|
|
68
|
+
|
|
69
|
+
with ChileOpenDataClient() as client:
|
|
70
|
+
result = client.actions.call("package_search", {"q": "transport", "rows": 5})
|
|
71
|
+
datasets = result.get("results") if isinstance(result, dict) else None
|
|
72
|
+
if isinstance(datasets, list):
|
|
73
|
+
for dataset in datasets:
|
|
74
|
+
if isinstance(dataset, dict):
|
|
75
|
+
print(dataset.get("title"))
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
The default endpoint is `https://datos.gob.cl/api/3/action`. Calls return the
|
|
79
|
+
unwrapped JSON `result`, preserving additional server fields. Availability and
|
|
80
|
+
metadata quality depend on the upstream portal.
|
|
81
|
+
|
|
82
|
+
## Another CKAN site
|
|
83
|
+
|
|
84
|
+
```python
|
|
85
|
+
from chile_open_data_sdk import CKANClient
|
|
86
|
+
|
|
87
|
+
with CKANClient(site_url="https://demo.ckan.org") as client:
|
|
88
|
+
print(client.actions.call("package_list", {"limit": 5}))
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Authentication and errors
|
|
92
|
+
|
|
93
|
+
Credentials are explicit and are sent only in the `Authorization` header. The
|
|
94
|
+
constructor does not read environment credentials. Your application can do so:
|
|
95
|
+
|
|
96
|
+
```python
|
|
97
|
+
import os
|
|
98
|
+
|
|
99
|
+
from chile_open_data_sdk import ChileOpenDataClient
|
|
100
|
+
from chile_open_data_sdk.errors import CKANError
|
|
101
|
+
|
|
102
|
+
try:
|
|
103
|
+
with ChileOpenDataClient(api_token=os.environ.get("CHILE_OPEN_DATA_API_TOKEN")) as client:
|
|
104
|
+
result = client.actions.call("package_list", {"limit": 5})
|
|
105
|
+
except CKANError as error:
|
|
106
|
+
print(error.action, error.status_code, error.error_type)
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Generic calls use JSON POST and **never retry automatically**, including read
|
|
110
|
+
operations. They can execute writes: the caller chooses the action and the server
|
|
111
|
+
enforces permissions. Redirects are not followed. Configuration and SDK error
|
|
112
|
+
representations exclude the configured token; do not log raw payloads or credentials.
|
|
113
|
+
|
|
114
|
+
## Documentation
|
|
115
|
+
|
|
116
|
+
- [Getting started](docs/getting-started.md)
|
|
117
|
+
- [Generic actions and querying examples](docs/actions.md)
|
|
118
|
+
- [Configuration and authentication](docs/configuration.md)
|
|
119
|
+
- [Error handling](docs/errors.md)
|
|
120
|
+
- [Public API reference](docs/api.md)
|
|
121
|
+
- [Development tools and benchmarks](docs/development.md)
|
|
122
|
+
- [Architecture](docs/architecture.md)
|
|
123
|
+
- [Compatibility and limitations](docs/compatibility.md)
|
|
124
|
+
- [Roadmap](docs/roadmap.md)
|
|
125
|
+
- [Release process](docs/releasing.md)
|
|
126
|
+
- [v0.1.0 release notes](docs/release-notes.md)
|
|
127
|
+
- [Changelog](CHANGELOG.md)
|
|
128
|
+
|
|
129
|
+
Build the documentation locally with `uv run --group docs mkdocs build --strict`,
|
|
130
|
+
or preview with `uv run --group docs mkdocs serve`. No hosted documentation URL
|
|
131
|
+
is claimed until deployment is configured.
|
|
132
|
+
|
|
133
|
+
## Development
|
|
134
|
+
|
|
135
|
+
```console
|
|
136
|
+
uv sync --locked --group docs
|
|
137
|
+
uv run ruff check .
|
|
138
|
+
uv run ruff format --check .
|
|
139
|
+
uv run mypy src
|
|
140
|
+
uv run basedpyright
|
|
141
|
+
uv run pytest
|
|
142
|
+
uv run --group docs mkdocs build --strict
|
|
143
|
+
uv build
|
|
144
|
+
uv run python scripts/check_artifacts.py
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Normal tests use mocked HTTP; live portal tests are opt-in. See
|
|
148
|
+
[Contributing](CONTRIBUTING.md) and [Security](SECURITY.md).
|
|
149
|
+
|
|
150
|
+
Existing CKAN clients such as [ckanapi](https://github.com/ckan/ckanapi) serve the
|
|
151
|
+
community already. This project's direction is Chile-first defaults, modern
|
|
152
|
+
Python typing, and progressively richer sync/async workflows.
|
|
153
|
+
|
|
154
|
+
## License
|
|
155
|
+
|
|
156
|
+
[MIT](LICENSE.md). Portal datasets may have their own licenses; the SDK's license
|
|
157
|
+
does not determine permission to use upstream data.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
chile_open_data_sdk/__init__.py,sha256=cZM8N04lEAF5LKohv6Y4gncfJHxvkxaQPN-GSv_xYCo,337
|
|
2
|
+
chile_open_data_sdk/_internal/__init__.py,sha256=-aV_bdzUpMEuUpWP6np8AckdhNg8nVPLRcpaYpCryMo,58
|
|
3
|
+
chile_open_data_sdk/_internal/responses.py,sha256=hqlm6ZmJ7VNJfRK6eUZ5taS35TEXEik_2uF028aWjYY,3826
|
|
4
|
+
chile_open_data_sdk/client.py,sha256=V192S_Eddr2aaA7WHVTr2Sbo_37X3DsbKQ_P6zUbitM,5284
|
|
5
|
+
chile_open_data_sdk/config.py,sha256=2RCatEv9bIZt2PR72SpYo4PJm-DqDDvGsAYTVd7Ad7I,3845
|
|
6
|
+
chile_open_data_sdk/errors.py,sha256=MESVxHjzupP6x6jyj_4YoYBk5AgzUO-CCyZhIVnRaL0,1726
|
|
7
|
+
chile_open_data_sdk/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
8
|
+
chile_open_data_sdk/types.py,sha256=VIkGbsOPuQVQWvUHuDVExFMUNfxgnZmmmTsG9YX2_0g,196
|
|
9
|
+
chile_open_data_sdk-0.1.0.dist-info/licenses/LICENSE.md,sha256=_52YphtXeFMUcoxi_K3XL3uasy7QqRl_0nbFfBfTHrM,1084
|
|
10
|
+
chile_open_data_sdk-0.1.0.dist-info/WHEEL,sha256=-i9oRNYVXXZJUIYl5zclLIg6onEb0NLibTX34uln84w,81
|
|
11
|
+
chile_open_data_sdk-0.1.0.dist-info/METADATA,sha256=05V1qBuynEMfh9-ZGCs22-wC8AukImFx0sAXQNvDWHs,5462
|
|
12
|
+
chile_open_data_sdk-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Eli-ezer Reuven Ramirez Ruiz
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
6
|
+
|
|
7
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
8
|
+
|
|
9
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|