uiprotect 7.12.0__tar.gz → 7.13.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.

Potentially problematic release.


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

Files changed (38) hide show
  1. {uiprotect-7.12.0 → uiprotect-7.13.0}/PKG-INFO +1 -1
  2. {uiprotect-7.12.0 → uiprotect-7.13.0}/pyproject.toml +1 -1
  3. {uiprotect-7.12.0 → uiprotect-7.13.0}/src/uiprotect/api.py +46 -15
  4. {uiprotect-7.12.0 → uiprotect-7.13.0}/src/uiprotect/cli/__init__.py +28 -1
  5. {uiprotect-7.12.0 → uiprotect-7.13.0}/src/uiprotect/data/nvr.py +4 -0
  6. {uiprotect-7.12.0 → uiprotect-7.13.0}/LICENSE +0 -0
  7. {uiprotect-7.12.0 → uiprotect-7.13.0}/README.md +0 -0
  8. {uiprotect-7.12.0 → uiprotect-7.13.0}/src/uiprotect/__init__.py +0 -0
  9. {uiprotect-7.12.0 → uiprotect-7.13.0}/src/uiprotect/__main__.py +0 -0
  10. {uiprotect-7.12.0 → uiprotect-7.13.0}/src/uiprotect/_compat.py +0 -0
  11. {uiprotect-7.12.0 → uiprotect-7.13.0}/src/uiprotect/cli/aiports.py +0 -0
  12. {uiprotect-7.12.0 → uiprotect-7.13.0}/src/uiprotect/cli/backup.py +0 -0
  13. {uiprotect-7.12.0 → uiprotect-7.13.0}/src/uiprotect/cli/base.py +0 -0
  14. {uiprotect-7.12.0 → uiprotect-7.13.0}/src/uiprotect/cli/cameras.py +0 -0
  15. {uiprotect-7.12.0 → uiprotect-7.13.0}/src/uiprotect/cli/chimes.py +0 -0
  16. {uiprotect-7.12.0 → uiprotect-7.13.0}/src/uiprotect/cli/doorlocks.py +0 -0
  17. {uiprotect-7.12.0 → uiprotect-7.13.0}/src/uiprotect/cli/events.py +0 -0
  18. {uiprotect-7.12.0 → uiprotect-7.13.0}/src/uiprotect/cli/lights.py +0 -0
  19. {uiprotect-7.12.0 → uiprotect-7.13.0}/src/uiprotect/cli/liveviews.py +0 -0
  20. {uiprotect-7.12.0 → uiprotect-7.13.0}/src/uiprotect/cli/nvr.py +0 -0
  21. {uiprotect-7.12.0 → uiprotect-7.13.0}/src/uiprotect/cli/sensors.py +0 -0
  22. {uiprotect-7.12.0 → uiprotect-7.13.0}/src/uiprotect/cli/viewers.py +0 -0
  23. {uiprotect-7.12.0 → uiprotect-7.13.0}/src/uiprotect/data/__init__.py +0 -0
  24. {uiprotect-7.12.0 → uiprotect-7.13.0}/src/uiprotect/data/base.py +0 -0
  25. {uiprotect-7.12.0 → uiprotect-7.13.0}/src/uiprotect/data/bootstrap.py +0 -0
  26. {uiprotect-7.12.0 → uiprotect-7.13.0}/src/uiprotect/data/convert.py +0 -0
  27. {uiprotect-7.12.0 → uiprotect-7.13.0}/src/uiprotect/data/devices.py +0 -0
  28. {uiprotect-7.12.0 → uiprotect-7.13.0}/src/uiprotect/data/types.py +0 -0
  29. {uiprotect-7.12.0 → uiprotect-7.13.0}/src/uiprotect/data/user.py +0 -0
  30. {uiprotect-7.12.0 → uiprotect-7.13.0}/src/uiprotect/data/websocket.py +0 -0
  31. {uiprotect-7.12.0 → uiprotect-7.13.0}/src/uiprotect/exceptions.py +0 -0
  32. {uiprotect-7.12.0 → uiprotect-7.13.0}/src/uiprotect/py.typed +0 -0
  33. {uiprotect-7.12.0 → uiprotect-7.13.0}/src/uiprotect/release_cache.json +0 -0
  34. {uiprotect-7.12.0 → uiprotect-7.13.0}/src/uiprotect/stream.py +0 -0
  35. {uiprotect-7.12.0 → uiprotect-7.13.0}/src/uiprotect/test_util/__init__.py +0 -0
  36. {uiprotect-7.12.0 → uiprotect-7.13.0}/src/uiprotect/test_util/anonymize.py +0 -0
  37. {uiprotect-7.12.0 → uiprotect-7.13.0}/src/uiprotect/utils.py +0 -0
  38. {uiprotect-7.12.0 → uiprotect-7.13.0}/src/uiprotect/websocket.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: uiprotect
3
- Version: 7.12.0
3
+ Version: 7.13.0
4
4
  Summary: Python API for Unifi Protect (Unofficial)
5
5
  License: MIT
6
6
  Author: UI Protect Maintainers
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "uiprotect"
3
- version = "7.12.0"
3
+ version = "7.13.0"
4
4
  license = "MIT"
5
5
  description = "Python API for Unifi Protect (Unofficial)"
6
6
  authors = [{ name = "UI Protect Maintainers", email = "ui@koston.org" }]
@@ -28,6 +28,7 @@ from platformdirs import user_cache_dir, user_config_dir
28
28
  from yarl import URL
29
29
 
30
30
  from uiprotect.data.convert import list_from_unifi_list
31
+ from uiprotect.data.nvr import MetaInfo
31
32
  from uiprotect.data.user import Keyring, Keyrings, UlpUser, UlpUsers
32
33
 
33
34
  from ._compat import cached_property
@@ -175,10 +176,11 @@ class BaseApiClient:
175
176
  _cookiename = "TOKEN"
176
177
 
177
178
  headers: dict[str, str] | None = None
178
- _websocket: Websocket | None = None
179
+ _private_websocket: Websocket | None = None
179
180
 
180
- api_path: str = "/proxy/protect/api/"
181
- ws_path: str = "/proxy/protect/ws/updates"
181
+ private_api_path: str = "/proxy/protect/api/"
182
+ public_api_path: str = "/proxy/protect/integration"
183
+ private_ws_path: str = "/proxy/protect/ws/updates"
182
184
 
183
185
  cache_dir: Path
184
186
  config_dir: Path
@@ -229,10 +231,10 @@ class BaseApiClient:
229
231
  """Updates the url after changing _host or _port."""
230
232
  if self._port != 443:
231
233
  self._url = URL(f"https://{self._host}:{self._port}")
232
- self._ws_url = URL(f"wss://{self._host}:{self._port}{self.ws_path}")
234
+ self._ws_url = URL(f"wss://{self._host}:{self._port}{self.private_ws_path}")
233
235
  else:
234
236
  self._url = URL(f"https://{self._host}")
235
- self._ws_url = URL(f"wss://{self._host}{self.ws_path}")
237
+ self._ws_url = URL(f"wss://{self._host}{self.private_ws_path}")
236
238
 
237
239
  self.base_url = str(self._url)
238
240
 
@@ -276,8 +278,8 @@ class BaseApiClient:
276
278
 
277
279
  def _get_websocket(self) -> Websocket:
278
280
  """Gets or creates current Websocket."""
279
- if self._websocket is None:
280
- self._websocket = Websocket(
281
+ if self._private_websocket is None:
282
+ self._private_websocket = Websocket(
281
283
  self._get_websocket_url,
282
284
  self._auth_websocket,
283
285
  self._update_bootstrap_soon,
@@ -288,7 +290,7 @@ class BaseApiClient:
288
290
  timeout=self._ws_timeout,
289
291
  receive_timeout=self._ws_receive_timeout,
290
292
  )
291
- return self._websocket
293
+ return self._private_websocket
292
294
 
293
295
  def _update_bootstrap_soon(self) -> None:
294
296
  """Update bootstrap soon."""
@@ -328,16 +330,21 @@ class BaseApiClient:
328
330
  url: str,
329
331
  require_auth: bool = False,
330
332
  auto_close: bool = True,
333
+ public_api: bool = False,
331
334
  **kwargs: Any,
332
335
  ) -> aiohttp.ClientResponse:
333
336
  """Make a request to UniFi Protect"""
334
- if require_auth:
337
+ if require_auth and not public_api:
335
338
  await self.ensure_authenticated()
336
339
 
337
340
  request_url = self._url.join(
338
341
  URL(SplitResult("", "", url, "", ""), encoded=True)
339
342
  )
340
- headers = kwargs.get("headers") or self.headers
343
+ headers = kwargs.get("headers") or self.headers or {}
344
+ if require_auth and public_api:
345
+ if self._api_key is None:
346
+ raise NotAuthorized("API key is required for public API requests")
347
+ headers = {"X-API-KEY": self._api_key}
341
348
  _LOGGER.debug("Request url: %s", request_url)
342
349
  if not self._verify_ssl:
343
350
  kwargs["ssl"] = False
@@ -394,16 +401,22 @@ class BaseApiClient:
394
401
  require_auth: bool = True,
395
402
  raise_exception: bool = True,
396
403
  api_path: str | None = None,
404
+ public_api: bool = False,
397
405
  **kwargs: Any,
398
406
  ) -> bytes | None:
399
407
  """Make a API request"""
400
- path = api_path if api_path is not None else self.api_path
408
+ path = self.private_api_path
409
+ if api_path is not None:
410
+ path = api_path
411
+ elif public_api:
412
+ path = self.public_api_path
401
413
 
402
414
  response = await self.request(
403
415
  method,
404
416
  f"{path}{url}",
405
417
  require_auth=require_auth,
406
418
  auto_close=False,
419
+ public_api=public_api,
407
420
  **kwargs,
408
421
  )
409
422
 
@@ -456,6 +469,7 @@ class BaseApiClient:
456
469
  require_auth: bool = True,
457
470
  raise_exception: bool = True,
458
471
  api_path: str | None = None,
472
+ public_api: bool = False,
459
473
  **kwargs: Any,
460
474
  ) -> list[Any] | dict[str, Any] | None:
461
475
  data = await self.api_request_raw(
@@ -464,6 +478,7 @@ class BaseApiClient:
464
478
  require_auth=require_auth,
465
479
  raise_exception=raise_exception,
466
480
  api_path=api_path,
481
+ public_api=public_api,
467
482
  **kwargs,
468
483
  )
469
484
 
@@ -483,6 +498,7 @@ class BaseApiClient:
483
498
  method: str = "get",
484
499
  require_auth: bool = True,
485
500
  raise_exception: bool = True,
501
+ public_api: bool = False,
486
502
  **kwargs: Any,
487
503
  ) -> dict[str, Any]:
488
504
  data = await self.api_request(
@@ -490,6 +506,7 @@ class BaseApiClient:
490
506
  method=method,
491
507
  require_auth=require_auth,
492
508
  raise_exception=raise_exception,
509
+ public_api=public_api,
493
510
  **kwargs,
494
511
  )
495
512
 
@@ -504,6 +521,7 @@ class BaseApiClient:
504
521
  method: str = "get",
505
522
  require_auth: bool = True,
506
523
  raise_exception: bool = True,
524
+ public_api: bool = False,
507
525
  **kwargs: Any,
508
526
  ) -> list[Any]:
509
527
  data = await self.api_request(
@@ -511,6 +529,7 @@ class BaseApiClient:
511
529
  method=method,
512
530
  require_auth=require_auth,
513
531
  raise_exception=raise_exception,
532
+ public_api=public_api,
514
533
  **kwargs,
515
534
  )
516
535
 
@@ -689,11 +708,11 @@ class BaseApiClient:
689
708
 
690
709
  async def async_disconnect_ws(self) -> None:
691
710
  """Disconnect from Websocket."""
692
- if self._websocket:
711
+ if self._private_websocket:
693
712
  websocket = self._get_websocket()
694
713
  websocket.stop()
695
714
  await websocket.wait_closed()
696
- self._websocket = None
715
+ self._private_websocket = None
697
716
 
698
717
  def _process_ws_message(self, msg: aiohttp.WSMessage) -> None:
699
718
  raise NotImplementedError
@@ -1590,10 +1609,12 @@ class ProtectApiClient(BaseApiClient):
1590
1609
  raise_exception=False,
1591
1610
  )
1592
1611
 
1593
- _LOGGER.debug("Requesting camera video: %s%s %s", self.api_path, path, params)
1612
+ _LOGGER.debug(
1613
+ "Requesting camera video: %s%s %s", self.private_api_path, path, params
1614
+ )
1594
1615
  r = await self.request(
1595
1616
  "get",
1596
- f"{self.api_path}{path}",
1617
+ f"{self.private_api_path}{path}",
1597
1618
  auto_close=False,
1598
1619
  timeout=0,
1599
1620
  params=params,
@@ -2078,3 +2099,13 @@ class ProtectApiClient(BaseApiClient):
2078
2099
  raise BadRequest("Failed to create API key")
2079
2100
 
2080
2101
  return response["data"]["full_api_key"]
2102
+
2103
+ async def get_meta_info(self) -> MetaInfo:
2104
+ """Get metadata about the NVR."""
2105
+ data = await self.api_request(
2106
+ url="/v1/meta/info",
2107
+ public_api=True,
2108
+ )
2109
+ if not isinstance(data, dict):
2110
+ raise NvrError("Failed to retrieve meta info from public API")
2111
+ return MetaInfo(**data)
@@ -11,7 +11,7 @@ import orjson
11
11
  import typer
12
12
  from rich.progress import track
13
13
 
14
- from uiprotect.api import ProtectApiClient
14
+ from uiprotect.api import MetaInfo, ProtectApiClient
15
15
 
16
16
  from ..data import Version, WSPacket
17
17
  from ..test_util import SampleDataGenerator
@@ -60,6 +60,15 @@ OPTION_PASSWORD = typer.Option(
60
60
  hide_input=True,
61
61
  envvar="UFP_PASSWORD",
62
62
  )
63
+ OPTION_API_KEY = typer.Option(
64
+ ...,
65
+ "--api-key",
66
+ "-k",
67
+ help="UniFi Protect API key",
68
+ prompt=True,
69
+ hide_input=True,
70
+ envvar="UFP_API_KEY",
71
+ )
63
72
  OPTION_ADDRESS = typer.Option(
64
73
  ...,
65
74
  "--address",
@@ -140,6 +149,7 @@ def main(
140
149
  ctx: typer.Context,
141
150
  username: str = OPTION_USERNAME,
142
151
  password: str = OPTION_PASSWORD,
152
+ api_key: str = OPTION_API_KEY,
143
153
  address: str = OPTION_ADDRESS,
144
154
  port: int = OPTION_PORT,
145
155
  verify: bool = OPTION_VERIFY,
@@ -155,6 +165,7 @@ def main(
155
165
  port,
156
166
  username,
157
167
  password,
168
+ api_key,
158
169
  verify_ssl=verify,
159
170
  ignore_unadopted=not include_unadopted,
160
171
  )
@@ -338,3 +349,19 @@ def create_api_key(
338
349
  _setup_logger()
339
350
  result = run_async(callback())
340
351
  typer.echo(result)
352
+
353
+
354
+ @app.command()
355
+ def get_meta_info(ctx: typer.Context) -> None:
356
+ """Get metadata about the current UniFi Protect instance."""
357
+ protect = cast(ProtectApiClient, ctx.obj.protect)
358
+
359
+ async def callback() -> MetaInfo:
360
+ meta = await protect.get_meta_info()
361
+ await protect.close_session()
362
+ return meta
363
+
364
+ _setup_logger()
365
+
366
+ result = run_async(callback())
367
+ typer.echo(result.model_dump_json())
@@ -71,6 +71,10 @@ DELETE_KEYS_THUMB = {"color", "vehicleType"}
71
71
  DELETE_KEYS_EVENT = {"deletedAt", "category", "subCategory"}
72
72
 
73
73
 
74
+ class MetaInfo(ProtectBaseObject):
75
+ applicationVersion: str
76
+
77
+
74
78
  class NVRLocation(UserLocation):
75
79
  is_geofencing_enabled: bool
76
80
  radius: int
File without changes
File without changes