model-compose 0.0.8__tar.gz → 0.0.9__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 (106) hide show
  1. {model_compose-0.0.8/src/model_compose.egg-info → model_compose-0.0.9}/PKG-INFO +1 -1
  2. {model_compose-0.0.8 → model_compose-0.0.9}/pyproject.toml +1 -1
  3. model_compose-0.0.9/src/mindor/core/component/engine/http_client.py +120 -0
  4. model_compose-0.0.9/src/mindor/core/gateway/__init__.py +2 -0
  5. model_compose-0.0.9/src/mindor/core/gateway/engine/__init__.py +2 -0
  6. model_compose-0.0.9/src/mindor/core/gateway/engine/base.py +12 -0
  7. model_compose-0.0.9/src/mindor/core/gateway/engine/http_tunnel.py +9 -0
  8. model_compose-0.0.9/src/mindor/core/gateway/gateway.py +9 -0
  9. {model_compose-0.0.8 → model_compose-0.0.9}/src/mindor/core/listener/engine/http_callback.py +9 -9
  10. {model_compose-0.0.8 → model_compose-0.0.9}/src/mindor/core/services/async_service.py +1 -1
  11. model_compose-0.0.9/src/mindor/core/utils/http_client.py +79 -0
  12. {model_compose-0.0.8 → model_compose-0.0.9}/src/mindor/core/utils/template.py +17 -8
  13. model_compose-0.0.9/src/mindor/core/utils/time.py +13 -0
  14. {model_compose-0.0.8 → model_compose-0.0.9}/src/mindor/dsl/schema/action/impl/http_client.py +11 -8
  15. model_compose-0.0.9/src/mindor/dsl/schema/gateway/__init__.py +1 -0
  16. model_compose-0.0.9/src/mindor/dsl/schema/gateway/gateway.py +10 -0
  17. model_compose-0.0.9/src/mindor/dsl/schema/gateway/impl/__init__.py +2 -0
  18. model_compose-0.0.9/src/mindor/dsl/schema/gateway/impl/common.py +7 -0
  19. model_compose-0.0.9/src/mindor/dsl/schema/gateway/impl/http_tunnel.py +9 -0
  20. model_compose-0.0.9/src/mindor/dsl/schema/gateway/impl/types.py +4 -0
  21. {model_compose-0.0.8 → model_compose-0.0.9}/src/mindor/dsl/schema/listener/impl/http_callback.py +1 -1
  22. {model_compose-0.0.8 → model_compose-0.0.9/src/model_compose.egg-info}/PKG-INFO +1 -1
  23. {model_compose-0.0.8 → model_compose-0.0.9}/src/model_compose.egg-info/SOURCES.txt +13 -0
  24. model_compose-0.0.8/src/mindor/core/component/engine/http_client.py +0 -141
  25. {model_compose-0.0.8 → model_compose-0.0.9}/LICENSE +0 -0
  26. {model_compose-0.0.8 → model_compose-0.0.9}/README.md +0 -0
  27. {model_compose-0.0.8 → model_compose-0.0.9}/setup.cfg +0 -0
  28. {model_compose-0.0.8 → model_compose-0.0.9}/src/mindor/__init__.py +0 -0
  29. {model_compose-0.0.8 → model_compose-0.0.9}/src/mindor/cli/__init__.py +0 -0
  30. {model_compose-0.0.8 → model_compose-0.0.9}/src/mindor/cli/compose.py +0 -0
  31. {model_compose-0.0.8 → model_compose-0.0.9}/src/mindor/core/__init__.py +0 -0
  32. {model_compose-0.0.8 → model_compose-0.0.9}/src/mindor/core/component/__init__.py +0 -0
  33. {model_compose-0.0.8 → model_compose-0.0.9}/src/mindor/core/component/component.py +0 -0
  34. {model_compose-0.0.8 → model_compose-0.0.9}/src/mindor/core/component/engine/__init__.py +0 -0
  35. {model_compose-0.0.8 → model_compose-0.0.9}/src/mindor/core/component/engine/base.py +0 -0
  36. {model_compose-0.0.8 → model_compose-0.0.9}/src/mindor/core/component/engine/context.py +0 -0
  37. {model_compose-0.0.8 → model_compose-0.0.9}/src/mindor/core/component/engine/http_server.py +0 -0
  38. {model_compose-0.0.8 → model_compose-0.0.9}/src/mindor/core/compose/__init__.py +0 -0
  39. {model_compose-0.0.8 → model_compose-0.0.9}/src/mindor/core/compose/compose.py +0 -0
  40. {model_compose-0.0.8 → model_compose-0.0.9}/src/mindor/core/compose/manager.py +0 -0
  41. {model_compose-0.0.8 → model_compose-0.0.9}/src/mindor/core/controller/__init__.py +0 -0
  42. {model_compose-0.0.8 → model_compose-0.0.9}/src/mindor/core/controller/controller.py +0 -0
  43. {model_compose-0.0.8 → model_compose-0.0.9}/src/mindor/core/controller/engine/__init__.py +0 -0
  44. {model_compose-0.0.8 → model_compose-0.0.9}/src/mindor/core/controller/engine/base.py +0 -0
  45. {model_compose-0.0.8 → model_compose-0.0.9}/src/mindor/core/controller/engine/http_server.py +0 -0
  46. {model_compose-0.0.8 → model_compose-0.0.9}/src/mindor/core/controller/engine/mcp_server.py +0 -0
  47. {model_compose-0.0.8 → model_compose-0.0.9}/src/mindor/core/listener/__init__.py +0 -0
  48. {model_compose-0.0.8 → model_compose-0.0.9}/src/mindor/core/listener/engine/__init__.py +0 -0
  49. {model_compose-0.0.8 → model_compose-0.0.9}/src/mindor/core/listener/engine/base.py +0 -0
  50. {model_compose-0.0.8 → model_compose-0.0.9}/src/mindor/core/listener/listener.py +0 -0
  51. {model_compose-0.0.8 → model_compose-0.0.9}/src/mindor/core/runtime/__init__.py +0 -0
  52. {model_compose-0.0.8 → model_compose-0.0.9}/src/mindor/core/runtime/env.py +0 -0
  53. {model_compose-0.0.8 → model_compose-0.0.9}/src/mindor/core/services/__init__.py +0 -0
  54. {model_compose-0.0.8 → model_compose-0.0.9}/src/mindor/core/utils/__init__.py +0 -0
  55. {model_compose-0.0.8 → model_compose-0.0.9}/src/mindor/core/utils/expiring.py +0 -0
  56. {model_compose-0.0.8 → model_compose-0.0.9}/src/mindor/core/utils/http_headers.py +0 -0
  57. {model_compose-0.0.8 → model_compose-0.0.9}/src/mindor/core/utils/resources.py +0 -0
  58. {model_compose-0.0.8 → model_compose-0.0.9}/src/mindor/core/utils/workqueue.py +0 -0
  59. {model_compose-0.0.8 → model_compose-0.0.9}/src/mindor/core/workflow/__init__.py +0 -0
  60. {model_compose-0.0.8 → model_compose-0.0.9}/src/mindor/core/workflow/context.py +0 -0
  61. {model_compose-0.0.8 → model_compose-0.0.9}/src/mindor/core/workflow/job.py +0 -0
  62. {model_compose-0.0.8 → model_compose-0.0.9}/src/mindor/core/workflow/workflow.py +0 -0
  63. {model_compose-0.0.8 → model_compose-0.0.9}/src/mindor/dsl/__init__.py +0 -0
  64. {model_compose-0.0.8 → model_compose-0.0.9}/src/mindor/dsl/loader.py +0 -0
  65. {model_compose-0.0.8 → model_compose-0.0.9}/src/mindor/dsl/schema/__init__.py +0 -0
  66. {model_compose-0.0.8 → model_compose-0.0.9}/src/mindor/dsl/schema/action/__init__.py +0 -0
  67. {model_compose-0.0.8 → model_compose-0.0.9}/src/mindor/dsl/schema/action/action.py +0 -0
  68. {model_compose-0.0.8 → model_compose-0.0.9}/src/mindor/dsl/schema/action/impl/__init__.py +0 -0
  69. {model_compose-0.0.8 → model_compose-0.0.9}/src/mindor/dsl/schema/action/impl/common.py +0 -0
  70. {model_compose-0.0.8 → model_compose-0.0.9}/src/mindor/dsl/schema/action/impl/http_server.py +0 -0
  71. {model_compose-0.0.8 → model_compose-0.0.9}/src/mindor/dsl/schema/action/impl/mcp_client.py +0 -0
  72. {model_compose-0.0.8 → model_compose-0.0.9}/src/mindor/dsl/schema/action/impl/mcp_server.py +0 -0
  73. {model_compose-0.0.8 → model_compose-0.0.9}/src/mindor/dsl/schema/component/__init__.py +0 -0
  74. {model_compose-0.0.8 → model_compose-0.0.9}/src/mindor/dsl/schema/component/component.py +0 -0
  75. {model_compose-0.0.8 → model_compose-0.0.9}/src/mindor/dsl/schema/component/impl/__init__.py +0 -0
  76. {model_compose-0.0.8 → model_compose-0.0.9}/src/mindor/dsl/schema/component/impl/common.py +0 -0
  77. {model_compose-0.0.8 → model_compose-0.0.9}/src/mindor/dsl/schema/component/impl/http_client.py +0 -0
  78. {model_compose-0.0.8 → model_compose-0.0.9}/src/mindor/dsl/schema/component/impl/http_server.py +0 -0
  79. {model_compose-0.0.8 → model_compose-0.0.9}/src/mindor/dsl/schema/component/impl/mcp_client.py +0 -0
  80. {model_compose-0.0.8 → model_compose-0.0.9}/src/mindor/dsl/schema/component/impl/mcp_server.py +0 -0
  81. {model_compose-0.0.8 → model_compose-0.0.9}/src/mindor/dsl/schema/component/impl/types.py +0 -0
  82. {model_compose-0.0.8 → model_compose-0.0.9}/src/mindor/dsl/schema/compose.py +0 -0
  83. {model_compose-0.0.8 → model_compose-0.0.9}/src/mindor/dsl/schema/controller/__init__.py +0 -0
  84. {model_compose-0.0.8 → model_compose-0.0.9}/src/mindor/dsl/schema/controller/controller.py +0 -0
  85. {model_compose-0.0.8 → model_compose-0.0.9}/src/mindor/dsl/schema/controller/impl/__init__.py +0 -0
  86. {model_compose-0.0.8 → model_compose-0.0.9}/src/mindor/dsl/schema/controller/impl/common.py +0 -0
  87. {model_compose-0.0.8 → model_compose-0.0.9}/src/mindor/dsl/schema/controller/impl/http_server.py +0 -0
  88. {model_compose-0.0.8 → model_compose-0.0.9}/src/mindor/dsl/schema/controller/impl/mcp_server.py +0 -0
  89. {model_compose-0.0.8 → model_compose-0.0.9}/src/mindor/dsl/schema/controller/impl/types.py +0 -0
  90. {model_compose-0.0.8 → model_compose-0.0.9}/src/mindor/dsl/schema/listener/__init__.py +0 -0
  91. {model_compose-0.0.8 → model_compose-0.0.9}/src/mindor/dsl/schema/listener/impl/__init__.py +0 -0
  92. {model_compose-0.0.8 → model_compose-0.0.9}/src/mindor/dsl/schema/listener/impl/common.py +0 -0
  93. {model_compose-0.0.8 → model_compose-0.0.9}/src/mindor/dsl/schema/listener/impl/types.py +0 -0
  94. {model_compose-0.0.8 → model_compose-0.0.9}/src/mindor/dsl/schema/listener/listener.py +0 -0
  95. {model_compose-0.0.8 → model_compose-0.0.9}/src/mindor/dsl/schema/runtime/__init__.py +0 -0
  96. {model_compose-0.0.8 → model_compose-0.0.9}/src/mindor/dsl/schema/runtime/impl/__init__.py +0 -0
  97. {model_compose-0.0.8 → model_compose-0.0.9}/src/mindor/dsl/schema/runtime/impl/common.py +0 -0
  98. {model_compose-0.0.8 → model_compose-0.0.9}/src/mindor/dsl/schema/runtime/impl/docker.py +0 -0
  99. {model_compose-0.0.8 → model_compose-0.0.9}/src/mindor/dsl/schema/runtime/impl/types.py +0 -0
  100. {model_compose-0.0.8 → model_compose-0.0.9}/src/mindor/dsl/schema/runtime/runtime.py +0 -0
  101. {model_compose-0.0.8 → model_compose-0.0.9}/src/mindor/dsl/schema/workflow.py +0 -0
  102. {model_compose-0.0.8 → model_compose-0.0.9}/src/model_compose.egg-info/dependency_links.txt +0 -0
  103. {model_compose-0.0.8 → model_compose-0.0.9}/src/model_compose.egg-info/entry_points.txt +0 -0
  104. {model_compose-0.0.8 → model_compose-0.0.9}/src/model_compose.egg-info/requires.txt +0 -0
  105. {model_compose-0.0.8 → model_compose-0.0.9}/src/model_compose.egg-info/top_level.txt +0 -0
  106. {model_compose-0.0.8 → model_compose-0.0.9}/tests/test_cli.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: model-compose
3
- Version: 0.0.8
3
+ Version: 0.0.9
4
4
  Summary: model-compose: Declarative AI Model and Workflow Orchestrator (from Mindor)
5
5
  Author-email: Hanyeol Cho <hanyeol.cho@gmail.com>
6
6
  License-Expression: MIT
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "model-compose"
3
- version = "0.0.8"
3
+ version = "0.0.9"
4
4
  description = "model-compose: Declarative AI Model and Workflow Orchestrator (from Mindor)"
5
5
  authors = [
6
6
  { name = "Hanyeol Cho", email = "hanyeol.cho@gmail.com" }
@@ -0,0 +1,120 @@
1
+ from typing import Type, Union, Literal, Optional, Dict, List, Tuple, Set, Annotated, AsyncIterator, Any
2
+ from mindor.dsl.schema.component import HttpClientComponentConfig
3
+ from mindor.dsl.schema.action import ActionConfig, HttpClientActionConfig, HttpClientCompletionConfig
4
+ from mindor.core.listener import HttpCallbackListener
5
+ from mindor.core.utils.http_client import HttpClient
6
+ from mindor.core.utils.time import parse_duration
7
+ from .base import BaseComponent, ComponentType, ComponentEngineMap
8
+ from .context import ComponentContext
9
+ from datetime import datetime, timezone
10
+ import asyncio
11
+
12
+ class HttpClientPollingCompletion:
13
+ def __init__(self, base_url: Optional[str], config: HttpClientCompletionConfig):
14
+ self.base_url: Optional[str] = base_url
15
+ self.config: HttpClientCompletionConfig = config
16
+ self.client: HttpClient = HttpClient()
17
+
18
+ async def run(self, context: ComponentContext) -> Any:
19
+ url = self._resolve_request_url(context)
20
+ method = context.render_template(self.config.method)
21
+ params = context.render_template(self.config.params)
22
+ body = context.render_template(self.config.body)
23
+ headers = context.render_template(self.config.headers)
24
+
25
+ interval = parse_duration(self.config.interval) if self.config.interval else 5.0
26
+ timeout = parse_duration(self.config.timeout) if self.config.timeout else 300
27
+ deadline = datetime.now(timezone.utc) + timeout
28
+
29
+ await asyncio.sleep(interval.total_seconds())
30
+
31
+ while datetime.now(timezone.utc) < deadline:
32
+ response = await self.client.request(url, method, params, body, headers)
33
+
34
+ context.register_source("response", response)
35
+ status = context.render_template(self.config.status) if self.config.status else response["status"]
36
+
37
+ success_when = context.render_template(self.config.success_when) if self.config.success_when else "completed"
38
+ if success_when and status == success_when:
39
+ return response
40
+
41
+ fail_when = context.render_template(self.config.fail_when) if self.config.fail_when else "failed"
42
+ if fail_when and status == fail_when:
43
+ raise RuntimeError(f"Polling failed: status='{status}'.")
44
+
45
+ await asyncio.sleep(interval.total_seconds())
46
+
47
+ raise TimeoutError(f"Polling timed out after {timeout}")
48
+
49
+ def _resolve_request_url(self, context: ComponentContext) -> str:
50
+ if self.base_url and self.config.path:
51
+ return context.render_template(self.base_url) + context.render_template(self.config.path)
52
+
53
+ return context.render_template(self.config.endpoint)
54
+
55
+ class HttpClientCallbackCompletion:
56
+ def __init__(self, config: HttpClientCompletionConfig):
57
+ self.config: HttpClientCompletionConfig = config
58
+
59
+ async def run(self, context: ComponentContext) -> Any:
60
+ callback_id = context.render_template(self.config.wait_for)
61
+ future: asyncio.Future = asyncio.get_event_loop().create_future()
62
+ HttpCallbackListener.register_pending_future(callback_id, future)
63
+
64
+ return await future
65
+
66
+ class HttpClientAction:
67
+ def __init__(self, base_url: Optional[str], config: HttpClientActionConfig):
68
+ self.base_url: Optional[str] = base_url
69
+ self.config: HttpClientActionConfig = config
70
+ self.client: HttpClient = HttpClient()
71
+
72
+ async def run(self, context: ComponentContext) -> Any:
73
+ url = self._resolve_request_url(context)
74
+ method = context.render_template(self.config.method)
75
+ params = context.render_template(self.config.params)
76
+ body = context.render_template(self.config.body)
77
+ headers = context.render_template(self.config.headers)
78
+
79
+ response = await self.client.request(url, method, params, body, headers)
80
+
81
+ if response:
82
+ context.register_source("response", response)
83
+
84
+ if self.config.completion:
85
+ result = await self._handle_completion(self.config.completion, context)
86
+
87
+ if result:
88
+ context.register_source("result", result)
89
+
90
+ return context.render_template(self.config.output) if self.config.output else (result or response)
91
+
92
+ def _resolve_request_url(self, context: ComponentContext) -> str:
93
+ if self.base_url and self.config.path:
94
+ return context.render_template(self.base_url) + context.render_template(self.config.path)
95
+
96
+ return context.render_template(self.config.endpoint)
97
+
98
+ async def _handle_completion(self, completion: HttpClientCompletionConfig, context: ComponentContext) -> Any:
99
+ if completion.type == "polling":
100
+ return await HttpClientPollingCompletion(self.base_url, completion).run(context)
101
+
102
+ if completion.type == "callback":
103
+ return await HttpClientCallbackCompletion(completion).run(context)
104
+
105
+ return None
106
+
107
+ class HttpClientComponent(BaseComponent):
108
+ def __init__(self, id: str, config: HttpClientComponentConfig, env: Dict[str, str], daemon: bool):
109
+ super().__init__(id, config, env, daemon)
110
+
111
+ async def _serve(self) -> None:
112
+ pass
113
+
114
+ async def _shutdown(self) -> None:
115
+ pass
116
+
117
+ async def _run(self, action: ActionConfig, context: ComponentContext) -> Any:
118
+ return await HttpClientAction(self.config.base_url, action).run(context)
119
+
120
+ ComponentEngineMap[ComponentType.HTTP_CLIENT] = HttpClientComponent
@@ -0,0 +1,2 @@
1
+ from .gateway import *
2
+ from .engine import HttpTunnelGateway
@@ -0,0 +1,2 @@
1
+ from .base import *
2
+ from .http_tunnel import *
@@ -0,0 +1,12 @@
1
+ from typing import Type, Union, Literal, Optional, Dict, List, Tuple, Set, Annotated, Any
2
+ from mindor.dsl.schema.gateway import GatewayConfig, GatewayType
3
+ from mindor.core.services import AsyncService
4
+
5
+ class BaseGateway(AsyncService):
6
+ def __init__(self, config: GatewayConfig, env: Dict[str, str], daemon: bool):
7
+ super().__init__(daemon)
8
+
9
+ self.config: GatewayConfig = config
10
+ self.env: Dict[str, str] = env
11
+
12
+ GatewayEngineMap: Dict[GatewayType, Type[BaseGateway]] = {}
@@ -0,0 +1,9 @@
1
+ from typing import Type, Union, Literal, Optional, Dict, List, Tuple, Set, Annotated, Callable, Iterator, Any
2
+ from mindor.dsl.schema.gateway import HttpTunnelGatewayConfig
3
+ from .base import BaseGateway, GatewayType, GatewayEngineMap
4
+
5
+ class HttpTunnelGateway(BaseGateway):
6
+ def __init__(self, config: HttpTunnelGatewayConfig, env: Dict[str, str], daemon: bool):
7
+ super().__init__(config, env, daemon)
8
+
9
+ GatewayEngineMap[GatewayType.HTTP_TUNNEL] = HttpTunnelGateway
@@ -0,0 +1,9 @@
1
+ from typing import Type, Union, Literal, Optional, Dict, List, Tuple, Set, Annotated, Any
2
+ from mindor.dsl.schema.gateway import GatewayConfig
3
+ from .engine import BaseGateway, GatewayEngineMap
4
+
5
+ def create_gateway(config: GatewayConfig, env: Dict[str, str], daemon: bool) -> BaseGateway:
6
+ try:
7
+ return GatewayEngineMap[config.type](config, env, daemon)
8
+ except KeyError:
9
+ raise ValueError(f"Unsupported gateway type: {config.type}")
@@ -14,11 +14,11 @@ _pending_futures: Dict[str, asyncio.Future] = {}
14
14
  _pending_futures_lock: Lock = Lock()
15
15
 
16
16
  class HttpCallbackContext:
17
- def __init__(self, body: Optional[Any], query: Optional[Dict[str, str]], bulk: bool, item_at: Optional[str]):
17
+ def __init__(self, body: Optional[Any], query: Optional[Dict[str, str]], bulk: bool, item: Optional[str]):
18
18
  self.body: Optional[Any] = body
19
19
  self.query: Optional[Dict[str, str]] = query
20
20
  self.bulk: bool = bulk
21
- self.item_at: Optional[str] = item_at
21
+ self.item: Optional[str] = item
22
22
  self.renderer: TemplateRenderer = TemplateRenderer(self._resolve_source)
23
23
 
24
24
  def items(self) -> Iterator["HttpCallbackContext"]:
@@ -29,11 +29,11 @@ class HttpCallbackContext:
29
29
  return self.renderer.render(template)
30
30
 
31
31
  def _items(self) -> List[Any]:
32
- item = self.renderer.render(self.item_at) if self.item_at else self.body
32
+ item = self.renderer.render(self.item) if self.item else self.body
33
33
 
34
34
  if self.bulk:
35
35
  if not isinstance(item, list):
36
- raise ValueError("Expected a list because 'bulk' is True, but got a non-list.")
36
+ raise ValueError("Expected a list because 'bulk' is true, but got a non-list.")
37
37
  return item
38
38
 
39
39
  return [ item ]
@@ -67,16 +67,16 @@ class HttpCallbackListener(BaseListener):
67
67
 
68
68
  def _make_callback_handler(self, callback: HttpCallbackConfig) -> Callable:
69
69
  async def handler(request: Request) -> Response:
70
- body, query = await self._parse_request_content(request), request.query_params
71
- context = HttpCallbackContext(body, query, callback.bulk, callback.item_at)
70
+ body, query = await self._parse_request_content(request), request.query_params
71
+ context = HttpCallbackContext(body, query, callback.bulk, callback.item)
72
72
 
73
73
  for item in context.items():
74
- action_id = item.render_template(callback.identify_by)
75
- future: asyncio.Future = self._get_pending_future(action_id) if isinstance(action_id, str) else None
74
+ callback_id = item.render_template(callback.identify_by)
75
+ future: asyncio.Future = self._get_pending_future(callback_id)
76
76
 
77
77
  if future:
78
78
  future.set_result(item.render_template(callback.result) if callback.result else item.body)
79
- self._remove_pending_future(action_id)
79
+ self._remove_pending_future(callback_id)
80
80
 
81
81
  return Response()
82
82
 
@@ -1,4 +1,4 @@
1
- from typing import Type, Union, Literal, Optional, Dict, List, Tuple, Set, Annotated, Callable, Any
1
+ from typing import Optional
2
2
  from abc import ABC, abstractmethod
3
3
  from threading import Thread
4
4
  import asyncio
@@ -0,0 +1,79 @@
1
+ from typing import Type, Union, Literal, Optional, Dict, List, Tuple, Set, Annotated, AsyncIterator, Any
2
+ from .http_headers import parse_options_header, get_header_value
3
+ from .resources import StreamResource
4
+
5
+ from urllib.parse import urlencode
6
+ import aiohttp, json
7
+
8
+ class HttpStreamResource(StreamResource):
9
+ def __init__(self, session: aiohttp.ClientSession, stream: aiohttp.StreamReader, content_type: Optional[str], filename: Optional[str]):
10
+ super().__init__(content_type, filename)
11
+
12
+ self.session: aiohttp.ClientSession = session
13
+ self.stream: aiohttp.StreamReader = stream
14
+
15
+ def get_stream(self):
16
+ return self.stream
17
+
18
+ async def close(self):
19
+ await self.session.close()
20
+
21
+ async def _iterate_stream(self) -> AsyncIterator[bytes]:
22
+ _, buffer_size = self.stream.get_read_buffer_limits()
23
+ chunk_size = buffer_size or 65536
24
+
25
+ while not self.stream.at_eof():
26
+ chunk = await self.stream.read(chunk_size)
27
+ if not chunk:
28
+ break
29
+ yield chunk
30
+
31
+ class HttpClient:
32
+ async def request(self, url: str, method: str, params: Optional[Dict[str, Any]], body: Optional[Any], headers: Optional[Dict[str, str]]) -> Any:
33
+ session = aiohttp.ClientSession()
34
+ try:
35
+ response = await session.request(
36
+ method,
37
+ url,
38
+ params=params,
39
+ data=self._serialize_request_body(body, headers) if body is not None else None,
40
+ headers=headers
41
+ )
42
+
43
+ if response.status >= 400:
44
+ raise ValueError(f"Request failed with status {response.status}")
45
+
46
+ content = await self._parse_response_content(session, response)
47
+
48
+ if not isinstance(content, HttpStreamResource):
49
+ await session.close()
50
+
51
+ return content
52
+ except:
53
+ await session.close()
54
+ raise
55
+
56
+ def _serialize_request_body(self, body: Any, headers: Optional[Dict[str, str]]) -> Any:
57
+ content_type, _ = parse_options_header(get_header_value(headers, "Content-Type", ""))
58
+
59
+ if content_type == "application/x-www-form-urlencoded":
60
+ return urlencode(body)
61
+
62
+ if isinstance(body, (str, bytes)):
63
+ return body
64
+
65
+ return json.dumps(body)
66
+
67
+ async def _parse_response_content(self, session: aiohttp.ClientSession, response: aiohttp.ClientResponse) -> Any:
68
+ content_type, _ = parse_options_header(response.headers.get("Content-Type", ""))
69
+
70
+ if content_type == "application/json":
71
+ return await response.json()
72
+
73
+ if content_type.startswith("text/"):
74
+ return await response.text()
75
+
76
+ _, disposition = parse_options_header(response.headers.get("Content-Disposition", ""))
77
+ filename = disposition.get("filename")
78
+
79
+ return HttpStreamResource(session, response.content, content_type, filename)
@@ -22,24 +22,33 @@ class TemplateRenderer:
22
22
  def _render_element(self, element: Any) -> Any:
23
23
  if isinstance(element, str):
24
24
  return self._render_text(element)
25
+
25
26
  if isinstance(element, dict):
26
27
  return { key: self._render_element(value) for key, value in element.items() }
28
+
27
29
  if isinstance(element, list):
28
30
  return [ self._render_element(item) for item in element ]
31
+
29
32
  return element
30
33
 
31
34
  def _render_text(self, text: str) -> Any:
32
- match = self.patterns["placeholder"].search(text)
33
- if match:
35
+ while True:
36
+ match = self.patterns["placeholder"].search(text)
37
+
38
+ if not match:
39
+ break
40
+
34
41
  key, path, default, placeholder = match.group(1), match.group(2), match.group(3), match.group(0)
35
42
  try:
36
- source = self.source_resolver(key)
37
- value = self._resolve_by_path(source, path)
38
- if text != placeholder:
39
- return text.replace(placeholder, value)
40
- return value
43
+ value = self._resolve_by_path(self.source_resolver(key), path)
41
44
  except Exception:
42
- return default or ""
45
+ value = default or ""
46
+
47
+ if placeholder == text:
48
+ return value
49
+
50
+ text = text.replace(placeholder, str(value))
51
+
43
52
  return text
44
53
 
45
54
  def _resolve_by_path(self, source: Any, path: Optional[str]) -> Any:
@@ -0,0 +1,13 @@
1
+ from datetime import timedelta
2
+
3
+ def parse_duration(string: str) -> timedelta:
4
+ if string.endswith("s"):
5
+ return timedelta(seconds=int(string[:-1]))
6
+
7
+ if string.endswith("m"):
8
+ return timedelta(minutes=int(string[:-1]))
9
+
10
+ if string.endswith("h"):
11
+ return timedelta(hours=int(string[:-1]))
12
+
13
+ raise ValueError(f"Unsupported duration format: {string}")
@@ -4,11 +4,7 @@ from pydantic import model_validator
4
4
  from .common import CommonActionConfig
5
5
 
6
6
  class CommonCompletionConfig(BaseModel):
7
- type: Literal[ "callback", "polling" ]
8
-
9
- class CallbackCompletionConfig(CommonCompletionConfig):
10
- type: Literal["callback"]
11
- wait_for: str
7
+ type: Literal[ "polling", "callback" ]
12
8
 
13
9
  class PollingCompletionConfig(CommonCompletionConfig):
14
10
  type: Literal["polling"]
@@ -18,6 +14,9 @@ class PollingCompletionConfig(CommonCompletionConfig):
18
14
  headers: Optional[Dict[str, str]] = Field(default_factory=dict)
19
15
  body: Optional[Dict[str, Any]] = Field(default_factory=dict)
20
16
  params: Optional[Dict[str, str]] = Field(default_factory=dict)
17
+ status: Optional[str] = None
18
+ success_when: Optional[str] = None
19
+ fail_when: Optional[str] = None
21
20
  interval: Optional[str] = None
22
21
  timeout: Optional[str] = None
23
22
 
@@ -27,10 +26,14 @@ class PollingCompletionConfig(CommonCompletionConfig):
27
26
  raise ValueError("Either 'endpoint' or 'path' must be set, but not both.")
28
27
  return values
29
28
 
29
+ class CallbackCompletionConfig(CommonCompletionConfig):
30
+ type: Literal["callback"]
31
+ wait_for: str
32
+
30
33
  HttpClientCompletionConfig = Annotated[
31
- Union[
32
- CallbackCompletionConfig,
33
- PollingCompletionConfig
34
+ Union[
35
+ PollingCompletionConfig,
36
+ CallbackCompletionConfig
34
37
  ],
35
38
  Field(discriminator="type")
36
39
  ]
@@ -0,0 +1 @@
1
+ from .gateway import *
@@ -0,0 +1,10 @@
1
+ from typing import Type, Union, Literal, Optional, Dict, List, Tuple, Set, Annotated, Any
2
+ from pydantic import BaseModel, Field
3
+ from .impl import *
4
+
5
+ GatewayConfig = Annotated[
6
+ Union[
7
+ HttpTunnelGatewayConfig
8
+ ],
9
+ Field(discriminator="type")
10
+ ]
@@ -0,0 +1,2 @@
1
+ from .common import *
2
+ from .http_tunnel import *
@@ -0,0 +1,7 @@
1
+ from typing import Type, Union, Literal, Optional, Dict, List, Tuple, Set, Annotated, Any
2
+ from pydantic import BaseModel, Field
3
+ from .types import GatewayType
4
+
5
+ class CommonGatewayConfig(BaseModel):
6
+ type: GatewayType
7
+ runtime: Literal[ "docker", "native" ] = "native"
@@ -0,0 +1,9 @@
1
+ from typing import Type, Union, Literal, Optional, Dict, List, Tuple, Set, Annotated, Any
2
+ from pydantic import BaseModel, Field
3
+ from pydantic import model_validator
4
+ from .common import GatewayType, CommonGatewayConfig
5
+
6
+ class HttpTunnelGatewayConfig(CommonGatewayConfig):
7
+ type: Literal[GatewayType.HTTP_TUNNEL]
8
+ driver: Literal[ "ngrok" ]
9
+ port: Optional[int] = 8090
@@ -0,0 +1,4 @@
1
+ from enum import Enum
2
+
3
+ class GatewayType(str, Enum):
4
+ HTTP_TUNNEL = "http-tunnel"
@@ -7,7 +7,7 @@ class HttpCallbackConfig(BaseModel):
7
7
  path: str
8
8
  method: Literal[ "GET", "POST", "PUT", "DELETE", "PATCH" ] = "POST"
9
9
  bulk: bool = False
10
- item_at: Optional[str] = None
10
+ item: Optional[str] = None
11
11
  identify_by: str
12
12
  result: Optional[Any] = None
13
13
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: model-compose
3
- Version: 0.0.8
3
+ Version: 0.0.9
4
4
  Summary: model-compose: Declarative AI Model and Workflow Orchestrator (from Mindor)
5
5
  Author-email: Hanyeol Cho <hanyeol.cho@gmail.com>
6
6
  License-Expression: MIT
@@ -21,6 +21,11 @@ src/mindor/core/controller/engine/__init__.py
21
21
  src/mindor/core/controller/engine/base.py
22
22
  src/mindor/core/controller/engine/http_server.py
23
23
  src/mindor/core/controller/engine/mcp_server.py
24
+ src/mindor/core/gateway/__init__.py
25
+ src/mindor/core/gateway/gateway.py
26
+ src/mindor/core/gateway/engine/__init__.py
27
+ src/mindor/core/gateway/engine/base.py
28
+ src/mindor/core/gateway/engine/http_tunnel.py
24
29
  src/mindor/core/listener/__init__.py
25
30
  src/mindor/core/listener/listener.py
26
31
  src/mindor/core/listener/engine/__init__.py
@@ -32,9 +37,11 @@ src/mindor/core/services/__init__.py
32
37
  src/mindor/core/services/async_service.py
33
38
  src/mindor/core/utils/__init__.py
34
39
  src/mindor/core/utils/expiring.py
40
+ src/mindor/core/utils/http_client.py
35
41
  src/mindor/core/utils/http_headers.py
36
42
  src/mindor/core/utils/resources.py
37
43
  src/mindor/core/utils/template.py
44
+ src/mindor/core/utils/time.py
38
45
  src/mindor/core/utils/workqueue.py
39
46
  src/mindor/core/workflow/__init__.py
40
47
  src/mindor/core/workflow/context.py
@@ -69,6 +76,12 @@ src/mindor/dsl/schema/controller/impl/common.py
69
76
  src/mindor/dsl/schema/controller/impl/http_server.py
70
77
  src/mindor/dsl/schema/controller/impl/mcp_server.py
71
78
  src/mindor/dsl/schema/controller/impl/types.py
79
+ src/mindor/dsl/schema/gateway/__init__.py
80
+ src/mindor/dsl/schema/gateway/gateway.py
81
+ src/mindor/dsl/schema/gateway/impl/__init__.py
82
+ src/mindor/dsl/schema/gateway/impl/common.py
83
+ src/mindor/dsl/schema/gateway/impl/http_tunnel.py
84
+ src/mindor/dsl/schema/gateway/impl/types.py
72
85
  src/mindor/dsl/schema/listener/__init__.py
73
86
  src/mindor/dsl/schema/listener/listener.py
74
87
  src/mindor/dsl/schema/listener/impl/__init__.py
@@ -1,141 +0,0 @@
1
- from typing import Type, Union, Literal, Optional, Dict, List, Tuple, Set, Annotated, AsyncIterator, Any
2
- from mindor.dsl.schema.component import HttpClientComponentConfig
3
- from mindor.dsl.schema.action import ActionConfig, HttpClientActionConfig, HttpClientCompletionConfig
4
- from mindor.core.listener import HttpCallbackListener
5
- from mindor.core.utils.http_headers import parse_options_header, get_header_value
6
- from mindor.core.utils.resources import StreamResource
7
- from .base import BaseComponent, ComponentType, ComponentEngineMap
8
- from .context import ComponentContext
9
-
10
- from urllib.parse import urlencode
11
- import asyncio, aiohttp, json
12
-
13
- class HttpStreamResource(StreamResource):
14
- def __init__(self, session: aiohttp.ClientSession, stream: aiohttp.StreamReader, content_type: Optional[str], filename: Optional[str]):
15
- super().__init__(content_type, filename)
16
-
17
- self.session: aiohttp.ClientSession = session
18
- self.stream: aiohttp.StreamReader = stream
19
-
20
- def get_stream(self):
21
- return self.stream
22
-
23
- async def close(self):
24
- await self.session.close()
25
-
26
- async def _iterate_stream(self) -> AsyncIterator[bytes]:
27
- _, buffer_size = self.stream.get_read_buffer_limits()
28
- chunk_size = buffer_size or 65536
29
-
30
- while not self.stream.at_eof():
31
- chunk = await self.stream.read(chunk_size)
32
- if not chunk:
33
- break
34
- yield chunk
35
-
36
- class HttpClientAction:
37
- def __init__(self, base_url: Optional[str], config: HttpClientActionConfig):
38
- self.base_url: Optional[str] = base_url
39
- self.config: HttpClientActionConfig = config
40
- self.future: Optional[asyncio.Future] = None
41
-
42
- async def run(self, context: ComponentContext) -> Any:
43
- url = self._resolve_request_url(context)
44
- method = context.render_template(self.config.method)
45
- params = context.render_template(self.config.params)
46
- body = context.render_template(self.config.body)
47
- headers = context.render_template(self.config.headers)
48
-
49
- response = await self._request(url, method, params, body, headers)
50
-
51
- if response:
52
- context.register_source("response", response)
53
-
54
- if self.config.completion:
55
- result = await self._handle_completion(self.config.completion, context)
56
-
57
- return context.render_template(self.config.output) if self.config.output else (result or response)
58
-
59
- async def _request(self, url: str, method: str, params: Optional[Dict[str, Any]], body: Optional[Any], headers: Optional[Dict[str, str]]) -> Any:
60
- session = aiohttp.ClientSession()
61
- try:
62
- response = await session.request(
63
- method,
64
- url,
65
- params=params,
66
- data=self._serialize_request_body(body, headers) if body is not None else None,
67
- headers=headers
68
- )
69
-
70
- if response.status >= 400:
71
- raise ValueError(f"Request failed with status {response.status}")
72
-
73
- content = await self._parse_response_content(session, response)
74
-
75
- if isinstance(content, HttpStreamResource):
76
- pass
77
- else:
78
- await session.close()
79
-
80
- return content
81
- except:
82
- await session.close()
83
- raise
84
-
85
- def _resolve_request_url(self, context: ComponentContext) -> str:
86
- if self.base_url and self.config.path:
87
- return context.render_template(str(self.base_url)) + context.render_template(self.config.path)
88
-
89
- return context.render_template(str(self.config.endpoint))
90
-
91
- def _serialize_request_body(self, body: Any, headers: Optional[Dict[str, str]]) -> Any:
92
- content_type, _ = parse_options_header(get_header_value(headers, "Content-Type", ""))
93
-
94
- if content_type == "application/x-www-form-urlencoded":
95
- return urlencode(body)
96
-
97
- if isinstance(body, (str, bytes)):
98
- return body
99
-
100
- return json.dumps(body)
101
-
102
- async def _parse_response_content(self, session: aiohttp.ClientSession, response: aiohttp.ClientResponse) -> Any:
103
- content_type, _ = parse_options_header(response.headers.get("Content-Type", ""))
104
-
105
- if content_type == "application/json":
106
- return await response.json()
107
-
108
- if content_type.startswith("text/"):
109
- return await response.text()
110
-
111
- _, disposition = parse_options_header(response.headers.get("Content-Disposition", ""))
112
- filename = disposition.get("filename")
113
-
114
- return HttpStreamResource(session, response.content, content_type, filename)
115
-
116
- async def _handle_completion(self, completion: HttpClientCompletionConfig, context: ComponentContext) -> Any:
117
- if completion.type == "callback":
118
- future: asyncio.Future = asyncio.get_event_loop().create_future()
119
- HttpCallbackListener.register_pending_future(context.render_template(completion.wait_for), future)
120
- result = await future
121
- if result:
122
- context.register_source("result", result)
123
- return result
124
-
125
- if completion.type == "polling":
126
- return None
127
-
128
- class HttpClientComponent(BaseComponent):
129
- def __init__(self, id: str, config: HttpClientComponentConfig, env: Dict[str, str], daemon: bool):
130
- super().__init__(id, config, env, daemon)
131
-
132
- async def _serve(self) -> None:
133
- pass
134
-
135
- async def _shutdown(self) -> None:
136
- pass
137
-
138
- async def _run(self, action: ActionConfig, context: ComponentContext) -> Any:
139
- return await HttpClientAction(self.config.base_url, action).run(context)
140
-
141
- ComponentEngineMap[ComponentType.HTTP_CLIENT] = HttpClientComponent
File without changes
File without changes
File without changes