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.

Files changed (79) hide show
  1. dataops_sdk/__init__.py +84 -0
  2. dataops_sdk/constants.py +6 -0
  3. dataops_sdk/exceptions.py +31 -0
  4. dataops_sdk/runner/__init__.py +46 -0
  5. dataops_sdk/runner/datasource_factory.py +83 -0
  6. dataops_sdk/runner/schemas/nacos/__init__.py +5 -0
  7. dataops_sdk/runner/schemas/nacos/config.py +40 -0
  8. dataops_sdk/runner/schemas/nacos/constants.py +235 -0
  9. dataops_sdk/runner/schemas/nacos/instance.py +103 -0
  10. dataops_sdk/runner/schemas/nacos/nacos_exception.py +44 -0
  11. dataops_sdk/runner/schemas/nacos/naming_param.py +62 -0
  12. dataops_sdk/runner/schemas/nacos/naming_request.py +84 -0
  13. dataops_sdk/runner/schemas/nacos/naming_response.py +50 -0
  14. dataops_sdk/runner/schemas/nacos/preserved_metadata_key.py +10 -0
  15. dataops_sdk/runner/schemas/nacos/service.py +119 -0
  16. dataops_sdk/runner/utils/__init__.py +42 -0
  17. dataops_sdk/runner/utils/common.py +209 -0
  18. dataops_sdk/runner/utils/dbConn_base.py +256 -0
  19. dataops_sdk/runner/utils/dbConn_sync.py +485 -0
  20. dataops_sdk/runner/utils/duckdb_manager.py +203 -0
  21. dataops_sdk/runner/utils/file_transfer_base.py +307 -0
  22. dataops_sdk/runner/utils/ftp_file_transfer.py +242 -0
  23. dataops_sdk/runner/utils/log.py +50 -0
  24. dataops_sdk/runner/utils/nacos.py +96 -0
  25. dataops_sdk/runner/utils/neo4j_sync.py +236 -0
  26. dataops_sdk/runner/utils/queue_base.py +224 -0
  27. dataops_sdk/runner/utils/queue_kafka.py +150 -0
  28. dataops_sdk/runner/utils/sftp_file_transfer.py +243 -0
  29. dataops_sdk/schemas/__init__.py +32 -0
  30. dataops_sdk/schemas/alarm.py +38 -0
  31. dataops_sdk/schemas/api.py +37 -0
  32. dataops_sdk/schemas/data_table.py +437 -0
  33. dataops_sdk/schemas/datasource.py +26 -0
  34. dataops_sdk/schemas/dict_.py +13 -0
  35. dataops_sdk/schemas/enums.py +120 -0
  36. dataops_sdk/schemas/group.py +40 -0
  37. dataops_sdk/schemas/hazard.py +151 -0
  38. dataops_sdk/schemas/meta.py +175 -0
  39. dataops_sdk/schemas/query.py +175 -0
  40. dataops_sdk/schemas/report.py +22 -0
  41. dataops_sdk/schemas/skill.py +70 -0
  42. dataops_sdk/schemas/table.py +65 -0
  43. dataops_sdk/schemas/user.py +52 -0
  44. dataops_sdk/schemas/widget.py +651 -0
  45. dataops_sdk/schemas/workflow.py +63 -0
  46. dataops_sdk/services/__init__.py +110 -0
  47. dataops_sdk/services/alarm.py +324 -0
  48. dataops_sdk/services/api.py +28 -0
  49. dataops_sdk/services/auth.py +120 -0
  50. dataops_sdk/services/data_table.py +485 -0
  51. dataops_sdk/services/dataset.py +127 -0
  52. dataops_sdk/services/datasource.py +57 -0
  53. dataops_sdk/services/dict_.py +69 -0
  54. dataops_sdk/services/hazard.py +260 -0
  55. dataops_sdk/services/meta.py +523 -0
  56. dataops_sdk/services/metrics.py +42 -0
  57. dataops_sdk/services/ref.py +66 -0
  58. dataops_sdk/services/report.py +36 -0
  59. dataops_sdk/services/skill.py +276 -0
  60. dataops_sdk/services/table.py +340 -0
  61. dataops_sdk/services/widget.py +143 -0
  62. dataops_sdk/services/workflow.py +102 -0
  63. dataops_sdk/session.py +292 -0
  64. dataops_sdk/tiptap/__init__.py +30 -0
  65. dataops_sdk/tiptap/builder.py +75 -0
  66. dataops_sdk/tiptap/constants.py +133 -0
  67. dataops_sdk/tiptap/markdown_parser.py +294 -0
  68. dataops_sdk/tiptap/models.py +43 -0
  69. dataops_sdk/tiptap/parse_utils.py +889 -0
  70. dataops_sdk/tiptap/render_utils.py +846 -0
  71. dataops_sdk/tiptap/renderers.py +51 -0
  72. dataops_sdk/tiptap/tool.py +629 -0
  73. dataops_sdk/utils/__init__.py +15 -0
  74. dataops_sdk/utils/common.py +211 -0
  75. dataops_sdk/utils/utils.py +120 -0
  76. dataops_sdk-0.3.0.dist-info/METADATA +25 -0
  77. dataops_sdk-0.3.0.dist-info/RECORD +79 -0
  78. dataops_sdk-0.3.0.dist-info/WHEEL +5 -0
  79. 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