anycubic-cloud-api 0.4.4__tar.gz → 0.4.6__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.
- {anycubic_cloud_api-0.4.4/src/anycubic_cloud_api.egg-info → anycubic_cloud_api-0.4.6}/PKG-INFO +1 -1
- {anycubic_cloud_api-0.4.4 → anycubic_cloud_api-0.4.6}/pyproject.toml +1 -1
- {anycubic_cloud_api-0.4.4 → anycubic_cloud_api-0.4.6}/src/anycubic_cloud_api/api/base.py +11 -1
- {anycubic_cloud_api-0.4.4 → anycubic_cloud_api-0.4.6}/src/anycubic_cloud_api/data_models/printer.py +19 -5
- {anycubic_cloud_api-0.4.4 → anycubic_cloud_api-0.4.6}/src/anycubic_cloud_api/models/auth.py +21 -0
- {anycubic_cloud_api-0.4.4 → anycubic_cloud_api-0.4.6/src/anycubic_cloud_api.egg-info}/PKG-INFO +1 -1
- {anycubic_cloud_api-0.4.4 → anycubic_cloud_api-0.4.6}/src/anycubic_cloud_api.egg-info/SOURCES.txt +1 -0
- anycubic_cloud_api-0.4.6/tests/test_auth_mode_fallback.py +66 -0
- {anycubic_cloud_api-0.4.4 → anycubic_cloud_api-0.4.6}/tests/test_lan_reports.py +62 -0
- {anycubic_cloud_api-0.4.4 → anycubic_cloud_api-0.4.6}/COPYRIGHT +0 -0
- {anycubic_cloud_api-0.4.4 → anycubic_cloud_api-0.4.6}/LICENSE +0 -0
- {anycubic_cloud_api-0.4.4 → anycubic_cloud_api-0.4.6}/MANIFEST.in +0 -0
- {anycubic_cloud_api-0.4.4 → anycubic_cloud_api-0.4.6}/README.md +0 -0
- {anycubic_cloud_api-0.4.4 → anycubic_cloud_api-0.4.6}/setup.cfg +0 -0
- {anycubic_cloud_api-0.4.4 → anycubic_cloud_api-0.4.6}/src/anycubic_cloud_api/__init__.py +0 -0
- {anycubic_cloud_api-0.4.4 → anycubic_cloud_api-0.4.6}/src/anycubic_cloud_api/anycubic_api.py +0 -0
- {anycubic_cloud_api-0.4.4 → anycubic_cloud_api-0.4.6}/src/anycubic_cloud_api/api/functions.py +0 -0
- {anycubic_cloud_api-0.4.4 → anycubic_cloud_api-0.4.6}/src/anycubic_cloud_api/api/mqtt.py +0 -0
- {anycubic_cloud_api-0.4.4 → anycubic_cloud_api-0.4.6}/src/anycubic_cloud_api/const/api_endpoints.py +0 -0
- {anycubic_cloud_api-0.4.4 → anycubic_cloud_api-0.4.6}/src/anycubic_cloud_api/const/const.py +0 -0
- {anycubic_cloud_api-0.4.4 → anycubic_cloud_api-0.4.6}/src/anycubic_cloud_api/const/enums.py +0 -0
- {anycubic_cloud_api-0.4.4 → anycubic_cloud_api-0.4.6}/src/anycubic_cloud_api/const/lan.py +0 -0
- {anycubic_cloud_api-0.4.4 → anycubic_cloud_api-0.4.6}/src/anycubic_cloud_api/const/mqtt.py +0 -0
- {anycubic_cloud_api-0.4.4 → anycubic_cloud_api-0.4.6}/src/anycubic_cloud_api/data_models/consumable.py +0 -0
- {anycubic_cloud_api-0.4.4 → anycubic_cloud_api-0.4.6}/src/anycubic_cloud_api/data_models/files.py +0 -0
- {anycubic_cloud_api-0.4.4 → anycubic_cloud_api-0.4.6}/src/anycubic_cloud_api/data_models/gcode_file.py +0 -0
- {anycubic_cloud_api-0.4.4 → anycubic_cloud_api-0.4.6}/src/anycubic_cloud_api/data_models/orders.py +0 -0
- {anycubic_cloud_api-0.4.4 → anycubic_cloud_api-0.4.6}/src/anycubic_cloud_api/data_models/print_response.py +0 -0
- {anycubic_cloud_api-0.4.4 → anycubic_cloud_api-0.4.6}/src/anycubic_cloud_api/data_models/print_speed_mode.py +0 -0
- {anycubic_cloud_api-0.4.4 → anycubic_cloud_api-0.4.6}/src/anycubic_cloud_api/data_models/printer_properties.py +0 -0
- {anycubic_cloud_api-0.4.4 → anycubic_cloud_api-0.4.6}/src/anycubic_cloud_api/data_models/printing_settings.py +0 -0
- {anycubic_cloud_api-0.4.4 → anycubic_cloud_api-0.4.6}/src/anycubic_cloud_api/data_models/project.py +0 -0
- {anycubic_cloud_api-0.4.4 → anycubic_cloud_api-0.4.6}/src/anycubic_cloud_api/exceptions/error_strings.py +0 -0
- {anycubic_cloud_api-0.4.4 → anycubic_cloud_api-0.4.6}/src/anycubic_cloud_api/exceptions/exceptions.py +0 -0
- {anycubic_cloud_api-0.4.4 → anycubic_cloud_api-0.4.6}/src/anycubic_cloud_api/helpers/helpers.py +0 -0
- {anycubic_cloud_api-0.4.4 → anycubic_cloud_api-0.4.6}/src/anycubic_cloud_api/lan/__init__.py +0 -0
- {anycubic_cloud_api-0.4.4 → anycubic_cloud_api-0.4.6}/src/anycubic_cloud_api/lan/client.py +0 -0
- {anycubic_cloud_api-0.4.4 → anycubic_cloud_api-0.4.6}/src/anycubic_cloud_api/lan/handshake.py +0 -0
- {anycubic_cloud_api-0.4.4 → anycubic_cloud_api-0.4.6}/src/anycubic_cloud_api/models/cloud_upload.py +0 -0
- {anycubic_cloud_api-0.4.4 → anycubic_cloud_api-0.4.6}/src/anycubic_cloud_api/models/http.py +0 -0
- {anycubic_cloud_api-0.4.4 → anycubic_cloud_api-0.4.6}/src/anycubic_cloud_api/resources/anycubic_mqqt_tls_ca.crt +0 -0
- {anycubic_cloud_api-0.4.4 → anycubic_cloud_api-0.4.6}/src/anycubic_cloud_api/resources/anycubic_mqqt_tls_client.crt +0 -0
- {anycubic_cloud_api-0.4.4 → anycubic_cloud_api-0.4.6}/src/anycubic_cloud_api/resources/anycubic_mqqt_tls_client.key +0 -0
- {anycubic_cloud_api-0.4.4 → anycubic_cloud_api-0.4.6}/src/anycubic_cloud_api.egg-info/dependency_links.txt +0 -0
- {anycubic_cloud_api-0.4.4 → anycubic_cloud_api-0.4.6}/src/anycubic_cloud_api.egg-info/requires.txt +0 -0
- {anycubic_cloud_api-0.4.4 → anycubic_cloud_api-0.4.6}/src/anycubic_cloud_api.egg-info/top_level.txt +0 -0
- {anycubic_cloud_api-0.4.4 → anycubic_cloud_api-0.4.6}/tests/test_axis_position.py +0 -0
- {anycubic_cloud_api-0.4.4 → anycubic_cloud_api-0.4.6}/tests/test_lan_client.py +0 -0
- {anycubic_cloud_api-0.4.4 → anycubic_cloud_api-0.4.6}/tests/test_lan_handshake.py +0 -0
- {anycubic_cloud_api-0.4.4 → anycubic_cloud_api-0.4.6}/tests/test_partial_reports.py +0 -0
|
@@ -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
|
-
|
|
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
|
{anycubic_cloud_api-0.4.4 → anycubic_cloud_api-0.4.6}/src/anycubic_cloud_api/data_models/printer.py
RENAMED
|
@@ -1995,11 +1995,25 @@ class AnycubicPrinter:
|
|
|
1995
1995
|
|
|
1996
1996
|
@property
|
|
1997
1997
|
def primary_multi_color_box_loaded_slot(self) -> int | None:
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
1998
|
+
box = self.primary_multi_color_box
|
|
1999
|
+
|
|
2000
|
+
if not box:
|
|
2001
|
+
return None
|
|
2002
|
+
|
|
2003
|
+
slot = box.loaded_slot
|
|
2004
|
+
|
|
2005
|
+
# -1 means nothing is loaded; surface that as unknown rather than a
|
|
2006
|
+
# slot number that doesn't exist.
|
|
2007
|
+
if slot is not None and slot >= 0:
|
|
2008
|
+
return slot
|
|
2009
|
+
|
|
2010
|
+
# Some printers leave the box-level field at -1 even mid-print, while
|
|
2011
|
+
# still marking the feeding slot's own status as loaded. Observed on a
|
|
2012
|
+
# Kobra S1 printing from slot 3. Without this the filament used by such
|
|
2013
|
+
# a job cannot be charged to any spool.
|
|
2014
|
+
for spool in box.slots or []:
|
|
2015
|
+
if spool is not None and spool.spool_loaded:
|
|
2016
|
+
return int(spool.slot_index)
|
|
2003
2017
|
|
|
2004
2018
|
return None
|
|
2005
2019
|
|
|
@@ -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,
|
{anycubic_cloud_api-0.4.4 → anycubic_cloud_api-0.4.6}/src/anycubic_cloud_api.egg-info/SOURCES.txt
RENAMED
|
@@ -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
|
|
@@ -479,3 +479,65 @@ class TestLevellingReadsAsLevelling:
|
|
|
479
479
|
|
|
480
480
|
def test_an_unmapped_code_with_no_text_is_unknown(self):
|
|
481
481
|
assert self._status(99) == "unknown"
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
class TestLoadedSlotFallback:
|
|
485
|
+
"""Some printers leave the box-level loaded_slot at -1 while printing.
|
|
486
|
+
|
|
487
|
+
Observed live on a Kobra S1 feeding from slot 3: the box reported -1, but
|
|
488
|
+
the slot's own status was 5 (loaded). Filament used by such a job could not
|
|
489
|
+
be charged to any spool, because attribution falls back on this value when
|
|
490
|
+
the job carries no per-slot breakdown.
|
|
491
|
+
"""
|
|
492
|
+
|
|
493
|
+
def _printer(self, loaded_slot, statuses):
|
|
494
|
+
from unittest.mock import MagicMock
|
|
495
|
+
from anycubic_cloud_api.data_models.printer import AnycubicPrinter
|
|
496
|
+
|
|
497
|
+
slots = [
|
|
498
|
+
{
|
|
499
|
+
"index": i,
|
|
500
|
+
"sku": "",
|
|
501
|
+
"type": "PLA",
|
|
502
|
+
"color": [1, 2, 3],
|
|
503
|
+
"status": st,
|
|
504
|
+
"edit_status": 1,
|
|
505
|
+
}
|
|
506
|
+
for i, st in enumerate(statuses)
|
|
507
|
+
]
|
|
508
|
+
return AnycubicPrinter(
|
|
509
|
+
api_parent=MagicMock(),
|
|
510
|
+
machine_type=20025,
|
|
511
|
+
machine_name="Kobra S1",
|
|
512
|
+
id=1,
|
|
513
|
+
multi_color_box=[{"id": 0, "box_id": 0, "loaded_slot": loaded_slot, "slots": slots}],
|
|
514
|
+
ignore_init_errors=True,
|
|
515
|
+
)
|
|
516
|
+
|
|
517
|
+
def test_the_box_level_field_is_preferred_when_it_is_set(self):
|
|
518
|
+
printer = self._printer(1, [4, 5, 4, 4])
|
|
519
|
+
|
|
520
|
+
assert printer.primary_multi_color_box_loaded_slot == 1
|
|
521
|
+
|
|
522
|
+
def test_minus_one_falls_back_to_the_slot_marked_loaded(self):
|
|
523
|
+
"""The live case: box says -1, slot 2 says status 5."""
|
|
524
|
+
printer = self._printer(-1, [4, 4, 5, 4])
|
|
525
|
+
|
|
526
|
+
assert printer.primary_multi_color_box_loaded_slot == 2
|
|
527
|
+
|
|
528
|
+
def test_nothing_loaded_still_reads_as_unknown(self):
|
|
529
|
+
printer = self._printer(-1, [4, 4, 4, 4])
|
|
530
|
+
|
|
531
|
+
assert printer.primary_multi_color_box_loaded_slot is None
|
|
532
|
+
|
|
533
|
+
def test_a_box_with_no_slots_is_harmless(self):
|
|
534
|
+
from unittest.mock import MagicMock
|
|
535
|
+
from anycubic_cloud_api.data_models.printer import AnycubicPrinter
|
|
536
|
+
|
|
537
|
+
printer = AnycubicPrinter(
|
|
538
|
+
api_parent=MagicMock(), machine_type=20025, machine_name="x", id=1,
|
|
539
|
+
multi_color_box=[{"id": 0, "box_id": 0, "loaded_slot": -1, "slots": []}],
|
|
540
|
+
ignore_init_errors=True,
|
|
541
|
+
)
|
|
542
|
+
|
|
543
|
+
assert printer.primary_multi_color_box_loaded_slot is None
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{anycubic_cloud_api-0.4.4 → anycubic_cloud_api-0.4.6}/src/anycubic_cloud_api/anycubic_api.py
RENAMED
|
File without changes
|
{anycubic_cloud_api-0.4.4 → anycubic_cloud_api-0.4.6}/src/anycubic_cloud_api/api/functions.py
RENAMED
|
File without changes
|
|
File without changes
|
{anycubic_cloud_api-0.4.4 → anycubic_cloud_api-0.4.6}/src/anycubic_cloud_api/const/api_endpoints.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{anycubic_cloud_api-0.4.4 → anycubic_cloud_api-0.4.6}/src/anycubic_cloud_api/data_models/files.py
RENAMED
|
File without changes
|
|
File without changes
|
{anycubic_cloud_api-0.4.4 → anycubic_cloud_api-0.4.6}/src/anycubic_cloud_api/data_models/orders.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{anycubic_cloud_api-0.4.4 → anycubic_cloud_api-0.4.6}/src/anycubic_cloud_api/data_models/project.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{anycubic_cloud_api-0.4.4 → anycubic_cloud_api-0.4.6}/src/anycubic_cloud_api/helpers/helpers.py
RENAMED
|
File without changes
|
{anycubic_cloud_api-0.4.4 → anycubic_cloud_api-0.4.6}/src/anycubic_cloud_api/lan/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{anycubic_cloud_api-0.4.4 → anycubic_cloud_api-0.4.6}/src/anycubic_cloud_api/lan/handshake.py
RENAMED
|
File without changes
|
{anycubic_cloud_api-0.4.4 → anycubic_cloud_api-0.4.6}/src/anycubic_cloud_api/models/cloud_upload.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{anycubic_cloud_api-0.4.4 → anycubic_cloud_api-0.4.6}/src/anycubic_cloud_api.egg-info/requires.txt
RENAMED
|
File without changes
|
{anycubic_cloud_api-0.4.4 → anycubic_cloud_api-0.4.6}/src/anycubic_cloud_api.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|