service-forge 0.1.0__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.
- service_forge/api/http_api.py +138 -0
- service_forge/api/kafka_api.py +126 -0
- service_forge/api/task_manager.py +141 -0
- service_forge/api/websocket_api.py +86 -0
- service_forge/api/websocket_manager.py +425 -0
- service_forge/db/__init__.py +1 -0
- service_forge/db/database.py +119 -0
- service_forge/llm/__init__.py +62 -0
- service_forge/llm/llm.py +56 -0
- service_forge/main.py +121 -0
- service_forge/model/__init__.py +0 -0
- service_forge/model/websocket.py +13 -0
- service_forge/proto/foo_input.py +5 -0
- service_forge/service.py +111 -0
- service_forge/service_config.py +115 -0
- service_forge/sft/cli.py +91 -0
- service_forge/sft/cmd/config_command.py +67 -0
- service_forge/sft/cmd/deploy_service.py +124 -0
- service_forge/sft/cmd/list_tars.py +41 -0
- service_forge/sft/cmd/service_command.py +149 -0
- service_forge/sft/cmd/upload_service.py +36 -0
- service_forge/sft/config/injector.py +87 -0
- service_forge/sft/config/injector_default_files.py +97 -0
- service_forge/sft/config/sf_metadata.py +30 -0
- service_forge/sft/config/sft_config.py +125 -0
- service_forge/sft/file/__init__.py +0 -0
- service_forge/sft/file/ignore_pattern.py +80 -0
- service_forge/sft/file/sft_file_manager.py +107 -0
- service_forge/sft/kubernetes/kubernetes_manager.py +257 -0
- service_forge/sft/util/assert_util.py +25 -0
- service_forge/sft/util/logger.py +16 -0
- service_forge/sft/util/name_util.py +2 -0
- service_forge/utils/__init__.py +0 -0
- service_forge/utils/default_type_converter.py +12 -0
- service_forge/utils/register.py +39 -0
- service_forge/utils/type_converter.py +74 -0
- service_forge/workflow/__init__.py +1 -0
- service_forge/workflow/context.py +13 -0
- service_forge/workflow/edge.py +31 -0
- service_forge/workflow/node.py +179 -0
- service_forge/workflow/nodes/__init__.py +7 -0
- service_forge/workflow/nodes/control/if_node.py +29 -0
- service_forge/workflow/nodes/input/console_input_node.py +26 -0
- service_forge/workflow/nodes/llm/query_llm_node.py +41 -0
- service_forge/workflow/nodes/nested/workflow_node.py +28 -0
- service_forge/workflow/nodes/output/kafka_output_node.py +27 -0
- service_forge/workflow/nodes/output/print_node.py +29 -0
- service_forge/workflow/nodes/test/if_console_input_node.py +33 -0
- service_forge/workflow/nodes/test/time_consuming_node.py +61 -0
- service_forge/workflow/port.py +86 -0
- service_forge/workflow/trigger.py +20 -0
- service_forge/workflow/triggers/__init__.py +4 -0
- service_forge/workflow/triggers/fast_api_trigger.py +125 -0
- service_forge/workflow/triggers/kafka_api_trigger.py +44 -0
- service_forge/workflow/triggers/once_trigger.py +20 -0
- service_forge/workflow/triggers/period_trigger.py +26 -0
- service_forge/workflow/workflow.py +251 -0
- service_forge/workflow/workflow_factory.py +227 -0
- service_forge/workflow/workflow_group.py +23 -0
- service_forge/workflow/workflow_type.py +52 -0
- service_forge-0.1.0.dist-info/METADATA +93 -0
- service_forge-0.1.0.dist-info/RECORD +64 -0
- service_forge-0.1.0.dist-info/WHEEL +4 -0
- service_forge-0.1.0.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
from typing import List, Optional
|
|
2
|
+
import typer
|
|
3
|
+
from rich.console import Console
|
|
4
|
+
from rich.table import Table
|
|
5
|
+
from kubernetes import client, config
|
|
6
|
+
from kubernetes.client.rest import ApiException
|
|
7
|
+
|
|
8
|
+
from service_forge.sft.config.sft_config import sft_config
|
|
9
|
+
from service_forge.sft.util.logger import log_error, log_info, log_success, log_warning
|
|
10
|
+
from service_forge.sft.kubernetes.kubernetes_manager import KubernetesManager
|
|
11
|
+
|
|
12
|
+
def list_services() -> None:
|
|
13
|
+
namespace = sft_config.k8s_namespace
|
|
14
|
+
kubernetes_manager = KubernetesManager()
|
|
15
|
+
services = kubernetes_manager.get_services_in_namespace(namespace)
|
|
16
|
+
|
|
17
|
+
if not services:
|
|
18
|
+
log_warning(f"No services starting with 'sf-' found in namespace '{namespace}'")
|
|
19
|
+
return
|
|
20
|
+
|
|
21
|
+
console = Console()
|
|
22
|
+
table = Table(title=f"Services in namespace '{namespace}' (sf-*)", show_header=True, header_style="bold magenta")
|
|
23
|
+
table.add_column("Name", style="cyan", no_wrap=True)
|
|
24
|
+
table.add_column("Type", style="green")
|
|
25
|
+
table.add_column("Port", style="yellow")
|
|
26
|
+
table.add_column("Target Port", style="yellow")
|
|
27
|
+
|
|
28
|
+
for service_name in sorted(services):
|
|
29
|
+
details = kubernetes_manager.get_service_details(namespace, service_name)
|
|
30
|
+
table.add_row(
|
|
31
|
+
details.name,
|
|
32
|
+
details.type or "-",
|
|
33
|
+
str(details.port or "-"),
|
|
34
|
+
str(details.target_port or "-")
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
console.print(table)
|
|
38
|
+
log_info(f"Found {len(services)} service(s)")
|
|
39
|
+
|
|
40
|
+
def show_logs(
|
|
41
|
+
namespace: str,
|
|
42
|
+
pod_name: str,
|
|
43
|
+
container_name: str,
|
|
44
|
+
tail: int,
|
|
45
|
+
follow: bool,
|
|
46
|
+
previous: bool
|
|
47
|
+
) -> None:
|
|
48
|
+
kubernetes_manager = KubernetesManager()
|
|
49
|
+
try:
|
|
50
|
+
if not follow:
|
|
51
|
+
log_info(f"Fetching logs from pod '{pod_name}' (container: {container_name})...")
|
|
52
|
+
|
|
53
|
+
logs = kubernetes_manager.get_pod_logs(
|
|
54
|
+
namespace=namespace,
|
|
55
|
+
pod_name=pod_name,
|
|
56
|
+
container_name=container_name,
|
|
57
|
+
tail=tail,
|
|
58
|
+
follow=follow,
|
|
59
|
+
previous=previous
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
if follow:
|
|
63
|
+
try:
|
|
64
|
+
for line in logs:
|
|
65
|
+
log_info(line, end="")
|
|
66
|
+
except KeyboardInterrupt:
|
|
67
|
+
log_warning("Log streaming interrupted")
|
|
68
|
+
raise typer.Exit(0)
|
|
69
|
+
else:
|
|
70
|
+
if logs:
|
|
71
|
+
log_info(logs)
|
|
72
|
+
else:
|
|
73
|
+
log_warning(f"No logs available for pod '{pod_name}' container '{container_name}'")
|
|
74
|
+
|
|
75
|
+
except ApiException as e:
|
|
76
|
+
if e.status == 404:
|
|
77
|
+
log_error(f"Pod '{pod_name}' or container '{container_name}' not found")
|
|
78
|
+
elif e.status == 400:
|
|
79
|
+
log_error(f"Bad request: {e.reason}")
|
|
80
|
+
if "previous" in str(e.body).lower():
|
|
81
|
+
log_info("Note: 'previous' flag only works for stopped containers")
|
|
82
|
+
else:
|
|
83
|
+
log_error(f"Failed to get logs: {e.reason}")
|
|
84
|
+
if e.body:
|
|
85
|
+
log_error(f"Error details: {e.body}")
|
|
86
|
+
except Exception as e:
|
|
87
|
+
log_error(f"Failed to get logs: {e}")
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def show_service_logs(
|
|
91
|
+
service_name: str,
|
|
92
|
+
container: Optional[str] = None,
|
|
93
|
+
tail: int = 100,
|
|
94
|
+
follow: bool = False,
|
|
95
|
+
previous: bool = False
|
|
96
|
+
) -> None:
|
|
97
|
+
namespace = sft_config.k8s_namespace
|
|
98
|
+
kubernetes_manager = KubernetesManager()
|
|
99
|
+
|
|
100
|
+
if not service_name.startswith("sf-"):
|
|
101
|
+
log_warning(f"Service name '{service_name}' does not start with 'sf-'. Proceeding anyway...")
|
|
102
|
+
|
|
103
|
+
services = kubernetes_manager.get_services_in_namespace(namespace)
|
|
104
|
+
if service_name not in services:
|
|
105
|
+
log_error(f"Service '{service_name}' not found in namespace '{namespace}'")
|
|
106
|
+
log_info(f"Available services: {', '.join(services) if services else 'None'}")
|
|
107
|
+
raise typer.Exit(1)
|
|
108
|
+
|
|
109
|
+
pod_names = kubernetes_manager.get_pods_for_service(namespace, service_name)
|
|
110
|
+
if not pod_names:
|
|
111
|
+
log_error(f"No pods found for service '{service_name}' in namespace '{namespace}'")
|
|
112
|
+
raise typer.Exit(1)
|
|
113
|
+
|
|
114
|
+
log_info(f"Found {len(pod_names)} pod(s) for service '{service_name}'")
|
|
115
|
+
|
|
116
|
+
for pod_name in pod_names:
|
|
117
|
+
containers = kubernetes_manager.get_pod_containers(namespace, pod_name)
|
|
118
|
+
|
|
119
|
+
if not containers:
|
|
120
|
+
log_warning(f"No containers found in pod '{pod_name}'")
|
|
121
|
+
continue
|
|
122
|
+
|
|
123
|
+
if container and container not in containers:
|
|
124
|
+
log_error(f"Container '{container}' not found in pod '{pod_name}'")
|
|
125
|
+
log_info(f"Available containers: {', '.join(containers)}")
|
|
126
|
+
continue
|
|
127
|
+
|
|
128
|
+
target_containers = [container] if container else containers
|
|
129
|
+
|
|
130
|
+
for container_name in target_containers:
|
|
131
|
+
show_logs(namespace, pod_name, container_name, tail, follow, previous)
|
|
132
|
+
|
|
133
|
+
def delete_service(service_name: str, force: bool = False) -> None:
|
|
134
|
+
namespace = sft_config.k8s_namespace
|
|
135
|
+
|
|
136
|
+
if not service_name.startswith("sf-"):
|
|
137
|
+
log_error(f"Service name '{service_name}' does not start with 'sf-'")
|
|
138
|
+
raise typer.Exit(1)
|
|
139
|
+
|
|
140
|
+
kubernetes_manager = KubernetesManager()
|
|
141
|
+
|
|
142
|
+
services = kubernetes_manager.get_services_in_namespace(namespace)
|
|
143
|
+
if service_name not in services:
|
|
144
|
+
log_warning(f"Service '{service_name}' not found in namespace '{namespace}'")
|
|
145
|
+
log_info(f"Available services: {', '.join(services) if services else 'None'}")
|
|
146
|
+
|
|
147
|
+
log_info(f"Deleting service '{service_name}' from namespace '{namespace}'...")
|
|
148
|
+
kubernetes_manager.delete_service(namespace, service_name, force)
|
|
149
|
+
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import typer
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from service_forge.sft.util.logger import log_error, log_success
|
|
4
|
+
from service_forge.sft.file.sft_file_manager import sft_file_manager
|
|
5
|
+
from service_forge.sft.util.assert_util import assert_dir_exists, assert_file_exists
|
|
6
|
+
from service_forge.sft.config.sf_metadata import load_metadata
|
|
7
|
+
|
|
8
|
+
def upload_service(project_path: str) -> None:
|
|
9
|
+
project_dir = Path(project_path).resolve()
|
|
10
|
+
assert_dir_exists(project_dir)
|
|
11
|
+
|
|
12
|
+
metadata_path = project_dir / "sf-meta.yaml"
|
|
13
|
+
assert_file_exists(metadata_path)
|
|
14
|
+
|
|
15
|
+
try:
|
|
16
|
+
meta_data = load_metadata(metadata_path)
|
|
17
|
+
except Exception as e:
|
|
18
|
+
log_error(f"Failed to read sf-meta.yaml: {e}")
|
|
19
|
+
raise typer.Exit(1)
|
|
20
|
+
|
|
21
|
+
try:
|
|
22
|
+
tar_file = sft_file_manager.create_tar(project_dir, meta_data.name, meta_data.version)
|
|
23
|
+
except Exception as e:
|
|
24
|
+
log_error(f"Failed to create tar file: {e}")
|
|
25
|
+
raise typer.Exit(1)
|
|
26
|
+
|
|
27
|
+
log_success(f"Packaging successful: {tar_file}")
|
|
28
|
+
|
|
29
|
+
# upload to the service
|
|
30
|
+
try:
|
|
31
|
+
sft_file_manager.upload_tar(tar_file)
|
|
32
|
+
except Exception as e:
|
|
33
|
+
log_error(f"Failed to upload tar file: {e}")
|
|
34
|
+
raise typer.Exit(1)
|
|
35
|
+
|
|
36
|
+
log_success(f"Upload successful: {tar_file}")
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
from omegaconf import OmegaConf
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from service_forge.sft.util.logger import log_info, log_error
|
|
4
|
+
from service_forge.sft.config.injector_default_files import *
|
|
5
|
+
from service_forge.sft.config.sf_metadata import load_metadata
|
|
6
|
+
from service_forge.sft.config.sft_config import sft_config
|
|
7
|
+
from service_forge.service_config import ServiceConfig
|
|
8
|
+
from service_forge.sft.util.name_util import get_service_name
|
|
9
|
+
|
|
10
|
+
class Injector:
|
|
11
|
+
def __init__(self, project_dir: Path):
|
|
12
|
+
self.project_dir = project_dir
|
|
13
|
+
self.deployment_yaml_path = project_dir / "deployment.yaml"
|
|
14
|
+
self.metadata_path = project_dir / "sf-meta.yaml"
|
|
15
|
+
self.ingress_yaml_path = project_dir / "ingress.yaml"
|
|
16
|
+
self.dockerfile_path = project_dir / "Dockerfile"
|
|
17
|
+
self.metadata = load_metadata(self.metadata_path)
|
|
18
|
+
self.name = self.metadata.name
|
|
19
|
+
self.version = self.metadata.version
|
|
20
|
+
self.namespace = sft_config.k8s_namespace
|
|
21
|
+
|
|
22
|
+
@property
|
|
23
|
+
def service_name(self) -> str:
|
|
24
|
+
return get_service_name(self.name, self.version)
|
|
25
|
+
|
|
26
|
+
def inject_deployment(self) -> None:
|
|
27
|
+
envs = {
|
|
28
|
+
"DEEPSEEK_API_KEY": sft_config.deepseek_api_key,
|
|
29
|
+
"DEEPSEEK_BASE_URL": sft_config.deepseek_base_url,
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
for env in self.metadata.env:
|
|
33
|
+
envs[env['name']] = env['value']
|
|
34
|
+
|
|
35
|
+
env_str = ""
|
|
36
|
+
for key, value in envs.items():
|
|
37
|
+
env_str += f" - name: {key}\n value: {value}\n"
|
|
38
|
+
|
|
39
|
+
deployment_yaml = DEFAULT_DEPLOYMENT_YAML.format(
|
|
40
|
+
service_name=self.service_name,
|
|
41
|
+
name=self.name,
|
|
42
|
+
version=self.version,
|
|
43
|
+
namespace=self.namespace,
|
|
44
|
+
env=env_str,
|
|
45
|
+
)
|
|
46
|
+
with open(self.deployment_yaml_path, "w") as f:
|
|
47
|
+
f.write(deployment_yaml)
|
|
48
|
+
print("deployment_yaml_path: ", self.deployment_yaml_path)
|
|
49
|
+
|
|
50
|
+
def inject_service_config(self) -> None:
|
|
51
|
+
service_config_path = self.project_dir / Path(self.metadata.service_config)
|
|
52
|
+
config = ServiceConfig.from_dict(OmegaConf.to_object(OmegaConf.load(self.project_dir / self.metadata.service_config)))
|
|
53
|
+
|
|
54
|
+
config.http_port = sft_config.inject_http_port
|
|
55
|
+
config.kafka_host = sft_config.inject_kafka_host
|
|
56
|
+
config.kafka_port = sft_config.inject_kafka_port
|
|
57
|
+
for database in config.databases:
|
|
58
|
+
database.postgres_host = sft_config.inject_postgres_host
|
|
59
|
+
database.postgres_port = sft_config.inject_postgres_port
|
|
60
|
+
database.postgres_user = sft_config.inject_postgres_user
|
|
61
|
+
database.postgres_password = sft_config.inject_postgres_password
|
|
62
|
+
database.postgres_db = self.service_name
|
|
63
|
+
|
|
64
|
+
with open(service_config_path, "w") as f:
|
|
65
|
+
f.write(OmegaConf.to_yaml(config.to_dict()))
|
|
66
|
+
|
|
67
|
+
def inject_ingress(self) -> None:
|
|
68
|
+
ingress_yaml = DEFAULT_TRAEFIK_INGRESS_YAML.format(
|
|
69
|
+
name=self.name,
|
|
70
|
+
version=self.version.replace(".", "-"),
|
|
71
|
+
namespace=self.namespace,
|
|
72
|
+
)
|
|
73
|
+
with open(self.ingress_yaml_path, "w") as f:
|
|
74
|
+
f.write(ingress_yaml)
|
|
75
|
+
print("ingress_yaml_path: ", self.ingress_yaml_path)
|
|
76
|
+
|
|
77
|
+
def inject_dockerfile(self) -> None:
|
|
78
|
+
dockerfile = DEFAULT_DOCKERFILE
|
|
79
|
+
with open(self.dockerfile_path, "w") as f:
|
|
80
|
+
f.write(dockerfile)
|
|
81
|
+
print("dockerfile_path: ", self.dockerfile_path)
|
|
82
|
+
|
|
83
|
+
def inject(self) -> None:
|
|
84
|
+
self.inject_deployment()
|
|
85
|
+
self.inject_service_config()
|
|
86
|
+
self.inject_ingress()
|
|
87
|
+
self.inject_dockerfile()
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
DEFAULT_DEPLOYMENT_YAML = """
|
|
2
|
+
apiVersion: apps/v1
|
|
3
|
+
kind: Deployment
|
|
4
|
+
metadata:
|
|
5
|
+
name: {service_name}
|
|
6
|
+
namespace: {namespace}
|
|
7
|
+
spec:
|
|
8
|
+
replicas: 1
|
|
9
|
+
selector:
|
|
10
|
+
matchLabels:
|
|
11
|
+
app: {service_name}
|
|
12
|
+
template:
|
|
13
|
+
metadata:
|
|
14
|
+
labels:
|
|
15
|
+
app: {service_name}
|
|
16
|
+
spec:
|
|
17
|
+
imagePullSecrets:
|
|
18
|
+
- name: aliyun-regcred
|
|
19
|
+
containers:
|
|
20
|
+
- name: {service_name}
|
|
21
|
+
image: crpi-cev6qq28wwgwwj0y.cn-beijing.personal.cr.aliyuncs.com/nexthci/sf-{name}:{version}
|
|
22
|
+
imagePullPolicy: Always
|
|
23
|
+
ports:
|
|
24
|
+
- containerPort: 8000
|
|
25
|
+
env:
|
|
26
|
+
{env}
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
apiVersion: v1
|
|
30
|
+
kind: Service
|
|
31
|
+
metadata:
|
|
32
|
+
name: {service_name}
|
|
33
|
+
namespace: {namespace}
|
|
34
|
+
spec:
|
|
35
|
+
type: ClusterIP
|
|
36
|
+
selector:
|
|
37
|
+
app: {service_name}
|
|
38
|
+
ports:
|
|
39
|
+
- protocol: TCP
|
|
40
|
+
port: 80
|
|
41
|
+
targetPort: 8000
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
DEFAULT_TRAEFIK_INGRESS_YAML = """
|
|
45
|
+
apiVersion: traefik.io/v1alpha1
|
|
46
|
+
kind: Middleware
|
|
47
|
+
metadata:
|
|
48
|
+
name: strip-prefix-sf-{name}-{version}v
|
|
49
|
+
namespace: {namespace}
|
|
50
|
+
spec:
|
|
51
|
+
stripPrefix:
|
|
52
|
+
prefixes:
|
|
53
|
+
- /api/v1/{name}-{version}
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
apiVersion: traefik.io/v1alpha1
|
|
58
|
+
kind: IngressRoute
|
|
59
|
+
metadata:
|
|
60
|
+
name: sf-{name}-{version}v
|
|
61
|
+
namespace: {namespace}
|
|
62
|
+
spec:
|
|
63
|
+
entryPoints:
|
|
64
|
+
- web
|
|
65
|
+
routes:
|
|
66
|
+
- match: PathPrefix(`/api/v1/{name}-{version}`)
|
|
67
|
+
kind: Rule
|
|
68
|
+
services:
|
|
69
|
+
- name: sf-{name}-{version}v
|
|
70
|
+
namespace: {namespace}
|
|
71
|
+
port: 80
|
|
72
|
+
middlewares:
|
|
73
|
+
- name: strip-prefix-sf-{name}-{version}v
|
|
74
|
+
namespace: {namespace}
|
|
75
|
+
- name: jwt-auth
|
|
76
|
+
namespace: {namespace}
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
DEFAULT_DOCKERFILE = """
|
|
80
|
+
FROM crpi-cev6qq28wwgwwj0y.cn-beijing.personal.cr.aliyuncs.com/nexthci/service-forge:latest
|
|
81
|
+
|
|
82
|
+
WORKDIR /app
|
|
83
|
+
|
|
84
|
+
COPY . ./service
|
|
85
|
+
|
|
86
|
+
WORKDIR /app
|
|
87
|
+
RUN uv sync
|
|
88
|
+
|
|
89
|
+
ENV PYTHONPATH=/app/service:/app:/app/src
|
|
90
|
+
ENV PATH="/app/.venv/bin:$PATH"
|
|
91
|
+
|
|
92
|
+
WORKDIR /app/service
|
|
93
|
+
|
|
94
|
+
RUN chmod +x start.sh
|
|
95
|
+
|
|
96
|
+
CMD ["./start.sh"]
|
|
97
|
+
"""
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
from omegaconf import OmegaConf
|
|
2
|
+
|
|
3
|
+
class SfMetadata:
|
|
4
|
+
def __init__(
|
|
5
|
+
self,
|
|
6
|
+
name: str,
|
|
7
|
+
version: str,
|
|
8
|
+
description: str,
|
|
9
|
+
service_config: str,
|
|
10
|
+
config_only: bool,
|
|
11
|
+
env: list[dict],
|
|
12
|
+
) -> None:
|
|
13
|
+
self.name = name
|
|
14
|
+
self.version = version
|
|
15
|
+
self.description = description
|
|
16
|
+
self.service_config = service_config
|
|
17
|
+
self.config_only = config_only
|
|
18
|
+
self.env = env
|
|
19
|
+
|
|
20
|
+
def load_metadata(path: str) -> SfMetadata:
|
|
21
|
+
with open(path, 'r') as file:
|
|
22
|
+
data = OmegaConf.load(file)
|
|
23
|
+
return SfMetadata(
|
|
24
|
+
name=data.get('name'),
|
|
25
|
+
version=data.get('version'),
|
|
26
|
+
description=data.get('description'),
|
|
27
|
+
service_config=data.get('service_config'),
|
|
28
|
+
config_only=data.get('config_only'),
|
|
29
|
+
env=data.get('env', []),
|
|
30
|
+
)
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import inspect
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Optional
|
|
6
|
+
from omegaconf import OmegaConf
|
|
7
|
+
|
|
8
|
+
class SftConfig:
|
|
9
|
+
CONFIG_ROOT = Path.home() / ".sft"
|
|
10
|
+
|
|
11
|
+
# Configuration descriptions mapping
|
|
12
|
+
CONFIG_DESCRIPTIONS = {
|
|
13
|
+
"sft_file_root": "SFT file storage root directory",
|
|
14
|
+
"service_center_address": "Service center address",
|
|
15
|
+
"k8s_namespace": "Kubernetes namespace",
|
|
16
|
+
"registry_address": "Registry address",
|
|
17
|
+
"inject_http_port": "HTTP port for services",
|
|
18
|
+
"inject_kafka_host": "Kafka host for services",
|
|
19
|
+
"inject_kafka_port": "Kafka port for services",
|
|
20
|
+
"inject_postgres_host": "Postgres host for services",
|
|
21
|
+
"inject_postgres_port": "Postgres port for services",
|
|
22
|
+
"inject_postgres_user": "Postgres user for services",
|
|
23
|
+
"inject_postgres_password": "Postgres password for services",
|
|
24
|
+
"deepseek_api_key": "DeepSeek API key",
|
|
25
|
+
"deepseek_base_url": "DeepSeek base URL",
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
def __init__(
|
|
29
|
+
self,
|
|
30
|
+
sft_file_root: str = "/tmp/sft",
|
|
31
|
+
service_center_address: str = "http://vps.shiweinan.com:37919/service_center",
|
|
32
|
+
k8s_namespace: str = "secondbrain",
|
|
33
|
+
registry_address: str = "crpi-cev6qq28wwgwwj0y.cn-beijing.personal.cr.aliyuncs.com/nexthci",
|
|
34
|
+
inject_http_port: int = 8000,
|
|
35
|
+
inject_kafka_host: str = "localhost",
|
|
36
|
+
inject_kafka_port: int = 9092,
|
|
37
|
+
inject_postgres_host: str = "second-brain-postgres-postgresql",
|
|
38
|
+
inject_postgres_port: int = 5432,
|
|
39
|
+
inject_postgres_user: str = "postgres",
|
|
40
|
+
inject_postgres_password: str = "gnBGWg7aL4",
|
|
41
|
+
deepseek_api_key: str = "82c9df22-f6ed-411e-90d7-c5255376b7ca",
|
|
42
|
+
deepseek_base_url: str = "https://ark.cn-beijing.volces.com/api/v3",
|
|
43
|
+
):
|
|
44
|
+
self.sft_file_root = sft_file_root
|
|
45
|
+
self.service_center_address = service_center_address
|
|
46
|
+
self.k8s_namespace = k8s_namespace
|
|
47
|
+
self.registry_address = registry_address
|
|
48
|
+
self.inject_http_port = inject_http_port
|
|
49
|
+
self.inject_kafka_host = inject_kafka_host
|
|
50
|
+
self.inject_kafka_port = inject_kafka_port
|
|
51
|
+
self.inject_postgres_host = inject_postgres_host
|
|
52
|
+
self.inject_postgres_port = inject_postgres_port
|
|
53
|
+
self.inject_postgres_user = inject_postgres_user
|
|
54
|
+
self.inject_postgres_password = inject_postgres_password
|
|
55
|
+
self.deepseek_api_key = deepseek_api_key
|
|
56
|
+
self.deepseek_base_url = deepseek_base_url
|
|
57
|
+
|
|
58
|
+
@property
|
|
59
|
+
def server_url(self) -> str:
|
|
60
|
+
return self.service_center_address
|
|
61
|
+
|
|
62
|
+
@property
|
|
63
|
+
def upload_timeout(self) -> int:
|
|
64
|
+
return 300 # 5 minutes default timeout
|
|
65
|
+
|
|
66
|
+
@classmethod
|
|
67
|
+
def get_config_keys(cls) -> list[str]:
|
|
68
|
+
sig = inspect.signature(cls.__init__)
|
|
69
|
+
return [param for param in sig.parameters.keys() if param != 'self']
|
|
70
|
+
|
|
71
|
+
@property
|
|
72
|
+
def config_file_path(self) -> Path:
|
|
73
|
+
return self.CONFIG_ROOT / "config.yaml"
|
|
74
|
+
|
|
75
|
+
def ensure_config_dir(self) -> None:
|
|
76
|
+
self.CONFIG_ROOT.mkdir(parents=True, exist_ok=True)
|
|
77
|
+
|
|
78
|
+
def to_dict(self) -> dict:
|
|
79
|
+
config_keys = self.get_config_keys()
|
|
80
|
+
return {key: getattr(self, key) for key in config_keys}
|
|
81
|
+
|
|
82
|
+
def from_dict(self, data: dict) -> None:
|
|
83
|
+
config_keys = self.get_config_keys()
|
|
84
|
+
for key in config_keys:
|
|
85
|
+
if key in data:
|
|
86
|
+
setattr(self, key, data[key])
|
|
87
|
+
|
|
88
|
+
def save(self) -> None:
|
|
89
|
+
self.ensure_config_dir()
|
|
90
|
+
config_dict = self.to_dict()
|
|
91
|
+
OmegaConf.save(config_dict, self.config_file_path)
|
|
92
|
+
|
|
93
|
+
def get(self, key: str, default: Optional[str] = None) -> Optional[str]:
|
|
94
|
+
return getattr(self, key, default)
|
|
95
|
+
|
|
96
|
+
def set(self, key: str, value: str) -> None:
|
|
97
|
+
if key in ["config_root"]:
|
|
98
|
+
raise ValueError(f"{key} is read-only")
|
|
99
|
+
if hasattr(self, key):
|
|
100
|
+
setattr(self, key, value)
|
|
101
|
+
else:
|
|
102
|
+
raise ValueError(f"Unknown config key: {key}")
|
|
103
|
+
|
|
104
|
+
def update(self, updates: dict) -> None:
|
|
105
|
+
for key, value in updates.items():
|
|
106
|
+
self.set(key, value)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def load_config() -> SftConfig:
|
|
110
|
+
# config = SftConfig()
|
|
111
|
+
config = SftConfig()
|
|
112
|
+
config_file = config.config_file_path
|
|
113
|
+
|
|
114
|
+
if config_file.exists():
|
|
115
|
+
try:
|
|
116
|
+
data = OmegaConf.load(config_file)
|
|
117
|
+
config = SftConfig(**OmegaConf.to_container(data, resolve=True))
|
|
118
|
+
except Exception as e:
|
|
119
|
+
...
|
|
120
|
+
|
|
121
|
+
config.save()
|
|
122
|
+
|
|
123
|
+
return config
|
|
124
|
+
|
|
125
|
+
sft_config = load_config()
|
|
File without changes
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import fnmatch
|
|
2
|
+
import re
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import List
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class IgnorePattern:
|
|
8
|
+
def __init__(self, patterns: List[str], base_path: Path):
|
|
9
|
+
self.base_path = base_path.resolve()
|
|
10
|
+
self.patterns: List[tuple[str, bool]] = []
|
|
11
|
+
|
|
12
|
+
for pattern in patterns:
|
|
13
|
+
pattern = pattern.strip()
|
|
14
|
+
if not pattern or pattern.startswith('#'):
|
|
15
|
+
continue
|
|
16
|
+
|
|
17
|
+
is_negation = pattern.startswith('!')
|
|
18
|
+
if is_negation:
|
|
19
|
+
pattern = pattern[1:].strip()
|
|
20
|
+
|
|
21
|
+
pattern = pattern.replace('\\', '/')
|
|
22
|
+
|
|
23
|
+
if pattern.startswith('/'):
|
|
24
|
+
pattern = pattern[1:]
|
|
25
|
+
|
|
26
|
+
self.patterns.append((pattern, is_negation))
|
|
27
|
+
|
|
28
|
+
def should_ignore(self, file_path: Path) -> bool:
|
|
29
|
+
file_path = file_path.resolve()
|
|
30
|
+
|
|
31
|
+
try:
|
|
32
|
+
relative_path = file_path.relative_to(self.base_path)
|
|
33
|
+
except ValueError:
|
|
34
|
+
return False
|
|
35
|
+
|
|
36
|
+
path_str = str(relative_path).replace('\\', '/')
|
|
37
|
+
|
|
38
|
+
ignored = False
|
|
39
|
+
for pattern, is_negation in self.patterns:
|
|
40
|
+
if self._match_pattern(pattern, path_str, file_path):
|
|
41
|
+
if is_negation:
|
|
42
|
+
ignored = False
|
|
43
|
+
else:
|
|
44
|
+
ignored = True
|
|
45
|
+
|
|
46
|
+
return ignored
|
|
47
|
+
|
|
48
|
+
def _match_pattern(self, pattern: str, path_str: str, file_path: Path) -> bool:
|
|
49
|
+
if pattern.endswith('/'):
|
|
50
|
+
pattern = pattern[:-1]
|
|
51
|
+
if not file_path.is_dir():
|
|
52
|
+
return False
|
|
53
|
+
|
|
54
|
+
if '**' in pattern:
|
|
55
|
+
regex_pattern = pattern.replace('**', '.*')
|
|
56
|
+
regex_pattern = fnmatch.translate(regex_pattern)
|
|
57
|
+
return bool(re.match(regex_pattern, path_str))
|
|
58
|
+
|
|
59
|
+
path_parts = path_str.split('/')
|
|
60
|
+
|
|
61
|
+
if '/' in pattern:
|
|
62
|
+
return fnmatch.fnmatch(path_str, pattern)
|
|
63
|
+
|
|
64
|
+
return any(fnmatch.fnmatch(part, pattern) for part in path_parts)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def load_ignore_patterns(project_path: Path) -> IgnorePattern:
|
|
68
|
+
project_path = project_path.resolve()
|
|
69
|
+
ignore_file = project_path / '.sftignore'
|
|
70
|
+
|
|
71
|
+
if not ignore_file.exists():
|
|
72
|
+
return IgnorePattern([], project_path)
|
|
73
|
+
|
|
74
|
+
try:
|
|
75
|
+
with open(ignore_file, 'r', encoding='utf-8') as f:
|
|
76
|
+
patterns = f.readlines()
|
|
77
|
+
return IgnorePattern(patterns, project_path)
|
|
78
|
+
except Exception:
|
|
79
|
+
return IgnorePattern([], project_path)
|
|
80
|
+
|