Skip to content

Commit f3bda1b

Browse files
committed
Mod rename to mediacore + some fixes
1 parent 366227c commit f3bda1b

23 files changed

Lines changed: 515 additions & 308 deletions

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@
22
htmlcov
33
.coverage
44
coverage.xml
5+
scripts/**

README.md

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
# datarhei Core PyClient
2-
For rapid development of Python applications around the [datarhei Core](https://github.com/datarhei/core).
1+
# datarhei MediaCore Python Client
2+
For rapid development of Python applications around [datarhei Core](https://github.com/datarhei/core) / MediaCore.
33
Requires Python 3.7+ and datarhei Core v16.10+.
44

55
---
@@ -63,6 +63,14 @@ pip install https://github.com/datarhei/core-client-python/archive/refs/tags/{re
6363

6464
## Usage
6565

66+
### Preferred import path
67+
68+
```python
69+
from datarhei.mediacore import MediaCoreClient, AsyncMediaCoreClient
70+
```
71+
72+
`core_client` is still supported as a backward-compatible alias during migration.
73+
6674
#### Init arguments
6775

6876
- `base_url: str`
@@ -76,9 +84,9 @@ pip install https://github.com/datarhei/core-client-python/archive/refs/tags/{re
7684
#### Sync
7785

7886
```python
79-
from core_client import Client
87+
from datarhei.mediacore import MediaCoreClient
8088

81-
client = Client(base_url="http://127.0.0.1:8080", username="admin", password="datarhei")
89+
client = MediaCoreClient(base_url="http://127.0.0.1:8080", username="admin", password="datarhei")
8290
client.login()
8391

8492
about = client.about_get()
@@ -89,9 +97,9 @@ print(about)
8997

9098
```python
9199
import asyncio
92-
from core_client import AsyncClient
100+
from datarhei.mediacore import AsyncMediaCoreClient
93101

94-
client = AsyncClient(base_url="http://127.0.0.1:8080", username="admin", password="datarhei")
102+
client = AsyncMediaCoreClient(base_url="http://127.0.0.1:8080", username="admin", password="datarhei")
95103
client.login()
96104

97105
async def main():

core_client/__init__.py

Lines changed: 63 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,7 @@
88
from pydantic import (
99
HttpUrl,
1010
ValidationError as PydanticValidationError,
11-
validate_call,
12-
TypeAdapter
11+
validate_call
1312
)
1413

1514
from . import base
@@ -38,7 +37,7 @@ def __init__(
3837
if str(base_url).endswith("/"):
3938
self.base_url = str(base_url)[:-1]
4039
else:
41-
self.base_url = base_url
40+
self.base_url = str(base_url)
4241
self.username = username
4342
self.password = password
4443
self.domain = domain
@@ -51,7 +50,7 @@ def __init__(
5150
self.timeout = timeout
5251

5352
def _basic_login(self):
54-
domain_param = '?domain={self.domain}' if self.domain else ''
53+
domain_param = f"?domain={self.domain}" if self.domain else ""
5554
r_login = httpx.post(
5655
url=f"{self.base_url}/api/login{domain_param}",
5756
json={
@@ -74,7 +73,7 @@ def _basic_login(self):
7473
raise HTTPError("Authorization failed")
7574

7675
def _auth0_login(self):
77-
_headers = self.headers
76+
_headers = self.headers.copy()
7877
_headers["authorization"] = f"Bearer {self.auth0_token}"
7978
r_login = httpx.post(
8079
url=f"{self.base_url}/api/login",
@@ -95,21 +94,32 @@ def _auth0_login(self):
9594
raise HTTPError("Authorization failed")
9695

9796
def _set_access_token_expires_at(self, response: Token = None):
98-
if response:
97+
if response and response.access_token:
9998
try:
10099
self.access_token_expires_at = jwt.decode(
101100
response.access_token, options={"verify_signature": False}
102101
)["exp"]
103-
except (AttributeError, IndexError):
102+
except (
103+
AttributeError,
104+
IndexError,
105+
KeyError,
106+
TypeError,
107+
jwt.exceptions.PyJWTError,
108+
):
104109
raise HTTPError("Authorization failed")
105110
self.access_token = response.access_token
106111
return self.access_token_expires_at
107112

108113
def _access_token_is_expired(self):
114+
if not self.access_token:
115+
return True
109116
if not self.access_token_expires_at:
110-
self._set_access_token_expires_at(
111-
Token(access_token=self.access_token)
112-
)
117+
try:
118+
self._set_access_token_expires_at(
119+
Token(access_token=self.access_token)
120+
)
121+
except (PydanticValidationError, TypeError, HTTPError):
122+
return True
113123
if (
114124
datetime.fromtimestamp(self.access_token_expires_at)
115125
> datetime.now()
@@ -118,21 +128,32 @@ def _access_token_is_expired(self):
118128
return True
119129

120130
def _set_refresh_token_expires_at(self, response: Token = None):
121-
if response:
131+
if response and response.refresh_token:
122132
try:
123133
self.refresh_token_expires_at = jwt.decode(
124134
response.refresh_token, options={"verify_signature": False}
125135
)["exp"]
126-
except (AttributeError, IndexError):
136+
except (
137+
AttributeError,
138+
IndexError,
139+
KeyError,
140+
TypeError,
141+
jwt.exceptions.PyJWTError,
142+
):
127143
raise HTTPError("Authorization failed")
128144
self.refresh_token = response.refresh_token
129145
return self.refresh_token_expires_at
130146

131147
def _refresh_token_is_expired(self):
148+
if not self.refresh_token:
149+
return True
132150
if not self.refresh_token_expires_at:
133-
self._set_refresh_token_expires_at(
134-
Token(refresh_token=self.refresh_token)
135-
)
151+
try:
152+
self._set_refresh_token_expires_at(
153+
Token(refresh_token=self.refresh_token)
154+
)
155+
except (PydanticValidationError, TypeError, HTTPError):
156+
return True
136157
if (
137158
datetime.fromtimestamp(self.refresh_token_expires_at)
138159
> datetime.now()
@@ -142,7 +163,7 @@ def _refresh_token_is_expired(self):
142163

143164
def _refresh_access_token(self):
144165
if self.refresh_token:
145-
_headers = self.headers
166+
_headers = self.headers.copy()
146167
_headers["authorization"] = f"Bearer {self.refresh_token}"
147168
r_refresh_access_token = httpx.get(
148169
url=f"{self.base_url}/api/login/refresh",
@@ -160,7 +181,7 @@ def _refresh_access_token(self):
160181
self._basic_login()
161182

162183
def _get_headers(self):
163-
_headers = self.headers
184+
_headers = self.headers.copy()
164185
if (self.username and self.password) or \
165186
self.access_token or self.refresh_token or self.auth0_token:
166187
if (
@@ -171,18 +192,22 @@ def _get_headers(self):
171192
self._refresh_access_token()
172193
elif self.refresh_token and self._refresh_token_is_expired() is True:
173194
self.login()
174-
_headers["authorization"] = f"Bearer {self.access_token}"
195+
if self.access_token:
196+
_headers["authorization"] = f"Bearer {self.access_token}"
175197
return _headers
176198

177199
def token(self):
200+
expires_at = None
201+
if self.access_token_expires_at is not None:
202+
expires_at = int(self.access_token_expires_at)
178203
return Token(
179204
access_token=self.access_token,
180205
refresh_token=self.refresh_token,
181-
expires_at=int(self.access_token_expires_at),
206+
expires_at=expires_at,
182207
)
183208

184209
def login(self):
185-
r_about = httpx.get(url=f"{self.base_url}/api")
210+
r_about = httpx.get(url=f"{self.base_url}/api", timeout=self.timeout)
186211
if r_about.status_code == 200:
187212
try:
188213
about = About(**r_about.json())
@@ -279,4 +304,21 @@ async def proxy_method(self, *args, **kwargs):
279304
Client._add_proxy_method(method_name, submodule.sync)
280305

281306
except Exception as e:
282-
print(f" ERROR processing {module_info.name}: {e}")
307+
print(f" ERROR processing {module_info.name}: {e}")
308+
309+
310+
_METHOD_ALIASES = {
311+
"v3_process_get_report": "v3_report_get_process",
312+
"v3_iam_put_user_policy_list": "v3_iam_put_user_policy",
313+
}
314+
315+
for alias, target in _METHOD_ALIASES.items():
316+
if hasattr(Client, target) and not hasattr(Client, alias):
317+
setattr(Client, alias, getattr(Client, target))
318+
if hasattr(AsyncClient, target) and not hasattr(AsyncClient, alias):
319+
setattr(AsyncClient, alias, getattr(AsyncClient, target))
320+
321+
322+
# New canonical class names for datarhei MediaCore branding.
323+
MediaCoreClient = Client
324+
AsyncMediaCoreClient = AsyncClient
File renamed without changes.

core_client/base/api/v3_srt_get.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import httpx
22
from pydantic import TypeAdapter, validate_call
3-
from typing import Union
43

54
from ...models import Client
65
from ..models import Error
@@ -29,9 +28,15 @@ def _build_request(
2928

3029
def _build_response(response: httpx.Response):
3130
if response.status_code == 200:
32-
print(response.json())
33-
response_200 = TypeAdapter(SrtList).validate_python(response.json())
34-
return response_200.root
31+
payload = response.json()
32+
if isinstance(payload, list):
33+
response_200 = TypeAdapter(SrtList).validate_python(payload)
34+
return response_200.root
35+
36+
# Core versions >=16.10 may return a single aggregate SRT object
37+
# instead of a list. Keep the public return type stable as list[Srt].
38+
response_200 = TypeAdapter(Srt).validate_python(payload)
39+
return [response_200]
3540
else:
3641
response_error = TypeAdapter(Error).validate_python(response.json())
3742
return response_error

core_client/base/models/login.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
from pydantic import BaseModel
2+
from typing import Optional
23

34

45
class Token(BaseModel):
5-
access_token: str = None
6-
refresh_token: str = None
6+
access_token: Optional[str] = None
7+
refresh_token: Optional[str] = None
8+
expires_at: Optional[int] = None
79

810

911
class AccessToken(BaseModel):

core_client/base/models/v3/srt.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,12 @@ class Srt(BaseModel):
4040

4141
@model_validator(mode="before")
4242
def remove_empty(cls, values):
43-
if values["name"] is None:
44-
values.pop("name")
45-
values.pop("socketid")
43+
if not isinstance(values, dict):
44+
return values
45+
46+
if values.get("name") is None:
47+
values.pop("name", None)
48+
values.pop("socketid", None)
4649
else:
47-
values.pop("publisher")
50+
values.pop("publisher", None)
4851
return values

core_client/base/models/v3/srt_connection_stats.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -116,10 +116,13 @@ class SrtConnectionStats(BaseModel):
116116

117117
@model_validator(mode="before")
118118
def remove_empty(cls, values):
119-
if values["sent_unique__bytes"] is None:
120-
values.pop("sent_unique__bytes")
121-
values.pop("recv_loss__bytes")
119+
if not isinstance(values, dict):
120+
return values
121+
122+
if values.get("sent_unique__bytes") is None:
123+
values.pop("sent_unique__bytes", None)
124+
values.pop("recv_loss__bytes", None)
122125
else:
123-
values.pop("sent_unique_bytes")
124-
values.pop("recv_loss_bytes")
126+
values.pop("sent_unique_bytes", None)
127+
values.pop("recv_loss_bytes", None)
125128
return values

datarhei/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
"""datarhei Python packages."""
2+

datarhei/mediacore/__init__.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
"""Public import namespace for the datarhei MediaCore Python client."""
2+
3+
from __future__ import annotations
4+
5+
import sys
6+
7+
import core_client.base as _base
8+
import core_client.base.api as _base_api
9+
import core_client.base.models as _base_models
10+
import core_client.base.models.v3 as _base_models_v3
11+
import core_client.models as _models
12+
from core_client import AsyncClient as AsyncMediaCoreClient
13+
from core_client import Client as MediaCoreClient
14+
15+
# Friendly names for the new package path.
16+
Client = MediaCoreClient
17+
AsyncClient = AsyncMediaCoreClient
18+
19+
# Expose legacy submodules on the new import path.
20+
base = _base
21+
models = _models
22+
sys.modules[f"{__name__}.base"] = _base
23+
sys.modules[f"{__name__}.base.api"] = _base_api
24+
sys.modules[f"{__name__}.base.models"] = _base_models
25+
sys.modules[f"{__name__}.base.models.v3"] = _base_models_v3
26+
sys.modules[f"{__name__}.models"] = _models
27+
28+
__all__ = [
29+
"MediaCoreClient",
30+
"AsyncMediaCoreClient",
31+
"Client",
32+
"AsyncClient",
33+
"base",
34+
"models",
35+
]
36+

0 commit comments

Comments
 (0)