aimage-sdk 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.
- aimage_sdk/__init__.py +34 -0
- aimage_sdk/client.py +236 -0
- aimage_sdk/errors.py +12 -0
- aimage_sdk/types.py +84 -0
- aimage_sdk-0.1.0.dist-info/METADATA +90 -0
- aimage_sdk-0.1.0.dist-info/RECORD +8 -0
- aimage_sdk-0.1.0.dist-info/WHEEL +4 -0
- aimage_sdk-0.1.0.dist-info/licenses/LICENSE +21 -0
aimage_sdk/__init__.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""Python SDK for the AI-Mage Search public API."""
|
|
2
|
+
|
|
3
|
+
from .client import AimageClient, ProgressStage
|
|
4
|
+
from .errors import AimageApiError
|
|
5
|
+
from .types import (
|
|
6
|
+
Clip,
|
|
7
|
+
DeleteVideoResponse,
|
|
8
|
+
MeProject,
|
|
9
|
+
MeResponse,
|
|
10
|
+
PingResponse,
|
|
11
|
+
ProcessingStatus,
|
|
12
|
+
ProjectRole,
|
|
13
|
+
SearchClipsResponse,
|
|
14
|
+
UploadUrlResponse,
|
|
15
|
+
Video,
|
|
16
|
+
VideoListResponse,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
__all__ = [
|
|
20
|
+
"AimageApiError",
|
|
21
|
+
"AimageClient",
|
|
22
|
+
"Clip",
|
|
23
|
+
"DeleteVideoResponse",
|
|
24
|
+
"MeProject",
|
|
25
|
+
"MeResponse",
|
|
26
|
+
"PingResponse",
|
|
27
|
+
"ProcessingStatus",
|
|
28
|
+
"ProgressStage",
|
|
29
|
+
"ProjectRole",
|
|
30
|
+
"SearchClipsResponse",
|
|
31
|
+
"UploadUrlResponse",
|
|
32
|
+
"Video",
|
|
33
|
+
"VideoListResponse",
|
|
34
|
+
]
|
aimage_sdk/client.py
ADDED
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import mimetypes
|
|
2
|
+
import time
|
|
3
|
+
from collections.abc import Callable
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any, Literal, cast
|
|
6
|
+
|
|
7
|
+
import httpx
|
|
8
|
+
|
|
9
|
+
from .errors import AimageApiError
|
|
10
|
+
from .types import (
|
|
11
|
+
DeleteVideoResponse,
|
|
12
|
+
MeResponse,
|
|
13
|
+
PingResponse,
|
|
14
|
+
SearchClipsResponse,
|
|
15
|
+
UploadUrlResponse,
|
|
16
|
+
Video,
|
|
17
|
+
VideoListResponse,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
ProgressStage = Literal["minting_upload_url", "uploading", "registering", "done"]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class AimageClient:
|
|
24
|
+
"""Thin client for the AI-Mage Search public API (/v0), httpx-based.
|
|
25
|
+
|
|
26
|
+
``base_url`` is the API server origin, e.g. ``https://api.search.ai-mage.jp``;
|
|
27
|
+
the client calls ``{base_url}/v0/...``. ``transport`` exists for testing
|
|
28
|
+
with ``httpx.MockTransport``.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
def __init__(
|
|
32
|
+
self,
|
|
33
|
+
api_key: str,
|
|
34
|
+
base_url: str,
|
|
35
|
+
*,
|
|
36
|
+
timeout: float = 30.0,
|
|
37
|
+
transport: httpx.BaseTransport | None = None,
|
|
38
|
+
) -> None:
|
|
39
|
+
self._api_key = api_key
|
|
40
|
+
self._base_url = base_url.rstrip("/")
|
|
41
|
+
# No default Authorization header: presigned S3 PUTs go through the
|
|
42
|
+
# same client and S3 rejects requests carrying both auth mechanisms.
|
|
43
|
+
self._client = httpx.Client(timeout=timeout, transport=transport)
|
|
44
|
+
|
|
45
|
+
def close(self) -> None:
|
|
46
|
+
self._client.close()
|
|
47
|
+
|
|
48
|
+
def __enter__(self) -> "AimageClient":
|
|
49
|
+
return self
|
|
50
|
+
|
|
51
|
+
def __exit__(self, *exc_info: object) -> None:
|
|
52
|
+
self.close()
|
|
53
|
+
|
|
54
|
+
# -- thin endpoint wrappers ------------------------------------------------
|
|
55
|
+
|
|
56
|
+
def ping(self) -> PingResponse:
|
|
57
|
+
return cast(PingResponse, self._request("GET", "/ping"))
|
|
58
|
+
|
|
59
|
+
def me(self) -> MeResponse:
|
|
60
|
+
return cast(MeResponse, self._request("GET", "/me"))
|
|
61
|
+
|
|
62
|
+
def mint_upload_url(
|
|
63
|
+
self, *, project_id: str, file_name: str, content_type: str
|
|
64
|
+
) -> UploadUrlResponse:
|
|
65
|
+
body = {"project_id": project_id, "file_name": file_name, "content_type": content_type}
|
|
66
|
+
return cast(UploadUrlResponse, self._request("POST", "/videos/upload-url", json=body))
|
|
67
|
+
|
|
68
|
+
def register_video(
|
|
69
|
+
self,
|
|
70
|
+
*,
|
|
71
|
+
project_id: str,
|
|
72
|
+
name: str,
|
|
73
|
+
file_name: str | None = None,
|
|
74
|
+
s3_key: str | None = None,
|
|
75
|
+
season: int | None = None,
|
|
76
|
+
episode: int | None = None,
|
|
77
|
+
auto_process: bool = True,
|
|
78
|
+
) -> Video:
|
|
79
|
+
if (file_name is None) == (s3_key is None):
|
|
80
|
+
raise ValueError("Provide exactly one of file_name or s3_key.")
|
|
81
|
+
body: dict[str, Any] = {
|
|
82
|
+
"project_id": project_id,
|
|
83
|
+
"name": name,
|
|
84
|
+
"auto_process": auto_process,
|
|
85
|
+
}
|
|
86
|
+
if file_name is not None:
|
|
87
|
+
body["file_name"] = file_name
|
|
88
|
+
if s3_key is not None:
|
|
89
|
+
body["s3_key"] = s3_key
|
|
90
|
+
if season is not None:
|
|
91
|
+
body["season"] = season
|
|
92
|
+
if episode is not None:
|
|
93
|
+
body["episode"] = episode
|
|
94
|
+
return cast(Video, self._request("POST", "/videos", json=body))
|
|
95
|
+
|
|
96
|
+
def get_video(self, video_id: str) -> Video:
|
|
97
|
+
return cast(Video, self._request("GET", f"/videos/{video_id}"))
|
|
98
|
+
|
|
99
|
+
def list_videos(
|
|
100
|
+
self, *, project_id: str, page: int = 1, page_size: int = 20
|
|
101
|
+
) -> VideoListResponse:
|
|
102
|
+
params = {"project_id": project_id, "page": page, "page_size": page_size}
|
|
103
|
+
return cast(VideoListResponse, self._request("GET", "/videos", params=params))
|
|
104
|
+
|
|
105
|
+
def delete_video(self, video_id: str) -> DeleteVideoResponse:
|
|
106
|
+
return cast(DeleteVideoResponse, self._request("DELETE", f"/videos/{video_id}"))
|
|
107
|
+
|
|
108
|
+
def search_clips(
|
|
109
|
+
self, *, project_id: str, query: str, page: int = 1, page_size: int = 20
|
|
110
|
+
) -> SearchClipsResponse:
|
|
111
|
+
body = {"project_id": project_id, "query": query, "page": page, "page_size": page_size}
|
|
112
|
+
return cast(SearchClipsResponse, self._request("POST", "/search/clips", json=body))
|
|
113
|
+
|
|
114
|
+
# -- high-level orchestration ----------------------------------------------
|
|
115
|
+
|
|
116
|
+
def upload_video(
|
|
117
|
+
self,
|
|
118
|
+
*,
|
|
119
|
+
project_id: str,
|
|
120
|
+
file: str | Path | bytes,
|
|
121
|
+
name: str,
|
|
122
|
+
file_name: str | None = None,
|
|
123
|
+
content_type: str | None = None,
|
|
124
|
+
season: int | None = None,
|
|
125
|
+
episode: int | None = None,
|
|
126
|
+
auto_process: bool = True,
|
|
127
|
+
on_progress: Callable[[ProgressStage], None] | None = None,
|
|
128
|
+
) -> Video:
|
|
129
|
+
"""Full upload orchestration: mint a presigned URL, PUT the file bytes
|
|
130
|
+
with the exact Content-Type, then register the video via its s3_key.
|
|
131
|
+
"""
|
|
132
|
+
if isinstance(file, bytes):
|
|
133
|
+
if file_name is None:
|
|
134
|
+
raise ValueError("file_name is required when uploading bytes.")
|
|
135
|
+
data = file
|
|
136
|
+
else:
|
|
137
|
+
path = Path(file)
|
|
138
|
+
file_name = file_name or path.name
|
|
139
|
+
data = path.read_bytes()
|
|
140
|
+
|
|
141
|
+
if content_type is None:
|
|
142
|
+
content_type, _ = mimetypes.guess_type(file_name)
|
|
143
|
+
if content_type is None:
|
|
144
|
+
raise ValueError(
|
|
145
|
+
f'Cannot infer content_type from file name "{file_name}". Pass it explicitly.'
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
if on_progress:
|
|
149
|
+
on_progress("minting_upload_url")
|
|
150
|
+
minted = self.mint_upload_url(
|
|
151
|
+
project_id=project_id, file_name=file_name, content_type=content_type
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
if on_progress:
|
|
155
|
+
on_progress("uploading")
|
|
156
|
+
upload_response = self._client.put(
|
|
157
|
+
minted["upload_url"], content=data, headers={"Content-Type": content_type}
|
|
158
|
+
)
|
|
159
|
+
if upload_response.is_error:
|
|
160
|
+
raise AimageApiError(
|
|
161
|
+
code="UPLOAD_FAILED",
|
|
162
|
+
message=f"Upload to presigned URL failed with status {upload_response.status_code}",
|
|
163
|
+
status=upload_response.status_code,
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
if on_progress:
|
|
167
|
+
on_progress("registering")
|
|
168
|
+
video = self.register_video(
|
|
169
|
+
project_id=project_id,
|
|
170
|
+
name=name,
|
|
171
|
+
s3_key=minted["s3_key"],
|
|
172
|
+
season=season,
|
|
173
|
+
episode=episode,
|
|
174
|
+
auto_process=auto_process,
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
if on_progress:
|
|
178
|
+
on_progress("done")
|
|
179
|
+
return video
|
|
180
|
+
|
|
181
|
+
def wait_for_processing(
|
|
182
|
+
self,
|
|
183
|
+
video_id: str,
|
|
184
|
+
*,
|
|
185
|
+
poll_interval_s: float = 5.0,
|
|
186
|
+
timeout_s: float = 600.0,
|
|
187
|
+
) -> Video:
|
|
188
|
+
"""Polls GET /videos/{id} until processing reaches a terminal state and
|
|
189
|
+
returns the final video — check processing_status ('completed' or
|
|
190
|
+
'failed') and processing_error yourself. Raises TimeoutError on timeout.
|
|
191
|
+
"""
|
|
192
|
+
deadline = time.monotonic() + timeout_s
|
|
193
|
+
while True:
|
|
194
|
+
video = self.get_video(video_id)
|
|
195
|
+
if video["processing_status"] in ("completed", "failed"):
|
|
196
|
+
return video
|
|
197
|
+
if time.monotonic() + poll_interval_s >= deadline:
|
|
198
|
+
raise TimeoutError(
|
|
199
|
+
f"Timed out waiting for video {video_id} to finish processing "
|
|
200
|
+
f"after {timeout_s}s"
|
|
201
|
+
)
|
|
202
|
+
time.sleep(poll_interval_s)
|
|
203
|
+
|
|
204
|
+
# -- internals ---------------------------------------------------------------
|
|
205
|
+
|
|
206
|
+
def _request(
|
|
207
|
+
self,
|
|
208
|
+
method: str,
|
|
209
|
+
path: str,
|
|
210
|
+
*,
|
|
211
|
+
params: dict[str, Any] | None = None,
|
|
212
|
+
json: dict[str, Any] | None = None,
|
|
213
|
+
) -> Any:
|
|
214
|
+
response = self._client.request(
|
|
215
|
+
method,
|
|
216
|
+
f"{self._base_url}/v0{path}",
|
|
217
|
+
params=params,
|
|
218
|
+
json=json,
|
|
219
|
+
headers={"Authorization": f"Bearer {self._api_key}"},
|
|
220
|
+
)
|
|
221
|
+
if response.is_error:
|
|
222
|
+
raise _to_api_error(response)
|
|
223
|
+
return response.json()
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def _to_api_error(response: httpx.Response) -> AimageApiError:
|
|
227
|
+
code = "UNKNOWN"
|
|
228
|
+
message = f"Request failed with status {response.status_code}"
|
|
229
|
+
try:
|
|
230
|
+
envelope = response.json().get("error")
|
|
231
|
+
if isinstance(envelope, dict):
|
|
232
|
+
code = str(envelope.get("code", code))
|
|
233
|
+
message = str(envelope.get("message", message))
|
|
234
|
+
except ValueError:
|
|
235
|
+
pass
|
|
236
|
+
return AimageApiError(code=code, message=message, status=response.status_code)
|
aimage_sdk/errors.py
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
class AimageApiError(Exception):
|
|
2
|
+
"""Raised when the AI-Mage Search API responds with a non-2xx status.
|
|
3
|
+
|
|
4
|
+
``code`` and ``message`` come from the API error envelope
|
|
5
|
+
``{"error": {"code", "message"}}``; ``status`` is the HTTP status code.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
def __init__(self, code: str, message: str, status: int) -> None:
|
|
9
|
+
super().__init__(message)
|
|
10
|
+
self.code = code
|
|
11
|
+
self.message = message
|
|
12
|
+
self.status = status
|
aimage_sdk/types.py
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""Wire-format types of the AI-Mage Search public API (/v0).
|
|
2
|
+
|
|
3
|
+
Field names are snake_case to match the HTTP payloads exactly; the source of
|
|
4
|
+
truth is packages/api-contract/src/schemas/public-api.ts in the monorepo.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Literal, TypedDict
|
|
8
|
+
|
|
9
|
+
ProcessingStatus = Literal["pending", "queued", "processing", "completed", "failed"]
|
|
10
|
+
|
|
11
|
+
ProjectRole = Literal["viewer", "admin"]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Video(TypedDict):
|
|
15
|
+
id: str
|
|
16
|
+
project_id: str
|
|
17
|
+
name: str
|
|
18
|
+
season: int | None
|
|
19
|
+
episode: int | None
|
|
20
|
+
duration: int
|
|
21
|
+
width: int
|
|
22
|
+
height: int
|
|
23
|
+
processing_status: ProcessingStatus
|
|
24
|
+
processing_error: str | None
|
|
25
|
+
created_at: str
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class Clip(TypedDict):
|
|
29
|
+
clip_id: str
|
|
30
|
+
video_id: str
|
|
31
|
+
project_id: str
|
|
32
|
+
start_time: float
|
|
33
|
+
end_time: float
|
|
34
|
+
subtitle: str | None
|
|
35
|
+
similarity: float
|
|
36
|
+
thumbnail_url: str
|
|
37
|
+
episode_title: str | None
|
|
38
|
+
season: int | None
|
|
39
|
+
episode: int | None
|
|
40
|
+
tags: list[str]
|
|
41
|
+
characters: list[str]
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class UploadUrlResponse(TypedDict):
|
|
45
|
+
upload_url: str
|
|
46
|
+
s3_key: str
|
|
47
|
+
expires_at: str
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class VideoListResponse(TypedDict):
|
|
51
|
+
videos: list[Video]
|
|
52
|
+
total: int
|
|
53
|
+
page: int
|
|
54
|
+
page_size: int
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class DeleteVideoResponse(TypedDict):
|
|
58
|
+
deleted: Literal[True]
|
|
59
|
+
id: str
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class SearchClipsResponse(TypedDict):
|
|
63
|
+
clips: list[Clip]
|
|
64
|
+
total_available: int
|
|
65
|
+
page: int
|
|
66
|
+
page_size: int
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class MeProject(TypedDict):
|
|
70
|
+
id: str
|
|
71
|
+
name: str
|
|
72
|
+
role: ProjectRole
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class MeResponse(TypedDict):
|
|
76
|
+
key_name: str
|
|
77
|
+
key_preview: str
|
|
78
|
+
projects: list[MeProject]
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class PingResponse(TypedDict):
|
|
82
|
+
status: Literal["ok"]
|
|
83
|
+
version: str
|
|
84
|
+
time: str
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: aimage-sdk
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python SDK for the AI-Mage Search public API
|
|
5
|
+
Project-URL: Documentation, https://api.search.ai-mage.jp/docs
|
|
6
|
+
Project-URL: Repository, https://github.com/aimagexyz/aimage-monorepo
|
|
7
|
+
Author: AI-Mage
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
14
|
+
Classifier: Typing :: Typed
|
|
15
|
+
Requires-Python: >=3.12
|
|
16
|
+
Requires-Dist: httpx<1,>=0.28
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
|
|
19
|
+
# aimage-sdk
|
|
20
|
+
|
|
21
|
+
Python SDK for the AI-Mage Search public API (`/v0`). Python 3.12+, `httpx`
|
|
22
|
+
is the only dependency.
|
|
23
|
+
|
|
24
|
+
## Install
|
|
25
|
+
|
|
26
|
+
The package is not yet published to PyPI. Install from the repository:
|
|
27
|
+
|
|
28
|
+
```sh
|
|
29
|
+
uv add "aimage-sdk @ file:///path/to/aimage-monorepo/sdks/python"
|
|
30
|
+
# or: pip install /path/to/aimage-monorepo/sdks/python
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Quickstart
|
|
34
|
+
|
|
35
|
+
```python
|
|
36
|
+
import os
|
|
37
|
+
|
|
38
|
+
from aimage_sdk import AimageClient
|
|
39
|
+
|
|
40
|
+
client = AimageClient(
|
|
41
|
+
api_key=os.environ["AIMAGE_API_KEY"], # "aimage_api_..."
|
|
42
|
+
base_url="https://api.search.ai-mage.jp", # API server origin; the client calls {base_url}/v0/...
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
# Discover accessible projects.
|
|
46
|
+
me = client.me()
|
|
47
|
+
project_id = me["projects"][0]["id"]
|
|
48
|
+
|
|
49
|
+
# Upload a video: mints a presigned URL, PUTs the bytes, registers the video.
|
|
50
|
+
video = client.upload_video(
|
|
51
|
+
project_id=project_id,
|
|
52
|
+
file="./episode-01.mp4", # str | pathlib.Path | bytes (bytes need file_name=)
|
|
53
|
+
name="Episode 1",
|
|
54
|
+
season=1,
|
|
55
|
+
episode=1,
|
|
56
|
+
on_progress=print,
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
# Wait until clip extraction and AI tagging finish.
|
|
60
|
+
processed = client.wait_for_processing(video["id"], poll_interval_s=10, timeout_s=1800)
|
|
61
|
+
if processed["processing_status"] == "failed":
|
|
62
|
+
raise RuntimeError(processed["processing_error"])
|
|
63
|
+
|
|
64
|
+
# Search clips with a natural-language query.
|
|
65
|
+
result = client.search_clips(project_id=project_id, query="two characters arguing in the rain")
|
|
66
|
+
for clip in result["clips"]:
|
|
67
|
+
print(clip["start_time"], clip["subtitle"], clip["tags"], clip["characters"])
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## API surface
|
|
71
|
+
|
|
72
|
+
- `ping()` / `me()`
|
|
73
|
+
- `mint_upload_url(project_id=, file_name=, content_type=)`
|
|
74
|
+
- `register_video(project_id=, name=, file_name=|s3_key=, season=, episode=, auto_process=)`
|
|
75
|
+
- `get_video(video_id)` / `list_videos(project_id=, page=, page_size=)` / `delete_video(video_id)`
|
|
76
|
+
- `search_clips(project_id=, query=, page=, page_size=)`
|
|
77
|
+
- `upload_video(project_id=, file=, name=, ...)` — full 3-step upload orchestration
|
|
78
|
+
- `wait_for_processing(video_id, poll_interval_s=, timeout_s=)`
|
|
79
|
+
|
|
80
|
+
Failed API calls raise `AimageApiError` with `code`, `message`, and HTTP
|
|
81
|
+
`status` parsed from the `{"error": {"code", "message"}}` envelope.
|
|
82
|
+
|
|
83
|
+
## Development
|
|
84
|
+
|
|
85
|
+
```sh
|
|
86
|
+
cd sdks/python
|
|
87
|
+
uv sync
|
|
88
|
+
uv run pytest
|
|
89
|
+
uv run ruff check .
|
|
90
|
+
```
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
aimage_sdk/__init__.py,sha256=t6MViCk3c0qXdjip-UUbDWXqmXSm-HvmBVU7WIEK6iM,669
|
|
2
|
+
aimage_sdk/client.py,sha256=eh3QtlMGSX21dFQfVKm4KWTEJcTJ9fVRJE0lVWBQj7c,8079
|
|
3
|
+
aimage_sdk/errors.py,sha256=z8gyggro1uUb2JTQkSpaCvDp5hAnBYn3TvWjy1DBb9U,444
|
|
4
|
+
aimage_sdk/types.py,sha256=IUadXu1p2YoL2eqgVJaLlDthenMYjtH4MCW3uQo33Ig,1631
|
|
5
|
+
aimage_sdk-0.1.0.dist-info/METADATA,sha256=tz0Bzd3yqKbEzlweXbNkm7eD-ekg3SAPSdTddljv9y0,2812
|
|
6
|
+
aimage_sdk-0.1.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
|
|
7
|
+
aimage_sdk-0.1.0.dist-info/licenses/LICENSE,sha256=JHz8EsONffU_-p1_yWwd7VnkRGoWukmQsAI0kHC2eAo,1069
|
|
8
|
+
aimage_sdk-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 AI-Mage Inc.
|
|
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.
|