model-compose 0.1.3__tar.gz → 0.1.5__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.
- {model_compose-0.1.3 → model_compose-0.1.5}/PKG-INFO +1 -1
- {model_compose-0.1.3 → model_compose-0.1.5}/pyproject.toml +1 -1
- {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/core/component/engine/context.py +2 -2
- {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/core/component/engine/http_client.py +1 -1
- model_compose-0.1.5/src/mindor/core/controller/runner/http_client.py +68 -0
- {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/core/controller/webui/gradio.py +69 -34
- {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/core/controller/webui/schema.py +27 -17
- {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/core/listener/engine/http_callback.py +3 -3
- model_compose-0.1.5/src/mindor/core/utils/http_client.py +97 -0
- {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/core/utils/http_request.py +39 -4
- {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/core/utils/streaming.py +8 -2
- {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/core/utils/template.py +34 -11
- {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/core/workflow/context.py +2 -2
- {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/core/workflow/job.py +1 -1
- {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/dsl/schema/workflow.py +2 -1
- {model_compose-0.1.3 → model_compose-0.1.5}/src/model_compose.egg-info/PKG-INFO +1 -1
- model_compose-0.1.3/src/mindor/core/controller/runner/http_client.py +0 -26
- model_compose-0.1.3/src/mindor/core/utils/http_client.py +0 -73
- {model_compose-0.1.3 → model_compose-0.1.5}/LICENSE +0 -0
- {model_compose-0.1.3 → model_compose-0.1.5}/README.md +0 -0
- {model_compose-0.1.3 → model_compose-0.1.5}/setup.cfg +0 -0
- {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/__init__.py +0 -0
- {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/cli/__init__.py +0 -0
- {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/cli/compose.py +0 -0
- {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/core/__init__.py +0 -0
- {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/core/component/__init__.py +0 -0
- {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/core/component/component.py +0 -0
- {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/core/component/engine/__init__.py +0 -0
- {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/core/component/engine/base.py +0 -0
- {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/core/component/engine/http_server.py +0 -0
- {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/core/compose/__init__.py +0 -0
- {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/core/compose/compose.py +0 -0
- {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/core/compose/manager.py +0 -0
- {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/core/controller/__init__.py +0 -0
- {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/core/controller/controller.py +0 -0
- {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/core/controller/engine/__init__.py +0 -0
- {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/core/controller/engine/base.py +0 -0
- {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/core/controller/engine/http_server.py +0 -0
- {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/core/controller/engine/mcp_server.py +0 -0
- {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/core/controller/runner/__init__.py +0 -0
- {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/core/controller/runner/client.py +0 -0
- {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/core/controller/runner/mcp_client.py +0 -0
- {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/core/controller/runner/runner.py +0 -0
- {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/core/controller/webui/__init__.py +0 -0
- {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/core/controller/webui/webui.py +0 -0
- {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/core/gateway/__init__.py +0 -0
- {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/core/gateway/engine/__init__.py +0 -0
- {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/core/gateway/engine/base.py +0 -0
- {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/core/gateway/engine/http_tunnel.py +0 -0
- {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/core/gateway/gateway.py +0 -0
- {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/core/listener/__init__.py +0 -0
- {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/core/listener/engine/__init__.py +0 -0
- {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/core/listener/engine/base.py +0 -0
- {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/core/listener/listener.py +0 -0
- {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/core/runtime/__init__.py +0 -0
- {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/core/runtime/env.py +0 -0
- {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/core/services/__init__.py +0 -0
- {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/core/services/async_service.py +0 -0
- {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/core/utils/__init__.py +0 -0
- {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/core/utils/expiring.py +0 -0
- {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/core/utils/image.py +0 -0
- {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/core/utils/time.py +0 -0
- {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/core/utils/workqueue.py +0 -0
- {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/core/workflow/__init__.py +0 -0
- {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/core/workflow/workflow.py +0 -0
- {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/dsl/__init__.py +0 -0
- {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/dsl/loader.py +0 -0
- {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/dsl/schema/__init__.py +0 -0
- {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/dsl/schema/action/__init__.py +0 -0
- {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/dsl/schema/action/action.py +0 -0
- {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/dsl/schema/action/impl/__init__.py +0 -0
- {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/dsl/schema/action/impl/common.py +0 -0
- {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/dsl/schema/action/impl/http_client.py +0 -0
- {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/dsl/schema/action/impl/http_server.py +0 -0
- {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/dsl/schema/action/impl/mcp_client.py +0 -0
- {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/dsl/schema/action/impl/mcp_server.py +0 -0
- {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/dsl/schema/component/__init__.py +0 -0
- {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/dsl/schema/component/component.py +0 -0
- {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/dsl/schema/component/impl/__init__.py +0 -0
- {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/dsl/schema/component/impl/common.py +0 -0
- {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/dsl/schema/component/impl/http_client.py +0 -0
- {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/dsl/schema/component/impl/http_server.py +0 -0
- {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/dsl/schema/component/impl/mcp_client.py +0 -0
- {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/dsl/schema/component/impl/mcp_server.py +0 -0
- {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/dsl/schema/component/impl/types.py +0 -0
- {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/dsl/schema/compose.py +0 -0
- {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/dsl/schema/controller/__init__.py +0 -0
- {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/dsl/schema/controller/controller.py +0 -0
- {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/dsl/schema/controller/impl/__init__.py +0 -0
- {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/dsl/schema/controller/impl/common.py +0 -0
- {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/dsl/schema/controller/impl/http_server.py +0 -0
- {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/dsl/schema/controller/impl/mcp_server.py +0 -0
- {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/dsl/schema/controller/impl/types.py +0 -0
- {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/dsl/schema/controller/impl/webui.py +0 -0
- {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/dsl/schema/gateway/__init__.py +0 -0
- {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/dsl/schema/gateway/gateway.py +0 -0
- {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/dsl/schema/gateway/impl/__init__.py +0 -0
- {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/dsl/schema/gateway/impl/common.py +0 -0
- {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/dsl/schema/gateway/impl/http_tunnel.py +0 -0
- {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/dsl/schema/gateway/impl/types.py +0 -0
- {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/dsl/schema/listener/__init__.py +0 -0
- {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/dsl/schema/listener/impl/__init__.py +0 -0
- {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/dsl/schema/listener/impl/common.py +0 -0
- {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/dsl/schema/listener/impl/http_callback.py +0 -0
- {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/dsl/schema/listener/impl/types.py +0 -0
- {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/dsl/schema/listener/listener.py +0 -0
- {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/dsl/schema/runtime/__init__.py +0 -0
- {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/dsl/schema/runtime/impl/__init__.py +0 -0
- {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/dsl/schema/runtime/impl/common.py +0 -0
- {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/dsl/schema/runtime/impl/docker.py +0 -0
- {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/dsl/schema/runtime/impl/types.py +0 -0
- {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/dsl/schema/runtime/runtime.py +0 -0
- {model_compose-0.1.3 → model_compose-0.1.5}/src/model_compose.egg-info/SOURCES.txt +0 -0
- {model_compose-0.1.3 → model_compose-0.1.5}/src/model_compose.egg-info/dependency_links.txt +0 -0
- {model_compose-0.1.3 → model_compose-0.1.5}/src/model_compose.egg-info/entry_points.txt +0 -0
- {model_compose-0.1.3 → model_compose-0.1.5}/src/model_compose.egg-info/requires.txt +0 -0
- {model_compose-0.1.3 → model_compose-0.1.5}/src/model_compose.egg-info/top_level.txt +0 -0
- {model_compose-0.1.3 → model_compose-0.1.5}/tests/test_cli.py +0 -0
|
@@ -14,8 +14,8 @@ class ComponentContext:
|
|
|
14
14
|
def register_source(self, key: str, source: Any) -> None:
|
|
15
15
|
self.sources[key] = source
|
|
16
16
|
|
|
17
|
-
async def render_template(self, data: Dict[str, Any]) -> Any:
|
|
18
|
-
return await self.renderer.render(data)
|
|
17
|
+
async def render_template(self, data: Dict[str, Any], ignore_files: bool = False) -> Any:
|
|
18
|
+
return await self.renderer.render(data, ignore_files)
|
|
19
19
|
|
|
20
20
|
async def _resolve_source(self, key: str) -> Any:
|
|
21
21
|
if key in self.sources:
|
|
@@ -88,7 +88,7 @@ class HttpClientAction:
|
|
|
88
88
|
result = await self._handle_completion(self.config.completion, context)
|
|
89
89
|
context.register_source("result", result)
|
|
90
90
|
|
|
91
|
-
return (await context.render_template(self.config.output)) if self.config.output else (result or response)
|
|
91
|
+
return (await context.render_template(self.config.output, ignore_files=True)) if self.config.output else (result or response)
|
|
92
92
|
|
|
93
93
|
async def _resolve_request_url(self, context: ComponentContext) -> str:
|
|
94
94
|
if self.base_url and self.config.path:
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
from typing import Type, Union, Literal, Optional, Dict, List, Tuple, Set, Annotated, Any
|
|
2
|
+
from mindor.dsl.schema.controller import ControllerConfig
|
|
3
|
+
from mindor.core.utils.http_client import HttpClient
|
|
4
|
+
from .runner import ControllerClient
|
|
5
|
+
from starlette.datastructures import UploadFile
|
|
6
|
+
|
|
7
|
+
class HttpControllerClient(ControllerClient):
|
|
8
|
+
def __init__(self, config: ControllerConfig):
|
|
9
|
+
super().__init__(config)
|
|
10
|
+
|
|
11
|
+
self.client: HttpClient = HttpClient()
|
|
12
|
+
|
|
13
|
+
async def run_workflow(self, workflow_id: Optional[str], input: Any) -> Any:
|
|
14
|
+
base_path = self.config.base_path if self.config.base_path else ""
|
|
15
|
+
url = f"http://localhost:{self.config.port}{base_path}/workflows"
|
|
16
|
+
method = "POST"
|
|
17
|
+
body = {
|
|
18
|
+
"workflow_id": workflow_id,
|
|
19
|
+
"input": input,
|
|
20
|
+
"wait_for_completion": True,
|
|
21
|
+
"output_only": True
|
|
22
|
+
}
|
|
23
|
+
headers = {
|
|
24
|
+
"Content-Type": "application/json"
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if self._contains_upload_file(input):
|
|
28
|
+
body = { key: value for key, value in self._flatten_for_multipart(body) }
|
|
29
|
+
headers = {
|
|
30
|
+
"Content-Type": "multipart/form-data"
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return await self.client.request(url, method, None, body, headers)
|
|
34
|
+
|
|
35
|
+
def _flatten_for_multipart(self, body: Dict[str, Any], key: str = "") -> List[Tuple[str, Any]]:
|
|
36
|
+
flattened = []
|
|
37
|
+
|
|
38
|
+
for subkey, value in body.items():
|
|
39
|
+
full_key = f"{key}[{subkey}]" if key else subkey
|
|
40
|
+
|
|
41
|
+
if isinstance(value, dict):
|
|
42
|
+
flattened.extend(self._flatten_for_multipart(value, full_key))
|
|
43
|
+
continue
|
|
44
|
+
|
|
45
|
+
if isinstance(value, list):
|
|
46
|
+
for item in value:
|
|
47
|
+
list_key = f"{full_key}[]"
|
|
48
|
+
if isinstance(item, (dict, list)):
|
|
49
|
+
flattened.extend(self._flatten_for_multipart({ list_key: item }))
|
|
50
|
+
else:
|
|
51
|
+
flattened.append((list_key, item))
|
|
52
|
+
continue
|
|
53
|
+
|
|
54
|
+
flattened.append((full_key, value))
|
|
55
|
+
|
|
56
|
+
return flattened
|
|
57
|
+
|
|
58
|
+
def _contains_upload_file(self, value: Any) -> bool:
|
|
59
|
+
if isinstance(value, UploadFile):
|
|
60
|
+
return True
|
|
61
|
+
|
|
62
|
+
if isinstance(value, dict):
|
|
63
|
+
return any(self._contains_upload_file(v) for v in value.values())
|
|
64
|
+
|
|
65
|
+
if isinstance(value, list):
|
|
66
|
+
return any(self._contains_upload_file(v) for v in value)
|
|
67
|
+
|
|
68
|
+
return False
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
from typing import Type, Union, Literal, Optional, Dict, List, Tuple, Set, Annotated, Callable, Awaitable, Any
|
|
2
2
|
from mindor.dsl.schema.workflow import WorkflowVariableConfig, WorkflowVariableGroupConfig
|
|
3
|
-
from mindor.core.utils.
|
|
3
|
+
from mindor.core.utils.streaming import StreamResource, Base64StreamResource
|
|
4
|
+
from mindor.core.utils.streaming import save_stream_to_temporary_file
|
|
5
|
+
from mindor.core.utils.http_request import create_upload_file
|
|
6
|
+
from mindor.core.utils.http_client import HttpClient
|
|
4
7
|
from mindor.core.utils.image import load_image_from_stream
|
|
5
|
-
from mindor.core.utils.streaming import Base64StreamResource, save_stream_to_temporary_file
|
|
6
8
|
from .schema import WorkflowSchema
|
|
9
|
+
from starlette.datastructures import UploadFile
|
|
7
10
|
import gradio as gr
|
|
8
|
-
import json
|
|
11
|
+
import json, os
|
|
9
12
|
|
|
10
13
|
class ComponentGroup:
|
|
11
14
|
def __init__(self, group: gr.Component, components: List[gr.Component]):
|
|
@@ -45,9 +48,9 @@ class GradioWebUIBuilder:
|
|
|
45
48
|
output_components = gr.Textbox(label="", lines=8, interactive=False, show_copy_button=True)
|
|
46
49
|
|
|
47
50
|
async def _run_workflow(*args):
|
|
48
|
-
input =
|
|
51
|
+
input = await self._build_input_values(args, workflow.input)
|
|
49
52
|
output = await runner(input)
|
|
50
|
-
output = await self.
|
|
53
|
+
output = await self._flatten_output_values(output, workflow.output)
|
|
51
54
|
return output[0] if len(output) == 1 else output
|
|
52
55
|
|
|
53
56
|
run_button.click(
|
|
@@ -59,11 +62,11 @@ class GradioWebUIBuilder:
|
|
|
59
62
|
return section
|
|
60
63
|
|
|
61
64
|
def _build_input_component(self, variable: WorkflowVariableConfig) -> gr.Component:
|
|
62
|
-
label = variable.name + (" *" if variable.required else "") + (f" (default: {variable.default})" if variable.default else "")
|
|
65
|
+
label = (variable.name or "") + (" *" if variable.required else "") + (f" (default: {variable.default})" if variable.default else "")
|
|
63
66
|
info = variable.description or ""
|
|
64
67
|
default = variable.default
|
|
65
68
|
|
|
66
|
-
if variable.type == "string":
|
|
69
|
+
if variable.type == "string" or variable.format in [ "base64", "url" ]:
|
|
67
70
|
return gr.Textbox(label=label, value="", info=info)
|
|
68
71
|
|
|
69
72
|
if variable.type == "number":
|
|
@@ -78,8 +81,11 @@ class GradioWebUIBuilder:
|
|
|
78
81
|
if variable.type == "image":
|
|
79
82
|
return gr.Image(label=label, type="filepath")
|
|
80
83
|
|
|
84
|
+
if variable.type == "audio":
|
|
85
|
+
return gr.Audio(label=label, type="filepath")
|
|
86
|
+
|
|
81
87
|
if variable.type == "video":
|
|
82
|
-
return gr.Video(label=label)
|
|
88
|
+
return gr.Video(label=label, type="filepath")
|
|
83
89
|
|
|
84
90
|
if variable.type == "file":
|
|
85
91
|
return gr.File(label=label)
|
|
@@ -88,6 +94,25 @@ class GradioWebUIBuilder:
|
|
|
88
94
|
return gr.Dropdown(choices=variable.options or [], label=label, value=default, info=info)
|
|
89
95
|
|
|
90
96
|
return gr.Textbox(label=label, value=default, info=f"Unsupported type: {variable.type}")
|
|
97
|
+
|
|
98
|
+
async def _build_input_values(self, arguments: List[Any], variables: List[WorkflowVariableConfig]) -> Any:
|
|
99
|
+
if len(variables) == 1 and not variables[0].name:
|
|
100
|
+
return arguments[0]
|
|
101
|
+
|
|
102
|
+
input: Dict[str, Any] = {}
|
|
103
|
+
|
|
104
|
+
for value, variable in zip(arguments, variables):
|
|
105
|
+
input[variable.name] = await self._convert_input_value(value, variable.type, variable.subtype, variable.format, variable.internal)
|
|
106
|
+
|
|
107
|
+
return input
|
|
108
|
+
|
|
109
|
+
async def _convert_input_value(self, value: Any, type: Optional[str], subtype: Optional[str], format: Optional[str], internal: bool) -> Any:
|
|
110
|
+
if type in [ "image", "audio", "video", "file" ] and (not internal or not format):
|
|
111
|
+
if internal and format and format != "path":
|
|
112
|
+
value = await self._save_value_to_temporary_file(value, subtype, format)
|
|
113
|
+
return create_upload_file(value, type, subtype)
|
|
114
|
+
|
|
115
|
+
return value if value != "" else None
|
|
91
116
|
|
|
92
117
|
def _build_output_component(self, variable: Union[WorkflowVariableConfig, WorkflowVariableGroupConfig]) -> Union[gr.Component, List[ComponentGroup]]:
|
|
93
118
|
if isinstance(variable, WorkflowVariableGroupConfig):
|
|
@@ -102,15 +127,15 @@ class GradioWebUIBuilder:
|
|
|
102
127
|
label = variable.name or ""
|
|
103
128
|
info = variable.description or ""
|
|
104
129
|
|
|
105
|
-
if variable.type
|
|
130
|
+
if variable.type in [ "string", "base64" ]:
|
|
106
131
|
return gr.Textbox(label=label, interactive=False, show_copy_button=True, info=info)
|
|
107
|
-
|
|
132
|
+
|
|
108
133
|
if variable.type == "image":
|
|
109
134
|
return gr.Image(label=label, interactive=False)
|
|
110
|
-
|
|
135
|
+
|
|
111
136
|
if variable.type == "audio":
|
|
112
137
|
return gr.Audio(label=label)
|
|
113
|
-
|
|
138
|
+
|
|
114
139
|
if variable.type == "video":
|
|
115
140
|
return gr.Video(label=label)
|
|
116
141
|
|
|
@@ -126,40 +151,50 @@ class GradioWebUIBuilder:
|
|
|
126
151
|
flattened.append(item)
|
|
127
152
|
return flattened
|
|
128
153
|
|
|
129
|
-
async def
|
|
154
|
+
async def _flatten_output_values(self, output: Any, variables: List[Union[WorkflowVariableConfig, WorkflowVariableGroupConfig]]) -> Any:
|
|
130
155
|
flattened = []
|
|
131
156
|
for variable in variables:
|
|
132
157
|
if isinstance(variable, WorkflowVariableGroupConfig):
|
|
133
158
|
group = output[variable.name] if variable.name else output
|
|
134
159
|
for value in group:
|
|
135
|
-
flattened.extend(await self.
|
|
160
|
+
flattened.extend(await self._flatten_output_values(value, variable.variables))
|
|
136
161
|
else:
|
|
137
162
|
value = output[variable.name] if variable.name else output
|
|
138
|
-
flattened.append(await self.
|
|
163
|
+
flattened.append(await self._convert_output_value(value, variable.type, variable.subtype, variable.format, variable.internal))
|
|
139
164
|
return flattened
|
|
140
165
|
|
|
141
|
-
async def
|
|
166
|
+
async def _convert_output_value(self, value: Any, type: Optional[str], subtype: Optional[str], format: Optional[str], internal: bool) -> Any:
|
|
142
167
|
if type == "string":
|
|
143
|
-
if isinstance(value, (dict, list))
|
|
144
|
-
|
|
145
|
-
return str(value)
|
|
146
|
-
|
|
168
|
+
return json.dumps(value) if isinstance(value, (dict, list)) else str(value)
|
|
169
|
+
|
|
147
170
|
if type == "image":
|
|
148
|
-
|
|
149
|
-
return await load_image_from_stream(await HttpClient().request(value), subtype)
|
|
150
|
-
if format == "base64" and isinstance(value, str):
|
|
151
|
-
return await load_image_from_stream(Base64StreamResource(value), subtype)
|
|
152
|
-
if isinstance(value, HttpStreamResource):
|
|
153
|
-
return await load_image_from_stream(value, subtype)
|
|
154
|
-
return None
|
|
171
|
+
return await self._load_image_from_value(value, subtype, format)
|
|
155
172
|
|
|
156
173
|
if type in [ "audio", "video" ]:
|
|
157
|
-
|
|
158
|
-
return await save_stream_to_temporary_file(await HttpClient().request(value), subtype)
|
|
159
|
-
if format == "base64" and isinstance(value, str):
|
|
160
|
-
return await save_stream_to_temporary_file(Base64StreamResource(value), subtype)
|
|
161
|
-
if isinstance(value, HttpStreamResource):
|
|
162
|
-
return await save_stream_to_temporary_file(value, subtype)
|
|
163
|
-
return None
|
|
174
|
+
return await self._save_value_to_temporary_file(value, subtype, format)
|
|
164
175
|
|
|
165
176
|
return value
|
|
177
|
+
|
|
178
|
+
async def _load_image_from_value(self, value: Any, subtype: Optional[str], format: Optional[str]) -> Optional[str]:
|
|
179
|
+
if format == "base64" and isinstance(value, str):
|
|
180
|
+
return await load_image_from_stream(Base64StreamResource(value), subtype)
|
|
181
|
+
|
|
182
|
+
if format == "url" and isinstance(value, str):
|
|
183
|
+
return await load_image_from_stream(await HttpClient().request(value), subtype)
|
|
184
|
+
|
|
185
|
+
if isinstance(value, StreamResource):
|
|
186
|
+
return await load_image_from_stream(value, subtype)
|
|
187
|
+
|
|
188
|
+
return None
|
|
189
|
+
|
|
190
|
+
async def _save_value_to_temporary_file(self, value: Any, subtype: Optional[str], format: Optional[str]) -> Optional[str]:
|
|
191
|
+
if format == "base64" and isinstance(value, str):
|
|
192
|
+
return await save_stream_to_temporary_file(Base64StreamResource(value), subtype)
|
|
193
|
+
|
|
194
|
+
if format == "url" and isinstance(value, str):
|
|
195
|
+
return await save_stream_to_temporary_file(await HttpClient().request(value), subtype)
|
|
196
|
+
|
|
197
|
+
if isinstance(value, StreamResource):
|
|
198
|
+
return await save_stream_to_temporary_file(value, subtype)
|
|
199
|
+
|
|
200
|
+
return None
|
|
@@ -5,12 +5,13 @@ from mindor.dsl.schema.component import ComponentConfig
|
|
|
5
5
|
import re, json
|
|
6
6
|
|
|
7
7
|
class WorkflowVariable:
|
|
8
|
-
def __init__(self, name: Optional[str], type: str, subtype: Optional[str], format: Optional[str], default: Optional[Any]):
|
|
8
|
+
def __init__(self, name: Optional[str], type: str, subtype: Optional[str], format: Optional[str], default: Optional[Any], internal: bool = False):
|
|
9
9
|
self.name: Optional[str] = name
|
|
10
10
|
self.type: str = type
|
|
11
11
|
self.subtype: Optional[str] = subtype
|
|
12
12
|
self.format: Optional[str] = format
|
|
13
13
|
self.default: Optional[Any] = default
|
|
14
|
+
self.internal: bool = internal
|
|
14
15
|
|
|
15
16
|
def to_dict(self) -> Dict[str, Any]:
|
|
16
17
|
return {
|
|
@@ -18,7 +19,8 @@ class WorkflowVariable:
|
|
|
18
19
|
"type": self.type,
|
|
19
20
|
"subtype": self.subtype,
|
|
20
21
|
"format": self.format,
|
|
21
|
-
"default": self.default
|
|
22
|
+
"default": self.default,
|
|
23
|
+
"internal": self.internal
|
|
22
24
|
}
|
|
23
25
|
|
|
24
26
|
def __eq__(self, other):
|
|
@@ -52,7 +54,7 @@ class WorkflowVariableResolver:
|
|
|
52
54
|
"keypath": re.compile(r"[-_\w]+|\[\d+\]"),
|
|
53
55
|
}
|
|
54
56
|
|
|
55
|
-
def _enumerate_input_variables(self, value: Any, wanted_key: str) -> List[WorkflowVariable]:
|
|
57
|
+
def _enumerate_input_variables(self, value: Any, wanted_key: str, internal: bool = False) -> List[WorkflowVariable]:
|
|
56
58
|
if isinstance(value, str):
|
|
57
59
|
variables: List[WorkflowVariable] = []
|
|
58
60
|
|
|
@@ -63,22 +65,22 @@ class WorkflowVariableResolver:
|
|
|
63
65
|
default = self._parse_as_type(default, type)
|
|
64
66
|
|
|
65
67
|
if key == wanted_key:
|
|
66
|
-
variables.append(WorkflowVariable(path, type or "string", subtype, format, default))
|
|
68
|
+
variables.append(WorkflowVariable(path, type or "string", subtype, format, default, internal))
|
|
67
69
|
|
|
68
70
|
return variables
|
|
69
71
|
|
|
70
72
|
if isinstance(value, BaseModel):
|
|
71
|
-
return self._enumerate_input_variables(value.model_dump(exclude_none=True), wanted_key)
|
|
73
|
+
return self._enumerate_input_variables(value.model_dump(exclude_none=True), wanted_key, internal)
|
|
72
74
|
|
|
73
75
|
if isinstance(value, dict):
|
|
74
|
-
return sum([ self._enumerate_input_variables(v, wanted_key) for v in value.values() ], [])
|
|
76
|
+
return sum([ self._enumerate_input_variables(v, wanted_key, internal) for v in value.values() ], [])
|
|
75
77
|
|
|
76
78
|
if isinstance(value, list):
|
|
77
|
-
return sum([ self._enumerate_input_variables(v, wanted_key) for v in value ], [])
|
|
79
|
+
return sum([ self._enumerate_input_variables(v, wanted_key, internal) for v in value ], [])
|
|
78
80
|
|
|
79
81
|
return []
|
|
80
82
|
|
|
81
|
-
def _enumerate_output_variables(self, name: Optional[str], value: Any) -> List[WorkflowVariable]:
|
|
83
|
+
def _enumerate_output_variables(self, name: Optional[str], value: Any, internal: bool = False) -> List[WorkflowVariable]:
|
|
82
84
|
variables: List[WorkflowVariable] = []
|
|
83
85
|
|
|
84
86
|
if isinstance(value, str):
|
|
@@ -88,18 +90,18 @@ class WorkflowVariableResolver:
|
|
|
88
90
|
if type and default:
|
|
89
91
|
default = self._parse_as_type(default, type)
|
|
90
92
|
|
|
91
|
-
variables.append(WorkflowVariable(name, type or "string", subtype, format, default))
|
|
93
|
+
variables.append(WorkflowVariable(name, type or "string", subtype, format, default, internal))
|
|
92
94
|
|
|
93
95
|
return variables
|
|
94
96
|
|
|
95
97
|
if isinstance(value, BaseModel):
|
|
96
|
-
return self._enumerate_output_variables(name, value.model_dump(exclude_none=True))
|
|
98
|
+
return self._enumerate_output_variables(name, value.model_dump(exclude_none=True), internal)
|
|
97
99
|
|
|
98
100
|
if isinstance(value, dict):
|
|
99
|
-
return sum([ self._enumerate_output_variables(f"{name}.{k}" if name else f"{k}", v) for k, v in value.items() ], [])
|
|
101
|
+
return sum([ self._enumerate_output_variables(f"{name}.{k}" if name else f"{k}", v, internal) for k, v in value.items() ], [])
|
|
100
102
|
|
|
101
103
|
if isinstance(value, list):
|
|
102
|
-
return sum([ self._enumerate_output_variables(f"{name}[{i}]" if name else f"[{i}]", v) for i, v in enumerate(value) ], [])
|
|
104
|
+
return sum([ self._enumerate_output_variables(f"{name}[{i}]" if name else f"[{i}]", v, internal) for i, v in enumerate(value) ], [])
|
|
103
105
|
|
|
104
106
|
return []
|
|
105
107
|
|
|
@@ -126,7 +128,7 @@ class WorkflowVariableResolver:
|
|
|
126
128
|
def _to_variable_config(self, variable: WorkflowVariable) -> WorkflowVariableConfig:
|
|
127
129
|
config_dict = variable.to_dict()
|
|
128
130
|
|
|
129
|
-
if variable.type
|
|
131
|
+
if variable.type in [ "image", "audio", "video", "file", "select" ] and variable.subtype:
|
|
130
132
|
config_dict["options"] = variable.subtype.split(",")
|
|
131
133
|
|
|
132
134
|
return WorkflowVariableConfig(**config_dict)
|
|
@@ -156,9 +158,13 @@ class WorkflowInputVariableResolver(WorkflowVariableResolver):
|
|
|
156
158
|
if isinstance(job.component, str):
|
|
157
159
|
component: Optional[ComponentConfig] = components[job.component] if job.component in components else None
|
|
158
160
|
if component:
|
|
159
|
-
|
|
161
|
+
action = component.actions[action_id] if action_id in component.actions else None
|
|
162
|
+
if action:
|
|
163
|
+
variables.extend(self._enumerate_input_variables(action, "input", internal=True))
|
|
160
164
|
else:
|
|
161
|
-
|
|
165
|
+
action = job.component.actions[action_id] if action_id in job.component.actions else None
|
|
166
|
+
if action:
|
|
167
|
+
variables.extend(self._enumerate_input_variables(action, "input", internal=True))
|
|
162
168
|
else:
|
|
163
169
|
variables.extend(self._enumerate_input_variables(job.input, "input"))
|
|
164
170
|
|
|
@@ -186,9 +192,13 @@ class WorkflowOutputVariableResolver(WorkflowVariableResolver):
|
|
|
186
192
|
if isinstance(job.component, str):
|
|
187
193
|
component: Optional[ComponentConfig] = components[job.component] if job.component in components else None
|
|
188
194
|
if component:
|
|
189
|
-
|
|
195
|
+
action = component.actions[action_id] if action_id in component.actions else None
|
|
196
|
+
if action:
|
|
197
|
+
job_variables.extend(self._enumerate_output_variables(None, action.output, internal=True))
|
|
190
198
|
else:
|
|
191
|
-
|
|
199
|
+
action = job.component.actions[action_id] if action_id in job.component.actions else None
|
|
200
|
+
if action:
|
|
201
|
+
job_variables.extend(self._enumerate_output_variables(None, action.output, internal=True))
|
|
192
202
|
else:
|
|
193
203
|
job_variables.extend(self._enumerate_output_variables(None, job.output))
|
|
194
204
|
|
{model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/core/listener/engine/http_callback.py
RENAMED
|
@@ -25,8 +25,8 @@ class HttpCallbackContext:
|
|
|
25
25
|
async for item in self._items():
|
|
26
26
|
yield HttpCallbackContext(item, self.query, False, None)
|
|
27
27
|
|
|
28
|
-
async def render_template(self, template: str) -> Any:
|
|
29
|
-
return await self.renderer.render(template)
|
|
28
|
+
async def render_template(self, template: str, ignore_files: bool = True) -> Any:
|
|
29
|
+
return await self.renderer.render(template, ignore_files)
|
|
30
30
|
|
|
31
31
|
async def _items(self) -> List[Any]:
|
|
32
32
|
item = await self.renderer.render(self.item) if self.item else self.body
|
|
@@ -78,7 +78,7 @@ class HttpCallbackListener(ListenerEngine):
|
|
|
78
78
|
|
|
79
79
|
if future:
|
|
80
80
|
if succeeded:
|
|
81
|
-
future.set_result((await item.render_template(callback.result)) if callback.result else item.body)
|
|
81
|
+
future.set_result((await item.render_template(callback.result, ignore_files=True)) if callback.result else item.body)
|
|
82
82
|
else:
|
|
83
83
|
future.set_exception(RuntimeError(f"Task failed for '{callback_id}'"))
|
|
84
84
|
self._remove_pending_future(callback_id)
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
from typing import Type, Union, Literal, Optional, Dict, List, Tuple, Set, Annotated, AsyncIterator, Any
|
|
2
|
+
from .http_request import build_request_body, parse_options_header
|
|
3
|
+
from .streaming import StreamResource
|
|
4
|
+
from requests.structures import CaseInsensitiveDict
|
|
5
|
+
import aiohttp
|
|
6
|
+
|
|
7
|
+
class HttpStreamResource(StreamResource):
|
|
8
|
+
def __init__(
|
|
9
|
+
self,
|
|
10
|
+
session: aiohttp.ClientSession,
|
|
11
|
+
stream: aiohttp.StreamReader,
|
|
12
|
+
content_type: Optional[str] = None,
|
|
13
|
+
filename: Optional[str] = None
|
|
14
|
+
):
|
|
15
|
+
super().__init__(content_type, filename)
|
|
16
|
+
|
|
17
|
+
self.session: aiohttp.ClientSession = session
|
|
18
|
+
self.stream: aiohttp.StreamReader = stream
|
|
19
|
+
|
|
20
|
+
async def close(self):
|
|
21
|
+
await self.session.close()
|
|
22
|
+
self.session = None
|
|
23
|
+
self.stream = None
|
|
24
|
+
|
|
25
|
+
async def _iterate_stream(self) -> AsyncIterator[bytes]:
|
|
26
|
+
_, buffer_size = self.stream.get_read_buffer_limits()
|
|
27
|
+
chunk_size = buffer_size or 65536
|
|
28
|
+
|
|
29
|
+
while not self.stream.at_eof():
|
|
30
|
+
chunk = await self.stream.read(chunk_size)
|
|
31
|
+
if not chunk:
|
|
32
|
+
break
|
|
33
|
+
yield chunk
|
|
34
|
+
|
|
35
|
+
class HttpClient:
|
|
36
|
+
async def request(
|
|
37
|
+
self,
|
|
38
|
+
url: str,
|
|
39
|
+
method: Optional[str] = "GET",
|
|
40
|
+
params: Optional[Dict[str, Any]] = None,
|
|
41
|
+
body: Optional[Any] = None,
|
|
42
|
+
headers: Optional[Dict[str, str]] = None
|
|
43
|
+
) -> Any:
|
|
44
|
+
session = aiohttp.ClientSession()
|
|
45
|
+
try:
|
|
46
|
+
response = await self._request_with_session(session, url, method, params, body, headers)
|
|
47
|
+
content, _ = await self._parse_response_content(session, response)
|
|
48
|
+
|
|
49
|
+
if response.status >= 400:
|
|
50
|
+
raise ValueError(f"Request failed with status {response.status}: {content}")
|
|
51
|
+
|
|
52
|
+
if not isinstance(content, HttpStreamResource):
|
|
53
|
+
await session.close()
|
|
54
|
+
|
|
55
|
+
return content
|
|
56
|
+
except:
|
|
57
|
+
await session.close()
|
|
58
|
+
raise
|
|
59
|
+
|
|
60
|
+
async def _request_with_session(
|
|
61
|
+
self,
|
|
62
|
+
session: aiohttp.ClientSession,
|
|
63
|
+
url: str,
|
|
64
|
+
method: str,
|
|
65
|
+
params: Optional[Dict[str, Any]],
|
|
66
|
+
body: Optional[Any],
|
|
67
|
+
headers: Optional[Dict[str, str]]
|
|
68
|
+
) -> aiohttp.ClientResponse:
|
|
69
|
+
data, content_type = self._build_request_body(body, headers)
|
|
70
|
+
|
|
71
|
+
if content_type == "multipart/form-data":
|
|
72
|
+
headers = CaseInsensitiveDict(headers)
|
|
73
|
+
headers.pop("Content-Type", None)
|
|
74
|
+
|
|
75
|
+
return await session.request(method, url, params=params, data=data, headers=headers)
|
|
76
|
+
|
|
77
|
+
def _build_request_body(self, body: Optional[Any], headers: Optional[Dict[str, str]]) -> Tuple[Any, str]:
|
|
78
|
+
content_type, _ = parse_options_header(headers, "Content-Type")
|
|
79
|
+
|
|
80
|
+
if content_type and body is not None:
|
|
81
|
+
return (build_request_body(body, content_type), content_type)
|
|
82
|
+
|
|
83
|
+
return (body, content_type)
|
|
84
|
+
|
|
85
|
+
async def _parse_response_content(self, session: aiohttp.ClientSession, response: aiohttp.ClientResponse) -> Tuple[Any, str]:
|
|
86
|
+
content_type, _ = parse_options_header(response.headers, "Content-Type")
|
|
87
|
+
|
|
88
|
+
if content_type == "application/json":
|
|
89
|
+
return (await response.json(), content_type)
|
|
90
|
+
|
|
91
|
+
if content_type.startswith("text/"):
|
|
92
|
+
return (await response.text(), content_type)
|
|
93
|
+
|
|
94
|
+
_, disposition = parse_options_header(response.headers, "Content-Disposition")
|
|
95
|
+
filename = disposition.get("filename")
|
|
96
|
+
|
|
97
|
+
return (HttpStreamResource(session, response.content, content_type, filename), content_type)
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
from typing import Dict, List, Tuple, Optional, Any
|
|
2
2
|
from requests.structures import CaseInsensitiveDict
|
|
3
|
-
from starlette.datastructures import FormData
|
|
3
|
+
from starlette.datastructures import FormData, UploadFile
|
|
4
4
|
from urllib.parse import urlencode
|
|
5
5
|
from fastapi import Request
|
|
6
|
-
import re, json
|
|
6
|
+
import aiohttp, re, json, os
|
|
7
7
|
|
|
8
8
|
_token_pattern = r"([\w!#$%&'*+\-.^_`|~]+)"
|
|
9
9
|
_quoted_pattern = r'"([^"]*)"'
|
|
@@ -37,12 +37,12 @@ class NestedFormParser:
|
|
|
37
37
|
|
|
38
38
|
return parts
|
|
39
39
|
|
|
40
|
-
|
|
40
|
+
def build_request_body(body: Any, content_type: Optional[str]) -> Any:
|
|
41
41
|
if content_type == "application/json":
|
|
42
42
|
return json.dumps(body)
|
|
43
43
|
|
|
44
44
|
if content_type == "multipart/form-data":
|
|
45
|
-
return
|
|
45
|
+
return build_request_form(body)
|
|
46
46
|
|
|
47
47
|
if content_type == "application/x-www-form-urlencoded":
|
|
48
48
|
return urlencode(body)
|
|
@@ -52,6 +52,22 @@ async def serialize_request_body(body: Any, content_type: Optional[str]) -> Any:
|
|
|
52
52
|
|
|
53
53
|
return body
|
|
54
54
|
|
|
55
|
+
def build_request_form(body: Any) -> aiohttp.FormData:
|
|
56
|
+
form = aiohttp.FormData()
|
|
57
|
+
|
|
58
|
+
for key, value in body.items() if isinstance(body, dict) else {}:
|
|
59
|
+
if isinstance(value, UploadFile):
|
|
60
|
+
form.add_field(
|
|
61
|
+
name=key,
|
|
62
|
+
value=value.file,
|
|
63
|
+
filename=value.filename,
|
|
64
|
+
content_type=value.content_type or "application/octet-stream"
|
|
65
|
+
)
|
|
66
|
+
else:
|
|
67
|
+
form.add_field(name=key, value=str(value))
|
|
68
|
+
|
|
69
|
+
return form
|
|
70
|
+
|
|
55
71
|
async def parse_request_body(request: Request, content_type: str, nested: bool = False) -> Any:
|
|
56
72
|
if content_type in [ "multipart/form-data", "application/x-www-form-urlencoded" ]:
|
|
57
73
|
return await parse_request_form(request, nested)
|
|
@@ -75,6 +91,7 @@ async def parse_request_form(request: Request, nested: bool = False) -> Any:
|
|
|
75
91
|
def parse_options_header(headers: Dict[str, str], header_name: str) -> Tuple[str, Dict[str, str]]:
|
|
76
92
|
value = CaseInsensitiveDict(headers or {}).get(header_name, "")
|
|
77
93
|
pos = _firefox_escape_fix_regex.sub("%22", value).find(";")
|
|
94
|
+
|
|
78
95
|
if pos >= 0:
|
|
79
96
|
options = {
|
|
80
97
|
m.group(1).lower(): m.group(2) or m.group(3).replace("%22", "") for m in _option_regex.finditer(value[pos:])
|
|
@@ -84,3 +101,21 @@ def parse_options_header(headers: Dict[str, str], header_name: str) -> Tuple[str
|
|
|
84
101
|
options = {}
|
|
85
102
|
|
|
86
103
|
return value.strip().lower(), options
|
|
104
|
+
|
|
105
|
+
def create_upload_file(path: str, type: Optional[str], subtype: Optional[str]) -> UploadFile:
|
|
106
|
+
file, filename = open(path, "rb"), os.path.basename(path)
|
|
107
|
+
content_type = guess_file_content_type(filename, type, subtype)
|
|
108
|
+
headers = {
|
|
109
|
+
"Content-Type": content_type,
|
|
110
|
+
"Content-Disposition": f'form-data; filename="{filename}"'
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return UploadFile(file=file, filename=filename, headers=headers)
|
|
114
|
+
|
|
115
|
+
def guess_file_content_type(filename: str, type: Optional[str], subtype: Optional[str]) -> str:
|
|
116
|
+
subtype = filename.split(".")[-1] if not subtype else subtype
|
|
117
|
+
|
|
118
|
+
if type in [ "image", "audio", "video" ] and subtype:
|
|
119
|
+
return f"{type}/{subtype}"
|
|
120
|
+
|
|
121
|
+
return "application/octet-stream"
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
from typing import Optional, AsyncIterator
|
|
2
2
|
from abc import ABC, abstractmethod
|
|
3
3
|
from tempfile import NamedTemporaryFile
|
|
4
|
-
import
|
|
4
|
+
from aiofiles.threadpool.binary import AsyncBufferedReader
|
|
5
|
+
import aiofiles, io, base64
|
|
5
6
|
|
|
6
7
|
class StreamResource(ABC):
|
|
7
8
|
def __init__(self, content_type: Optional[str], filename: Optional[str]):
|
|
@@ -29,7 +30,12 @@ class Base64StreamResource(StreamResource):
|
|
|
29
30
|
def __init__(self, encoded: str, content_type: Optional[str] = None, filename: Optional[str] = None):
|
|
30
31
|
super().__init__(content_type, filename)
|
|
31
32
|
|
|
32
|
-
self.
|
|
33
|
+
self.encoded: str = encoded
|
|
34
|
+
self.stream: Optional[io.BytesIO] = None
|
|
35
|
+
|
|
36
|
+
async def __aenter__(self):
|
|
37
|
+
self.stream = io.BytesIO(base64.b64decode(self.encoded))
|
|
38
|
+
return self
|
|
33
39
|
|
|
34
40
|
async def close(self):
|
|
35
41
|
self.stream.close()
|