uiprotect 6.6.5__tar.gz → 6.8.0__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.
- {uiprotect-6.6.5 → uiprotect-6.8.0}/PKG-INFO +1 -1
- {uiprotect-6.6.5 → uiprotect-6.8.0}/pyproject.toml +2 -2
- {uiprotect-6.6.5 → uiprotect-6.8.0}/src/uiprotect/api.py +22 -0
- {uiprotect-6.6.5 → uiprotect-6.8.0}/src/uiprotect/data/bootstrap.py +68 -9
- {uiprotect-6.6.5 → uiprotect-6.8.0}/src/uiprotect/data/convert.py +15 -2
- {uiprotect-6.6.5 → uiprotect-6.8.0}/src/uiprotect/data/types.py +2 -0
- {uiprotect-6.6.5 → uiprotect-6.8.0}/src/uiprotect/data/user.py +117 -1
- {uiprotect-6.6.5 → uiprotect-6.8.0}/LICENSE +0 -0
- {uiprotect-6.6.5 → uiprotect-6.8.0}/README.md +0 -0
- {uiprotect-6.6.5 → uiprotect-6.8.0}/src/uiprotect/__init__.py +0 -0
- {uiprotect-6.6.5 → uiprotect-6.8.0}/src/uiprotect/__main__.py +0 -0
- {uiprotect-6.6.5 → uiprotect-6.8.0}/src/uiprotect/_compat.py +0 -0
- {uiprotect-6.6.5 → uiprotect-6.8.0}/src/uiprotect/cli/__init__.py +0 -0
- {uiprotect-6.6.5 → uiprotect-6.8.0}/src/uiprotect/cli/backup.py +0 -0
- {uiprotect-6.6.5 → uiprotect-6.8.0}/src/uiprotect/cli/base.py +0 -0
- {uiprotect-6.6.5 → uiprotect-6.8.0}/src/uiprotect/cli/cameras.py +0 -0
- {uiprotect-6.6.5 → uiprotect-6.8.0}/src/uiprotect/cli/chimes.py +0 -0
- {uiprotect-6.6.5 → uiprotect-6.8.0}/src/uiprotect/cli/doorlocks.py +0 -0
- {uiprotect-6.6.5 → uiprotect-6.8.0}/src/uiprotect/cli/events.py +0 -0
- {uiprotect-6.6.5 → uiprotect-6.8.0}/src/uiprotect/cli/lights.py +0 -0
- {uiprotect-6.6.5 → uiprotect-6.8.0}/src/uiprotect/cli/liveviews.py +0 -0
- {uiprotect-6.6.5 → uiprotect-6.8.0}/src/uiprotect/cli/nvr.py +0 -0
- {uiprotect-6.6.5 → uiprotect-6.8.0}/src/uiprotect/cli/sensors.py +0 -0
- {uiprotect-6.6.5 → uiprotect-6.8.0}/src/uiprotect/cli/viewers.py +0 -0
- {uiprotect-6.6.5 → uiprotect-6.8.0}/src/uiprotect/data/__init__.py +0 -0
- {uiprotect-6.6.5 → uiprotect-6.8.0}/src/uiprotect/data/base.py +0 -0
- {uiprotect-6.6.5 → uiprotect-6.8.0}/src/uiprotect/data/devices.py +0 -0
- {uiprotect-6.6.5 → uiprotect-6.8.0}/src/uiprotect/data/nvr.py +0 -0
- {uiprotect-6.6.5 → uiprotect-6.8.0}/src/uiprotect/data/websocket.py +0 -0
- {uiprotect-6.6.5 → uiprotect-6.8.0}/src/uiprotect/exceptions.py +0 -0
- {uiprotect-6.6.5 → uiprotect-6.8.0}/src/uiprotect/py.typed +0 -0
- {uiprotect-6.6.5 → uiprotect-6.8.0}/src/uiprotect/release_cache.json +0 -0
- {uiprotect-6.6.5 → uiprotect-6.8.0}/src/uiprotect/stream.py +0 -0
- {uiprotect-6.6.5 → uiprotect-6.8.0}/src/uiprotect/test_util/__init__.py +0 -0
- {uiprotect-6.6.5 → uiprotect-6.8.0}/src/uiprotect/test_util/anonymize.py +0 -0
- {uiprotect-6.6.5 → uiprotect-6.8.0}/src/uiprotect/utils.py +0 -0
- {uiprotect-6.6.5 → uiprotect-6.8.0}/src/uiprotect/websocket.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "uiprotect"
|
|
3
|
-
version = "6.
|
|
3
|
+
version = "6.8.0"
|
|
4
4
|
description = "Python API for Unifi Protect (Unofficial)"
|
|
5
5
|
authors = ["UI Protect Maintainers <ui@koston.org>"]
|
|
6
6
|
readme = "README.md"
|
|
@@ -52,7 +52,7 @@ propcache = ">=0.0.0"
|
|
|
52
52
|
pytest = ">=7,<9"
|
|
53
53
|
pytest-cov = ">=3,<7"
|
|
54
54
|
aiosqlite = ">=0.20.0"
|
|
55
|
-
asttokens = "
|
|
55
|
+
asttokens = ">=2.4.1,<4.0.0"
|
|
56
56
|
pytest-asyncio = ">=0.23.7,<0.25.0"
|
|
57
57
|
pytest-benchmark = ">=4,<6"
|
|
58
58
|
pytest-sugar = "^1.0.0"
|
|
@@ -27,6 +27,9 @@ 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 list_from_unifi_list
|
|
31
|
+
from uiprotect.data.user import Keyring, Keyrings, UlpUser, UlpUsers
|
|
32
|
+
|
|
30
33
|
from ._compat import cached_property
|
|
31
34
|
from .data import (
|
|
32
35
|
NVR,
|
|
@@ -90,6 +93,8 @@ If your Protect instance has a lot of events, this request will take much longer
|
|
|
90
93
|
_LOGGER = logging.getLogger(__name__)
|
|
91
94
|
_COOKIE_RE = re.compile(r"^set-cookie: ", re.IGNORECASE)
|
|
92
95
|
|
|
96
|
+
NFC_FINGERPRINT_SUPPORT_VERSION = Version("5.1.57")
|
|
97
|
+
|
|
93
98
|
# TODO: Urls to still support
|
|
94
99
|
# Backups
|
|
95
100
|
# * GET /backups - list backends
|
|
@@ -823,6 +828,23 @@ class ProtectApiClient(BaseApiClient):
|
|
|
823
828
|
"""
|
|
824
829
|
async with self._update_lock:
|
|
825
830
|
bootstrap = await self.get_bootstrap()
|
|
831
|
+
if bootstrap.nvr.version >= NFC_FINGERPRINT_SUPPORT_VERSION:
|
|
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
|
+
)
|
|
839
|
+
)
|
|
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
|
+
)
|
|
847
|
+
)
|
|
826
848
|
self.__dict__.pop("bootstrap", None)
|
|
827
849
|
self._bootstrap = bootstrap
|
|
828
850
|
return bootstrap
|
|
@@ -6,14 +6,14 @@ import asyncio
|
|
|
6
6
|
import logging
|
|
7
7
|
from dataclasses import dataclass
|
|
8
8
|
from datetime import datetime
|
|
9
|
-
from typing import TYPE_CHECKING, Any
|
|
9
|
+
from typing import TYPE_CHECKING, Any, cast
|
|
10
10
|
|
|
11
11
|
from aiohttp.client_exceptions import ServerDisconnectedError
|
|
12
12
|
from convertertools import pop_dict_set, pop_dict_tuple
|
|
13
13
|
from pydantic.v1 import PrivateAttr, ValidationError
|
|
14
14
|
|
|
15
15
|
from ..exceptions import ClientError
|
|
16
|
-
from ..utils import normalize_mac, utc_now
|
|
16
|
+
from ..utils import normalize_mac, to_snake_case, utc_now
|
|
17
17
|
from .base import (
|
|
18
18
|
RECENT_EVENT_MAX,
|
|
19
19
|
ProtectBaseObject,
|
|
@@ -21,7 +21,7 @@ from .base import (
|
|
|
21
21
|
ProtectModel,
|
|
22
22
|
ProtectModelWithId,
|
|
23
23
|
)
|
|
24
|
-
from .convert import create_from_unifi_dict
|
|
24
|
+
from .convert import MODEL_TO_CLASS, create_from_unifi_dict
|
|
25
25
|
from .devices import (
|
|
26
26
|
Bridge,
|
|
27
27
|
Camera,
|
|
@@ -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, User
|
|
37
|
+
from .user import Group, Keyrings, UlpUserKeyringBase, UlpUsers, User
|
|
38
38
|
from .websocket import (
|
|
39
39
|
WSAction,
|
|
40
40
|
WSPacket,
|
|
@@ -188,6 +188,8 @@ class Bootstrap(ProtectBaseObject):
|
|
|
188
188
|
# agreements
|
|
189
189
|
|
|
190
190
|
# not directly from UniFi
|
|
191
|
+
keyrings: Keyrings = Keyrings()
|
|
192
|
+
ulp_users: UlpUsers = UlpUsers()
|
|
191
193
|
events: dict[str, Event] = FixSizeOrderedDict()
|
|
192
194
|
capture_ws_stats: bool = False
|
|
193
195
|
mac_lookup: dict[str, ProtectDeviceRef] = {}
|
|
@@ -384,6 +386,60 @@ class Bootstrap(ProtectBaseObject):
|
|
|
384
386
|
old_obj=device,
|
|
385
387
|
)
|
|
386
388
|
|
|
389
|
+
def _process_ws_keyring_or_ulp_user_message(
|
|
390
|
+
self,
|
|
391
|
+
action: dict[str, Any],
|
|
392
|
+
data: dict[str, Any],
|
|
393
|
+
model_type: ModelType,
|
|
394
|
+
) -> WSSubscriptionMessage | None:
|
|
395
|
+
action_id = action["id"]
|
|
396
|
+
obj_from_bootstrap: UlpUserKeyringBase[ProtectModelWithId] = getattr(
|
|
397
|
+
self, to_snake_case(model_type.devices_key)
|
|
398
|
+
)
|
|
399
|
+
action_type = action["action"]
|
|
400
|
+
if action_type == "add":
|
|
401
|
+
add_obj = create_from_unifi_dict(data, api=self._api, model_type=model_type)
|
|
402
|
+
if TYPE_CHECKING:
|
|
403
|
+
model_class = MODEL_TO_CLASS.get(model_type)
|
|
404
|
+
assert model_class is not None and isinstance(add_obj, model_class)
|
|
405
|
+
add_obj = cast(ProtectModelWithId, add_obj)
|
|
406
|
+
obj_from_bootstrap.add(add_obj)
|
|
407
|
+
return WSSubscriptionMessage(
|
|
408
|
+
action=WSAction.ADD,
|
|
409
|
+
new_update_id=self.last_update_id,
|
|
410
|
+
changed_data=add_obj.dict(),
|
|
411
|
+
new_obj=add_obj,
|
|
412
|
+
)
|
|
413
|
+
elif action_type == "remove":
|
|
414
|
+
to_remove = obj_from_bootstrap.by_id(action_id)
|
|
415
|
+
if to_remove is None:
|
|
416
|
+
return None
|
|
417
|
+
obj_from_bootstrap.remove(to_remove)
|
|
418
|
+
return WSSubscriptionMessage(
|
|
419
|
+
action=WSAction.REMOVE,
|
|
420
|
+
new_update_id=self.last_update_id,
|
|
421
|
+
changed_data={},
|
|
422
|
+
old_obj=to_remove,
|
|
423
|
+
)
|
|
424
|
+
elif action_type == "update":
|
|
425
|
+
updated_obj = obj_from_bootstrap.by_id(action_id)
|
|
426
|
+
if updated_obj is None:
|
|
427
|
+
return None
|
|
428
|
+
|
|
429
|
+
old_obj = updated_obj.copy()
|
|
430
|
+
updated_data = {to_snake_case(k): v for k, v in data.items()}
|
|
431
|
+
updated_obj.update_from_dict(updated_data)
|
|
432
|
+
|
|
433
|
+
return WSSubscriptionMessage(
|
|
434
|
+
action=WSAction.UPDATE,
|
|
435
|
+
new_update_id=self.last_update_id,
|
|
436
|
+
changed_data=updated_data,
|
|
437
|
+
new_obj=updated_obj,
|
|
438
|
+
old_obj=old_obj,
|
|
439
|
+
)
|
|
440
|
+
_LOGGER.debug("Unexpected ws action for %s: %s", model_type, action_type)
|
|
441
|
+
return None
|
|
442
|
+
|
|
387
443
|
def _process_nvr_update(
|
|
388
444
|
self,
|
|
389
445
|
action: dict[str, Any],
|
|
@@ -540,13 +596,16 @@ class Bootstrap(ProtectBaseObject):
|
|
|
540
596
|
return None
|
|
541
597
|
|
|
542
598
|
action_action: str = action["action"]
|
|
543
|
-
if action_action == "remove":
|
|
544
|
-
return self._process_remove_packet(model_type, action)
|
|
545
|
-
|
|
546
|
-
if not data and not is_ping_back:
|
|
547
|
-
return None
|
|
548
599
|
|
|
549
600
|
try:
|
|
601
|
+
if model_type in {ModelType.KEYRING, ModelType.ULP_USER}:
|
|
602
|
+
return self._process_ws_keyring_or_ulp_user_message(
|
|
603
|
+
action, data, model_type
|
|
604
|
+
)
|
|
605
|
+
if action_action == "remove":
|
|
606
|
+
return self._process_remove_packet(model_type, action)
|
|
607
|
+
if not data and not is_ping_back:
|
|
608
|
+
return None
|
|
550
609
|
if action_action == "add":
|
|
551
610
|
return self._process_add_packet(model_type, data)
|
|
552
611
|
if action_action == "update":
|
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
from typing import TYPE_CHECKING, Any
|
|
5
|
+
from typing import TYPE_CHECKING, Any, cast
|
|
6
|
+
|
|
7
|
+
from uiprotect.data.base import ProtectModelWithId
|
|
6
8
|
|
|
7
9
|
from ..exceptions import DataDecodeError
|
|
8
10
|
from .devices import (
|
|
@@ -16,7 +18,7 @@ from .devices import (
|
|
|
16
18
|
)
|
|
17
19
|
from .nvr import NVR, Event, Liveview
|
|
18
20
|
from .types import ModelType
|
|
19
|
-
from .user import CloudAccount, Group, User, UserLocation
|
|
21
|
+
from .user import CloudAccount, Group, Keyring, UlpUser, User, UserLocation
|
|
20
22
|
|
|
21
23
|
if TYPE_CHECKING:
|
|
22
24
|
from ..api import ProtectApiClient
|
|
@@ -38,6 +40,8 @@ MODEL_TO_CLASS: dict[str, type[ProtectModel]] = {
|
|
|
38
40
|
ModelType.SENSOR: Sensor,
|
|
39
41
|
ModelType.DOORLOCK: Doorlock,
|
|
40
42
|
ModelType.CHIME: Chime,
|
|
43
|
+
ModelType.KEYRING: Keyring,
|
|
44
|
+
ModelType.ULP_USER: UlpUser,
|
|
41
45
|
}
|
|
42
46
|
|
|
43
47
|
|
|
@@ -79,3 +83,12 @@ def create_from_unifi_dict(
|
|
|
79
83
|
klass = get_klass_from_dict(data)
|
|
80
84
|
|
|
81
85
|
return klass.from_unifi_dict(**data, api=api)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def list_from_unifi_list(
|
|
89
|
+
api: ProtectApiClient, unifi_list: list[dict[str, ProtectModelWithId]]
|
|
90
|
+
) -> list[ProtectModelWithId]:
|
|
91
|
+
return [
|
|
92
|
+
cast(ProtectModelWithId, create_from_unifi_dict(obj_dict, api))
|
|
93
|
+
for obj_dict in unifi_list
|
|
94
|
+
]
|
|
@@ -105,6 +105,8 @@ class ModelType(str, UnknownValuesEnumMixin, enum.Enum):
|
|
|
105
105
|
CHIME = "chime"
|
|
106
106
|
DEVICE_GROUP = "deviceGroup"
|
|
107
107
|
RECORDING_SCHEDULE = "recordingSchedule"
|
|
108
|
+
ULP_USER = "ulpUser"
|
|
109
|
+
KEYRING = "keyring"
|
|
108
110
|
UNKNOWN = "unknown"
|
|
109
111
|
|
|
110
112
|
bootstrap_model_types: tuple[ModelType, ...]
|
|
@@ -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
|
|
@@ -234,3 +241,112 @@ class User(ProtectModelWithId):
|
|
|
234
241
|
return True
|
|
235
242
|
self._perm_cache[perm_str] = False
|
|
236
243
|
return False
|
|
244
|
+
|
|
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
|
+
|
|
287
|
+
class Keyring(ProtectModelWithId):
|
|
288
|
+
device_type: str
|
|
289
|
+
device_id: str
|
|
290
|
+
registry_type: str
|
|
291
|
+
registry_id: str
|
|
292
|
+
last_activity: datetime | None = None
|
|
293
|
+
ulp_user: str
|
|
294
|
+
|
|
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
|
+
|
|
324
|
+
class UlpUser(ProtectModelWithId):
|
|
325
|
+
ulp_id: str
|
|
326
|
+
first_name: str
|
|
327
|
+
last_name: str
|
|
328
|
+
full_name: str
|
|
329
|
+
avatar: str
|
|
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)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|