anycubic-cloud-api 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.
Files changed (33) hide show
  1. anycubic_cloud_api/__init__.py +62 -0
  2. anycubic_cloud_api/anycubic_api.py +7 -0
  3. anycubic_cloud_api/api/base.py +400 -0
  4. anycubic_cloud_api/api/functions.py +2055 -0
  5. anycubic_cloud_api/api/mqtt.py +449 -0
  6. anycubic_cloud_api/const/api_endpoints.py +106 -0
  7. anycubic_cloud_api/const/const.py +47 -0
  8. anycubic_cloud_api/const/enums.py +97 -0
  9. anycubic_cloud_api/const/mqtt.py +13 -0
  10. anycubic_cloud_api/data_models/consumable.py +116 -0
  11. anycubic_cloud_api/data_models/files.py +429 -0
  12. anycubic_cloud_api/data_models/gcode_file.py +107 -0
  13. anycubic_cloud_api/data_models/orders.py +369 -0
  14. anycubic_cloud_api/data_models/print_response.py +71 -0
  15. anycubic_cloud_api/data_models/print_speed_mode.py +61 -0
  16. anycubic_cloud_api/data_models/printer.py +2753 -0
  17. anycubic_cloud_api/data_models/printer_properties.py +1107 -0
  18. anycubic_cloud_api/data_models/printing_settings.py +130 -0
  19. anycubic_cloud_api/data_models/project.py +1234 -0
  20. anycubic_cloud_api/exceptions/error_strings.py +340 -0
  21. anycubic_cloud_api/exceptions/exceptions.py +91 -0
  22. anycubic_cloud_api/helpers/helpers.py +240 -0
  23. anycubic_cloud_api/models/auth.py +396 -0
  24. anycubic_cloud_api/models/cloud_upload.py +249 -0
  25. anycubic_cloud_api/models/http.py +29 -0
  26. anycubic_cloud_api/resources/anycubic_mqqt_tls_ca.crt +24 -0
  27. anycubic_cloud_api/resources/anycubic_mqqt_tls_client.crt +24 -0
  28. anycubic_cloud_api/resources/anycubic_mqqt_tls_client.key +28 -0
  29. anycubic_cloud_api-0.1.0.dist-info/METADATA +79 -0
  30. anycubic_cloud_api-0.1.0.dist-info/RECORD +33 -0
  31. anycubic_cloud_api-0.1.0.dist-info/WHEEL +5 -0
  32. anycubic_cloud_api-0.1.0.dist-info/licenses/LICENSE +674 -0
  33. anycubic_cloud_api-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,62 @@
1
+ """Async client for the Anycubic Cloud API and its MQTT stream.
2
+
3
+ Extracted from the Home Assistant `anycubic_cloud` integration so the client
4
+ can be versioned, tested and consumed independently.
5
+ """
6
+
7
+ from .anycubic_api import AnycubicAPI, AnycubicMQTTAPI
8
+ from .const.enums import (
9
+ AnycubicFeedType,
10
+ AnycubicFunctionID,
11
+ AnycubicOrderID,
12
+ AnycubicPrinterMaterialType,
13
+ AnycubicPrintStatus,
14
+ )
15
+ from .data_models.printer import AnycubicPrinter
16
+ from .data_models.project import AnycubicProject
17
+ from .exceptions.exceptions import (
18
+ AnycubicAPIError,
19
+ AnycubicAPIParsingError,
20
+ AnycubicAuthError,
21
+ AnycubicAuthTokensExpired,
22
+ AnycubicCloudUploadError,
23
+ AnycubicDataParsingError,
24
+ AnycubicFileNotFoundError,
25
+ AnycubicGcodeParsingError,
26
+ AnycubicInvalidValue,
27
+ AnycubicMQTTClientError,
28
+ AnycubicMQTTUnhandledData,
29
+ AnycubicMQTTUnknownUpdate,
30
+ AnycubicPropertiesNotLoaded,
31
+ )
32
+ from .models.auth import AnycubicAuthentication, AnycubicAuthMode
33
+
34
+ __version__ = "0.1.0"
35
+
36
+ __all__ = [
37
+ "AnycubicAPI",
38
+ "AnycubicAPIError",
39
+ "AnycubicAPIParsingError",
40
+ "AnycubicAuthError",
41
+ "AnycubicAuthMode",
42
+ "AnycubicAuthTokensExpired",
43
+ "AnycubicAuthentication",
44
+ "AnycubicCloudUploadError",
45
+ "AnycubicDataParsingError",
46
+ "AnycubicFeedType",
47
+ "AnycubicFileNotFoundError",
48
+ "AnycubicFunctionID",
49
+ "AnycubicGcodeParsingError",
50
+ "AnycubicInvalidValue",
51
+ "AnycubicMQTTAPI",
52
+ "AnycubicMQTTClientError",
53
+ "AnycubicMQTTUnhandledData",
54
+ "AnycubicMQTTUnknownUpdate",
55
+ "AnycubicOrderID",
56
+ "AnycubicPrintStatus",
57
+ "AnycubicPrinter",
58
+ "AnycubicPrinterMaterialType",
59
+ "AnycubicProject",
60
+ "AnycubicPropertiesNotLoaded",
61
+ "__version__",
62
+ ]
@@ -0,0 +1,7 @@
1
+ from .api.functions import AnycubicAPIFunctions as AnycubicAPI
2
+ from .api.mqtt import AnycubicMQTTAPI
3
+
4
+ __all__ = [
5
+ "AnycubicAPI",
6
+ "AnycubicMQTTAPI",
7
+ ]
@@ -0,0 +1,400 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import json
5
+ import time
6
+ from typing import Any, overload
7
+
8
+ import aiohttp
9
+ from aiofiles import open as aio_file_open
10
+ from aiofiles.os import path as aio_path
11
+
12
+ from ..const.api_endpoints import API_ENDPOINT
13
+ from ..const.const import (
14
+ ACCESS_TOKEN_LOGIN_RETRIES,
15
+ ACCESS_TOKEN_LOGIN_RETRY_INTERVAL,
16
+ AUTH_DOMAIN,
17
+ BASE_DOMAIN,
18
+ DEFAULT_USER_AGENT,
19
+ MAX_API_FETCH_TIME_WARN,
20
+ PUBLIC_API_ENDPOINT,
21
+ WARN_INTERVAL_API_DURATION,
22
+ )
23
+ from ..exceptions.error_strings import (
24
+ ErrorsAPIParsing,
25
+ ErrorsAuth,
26
+ ErrorsAuthTokenExpired,
27
+ )
28
+ from ..exceptions.exceptions import (
29
+ AnycubicAPIParsingError,
30
+ AnycubicAuthError,
31
+ AnycubicAuthTokensExpired,
32
+ )
33
+ from ..models.auth import AnycubicAuthentication, AnycubicAuthMode
34
+ from ..models.http import HTTP_METHODS, AnycubicAPIEndpoint
35
+
36
+
37
+ class AnycubicAPIBase:
38
+ __slots__ = (
39
+ "_cached_web_auth_token_path",
40
+ "_base_url",
41
+ "_public_api_root",
42
+ "_session",
43
+ "_sessionjar",
44
+ "_debug_logger",
45
+ "_tokens_changed",
46
+ "_log_api_call_info",
47
+ "_last_warn_api_duration",
48
+ "_anycubic_auth",
49
+ )
50
+
51
+ def __init__(
52
+ self,
53
+ session: aiohttp.ClientSession,
54
+ cookie_jar: aiohttp.CookieJar,
55
+ debug_logger: Any = None,
56
+ auth_token: str | None = None,
57
+ auth_mode: AnycubicAuthMode | None = None,
58
+ device_id: str | None = None,
59
+ ) -> None:
60
+ # Cache
61
+ self._cached_web_auth_token_path: str | None = None
62
+ # API
63
+ self._base_url: str = f"https://{BASE_DOMAIN}/"
64
+ self._public_api_root: str = f"{self.base_url}{PUBLIC_API_ENDPOINT}"
65
+ # Internal
66
+ self._session: aiohttp.ClientSession = session
67
+ self._sessionjar: aiohttp.CookieJar = cookie_jar
68
+ self._debug_logger: Any = debug_logger
69
+ self._tokens_changed: bool = False
70
+ self._log_api_call_info: bool = False
71
+ self._last_warn_api_duration: int | None = None
72
+ self._anycubic_auth: AnycubicAuthentication | None = None
73
+
74
+ if auth_token:
75
+ self.set_authentication(
76
+ auth_token=auth_token,
77
+ auth_mode=auth_mode,
78
+ device_id=device_id,
79
+ )
80
+
81
+ @property
82
+ def base_url(self) -> str:
83
+ return self._base_url
84
+
85
+ def set_log_api_call_info(
86
+ self,
87
+ val: bool,
88
+ ) -> None:
89
+ self._log_api_call_info = bool(val)
90
+
91
+ @property
92
+ def anycubic_auth(self) -> AnycubicAuthentication:
93
+ if self._anycubic_auth is None:
94
+ raise AnycubicAuthError(ErrorsAuth.missing_auth)
95
+ return self._anycubic_auth
96
+
97
+ @property
98
+ def tokens_changed(self) -> bool:
99
+ return self._tokens_changed
100
+
101
+ def _log_to_debug(self, msg: str) -> None:
102
+ if self._debug_logger:
103
+ self._debug_logger.debug(msg)
104
+
105
+ def _log_to_warn(self, msg: str) -> None:
106
+ if self._debug_logger:
107
+ self._debug_logger.warning(msg)
108
+
109
+ def _log_to_error(self, msg: str) -> None:
110
+ if self._debug_logger:
111
+ self._debug_logger.error(msg)
112
+
113
+ #
114
+ #
115
+ # API Functions
116
+ # ------------------------------------------
117
+
118
+ def _web_headers(self, with_origin: str | None = AUTH_DOMAIN) -> dict[str, Any]:
119
+ header_dict = {}
120
+ if self.anycubic_auth.requires_user_agent:
121
+ header_dict['User-Agent'] = DEFAULT_USER_AGENT
122
+
123
+ if with_origin:
124
+ header_dict['Origin'] = f'https://{with_origin}'
125
+
126
+ return header_dict
127
+
128
+ def _build_api_url(self, endpoint: AnycubicAPIEndpoint) -> str:
129
+ return f"{self._public_api_root}{endpoint.endpoint}"
130
+
131
+ @overload
132
+ async def _fetch_ext_resp(
133
+ self,
134
+ method: HTTP_METHODS,
135
+ base_url: str,
136
+ query: dict[str, Any] | None = None,
137
+ params: dict[str, Any] = {},
138
+ extra_headers: dict[str, Any] = {},
139
+ with_origin: str | None = AUTH_DOMAIN,
140
+ put_data: bytes | None = None,
141
+ ) -> dict[Any, Any]: ...
142
+
143
+ @overload
144
+ async def _fetch_ext_resp(
145
+ self,
146
+ method: HTTP_METHODS,
147
+ base_url: str,
148
+ query: dict[str, Any] | None = None,
149
+ params: dict[str, Any] = {},
150
+ extra_headers: dict[str, Any] = {},
151
+ with_origin: str | None = AUTH_DOMAIN,
152
+ put_data: bytes | None = None,
153
+ is_json: bool = True,
154
+ return_url: bool = False,
155
+ ) -> dict[Any, Any] | str: ...
156
+
157
+ async def _fetch_ext_resp(
158
+ self,
159
+ method: HTTP_METHODS,
160
+ base_url: str,
161
+ query: dict[str, Any] | None = None,
162
+ params: dict[str, Any] | list[Any] | str | None = {},
163
+ extra_headers: dict[str, Any] = {},
164
+ with_origin: str | None = AUTH_DOMAIN,
165
+ put_data: bytes | None = None,
166
+ is_json: bool = True,
167
+ return_url: bool = False,
168
+ ) -> dict[Any, Any] | str:
169
+ url = base_url
170
+ time_start: float = time.time()
171
+ headers = {**self._web_headers(with_origin=with_origin), **extra_headers}
172
+ if method == HTTP_METHODS.POST:
173
+ if params is not None and (isinstance(params, dict) or isinstance(params, list)):
174
+ data = json.dumps(params)
175
+ elif params is not None:
176
+ data = str(params)
177
+ else:
178
+ data = None
179
+ h_coro = self._session.post(url, params=query, data=data, headers=headers)
180
+ elif method == HTTP_METHODS.PUT:
181
+ h_coro = self._session.put(url, params=query, data=put_data, headers=headers)
182
+ else:
183
+ h_coro = self._session.get(url, params=query, headers=headers)
184
+
185
+ response_url = None
186
+
187
+ try:
188
+ async with h_coro as resp:
189
+ if is_json:
190
+ resp_data: dict[str, Any] | str = await resp.json()
191
+ else:
192
+ resp_data = await resp.text()
193
+
194
+ response_url = resp.url
195
+ except Exception:
196
+ raise AnycubicAPIParsingError(ErrorsAPIParsing.api_error_server_maintenance)
197
+
198
+ time_end: float = time.time()
199
+ time_diff: float = time_end - time_start
200
+ over_limit: bool = int(time_diff) > MAX_API_FETCH_TIME_WARN
201
+ if (
202
+ over_limit
203
+ and (
204
+ not self._last_warn_api_duration
205
+ or time_end > self._last_warn_api_duration + WARN_INTERVAL_API_DURATION
206
+ )
207
+ ):
208
+ self._log_to_warn(
209
+ f"Responses from server are taking over {MAX_API_FETCH_TIME_WARN}s (Took {int(time_diff)}s)"
210
+ )
211
+ if self._log_api_call_info:
212
+ self._log_to_debug(
213
+ f"Finished fetching {url} in {time_diff:.2f}s."
214
+ )
215
+
216
+ if return_url:
217
+ return str(response_url)
218
+ return resp_data
219
+
220
+ async def _fetch_aws_put_resp(self, final_url: str, put_data: bytes) -> dict[Any, Any] | str:
221
+ resp = await self._fetch_ext_resp(
222
+ method=HTTP_METHODS.PUT,
223
+ base_url=final_url,
224
+ is_json=False,
225
+ put_data=put_data,
226
+ )
227
+
228
+ if isinstance(resp, str) and len(resp) > 0:
229
+ raise AnycubicAPIParsingError(ErrorsAPIParsing.api_error_aws.format(resp))
230
+
231
+ return resp
232
+
233
+ async def _fetch_api_resp(
234
+ self,
235
+ endpoint: AnycubicAPIEndpoint,
236
+ query: dict[str, Any] | None = None,
237
+ params: dict[str, Any] = {},
238
+ extra_headers: dict[str, Any] = {},
239
+ with_origin: str | None = AUTH_DOMAIN,
240
+ with_token: bool = True,
241
+ ) -> dict[Any, Any]:
242
+ resp = await self._fetch_ext_resp(
243
+ method=endpoint.method,
244
+ base_url=self._build_api_url(endpoint),
245
+ query=query,
246
+ params=params,
247
+ extra_headers=self.anycubic_auth.get_auth_headers(
248
+ with_token=with_token
249
+ ),
250
+ with_origin=with_origin,
251
+ )
252
+ return resp
253
+
254
+ #
255
+ #
256
+ # Login Functions
257
+ # ------------------------------------------
258
+
259
+ def set_authentication(
260
+ self,
261
+ auth_token: str | None,
262
+ auth_mode: AnycubicAuthMode | int | None = None,
263
+ device_id: str | None = None,
264
+ auth_access_token: str | None = None,
265
+ auto_pick_token: bool = True,
266
+ ) -> None:
267
+ if not auth_token and not auth_access_token:
268
+ raise AnycubicAuthError(ErrorsAuth.set_auth_missing_token)
269
+
270
+ if isinstance(auth_mode, int):
271
+ auth_mode = AnycubicAuthMode(auth_mode)
272
+
273
+ if (
274
+ auto_pick_token and (
275
+ not auth_access_token
276
+ and auth_mode == AnycubicAuthMode.SLICER
277
+ )
278
+ ):
279
+ auth_access_token = f"{auth_token}"
280
+ auth_token = None
281
+
282
+ self._anycubic_auth = AnycubicAuthentication(
283
+ auth_token=auth_token,
284
+ auth_mode=auth_mode,
285
+ device_id=device_id,
286
+ auth_access_token=auth_access_token,
287
+ )
288
+
289
+ async def _get_user_token_with_access_token_with_retry(self) -> None:
290
+ retries = ACCESS_TOKEN_LOGIN_RETRIES
291
+ for x in range(retries):
292
+ try:
293
+ await self._get_user_token_with_access_token()
294
+ return
295
+ except AnycubicAuthError:
296
+ if x < retries - 1:
297
+ await asyncio.sleep(ACCESS_TOKEN_LOGIN_RETRY_INTERVAL)
298
+ else:
299
+ raise
300
+
301
+ async def _get_user_token_with_access_token(self) -> None:
302
+ params = self.anycubic_auth.auth_access_token_payload
303
+ resp = await self._fetch_api_resp(
304
+ endpoint=API_ENDPOINT.auth_sig_token,
305
+ query=None,
306
+ params=params,
307
+ with_token=False,
308
+ )
309
+ if not resp or not resp['data']:
310
+ server_message = resp.get('msg') if resp else None
311
+ error_message = ErrorsAuth.access_token_login_failed.format(server_message)
312
+ self._log_to_debug(error_message)
313
+ raise AnycubicAuthError(error_message)
314
+ self.anycubic_auth.set_auth_token(
315
+ resp['data']['token']
316
+ )
317
+ self._log_to_debug("Logged in and retrieved user token with access_token.")
318
+
319
+ def get_auth_config_dict(self) -> dict[str, Any]:
320
+ self._tokens_changed = False
321
+
322
+ return self.anycubic_auth.get_auth_config_dict()
323
+
324
+ def load_auth_config_from_dict(
325
+ self,
326
+ data: dict[str, Any],
327
+ minimal: bool = False,
328
+ ) -> None:
329
+ self.anycubic_auth.load_auth_config_from_dict(
330
+ data,
331
+ minimal=minimal,
332
+ )
333
+ self._log_to_debug("Loaded auth tokens from dict.")
334
+
335
+ async def _load_cached_web_auth_token(self) -> None:
336
+ if (
337
+ self._cached_web_auth_token_path is not None
338
+ and (await aio_path.exists(self._cached_web_auth_token_path))
339
+ ):
340
+
341
+ try:
342
+ async with aio_file_open(self._cached_web_auth_token_path, mode='r') as wo:
343
+ token = await wo.read()
344
+ self.set_authentication(
345
+ auth_token=token,
346
+ )
347
+
348
+ except Exception:
349
+ pass
350
+
351
+ async def _check_can_access_api(
352
+ self,
353
+ ) -> bool:
354
+ await self._load_cached_web_auth_token()
355
+ if self.anycubic_auth.requires_access_token:
356
+ try:
357
+ await self._get_user_token_with_access_token_with_retry()
358
+ except AnycubicAuthError:
359
+ return False
360
+ try:
361
+ await self.get_user_info()
362
+ return True
363
+ except AnycubicAuthTokensExpired:
364
+ self._log_to_debug("Tokens expired.")
365
+ return False
366
+
367
+ async def check_api_tokens(self) -> bool:
368
+ if not await self._check_can_access_api():
369
+ if self.anycubic_auth.clear_cached_access_user_token():
370
+ self._tokens_changed = True
371
+ self._log_to_debug("Cleared cached user token.")
372
+ return await self._check_can_access_api()
373
+ return False
374
+
375
+ return True
376
+
377
+ async def get_user_info(
378
+ self,
379
+ raw_data: bool = False,
380
+ ) -> dict[str, Any]:
381
+ resp = await self._fetch_api_resp(endpoint=API_ENDPOINT.user_info)
382
+ if raw_data:
383
+ return resp
384
+
385
+ data: dict[str, Any] | None = resp['data']
386
+ if resp and resp.get('msg') == 'request error':
387
+ raise AnycubicAPIParsingError(ErrorsAPIParsing.api_error_user_server_maintenance)
388
+ if data is None:
389
+ raise AnycubicAuthTokensExpired(ErrorsAuthTokenExpired.invalid_credentials)
390
+
391
+ # A rejected token can still return a data object, just without a user
392
+ # id. Treat that as invalid credentials so it surfaces as "invalid
393
+ # auth" rather than a TypeError from int(None) further down.
394
+ if data.get('id') is None:
395
+ raise AnycubicAuthTokensExpired(ErrorsAuthTokenExpired.invalid_credentials)
396
+
397
+ self.anycubic_auth.set_api_user_id(data['id'])
398
+ self.anycubic_auth.set_api_user_email(data.get('user_email'))
399
+
400
+ return data