model-compose 0.1.13__tar.gz → 0.2.1__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.
Files changed (146) hide show
  1. {model_compose-0.1.13 → model_compose-0.2.1}/PKG-INFO +2 -2
  2. {model_compose-0.1.13 → model_compose-0.2.1}/pyproject.toml +2 -2
  3. {model_compose-0.1.13/src/mindor/core/component/engine → model_compose-0.2.1/src/mindor/core/component}/base.py +7 -2
  4. {model_compose-0.1.13 → model_compose-0.2.1}/src/mindor/core/component/component.py +4 -2
  5. {model_compose-0.1.13 → model_compose-0.2.1}/src/mindor/core/component/engine/__init__.py +1 -1
  6. {model_compose-0.1.13 → model_compose-0.2.1}/src/mindor/core/component/engine/http_client.py +5 -17
  7. {model_compose-0.1.13 → model_compose-0.2.1}/src/mindor/core/component/engine/http_server.py +3 -4
  8. {model_compose-0.1.13 → model_compose-0.2.1}/src/mindor/core/component/engine/mcp_client.py +3 -7
  9. {model_compose-0.1.13 → model_compose-0.2.1}/src/mindor/core/component/engine/mcp_server.py +3 -4
  10. {model_compose-0.1.13 → model_compose-0.2.1}/src/mindor/core/component/engine/shell.py +3 -4
  11. model_compose-0.2.1/src/mindor/core/component/engine/workflow.py +41 -0
  12. {model_compose-0.1.13/src/mindor/core/controller/engine → model_compose-0.2.1/src/mindor/core/controller}/base.py +8 -2
  13. {model_compose-0.1.13 → model_compose-0.2.1}/src/mindor/core/controller/controller.py +4 -2
  14. {model_compose-0.1.13 → model_compose-0.2.1}/src/mindor/core/controller/engine/__init__.py +0 -1
  15. {model_compose-0.1.13 → model_compose-0.2.1}/src/mindor/core/controller/engine/http_server.py +2 -5
  16. {model_compose-0.1.13 → model_compose-0.2.1}/src/mindor/core/controller/engine/mcp_server.py +4 -5
  17. {model_compose-0.1.13 → model_compose-0.2.1}/src/mindor/core/controller/runner/http_client.py +2 -2
  18. {model_compose-0.1.13 → model_compose-0.2.1}/src/mindor/core/controller/runner/mcp_client.py +2 -2
  19. {model_compose-0.1.13 → model_compose-0.2.1}/src/mindor/core/controller/webui/gradio.py +3 -3
  20. model_compose-0.2.1/src/mindor/core/gateway/__init__.py +2 -0
  21. {model_compose-0.1.13/src/mindor/core/gateway/engine → model_compose-0.2.1/src/mindor/core/gateway}/base.py +7 -1
  22. {model_compose-0.1.13 → model_compose-0.2.1}/src/mindor/core/gateway/engine/__init__.py +1 -1
  23. {model_compose-0.1.13 → model_compose-0.2.1}/src/mindor/core/gateway/engine/http_tunnel.py +2 -3
  24. {model_compose-0.1.13 → model_compose-0.2.1}/src/mindor/core/gateway/engine/ssh_tunnel.py +2 -3
  25. {model_compose-0.1.13 → model_compose-0.2.1}/src/mindor/core/gateway/gateway.py +4 -2
  26. {model_compose-0.1.13/src/mindor/core/listener/engine → model_compose-0.2.1/src/mindor/core/listener}/base.py +7 -1
  27. {model_compose-0.1.13 → model_compose-0.2.1}/src/mindor/core/listener/engine/__init__.py +0 -1
  28. {model_compose-0.1.13 → model_compose-0.2.1}/src/mindor/core/listener/engine/http_callback.py +2 -3
  29. {model_compose-0.1.13 → model_compose-0.2.1}/src/mindor/core/listener/listener.py +4 -2
  30. {model_compose-0.1.13 → model_compose-0.2.1}/src/mindor/core/utils/http_client.py +12 -1
  31. {model_compose-0.1.13 → model_compose-0.2.1}/src/mindor/core/utils/image.py +1 -1
  32. {model_compose-0.1.13 → model_compose-0.2.1}/src/mindor/core/utils/renderer.py +2 -2
  33. model_compose-0.2.1/src/mindor/core/utils/time.py +29 -0
  34. model_compose-0.2.1/src/mindor/core/workflow/job/__init__.py +1 -0
  35. model_compose-0.2.1/src/mindor/core/workflow/job/base.py +23 -0
  36. model_compose-0.2.1/src/mindor/core/workflow/job/impl/__init__.py +2 -0
  37. model_compose-0.1.13/src/mindor/core/workflow/job.py → model_compose-0.2.1/src/mindor/core/workflow/job/impl/action.py +17 -9
  38. model_compose-0.2.1/src/mindor/core/workflow/job/impl/delay.py +42 -0
  39. model_compose-0.2.1/src/mindor/core/workflow/job/job.py +9 -0
  40. {model_compose-0.1.13 → model_compose-0.2.1}/src/mindor/core/workflow/schema.py +7 -6
  41. {model_compose-0.1.13 → model_compose-0.2.1}/src/mindor/core/workflow/workflow.py +5 -15
  42. {model_compose-0.1.13 → model_compose-0.2.1}/src/mindor/dsl/schema/action/action.py +3 -1
  43. {model_compose-0.1.13 → model_compose-0.2.1}/src/mindor/dsl/schema/action/impl/__init__.py +1 -0
  44. {model_compose-0.1.13 → model_compose-0.2.1}/src/mindor/dsl/schema/action/impl/http_client.py +6 -6
  45. model_compose-0.2.1/src/mindor/dsl/schema/action/impl/mcp_client.py +9 -0
  46. {model_compose-0.1.13 → model_compose-0.2.1}/src/mindor/dsl/schema/action/impl/shell.py +1 -1
  47. model_compose-0.2.1/src/mindor/dsl/schema/action/impl/workflow.py +8 -0
  48. {model_compose-0.1.13 → model_compose-0.2.1}/src/mindor/dsl/schema/component/component.py +1 -0
  49. {model_compose-0.1.13 → model_compose-0.2.1}/src/mindor/dsl/schema/component/impl/__init__.py +1 -0
  50. {model_compose-0.1.13 → model_compose-0.2.1}/src/mindor/dsl/schema/component/impl/common.py +4 -4
  51. {model_compose-0.1.13 → model_compose-0.2.1}/src/mindor/dsl/schema/component/impl/http_client.py +3 -3
  52. {model_compose-0.1.13 → model_compose-0.2.1}/src/mindor/dsl/schema/component/impl/http_server.py +3 -3
  53. {model_compose-0.1.13 → model_compose-0.2.1}/src/mindor/dsl/schema/component/impl/mcp_client.py +3 -3
  54. {model_compose-0.1.13 → model_compose-0.2.1}/src/mindor/dsl/schema/component/impl/mcp_server.py +1 -1
  55. {model_compose-0.1.13 → model_compose-0.2.1}/src/mindor/dsl/schema/component/impl/shell.py +1 -1
  56. {model_compose-0.1.13 → model_compose-0.2.1}/src/mindor/dsl/schema/component/impl/types.py +1 -0
  57. model_compose-0.2.1/src/mindor/dsl/schema/component/impl/workflow.py +17 -0
  58. model_compose-0.2.1/src/mindor/dsl/schema/job/__init__.py +1 -0
  59. model_compose-0.2.1/src/mindor/dsl/schema/job/impl/__init__.py +3 -0
  60. model_compose-0.2.1/src/mindor/dsl/schema/job/impl/action.py +18 -0
  61. model_compose-0.2.1/src/mindor/dsl/schema/job/impl/common.py +10 -0
  62. model_compose-0.2.1/src/mindor/dsl/schema/job/impl/delay.py +31 -0
  63. model_compose-0.2.1/src/mindor/dsl/schema/job/impl/types.py +5 -0
  64. model_compose-0.2.1/src/mindor/dsl/schema/job/job.py +11 -0
  65. {model_compose-0.1.13 → model_compose-0.2.1}/src/mindor/dsl/schema/listener/impl/http_callback.py +4 -4
  66. model_compose-0.2.1/src/mindor/dsl/schema/utils/__init__.py +0 -0
  67. model_compose-0.2.1/src/mindor/dsl/schema/utils/annotation.py +19 -0
  68. {model_compose-0.1.13 → model_compose-0.2.1}/src/mindor/dsl/schema/workflow.py +35 -35
  69. {model_compose-0.1.13 → model_compose-0.2.1}/src/model_compose.egg-info/PKG-INFO +2 -2
  70. {model_compose-0.1.13 → model_compose-0.2.1}/src/model_compose.egg-info/SOURCES.txt +23 -6
  71. {model_compose-0.1.13 → model_compose-0.2.1}/src/model_compose.egg-info/requires.txt +1 -1
  72. model_compose-0.1.13/src/mindor/core/gateway/__init__.py +0 -2
  73. model_compose-0.1.13/src/mindor/core/utils/time.py +0 -13
  74. model_compose-0.1.13/src/mindor/dsl/schema/action/impl/mcp_client.py +0 -9
  75. {model_compose-0.1.13 → model_compose-0.2.1}/LICENSE +0 -0
  76. {model_compose-0.1.13 → model_compose-0.2.1}/README.md +0 -0
  77. {model_compose-0.1.13 → model_compose-0.2.1}/setup.cfg +0 -0
  78. {model_compose-0.1.13 → model_compose-0.2.1}/src/mindor/__init__.py +0 -0
  79. {model_compose-0.1.13 → model_compose-0.2.1}/src/mindor/cli/__init__.py +0 -0
  80. {model_compose-0.1.13 → model_compose-0.2.1}/src/mindor/cli/compose.py +0 -0
  81. {model_compose-0.1.13 → model_compose-0.2.1}/src/mindor/core/__init__.py +0 -0
  82. {model_compose-0.1.13 → model_compose-0.2.1}/src/mindor/core/component/__init__.py +0 -0
  83. {model_compose-0.1.13/src/mindor/core/component/engine → model_compose-0.2.1/src/mindor/core/component}/context.py +0 -0
  84. {model_compose-0.1.13 → model_compose-0.2.1}/src/mindor/core/compose/__init__.py +0 -0
  85. {model_compose-0.1.13 → model_compose-0.2.1}/src/mindor/core/compose/compose.py +0 -0
  86. {model_compose-0.1.13 → model_compose-0.2.1}/src/mindor/core/compose/manager.py +0 -0
  87. {model_compose-0.1.13 → model_compose-0.2.1}/src/mindor/core/controller/__init__.py +0 -0
  88. {model_compose-0.1.13 → model_compose-0.2.1}/src/mindor/core/controller/runner/__init__.py +0 -0
  89. {model_compose-0.1.13 → model_compose-0.2.1}/src/mindor/core/controller/runner/client.py +0 -0
  90. {model_compose-0.1.13 → model_compose-0.2.1}/src/mindor/core/controller/runner/runner.py +0 -0
  91. {model_compose-0.1.13 → model_compose-0.2.1}/src/mindor/core/controller/webui/__init__.py +0 -0
  92. {model_compose-0.1.13 → model_compose-0.2.1}/src/mindor/core/controller/webui/webui.py +0 -0
  93. {model_compose-0.1.13 → model_compose-0.2.1}/src/mindor/core/listener/__init__.py +0 -0
  94. {model_compose-0.1.13 → model_compose-0.2.1}/src/mindor/core/runtime/__init__.py +0 -0
  95. {model_compose-0.1.13 → model_compose-0.2.1}/src/mindor/core/runtime/env.py +0 -0
  96. {model_compose-0.1.13 → model_compose-0.2.1}/src/mindor/core/services/__init__.py +0 -0
  97. {model_compose-0.1.13 → model_compose-0.2.1}/src/mindor/core/services/async_service.py +0 -0
  98. {model_compose-0.1.13 → model_compose-0.2.1}/src/mindor/core/utils/__init__.py +0 -0
  99. {model_compose-0.1.13 → model_compose-0.2.1}/src/mindor/core/utils/expiring.py +0 -0
  100. {model_compose-0.1.13 → model_compose-0.2.1}/src/mindor/core/utils/http_request.py +0 -0
  101. {model_compose-0.1.13 → model_compose-0.2.1}/src/mindor/core/utils/http_status.py +0 -0
  102. {model_compose-0.1.13 → model_compose-0.2.1}/src/mindor/core/utils/mcp_client.py +0 -0
  103. {model_compose-0.1.13 → model_compose-0.2.1}/src/mindor/core/utils/ssh_client.py +0 -0
  104. {model_compose-0.1.13 → model_compose-0.2.1}/src/mindor/core/utils/streaming.py +0 -0
  105. {model_compose-0.1.13 → model_compose-0.2.1}/src/mindor/core/utils/workqueue.py +0 -0
  106. {model_compose-0.1.13 → model_compose-0.2.1}/src/mindor/core/workflow/__init__.py +0 -0
  107. {model_compose-0.1.13 → model_compose-0.2.1}/src/mindor/core/workflow/context.py +0 -0
  108. {model_compose-0.1.13 → model_compose-0.2.1}/src/mindor/dsl/__init__.py +0 -0
  109. {model_compose-0.1.13 → model_compose-0.2.1}/src/mindor/dsl/loader.py +0 -0
  110. {model_compose-0.1.13 → model_compose-0.2.1}/src/mindor/dsl/schema/__init__.py +0 -0
  111. {model_compose-0.1.13 → model_compose-0.2.1}/src/mindor/dsl/schema/action/__init__.py +0 -0
  112. {model_compose-0.1.13 → model_compose-0.2.1}/src/mindor/dsl/schema/action/impl/common.py +0 -0
  113. {model_compose-0.1.13 → model_compose-0.2.1}/src/mindor/dsl/schema/action/impl/http_server.py +0 -0
  114. {model_compose-0.1.13 → model_compose-0.2.1}/src/mindor/dsl/schema/action/impl/mcp_server.py +0 -0
  115. {model_compose-0.1.13 → model_compose-0.2.1}/src/mindor/dsl/schema/component/__init__.py +0 -0
  116. {model_compose-0.1.13 → model_compose-0.2.1}/src/mindor/dsl/schema/compose.py +0 -0
  117. {model_compose-0.1.13 → model_compose-0.2.1}/src/mindor/dsl/schema/controller/__init__.py +0 -0
  118. {model_compose-0.1.13 → model_compose-0.2.1}/src/mindor/dsl/schema/controller/controller.py +0 -0
  119. {model_compose-0.1.13 → model_compose-0.2.1}/src/mindor/dsl/schema/controller/impl/__init__.py +0 -0
  120. {model_compose-0.1.13 → model_compose-0.2.1}/src/mindor/dsl/schema/controller/impl/common.py +0 -0
  121. {model_compose-0.1.13 → model_compose-0.2.1}/src/mindor/dsl/schema/controller/impl/http_server.py +0 -0
  122. {model_compose-0.1.13 → model_compose-0.2.1}/src/mindor/dsl/schema/controller/impl/mcp_server.py +0 -0
  123. {model_compose-0.1.13 → model_compose-0.2.1}/src/mindor/dsl/schema/controller/impl/types.py +0 -0
  124. {model_compose-0.1.13 → model_compose-0.2.1}/src/mindor/dsl/schema/controller/impl/webui.py +0 -0
  125. {model_compose-0.1.13 → model_compose-0.2.1}/src/mindor/dsl/schema/gateway/__init__.py +0 -0
  126. {model_compose-0.1.13 → model_compose-0.2.1}/src/mindor/dsl/schema/gateway/gateway.py +0 -0
  127. {model_compose-0.1.13 → model_compose-0.2.1}/src/mindor/dsl/schema/gateway/impl/__init__.py +0 -0
  128. {model_compose-0.1.13 → model_compose-0.2.1}/src/mindor/dsl/schema/gateway/impl/common.py +0 -0
  129. {model_compose-0.1.13 → model_compose-0.2.1}/src/mindor/dsl/schema/gateway/impl/http_tunnel.py +0 -0
  130. {model_compose-0.1.13 → model_compose-0.2.1}/src/mindor/dsl/schema/gateway/impl/ssh_tunnel.py +0 -0
  131. {model_compose-0.1.13 → model_compose-0.2.1}/src/mindor/dsl/schema/gateway/impl/types.py +0 -0
  132. {model_compose-0.1.13 → model_compose-0.2.1}/src/mindor/dsl/schema/listener/__init__.py +0 -0
  133. {model_compose-0.1.13 → model_compose-0.2.1}/src/mindor/dsl/schema/listener/impl/__init__.py +0 -0
  134. {model_compose-0.1.13 → model_compose-0.2.1}/src/mindor/dsl/schema/listener/impl/common.py +0 -0
  135. {model_compose-0.1.13 → model_compose-0.2.1}/src/mindor/dsl/schema/listener/impl/types.py +0 -0
  136. {model_compose-0.1.13 → model_compose-0.2.1}/src/mindor/dsl/schema/listener/listener.py +0 -0
  137. {model_compose-0.1.13 → model_compose-0.2.1}/src/mindor/dsl/schema/runtime/__init__.py +0 -0
  138. {model_compose-0.1.13 → model_compose-0.2.1}/src/mindor/dsl/schema/runtime/impl/__init__.py +0 -0
  139. {model_compose-0.1.13 → model_compose-0.2.1}/src/mindor/dsl/schema/runtime/impl/common.py +0 -0
  140. {model_compose-0.1.13 → model_compose-0.2.1}/src/mindor/dsl/schema/runtime/impl/docker.py +0 -0
  141. {model_compose-0.1.13 → model_compose-0.2.1}/src/mindor/dsl/schema/runtime/impl/types.py +0 -0
  142. {model_compose-0.1.13 → model_compose-0.2.1}/src/mindor/dsl/schema/runtime/runtime.py +0 -0
  143. {model_compose-0.1.13 → model_compose-0.2.1}/src/model_compose.egg-info/dependency_links.txt +0 -0
  144. {model_compose-0.1.13 → model_compose-0.2.1}/src/model_compose.egg-info/entry_points.txt +0 -0
  145. {model_compose-0.1.13 → model_compose-0.2.1}/src/model_compose.egg-info/top_level.txt +0 -0
  146. {model_compose-0.1.13 → model_compose-0.2.1}/tests/test_cli.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: model-compose
3
- Version: 0.1.13
3
+ Version: 0.2.1
4
4
  Summary: model-compose: Declarative AI Model and Workflow Orchestrator
5
5
  Author-email: Hanyeol Cho <hanyeol.cho@gmail.com>
6
6
  License-Expression: MIT
@@ -15,7 +15,7 @@ Requires-Dist: aiohttp
15
15
  Requires-Dist: requests
16
16
  Requires-Dist: fastapi
17
17
  Requires-Dist: uvicorn
18
- Requires-Dist: mcp
18
+ Requires-Dist: mcp>=1.10.1
19
19
  Requires-Dist: pyngrok
20
20
  Requires-Dist: ulid
21
21
  Requires-Dist: gradio
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "model-compose"
3
- version = "0.1.13"
3
+ version = "0.2.1"
4
4
  description = "model-compose: Declarative AI Model and Workflow Orchestrator"
5
5
  authors = [
6
6
  { name = "Hanyeol Cho", email = "hanyeol.cho@gmail.com" }
@@ -16,7 +16,7 @@ dependencies = [
16
16
  "requests",
17
17
  "fastapi",
18
18
  "uvicorn",
19
- "mcp",
19
+ "mcp>=1.10.1",
20
20
  "pyngrok",
21
21
  "ulid",
22
22
  "gradio",
@@ -1,6 +1,5 @@
1
1
  from typing import Type, Union, Literal, Optional, Dict, List, Tuple, Set, Annotated, Callable, Any
2
2
  from abc import ABC, abstractmethod
3
-
4
3
  from mindor.dsl.schema.component import ComponentConfig, ComponentType
5
4
  from mindor.dsl.schema.action import ActionConfig
6
5
  from mindor.core.services import AsyncService
@@ -66,4 +65,10 @@ class ComponentEngine(AsyncService):
66
65
  async def _run(self, action: ActionConfig, context: ComponentActionContext) -> Any:
67
66
  pass
68
67
 
69
- ComponentEngineMap: Dict[ComponentType, Type[ComponentEngine]] = {}
68
+ def register_component(type: ComponentType):
69
+ def decorator(cls: Type[ComponentEngine]) -> Type[ComponentEngine]:
70
+ ComponentRegistry[type] = cls
71
+ return cls
72
+ return decorator
73
+
74
+ ComponentRegistry: Dict[ComponentType, Type[ComponentEngine]] = {}
@@ -1,6 +1,6 @@
1
1
  from typing import Type, Union, Literal, Optional, Dict, List, Tuple, Set, Annotated, Any
2
2
  from mindor.dsl.schema.component import ComponentConfig
3
- from .engine import ComponentEngine, ComponentEngineMap
3
+ from .base import ComponentEngine, ComponentRegistry
4
4
 
5
5
  ComponentInstances: Dict[str, ComponentEngine] = {}
6
6
 
@@ -32,7 +32,9 @@ def create_component(id: str, config: ComponentConfig, daemon: bool) -> Componen
32
32
  component = ComponentInstances[id] if id in ComponentInstances else None
33
33
 
34
34
  if not component:
35
- component = ComponentEngineMap[config.type](id, config, daemon)
35
+ if not ComponentRegistry:
36
+ from . import engine
37
+ component = ComponentRegistry[config.type](id, config, daemon)
36
38
  ComponentInstances[id] = component
37
39
 
38
40
  return component
@@ -1,6 +1,6 @@
1
- from .base import *
2
1
  from .http_server import *
3
2
  from .http_client import *
4
3
  from .mcp_server import *
5
4
  from .mcp_client import *
5
+ from .workflow import *
6
6
  from .shell import *
@@ -6,8 +6,8 @@ from mindor.core.listener import HttpCallbackListener
6
6
  from mindor.core.utils.http_client import HttpClient
7
7
  from mindor.core.utils.http_status import is_status_code_matched
8
8
  from mindor.core.utils.time import parse_duration
9
- from .base import ComponentEngine, ComponentType, ComponentEngineMap
10
- from .context import ComponentActionContext
9
+ from ..base import ComponentEngine, ComponentType, register_component
10
+ from ..context import ComponentActionContext
11
11
  from datetime import datetime, timezone
12
12
  import asyncio
13
13
 
@@ -19,10 +19,6 @@ class HttpClientCompletion(ABC):
19
19
  async def run(self, context: ComponentActionContext, client: HttpClient) -> Any:
20
20
  pass
21
21
 
22
- @abstractmethod
23
- async def close(self) -> None:
24
- pass
25
-
26
22
  class HttpClientPollingCompletion(HttpClientCompletion):
27
23
  async def run(self, context: ComponentActionContext, client: HttpClient) -> Any:
28
24
  url_or_path = await self._resolve_url_or_path(context)
@@ -31,8 +27,8 @@ class HttpClientPollingCompletion(HttpClientCompletion):
31
27
  body = await context.render_variable(self.config.body)
32
28
  headers = await context.render_variable(self.config.headers)
33
29
 
34
- interval = parse_duration(self.config.interval) if self.config.interval else 5.0
35
- timeout = parse_duration(self.config.timeout) if self.config.timeout else 300
30
+ interval = parse_duration((await context.render_variable(self.config.interval)) or 5.0)
31
+ timeout = parse_duration((await context.render_variable(self.config.timeout)) or 300.0)
36
32
  deadline = datetime.now(timezone.utc) + timeout
37
33
 
38
34
  await asyncio.sleep(interval.total_seconds())
@@ -73,9 +69,6 @@ class HttpClientCallbackCompletion(HttpClientCompletion):
73
69
  HttpCallbackListener.register_pending_future(callback_id, future)
74
70
 
75
71
  return await future
76
-
77
- async def close(self) -> None:
78
- pass
79
72
 
80
73
  class HttpClientAction:
81
74
  def __init__(self, config: HttpClientActionConfig):
@@ -112,16 +105,13 @@ class HttpClientAction:
112
105
 
113
106
  return (await context.render_variable(self.config.output, ignore_files=True)) if self.config.output else (result or response)
114
107
 
115
- async def close(self) -> None:
116
- if self.completion:
117
- await self.completion.close()
118
-
119
108
  async def _resolve_url_or_path(self, context: ComponentActionContext) -> str:
120
109
  if self.config.path:
121
110
  return await context.render_variable(self.config.path)
122
111
 
123
112
  return await context.render_variable(self.config.endpoint)
124
113
 
114
+ @register_component(ComponentType.HTTP_CLIENT)
125
115
  class HttpClientComponent(ComponentEngine):
126
116
  def __init__(self, id: str, config: HttpClientComponentConfig, daemon: bool):
127
117
  super().__init__(id, config, daemon)
@@ -137,5 +127,3 @@ class HttpClientComponent(ComponentEngine):
137
127
 
138
128
  async def _run(self, action: ActionConfig, context: ComponentActionContext) -> Any:
139
129
  return await HttpClientAction(action).run(context, self.client)
140
-
141
- ComponentEngineMap[ComponentType.HTTP_CLIENT] = HttpClientComponent
@@ -1,8 +1,9 @@
1
1
  from typing import Type, Union, Literal, Optional, Dict, List, Tuple, Set, Annotated, Any
2
2
  from mindor.dsl.schema.component import HttpServerComponentConfig
3
- from .base import ComponentEngine, ComponentType, ComponentEngineMap, ActionConfig
4
- from .context import ComponentActionContext
3
+ from ..base import ComponentEngine, ComponentType, ActionConfig, register_component
4
+ from ..context import ComponentActionContext
5
5
 
6
+ @register_component(ComponentType.HTTP_SERVER)
6
7
  class HttpServerComponent(ComponentEngine):
7
8
  def __init__(self, id: str, config: HttpServerComponentConfig, daemon: bool):
8
9
  super().__init__(id, config, daemon)
@@ -15,5 +16,3 @@ class HttpServerComponent(ComponentEngine):
15
16
 
16
17
  async def _run(self, action: ActionConfig, context: ComponentActionContext) -> Any:
17
18
  return {}
18
-
19
- ComponentEngineMap[ComponentType.HTTP_SERVER] = HttpServerComponent
@@ -2,8 +2,8 @@ from typing import Type, Union, Literal, Optional, Dict, List, Tuple, Set, Annot
2
2
  from mindor.dsl.schema.component import McpClientComponentConfig
3
3
  from mindor.dsl.schema.action import ActionConfig, McpClientActionConfig
4
4
  from mindor.core.utils.mcp_client import McpClient, ContentBlock, TextContent, ImageContent, AudioContent
5
- from .base import ComponentEngine, ComponentType, ComponentEngineMap, ActionConfig
6
- from .context import ComponentActionContext
5
+ from ..base import ComponentEngine, ComponentType, ActionConfig, register_component
6
+ from ..context import ComponentActionContext
7
7
 
8
8
  class McpClientAction():
9
9
  def __init__(self, config: McpClientActionConfig):
@@ -17,9 +17,6 @@ class McpClientAction():
17
17
  context.register_source("response", response)
18
18
 
19
19
  return (await context.render_variable(self.config.output, ignore_files=True)) if self.config.output else response
20
-
21
- async def close(self) -> None:
22
- pass
23
20
 
24
21
  async def _convert_output_value(self, content: ContentBlock) -> Any:
25
22
  if isinstance(content, TextContent):
@@ -30,6 +27,7 @@ class McpClientAction():
30
27
 
31
28
  return None
32
29
 
30
+ @register_component(ComponentType.MCP_CLIENT)
33
31
  class McpClientComponent(ComponentEngine):
34
32
  def __init__(self, id: str, config: McpClientComponentConfig, daemon: bool):
35
33
  super().__init__(id, config, daemon)
@@ -43,5 +41,3 @@ class McpClientComponent(ComponentEngine):
43
41
 
44
42
  async def _run(self, action: ActionConfig, context: ComponentActionContext) -> Any:
45
43
  return await McpClientAction(action).run(context, self.client)
46
-
47
- ComponentEngineMap[ComponentType.MCP_CLIENT] = McpClientComponent
@@ -1,8 +1,9 @@
1
1
  from typing import Type, Union, Literal, Optional, Dict, List, Tuple, Set, Annotated, Any
2
2
  from mindor.dsl.schema.component import McpServerComponentConfig
3
- from .base import ComponentEngine, ComponentType, ComponentEngineMap, ActionConfig
4
- from .context import ComponentActionContext
3
+ from ..base import ComponentEngine, ComponentType, ActionConfig, register_component
4
+ from ..context import ComponentActionContext
5
5
 
6
+ @register_component(ComponentType.MCP_SERVER)
6
7
  class McpServerComponent(ComponentEngine):
7
8
  def __init__(self, id: str, config: McpServerComponentConfig, daemon: bool):
8
9
  super().__init__(id, config, daemon)
@@ -15,5 +16,3 @@ class McpServerComponent(ComponentEngine):
15
16
 
16
17
  async def _run(self, action: ActionConfig, context: ComponentActionContext) -> Any:
17
18
  return {}
18
-
19
- ComponentEngineMap[ComponentType.MCP_SERVER] = McpServerComponent
@@ -1,8 +1,8 @@
1
1
  from typing import Type, Union, Literal, Optional, Dict, List, Tuple, Set, Annotated, Any
2
2
  from mindor.dsl.schema.component import ShellComponentConfig
3
3
  from mindor.dsl.schema.action import ActionConfig, ShellActionConfig
4
- from .base import ComponentEngine, ComponentType, ComponentEngineMap, ActionConfig
5
- from .context import ComponentActionContext
4
+ from ..base import ComponentEngine, ComponentType, ActionConfig, register_component
5
+ from ..context import ComponentActionContext
6
6
  from asyncio.subprocess import Process
7
7
  import asyncio, os
8
8
 
@@ -64,6 +64,7 @@ class ShellAction:
64
64
  working_dir = self.base_dir or os.getcwd()
65
65
  return working_dir
66
66
 
67
+ @register_component(ComponentType.SHELL)
67
68
  class ShellComponent(ComponentEngine):
68
69
  def __init__(self, id: str, config: ShellComponentConfig, daemon: bool):
69
70
  super().__init__(id, config, daemon)
@@ -76,5 +77,3 @@ class ShellComponent(ComponentEngine):
76
77
 
77
78
  async def _run(self, action: ActionConfig, context: ComponentActionContext) -> Any:
78
79
  return await ShellAction(action, self.config.base_dir, self.config.env).run(context)
79
-
80
- ComponentEngineMap[ComponentType.SHELL] = ShellComponent
@@ -0,0 +1,41 @@
1
+ from typing import Type, Union, Literal, Optional, Dict, List, Tuple, Set, Annotated, Any
2
+ from mindor.dsl.schema.component import ComponentConfig, WorkflowComponentConfig
3
+ from mindor.dsl.schema.action import ActionConfig, WorkflowActionConfig
4
+ from mindor.dsl.schema.workflow import WorkflowConfig
5
+ from mindor.core.workflow import Workflow, WorkflowResolver, create_workflow
6
+ from ..base import ComponentEngine, ComponentType, ActionConfig, register_component
7
+ from ..context import ComponentActionContext
8
+ import asyncio, os
9
+
10
+ class WorkflowAction:
11
+ def __init__(self, config: WorkflowActionConfig):
12
+ self.config: WorkflowActionConfig = config
13
+ self.components: Dict[str, ComponentConfig] = None
14
+ self.workflows: Dict[str, WorkflowConfig] = None
15
+
16
+ async def run(self, context: ComponentActionContext) -> Any:
17
+ workflow_id = await context.render_variable(self.config.workflow)
18
+ input = await context.render_variable(self.config.input)
19
+
20
+ workflow = self._create_workflow(workflow_id)
21
+ output = await workflow.run(context.call_id, input)
22
+ context.register_source("output", output)
23
+
24
+ return (await context.render_variable(self.config.output, ignore_files=True)) if self.config.output else output
25
+
26
+ def _create_workflow(self, workflow_id: Optional[str]) -> Workflow:
27
+ return create_workflow(*WorkflowResolver(self.workflows).resolve(workflow_id), self.components)
28
+
29
+ @register_component(ComponentType.WORKFLOW)
30
+ class WorkflowComponent(ComponentEngine):
31
+ def __init__(self, id: str, config: WorkflowComponentConfig, daemon: bool):
32
+ super().__init__(id, config, daemon)
33
+
34
+ async def _serve(self) -> None:
35
+ pass
36
+
37
+ async def _shutdown(self) -> None:
38
+ pass
39
+
40
+ async def _run(self, action: ActionConfig, context: ComponentActionContext) -> Any:
41
+ return await WorkflowAction(action).run(context)
@@ -127,7 +127,7 @@ class ControllerEngine(AsyncService):
127
127
  self.task_states.set(task_id, state)
128
128
 
129
129
  try:
130
- workflow = create_workflow(*WorkflowResolver(self.workflows).resolve(workflow_id), self.components)
130
+ workflow = self._create_workflow(workflow_id)
131
131
  output = await workflow.run(task_id, input)
132
132
  state = TaskState(task_id=task_id, status="completed", output=output)
133
133
  except Exception as e:
@@ -153,4 +153,10 @@ class ControllerEngine(AsyncService):
153
153
  def _create_workflow(self, workflow_id: Optional[str]) -> Workflow:
154
154
  return create_workflow(*WorkflowResolver(self.workflows).resolve(workflow_id), self.components)
155
155
 
156
- ControllerEngineMap: Dict[ControllerType, Type[ControllerEngine]] = {}
156
+ def register_controller(type: ControllerType):
157
+ def decorator(cls: Type[ControllerEngine]) -> Type[ControllerEngine]:
158
+ ControllerRegistry[type] = cls
159
+ return cls
160
+ return decorator
161
+
162
+ ControllerRegistry: Dict[ControllerType, Type[ControllerEngine]] = {}
@@ -4,7 +4,7 @@ from mindor.dsl.schema.component import ComponentConfig
4
4
  from mindor.dsl.schema.listener import ListenerConfig
5
5
  from mindor.dsl.schema.gateway import GatewayConfig
6
6
  from mindor.dsl.schema.workflow import WorkflowConfig
7
- from .engine import ControllerEngine, ControllerEngineMap
7
+ from .base import ControllerEngine, ControllerRegistry
8
8
 
9
9
  def create_controller(
10
10
  config: ControllerConfig,
@@ -15,6 +15,8 @@ def create_controller(
15
15
  daemon: bool
16
16
  ) -> ControllerEngine:
17
17
  try:
18
- return ControllerEngineMap[config.type](config, components, listeners, gateways, workflows, daemon)
18
+ if not ControllerRegistry:
19
+ from . import engine
20
+ return ControllerRegistry[config.type](config, components, listeners, gateways, workflows, daemon)
19
21
  except KeyError:
20
22
  raise ValueError(f"Unsupported controller type: {config.type}")
@@ -1,3 +1,2 @@
1
- from .base import *
2
1
  from .http_server import *
3
2
  from .mcp_server import *
@@ -1,7 +1,6 @@
1
1
  from typing import Type, Union, Literal, Optional, Dict, List, Tuple, Set, Annotated, Any
2
2
  from typing_extensions import Self
3
3
  from pydantic import BaseModel
4
-
5
4
  from mindor.dsl.schema.controller import HttpServerControllerConfig
6
5
  from mindor.dsl.schema.component import ComponentConfig
7
6
  from mindor.dsl.schema.listener import ListenerConfig
@@ -9,8 +8,7 @@ from mindor.dsl.schema.gateway import GatewayConfig
9
8
  from mindor.dsl.schema.workflow import WorkflowConfig
10
9
  from mindor.core.utils.http_request import parse_request_body, parse_options_header
11
10
  from mindor.core.utils.streaming import StreamResource
12
- from .base import ControllerEngine, ControllerType, ControllerEngineMap, TaskState
13
-
11
+ from ..base import ControllerEngine, ControllerType, TaskState, register_controller
14
12
  from fastapi import FastAPI, APIRouter, Request, Body, HTTPException
15
13
  from fastapi.middleware.cors import CORSMiddleware
16
14
  from fastapi.responses import Response, JSONResponse, StreamingResponse
@@ -38,6 +36,7 @@ class TaskResult(BaseModel):
38
36
  error=instance.error
39
37
  )
40
38
 
39
+ @register_controller(ControllerType.HTTP_SERVER)
41
40
  class HttpServerController(ControllerEngine):
42
41
  def __init__(
43
42
  self,
@@ -160,5 +159,3 @@ class HttpServerController(ControllerEngine):
160
159
  headers["Content-Disposition"] = f'attachment; filename="{filename}"'
161
160
 
162
161
  return headers
163
-
164
- ControllerEngineMap[ControllerType.HTTP_SERVER] = HttpServerController
@@ -7,8 +7,8 @@ from mindor.dsl.schema.workflow import WorkflowConfig, WorkflowVariableConfig
7
7
  from mindor.core.workflow.schema import WorkflowSchema, create_workflow_schema
8
8
  from mindor.core.utils.streaming import StreamResource, Base64StreamResource
9
9
  from mindor.core.utils.streaming import save_stream_to_temporary_file
10
- from mindor.core.utils.http_client import HttpClient
11
- from .base import ControllerEngine, ControllerType, ControllerEngineMap, TaskState
10
+ from mindor.core.utils.http_client import create_stream_with_url
11
+ from ..base import ControllerEngine, ControllerType, TaskState, register_controller
12
12
  from mcp.server.fastmcp.server import FastMCP
13
13
  from mcp.types import TextContent, ImageContent, AudioContent
14
14
  import uvicorn, re, json
@@ -83,7 +83,7 @@ class WorkflowToolGenerator():
83
83
  return await save_stream_to_temporary_file(Base64StreamResource(value), subtype)
84
84
 
85
85
  if format == "url" and isinstance(value, str):
86
- return await save_stream_to_temporary_file(await HttpClient.request_once(value), subtype)
86
+ return await save_stream_to_temporary_file(await create_stream_with_url(value), subtype)
87
87
 
88
88
  if isinstance(value, StreamResource):
89
89
  return await save_stream_to_temporary_file(value, subtype)
@@ -132,6 +132,7 @@ class WorkflowToolGenerator():
132
132
 
133
133
  return "str"
134
134
 
135
+ @register_controller(ControllerType.MCP_SERVER)
135
136
  class McpServerController(ControllerEngine):
136
137
  def __init__(
137
138
  self,
@@ -180,5 +181,3 @@ class McpServerController(ControllerEngine):
180
181
  async def _shutdown(self) -> None:
181
182
  if self.server:
182
183
  self.server.should_exit = True
183
-
184
- ControllerEngineMap[ControllerType.MCP_SERVER] = McpServerController
@@ -36,10 +36,10 @@ class HttpControllerClient(ControllerClient):
36
36
  def _resolve_controller_url(self) -> str:
37
37
  return f"http://localhost:{self.config.port}{self.config.base_path or ''}"
38
38
 
39
- def _flatten_for_multipart(self, body: Dict[str, Any], key: str = "") -> List[Tuple[str, Any]]:
39
+ def _flatten_for_multipart(self, data: Dict[str, Any], key: str = "") -> List[Tuple[str, Any]]:
40
40
  flattened = []
41
41
 
42
- for subkey, value in body.items():
42
+ for subkey, value in data.items():
43
43
  full_key = f"{key}[{subkey}]" if key else subkey
44
44
 
45
45
  if isinstance(value, dict):
@@ -4,7 +4,7 @@ from mindor.core.workflow.schema import WorkflowSchema
4
4
  from mindor.core.utils.mcp_client import McpClient, ContentBlock, TextContent, ImageContent, AudioContent
5
5
  from mindor.core.utils.streaming import StreamResource, FileStreamResource, Base64StreamResource
6
6
  from mindor.core.utils.streaming import encode_stream_to_base64
7
- from mindor.core.utils.http_client import HttpClient
7
+ from mindor.core.utils.http_client import create_stream_with_url
8
8
  from mindor.core.utils.image import load_image_from_stream
9
9
  from .runner import ControllerClient
10
10
  import json, os
@@ -59,7 +59,7 @@ class McpControllerClient(ControllerClient):
59
59
  return await load_image_from_stream(Base64StreamResource(value), subtype)
60
60
 
61
61
  if format == "url" and isinstance(value, str):
62
- return await load_image_from_stream(await HttpClient.request_once(value), subtype)
62
+ return await load_image_from_stream(await create_stream_with_url(value), subtype)
63
63
 
64
64
  if isinstance(value, StreamResource):
65
65
  return await load_image_from_stream(value, subtype)
@@ -4,7 +4,7 @@ from mindor.core.workflow.schema import WorkflowSchema
4
4
  from mindor.core.utils.streaming import StreamResource, Base64StreamResource
5
5
  from mindor.core.utils.streaming import save_stream_to_temporary_file
6
6
  from mindor.core.utils.http_request import create_upload_file
7
- from mindor.core.utils.http_client import HttpClient
7
+ from mindor.core.utils.http_client import create_stream_with_url
8
8
  from mindor.core.utils.image import load_image_from_stream
9
9
  import gradio as gr
10
10
  import json
@@ -184,7 +184,7 @@ class GradioWebUIBuilder:
184
184
  return await load_image_from_stream(Base64StreamResource(value), subtype)
185
185
 
186
186
  if format == "url" and isinstance(value, str):
187
- return await load_image_from_stream(await HttpClient.request_once(value), subtype)
187
+ return await load_image_from_stream(await create_stream_with_url(value), subtype)
188
188
 
189
189
  if isinstance(value, StreamResource):
190
190
  return await load_image_from_stream(value, subtype)
@@ -196,7 +196,7 @@ class GradioWebUIBuilder:
196
196
  return await save_stream_to_temporary_file(Base64StreamResource(value), subtype)
197
197
 
198
198
  if format == "url" and isinstance(value, str):
199
- return await save_stream_to_temporary_file(await HttpClient.request_once(value), subtype)
199
+ return await save_stream_to_temporary_file(await create_stream_with_url(value), subtype)
200
200
 
201
201
  if isinstance(value, StreamResource):
202
202
  return await save_stream_to_temporary_file(value, subtype)
@@ -0,0 +1,2 @@
1
+ from .gateway import *
2
+ from .engine import HttpTunnelGateway, SshTunnelGateway
@@ -14,4 +14,10 @@ class GatewayEngine(AsyncService):
14
14
  def get_context(self) -> Dict[str, Any]:
15
15
  pass
16
16
 
17
- GatewayEngineMap: Dict[GatewayType, Type[GatewayEngine]] = {}
17
+ def register_gateway(type: GatewayType):
18
+ def decorator(cls: Type[GatewayEngine]) -> Type[GatewayEngine]:
19
+ GatewayRegistry[type] = cls
20
+ return cls
21
+ return decorator
22
+
23
+ GatewayRegistry: Dict[GatewayType, Type[GatewayEngine]] = {}
@@ -1,2 +1,2 @@
1
- from .base import *
2
1
  from .http_tunnel import *
2
+ from .ssh_tunnel import *
@@ -1,8 +1,9 @@
1
1
  from typing import Type, Union, Literal, Optional, Dict, List, Tuple, Set, Annotated, Callable, Iterator, Any
2
2
  from mindor.dsl.schema.gateway import HttpTunnelGatewayConfig
3
- from .base import GatewayEngine, GatewayType, GatewayEngineMap
3
+ from ..base import GatewayEngine, GatewayType, register_gateway
4
4
  from pyngrok import ngrok
5
5
 
6
+ @register_gateway(GatewayType.HTTP_TUNNEL)
6
7
  class HttpTunnelGateway(GatewayEngine):
7
8
  def __init__(self, id: str, config: HttpTunnelGatewayConfig, daemon: bool):
8
9
  super().__init__(id, config, daemon)
@@ -29,5 +30,3 @@ class HttpTunnelGateway(GatewayEngine):
29
30
  self.tunnel = None
30
31
  self.public_url = None
31
32
  return
32
-
33
- GatewayEngineMap[GatewayType.HTTP_TUNNEL] = HttpTunnelGateway
@@ -1,8 +1,9 @@
1
1
  from typing import Type, Union, Literal, Optional, Dict, List, Tuple, Set, Annotated, Callable, Iterator, Any
2
2
  from mindor.dsl.schema.gateway import SshTunnelGatewayConfig
3
- from .base import GatewayEngine, GatewayType, GatewayEngineMap
3
+ from ..base import GatewayEngine, GatewayType, register_gateway
4
4
  from pyngrok import ngrok
5
5
 
6
+ @register_gateway(GatewayType.SSH_TUNNEL)
6
7
  class SshTunnelGateway(GatewayEngine):
7
8
  def __init__(self, id: str, config: SshTunnelGatewayConfig, daemon: bool):
8
9
  super().__init__(id, config, daemon)
@@ -21,5 +22,3 @@ class SshTunnelGateway(GatewayEngine):
21
22
 
22
23
  async def _shutdown(self) -> None:
23
24
  pass
24
-
25
- GatewayEngineMap[GatewayType.SSH_TUNNEL] = SshTunnelGateway
@@ -1,6 +1,6 @@
1
1
  from typing import Type, Union, Literal, Optional, Dict, List, Tuple, Set, Annotated, Any
2
2
  from mindor.dsl.schema.gateway import GatewayConfig
3
- from .engine import GatewayEngine, GatewayEngineMap
3
+ from .base import GatewayEngine, GatewayRegistry
4
4
 
5
5
  GatewayInstances: Dict[str, GatewayEngine] = {}
6
6
 
@@ -9,7 +9,9 @@ def create_gateway(id: str, config: GatewayConfig, daemon: bool) -> GatewayEngin
9
9
  gateway = GatewayInstances[id] if id in GatewayInstances else None
10
10
 
11
11
  if not gateway:
12
- gateway = GatewayEngineMap[config.type](id, config, daemon)
12
+ if not GatewayRegistry:
13
+ from . import engine
14
+ gateway = GatewayRegistry[config.type](id, config, daemon)
13
15
  GatewayInstances[id] = gateway
14
16
 
15
17
  return gateway
@@ -26,4 +26,10 @@ class ListenerEngine(AsyncService):
26
26
 
27
27
  await super()._stop()
28
28
 
29
- ListenerEngineMap: Dict[ListenerType, Type[ListenerEngine]] = {}
29
+ def register_listener(type: ListenerType):
30
+ def decorator(cls: Type[ListenerEngine]) -> Type[ListenerEngine]:
31
+ ListenerRegistry[type] = cls
32
+ return cls
33
+ return decorator
34
+
35
+ ListenerRegistry: Dict[ListenerType, Type[ListenerEngine]] = {}
@@ -1,2 +1 @@
1
- from .base import *
2
1
  from .http_callback import *
@@ -3,7 +3,7 @@ from typing import Type, Union, Literal, Optional, Dict, List, Tuple, Set, Annot
3
3
  from mindor.dsl.schema.listener import HttpCallbackListenerConfig, HttpCallbackConfig
4
4
  from mindor.core.utils.http_request import parse_request_body, parse_options_header
5
5
  from mindor.core.utils.renderer import VariableRenderer
6
- from .base import ListenerEngine, ListenerType, ListenerEngineMap
6
+ from ..base import ListenerEngine, ListenerType, register_listener
7
7
 
8
8
  from fastapi import FastAPI, APIRouter, Body, HTTPException, Request
9
9
  from fastapi.responses import Response, JSONResponse
@@ -45,6 +45,7 @@ class HttpCallbackContext:
45
45
  return self.query
46
46
  raise KeyError(f"Unknown source: {key}")
47
47
 
48
+ @register_listener(ListenerType.HTTP_CALLBACK)
48
49
  class HttpCallbackListener(ListenerEngine):
49
50
  def __init__(self, id: str, config: HttpCallbackListenerConfig, daemon: bool):
50
51
  super().__init__(id, config, daemon)
@@ -124,5 +125,3 @@ class HttpCallbackListener(ListenerEngine):
124
125
  def register_pending_future(cls, id: str, future: asyncio.Future) -> None:
125
126
  with _pending_futures_lock:
126
127
  _pending_futures[id] = future
127
-
128
- ListenerEngineMap[ListenerType.HTTP_CALLBACK] = HttpCallbackListener
@@ -1,6 +1,6 @@
1
1
  from typing import Type, Union, Literal, Optional, Dict, List, Tuple, Set, Annotated, Any
2
2
  from mindor.dsl.schema.listener import ListenerConfig
3
- from .engine import ListenerEngine, ListenerEngineMap
3
+ from .base import ListenerEngine, ListenerRegistry
4
4
 
5
5
  ListenerInstances: Dict[str, ListenerEngine] = {}
6
6
 
@@ -9,7 +9,9 @@ def create_listener(id: str, config: ListenerConfig, daemon: bool) -> ListenerEn
9
9
  listener = ListenerInstances[id] if id in ListenerInstances else None
10
10
 
11
11
  if not listener:
12
- listener = ListenerEngineMap[config.type](id, config, daemon)
12
+ if not ListenerEngine:
13
+ from . import engine
14
+ listener = ListenerRegistry[config.type](id, config, daemon)
13
15
  ListenerInstances[id] = listener
14
16
 
15
17
  return listener
@@ -32,6 +32,8 @@ class HttpStreamResource(StreamResource):
32
32
  yield chunk
33
33
 
34
34
  class HttpClient:
35
+ shared_instance: Optional["HttpClient"] = None
36
+
35
37
  def __init__(self, base_url: Optional[str] = None, headers: Optional[Dict[str, str]] = None):
36
38
  self.base_url: Optional[str] = base_url
37
39
  self.headers: Optional[Dict[str, str]] = headers
@@ -76,6 +78,12 @@ class HttpClient:
76
78
  await self.session.close()
77
79
  self.session = None
78
80
 
81
+ @classmethod
82
+ def get_shared_instance(cls) -> "HttpClient":
83
+ if not cls.shared_instance:
84
+ cls.shared_instance = HttpClient()
85
+ return cls.shared_instance
86
+
79
87
  @classmethod
80
88
  async def request_once(cls, *args, **kwargs):
81
89
  instance = cls()
@@ -83,7 +91,7 @@ class HttpClient:
83
91
  return await instance.request(*args, **kwargs)
84
92
  finally:
85
93
  await instance.close()
86
-
94
+
87
95
  def _create_session(self, base_url: Optional[str]) -> aiohttp.ClientSession:
88
96
  return aiohttp.ClientSession(base_url.rstrip("/") + "/" if base_url else None)
89
97
 
@@ -125,3 +133,6 @@ class HttpClient:
125
133
  filename = disposition.get("filename")
126
134
 
127
135
  return (HttpStreamResource(response, content_type, filename), content_type)
136
+
137
+ async def create_stream_with_url(url: str):
138
+ return await HttpClient.get_shared_instance().request(url)
@@ -10,5 +10,5 @@ async def load_image_from_stream(stream: StreamResource, extension: Optional[str
10
10
  async for chunk in stream:
11
11
  data.extend(chunk)
12
12
  return PILImage.open(io.BytesIO(data))
13
- except Exception:
13
+ except Exception as e:
14
14
  return None