aionanit 1.0.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.
@@ -0,0 +1,78 @@
1
+ Metadata-Version: 2.4
2
+ Name: aionanit
3
+ Version: 1.0.0
4
+ Summary: Async Python client for Nanit baby cameras
5
+ License-Expression: MIT
6
+ Requires-Python: >=3.12
7
+ Description-Content-Type: text/markdown
8
+ Requires-Dist: aiohttp>=3.9.0
9
+ Requires-Dist: betterproto>=2.0.0b7
10
+ Provides-Extra: dev
11
+ Requires-Dist: grpcio-tools; extra == "dev"
12
+ Requires-Dist: pytest; extra == "dev"
13
+ Requires-Dist: pytest-asyncio; extra == "dev"
14
+ Requires-Dist: aioresponses; extra == "dev"
15
+ Requires-Dist: pytest-cov; extra == "dev"
16
+
17
+ # aionanit
18
+
19
+ Async Python client library for Nanit baby cameras.
20
+
21
+ ## Features
22
+
23
+ - **Authentication**: Email/password login, MFA verification, automatic token refresh.
24
+ - **WebSocket**: Protobuf-over-WebSocket communication with cameras (cloud and local).
25
+ - **REST API**: Baby metadata, cloud events, snapshots.
26
+ - **Streaming**: RTMPS URL construction for live video.
27
+ - **Push-based**: Subscribe to real-time camera state changes (sensors, settings, controls).
28
+
29
+ ## Installation
30
+
31
+ ```bash
32
+ pip install aionanit
33
+ ```
34
+
35
+ ## Quick Start
36
+
37
+ ```python
38
+ import aiohttp
39
+ from aionanit import NanitClient
40
+
41
+ async with aiohttp.ClientSession() as session:
42
+ client = NanitClient(session)
43
+
44
+ # Login
45
+ tokens = await client.async_login("you@example.com", "password")
46
+
47
+ # Get babies
48
+ babies = await client.async_get_babies()
49
+ baby = babies[0]
50
+
51
+ # Connect to camera
52
+ camera = client.camera(baby.camera_uid, baby.uid)
53
+ await camera.async_start()
54
+
55
+ # Subscribe to state changes
56
+ def on_event(event):
57
+ print(f"Sensors: {event.state.sensors}")
58
+
59
+ unsub = camera.subscribe(on_event)
60
+
61
+ # Get RTMPS stream URL
62
+ url = await camera.async_get_stream_rtmps_url()
63
+ print(f"Stream: {url}")
64
+
65
+ # Cleanup
66
+ unsub()
67
+ await client.async_close()
68
+ ```
69
+
70
+ ## Requirements
71
+
72
+ - Python 3.12+
73
+ - aiohttp >= 3.9.0
74
+ - betterproto >= 2.0.0b7
75
+
76
+ ## License
77
+
78
+ MIT
@@ -0,0 +1,62 @@
1
+ # aionanit
2
+
3
+ Async Python client library for Nanit baby cameras.
4
+
5
+ ## Features
6
+
7
+ - **Authentication**: Email/password login, MFA verification, automatic token refresh.
8
+ - **WebSocket**: Protobuf-over-WebSocket communication with cameras (cloud and local).
9
+ - **REST API**: Baby metadata, cloud events, snapshots.
10
+ - **Streaming**: RTMPS URL construction for live video.
11
+ - **Push-based**: Subscribe to real-time camera state changes (sensors, settings, controls).
12
+
13
+ ## Installation
14
+
15
+ ```bash
16
+ pip install aionanit
17
+ ```
18
+
19
+ ## Quick Start
20
+
21
+ ```python
22
+ import aiohttp
23
+ from aionanit import NanitClient
24
+
25
+ async with aiohttp.ClientSession() as session:
26
+ client = NanitClient(session)
27
+
28
+ # Login
29
+ tokens = await client.async_login("you@example.com", "password")
30
+
31
+ # Get babies
32
+ babies = await client.async_get_babies()
33
+ baby = babies[0]
34
+
35
+ # Connect to camera
36
+ camera = client.camera(baby.camera_uid, baby.uid)
37
+ await camera.async_start()
38
+
39
+ # Subscribe to state changes
40
+ def on_event(event):
41
+ print(f"Sensors: {event.state.sensors}")
42
+
43
+ unsub = camera.subscribe(on_event)
44
+
45
+ # Get RTMPS stream URL
46
+ url = await camera.async_get_stream_rtmps_url()
47
+ print(f"Stream: {url}")
48
+
49
+ # Cleanup
50
+ unsub()
51
+ await client.async_close()
52
+ ```
53
+
54
+ ## Requirements
55
+
56
+ - Python 3.12+
57
+ - aiohttp >= 3.9.0
58
+ - betterproto >= 2.0.0b7
59
+
60
+ ## License
61
+
62
+ MIT
@@ -0,0 +1,69 @@
1
+ """aionanit — async Python client for Nanit baby cameras."""
2
+
3
+ from .auth import TokenManager
4
+ from .camera import NanitCamera
5
+ from .client import NanitClient
6
+ from .exceptions import (
7
+ NanitAuthError,
8
+ NanitCameraUnavailable,
9
+ NanitConnectionError,
10
+ NanitError,
11
+ NanitMfaRequiredError,
12
+ NanitProtocolError,
13
+ NanitRequestTimeout,
14
+ NanitTransportError,
15
+ )
16
+ from .models import (
17
+ Baby,
18
+ CameraEvent,
19
+ CameraEventKind,
20
+ CameraState,
21
+ CloudEvent,
22
+ ConnectionInfo,
23
+ ConnectionState,
24
+ ControlState,
25
+ NightLightState,
26
+ SensorReading,
27
+ SensorState,
28
+ SensorType,
29
+ SettingsState,
30
+ StatusState,
31
+ TransportKind,
32
+ )
33
+ from .rest import NanitRestClient
34
+
35
+ __all__ = [
36
+ # auth
37
+ "TokenManager",
38
+ # camera
39
+ "NanitCamera",
40
+ # client
41
+ "NanitClient",
42
+ # rest
43
+ "NanitRestClient",
44
+ # models
45
+ "Baby",
46
+ "CameraEvent",
47
+ "CameraEventKind",
48
+ "CameraState",
49
+ "CloudEvent",
50
+ "ConnectionInfo",
51
+ "ConnectionState",
52
+ "ControlState",
53
+ "NightLightState",
54
+ "SensorReading",
55
+ "SensorState",
56
+ "SensorType",
57
+ "SettingsState",
58
+ "StatusState",
59
+ "TransportKind",
60
+ # exceptions
61
+ "NanitAuthError",
62
+ "NanitCameraUnavailable",
63
+ "NanitConnectionError",
64
+ "NanitError",
65
+ "NanitMfaRequiredError",
66
+ "NanitProtocolError",
67
+ "NanitRequestTimeout",
68
+ "NanitTransportError",
69
+ ]
@@ -0,0 +1,96 @@
1
+ """Token management with proactive refresh for the Nanit API."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import time
7
+ from collections.abc import Callable
8
+ from typing import TYPE_CHECKING
9
+
10
+ from .exceptions import NanitAuthError
11
+
12
+ if TYPE_CHECKING:
13
+ from .rest import NanitRestClient
14
+
15
+
16
+ class TokenManager:
17
+ """Manages access/refresh tokens with automatic proactive renewal.
18
+
19
+ Does not own the REST client — caller provides it. Acquires an
20
+ asyncio.Lock around refresh operations to prevent concurrent refreshes.
21
+ """
22
+
23
+ def __init__(
24
+ self,
25
+ rest: NanitRestClient,
26
+ access_token: str,
27
+ refresh_token: str,
28
+ expires_in: float = 3600.0,
29
+ ) -> None:
30
+ self._rest: NanitRestClient = rest
31
+ self._access_token: str = access_token
32
+ self._refresh_token: str = refresh_token
33
+ self._expires_at: float = time.monotonic() + expires_in
34
+ self._lock: asyncio.Lock = asyncio.Lock()
35
+ self._callbacks: list[Callable[[str, str], None]] = []
36
+
37
+ @property
38
+ def access_token(self) -> str:
39
+ return self._access_token
40
+
41
+ @property
42
+ def refresh_token(self) -> str:
43
+ return self._refresh_token
44
+
45
+ def update_tokens(
46
+ self,
47
+ access_token: str,
48
+ refresh_token: str,
49
+ expires_in: float = 3600.0,
50
+ ) -> None:
51
+ self._access_token = access_token
52
+ self._refresh_token = refresh_token
53
+ self._expires_at = time.monotonic() + expires_in
54
+
55
+ async def async_get_access_token(
56
+ self, min_ttl: float = 60.0
57
+ ) -> str:
58
+ async with self._lock:
59
+ if time.monotonic() + min_ttl >= self._expires_at:
60
+ await self._async_refresh()
61
+ return self._access_token
62
+
63
+ async def async_force_refresh(self) -> None:
64
+ async with self._lock:
65
+ await self._async_refresh()
66
+
67
+ async def _async_refresh(self) -> None:
68
+ try:
69
+ tokens = await self._rest.async_refresh_token(
70
+ self._access_token, self._refresh_token
71
+ )
72
+ except NanitAuthError:
73
+ raise
74
+ except Exception as err:
75
+ raise NanitAuthError(f"Token refresh failed: {err}") from err
76
+
77
+ self._access_token = tokens["access_token"]
78
+ self._refresh_token = tokens["refresh_token"]
79
+ self._expires_at = time.monotonic() + 3600.0
80
+
81
+ for callback in self._callbacks:
82
+ callback(self._access_token, self._refresh_token)
83
+
84
+ def on_tokens_refreshed(
85
+ self, callback: Callable[[str, str], None]
86
+ ) -> Callable[[], None]:
87
+ """Register a callback invoked with (access_token, refresh_token) after refresh.
88
+
89
+ Returns an unsubscribe function that removes the callback.
90
+ """
91
+ self._callbacks.append(callback)
92
+
93
+ def _unsubscribe() -> None:
94
+ self._callbacks.remove(callback)
95
+
96
+ return _unsubscribe