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.
@@ -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
+ )