indox-client 0.2.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.
@@ -0,0 +1,16 @@
1
+ """Indox Python SDK for media and document conversion services."""
2
+
3
+ from .version import __version__
4
+ from .exceptions import IndoxClientError, IndoxHTTPError
5
+ from .client import IndoxClient
6
+ from .docs import DocsClient
7
+ from .media import MediaClient
8
+
9
+ __all__ = [
10
+ "__version__",
11
+ "IndoxClient",
12
+ "DocsClient",
13
+ "MediaClient",
14
+ "IndoxClientError",
15
+ "IndoxHTTPError",
16
+ ]
indox_client/base.py ADDED
@@ -0,0 +1,131 @@
1
+ """Shared HTTP plumbing for DocsClient and MediaClient."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from typing import Any, Iterable, Mapping, MutableMapping, Optional
7
+
8
+ import requests
9
+
10
+ from .exceptions import IndoxClientError, IndoxHTTPError
11
+ from .version import __version__
12
+
13
+
14
+ DEFAULT_TIMEOUT = (5, 60) # (connect, read)
15
+
16
+
17
+ class BaseServiceClient:
18
+ """Convenience wrapper around `requests` with Indox defaults."""
19
+
20
+ def __init__(
21
+ self,
22
+ *,
23
+ base_url: str,
24
+ api_key: str,
25
+ timeout: Optional[tuple[float, float] | float] = None,
26
+ session: Optional[requests.Session] = None,
27
+ ) -> None:
28
+ if not base_url:
29
+ raise ValueError("base_url is required")
30
+ if not api_key:
31
+ raise ValueError("api_key is required")
32
+
33
+ self.base_url = base_url.rstrip("/")
34
+ self.api_key = api_key.strip()
35
+ self.timeout = timeout or DEFAULT_TIMEOUT
36
+ self._session = session or requests.Session()
37
+ self._user_agent = f"indox-client/{__version__}"
38
+
39
+ # --------------------------------------------------------------------- #
40
+ # Internal helpers
41
+ # --------------------------------------------------------------------- #
42
+ def _build_url(self, path: str) -> str:
43
+ if path.startswith("http://") or path.startswith("https://"):
44
+ return path
45
+ if not path.startswith("/"):
46
+ path = f"/{path}"
47
+ return f"{self.base_url}{path}"
48
+
49
+ def _headers(self, extra: Optional[Mapping[str, str]] = None) -> MutableMapping[str, str]:
50
+ headers: MutableMapping[str, str] = {
51
+ "Authorization": f"Bearer {self.api_key}",
52
+ "Accept": "application/json",
53
+ "User-Agent": self._user_agent,
54
+ }
55
+ if extra:
56
+ headers.update({k: v for k, v in extra.items() if v is not None})
57
+ return headers
58
+
59
+ def _request(
60
+ self,
61
+ method: str,
62
+ path: str,
63
+ *,
64
+ params: Optional[Mapping[str, Any]] = None,
65
+ json_body: Optional[Mapping[str, Any]] = None,
66
+ data: Optional[Mapping[str, Any]] = None,
67
+ files: Optional[Mapping[str, Any]] = None,
68
+ headers: Optional[Mapping[str, str]] = None,
69
+ timeout: Optional[tuple[float, float] | float] = None,
70
+ expected: Iterable[int] = (200, 201, 202),
71
+ stream: bool = False,
72
+ ) -> Any:
73
+ url = self._build_url(path)
74
+ final_headers = self._headers(headers)
75
+
76
+ try:
77
+ response = self._session.request(
78
+ method=method.upper(),
79
+ url=url,
80
+ params=params,
81
+ json=json_body,
82
+ data=data,
83
+ files=files,
84
+ headers=final_headers,
85
+ timeout=timeout or self.timeout,
86
+ stream=stream,
87
+ )
88
+ except requests.RequestException as exc:
89
+ raise IndoxClientError(f"Request to {url} failed: {exc}") from exc
90
+
91
+ if response.status_code not in expected:
92
+ payload = _safe_json(response)
93
+ request_id = response.headers.get("X-Request-ID")
94
+ message = payload.get("detail") if isinstance(payload, dict) else response.text
95
+ raise IndoxHTTPError(
96
+ response.status_code,
97
+ message or f"Unexpected status {response.status_code}",
98
+ payload=payload if isinstance(payload, Mapping) else {"raw": response.text},
99
+ request_id=request_id,
100
+ url=url,
101
+ )
102
+
103
+ if stream:
104
+ return response
105
+
106
+ if not response.content:
107
+ return {}
108
+
109
+ return _safe_json(response)
110
+
111
+ def close(self) -> None:
112
+ self._session.close()
113
+
114
+ # ------------------------------------------------------------------ #
115
+ # Convenience wrappers for subclasses
116
+ # ------------------------------------------------------------------ #
117
+ def _post_json(self, path: str, payload: Mapping[str, Any], **kwargs) -> Any:
118
+ return self._request("POST", path, json_body=payload, **kwargs)
119
+
120
+ def _post_multipart(self, path: str, data: Mapping[str, Any], files: Mapping[str, Any], **kwargs) -> Any:
121
+ return self._request("POST", path, data=data, files=files, **kwargs)
122
+
123
+ def _get(self, path: str, params: Optional[Mapping[str, Any]] = None, **kwargs) -> Any:
124
+ return self._request("GET", path, params=params, **kwargs)
125
+
126
+
127
+ def _safe_json(response: requests.Response) -> Any:
128
+ try:
129
+ return response.json()
130
+ except json.JSONDecodeError:
131
+ return {"raw": response.text}
indox_client/client.py ADDED
@@ -0,0 +1,79 @@
1
+ """Facade that exposes DocsClient and MediaClient under one object."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ from typing import Optional
7
+
8
+ from .docs import DocsClient
9
+ from .media import MediaClient
10
+
11
+
12
+ DEFAULT_DOCS_BASE_URL = "https://indox.org/docs"
13
+ DEFAULT_MEDIA_BASE_URL = "https://indox.org/media"
14
+
15
+
16
+ class IndoxClient:
17
+ """Convenience wrapper that bundles docs + media clients."""
18
+
19
+ def __init__(
20
+ self,
21
+ *,
22
+ api_key: str,
23
+ docs_base_url: str = DEFAULT_DOCS_BASE_URL,
24
+ media_base_url: str = DEFAULT_MEDIA_BASE_URL,
25
+ timeout: Optional[tuple[float, float] | float] = None,
26
+ ) -> None:
27
+ docs_url = (docs_base_url or DEFAULT_DOCS_BASE_URL or "").strip().rstrip("/")
28
+ media_url = (media_base_url or DEFAULT_MEDIA_BASE_URL or "").strip().rstrip("/")
29
+
30
+ if not docs_url:
31
+ raise ValueError("docs_base_url is required")
32
+ if not media_url:
33
+ raise ValueError("media_base_url is required")
34
+
35
+ self.docs = DocsClient(base_url=docs_url, api_key=api_key, timeout=timeout)
36
+ self.media = MediaClient(base_url=media_url, api_key=api_key, timeout=timeout)
37
+
38
+ @classmethod
39
+ def from_env(
40
+ cls,
41
+ *,
42
+ api_key: Optional[str] = None,
43
+ docs_base_url: Optional[str] = None,
44
+ media_base_url: Optional[str] = None,
45
+ timeout: Optional[tuple[float, float] | float] = None,
46
+ ) -> "IndoxClient":
47
+ """Instantiate the client using standard environment variables."""
48
+ key = (api_key or os.getenv("INDOX_API_KEY") or "").strip()
49
+ docs_url = (
50
+ docs_base_url
51
+ or os.getenv("INDOX_DOCS_URL")
52
+ or DEFAULT_DOCS_BASE_URL
53
+ ).strip()
54
+ media_url = (
55
+ media_base_url
56
+ or os.getenv("INDOX_MEDIA_URL")
57
+ or DEFAULT_MEDIA_BASE_URL
58
+ ).strip()
59
+
60
+ if not key:
61
+ raise ValueError("api_key or INDOX_API_KEY is required")
62
+
63
+ return cls(
64
+ api_key=key,
65
+ docs_base_url=docs_url,
66
+ media_base_url=media_url,
67
+ timeout=timeout,
68
+ )
69
+
70
+ def close(self) -> None:
71
+ """Close the underlying HTTP sessions."""
72
+ self.docs.close()
73
+ self.media.close()
74
+
75
+ def __enter__(self) -> "IndoxClient":
76
+ return self
77
+
78
+ def __exit__(self, exc_type, exc, tb) -> None:
79
+ self.close()
indox_client/docs.py ADDED
@@ -0,0 +1,139 @@
1
+ """Client for the Indox document conversion service."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import os
7
+ from pathlib import Path
8
+ from typing import Any, Dict, Iterable, Mapping, MutableMapping, Optional
9
+
10
+ from .base import BaseServiceClient
11
+ from .exceptions import IndoxClientError
12
+
13
+
14
+ class DocsClient(BaseServiceClient):
15
+ """High-level wrapper around `/api/v1/docs/*` endpoints."""
16
+
17
+ def supported_conversions(self) -> Mapping[str, Any]:
18
+ """Return the formats/operations advertised by the service."""
19
+ return self._get("/api/v1/docs/formats/")
20
+
21
+ def convert_file(
22
+ self,
23
+ *,
24
+ file_path: str | os.PathLike[str],
25
+ target_formats: Iterable[str],
26
+ destination: str = "aws",
27
+ redirect_url: Optional[str] = None,
28
+ engine: Optional[str] = None,
29
+ options: Optional[Mapping[str, Any]] = None,
30
+ s3_key: Optional[str] = None,
31
+ ) -> Mapping[str, Any]:
32
+ """Upload a local file for conversion."""
33
+ formats = _clean_formats(target_formats)
34
+ if not formats:
35
+ raise IndoxClientError("target_formats is required")
36
+
37
+ payload: Dict[str, Any] = {
38
+ "target_formats": formats,
39
+ "destination": destination,
40
+ }
41
+ if redirect_url:
42
+ payload["redirect_url"] = redirect_url
43
+ if engine:
44
+ payload["engine"] = engine
45
+ if s3_key:
46
+ payload["s3_key"] = s3_key
47
+ if options is not None:
48
+ payload["options"] = json.dumps(options)
49
+
50
+ path = Path(file_path)
51
+ if not path.exists():
52
+ raise FileNotFoundError(path)
53
+
54
+ with path.open("rb") as fh:
55
+ files = {"file": (path.name, fh)}
56
+ return self._post_multipart("/api/v1/docs/convert/", data=payload, files=files)
57
+
58
+ def convert_url(
59
+ self,
60
+ *,
61
+ file_url: Optional[str] = None,
62
+ s3_key: Optional[str] = None,
63
+ target_formats: Iterable[str],
64
+ destination: str = "aws",
65
+ redirect_url: Optional[str] = None,
66
+ engine: Optional[str] = None,
67
+ options: Optional[Mapping[str, Any]] = None,
68
+ ) -> Mapping[str, Any]:
69
+ """Trigger a conversion for a remote file or S3 object."""
70
+ formats = _clean_formats(target_formats)
71
+ if not formats:
72
+ raise IndoxClientError("target_formats is required")
73
+ if not file_url and not s3_key:
74
+ raise IndoxClientError("Provide file_url or s3_key")
75
+
76
+ payload: Dict[str, Any] = {
77
+ "target_formats": formats,
78
+ "destination": destination,
79
+ }
80
+ if file_url:
81
+ payload["url"] = file_url
82
+ if s3_key:
83
+ payload["s3_key"] = s3_key
84
+ if redirect_url:
85
+ payload["redirect_url"] = redirect_url
86
+ if engine:
87
+ payload["engine"] = engine
88
+ if options is not None:
89
+ payload["options"] = options
90
+
91
+ return self._post_json("/api/v1/docs/convert/json/", payload)
92
+
93
+ def get_conversion(self, conversion_id: str) -> Mapping[str, Any]:
94
+ """Fetch metadata for a conversion or batch."""
95
+ return self._get(f"/api/v1/conversion/{conversion_id}/")
96
+
97
+ def wait_for_completion(
98
+ self,
99
+ conversion_id: str,
100
+ *,
101
+ timeout_seconds: float = 60.0,
102
+ poll_interval: float = 0.5,
103
+ ) -> Mapping[str, Any]:
104
+ """Block until a conversion completes or the timeout elapses."""
105
+ params = {"timeout": timeout_seconds, "interval": poll_interval}
106
+ return self._get(f"/api/v1/conversion/wait/{conversion_id}/", params=params)
107
+
108
+ def download(
109
+ self,
110
+ *,
111
+ conversion_id: str,
112
+ output_path: str | os.PathLike[str],
113
+ fmt: Optional[str] = None,
114
+ ) -> Path:
115
+ """Download the converted artifact to disk."""
116
+ params = {"format": fmt} if fmt else None
117
+ response = self._request(
118
+ "GET",
119
+ f"/api/v1/docs/{conversion_id}/download/",
120
+ params=params,
121
+ stream=True,
122
+ expected=(200,),
123
+ )
124
+ target = Path(output_path)
125
+ target.parent.mkdir(parents=True, exist_ok=True)
126
+ with target.open("wb") as fh:
127
+ for chunk in response.iter_content(chunk_size=8192):
128
+ if chunk:
129
+ fh.write(chunk)
130
+ return target
131
+
132
+
133
+ def _clean_formats(formats: Iterable[str]) -> list[str]:
134
+ cleaned: list[str] = []
135
+ for fmt in formats:
136
+ token = (fmt or "").strip().lower().lstrip(".")
137
+ if token:
138
+ cleaned.append(token)
139
+ return cleaned
@@ -0,0 +1,41 @@
1
+ """Custom exception hierarchy used by the Indox client."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any, Mapping, MutableMapping, Optional
6
+
7
+
8
+ class IndoxClientError(Exception):
9
+ """Base class for all SDK errors."""
10
+
11
+ def __init__(self, message: str, *, payload: Optional[Mapping[str, Any]] = None):
12
+ super().__init__(message)
13
+ self.payload = payload or {}
14
+
15
+
16
+ class IndoxHTTPError(IndoxClientError):
17
+ """Raised when the remote service returns a non-success HTTP status."""
18
+
19
+ def __init__(
20
+ self,
21
+ status_code: int,
22
+ message: str,
23
+ *,
24
+ payload: Optional[Mapping[str, Any]] = None,
25
+ request_id: Optional[str] = None,
26
+ url: Optional[str] = None,
27
+ ):
28
+ super().__init__(message, payload=payload)
29
+ self.status_code = status_code
30
+ self.request_id = request_id
31
+ self.url = url
32
+
33
+ def to_dict(self) -> MutableMapping[str, Any]:
34
+ """Return a dict representation that is easy to log/serialize."""
35
+ return {
36
+ "status_code": self.status_code,
37
+ "message": str(self),
38
+ "request_id": self.request_id,
39
+ "url": self.url,
40
+ "payload": self.payload,
41
+ }
indox_client/media.py ADDED
@@ -0,0 +1,175 @@
1
+ """Client for the Indox media service (images + videos)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import os
7
+ from pathlib import Path
8
+ from typing import Any, Iterable, Mapping, Optional
9
+
10
+ from .base import BaseServiceClient
11
+ from .exceptions import IndoxClientError
12
+
13
+
14
+ class MediaClient(BaseServiceClient):
15
+ """Wrapper around `/api/v1/image/*`, `/api/v1/video/*`, and download routes."""
16
+
17
+ def image_formats(self) -> Mapping[str, Any]:
18
+ """List supported image formats from `/api/v1/image/formats/`."""
19
+ return self._get("/api/v1/image/formats/")
20
+
21
+ def video_formats(self) -> Mapping[str, Any]:
22
+ """List supported video formats from `/api/v1/video/formats/`."""
23
+ return self._get("/api/v1/video/formats/")
24
+
25
+ def image_operations(self) -> Mapping[str, Any]:
26
+ """Fetch the advertised image operations for discovery."""
27
+ return self._get("/api/v1/image/operations/")
28
+
29
+ def video_operations(self) -> Mapping[str, Any]:
30
+ """Fetch the advertised video operations for discovery."""
31
+ return self._get("/api/v1/video/operations/")
32
+
33
+ def convert_image(
34
+ self,
35
+ file_path: str | os.PathLike[str] | None = None,
36
+ target_formats: Iterable[str] = (),
37
+ *,
38
+ file_url: Optional[str] = None,
39
+ s3_key: Optional[str] = None,
40
+ destination: str = "aws",
41
+ redirect_url: Optional[str] = None,
42
+ engine: Optional[str] = None,
43
+ options: Optional[Mapping[str, Any]] = None,
44
+ ) -> Mapping[str, Any]:
45
+ """Start an image conversion job."""
46
+ return self._convert_media(
47
+ endpoint="/api/v1/image/convert/",
48
+ file_path=file_path,
49
+ file_url=file_url,
50
+ s3_key=s3_key,
51
+ target_formats=target_formats,
52
+ destination=destination,
53
+ redirect_url=redirect_url,
54
+ engine=engine,
55
+ options=options,
56
+ )
57
+
58
+ def convert_video(
59
+ self,
60
+ file_path: str | os.PathLike[str] | None = None,
61
+ target_formats: Iterable[str] = (),
62
+ *,
63
+ file_url: Optional[str] = None,
64
+ s3_key: Optional[str] = None,
65
+ destination: str = "aws",
66
+ redirect_url: Optional[str] = None,
67
+ engine: Optional[str] = None,
68
+ options: Optional[Mapping[str, Any]] = None,
69
+ ) -> Mapping[str, Any]:
70
+ """Start a video conversion job."""
71
+ return self._convert_media(
72
+ endpoint="/api/v1/video/convert/",
73
+ file_path=file_path,
74
+ file_url=file_url,
75
+ s3_key=s3_key,
76
+ target_formats=target_formats,
77
+ destination=destination,
78
+ redirect_url=redirect_url,
79
+ engine=engine,
80
+ options=options,
81
+ )
82
+
83
+ def get_image_conversion(self, conversion_id: str) -> Mapping[str, Any]:
84
+ return self._get(f"/api/v1/image/conversion/{conversion_id}/")
85
+
86
+ def get_video_conversion(self, conversion_id: str) -> Mapping[str, Any]:
87
+ return self._get(f"/api/v1/video/conversion/{conversion_id}/")
88
+
89
+ def wait_for_conversion(
90
+ self,
91
+ conversion_id: str,
92
+ *,
93
+ timeout_seconds: float = 60.0,
94
+ poll_interval: float = 0.5,
95
+ ) -> Mapping[str, Any]:
96
+ params = {"timeout": timeout_seconds, "interval": poll_interval}
97
+ return self._get(f"/api/v1/conversion/wait/{conversion_id}/", params=params)
98
+
99
+ def download_media(
100
+ self,
101
+ *,
102
+ conversion_id: str,
103
+ output_path: str | os.PathLike[str],
104
+ fmt: Optional[str] = None,
105
+ ) -> Path:
106
+ params = {"format": fmt} if fmt else None
107
+ response = self._request(
108
+ "GET",
109
+ f"/api/v1/media/{conversion_id}/download/",
110
+ params=params,
111
+ stream=True,
112
+ expected=(200,),
113
+ )
114
+ target = Path(output_path)
115
+ target.parent.mkdir(parents=True, exist_ok=True)
116
+ with target.open("wb") as fh:
117
+ for chunk in response.iter_content(chunk_size=8192):
118
+ if chunk:
119
+ fh.write(chunk)
120
+ return target
121
+
122
+ def _convert_media(
123
+ self,
124
+ *,
125
+ endpoint: str,
126
+ file_path: str | os.PathLike[str] | None,
127
+ file_url: Optional[str],
128
+ s3_key: Optional[str],
129
+ target_formats: Iterable[str],
130
+ destination: str,
131
+ redirect_url: Optional[str],
132
+ engine: Optional[str],
133
+ options: Optional[Mapping[str, Any]],
134
+ ) -> Mapping[str, Any]:
135
+ formats = _clean_formats(target_formats)
136
+ if not formats:
137
+ raise IndoxClientError("target_formats is required")
138
+
139
+ payload: dict[str, Any] = {
140
+ "target_formats": formats,
141
+ "destination": destination,
142
+ }
143
+ if file_url:
144
+ payload["file_url"] = file_url
145
+ if s3_key:
146
+ payload["s3_key"] = s3_key
147
+ if redirect_url:
148
+ payload["redirect_url"] = redirect_url
149
+ if engine:
150
+ payload["engine"] = engine
151
+ if options is not None:
152
+ payload["options"] = json.dumps(options) if file_path else options
153
+
154
+ has_upload = file_path is not None
155
+ if has_upload:
156
+ path = Path(file_path)
157
+ if not path.exists():
158
+ raise FileNotFoundError(path)
159
+ with path.open("rb") as fh:
160
+ files = {"file": (path.name, fh)}
161
+ return self._post_multipart(endpoint, data=payload, files=files)
162
+
163
+ if not file_url and not s3_key:
164
+ raise IndoxClientError("Provide file_path, file_url, or s3_key")
165
+
166
+ return self._post_json(endpoint, payload)
167
+
168
+
169
+ def _clean_formats(formats: Iterable[str]) -> list[str]:
170
+ cleaned: list[str] = []
171
+ for fmt in formats:
172
+ token = (fmt or "").strip().lower().lstrip(".")
173
+ if token:
174
+ cleaned.append(token)
175
+ return cleaned
@@ -0,0 +1,3 @@
1
+ """Central place for the package version so every module can import it."""
2
+
3
+ __version__ = "0.1.1"
@@ -0,0 +1,157 @@
1
+ Metadata-Version: 2.4
2
+ Name: indox-client
3
+ Version: 0.2.0
4
+ Summary: Python SDK for the Indox media and document conversion services.
5
+ Author-email: Indox <support@indox.org>
6
+ License-Expression: LicenseRef-Proprietary
7
+ Project-URL: Homepage, https://indox.org
8
+ Project-URL: Documentation, https://docs.indox.org
9
+ Project-URL: Repository, https://github.com/indox-org/indox_client
10
+ Project-URL: Issues, https://github.com/indox-org/indox_client/issues
11
+ Keywords: indox,document-conversion,media-conversion,sdk,api-client
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.9
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
20
+ Requires-Python: >=3.9
21
+ Description-Content-Type: text/markdown
22
+ License-File: LICENSE
23
+ Requires-Dist: requests>=2.28.0
24
+ Provides-Extra: dev
25
+ Requires-Dist: pytest>=7.0; extra == "dev"
26
+ Requires-Dist: pytest-cov>=4.0; extra == "dev"
27
+ Requires-Dist: responses>=0.23.0; extra == "dev"
28
+ Requires-Dist: ruff>=0.1.0; extra == "dev"
29
+ Requires-Dist: mypy>=1.0; extra == "dev"
30
+ Requires-Dist: types-requests>=2.28; extra == "dev"
31
+ Dynamic: license-file
32
+
33
+ # indox-client
34
+
35
+ Python SDK for the Indox media and document conversion services.
36
+
37
+ ## Installation
38
+
39
+ ```bash
40
+ pip install indox-client
41
+ ```
42
+
43
+ ## Quick Start
44
+
45
+ ```python
46
+ from indox_client import IndoxClient
47
+
48
+ # Initialize with API key
49
+ client = IndoxClient(api_key="your-api-key")
50
+
51
+ # Or use environment variables
52
+ # export INDOX_API_KEY=your-api-key
53
+ client = IndoxClient.from_env()
54
+
55
+ # Convert a document
56
+ result = client.docs.convert_file(
57
+ file_path="document.pdf",
58
+ target_formats=["docx", "txt"]
59
+ )
60
+
61
+ # Wait for completion
62
+ completed = client.docs.wait_for_completion(result["conversion_id"])
63
+
64
+ # Download the result
65
+ client.docs.download(
66
+ conversion_id=result["conversion_id"],
67
+ output_path="output.docx",
68
+ fmt="docx"
69
+ )
70
+ ```
71
+
72
+ ## Features
73
+
74
+ - **Document Conversion**: Convert between PDF, DOCX, TXT, and more
75
+ - **Media Conversion**: Convert images and videos between formats
76
+ - **Async Support**: Long-polling for conversion status
77
+ - **Multiple Sources**: Upload files, URLs, or S3 keys
78
+
79
+ ## Usage
80
+
81
+ ### Document Conversion
82
+
83
+ ```python
84
+ # From local file
85
+ result = client.docs.convert_file(
86
+ file_path="input.pdf",
87
+ target_formats=["docx"]
88
+ )
89
+
90
+ # From URL
91
+ result = client.docs.convert_url(
92
+ file_url="https://example.com/document.pdf",
93
+ target_formats=["docx"]
94
+ )
95
+
96
+ # Check supported formats
97
+ formats = client.docs.supported_conversions()
98
+ ```
99
+
100
+ ### Media Conversion
101
+
102
+ ```python
103
+ # Convert image
104
+ result = client.media.convert_image(
105
+ file_path="image.png",
106
+ target_formats=["webp", "jpg"]
107
+ )
108
+
109
+ # Convert video
110
+ result = client.media.convert_video(
111
+ file_path="video.mp4",
112
+ target_formats=["webm"]
113
+ )
114
+
115
+ # Check supported formats
116
+ image_formats = client.media.image_formats()
117
+ video_formats = client.media.video_formats()
118
+ ```
119
+
120
+ ### Context Manager
121
+
122
+ ```python
123
+ with IndoxClient.from_env() as client:
124
+ result = client.docs.convert_file(
125
+ file_path="document.pdf",
126
+ target_formats=["docx"]
127
+ )
128
+ ```
129
+
130
+ ## Configuration
131
+
132
+ | Environment Variable | Description |
133
+ |---------------------|-------------|
134
+ | `INDOX_API_KEY` | Your API key |
135
+ | `INDOX_DOCS_URL` | Docs service URL (default: https://indox.org/docs) |
136
+ | `INDOX_MEDIA_URL` | Media service URL (default: https://indox.org/media) |
137
+
138
+ ## Error Handling
139
+
140
+ ```python
141
+ from indox_client import IndoxClient, IndoxHTTPError, IndoxClientError
142
+
143
+ try:
144
+ result = client.docs.convert_file(
145
+ file_path="document.pdf",
146
+ target_formats=["docx"]
147
+ )
148
+ except IndoxHTTPError as e:
149
+ print(f"HTTP {e.status_code}: {e}")
150
+ print(f"Request ID: {e.request_id}")
151
+ except IndoxClientError as e:
152
+ print(f"Client error: {e}")
153
+ ```
154
+
155
+ ## License
156
+
157
+ Proprietary - see LICENSE for details.
@@ -0,0 +1,12 @@
1
+ indox_client/__init__.py,sha256=Xssli0gJ593zRfr4LZnHIrBkygXDs8JQ7LSl2Z15QNM,386
2
+ indox_client/base.py,sha256=6REwwY6BPXJlvO-w_RRmFGz1bgzY7RlC2nWczd1aNtc,4727
3
+ indox_client/client.py,sha256=rCwAi8T5Jr0dVP9fbwbXtjm0BGp8XmW3W2XvQp7KrEM,2472
4
+ indox_client/docs.py,sha256=pXQyZKUf8Oxu0XOEmeVsaSy64Hh-MNy6qvXUmgpY4vw,4739
5
+ indox_client/exceptions.py,sha256=X9yMCegs0i1o9OIYgXQ2WgDnZICXyOIslQ_LHMODj4g,1269
6
+ indox_client/media.py,sha256=y5NL5PfG9dp2DgUy8LsUwH2_fAQJTC0SIpfv32JDDdA,6061
7
+ indox_client/version.py,sha256=zqOsZOF2tG8hXq54ctAM5xbPN9XBoQMK0qEiZogKO8k,101
8
+ indox_client-0.2.0.dist-info/licenses/LICENSE,sha256=tIcLnvMXVUZkCsEYTnKBIylDvVsKJ3trWc0vpDMoVow,406
9
+ indox_client-0.2.0.dist-info/METADATA,sha256=S-0zP8yFgh4HA3GxUtUnpTe_gsUcBxtxAJRlY8vbTxc,4153
10
+ indox_client-0.2.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
11
+ indox_client-0.2.0.dist-info/top_level.txt,sha256=csTN17Xr15a7H0Zjq4HnR4tdceTLzULmEtkPtT5Mbho,13
12
+ indox_client-0.2.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.9.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,11 @@
1
+ Copyright (c) 2024-2025 Indox
2
+
3
+ All rights reserved.
4
+
5
+ This software and associated documentation files (the "Software") are proprietary
6
+ and confidential. Unauthorized copying, modification, distribution, or use of
7
+ this Software, via any medium, is strictly prohibited.
8
+
9
+ The Software is provided "AS IS", without warranty of any kind, express or implied.
10
+
11
+ For licensing inquiries, contact: support@indox.org
@@ -0,0 +1 @@
1
+ indox_client