multipletools 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.
- multipletools/__init__.py +32 -0
- multipletools/_http.py +121 -0
- multipletools/client.py +48 -0
- multipletools/exceptions.py +55 -0
- multipletools/jobs.py +93 -0
- multipletools/models.py +84 -0
- multipletools/py.typed +1 -0
- multipletools/tools.py +20 -0
- multipletools-0.1.0.dist-info/METADATA +76 -0
- multipletools-0.1.0.dist-info/RECORD +12 -0
- multipletools-0.1.0.dist-info/WHEEL +5 -0
- multipletools-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
from multipletools.client import Client
|
|
2
|
+
from multipletools.exceptions import (
|
|
3
|
+
APIError,
|
|
4
|
+
AuthenticationError,
|
|
5
|
+
ConflictError,
|
|
6
|
+
ConnectionError,
|
|
7
|
+
MultipleToolsError,
|
|
8
|
+
NotFoundError,
|
|
9
|
+
PermissionDeniedError,
|
|
10
|
+
RateLimitError,
|
|
11
|
+
TimeoutError,
|
|
12
|
+
ValidationError,
|
|
13
|
+
)
|
|
14
|
+
from multipletools.models import Job, Tool
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
"APIError",
|
|
18
|
+
"AuthenticationError",
|
|
19
|
+
"Client",
|
|
20
|
+
"ConflictError",
|
|
21
|
+
"ConnectionError",
|
|
22
|
+
"Job",
|
|
23
|
+
"MultipleToolsError",
|
|
24
|
+
"NotFoundError",
|
|
25
|
+
"PermissionDeniedError",
|
|
26
|
+
"RateLimitError",
|
|
27
|
+
"TimeoutError",
|
|
28
|
+
"Tool",
|
|
29
|
+
"ValidationError",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
__version__ = "0.1.0"
|
multipletools/_http.py
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
from collections.abc import Iterator, Mapping
|
|
2
|
+
from contextlib import contextmanager
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
import httpx
|
|
6
|
+
|
|
7
|
+
from multipletools.exceptions import (
|
|
8
|
+
APIError,
|
|
9
|
+
AuthenticationError,
|
|
10
|
+
ConflictError,
|
|
11
|
+
ConnectionError,
|
|
12
|
+
NotFoundError,
|
|
13
|
+
PermissionDeniedError,
|
|
14
|
+
RateLimitError,
|
|
15
|
+
TimeoutError,
|
|
16
|
+
ValidationError,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
_ERROR_TYPES: dict[int, type[APIError]] = {
|
|
21
|
+
400: ValidationError,
|
|
22
|
+
401: AuthenticationError,
|
|
23
|
+
403: PermissionDeniedError,
|
|
24
|
+
404: NotFoundError,
|
|
25
|
+
409: ConflictError,
|
|
26
|
+
422: ValidationError,
|
|
27
|
+
429: RateLimitError,
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class HttpClient:
|
|
32
|
+
def __init__(
|
|
33
|
+
self,
|
|
34
|
+
*,
|
|
35
|
+
api_key: str,
|
|
36
|
+
base_url: str,
|
|
37
|
+
timeout: float | httpx.Timeout,
|
|
38
|
+
transport: httpx.BaseTransport | None = None,
|
|
39
|
+
) -> None:
|
|
40
|
+
self._client = httpx.Client(
|
|
41
|
+
base_url=base_url.rstrip("/") + "/",
|
|
42
|
+
timeout=timeout,
|
|
43
|
+
transport=transport,
|
|
44
|
+
follow_redirects=False,
|
|
45
|
+
headers={
|
|
46
|
+
"Authorization": f"Bearer {api_key}",
|
|
47
|
+
"Accept": "application/json",
|
|
48
|
+
"User-Agent": "multipletools-python/0.1.0",
|
|
49
|
+
},
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
def request_json(
|
|
53
|
+
self,
|
|
54
|
+
method: str,
|
|
55
|
+
path: str,
|
|
56
|
+
*,
|
|
57
|
+
headers: Mapping[str, str] | None = None,
|
|
58
|
+
data: Mapping[str, str] | None = None,
|
|
59
|
+
files: Mapping[str, Any] | None = None,
|
|
60
|
+
) -> Any:
|
|
61
|
+
try:
|
|
62
|
+
response = self._client.request(
|
|
63
|
+
method, path.lstrip("/"), headers=headers, data=data, files=files
|
|
64
|
+
)
|
|
65
|
+
except httpx.TimeoutException as error:
|
|
66
|
+
raise TimeoutError("The Multiple Tools API request timed out") from error
|
|
67
|
+
except httpx.RequestError as error:
|
|
68
|
+
raise ConnectionError(
|
|
69
|
+
f"Could not reach the Multiple Tools API: {error}"
|
|
70
|
+
) from error
|
|
71
|
+
raise_for_status(response)
|
|
72
|
+
try:
|
|
73
|
+
return response.json()
|
|
74
|
+
except ValueError as error:
|
|
75
|
+
raise APIError(
|
|
76
|
+
"The Multiple Tools API returned invalid JSON",
|
|
77
|
+
status_code=response.status_code,
|
|
78
|
+
request_id=response.headers.get("x-request-id"),
|
|
79
|
+
) from error
|
|
80
|
+
|
|
81
|
+
@contextmanager
|
|
82
|
+
def stream(self, method: str, path: str) -> Iterator[httpx.Response]:
|
|
83
|
+
try:
|
|
84
|
+
with self._client.stream(method, path.lstrip("/")) as response:
|
|
85
|
+
if not 200 <= response.status_code < 300:
|
|
86
|
+
response.read()
|
|
87
|
+
raise_for_status(response)
|
|
88
|
+
yield response
|
|
89
|
+
except httpx.TimeoutException as error:
|
|
90
|
+
raise TimeoutError("The Multiple Tools API request timed out") from error
|
|
91
|
+
except httpx.RequestError as error:
|
|
92
|
+
raise ConnectionError(
|
|
93
|
+
f"Could not reach the Multiple Tools API: {error}"
|
|
94
|
+
) from error
|
|
95
|
+
|
|
96
|
+
def close(self) -> None:
|
|
97
|
+
self._client.close()
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def raise_for_status(response: httpx.Response) -> None:
|
|
101
|
+
if 200 <= response.status_code < 300:
|
|
102
|
+
return
|
|
103
|
+
details: Any = None
|
|
104
|
+
message = f"Multiple Tools API returned HTTP {response.status_code}"
|
|
105
|
+
try:
|
|
106
|
+
details = response.json()
|
|
107
|
+
if isinstance(details, dict):
|
|
108
|
+
detail = details.get("detail")
|
|
109
|
+
if isinstance(detail, str) and detail:
|
|
110
|
+
message = detail
|
|
111
|
+
elif detail is not None:
|
|
112
|
+
message = str(detail)
|
|
113
|
+
except ValueError:
|
|
114
|
+
pass
|
|
115
|
+
error_type = _ERROR_TYPES.get(response.status_code, APIError)
|
|
116
|
+
raise error_type(
|
|
117
|
+
message,
|
|
118
|
+
status_code=response.status_code,
|
|
119
|
+
request_id=response.headers.get("x-request-id"),
|
|
120
|
+
details=details,
|
|
121
|
+
)
|
multipletools/client.py
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from types import TracebackType
|
|
3
|
+
|
|
4
|
+
import httpx
|
|
5
|
+
|
|
6
|
+
from multipletools._http import HttpClient
|
|
7
|
+
from multipletools.jobs import Jobs
|
|
8
|
+
from multipletools.tools import Tools
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
_API_KEY_PATTERN = re.compile(r"^mt_live_[a-f0-9]{24}_[A-Za-z0-9_-]{32,128}$")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Client:
|
|
15
|
+
def __init__(
|
|
16
|
+
self,
|
|
17
|
+
*,
|
|
18
|
+
api_key: str,
|
|
19
|
+
base_url: str = "http://localhost:8000",
|
|
20
|
+
timeout: float | httpx.Timeout = 30.0,
|
|
21
|
+
transport: httpx.BaseTransport | None = None,
|
|
22
|
+
) -> None:
|
|
23
|
+
if not _API_KEY_PATTERN.fullmatch(api_key):
|
|
24
|
+
raise ValueError("api_key must use the mt_live_<key_id>_<secret> format")
|
|
25
|
+
if not base_url.startswith(("http://", "https://")):
|
|
26
|
+
raise ValueError("base_url must be an absolute HTTP or HTTPS URL")
|
|
27
|
+
self._http = HttpClient(
|
|
28
|
+
api_key=api_key,
|
|
29
|
+
base_url=base_url,
|
|
30
|
+
timeout=timeout,
|
|
31
|
+
transport=transport,
|
|
32
|
+
)
|
|
33
|
+
self.jobs = Jobs(self._http)
|
|
34
|
+
self.tools = Tools(self._http)
|
|
35
|
+
|
|
36
|
+
def close(self) -> None:
|
|
37
|
+
self._http.close()
|
|
38
|
+
|
|
39
|
+
def __enter__(self) -> "Client":
|
|
40
|
+
return self
|
|
41
|
+
|
|
42
|
+
def __exit__(
|
|
43
|
+
self,
|
|
44
|
+
exc_type: type[BaseException] | None,
|
|
45
|
+
exc_value: BaseException | None,
|
|
46
|
+
traceback: TracebackType | None,
|
|
47
|
+
) -> None:
|
|
48
|
+
self.close()
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class MultipleToolsError(Exception):
|
|
5
|
+
"""Base exception for all SDK failures."""
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ConnectionError(MultipleToolsError):
|
|
9
|
+
"""The API could not be reached."""
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class TimeoutError(MultipleToolsError):
|
|
13
|
+
"""The API request exceeded its configured timeout."""
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class APIError(MultipleToolsError):
|
|
17
|
+
"""The API returned a non-success response."""
|
|
18
|
+
|
|
19
|
+
def __init__(
|
|
20
|
+
self,
|
|
21
|
+
message: str,
|
|
22
|
+
*,
|
|
23
|
+
status_code: int,
|
|
24
|
+
request_id: str | None = None,
|
|
25
|
+
details: Any = None,
|
|
26
|
+
) -> None:
|
|
27
|
+
super().__init__(message)
|
|
28
|
+
self.message = message
|
|
29
|
+
self.status_code = status_code
|
|
30
|
+
self.request_id = request_id
|
|
31
|
+
self.details = details
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class ValidationError(APIError):
|
|
35
|
+
pass
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class AuthenticationError(APIError):
|
|
39
|
+
pass
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class PermissionDeniedError(APIError):
|
|
43
|
+
pass
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class NotFoundError(APIError):
|
|
47
|
+
pass
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class ConflictError(APIError):
|
|
51
|
+
pass
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class RateLimitError(APIError):
|
|
55
|
+
pass
|
multipletools/jobs.py
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import mimetypes
|
|
3
|
+
import os
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import BinaryIO, Mapping
|
|
6
|
+
from urllib.parse import quote
|
|
7
|
+
from uuid import uuid4
|
|
8
|
+
|
|
9
|
+
from multipletools._http import HttpClient
|
|
10
|
+
from multipletools.models import Job
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
FileInput = str | os.PathLike[str] | BinaryIO
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Jobs:
|
|
17
|
+
def __init__(self, http: HttpClient) -> None:
|
|
18
|
+
self._http = http
|
|
19
|
+
|
|
20
|
+
def create(
|
|
21
|
+
self,
|
|
22
|
+
*,
|
|
23
|
+
tool: str,
|
|
24
|
+
file: FileInput,
|
|
25
|
+
options: Mapping[str, object] | None = None,
|
|
26
|
+
idempotency_key: str | None = None,
|
|
27
|
+
media_type: str | None = None,
|
|
28
|
+
) -> Job:
|
|
29
|
+
canonical_tool = tool.strip().replace("_", "-")
|
|
30
|
+
if not canonical_tool:
|
|
31
|
+
raise ValueError("tool must not be empty")
|
|
32
|
+
headers = {"Idempotency-Key": idempotency_key or uuid4().hex}
|
|
33
|
+
data = {"tool": canonical_tool}
|
|
34
|
+
if options:
|
|
35
|
+
data["options"] = json.dumps(options, separators=(",", ":"))
|
|
36
|
+
|
|
37
|
+
if isinstance(file, (str, os.PathLike)):
|
|
38
|
+
path = Path(file)
|
|
39
|
+
resolved_media_type = media_type or _media_type(path.name)
|
|
40
|
+
with path.open("rb") as stream:
|
|
41
|
+
payload = self._http.request_json(
|
|
42
|
+
"POST",
|
|
43
|
+
"/v1/jobs",
|
|
44
|
+
headers=headers,
|
|
45
|
+
data=data,
|
|
46
|
+
files={"file": (path.name, stream, resolved_media_type)},
|
|
47
|
+
)
|
|
48
|
+
else:
|
|
49
|
+
filename = Path(str(getattr(file, "name", "upload.bin"))).name
|
|
50
|
+
resolved_media_type = media_type or _media_type(filename)
|
|
51
|
+
payload = self._http.request_json(
|
|
52
|
+
"POST",
|
|
53
|
+
"/v1/jobs",
|
|
54
|
+
headers=headers,
|
|
55
|
+
data=data,
|
|
56
|
+
files={"file": (filename, file, resolved_media_type)},
|
|
57
|
+
)
|
|
58
|
+
return Job.from_dict(payload)
|
|
59
|
+
|
|
60
|
+
def get(self, job_id: str) -> Job:
|
|
61
|
+
payload = self._http.request_json("GET", f"/v1/jobs/{_job_id(job_id)}")
|
|
62
|
+
return Job.from_dict(payload)
|
|
63
|
+
|
|
64
|
+
def download(
|
|
65
|
+
self,
|
|
66
|
+
job_id: str,
|
|
67
|
+
destination: str | os.PathLike[str],
|
|
68
|
+
) -> Path:
|
|
69
|
+
target = Path(destination)
|
|
70
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
71
|
+
temporary = target.with_name(f".{target.name}.{uuid4().hex}.part")
|
|
72
|
+
try:
|
|
73
|
+
with self._http.stream(
|
|
74
|
+
"GET", f"/v1/jobs/{_job_id(job_id)}/output"
|
|
75
|
+
) as response:
|
|
76
|
+
with temporary.open("wb") as output:
|
|
77
|
+
for chunk in response.iter_bytes():
|
|
78
|
+
output.write(chunk)
|
|
79
|
+
temporary.replace(target)
|
|
80
|
+
except BaseException:
|
|
81
|
+
temporary.unlink(missing_ok=True)
|
|
82
|
+
raise
|
|
83
|
+
return target
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _job_id(job_id: str) -> str:
|
|
87
|
+
if not job_id:
|
|
88
|
+
raise ValueError("job_id must not be empty")
|
|
89
|
+
return quote(job_id, safe="")
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _media_type(filename: str) -> str:
|
|
93
|
+
return mimetypes.guess_type(filename)[0] or "application/octet-stream"
|
multipletools/models.py
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
from typing import Any, Literal, Mapping, cast
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
JobStatus = Literal["QUEUED", "RUNNING", "SUCCESS", "FAILED"]
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass(frozen=True, slots=True)
|
|
10
|
+
class Job:
|
|
11
|
+
id: str
|
|
12
|
+
tool_name: str
|
|
13
|
+
tool_version: str
|
|
14
|
+
status: JobStatus
|
|
15
|
+
progress: int
|
|
16
|
+
input_filename: str
|
|
17
|
+
output_filename: str | None
|
|
18
|
+
output_url: str | None
|
|
19
|
+
error: str | None
|
|
20
|
+
created_at: datetime
|
|
21
|
+
started_at: datetime | None
|
|
22
|
+
completed_at: datetime | None
|
|
23
|
+
|
|
24
|
+
@classmethod
|
|
25
|
+
def from_dict(cls, data: Mapping[str, Any]) -> "Job":
|
|
26
|
+
status = str(data["status"])
|
|
27
|
+
if status not in {"QUEUED", "RUNNING", "SUCCESS", "FAILED"}:
|
|
28
|
+
raise ValueError(f"Unknown job status: {status}")
|
|
29
|
+
progress = int(data["progress"])
|
|
30
|
+
if not 0 <= progress <= 100:
|
|
31
|
+
raise ValueError("Job progress must be between 0 and 100")
|
|
32
|
+
return cls(
|
|
33
|
+
id=str(data["id"]),
|
|
34
|
+
tool_name=str(data["tool_name"]),
|
|
35
|
+
tool_version=str(data["tool_version"]),
|
|
36
|
+
status=cast(JobStatus, status),
|
|
37
|
+
progress=progress,
|
|
38
|
+
input_filename=str(data["input_filename"]),
|
|
39
|
+
output_filename=_optional_string(data.get("output_filename")),
|
|
40
|
+
output_url=_optional_string(data.get("output_url")),
|
|
41
|
+
error=_optional_string(data.get("error")),
|
|
42
|
+
created_at=_datetime(data["created_at"]),
|
|
43
|
+
started_at=_optional_datetime(data.get("started_at")),
|
|
44
|
+
completed_at=_optional_datetime(data.get("completed_at")),
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass(frozen=True, slots=True)
|
|
49
|
+
class Tool:
|
|
50
|
+
name: str
|
|
51
|
+
version: str
|
|
52
|
+
description: str
|
|
53
|
+
input_suffixes: tuple[str, ...]
|
|
54
|
+
input_media_types: tuple[str, ...]
|
|
55
|
+
output_suffix: str
|
|
56
|
+
output_media_type: str
|
|
57
|
+
|
|
58
|
+
@classmethod
|
|
59
|
+
def from_dict(cls, data: Mapping[str, Any]) -> "Tool":
|
|
60
|
+
return cls(
|
|
61
|
+
name=str(data["name"]),
|
|
62
|
+
version=str(data["version"]),
|
|
63
|
+
description=str(data["description"]),
|
|
64
|
+
input_suffixes=tuple(str(value) for value in data["input_suffixes"]),
|
|
65
|
+
input_media_types=tuple(
|
|
66
|
+
str(value) for value in data["input_media_types"]
|
|
67
|
+
),
|
|
68
|
+
output_suffix=str(data["output_suffix"]),
|
|
69
|
+
output_media_type=str(data["output_media_type"]),
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _datetime(value: Any) -> datetime:
|
|
74
|
+
if not isinstance(value, str):
|
|
75
|
+
raise ValueError("Expected an ISO-8601 datetime string")
|
|
76
|
+
return datetime.fromisoformat(value.replace("Z", "+00:00"))
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _optional_datetime(value: Any) -> datetime | None:
|
|
80
|
+
return None if value is None else _datetime(value)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _optional_string(value: Any) -> str | None:
|
|
84
|
+
return None if value is None else str(value)
|
multipletools/py.typed
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
multipletools/tools.py
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from multipletools._http import HttpClient
|
|
2
|
+
from multipletools.models import Tool
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class Tools:
|
|
6
|
+
def __init__(self, http: HttpClient) -> None:
|
|
7
|
+
self._http = http
|
|
8
|
+
|
|
9
|
+
def list(self) -> list[Tool]:
|
|
10
|
+
payload = self._http.request_json("GET", "/v1/tools")
|
|
11
|
+
if not isinstance(payload, list):
|
|
12
|
+
raise ValueError("Expected the tools API to return a list")
|
|
13
|
+
return [Tool.from_dict(item) for item in payload]
|
|
14
|
+
|
|
15
|
+
def get(self, name: str) -> Tool:
|
|
16
|
+
canonical_name = name.replace("_", "-")
|
|
17
|
+
for tool in self.list():
|
|
18
|
+
if tool.name == canonical_name:
|
|
19
|
+
return tool
|
|
20
|
+
raise LookupError(f"Unknown tool: {name}")
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: multipletools
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Official Python SDK for the Multiple Tools API
|
|
5
|
+
Author: Abhishek Kumbhar
|
|
6
|
+
Project-URL: Homepage, https://github.com/Abhiiishek44/multipletools
|
|
7
|
+
Project-URL: Repository, https://github.com/Abhiiishek44/multipletools
|
|
8
|
+
Project-URL: Issues, https://github.com/Abhiiishek44/multipletools/issues
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
15
|
+
Classifier: Typing :: Typed
|
|
16
|
+
Requires-Python: >=3.10
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
Requires-Dist: httpx<1,>=0.27
|
|
19
|
+
Provides-Extra: test
|
|
20
|
+
Requires-Dist: pytest<9,>=8; extra == "test"
|
|
21
|
+
|
|
22
|
+
# Multiple Tools Python SDK
|
|
23
|
+
|
|
24
|
+
Typed synchronous Python client for the Multiple Tools HTTP API. The SDK talks
|
|
25
|
+
only to FastAPI and has no database, object-storage, Redis, Celery, or plugin
|
|
26
|
+
dependencies.
|
|
27
|
+
|
|
28
|
+
## Installation
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
pip install ./sdks/python
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Usage
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
from multipletools import Client
|
|
38
|
+
|
|
39
|
+
client = Client(
|
|
40
|
+
api_key="mt_live_...",
|
|
41
|
+
base_url="https://api.example.com",
|
|
42
|
+
timeout=60.0,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
job = client.jobs.create(
|
|
46
|
+
tool="pdf_to_word", # underscore aliases are normalized to pdf-to-word
|
|
47
|
+
file="document.pdf",
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
job = client.jobs.get(job.id)
|
|
51
|
+
if job.status == "SUCCESS":
|
|
52
|
+
client.jobs.download(job.id, "converted.docx")
|
|
53
|
+
|
|
54
|
+
client.close()
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
The client can also be used as a context manager. `jobs.create` generates an
|
|
58
|
+
idempotency key automatically; pass `idempotency_key=` to reuse one across an
|
|
59
|
+
application-level retry. Tool-specific options are sent with `options={...}`.
|
|
60
|
+
If the platform cannot infer a file's MIME type, pass `media_type=` explicitly.
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
with Client(api_key="mt_live_...", base_url="http://localhost:8000") as client:
|
|
64
|
+
tools = client.tools.list()
|
|
65
|
+
job = client.jobs.create(
|
|
66
|
+
tool="rotate-pdf",
|
|
67
|
+
file="document.pdf",
|
|
68
|
+
options={"angle": 90},
|
|
69
|
+
idempotency_key="conversion-123",
|
|
70
|
+
)
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
HTTP failures raise typed exceptions from `multipletools.exceptions`, including
|
|
74
|
+
`AuthenticationError`, `PermissionDeniedError`, `ValidationError`,
|
|
75
|
+
`NotFoundError`, `ConflictError`, `RateLimitError`, `TimeoutError`, and
|
|
76
|
+
`ConnectionError`.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
multipletools/__init__.py,sha256=2Qtt1SIa8zwdwZPc-EGrL-OfNmF8J73NtUqn6La3JQA,635
|
|
2
|
+
multipletools/_http.py,sha256=vNwD-p3HHQ_FKPfnqyKgCESvXvA9UBhnlT9JrrxYvnQ,3729
|
|
3
|
+
multipletools/client.py,sha256=fpTH98-7X1whBN0gdEdETY7gSL0DgnCyvtR4eI8k0-8,1332
|
|
4
|
+
multipletools/exceptions.py,sha256=6Ix-xYf76K2-9Wis8iDGuFiyzqt2zSjNGtToGyaf9xM,1000
|
|
5
|
+
multipletools/jobs.py,sha256=i3VRs8oum2KhkMbLYf_OEhiR2gT4UUEASV0wBqKtguI,3005
|
|
6
|
+
multipletools/models.py,sha256=fA2Hwl7Vte3ShofciJ_Ji2HrkWm9KOcSZHCsLs117ZE,2778
|
|
7
|
+
multipletools/py.typed,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
|
8
|
+
multipletools/tools.py,sha256=QV8SMgSM5HHR3GS8Z1TAN6EKbh5eJDLstwhP3hbpBIk,684
|
|
9
|
+
multipletools-0.1.0.dist-info/METADATA,sha256=qrVZTUshGjRRglmgFP3zi38wo53nL4Z_RHcwgYp6XfU,2369
|
|
10
|
+
multipletools-0.1.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
11
|
+
multipletools-0.1.0.dist-info/top_level.txt,sha256=JJEg9m6NFvTzxwvdfbonsyix75cVymVrbwSNpG5bqgw,14
|
|
12
|
+
multipletools-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
multipletools
|