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.
- {model_compose-0.0.8/src/model_compose.egg-info → model_compose-0.0.9}/PKG-INFO +1 -1
- {model_compose-0.0.8 → model_compose-0.0.9}/pyproject.toml +1 -1
- model_compose-0.0.9/src/mindor/core/component/engine/http_client.py +120 -0
- model_compose-0.0.9/src/mindor/core/gateway/__init__.py +2 -0
- model_compose-0.0.9/src/mindor/core/gateway/engine/__init__.py +2 -0
- model_compose-0.0.9/src/mindor/core/gateway/engine/base.py +12 -0
- model_compose-0.0.9/src/mindor/core/gateway/engine/http_tunnel.py +9 -0
- model_compose-0.0.9/src/mindor/core/gateway/gateway.py +9 -0
- {model_compose-0.0.8 → model_compose-0.0.9}/src/mindor/core/listener/engine/http_callback.py +9 -9
- {model_compose-0.0.8 → model_compose-0.0.9}/src/mindor/core/services/async_service.py +1 -1
- model_compose-0.0.9/src/mindor/core/utils/http_client.py +79 -0
- {model_compose-0.0.8 → model_compose-0.0.9}/src/mindor/core/utils/template.py +17 -8
- model_compose-0.0.9/src/mindor/core/utils/time.py +13 -0
- {model_compose-0.0.8 → model_compose-0.0.9}/src/mindor/dsl/schema/action/impl/http_client.py +11 -8
- model_compose-0.0.9/src/mindor/dsl/schema/gateway/__init__.py +1 -0
- model_compose-0.0.9/src/mindor/dsl/schema/gateway/gateway.py +10 -0
- model_compose-0.0.9/src/mindor/dsl/schema/gateway/impl/__init__.py +2 -0
- model_compose-0.0.9/src/mindor/dsl/schema/gateway/impl/common.py +7 -0
- model_compose-0.0.9/src/mindor/dsl/schema/gateway/impl/http_tunnel.py +9 -0
- model_compose-0.0.9/src/mindor/dsl/schema/gateway/impl/types.py +4 -0
- {model_compose-0.0.8 → model_compose-0.0.9}/src/mindor/dsl/schema/listener/impl/http_callback.py +1 -1
- {model_compose-0.0.8 → model_compose-0.0.9/src/model_compose.egg-info}/PKG-INFO +1 -1
- {model_compose-0.0.8 → model_compose-0.0.9}/src/model_compose.egg-info/SOURCES.txt +13 -0
- model_compose-0.0.8/src/mindor/core/component/engine/http_client.py +0 -141
- {model_compose-0.0.8 → model_compose-0.0.9}/LICENSE +0 -0
- {model_compose-0.0.8 → model_compose-0.0.9}/README.md +0 -0
- {model_compose-0.0.8 → model_compose-0.0.9}/setup.cfg +0 -0
- {model_compose-0.0.8 → model_compose-0.0.9}/src/mindor/__init__.py +0 -0
- {model_compose-0.0.8 → model_compose-0.0.9}/src/mindor/cli/__init__.py +0 -0
- {model_compose-0.0.8 → model_compose-0.0.9}/src/mindor/cli/compose.py +0 -0
- {model_compose-0.0.8 → model_compose-0.0.9}/src/mindor/core/__init__.py +0 -0
- {model_compose-0.0.8 → model_compose-0.0.9}/src/mindor/core/component/__init__.py +0 -0
- {model_compose-0.0.8 → model_compose-0.0.9}/src/mindor/core/component/component.py +0 -0
- {model_compose-0.0.8 → model_compose-0.0.9}/src/mindor/core/component/engine/__init__.py +0 -0
- {model_compose-0.0.8 → model_compose-0.0.9}/src/mindor/core/component/engine/base.py +0 -0
- {model_compose-0.0.8 → model_compose-0.0.9}/src/mindor/core/component/engine/context.py +0 -0
- {model_compose-0.0.8 → model_compose-0.0.9}/src/mindor/core/component/engine/http_server.py +0 -0
- {model_compose-0.0.8 → model_compose-0.0.9}/src/mindor/core/compose/__init__.py +0 -0
- {model_compose-0.0.8 → model_compose-0.0.9}/src/mindor/core/compose/compose.py +0 -0
- {model_compose-0.0.8 → model_compose-0.0.9}/src/mindor/core/compose/manager.py +0 -0
- {model_compose-0.0.8 → model_compose-0.0.9}/src/mindor/core/controller/__init__.py +0 -0
- {model_compose-0.0.8 → model_compose-0.0.9}/src/mindor/core/controller/controller.py +0 -0
- {model_compose-0.0.8 → model_compose-0.0.9}/src/mindor/core/controller/engine/__init__.py +0 -0
- {model_compose-0.0.8 → model_compose-0.0.9}/src/mindor/core/controller/engine/base.py +0 -0
- {model_compose-0.0.8 → model_compose-0.0.9}/src/mindor/core/controller/engine/http_server.py +0 -0
- {model_compose-0.0.8 → model_compose-0.0.9}/src/mindor/core/controller/engine/mcp_server.py +0 -0
- {model_compose-0.0.8 → model_compose-0.0.9}/src/mindor/core/listener/__init__.py +0 -0
- {model_compose-0.0.8 → model_compose-0.0.9}/src/mindor/core/listener/engine/__init__.py +0 -0
- {model_compose-0.0.8 → model_compose-0.0.9}/src/mindor/core/listener/engine/base.py +0 -0
- {model_compose-0.0.8 → model_compose-0.0.9}/src/mindor/core/listener/listener.py +0 -0
- {model_compose-0.0.8 → model_compose-0.0.9}/src/mindor/core/runtime/__init__.py +0 -0
- {model_compose-0.0.8 → model_compose-0.0.9}/src/mindor/core/runtime/env.py +0 -0
- {model_compose-0.0.8 → model_compose-0.0.9}/src/mindor/core/services/__init__.py +0 -0
- {model_compose-0.0.8 → model_compose-0.0.9}/src/mindor/core/utils/__init__.py +0 -0
- {model_compose-0.0.8 → model_compose-0.0.9}/src/mindor/core/utils/expiring.py +0 -0
- {model_compose-0.0.8 → model_compose-0.0.9}/src/mindor/core/utils/http_headers.py +0 -0
- {model_compose-0.0.8 → model_compose-0.0.9}/src/mindor/core/utils/resources.py +0 -0
- {model_compose-0.0.8 → model_compose-0.0.9}/src/mindor/core/utils/workqueue.py +0 -0
- {model_compose-0.0.8 → model_compose-0.0.9}/src/mindor/core/workflow/__init__.py +0 -0
- {model_compose-0.0.8 → model_compose-0.0.9}/src/mindor/core/workflow/context.py +0 -0
- {model_compose-0.0.8 → model_compose-0.0.9}/src/mindor/core/workflow/job.py +0 -0
- {model_compose-0.0.8 → model_compose-0.0.9}/src/mindor/core/workflow/workflow.py +0 -0
- {model_compose-0.0.8 → model_compose-0.0.9}/src/mindor/dsl/__init__.py +0 -0
- {model_compose-0.0.8 → model_compose-0.0.9}/src/mindor/dsl/loader.py +0 -0
- {model_compose-0.0.8 → model_compose-0.0.9}/src/mindor/dsl/schema/__init__.py +0 -0
- {model_compose-0.0.8 → model_compose-0.0.9}/src/mindor/dsl/schema/action/__init__.py +0 -0
- {model_compose-0.0.8 → model_compose-0.0.9}/src/mindor/dsl/schema/action/action.py +0 -0
- {model_compose-0.0.8 → model_compose-0.0.9}/src/mindor/dsl/schema/action/impl/__init__.py +0 -0
- {model_compose-0.0.8 → model_compose-0.0.9}/src/mindor/dsl/schema/action/impl/common.py +0 -0
- {model_compose-0.0.8 → model_compose-0.0.9}/src/mindor/dsl/schema/action/impl/http_server.py +0 -0
- {model_compose-0.0.8 → model_compose-0.0.9}/src/mindor/dsl/schema/action/impl/mcp_client.py +0 -0
- {model_compose-0.0.8 → model_compose-0.0.9}/src/mindor/dsl/schema/action/impl/mcp_server.py +0 -0
- {model_compose-0.0.8 → model_compose-0.0.9}/src/mindor/dsl/schema/component/__init__.py +0 -0
- {model_compose-0.0.8 → model_compose-0.0.9}/src/mindor/dsl/schema/component/component.py +0 -0
- {model_compose-0.0.8 → model_compose-0.0.9}/src/mindor/dsl/schema/component/impl/__init__.py +0 -0
- {model_compose-0.0.8 → model_compose-0.0.9}/src/mindor/dsl/schema/component/impl/common.py +0 -0
- {model_compose-0.0.8 → model_compose-0.0.9}/src/mindor/dsl/schema/component/impl/http_client.py +0 -0
- {model_compose-0.0.8 → model_compose-0.0.9}/src/mindor/dsl/schema/component/impl/http_server.py +0 -0
- {model_compose-0.0.8 → model_compose-0.0.9}/src/mindor/dsl/schema/component/impl/mcp_client.py +0 -0
- {model_compose-0.0.8 → model_compose-0.0.9}/src/mindor/dsl/schema/component/impl/mcp_server.py +0 -0
- {model_compose-0.0.8 → model_compose-0.0.9}/src/mindor/dsl/schema/component/impl/types.py +0 -0
- {model_compose-0.0.8 → model_compose-0.0.9}/src/mindor/dsl/schema/compose.py +0 -0
- {model_compose-0.0.8 → model_compose-0.0.9}/src/mindor/dsl/schema/controller/__init__.py +0 -0
- {model_compose-0.0.8 → model_compose-0.0.9}/src/mindor/dsl/schema/controller/controller.py +0 -0
- {model_compose-0.0.8 → model_compose-0.0.9}/src/mindor/dsl/schema/controller/impl/__init__.py +0 -0
- {model_compose-0.0.8 → model_compose-0.0.9}/src/mindor/dsl/schema/controller/impl/common.py +0 -0
- {model_compose-0.0.8 → model_compose-0.0.9}/src/mindor/dsl/schema/controller/impl/http_server.py +0 -0
- {model_compose-0.0.8 → model_compose-0.0.9}/src/mindor/dsl/schema/controller/impl/mcp_server.py +0 -0
- {model_compose-0.0.8 → model_compose-0.0.9}/src/mindor/dsl/schema/controller/impl/types.py +0 -0
- {model_compose-0.0.8 → model_compose-0.0.9}/src/mindor/dsl/schema/listener/__init__.py +0 -0
- {model_compose-0.0.8 → model_compose-0.0.9}/src/mindor/dsl/schema/listener/impl/__init__.py +0 -0
- {model_compose-0.0.8 → model_compose-0.0.9}/src/mindor/dsl/schema/listener/impl/common.py +0 -0
- {model_compose-0.0.8 → model_compose-0.0.9}/src/mindor/dsl/schema/listener/impl/types.py +0 -0
- {model_compose-0.0.8 → model_compose-0.0.9}/src/mindor/dsl/schema/listener/listener.py +0 -0
- {model_compose-0.0.8 → model_compose-0.0.9}/src/mindor/dsl/schema/runtime/__init__.py +0 -0
- {model_compose-0.0.8 → model_compose-0.0.9}/src/mindor/dsl/schema/runtime/impl/__init__.py +0 -0
- {model_compose-0.0.8 → model_compose-0.0.9}/src/mindor/dsl/schema/runtime/impl/common.py +0 -0
- {model_compose-0.0.8 → model_compose-0.0.9}/src/mindor/dsl/schema/runtime/impl/docker.py +0 -0
- {model_compose-0.0.8 → model_compose-0.0.9}/src/mindor/dsl/schema/runtime/impl/types.py +0 -0
- {model_compose-0.0.8 → model_compose-0.0.9}/src/mindor/dsl/schema/runtime/runtime.py +0 -0
- {model_compose-0.0.8 → model_compose-0.0.9}/src/mindor/dsl/schema/workflow.py +0 -0
- {model_compose-0.0.8 → model_compose-0.0.9}/src/model_compose.egg-info/dependency_links.txt +0 -0
- {model_compose-0.0.8 → model_compose-0.0.9}/src/model_compose.egg-info/entry_points.txt +0 -0
- {model_compose-0.0.8 → model_compose-0.0.9}/src/model_compose.egg-info/requires.txt +0 -0
- {model_compose-0.0.8 → model_compose-0.0.9}/src/model_compose.egg-info/top_level.txt +0 -0
- {model_compose-0.0.8 → model_compose-0.0.9}/tests/test_cli.py +0 -0
|
@@ -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,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}")
|
{model_compose-0.0.8 → model_compose-0.0.9}/src/mindor/core/listener/engine/http_callback.py
RENAMED
|
@@ -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,
|
|
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.
|
|
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.
|
|
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
|
|
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
|
|
71
|
-
context = HttpCallbackContext(body, query, callback.bulk, callback.
|
|
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
|
-
|
|
75
|
-
future: asyncio.Future = self._get_pending_future(
|
|
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(
|
|
79
|
+
self._remove_pending_future(callback_id)
|
|
80
80
|
|
|
81
81
|
return Response()
|
|
82
82
|
|
|
@@ -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
|
-
|
|
33
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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}")
|
{model_compose-0.0.8 → model_compose-0.0.9}/src/mindor/dsl/schema/action/impl/http_client.py
RENAMED
|
@@ -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[ "
|
|
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
|
-
|
|
33
|
-
|
|
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,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
|
|
@@ -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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{model_compose-0.0.8 → model_compose-0.0.9}/src/mindor/core/controller/engine/http_server.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{model_compose-0.0.8 → model_compose-0.0.9}/src/mindor/dsl/schema/action/impl/http_server.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{model_compose-0.0.8 → model_compose-0.0.9}/src/mindor/dsl/schema/component/impl/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{model_compose-0.0.8 → model_compose-0.0.9}/src/mindor/dsl/schema/component/impl/http_client.py
RENAMED
|
File without changes
|
{model_compose-0.0.8 → model_compose-0.0.9}/src/mindor/dsl/schema/component/impl/http_server.py
RENAMED
|
File without changes
|
{model_compose-0.0.8 → model_compose-0.0.9}/src/mindor/dsl/schema/component/impl/mcp_client.py
RENAMED
|
File without changes
|
{model_compose-0.0.8 → model_compose-0.0.9}/src/mindor/dsl/schema/component/impl/mcp_server.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{model_compose-0.0.8 → model_compose-0.0.9}/src/mindor/dsl/schema/controller/impl/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{model_compose-0.0.8 → model_compose-0.0.9}/src/mindor/dsl/schema/controller/impl/http_server.py
RENAMED
|
File without changes
|
{model_compose-0.0.8 → model_compose-0.0.9}/src/mindor/dsl/schema/controller/impl/mcp_server.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|