pd4castr-api-sdk 0.0.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.
@@ -0,0 +1,13 @@
1
+ from .client import Client
2
+ from .errors import ApiError, NotFoundError
3
+ from .models import Organisation, User
4
+
5
+ __all__ = [
6
+ "Client",
7
+ "ApiError",
8
+ "NotFoundError",
9
+ "Organisation",
10
+ "User",
11
+ ]
12
+
13
+ __version__ = "0.0.0"
@@ -0,0 +1,69 @@
1
+ from __future__ import annotations
2
+
3
+ import time
4
+ from dataclasses import dataclass
5
+ from typing import Any
6
+
7
+ from .errors import AuthenticationError
8
+ from .transport import Transport
9
+
10
+
11
+ def _normalize_auth0_domain(domain: str) -> str:
12
+ if domain.startswith("https://") or domain.startswith("http://"):
13
+ return domain.rstrip("/")
14
+ return f"https://{domain.rstrip('/')}"
15
+
16
+
17
+ @dataclass
18
+ class ClientCredentialsAuth:
19
+ auth0_domain: str
20
+ audience: str
21
+ client_id: str
22
+ client_secret: str
23
+
24
+ _access_token: str | None = None
25
+ _expires_at_epoch: float | None = None
26
+
27
+ def get_access_token(self, transport: Transport) -> str:
28
+ if self._access_token and self._expires_at_epoch:
29
+ # refresh a little early to avoid clock skew.
30
+ if time.time() < (self._expires_at_epoch - 10):
31
+ return self._access_token
32
+
33
+ token_url = f"{_normalize_auth0_domain(self.auth0_domain)}/oauth/token"
34
+
35
+ response = transport.request(
36
+ "POST",
37
+ token_url,
38
+ headers={"content-type": "application/x-www-form-urlencoded"},
39
+ data={
40
+ "grant_type": "client_credentials",
41
+ "client_id": self.client_id,
42
+ "client_secret": self.client_secret,
43
+ "audience": self.audience,
44
+ },
45
+ )
46
+
47
+ if response.status_code >= 400:
48
+ raise AuthenticationError(
49
+ f"auth0 token request failed ({response.status_code})"
50
+ )
51
+
52
+ try:
53
+ payload: dict[str, Any] = response.json()
54
+ except Exception as exc: # pragma: no cover
55
+ raise AuthenticationError("auth0 token response was not json") from exc
56
+
57
+ access_token = payload.get("access_token")
58
+ expires_in = payload.get("expires_in")
59
+
60
+ if not isinstance(access_token, str):
61
+ raise AuthenticationError("auth0 token response missing access_token")
62
+
63
+ if not isinstance(expires_in, int):
64
+ raise AuthenticationError("auth0 token response missing expires_in")
65
+
66
+ self._access_token = access_token
67
+ self._expires_at_epoch = time.time() + expires_in
68
+
69
+ return access_token
@@ -0,0 +1,123 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ import httpx
6
+
7
+ from .auth import ClientCredentialsAuth
8
+ from .config import (
9
+ DEFAULT_API_URL,
10
+ DEFAULT_AUTH0_AUDIENCE,
11
+ DEFAULT_AUTH0_DOMAIN,
12
+ load_config,
13
+ )
14
+ from .errors import ApiError, NotFoundError
15
+ from .models import Model, User
16
+ from .transport import HttpxSyncTransport, Transport
17
+
18
+
19
+ def _normalize_base_url(base_url: str) -> str:
20
+ return base_url.rstrip("/")
21
+
22
+
23
+ class Client:
24
+ def __init__(
25
+ self,
26
+ *,
27
+ base_url: str | None = None,
28
+ auth0_domain: str | None = None,
29
+ auth0_audience: str | None = None,
30
+ client_id: str,
31
+ client_secret: str,
32
+ timeout: float = 30.0,
33
+ transport: Transport | None = None,
34
+ ) -> None:
35
+ config = load_config()
36
+
37
+ resolved_base_url = base_url or config.base_url or DEFAULT_API_URL
38
+ resolved_domain = auth0_domain or config.auth0_domain or DEFAULT_AUTH0_DOMAIN
39
+ resolved_audience = (
40
+ auth0_audience or config.auth0_audience or DEFAULT_AUTH0_AUDIENCE
41
+ )
42
+
43
+ self._base_url = _normalize_base_url(resolved_base_url)
44
+ self._auth = ClientCredentialsAuth(
45
+ auth0_domain=resolved_domain,
46
+ audience=resolved_audience,
47
+ client_id=client_id,
48
+ client_secret=client_secret,
49
+ )
50
+
51
+ self._transport = transport or HttpxSyncTransport(
52
+ client=httpx.Client(timeout=timeout)
53
+ )
54
+
55
+ def close(self) -> None:
56
+ self._transport.close()
57
+
58
+ def __enter__(self) -> Client:
59
+ return self
60
+
61
+ def __exit__(self, exc_type: object, exc: object, tb: object) -> None:
62
+ self.close()
63
+
64
+ def get_models(self, *, time_horizon: str | None = None) -> list[Model]:
65
+ params: dict[str, Any] = {}
66
+ if time_horizon is not None:
67
+ params["timeHorizon"] = time_horizon
68
+
69
+ data = self._request_json("GET", "/model", params=params)
70
+ return [Model.model_validate(item) for item in data]
71
+
72
+ def get_model(self, model_id: str) -> Model:
73
+ data = self._request_json("GET", f"/model/{model_id}")
74
+ return Model.model_validate(data)
75
+
76
+ def get_current_user(self) -> User:
77
+ data = self._request_json("GET", "/user/current")
78
+ return User.model_validate(data)
79
+
80
+ def _request_json(
81
+ self,
82
+ method: str,
83
+ path: str,
84
+ *,
85
+ params: dict[str, Any] | None = None,
86
+ ) -> Any:
87
+ url = f"{self._base_url}{path}"
88
+ token = self._auth.get_access_token(self._transport)
89
+
90
+ response = self._transport.request(
91
+ method,
92
+ url,
93
+ headers={
94
+ "authorization": f"Bearer {token}",
95
+ "accept": "application/json",
96
+ },
97
+ params=params,
98
+ )
99
+
100
+ if response.status_code == 404:
101
+ raise NotFoundError("resource not found")
102
+
103
+ if response.status_code >= 400:
104
+ body: Any | None
105
+ try:
106
+ body = response.json()
107
+ except Exception:
108
+ body = response.text
109
+
110
+ raise ApiError(
111
+ status_code=response.status_code,
112
+ message="request failed",
113
+ body=body,
114
+ )
115
+
116
+ try:
117
+ return response.json()
118
+ except ValueError as e:
119
+ raise ApiError(
120
+ status_code=response.status_code,
121
+ message=f"invalid JSON response: {e}",
122
+ body=response.text,
123
+ ) from e
@@ -0,0 +1,29 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ from dataclasses import dataclass
5
+
6
+ DEFAULT_API_URL = "https://api.v2.pd4castr.com.au"
7
+ DEFAULT_AUTH0_DOMAIN = "pdview.au.auth0.com"
8
+ DEFAULT_AUTH0_AUDIENCE = "https://api.pd4castr.com.au"
9
+
10
+
11
+ @dataclass(frozen=True)
12
+ class ClientConfig:
13
+ base_url: str
14
+ auth0_domain: str
15
+ auth0_audience: str
16
+
17
+
18
+ def load_config() -> ClientConfig:
19
+ # allow overrides for advanced usage
20
+ base_url = os.environ.get("PD4CASTR_API_URL", DEFAULT_API_URL)
21
+ audience = os.environ.get("PD4CASTR_AUTH0_AUDIENCE", DEFAULT_AUTH0_AUDIENCE)
22
+
23
+ domain = os.environ.get("PD4CASTR_AUTH0_DOMAIN", DEFAULT_AUTH0_DOMAIN)
24
+
25
+ return ClientConfig(
26
+ base_url=base_url,
27
+ auth0_domain=domain,
28
+ auth0_audience=audience,
29
+ )
@@ -0,0 +1,27 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Any
5
+
6
+
7
+ class Pd4castrApiError(Exception):
8
+ pass
9
+
10
+
11
+ class AuthenticationError(Pd4castrApiError):
12
+ pass
13
+
14
+
15
+ class NotFoundError(Pd4castrApiError):
16
+ pass
17
+
18
+
19
+ # exceptions must be mutable so python can attach traceback/cause details
20
+ @dataclass
21
+ class ApiError(Pd4castrApiError):
22
+ status_code: int
23
+ message: str
24
+ body: Any | None = None
25
+
26
+ def __str__(self) -> str: # pragma: no cover
27
+ return f"{self.status_code}: {self.message}"
@@ -0,0 +1,83 @@
1
+ from __future__ import annotations
2
+
3
+ from enum import Enum
4
+
5
+ from pydantic import BaseModel, ConfigDict
6
+
7
+
8
+ class Pd4castrBaseModel(BaseModel):
9
+ model_config = ConfigDict(extra="ignore")
10
+
11
+
12
+ class FileFormat(str, Enum):
13
+ json = "json"
14
+ csv = "csv"
15
+ parquet = "parquet"
16
+
17
+
18
+ class TimeHorizon(Pd4castrBaseModel):
19
+ name: str
20
+ shortName: str
21
+ key: str
22
+
23
+
24
+ class ModelMetadata(Pd4castrBaseModel):
25
+ description: str | None = None
26
+ releaseDate: str | None = None
27
+ resolution: str | None = None
28
+ baseComparisonModel: str | None = None
29
+
30
+
31
+ class UISpecification(Pd4castrBaseModel):
32
+ seriesKey: bool | None = None
33
+ colours: list[str] | None = None
34
+
35
+
36
+ class ColumnSpecification(Pd4castrBaseModel):
37
+ name: str
38
+ type: str
39
+ ui: UISpecification | None = None
40
+
41
+
42
+ class SensitivitySpecification(Pd4castrBaseModel):
43
+ name: str
44
+ categories: list[str]
45
+
46
+
47
+ class ForecastVariableSpecification(Pd4castrBaseModel):
48
+ name: str
49
+ shortName: str
50
+ key: str
51
+
52
+
53
+ class Model(Pd4castrBaseModel):
54
+ id: str
55
+ modelGroupId: str
56
+ name: str
57
+ displayName: str
58
+ notes: str
59
+ revision: int
60
+ dockerImage: str
61
+ createdAt: str
62
+ horizon: TimeHorizon
63
+ modelMetadata: ModelMetadata
64
+ outputSpecification: list[ColumnSpecification]
65
+ tags: list[str]
66
+ sensitivities: list[SensitivitySpecification]
67
+ outputFileFormat: FileFormat
68
+ displayTimezone: str
69
+ forecastVariable: ForecastVariableSpecification
70
+
71
+
72
+ class Organisation(Pd4castrBaseModel):
73
+ id: str
74
+ displayName: str
75
+ slug: str
76
+ domains: list[str]
77
+ permissions: list[str]
78
+
79
+
80
+ class User(Pd4castrBaseModel):
81
+ id: str
82
+ email: str
83
+ organisation: Organisation | None = None
@@ -0,0 +1 @@
1
+
@@ -0,0 +1,48 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Any, Protocol
5
+
6
+ import httpx
7
+
8
+
9
+ class Transport(Protocol):
10
+ def request(
11
+ self,
12
+ method: str,
13
+ url: str,
14
+ *,
15
+ headers: dict[str, str] | None = None,
16
+ params: dict[str, Any] | None = None,
17
+ json: Any | None = None,
18
+ data: Any | None = None,
19
+ ) -> httpx.Response: ...
20
+
21
+ def close(self) -> None: ...
22
+
23
+
24
+ @dataclass
25
+ class HttpxSyncTransport:
26
+ client: httpx.Client
27
+
28
+ def request(
29
+ self,
30
+ method: str,
31
+ url: str,
32
+ *,
33
+ headers: dict[str, str] | None = None,
34
+ params: dict[str, Any] | None = None,
35
+ json: Any | None = None,
36
+ data: Any | None = None,
37
+ ) -> httpx.Response:
38
+ return self.client.request(
39
+ method,
40
+ url,
41
+ headers=headers,
42
+ params=params,
43
+ json=json,
44
+ data=data,
45
+ )
46
+
47
+ def close(self) -> None:
48
+ self.client.close()
@@ -0,0 +1,338 @@
1
+ Metadata-Version: 2.4
2
+ Name: pd4castr-api-sdk
3
+ Version: 0.0.0
4
+ Summary: Python client for the pd4castr API
5
+ Author: pd4castr
6
+ License: MIT License (MIT)
7
+
8
+ Copyright (c) 2026 Endgame Economics Pty Ltd t/as Endgame Analytics
9
+ <contact@endgameanalytics.com.au> https://endgameanalytics.com.au
10
+
11
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
12
+ this software and associated documentation files (the "Software"), to deal in
13
+ the Software without restriction, including without limitation the rights to
14
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
15
+ the Software, and to permit persons to whom the Software is furnished to do so,
16
+ subject to the following conditions:
17
+
18
+ The above copyright notice and this permission notice shall be included in all
19
+ copies or substantial portions of the Software.
20
+
21
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
22
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
23
+ FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
24
+ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
25
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
26
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
27
+ License-File: LICENSE.md
28
+ Requires-Python: >=3.11
29
+ Requires-Dist: httpx>=0.27.2
30
+ Requires-Dist: pydantic>=2.7.0
31
+ Provides-Extra: dev
32
+ Requires-Dist: mypy>=1.11.0; extra == 'dev'
33
+ Requires-Dist: pytest-cov>=5.0.0; extra == 'dev'
34
+ Requires-Dist: pytest-sugar>=1.0.0; extra == 'dev'
35
+ Requires-Dist: pytest>=8.2.0; extra == 'dev'
36
+ Requires-Dist: respx>=0.21.1; extra == 'dev'
37
+ Requires-Dist: ruff>=0.6.0; extra == 'dev'
38
+ Description-Content-Type: text/markdown
39
+
40
+ <div align="center">
41
+ <img src="./docs/assets/logo.png" alt="pdView logo" width="92">
42
+
43
+ <h1>pd4castr API SDK</h1>
44
+
45
+ <p>Official Python client for the pd4castr API</p>
46
+
47
+ <p>
48
+ <a href="#installation">Installation</a> •
49
+ <a href="#quick-start">Quick Start</a> •
50
+ <a href="#usage">Usage</a> •
51
+ <a href="#api-reference">API Reference</a>
52
+ </p>
53
+ </div>
54
+
55
+ ---
56
+
57
+ ## Installation
58
+
59
+ > Requires Python >= 3.8
60
+
61
+ ```bash
62
+ pip install pd4castr-api-sdk
63
+ ```
64
+
65
+ ## Quick Start
66
+
67
+ Authenticate using your [pd4castr](https://v2.pd4castr.com.au) API credentials:
68
+
69
+ ```python
70
+ from pd4castr_api_sdk import Client
71
+
72
+ client = Client(
73
+ client_id="my-client-id",
74
+ client_secret="my-client-secret",
75
+ )
76
+
77
+ models = client.get_models()
78
+ print(f"Found {len(models)} models")
79
+
80
+ client.close()
81
+ ```
82
+
83
+ ### Context Manager
84
+
85
+ The client implements context manager protocol for automatic cleanup:
86
+
87
+ ```python
88
+ with Client(
89
+ client_id="my-client-id",
90
+ client_secret="my-client-secret",
91
+ ) as client:
92
+ models = client.get_models()
93
+ ```
94
+
95
+ ## Usage
96
+
97
+ ### List all Models
98
+
99
+ [Reference: `Client.getModels`](#get_models)
100
+
101
+ Retrieve all available models, optionally filtered by time horizon.
102
+
103
+ ```python
104
+ # Get all models
105
+ models = client.get_models()
106
+
107
+ # Filter by time horizon
108
+ models = client.get_models(time_horizon="day_ahead")
109
+ ```
110
+
111
+ ### Get a Model
112
+
113
+ [Reference: `Client.get_model`](#clientgetmodel)
114
+
115
+ Retrieve detailed information for a specific model.
116
+
117
+ ```python
118
+ model = client.get_model("model-id")
119
+ print(f"Model: {model.displayName}")
120
+ ```
121
+
122
+ ### Get Current User
123
+
124
+ [Reference: `Client.get_current_user`](#clientgetcurrentuser)
125
+
126
+ Retrieve information about the currently authenticated user.
127
+
128
+ ```python
129
+ user = client.get_current_user()
130
+ print(f"User: {user.email}")
131
+ if user.organisation:
132
+ print(f"Organisation: {user.organisation.displayName}")
133
+ ```
134
+
135
+ ### Error Handling
136
+
137
+ [Reference: Exceptions](#exceptions)
138
+
139
+ Handle common error cases with typed exceptions.
140
+
141
+ ```python
142
+ from pd4castr_api_sdk import NotFoundError, ApiError
143
+
144
+ try:
145
+ model = client.get_model("invalid-id")
146
+ except NotFoundError:
147
+ print("Model not found")
148
+ except ApiError as e:
149
+ print(f"API error {e.status_code}: {e.message}")
150
+ ```
151
+
152
+ ## API Reference
153
+
154
+ **Client:**
155
+
156
+ - [`Client`](#client) - Main API client
157
+ - [`Client#getModels`](#clientgetmodels) - List all models
158
+ - [`Client#getModel`](#clientgetmodel) - Get a specific model
159
+ - [`Client#getCurrentUser`](#clientgetcurrentuser) - Get the current user
160
+
161
+ **Types:**
162
+
163
+ - [`Model`](#model) - Model information and related types
164
+ - [`User`](#user) - User information and related types
165
+
166
+ **Exceptions:**
167
+
168
+ - [`ApiError`](#apierror) - Base API error
169
+ - [`NotFoundError`](#notfounderror) - Resource not found error
170
+
171
+ ### `Client`
172
+
173
+ ```python
174
+ Client(
175
+ client_id: str,
176
+ client_secret: str,
177
+ timeout: float = 30.0,
178
+ )
179
+ ```
180
+
181
+ Main client class for interacting with the pd4castr API. Supports context
182
+ manager protocol for automatic cleanup.
183
+
184
+ **Configuration:**
185
+
186
+ - `client_id` - OAuth client ID for authentication
187
+ - `client_secret` - OAuth client secret for authentication
188
+ - `timeout` - Request timeout in seconds (default: 30.0)
189
+
190
+ ### `Client#getModels`
191
+
192
+ List all models with optional time horizon filter.
193
+
194
+ `Client#getModels(*, time_horizon: str | None = None) -> list[`[`Model`](#model)`]`
195
+
196
+ **Parameters:**
197
+
198
+ - `time_horizon` (optional) - Filter models by time horizon:
199
+ - `day_ahead`
200
+ - `week_ahead`
201
+ - `quarterly`
202
+ - `historical`
203
+
204
+ **Returns:** A list of [`Model`](#model) instances
205
+
206
+ ---
207
+
208
+ ### `Client#getModel`
209
+
210
+ Get a specific model by ID.
211
+
212
+ `Client#get_model(model_id: str) ->`[`Model`](#model)
213
+
214
+ **Parameters:**
215
+
216
+ - `model_id` - Unique model identifier
217
+
218
+ **Returns:** [`Model`](#model) instance
219
+
220
+ **Raises:**
221
+
222
+ - [`NotFoundError`](#notfounderror) if model doesn't exist
223
+
224
+ ---
225
+
226
+ ### `Client#getCurrentUser`
227
+
228
+ Get the currently authenticated user.
229
+
230
+ `Client#get_current_user() ->`[`User`](#user)
231
+
232
+ **Returns:** [`User`](#user) instance
233
+
234
+ ---
235
+
236
+ ### Types
237
+
238
+ All types are implemented as
239
+ [pydantic models](https://docs.pydantic.dev/latest/).
240
+
241
+ #### `Model`
242
+
243
+ ```python
244
+ class Model(BaseModel):
245
+ id: str
246
+ modelGroupId: str
247
+ name: str
248
+ displayName: str
249
+ displayTimezone: str
250
+ revision: int
251
+ dockerImage: str
252
+ notes: str
253
+ createdAt: str
254
+ horizon: TimeHorizon
255
+ modelMetadata: ModelMetadata
256
+ outputSpecification: list[ColumnSpecification]
257
+ tags: list[str]
258
+ sensitivities: list[SensitivitySpecification]
259
+ outputFileFormat: FileFormat
260
+ forecastVariable: ForecastVariableSpecification
261
+
262
+ class TimeHorizon(BaseModel):
263
+ name: str
264
+ shortName: str
265
+ key: str
266
+
267
+ class ModelMetadata(BaseModel):
268
+ description: str | None
269
+ releaseDate: str | None
270
+ resolution: str | None
271
+ baseComparisonModel: str | None
272
+
273
+ class ColumnSpecification(BaseModel):
274
+ name: str
275
+ type: str
276
+ ui: UISpecification | None
277
+
278
+ class UISpecification(BaseModel):
279
+ seriesKey: bool | None
280
+ colours: list[str] | None
281
+
282
+ class SensitivitySpecification(BaseModel):
283
+ name: str
284
+ categories: list[str]
285
+
286
+ class ForecastVariableSpecification(BaseModel):
287
+ name: str
288
+ shortName: str
289
+ key: str
290
+
291
+ class FileFormat(Enum):
292
+ json = "json"
293
+ csv = "csv"
294
+ parquet = "parquet"
295
+ ```
296
+
297
+ #### `User`
298
+
299
+ ```python
300
+ class User(BaseModel):
301
+ id: str
302
+ email: str
303
+ organisation: Organisation | None
304
+
305
+ class Organisation(BaseModel):
306
+ id: str
307
+ displayName: str
308
+ slug: str
309
+ domains: list[str]
310
+ permissions: list[str]
311
+ ```
312
+
313
+ ### Exceptions
314
+
315
+ #### `ApiError`
316
+
317
+ Base exception for API errors.
318
+
319
+ ```python
320
+ class ApiError(Exception):
321
+ status_code: int
322
+ message: str
323
+ body: dict | None
324
+ ```
325
+
326
+ #### `NotFoundError`
327
+
328
+ Resource not found error (HTTP 404). Inherits from [`ApiError`](#apierror).
329
+
330
+ ```python
331
+ class NotFoundError(ApiError):
332
+ pass
333
+ ```
334
+
335
+ ## Contributing
336
+
337
+ See [CONTRIBUTING.md](./CONTRIBUTING.md) for development setup, testing, and
338
+ contribution guidelines.
@@ -0,0 +1,12 @@
1
+ pd4castr_api_sdk/__init__.py,sha256=ZN-RwRsRe74lMXzYMth9tEeWSrm8EDJlu_cQV5wgTGY,231
2
+ pd4castr_api_sdk/auth.py,sha256=eLZIzHvH5AW8ld_LKbNJTbjECyhYAs-snWnMkSi2ivw,2192
3
+ pd4castr_api_sdk/client.py,sha256=490kYHScEAnBm77d7FNk32zsnI2mECBw4UnneugMJEU,3530
4
+ pd4castr_api_sdk/config.py,sha256=LGP8xyfkdJNbMv4f9VYTMSjsQ6o2-BM7oJtIvs1UCxk,765
5
+ pd4castr_api_sdk/errors.py,sha256=c_NwtrDG2IRHdzAhFGd4L4iG1JHMyFwxSXfDtbHuVow,536
6
+ pd4castr_api_sdk/models.py,sha256=BBM5KDWKZkytVSuZPxmN-GILKvSLJwpfoTnBlXbnLc4,1684
7
+ pd4castr_api_sdk/py.typed,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
8
+ pd4castr_api_sdk/transport.py,sha256=caYhcCrig396wSgR4sF1tIGWYI3Ma04Jl9NDUpp22Y8,1022
9
+ pd4castr_api_sdk-0.0.0.dist-info/METADATA,sha256=vMYZoqHhzl6hVNiKezdeP4rxORdw1UTkzbCuF-Elyb4,7765
10
+ pd4castr_api_sdk-0.0.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
11
+ pd4castr_api_sdk-0.0.0.dist-info/licenses/LICENSE.md,sha256=6UsCUcnUXgK2y-9gh9HBt7c9gl8VjONd-RVJHMSAmm8,1177
12
+ pd4castr_api_sdk-0.0.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.28.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License (MIT)
2
+
3
+ Copyright (c) 2026 Endgame Economics Pty Ltd t/as Endgame Analytics
4
+ <contact@endgameanalytics.com.au> https://endgameanalytics.com.au
5
+
6
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
7
+ this software and associated documentation files (the "Software"), to deal in
8
+ the Software without restriction, including without limitation the rights to
9
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
10
+ the Software, and to permit persons to whom the Software is furnished to do so,
11
+ subject to the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be included in all
14
+ copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
18
+ FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
19
+ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
20
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
21
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.