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.
Files changed (155) hide show
  1. koios_client/__init__.py +85 -0
  2. koios_client/_auth.py +229 -0
  3. koios_client/_generated/__init__.py +1447 -0
  4. koios_client/_generated/acknowledge_all_events.py +40 -0
  5. koios_client/_generated/acknowledge_event.py +40 -0
  6. koios_client/_generated/acknowledge_events.py +40 -0
  7. koios_client/_generated/base_client.py +211 -0
  8. koios_client/_generated/base_model.py +30 -0
  9. koios_client/_generated/client.py +4367 -0
  10. koios_client/_generated/create_component_environment.py +45 -0
  11. koios_client/_generated/create_component_instance.py +41 -0
  12. koios_client/_generated/create_device.py +39 -0
  13. koios_client/_generated/create_device_set.py +39 -0
  14. koios_client/_generated/create_device_set_item.py +38 -0
  15. koios_client/_generated/create_endpoint_client.py +41 -0
  16. koios_client/_generated/create_local_value.py +39 -0
  17. koios_client/_generated/create_model.py +39 -0
  18. koios_client/_generated/create_scan_group.py +39 -0
  19. koios_client/_generated/create_tag.py +39 -0
  20. koios_client/_generated/create_trend.py +38 -0
  21. koios_client/_generated/create_trend_item.py +38 -0
  22. koios_client/_generated/create_wire.py +37 -0
  23. koios_client/_generated/delete_component_environment.py +45 -0
  24. koios_client/_generated/delete_component_instance.py +41 -0
  25. koios_client/_generated/delete_component_library.py +43 -0
  26. koios_client/_generated/delete_device.py +38 -0
  27. koios_client/_generated/delete_device_set.py +39 -0
  28. koios_client/_generated/delete_device_set_item.py +37 -0
  29. koios_client/_generated/delete_device_set_items.py +19 -0
  30. koios_client/_generated/delete_device_sets.py +19 -0
  31. koios_client/_generated/delete_devices.py +17 -0
  32. koios_client/_generated/delete_endpoint_client.py +38 -0
  33. koios_client/_generated/delete_events.py +17 -0
  34. koios_client/_generated/delete_local_value.py +39 -0
  35. koios_client/_generated/delete_local_values.py +19 -0
  36. koios_client/_generated/delete_model.py +38 -0
  37. koios_client/_generated/delete_models.py +17 -0
  38. koios_client/_generated/delete_scan_group.py +39 -0
  39. koios_client/_generated/delete_scan_groups.py +19 -0
  40. koios_client/_generated/delete_tag.py +38 -0
  41. koios_client/_generated/delete_tags.py +17 -0
  42. koios_client/_generated/delete_trend.py +38 -0
  43. koios_client/_generated/delete_trend_item.py +38 -0
  44. koios_client/_generated/delete_wire.py +37 -0
  45. koios_client/_generated/duplicate_device.py +39 -0
  46. koios_client/_generated/duplicate_model.py +39 -0
  47. koios_client/_generated/duplicate_tag.py +38 -0
  48. koios_client/_generated/duplicate_trend.py +39 -0
  49. koios_client/_generated/enums.py +528 -0
  50. koios_client/_generated/exceptions.py +85 -0
  51. koios_client/_generated/get_bindings.py +99 -0
  52. koios_client/_generated/get_component_definition.py +44 -0
  53. koios_client/_generated/get_component_definitions.py +42 -0
  54. koios_client/_generated/get_component_environment.py +71 -0
  55. koios_client/_generated/get_component_environment_live.py +30 -0
  56. koios_client/_generated/get_component_environments.py +40 -0
  57. koios_client/_generated/get_component_environments_live.py +37 -0
  58. koios_client/_generated/get_component_instance.py +55 -0
  59. koios_client/_generated/get_component_instances.py +50 -0
  60. koios_client/_generated/get_component_libraries.py +38 -0
  61. koios_client/_generated/get_component_library.py +73 -0
  62. koios_client/_generated/get_component_library_groups.py +36 -0
  63. koios_client/_generated/get_device.py +116 -0
  64. koios_client/_generated/get_device_live.py +32 -0
  65. koios_client/_generated/get_device_set.py +39 -0
  66. koios_client/_generated/get_device_sets.py +45 -0
  67. koios_client/_generated/get_devices.py +55 -0
  68. koios_client/_generated/get_devices_live.py +37 -0
  69. koios_client/_generated/get_endpoint_client.py +50 -0
  70. koios_client/_generated/get_endpoint_clients.py +51 -0
  71. koios_client/_generated/get_event_count_summary.py +20 -0
  72. koios_client/_generated/get_events.py +61 -0
  73. koios_client/_generated/get_failed_counts.py +19 -0
  74. koios_client/_generated/get_license_detail.py +28 -0
  75. koios_client/_generated/get_local_values.py +45 -0
  76. koios_client/_generated/get_model.py +124 -0
  77. koios_client/_generated/get_model_files.py +46 -0
  78. koios_client/_generated/get_model_live.py +59 -0
  79. koios_client/_generated/get_models.py +70 -0
  80. koios_client/_generated/get_models_live.py +64 -0
  81. koios_client/_generated/get_platform_version.py +31 -0
  82. koios_client/_generated/get_protocol.py +34 -0
  83. koios_client/_generated/get_protocols.py +34 -0
  84. koios_client/_generated/get_scan_group.py +53 -0
  85. koios_client/_generated/get_scan_group_live.py +33 -0
  86. koios_client/_generated/get_scan_groups.py +40 -0
  87. koios_client/_generated/get_scan_groups_live.py +38 -0
  88. koios_client/_generated/get_services.py +33 -0
  89. koios_client/_generated/get_system_health.py +17 -0
  90. koios_client/_generated/get_system_info.py +35 -0
  91. koios_client/_generated/get_tag.py +139 -0
  92. koios_client/_generated/get_tag_history.py +29 -0
  93. koios_client/_generated/get_tag_live.py +39 -0
  94. koios_client/_generated/get_tags.py +78 -0
  95. koios_client/_generated/get_tags_history.py +32 -0
  96. koios_client/_generated/get_tags_live.py +44 -0
  97. koios_client/_generated/get_trend.py +72 -0
  98. koios_client/_generated/get_trend_items.py +58 -0
  99. koios_client/_generated/get_trends.py +32 -0
  100. koios_client/_generated/get_wires.py +74 -0
  101. koios_client/_generated/input_types.py +1930 -0
  102. koios_client/_generated/mark_all_events_seen.py +39 -0
  103. koios_client/_generated/mark_event_seen.py +39 -0
  104. koios_client/_generated/obtain_client_token.py +40 -0
  105. koios_client/_generated/refresh_token.py +40 -0
  106. koios_client/_generated/regenerate_client_secret.py +41 -0
  107. koios_client/_generated/set_device_set_active_device.py +43 -0
  108. koios_client/_generated/set_endpoint_client_permissions.py +56 -0
  109. koios_client/_generated/set_protocol_visible.py +39 -0
  110. koios_client/_generated/update_binding.py +46 -0
  111. koios_client/_generated/update_component_environment.py +45 -0
  112. koios_client/_generated/update_component_instance.py +41 -0
  113. koios_client/_generated/update_device.py +39 -0
  114. koios_client/_generated/update_device_set.py +39 -0
  115. koios_client/_generated/update_device_set_item.py +38 -0
  116. koios_client/_generated/update_device_sets.py +21 -0
  117. koios_client/_generated/update_devices.py +20 -0
  118. koios_client/_generated/update_endpoint_client.py +40 -0
  119. koios_client/_generated/update_local_value.py +39 -0
  120. koios_client/_generated/update_local_values.py +21 -0
  121. koios_client/_generated/update_model.py +39 -0
  122. koios_client/_generated/update_models.py +20 -0
  123. koios_client/_generated/update_scan_group.py +39 -0
  124. koios_client/_generated/update_scan_groups.py +22 -0
  125. koios_client/_generated/update_tag.py +39 -0
  126. koios_client/_generated/update_tags.py +20 -0
  127. koios_client/_generated/update_trend.py +38 -0
  128. koios_client/_generated/update_trend_item.py +38 -0
  129. koios_client/_generated/update_wire.py +37 -0
  130. koios_client/_generated/verify_token.py +10 -0
  131. koios_client/client.py +1112 -0
  132. koios_client/exceptions.py +83 -0
  133. koios_client/py.typed +0 -0
  134. koios_client/resources/__init__.py +33 -0
  135. koios_client/resources/_base.py +93 -0
  136. koios_client/resources/_helpers.py +82 -0
  137. koios_client/resources/bindings.py +44 -0
  138. koios_client/resources/bindings.pyi +33 -0
  139. koios_client/resources/device_sets.py +104 -0
  140. koios_client/resources/device_sets.pyi +25 -0
  141. koios_client/resources/devices.py +167 -0
  142. koios_client/resources/devices.pyi +52 -0
  143. koios_client/resources/models.py +191 -0
  144. koios_client/resources/models.pyi +71 -0
  145. koios_client/resources/protocols.py +54 -0
  146. koios_client/resources/protocols.pyi +27 -0
  147. koios_client/resources/scan_groups.py +112 -0
  148. koios_client/resources/scan_groups.pyi +36 -0
  149. koios_client/resources/tags.py +134 -0
  150. koios_client/resources/tags.pyi +47 -0
  151. koios_client/types.py +265 -0
  152. koios_client-1.0.0.dist-info/METADATA +608 -0
  153. koios_client-1.0.0.dist-info/RECORD +155 -0
  154. koios_client-1.0.0.dist-info/WHEEL +5 -0
  155. koios_client-1.0.0.dist-info/top_level.txt +1 -0
@@ -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)