indox-client 0.2.0__tar.gz
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.
- indox_client-0.2.0/LICENSE +11 -0
- indox_client-0.2.0/PKG-INFO +157 -0
- indox_client-0.2.0/README.md +125 -0
- indox_client-0.2.0/pyproject.toml +67 -0
- indox_client-0.2.0/setup.cfg +4 -0
- indox_client-0.2.0/src/indox_client/__init__.py +16 -0
- indox_client-0.2.0/src/indox_client/base.py +131 -0
- indox_client-0.2.0/src/indox_client/client.py +79 -0
- indox_client-0.2.0/src/indox_client/docs.py +139 -0
- indox_client-0.2.0/src/indox_client/exceptions.py +41 -0
- indox_client-0.2.0/src/indox_client/media.py +175 -0
- indox_client-0.2.0/src/indox_client/version.py +3 -0
- indox_client-0.2.0/src/indox_client.egg-info/PKG-INFO +157 -0
- indox_client-0.2.0/src/indox_client.egg-info/SOURCES.txt +16 -0
- indox_client-0.2.0/src/indox_client.egg-info/dependency_links.txt +1 -0
- indox_client-0.2.0/src/indox_client.egg-info/requires.txt +9 -0
- indox_client-0.2.0/src/indox_client.egg-info/top_level.txt +1 -0
- indox_client-0.2.0/tests/test_client.py +128 -0
|
@@ -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,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,125 @@
|
|
|
1
|
+
# indox-client
|
|
2
|
+
|
|
3
|
+
Python SDK for the Indox media and document conversion services.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install indox-client
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
```python
|
|
14
|
+
from indox_client import IndoxClient
|
|
15
|
+
|
|
16
|
+
# Initialize with API key
|
|
17
|
+
client = IndoxClient(api_key="your-api-key")
|
|
18
|
+
|
|
19
|
+
# Or use environment variables
|
|
20
|
+
# export INDOX_API_KEY=your-api-key
|
|
21
|
+
client = IndoxClient.from_env()
|
|
22
|
+
|
|
23
|
+
# Convert a document
|
|
24
|
+
result = client.docs.convert_file(
|
|
25
|
+
file_path="document.pdf",
|
|
26
|
+
target_formats=["docx", "txt"]
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
# Wait for completion
|
|
30
|
+
completed = client.docs.wait_for_completion(result["conversion_id"])
|
|
31
|
+
|
|
32
|
+
# Download the result
|
|
33
|
+
client.docs.download(
|
|
34
|
+
conversion_id=result["conversion_id"],
|
|
35
|
+
output_path="output.docx",
|
|
36
|
+
fmt="docx"
|
|
37
|
+
)
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Features
|
|
41
|
+
|
|
42
|
+
- **Document Conversion**: Convert between PDF, DOCX, TXT, and more
|
|
43
|
+
- **Media Conversion**: Convert images and videos between formats
|
|
44
|
+
- **Async Support**: Long-polling for conversion status
|
|
45
|
+
- **Multiple Sources**: Upload files, URLs, or S3 keys
|
|
46
|
+
|
|
47
|
+
## Usage
|
|
48
|
+
|
|
49
|
+
### Document Conversion
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
# From local file
|
|
53
|
+
result = client.docs.convert_file(
|
|
54
|
+
file_path="input.pdf",
|
|
55
|
+
target_formats=["docx"]
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
# From URL
|
|
59
|
+
result = client.docs.convert_url(
|
|
60
|
+
file_url="https://example.com/document.pdf",
|
|
61
|
+
target_formats=["docx"]
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
# Check supported formats
|
|
65
|
+
formats = client.docs.supported_conversions()
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### Media Conversion
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
# Convert image
|
|
72
|
+
result = client.media.convert_image(
|
|
73
|
+
file_path="image.png",
|
|
74
|
+
target_formats=["webp", "jpg"]
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
# Convert video
|
|
78
|
+
result = client.media.convert_video(
|
|
79
|
+
file_path="video.mp4",
|
|
80
|
+
target_formats=["webm"]
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
# Check supported formats
|
|
84
|
+
image_formats = client.media.image_formats()
|
|
85
|
+
video_formats = client.media.video_formats()
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### Context Manager
|
|
89
|
+
|
|
90
|
+
```python
|
|
91
|
+
with IndoxClient.from_env() as client:
|
|
92
|
+
result = client.docs.convert_file(
|
|
93
|
+
file_path="document.pdf",
|
|
94
|
+
target_formats=["docx"]
|
|
95
|
+
)
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## Configuration
|
|
99
|
+
|
|
100
|
+
| Environment Variable | Description |
|
|
101
|
+
|---------------------|-------------|
|
|
102
|
+
| `INDOX_API_KEY` | Your API key |
|
|
103
|
+
| `INDOX_DOCS_URL` | Docs service URL (default: https://indox.org/docs) |
|
|
104
|
+
| `INDOX_MEDIA_URL` | Media service URL (default: https://indox.org/media) |
|
|
105
|
+
|
|
106
|
+
## Error Handling
|
|
107
|
+
|
|
108
|
+
```python
|
|
109
|
+
from indox_client import IndoxClient, IndoxHTTPError, IndoxClientError
|
|
110
|
+
|
|
111
|
+
try:
|
|
112
|
+
result = client.docs.convert_file(
|
|
113
|
+
file_path="document.pdf",
|
|
114
|
+
target_formats=["docx"]
|
|
115
|
+
)
|
|
116
|
+
except IndoxHTTPError as e:
|
|
117
|
+
print(f"HTTP {e.status_code}: {e}")
|
|
118
|
+
print(f"Request ID: {e.request_id}")
|
|
119
|
+
except IndoxClientError as e:
|
|
120
|
+
print(f"Client error: {e}")
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## License
|
|
124
|
+
|
|
125
|
+
Proprietary - see LICENSE for details.
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=67", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "indox-client"
|
|
7
|
+
version = "0.2.0"
|
|
8
|
+
description = "Python SDK for the Indox media and document conversion services."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = "LicenseRef-Proprietary"
|
|
12
|
+
authors = [
|
|
13
|
+
{name = "Indox", email = "support@indox.org"}
|
|
14
|
+
]
|
|
15
|
+
keywords = ["indox", "document-conversion", "media-conversion", "sdk", "api-client"]
|
|
16
|
+
dependencies = [
|
|
17
|
+
"requests>=2.28.0"
|
|
18
|
+
]
|
|
19
|
+
classifiers = [
|
|
20
|
+
"Development Status :: 4 - Beta",
|
|
21
|
+
"Intended Audience :: Developers",
|
|
22
|
+
"Programming Language :: Python :: 3",
|
|
23
|
+
"Programming Language :: Python :: 3.9",
|
|
24
|
+
"Programming Language :: Python :: 3.10",
|
|
25
|
+
"Programming Language :: Python :: 3.11",
|
|
26
|
+
"Programming Language :: Python :: 3.12",
|
|
27
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
[project.urls]
|
|
31
|
+
Homepage = "https://indox.org"
|
|
32
|
+
Documentation = "https://docs.indox.org"
|
|
33
|
+
Repository = "https://github.com/indox-org/indox_client"
|
|
34
|
+
Issues = "https://github.com/indox-org/indox_client/issues"
|
|
35
|
+
|
|
36
|
+
[project.optional-dependencies]
|
|
37
|
+
dev = [
|
|
38
|
+
"pytest>=7.0",
|
|
39
|
+
"pytest-cov>=4.0",
|
|
40
|
+
"responses>=0.23.0",
|
|
41
|
+
"ruff>=0.1.0",
|
|
42
|
+
"mypy>=1.0",
|
|
43
|
+
"types-requests>=2.28",
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
[tool.setuptools]
|
|
47
|
+
package-dir = {"" = "src"}
|
|
48
|
+
|
|
49
|
+
[tool.setuptools.packages.find]
|
|
50
|
+
where = ["src"]
|
|
51
|
+
exclude = ["tests*"]
|
|
52
|
+
|
|
53
|
+
[tool.ruff]
|
|
54
|
+
line-length = 100
|
|
55
|
+
target-version = "py39"
|
|
56
|
+
|
|
57
|
+
[tool.ruff.lint]
|
|
58
|
+
select = ["E", "F", "I", "W"]
|
|
59
|
+
|
|
60
|
+
[tool.mypy]
|
|
61
|
+
python_version = "3.9"
|
|
62
|
+
strict = true
|
|
63
|
+
warn_return_any = true
|
|
64
|
+
|
|
65
|
+
[tool.pytest.ini_options]
|
|
66
|
+
testpaths = ["tests"]
|
|
67
|
+
addopts = "-v --cov=indox_client --cov-report=term-missing"
|
|
@@ -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
|
+
]
|
|
@@ -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}
|
|
@@ -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()
|
|
@@ -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
|
+
}
|
|
@@ -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,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,16 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
src/indox_client/__init__.py
|
|
5
|
+
src/indox_client/base.py
|
|
6
|
+
src/indox_client/client.py
|
|
7
|
+
src/indox_client/docs.py
|
|
8
|
+
src/indox_client/exceptions.py
|
|
9
|
+
src/indox_client/media.py
|
|
10
|
+
src/indox_client/version.py
|
|
11
|
+
src/indox_client.egg-info/PKG-INFO
|
|
12
|
+
src/indox_client.egg-info/SOURCES.txt
|
|
13
|
+
src/indox_client.egg-info/dependency_links.txt
|
|
14
|
+
src/indox_client.egg-info/requires.txt
|
|
15
|
+
src/indox_client.egg-info/top_level.txt
|
|
16
|
+
tests/test_client.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
indox_client
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"""Unit tests for indox_client."""
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
import responses
|
|
5
|
+
from indox_client import IndoxClient, IndoxClientError, IndoxHTTPError
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class TestIndoxClient:
|
|
9
|
+
"""Test IndoxClient initialization."""
|
|
10
|
+
|
|
11
|
+
def test_init_requires_api_key(self):
|
|
12
|
+
with pytest.raises(ValueError, match="api_key is required"):
|
|
13
|
+
IndoxClient(api_key="")
|
|
14
|
+
|
|
15
|
+
def test_init_with_api_key(self):
|
|
16
|
+
client = IndoxClient(api_key="test-key")
|
|
17
|
+
assert client.docs.api_key == "test-key"
|
|
18
|
+
assert client.media.api_key == "test-key"
|
|
19
|
+
|
|
20
|
+
def test_context_manager(self):
|
|
21
|
+
with IndoxClient(api_key="test-key") as client:
|
|
22
|
+
assert client.docs is not None
|
|
23
|
+
assert client.media is not None
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class TestDocsClient:
|
|
27
|
+
"""Test DocsClient methods."""
|
|
28
|
+
|
|
29
|
+
@responses.activate
|
|
30
|
+
def test_supported_conversions(self):
|
|
31
|
+
responses.add(
|
|
32
|
+
responses.GET,
|
|
33
|
+
"https://indox.org/docs/api/v1/docs/formats/",
|
|
34
|
+
json={"formats": ["pdf", "docx"]},
|
|
35
|
+
status=200,
|
|
36
|
+
)
|
|
37
|
+
client = IndoxClient(api_key="test-key")
|
|
38
|
+
result = client.docs.supported_conversions()
|
|
39
|
+
assert result["formats"] == ["pdf", "docx"]
|
|
40
|
+
|
|
41
|
+
@responses.activate
|
|
42
|
+
def test_convert_url(self):
|
|
43
|
+
responses.add(
|
|
44
|
+
responses.POST,
|
|
45
|
+
"https://indox.org/docs/api/v1/docs/convert/json/",
|
|
46
|
+
json={"conversion_id": "abc123", "status": "pending"},
|
|
47
|
+
status=202,
|
|
48
|
+
)
|
|
49
|
+
client = IndoxClient(api_key="test-key")
|
|
50
|
+
result = client.docs.convert_url(
|
|
51
|
+
file_url="https://example.com/doc.pdf",
|
|
52
|
+
target_formats=["docx"],
|
|
53
|
+
)
|
|
54
|
+
assert result["conversion_id"] == "abc123"
|
|
55
|
+
|
|
56
|
+
def test_convert_url_requires_source(self):
|
|
57
|
+
client = IndoxClient(api_key="test-key")
|
|
58
|
+
with pytest.raises(IndoxClientError, match="Provide file_url or s3_key"):
|
|
59
|
+
client.docs.convert_url(target_formats=["docx"])
|
|
60
|
+
|
|
61
|
+
def test_convert_url_requires_formats(self):
|
|
62
|
+
client = IndoxClient(api_key="test-key")
|
|
63
|
+
with pytest.raises(IndoxClientError, match="target_formats is required"):
|
|
64
|
+
client.docs.convert_url(file_url="https://example.com/doc.pdf", target_formats=[])
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class TestMediaClient:
|
|
68
|
+
"""Test MediaClient methods."""
|
|
69
|
+
|
|
70
|
+
@responses.activate
|
|
71
|
+
def test_image_formats(self):
|
|
72
|
+
responses.add(
|
|
73
|
+
responses.GET,
|
|
74
|
+
"https://indox.org/media/api/v1/image/formats/",
|
|
75
|
+
json={"formats": ["png", "jpg", "webp"]},
|
|
76
|
+
status=200,
|
|
77
|
+
)
|
|
78
|
+
client = IndoxClient(api_key="test-key")
|
|
79
|
+
result = client.media.image_formats()
|
|
80
|
+
assert "png" in result["formats"]
|
|
81
|
+
|
|
82
|
+
@responses.activate
|
|
83
|
+
def test_convert_image_from_url(self):
|
|
84
|
+
responses.add(
|
|
85
|
+
responses.POST,
|
|
86
|
+
"https://indox.org/media/api/v1/image/convert/",
|
|
87
|
+
json={"conversion_id": "img123", "status": "pending"},
|
|
88
|
+
status=202,
|
|
89
|
+
)
|
|
90
|
+
client = IndoxClient(api_key="test-key")
|
|
91
|
+
result = client.media.convert_image(
|
|
92
|
+
file_url="https://example.com/image.png",
|
|
93
|
+
target_formats=["webp"],
|
|
94
|
+
)
|
|
95
|
+
assert result["conversion_id"] == "img123"
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class TestHTTPErrors:
|
|
99
|
+
"""Test error handling."""
|
|
100
|
+
|
|
101
|
+
@responses.activate
|
|
102
|
+
def test_http_error_raised(self):
|
|
103
|
+
responses.add(
|
|
104
|
+
responses.GET,
|
|
105
|
+
"https://indox.org/docs/api/v1/docs/formats/",
|
|
106
|
+
json={"detail": "Unauthorized"},
|
|
107
|
+
status=401,
|
|
108
|
+
)
|
|
109
|
+
client = IndoxClient(api_key="bad-key")
|
|
110
|
+
with pytest.raises(IndoxHTTPError) as exc_info:
|
|
111
|
+
client.docs.supported_conversions()
|
|
112
|
+
assert exc_info.value.status_code == 401
|
|
113
|
+
|
|
114
|
+
@responses.activate
|
|
115
|
+
def test_http_error_to_dict(self):
|
|
116
|
+
responses.add(
|
|
117
|
+
responses.GET,
|
|
118
|
+
"https://indox.org/docs/api/v1/docs/formats/",
|
|
119
|
+
json={"detail": "Not found"},
|
|
120
|
+
status=404,
|
|
121
|
+
headers={"X-Request-ID": "req-123"},
|
|
122
|
+
)
|
|
123
|
+
client = IndoxClient(api_key="test-key")
|
|
124
|
+
with pytest.raises(IndoxHTTPError) as exc_info:
|
|
125
|
+
client.docs.supported_conversions()
|
|
126
|
+
error_dict = exc_info.value.to_dict()
|
|
127
|
+
assert error_dict["status_code"] == 404
|
|
128
|
+
assert error_dict["request_id"] == "req-123"
|