fastpix-python 0.1.0__py3-none-any.whl
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.
- fastpix/__init__.py +4 -0
- fastpix/async_client/__init__.py +3 -0
- fastpix/async_client/api.py +33 -0
- fastpix/async_client/client.py +52 -0
- fastpix/async_client/live_streams.py +147 -0
- fastpix/async_client/media.py +121 -0
- fastpix/async_client/playback_ids.py +79 -0
- fastpix/client/__init__.py +3 -0
- fastpix/client/api.py +28 -0
- fastpix/client/client.py +57 -0
- fastpix/client/live_streams.py +150 -0
- fastpix/client/media.py +110 -0
- fastpix/client/playback_ids.py +80 -0
- fastpix/utilis/__init__.py +4 -0
- fastpix/utilis/constants.py +1 -0
- fastpix/utilis/exceptions.py +14 -0
- fastpix/utilis/utilis.py +17 -0
- fastpix_python-0.1.0.dist-info/METADATA +28 -0
- fastpix_python-0.1.0.dist-info/RECORD +22 -0
- fastpix_python-0.1.0.dist-info/WHEEL +5 -0
- fastpix_python-0.1.0.dist-info/licenses/LICENSE +21 -0
- fastpix_python-0.1.0.dist-info/top_level.txt +1 -0
fastpix/__init__.py
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import aiohttp
|
|
2
|
+
from fastpix.utilis.exceptions import APIError
|
|
3
|
+
from fastpix.utilis.constants import BASE_URL
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
async def make_request(method, endpoint, headers=None, data=None, params=None):
|
|
7
|
+
url = f"{BASE_URL}{endpoint}"
|
|
8
|
+
headers = headers or {}
|
|
9
|
+
|
|
10
|
+
try:
|
|
11
|
+
async with aiohttp.ClientSession() as session:
|
|
12
|
+
async with session.request(
|
|
13
|
+
method=method,
|
|
14
|
+
url=url,
|
|
15
|
+
headers=headers,
|
|
16
|
+
json=data,
|
|
17
|
+
params=params,
|
|
18
|
+
) as response:
|
|
19
|
+
try:
|
|
20
|
+
response_data = await response.json()
|
|
21
|
+
except aiohttp.ContentTypeError:
|
|
22
|
+
response_data = await response.text()
|
|
23
|
+
|
|
24
|
+
if response.ok:
|
|
25
|
+
return response_data
|
|
26
|
+
else:
|
|
27
|
+
raise APIError(
|
|
28
|
+
message=f"Request failed with status {response.status}",
|
|
29
|
+
status_code=response.status,
|
|
30
|
+
details=response_data
|
|
31
|
+
)
|
|
32
|
+
except aiohttp.ClientError as e:
|
|
33
|
+
raise APIError(f"Request failed: {str(e)}")
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
from fastpix.utilis.exceptions import APIError
|
|
3
|
+
from fastpix.async_client.media import MediaResource
|
|
4
|
+
from fastpix.async_client.playback_ids import PlaybackIDs
|
|
5
|
+
from fastpix.async_client.live_streams import Livestream
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Client:
|
|
9
|
+
def __init__(self, username=None, password=None, api_key=None):
|
|
10
|
+
"""
|
|
11
|
+
Initialize the client with flexible authentication.
|
|
12
|
+
|
|
13
|
+
Args:
|
|
14
|
+
username (str, optional): Username for authentication
|
|
15
|
+
password (str, optional): Password for authentication
|
|
16
|
+
api_key (str, optional): Pre-generated base64 encoded API key
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
# Validate and prepare API key
|
|
20
|
+
if api_key:
|
|
21
|
+
self.api_key = api_key
|
|
22
|
+
elif username and password:
|
|
23
|
+
# Encode credentials to base64
|
|
24
|
+
credentials = f"{username}:{password}"
|
|
25
|
+
self.api_key = base64.b64encode(credentials.encode('utf-8')).decode('utf-8')
|
|
26
|
+
else:
|
|
27
|
+
raise ValueError("Must provide either username and password or a pre-generated API key")
|
|
28
|
+
|
|
29
|
+
# Prepare headers
|
|
30
|
+
self.headers = {
|
|
31
|
+
"accept": "application/json",
|
|
32
|
+
"content-type": "application/json",
|
|
33
|
+
"Authorization": f"Basic {self.api_key}",
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
# Initialize resources
|
|
37
|
+
self.media = MediaResource(self)
|
|
38
|
+
self.playback_ids = PlaybackIDs(self)
|
|
39
|
+
self.livestreams = Livestream(self)
|
|
40
|
+
|
|
41
|
+
async def _validate_credentials(self):
|
|
42
|
+
"""
|
|
43
|
+
Validate credentials by attempting to fetch media.
|
|
44
|
+
Raises an APIError if authentication fails.
|
|
45
|
+
"""
|
|
46
|
+
try:
|
|
47
|
+
# Attempt to fetch media to verify credentials (async)
|
|
48
|
+
response = await self.media.get_all()
|
|
49
|
+
return response
|
|
50
|
+
|
|
51
|
+
except APIError as e:
|
|
52
|
+
raise APIError(f"Authentication failed: {str(e)}") from e
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
from fastpix.utilis.exceptions import APIError
|
|
2
|
+
from fastpix.async_client.api import make_request
|
|
3
|
+
from fastpix.utilis.utilis import validate_uuid, validate_request_body
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Livestream:
|
|
7
|
+
def __init__(self, client):
|
|
8
|
+
self.client = client
|
|
9
|
+
|
|
10
|
+
async def create(self, data):
|
|
11
|
+
"""Create a live stream."""
|
|
12
|
+
|
|
13
|
+
validate_request_body(data) # validating request body is valid json or not
|
|
14
|
+
|
|
15
|
+
try:
|
|
16
|
+
return await make_request(
|
|
17
|
+
method="POST",
|
|
18
|
+
endpoint="/live/streams",
|
|
19
|
+
headers=self.client.headers,
|
|
20
|
+
data=data
|
|
21
|
+
)
|
|
22
|
+
except APIError as e:
|
|
23
|
+
return e.to_dict()
|
|
24
|
+
|
|
25
|
+
async def list(self, params):
|
|
26
|
+
"""Retrieve all live streams."""
|
|
27
|
+
try:
|
|
28
|
+
return await make_request(
|
|
29
|
+
method="GET",
|
|
30
|
+
endpoint="/live/streams",
|
|
31
|
+
headers=self.client.headers,
|
|
32
|
+
params=params
|
|
33
|
+
)
|
|
34
|
+
except APIError as e:
|
|
35
|
+
return e.to_dict()
|
|
36
|
+
|
|
37
|
+
async def get(self, stream_id):
|
|
38
|
+
"""Retrieve a specific live stream by ID."""
|
|
39
|
+
|
|
40
|
+
validate_uuid(stream_id) # validating stream_id
|
|
41
|
+
|
|
42
|
+
try:
|
|
43
|
+
return await make_request(
|
|
44
|
+
method="GET",
|
|
45
|
+
endpoint=f"/live/streams/{stream_id}",
|
|
46
|
+
headers=self.client.headers
|
|
47
|
+
)
|
|
48
|
+
except APIError as e:
|
|
49
|
+
return e.to_dict()
|
|
50
|
+
|
|
51
|
+
async def update(self, stream_id, data):
|
|
52
|
+
"""Update a specific live stream."""
|
|
53
|
+
if not stream_id:
|
|
54
|
+
raise ValueError("Stream ID must be provided.")
|
|
55
|
+
|
|
56
|
+
try:
|
|
57
|
+
return await make_request(
|
|
58
|
+
method="PATCH",
|
|
59
|
+
endpoint=f"/live/streams/{stream_id}",
|
|
60
|
+
headers=self.client.headers,
|
|
61
|
+
data=data
|
|
62
|
+
)
|
|
63
|
+
except APIError as e:
|
|
64
|
+
return e.to_dict()
|
|
65
|
+
|
|
66
|
+
async def delete(self, stream_id):
|
|
67
|
+
"""Delete a specific live stream."""
|
|
68
|
+
|
|
69
|
+
validate_uuid(stream_id) # validating stream_id
|
|
70
|
+
|
|
71
|
+
try:
|
|
72
|
+
return await make_request(
|
|
73
|
+
method="DELETE",
|
|
74
|
+
endpoint=f"/live/streams/{stream_id}",
|
|
75
|
+
headers=self.client.headers
|
|
76
|
+
)
|
|
77
|
+
except APIError as e:
|
|
78
|
+
return e.to_dict()
|
|
79
|
+
|
|
80
|
+
async def create_simulcast(self, stream_id, data):
|
|
81
|
+
"""Create a simulcast for a live stream."""
|
|
82
|
+
|
|
83
|
+
validate_uuid(stream_id) # validating stream_id
|
|
84
|
+
validate_request_body(data) # validating request body as valid json body or not
|
|
85
|
+
|
|
86
|
+
try:
|
|
87
|
+
return await make_request(
|
|
88
|
+
method="POST",
|
|
89
|
+
endpoint=f"/live/streams/{stream_id}/simulcast",
|
|
90
|
+
headers=self.client.headers,
|
|
91
|
+
data=data
|
|
92
|
+
)
|
|
93
|
+
except APIError as e:
|
|
94
|
+
return e.to_dict()
|
|
95
|
+
|
|
96
|
+
async def get_simulcast(self, stream_id, simulcast_id):
|
|
97
|
+
"""Retrieve a specific simulcast."""
|
|
98
|
+
|
|
99
|
+
if simulcast_id is None:
|
|
100
|
+
raise ValueError("Simulcast ID must be provided.")
|
|
101
|
+
|
|
102
|
+
validate_uuid(stream_id) # validating stream_id
|
|
103
|
+
|
|
104
|
+
try:
|
|
105
|
+
return await make_request(
|
|
106
|
+
method="GET",
|
|
107
|
+
endpoint=f"/live/streams/{stream_id}/simulcast/{simulcast_id}",
|
|
108
|
+
headers=self.client.headers
|
|
109
|
+
)
|
|
110
|
+
except APIError as e:
|
|
111
|
+
return e.to_dict()
|
|
112
|
+
|
|
113
|
+
async def update_simulcast(self, stream_id, simulcast_id, data):
|
|
114
|
+
"""Update a specific simulcast."""
|
|
115
|
+
|
|
116
|
+
if simulcast_id is None:
|
|
117
|
+
raise ValueError("Simulcast ID must be provided.")
|
|
118
|
+
|
|
119
|
+
validate_uuid(stream_id) # validating stream_id
|
|
120
|
+
validate_request_body(data) # validating request body as valid json body or not
|
|
121
|
+
|
|
122
|
+
try:
|
|
123
|
+
return await make_request(
|
|
124
|
+
method="PUT",
|
|
125
|
+
endpoint=f"/live/streams/{stream_id}/simulcast/{simulcast_id}",
|
|
126
|
+
headers=self.client.headers,
|
|
127
|
+
data=data
|
|
128
|
+
)
|
|
129
|
+
except APIError as e:
|
|
130
|
+
return e.to_dict()
|
|
131
|
+
|
|
132
|
+
async def delete_simulcast(self, stream_id, simulcast_id):
|
|
133
|
+
"""Delete a specific simulcast."""
|
|
134
|
+
|
|
135
|
+
if simulcast_id is None:
|
|
136
|
+
raise ValueError("Simulcast ID must be provided.")
|
|
137
|
+
|
|
138
|
+
validate_uuid(stream_id) # validating stream_id
|
|
139
|
+
|
|
140
|
+
try:
|
|
141
|
+
return await make_request(
|
|
142
|
+
method="DELETE",
|
|
143
|
+
endpoint=f"/live/streams/{stream_id}/simulcast/{simulcast_id}",
|
|
144
|
+
headers=self.client.headers
|
|
145
|
+
)
|
|
146
|
+
except APIError as e:
|
|
147
|
+
return e.to_dict()
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
from fastpix.utilis.exceptions import APIError
|
|
2
|
+
from fastpix.async_client.api import make_request
|
|
3
|
+
from fastpix.utilis.utilis import validate_uuid, validate_request_body
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class MediaResource:
|
|
7
|
+
def __init__(self, client):
|
|
8
|
+
self.client = client
|
|
9
|
+
|
|
10
|
+
async def get_all_media(self, params):
|
|
11
|
+
"""Fetch all medias."""
|
|
12
|
+
try:
|
|
13
|
+
return await make_request(
|
|
14
|
+
method="GET",
|
|
15
|
+
endpoint="/on-demand",
|
|
16
|
+
headers=self.client.headers,
|
|
17
|
+
params=params
|
|
18
|
+
)
|
|
19
|
+
except APIError as e:
|
|
20
|
+
return e.to_dict()
|
|
21
|
+
|
|
22
|
+
async def get_by_mediaId(self, media_id):
|
|
23
|
+
"""Fetch media by its ID."""
|
|
24
|
+
|
|
25
|
+
validate_uuid(media_id) # validating media id
|
|
26
|
+
|
|
27
|
+
try:
|
|
28
|
+
return await make_request(
|
|
29
|
+
method="GET",
|
|
30
|
+
endpoint=f"/on-demand/{media_id}",
|
|
31
|
+
headers=self.client.headers
|
|
32
|
+
)
|
|
33
|
+
except APIError as e:
|
|
34
|
+
return e.to_dict()
|
|
35
|
+
|
|
36
|
+
async def update(self, media_id, data):
|
|
37
|
+
"""Update media by its ID."""
|
|
38
|
+
|
|
39
|
+
validate_uuid(media_id) # validating media id
|
|
40
|
+
validate_request_body(data) # validating request body is valid json or not
|
|
41
|
+
|
|
42
|
+
try:
|
|
43
|
+
return await make_request(
|
|
44
|
+
method="PATCH",
|
|
45
|
+
endpoint=f"/on-demand/{media_id}",
|
|
46
|
+
headers=self.client.headers,
|
|
47
|
+
data=data
|
|
48
|
+
)
|
|
49
|
+
except APIError as e:
|
|
50
|
+
return e.to_dict()
|
|
51
|
+
|
|
52
|
+
async def delete(self, media_id):
|
|
53
|
+
"""Delete media by its ID."""
|
|
54
|
+
|
|
55
|
+
validate_uuid(media_id) # validating media id
|
|
56
|
+
|
|
57
|
+
try:
|
|
58
|
+
return await make_request(
|
|
59
|
+
method="DELETE",
|
|
60
|
+
endpoint=f"/on-demand/{media_id}",
|
|
61
|
+
headers=self.client.headers
|
|
62
|
+
)
|
|
63
|
+
except APIError as e:
|
|
64
|
+
return e.to_dict()
|
|
65
|
+
|
|
66
|
+
async def create_pull_video(self, data):
|
|
67
|
+
"""Create an on-demand request by calling the /on-demand endpoint."""
|
|
68
|
+
endpoint = "/on-demand"
|
|
69
|
+
|
|
70
|
+
validate_request_body(data) # validating body
|
|
71
|
+
|
|
72
|
+
if "accessPolicy" not in data:
|
|
73
|
+
data["accessPolicy"] = "public"
|
|
74
|
+
|
|
75
|
+
try:
|
|
76
|
+
response = await make_request(
|
|
77
|
+
method="POST",
|
|
78
|
+
endpoint=endpoint,
|
|
79
|
+
headers=self.client.headers,
|
|
80
|
+
data=data
|
|
81
|
+
)
|
|
82
|
+
return response
|
|
83
|
+
except APIError as e:
|
|
84
|
+
return e.to_dict()
|
|
85
|
+
|
|
86
|
+
async def get_presigned_url(self, data):
|
|
87
|
+
"""Create an on-demand presigned URL request by calling the /on-demand/uploads endpoint."""
|
|
88
|
+
endpoint = "/on-demand/uploads"
|
|
89
|
+
|
|
90
|
+
validate_request_body(data) # validating body
|
|
91
|
+
|
|
92
|
+
if "corsOrigin" not in data:
|
|
93
|
+
data["corsOrigin"] = "*"
|
|
94
|
+
|
|
95
|
+
try:
|
|
96
|
+
response = await make_request(
|
|
97
|
+
method="POST",
|
|
98
|
+
endpoint=endpoint,
|
|
99
|
+
headers=self.client.headers,
|
|
100
|
+
data=data
|
|
101
|
+
)
|
|
102
|
+
return response
|
|
103
|
+
except APIError as e:
|
|
104
|
+
return e.to_dict()
|
|
105
|
+
|
|
106
|
+
async def get_media_info(self, media_id):
|
|
107
|
+
"""Retrieve media input info by media ID."""
|
|
108
|
+
endpoint = f"/on-demand/{media_id}/input-info"
|
|
109
|
+
|
|
110
|
+
validate_uuid(media_id) # validating media id
|
|
111
|
+
|
|
112
|
+
try:
|
|
113
|
+
response = await make_request(
|
|
114
|
+
method="GET",
|
|
115
|
+
endpoint=endpoint,
|
|
116
|
+
headers=self.client.headers
|
|
117
|
+
)
|
|
118
|
+
return response
|
|
119
|
+
except APIError as e:
|
|
120
|
+
return e.to_dict()
|
|
121
|
+
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
from fastpix.utilis.exceptions import APIError
|
|
2
|
+
from fastpix.async_client.api import make_request
|
|
3
|
+
from fastpix.utilis.utilis import validate_uuid, validate_request_body
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class PlaybackIDs:
|
|
7
|
+
def __init__(self, client):
|
|
8
|
+
self.client = client
|
|
9
|
+
|
|
10
|
+
def _get_endpoint(self, media_type, media_id):
|
|
11
|
+
"""Helper method to get the correct endpoint based on media type."""
|
|
12
|
+
if media_type == "livestream":
|
|
13
|
+
return f"/live/streams/{media_id}/playback-ids"
|
|
14
|
+
elif media_type == "video_on_demand":
|
|
15
|
+
return f"/on-demand/{media_id}/playback-ids"
|
|
16
|
+
else:
|
|
17
|
+
raise ValueError("Invalid media type. Must be 'livestream' or 'video_on_demand'.")
|
|
18
|
+
|
|
19
|
+
async def create(self, media_type, media_id, data):
|
|
20
|
+
"""Create a playback ID for a media resource (livestream or video on demand)."""
|
|
21
|
+
|
|
22
|
+
# Validate media_id
|
|
23
|
+
validate_uuid(media_id) # Ensure valid UUID format
|
|
24
|
+
|
|
25
|
+
# Validate request body (data)
|
|
26
|
+
validate_request_body(data) # Ensure the request body is valid JSON
|
|
27
|
+
|
|
28
|
+
try:
|
|
29
|
+
# Get the correct endpoint based on media type
|
|
30
|
+
endpoint = self._get_endpoint(media_type, media_id)
|
|
31
|
+
|
|
32
|
+
return await make_request(
|
|
33
|
+
method="POST",
|
|
34
|
+
endpoint=endpoint,
|
|
35
|
+
headers=self.client.headers,
|
|
36
|
+
data=data
|
|
37
|
+
)
|
|
38
|
+
except APIError as e:
|
|
39
|
+
return e.to_dict()
|
|
40
|
+
|
|
41
|
+
async def delete(self, media_type, media_id, playback_ids):
|
|
42
|
+
"""Delete a specific playback ID for a media resource (livestream or video on demand)."""
|
|
43
|
+
|
|
44
|
+
# Validate media_id
|
|
45
|
+
validate_uuid(media_id) # Ensure valid UUID format
|
|
46
|
+
|
|
47
|
+
try:
|
|
48
|
+
# Get the correct endpoint based on media type
|
|
49
|
+
endpoint = self._get_endpoint(media_type, media_id)
|
|
50
|
+
|
|
51
|
+
return await make_request(
|
|
52
|
+
method="DELETE",
|
|
53
|
+
endpoint=endpoint,
|
|
54
|
+
headers=self.client.headers,
|
|
55
|
+
params={'playbackId': playback_ids}
|
|
56
|
+
)
|
|
57
|
+
except APIError as e:
|
|
58
|
+
return e.to_dict()
|
|
59
|
+
|
|
60
|
+
async def get(self, media_type, media_id, playback_id):
|
|
61
|
+
"""Retrieve a specific playback ID for a media resource (livestream or video on demand)."""
|
|
62
|
+
|
|
63
|
+
# Validate media_id
|
|
64
|
+
validate_uuid(media_id) # Ensure valid UUID format
|
|
65
|
+
|
|
66
|
+
if media_type == "video_on_demand":
|
|
67
|
+
return {"error": "The 'get' method is not available for video_on_demand."}
|
|
68
|
+
|
|
69
|
+
try:
|
|
70
|
+
# Get the correct endpoint based on media type
|
|
71
|
+
endpoint = self._get_endpoint(media_type, media_id) + f"/{playback_id}"
|
|
72
|
+
|
|
73
|
+
return await make_request(
|
|
74
|
+
method="GET",
|
|
75
|
+
endpoint=endpoint,
|
|
76
|
+
headers=self.client.headers
|
|
77
|
+
)
|
|
78
|
+
except APIError as e:
|
|
79
|
+
return e.to_dict()
|
fastpix/client/api.py
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import requests
|
|
2
|
+
from fastpix.utilis.exceptions import APIError
|
|
3
|
+
from fastpix.utilis.constants import BASE_URL
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def make_request(method, endpoint, headers=None, data=None, params=None):
|
|
7
|
+
url = f"{BASE_URL}{endpoint}"
|
|
8
|
+
headers = headers or {}
|
|
9
|
+
|
|
10
|
+
try:
|
|
11
|
+
response = requests.request(
|
|
12
|
+
method=method,
|
|
13
|
+
url=url,
|
|
14
|
+
headers=headers,
|
|
15
|
+
json=data,
|
|
16
|
+
params=params
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
if response.ok:
|
|
20
|
+
return response.json()
|
|
21
|
+
else:
|
|
22
|
+
raise APIError(
|
|
23
|
+
message=f"Request failed with status {response.status_code}",
|
|
24
|
+
status_code=response.status_code,
|
|
25
|
+
details=response.json() if response.headers.get("content-type") == "application/json" else response.text
|
|
26
|
+
)
|
|
27
|
+
except requests.RequestException as e:
|
|
28
|
+
raise APIError(f"Request failed: {str(e)}")
|
fastpix/client/client.py
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
from fastpix.utilis.exceptions import APIError
|
|
3
|
+
from fastpix.client.media import MediaResource
|
|
4
|
+
from fastpix.client.playback_ids import PlaybackIDs
|
|
5
|
+
from fastpix.client.live_streams import Livestream
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Client:
|
|
9
|
+
def __init__(self, username=None, password=None, api_key=None):
|
|
10
|
+
"""
|
|
11
|
+
Initialize the client with flexible authentication.
|
|
12
|
+
|
|
13
|
+
Args:
|
|
14
|
+
username (str, optional): Username for authentication
|
|
15
|
+
password (str, optional): Password for authentication
|
|
16
|
+
api_key (str, optional): Pre-generated base64 encoded API key
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
# Validate and prepare API key
|
|
20
|
+
if api_key:
|
|
21
|
+
self.api_key = api_key
|
|
22
|
+
elif username and password:
|
|
23
|
+
|
|
24
|
+
# Encode credentials to base64
|
|
25
|
+
credentials = f"{username}:{password}"
|
|
26
|
+
self.api_key = base64.b64encode(credentials.encode('utf-8')).decode('utf-8')
|
|
27
|
+
else:
|
|
28
|
+
raise ValueError("Must provide either username and password or a pre-generated API key")
|
|
29
|
+
|
|
30
|
+
# Prepare headers
|
|
31
|
+
self.headers = {
|
|
32
|
+
"accept": "application/json",
|
|
33
|
+
"content-type": "application/json",
|
|
34
|
+
"Authorization": f"Basic {self.api_key}",
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
# Initialize resources
|
|
38
|
+
self.media = MediaResource(self)
|
|
39
|
+
self.playback_ids = PlaybackIDs(self)
|
|
40
|
+
self.livestreams = Livestream(self)
|
|
41
|
+
|
|
42
|
+
# Validate credentials
|
|
43
|
+
self._validate_credentials()
|
|
44
|
+
|
|
45
|
+
def _validate_credentials(self):
|
|
46
|
+
"""
|
|
47
|
+
Validate credentials by attempting to fetch media.
|
|
48
|
+
Raises an APIError if authentication fails.
|
|
49
|
+
"""
|
|
50
|
+
try:
|
|
51
|
+
# Attempt to fetch media to verify credentials
|
|
52
|
+
response = self.media.get_all_media()
|
|
53
|
+
return response
|
|
54
|
+
|
|
55
|
+
except APIError as e:
|
|
56
|
+
raise APIError(f"Authentication failed: {str(e)}") from e
|
|
57
|
+
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
from fastpix.utilis.exceptions import APIError
|
|
2
|
+
from fastpix.client.api import make_request
|
|
3
|
+
from fastpix.utilis.utilis import validate_uuid, validate_request_body
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Livestream:
|
|
7
|
+
def __init__(self, client):
|
|
8
|
+
self.client = client
|
|
9
|
+
|
|
10
|
+
def create(self, data):
|
|
11
|
+
"""Create a live stream."""
|
|
12
|
+
|
|
13
|
+
validate_request_body(data) # validating request body is valid json or not
|
|
14
|
+
|
|
15
|
+
try:
|
|
16
|
+
return make_request(
|
|
17
|
+
method="POST",
|
|
18
|
+
endpoint="/live/streams",
|
|
19
|
+
headers=self.client.headers,
|
|
20
|
+
data=data
|
|
21
|
+
)
|
|
22
|
+
except APIError as e:
|
|
23
|
+
return e.to_dict()
|
|
24
|
+
|
|
25
|
+
def list(self, params=None):
|
|
26
|
+
"""Retrieve all live streams."""
|
|
27
|
+
|
|
28
|
+
try:
|
|
29
|
+
return make_request(
|
|
30
|
+
method="GET",
|
|
31
|
+
endpoint="/live/streams",
|
|
32
|
+
headers=self.client.headers,
|
|
33
|
+
params=params
|
|
34
|
+
)
|
|
35
|
+
except APIError as e:
|
|
36
|
+
return e.to_dict()
|
|
37
|
+
|
|
38
|
+
def get(self, stream_id):
|
|
39
|
+
"""Retrieve a specific live stream by ID."""
|
|
40
|
+
|
|
41
|
+
validate_uuid(stream_id) # validating stream_id
|
|
42
|
+
|
|
43
|
+
try:
|
|
44
|
+
return make_request(
|
|
45
|
+
method="GET",
|
|
46
|
+
endpoint=f"/live/streams/{stream_id}",
|
|
47
|
+
headers=self.client.headers
|
|
48
|
+
)
|
|
49
|
+
except APIError as e:
|
|
50
|
+
return e.to_dict()
|
|
51
|
+
|
|
52
|
+
def update(self, stream_id, data):
|
|
53
|
+
"""Update a specific live stream."""
|
|
54
|
+
|
|
55
|
+
validate_uuid(stream_id) # validating stream_id
|
|
56
|
+
validate_request_body(data) # validating request body is valid json or not
|
|
57
|
+
|
|
58
|
+
try:
|
|
59
|
+
return make_request(
|
|
60
|
+
method="PATCH",
|
|
61
|
+
endpoint=f"/live/streams/{stream_id}",
|
|
62
|
+
headers=self.client.headers,
|
|
63
|
+
data=data
|
|
64
|
+
)
|
|
65
|
+
except APIError as e:
|
|
66
|
+
return e.to_dict()
|
|
67
|
+
|
|
68
|
+
def delete(self, stream_id):
|
|
69
|
+
"""Delete a specific live stream."""
|
|
70
|
+
|
|
71
|
+
validate_uuid(stream_id) # validating stream_id
|
|
72
|
+
|
|
73
|
+
try:
|
|
74
|
+
return make_request(
|
|
75
|
+
method="DELETE",
|
|
76
|
+
endpoint=f"/live/streams/{stream_id}",
|
|
77
|
+
headers=self.client.headers
|
|
78
|
+
)
|
|
79
|
+
except APIError as e:
|
|
80
|
+
return e.to_dict()
|
|
81
|
+
|
|
82
|
+
def create_simulcast(self, stream_id, data):
|
|
83
|
+
"""Create a simulcast for a live stream."""
|
|
84
|
+
|
|
85
|
+
validate_uuid(stream_id) # validating stream_id
|
|
86
|
+
validate_request_body(data) # validating request body as valid json body or not
|
|
87
|
+
|
|
88
|
+
try:
|
|
89
|
+
return make_request(
|
|
90
|
+
method="POST",
|
|
91
|
+
endpoint=f"/live/streams/{stream_id}/simulcast",
|
|
92
|
+
headers=self.client.headers,
|
|
93
|
+
data=data
|
|
94
|
+
)
|
|
95
|
+
except APIError as e:
|
|
96
|
+
return e.to_dict()
|
|
97
|
+
|
|
98
|
+
def get_simulcast(self, stream_id, simulcast_id):
|
|
99
|
+
"""Retrieve a specific simulcast."""
|
|
100
|
+
|
|
101
|
+
if simulcast_id is None:
|
|
102
|
+
raise ValueError("Simulcast ID must be provided.")
|
|
103
|
+
|
|
104
|
+
validate_uuid(stream_id) # validating stream_id
|
|
105
|
+
|
|
106
|
+
try:
|
|
107
|
+
return make_request(
|
|
108
|
+
method="GET",
|
|
109
|
+
endpoint=f"/live/streams/{stream_id}/simulcast/{simulcast_id}",
|
|
110
|
+
headers=self.client.headers
|
|
111
|
+
)
|
|
112
|
+
except APIError as e:
|
|
113
|
+
return e.to_dict()
|
|
114
|
+
|
|
115
|
+
def update_simulcast(self, stream_id, simulcast_id, data):
|
|
116
|
+
"""Update a specific simulcast."""
|
|
117
|
+
|
|
118
|
+
if simulcast_id is None:
|
|
119
|
+
raise ValueError("Simulcast ID must be provided.")
|
|
120
|
+
|
|
121
|
+
validate_uuid(stream_id) # validating stream_id
|
|
122
|
+
validate_request_body(data) # validating request body as valid json body or not
|
|
123
|
+
|
|
124
|
+
try:
|
|
125
|
+
return make_request(
|
|
126
|
+
method="PUT",
|
|
127
|
+
endpoint=f"/live/streams/{stream_id}/simulcast/{simulcast_id}",
|
|
128
|
+
headers=self.client.headers,
|
|
129
|
+
data=data
|
|
130
|
+
)
|
|
131
|
+
except APIError as e:
|
|
132
|
+
return e.to_dict()
|
|
133
|
+
|
|
134
|
+
def delete_simulcast(self, stream_id, simulcast_id):
|
|
135
|
+
"""Delete a specific simulcast."""
|
|
136
|
+
|
|
137
|
+
if simulcast_id is None:
|
|
138
|
+
raise ValueError("Simulcast ID must be provided.")
|
|
139
|
+
|
|
140
|
+
validate_uuid(stream_id) # validating stream_id
|
|
141
|
+
|
|
142
|
+
try:
|
|
143
|
+
return make_request(
|
|
144
|
+
method="DELETE",
|
|
145
|
+
endpoint=f"/live/streams/{stream_id}/simulcast/{simulcast_id}",
|
|
146
|
+
headers=self.client.headers
|
|
147
|
+
)
|
|
148
|
+
except APIError as e:
|
|
149
|
+
return e.to_dict()
|
|
150
|
+
|
fastpix/client/media.py
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
from fastpix.utilis.exceptions import APIError
|
|
2
|
+
from fastpix.client.api import make_request
|
|
3
|
+
from fastpix.utilis.utilis import validate_uuid, validate_request_body
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class MediaResource:
|
|
7
|
+
def __init__(self, client):
|
|
8
|
+
self.client = client
|
|
9
|
+
|
|
10
|
+
def get_all_media(self, params):
|
|
11
|
+
"""Fetch all medias."""
|
|
12
|
+
try:
|
|
13
|
+
return make_request(
|
|
14
|
+
method="GET",
|
|
15
|
+
endpoint="/on-demand",
|
|
16
|
+
headers=self.client.headers,
|
|
17
|
+
params=params
|
|
18
|
+
)
|
|
19
|
+
except APIError as e:
|
|
20
|
+
return e.to_dict()
|
|
21
|
+
|
|
22
|
+
def get_by_mediaId(self, media_id):
|
|
23
|
+
"""Fetch media by its ID."""
|
|
24
|
+
validate_uuid(media_id) # Validate media ID
|
|
25
|
+
try:
|
|
26
|
+
return make_request(
|
|
27
|
+
method="GET",
|
|
28
|
+
endpoint=f"/on-demand/{media_id}",
|
|
29
|
+
headers=self.client.headers
|
|
30
|
+
)
|
|
31
|
+
except APIError as e:
|
|
32
|
+
return e.to_dict()
|
|
33
|
+
|
|
34
|
+
def update(self, media_id, data):
|
|
35
|
+
"""Update media by its ID."""
|
|
36
|
+
validate_uuid(media_id) # Validate media ID
|
|
37
|
+
validate_request_body(data) # Validate request body
|
|
38
|
+
|
|
39
|
+
try:
|
|
40
|
+
return make_request(
|
|
41
|
+
method="PATCH",
|
|
42
|
+
endpoint=f"/on-demand/{media_id}",
|
|
43
|
+
headers=self.client.headers,
|
|
44
|
+
data=data
|
|
45
|
+
)
|
|
46
|
+
except APIError as e:
|
|
47
|
+
return e.to_dict()
|
|
48
|
+
|
|
49
|
+
def delete(self, media_id):
|
|
50
|
+
"""Delete media by its ID."""
|
|
51
|
+
validate_uuid(media_id) # Validate media ID
|
|
52
|
+
|
|
53
|
+
try:
|
|
54
|
+
return make_request(
|
|
55
|
+
method="DELETE",
|
|
56
|
+
endpoint=f"/on-demand/{media_id}",
|
|
57
|
+
headers=self.client.headers
|
|
58
|
+
)
|
|
59
|
+
except APIError as e:
|
|
60
|
+
return e.to_dict()
|
|
61
|
+
|
|
62
|
+
def create_pull_video(self, data):
|
|
63
|
+
"""Create an on-demand request by calling the /on-demand endpoint."""
|
|
64
|
+
endpoint = "/on-demand"
|
|
65
|
+
validate_request_body(data) # Validate body
|
|
66
|
+
|
|
67
|
+
if "accessPolicy" not in data:
|
|
68
|
+
data["accessPolicy"] = "public"
|
|
69
|
+
|
|
70
|
+
try:
|
|
71
|
+
return make_request(
|
|
72
|
+
method="POST",
|
|
73
|
+
endpoint=endpoint,
|
|
74
|
+
headers=self.client.headers,
|
|
75
|
+
data=data
|
|
76
|
+
)
|
|
77
|
+
except APIError as e:
|
|
78
|
+
return e.to_dict()
|
|
79
|
+
|
|
80
|
+
def get_presigned_url(self, data):
|
|
81
|
+
"""Create an on-demand presigned URL request by calling the /on-demand/uploads endpoint."""
|
|
82
|
+
endpoint = "/on-demand/uploads"
|
|
83
|
+
validate_request_body(data) # Validate body
|
|
84
|
+
|
|
85
|
+
if "corsOrigin" not in data:
|
|
86
|
+
data["corsOrigin"] = "*"
|
|
87
|
+
|
|
88
|
+
try:
|
|
89
|
+
return make_request(
|
|
90
|
+
method="POST",
|
|
91
|
+
endpoint=endpoint,
|
|
92
|
+
headers=self.client.headers,
|
|
93
|
+
data=data
|
|
94
|
+
)
|
|
95
|
+
except APIError as e:
|
|
96
|
+
return e.to_dict()
|
|
97
|
+
|
|
98
|
+
def get_media_info(self, media_id):
|
|
99
|
+
"""Retrieve media input info by media ID."""
|
|
100
|
+
validate_uuid(media_id) # Validate media ID
|
|
101
|
+
|
|
102
|
+
try:
|
|
103
|
+
endpoint = f"/on-demand/{media_id}/input-info"
|
|
104
|
+
return make_request(
|
|
105
|
+
method="GET",
|
|
106
|
+
endpoint=endpoint,
|
|
107
|
+
headers=self.client.headers
|
|
108
|
+
)
|
|
109
|
+
except APIError as e:
|
|
110
|
+
return e.to_dict()
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
from fastpix.utilis.exceptions import APIError
|
|
2
|
+
from fastpix.client.api import make_request
|
|
3
|
+
from fastpix.utilis.utilis import validate_uuid, validate_request_body
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class PlaybackIDs:
|
|
7
|
+
def __init__(self, client):
|
|
8
|
+
self.client = client
|
|
9
|
+
|
|
10
|
+
def _get_endpoint(self, media_type, media_id):
|
|
11
|
+
"""Helper method to get the correct endpoint based on media type."""
|
|
12
|
+
if media_type == "livestream":
|
|
13
|
+
return f"/live/streams/{media_id}/playback-ids"
|
|
14
|
+
elif media_type == "video_on_demand":
|
|
15
|
+
return f"/on-demand/{media_id}/playback-ids"
|
|
16
|
+
else:
|
|
17
|
+
raise ValueError("Invalid media type. Must be 'livestream' or 'video_on_demand'.")
|
|
18
|
+
|
|
19
|
+
def create(self, media_type, media_id, data):
|
|
20
|
+
"""Create a playback ID for a media resource (livestream or video on demand)."""
|
|
21
|
+
|
|
22
|
+
# Validate media_id
|
|
23
|
+
validate_uuid(media_id) # Ensure valid UUID format
|
|
24
|
+
|
|
25
|
+
# Validate request body (data)
|
|
26
|
+
validate_request_body(data) # Ensure the request body is valid JSON
|
|
27
|
+
|
|
28
|
+
try:
|
|
29
|
+
# Get the correct endpoint based on media type
|
|
30
|
+
endpoint = self._get_endpoint(media_type, media_id)
|
|
31
|
+
|
|
32
|
+
return make_request(
|
|
33
|
+
method="POST",
|
|
34
|
+
endpoint=endpoint,
|
|
35
|
+
headers=self.client.headers,
|
|
36
|
+
data=data
|
|
37
|
+
)
|
|
38
|
+
except APIError as e:
|
|
39
|
+
return e.to_dict()
|
|
40
|
+
|
|
41
|
+
def delete(self, media_type, media_id, playback_ids):
|
|
42
|
+
"""Delete a specific playback ID for a media resource (livestream or video on demand)."""
|
|
43
|
+
|
|
44
|
+
# Validate media_id
|
|
45
|
+
validate_uuid(media_id) # Ensure valid UUID format
|
|
46
|
+
|
|
47
|
+
try:
|
|
48
|
+
# Get the correct endpoint based on media type
|
|
49
|
+
endpoint = self._get_endpoint(media_type, media_id)
|
|
50
|
+
|
|
51
|
+
return make_request(
|
|
52
|
+
method="DELETE",
|
|
53
|
+
endpoint=endpoint,
|
|
54
|
+
headers=self.client.headers,
|
|
55
|
+
params={'playbackId': playback_ids}
|
|
56
|
+
)
|
|
57
|
+
except APIError as e:
|
|
58
|
+
return e.to_dict()
|
|
59
|
+
|
|
60
|
+
def get(self, media_type, media_id, playback_id):
|
|
61
|
+
"""Retrieve a specific playback ID for a media resource (livestream or video on demand)."""
|
|
62
|
+
|
|
63
|
+
# Validate media_id
|
|
64
|
+
validate_uuid(media_id) # Ensure valid UUID format
|
|
65
|
+
|
|
66
|
+
if media_type == "video_on_demand":
|
|
67
|
+
return {"error": "The 'get' method is not available for video_on_demand."}
|
|
68
|
+
|
|
69
|
+
try:
|
|
70
|
+
# Get the correct endpoint based on media type
|
|
71
|
+
endpoint = self._get_endpoint(media_type, media_id) + f"/{playback_id}"
|
|
72
|
+
|
|
73
|
+
return make_request(
|
|
74
|
+
method="GET",
|
|
75
|
+
endpoint=endpoint,
|
|
76
|
+
headers=self.client.headers
|
|
77
|
+
)
|
|
78
|
+
except APIError as e:
|
|
79
|
+
return e.to_dict()
|
|
80
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
BASE_URL = "https://v1.fastpix.io"
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
class APIError(Exception):
|
|
2
|
+
"""Custom exception for API-related errors."""
|
|
3
|
+
def __init__(self, message, status_code=None, details=None):
|
|
4
|
+
super().__init__(message)
|
|
5
|
+
self.message = message
|
|
6
|
+
self.status_code = status_code
|
|
7
|
+
self.details = details
|
|
8
|
+
|
|
9
|
+
def to_dict(self):
|
|
10
|
+
return {
|
|
11
|
+
"error": self.message,
|
|
12
|
+
"status_code": self.status_code,
|
|
13
|
+
"details": self.details,
|
|
14
|
+
}
|
fastpix/utilis/utilis.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import uuid
|
|
2
|
+
import json
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def validate_uuid(id_str):
|
|
6
|
+
try:
|
|
7
|
+
uuid.UUID(str(id_str))
|
|
8
|
+
except ValueError:
|
|
9
|
+
raise ValueError(f"Invalid UUID format: {id_str}")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def validate_request_body(data):
|
|
13
|
+
try:
|
|
14
|
+
json.dumps(data)
|
|
15
|
+
except (TypeError, ValueError):
|
|
16
|
+
raise ValueError("Invalid JSON data in request body")
|
|
17
|
+
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: fastpix-python
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: FastPix SDK with both sync and async support
|
|
5
|
+
Home-page: https://github.com/fastpix-io/fastpix-python-server-sdk
|
|
6
|
+
Author: FastPix
|
|
7
|
+
Author-email: dev@fastpix.io
|
|
8
|
+
Classifier: Development Status :: 3 - Alpha
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.7
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
15
|
+
Requires-Python: >=3.7
|
|
16
|
+
License-File: LICENSE
|
|
17
|
+
Requires-Dist: requests>=2.25.0
|
|
18
|
+
Provides-Extra: async
|
|
19
|
+
Requires-Dist: aiohttp>=3.8.0; extra == "async"
|
|
20
|
+
Dynamic: author
|
|
21
|
+
Dynamic: author-email
|
|
22
|
+
Dynamic: classifier
|
|
23
|
+
Dynamic: home-page
|
|
24
|
+
Dynamic: license-file
|
|
25
|
+
Dynamic: provides-extra
|
|
26
|
+
Dynamic: requires-dist
|
|
27
|
+
Dynamic: requires-python
|
|
28
|
+
Dynamic: summary
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
fastpix/__init__.py,sha256=loB8C7K059okqu9xAO32Jr9_wBF8kzZXIVjWWp_v9x8,140
|
|
2
|
+
fastpix/async_client/__init__.py,sha256=0brJ1RCLOd7cNl6cjtr96AqftUd66Owp2W1fkfo4Rbo,89
|
|
3
|
+
fastpix/async_client/api.py,sha256=jmALp5zny5FaqUm7RDmVXF9T0iFnNyCKtJpEQ0cxtiA,1144
|
|
4
|
+
fastpix/async_client/client.py,sha256=UggrQiehFSNBvF3dH0evq0DacVsvEooxRaAx-Wpr88g,1878
|
|
5
|
+
fastpix/async_client/live_streams.py,sha256=sx9_xtiR1FD6MGg_F3aq8NwjXHOwuVD1eBvp4NjGBvk,4765
|
|
6
|
+
fastpix/async_client/media.py,sha256=l4CMp61eEvX0v67jrcurZ2CAysvJQZbr4VRQDNOowtc,3704
|
|
7
|
+
fastpix/async_client/playback_ids.py,sha256=1pKQ9VNuOUzADZQiAlJPftRkdB3gS97srwz7QUB4WJY,2924
|
|
8
|
+
fastpix/client/__init__.py,sha256=3C-Vhg1RULZzdjI0kCnfNj_LzwOHSHO-di--gSA-gTQ,63
|
|
9
|
+
fastpix/client/api.py,sha256=WUN0Z7crvw7V3E5_0XzrTXfmLb4mFoDogj1ADoru4sk,890
|
|
10
|
+
fastpix/client/client.py,sha256=GiFPu3jq0cXrUw1iqmwvJRIXaZKC_HtgMlklsYXNUyY,1928
|
|
11
|
+
fastpix/client/live_streams.py,sha256=p_3r4PzipnTEYmd6KlZUi4kHSwiOkOvvJxT2QAVEav4,4706
|
|
12
|
+
fastpix/client/media.py,sha256=-VdOb5fm18G-f11DGnZD7dTjaBY9BeI2c1jebrUh8tE,3369
|
|
13
|
+
fastpix/client/playback_ids.py,sha256=2duK4WAjOIhL-nOBKHt1PpFbPSclUsMh5Upta1lWIcQ,2890
|
|
14
|
+
fastpix/utilis/__init__.py,sha256=-9bDUz5FeRXp-ZyeMJEap0q5EXvhIr-ljadB24_5yUo,142
|
|
15
|
+
fastpix/utilis/constants.py,sha256=s4jcq1qk7N5-t4wcZE4aIutzsIoHx8pattbg4NBIAnw,35
|
|
16
|
+
fastpix/utilis/exceptions.py,sha256=fctNkUrSg2VkukwVdTGa1j8v2VMcUM4rPna76VHIe3o,454
|
|
17
|
+
fastpix/utilis/utilis.py,sha256=3JIwmN-fuDc8JQ1jpzPLom-vQI3aXEuch0V9LactpxU,343
|
|
18
|
+
fastpix_python-0.1.0.dist-info/licenses/LICENSE,sha256=F9rynAolz0RQr0fiAQO0TSptdJq_Q-KPx0sguGiLdAI,1063
|
|
19
|
+
fastpix_python-0.1.0.dist-info/METADATA,sha256=_2990kEoq7jVBiEkt2fp0QqmP3SiqxA-XWMi8k9_dQY,896
|
|
20
|
+
fastpix_python-0.1.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
21
|
+
fastpix_python-0.1.0.dist-info/top_level.txt,sha256=SADFSkxaYaU-AHZY56jucIDG-iqlTMU3y3EL2FksnDM,8
|
|
22
|
+
fastpix_python-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 FastPix
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
fastpix
|