koios-client 1.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.
- koios_client/__init__.py +85 -0
- koios_client/_auth.py +229 -0
- koios_client/_generated/__init__.py +1447 -0
- koios_client/_generated/acknowledge_all_events.py +40 -0
- koios_client/_generated/acknowledge_event.py +40 -0
- koios_client/_generated/acknowledge_events.py +40 -0
- koios_client/_generated/base_client.py +211 -0
- koios_client/_generated/base_model.py +30 -0
- koios_client/_generated/client.py +4367 -0
- koios_client/_generated/create_component_environment.py +45 -0
- koios_client/_generated/create_component_instance.py +41 -0
- koios_client/_generated/create_device.py +39 -0
- koios_client/_generated/create_device_set.py +39 -0
- koios_client/_generated/create_device_set_item.py +38 -0
- koios_client/_generated/create_endpoint_client.py +41 -0
- koios_client/_generated/create_local_value.py +39 -0
- koios_client/_generated/create_model.py +39 -0
- koios_client/_generated/create_scan_group.py +39 -0
- koios_client/_generated/create_tag.py +39 -0
- koios_client/_generated/create_trend.py +38 -0
- koios_client/_generated/create_trend_item.py +38 -0
- koios_client/_generated/create_wire.py +37 -0
- koios_client/_generated/delete_component_environment.py +45 -0
- koios_client/_generated/delete_component_instance.py +41 -0
- koios_client/_generated/delete_component_library.py +43 -0
- koios_client/_generated/delete_device.py +38 -0
- koios_client/_generated/delete_device_set.py +39 -0
- koios_client/_generated/delete_device_set_item.py +37 -0
- koios_client/_generated/delete_device_set_items.py +19 -0
- koios_client/_generated/delete_device_sets.py +19 -0
- koios_client/_generated/delete_devices.py +17 -0
- koios_client/_generated/delete_endpoint_client.py +38 -0
- koios_client/_generated/delete_events.py +17 -0
- koios_client/_generated/delete_local_value.py +39 -0
- koios_client/_generated/delete_local_values.py +19 -0
- koios_client/_generated/delete_model.py +38 -0
- koios_client/_generated/delete_models.py +17 -0
- koios_client/_generated/delete_scan_group.py +39 -0
- koios_client/_generated/delete_scan_groups.py +19 -0
- koios_client/_generated/delete_tag.py +38 -0
- koios_client/_generated/delete_tags.py +17 -0
- koios_client/_generated/delete_trend.py +38 -0
- koios_client/_generated/delete_trend_item.py +38 -0
- koios_client/_generated/delete_wire.py +37 -0
- koios_client/_generated/duplicate_device.py +39 -0
- koios_client/_generated/duplicate_model.py +39 -0
- koios_client/_generated/duplicate_tag.py +38 -0
- koios_client/_generated/duplicate_trend.py +39 -0
- koios_client/_generated/enums.py +528 -0
- koios_client/_generated/exceptions.py +85 -0
- koios_client/_generated/get_bindings.py +99 -0
- koios_client/_generated/get_component_definition.py +44 -0
- koios_client/_generated/get_component_definitions.py +42 -0
- koios_client/_generated/get_component_environment.py +71 -0
- koios_client/_generated/get_component_environment_live.py +30 -0
- koios_client/_generated/get_component_environments.py +40 -0
- koios_client/_generated/get_component_environments_live.py +37 -0
- koios_client/_generated/get_component_instance.py +55 -0
- koios_client/_generated/get_component_instances.py +50 -0
- koios_client/_generated/get_component_libraries.py +38 -0
- koios_client/_generated/get_component_library.py +73 -0
- koios_client/_generated/get_component_library_groups.py +36 -0
- koios_client/_generated/get_device.py +116 -0
- koios_client/_generated/get_device_live.py +32 -0
- koios_client/_generated/get_device_set.py +39 -0
- koios_client/_generated/get_device_sets.py +45 -0
- koios_client/_generated/get_devices.py +55 -0
- koios_client/_generated/get_devices_live.py +37 -0
- koios_client/_generated/get_endpoint_client.py +50 -0
- koios_client/_generated/get_endpoint_clients.py +51 -0
- koios_client/_generated/get_event_count_summary.py +20 -0
- koios_client/_generated/get_events.py +61 -0
- koios_client/_generated/get_failed_counts.py +19 -0
- koios_client/_generated/get_license_detail.py +28 -0
- koios_client/_generated/get_local_values.py +45 -0
- koios_client/_generated/get_model.py +124 -0
- koios_client/_generated/get_model_files.py +46 -0
- koios_client/_generated/get_model_live.py +59 -0
- koios_client/_generated/get_models.py +70 -0
- koios_client/_generated/get_models_live.py +64 -0
- koios_client/_generated/get_platform_version.py +31 -0
- koios_client/_generated/get_protocol.py +34 -0
- koios_client/_generated/get_protocols.py +34 -0
- koios_client/_generated/get_scan_group.py +53 -0
- koios_client/_generated/get_scan_group_live.py +33 -0
- koios_client/_generated/get_scan_groups.py +40 -0
- koios_client/_generated/get_scan_groups_live.py +38 -0
- koios_client/_generated/get_services.py +33 -0
- koios_client/_generated/get_system_health.py +17 -0
- koios_client/_generated/get_system_info.py +35 -0
- koios_client/_generated/get_tag.py +139 -0
- koios_client/_generated/get_tag_history.py +29 -0
- koios_client/_generated/get_tag_live.py +39 -0
- koios_client/_generated/get_tags.py +78 -0
- koios_client/_generated/get_tags_history.py +32 -0
- koios_client/_generated/get_tags_live.py +44 -0
- koios_client/_generated/get_trend.py +72 -0
- koios_client/_generated/get_trend_items.py +58 -0
- koios_client/_generated/get_trends.py +32 -0
- koios_client/_generated/get_wires.py +74 -0
- koios_client/_generated/input_types.py +1930 -0
- koios_client/_generated/mark_all_events_seen.py +39 -0
- koios_client/_generated/mark_event_seen.py +39 -0
- koios_client/_generated/obtain_client_token.py +40 -0
- koios_client/_generated/refresh_token.py +40 -0
- koios_client/_generated/regenerate_client_secret.py +41 -0
- koios_client/_generated/set_device_set_active_device.py +43 -0
- koios_client/_generated/set_endpoint_client_permissions.py +56 -0
- koios_client/_generated/set_protocol_visible.py +39 -0
- koios_client/_generated/update_binding.py +46 -0
- koios_client/_generated/update_component_environment.py +45 -0
- koios_client/_generated/update_component_instance.py +41 -0
- koios_client/_generated/update_device.py +39 -0
- koios_client/_generated/update_device_set.py +39 -0
- koios_client/_generated/update_device_set_item.py +38 -0
- koios_client/_generated/update_device_sets.py +21 -0
- koios_client/_generated/update_devices.py +20 -0
- koios_client/_generated/update_endpoint_client.py +40 -0
- koios_client/_generated/update_local_value.py +39 -0
- koios_client/_generated/update_local_values.py +21 -0
- koios_client/_generated/update_model.py +39 -0
- koios_client/_generated/update_models.py +20 -0
- koios_client/_generated/update_scan_group.py +39 -0
- koios_client/_generated/update_scan_groups.py +22 -0
- koios_client/_generated/update_tag.py +39 -0
- koios_client/_generated/update_tags.py +20 -0
- koios_client/_generated/update_trend.py +38 -0
- koios_client/_generated/update_trend_item.py +38 -0
- koios_client/_generated/update_wire.py +37 -0
- koios_client/_generated/verify_token.py +10 -0
- koios_client/client.py +1112 -0
- koios_client/exceptions.py +83 -0
- koios_client/py.typed +0 -0
- koios_client/resources/__init__.py +33 -0
- koios_client/resources/_base.py +93 -0
- koios_client/resources/_helpers.py +82 -0
- koios_client/resources/bindings.py +44 -0
- koios_client/resources/bindings.pyi +33 -0
- koios_client/resources/device_sets.py +104 -0
- koios_client/resources/device_sets.pyi +25 -0
- koios_client/resources/devices.py +167 -0
- koios_client/resources/devices.pyi +52 -0
- koios_client/resources/models.py +191 -0
- koios_client/resources/models.pyi +71 -0
- koios_client/resources/protocols.py +54 -0
- koios_client/resources/protocols.pyi +27 -0
- koios_client/resources/scan_groups.py +112 -0
- koios_client/resources/scan_groups.pyi +36 -0
- koios_client/resources/tags.py +134 -0
- koios_client/resources/tags.pyi +47 -0
- koios_client/types.py +265 -0
- koios_client-1.0.0.dist-info/METADATA +608 -0
- koios_client-1.0.0.dist-info/RECORD +155 -0
- koios_client-1.0.0.dist-info/WHEEL +5 -0
- koios_client-1.0.0.dist-info/top_level.txt +1 -0
koios_client/__init__.py
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""Koios Client — typed Python SDK for the Koios IoT platform.
|
|
2
|
+
|
|
3
|
+
Quick start:
|
|
4
|
+
from koios_client import KoiosClient
|
|
5
|
+
|
|
6
|
+
client = KoiosClient(
|
|
7
|
+
hostname="koios.example.com",
|
|
8
|
+
client_id="your-client-uuid",
|
|
9
|
+
client_secret="your-client-secret",
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
# High-level resource API (lookup by ID or UUID slug)
|
|
13
|
+
device = client.device(1) # by ID (default)
|
|
14
|
+
device.enable()
|
|
15
|
+
for tag in device.tags():
|
|
16
|
+
print(tag.name, tag.value)
|
|
17
|
+
|
|
18
|
+
# Low-level GraphQL (fully typed — IDE autocomplete works)
|
|
19
|
+
result = client.gql.get_devices()
|
|
20
|
+
for device in result.devices.results:
|
|
21
|
+
print(device.name, device.status)
|
|
22
|
+
|
|
23
|
+
Enums, input types, and response types are available from koios_client.types:
|
|
24
|
+
from koios_client.types import (
|
|
25
|
+
StatusChoices, UsageChoices, DeviceInput, OffsetPaginationInput,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
Resource classes are available from koios_client.resources:
|
|
29
|
+
from koios_client.resources import Device, Tag, Model, PaginatedList
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
from koios_client.client import KoiosClient
|
|
33
|
+
from koios_client.exceptions import (
|
|
34
|
+
AuthenticationError,
|
|
35
|
+
GraphQLError,
|
|
36
|
+
KoiosConnectionError,
|
|
37
|
+
KoiosError,
|
|
38
|
+
KoiosPermissionError,
|
|
39
|
+
NotFoundError,
|
|
40
|
+
OperationError,
|
|
41
|
+
OperationMessage,
|
|
42
|
+
ValidationError,
|
|
43
|
+
)
|
|
44
|
+
from koios_client.resources import (
|
|
45
|
+
Binding,
|
|
46
|
+
Device,
|
|
47
|
+
DeviceSet,
|
|
48
|
+
Model,
|
|
49
|
+
PaginatedList,
|
|
50
|
+
Protocol,
|
|
51
|
+
Resource,
|
|
52
|
+
ScanGroup,
|
|
53
|
+
Tag,
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
__all__ = [
|
|
57
|
+
# Client
|
|
58
|
+
"KoiosClient",
|
|
59
|
+
# Resources
|
|
60
|
+
"Binding",
|
|
61
|
+
"Device",
|
|
62
|
+
"DeviceSet",
|
|
63
|
+
"Model",
|
|
64
|
+
"PaginatedList",
|
|
65
|
+
"Protocol",
|
|
66
|
+
"Resource",
|
|
67
|
+
"ScanGroup",
|
|
68
|
+
"Tag",
|
|
69
|
+
# Exceptions
|
|
70
|
+
"KoiosError",
|
|
71
|
+
"AuthenticationError",
|
|
72
|
+
"KoiosConnectionError",
|
|
73
|
+
"GraphQLError",
|
|
74
|
+
"NotFoundError",
|
|
75
|
+
"OperationError",
|
|
76
|
+
"OperationMessage",
|
|
77
|
+
"KoiosPermissionError",
|
|
78
|
+
"ValidationError",
|
|
79
|
+
]
|
|
80
|
+
try:
|
|
81
|
+
from importlib.metadata import version as _get_version
|
|
82
|
+
|
|
83
|
+
__version__ = _get_version("koios-client")
|
|
84
|
+
except Exception:
|
|
85
|
+
__version__ = "0.0.0"
|
koios_client/_auth.py
ADDED
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
"""JWT token management for Koios API authentication.
|
|
2
|
+
|
|
3
|
+
Handles obtaining and refreshing tokens via the GraphQL endpoint.
|
|
4
|
+
Uses a dedicated httpx.Client (not the generated client) to avoid circular auth dependency.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
import time
|
|
11
|
+
from collections.abc import Generator
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
import httpx
|
|
16
|
+
|
|
17
|
+
from koios_client.exceptions import AuthenticationError
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
# Shared inline fragment for token union responses.
|
|
22
|
+
_TOKEN_FRAGMENTS = """
|
|
23
|
+
... on TokenResultType {
|
|
24
|
+
__typename
|
|
25
|
+
access
|
|
26
|
+
refresh
|
|
27
|
+
expiresIn
|
|
28
|
+
}
|
|
29
|
+
... on OperationInfo {
|
|
30
|
+
__typename
|
|
31
|
+
messages {
|
|
32
|
+
message
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
_OBTAIN_TOKEN_QUERY = f"""
|
|
38
|
+
mutation ObtainClientToken($credentials: ClientCredentialsInput!) {{
|
|
39
|
+
obtainClientToken(credentials: $credentials) {{
|
|
40
|
+
{_TOKEN_FRAGMENTS}
|
|
41
|
+
}}
|
|
42
|
+
}}
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
_REFRESH_TOKEN_QUERY = f"""
|
|
46
|
+
mutation RefreshToken($refreshInput: RefreshTokenInput!) {{
|
|
47
|
+
refreshToken(refreshInput: $refreshInput) {{
|
|
48
|
+
{_TOKEN_FRAGMENTS}
|
|
49
|
+
}}
|
|
50
|
+
}}
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
# Buffer in seconds — refresh tokens before they actually expire.
|
|
54
|
+
_EXPIRY_BUFFER_SECONDS = 60
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@dataclass
|
|
58
|
+
class TokenPair:
|
|
59
|
+
"""JWT access and refresh token pair with expiry tracking."""
|
|
60
|
+
|
|
61
|
+
access: str
|
|
62
|
+
refresh: str
|
|
63
|
+
expires_at: float # Unix timestamp
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class KoiosAuth(httpx.Auth):
|
|
67
|
+
"""httpx Auth handler that manages Koios JWT tokens.
|
|
68
|
+
|
|
69
|
+
Obtains an initial token on first use and automatically refreshes
|
|
70
|
+
before expiry or on HTTP 401.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
url: GraphQL endpoint URL (e.g. "https://koios.example.com:443/graphql/").
|
|
74
|
+
client_id: Endpoint client UUID.
|
|
75
|
+
client_secret: Endpoint client secret.
|
|
76
|
+
verify_ssl: Whether to verify TLS certificates.
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
requires_response_body = True
|
|
80
|
+
|
|
81
|
+
def __init__(
|
|
82
|
+
self,
|
|
83
|
+
url: str,
|
|
84
|
+
client_id: str,
|
|
85
|
+
client_secret: str,
|
|
86
|
+
*,
|
|
87
|
+
verify_ssl: bool = True,
|
|
88
|
+
) -> None:
|
|
89
|
+
self._url = url
|
|
90
|
+
self._client_id = client_id
|
|
91
|
+
self._client_secret = client_secret
|
|
92
|
+
self._verify_ssl = verify_ssl
|
|
93
|
+
self._token: TokenPair | None = None
|
|
94
|
+
# Dedicated client for auth requests — reuses TCP/TLS connections.
|
|
95
|
+
self._auth_http = httpx.Client(verify=verify_ssl, timeout=30.0)
|
|
96
|
+
|
|
97
|
+
def auth_flow( # type: ignore[override]
|
|
98
|
+
self, request: httpx.Request
|
|
99
|
+
) -> Generator[httpx.Request, httpx.Response, None]:
|
|
100
|
+
"""Inject Bearer token, refreshing if needed."""
|
|
101
|
+
if self._token is None:
|
|
102
|
+
self._obtain_token()
|
|
103
|
+
elif self._is_expired():
|
|
104
|
+
self._refresh_or_obtain()
|
|
105
|
+
|
|
106
|
+
assert self._token is not None # ensured by _obtain_token / _refresh_or_obtain
|
|
107
|
+
request.headers["Authorization"] = f"Bearer {self._token.access}"
|
|
108
|
+
response = yield request
|
|
109
|
+
|
|
110
|
+
# Retry once on 401 (token may have been invalidated server-side)
|
|
111
|
+
if response.status_code == 401:
|
|
112
|
+
logger.debug("Got 401, attempting token refresh")
|
|
113
|
+
self._refresh_or_obtain()
|
|
114
|
+
assert self._token is not None
|
|
115
|
+
request.headers["Authorization"] = f"Bearer {self._token.access}"
|
|
116
|
+
yield request
|
|
117
|
+
|
|
118
|
+
@property
|
|
119
|
+
def is_authenticated(self) -> bool:
|
|
120
|
+
"""Whether we currently hold a valid (non-expired) token."""
|
|
121
|
+
return self._token is not None and not self._is_expired()
|
|
122
|
+
|
|
123
|
+
def force_refresh(self) -> None:
|
|
124
|
+
"""Force a token refresh. Useful if permissions changed server-side."""
|
|
125
|
+
if self._token is not None:
|
|
126
|
+
self._refresh_token()
|
|
127
|
+
else:
|
|
128
|
+
self._obtain_token()
|
|
129
|
+
|
|
130
|
+
def close(self) -> None:
|
|
131
|
+
"""Close the dedicated auth HTTP client."""
|
|
132
|
+
self._auth_http.close()
|
|
133
|
+
|
|
134
|
+
def _is_expired(self) -> bool:
|
|
135
|
+
if self._token is None:
|
|
136
|
+
return True
|
|
137
|
+
return time.time() >= (self._token.expires_at - _EXPIRY_BUFFER_SECONDS)
|
|
138
|
+
|
|
139
|
+
def _refresh_or_obtain(self) -> None:
|
|
140
|
+
"""Try refresh first; fall back to full re-authentication."""
|
|
141
|
+
try:
|
|
142
|
+
self._refresh_token()
|
|
143
|
+
except Exception:
|
|
144
|
+
logger.debug("Token refresh failed, re-authenticating")
|
|
145
|
+
self._obtain_token()
|
|
146
|
+
|
|
147
|
+
def _obtain_token(self) -> None:
|
|
148
|
+
"""Obtain a new token pair using client credentials."""
|
|
149
|
+
result = self._execute_auth_mutation(
|
|
150
|
+
query=_OBTAIN_TOKEN_QUERY,
|
|
151
|
+
variables={
|
|
152
|
+
"credentials": {
|
|
153
|
+
"clientId": self._client_id,
|
|
154
|
+
"clientSecret": self._client_secret,
|
|
155
|
+
}
|
|
156
|
+
},
|
|
157
|
+
operation="obtainClientToken",
|
|
158
|
+
)
|
|
159
|
+
self._parse_token_result(result, "obtainClientToken")
|
|
160
|
+
|
|
161
|
+
def _refresh_token(self) -> None:
|
|
162
|
+
"""Refresh the access token using the refresh token."""
|
|
163
|
+
if self._token is None:
|
|
164
|
+
raise AuthenticationError("No token to refresh")
|
|
165
|
+
|
|
166
|
+
result = self._execute_auth_mutation(
|
|
167
|
+
query=_REFRESH_TOKEN_QUERY,
|
|
168
|
+
variables={
|
|
169
|
+
"refreshInput": {
|
|
170
|
+
"refreshToken": self._token.refresh,
|
|
171
|
+
}
|
|
172
|
+
},
|
|
173
|
+
operation="refreshToken",
|
|
174
|
+
)
|
|
175
|
+
self._parse_token_result(result, "refreshToken")
|
|
176
|
+
|
|
177
|
+
def _execute_auth_mutation(
|
|
178
|
+
self, *, query: str, variables: dict[str, Any], operation: str
|
|
179
|
+
) -> dict[str, Any]:
|
|
180
|
+
"""Execute a raw GraphQL mutation for auth and return the result dict."""
|
|
181
|
+
response = self._auth_http.post(
|
|
182
|
+
self._url,
|
|
183
|
+
json={"query": query, "variables": variables},
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
if response.status_code != 200:
|
|
187
|
+
raise AuthenticationError(f"{operation} failed with HTTP {response.status_code}")
|
|
188
|
+
|
|
189
|
+
data = response.json()
|
|
190
|
+
|
|
191
|
+
if "errors" in data:
|
|
192
|
+
msg = data["errors"][0].get("message", "Unknown error")
|
|
193
|
+
raise AuthenticationError(f"{operation} failed: {msg}")
|
|
194
|
+
|
|
195
|
+
return data.get("data", {}).get(operation, {})
|
|
196
|
+
|
|
197
|
+
def _parse_token_result(self, result: dict[str, Any], operation: str) -> None:
|
|
198
|
+
"""Parse a token result, handling both success and OperationInfo."""
|
|
199
|
+
if not result:
|
|
200
|
+
raise AuthenticationError(f"{operation} returned empty result")
|
|
201
|
+
|
|
202
|
+
# Check for OperationInfo (error response)
|
|
203
|
+
typename = result.get("__typename", "")
|
|
204
|
+
if typename == "OperationInfo":
|
|
205
|
+
messages = result.get("messages", [])
|
|
206
|
+
msg = messages[0]["message"] if messages else "Unknown error"
|
|
207
|
+
raise AuthenticationError(f"{operation} failed: {msg}")
|
|
208
|
+
|
|
209
|
+
# Parse successful token response
|
|
210
|
+
access = result.get("access")
|
|
211
|
+
refresh = result.get("refresh")
|
|
212
|
+
expires_in = result.get("expiresIn")
|
|
213
|
+
|
|
214
|
+
if not access:
|
|
215
|
+
raise AuthenticationError(f"{operation} returned incomplete token data")
|
|
216
|
+
|
|
217
|
+
# On refresh, the server may omit the refresh token to signal "keep the
|
|
218
|
+
# existing one" — this keeps refresh tokens long-lived across access refreshes.
|
|
219
|
+
if not refresh:
|
|
220
|
+
if self._token is None:
|
|
221
|
+
raise AuthenticationError(f"{operation} returned no refresh token")
|
|
222
|
+
refresh = self._token.refresh
|
|
223
|
+
|
|
224
|
+
self._token = TokenPair(
|
|
225
|
+
access=access,
|
|
226
|
+
refresh=refresh,
|
|
227
|
+
expires_at=time.time() + (expires_in or 3600),
|
|
228
|
+
)
|
|
229
|
+
logger.debug("Token obtained, expires in %ds", expires_in or 3600)
|