uiprotect 6.7.0__py3-none-any.whl → 6.8.0__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.

Potentially problematic release.


This version of uiprotect might be problematic. Click here for more details.

uiprotect/api.py CHANGED
@@ -27,8 +27,8 @@ from aiohttp import CookieJar, client_exceptions
27
27
  from platformdirs import user_cache_dir, user_config_dir
28
28
  from yarl import URL
29
29
 
30
- from uiprotect.data.convert import dict_from_unifi_list
31
- from uiprotect.data.user import Keyring, UlpUser
30
+ from uiprotect.data.convert import list_from_unifi_list
31
+ from uiprotect.data.user import Keyring, Keyrings, UlpUser, UlpUsers
32
32
 
33
33
  from ._compat import cached_property
34
34
  from .data import (
@@ -829,15 +829,21 @@ class ProtectApiClient(BaseApiClient):
829
829
  async with self._update_lock:
830
830
  bootstrap = await self.get_bootstrap()
831
831
  if bootstrap.nvr.version >= NFC_FINGERPRINT_SUPPORT_VERSION:
832
- bootstrap.keyrings = cast(
833
- dict[str, Keyring],
834
- dict_from_unifi_list(self, await self.api_request_list("keyrings")),
832
+ bootstrap.keyrings = Keyrings.from_list(
833
+ cast(
834
+ list[Keyring],
835
+ list_from_unifi_list(
836
+ self, await self.api_request_list("keyrings")
837
+ ),
838
+ )
835
839
  )
836
- bootstrap.ulp_users = cast(
837
- dict[str, UlpUser],
838
- dict_from_unifi_list(
839
- self, await self.api_request_list("ulp-users")
840
- ),
840
+ bootstrap.ulp_users = UlpUsers.from_list(
841
+ cast(
842
+ list[UlpUser],
843
+ list_from_unifi_list(
844
+ self, await self.api_request_list("ulp-users")
845
+ ),
846
+ )
841
847
  )
842
848
  self.__dict__.pop("bootstrap", None)
843
849
  self._bootstrap = bootstrap
@@ -34,7 +34,7 @@ from .devices import (
34
34
  )
35
35
  from .nvr import NVR, Event, Liveview
36
36
  from .types import EventType, FixSizeOrderedDict, ModelType
37
- from .user import Group, Keyring, UlpUser, User
37
+ from .user import Group, Keyrings, UlpUserKeyringBase, UlpUsers, User
38
38
  from .websocket import (
39
39
  WSAction,
40
40
  WSPacket,
@@ -188,8 +188,8 @@ class Bootstrap(ProtectBaseObject):
188
188
  # agreements
189
189
 
190
190
  # not directly from UniFi
191
- keyrings: dict[str, Keyring] = {}
192
- ulp_users: dict[str, UlpUser] = {}
191
+ keyrings: Keyrings = Keyrings()
192
+ ulp_users: UlpUsers = UlpUsers()
193
193
  events: dict[str, Event] = FixSizeOrderedDict()
194
194
  capture_ws_stats: bool = False
195
195
  mac_lookup: dict[str, ProtectDeviceRef] = {}
@@ -393,7 +393,7 @@ class Bootstrap(ProtectBaseObject):
393
393
  model_type: ModelType,
394
394
  ) -> WSSubscriptionMessage | None:
395
395
  action_id = action["id"]
396
- dict_from_bootstrap: dict[str, ProtectModelWithId] = getattr(
396
+ obj_from_bootstrap: UlpUserKeyringBase[ProtectModelWithId] = getattr(
397
397
  self, to_snake_case(model_type.devices_key)
398
398
  )
399
399
  action_type = action["action"]
@@ -403,7 +403,7 @@ class Bootstrap(ProtectBaseObject):
403
403
  model_class = MODEL_TO_CLASS.get(model_type)
404
404
  assert model_class is not None and isinstance(add_obj, model_class)
405
405
  add_obj = cast(ProtectModelWithId, add_obj)
406
- dict_from_bootstrap[add_obj.id] = add_obj
406
+ obj_from_bootstrap.add(add_obj)
407
407
  return WSSubscriptionMessage(
408
408
  action=WSAction.ADD,
409
409
  new_update_id=self.last_update_id,
@@ -411,17 +411,18 @@ class Bootstrap(ProtectBaseObject):
411
411
  new_obj=add_obj,
412
412
  )
413
413
  elif action_type == "remove":
414
- removed_obj = dict_from_bootstrap.pop(action_id, None)
415
- if removed_obj is None:
414
+ to_remove = obj_from_bootstrap.by_id(action_id)
415
+ if to_remove is None:
416
416
  return None
417
+ obj_from_bootstrap.remove(to_remove)
417
418
  return WSSubscriptionMessage(
418
419
  action=WSAction.REMOVE,
419
420
  new_update_id=self.last_update_id,
420
421
  changed_data={},
421
- old_obj=removed_obj,
422
+ old_obj=to_remove,
422
423
  )
423
424
  elif action_type == "update":
424
- updated_obj = dict_from_bootstrap.get(action_id)
425
+ updated_obj = obj_from_bootstrap.by_id(action_id)
425
426
  if updated_obj is None:
426
427
  return None
427
428
 
uiprotect/data/convert.py CHANGED
@@ -85,11 +85,10 @@ def create_from_unifi_dict(
85
85
  return klass.from_unifi_dict(**data, api=api)
86
86
 
87
87
 
88
- def dict_from_unifi_list(
88
+ def list_from_unifi_list(
89
89
  api: ProtectApiClient, unifi_list: list[dict[str, ProtectModelWithId]]
90
- ) -> dict[str, ProtectModelWithId]:
91
- return_dict: dict[str, ProtectModelWithId] = {}
92
- for obj_dict in unifi_list:
93
- obj = cast(ProtectModelWithId, create_from_unifi_dict(obj_dict, api))
94
- return_dict[obj.id] = obj
95
- return return_dict
90
+ ) -> list[ProtectModelWithId]:
91
+ return [
92
+ cast(ProtectModelWithId, create_from_unifi_dict(obj_dict, api))
93
+ for obj_dict in unifi_list
94
+ ]
uiprotect/data/user.py CHANGED
@@ -2,15 +2,22 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import sys
6
+ from abc import abstractmethod
5
7
  from datetime import datetime
6
8
  from functools import cache
7
- from typing import Any
9
+ from typing import TYPE_CHECKING, Any, Generic, TypeVar
8
10
 
9
11
  from pydantic.v1.fields import PrivateAttr
10
12
 
11
13
  from .base import ProtectBaseObject, ProtectModel, ProtectModelWithId
12
14
  from .types import ModelType, PermissionNode
13
15
 
16
+ if sys.version_info >= (3, 11):
17
+ from typing import Self
18
+ else:
19
+ from typing_extensions import Self
20
+
14
21
 
15
22
  class Permission(ProtectBaseObject):
16
23
  raw_permission: str
@@ -236,6 +243,47 @@ class User(ProtectModelWithId):
236
243
  return False
237
244
 
238
245
 
246
+ T = TypeVar("T", bound="ProtectModelWithId")
247
+
248
+
249
+ class UlpUserKeyringBase(Generic[T]):
250
+ """Base class for collections of ULP users and keyrings."""
251
+
252
+ def __init__(self) -> None:
253
+ self._id_to_item: dict[str, T] = {}
254
+
255
+ @classmethod
256
+ def from_list(cls, items: list[T]) -> Self:
257
+ instance = cls()
258
+ for item in items:
259
+ instance.add(item)
260
+ return instance
261
+
262
+ def add(self, item: T) -> None:
263
+ """Add an item to the collection."""
264
+ self._id_to_item[item.id] = item
265
+
266
+ def remove(self, item: T) -> None:
267
+ """Remove an item from the collection."""
268
+ self._id_to_item.pop(item.id, None)
269
+
270
+ def by_id(self, item_id: str) -> T | None:
271
+ """Retrieve an item by its ID."""
272
+ return self._id_to_item.get(item_id)
273
+
274
+ @abstractmethod
275
+ def by_ulp_id(self, item_id: str) -> T | None:
276
+ """Retrieve an item by its ULP ID."""
277
+
278
+ def as_list(self) -> list[T]:
279
+ return list(self._id_to_item.values())
280
+
281
+ def __eq__(self, other: Any) -> bool:
282
+ if TYPE_CHECKING:
283
+ assert isinstance(other, UlpUserKeyringBase)
284
+ return self._id_to_item == other._id_to_item
285
+
286
+
239
287
  class Keyring(ProtectModelWithId):
240
288
  device_type: str
241
289
  device_id: str
@@ -245,6 +293,34 @@ class Keyring(ProtectModelWithId):
245
293
  ulp_user: str
246
294
 
247
295
 
296
+ class Keyrings(UlpUserKeyringBase[Keyring]):
297
+ def __init__(self) -> None:
298
+ super().__init__()
299
+ self._keyrings_by_registry_id: dict[str, Keyring] = {}
300
+ self._keyrings_by_ulp_user: dict[str, Keyring] = {}
301
+
302
+ def add(self, keyring: Keyring) -> None:
303
+ super().add(keyring)
304
+ self._keyrings_by_registry_id[keyring.registry_id] = keyring
305
+ self._keyrings_by_ulp_user[keyring.ulp_user] = keyring
306
+
307
+ def remove(self, keyring: Keyring) -> None:
308
+ super().remove(keyring)
309
+ self._keyrings_by_registry_id.pop(keyring.registry_id, None)
310
+ self._keyrings_by_ulp_user.pop(keyring.ulp_user, None)
311
+
312
+ def by_ulp_id(self, ulp_id: str) -> Keyring | None:
313
+ return self._keyrings_by_ulp_user.get(ulp_id)
314
+
315
+ def by_registry_id(self, registry_id: str) -> Keyring | None:
316
+ return self._keyrings_by_registry_id.get(registry_id)
317
+
318
+ def __eq__(self, other: Any) -> bool:
319
+ if not isinstance(other, Keyrings):
320
+ return NotImplemented
321
+ return super().__eq__(other)
322
+
323
+
248
324
  class UlpUser(ProtectModelWithId):
249
325
  ulp_id: str
250
326
  first_name: str
@@ -252,3 +328,25 @@ class UlpUser(ProtectModelWithId):
252
328
  full_name: str
253
329
  avatar: str
254
330
  status: str
331
+
332
+
333
+ class UlpUsers(UlpUserKeyringBase[UlpUser]):
334
+ def __init__(self) -> None:
335
+ super().__init__()
336
+ self._users_by_ulp_id: dict[str, UlpUser] = {}
337
+
338
+ def add(self, user: UlpUser) -> None:
339
+ super().add(user)
340
+ self._users_by_ulp_id[user.ulp_id] = user
341
+
342
+ def remove(self, user: UlpUser) -> None:
343
+ super().remove(user)
344
+ self._users_by_ulp_id.pop(user.ulp_id, None)
345
+
346
+ def by_ulp_id(self, ulp_id: str) -> UlpUser | None:
347
+ return self._users_by_ulp_id.get(ulp_id)
348
+
349
+ def __eq__(self, other: Any) -> bool:
350
+ if not isinstance(other, UlpUsers):
351
+ return NotImplemented
352
+ return super().__eq__(other)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: uiprotect
3
- Version: 6.7.0
3
+ Version: 6.8.0
4
4
  Summary: Python API for Unifi Protect (Unofficial)
5
5
  Home-page: https://github.com/uilibs/uiprotect
6
6
  Author: UI Protect Maintainers
@@ -1,7 +1,7 @@
1
1
  uiprotect/__init__.py,sha256=Oz6i1tonIz4QWVnEPkbielJDJ3WQdwZVgYtjY4IwGAQ,636
2
2
  uiprotect/__main__.py,sha256=C_bHCOkv5qj6WMy-6ELoY3Y6HDhLxOa1a30CzmbZhsg,462
3
3
  uiprotect/_compat.py,sha256=HThmb1zQZCEssCxYYbQzFhJq8zYYlVaSnIEZabKc-6U,302
4
- uiprotect/api.py,sha256=NJ0W7DwMTk2kiB94sxXbiECFxbI4U8xeEkiAqfNyxHQ,68638
4
+ uiprotect/api.py,sha256=pSQ_C2cjtttnFnrVzjmHlqS7zOfMH-bwk4NxbraUCKI,68850
5
5
  uiprotect/cli/__init__.py,sha256=1MO8rJmjjAsfVx2x01gn5DJo8B64xdPGo6gRVJbWd18,8868
6
6
  uiprotect/cli/backup.py,sha256=ZiS7RZnJGKI8TJKLW2cOUzkRM8nyTvE5Ov_jZZGtvSM,36708
7
7
  uiprotect/cli/base.py,sha256=k-_qGuNT7br0iV0KE5F4wYXF75iyLLjBEckTqxC71xM,7591
@@ -16,12 +16,12 @@ uiprotect/cli/sensors.py,sha256=fQtcDJCVxs4VbAqcavgBy2ABiVxAW3GXtna6_XFBp2k,8153
16
16
  uiprotect/cli/viewers.py,sha256=2cyrp104ffIvgT0wYGIO0G35QMkEbFe7fSVqLwDXQYQ,2171
17
17
  uiprotect/data/__init__.py,sha256=OcfuJl2qXfHcj_mdnrHhzZ5tEIZrw8auziX5IE7dn-I,2938
18
18
  uiprotect/data/base.py,sha256=sn7IHKQN96uiZL6ImN1gdCHV97EpUmy-X7xWTUAtWsg,35054
19
- uiprotect/data/bootstrap.py,sha256=LfxYgzcDFKeky2cWzf2dsUZMQLkXIviaYETKieAe660,23169
20
- uiprotect/data/convert.py,sha256=TDJ-6nNk3F1pJdeP-ogyki6jd3xHY_RlUkYDQk-DxIE,2684
19
+ uiprotect/data/bootstrap.py,sha256=j7bVRWwc3zI6gjn30LA_N5rqQbUYu6yxdy8RmrmCuSc,23224
20
+ uiprotect/data/convert.py,sha256=CDPkSMxSEhvDigmzmLFKpjrz0oa5FOvOdkNIHZrOZ4Q,2586
21
21
  uiprotect/data/devices.py,sha256=P5U47i_YMpvD0jaWiDYbFz_mZt_Wiop_ssWGWnEMySU,113489
22
22
  uiprotect/data/nvr.py,sha256=FGI0eIAyy3Zy9kaxcr67HxwaVCUU8wq3oZyWvoDq7Sg,47251
23
23
  uiprotect/data/types.py,sha256=8fEf-fdm_Fqrfb3zwiJJle0ten-Rv3_0Vwk5uimf_vk,18571
24
- uiprotect/data/user.py,sha256=-L2NFKjijK9K7DVe_wHDkxTC_BtiGtoR7Sw6odWvsN0,7326
24
+ uiprotect/data/user.py,sha256=4rDMUPo02LhoVGfDjwXB9NASL4RnZjP3pvqtwjudZeE,10398
25
25
  uiprotect/data/websocket.py,sha256=m4EV1Qfh08eKOihy70ycViYgEQpeNSGZQJWdtGIYJDA,6791
26
26
  uiprotect/exceptions.py,sha256=kgn0cRM6lTtgLza09SDa3ZiX6ue1QqHCOogQ4qu6KTQ,965
27
27
  uiprotect/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -31,8 +31,8 @@ uiprotect/test_util/__init__.py,sha256=Ky8mTL61nhp5II2mxTKBAsSGvNqK8U_CfKC5AGwTo
31
31
  uiprotect/test_util/anonymize.py,sha256=f-8ijU-_y9r-uAbhIPn0f0I6hzJpAkvJzc8UpWihObI,8478
32
32
  uiprotect/utils.py,sha256=jIWT7n_reL90oY91svBfQ4naRxo28qHzP5jNOL12mQE,20342
33
33
  uiprotect/websocket.py,sha256=tEyenqblNXHcjWYuf4oRP1E7buNwx6zoECMwpBr-jig,8191
34
- uiprotect-6.7.0.dist-info/LICENSE,sha256=INx18jhdbVXMEiiBANeKEbrbz57ckgzxk5uutmmcxGk,1111
35
- uiprotect-6.7.0.dist-info/METADATA,sha256=kh9jg5pC-BL0FbdCAIEUmZsO01wpnv_Zb2Qjx02NuY0,11096
36
- uiprotect-6.7.0.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
37
- uiprotect-6.7.0.dist-info/entry_points.txt,sha256=J78AUTPrTTxgI3s7SVgrmGqDP7piX2wuuEORzhDdVRA,47
38
- uiprotect-6.7.0.dist-info/RECORD,,
34
+ uiprotect-6.8.0.dist-info/LICENSE,sha256=INx18jhdbVXMEiiBANeKEbrbz57ckgzxk5uutmmcxGk,1111
35
+ uiprotect-6.8.0.dist-info/METADATA,sha256=8FkmORQLIos0mSeuDePD0xTl_CfGdDiE68a1VNTpb9o,11096
36
+ uiprotect-6.8.0.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
37
+ uiprotect-6.8.0.dist-info/entry_points.txt,sha256=J78AUTPrTTxgI3s7SVgrmGqDP7piX2wuuEORzhDdVRA,47
38
+ uiprotect-6.8.0.dist-info/RECORD,,