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.
- aionanit-1.0.0/PKG-INFO +78 -0
- aionanit-1.0.0/README.md +62 -0
- aionanit-1.0.0/aionanit/__init__.py +69 -0
- aionanit-1.0.0/aionanit/auth.py +96 -0
- aionanit-1.0.0/aionanit/camera.py +820 -0
- aionanit-1.0.0/aionanit/client.py +177 -0
- aionanit-1.0.0/aionanit/exceptions.py +51 -0
- aionanit-1.0.0/aionanit/models.py +129 -0
- aionanit-1.0.0/aionanit/proto/__init__.py +65 -0
- aionanit-1.0.0/aionanit/proto/nanit.py +278 -0
- aionanit-1.0.0/aionanit/rest.py +168 -0
- aionanit-1.0.0/aionanit/ws/__init__.py +25 -0
- aionanit-1.0.0/aionanit/ws/pending.py +70 -0
- aionanit-1.0.0/aionanit/ws/protocol.py +86 -0
- aionanit-1.0.0/aionanit/ws/transport.py +281 -0
- aionanit-1.0.0/aionanit.egg-info/PKG-INFO +78 -0
- aionanit-1.0.0/aionanit.egg-info/SOURCES.txt +27 -0
- aionanit-1.0.0/aionanit.egg-info/dependency_links.txt +1 -0
- aionanit-1.0.0/aionanit.egg-info/requires.txt +9 -0
- aionanit-1.0.0/aionanit.egg-info/top_level.txt +1 -0
- aionanit-1.0.0/pyproject.toml +31 -0
- aionanit-1.0.0/setup.cfg +4 -0
- aionanit-1.0.0/tests/test_auth.py +189 -0
- aionanit-1.0.0/tests/test_camera.py +660 -0
- aionanit-1.0.0/tests/test_client.py +222 -0
- aionanit-1.0.0/tests/test_pending.py +112 -0
- aionanit-1.0.0/tests/test_protocol.py +146 -0
- aionanit-1.0.0/tests/test_rest.py +238 -0
- aionanit-1.0.0/tests/test_transport.py +156 -0
aionanit-1.0.0/PKG-INFO
ADDED
|
@@ -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
|
aionanit-1.0.0/README.md
ADDED
|
@@ -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
|