-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathbeatapi.py
More file actions
150 lines (127 loc) · 4.36 KB
/
Copy pathbeatapi.py
File metadata and controls
150 lines (127 loc) · 4.36 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
"""Small dependency-free BeatAPI reference client.
This module is example code, not an officially versioned SDK.
"""
from __future__ import annotations
import json
import os
import random as random_module
import time
import urllib.error
import urllib.parse
import urllib.request
from typing import Any, Callable
TERMINAL_STATUSES = {"succeeded", "failed"}
class BeatAPIError(Exception):
def __init__(
self,
message: str,
*,
status: int | None = None,
code: str | None = None,
request_id: str | None = None,
details: Any = None,
) -> None:
super().__init__(message)
self.status = status
self.code = code
self.request_id = request_id
self.details = details
class BeatAPIClient:
def __init__(
self,
*,
api_key: str | None = None,
base_url: str | None = None,
transport: Callable[[urllib.request.Request], Any] = urllib.request.urlopen,
sleep: Callable[[float], None] = time.sleep,
random: Callable[[], float] = random_module.random,
) -> None:
self.api_key = api_key if api_key is not None else os.getenv("BEATAPI_API_KEY")
if not self.api_key:
raise ValueError(
"BEATAPI_API_KEY is required. Create a key in the BeatAPI dashboard."
)
self.base_url = (
base_url or os.getenv("BEATAPI_BASE_URL") or "https://api.beatapi.io"
).rstrip("/")
self.transport = transport
self.sleep = sleep
self.random = random
def request(
self,
path: str,
*,
method: str = "GET",
body: dict[str, Any] | None = None,
) -> Any:
data = json.dumps(body).encode() if body is not None else None
headers = {
"Accept": "application/json",
"Authorization": f"Bearer {self.api_key}",
}
if data is not None:
headers["Content-Type"] = "application/json"
request = urllib.request.Request(
f"{self.base_url}{path}",
data=data,
headers=headers,
method=method,
)
try:
response = self.transport(request)
with response:
status = response.status
payload = json.loads(response.read().decode() or "{}")
except urllib.error.HTTPError as error:
status = error.code
payload = json.loads(error.read().decode() or "{}")
if status < 200 or status >= 300:
public_error = payload.get("error", {})
raise BeatAPIError(
public_error.get(
"message", f"BeatAPI request failed with HTTP {status}."
),
status=status,
code=public_error.get("code"),
request_id=public_error.get("request_id"),
details=public_error.get("details"),
)
return payload.get("data")
def create_music_video_task(self, input_data: dict[str, Any]) -> dict[str, Any]:
return self.request(
"/v1/music-video/tasks",
method="POST",
body=input_data,
)
def create_ecommerce_video_task(
self, input_data: dict[str, Any]
) -> dict[str, Any]:
return self.request(
"/v1/ecommerce-video/tasks",
method="POST",
body=input_data,
)
def get_task(self, task_id: str) -> dict[str, Any]:
return self.request(f"/v1/tasks/{urllib.parse.quote(task_id, safe='')}")
def get_usage(self) -> dict[str, Any]:
return self.request("/v1/usage")
def wait_for_task(
self,
task_id: str,
*,
interval_seconds: float = 5,
max_attempts: int = 120,
on_update: Callable[[dict[str, Any], int], None] | None = None,
) -> dict[str, Any]:
for attempt in range(1, max_attempts + 1):
task = self.get_task(task_id)
if on_update:
on_update(task, attempt)
if task["status"] in TERMINAL_STATUSES:
return task
if attempt < max_attempts:
jitter = interval_seconds * 0.2 * self.random()
self.sleep(interval_seconds + jitter)
raise TimeoutError(
f"Task {task_id} did not finish after {max_attempts} attempts."
)