dataops-sdk 0.3.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.
Potentially problematic release.
This version of dataops-sdk might be problematic. Click here for more details.
- dataops_sdk/__init__.py +84 -0
- dataops_sdk/constants.py +6 -0
- dataops_sdk/exceptions.py +31 -0
- dataops_sdk/runner/__init__.py +46 -0
- dataops_sdk/runner/datasource_factory.py +83 -0
- dataops_sdk/runner/schemas/nacos/__init__.py +5 -0
- dataops_sdk/runner/schemas/nacos/config.py +40 -0
- dataops_sdk/runner/schemas/nacos/constants.py +235 -0
- dataops_sdk/runner/schemas/nacos/instance.py +103 -0
- dataops_sdk/runner/schemas/nacos/nacos_exception.py +44 -0
- dataops_sdk/runner/schemas/nacos/naming_param.py +62 -0
- dataops_sdk/runner/schemas/nacos/naming_request.py +84 -0
- dataops_sdk/runner/schemas/nacos/naming_response.py +50 -0
- dataops_sdk/runner/schemas/nacos/preserved_metadata_key.py +10 -0
- dataops_sdk/runner/schemas/nacos/service.py +119 -0
- dataops_sdk/runner/utils/__init__.py +42 -0
- dataops_sdk/runner/utils/common.py +209 -0
- dataops_sdk/runner/utils/dbConn_base.py +256 -0
- dataops_sdk/runner/utils/dbConn_sync.py +485 -0
- dataops_sdk/runner/utils/duckdb_manager.py +203 -0
- dataops_sdk/runner/utils/file_transfer_base.py +307 -0
- dataops_sdk/runner/utils/ftp_file_transfer.py +242 -0
- dataops_sdk/runner/utils/log.py +50 -0
- dataops_sdk/runner/utils/nacos.py +96 -0
- dataops_sdk/runner/utils/neo4j_sync.py +236 -0
- dataops_sdk/runner/utils/queue_base.py +224 -0
- dataops_sdk/runner/utils/queue_kafka.py +150 -0
- dataops_sdk/runner/utils/sftp_file_transfer.py +243 -0
- dataops_sdk/schemas/__init__.py +32 -0
- dataops_sdk/schemas/alarm.py +38 -0
- dataops_sdk/schemas/api.py +37 -0
- dataops_sdk/schemas/data_table.py +437 -0
- dataops_sdk/schemas/datasource.py +26 -0
- dataops_sdk/schemas/dict_.py +13 -0
- dataops_sdk/schemas/enums.py +120 -0
- dataops_sdk/schemas/group.py +40 -0
- dataops_sdk/schemas/hazard.py +151 -0
- dataops_sdk/schemas/meta.py +175 -0
- dataops_sdk/schemas/query.py +175 -0
- dataops_sdk/schemas/report.py +22 -0
- dataops_sdk/schemas/skill.py +70 -0
- dataops_sdk/schemas/table.py +65 -0
- dataops_sdk/schemas/user.py +52 -0
- dataops_sdk/schemas/widget.py +651 -0
- dataops_sdk/schemas/workflow.py +63 -0
- dataops_sdk/services/__init__.py +110 -0
- dataops_sdk/services/alarm.py +324 -0
- dataops_sdk/services/api.py +28 -0
- dataops_sdk/services/auth.py +120 -0
- dataops_sdk/services/data_table.py +485 -0
- dataops_sdk/services/dataset.py +127 -0
- dataops_sdk/services/datasource.py +57 -0
- dataops_sdk/services/dict_.py +69 -0
- dataops_sdk/services/hazard.py +260 -0
- dataops_sdk/services/meta.py +523 -0
- dataops_sdk/services/metrics.py +42 -0
- dataops_sdk/services/ref.py +66 -0
- dataops_sdk/services/report.py +36 -0
- dataops_sdk/services/skill.py +276 -0
- dataops_sdk/services/table.py +340 -0
- dataops_sdk/services/widget.py +143 -0
- dataops_sdk/services/workflow.py +102 -0
- dataops_sdk/session.py +292 -0
- dataops_sdk/tiptap/__init__.py +30 -0
- dataops_sdk/tiptap/builder.py +75 -0
- dataops_sdk/tiptap/constants.py +133 -0
- dataops_sdk/tiptap/markdown_parser.py +294 -0
- dataops_sdk/tiptap/models.py +43 -0
- dataops_sdk/tiptap/parse_utils.py +889 -0
- dataops_sdk/tiptap/render_utils.py +846 -0
- dataops_sdk/tiptap/renderers.py +51 -0
- dataops_sdk/tiptap/tool.py +629 -0
- dataops_sdk/utils/__init__.py +15 -0
- dataops_sdk/utils/common.py +211 -0
- dataops_sdk/utils/utils.py +120 -0
- dataops_sdk-0.3.0.dist-info/METADATA +25 -0
- dataops_sdk-0.3.0.dist-info/RECORD +79 -0
- dataops_sdk-0.3.0.dist-info/WHEEL +5 -0
- dataops_sdk-0.3.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
from collections.abc import Callable
|
|
2
|
+
from typing import Annotated
|
|
3
|
+
|
|
4
|
+
from pydantic import BaseModel, Field
|
|
5
|
+
|
|
6
|
+
from .constants import Constants
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class RegisterInstanceParam(BaseModel):
|
|
10
|
+
ip: Annotated[str, Field(description="实例IP地址")]
|
|
11
|
+
port: Annotated[int, Field(description="实例端口号")]
|
|
12
|
+
weight: Annotated[float, Field(description="实例权重")] = 1.0
|
|
13
|
+
enabled: Annotated[bool, Field(description="实例是否启用")] = True
|
|
14
|
+
healthy: Annotated[bool, Field(description="实例健康状态")] = True
|
|
15
|
+
metadata: Annotated[dict[str, str], Field(description="实例元数据")] = {}
|
|
16
|
+
cluster_name: Annotated[str, Field(description="实例所在集群名称")] = ""
|
|
17
|
+
service_name: Annotated[str, Field(description="实例所在服务名称")]
|
|
18
|
+
group_name: Annotated[str, Field(description="实例所在分组")] = Constants.DEFAULT_GROUP
|
|
19
|
+
ephemeral: Annotated[bool, Field(description="实例是否临时实例")] = True
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class BatchRegisterInstanceParam(BaseModel):
|
|
23
|
+
service_name: Annotated[str, Field(description="服务名称")]
|
|
24
|
+
group_name: Annotated[str, Field(description="分组名称")] = Constants.DEFAULT_GROUP
|
|
25
|
+
instances: Annotated[list[RegisterInstanceParam], Field(description="实例列表")] = []
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class DeregisterInstanceParam(BaseModel):
|
|
29
|
+
ip: Annotated[str, Field(description="实例IP地址")]
|
|
30
|
+
port: Annotated[int, Field(description="实例端口号")]
|
|
31
|
+
cluster_name: Annotated[str, Field(description="实例所在集群名称")] = ""
|
|
32
|
+
service_name: Annotated[str, Field(description="实例所在服务名称")]
|
|
33
|
+
group_name: Annotated[str, Field(description="实例所在分组")] = Constants.DEFAULT_GROUP
|
|
34
|
+
ephemeral: Annotated[bool, Field(description="实例是否临时实例")] = True
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class ListInstanceParam(BaseModel):
|
|
38
|
+
service_name: Annotated[str, Field(description="服务名称")]
|
|
39
|
+
group_name: Annotated[str, Field(description="分组名称")] = Constants.DEFAULT_GROUP
|
|
40
|
+
clusters: Annotated[list[str], Field(description="集群列表")] = []
|
|
41
|
+
subscribe: Annotated[bool, Field(description="是否订阅")] = True
|
|
42
|
+
healthy_only: Annotated[bool | None, Field(description="仅返回健康实例")] = None
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class SubscribeServiceParam(BaseModel):
|
|
46
|
+
service_name: Annotated[str, Field(description="服务名称")]
|
|
47
|
+
group_name: Annotated[str, Field(description="分组名称")] = Constants.DEFAULT_GROUP
|
|
48
|
+
clusters: Annotated[list[str], Field(description="集群列表")] = []
|
|
49
|
+
subscribe_callback: Annotated[Callable | None, Field(description="订阅回调函数")] = None
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class GetServiceParam(BaseModel):
|
|
53
|
+
service_name: Annotated[str, Field(description="服务名称")]
|
|
54
|
+
group_name: Annotated[str, Field(description="分组名称")] = Constants.DEFAULT_GROUP
|
|
55
|
+
clusters: Annotated[list[str], Field(description="集群列表")] = []
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class ListServiceParam(BaseModel):
|
|
59
|
+
namespace_id: Annotated[str, Field(description="命名空间ID")] = Constants.DEFAULT_NAMESPACE_ID
|
|
60
|
+
group_name: Annotated[str, Field(description="分组名称")] = Constants.DEFAULT_GROUP
|
|
61
|
+
page_no: Annotated[int, Field(description="页码")] = 1
|
|
62
|
+
page_size: Annotated[int, Field(description="每页数量")] = 10
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from abc import ABC
|
|
4
|
+
from typing import Annotated
|
|
5
|
+
|
|
6
|
+
from pydantic import Field
|
|
7
|
+
from v2.nacos.naming.model.instance import Instance
|
|
8
|
+
from v2.nacos.transport.model.rpc_request import Request
|
|
9
|
+
|
|
10
|
+
from .service import Service
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class AbstractNamingRequest(Request, ABC):
|
|
14
|
+
namespace: Annotated[str | None, Field(description="命名空间")] = ""
|
|
15
|
+
serviceName: Annotated[str | None, Field(description="服务名称")] = ""
|
|
16
|
+
groupName: Annotated[str | None, Field(description="分组名称")] = ""
|
|
17
|
+
|
|
18
|
+
def get_module(self):
|
|
19
|
+
return "naming"
|
|
20
|
+
|
|
21
|
+
def get_request_type(self) -> str:
|
|
22
|
+
"""
|
|
23
|
+
提供一个默认实现或抛出NotImplementedError,明确指示子类需要覆盖此方法。
|
|
24
|
+
"""
|
|
25
|
+
raise NotImplementedError("Subclasses should implement this method.")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
NOTIFY_SUBSCRIBER_REQUEST_TYPE = "NotifySubscriberRequest"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class ServiceQueryRequest(AbstractNamingRequest):
|
|
32
|
+
cluster: Annotated[str | None, Field(description="集群名称")]
|
|
33
|
+
healthOnly: Annotated[bool | None, Field(description="仅健康实例")]
|
|
34
|
+
udpPort: Annotated[int | None, Field(description="UDP端口")] = None
|
|
35
|
+
|
|
36
|
+
def get_request_type(self) -> str:
|
|
37
|
+
return "ServiceQueryRequest"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class InstanceRequest(AbstractNamingRequest):
|
|
41
|
+
type: Annotated[str | None, Field(description="请求类型")]
|
|
42
|
+
instance: Annotated[Instance | None, Field(description="实例")]
|
|
43
|
+
|
|
44
|
+
def get_request_type(self) -> str:
|
|
45
|
+
return "InstanceRequest"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class PersistentInstanceRequest(AbstractNamingRequest):
|
|
49
|
+
type: Annotated[str | None, Field(description="请求类型")]
|
|
50
|
+
instance: Annotated[Instance | None, Field(description="实例")]
|
|
51
|
+
|
|
52
|
+
def get_request_type(self) -> str:
|
|
53
|
+
return "PersistentInstanceRequest"
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class BatchInstanceRequest(AbstractNamingRequest):
|
|
57
|
+
type: Annotated[str | None, Field(description="请求类型")]
|
|
58
|
+
instances: Annotated[list[Instance] | None, Field(description="实例列表")]
|
|
59
|
+
|
|
60
|
+
def get_request_type(self) -> str:
|
|
61
|
+
return "BatchInstanceRequest"
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class NotifySubscriberRequest(AbstractNamingRequest):
|
|
65
|
+
serviceInfo: Annotated[Service | None, Field(description="服务信息")]
|
|
66
|
+
|
|
67
|
+
def get_request_type(self) -> str:
|
|
68
|
+
return "NotifySubscriberRequest"
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class ServiceListRequest(AbstractNamingRequest):
|
|
72
|
+
pageNo: Annotated[int | None, Field(description="页码")]
|
|
73
|
+
pageSize: Annotated[int | None, Field(description="每页数量")]
|
|
74
|
+
|
|
75
|
+
def get_request_type(self) -> str:
|
|
76
|
+
return "ServiceListRequest"
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class SubscribeServiceRequest(AbstractNamingRequest):
|
|
80
|
+
subscribe: Annotated[bool | None, Field(description="是否订阅")]
|
|
81
|
+
clusters: Annotated[str | None, Field(description="集群名称")]
|
|
82
|
+
|
|
83
|
+
def get_request_type(self) -> str:
|
|
84
|
+
return "SubscribeServiceRequest"
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Annotated
|
|
4
|
+
|
|
5
|
+
from pydantic import Field
|
|
6
|
+
from v2.nacos.naming.model.service import Service
|
|
7
|
+
from v2.nacos.transport.model.rpc_response import Response
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class NotifySubscriberResponse(Response):
|
|
11
|
+
def get_response_type(self) -> str:
|
|
12
|
+
return "NotifySubscriberResponse"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class QueryServiceResponse(Response):
|
|
16
|
+
serviceInfo: Annotated[Service | None, Field(description="服务信息")] = None
|
|
17
|
+
|
|
18
|
+
def get_service_info(self) -> Service:
|
|
19
|
+
return self.serviceInfo
|
|
20
|
+
|
|
21
|
+
def get_response_type(self) -> str:
|
|
22
|
+
return "QueryServiceResponse"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class SubscribeServiceResponse(Response):
|
|
26
|
+
serviceInfo: Annotated[Service | None, Field(description="服务信息")] = None
|
|
27
|
+
|
|
28
|
+
def get_response_type(self) -> str:
|
|
29
|
+
return "SubscribeServiceResponse"
|
|
30
|
+
|
|
31
|
+
def get_service_info(self) -> Service:
|
|
32
|
+
return self.serviceInfo
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class InstanceResponse(Response):
|
|
36
|
+
def get_response_type(self) -> str:
|
|
37
|
+
return "InstanceResponse"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class BatchInstanceResponse(Response):
|
|
41
|
+
def get_response_type(self) -> str:
|
|
42
|
+
return "BatchInstanceResponse"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class ServiceListResponse(Response):
|
|
46
|
+
count: Annotated[int, Field(description="服务数量")]
|
|
47
|
+
serviceNames: Annotated[list[str], Field(description="服务名称列表")]
|
|
48
|
+
|
|
49
|
+
def get_response_type(self) -> str:
|
|
50
|
+
return "ServiceListResponse"
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
class PreservedMetadataKeys:
|
|
2
|
+
REGISTER_SOURCE = "preserved.register.source"
|
|
3
|
+
|
|
4
|
+
HEART_BEAT_TIMEOUT = "preserved.heart.beat.timeout"
|
|
5
|
+
|
|
6
|
+
IP_DELETE_TIMEOUT = "preserved.ip.delete.timeout"
|
|
7
|
+
|
|
8
|
+
HEART_BEAT_INTERVAL = "preserved.heart.beat.interval"
|
|
9
|
+
|
|
10
|
+
INSTANCE_ID_GENERATOR = "preserved.instance.id.generator"
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import time
|
|
4
|
+
import urllib.parse
|
|
5
|
+
from typing import Annotated
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel, Field
|
|
8
|
+
|
|
9
|
+
from .constants import Constants
|
|
10
|
+
from .instance import Instance
|
|
11
|
+
from .nacos_exception import NacosException
|
|
12
|
+
|
|
13
|
+
EMPTY = ""
|
|
14
|
+
|
|
15
|
+
ALL_IPS = "000--00-ALL_IPS--00--000"
|
|
16
|
+
|
|
17
|
+
SPLITER = "@@"
|
|
18
|
+
|
|
19
|
+
DEFAULT_CHARSET = "UTF-8"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class Service(BaseModel):
|
|
23
|
+
name: Annotated[str, Field(description="服务名称")]
|
|
24
|
+
groupName: Annotated[str, Field(description="分组名称")]
|
|
25
|
+
clusters: Annotated[str | None, Field(description="集群名称")] = ""
|
|
26
|
+
cacheMillis: Annotated[int, Field(description="缓存时间(毫秒)")] = 1000
|
|
27
|
+
hosts: Annotated[list[Instance], Field(description="实例列表")] = []
|
|
28
|
+
lastRefTime: Annotated[int, Field(description="最后引用时间")] = 0
|
|
29
|
+
checksum: Annotated[str, Field(description="校验和")] = ""
|
|
30
|
+
allIps: Annotated[bool, Field(description="是否所有IP")] = False
|
|
31
|
+
reachProtectionThreshold: Annotated[bool, Field(description="是否达到保护阈值")] = False
|
|
32
|
+
jsonFromServer: Annotated[str, Field(description="服务器返回的JSON")] = ""
|
|
33
|
+
|
|
34
|
+
def init_from_key(self, key=None):
|
|
35
|
+
if key:
|
|
36
|
+
max_index = 2
|
|
37
|
+
cluster_index = 2
|
|
38
|
+
service_name_index = 1
|
|
39
|
+
group_index = 0
|
|
40
|
+
keys = key.split(Constants.SERVICE_INFO_SPLITER)
|
|
41
|
+
if len(keys) >= max_index + 1:
|
|
42
|
+
self.groupName = keys[group_index]
|
|
43
|
+
self.name = keys[service_name_index]
|
|
44
|
+
self.clusters = keys[cluster_index]
|
|
45
|
+
elif len(keys) == max_index:
|
|
46
|
+
self.groupName = keys[group_index]
|
|
47
|
+
self.name = keys[service_name_index]
|
|
48
|
+
else:
|
|
49
|
+
raise NacosException("Can't parse out 'group_name', but it must not None!")
|
|
50
|
+
|
|
51
|
+
def get_ip_count(self):
|
|
52
|
+
return len(self.hosts)
|
|
53
|
+
|
|
54
|
+
def is_expired(self):
|
|
55
|
+
return int(round(time.time() * 1000)) - self.lastRefTime > self.cacheMillis
|
|
56
|
+
|
|
57
|
+
def add_host(self, host):
|
|
58
|
+
self.hosts.append(host)
|
|
59
|
+
|
|
60
|
+
def add_all_hosts(self, hosts):
|
|
61
|
+
self.hosts.extend(hosts)
|
|
62
|
+
|
|
63
|
+
def is_valid(self):
|
|
64
|
+
return self.hosts != []
|
|
65
|
+
|
|
66
|
+
def validate(self):
|
|
67
|
+
if self.allIps:
|
|
68
|
+
return True
|
|
69
|
+
|
|
70
|
+
if not self.hosts:
|
|
71
|
+
return False
|
|
72
|
+
|
|
73
|
+
valid_hosts = []
|
|
74
|
+
for host in self.hosts:
|
|
75
|
+
if not host.is_healthy():
|
|
76
|
+
continue
|
|
77
|
+
|
|
78
|
+
for i in range(host.get_weight()):
|
|
79
|
+
valid_hosts.append(i)
|
|
80
|
+
|
|
81
|
+
return len(valid_hosts) > 0
|
|
82
|
+
|
|
83
|
+
def get_key_default(self):
|
|
84
|
+
service_name = self.get_grouped_service_name()
|
|
85
|
+
return self.get_key(service_name, self.clusters)
|
|
86
|
+
|
|
87
|
+
def get_key_encoded(self):
|
|
88
|
+
service_name = self.get_grouped_service_name().encode("utf-8")
|
|
89
|
+
service_name = urllib.parse.quote(service_name)
|
|
90
|
+
return self.get_key(service_name, self.clusters)
|
|
91
|
+
|
|
92
|
+
def get_grouped_service_name(self):
|
|
93
|
+
service_name = self.name
|
|
94
|
+
if self.groupName and Constants.SERVICE_INFO_SPLITER not in service_name:
|
|
95
|
+
service_name = self.groupName + Constants.SERVICE_INFO_SPLITER + service_name
|
|
96
|
+
return service_name
|
|
97
|
+
|
|
98
|
+
@staticmethod
|
|
99
|
+
def from_key(key: str):
|
|
100
|
+
info = key.split(Constants.SERVICE_INFO_SPLITER)
|
|
101
|
+
if len(info) > 2:
|
|
102
|
+
service = Service(name=info[1], groupName=info[0], clusters=info[2])
|
|
103
|
+
else:
|
|
104
|
+
service = Service(name=info[1], groupName=info[0], clusters="")
|
|
105
|
+
return service
|
|
106
|
+
|
|
107
|
+
def get_hosts_str(self):
|
|
108
|
+
hosts_str = ""
|
|
109
|
+
for host in self.hosts:
|
|
110
|
+
hosts_str += host.json() + ";"
|
|
111
|
+
return hosts_str
|
|
112
|
+
|
|
113
|
+
class Config:
|
|
114
|
+
arbitrary_types_allowed = True
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
class ServiceList(BaseModel):
|
|
118
|
+
count: Annotated[int, Field(description="服务数量")]
|
|
119
|
+
services: Annotated[list[str], Field(description="服务名称列表")]
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""Runner 工具模块。
|
|
2
|
+
|
|
3
|
+
提供数据库连接、数据转换、日志等工具函数。
|
|
4
|
+
|
|
5
|
+
Functions:
|
|
6
|
+
encrypt_body: 加密请求体
|
|
7
|
+
decrypt_body: 解密请求体
|
|
8
|
+
generate_uuid: 生成 UUID
|
|
9
|
+
print_json: 打印 JSON 格式数据
|
|
10
|
+
|
|
11
|
+
Classes:
|
|
12
|
+
DatabaseConn: 数据库连接管理类
|
|
13
|
+
DuckDBManager: DuckDB 连接管理类
|
|
14
|
+
Neo4jConn: Neo4j 图数据库连接类
|
|
15
|
+
|
|
16
|
+
Example:
|
|
17
|
+
>>> from runner.utils import DatabaseConn, encrypt_body
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from .common import decrypt_body, encrypt_body, generate_uuid, parse_args, print_json
|
|
21
|
+
from .dbConn_sync import DatabaseConn
|
|
22
|
+
from .ftp_file_transfer import FTPFileTransfer
|
|
23
|
+
from .log import getLogger
|
|
24
|
+
from .nacos import get_service_instance
|
|
25
|
+
from .neo4j_sync import Neo4jConn
|
|
26
|
+
from .queue_kafka import KafkaQueue
|
|
27
|
+
from .sftp_file_transfer import SFTPFileTransfer
|
|
28
|
+
|
|
29
|
+
__all__ = [
|
|
30
|
+
"decrypt_body",
|
|
31
|
+
"encrypt_body",
|
|
32
|
+
"generate_uuid",
|
|
33
|
+
"print_json",
|
|
34
|
+
"DatabaseConn",
|
|
35
|
+
"getLogger",
|
|
36
|
+
"Neo4jConn",
|
|
37
|
+
"get_service_instance",
|
|
38
|
+
"KafkaQueue",
|
|
39
|
+
"FTPFileTransfer",
|
|
40
|
+
"SFTPFileTransfer",
|
|
41
|
+
"parse_args",
|
|
42
|
+
]
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
"""通用工具模块。
|
|
2
|
+
|
|
3
|
+
提供加解密、时间解析、UUID 生成和 JSON 打印等功能。
|
|
4
|
+
|
|
5
|
+
Functions:
|
|
6
|
+
encrypt_body: 加密请求体
|
|
7
|
+
decrypt_body: 解密请求体
|
|
8
|
+
generate_uuid: 生成 UUID
|
|
9
|
+
print_json: 打印 JSON 格式数据
|
|
10
|
+
parse_trigger_time: 解析时间字符串为 datetime 对象
|
|
11
|
+
build_trigger_time_vars: 根据触发时间构建日期变量
|
|
12
|
+
|
|
13
|
+
Example:
|
|
14
|
+
>>> encrypt_body({"key": "value"})
|
|
15
|
+
{'__base64': '...'}
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from base64 import b64decode, b64encode
|
|
19
|
+
from calendar import monthrange
|
|
20
|
+
from datetime import date, datetime, timedelta
|
|
21
|
+
from email.utils import parsedate_to_datetime
|
|
22
|
+
from typing import Any, Literal, Mapping
|
|
23
|
+
from uuid import uuid4
|
|
24
|
+
|
|
25
|
+
from pydantic import BaseModel, JsonValue
|
|
26
|
+
from pydantic_core import PydanticSerializationError, from_json, to_json
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def encrypt_body(body: JsonValue | BaseModel):
|
|
30
|
+
if body is None:
|
|
31
|
+
raise ValueError("body is None")
|
|
32
|
+
return {"__base64": f"FMIND-{b64encode(to_json(body)).decode('utf-8')}"}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def decrypt_body(body: dict[Literal["__base64"], str]) -> Mapping[str, JsonValue]:
|
|
36
|
+
"""
|
|
37
|
+
解密请求体
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
body (dict[Literal["__base64"], str]): 加密后的请求体,包含一个 "__base64" 键,其值为原始请求体的 Base64 编码字符串
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
JsonValue: 原始请求体
|
|
44
|
+
"""
|
|
45
|
+
if "__base64" not in body:
|
|
46
|
+
raise ValueError("Invalid body format: missing '__base64' key")
|
|
47
|
+
base64_str = body["__base64"]
|
|
48
|
+
if base64_str.startswith("FMIND-"):
|
|
49
|
+
base64_str = base64_str[6:]
|
|
50
|
+
bytes_body = b64decode(base64_str)
|
|
51
|
+
return cast(Mapping[str, JsonValue], from_json(bytes_body))
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def generate_uuid() -> int:
|
|
55
|
+
"""
|
|
56
|
+
生成 UUID
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
int: 64位无符号整数对应的字符串形式
|
|
60
|
+
"""
|
|
61
|
+
return uuid4().int
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def print_json(data: Any, indent: int | None = None) -> None:
|
|
65
|
+
"""打印 Pydantic 兼容的 JSON 格式数据。
|
|
66
|
+
|
|
67
|
+
将数据序列化为 JSON 字符串并打印到标准输出。
|
|
68
|
+
仅支持字典或 Pydantic 模型等可序列化对象。
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
data: 要打印的数据,必须为字典或可序列化为 JSON 的对象。
|
|
72
|
+
|
|
73
|
+
Example:
|
|
74
|
+
>>> print_json({"name": "test", "value": 123})
|
|
75
|
+
#
|
|
76
|
+
# {"name": "test", "value": 123}
|
|
77
|
+
"""
|
|
78
|
+
if not isinstance(data, dict):
|
|
79
|
+
print("ERROR: data must be a dict or json serializable object.")
|
|
80
|
+
return
|
|
81
|
+
try:
|
|
82
|
+
json_str = to_json(data, indent=indent).decode("utf-8")
|
|
83
|
+
print()
|
|
84
|
+
print(json_str, end="")
|
|
85
|
+
except PydanticSerializationError as e:
|
|
86
|
+
print(f"ERROR in Serialization: {e}")
|
|
87
|
+
except Exception as e:
|
|
88
|
+
print(f"ERROR: {e}")
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def parse_trigger_time(time_str: str, strict: bool = True) -> datetime:
|
|
92
|
+
"""解析时间字符串为 datetime 对象
|
|
93
|
+
|
|
94
|
+
支持格式:
|
|
95
|
+
- 秒级时间戳: 1704067200
|
|
96
|
+
- 毫秒级时间戳: 1704067200000
|
|
97
|
+
- 标准格式: 2024-01-01 12:00:00
|
|
98
|
+
- ISO 格式: 2024-01-01T12:00:00, 2024-01-01T12:00:00Z
|
|
99
|
+
- 其他常见格式: 2024/01/01 12:00:00, 2024-01-01
|
|
100
|
+
"""
|
|
101
|
+
if not time_str:
|
|
102
|
+
return datetime.now()
|
|
103
|
+
|
|
104
|
+
time_str = time_str.strip()
|
|
105
|
+
|
|
106
|
+
# 尝试解析为数字(时间戳)
|
|
107
|
+
if time_str.isdigit():
|
|
108
|
+
ts = int(time_str)
|
|
109
|
+
# 判断是秒级还是毫秒级
|
|
110
|
+
if ts > 1e11: # 毫秒级时间戳
|
|
111
|
+
return datetime.fromtimestamp(ts / 1000)
|
|
112
|
+
else: # 秒级时间戳
|
|
113
|
+
return datetime.fromtimestamp(ts)
|
|
114
|
+
|
|
115
|
+
# 尝试 ISO 格式
|
|
116
|
+
try:
|
|
117
|
+
# 处理带 Z 的 ISO 格式
|
|
118
|
+
return datetime.fromisoformat(time_str)
|
|
119
|
+
except ValueError:
|
|
120
|
+
pass
|
|
121
|
+
|
|
122
|
+
# 尝试常见格式
|
|
123
|
+
formats = [
|
|
124
|
+
"%Y-%m-%d %H:%M:%S",
|
|
125
|
+
"%Y-%m-%d %H:%M",
|
|
126
|
+
"%Y-%m-%d",
|
|
127
|
+
"%Y/%m/%d %H:%M:%S",
|
|
128
|
+
"%Y/%m/%d %H:%M",
|
|
129
|
+
"%Y/%m/%d",
|
|
130
|
+
"%Y%m%d",
|
|
131
|
+
]
|
|
132
|
+
|
|
133
|
+
for fmt in formats:
|
|
134
|
+
try:
|
|
135
|
+
return datetime.strptime(time_str, fmt)
|
|
136
|
+
except ValueError:
|
|
137
|
+
continue
|
|
138
|
+
|
|
139
|
+
# 尝试 RFC 2822 格式
|
|
140
|
+
try:
|
|
141
|
+
return parsedate_to_datetime(time_str)
|
|
142
|
+
except Exception:
|
|
143
|
+
pass
|
|
144
|
+
|
|
145
|
+
# 无法解析,抛出异常
|
|
146
|
+
if strict:
|
|
147
|
+
raise ValueError(f"无法解析时间字符串: {time_str}")
|
|
148
|
+
return datetime.now()
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def build_trigger_time_vars(trigger_time_raw: str) -> dict[str, datetime | date | int]:
|
|
152
|
+
"""根据触发时间构建日期变量"""
|
|
153
|
+
trigger_time = parse_trigger_time(trigger_time_raw)
|
|
154
|
+
today = trigger_time.date()
|
|
155
|
+
yesterday = today - timedelta(days=1)
|
|
156
|
+
before_yesterday = today - timedelta(days=2)
|
|
157
|
+
tomorrow = today + timedelta(days=1)
|
|
158
|
+
|
|
159
|
+
# 月首月末
|
|
160
|
+
_, month_last_day_num = monthrange(today.year, today.month)
|
|
161
|
+
month_first_day = today.replace(day=1)
|
|
162
|
+
month_last_day = today.replace(day=month_last_day_num)
|
|
163
|
+
|
|
164
|
+
# 上月
|
|
165
|
+
last_month_num = today.month - 1 if today.month != 1 else 12
|
|
166
|
+
last_month_year_num = today.year if today.month != 1 else today.year - 1
|
|
167
|
+
_, last_month_last_num = monthrange(last_month_year_num, last_month_num)
|
|
168
|
+
if today.month == 1:
|
|
169
|
+
last_month = today.replace(year=today.year - 1, month=12)
|
|
170
|
+
else:
|
|
171
|
+
last_month = today.replace(month=today.month - 1, day=min(today.day, last_month_last_num))
|
|
172
|
+
last_month_first_day = last_month.replace(day=1)
|
|
173
|
+
last_month_last_day = last_month.replace(day=last_month_last_num)
|
|
174
|
+
|
|
175
|
+
# 季度首日
|
|
176
|
+
quarter = (today.month - 1) // 3
|
|
177
|
+
quarter_first_month = quarter * 3 + 1
|
|
178
|
+
quarter_first_day = today.replace(month=quarter_first_month, day=1)
|
|
179
|
+
|
|
180
|
+
# 周一
|
|
181
|
+
weekday = today.weekday() # 0=Monday, 6=Sunday
|
|
182
|
+
monday = today - timedelta(days=weekday)
|
|
183
|
+
|
|
184
|
+
# 设置全局变量
|
|
185
|
+
return {
|
|
186
|
+
"trigger_time": trigger_time,
|
|
187
|
+
"today": today,
|
|
188
|
+
"tomorrow": tomorrow,
|
|
189
|
+
"yesterday": yesterday,
|
|
190
|
+
"before_yesterday": before_yesterday,
|
|
191
|
+
"month_first_day": month_first_day,
|
|
192
|
+
"month_last_day": month_last_day,
|
|
193
|
+
"last_month": last_month,
|
|
194
|
+
"last_month_first_day": last_month_first_day,
|
|
195
|
+
"last_month_last_day": last_month_last_day,
|
|
196
|
+
"quarter_first_day": quarter_first_day,
|
|
197
|
+
"monday": monday,
|
|
198
|
+
# 拓展
|
|
199
|
+
"cur_year": today.year,
|
|
200
|
+
"cur_month": today.month,
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def parse_args() -> tuple[str, ...]:
|
|
205
|
+
"""解析命令行参数"""
|
|
206
|
+
import sys
|
|
207
|
+
|
|
208
|
+
args = sys.argv[1:]
|
|
209
|
+
return args
|