anycubic-cloud-api 0.4.5__tar.gz → 0.4.7__tar.gz

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 (50) hide show
  1. {anycubic_cloud_api-0.4.5/src/anycubic_cloud_api.egg-info → anycubic_cloud_api-0.4.7}/PKG-INFO +1 -1
  2. {anycubic_cloud_api-0.4.5 → anycubic_cloud_api-0.4.7}/pyproject.toml +1 -1
  3. {anycubic_cloud_api-0.4.5 → anycubic_cloud_api-0.4.7}/src/anycubic_cloud_api/api/base.py +11 -1
  4. {anycubic_cloud_api-0.4.5 → anycubic_cloud_api-0.4.7}/src/anycubic_cloud_api/data_models/printer_properties.py +20 -0
  5. {anycubic_cloud_api-0.4.5 → anycubic_cloud_api-0.4.7}/src/anycubic_cloud_api/models/auth.py +21 -0
  6. {anycubic_cloud_api-0.4.5 → anycubic_cloud_api-0.4.7/src/anycubic_cloud_api.egg-info}/PKG-INFO +1 -1
  7. {anycubic_cloud_api-0.4.5 → anycubic_cloud_api-0.4.7}/src/anycubic_cloud_api.egg-info/SOURCES.txt +1 -0
  8. anycubic_cloud_api-0.4.7/tests/test_auth_mode_fallback.py +66 -0
  9. {anycubic_cloud_api-0.4.5 → anycubic_cloud_api-0.4.7}/COPYRIGHT +0 -0
  10. {anycubic_cloud_api-0.4.5 → anycubic_cloud_api-0.4.7}/LICENSE +0 -0
  11. {anycubic_cloud_api-0.4.5 → anycubic_cloud_api-0.4.7}/MANIFEST.in +0 -0
  12. {anycubic_cloud_api-0.4.5 → anycubic_cloud_api-0.4.7}/README.md +0 -0
  13. {anycubic_cloud_api-0.4.5 → anycubic_cloud_api-0.4.7}/setup.cfg +0 -0
  14. {anycubic_cloud_api-0.4.5 → anycubic_cloud_api-0.4.7}/src/anycubic_cloud_api/__init__.py +0 -0
  15. {anycubic_cloud_api-0.4.5 → anycubic_cloud_api-0.4.7}/src/anycubic_cloud_api/anycubic_api.py +0 -0
  16. {anycubic_cloud_api-0.4.5 → anycubic_cloud_api-0.4.7}/src/anycubic_cloud_api/api/functions.py +0 -0
  17. {anycubic_cloud_api-0.4.5 → anycubic_cloud_api-0.4.7}/src/anycubic_cloud_api/api/mqtt.py +0 -0
  18. {anycubic_cloud_api-0.4.5 → anycubic_cloud_api-0.4.7}/src/anycubic_cloud_api/const/api_endpoints.py +0 -0
  19. {anycubic_cloud_api-0.4.5 → anycubic_cloud_api-0.4.7}/src/anycubic_cloud_api/const/const.py +0 -0
  20. {anycubic_cloud_api-0.4.5 → anycubic_cloud_api-0.4.7}/src/anycubic_cloud_api/const/enums.py +0 -0
  21. {anycubic_cloud_api-0.4.5 → anycubic_cloud_api-0.4.7}/src/anycubic_cloud_api/const/lan.py +0 -0
  22. {anycubic_cloud_api-0.4.5 → anycubic_cloud_api-0.4.7}/src/anycubic_cloud_api/const/mqtt.py +0 -0
  23. {anycubic_cloud_api-0.4.5 → anycubic_cloud_api-0.4.7}/src/anycubic_cloud_api/data_models/consumable.py +0 -0
  24. {anycubic_cloud_api-0.4.5 → anycubic_cloud_api-0.4.7}/src/anycubic_cloud_api/data_models/files.py +0 -0
  25. {anycubic_cloud_api-0.4.5 → anycubic_cloud_api-0.4.7}/src/anycubic_cloud_api/data_models/gcode_file.py +0 -0
  26. {anycubic_cloud_api-0.4.5 → anycubic_cloud_api-0.4.7}/src/anycubic_cloud_api/data_models/orders.py +0 -0
  27. {anycubic_cloud_api-0.4.5 → anycubic_cloud_api-0.4.7}/src/anycubic_cloud_api/data_models/print_response.py +0 -0
  28. {anycubic_cloud_api-0.4.5 → anycubic_cloud_api-0.4.7}/src/anycubic_cloud_api/data_models/print_speed_mode.py +0 -0
  29. {anycubic_cloud_api-0.4.5 → anycubic_cloud_api-0.4.7}/src/anycubic_cloud_api/data_models/printer.py +0 -0
  30. {anycubic_cloud_api-0.4.5 → anycubic_cloud_api-0.4.7}/src/anycubic_cloud_api/data_models/printing_settings.py +0 -0
  31. {anycubic_cloud_api-0.4.5 → anycubic_cloud_api-0.4.7}/src/anycubic_cloud_api/data_models/project.py +0 -0
  32. {anycubic_cloud_api-0.4.5 → anycubic_cloud_api-0.4.7}/src/anycubic_cloud_api/exceptions/error_strings.py +0 -0
  33. {anycubic_cloud_api-0.4.5 → anycubic_cloud_api-0.4.7}/src/anycubic_cloud_api/exceptions/exceptions.py +0 -0
  34. {anycubic_cloud_api-0.4.5 → anycubic_cloud_api-0.4.7}/src/anycubic_cloud_api/helpers/helpers.py +0 -0
  35. {anycubic_cloud_api-0.4.5 → anycubic_cloud_api-0.4.7}/src/anycubic_cloud_api/lan/__init__.py +0 -0
  36. {anycubic_cloud_api-0.4.5 → anycubic_cloud_api-0.4.7}/src/anycubic_cloud_api/lan/client.py +0 -0
  37. {anycubic_cloud_api-0.4.5 → anycubic_cloud_api-0.4.7}/src/anycubic_cloud_api/lan/handshake.py +0 -0
  38. {anycubic_cloud_api-0.4.5 → anycubic_cloud_api-0.4.7}/src/anycubic_cloud_api/models/cloud_upload.py +0 -0
  39. {anycubic_cloud_api-0.4.5 → anycubic_cloud_api-0.4.7}/src/anycubic_cloud_api/models/http.py +0 -0
  40. {anycubic_cloud_api-0.4.5 → anycubic_cloud_api-0.4.7}/src/anycubic_cloud_api/resources/anycubic_mqqt_tls_ca.crt +0 -0
  41. {anycubic_cloud_api-0.4.5 → anycubic_cloud_api-0.4.7}/src/anycubic_cloud_api/resources/anycubic_mqqt_tls_client.crt +0 -0
  42. {anycubic_cloud_api-0.4.5 → anycubic_cloud_api-0.4.7}/src/anycubic_cloud_api/resources/anycubic_mqqt_tls_client.key +0 -0
  43. {anycubic_cloud_api-0.4.5 → anycubic_cloud_api-0.4.7}/src/anycubic_cloud_api.egg-info/dependency_links.txt +0 -0
  44. {anycubic_cloud_api-0.4.5 → anycubic_cloud_api-0.4.7}/src/anycubic_cloud_api.egg-info/requires.txt +0 -0
  45. {anycubic_cloud_api-0.4.5 → anycubic_cloud_api-0.4.7}/src/anycubic_cloud_api.egg-info/top_level.txt +0 -0
  46. {anycubic_cloud_api-0.4.5 → anycubic_cloud_api-0.4.7}/tests/test_axis_position.py +0 -0
  47. {anycubic_cloud_api-0.4.5 → anycubic_cloud_api-0.4.7}/tests/test_lan_client.py +0 -0
  48. {anycubic_cloud_api-0.4.5 → anycubic_cloud_api-0.4.7}/tests/test_lan_handshake.py +0 -0
  49. {anycubic_cloud_api-0.4.5 → anycubic_cloud_api-0.4.7}/tests/test_lan_reports.py +0 -0
  50. {anycubic_cloud_api-0.4.5 → anycubic_cloud_api-0.4.7}/tests/test_partial_reports.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: anycubic-cloud-api
3
- Version: 0.4.5
3
+ Version: 0.4.7
4
4
  Summary: Async client for the Anycubic Cloud API and its MQTT stream
5
5
  Author: WaresWichall, Nino Bondonno
6
6
  Maintainer: Nino Bondonno
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "anycubic-cloud-api"
7
- version = "0.4.5"
7
+ version = "0.4.7"
8
8
  description = "Async client for the Anycubic Cloud API and its MQTT stream"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.12"
@@ -356,7 +356,17 @@ class AnycubicAPIBase:
356
356
  try:
357
357
  await self._get_user_token_with_access_token_with_retry()
358
358
  except AnycubicAuthError:
359
- return False
359
+ # A web token is a JWT too, so one can be mistaken for a slicer
360
+ # token and sent to a login endpoint that will never accept it.
361
+ # Try it as a plain user token before declaring the credentials
362
+ # bad -- they may be perfectly good.
363
+ if self.anycubic_auth.retry_access_token_as_user_token():
364
+ self._log_to_debug(
365
+ "Access token login failed; retrying it as a web token."
366
+ )
367
+ self._tokens_changed = True
368
+ else:
369
+ return False
360
370
  try:
361
371
  await self.get_user_info()
362
372
  return True
@@ -493,6 +493,10 @@ class AnycubicDryingStatus:
493
493
  )
494
494
 
495
495
 
496
+ # `edit_status` on a slot: 0 read from a tag, 1 entered by hand, 2 empty.
497
+ SPOOL_EDIT_STATUS_EMPTY = 2
498
+
499
+
496
500
  class AnycubicSpoolInfo:
497
501
  __slots__ = (
498
502
  "_index",
@@ -599,6 +603,22 @@ class AnycubicSpoolInfo:
599
603
  def edit_status(self) -> int:
600
604
  return self._edit_status
601
605
 
606
+ @property
607
+ def spool_present(self) -> bool:
608
+ """Whether there is actually a reel in this slot.
609
+
610
+ The ACE keeps reporting the last material, colour and SKU it saw in a
611
+ slot after the reel is taken out, so those fields say nothing about
612
+ whether anything is loaded. `edit_status` does: 0 means the details
613
+ were read from a tag, 1 that they were typed in, and 2 that the slot
614
+ is empty.
615
+
616
+ Confirmed on a Kobra S1 with one reel in slot 3 -- that slot alone
617
+ reported 0, and the three empty ones all reported 2 while still
618
+ showing the material they last held.
619
+ """
620
+ return self._edit_status != SPOOL_EDIT_STATUS_EMPTY
621
+
602
622
  @property
603
623
  def icon_type(self) -> int | None:
604
624
  return self._icon_type
@@ -133,6 +133,27 @@ class AnycubicAuthentication:
133
133
  or self._auth_mode == AnycubicAuthMode.ANDROID
134
134
  )
135
135
 
136
+ def retry_access_token_as_user_token(self) -> bool:
137
+ """Treat a rejected slicer token as a web token instead.
138
+
139
+ Slicer and web tokens are both JWTs and cannot be told apart by
140
+ looking at them, so a web token can be taken for a slicer one. That
141
+ sends it to a login endpoint which answers "User does not exist", and
142
+ setup fails for a token that was perfectly good.
143
+
144
+ Rather than guess harder, the failure is used as the signal: move the
145
+ token back to being a plain user token and carry on as web. Returns
146
+ False if there is nothing left to try.
147
+ """
148
+ if self._auth_access_token is None or self._auth_token is not None:
149
+ return False
150
+
151
+ self._auth_token = self._auth_access_token
152
+ self._auth_access_token = None
153
+ self._auth_mode = AnycubicAuthMode.WEB
154
+
155
+ return True
156
+
136
157
  def set_auth_token(
137
158
  self,
138
159
  auth_token: str,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: anycubic-cloud-api
3
- Version: 0.4.5
3
+ Version: 0.4.7
4
4
  Summary: Async client for the Anycubic Cloud API and its MQTT stream
5
5
  Author: WaresWichall, Nino Bondonno
6
6
  Maintainer: Nino Bondonno
@@ -40,6 +40,7 @@ src/anycubic_cloud_api/models/http.py
40
40
  src/anycubic_cloud_api/resources/anycubic_mqqt_tls_ca.crt
41
41
  src/anycubic_cloud_api/resources/anycubic_mqqt_tls_client.crt
42
42
  src/anycubic_cloud_api/resources/anycubic_mqqt_tls_client.key
43
+ tests/test_auth_mode_fallback.py
43
44
  tests/test_axis_position.py
44
45
  tests/test_lan_client.py
45
46
  tests/test_lan_handshake.py
@@ -0,0 +1,66 @@
1
+ """A web token mistaken for a slicer token must still work.
2
+
3
+ Reported by a user on a Kobra 2 Pro: a valid web token was rejected with
4
+ "Authentication failed. Check credentials." Both kinds are JWTs, so the shape
5
+ of the token cannot tell them apart -- the integration guessed slicer, sent it
6
+ to a login endpoint that answered "User does not exist", and gave up.
7
+ """
8
+
9
+ import pytest
10
+
11
+ from anycubic_cloud_api.models.auth import AnycubicAuthentication, AnycubicAuthMode
12
+
13
+ TOKEN = "eyJhbGciOiJSUzI1NiJ9.payload.signature"
14
+
15
+
16
+ def slicer_auth():
17
+ """What set_authentication produces for a token guessed to be a slicer one."""
18
+ return AnycubicAuthentication(
19
+ auth_token=None,
20
+ auth_mode=AnycubicAuthMode.SLICER,
21
+ device_id=None,
22
+ auth_access_token=TOKEN,
23
+ )
24
+
25
+
26
+ class TestRetryingAsAWebToken:
27
+ def test_a_rejected_slicer_token_becomes_a_web_token(self):
28
+ auth = slicer_auth()
29
+
30
+ assert auth.retry_access_token_as_user_token() is True
31
+ assert auth._auth_token == TOKEN
32
+ assert auth._auth_access_token is None
33
+ assert auth._auth_mode == AnycubicAuthMode.WEB
34
+
35
+ def test_it_no_longer_asks_for_an_access_token_login(self):
36
+ """Otherwise the caller would loop on the same failing endpoint."""
37
+ auth = slicer_auth()
38
+ assert auth.requires_access_token is True
39
+
40
+ auth.retry_access_token_as_user_token()
41
+
42
+ assert auth.requires_access_token is False
43
+
44
+ def test_it_only_fires_once(self):
45
+ auth = slicer_auth()
46
+
47
+ assert auth.retry_access_token_as_user_token() is True
48
+ assert auth.retry_access_token_as_user_token() is False
49
+
50
+ def test_a_genuine_web_token_has_nothing_to_retry(self):
51
+ auth = AnycubicAuthentication(
52
+ auth_token=TOKEN, auth_mode=AnycubicAuthMode.WEB,
53
+ device_id=None, auth_access_token=None,
54
+ )
55
+
56
+ assert auth.retry_access_token_as_user_token() is False
57
+
58
+ def test_an_android_token_is_left_alone(self):
59
+ """Android supplies a device id and is never ambiguous."""
60
+ auth = AnycubicAuthentication(
61
+ auth_token=TOKEN, auth_mode=AnycubicAuthMode.ANDROID,
62
+ device_id="device123", auth_access_token=None,
63
+ )
64
+
65
+ assert auth.retry_access_token_as_user_token() is False
66
+ assert auth._auth_mode == AnycubicAuthMode.ANDROID