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 +52 -0
- lvqr-0.1.0/PKG-INFO +18 -0
- lvqr-0.1.0/pyproject.toml +34 -0
- lvqr-0.1.0/python/lvqr/__init__.py +8 -0
- lvqr-0.1.0/python/lvqr/client.py +83 -0
- lvqr-0.1.0/python/lvqr/types.py +23 -0
- lvqr-0.1.0/tests/test_client.py +76 -0
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,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()
|