lvqr 0.1.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.
lvqr-0.1.0/.gitignore ADDED
@@ -0,0 +1,52 @@
1
+ # Rust
2
+ /target/
3
+ **/*.rs.bk
4
+ *.pdb
5
+
6
+ # WASM
7
+ /pkg/
8
+ crates/lvqr-wasm/pkg/
9
+ bindings/js/packages/*/wasm/
10
+
11
+ # Node
12
+ node_modules/
13
+ bindings/js/packages/*/dist/
14
+
15
+ # Python
16
+ __pycache__/
17
+ *.py[cod]
18
+ *$py.class
19
+ *.egg-info/
20
+ dist/
21
+ build/
22
+ .eggs/
23
+ *.whl
24
+ venv/
25
+ .venv/
26
+
27
+ # IDE
28
+ .idea/
29
+ .vscode/
30
+ *.swp
31
+ *.swo
32
+ *~
33
+
34
+ # OS
35
+ .DS_Store
36
+ Thumbs.db
37
+
38
+ # Environment
39
+ .env
40
+ .env.local
41
+ .env.*.local
42
+
43
+ # Logs
44
+ *.log
45
+
46
+ # TLS dev certs (auto-generated)
47
+ *.pem
48
+ !deploy/certs/*.pem
49
+
50
+ # Coverage
51
+ lcov.info
52
+ tarpaulin-report.html
lvqr-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,18 @@
1
+ Metadata-Version: 2.4
2
+ Name: lvqr
3
+ Version: 0.1.0
4
+ Summary: LVQR client for Python - Live Video QUIC Relay admin and monitoring
5
+ Project-URL: Repository, https://github.com/virgilvox/lvqr
6
+ Project-URL: Documentation, https://github.com/virgilvox/lvqr/tree/main/bindings/python
7
+ Author-email: Moheeb Zara <hackbuildvideo@gmail.com>
8
+ License: MIT OR Apache-2.0
9
+ Keywords: moq,quic,relay,streaming,video
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Topic :: Multimedia :: Video
14
+ Requires-Python: >=3.9
15
+ Requires-Dist: httpx>=0.27
16
+ Provides-Extra: dev
17
+ Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
18
+ Requires-Dist: pytest>=8.0; extra == 'dev'
@@ -0,0 +1,34 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "lvqr"
7
+ version = "0.1.0"
8
+ description = "LVQR client for Python - Live Video QUIC Relay admin and monitoring"
9
+ license = {text = "MIT OR Apache-2.0"}
10
+ requires-python = ">=3.9"
11
+ authors = [{name = "Moheeb Zara", email = "hackbuildvideo@gmail.com"}]
12
+ keywords = ["streaming", "video", "quic", "moq", "relay"]
13
+ classifiers = [
14
+ "Development Status :: 3 - Alpha",
15
+ "Intended Audience :: Developers",
16
+ "Topic :: Multimedia :: Video",
17
+ "Programming Language :: Python :: 3",
18
+ ]
19
+ dependencies = [
20
+ "httpx>=0.27",
21
+ ]
22
+
23
+ [project.optional-dependencies]
24
+ dev = [
25
+ "pytest>=8.0",
26
+ "pytest-asyncio>=0.23",
27
+ ]
28
+
29
+ [project.urls]
30
+ Repository = "https://github.com/virgilvox/lvqr"
31
+ Documentation = "https://github.com/virgilvox/lvqr/tree/main/bindings/python"
32
+
33
+ [tool.hatch.build.targets.wheel]
34
+ packages = ["python/lvqr"]
@@ -0,0 +1,8 @@
1
+ """LVQR - Live Video QUIC Relay Python client."""
2
+
3
+ __version__ = "0.1.0"
4
+
5
+ from .client import LvqrClient
6
+ from .types import RelayStats, StreamInfo
7
+
8
+ __all__ = ["LvqrClient", "RelayStats", "StreamInfo"]
@@ -0,0 +1,83 @@
1
+ """LVQR admin API client."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import httpx
6
+
7
+ from .types import RelayStats, StreamInfo
8
+
9
+
10
+ class LvqrClient:
11
+ """Client for the LVQR admin HTTP API.
12
+
13
+ Args:
14
+ base_url: Base URL of the LVQR admin server (e.g., "http://localhost:8080").
15
+ timeout: Request timeout in seconds.
16
+
17
+ Example::
18
+
19
+ client = LvqrClient("http://localhost:8080")
20
+ if client.healthz():
21
+ stats = client.stats()
22
+ print(f"Tracks: {stats.tracks}, Subscribers: {stats.subscribers}")
23
+ """
24
+
25
+ def __init__(self, base_url: str, timeout: float = 10.0):
26
+ self.base_url = base_url.rstrip("/")
27
+ self._client = httpx.Client(base_url=self.base_url, timeout=timeout)
28
+
29
+ def close(self) -> None:
30
+ """Close the HTTP client."""
31
+ self._client.close()
32
+
33
+ def __enter__(self) -> LvqrClient:
34
+ return self
35
+
36
+ def __exit__(self, *args: object) -> None:
37
+ self.close()
38
+
39
+ def healthz(self) -> bool:
40
+ """Check if the relay is healthy.
41
+
42
+ Returns:
43
+ True if the server responds with 200 OK.
44
+ """
45
+ try:
46
+ resp = self._client.get("/healthz")
47
+ return resp.status_code == 200
48
+ except httpx.HTTPError:
49
+ return False
50
+
51
+ def stats(self) -> RelayStats:
52
+ """Get relay statistics.
53
+
54
+ Returns:
55
+ RelayStats with current server metrics.
56
+ """
57
+ resp = self._client.get("/api/v1/stats")
58
+ resp.raise_for_status()
59
+ data = resp.json()
60
+ return RelayStats(
61
+ publishers=data.get("publishers", 0),
62
+ subscribers=data.get("subscribers", 0),
63
+ tracks=data.get("tracks", 0),
64
+ bytes_received=data.get("bytes_received", 0),
65
+ bytes_sent=data.get("bytes_sent", 0),
66
+ uptime_secs=data.get("uptime_secs", 0),
67
+ )
68
+
69
+ def list_streams(self) -> list[StreamInfo]:
70
+ """List active streams.
71
+
72
+ Returns:
73
+ List of StreamInfo for each active stream.
74
+ """
75
+ resp = self._client.get("/api/v1/streams")
76
+ resp.raise_for_status()
77
+ return [
78
+ StreamInfo(
79
+ name=s.get("name", ""),
80
+ subscribers=s.get("subscribers", 0),
81
+ )
82
+ for s in resp.json()
83
+ ]
@@ -0,0 +1,23 @@
1
+ """Data types for the LVQR admin API."""
2
+
3
+ from dataclasses import dataclass
4
+
5
+
6
+ @dataclass
7
+ class RelayStats:
8
+ """Relay server statistics."""
9
+
10
+ publishers: int = 0
11
+ subscribers: int = 0
12
+ tracks: int = 0
13
+ bytes_received: int = 0
14
+ bytes_sent: int = 0
15
+ uptime_secs: int = 0
16
+
17
+
18
+ @dataclass
19
+ class StreamInfo:
20
+ """Information about an active stream."""
21
+
22
+ name: str
23
+ subscribers: int = 0
@@ -0,0 +1,76 @@
1
+ """Tests for the LVQR Python client."""
2
+
3
+ import json
4
+ from unittest.mock import MagicMock, patch
5
+
6
+ from lvqr import LvqrClient, RelayStats, StreamInfo
7
+
8
+
9
+ class TestTypes:
10
+ def test_relay_stats_defaults(self):
11
+ stats = RelayStats()
12
+ assert stats.publishers == 0
13
+ assert stats.subscribers == 0
14
+ assert stats.tracks == 0
15
+
16
+ def test_stream_info(self):
17
+ info = StreamInfo(name="live/test", subscribers=5)
18
+ assert info.name == "live/test"
19
+ assert info.subscribers == 5
20
+
21
+
22
+ class TestClient:
23
+ def test_context_manager(self):
24
+ with LvqrClient("http://localhost:8080") as client:
25
+ assert client.base_url == "http://localhost:8080"
26
+
27
+ def test_base_url_trailing_slash(self):
28
+ client = LvqrClient("http://localhost:8080/")
29
+ assert client.base_url == "http://localhost:8080"
30
+ client.close()
31
+
32
+ @patch("httpx.Client.get")
33
+ def test_healthz_ok(self, mock_get):
34
+ mock_get.return_value = MagicMock(status_code=200)
35
+ client = LvqrClient("http://localhost:8080")
36
+ assert client.healthz() is True
37
+ client.close()
38
+
39
+ @patch("httpx.Client.get")
40
+ def test_healthz_down(self, mock_get):
41
+ import httpx
42
+ mock_get.side_effect = httpx.ConnectError("connection refused")
43
+ client = LvqrClient("http://localhost:8080")
44
+ assert client.healthz() is False
45
+ client.close()
46
+
47
+ @patch("httpx.Client.get")
48
+ def test_stats(self, mock_get):
49
+ mock_get.return_value = MagicMock(
50
+ status_code=200,
51
+ json=lambda: {"tracks": 3, "subscribers": 10, "publishers": 1},
52
+ raise_for_status=lambda: None,
53
+ )
54
+ client = LvqrClient("http://localhost:8080")
55
+ stats = client.stats()
56
+ assert stats.tracks == 3
57
+ assert stats.subscribers == 10
58
+ assert stats.publishers == 1
59
+ client.close()
60
+
61
+ @patch("httpx.Client.get")
62
+ def test_list_streams(self, mock_get):
63
+ mock_get.return_value = MagicMock(
64
+ status_code=200,
65
+ json=lambda: [
66
+ {"name": "live/stream1", "subscribers": 5},
67
+ {"name": "live/stream2", "subscribers": 12},
68
+ ],
69
+ raise_for_status=lambda: None,
70
+ )
71
+ client = LvqrClient("http://localhost:8080")
72
+ streams = client.list_streams()
73
+ assert len(streams) == 2
74
+ assert streams[0].name == "live/stream1"
75
+ assert streams[1].subscribers == 12
76
+ client.close()