pixlub 0.3.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.
- pixlub/__init__.py +71 -0
- pixlub/_config.py +83 -0
- pixlub/_core.py +808 -0
- pixlub/_files.py +393 -0
- pixlub/_retry.py +78 -0
- pixlub/_version.py +1 -0
- pixlub/async_client.py +330 -0
- pixlub/client.py +341 -0
- pixlub/errors.py +135 -0
- pixlub/models.py +183 -0
- pixlub/py.typed +0 -0
- pixlub/tools.py +311 -0
- pixlub-0.3.0.dist-info/METADATA +242 -0
- pixlub-0.3.0.dist-info/RECORD +17 -0
- pixlub-0.3.0.dist-info/WHEEL +5 -0
- pixlub-0.3.0.dist-info/licenses/LICENSE +21 -0
- pixlub-0.3.0.dist-info/top_level.txt +1 -0
pixlub/__init__.py
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""Official Python SDK for the Pixlub API::
|
|
2
|
+
|
|
3
|
+
import pixlub
|
|
4
|
+
|
|
5
|
+
client = pixlub.Client() # reads PIXLUB_API_KEY
|
|
6
|
+
result = client.tools.run("removeBgApi", image="photo.jpg")
|
|
7
|
+
result.save(f"photo-nobg{result.extension}")
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from ._config import API_KEY_ENV_VAR, BASE_URL_ENV_VAR, DEFAULT_BASE_URL
|
|
11
|
+
from ._core import DIRECT_UPLOAD_MAX_BYTES, MAX_INPUT_FILES
|
|
12
|
+
from ._files import SourceURL
|
|
13
|
+
from ._version import __version__
|
|
14
|
+
from .async_client import AsyncClient
|
|
15
|
+
from .client import Client
|
|
16
|
+
from .errors import (
|
|
17
|
+
APIConnectionError,
|
|
18
|
+
APIError,
|
|
19
|
+
APITimeoutError,
|
|
20
|
+
AuthenticationError,
|
|
21
|
+
ConfigurationError,
|
|
22
|
+
FileTooLargeError,
|
|
23
|
+
InsufficientCreditsError,
|
|
24
|
+
InvalidInputError,
|
|
25
|
+
InvalidRequestError,
|
|
26
|
+
JobTimeoutError,
|
|
27
|
+
NotFoundError,
|
|
28
|
+
PermissionDeniedError,
|
|
29
|
+
PixlubError,
|
|
30
|
+
RateLimitError,
|
|
31
|
+
ServerError,
|
|
32
|
+
ServiceUnavailableError,
|
|
33
|
+
UnexpectedResponseError,
|
|
34
|
+
UnknownToolError,
|
|
35
|
+
)
|
|
36
|
+
from .models import Tool, ToolInput, ToolOutput, ToolOutputSpec, ToolResult
|
|
37
|
+
|
|
38
|
+
__all__ = [
|
|
39
|
+
"API_KEY_ENV_VAR",
|
|
40
|
+
"BASE_URL_ENV_VAR",
|
|
41
|
+
"DEFAULT_BASE_URL",
|
|
42
|
+
"DIRECT_UPLOAD_MAX_BYTES",
|
|
43
|
+
"MAX_INPUT_FILES",
|
|
44
|
+
"APIConnectionError",
|
|
45
|
+
"APIError",
|
|
46
|
+
"APITimeoutError",
|
|
47
|
+
"AsyncClient",
|
|
48
|
+
"AuthenticationError",
|
|
49
|
+
"Client",
|
|
50
|
+
"ConfigurationError",
|
|
51
|
+
"FileTooLargeError",
|
|
52
|
+
"InsufficientCreditsError",
|
|
53
|
+
"InvalidInputError",
|
|
54
|
+
"InvalidRequestError",
|
|
55
|
+
"JobTimeoutError",
|
|
56
|
+
"NotFoundError",
|
|
57
|
+
"PermissionDeniedError",
|
|
58
|
+
"PixlubError",
|
|
59
|
+
"RateLimitError",
|
|
60
|
+
"ServerError",
|
|
61
|
+
"ServiceUnavailableError",
|
|
62
|
+
"SourceURL",
|
|
63
|
+
"Tool",
|
|
64
|
+
"ToolInput",
|
|
65
|
+
"ToolOutput",
|
|
66
|
+
"ToolOutputSpec",
|
|
67
|
+
"ToolResult",
|
|
68
|
+
"UnexpectedResponseError",
|
|
69
|
+
"UnknownToolError",
|
|
70
|
+
"__version__",
|
|
71
|
+
]
|
pixlub/_config.py
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""API key and base URL."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import re
|
|
7
|
+
from typing import Optional
|
|
8
|
+
from urllib.parse import urljoin, urlsplit, urlunsplit
|
|
9
|
+
|
|
10
|
+
from .errors import ConfigurationError
|
|
11
|
+
|
|
12
|
+
API_KEY_ENV_VAR = "PIXLUB_API_KEY"
|
|
13
|
+
BASE_URL_ENV_VAR = "PIXLUB_BASE_URL"
|
|
14
|
+
DEFAULT_BASE_URL = "https://api.pixlub.com"
|
|
15
|
+
|
|
16
|
+
_API_KEY_RE = re.compile(r"pix\.[A-Za-z0-9_-]{1,64}\.[A-Za-z0-9_-]{1,256}")
|
|
17
|
+
_LOOPBACK_HOSTS = frozenset({"localhost", "127.0.0.1", "::1"})
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def resolve_api_key(api_key: Optional[str]) -> str:
|
|
21
|
+
raw = api_key if api_key is not None else os.environ.get(API_KEY_ENV_VAR)
|
|
22
|
+
key = raw.strip() if raw else ""
|
|
23
|
+
if not key:
|
|
24
|
+
raise ConfigurationError(
|
|
25
|
+
"No API key provided. Pass api_key=... or set the "
|
|
26
|
+
f"{API_KEY_ENV_VAR} environment variable."
|
|
27
|
+
)
|
|
28
|
+
if not _API_KEY_RE.fullmatch(key):
|
|
29
|
+
raise ConfigurationError(
|
|
30
|
+
"The API key is malformed. Copy the whole key (pix.<prefix>.<secret>) "
|
|
31
|
+
"from your dashboard."
|
|
32
|
+
)
|
|
33
|
+
return key
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def resolve_base_url(base_url: Optional[str]) -> str:
|
|
37
|
+
raw = base_url if base_url is not None else os.environ.get(BASE_URL_ENV_VAR)
|
|
38
|
+
url = (raw or DEFAULT_BASE_URL).strip()
|
|
39
|
+
try:
|
|
40
|
+
parts = urlsplit(url)
|
|
41
|
+
parts.port # noqa: B018 - raises ValueError on an invalid port
|
|
42
|
+
except ValueError:
|
|
43
|
+
raise ConfigurationError(
|
|
44
|
+
"Invalid base URL. Expected a form like https://api.example.com"
|
|
45
|
+
) from None
|
|
46
|
+
|
|
47
|
+
if parts.scheme not in ("http", "https") or not parts.hostname:
|
|
48
|
+
raise ConfigurationError(
|
|
49
|
+
"The base URL must start with http:// or https:// and name a host."
|
|
50
|
+
)
|
|
51
|
+
if parts.username is not None or parts.password is not None:
|
|
52
|
+
raise ConfigurationError("Do not put credentials in the base URL. Use api_key instead.")
|
|
53
|
+
if parts.query or parts.fragment:
|
|
54
|
+
raise ConfigurationError("The base URL must not contain a query string or a fragment.")
|
|
55
|
+
if parts.scheme == "http" and parts.hostname not in _LOOPBACK_HOSTS:
|
|
56
|
+
raise ConfigurationError(
|
|
57
|
+
"Plain http:// is only accepted for localhost. Use https:// so your API key "
|
|
58
|
+
"is encrypted in transit."
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
return urlunsplit((parts.scheme, parts.netloc, parts.path.rstrip("/"), "", ""))
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def resolve_download_url(url: str, base_url: str) -> str:
|
|
65
|
+
if not isinstance(url, str) or not url.strip():
|
|
66
|
+
raise ValueError("empty link")
|
|
67
|
+
absolute = urljoin(base_url + "/", url.strip())
|
|
68
|
+
parts = urlsplit(absolute)
|
|
69
|
+
if parts.scheme not in ("http", "https") or not parts.hostname:
|
|
70
|
+
raise ValueError("link is not an http(s) URL")
|
|
71
|
+
if parts.username is not None or parts.password is not None:
|
|
72
|
+
raise ValueError("link carries credentials")
|
|
73
|
+
if parts.scheme == "http" and parts.hostname not in _LOOPBACK_HOSTS:
|
|
74
|
+
raise ValueError("link uses plain http outside localhost")
|
|
75
|
+
return absolute
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def key_mask(api_key: str) -> str:
|
|
79
|
+
return f"pix.{api_key.split('.')[1]}.***"
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def redact(text: str, api_key: str) -> str:
|
|
83
|
+
return text.replace(api_key, key_mask(api_key))
|