service-forge 0.1.18__py3-none-any.whl → 0.1.39__py3-none-any.whl

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.

Potentially problematic release.


This version of service-forge might be problematic. Click here for more details.

Files changed (80) hide show
  1. service_forge/__init__.py +0 -0
  2. service_forge/api/deprecated_websocket_api.py +91 -33
  3. service_forge/api/deprecated_websocket_manager.py +70 -53
  4. service_forge/api/http_api.py +205 -55
  5. service_forge/api/kafka_api.py +113 -25
  6. service_forge/api/routers/meta_api/meta_api_router.py +57 -0
  7. service_forge/api/routers/service/service_router.py +42 -6
  8. service_forge/api/routers/trace/trace_router.py +326 -0
  9. service_forge/api/routers/websocket/websocket_router.py +69 -1
  10. service_forge/api/service_studio.py +9 -0
  11. service_forge/db/database.py +17 -0
  12. service_forge/execution_context.py +106 -0
  13. service_forge/frontend/static/assets/CreateNewNodeDialog-DkrEMxSH.js +1 -0
  14. service_forge/frontend/static/assets/CreateNewNodeDialog-DwFcBiGp.css +1 -0
  15. service_forge/frontend/static/assets/EditorSidePanel-BNVms9Fq.css +1 -0
  16. service_forge/frontend/static/assets/EditorSidePanel-DZbB3ILL.js +1 -0
  17. service_forge/frontend/static/assets/FeedbackPanel-CC8HX7Yo.js +1 -0
  18. service_forge/frontend/static/assets/FeedbackPanel-ClgniIVk.css +1 -0
  19. service_forge/frontend/static/assets/FormattedCodeViewer.vue_vue_type_script_setup_true_lang-BNuI1NCs.js +1 -0
  20. service_forge/frontend/static/assets/NodeDetailWrapper-BqFFM7-r.js +1 -0
  21. service_forge/frontend/static/assets/NodeDetailWrapper-pZBxv3J0.css +1 -0
  22. service_forge/frontend/static/assets/TestRunningDialog-D0GrCoYs.js +1 -0
  23. service_forge/frontend/static/assets/TestRunningDialog-dhXOsPgH.css +1 -0
  24. service_forge/frontend/static/assets/TracePanelWrapper-B9zvDSc_.js +1 -0
  25. service_forge/frontend/static/assets/TracePanelWrapper-BiednCrq.css +1 -0
  26. service_forge/frontend/static/assets/WorkflowEditor-CcaGGbko.js +3 -0
  27. service_forge/frontend/static/assets/WorkflowEditor-CmasOOYK.css +1 -0
  28. service_forge/frontend/static/assets/WorkflowList-Copuwi-a.css +1 -0
  29. service_forge/frontend/static/assets/WorkflowList-LrRJ7B7h.js +1 -0
  30. service_forge/frontend/static/assets/WorkflowStudio-CthjgII2.css +1 -0
  31. service_forge/frontend/static/assets/WorkflowStudio-FCyhGD4y.js +2 -0
  32. service_forge/frontend/static/assets/api-BDer3rj7.css +1 -0
  33. service_forge/frontend/static/assets/api-DyiqpKJK.js +1 -0
  34. service_forge/frontend/static/assets/code-editor-DBSql_sc.js +12 -0
  35. service_forge/frontend/static/assets/el-collapse-item-D4LG0FJ0.css +1 -0
  36. service_forge/frontend/static/assets/el-empty-D4ZqTl4F.css +1 -0
  37. service_forge/frontend/static/assets/el-form-item-BWkJzdQ_.css +1 -0
  38. service_forge/frontend/static/assets/el-input-D6B3r8CH.css +1 -0
  39. service_forge/frontend/static/assets/el-select-B0XIb2QK.css +1 -0
  40. service_forge/frontend/static/assets/el-tag-DljBBxJR.css +1 -0
  41. service_forge/frontend/static/assets/element-ui-D3x2y3TA.js +12 -0
  42. service_forge/frontend/static/assets/elkjs-Dm5QV7uy.js +24 -0
  43. service_forge/frontend/static/assets/highlightjs-D4ATuRwX.js +3 -0
  44. service_forge/frontend/static/assets/index-BMvodlwc.js +2 -0
  45. service_forge/frontend/static/assets/index-CjSe8i2q.css +1 -0
  46. service_forge/frontend/static/assets/js-yaml-yTPt38rv.js +32 -0
  47. service_forge/frontend/static/assets/time-DKCKV6Ug.js +1 -0
  48. service_forge/frontend/static/assets/ui-components-DQ7-U3pr.js +1 -0
  49. service_forge/frontend/static/assets/vue-core-DL-LgTX0.js +1 -0
  50. service_forge/frontend/static/assets/vue-flow-Dn7R8GPr.js +39 -0
  51. service_forge/frontend/static/index.html +16 -0
  52. service_forge/frontend/static/vite.svg +1 -0
  53. service_forge/model/meta_api/__init__.py +0 -0
  54. service_forge/model/meta_api/schema.py +29 -0
  55. service_forge/model/trace.py +82 -0
  56. service_forge/service.py +39 -11
  57. service_forge/service_config.py +14 -0
  58. service_forge/sft/cli.py +39 -0
  59. service_forge/sft/cmd/remote_deploy.py +160 -0
  60. service_forge/sft/cmd/remote_list_tars.py +111 -0
  61. service_forge/sft/config/injector.py +54 -7
  62. service_forge/sft/config/injector_default_files.py +13 -1
  63. service_forge/sft/config/sf_metadata.py +31 -27
  64. service_forge/sft/config/sft_config.py +18 -0
  65. service_forge/sft/util/assert_util.py +0 -1
  66. service_forge/telemetry.py +66 -0
  67. service_forge/utils/default_type_converter.py +1 -1
  68. service_forge/utils/type_converter.py +5 -0
  69. service_forge/utils/workflow_clone.py +1 -0
  70. service_forge/workflow/node.py +274 -27
  71. service_forge/workflow/triggers/fast_api_trigger.py +64 -28
  72. service_forge/workflow/triggers/websocket_api_trigger.py +66 -38
  73. service_forge/workflow/workflow.py +140 -37
  74. service_forge/workflow/workflow_callback.py +27 -4
  75. service_forge/workflow/workflow_factory.py +14 -0
  76. {service_forge-0.1.18.dist-info → service_forge-0.1.39.dist-info}/METADATA +4 -1
  77. service_forge-0.1.39.dist-info/RECORD +134 -0
  78. service_forge-0.1.18.dist-info/RECORD +0 -83
  79. {service_forge-0.1.18.dist-info → service_forge-0.1.39.dist-info}/WHEEL +0 -0
  80. {service_forge-0.1.18.dist-info → service_forge-0.1.39.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,16 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <link rel="icon" type="image/svg+xml" href="./vite.svg" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+ <title>vite-project</title>
8
+ <script type="module" crossorigin src="./assets/index-BMvodlwc.js"></script>
9
+ <link rel="modulepreload" crossorigin href="./assets/vue-core-DL-LgTX0.js">
10
+ <link rel="modulepreload" crossorigin href="./assets/ui-components-DQ7-U3pr.js">
11
+ <link rel="stylesheet" crossorigin href="./assets/index-CjSe8i2q.css">
12
+ </head>
13
+ <body>
14
+ <div id="app"></div>
15
+ </body>
16
+ </html>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
File without changes
@@ -0,0 +1,29 @@
1
+ from typing import Literal, Optional
2
+
3
+ from pydantic import BaseModel
4
+
5
+ class NodeInputPortSchema(BaseModel):
6
+ """
7
+ 节点的输入端口定义数据
8
+ """
9
+ name: str
10
+ type: str
11
+ default_value: Optional[str | float | bool | int] = None
12
+
13
+
14
+ class NodeOutputPortSchema(BaseModel):
15
+ """
16
+ 节点的输出端口定义数据
17
+ """
18
+ name: str
19
+ type: str
20
+
21
+
22
+ class NodeTypeSchema(BaseModel):
23
+ """
24
+ 节点的定义数据
25
+ """
26
+ name: str
27
+ is_trigger: bool
28
+ inputs: dict[str, NodeInputPortSchema]
29
+ outputs: dict[str, NodeOutputPortSchema]
@@ -0,0 +1,82 @@
1
+ from pydantic import BaseModel
2
+ from typing import Optional, List, Dict, Any
3
+ from enum import IntEnum
4
+
5
+ # Type aliases
6
+ TraceId = str
7
+ SpanId = str
8
+ ParentSpanId = str
9
+
10
+ # Enums
11
+ class SpanKind(IntEnum):
12
+ INTERNAL = 0
13
+ SERVER = 1
14
+ CLIENT = 2
15
+ PRODUCER = 3
16
+ CONSUMER = 4
17
+ UNKNOWN = 5
18
+
19
+ class StatusCode(IntEnum):
20
+ UNSET = 0
21
+ OK = 1
22
+ ERROR = 2
23
+
24
+ # Models
25
+ class Span(BaseModel):
26
+ span_id: SpanId
27
+ parent_span_id: ParentSpanId
28
+ name: str
29
+ kind: SpanKind
30
+ timestamp: str
31
+ duration_nano: int
32
+ status_code: StatusCode
33
+ service_name: str
34
+ workflow_name: Optional[str] = None
35
+ workflow_task_id: Optional[str] = None
36
+ node_name: Optional[str] = None
37
+ attributes: Optional[Dict[str, Any]] = None
38
+
39
+ class TraceListItem(BaseModel):
40
+ trace_id: TraceId
41
+ service_name: str
42
+ workflow_name: Optional[str] = None
43
+ workflow_task_id: Optional[str] = None
44
+ timestamp: str
45
+ duration_nano: int
46
+ span_count: int
47
+ has_error: bool
48
+ status_code: StatusCode
49
+
50
+ class TraceDetail(BaseModel):
51
+ trace_id: TraceId
52
+ service_name: str
53
+ workflow_name: Optional[str] = None
54
+ workflow_task_id: Optional[str] = None
55
+ spans: List[Span]
56
+ start_time: Optional[str] = None
57
+ end_time: Optional[str] = None
58
+ span_count: int
59
+ has_error: bool
60
+
61
+ class GetTraceListParams(BaseModel):
62
+ service_name: Optional[str] = None
63
+ workflow_name: Optional[str] = None
64
+ workflow_task_id: Optional[str] = None
65
+ start_time: Optional[str] = None
66
+ end_time: Optional[str] = None
67
+ limit: Optional[int] = None
68
+ offset: Optional[int] = None
69
+ has_error: Optional[bool] = None
70
+
71
+ class GetTraceListResponse(BaseModel):
72
+ traces: List[TraceListItem]
73
+ total: int
74
+ limit: int
75
+ offset: int
76
+
77
+ class GetTraceDetailParams(BaseModel):
78
+ trace_id: TraceId
79
+ service_name: Optional[str] = None
80
+
81
+ class GetTraceDetailResponse(BaseModel):
82
+ trace: TraceDetail
service_forge/service.py CHANGED
@@ -1,9 +1,13 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import os
4
+ import uuid
4
5
  import asyncio
5
6
  import threading
6
- import uuid
7
+ from loguru import logger
8
+
9
+ from service_forge.telemetry import setup_tracing
10
+ from importlib.metadata import version
7
11
  from loguru import logger
8
12
  from typing import Callable, AsyncIterator, Awaitable, Any, TYPE_CHECKING
9
13
  from service_forge.workflow.node import node_register
@@ -57,6 +61,7 @@ class Service:
57
61
  return self.metadata.description
58
62
 
59
63
  async def start(self):
64
+ setup_tracing(service_name=self.name, config=self.config.trace)
60
65
  set_service(self)
61
66
 
62
67
  if self.config.enable_http:
@@ -78,7 +83,7 @@ class Service:
78
83
  workflow_group = create_workflow_group(
79
84
  config_path=self.parse_workflow_path(workflow_config_path),
80
85
  service_env=self.service_env,
81
- _handle_stream_output=self._handle_stream_output,
86
+ _handle_stream_output=self._handle_stream_output,
82
87
  _handle_query_user=self._handle_query_user,
83
88
  database_manager=self.database_manager,
84
89
  )
@@ -139,23 +144,23 @@ class Service:
139
144
 
140
145
  def get_workflow_group_by_id(self, workflow_id: str, allow_none: bool = True) -> WorkflowGroup | None:
141
146
  for workflow_group in self.workflow_groups:
142
- if workflow_group.get_workflow_by_id(workflow_id) is not None:
147
+ if workflow_group.get_workflow_by_id(uuid.UUID(workflow_id)) is not None:
143
148
  return workflow_group
144
149
  if not allow_none:
145
150
  raise ValueError(f"Workflow group with id {workflow_id} not found in service {self.name}")
146
151
  return None
147
152
 
148
- def trigger_workflow(self, workflow_group: WorkflowGroup, trigger_name: str, **kwargs) -> uuid.UUID:
153
+ def trigger_workflow(self, workflow_group: WorkflowGroup, trigger_name: str, assigned_task_id: uuid.UUID | None, **kwargs) -> uuid.UUID:
149
154
  workflow = workflow_group.get_main_workflow(allow_none=False)
150
- return workflow.trigger(trigger_name, **kwargs)
155
+ return workflow.trigger(trigger_name, assigned_task_id, **kwargs)
151
156
 
152
- def trigger_workflow_by_name(self, workflow_name: str, workflow_version: str, trigger_name: str, **kwargs) -> uuid.UUID:
157
+ def trigger_workflow_by_name(self, workflow_name: str, workflow_version: str, trigger_name: str, assigned_task_id: uuid.UUID | None, **kwargs) -> uuid.UUID:
153
158
  workflow_group = self.get_workflow_group_by_name(workflow_name, workflow_version, allow_none=False)
154
- return self.trigger_workflow(workflow_group, trigger_name, **kwargs)
159
+ return self.trigger_workflow(workflow_group, trigger_name, assigned_task_id, **kwargs)
155
160
 
156
- def trigger_workflow_by_id(self, workflow_id: str, trigger_name: str, **kwargs) -> uuid.UUID:
161
+ def trigger_workflow_by_id(self, workflow_id: str, trigger_name: str, assigned_task_id: uuid.UUID | None, **kwargs) -> uuid.UUID:
157
162
  workflow_group = self.get_workflow_group_by_id(workflow_id, allow_none=False)
158
- return self.trigger_workflow(workflow_group, trigger_name, **kwargs)
163
+ return self.trigger_workflow(workflow_group, trigger_name, assigned_task_id, **kwargs)
159
164
 
160
165
  def start_workflow(self, workflow_group: WorkflowGroup) -> bool:
161
166
  workflow = workflow_group.get_main_workflow(allow_none=False)
@@ -207,7 +212,7 @@ class Service:
207
212
  workflow_group = self.get_workflow_group_by_id(workflow_id, allow_none=False)
208
213
  return await self.stop_workflow(workflow_group)
209
214
 
210
- async def load_workflow_from_config(self, config_path: str = None, config: dict = None) -> uuid.UUID:
215
+ async def load_workflow_from_config(self, config_path: str = None, config: dict = None, debug_version: bool = False) -> uuid.UUID:
211
216
  workflow_group = create_workflow_group(
212
217
  config_path=config_path,
213
218
  config=config,
@@ -215,6 +220,7 @@ class Service:
215
220
  _handle_stream_output=self._handle_stream_output,
216
221
  _handle_query_user=self._handle_query_user,
217
222
  database_manager=self.database_manager,
223
+ debug_version=debug_version,
218
224
  )
219
225
 
220
226
  for workflow in workflow_group.workflows:
@@ -231,10 +237,12 @@ class Service:
231
237
  self.start_workflow(workflow_group)
232
238
  return main_workflow.id
233
239
 
234
- def get_service_status(self) -> dict[str, Any]:
240
+ def get_service_status(self, exclude_debug: bool = False) -> dict[str, Any]:
235
241
  workflow_statuses = []
236
242
  for workflow_group in self.workflow_groups:
237
243
  for workflow in workflow_group.workflows:
244
+ if exclude_debug and workflow.debug_version:
245
+ continue
238
246
  workflow_id = workflow.id
239
247
  workflow_version = workflow.version
240
248
  workflow_config = workflow.config
@@ -257,9 +265,29 @@ class Service:
257
265
  "description": self.description,
258
266
  "workflows": workflow_statuses,
259
267
  }
268
+
269
+ def get_workflow_status(self, workflow_id: str) -> dict[str, Any]:
270
+ workflow_group = self.get_workflow_group_by_id(workflow_id, allow_none=False)
271
+ main_workflow = workflow_group.get_workflow_by_id(uuid.UUID(workflow_id))
272
+ is_running = workflow_id in self.workflow_tasks and not self.workflow_tasks[workflow_id].done()
273
+ return {
274
+ "name": main_workflow.name,
275
+ "id": main_workflow.id,
276
+ "version": main_workflow.version,
277
+ "config": main_workflow.config,
278
+ "description": main_workflow.description,
279
+ "status": "running" if is_running else "stopped",
280
+ "debug": main_workflow.debug_version or False
281
+ }
260
282
 
261
283
  @staticmethod
262
284
  def from_config(metadata: SfMetadata, service_env: dict[str, Any] = None, config: ServiceConfig = None) -> Service:
285
+ try:
286
+ service_forge_version = version("service-forge")
287
+ logger.info(f"service-forge version: {service_forge_version}")
288
+ except Exception as e:
289
+ logger.warning(f"Failed to get service-forge version: {e}")
290
+
263
291
  if config is not None:
264
292
  config_path = None
265
293
  else:
@@ -7,6 +7,10 @@ class ServiceFeedbackConfig(BaseModel):
7
7
  api_url: str
8
8
  api_timeout: int = 5
9
9
 
10
+ class SignozConfig(BaseModel):
11
+ api_url: str
12
+ api_key: str
13
+
10
14
  class ServiceDatabaseConfig(BaseModel):
11
15
  name: str
12
16
  postgres_user: str | None = None
@@ -25,6 +29,14 @@ class ServiceDatabaseConfig(BaseModel):
25
29
  redis_port: int | None = None
26
30
  redis_password: str | None = None
27
31
 
32
+ class TraceConfig(BaseModel):
33
+ enable: bool = False
34
+ url: str | None = None
35
+ headers: str | None = None
36
+ arg: float | None = None
37
+ namespace: str | None = None
38
+ hostname: str | None = None
39
+
28
40
  class ServiceConfig(BaseModel):
29
41
  name: str
30
42
  workflows: list[str]
@@ -36,6 +48,8 @@ class ServiceConfig(BaseModel):
36
48
  kafka_port: int | None = None
37
49
  databases: list[ServiceDatabaseConfig] | None = None
38
50
  feedback: ServiceFeedbackConfig | None = None
51
+ signoz: SignozConfig | None = None
52
+ trace: TraceConfig | None = None
39
53
 
40
54
  @classmethod
41
55
  def from_yaml_file(cls, filepath: str) -> ServiceConfig:
service_forge/sft/cli.py CHANGED
@@ -9,6 +9,8 @@ from service_forge.sft.cmd.upload_service import upload_service
9
9
  from service_forge.sft.cmd.deploy_service import deploy_service
10
10
  from service_forge.sft.cmd.config_command import list_config, get_config, set_config
11
11
  from service_forge.sft.cmd.service_command import list_services, delete_service, show_service_logs
12
+ from service_forge.sft.cmd.remote_list_tars import remote_list_tars
13
+ from service_forge.sft.cmd.remote_deploy import remote_deploy_tar, remote_list_and_deploy
12
14
 
13
15
  app = typer.Typer(
14
16
  name="sft",
@@ -33,6 +35,43 @@ def list_tars_command() -> None:
33
35
  def deploy_service_command(name: str, version: str) -> None:
34
36
  deploy_service(name, version)
35
37
 
38
+ @app.command(name="remote-list")
39
+ def remote_list_tars_command(
40
+ url: str = typer.Option(
41
+ None,
42
+ "--url",
43
+ "-u",
44
+ help="Service Center URL (default: http://localhost:5000 or from service_center_address config)"
45
+ )
46
+ ) -> None:
47
+ """List tar packages and their status on remote server"""
48
+ remote_list_tars(url)
49
+
50
+ @app.command(name="remote-deploy")
51
+ def remote_deploy_command(
52
+ filename: str = typer.Argument(help="Filename of the tar package to deploy"),
53
+ url: str = typer.Option(
54
+ None,
55
+ "--url",
56
+ "-u",
57
+ help="Service Center URL (default: http://localhost:5000 or from service_center_address config)"
58
+ )
59
+ ) -> None:
60
+ """Remote deploy specified tar package"""
61
+ remote_deploy_tar(filename, url)
62
+
63
+ @app.command(name="remote-deploy-interactive")
64
+ def remote_deploy_interactive_command(
65
+ url: str = typer.Option(
66
+ None,
67
+ "--url",
68
+ "-u",
69
+ help="Service Center URL (default: http://localhost:5000 or from service_center_address config)"
70
+ )
71
+ ) -> None:
72
+ """Interactive remote deployment of tar packages (list available packages first, then select for deployment)"""
73
+ remote_list_and_deploy(url)
74
+
36
75
  config_app = typer.Typer(
37
76
  name="config",
38
77
  help="Configuration management commands",
@@ -0,0 +1,160 @@
1
+ import os
2
+ import json
3
+ import requests
4
+ from pathlib import Path
5
+ from service_forge.sft.util.logger import log_error, log_info, log_success, log_warning
6
+ from service_forge.sft.config.sft_config import sft_config
7
+
8
+ def remote_deploy_tar(filename: str, service_center_url: str = None) -> None:
9
+ """
10
+ Remote deploy specified tar package from service-center
11
+ """
12
+ # If URL is not provided, try to get it from configuration
13
+ if not service_center_url:
14
+ service_center_url = getattr(sft_config, 'service_center_address', 'http://localhost:5000')
15
+
16
+ # Ensure URL ends with /
17
+ if not service_center_url.endswith('/'):
18
+ service_center_url += '/'
19
+
20
+ api_url = f"{service_center_url}api/v1/services/deploy-from-tar"
21
+
22
+ log_info(f"Sending deployment request to {api_url}...")
23
+ log_info(f"Tar package to deploy: {filename}")
24
+
25
+ try:
26
+ # Prepare request data
27
+ data = {
28
+ "filename": filename
29
+ }
30
+
31
+ # Send POST request
32
+ response = requests.post(
33
+ api_url,
34
+ json=data,
35
+ headers={'Content-Type': 'application/json'},
36
+ timeout=300 # 5 minute timeout
37
+ )
38
+
39
+ if response.status_code != 200:
40
+ log_error(f"Deployment request failed, status code: {response.status_code}")
41
+ try:
42
+ error_data = response.json()
43
+ log_error(f"Error message: {error_data.get('message', 'Unknown error')}")
44
+ if 'data' in error_data and error_data['data']:
45
+ log_error(f"Details: {json.dumps(error_data['data'], indent=2, ensure_ascii=False)}")
46
+ except:
47
+ log_error(f"Response content: {response.text}")
48
+ return
49
+
50
+ # Parse response data
51
+ result = response.json()
52
+
53
+ if result.get('code') != 200:
54
+ log_error(f"Deployment failed: {result.get('message', 'Unknown error')}")
55
+ if 'data' in result and result['data']:
56
+ log_error(f"Details: {json.dumps(result['data'], indent=2, ensure_ascii=False)}")
57
+ return
58
+
59
+ # Deployment successful
60
+ data = result.get('data', {})
61
+ service_name = data.get('service_name', 'Unknown')
62
+ version = data.get('version', 'Unknown')
63
+ deploy_output = data.get('deploy_output', '')
64
+
65
+ log_success(f"Successfully deployed service: {service_name} version: {version}")
66
+
67
+ if deploy_output:
68
+ log_info("Deployment output:")
69
+ print(deploy_output)
70
+
71
+ except requests.exceptions.Timeout:
72
+ log_error("Deployment request timed out (exceeded 5 minutes), please check service status or try again later")
73
+ except requests.exceptions.RequestException as e:
74
+ log_error(f"Request failed: {str(e)}")
75
+ log_info(f"Please check if service-center service is running normally and if the URL is correct: {service_center_url}")
76
+ except Exception as e:
77
+ log_error(f"Exception occurred while deploying tar package: {str(e)}")
78
+
79
+ def remote_list_and_deploy(service_center_url: str = None) -> None:
80
+ """
81
+ List remote tar packages first, then let user select which package to deploy
82
+ """
83
+ # If URL is not provided, try to get it from configuration
84
+ if not service_center_url:
85
+ service_center_url = getattr(sft_config, 'service_center_address', 'http://localhost:5000')
86
+
87
+ # Ensure URL ends with /
88
+ if not service_center_url.endswith('/'):
89
+ service_center_url += '/'
90
+
91
+ api_url = f"{service_center_url}api/v1/services/tar-list"
92
+
93
+ log_info(f"Getting tar package list from {api_url}...")
94
+
95
+ try:
96
+ # 发送GET请求获取tar包列表
97
+ response = requests.get(api_url, timeout=30)
98
+
99
+ if response.status_code != 200:
100
+ log_error(f"Failed to get tar package list, status code: {response.status_code}")
101
+ return
102
+
103
+ # Parse response data
104
+ result = response.json()
105
+
106
+ if result.get('code') != 200:
107
+ log_error(f"Failed to get tar package list: {result.get('message', 'Unknown error')}")
108
+ return
109
+
110
+ tar_files = result.get('data', [])
111
+
112
+ if not tar_files:
113
+ log_info("No tar packages found")
114
+ return
115
+
116
+ # Display tar package list
117
+ log_info("Available tar package list:")
118
+ for i, tar_file in enumerate(tar_files, 1):
119
+ filename = tar_file.get('filename', '-')
120
+ service_name = tar_file.get('service_name', '-')
121
+ version = tar_file.get('version', '-')
122
+ deployed_status = "Deployed" if tar_file.get('deployed_status', False) else "Not Deployed"
123
+
124
+ print(f"{i}. {filename} (service: {service_name}, version: {version}, status: {deployed_status})")
125
+
126
+ # Let user choose
127
+ try:
128
+ choice = input("\nEnter the number of the tar package to deploy (enter 'q' to exit): ").strip()
129
+
130
+ if choice.lower() == 'q':
131
+ log_info("Deployment cancelled")
132
+ return
133
+
134
+ index = int(choice) - 1
135
+ if 0 <= index < len(tar_files):
136
+ selected_tar = tar_files[index]
137
+ filename = selected_tar.get('filename')
138
+
139
+ if selected_tar.get('deployed_status', False):
140
+ log_warning(f"Tar package {filename} is already deployed, continue deployment?")
141
+ confirm = input("Enter 'y' to continue, any other key to cancel: ").strip().lower()
142
+ if confirm != 'y':
143
+ log_info("Deployment cancelled")
144
+ return
145
+
146
+ log_info(f"Selected for deployment: {filename}")
147
+ remote_deploy_tar(filename, service_center_url)
148
+ else:
149
+ log_error("Invalid selection")
150
+
151
+ except ValueError:
152
+ log_error("Please enter a valid number")
153
+ except KeyboardInterrupt:
154
+ log_info("\nDeployment cancelled")
155
+
156
+ except requests.exceptions.RequestException as e:
157
+ log_error(f"Request failed: {str(e)}")
158
+ log_info(f"Please check if service-center service is running normally and if the URL is correct: {service_center_url}")
159
+ except Exception as e:
160
+ log_error(f"Exception occurred while getting tar package list: {str(e)}")
@@ -0,0 +1,111 @@
1
+ import os
2
+ import json
3
+ import requests
4
+ from pathlib import Path
5
+ from rich.console import Console
6
+ from rich.table import Table
7
+ from service_forge.sft.util.logger import log_error, log_info, log_success, log_warning
8
+ from service_forge.sft.config.sft_config import sft_config
9
+
10
+ def remote_list_tars(service_center_url: str = None) -> None:
11
+ """
12
+ Get remote tar package list and status from service-center
13
+ """
14
+ # If URL is not provided, try to get it from configuration
15
+ if not service_center_url:
16
+ service_center_url = getattr(sft_config, 'service_center_address', 'http://localhost:5000')
17
+
18
+ # Ensure URL ends with /
19
+ if not service_center_url.endswith('/'):
20
+ service_center_url += '/'
21
+
22
+ api_url = f"{service_center_url}api/v1/services/tar-list"
23
+
24
+ log_info(f"Getting tar package list from {api_url}...")
25
+
26
+ try:
27
+ # 发送GET请求
28
+ response = requests.get(api_url, timeout=30)
29
+
30
+ if response.status_code != 200:
31
+ log_error(f"Failed to get tar package list, status code: {response.status_code}")
32
+ try:
33
+ error_data = response.json()
34
+ log_error(f"Error message: {error_data.get('message', 'Unknown error')}")
35
+ except:
36
+ log_error(f"Response content: {response.text}")
37
+ return
38
+
39
+ # Parse response data
40
+ result = response.json()
41
+
42
+ if result.get('code') != 200:
43
+ log_error(f"Failed to get tar package list: {result.get('message', 'Unknown error')}")
44
+ return
45
+
46
+ tar_files = result.get('data', [])
47
+
48
+ if not tar_files:
49
+ log_info("No tar packages found")
50
+ return
51
+
52
+ # Use rich table to display results
53
+ console = Console()
54
+ table = Table(title="Remote Server Tar Package List", show_header=True, header_style="bold magenta")
55
+ table.add_column("Filename", style="cyan", no_wrap=True)
56
+ table.add_column("Service Name", style="green", no_wrap=True)
57
+ table.add_column("Version", style="blue", no_wrap=True)
58
+ table.add_column("Size", justify="right", style="yellow")
59
+ table.add_column("Modified Time", style="dim")
60
+ table.add_column("Deploy Status", justify="center", style="bold")
61
+
62
+ for tar_file in tar_files:
63
+ # Format file size
64
+ size = _format_size(tar_file.get('file_size', 0))
65
+
66
+ # Format modification time
67
+ modified_time = _format_time(tar_file.get('modified_time', 0))
68
+
69
+ # Deployment status
70
+ deployed_status = "✅ Deployed" if tar_file.get('deployed_status', False) else "❌ Not Deployed"
71
+ status_style = "green" if tar_file.get('deployed_status', False) else "red"
72
+
73
+ table.add_row(
74
+ tar_file.get('filename', '-'),
75
+ tar_file.get('service_name', '-'),
76
+ tar_file.get('version', '-'),
77
+ size,
78
+ modified_time,
79
+ f"[{status_style}]{deployed_status}[/{status_style}]"
80
+ )
81
+
82
+ console.print(table)
83
+ log_success(f"Found {len(tar_files)} tar packages in total")
84
+
85
+ except requests.exceptions.RequestException as e:
86
+ log_error(f"Request failed: {str(e)}")
87
+ log_info(f"Please check if service-center service is running normally and if the URL is correct: {service_center_url}")
88
+ except Exception as e:
89
+ log_error(f"Exception occurred while getting tar package list: {str(e)}")
90
+
91
+ def _format_size(size_bytes: int) -> str:
92
+ """Format file size"""
93
+ if size_bytes == 0:
94
+ return "0 B"
95
+
96
+ for unit in ['B', 'KB', 'MB', 'GB']:
97
+ if size_bytes < 1024.0:
98
+ return f"{size_bytes:.2f} {unit}"
99
+ size_bytes /= 1024.0
100
+ return f"{size_bytes:.2f} TB"
101
+
102
+ def _format_time(timestamp: float) -> str:
103
+ """Format timestamp"""
104
+ if timestamp == 0:
105
+ return "-"
106
+
107
+ try:
108
+ from datetime import datetime
109
+ return datetime.fromtimestamp(timestamp).strftime("%Y-%m-%d %H:%M:%S")
110
+ except:
111
+ return "-"