uiprotect 0.1.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.

@@ -0,0 +1,630 @@
1
+ from __future__ import annotations
2
+
3
+ import enum
4
+ from collections.abc import Callable, Coroutine
5
+ from typing import Any, Literal, Optional, TypeVar, Union
6
+
7
+ from packaging.version import Version as BaseVersion
8
+ from pydantic.v1 import BaseModel, ConstrainedInt
9
+ from pydantic.v1.color import Color as BaseColor
10
+ from pydantic.v1.types import ConstrainedFloat, ConstrainedStr
11
+
12
+ KT = TypeVar("KT")
13
+ VT = TypeVar("VT")
14
+
15
+
16
+ DEFAULT = "DEFAULT_VALUE"
17
+ DEFAULT_TYPE = Literal["DEFAULT_VALUE"]
18
+ EventCategories = Literal[
19
+ "critical",
20
+ "update",
21
+ "admin",
22
+ "ring",
23
+ "motion",
24
+ "smart",
25
+ "iot",
26
+ ]
27
+
28
+ ProgressCallback = Callable[[int, int, int], Coroutine[Any, Any, None]]
29
+ IteratorCallback = Callable[[int, Optional[bytes]], Coroutine[Any, Any, None]]
30
+
31
+
32
+ class FixSizeOrderedDict(dict[KT, VT]):
33
+ """A fixed size ordered dict."""
34
+
35
+ def __init__(self, *args: Any, max_size: int = 0, **kwargs: Any) -> None:
36
+ """Create the FixSizeOrderedDict."""
37
+ self._max_size = max_size
38
+ super().__init__(*args, **kwargs)
39
+
40
+ def __setitem__(self, key: KT, value: VT) -> None:
41
+ """Set an update up to the max size."""
42
+ dict.__setitem__(self, key, value)
43
+ if self._max_size > 0 and len(self) > 0 and len(self) > self._max_size:
44
+ del self[next(iter(self.keys()))]
45
+
46
+
47
+ class ValuesEnumMixin:
48
+ _values: list[str] | None = None
49
+ _values_normalized: dict[str, str] | None = None
50
+
51
+ @classmethod
52
+ def values(cls) -> list[str]:
53
+ if cls._values is None:
54
+ cls._values = [e.value for e in cls] # type: ignore[attr-defined]
55
+ return cls._values
56
+
57
+ @classmethod
58
+ def _missing_(cls, value: Any) -> Any | None:
59
+ if cls._values_normalized is None:
60
+ cls._values_normalized = {e.value.lower(): e for e in cls} # type: ignore[attr-defined]
61
+
62
+ value_normal = value
63
+ if isinstance(value, str):
64
+ value_normal = value.lower()
65
+ return cls._values_normalized.get(value_normal)
66
+
67
+
68
+ class UnknownValuesEnumMixin(ValuesEnumMixin):
69
+ @classmethod
70
+ def _missing_(cls, value: Any) -> Any | None:
71
+ # value always set in superclass _missing
72
+ return super()._missing_(value) or cls._values_normalized.get("unknown") # type: ignore[union-attr]
73
+
74
+
75
+ @enum.unique
76
+ class ModelType(str, UnknownValuesEnumMixin, enum.Enum):
77
+ CAMERA = "camera"
78
+ CLOUD_IDENTITY = "cloudIdentity"
79
+ EVENT = "event"
80
+ GROUP = "group"
81
+ LIGHT = "light"
82
+ LIVEVIEW = "liveview"
83
+ NVR = "nvr"
84
+ USER = "user"
85
+ USER_LOCATION = "userLocation"
86
+ VIEWPORT = "viewer"
87
+ BRIDGE = "bridge"
88
+ SENSOR = "sensor"
89
+ DOORLOCK = "doorlock"
90
+ SCHEDULE = "schedule"
91
+ CHIME = "chime"
92
+ DEVICE_GROUP = "deviceGroup"
93
+ RECORDING_SCHEDULE = "recordingSchedule"
94
+ UNKNOWN = "unknown"
95
+
96
+ @staticmethod
97
+ def bootstrap_models() -> tuple[str, ...]:
98
+ # TODO:
99
+ # legacyUFV
100
+ # display
101
+
102
+ return (
103
+ ModelType.CAMERA.value,
104
+ ModelType.USER.value,
105
+ ModelType.GROUP.value,
106
+ ModelType.LIVEVIEW.value,
107
+ ModelType.VIEWPORT.value,
108
+ ModelType.LIGHT.value,
109
+ ModelType.BRIDGE.value,
110
+ ModelType.SENSOR.value,
111
+ ModelType.DOORLOCK.value,
112
+ ModelType.CHIME.value,
113
+ )
114
+
115
+
116
+ @enum.unique
117
+ class EventType(str, ValuesEnumMixin, enum.Enum):
118
+ DISCONNECT = "disconnect"
119
+ FACTORY_RESET = "factoryReset"
120
+ PROVISION = "provision"
121
+ UPDATE = "update"
122
+ CAMERA_POWER_CYCLE = "cameraPowerCycling"
123
+ RING = "ring"
124
+ DOOR_ACCESS = "doorAccess"
125
+ RESOLUTION_LOWERED = "resolutionLowered"
126
+ POOR_CONNECTION = "poorConnection"
127
+ STREAM_RECOVERY = "streamRecovery"
128
+ MOTION = "motion"
129
+ RECORDING_DELETED = "recordingDeleted"
130
+ SMART_AUDIO_DETECT = "smartAudioDetect"
131
+ SMART_DETECT = "smartDetectZone"
132
+ SMART_DETECT_LINE = "smartDetectLine"
133
+ NO_SCHEDULE = "nonScheduledRecording"
134
+ RECORDING_MODE_CHANGED = "recordingModeChanged"
135
+ HOTPLUG = "hotplug"
136
+ FACE_GROUP_DETECTED = "faceGroupDetected"
137
+ CONSOLIDATED_RESOLUTION_LOWERED = "consolidatedResolutionLowered"
138
+ CONSOLIDATED_POOR_CONNECTION = "consolidatedPoorConnection"
139
+ CAMERA_CONNECTED = "cameraConnected"
140
+ CAMERA_REBOOTED = "cameraRebooted"
141
+ CAMERA_DISCONNECTED = "cameraDisconnected"
142
+ # ---
143
+ INSTALLED_DISK = "installed"
144
+ CORRUPTED_DB_RECOVERED = "corruptedDbRecovered"
145
+ OFFLINE = "offline"
146
+ OFF = "off"
147
+ REBOOT = "reboot"
148
+ FIRMWARE_UPDATE = "fwUpdate"
149
+ APP_UPDATE = "applicationUpdate"
150
+ APPLICATION_UPDATABLE = "applicationUpdatable"
151
+ ACCESS = "access"
152
+ DRIVE_FAILED = "driveFailed"
153
+ CAMERA_UTILIZATION_LIMIT_REACHED = "cameraUtilizationLimitReached"
154
+ CAMERA_UTILIZATION_LIMIT_EXCEEDED = "cameraUtilizationLimitExceeded"
155
+ DRIVE_SLOW = "driveSlow"
156
+ GLOBAL_RECORDING_MODE_CHANGED = "globalRecordingModeChanged"
157
+ NVR_SETTINGS_CHANGED = "nvrSettingsChanged"
158
+ # ---
159
+ UNADOPTED_DEVICE_DISCOVERED = "unadoptedDeviceDiscovered"
160
+ MULTIPLE_UNADOPTED_DEVICE_DISCOVERED = "multipleUnadoptedDeviceDiscovered"
161
+ DEVICE_ADOPTED = "deviceAdopted"
162
+ DEVICE_UNADOPTED = "deviceUnadopted"
163
+ UVF_DISCOVERED = "ufvDiscovered"
164
+ DEVICE_PASSWORD_UPDATE = "devicesPasswordUpdated" # noqa: S105
165
+ DEVICE_UPDATABLE = "deviceUpdatable"
166
+ MULTIPLE_DEVICE_UPDATABLE = "multipleDeviceUpdatable"
167
+ DEVICE_CONNECTED = "deviceConnected"
168
+ DEVICE_REBOOTED = "deviceRebooted"
169
+ DEVICE_DISCONNECTED = "deviceDisconnected"
170
+ NETWORK_DEVICE_OFFLINE = "networkDeviceOffline"
171
+ # ---
172
+ USER_LEFT = "userLeft"
173
+ USER_ARRIVED = "userArrived"
174
+ VIDEO_EXPORTED = "videoExported"
175
+ MIC_DISABLED = "microphoneDisabled"
176
+ VIDEO_DELETED = "videoDeleted"
177
+ SCHEDULE_CHANGED = "recordingScheduleChanged"
178
+ # ---
179
+ MOTION_SENSOR = "sensorMotion"
180
+ SENSOR_OPENED = "sensorOpened"
181
+ SENSOR_CLOSED = "sensorClosed"
182
+ SENSOR_ALARM = "sensorAlarm"
183
+ SENSOR_EXTREME_VALUE = "sensorExtremeValues"
184
+ SENSOR_WATER_LEAK = "sensorWaterLeak"
185
+ SENSOR_BATTERY_LOW = "sensorBatteryLow"
186
+ # ---
187
+ MOTION_LIGHT = "lightMotion"
188
+ # ---
189
+ DOORLOCK_OPEN = "doorlockOpened"
190
+ DOORLOCK_CLOSE = "doorlockClosed"
191
+ DOORLOCK_BATTERY_LOW = "doorlockBatteryLow"
192
+ # ---
193
+ DISRUPTED_CONDITIONS = "ringDisruptedConditions"
194
+ # ---
195
+ RECORDING_OFF = "recordingOff"
196
+
197
+ @staticmethod
198
+ def device_events() -> list[str]:
199
+ return [
200
+ EventType.MOTION.value,
201
+ EventType.RING.value,
202
+ EventType.SMART_DETECT.value,
203
+ ]
204
+
205
+ @staticmethod
206
+ def motion_events() -> list[str]:
207
+ return [EventType.MOTION.value, EventType.SMART_DETECT.value]
208
+
209
+
210
+ @enum.unique
211
+ class StateType(str, ValuesEnumMixin, enum.Enum):
212
+ CONNECTED = "CONNECTED"
213
+ CONNECTING = "CONNECTING"
214
+ DISCONNECTED = "DISCONNECTED"
215
+
216
+
217
+ @enum.unique
218
+ class ProtectWSPayloadFormat(int, enum.Enum):
219
+ """Websocket Payload formats."""
220
+
221
+ JSON = 1
222
+ UTF8String = 2
223
+ NodeBuffer = 3
224
+
225
+
226
+ @enum.unique
227
+ class SmartDetectObjectType(str, ValuesEnumMixin, enum.Enum):
228
+ PERSON = "person"
229
+ ANIMAL = "animal"
230
+ VEHICLE = "vehicle"
231
+ LICENSE_PLATE = "licensePlate"
232
+ PACKAGE = "package"
233
+ SMOKE = "alrmSmoke"
234
+ CMONX = "alrmCmonx"
235
+ SIREN = "alrmSiren"
236
+ BABY_CRY = "alrmBabyCry"
237
+ SPEAK = "alrmSpeak"
238
+ BARK = "alrmBark"
239
+ BURGLAR = "alrmBurglar"
240
+ CAR_HORN = "alrmCarHorn"
241
+ GLASS_BREAK = "alrmGlassBreak"
242
+ FACE = "face"
243
+ # old?
244
+ CAR = "car"
245
+ PET = "pet"
246
+
247
+ @property
248
+ def audio_type(self) -> SmartDetectAudioType | None:
249
+ return OBJECT_TO_AUDIO_MAP.get(self)
250
+
251
+
252
+ @enum.unique
253
+ class SmartDetectAudioType(str, ValuesEnumMixin, enum.Enum):
254
+ SMOKE = "alrmSmoke"
255
+ CMONX = "alrmCmonx"
256
+ SMOKE_CMONX = "smoke_cmonx"
257
+ SIREN = "alrmSiren"
258
+ BABY_CRY = "alrmBabyCry"
259
+ SPEAK = "alrmSpeak"
260
+ BARK = "alrmBark"
261
+ BURGLAR = "alrmBurglar"
262
+ CAR_HORN = "alrmCarHorn"
263
+ GLASS_BREAK = "alrmGlassBreak"
264
+
265
+
266
+ @enum.unique
267
+ class DetectionColor(str, ValuesEnumMixin, enum.Enum):
268
+ BLACK = "black"
269
+ BLUE = "blue"
270
+ BROWN = "brown"
271
+ GRAY = "gray"
272
+ GREEN = "green"
273
+ ORANGE = "orange"
274
+ PINK = "pink"
275
+ PURPLE = "purple"
276
+ RED = "red"
277
+ WHITE = "white"
278
+ YELLOW = "yellow"
279
+
280
+
281
+ OBJECT_TO_AUDIO_MAP = {
282
+ SmartDetectObjectType.SMOKE: SmartDetectAudioType.SMOKE,
283
+ SmartDetectObjectType.CMONX: SmartDetectAudioType.CMONX,
284
+ SmartDetectObjectType.SIREN: SmartDetectAudioType.SIREN,
285
+ SmartDetectObjectType.BABY_CRY: SmartDetectAudioType.BABY_CRY,
286
+ SmartDetectObjectType.SPEAK: SmartDetectAudioType.SPEAK,
287
+ SmartDetectObjectType.BARK: SmartDetectAudioType.BARK,
288
+ SmartDetectObjectType.BURGLAR: SmartDetectAudioType.BURGLAR,
289
+ SmartDetectObjectType.CAR_HORN: SmartDetectAudioType.CAR_HORN,
290
+ SmartDetectObjectType.GLASS_BREAK: SmartDetectAudioType.GLASS_BREAK,
291
+ }
292
+
293
+
294
+ @enum.unique
295
+ class DoorbellMessageType(str, ValuesEnumMixin, enum.Enum):
296
+ LEAVE_PACKAGE_AT_DOOR = "LEAVE_PACKAGE_AT_DOOR"
297
+ DO_NOT_DISTURB = "DO_NOT_DISTURB"
298
+ CUSTOM_MESSAGE = "CUSTOM_MESSAGE"
299
+
300
+
301
+ @enum.unique
302
+ class LightModeEnableType(str, ValuesEnumMixin, enum.Enum):
303
+ DARK = "dark"
304
+ ALWAYS = "fulltime"
305
+ NIGHT = "night"
306
+
307
+
308
+ @enum.unique
309
+ class LightModeType(str, ValuesEnumMixin, enum.Enum):
310
+ MOTION = "motion"
311
+ WHEN_DARK = "always"
312
+ MANUAL = "off"
313
+ SCHEDULE = "schedule"
314
+
315
+
316
+ @enum.unique
317
+ class VideoMode(str, ValuesEnumMixin, enum.Enum):
318
+ DEFAULT = "default"
319
+ HIGH_FPS = "highFps"
320
+ HOMEKIT = "homekit"
321
+ SPORT = "sport"
322
+ SLOW_SHUTTER = "slowShutter"
323
+ # should only be for unadopted devices
324
+ UNKNOWN = "unknown"
325
+
326
+
327
+ @enum.unique
328
+ class AudioStyle(str, UnknownValuesEnumMixin, enum.Enum):
329
+ NATURE = "nature"
330
+ NOISE_REDUCED = "noiseReduced"
331
+
332
+
333
+ @enum.unique
334
+ class RecordingMode(str, ValuesEnumMixin, enum.Enum):
335
+ ALWAYS = "always"
336
+ NEVER = "never"
337
+ SCHEDULE = "schedule"
338
+ DETECTIONS = "detections"
339
+
340
+
341
+ @enum.unique
342
+ class AnalyticsOption(str, ValuesEnumMixin, enum.Enum):
343
+ NONE = "none"
344
+ ANONYMOUS = "anonymous"
345
+ FULL = "full"
346
+
347
+
348
+ @enum.unique
349
+ class RecordingType(str, ValuesEnumMixin, enum.Enum):
350
+ TIMELAPSE = "timelapse"
351
+ CONTINUOUS = "rotating"
352
+ DETECTIONS = "detections"
353
+
354
+
355
+ @enum.unique
356
+ class ResolutionStorageType(str, ValuesEnumMixin, enum.Enum):
357
+ UHD = "4K"
358
+ HD = "HD"
359
+ FREE = "free"
360
+
361
+
362
+ @enum.unique
363
+ class IRLEDMode(str, UnknownValuesEnumMixin, enum.Enum):
364
+ AUTO = "auto"
365
+ ON = "on"
366
+ AUTO_NO_LED = "autoFilterOnly"
367
+ OFF = "off"
368
+ MANUAL = "manual"
369
+ CUSTOM = "custom"
370
+ UNKNOWN = "unknown"
371
+
372
+
373
+ @enum.unique
374
+ class MountType(str, ValuesEnumMixin, enum.Enum):
375
+ NONE = "none"
376
+ LEAK = "leak"
377
+ DOOR = "door"
378
+ WINDOW = "window"
379
+ GARAGE = "garage"
380
+
381
+
382
+ @enum.unique
383
+ class SensorType(str, ValuesEnumMixin, enum.Enum):
384
+ TEMPERATURE = "temperature"
385
+ LIGHT = "light"
386
+ HUMIDITY = "humidity"
387
+
388
+
389
+ @enum.unique
390
+ class SensorStatusType(str, UnknownValuesEnumMixin, enum.Enum):
391
+ OFFLINE = "offline"
392
+ UNKNOWN = "unknown"
393
+ SAFE = "safe"
394
+ NEUTRAL = "neutral"
395
+ LOW = "low"
396
+ HIGH = "high"
397
+
398
+
399
+ @enum.unique
400
+ class SleepStateType(str, ValuesEnumMixin, enum.Enum):
401
+ DISCONNECTED = "disconnected"
402
+ AWAKE = "awake"
403
+ START_SLEEP = "goingToSleep"
404
+ ASLEEP = "asleep"
405
+ WAKING = "waking"
406
+
407
+
408
+ @enum.unique
409
+ class AutoExposureMode(str, ValuesEnumMixin, enum.Enum):
410
+ MANUAL = "manual"
411
+ AUTO = "auto"
412
+ SHUTTER = "shutter"
413
+ FLICK50 = "flick50"
414
+ FLICK60 = "flick60"
415
+
416
+
417
+ @enum.unique
418
+ class FocusMode(str, ValuesEnumMixin, enum.Enum):
419
+ MANUAL = "manual"
420
+ AUTO = "auto"
421
+ ZTRIG = "ztrig"
422
+ TOUCH = "touch"
423
+
424
+
425
+ @enum.unique
426
+ class MountPosition(str, UnknownValuesEnumMixin, enum.Enum):
427
+ CEILING = "ceiling"
428
+ WALL = "wall"
429
+ DESK = "desk"
430
+ NONE = "none"
431
+ UNKNOWN = "unknown"
432
+
433
+
434
+ @enum.unique
435
+ class GeofencingSetting(str, ValuesEnumMixin, enum.Enum):
436
+ OFF = "off"
437
+ ALL_AWAY = "allAway"
438
+
439
+
440
+ @enum.unique
441
+ class MotionAlgorithm(str, ValuesEnumMixin, enum.Enum):
442
+ STABLE = "stable"
443
+ ENHANCED = "enhanced"
444
+
445
+
446
+ @enum.unique
447
+ class AudioCodecs(str, ValuesEnumMixin, enum.Enum):
448
+ AAC = "aac"
449
+ VORBIS = "vorbis"
450
+ OPUS = "opus"
451
+
452
+
453
+ @enum.unique
454
+ class LowMedHigh(str, ValuesEnumMixin, enum.Enum):
455
+ LOW = "low"
456
+ MEDIUM = "medium"
457
+ HIGH = "high"
458
+
459
+
460
+ @enum.unique
461
+ class StorageType(str, UnknownValuesEnumMixin, enum.Enum):
462
+ DISK = "hdd"
463
+ RAID = "raid"
464
+ SD_CARD = "sdcard"
465
+ INTERNAL_SSD = "internalSSD"
466
+ UNKNOWN = "UNKNOWN"
467
+
468
+
469
+ @enum.unique
470
+ class FirmwareReleaseChannel(str, ValuesEnumMixin, enum.Enum):
471
+ INTERNAL = "internal"
472
+ ALPHA = "alpha"
473
+ BETA = "beta"
474
+ RELEASE_CANDIDATE = "release-candidate"
475
+ RELEASE = "release"
476
+
477
+
478
+ @enum.unique
479
+ class ChimeType(int, enum.Enum):
480
+ NONE = 0
481
+ MECHANICAL = 300
482
+ DIGITAL = 1000
483
+
484
+
485
+ @enum.unique
486
+ class LockStatusType(str, ValuesEnumMixin, enum.Enum):
487
+ OPEN = "OPEN"
488
+ OPENING = "OPENING"
489
+ CLOSED = "CLOSED"
490
+ CLOSING = "CLOSING"
491
+ JAMMED_WHILE_CLOSING = "JAMMED_WHILE_CLOSING"
492
+ JAMMED_WHILE_OPENING = "JAMMED_WHILE_OPENING"
493
+ FAILED_WHILE_CLOSING = "FAILED_WHILE_CLOSING"
494
+ FAILED_WHILE_OPENING = "FAILED_WHILE_OPENING"
495
+ NOT_CALIBRATED = "NOT_CALIBRATED"
496
+ AUTO_CALIBRATION_IN_PROGRESS = "AUTO_CALIBRATION_IN_PROGRESS"
497
+ CALIBRATION_WAITING_OPEN = "CALIBRATION_WAITING_OPEN"
498
+ CALIBRATION_WAITING_CLOSE = "CALIBRATION_WAITING_CLOSE"
499
+
500
+
501
+ @enum.unique
502
+ class PermissionNode(str, UnknownValuesEnumMixin, enum.Enum):
503
+ CREATE = "create"
504
+ READ = "read"
505
+ WRITE = "write"
506
+ DELETE = "delete"
507
+ READ_MEDIA = "readmedia"
508
+ DELETE_MEDIA = "deletemedia"
509
+ READ_LIVE = "readlive"
510
+ UNKNOWN = "unknown"
511
+
512
+
513
+ @enum.unique
514
+ class HDRMode(str, UnknownValuesEnumMixin, enum.Enum):
515
+ NORMAL = "normal"
516
+ ALWAYS_ON = "superHdr"
517
+
518
+
519
+ @enum.unique
520
+ class LensType(str, enum.Enum):
521
+ NONE = "none"
522
+ FULL_360 = "360"
523
+ WIDE = "wide"
524
+ TELESCOPIC = "tele"
525
+ DLSR_17 = "m43"
526
+
527
+
528
+ class DoorbellText(ConstrainedStr):
529
+ max_length = 30
530
+
531
+
532
+ class ICRCustomValue(ConstrainedInt):
533
+ ge = 0
534
+ le = 10
535
+
536
+
537
+ class ICRLuxValue(ConstrainedInt):
538
+ ge = 1
539
+ le = 30
540
+
541
+
542
+ class LEDLevel(ConstrainedInt):
543
+ ge = 0
544
+ le = 6
545
+
546
+
547
+ class PercentInt(ConstrainedInt):
548
+ ge = 0
549
+ le = 100
550
+
551
+
552
+ class TwoByteInt(ConstrainedInt):
553
+ ge = 1
554
+ le = 255
555
+
556
+
557
+ class PercentFloat(ConstrainedFloat):
558
+ ge = 0
559
+ le = 100
560
+
561
+
562
+ class WDRLevel(ConstrainedInt):
563
+ ge = 0
564
+ le = 3
565
+
566
+
567
+ class ICRSensitivity(ConstrainedInt):
568
+ ge = 0
569
+ le = 3
570
+
571
+
572
+ class Percent(ConstrainedFloat):
573
+ ge = 0
574
+ le = 1
575
+
576
+
577
+ class RepeatTimes(ConstrainedInt):
578
+ ge = 1
579
+ le = 6
580
+
581
+
582
+ class PTZPositionDegree(BaseModel):
583
+ pan: float
584
+ tilt: float
585
+ zoom: int
586
+
587
+
588
+ class PTZPositionSteps(BaseModel):
589
+ focus: int
590
+ pan: int
591
+ tilt: int
592
+ zoom: int
593
+
594
+
595
+ class PTZPosition(BaseModel):
596
+ degree: PTZPositionDegree
597
+ steps: PTZPositionSteps
598
+
599
+
600
+ class PTZPresetPosition(BaseModel):
601
+ pan: int
602
+ tilt: int
603
+ zoom: int
604
+
605
+
606
+ class PTZPreset(BaseModel):
607
+ id: str
608
+ name: str
609
+ slot: int
610
+ ptz: PTZPresetPosition
611
+
612
+
613
+ CoordType = Union[Percent, int, float]
614
+
615
+
616
+ # TODO: fix when upgrading to pydantic v2
617
+ class Color(BaseColor):
618
+ def __eq__(self, o: object) -> bool:
619
+ if isinstance(o, Color):
620
+ return self.as_hex() == o.as_hex()
621
+
622
+ return super().__eq__(o)
623
+
624
+
625
+ class Version(BaseVersion):
626
+ def __str__(self) -> str:
627
+ super_str = super().__str__()
628
+ if self.pre is not None and self.pre[0] == "b":
629
+ super_str = super_str.replace("b", "-beta.")
630
+ return super_str