forked from uusense/python-devicebase
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy patherror_handling.py
More file actions
144 lines (112 loc) · 4.86 KB
/
Copy patherror_handling.py
File metadata and controls
144 lines (112 loc) · 4.86 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
"""Error handling: the two failure layers, and recovering from them.
The API reports failures in two places, and only one of them is the HTTP status
line. An action that fails — a selector matching nothing, a device that went
offline — still answers HTTP 200 and carries the failure in the envelope:
{"code": 502, "message": "-32602: Invalid parameters"}
Both layers raise here, so a caller that catches ``DeviceBaseError`` cannot
mistake a failed action for a successful one.
Run with:
DEVICEBASE_API_KEY=… python examples/error_handling.py
"""
import time
from devicebase import (
AuthenticationError,
BusinessError,
DeviceBaseClient,
DeviceBaseError,
DeviceNotFoundError,
ValidationError,
list_devices,
)
MISSING_SERIAL = "definitely-not-a-device"
def bad_api_key() -> None:
"""A rejected key is an HTTP 401, carrying the server's own message."""
with DeviceBaseClient(serialno="x", api_key="invalid-key") as client:
try:
client.get_device_info()
except AuthenticationError as exc:
print(f"auth HTTP {exc.status_code}: {exc.message[:70]}")
def wrong_serial() -> None:
try:
with DeviceBaseClient(serialno=MISSING_SERIAL, timeout=60.0) as client:
client.get_device_info()
except DeviceNotFoundError as exc:
print(f"not found HTTP {exc.status_code}: {exc.message[:70]}")
def failed_action() -> None:
"""A failure inside a successful HTTP response."""
devices = list_devices(limit=1)
if not devices:
print("no devices available")
return
with DeviceBaseClient(serialno=devices[0].serialno) as client:
try:
client.tap(1, 1)
except BusinessError as exc:
# status_code is None here: the transport succeeded, the action did
# not. exc.code carries the envelope's code.
print(f"business code {exc.code} (HTTP {exc.status_code}): {exc.message[:60]}")
print(f" body: {exc.body[:80]}")
except DeviceNotFoundError as exc:
print(f"not found HTTP {exc.status_code}: {exc.message[:70]}")
def one_handler_for_everything() -> None:
"""Every error here is a DeviceBaseError, so the specific ones go first.
ValidationError and DeviceNotFoundError are siblings, and both are caught
by DeviceBaseError — catching the base class first would swallow them.
"""
try:
with DeviceBaseClient(serialno=MISSING_SERIAL, timeout=60.0) as client:
client.get_device_info()
except ValidationError as exc:
print(f"validation: {exc.message[:70]}")
except DeviceNotFoundError as exc:
print(f"not found: {exc.message[:70]}") # this one fires
except DeviceBaseError as exc:
print(f"other: {exc.message[:70]}")
def rejected_argument() -> None:
"""A bad argument is rejected locally, before anything is sent.
It raises the SDK's own ValidationError rather than a bare ValueError, so
the same ``except DeviceBaseError`` above covers it. ``status_code`` is None
— no request was made.
"""
with DeviceBaseClient(base_url="https://api.devicebase.cn", api_key="unused") as client:
try:
client.http.computer_scroll("pc-1", "diagonal") # type: ignore[arg-type]
except ValidationError as exc:
# status_code is None: rejected locally, so no request was made.
where = f"HTTP {exc.status_code}" if exc.status_code else "local"
print(f"rejected {where}: {exc.message[:70]}")
def retry_with_backoff(max_attempts: int = 3, delay: float = 1.0) -> None:
"""Retry a call that fails because the device is not ready.
Only transient failures are worth retrying. A ``ValidationError`` means the
request itself is malformed and will fail identically every time.
"""
devices = list_devices(limit=1)
if not devices:
print("no devices available")
return
with DeviceBaseClient(serialno=devices[0].serialno) as client:
for attempt in range(1, max_attempts + 1):
try:
client.get_device_info()
except DeviceBaseError as exc:
print(f"attempt {attempt}/{max_attempts}: {type(exc).__name__}")
if attempt == max_attempts:
print("giving up")
return
time.sleep(delay * 2 ** (attempt - 1)) # exponential backoff
else:
print(f"attempt {attempt}: ok")
return
if __name__ == "__main__":
print("=== Rejected API key ===")
bad_api_key()
print("\n=== Unknown serialno ===")
wrong_serial()
print("\n=== Action failure inside HTTP 200 ===")
failed_action()
print("\n=== Rejected argument ===")
rejected_argument()
print("\n=== Catch order ===")
one_handler_for_everything()
print("\n=== Retry ===")
retry_with_backoff()