runapi-midjourney 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_midjourney-0.1.0/PKG-INFO +51 -0
- runapi_midjourney-0.1.0/README.md +36 -0
- runapi_midjourney-0.1.0/pyproject.toml +32 -0
- runapi_midjourney-0.1.0/src/runapi/midjourney/__init__.py +24 -0
- runapi_midjourney-0.1.0/src/runapi/midjourney/client.py +25 -0
- runapi_midjourney-0.1.0/src/runapi/midjourney/contract_gen.py +58 -0
- runapi_midjourney-0.1.0/src/runapi/midjourney/py.typed +0 -0
- runapi_midjourney-0.1.0/src/runapi/midjourney/resources/__init__.py +7 -0
- runapi_midjourney-0.1.0/src/runapi/midjourney/resources/edit_image.py +29 -0
- runapi_midjourney-0.1.0/src/runapi/midjourney/resources/get_seed.py +18 -0
- runapi_midjourney-0.1.0/src/runapi/midjourney/resources/image_to_prompt.py +18 -0
- runapi_midjourney-0.1.0/src/runapi/midjourney/resources/image_to_video.py +29 -0
- runapi_midjourney-0.1.0/src/runapi/midjourney/resources/text_to_image.py +29 -0
- runapi_midjourney-0.1.0/src/runapi/midjourney/types.py +44 -0
- runapi_midjourney-0.1.0/tests/test_client.py +132 -0
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: runapi-midjourney
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: RunAPI Midjourney SDK for image generation, editing, image-to-video, and image helper workflows in JavaScript, Python, Ruby, Go, Java, and PHP
|
|
5
|
+
Project-URL: Homepage, https://runapi.ai/models/midjourney
|
|
6
|
+
Project-URL: Documentation, https://runapi.ai/docs#sdk-midjourney
|
|
7
|
+
Project-URL: Source, https://github.com/runapi-ai/midjourney-sdk
|
|
8
|
+
Project-URL: Issues, https://github.com/runapi-ai/midjourney-sdk/issues
|
|
9
|
+
Author-email: RunAPI <contact@runapi.ai>
|
|
10
|
+
License-Expression: Apache-2.0
|
|
11
|
+
Keywords: api,golang,gradle,image-generation,java,maven,midjourney,midjourney-api,python,ruby,runapi,runapi-ai,sdk,typescript,video-generation
|
|
12
|
+
Requires-Python: >=3.9
|
|
13
|
+
Requires-Dist: runapi-core>=0.1.5
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
|
|
16
|
+
# Midjourney Python SDK for RunAPI
|
|
17
|
+
|
|
18
|
+
Use `runapi-midjourney` for Midjourney image generation, editing, image-to-video, image-to-prompt, and seed lookup in Python applications and workers.
|
|
19
|
+
|
|
20
|
+
## Install
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
pip install runapi-midjourney
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Quick Start
|
|
27
|
+
|
|
28
|
+
```python
|
|
29
|
+
from runapi.midjourney import MidjourneyClient
|
|
30
|
+
|
|
31
|
+
client = MidjourneyClient()
|
|
32
|
+
result = client.text_to_image.run(
|
|
33
|
+
model="midjourney-v8.1",
|
|
34
|
+
prompt="A cobalt blue ceramic vase on white marble",
|
|
35
|
+
)
|
|
36
|
+
print(result.images[0].url)
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Use `create`, `get`, and `run` for `text_to_image`, `edit_image`, and `image_to_video`. Use `run` directly for `image_to_prompt` and `get_seed`. Generated media URLs are temporary and should be stored in durable storage.
|
|
40
|
+
|
|
41
|
+
## Links
|
|
42
|
+
|
|
43
|
+
- Model page: https://runapi.ai/models/midjourney
|
|
44
|
+
- Product docs: https://runapi.ai/docs#midjourney
|
|
45
|
+
- SDK docs: https://runapi.ai/docs#sdk-midjourney
|
|
46
|
+
- V8.1 details: https://runapi.ai/models/midjourney/v8.1
|
|
47
|
+
- Image editing details: https://runapi.ai/models/midjourney/edit-image
|
|
48
|
+
- Image-to-video details: https://runapi.ai/models/midjourney/image-to-video
|
|
49
|
+
- Repository: https://github.com/runapi-ai/midjourney-sdk
|
|
50
|
+
|
|
51
|
+
Licensed under the Apache License, Version 2.0.
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Midjourney Python SDK for RunAPI
|
|
2
|
+
|
|
3
|
+
Use `runapi-midjourney` for Midjourney image generation, editing, image-to-video, image-to-prompt, and seed lookup in Python applications and workers.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install runapi-midjourney
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
```python
|
|
14
|
+
from runapi.midjourney import MidjourneyClient
|
|
15
|
+
|
|
16
|
+
client = MidjourneyClient()
|
|
17
|
+
result = client.text_to_image.run(
|
|
18
|
+
model="midjourney-v8.1",
|
|
19
|
+
prompt="A cobalt blue ceramic vase on white marble",
|
|
20
|
+
)
|
|
21
|
+
print(result.images[0].url)
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Use `create`, `get`, and `run` for `text_to_image`, `edit_image`, and `image_to_video`. Use `run` directly for `image_to_prompt` and `get_seed`. Generated media URLs are temporary and should be stored in durable storage.
|
|
25
|
+
|
|
26
|
+
## Links
|
|
27
|
+
|
|
28
|
+
- Model page: https://runapi.ai/models/midjourney
|
|
29
|
+
- Product docs: https://runapi.ai/docs#midjourney
|
|
30
|
+
- SDK docs: https://runapi.ai/docs#sdk-midjourney
|
|
31
|
+
- V8.1 details: https://runapi.ai/models/midjourney/v8.1
|
|
32
|
+
- Image editing details: https://runapi.ai/models/midjourney/edit-image
|
|
33
|
+
- Image-to-video details: https://runapi.ai/models/midjourney/image-to-video
|
|
34
|
+
- Repository: https://github.com/runapi-ai/midjourney-sdk
|
|
35
|
+
|
|
36
|
+
Licensed under the Apache License, Version 2.0.
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "runapi-midjourney"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "RunAPI Midjourney SDK for image generation, editing, image-to-video, and image helper workflows in JavaScript, Python, Ruby, Go, Java, and PHP"
|
|
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", "runapi-ai", "midjourney", "api", "sdk", "typescript", "python", "ruby", "golang", "java", "maven", "gradle", "image-generation", "video-generation", "midjourney-api"]
|
|
14
|
+
dependencies = ["runapi-core>=0.1.5"]
|
|
15
|
+
|
|
16
|
+
[project.urls]
|
|
17
|
+
Homepage = "https://runapi.ai/models/midjourney"
|
|
18
|
+
Documentation = "https://runapi.ai/docs#sdk-midjourney"
|
|
19
|
+
Source = "https://github.com/runapi-ai/midjourney-sdk"
|
|
20
|
+
Issues = "https://github.com/runapi-ai/midjourney-sdk/issues"
|
|
21
|
+
|
|
22
|
+
[tool.hatch.build.targets.wheel]
|
|
23
|
+
packages = ["src/runapi"]
|
|
24
|
+
|
|
25
|
+
[tool.uv]
|
|
26
|
+
package = true
|
|
27
|
+
|
|
28
|
+
[dependency-groups]
|
|
29
|
+
dev = ["pytest>=8"]
|
|
30
|
+
|
|
31
|
+
[tool.runapi]
|
|
32
|
+
slug = "midjourney"
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""Midjourney 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 MidjourneyClient
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"MidjourneyClient",
|
|
17
|
+
"AuthenticationError",
|
|
18
|
+
"RateLimitError",
|
|
19
|
+
"InsufficientCreditsError",
|
|
20
|
+
"NotFoundError",
|
|
21
|
+
"ValidationError",
|
|
22
|
+
"TaskFailedError",
|
|
23
|
+
"TaskTimeoutError",
|
|
24
|
+
]
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Midjourney client."""
|
|
2
|
+
|
|
3
|
+
from typing import Any, Optional
|
|
4
|
+
|
|
5
|
+
from runapi.core import ClientOptions, HttpClient, resolve_api_key
|
|
6
|
+
|
|
7
|
+
from .resources.edit_image import EditImage
|
|
8
|
+
from .resources.get_seed import GetSeed
|
|
9
|
+
from .resources.image_to_prompt import ImageToPrompt
|
|
10
|
+
from .resources.image_to_video import ImageToVideo
|
|
11
|
+
from .resources.text_to_image import TextToImage
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class MidjourneyClient:
|
|
15
|
+
"""Midjourney image generation, editing, image-to-video, and helper client."""
|
|
16
|
+
|
|
17
|
+
def __init__(self, api_key: Optional[str] = None, **options: Any) -> None:
|
|
18
|
+
resolved_api_key = resolve_api_key(api_key)
|
|
19
|
+
client_options = ClientOptions(api_key=resolved_api_key, **options)
|
|
20
|
+
http = client_options.http_client or HttpClient(client_options)
|
|
21
|
+
self.text_to_image = TextToImage(http)
|
|
22
|
+
self.edit_image = EditImage(http)
|
|
23
|
+
self.image_to_video = ImageToVideo(http)
|
|
24
|
+
self.image_to_prompt = ImageToPrompt(http)
|
|
25
|
+
self.get_seed = GetSeed(http)
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
CONTRACT = {
|
|
2
|
+
"edit-image": {
|
|
3
|
+
"models": ["midjourney-edit-image"],
|
|
4
|
+
"fields_by_model": {
|
|
5
|
+
"midjourney-edit-image": {
|
|
6
|
+
"prompt": {
|
|
7
|
+
"required": True
|
|
8
|
+
},
|
|
9
|
+
"source_image_url": {
|
|
10
|
+
"required": True
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"get-seed": {
|
|
16
|
+
"models": [],
|
|
17
|
+
"fields_by_model": {
|
|
18
|
+
"_": {
|
|
19
|
+
"image_id": {
|
|
20
|
+
"required": True
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"image-to-prompt": {
|
|
26
|
+
"models": [],
|
|
27
|
+
"fields_by_model": {
|
|
28
|
+
"_": {
|
|
29
|
+
"source_image_url": {
|
|
30
|
+
"required": True
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
"image-to-video": {
|
|
36
|
+
"models": ["midjourney-image-to-video"],
|
|
37
|
+
"fields_by_model": {
|
|
38
|
+
"midjourney-image-to-video": {
|
|
39
|
+
"output_resolution": {
|
|
40
|
+
"enum": ["480p"]
|
|
41
|
+
},
|
|
42
|
+
"source_image_url": {
|
|
43
|
+
"required": True
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
"text-to-image": {
|
|
49
|
+
"models": ["midjourney-v8.1"],
|
|
50
|
+
"fields_by_model": {
|
|
51
|
+
"midjourney-v8.1": {
|
|
52
|
+
"prompt": {
|
|
53
|
+
"required": True
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
File without changes
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
from .edit_image import EditImage
|
|
2
|
+
from .get_seed import GetSeed
|
|
3
|
+
from .image_to_prompt import ImageToPrompt
|
|
4
|
+
from .image_to_video import ImageToVideo
|
|
5
|
+
from .text_to_image import TextToImage
|
|
6
|
+
|
|
7
|
+
__all__ = ["TextToImage", "EditImage", "ImageToVideo", "ImageToPrompt", "GetSeed"]
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""Midjourney edit-image resource."""
|
|
2
|
+
|
|
3
|
+
from typing import Any, Dict, Optional
|
|
4
|
+
|
|
5
|
+
from runapi.core import RequestOptions, Resource
|
|
6
|
+
|
|
7
|
+
from ..contract_gen import CONTRACT
|
|
8
|
+
from ..types import CompletedImageTaskResponse, ImageTaskResponse
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class EditImage(Resource):
|
|
12
|
+
ENDPOINT = "/api/v1/midjourney/edit_image"
|
|
13
|
+
RESPONSE_CLASS = ImageTaskResponse
|
|
14
|
+
COMPLETED_RESPONSE_CLASS = CompletedImageTaskResponse
|
|
15
|
+
|
|
16
|
+
def run(self, options: Optional[RequestOptions] = None, **params: Any) -> Any:
|
|
17
|
+
task = self.create(options=options, **params)
|
|
18
|
+
return self._poll_until_complete(lambda: self.get(task.id, options=options))
|
|
19
|
+
|
|
20
|
+
def create(self, options: Optional[RequestOptions] = None, **params: Any) -> Any:
|
|
21
|
+
compacted = self._compact_params(params)
|
|
22
|
+
self._validate_params(compacted)
|
|
23
|
+
return self._request("post", self.ENDPOINT, body=compacted, options=options)
|
|
24
|
+
|
|
25
|
+
def get(self, id: str, options: Optional[RequestOptions] = None) -> Any:
|
|
26
|
+
return self._request("get", f"{self.ENDPOINT}/{id}", options=options)
|
|
27
|
+
|
|
28
|
+
def _validate_params(self, params: Dict[str, Any]) -> None:
|
|
29
|
+
self._validate_contract(CONTRACT["edit-image"], params)
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""Midjourney get-seed resource."""
|
|
2
|
+
|
|
3
|
+
from typing import Any, Optional
|
|
4
|
+
|
|
5
|
+
from runapi.core import RequestOptions, Resource
|
|
6
|
+
|
|
7
|
+
from ..contract_gen import CONTRACT
|
|
8
|
+
from ..types import GetSeedResponse
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class GetSeed(Resource):
|
|
12
|
+
ENDPOINT = "/api/v1/midjourney/get_seed"
|
|
13
|
+
RESPONSE_CLASS = GetSeedResponse
|
|
14
|
+
|
|
15
|
+
def run(self, options: Optional[RequestOptions] = None, **params: Any) -> Any:
|
|
16
|
+
compacted = self._compact_params(params)
|
|
17
|
+
self._validate_contract(CONTRACT["get-seed"], compacted)
|
|
18
|
+
return self._request("post", self.ENDPOINT, body=compacted, options=options)
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""Midjourney image-to-prompt resource."""
|
|
2
|
+
|
|
3
|
+
from typing import Any, Dict, Optional
|
|
4
|
+
|
|
5
|
+
from runapi.core import RequestOptions, Resource
|
|
6
|
+
|
|
7
|
+
from ..contract_gen import CONTRACT
|
|
8
|
+
from ..types import ImageToPromptResponse
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ImageToPrompt(Resource):
|
|
12
|
+
ENDPOINT = "/api/v1/midjourney/image_to_prompt"
|
|
13
|
+
RESPONSE_CLASS = ImageToPromptResponse
|
|
14
|
+
|
|
15
|
+
def run(self, options: Optional[RequestOptions] = None, **params: Any) -> Any:
|
|
16
|
+
compacted = self._compact_params(params)
|
|
17
|
+
self._validate_contract(CONTRACT["image-to-prompt"], compacted)
|
|
18
|
+
return self._request("post", self.ENDPOINT, body=compacted, options=options)
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""Midjourney image-to-video resource."""
|
|
2
|
+
|
|
3
|
+
from typing import Any, Dict, Optional
|
|
4
|
+
|
|
5
|
+
from runapi.core import RequestOptions, Resource
|
|
6
|
+
|
|
7
|
+
from ..contract_gen import CONTRACT
|
|
8
|
+
from ..types import CompletedVideoTaskResponse, VideoTaskResponse
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ImageToVideo(Resource):
|
|
12
|
+
ENDPOINT = "/api/v1/midjourney/image_to_video"
|
|
13
|
+
RESPONSE_CLASS = VideoTaskResponse
|
|
14
|
+
COMPLETED_RESPONSE_CLASS = CompletedVideoTaskResponse
|
|
15
|
+
|
|
16
|
+
def run(self, options: Optional[RequestOptions] = None, **params: Any) -> Any:
|
|
17
|
+
task = self.create(options=options, **params)
|
|
18
|
+
return self._poll_until_complete(lambda: self.get(task.id, options=options))
|
|
19
|
+
|
|
20
|
+
def create(self, options: Optional[RequestOptions] = None, **params: Any) -> Any:
|
|
21
|
+
compacted = self._compact_params(params)
|
|
22
|
+
self._validate_params(compacted)
|
|
23
|
+
return self._request("post", self.ENDPOINT, body=compacted, options=options)
|
|
24
|
+
|
|
25
|
+
def get(self, id: str, options: Optional[RequestOptions] = None) -> Any:
|
|
26
|
+
return self._request("get", f"{self.ENDPOINT}/{id}", options=options)
|
|
27
|
+
|
|
28
|
+
def _validate_params(self, params: Dict[str, Any]) -> None:
|
|
29
|
+
self._validate_contract(CONTRACT["image-to-video"], params)
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""Midjourney text-to-image resource."""
|
|
2
|
+
|
|
3
|
+
from typing import Any, Dict, Optional
|
|
4
|
+
|
|
5
|
+
from runapi.core import RequestOptions, Resource
|
|
6
|
+
|
|
7
|
+
from ..contract_gen import CONTRACT
|
|
8
|
+
from ..types import CompletedImageTaskResponse, ImageTaskResponse
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class TextToImage(Resource):
|
|
12
|
+
ENDPOINT = "/api/v1/midjourney/text_to_image"
|
|
13
|
+
RESPONSE_CLASS = ImageTaskResponse
|
|
14
|
+
COMPLETED_RESPONSE_CLASS = CompletedImageTaskResponse
|
|
15
|
+
|
|
16
|
+
def run(self, options: Optional[RequestOptions] = None, **params: Any) -> Any:
|
|
17
|
+
task = self.create(options=options, **params)
|
|
18
|
+
return self._poll_until_complete(lambda: self.get(task.id, options=options))
|
|
19
|
+
|
|
20
|
+
def create(self, options: Optional[RequestOptions] = None, **params: Any) -> Any:
|
|
21
|
+
compacted = self._compact_params(params)
|
|
22
|
+
self._validate_params(compacted)
|
|
23
|
+
return self._request("post", self.ENDPOINT, body=compacted, options=options)
|
|
24
|
+
|
|
25
|
+
def get(self, id: str, options: Optional[RequestOptions] = None) -> Any:
|
|
26
|
+
return self._request("get", f"{self.ENDPOINT}/{id}", options=options)
|
|
27
|
+
|
|
28
|
+
def _validate_params(self, params: Dict[str, Any]) -> None:
|
|
29
|
+
self._validate_contract(CONTRACT["text-to-image"], params)
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""Midjourney response models."""
|
|
2
|
+
|
|
3
|
+
from runapi.core import BaseModel, TaskResponse, optional, required
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class MediaUrl(BaseModel):
|
|
7
|
+
url = optional(str)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ImageTaskResponse(TaskResponse):
|
|
11
|
+
id = required(str)
|
|
12
|
+
status = optional(str, enum=lambda: TaskResponse.Status.ALL)
|
|
13
|
+
images = optional([lambda: MediaUrl])
|
|
14
|
+
image_id = optional(str)
|
|
15
|
+
actions = optional([str])
|
|
16
|
+
progress = optional(int)
|
|
17
|
+
error = optional(str)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class CompletedImageTaskResponse(ImageTaskResponse):
|
|
21
|
+
images = required([lambda: MediaUrl])
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class VideoTaskResponse(TaskResponse):
|
|
25
|
+
id = required(str)
|
|
26
|
+
status = optional(str, enum=lambda: TaskResponse.Status.ALL)
|
|
27
|
+
videos = optional([lambda: MediaUrl])
|
|
28
|
+
video_id = optional(str)
|
|
29
|
+
progress = optional(int)
|
|
30
|
+
error = optional(str)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class CompletedVideoTaskResponse(VideoTaskResponse):
|
|
34
|
+
videos = required([lambda: MediaUrl])
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class ImageToPromptResponse(BaseModel):
|
|
38
|
+
prompts = required([str])
|
|
39
|
+
error = optional(str)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class GetSeedResponse(BaseModel):
|
|
43
|
+
seed = optional(int)
|
|
44
|
+
error = optional(str)
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
|
|
3
|
+
from runapi.core import config
|
|
4
|
+
from runapi.core.errors import AuthenticationError, ValidationError
|
|
5
|
+
from runapi.midjourney import MidjourneyClient
|
|
6
|
+
from runapi.midjourney.resources import EditImage, GetSeed, ImageToPrompt, ImageToVideo, TextToImage
|
|
7
|
+
from runapi.midjourney.types import GetSeedResponse, ImageToPromptResponse
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class FakeHttp:
|
|
11
|
+
def __init__(self, *responses):
|
|
12
|
+
self._responses = list(responses)
|
|
13
|
+
self.calls = []
|
|
14
|
+
|
|
15
|
+
def request(self, method, path, body=None, options=None):
|
|
16
|
+
self.calls.append((method, path, body))
|
|
17
|
+
if self._responses:
|
|
18
|
+
return self._responses.pop(0)
|
|
19
|
+
return {"id": "task_1", "status": "processing"}
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@pytest.fixture(autouse=True)
|
|
23
|
+
def reset_config(monkeypatch):
|
|
24
|
+
monkeypatch.delenv("RUNAPI_API_KEY", raising=False)
|
|
25
|
+
monkeypatch.setattr(config, "api_key", None)
|
|
26
|
+
yield
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def test_requires_api_key():
|
|
30
|
+
with pytest.raises(AuthenticationError, match="API key is required"):
|
|
31
|
+
MidjourneyClient()
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def test_exposes_resources_with_injected_http():
|
|
35
|
+
fake = FakeHttp()
|
|
36
|
+
client = MidjourneyClient(api_key="k", http_client=fake)
|
|
37
|
+
|
|
38
|
+
assert isinstance(client.text_to_image, TextToImage)
|
|
39
|
+
assert isinstance(client.edit_image, EditImage)
|
|
40
|
+
assert isinstance(client.image_to_video, ImageToVideo)
|
|
41
|
+
assert isinstance(client.image_to_prompt, ImageToPrompt)
|
|
42
|
+
assert isinstance(client.get_seed, GetSeed)
|
|
43
|
+
assert client.text_to_image._http is fake
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def test_async_resources_use_public_paths_and_flat_bodies():
|
|
47
|
+
fake = FakeHttp()
|
|
48
|
+
client = MidjourneyClient(api_key="k", http_client=fake)
|
|
49
|
+
|
|
50
|
+
client.text_to_image.create(
|
|
51
|
+
model="midjourney-v8.1",
|
|
52
|
+
prompt="A cobalt vase",
|
|
53
|
+
include_split_images=False,
|
|
54
|
+
)
|
|
55
|
+
client.text_to_image.get("task_1")
|
|
56
|
+
client.edit_image.create(
|
|
57
|
+
model="midjourney-edit-image",
|
|
58
|
+
prompt="Change the vase color",
|
|
59
|
+
source_image_url="https://cdn.runapi.ai/public/samples/source.png",
|
|
60
|
+
mask_url="https://cdn.runapi.ai/public/samples/mask.png",
|
|
61
|
+
)
|
|
62
|
+
client.image_to_video.create(
|
|
63
|
+
model="midjourney-image-to-video",
|
|
64
|
+
source_image_url="https://cdn.runapi.ai/public/samples/source.png",
|
|
65
|
+
output_resolution="480p",
|
|
66
|
+
enable_loop=False,
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
assert fake.calls == [
|
|
70
|
+
(
|
|
71
|
+
"post",
|
|
72
|
+
"/api/v1/midjourney/text_to_image",
|
|
73
|
+
{"model": "midjourney-v8.1", "prompt": "A cobalt vase", "include_split_images": False},
|
|
74
|
+
),
|
|
75
|
+
("get", "/api/v1/midjourney/text_to_image/task_1", None),
|
|
76
|
+
(
|
|
77
|
+
"post",
|
|
78
|
+
"/api/v1/midjourney/edit_image",
|
|
79
|
+
{
|
|
80
|
+
"model": "midjourney-edit-image",
|
|
81
|
+
"prompt": "Change the vase color",
|
|
82
|
+
"source_image_url": "https://cdn.runapi.ai/public/samples/source.png",
|
|
83
|
+
"mask_url": "https://cdn.runapi.ai/public/samples/mask.png",
|
|
84
|
+
},
|
|
85
|
+
),
|
|
86
|
+
(
|
|
87
|
+
"post",
|
|
88
|
+
"/api/v1/midjourney/image_to_video",
|
|
89
|
+
{
|
|
90
|
+
"model": "midjourney-image-to-video",
|
|
91
|
+
"source_image_url": "https://cdn.runapi.ai/public/samples/source.png",
|
|
92
|
+
"output_resolution": "480p",
|
|
93
|
+
"enable_loop": False,
|
|
94
|
+
},
|
|
95
|
+
),
|
|
96
|
+
]
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def test_synchronous_helpers_decode_responses():
|
|
100
|
+
fake = FakeHttp(
|
|
101
|
+
{"prompts": ["one", "two", "three", "four"]},
|
|
102
|
+
{"seed": 8675309},
|
|
103
|
+
)
|
|
104
|
+
client = MidjourneyClient(api_key="k", http_client=fake)
|
|
105
|
+
|
|
106
|
+
prompts = client.image_to_prompt.run(source_image_url="https://cdn.runapi.ai/public/samples/source.png")
|
|
107
|
+
seed = client.get_seed.run(image_id="image-1")
|
|
108
|
+
|
|
109
|
+
assert isinstance(prompts, ImageToPromptResponse)
|
|
110
|
+
assert prompts.prompts == ["one", "two", "three", "four"]
|
|
111
|
+
assert isinstance(seed, GetSeedResponse)
|
|
112
|
+
assert seed.seed == 8675309
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def test_get_seed_preserves_error_response_without_seed():
|
|
116
|
+
fake = FakeHttp({"error": "Seed lookup failed"})
|
|
117
|
+
client = MidjourneyClient(api_key="k", http_client=fake)
|
|
118
|
+
|
|
119
|
+
result = client.get_seed.run(image_id="image-1")
|
|
120
|
+
|
|
121
|
+
assert isinstance(result, GetSeedResponse)
|
|
122
|
+
assert result.seed is None
|
|
123
|
+
assert result.error == "Seed lookup failed"
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def test_edit_image_requires_prompt_from_generated_contract():
|
|
127
|
+
client = MidjourneyClient(api_key="k", http_client=FakeHttp())
|
|
128
|
+
with pytest.raises(ValidationError, match="prompt is required"):
|
|
129
|
+
client.edit_image.create(
|
|
130
|
+
model="midjourney-edit-image",
|
|
131
|
+
source_image_url="https://cdn.runapi.ai/public/samples/source.png",
|
|
132
|
+
)
|