service-forge 0.1.21__py3-none-any.whl → 0.1.24__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.
- service_forge/db/database.py +17 -0
- service_forge/sft/cli.py +39 -0
- service_forge/sft/cmd/remote_deploy.py +160 -0
- service_forge/sft/cmd/remote_list_tars.py +111 -0
- service_forge/sft/config/injector.py +13 -1
- service_forge/sft/config/injector_default_files.py +1 -1
- service_forge/utils/default_type_converter.py +1 -1
- service_forge/utils/type_converter.py +5 -0
- service_forge/workflow/workflow.py +48 -27
- service_forge/workflow/workflow_callback.py +3 -2
- {service_forge-0.1.21.dist-info → service_forge-0.1.24.dist-info}/METADATA +1 -1
- {service_forge-0.1.21.dist-info → service_forge-0.1.24.dist-info}/RECORD +14 -12
- {service_forge-0.1.21.dist-info → service_forge-0.1.24.dist-info}/WHEEL +0 -0
- {service_forge-0.1.21.dist-info → service_forge-0.1.24.dist-info}/entry_points.txt +0 -0
service_forge/db/database.py
CHANGED
|
@@ -7,6 +7,7 @@ from typing import AsyncGenerator
|
|
|
7
7
|
from loguru import logger
|
|
8
8
|
from sqlalchemy.ext.asyncio import AsyncEngine, AsyncSession, async_sessionmaker, create_async_engine
|
|
9
9
|
from service_forge.service_config import ServiceConfig
|
|
10
|
+
from pymongo import AsyncMongoClient
|
|
10
11
|
|
|
11
12
|
class PostgresDatabase:
|
|
12
13
|
def __init__(
|
|
@@ -114,6 +115,7 @@ class MongoDatabase:
|
|
|
114
115
|
self.mongo_password = mongo_password
|
|
115
116
|
self.mongo_db = mongo_db or ""
|
|
116
117
|
self.client = pymongo.MongoClient(self.database_url)
|
|
118
|
+
self.async_client = AsyncMongoClient(self.database_url)
|
|
117
119
|
self.test_connection()
|
|
118
120
|
|
|
119
121
|
@property
|
|
@@ -129,6 +131,21 @@ class MongoDatabase:
|
|
|
129
131
|
logger.error(f"MongoDB connection test failed for database '{self.name}': {e}")
|
|
130
132
|
return False
|
|
131
133
|
|
|
134
|
+
async def test_async_connection(self) -> bool:
|
|
135
|
+
try:
|
|
136
|
+
await self.async_client.admin.command('ping')
|
|
137
|
+
logger.info(f"Async MongoDB connection test successful for database '{self.name}'")
|
|
138
|
+
return True
|
|
139
|
+
except Exception as e:
|
|
140
|
+
logger.error(f"Async MongoDB connection test failed for database '{self.name}': {e}")
|
|
141
|
+
return False
|
|
142
|
+
|
|
143
|
+
def get_sync_collection(self, collection_name: str):
|
|
144
|
+
return self.client[self.mongo_db][collection_name]
|
|
145
|
+
|
|
146
|
+
def get_async_collection(self, collection_name: str):
|
|
147
|
+
return self.async_client[self.mongo_db][collection_name]
|
|
148
|
+
|
|
132
149
|
class RedisDatabase:
|
|
133
150
|
def __init__(
|
|
134
151
|
self,
|
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 "-"
|
|
@@ -16,6 +16,7 @@ class Injector:
|
|
|
16
16
|
self.ingress_yaml_path = project_dir / "ingress.yaml"
|
|
17
17
|
self.dockerfile_path = project_dir / "Dockerfile"
|
|
18
18
|
self.pyproject_toml_path = project_dir / "pyproject.toml"
|
|
19
|
+
self.start_sh_path = project_dir / "start.sh"
|
|
19
20
|
self.metadata = load_metadata(self.metadata_path)
|
|
20
21
|
self.name = self.metadata.name
|
|
21
22
|
self.version = self.metadata.version
|
|
@@ -121,9 +122,20 @@ class Injector:
|
|
|
121
122
|
f.write(pyproject_toml)
|
|
122
123
|
print("pyproject_toml_path: ", self.pyproject_toml_path)
|
|
123
124
|
|
|
125
|
+
def clear_start_sh(self) -> None:
|
|
126
|
+
if Path(self.start_sh_path).exists():
|
|
127
|
+
with open(self.start_sh_path, "rb") as f:
|
|
128
|
+
content = f.read()
|
|
129
|
+
content_str = content.decode("utf-8")
|
|
130
|
+
lines = content_str.splitlines()
|
|
131
|
+
new_content = "\n".join(lines) + ("\n" if content_str.endswith(('\n', '\r')) else "")
|
|
132
|
+
with open(self.start_sh_path, "w", encoding="utf-8", newline="\n") as f:
|
|
133
|
+
f.write(new_content)
|
|
134
|
+
|
|
124
135
|
def inject(self) -> None:
|
|
125
136
|
self.inject_deployment()
|
|
126
137
|
self.inject_service_config()
|
|
127
138
|
self.inject_ingress()
|
|
128
139
|
self.inject_dockerfile()
|
|
129
|
-
self.inject_pyproject_toml()
|
|
140
|
+
self.inject_pyproject_toml()
|
|
141
|
+
self.clear_start_sh()
|
|
@@ -2,8 +2,8 @@ from ..utils.type_converter import TypeConverter
|
|
|
2
2
|
from ..workflow.workflow import Workflow
|
|
3
3
|
from ..api.http_api import fastapi_app
|
|
4
4
|
from ..api.kafka_api import KafkaApp, kafka_app
|
|
5
|
-
from fastapi import FastAPI
|
|
6
5
|
from ..workflow.workflow_type import WorkflowType, workflow_type_register
|
|
6
|
+
from fastapi import FastAPI
|
|
7
7
|
|
|
8
8
|
type_converter = TypeConverter()
|
|
9
9
|
type_converter.register(str, Workflow, lambda s, node: node.sub_workflows.get_workflow(s))
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
from typing import Any, Callable, Type, Dict, Tuple, Set, List
|
|
2
2
|
from collections import deque
|
|
3
3
|
import inspect
|
|
4
|
+
import traceback
|
|
5
|
+
from pydantic import BaseModel
|
|
4
6
|
from typing_extensions import get_origin, get_args
|
|
5
7
|
|
|
6
8
|
def is_type(value, dst_type):
|
|
@@ -57,6 +59,9 @@ class TypeConverter:
|
|
|
57
59
|
except Exception:
|
|
58
60
|
pass
|
|
59
61
|
|
|
62
|
+
if issubclass(dst_type, BaseModel) and isinstance(value, dict):
|
|
63
|
+
return dst_type(**value)
|
|
64
|
+
|
|
60
65
|
path = self._find_path(src_type, dst_type)
|
|
61
66
|
if not path:
|
|
62
67
|
raise TypeError(f"No conversion path found from {src_type.__name__} to {dst_type.__name__}.")
|
|
@@ -79,6 +79,8 @@ class Workflow:
|
|
|
79
79
|
await callback.on_workflow_start(*args, **kwargs)
|
|
80
80
|
elif callback_type == CallbackEvent.ON_WORKFLOW_END:
|
|
81
81
|
await callback.on_workflow_end(*args, **kwargs)
|
|
82
|
+
elif callback_type == CallbackEvent.ON_WORKFLOW_ERROR:
|
|
83
|
+
await callback.on_workflow_error(*args, **kwargs)
|
|
82
84
|
elif callback_type == CallbackEvent.ON_NODE_START:
|
|
83
85
|
await callback.on_node_start(*args, **kwargs)
|
|
84
86
|
elif callback_type == CallbackEvent.ON_NODE_END:
|
|
@@ -122,7 +124,7 @@ class Workflow:
|
|
|
122
124
|
raise ValueError("Multiple trigger nodes found in workflow.")
|
|
123
125
|
return trigger_nodes[0]
|
|
124
126
|
|
|
125
|
-
async def _run_node_with_callbacks(self, node: Node) ->
|
|
127
|
+
async def _run_node_with_callbacks(self, node: Node) -> bool:
|
|
126
128
|
await self.call_callbacks(CallbackEvent.ON_NODE_START, node=node)
|
|
127
129
|
|
|
128
130
|
try:
|
|
@@ -131,8 +133,13 @@ class Workflow:
|
|
|
131
133
|
await self.handle_node_stream_output(node, result)
|
|
132
134
|
elif asyncio.iscoroutine(result):
|
|
133
135
|
await result
|
|
136
|
+
except Exception as e:
|
|
137
|
+
await self.call_callbacks(CallbackEvent.ON_WORKFLOW_ERROR, workflow=self, node=node, error=e)
|
|
138
|
+
logger.error(f"Error when running node {node.name}: {str(e)}, task_id: {self.task_id}")
|
|
139
|
+
return False
|
|
134
140
|
finally:
|
|
135
141
|
await self.call_callbacks(CallbackEvent.ON_NODE_END, node=node)
|
|
142
|
+
return True
|
|
136
143
|
|
|
137
144
|
async def run_after_trigger(self) -> Any:
|
|
138
145
|
logger.info(f"Running workflow: {self.name}")
|
|
@@ -143,30 +150,41 @@ class Workflow:
|
|
|
143
150
|
for edge in self.get_trigger_node().output_edges:
|
|
144
151
|
edge.end_port.trigger()
|
|
145
152
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
for
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
153
|
+
for input_port in self.input_ports:
|
|
154
|
+
if input_port.value is not None:
|
|
155
|
+
input_port.port.node.fill_input(input_port.port, input_port.value)
|
|
156
|
+
|
|
157
|
+
for node in self.nodes:
|
|
158
|
+
for key in node.AUTO_FILL_INPUT_PORTS:
|
|
159
|
+
if key[0] not in [edge.end_port.name for edge in node.input_edges]:
|
|
160
|
+
node.fill_input_by_name(key[0], key[1])
|
|
161
|
+
|
|
162
|
+
while self.ready_nodes:
|
|
163
|
+
nodes = self.ready_nodes.copy()
|
|
164
|
+
self.ready_nodes = []
|
|
165
|
+
|
|
166
|
+
tasks = []
|
|
167
|
+
for node in nodes:
|
|
168
|
+
tasks.append(asyncio.create_task(self._run_node_with_callbacks(node)))
|
|
169
|
+
|
|
170
|
+
results = await asyncio.gather(*tasks, return_exceptions=True)
|
|
171
|
+
|
|
172
|
+
for i, result in enumerate(results):
|
|
173
|
+
if isinstance(result, Exception):
|
|
174
|
+
for task in tasks:
|
|
175
|
+
if not task.done():
|
|
176
|
+
task.cancel()
|
|
177
|
+
await asyncio.gather(*tasks, return_exceptions=True)
|
|
178
|
+
return
|
|
179
|
+
# raise result
|
|
180
|
+
elif result is False:
|
|
181
|
+
logger.error(f"Node execution failed, stopping workflow: {nodes[i].name}")
|
|
182
|
+
for task in tasks:
|
|
183
|
+
if not task.done():
|
|
184
|
+
task.cancel()
|
|
185
|
+
await asyncio.gather(*tasks, return_exceptions=True)
|
|
186
|
+
return
|
|
187
|
+
# raise RuntimeError(f"Workflow stopped due to node execution failure: {nodes[i].name}")
|
|
170
188
|
|
|
171
189
|
if len(self.output_ports) > 0:
|
|
172
190
|
if len(self.output_ports) == 1:
|
|
@@ -191,8 +209,11 @@ class Workflow:
|
|
|
191
209
|
# TODO: clear new_workflow
|
|
192
210
|
|
|
193
211
|
except Exception as e:
|
|
194
|
-
|
|
195
|
-
|
|
212
|
+
await self.call_callbacks(CallbackEvent.ON_WORKFLOW_ERROR, workflow=self, node=None, error=e)
|
|
213
|
+
# error_msg = f"Error running workflow: {str(e)}, {traceback.format_exc()}"
|
|
214
|
+
# logger.error(error_msg)
|
|
215
|
+
# await self.call_callbacks(CallbackEvent.ON_WORKFLOW_END, workflow=self, node=None, error=e)
|
|
216
|
+
return
|
|
196
217
|
|
|
197
218
|
async def run(self):
|
|
198
219
|
tasks = []
|
|
@@ -31,7 +31,7 @@ class WorkflowCallback:
|
|
|
31
31
|
pass
|
|
32
32
|
|
|
33
33
|
@abstractmethod
|
|
34
|
-
async def on_workflow_error(self, workflow: Workflow, error: Any) -> None:
|
|
34
|
+
async def on_workflow_error(self, workflow: Workflow, node: Node, error: Any) -> None:
|
|
35
35
|
pass
|
|
36
36
|
|
|
37
37
|
@abstractmethod
|
|
@@ -90,7 +90,7 @@ class BuiltinWorkflowCallback(WorkflowCallback):
|
|
|
90
90
|
logger.error(f"发送 workflow_end 消息到 websocket 失败: {e}")
|
|
91
91
|
|
|
92
92
|
@override
|
|
93
|
-
async def on_workflow_error(self, workflow: Workflow, error: Any) -> None:
|
|
93
|
+
async def on_workflow_error(self, workflow: Workflow, node: Node | None, error: Any) -> None:
|
|
94
94
|
workflow_result = WorkflowResult(result=error, is_end=False, is_error=True)
|
|
95
95
|
|
|
96
96
|
if workflow.task_id in workflow.real_trigger_node.result_queues:
|
|
@@ -103,6 +103,7 @@ class BuiltinWorkflowCallback(WorkflowCallback):
|
|
|
103
103
|
message = {
|
|
104
104
|
"type": "workflow_error",
|
|
105
105
|
"task_id": str(workflow.task_id),
|
|
106
|
+
"node": node.name if node else None,
|
|
106
107
|
"error": self._serialize_result(error),
|
|
107
108
|
"is_end": False,
|
|
108
109
|
"is_error": True
|
|
@@ -12,7 +12,7 @@ service_forge/api/routers/service/service_router.py,sha256=hGOT-ScnXR7agHp-F9OFG
|
|
|
12
12
|
service_forge/api/routers/websocket/websocket_manager.py,sha256=j1AFqzXQhZZyaLQwhvZefXAS-zCOPzLcRMDEuusv6V0,3605
|
|
13
13
|
service_forge/api/routers/websocket/websocket_router.py,sha256=V0B7eQP8toO94-WbTrGraadXD3qeZ9lnKFcxwx6kLgM,3777
|
|
14
14
|
service_forge/db/__init__.py,sha256=EWLhH8bYsMOvRF_YXF6FgL3irKA6GZeLxSGvWDRM6f8,85
|
|
15
|
-
service_forge/db/database.py,sha256=
|
|
15
|
+
service_forge/db/database.py,sha256=WKtZ0MoOnbMw54ohfs9zKsrOZ5_qenLvXkAV_Gr2WOs,10068
|
|
16
16
|
service_forge/db/migrations/feedback_migration.py,sha256=-zQ71TsOlWmQPQo1NKSIu3C1T47v3cfD6IAQ5HE_ffk,4845
|
|
17
17
|
service_forge/db/models/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
18
18
|
service_forge/db/models/feedback.py,sha256=gltX3y-nNhXSR9z1cd82Vg-zwjF0JhnGbOvUapkcWKQ,1253
|
|
@@ -22,14 +22,16 @@ service_forge/model/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSu
|
|
|
22
22
|
service_forge/model/feedback.py,sha256=Is5tkplzYkjChGb67o-Qjtbu4kSspVuaKi4Aua_QdRo,1318
|
|
23
23
|
service_forge/model/websocket.py,sha256=YIUCW32sbHIEFPHjk5FiDM_rDe2aVD6OpzBQul2R5IM,267
|
|
24
24
|
service_forge/proto/foo_input.py,sha256=-POJZSIFrGdBGz7FqZZ03r5uztpc5Apin9A0Yxbk6YI,90
|
|
25
|
-
service_forge/sft/cli.py,sha256=
|
|
25
|
+
service_forge/sft/cli.py,sha256=xcM6kiGPJeHEUqfJd4uFngVFpjfVkDxW4G_PbNRD9Xs,4265
|
|
26
26
|
service_forge/sft/cmd/config_command.py,sha256=I9t2HG28S6lCXpExHyZUc47b_1yB3i51tCFVk5J6TTU,2382
|
|
27
27
|
service_forge/sft/cmd/deploy_service.py,sha256=5IYbCVI0Nlry1KXBhm9leJmr2bzUEXrSY-2BympLR0c,4686
|
|
28
28
|
service_forge/sft/cmd/list_tars.py,sha256=Z3zvu2JLb_wNbTwi5TZXL5cZ8PxYrKks9AxkOzoUd_Q,1380
|
|
29
|
+
service_forge/sft/cmd/remote_deploy.py,sha256=AStAlbqGD7XeZFhL0fx2j12YWP_MVbdURbO5ZENEMgc,6510
|
|
30
|
+
service_forge/sft/cmd/remote_list_tars.py,sha256=mx6hkNnu0ySMyBX2Qi6blKMj5xnNnrmXq3VD_nERlmw,4176
|
|
29
31
|
service_forge/sft/cmd/service_command.py,sha256=69GMMN61KtuoEFuYzFJ74ivNt8RX8q0I6rbePfJfEwQ,5538
|
|
30
32
|
service_forge/sft/cmd/upload_service.py,sha256=86PvvJSXCZKH4BU6rLytuc45grX-sRnQnOHCo9zUaPY,1232
|
|
31
|
-
service_forge/sft/config/injector.py,sha256
|
|
32
|
-
service_forge/sft/config/injector_default_files.py,sha256=
|
|
33
|
+
service_forge/sft/config/injector.py,sha256=-eU21Ob09ond9pwHVkjklAd5_qLWWbbf0b91iCg2Kwk,6335
|
|
34
|
+
service_forge/sft/config/injector_default_files.py,sha256=f7mNJ5Y9yb4e9kjLn414WiQoZrOue9ok_hq_POG4I2o,2717
|
|
33
35
|
service_forge/sft/config/sf_metadata.py,sha256=Y9akhSCgOd11-oqRs3LIs8FL9pvWNw6hyy57fuFcBhc,866
|
|
34
36
|
service_forge/sft/config/sft_config.py,sha256=MgurtgbcSmyXbGlVX3NG84KD4Hst1gZWHdF9a8zi-6U,7707
|
|
35
37
|
service_forge/sft/file/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
@@ -43,9 +45,9 @@ service_forge/sft/util/yaml_utils.py,sha256=9OhJNQlzj_C1NeQoUZVF8qpDovrE7RDWtNXe
|
|
|
43
45
|
service_forge/storage/__init__.py,sha256=8Jg4R9z2JHadheV1YrHtCsFxEL5aCl9n2dMQGHcJfvM,156
|
|
44
46
|
service_forge/storage/feedback_storage.py,sha256=wnuNTmEzpnS7iisiU9MrEJIgVa2G_HysqICWk_PxzfU,9124
|
|
45
47
|
service_forge/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
46
|
-
service_forge/utils/default_type_converter.py,sha256=
|
|
48
|
+
service_forge/utils/default_type_converter.py,sha256=KFWhlZJUrQc2e-Wm9-WfPUKp5UaI-fpVjzNLO6n37e8,625
|
|
47
49
|
service_forge/utils/register.py,sha256=nxiGQBCX238FoZZhsDoDdBMv_2QzeIZpM367HPNfaqM,874
|
|
48
|
-
service_forge/utils/type_converter.py,sha256=
|
|
50
|
+
service_forge/utils/type_converter.py,sha256=eGAAnqEr-va1PBwYOzuzzzExVJNtusn-yGRv7i9WjRY,3204
|
|
49
51
|
service_forge/utils/workflow_clone.py,sha256=K2Y4XXsGYQn4OTKcDYEa7UZHGXta_hztKW-pr4EYaDQ,4593
|
|
50
52
|
service_forge/workflow/__init__.py,sha256=9oh4qPyA33RugrUYRAlLmFtmQPUN2wxruFQE3omTJF8,49
|
|
51
53
|
service_forge/workflow/context.py,sha256=1PotSEN_l8Emd5p8_6mtXJngXGYd3NSbOs1EKHgvnlo,346
|
|
@@ -53,8 +55,8 @@ service_forge/workflow/edge.py,sha256=88Ex-9_dHAGD38OHgiqP0DrfxK0FrhvDAxThR3ilUi
|
|
|
53
55
|
service_forge/workflow/node.py,sha256=hoO8TdfbB5inpu55YCwecnasi4RS-Bg9R8Sp-M5c2Ys,7841
|
|
54
56
|
service_forge/workflow/port.py,sha256=JVj0JrnQeOWCsp7n48Cm03bfmO8r3V5oTSEsC-HTGPE,2967
|
|
55
57
|
service_forge/workflow/trigger.py,sha256=2OqiHi0dFcoC8g5GDqVpVEpHKlmqtDADb74Z7PRzHlo,879
|
|
56
|
-
service_forge/workflow/workflow.py,sha256=
|
|
57
|
-
service_forge/workflow/workflow_callback.py,sha256=
|
|
58
|
+
service_forge/workflow/workflow.py,sha256=YsA_Yeh5XjYQflztqKWoWBt9euEoHVOJMGMTquIoO04,9788
|
|
59
|
+
service_forge/workflow/workflow_callback.py,sha256=MJBG_DTQGCgqCjpnBhuSteZmOxitYRdtkxXlFCOh930,5219
|
|
58
60
|
service_forge/workflow/workflow_config.py,sha256=Yih10b-utKIpaR-X-nfy7fPnmBNhRvlD8Bw2_mQ5lJI,1821
|
|
59
61
|
service_forge/workflow/workflow_event.py,sha256=QG1VFJwUUF1bTKKPKvqBICnYxkBwpfYDEoAuxwQYhhE,371
|
|
60
62
|
service_forge/workflow/workflow_factory.py,sha256=KfIxjdQhsRC0KYrEkAhqlx3oY6tABoulQGhBwBBXLq0,9933
|
|
@@ -77,7 +79,7 @@ service_forge/workflow/triggers/kafka_api_trigger.py,sha256=Zv8J75Rmg1-xqxHwpBMB
|
|
|
77
79
|
service_forge/workflow/triggers/once_trigger.py,sha256=YmzSQBoKE-8liNFIoDCqi2UdqhHujizsXVDft81_8jA,572
|
|
78
80
|
service_forge/workflow/triggers/period_trigger.py,sha256=JFX3yBjKqoRP55jiulaSG_SPO-zWLMcwEb1BwcKsWUM,767
|
|
79
81
|
service_forge/workflow/triggers/websocket_api_trigger.py,sha256=JjRYb0VM_em90_w3QEErgxMDBCaBHCrSBhTaV4Fl4HY,6894
|
|
80
|
-
service_forge-0.1.
|
|
81
|
-
service_forge-0.1.
|
|
82
|
-
service_forge-0.1.
|
|
83
|
-
service_forge-0.1.
|
|
82
|
+
service_forge-0.1.24.dist-info/METADATA,sha256=tOQocyV5Smmw3sdoR6k-0MBwB-C0eaMGlG2hRDGj4AY,2308
|
|
83
|
+
service_forge-0.1.24.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
84
|
+
service_forge-0.1.24.dist-info/entry_points.txt,sha256=WHntHW7GAyKQUEeMcMvHDZ7_xAb0-cZeAK4iJeu9lm8,51
|
|
85
|
+
service_forge-0.1.24.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|