Skip to content
This repository was archived by the owner on Jun 28, 2024. It is now read-only.

Commit b6ec63a

Browse files
authored
feat: Return access code immediately from access_code.create
2 parents 7656dac + ecec2fa commit b6ec63a

File tree

3 files changed

+82
-9
lines changed

3 files changed

+82
-9
lines changed

seamapi/access_codes.py

Lines changed: 46 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1+
import time
2+
from datetime import datetime, timezone, timedelta
13
from seamapi.types import (
4+
WaitForAccessCodeFailedException,
25
AbstractAccessCodes,
36
AccessCode,
47
AccessCodeId,
@@ -126,6 +129,8 @@ def create(
126129
starts_at: Optional[str] = None,
127130
ends_at: Optional[str] = None,
128131
common_code_key: Optional[str] = None,
132+
wait_for_code: Optional[bool] = False,
133+
timeout: Optional[int] = 300,
129134
) -> AccessCode:
130135
"""Creates an access code on a device.
131136
@@ -141,11 +146,17 @@ def create(
141146
Time when access code becomes effective
142147
ends_at : str, optional
143148
Time when access code ceases to be effective
149+
wait_for_code : bool, optional
150+
Poll the access code until the code is known.
151+
timeout : int, optional:
152+
Maximum polling time in seconds.
144153
145154
Raises
146155
------
147156
Exception
148157
If the API request wasn't successful.
158+
WaitForAccessCodeFailedException
159+
If waiting for code aborts due to error or timeout.
149160
150161
Returns
151162
------
@@ -165,18 +176,47 @@ def create(
165176
if common_code_key is not None:
166177
create_payload["common_code_key"] = common_code_key
167178

179+
if (wait_for_code
180+
and starts_at is not None
181+
and datetime.fromisoformat(starts_at) > datetime.now() + timedelta(seconds=5)
182+
):
183+
raise RuntimeError("Cannot use wait_for_code with a future time bound code")
184+
168185
res = self.seam.make_request(
169186
"POST",
170187
"/access_codes/create",
171188
json=create_payload,
172189
)
173190

174-
action_attempt = self.seam.action_attempts.poll_until_ready(
175-
res["action_attempt"]["action_attempt_id"]
176-
)
177-
success_res: Any = action_attempt.result
178-
179-
return AccessCode.from_dict(success_res["access_code"])
191+
access_code = AccessCode.from_dict(res["access_code"])
192+
193+
duration = 0
194+
poll_interval = 0.25
195+
if wait_for_code:
196+
while (access_code.code is None):
197+
if (access_code.status == "unknown"):
198+
raise WaitForAccessCodeFailedException(
199+
"Access code status returned unknown",
200+
access_code_id=access_code.access_code_id
201+
)
202+
if (len(access_code.errors) > 0):
203+
raise WaitForAccessCodeFailedException(
204+
"Access code returned errors",
205+
access_code_id=access_code.access_code_id,
206+
errors=access_code.errors
207+
)
208+
time.sleep(poll_interval)
209+
duration += poll_interval
210+
if (duration > timeout):
211+
raise WaitForAccessCodeFailedException(
212+
f"Gave up after waiting the maximum timeout of {timeout} seconds",
213+
access_code_id=access_code.access_code_id,
214+
errors=access_code.errors
215+
)
216+
217+
access_code = access_codes.get(access_code)
218+
219+
return access_code
180220

181221
@report_error
182222
def create_multiple(

seamapi/types.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,19 @@ def __init__(
4646
f'Action Attempt for "{action_type}" Failed. {error_type}: {error_message} (action_attempt_id={action_attempt_id})'
4747
)
4848

49+
class WaitForAccessCodeFailedException(Exception):
50+
def __init__(
51+
self,
52+
message: str,
53+
access_code_id: str,
54+
errors: Optional[list] = []
55+
):
56+
self.access_code_id = access_code_id
57+
self.errors = errors
58+
super().__init__(
59+
f'Failed while waiting for access code. ${message}'
60+
)
61+
4962

5063
@dataclass
5164
class Device:

tests/access_codes/test_access_codes.py

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,26 @@ def test_access_codes(seam: Seam):
3131
delete_action_attempt = seam.access_codes.delete(created_access_code)
3232
assert delete_action_attempt.status == "success"
3333

34-
access_codes = seam.access_codes.create_multiple(devices=all_devices)
35-
assert len(access_codes) == len(all_devices)
36-
assert len(set([ac.common_code_key for ac in access_codes])) == 1
34+
# TODO: Can only test for salto devices.
35+
# access_codes = seam.access_codes.create_multiple(devices=all_devices)
36+
# assert len(access_codes) == len(all_devices)
37+
# assert len(set([ac.common_code_key for ac in access_codes])) == 1
38+
39+
def test_access_codes_create_wait_for_code(seam: Seam):
40+
run_august_factory(seam)
41+
42+
all_devices = seam.devices.list()
43+
some_device = all_devices[0]
44+
45+
created_access_code = seam.access_codes.create(
46+
some_device.device_id, "Test code", "4445", wait_for_code=True
47+
)
48+
49+
assert created_access_code.name == "Test code"
50+
assert created_access_code.code == "4445"
51+
52+
with pytest.raises(RuntimeError) as excinfo:
53+
seam.access_codes.create(
54+
some_device.device_id, "Test code", "4445", wait_for_code=True, starts_at="3001-01-01", ends_at="3001-01-03"
55+
)
56+
assert "future time bound code" in str(excinfo.value)

0 commit comments

Comments
 (0)