runapi-topaz 0.1.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.
- runapi_topaz-0.1.0/.gitignore +29 -0
- runapi_topaz-0.1.0/PKG-INFO +86 -0
- runapi_topaz-0.1.0/README.md +73 -0
- runapi_topaz-0.1.0/pyproject.toml +33 -0
- runapi_topaz-0.1.0/src/runapi/topaz/__init__.py +24 -0
- runapi_topaz-0.1.0/src/runapi/topaz/client.py +31 -0
- runapi_topaz-0.1.0/src/runapi/topaz/contract_gen.py +31 -0
- runapi_topaz-0.1.0/src/runapi/topaz/py.typed +0 -0
- runapi_topaz-0.1.0/src/runapi/topaz/resources/__init__.py +4 -0
- runapi_topaz-0.1.0/src/runapi/topaz/resources/upscale_image.py +58 -0
- runapi_topaz-0.1.0/src/runapi/topaz/resources/upscale_video.py +58 -0
- runapi_topaz-0.1.0/src/runapi/topaz/types.py +43 -0
- runapi_topaz-0.1.0/tests/test_client.py +202 -0
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Build artifacts
|
|
2
|
+
dist/
|
|
3
|
+
build/
|
|
4
|
+
*.egg-info/
|
|
5
|
+
*.egg
|
|
6
|
+
|
|
7
|
+
# Bytecode
|
|
8
|
+
__pycache__/
|
|
9
|
+
*.py[cod]
|
|
10
|
+
|
|
11
|
+
# Virtual environments
|
|
12
|
+
.venv/
|
|
13
|
+
venv/
|
|
14
|
+
|
|
15
|
+
# uv
|
|
16
|
+
uv.lock
|
|
17
|
+
|
|
18
|
+
# Test / type caches
|
|
19
|
+
.pytest_cache/
|
|
20
|
+
.mypy_cache/
|
|
21
|
+
.ruff_cache/
|
|
22
|
+
.coverage
|
|
23
|
+
htmlcov/
|
|
24
|
+
|
|
25
|
+
# IDE / OS
|
|
26
|
+
.idea/
|
|
27
|
+
.vscode/
|
|
28
|
+
*.swp
|
|
29
|
+
.DS_Store
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: runapi-topaz
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Topaz image and video upscale client for RunAPI
|
|
5
|
+
Project-URL: Homepage, https://runapi.ai/models/topaz
|
|
6
|
+
Project-URL: Documentation, https://runapi.ai/docs#sdk-topaz
|
|
7
|
+
Author-email: RunAPI <contact@runapi.ai>
|
|
8
|
+
License-Expression: Apache-2.0
|
|
9
|
+
Keywords: ai,runapi,sdk,topaz,upscale-image,upscale-video
|
|
10
|
+
Requires-Python: >=3.9
|
|
11
|
+
Requires-Dist: runapi-core
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
|
|
14
|
+
# Topaz Python SDK for RunAPI
|
|
15
|
+
|
|
16
|
+
The Topaz Python SDK is the language-specific package for Topaz on RunAPI. Use it
|
|
17
|
+
for image upscale and video upscale flows when your application needs JSON request
|
|
18
|
+
bodies, task status lookup, and consistent RunAPI errors in Python.
|
|
19
|
+
|
|
20
|
+
For model details, use https://runapi.ai/models/topaz; for API reference, use
|
|
21
|
+
https://runapi.ai/docs#topaz; for SDK docs, use https://runapi.ai/docs#sdk-topaz.
|
|
22
|
+
|
|
23
|
+
## Install
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
pip install runapi-topaz
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Quick start
|
|
30
|
+
|
|
31
|
+
```python
|
|
32
|
+
from runapi.topaz import TopazClient
|
|
33
|
+
|
|
34
|
+
client = TopazClient() # reads RUNAPI_API_KEY, or pass api_key="sk-..."
|
|
35
|
+
|
|
36
|
+
task = client.upscale_image.create(
|
|
37
|
+
model="topaz-upscale-image",
|
|
38
|
+
source_image_url="https://example.com/in.jpg",
|
|
39
|
+
upscale_factor=4,
|
|
40
|
+
)
|
|
41
|
+
status = client.upscale_image.get(task.id)
|
|
42
|
+
|
|
43
|
+
video = client.upscale_video.create(
|
|
44
|
+
model="topaz-upscale-video",
|
|
45
|
+
source_video_url="https://example.com/in.mp4",
|
|
46
|
+
upscale_factor=2,
|
|
47
|
+
)
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Use `create` to submit a task and return quickly, `get` to fetch the latest task
|
|
51
|
+
state, and `run` to create and poll until completion:
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
result = client.upscale_image.run(
|
|
55
|
+
model="topaz-upscale-image",
|
|
56
|
+
source_image_url="https://example.com/in.jpg",
|
|
57
|
+
upscale_factor=4,
|
|
58
|
+
)
|
|
59
|
+
print(result.images[0].url)
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
In web request handlers, prefer `create` plus webhook or later `get` polling so a
|
|
63
|
+
worker is not held open.
|
|
64
|
+
|
|
65
|
+
RunAPI-generated file URLs are temporary. Download and store generated images and
|
|
66
|
+
videos in your own durable storage within 7 days; do not treat returned URLs as
|
|
67
|
+
long-term assets.
|
|
68
|
+
|
|
69
|
+
## Language notes
|
|
70
|
+
|
|
71
|
+
Pass parameters as keyword arguments and catch the `runapi.topaz` error classes
|
|
72
|
+
when building upscaling jobs or scripts. The available resources are
|
|
73
|
+
`upscale_image` and `upscale_video`. Keep `RUNAPI_API_KEY` in the environment or
|
|
74
|
+
your secret manager; never commit API keys or callback secrets.
|
|
75
|
+
|
|
76
|
+
## Links
|
|
77
|
+
|
|
78
|
+
- Model page: https://runapi.ai/models/topaz
|
|
79
|
+
- SDK docs: https://runapi.ai/docs#sdk-topaz
|
|
80
|
+
- Product docs: https://runapi.ai/docs#topaz
|
|
81
|
+
- Pricing and rate limits: https://runapi.ai/models/topaz
|
|
82
|
+
- Full catalog: https://runapi.ai/models
|
|
83
|
+
|
|
84
|
+
## License
|
|
85
|
+
|
|
86
|
+
Licensed under the Apache License, Version 2.0.
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# Topaz Python SDK for RunAPI
|
|
2
|
+
|
|
3
|
+
The Topaz Python SDK is the language-specific package for Topaz on RunAPI. Use it
|
|
4
|
+
for image upscale and video upscale flows when your application needs JSON request
|
|
5
|
+
bodies, task status lookup, and consistent RunAPI errors in Python.
|
|
6
|
+
|
|
7
|
+
For model details, use https://runapi.ai/models/topaz; for API reference, use
|
|
8
|
+
https://runapi.ai/docs#topaz; for SDK docs, use https://runapi.ai/docs#sdk-topaz.
|
|
9
|
+
|
|
10
|
+
## Install
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
pip install runapi-topaz
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Quick start
|
|
17
|
+
|
|
18
|
+
```python
|
|
19
|
+
from runapi.topaz import TopazClient
|
|
20
|
+
|
|
21
|
+
client = TopazClient() # reads RUNAPI_API_KEY, or pass api_key="sk-..."
|
|
22
|
+
|
|
23
|
+
task = client.upscale_image.create(
|
|
24
|
+
model="topaz-upscale-image",
|
|
25
|
+
source_image_url="https://example.com/in.jpg",
|
|
26
|
+
upscale_factor=4,
|
|
27
|
+
)
|
|
28
|
+
status = client.upscale_image.get(task.id)
|
|
29
|
+
|
|
30
|
+
video = client.upscale_video.create(
|
|
31
|
+
model="topaz-upscale-video",
|
|
32
|
+
source_video_url="https://example.com/in.mp4",
|
|
33
|
+
upscale_factor=2,
|
|
34
|
+
)
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Use `create` to submit a task and return quickly, `get` to fetch the latest task
|
|
38
|
+
state, and `run` to create and poll until completion:
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
result = client.upscale_image.run(
|
|
42
|
+
model="topaz-upscale-image",
|
|
43
|
+
source_image_url="https://example.com/in.jpg",
|
|
44
|
+
upscale_factor=4,
|
|
45
|
+
)
|
|
46
|
+
print(result.images[0].url)
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
In web request handlers, prefer `create` plus webhook or later `get` polling so a
|
|
50
|
+
worker is not held open.
|
|
51
|
+
|
|
52
|
+
RunAPI-generated file URLs are temporary. Download and store generated images and
|
|
53
|
+
videos in your own durable storage within 7 days; do not treat returned URLs as
|
|
54
|
+
long-term assets.
|
|
55
|
+
|
|
56
|
+
## Language notes
|
|
57
|
+
|
|
58
|
+
Pass parameters as keyword arguments and catch the `runapi.topaz` error classes
|
|
59
|
+
when building upscaling jobs or scripts. The available resources are
|
|
60
|
+
`upscale_image` and `upscale_video`. Keep `RUNAPI_API_KEY` in the environment or
|
|
61
|
+
your secret manager; never commit API keys or callback secrets.
|
|
62
|
+
|
|
63
|
+
## Links
|
|
64
|
+
|
|
65
|
+
- Model page: https://runapi.ai/models/topaz
|
|
66
|
+
- SDK docs: https://runapi.ai/docs#sdk-topaz
|
|
67
|
+
- Product docs: https://runapi.ai/docs#topaz
|
|
68
|
+
- Pricing and rate limits: https://runapi.ai/models/topaz
|
|
69
|
+
- Full catalog: https://runapi.ai/models
|
|
70
|
+
|
|
71
|
+
## License
|
|
72
|
+
|
|
73
|
+
Licensed under the Apache License, Version 2.0.
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "runapi-topaz"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Topaz image and video upscale client for RunAPI"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = "Apache-2.0"
|
|
12
|
+
authors = [{ name = "RunAPI", email = "contact@runapi.ai" }]
|
|
13
|
+
keywords = ["runapi", "topaz", "upscale-image", "upscale-video", "ai", "sdk"]
|
|
14
|
+
dependencies = ["runapi-core"]
|
|
15
|
+
|
|
16
|
+
[project.urls]
|
|
17
|
+
Homepage = "https://runapi.ai/models/topaz"
|
|
18
|
+
Documentation = "https://runapi.ai/docs#sdk-topaz"
|
|
19
|
+
|
|
20
|
+
[tool.hatch.build.targets.wheel]
|
|
21
|
+
packages = ["src/runapi"]
|
|
22
|
+
|
|
23
|
+
[tool.uv]
|
|
24
|
+
package = true
|
|
25
|
+
|
|
26
|
+
[tool.uv.sources]
|
|
27
|
+
runapi-core = { workspace = true }
|
|
28
|
+
|
|
29
|
+
[dependency-groups]
|
|
30
|
+
dev = ["pytest>=8"]
|
|
31
|
+
|
|
32
|
+
[tool.runapi]
|
|
33
|
+
slug = "topaz"
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""Topaz client for RunAPI."""
|
|
2
|
+
|
|
3
|
+
from runapi.core import (
|
|
4
|
+
AuthenticationError,
|
|
5
|
+
InsufficientCreditsError,
|
|
6
|
+
NotFoundError,
|
|
7
|
+
RateLimitError,
|
|
8
|
+
TaskFailedError,
|
|
9
|
+
TaskTimeoutError,
|
|
10
|
+
ValidationError,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
from .client import TopazClient
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"TopazClient",
|
|
17
|
+
"AuthenticationError",
|
|
18
|
+
"RateLimitError",
|
|
19
|
+
"InsufficientCreditsError",
|
|
20
|
+
"NotFoundError",
|
|
21
|
+
"ValidationError",
|
|
22
|
+
"TaskFailedError",
|
|
23
|
+
"TaskTimeoutError",
|
|
24
|
+
]
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""Topaz client."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Optional
|
|
6
|
+
|
|
7
|
+
from runapi.core import ClientOptions, HttpClient, resolve_api_key
|
|
8
|
+
|
|
9
|
+
from .resources.upscale_image import UpscaleImage
|
|
10
|
+
from .resources.upscale_video import UpscaleVideo
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class TopazClient:
|
|
14
|
+
"""Topaz image and video upscale client.
|
|
15
|
+
|
|
16
|
+
Example::
|
|
17
|
+
|
|
18
|
+
client = TopazClient(api_key="sk-...")
|
|
19
|
+
result = client.upscale_image.run(
|
|
20
|
+
model="topaz-upscale-image",
|
|
21
|
+
source_image_url="https://example.com/in.jpg",
|
|
22
|
+
upscale_factor=4,
|
|
23
|
+
)
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
def __init__(self, api_key: Optional[str] = None, **options: Any) -> None:
|
|
27
|
+
resolved_api_key = resolve_api_key(api_key)
|
|
28
|
+
client_options = ClientOptions(api_key=resolved_api_key, **options)
|
|
29
|
+
http = client_options.http_client or HttpClient(client_options)
|
|
30
|
+
self.upscale_image = UpscaleImage(http)
|
|
31
|
+
self.upscale_video = UpscaleVideo(http)
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
CONTRACT = {
|
|
2
|
+
"upscale-image": {
|
|
3
|
+
"models": ["topaz-upscale-image"],
|
|
4
|
+
"fields_by_model": {
|
|
5
|
+
"topaz-upscale-image": {
|
|
6
|
+
"source_image_url": {
|
|
7
|
+
"required": True
|
|
8
|
+
},
|
|
9
|
+
"upscale_factor": {
|
|
10
|
+
"enum": [1, 2, 4, 8],
|
|
11
|
+
"required": True,
|
|
12
|
+
"type": "integer"
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"upscale-video": {
|
|
18
|
+
"models": ["topaz-upscale-video"],
|
|
19
|
+
"fields_by_model": {
|
|
20
|
+
"topaz-upscale-video": {
|
|
21
|
+
"source_video_url": {
|
|
22
|
+
"required": True
|
|
23
|
+
},
|
|
24
|
+
"upscale_factor": {
|
|
25
|
+
"enum": [1, 2, 4],
|
|
26
|
+
"type": "integer"
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
File without changes
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""Topaz upscale-image resource."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from runapi.core import Resource
|
|
8
|
+
|
|
9
|
+
from ..contract_gen import CONTRACT
|
|
10
|
+
from ..types import (
|
|
11
|
+
CompletedUpscaleImageResponse,
|
|
12
|
+
UpscaleImageResponse,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class UpscaleImage(Resource):
|
|
17
|
+
"""Upscale images with Topaz."""
|
|
18
|
+
|
|
19
|
+
ENDPOINT = "/api/v1/topaz/upscale_image"
|
|
20
|
+
|
|
21
|
+
RESPONSE_CLASS = UpscaleImageResponse
|
|
22
|
+
COMPLETED_RESPONSE_CLASS = CompletedUpscaleImageResponse
|
|
23
|
+
|
|
24
|
+
def run(self, **params: Any) -> Any:
|
|
25
|
+
"""Upscale an image and poll until it completes.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
**params: image upscale parameters (model, ...).
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
The completed (narrowed) image upscale response.
|
|
32
|
+
"""
|
|
33
|
+
task = self.create(**params)
|
|
34
|
+
return self._poll_until_complete(lambda: self.get(task.id))
|
|
35
|
+
|
|
36
|
+
def create(self, **params: Any) -> Any:
|
|
37
|
+
"""Create an image upscale task and return immediately with an id.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
**params: image upscale parameters (model, ...).
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
The task creation result with an id.
|
|
44
|
+
"""
|
|
45
|
+
compacted = self._compact_params(params)
|
|
46
|
+
self._validate_contract(CONTRACT["upscale-image"], compacted)
|
|
47
|
+
return self._request("post", self.ENDPOINT, body=compacted)
|
|
48
|
+
|
|
49
|
+
def get(self, id: str) -> Any:
|
|
50
|
+
"""Fetch the current status of an image upscale task.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
id: The task id returned by ``create``.
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
The current task status.
|
|
57
|
+
"""
|
|
58
|
+
return self._request("get", f"{self.ENDPOINT}/{id}")
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""Topaz upscale-video resource."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from runapi.core import Resource
|
|
8
|
+
|
|
9
|
+
from ..contract_gen import CONTRACT
|
|
10
|
+
from ..types import (
|
|
11
|
+
CompletedUpscaleVideoResponse,
|
|
12
|
+
UpscaleVideoResponse,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class UpscaleVideo(Resource):
|
|
17
|
+
"""Upscale videos with Topaz."""
|
|
18
|
+
|
|
19
|
+
ENDPOINT = "/api/v1/topaz/upscale_video"
|
|
20
|
+
|
|
21
|
+
RESPONSE_CLASS = UpscaleVideoResponse
|
|
22
|
+
COMPLETED_RESPONSE_CLASS = CompletedUpscaleVideoResponse
|
|
23
|
+
|
|
24
|
+
def run(self, **params: Any) -> Any:
|
|
25
|
+
"""Upscale a video and poll until it completes.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
**params: video upscale parameters (model, ...).
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
The completed (narrowed) video upscale response.
|
|
32
|
+
"""
|
|
33
|
+
task = self.create(**params)
|
|
34
|
+
return self._poll_until_complete(lambda: self.get(task.id))
|
|
35
|
+
|
|
36
|
+
def create(self, **params: Any) -> Any:
|
|
37
|
+
"""Create a video upscale task and return immediately with an id.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
**params: video upscale parameters (model, ...).
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
The task creation result with an id.
|
|
44
|
+
"""
|
|
45
|
+
compacted = self._compact_params(params)
|
|
46
|
+
self._validate_contract(CONTRACT["upscale-video"], compacted)
|
|
47
|
+
return self._request("post", self.ENDPOINT, body=compacted)
|
|
48
|
+
|
|
49
|
+
def get(self, id: str) -> Any:
|
|
50
|
+
"""Fetch the current status of a video upscale task.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
id: The task id returned by ``create``.
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
The current task status.
|
|
57
|
+
"""
|
|
58
|
+
return self._request("get", f"{self.ENDPOINT}/{id}")
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""Topaz response models."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from runapi.core import BaseModel, TaskResponse, optional, required
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Image(BaseModel):
|
|
9
|
+
url = optional(str)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Video(BaseModel):
|
|
13
|
+
url = optional(str)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class UpscaleImageResponse(TaskResponse):
|
|
17
|
+
"""Topaz image upscale task status response."""
|
|
18
|
+
|
|
19
|
+
id = required(str)
|
|
20
|
+
status = optional(str, enum=lambda: TaskResponse.Status.ALL)
|
|
21
|
+
images = optional([lambda: Image])
|
|
22
|
+
error = optional(str)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class UpscaleVideoResponse(TaskResponse):
|
|
26
|
+
"""Topaz video upscale task status response."""
|
|
27
|
+
|
|
28
|
+
id = required(str)
|
|
29
|
+
status = optional(str, enum=lambda: TaskResponse.Status.ALL)
|
|
30
|
+
videos = optional([lambda: Video])
|
|
31
|
+
error = optional(str)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class CompletedUpscaleImageResponse(UpscaleImageResponse):
|
|
35
|
+
"""Narrowed response from ``run()`` once polling observes completion."""
|
|
36
|
+
|
|
37
|
+
images = required([lambda: Image])
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class CompletedUpscaleVideoResponse(UpscaleVideoResponse):
|
|
41
|
+
"""Narrowed response from ``run()`` once polling observes completion."""
|
|
42
|
+
|
|
43
|
+
videos = required([lambda: Video])
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
|
|
3
|
+
from runapi.core import config
|
|
4
|
+
from runapi.core.errors import AuthenticationError, ValidationError
|
|
5
|
+
from runapi.topaz import TopazClient
|
|
6
|
+
from runapi.topaz.resources.upscale_image import UpscaleImage
|
|
7
|
+
from runapi.topaz.resources.upscale_video import UpscaleVideo
|
|
8
|
+
from runapi.topaz.types import (
|
|
9
|
+
CompletedUpscaleImageResponse,
|
|
10
|
+
CompletedUpscaleVideoResponse,
|
|
11
|
+
UpscaleImageResponse,
|
|
12
|
+
UpscaleVideoResponse,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class FakeHttp:
|
|
17
|
+
def __init__(self, *responses):
|
|
18
|
+
self._responses = list(responses)
|
|
19
|
+
self.calls = []
|
|
20
|
+
|
|
21
|
+
def request(self, method, path, body=None, options=None):
|
|
22
|
+
self.calls.append((method, path, body))
|
|
23
|
+
if self._responses:
|
|
24
|
+
return self._responses.pop(0)
|
|
25
|
+
return {"id": "task_1", "status": "pending"}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@pytest.fixture(autouse=True)
|
|
29
|
+
def reset_config(monkeypatch):
|
|
30
|
+
monkeypatch.delenv("RUNAPI_API_KEY", raising=False)
|
|
31
|
+
monkeypatch.setattr(config, "api_key", None)
|
|
32
|
+
yield
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
# --- auth -----------------------------------------------------------------
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def test_accepts_api_key_parameter():
|
|
39
|
+
assert isinstance(TopazClient(api_key="k", http_client=FakeHttp()), TopazClient)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def test_falls_back_to_global(monkeypatch):
|
|
43
|
+
monkeypatch.setattr(config, "api_key", "global-key")
|
|
44
|
+
assert isinstance(TopazClient(http_client=FakeHttp()), TopazClient)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def test_falls_back_to_env(monkeypatch):
|
|
48
|
+
monkeypatch.setenv("RUNAPI_API_KEY", "env-key")
|
|
49
|
+
assert isinstance(TopazClient(http_client=FakeHttp()), TopazClient)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def test_raises_without_api_key():
|
|
53
|
+
with pytest.raises(AuthenticationError, match="API key is required"):
|
|
54
|
+
TopazClient()
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
# --- injection / accessors ------------------------------------------------
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def test_uses_injected_http_client():
|
|
61
|
+
fake = FakeHttp()
|
|
62
|
+
client = TopazClient(api_key="k", http_client=fake)
|
|
63
|
+
assert client.upscale_image._http is fake
|
|
64
|
+
assert client.upscale_video._http is fake
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def test_exposes_resource_accessors():
|
|
68
|
+
client = TopazClient(api_key="k", http_client=FakeHttp())
|
|
69
|
+
assert isinstance(client.upscale_image, UpscaleImage)
|
|
70
|
+
assert isinstance(client.upscale_video, UpscaleVideo)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
# --- request shapes -------------------------------------------------------
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def test_create_posts_compacted_body():
|
|
77
|
+
fake = FakeHttp({"id": "t1", "status": "pending"})
|
|
78
|
+
client = TopazClient(api_key="k", http_client=fake)
|
|
79
|
+
result = client.upscale_image.create(
|
|
80
|
+
model="topaz-upscale-image",
|
|
81
|
+
source_image_url="https://x/y.jpg",
|
|
82
|
+
upscale_factor=4,
|
|
83
|
+
note=None,
|
|
84
|
+
)
|
|
85
|
+
assert fake.calls == [
|
|
86
|
+
(
|
|
87
|
+
"post",
|
|
88
|
+
"/api/v1/topaz/upscale_image",
|
|
89
|
+
{"model": "topaz-upscale-image", "source_image_url": "https://x/y.jpg", "upscale_factor": 4},
|
|
90
|
+
),
|
|
91
|
+
]
|
|
92
|
+
assert isinstance(result, UpscaleImageResponse)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def test_get_fetches_by_id():
|
|
96
|
+
fake = FakeHttp({"id": "t1", "status": "processing"})
|
|
97
|
+
client = TopazClient(api_key="k", http_client=fake)
|
|
98
|
+
client.upscale_image.get("t1")
|
|
99
|
+
assert fake.calls == [("get", "/api/v1/topaz/upscale_image/t1", None)]
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def test_video_create_posts_compacted_body():
|
|
103
|
+
fake = FakeHttp({"id": "t1", "status": "pending"})
|
|
104
|
+
client = TopazClient(api_key="k", http_client=fake)
|
|
105
|
+
result = client.upscale_video.create(
|
|
106
|
+
model="topaz-upscale-video",
|
|
107
|
+
source_video_url="https://x/y.mp4",
|
|
108
|
+
upscale_factor=2,
|
|
109
|
+
)
|
|
110
|
+
assert fake.calls == [
|
|
111
|
+
(
|
|
112
|
+
"post",
|
|
113
|
+
"/api/v1/topaz/upscale_video",
|
|
114
|
+
{"model": "topaz-upscale-video", "source_video_url": "https://x/y.mp4", "upscale_factor": 2},
|
|
115
|
+
),
|
|
116
|
+
]
|
|
117
|
+
assert isinstance(result, UpscaleVideoResponse)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def test_run_narrows_completed_image_type():
|
|
121
|
+
fake = FakeHttp(
|
|
122
|
+
{"id": "t1", "status": "pending"},
|
|
123
|
+
{"id": "t1", "status": "completed", "images": [{"url": "https://x/y.png"}]},
|
|
124
|
+
)
|
|
125
|
+
client = TopazClient(api_key="k", http_client=fake)
|
|
126
|
+
result = client.upscale_image.run(
|
|
127
|
+
model="topaz-upscale-image", source_image_url="https://x/y.jpg", upscale_factor=4
|
|
128
|
+
)
|
|
129
|
+
assert isinstance(result, CompletedUpscaleImageResponse)
|
|
130
|
+
assert result.images[0].url == "https://x/y.png"
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def test_run_narrows_completed_video_type():
|
|
134
|
+
fake = FakeHttp(
|
|
135
|
+
{"id": "t1", "status": "pending"},
|
|
136
|
+
{"id": "t1", "status": "completed", "videos": [{"url": "https://x/y.mp4"}]},
|
|
137
|
+
)
|
|
138
|
+
client = TopazClient(api_key="k", http_client=fake)
|
|
139
|
+
result = client.upscale_video.run(
|
|
140
|
+
model="topaz-upscale-video", source_video_url="https://x/y.mp4", upscale_factor=2
|
|
141
|
+
)
|
|
142
|
+
assert isinstance(result, CompletedUpscaleVideoResponse)
|
|
143
|
+
assert result.videos[0].url == "https://x/y.mp4"
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
# --- validation -----------------------------------------------------------
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def test_image_requires_model():
|
|
150
|
+
client = TopazClient(api_key="k", http_client=FakeHttp())
|
|
151
|
+
with pytest.raises(ValidationError, match="model must be one of: topaz-upscale-image"):
|
|
152
|
+
client.upscale_image.create(source_image_url="https://x/y.jpg", upscale_factor=4)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def test_image_requires_source_image_url():
|
|
156
|
+
client = TopazClient(api_key="k", http_client=FakeHttp())
|
|
157
|
+
with pytest.raises(ValidationError, match="source_image_url is required"):
|
|
158
|
+
client.upscale_image.create(model="topaz-upscale-image", upscale_factor=4)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def test_image_requires_upscale_factor():
|
|
162
|
+
client = TopazClient(api_key="k", http_client=FakeHttp())
|
|
163
|
+
with pytest.raises(ValidationError, match="upscale_factor is required"):
|
|
164
|
+
client.upscale_image.create(model="topaz-upscale-image", source_image_url="https://x/y.jpg")
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def test_image_rejects_bad_factor():
|
|
168
|
+
client = TopazClient(api_key="k", http_client=FakeHttp())
|
|
169
|
+
with pytest.raises(ValidationError, match="upscale_factor must be one of: 1, 2, 4, 8"):
|
|
170
|
+
client.upscale_image.create(
|
|
171
|
+
model="topaz-upscale-image", source_image_url="https://x/y.jpg", upscale_factor=3
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def test_video_requires_model():
|
|
176
|
+
client = TopazClient(api_key="k", http_client=FakeHttp())
|
|
177
|
+
with pytest.raises(ValidationError, match="model must be one of: topaz-upscale-video"):
|
|
178
|
+
client.upscale_video.create(source_video_url="https://x/y.mp4")
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def test_video_requires_source_video_url():
|
|
182
|
+
client = TopazClient(api_key="k", http_client=FakeHttp())
|
|
183
|
+
with pytest.raises(ValidationError, match="source_video_url is required"):
|
|
184
|
+
client.upscale_video.create(model="topaz-upscale-video")
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def test_video_allows_default_upscale_factor():
|
|
188
|
+
fake = FakeHttp({"id": "t1", "status": "pending"})
|
|
189
|
+
client = TopazClient(api_key="k", http_client=fake)
|
|
190
|
+
client.upscale_video.create(
|
|
191
|
+
model="topaz-upscale-video", source_video_url="https://x/y.mp4"
|
|
192
|
+
)
|
|
193
|
+
_, _, body = fake.calls[0]
|
|
194
|
+
assert "upscale_factor" not in body
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def test_video_rejects_bad_factor():
|
|
198
|
+
client = TopazClient(api_key="k", http_client=FakeHttp())
|
|
199
|
+
with pytest.raises(ValidationError, match="upscale_factor must be one of: 1, 2, 4"):
|
|
200
|
+
client.upscale_video.create(
|
|
201
|
+
model="topaz-upscale-video", source_video_url="https://x/y.mp4", upscale_factor=8
|
|
202
|
+
)
|