pykada 0.0.10__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.
- pykada/access_control/access_credentials.py +253 -0
- pykada/access_control/access_door_exceptions.py +353 -0
- pykada/access_control/access_doors.py +67 -0
- pykada/access_control/access_events.py +67 -0
- pykada/access_control/access_groups.py +133 -0
- pykada/access_control/access_levels.py +250 -0
- pykada/access_control/access_users.py +279 -0
- pykada/access_control/test_access_control_credentials.py +198 -0
- pykada/access_control/test_access_doors.py +97 -0
- pykada/access_control/test_access_events.py +117 -0
- pykada/access_control/test_access_groups.py +163 -0
- pykada/access_control/test_access_levels.py +206 -0
- pykada/access_control/test_access_users.py +267 -0
- pykada/access_control/test_door_exceptions.py +506 -0
- pykada/access_control/testbed/Cary-Grant.png +0 -0
- pykada/access_control/testbed/testbed.py +429 -0
- pykada/alarms/classic_alarms.py +48 -0
- pykada/alarms/test_classic_alarms.py +65 -0
- pykada/alarms/testbed/testbed.py +17 -0
- pykada/api_tokens.py +157 -0
- pykada/cameras/camera_stream.py +53 -0
- pykada/cameras/cameras.py +721 -0
- pykada/cameras/test_cameras.py +87 -0
- pykada/cameras/testbed/Cary-Grant.png +0 -0
- pykada/cameras/testbed/licenseplates.csv +3 -0
- pykada/cameras/testbed/testbed.py +408 -0
- pykada/core_command/core_command.py +223 -0
- pykada/core_command/test_core_command.py +77 -0
- pykada/core_command/testbed/testbed.py +70 -0
- pykada/endpoints.py +146 -0
- pykada/helix/helix.py +334 -0
- pykada/helix/test_helix.py +118 -0
- pykada/helix/testbed/testbed.py +98 -0
- pykada/helpers.py +422 -0
- pykada/sensors/sensors.py +178 -0
- pykada/sensors/test_sensor.py +49 -0
- pykada/sensors/testbed/testbed.py +48 -0
- pykada/verkada_requests.py +108 -0
- pykada/viewing_stations/test_viewing_stations.py +10 -0
- pykada/viewing_stations/testbed/testbed.py +12 -0
- pykada/viewing_stations/viewing_stations.py +13 -0
- pykada/workplace/deny_list_example.csv +4 -0
- pykada/workplace/test_workplace.py +69 -0
- pykada/workplace/testbed/testbed.py +47 -0
- pykada/workplace/workplace.py +142 -0
- pykada-0.0.10.dist-info/METADATA +181 -0
- pykada-0.0.10.dist-info/RECORD +48 -0
- pykada-0.0.10.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
from typeguard import typechecked
|
|
2
|
+
from typing import Optional, Dict, Any
|
|
3
|
+
|
|
4
|
+
from pykada.endpoints import *
|
|
5
|
+
from pykada.helpers import remove_null_fields, check_user_external_id, \
|
|
6
|
+
require_non_empty_str
|
|
7
|
+
from pykada.verkada_requests import *
|
|
8
|
+
|
|
9
|
+
@typechecked
|
|
10
|
+
def delete_access_card(card_id: str, user_id: Optional[str] = None, external_id: Optional[str] = None) -> Dict[str, Any]:
|
|
11
|
+
"""
|
|
12
|
+
Delete an access card for a user.
|
|
13
|
+
|
|
14
|
+
:param card_id: The unique identifier for the access card.
|
|
15
|
+
:param user_id: The internal user identifier (exactly one of user_id or external_id must be provided).
|
|
16
|
+
:param external_id: The external user identifier (exactly one of user_id or external_id must be provided).
|
|
17
|
+
:return: JSON response containing the result of the deletion.
|
|
18
|
+
:raises ValueError: If card_id is an empty string.
|
|
19
|
+
"""
|
|
20
|
+
if not card_id:
|
|
21
|
+
raise ValueError("card_id must be a non-empty string")
|
|
22
|
+
|
|
23
|
+
params = check_user_external_id(user_id, external_id)
|
|
24
|
+
params["card_id"] = card_id
|
|
25
|
+
|
|
26
|
+
return delete_request(ACCESS_CARD_ENDPOINT, params=params)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@typechecked
|
|
30
|
+
def add_card_to_user(user_id: Optional[str] = None,
|
|
31
|
+
external_id: Optional[str] = None,
|
|
32
|
+
active: Optional[bool] = False,
|
|
33
|
+
card_number: Optional[str] = None,
|
|
34
|
+
card_number_hex: Optional[str] = None,
|
|
35
|
+
card_number_base36: Optional[str] = None,
|
|
36
|
+
facility_code: str = "",
|
|
37
|
+
card_type: str = "") -> Dict[str, Any]:
|
|
38
|
+
"""
|
|
39
|
+
Add a card to a user.
|
|
40
|
+
|
|
41
|
+
Creates and adds an access card for a specified user (by user_id or external_id)
|
|
42
|
+
and organization. The card object is passed in the request body as JSON. This
|
|
43
|
+
request requires a facility code and exactly one of the following: card_number,
|
|
44
|
+
card_number_hex, or card_number_base36. The 'active' field defaults to False.
|
|
45
|
+
|
|
46
|
+
:param user_id: The internal user identifier.
|
|
47
|
+
:param external_id: The external user identifier.
|
|
48
|
+
:param active: Boolean flag indicating if the credential should be active. Defaults to False.
|
|
49
|
+
:param card_number: The card number used to grant or deny access.
|
|
50
|
+
:param card_number_hex: The card number in hexadecimal format.
|
|
51
|
+
:param card_number_base36: The card number in base36 format.
|
|
52
|
+
:param facility_code: The facility code used to grant or deny access.
|
|
53
|
+
:param card_type: The type of card (e.g., Standard 26-bit Wiegand, HID 37-bit, etc.).
|
|
54
|
+
:return: JSON response containing the created credential information.
|
|
55
|
+
:raises ValueError: If not exactly one of card_number, card_number_hex, or card_number_base36 is provided.
|
|
56
|
+
"""
|
|
57
|
+
params = check_user_external_id(user_id, external_id)
|
|
58
|
+
|
|
59
|
+
# Ensure exactly one card number format is provided.
|
|
60
|
+
card_number_options = [card_number, card_number_hex, card_number_base36]
|
|
61
|
+
if sum(x is not None for x in card_number_options) != 1:
|
|
62
|
+
raise ValueError("Exactly one of card_number, card_number_hex, or card_number_base36 must be provided.")
|
|
63
|
+
|
|
64
|
+
payload = {
|
|
65
|
+
"active": active,
|
|
66
|
+
"facility_code": facility_code,
|
|
67
|
+
"type": card_type,
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if card_number is not None:
|
|
71
|
+
payload["card_number"] = card_number
|
|
72
|
+
elif card_number_hex is not None:
|
|
73
|
+
payload["card_number_hex"] = card_number_hex
|
|
74
|
+
elif card_number_base36 is not None:
|
|
75
|
+
payload["card_number_base36"] = card_number_base36
|
|
76
|
+
|
|
77
|
+
return post_request(ACCESS_CARD_ENDPOINT, params=params, payload=payload)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@typechecked
|
|
81
|
+
def activate_access_card(card_id: str, user_id: Optional[str] = None, external_id: Optional[str] = None) -> Dict[str, Any]:
|
|
82
|
+
"""
|
|
83
|
+
Activate an access card for a user.
|
|
84
|
+
|
|
85
|
+
:param card_id: The unique identifier for the access card.
|
|
86
|
+
:param user_id: The internal user identifier (exactly one of user_id or external_id must be provided).
|
|
87
|
+
:param external_id: The external user identifier (exactly one of user_id or external_id must be provided).
|
|
88
|
+
:return: JSON response containing the result of the activation.
|
|
89
|
+
:raises ValueError: If card_id is an empty string.
|
|
90
|
+
"""
|
|
91
|
+
if not card_id:
|
|
92
|
+
raise ValueError("card_id must be a non-empty string")
|
|
93
|
+
|
|
94
|
+
params = check_user_external_id(user_id, external_id)
|
|
95
|
+
params["card_id"] = card_id
|
|
96
|
+
|
|
97
|
+
return put_request(ACCESS_CARD_ACTIVATE_ENDPOINT, params=params)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
@typechecked
|
|
101
|
+
def deactivate_access_card(card_id: str, user_id: Optional[str] = None, external_id: Optional[str] = None) -> Dict[str, Any]:
|
|
102
|
+
"""
|
|
103
|
+
Deactivate an access card for a user.
|
|
104
|
+
|
|
105
|
+
:param card_id: The unique identifier for the access card.
|
|
106
|
+
:param user_id: The internal user identifier (exactly one of user_id or external_id must be provided).
|
|
107
|
+
:param external_id: The external user identifier (exactly one of user_id or external_id must be provided).
|
|
108
|
+
:return: JSON response containing the result of the deactivation.
|
|
109
|
+
:raises ValueError: If card_id is an empty string.
|
|
110
|
+
"""
|
|
111
|
+
if not card_id:
|
|
112
|
+
raise ValueError("card_id must be a non-empty string")
|
|
113
|
+
|
|
114
|
+
params = check_user_external_id(user_id, external_id)
|
|
115
|
+
params["card_id"] = card_id
|
|
116
|
+
|
|
117
|
+
return put_request(ACCESS_CARD_DEACTIVATE_ENDPOINT, params=params)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
@typechecked
|
|
121
|
+
def delete_license_plate_from_user(license_plate_number: str, user_id: Optional[str] = None, external_id: Optional[str] = None) -> Dict[str, Any]:
|
|
122
|
+
"""
|
|
123
|
+
Delete a license plate from a user.
|
|
124
|
+
|
|
125
|
+
:param license_plate_number: The license plate number to be deleted.
|
|
126
|
+
:param user_id: The internal user identifier (exactly one of user_id or external_id must be provided).
|
|
127
|
+
:param external_id: The external user identifier (exactly one of user_id or external_id must be provided).
|
|
128
|
+
:return: JSON response containing the result of the deletion.
|
|
129
|
+
:raises ValueError: If license_plate_number is an empty string.
|
|
130
|
+
"""
|
|
131
|
+
if not license_plate_number:
|
|
132
|
+
raise ValueError("license_plate_number must be a non-empty string")
|
|
133
|
+
|
|
134
|
+
params = check_user_external_id(user_id, external_id)
|
|
135
|
+
params["license_plate_number"] = license_plate_number
|
|
136
|
+
|
|
137
|
+
return delete_request(ACCESS_LICENSE_PLATE_ENDPOINT, params=params)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
@typechecked
|
|
141
|
+
def add_license_plate_to_user(license_plate_number: str, active: Optional[bool] = False, name: Optional[str] = None,
|
|
142
|
+
user_id: Optional[str] = None, external_id: Optional[str] = None) -> Dict[str, Any]:
|
|
143
|
+
"""
|
|
144
|
+
Add a license plate to a user.
|
|
145
|
+
|
|
146
|
+
:param license_plate_number: The license plate number to be added.
|
|
147
|
+
:param active: Boolean flag indicating if the license plate should be active. Defaults to False.
|
|
148
|
+
:param name: Optional name associated with the license plate.
|
|
149
|
+
:param user_id: The internal user identifier (exactly one of user_id or external_id must be provided).
|
|
150
|
+
:param external_id: The external user identifier (exactly one of user_id or external_id must be provided).
|
|
151
|
+
:return: JSON response containing the added license plate details.
|
|
152
|
+
:raises ValueError: If license_plate_number is an empty string.
|
|
153
|
+
"""
|
|
154
|
+
if not license_plate_number:
|
|
155
|
+
raise ValueError("license_plate_number must be a non-empty string")
|
|
156
|
+
|
|
157
|
+
params = check_user_external_id(user_id, external_id)
|
|
158
|
+
|
|
159
|
+
payload = {
|
|
160
|
+
"license_plate_number": license_plate_number,
|
|
161
|
+
"active": active,
|
|
162
|
+
"name": name
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
payload = remove_null_fields(payload)
|
|
166
|
+
|
|
167
|
+
return post_request(ACCESS_LICENSE_PLATE_ENDPOINT, params=params, payload=payload)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
@typechecked
|
|
171
|
+
def activate_license_plate(license_plate_number: str, user_id: Optional[str] = None, external_id: Optional[str] = None) -> Dict[str, Any]:
|
|
172
|
+
"""
|
|
173
|
+
Activate a license plate for a user.
|
|
174
|
+
|
|
175
|
+
:param license_plate_number: The license plate number to be activated.
|
|
176
|
+
:param user_id: The internal user identifier (exactly one of user_id or external_id must be provided).
|
|
177
|
+
:param external_id: The external user identifier (exactly one of user_id or external_id must be provided).
|
|
178
|
+
:return: JSON response containing the result of the activation.
|
|
179
|
+
:raises ValueError: If license_plate_number is an empty string.
|
|
180
|
+
"""
|
|
181
|
+
if not license_plate_number:
|
|
182
|
+
raise ValueError("license_plate_number must be a non-empty string")
|
|
183
|
+
|
|
184
|
+
params = check_user_external_id(user_id, external_id)
|
|
185
|
+
params["license_plate_number"] = license_plate_number
|
|
186
|
+
|
|
187
|
+
return put_request(ACCESS_LICENSE_PLATE_ACTIVATE_ENDPOINT, params=params)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
@typechecked
|
|
191
|
+
def deactivate_license_plate(license_plate_number: str, user_id: Optional[str] = None, external_id: Optional[str] = None) -> Dict[str, Any]:
|
|
192
|
+
"""
|
|
193
|
+
Deactivate a license plate for a user.
|
|
194
|
+
|
|
195
|
+
:param license_plate_number: The license plate number to be deactivated.
|
|
196
|
+
:param user_id: The internal user identifier (exactly one of user_id or external_id must be provided).
|
|
197
|
+
:param external_id: The external user identifier (exactly one of user_id or external_id must be provided).
|
|
198
|
+
:return: JSON response containing the result of the deactivation.
|
|
199
|
+
:raises ValueError: If license_plate_number is an empty string.
|
|
200
|
+
"""
|
|
201
|
+
if not license_plate_number:
|
|
202
|
+
raise ValueError("license_plate_number must be a non-empty string")
|
|
203
|
+
|
|
204
|
+
params = check_user_external_id(user_id, external_id)
|
|
205
|
+
params["license_plate_number"] = license_plate_number
|
|
206
|
+
|
|
207
|
+
return put_request(ACCESS_LICENSE_PLATE_DEACTIVATE_ENDPOINT, params=params)
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
@typechecked
|
|
211
|
+
def delete_mfa_code_from_user(code: str, user_id: Optional[str] = None, external_id: Optional[str] = None) -> Dict[str, Any]:
|
|
212
|
+
"""
|
|
213
|
+
Delete an MFA code from a user.
|
|
214
|
+
|
|
215
|
+
:param code: The MFA code to be deleted.
|
|
216
|
+
:param user_id: The internal user identifier (exactly one of user_id or external_id must be provided).
|
|
217
|
+
:param external_id: The external user identifier (exactly one of user_id or external_id must be provided).
|
|
218
|
+
:return: JSON response containing the result of the deletion.
|
|
219
|
+
:raises ValueError: If code is an empty string.
|
|
220
|
+
"""
|
|
221
|
+
if not code:
|
|
222
|
+
raise ValueError("code must be a non-empty string")
|
|
223
|
+
|
|
224
|
+
params = check_user_external_id(user_id, external_id)
|
|
225
|
+
params["code"] = code
|
|
226
|
+
|
|
227
|
+
return delete_request(ACCESS_MFA_CODE_ENDPOINT, params=params)
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
@typechecked
|
|
231
|
+
def add_mfa_code_to_user(code: str, user_id: Optional[str] = None, external_id: Optional[str] = None) -> Dict[str, Any]:
|
|
232
|
+
"""
|
|
233
|
+
Add an MFA code to a user.
|
|
234
|
+
|
|
235
|
+
:param code: The MFA code to be added.
|
|
236
|
+
:param user_id: The internal user identifier (exactly one of user_id or external_id must be provided).
|
|
237
|
+
:param external_id: The external user identifier (exactly one of user_id or external_id must be provided).
|
|
238
|
+
:return: JSON response containing the added MFA code details.
|
|
239
|
+
:raises ValueError: If code is an empty string.
|
|
240
|
+
"""
|
|
241
|
+
|
|
242
|
+
require_non_empty_str(code, "code")
|
|
243
|
+
|
|
244
|
+
if not code:
|
|
245
|
+
raise ValueError("code must be a non-empty string")
|
|
246
|
+
|
|
247
|
+
params = check_user_external_id(user_id, external_id)
|
|
248
|
+
|
|
249
|
+
payload = {
|
|
250
|
+
"code": code
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return post_request(ACCESS_MFA_CODE_ENDPOINT, params=params, payload=payload)
|
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from typeguard import typechecked
|
|
3
|
+
from typing import Dict, Any, Optional, List
|
|
4
|
+
|
|
5
|
+
from pykada.endpoints import *
|
|
6
|
+
from pykada.verkada_requests import *
|
|
7
|
+
from pykada.helpers import \
|
|
8
|
+
require_non_empty_str, FREQUENCY_ENUM, DOOR_STATUS_ENUM, WEEKDAY_ENUM
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
# Helper functions
|
|
12
|
+
|
|
13
|
+
def is_valid_date(date_str: str) -> bool:
|
|
14
|
+
"""
|
|
15
|
+
Validates that a date string is in YYYY-MM-DD format.
|
|
16
|
+
"""
|
|
17
|
+
pattern = r"^\d{4}-\d{2}-\d{2}$"
|
|
18
|
+
return bool(re.match(pattern, date_str))
|
|
19
|
+
|
|
20
|
+
def is_valid_time(time_str: str) -> bool:
|
|
21
|
+
"""
|
|
22
|
+
Validates that a time string is in HH:MM format (00:00 to 23:59) with required leading zeros.
|
|
23
|
+
"""
|
|
24
|
+
pattern = r"^(0[0-9]|1[0-9]|2[0-3]):([0-5][0-9])$"
|
|
25
|
+
return bool(re.match(pattern, time_str))
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
# Recurrence Rule Validation
|
|
29
|
+
@typechecked
|
|
30
|
+
def validate_recurrence_rule(rr: Dict[str, Any], idx: Optional[int] = None) -> None:
|
|
31
|
+
"""
|
|
32
|
+
Validates a recurrence rule object based on the expected schema.
|
|
33
|
+
|
|
34
|
+
:param rr: The recurrence rule object.
|
|
35
|
+
:param idx: Optional index for context.
|
|
36
|
+
:raises ValueError: If any required field is missing or invalid.
|
|
37
|
+
"""
|
|
38
|
+
require_non_empty_str(rr.get("frequency", ""), "frequency", idx)
|
|
39
|
+
frequency = rr["frequency"]
|
|
40
|
+
|
|
41
|
+
if "interval" not in rr or not isinstance(rr["interval"], int):
|
|
42
|
+
raise ValueError(f"Exception at index {idx}: 'interval' must be an integer in recurrence_rule")
|
|
43
|
+
|
|
44
|
+
require_non_empty_str(rr.get("start_time", ""), "recurrence_rule start_time", idx)
|
|
45
|
+
if not is_valid_time(rr["start_time"]):
|
|
46
|
+
raise ValueError(f"Exception at index {idx}: 'recurrence_rule start_time' must be in HH:MM format")
|
|
47
|
+
|
|
48
|
+
if "by_day" in rr:
|
|
49
|
+
# Validate that by_day is a list of non-empty strings.
|
|
50
|
+
if not isinstance(rr["by_day"], list) or not all(isinstance(day, str) and day.strip() for day in rr["by_day"]):
|
|
51
|
+
raise ValueError(f"Exception at index {idx}: 'by_day' must be a list of non-empty strings")
|
|
52
|
+
# Validate allowed usage based on frequency.
|
|
53
|
+
if frequency == FREQUENCY_ENUM["DAILY"]:
|
|
54
|
+
raise ValueError(f"Exception at index {idx}: 'by_day' is not supported for DAILY frequency")
|
|
55
|
+
elif frequency == FREQUENCY_ENUM["WEEKLY"]:
|
|
56
|
+
if len(rr["by_day"]) < 1:
|
|
57
|
+
raise ValueError(f"Exception at index {idx}: 'by_day' must contain at least one value for WEEKLY frequency")
|
|
58
|
+
elif frequency in (FREQUENCY_ENUM["MONTHLY"], FREQUENCY_ENUM["YEARLY"]):
|
|
59
|
+
if "by_set_pos" not in rr or rr["by_set_pos"] is None:
|
|
60
|
+
raise ValueError(f"Exception at index {idx}: For MONTHLY or YEARLY frequency, 'by_set_pos' is required when 'by_day' is provided")
|
|
61
|
+
if len(rr["by_day"]) != 1:
|
|
62
|
+
raise ValueError(f"Exception at index {idx}: For MONTHLY or YEARLY frequency, 'by_day' must contain exactly one value")
|
|
63
|
+
# Validate that each day is one of the allowed weekdays.
|
|
64
|
+
if not set(rr["by_day"]).issubset(set(WEEKDAY_ENUM.values())):
|
|
65
|
+
raise ValueError(f"Exception at index {idx}: 'by_day' values must be one of {list(WEEKDAY_ENUM.values())}")
|
|
66
|
+
|
|
67
|
+
if "by_month" in rr:
|
|
68
|
+
if not isinstance(rr["by_month"], int) or not (1 <= rr["by_month"] <= 12) or frequency != FREQUENCY_ENUM["YEARLY"]:
|
|
69
|
+
raise ValueError(f"Exception at index {idx}: 'by_month' must be an integer between 1 and 12 and is only supported for YEARLY frequency.")
|
|
70
|
+
|
|
71
|
+
if "by_month_day" in rr:
|
|
72
|
+
if frequency not in (FREQUENCY_ENUM["MONTHLY"], FREQUENCY_ENUM["YEARLY"]):
|
|
73
|
+
raise ValueError(f"Exception at index {idx}: 'by_month_day' is only supported for MONTHLY or YEARLY frequency.")
|
|
74
|
+
if "by_set_pos" in rr:
|
|
75
|
+
raise ValueError(f"Exception at index {idx}: Only one of 'by_month_day' or 'by_set_pos' is allowed.")
|
|
76
|
+
if not isinstance(rr["by_month_day"], int) or not (1 <= rr["by_month_day"] <= 31):
|
|
77
|
+
raise ValueError(f"Exception at index {idx}: 'by_month_day' must be an integer between 1 and 31.")
|
|
78
|
+
|
|
79
|
+
if "by_set_pos" in rr:
|
|
80
|
+
if not isinstance(rr["by_set_pos"], int) or not (1 <= rr["by_set_pos"] <= 5):
|
|
81
|
+
raise ValueError(f"Exception at index {idx}: 'by_set_pos' must be an integer between 1 and 5.")
|
|
82
|
+
if frequency not in (FREQUENCY_ENUM["MONTHLY"], FREQUENCY_ENUM["YEARLY"]):
|
|
83
|
+
raise ValueError(f"Exception at index {idx}: 'by_set_pos' is only supported for MONTHLY or YEARLY frequency.")
|
|
84
|
+
|
|
85
|
+
if "excluded_dates" in rr:
|
|
86
|
+
if not isinstance(rr["excluded_dates"], list) or not all(isinstance(d, str) and is_valid_date(d) for d in rr["excluded_dates"]):
|
|
87
|
+
raise ValueError(f"Exception at index {idx}: 'excluded_dates' must be a list of valid dates in YYYY-MM-DD format")
|
|
88
|
+
|
|
89
|
+
if "until" in rr:
|
|
90
|
+
if not isinstance(rr["until"], str) or not is_valid_date(rr["until"]):
|
|
91
|
+
raise ValueError(f"Exception at index {idx}: 'until' must be a valid date in YYYY-MM-DD format")
|
|
92
|
+
|
|
93
|
+
if "count" in rr:
|
|
94
|
+
if not isinstance(rr["count"], int):
|
|
95
|
+
raise ValueError(f"Exception at index {idx}: 'count' must be an integer")
|
|
96
|
+
if "count" in rr and "until" in rr:
|
|
97
|
+
raise ValueError(f"Exception at index {idx}: Only one of 'count' or 'until' may be provided in recurrence_rule")
|
|
98
|
+
|
|
99
|
+
# Door Exception Object Validation
|
|
100
|
+
|
|
101
|
+
@typechecked
|
|
102
|
+
def validate_door_exception(exc: Dict[str, Any], idx: Optional[int] = None) -> None:
|
|
103
|
+
"""
|
|
104
|
+
Validates a door exception object based on the expected schema.
|
|
105
|
+
|
|
106
|
+
:param exc: The door exception object.
|
|
107
|
+
:param idx: Optional index for context.
|
|
108
|
+
:raises ValueError: If any required field is missing or invalid.
|
|
109
|
+
"""
|
|
110
|
+
if not isinstance(exc, dict):
|
|
111
|
+
raise ValueError(f"Exception at index {idx}: must be a dictionary")
|
|
112
|
+
|
|
113
|
+
if "date" not in exc:
|
|
114
|
+
raise ValueError(f"Exception at index {idx}: 'date' is required")
|
|
115
|
+
require_non_empty_str(exc["date"], "date", idx)
|
|
116
|
+
if not is_valid_date(exc["date"]):
|
|
117
|
+
raise ValueError(f"Exception at index {idx}: 'date' must be in YYYY-MM-DD format")
|
|
118
|
+
|
|
119
|
+
if "door_status" not in exc:
|
|
120
|
+
raise ValueError(f"Exception at index {idx}: 'door_status' is required")
|
|
121
|
+
require_non_empty_str(exc["door_status"], "door_status", idx)
|
|
122
|
+
if exc["door_status"] not in list(DOOR_STATUS_ENUM.values()):
|
|
123
|
+
raise ValueError(f"Exception at index {idx}: 'door_status' must be one of {list(DOOR_STATUS_ENUM.values())}")
|
|
124
|
+
|
|
125
|
+
if exc.get("double_badge", False):
|
|
126
|
+
if not isinstance(exc["double_badge"], bool):
|
|
127
|
+
raise ValueError(f"Exception at index {idx}: 'double_badge' must be a boolean")
|
|
128
|
+
if "double_badge_group_ids" not in exc or not isinstance(exc["double_badge_group_ids"], list):
|
|
129
|
+
raise ValueError(f"Exception at index {idx}: 'double_badge_group_ids' must be provided as a list when double_badge is True")
|
|
130
|
+
|
|
131
|
+
if "double_badge_group_ids" in exc:
|
|
132
|
+
if "double_badge" not in exc or exc.get("double_badge", False):
|
|
133
|
+
raise ValueError(
|
|
134
|
+
f"Exception at index {idx}: 'double_badge must also be set to TRUE if value is provided.")
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
all_day = exc.get("all_day_default", False)
|
|
138
|
+
if all_day:
|
|
139
|
+
if exc["door_status"] != "access_controlled":
|
|
140
|
+
raise ValueError(f"Exception at index {idx}: when all_day_default is True, door_status must be 'access_controlled'")
|
|
141
|
+
if "start_time" in exc and exc["start_time"] not in (None, "", "00:00"):
|
|
142
|
+
raise ValueError(f"Exception at index {idx}: when all_day_default is True, start_time must be '00:00' or not provided")
|
|
143
|
+
if "end_time" in exc and exc["end_time"] not in (None, "", "23:59"):
|
|
144
|
+
raise ValueError(f"Exception at index {idx}: when all_day_default is True, end_time must be '23:59' or not provided")
|
|
145
|
+
else:
|
|
146
|
+
if "start_time" not in exc:
|
|
147
|
+
raise ValueError(f"Exception at index {idx}: 'start_time' is required for non all-day exceptions")
|
|
148
|
+
require_non_empty_str(exc["start_time"], "start_time", idx)
|
|
149
|
+
if not is_valid_time(exc["start_time"]):
|
|
150
|
+
raise ValueError(f"Exception at index {idx}: 'start_time' must be in HH:MM format")
|
|
151
|
+
if "end_time" not in exc:
|
|
152
|
+
raise ValueError(f"Exception at index {idx}: 'end_time' is required for non all-day exceptions")
|
|
153
|
+
require_non_empty_str(exc["end_time"], "end_time", idx)
|
|
154
|
+
if not is_valid_time(exc["end_time"]):
|
|
155
|
+
raise ValueError(f"Exception at index {idx}: 'end_time' must be in HH:MM format")
|
|
156
|
+
|
|
157
|
+
if exc.get("first_person_in", False):
|
|
158
|
+
if "first_person_in_group_ids" not in exc or not isinstance(exc["first_person_in_group_ids"], list):
|
|
159
|
+
raise ValueError(f"Exception at index {idx}: 'first_person_in_group_ids' must be provided as a list when first_person_in is True")
|
|
160
|
+
|
|
161
|
+
if "recurrence_rule" in exc:
|
|
162
|
+
validate_recurrence_rule(exc["recurrence_rule"], idx)
|
|
163
|
+
|
|
164
|
+
# Door Exception Calendar Functions
|
|
165
|
+
|
|
166
|
+
@typechecked
|
|
167
|
+
def get_all_door_exception_calendars(last_updated_at: Optional[int] = None) -> Dict[str, Any]:
|
|
168
|
+
"""
|
|
169
|
+
Retrieve all available door exception calendars.
|
|
170
|
+
|
|
171
|
+
:param last_updated_at: Optional timestamp (Unix seconds) to filter calendars updated after this time.
|
|
172
|
+
:return: JSON response containing all available door exception calendars.
|
|
173
|
+
"""
|
|
174
|
+
params = {"last_updated_at": last_updated_at} if last_updated_at is not None else {}
|
|
175
|
+
return get_request(ACCESS_DOOR_EXCEPTIONS_ENDPOINT, params=params)
|
|
176
|
+
|
|
177
|
+
@typechecked
|
|
178
|
+
def get_door_exception_calendar(calendar_id: str) -> Dict[str, Any]:
|
|
179
|
+
"""
|
|
180
|
+
Retrieve a specific door exception calendar.
|
|
181
|
+
|
|
182
|
+
:param calendar_id: The unique identifier for the door exception calendar.
|
|
183
|
+
:return: JSON response containing the door exception calendar details.
|
|
184
|
+
:raises ValueError: If calendar_id is an empty string.
|
|
185
|
+
"""
|
|
186
|
+
require_non_empty_str(calendar_id, "calendar_id")
|
|
187
|
+
params = {'calendar_id': calendar_id}
|
|
188
|
+
return get_request(ACCESS_DOOR_EXCEPTIONS_ENDPOINT, params=params)
|
|
189
|
+
|
|
190
|
+
@typechecked
|
|
191
|
+
def create_door_exception_calendar(doors: List[str],
|
|
192
|
+
exceptions: List[Dict[str, Any]],
|
|
193
|
+
name: str) -> Dict[str, Any]:
|
|
194
|
+
"""
|
|
195
|
+
Create a new door exception calendar.
|
|
196
|
+
|
|
197
|
+
:param doors: A non-empty list of door IDs.
|
|
198
|
+
:param exceptions: A non-empty list of door exception objects.
|
|
199
|
+
:param name: Name of the door exception calendar.
|
|
200
|
+
:return: JSON response containing the created door exception calendar information.
|
|
201
|
+
:raises ValueError: If any required field is missing or invalid.
|
|
202
|
+
"""
|
|
203
|
+
require_non_empty_str(name, "name")
|
|
204
|
+
if not doors or not all(isinstance(door, str) and door.strip() for door in doors):
|
|
205
|
+
raise ValueError("doors must be a non-empty list of non-empty strings")
|
|
206
|
+
for idx, door in enumerate(doors):
|
|
207
|
+
require_non_empty_str(door, "door", idx)
|
|
208
|
+
|
|
209
|
+
for exc in exceptions:
|
|
210
|
+
validate_door_exception(exc)
|
|
211
|
+
|
|
212
|
+
payload = {
|
|
213
|
+
"doors": doors,
|
|
214
|
+
"exceptions": exceptions,
|
|
215
|
+
"name": name
|
|
216
|
+
}
|
|
217
|
+
return post_request(ACCESS_DOOR_EXCEPTIONS_ENDPOINT, payload=payload)
|
|
218
|
+
|
|
219
|
+
@typechecked
|
|
220
|
+
def update_door_exception_calendar(doors: List[str],
|
|
221
|
+
exceptions: List[Dict[str, Any]],
|
|
222
|
+
name: str,
|
|
223
|
+
calendar_id: str) -> Dict[str, Any]:
|
|
224
|
+
"""
|
|
225
|
+
Update an existing door exception calendar.
|
|
226
|
+
|
|
227
|
+
:param doors: A non-empty list of door IDs.
|
|
228
|
+
:param exceptions: A non-empty list of door exception objects.
|
|
229
|
+
:param name: Updated name of the door exception calendar.
|
|
230
|
+
:param calendar_id: The unique identifier of the calendar to update.
|
|
231
|
+
:return: JSON response containing the updated door exception calendar information.
|
|
232
|
+
:raises ValueError: If any required field is missing or invalid.
|
|
233
|
+
"""
|
|
234
|
+
require_non_empty_str(name, "name")
|
|
235
|
+
if not doors or not all(isinstance(door, str) and door.strip() for door in doors):
|
|
236
|
+
raise ValueError("doors must be a non-empty list of non-empty strings")
|
|
237
|
+
for idx, door in enumerate(doors):
|
|
238
|
+
require_non_empty_str(door, "door", idx)
|
|
239
|
+
|
|
240
|
+
for exc in exceptions:
|
|
241
|
+
validate_door_exception(exc)
|
|
242
|
+
|
|
243
|
+
payload = {
|
|
244
|
+
"doors": doors,
|
|
245
|
+
"exceptions": exceptions,
|
|
246
|
+
"name": name
|
|
247
|
+
}
|
|
248
|
+
params = {"calendar_id": calendar_id}
|
|
249
|
+
# Using put_request for update
|
|
250
|
+
return put_request(ACCESS_DOOR_EXCEPTIONS_ENDPOINT, payload=payload, params=params)
|
|
251
|
+
|
|
252
|
+
@typechecked
|
|
253
|
+
def delete_door_exception_calendar(calendar_id: str) -> Dict[str, Any]:
|
|
254
|
+
"""
|
|
255
|
+
Delete a door exception calendar.
|
|
256
|
+
|
|
257
|
+
:param calendar_id: The unique identifier for the door exception calendar to delete.
|
|
258
|
+
:return: JSON response confirming deletion.
|
|
259
|
+
:raises ValueError: If calendar_id is an empty string.
|
|
260
|
+
"""
|
|
261
|
+
require_non_empty_str(calendar_id, "calendar_id")
|
|
262
|
+
params = {"calendar_id": calendar_id}
|
|
263
|
+
return delete_request(ACCESS_DOOR_EXCEPTIONS_ENDPOINT, params=params)
|
|
264
|
+
|
|
265
|
+
@typechecked
|
|
266
|
+
def get_exception_on_door_exception_calendar(calendar_id: str,
|
|
267
|
+
exception_id: str) -> Dict[str, Any]:
|
|
268
|
+
"""
|
|
269
|
+
Retrieve a specific exception from a door exception calendar.
|
|
270
|
+
|
|
271
|
+
:param calendar_id: The unique identifier for the door exception calendar.
|
|
272
|
+
:param exception_id: The unique identifier for the exception.
|
|
273
|
+
:return: JSON response containing the exception details.
|
|
274
|
+
:raises ValueError: If calendar_id or exception_id is an empty string.
|
|
275
|
+
"""
|
|
276
|
+
require_non_empty_str(calendar_id, "calendar_id")
|
|
277
|
+
require_non_empty_str(exception_id, "exception_id")
|
|
278
|
+
url = f"{ACCESS_DOOR_EXCEPTIONS_ENDPOINT}/{calendar_id}/exception/{exception_id}"
|
|
279
|
+
return get_request(url)
|
|
280
|
+
|
|
281
|
+
@typechecked
|
|
282
|
+
def add_exception_to_door_exception_calendar(
|
|
283
|
+
calendar_id: str,
|
|
284
|
+
exception: Dict[str, Any]
|
|
285
|
+
) -> Dict[str, Any]:
|
|
286
|
+
"""
|
|
287
|
+
Add an Exception to a Door Exception Calendar.
|
|
288
|
+
|
|
289
|
+
Adds a new Exception to the Door Exception Calendar identified by `calendar_id` using the details provided in the exception object.
|
|
290
|
+
The exception object must follow the expected schema and will be validated using the `validate_door_exception` function.
|
|
291
|
+
|
|
292
|
+
:param calendar_id: The unique identifier for the door exception calendar.
|
|
293
|
+
:param exception: A dictionary representing the new exception details.
|
|
294
|
+
:return: JSON response containing the created exception information.
|
|
295
|
+
:raises ValueError: If calendar_id is an empty string or if the exception object fails validation.
|
|
296
|
+
"""
|
|
297
|
+
require_non_empty_str(calendar_id, "calendar_id")
|
|
298
|
+
validate_door_exception(exception)
|
|
299
|
+
|
|
300
|
+
url = f"{ACCESS_DOOR_EXCEPTIONS_ENDPOINT}/{calendar_id}/exception"
|
|
301
|
+
return post_request(url, payload=exception)
|
|
302
|
+
|
|
303
|
+
@typechecked
|
|
304
|
+
def update_exception_on_door_exception_calendar(
|
|
305
|
+
calendar_id: str,
|
|
306
|
+
exception_id: str,
|
|
307
|
+
exception: Dict[str, Any]
|
|
308
|
+
) -> Dict[str, Any]:
|
|
309
|
+
"""
|
|
310
|
+
Update an Exception on a Door Exception Calendar.
|
|
311
|
+
|
|
312
|
+
Updates the Exception identified by `exception_id` on the Door Exception Calendar
|
|
313
|
+
identified by `calendar_id` using the new exception details provided as a single object.
|
|
314
|
+
|
|
315
|
+
The provided exception object must follow the expected schema, which is validated using
|
|
316
|
+
the `validate_door_exception` function. For example, the object must include:
|
|
317
|
+
- date (in YYYY-MM-DD format)
|
|
318
|
+
- door_status (one of the allowed values)
|
|
319
|
+
- For non all-day exceptions, valid start_time and end_time (in HH:MM format)
|
|
320
|
+
- If all_day_default is True, door_status must be "access_controlled", and start_time/end_time
|
|
321
|
+
should be omitted or defaulted to "00:00" and "23:59", respectively.
|
|
322
|
+
- Optional fields such as first_person_in, double_badge, and their corresponding group IDs,
|
|
323
|
+
as well as an optional recurrence_rule object.
|
|
324
|
+
|
|
325
|
+
:param calendar_id: The unique identifier for the door exception calendar.
|
|
326
|
+
:param exception_id: The unique identifier for the exception to update.
|
|
327
|
+
:param exception: A dictionary representing the new exception details.
|
|
328
|
+
:return: JSON response containing the updated exception information.
|
|
329
|
+
:raises ValueError: If any required parameter is missing or if the exception object fails validation.
|
|
330
|
+
"""
|
|
331
|
+
require_non_empty_str(calendar_id, "calendar_id")
|
|
332
|
+
require_non_empty_str(exception_id, "exception_id")
|
|
333
|
+
validate_door_exception(exception)
|
|
334
|
+
|
|
335
|
+
url = f"{ACCESS_DOOR_EXCEPTIONS_ENDPOINT}/{calendar_id}/exception/{exception_id}"
|
|
336
|
+
return put_request(url, payload=exception)
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
@typechecked
|
|
340
|
+
def delete_exception_on_door_exception_calendar(calendar_id: str, exception_id: str) -> Dict[str, Any]:
|
|
341
|
+
"""
|
|
342
|
+
Delete an exception from a door exception calendar.
|
|
343
|
+
|
|
344
|
+
:param calendar_id: The unique identifier for the door exception calendar.
|
|
345
|
+
:param exception_id: The unique identifier for the exception to delete.
|
|
346
|
+
:return: JSON response confirming deletion of the exception.
|
|
347
|
+
:raises ValueError: If calendar_id or exception_id is an empty string.
|
|
348
|
+
"""
|
|
349
|
+
require_non_empty_str(calendar_id, "calendar_id")
|
|
350
|
+
require_non_empty_str(exception_id, "exception_id")
|
|
351
|
+
url = f"{ACCESS_DOOR_EXCEPTIONS_ENDPOINT}/{calendar_id}/exception/{exception_id}"
|
|
352
|
+
return delete_request(url)
|
|
353
|
+
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
from typeguard import typechecked
|
|
2
|
+
from typing import Optional, List, Dict, Any
|
|
3
|
+
|
|
4
|
+
from pykada.helpers import check_user_external_id
|
|
5
|
+
from pykada.endpoints import ACCESS_USER_UNLOCK_ENDPOINT, \
|
|
6
|
+
ACCESS_DOORS_ENDPOINT, ACCESS_ADMIN_UNLOCK_ENDPOINT
|
|
7
|
+
from pykada.verkada_requests import *
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@typechecked
|
|
11
|
+
def unlock_door_as_admin(door_id: str) -> Dict[str, Any]:
|
|
12
|
+
"""
|
|
13
|
+
Unlock a door as an administrator.
|
|
14
|
+
|
|
15
|
+
This function sends a request to unlock the specified door without requiring user identification.
|
|
16
|
+
|
|
17
|
+
:param door_id: The unique identifier for the door.
|
|
18
|
+
:return: JSON response containing the result of the unlock operation.
|
|
19
|
+
:raises ValueError: If door_id is an empty string.
|
|
20
|
+
"""
|
|
21
|
+
if not door_id:
|
|
22
|
+
raise ValueError("door_id must be a non-empty string")
|
|
23
|
+
|
|
24
|
+
payload = {"door_id": door_id}
|
|
25
|
+
return post_request(ACCESS_ADMIN_UNLOCK_ENDPOINT, payload=payload)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@typechecked
|
|
29
|
+
def unlock_door_as_user(door_id: str, user_id: Optional[str] = None,
|
|
30
|
+
external_id: Optional[str] = None) -> Dict[str, Any]:
|
|
31
|
+
"""
|
|
32
|
+
Unlock a door as a user.
|
|
33
|
+
|
|
34
|
+
This function sends a request to unlock the specified door, using either the internal user_id or the external_id.
|
|
35
|
+
|
|
36
|
+
:param door_id: The unique identifier for the door.
|
|
37
|
+
:param user_id: The internal user identifier (exactly one of user_id or external_id must be provided).
|
|
38
|
+
:param external_id: The external user identifier (exactly one of user_id or external_id must be provided).
|
|
39
|
+
:return: JSON response containing the result of the unlock operation.
|
|
40
|
+
:raises ValueError: If door_id is an empty string.
|
|
41
|
+
"""
|
|
42
|
+
if not door_id:
|
|
43
|
+
raise ValueError("door_id must be a non-empty string")
|
|
44
|
+
|
|
45
|
+
payload = check_user_external_id(user_id, external_id)
|
|
46
|
+
payload["door_id"] = door_id
|
|
47
|
+
|
|
48
|
+
return post_request(ACCESS_USER_UNLOCK_ENDPOINT, payload=payload)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@typechecked
|
|
52
|
+
def get_doors(door_id_list: Optional[List[Any]] = None,
|
|
53
|
+
site_id_list: Optional[List[Any]] = None) -> Dict[str, Any]:
|
|
54
|
+
"""
|
|
55
|
+
Retrieve door information.
|
|
56
|
+
|
|
57
|
+
This function sends a GET request to retrieve details for doors based on provided door IDs and/or site IDs.
|
|
58
|
+
|
|
59
|
+
:param door_id_list: A list of door IDs. If provided, these will be joined into a comma-separated string.
|
|
60
|
+
:param site_id_list: A list of site IDs. If provided, these will be joined into a comma-separated string.
|
|
61
|
+
:return: JSON response containing door information.
|
|
62
|
+
"""
|
|
63
|
+
door_ids = ",".join(map(str, door_id_list)) if door_id_list else None
|
|
64
|
+
site_ids = ",".join(map(str, site_id_list)) if site_id_list else None
|
|
65
|
+
|
|
66
|
+
params = {"door_ids": door_ids, "site_ids": site_ids}
|
|
67
|
+
return get_request(ACCESS_DOORS_ENDPOINT, params=params)
|