smartpush 1.9.7__py3-none-any.whl → 1.9.9__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.
- smartpush/base/request_base.py +6 -0
- smartpush/base/url_enum.py +57 -38
- smartpush/crowd/crowd.py +11 -15
- smartpush/email/activity.py +66 -0
- smartpush/email/schema.py +163 -3
- smartpush/email/universal_content.py +30 -9
- smartpush/flow/MockFlow.py +12 -7
- smartpush/utils/ListDictUtils.py +13 -0
- smartpush/utils/date_utils.py +339 -0
- {smartpush-1.9.7.dist-info → smartpush-1.9.9.dist-info}/METADATA +1 -1
- {smartpush-1.9.7.dist-info → smartpush-1.9.9.dist-info}/RECORD +13 -11
- {smartpush-1.9.7.dist-info → smartpush-1.9.9.dist-info}/WHEEL +0 -0
- {smartpush-1.9.7.dist-info → smartpush-1.9.9.dist-info}/top_level.txt +0 -0
smartpush/base/request_base.py
CHANGED
|
@@ -69,3 +69,9 @@ class CrowdRequestBase(RequestBase):
|
|
|
69
69
|
def __init__(self, crowd_id, host, headers, **kwargs):
|
|
70
70
|
super().__init__(host, headers, **kwargs)
|
|
71
71
|
self.crowd_id = crowd_id
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class ActivityTemplateRequestBase(RequestBase):
|
|
75
|
+
def __init__(self, activityTemplateId, host, headers, **kwargs):
|
|
76
|
+
super().__init__(host, headers, **kwargs)
|
|
77
|
+
self.activityTemplateId = activityTemplateId
|
smartpush/base/url_enum.py
CHANGED
|
@@ -1,54 +1,73 @@
|
|
|
1
1
|
from enum import Enum, unique
|
|
2
2
|
|
|
3
|
+
POST = 'POST'
|
|
4
|
+
GET = 'GET'
|
|
5
|
+
|
|
3
6
|
|
|
4
7
|
@unique
|
|
5
|
-
class
|
|
8
|
+
class BaseEnum(Enum):
|
|
9
|
+
@property
|
|
10
|
+
def method(self):
|
|
11
|
+
return self.value[1]
|
|
12
|
+
|
|
13
|
+
@property
|
|
14
|
+
def url(self):
|
|
15
|
+
return self.value[0]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class URL:
|
|
6
19
|
"""
|
|
7
20
|
GET的参数用params,
|
|
8
21
|
POST参数用data,
|
|
9
22
|
"""
|
|
10
23
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
24
|
+
class FormReport(BaseEnum):
|
|
25
|
+
"""
|
|
26
|
+
:type:表单报告
|
|
27
|
+
"""
|
|
28
|
+
pageFormReportDetail = '/formReport/detail/pageFormReportDetail', 'POST' # 获取表单收集数据
|
|
29
|
+
getFormReportDetail = '/formReport/getFormReportDetail', 'POST' # 获取表单报告数据(曝光/点击)
|
|
30
|
+
getFormPerformanceTrend = 'formReport/getFormPerformanceTrend', 'POST'
|
|
17
31
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
"""
|
|
27
|
-
:type 表单操作
|
|
28
|
-
"""
|
|
29
|
-
deleteForm = '/form/deleteFormInfo', 'GET'
|
|
30
|
-
getFormList = '/form/getFormList', 'POST'
|
|
31
|
-
getFormInfo = '/form/getFormInfo', 'GET'
|
|
32
|
+
class Crowd(BaseEnum):
|
|
33
|
+
"""
|
|
34
|
+
:type 群组
|
|
35
|
+
"""
|
|
36
|
+
editCrowdPackage = '/crowdPackage/editCrowdPackage', POST
|
|
37
|
+
crowdPersonListInPackage = '/crowdPackage/crowdPersonList', POST
|
|
38
|
+
crowdPackageDetail = '/crowdPackage/detail', POST
|
|
39
|
+
crowdPackageList = '/crowdPackage/list', POST
|
|
32
40
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
updateCampaignUsed = "/universalContent/updateCampaignUsed",'POST'
|
|
41
|
-
queryUsedDetail = "/universalContent/queryUsedDetail", 'POST'
|
|
41
|
+
class Form(BaseEnum):
|
|
42
|
+
"""
|
|
43
|
+
:type 表单操作
|
|
44
|
+
"""
|
|
45
|
+
deleteForm = '/form/deleteFormInfo', GET
|
|
46
|
+
getFormList = '/form/getFormList', POST
|
|
47
|
+
getFormInfo = '/form/getFormInfo', GET
|
|
42
48
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
49
|
+
class UniversalContent(BaseEnum):
|
|
50
|
+
"""
|
|
51
|
+
:type 素材收藏
|
|
52
|
+
"""
|
|
53
|
+
saveUniversalContent = "/universalContent/saveUniversalContent", POST
|
|
54
|
+
deleteUniversalContent = "/universalContent/deleteUniversalContent", GET
|
|
55
|
+
updateUniversalContent = "/universalContent/updateUniversalContent", POST
|
|
56
|
+
queryUniversalContent = "/universalContent/query", POST
|
|
57
|
+
updateCampaignUsed = "/universalContent/updateCampaignUsed", POST
|
|
58
|
+
queryUsedDetail = "/universalContent/queryUsedDetail", POST
|
|
46
59
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
60
|
+
class Activity(BaseEnum):
|
|
61
|
+
"""
|
|
62
|
+
:type 活动
|
|
63
|
+
"""
|
|
64
|
+
step1 = "/marketing/insertOrUpdateActivity/step1", POST
|
|
65
|
+
step2 = "/marketing/insertOrUpdateActivity/step2", POST
|
|
66
|
+
step2_get = "/marketing/activityDetail/step2", GET
|
|
50
67
|
|
|
51
68
|
|
|
52
69
|
if __name__ == '__main__':
|
|
53
|
-
print(URL.pageFormReportDetail.
|
|
54
|
-
print(URL.
|
|
70
|
+
print(URL.FormReport.pageFormReportDetail.url)
|
|
71
|
+
print(URL.FormReport.pageFormReportDetail.method)
|
|
72
|
+
print(BaseEnum.url)
|
|
73
|
+
# print(URL.getFormReportDetail.url)
|
smartpush/crowd/crowd.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import json
|
|
2
2
|
import time
|
|
3
|
-
|
|
3
|
+
from smartpush.utils import ListDictUtils
|
|
4
4
|
from smartpush.base.request_base import CrowdRequestBase, RequestBase
|
|
5
5
|
from smartpush.base.url_enum import URL
|
|
6
6
|
from smartpush.export.basic.ExcelExportChecker import compare_lists, compare_dicts
|
|
@@ -21,7 +21,7 @@ class Crowd(CrowdRequestBase):
|
|
|
21
21
|
"""
|
|
22
22
|
requestParam = {"id": self.crowd_id, "crowdName": crowdName, "groupRelation": groupRelation,
|
|
23
23
|
"groupRules": groupRules, "triggerStock": triggerStock}
|
|
24
|
-
result = self.request(method=URL.editCrowdPackage.method, path=URL.editCrowdPackage.url, data=requestParam)
|
|
24
|
+
result = self.request(method=URL.Crowd.editCrowdPackage.method, path=URL.Crowd.editCrowdPackage.url, data=requestParam)
|
|
25
25
|
return result['resultData']
|
|
26
26
|
|
|
27
27
|
def callCrowdPersonListInPackage(self, page=1, pageSize=20, filter_type=None, operator='eq', filter_value=None):
|
|
@@ -37,7 +37,7 @@ class Crowd(CrowdRequestBase):
|
|
|
37
37
|
requestParam = {"id": self.crowd_id, "page": page, "pageSize": pageSize}
|
|
38
38
|
if filter_value is not None:
|
|
39
39
|
requestParam["filter"] = {filter_type: {operator: filter_value}}
|
|
40
|
-
result = self.request(method=URL.crowdPersonListInPackage.method, path=URL.crowdPersonListInPackage.url,
|
|
40
|
+
result = self.request(method=URL.Crowd.crowdPersonListInPackage.method, path=URL.Crowd.crowdPersonListInPackage.url,
|
|
41
41
|
data=requestParam)
|
|
42
42
|
resultData = result['resultData']
|
|
43
43
|
return resultData
|
|
@@ -52,7 +52,7 @@ class Crowd(CrowdRequestBase):
|
|
|
52
52
|
requestParam = {"id": self.crowd_id, "page": page, "pageSize": pageSize, "filter": {}}
|
|
53
53
|
# if filter_value is not None:
|
|
54
54
|
# requestParam["filter"] = {filter_type: {"in": filter_value}}
|
|
55
|
-
result = self.request(method=URL.crowdPackageDetail.method, path=URL.crowdPackageDetail.url, data=requestParam)
|
|
55
|
+
result = self.request(method=URL.Crowd.crowdPackageDetail.method, path=URL.Crowd.crowdPackageDetail.url, data=requestParam)
|
|
56
56
|
resultData = result['resultData']
|
|
57
57
|
return resultData
|
|
58
58
|
|
|
@@ -61,24 +61,20 @@ class Crowd(CrowdRequestBase):
|
|
|
61
61
|
result = {}
|
|
62
62
|
# 校验群组详情条件
|
|
63
63
|
crowd_detail = self.callCrowdPackageDetail()
|
|
64
|
-
if crowd_detail["
|
|
64
|
+
if crowd_detail["groupRules"] == expected_rule:
|
|
65
65
|
result["rule"] = True
|
|
66
66
|
else:
|
|
67
|
-
result["rule"] = {"条件断言": False, "实际条件": crowd_detail["
|
|
67
|
+
result["rule"] = {"条件断言": False, "实际条件": crowd_detail["groupRules"]}
|
|
68
68
|
# 校验群组筛选人群
|
|
69
69
|
time.sleep(sleep)
|
|
70
70
|
crowd_persons = self.callCrowdPersonListInPackage()
|
|
71
|
-
crowd_person_uids = [person["uid"] for person in crowd_persons]
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
else:
|
|
76
|
-
result["uid"] = True
|
|
77
|
-
|
|
71
|
+
crowd_person_uids = [person["uid"] for person in crowd_persons["responseResult"]]
|
|
72
|
+
print("expected_ids", expected_ids, type(expected_ids))
|
|
73
|
+
print("crowd_person_uids", crowd_person_uids, type(crowd_person_uids))
|
|
74
|
+
result["联系人断言"] = ListDictUtils.check_values_in_list_set(a=expected_ids, b=crowd_person_uids)
|
|
78
75
|
return result
|
|
79
76
|
|
|
80
77
|
|
|
81
|
-
|
|
82
78
|
class CrowdList(RequestBase):
|
|
83
79
|
def callCrowdPackageList(self, page=1, pageSize=20):
|
|
84
80
|
"""
|
|
@@ -90,7 +86,7 @@ class CrowdList(RequestBase):
|
|
|
90
86
|
:return:
|
|
91
87
|
"""
|
|
92
88
|
requestParam = {"page": page, "pageSize": pageSize}
|
|
93
|
-
result = self.request(method=URL.crowdPackageList.method, path=URL.crowdPackageList.url,
|
|
89
|
+
result = self.request(method=URL.Crowd.crowdPackageList.method, path=URL.Crowd.crowdPackageList.url,
|
|
94
90
|
data=requestParam)
|
|
95
91
|
resultData = result['resultData']
|
|
96
92
|
return resultData
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import json
|
|
2
|
+
|
|
3
|
+
from smartpush.base.request_base import ActivityTemplateRequestBase, RequestBase
|
|
4
|
+
from smartpush.base.url_enum import URL
|
|
5
|
+
from smartpush.utils.date_utils import *
|
|
6
|
+
from smartpush.email.schema import *
|
|
7
|
+
|
|
8
|
+
"""
|
|
9
|
+
创建活动草稿
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
"""
|
|
13
|
+
更新活动草稿内容
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ActivityTemplate(ActivityTemplateRequestBase, RequestBase):
|
|
18
|
+
|
|
19
|
+
def create_activity_template(self):
|
|
20
|
+
requestParam = {"activityName": "自动化创建草稿活动-" + DateUtils.get_current_datetime_to_str(),
|
|
21
|
+
"pickContactPacks": [],
|
|
22
|
+
"smartSending": False, "subjectId": [], "emailName": "测试", "subtitle": "",
|
|
23
|
+
"sender": "SmartPush_AutoTest_ec2自动化店铺 AutoTestName", "senderDomain": "DEFAULT_DOMAIN",
|
|
24
|
+
"receiveAddress": "autotest-smartpushauto@smartpush.com", "utmConfigEnable": False,
|
|
25
|
+
"language": "en", "sendType": "EMAIL", "type": "MKT", "id": None, "domainType": 3,
|
|
26
|
+
"activityTemplateId": None, "activityId": None, "timezone": None, "timezoneGmt": None,
|
|
27
|
+
"originTemplate": 6, "createSource": 0, "sendStrategy": "DELAY", "booster": None,
|
|
28
|
+
"customerGroupIds": [], "excludeContactPacks": [], "excludeCustomerGroupIds": [],
|
|
29
|
+
"warmupPack": 0}
|
|
30
|
+
result = self.request(path=URL.Activity.step1.url, method=URL.Activity.step1.method, data=requestParam)
|
|
31
|
+
return json.dumps(result)
|
|
32
|
+
|
|
33
|
+
def update_activity_template_schema(self, currentJsonSchema, schema):
|
|
34
|
+
currentJsonSchema = self.add_element_to_section_column_children(currentJsonSchema, schema)
|
|
35
|
+
final_currentJsonSchema = json.dumps(isinstance) if isinstance(currentJsonSchema, dict) else currentJsonSchema
|
|
36
|
+
requestParam = {"id": self.activityTemplateId,
|
|
37
|
+
"activityImage": DateUtils.get_current_timestamp(),
|
|
38
|
+
"blocks": None,
|
|
39
|
+
"currentHtml": "",
|
|
40
|
+
"currentJsonSchema": final_currentJsonSchema,
|
|
41
|
+
"previewJsonSchema": final_currentJsonSchema,
|
|
42
|
+
"schemaAnalysis": genSchemaAnalysis(json.loads(final_currentJsonSchema))}
|
|
43
|
+
|
|
44
|
+
result = self.request(path=URL.Activity.step2.url, method=URL.Activity.step2.method,
|
|
45
|
+
data=requestParam)
|
|
46
|
+
return json.dumps(result)
|
|
47
|
+
|
|
48
|
+
@staticmethod
|
|
49
|
+
def add_element_to_section_column_children(currentJsonSchema, new_schema):
|
|
50
|
+
"""
|
|
51
|
+
在Section下的Column节点的children中添加元素,并返回原JSON(已修改)
|
|
52
|
+
:param new_schema: 原始JSON字典
|
|
53
|
+
:param currentJsonSchema: 要添加的元素(字典)
|
|
54
|
+
:return: 修改后的原始JSON字典
|
|
55
|
+
"""
|
|
56
|
+
# 1. 遍历找到type为Section的节点
|
|
57
|
+
for child in currentJsonSchema.get("children", []):
|
|
58
|
+
if child.get("type") == "Section":
|
|
59
|
+
# 2. 在Section的children中找到Column节点
|
|
60
|
+
for column in child.get("children", []):
|
|
61
|
+
if column.get("type") == "Column":
|
|
62
|
+
# 3. 向Column的children添加元素
|
|
63
|
+
column["children"].append(new_schema)
|
|
64
|
+
break # 若有多个Column可根据需求调整,这里匹配第一个Column
|
|
65
|
+
break # 若有多个Section可根据需求调整,这里匹配第一个Section
|
|
66
|
+
return currentJsonSchema
|
smartpush/email/schema.py
CHANGED
|
@@ -162,7 +162,7 @@ class BlockSchema(Enum):
|
|
|
162
162
|
"src": "https://client-test.smartpushedm.com/sp-media-support/gif/0894dd7053af4b189f665dd2ddb54606.gif?v=1762701524060",
|
|
163
163
|
"imgRatio": 0.26, "timerType": 1, "endTime": "1762874323452",
|
|
164
164
|
"timerZone": "America/New_York", "expire": False,
|
|
165
|
-
"expireText": "The current activity has expired", "duration": "", "layout": 1,
|
|
165
|
+
"expireText": "The current activity.py has expired", "duration": "", "layout": 1,
|
|
166
166
|
"numberFontFamily": "Arial Bold", "numberSize": "40px", "numberColor": "#FFFFFF",
|
|
167
167
|
"timeUnitFontFamily": "Arial Bold", "timeUnitSize": "16px", "timeUnitColor": "#FFFFFF",
|
|
168
168
|
"align": "center", "paddingLeft": "10px", "paddingRight": "10px", "paddingTop": "10px",
|
|
@@ -397,6 +397,116 @@ class BlockSchema(Enum):
|
|
|
397
397
|
}
|
|
398
398
|
]
|
|
399
399
|
}
|
|
400
|
+
Subscribe = {
|
|
401
|
+
"id": generate_UUID(9),
|
|
402
|
+
"type": "Subscribe",
|
|
403
|
+
"props": {
|
|
404
|
+
"content": "<div class='sp-font-12'><p style=\"text-align: center; \" data-mce-style=\"text-align: "
|
|
405
|
+
"center; \"><em><span class='sp-font-12' style=\"font-size: 12px; color: rgb(122, 132, "
|
|
406
|
+
"153);\" data-mce-style=\"font-size: 12px; color: #7a8499;\">Please add us to your email "
|
|
407
|
+
"contacts list to receive exclusive recommendations!</span></em></p><p style=\"text-align: "
|
|
408
|
+
"center; \" data-mce-style=\"text-align: center; \"><em><span class='sp-font-12' "
|
|
409
|
+
"style=\"font-size: 12px; color: rgb(122, 132, 153);\" data-mce-style=\"font-size: 12px; "
|
|
410
|
+
"color: #7a8499;\">You are receiving this message from [[shopName]] because you have signed up "
|
|
411
|
+
"for subscriptions to receive information about products, services, "
|
|
412
|
+
"and campaigns.</span></em></p></div><div class=\"sp-font-12\" style=\"color: rgb(122, 132, "
|
|
413
|
+
"153); font-family: arial, helvetica, sans-serif, Arial, Helvetica, sans-serif; font-size: "
|
|
414
|
+
"12px; text-align: center; margin-top: 12px;padding-left: 10px; padding-right: 10px; "
|
|
415
|
+
"padding-bottom: 20px;\"><div>Questions? Please <span "
|
|
416
|
+
"style=color:#479EEF;text-decoration:underline>contact us</span>, we are glad to "
|
|
417
|
+
"help.</div><div>If you don't want to receive our message, just <span "
|
|
418
|
+
"style=color:#479EEF;text-decoration:underline>click here</span> to cancel the "
|
|
419
|
+
"subscription.</div></div><div class=\"sp-font-12\" style=\"color: rgb(122, 132, "
|
|
420
|
+
"153); font-size: 12px; text-align: center; font-style: oblique;\">Questions? Please <a href=["
|
|
421
|
+
"[customerEmail]] style=\"color:#479EEF;text-decoration:underline\" "
|
|
422
|
+
"rel=\"noreferrer\">contact us</a>, we are glad to help.</div><div class=\"sp-font-12\" "
|
|
423
|
+
"style=\"color: rgb(122, 132, 153); font-size: 12px; text-align: center; font-style: "
|
|
424
|
+
"oblique;\">If you don't want to receive our message, just <a href=[[unsubscribe.url]] "
|
|
425
|
+
"target=\"_blank\" style=\"color:#479EEF;text-decoration:underline\" rel=\"noreferrer\">click "
|
|
426
|
+
"here</a> to cancel the subscription.</div>",
|
|
427
|
+
"containerBackgroundColor": "transparent"
|
|
428
|
+
},
|
|
429
|
+
"children": []
|
|
430
|
+
}
|
|
431
|
+
Header = {
|
|
432
|
+
"id": generate_UUID(9),
|
|
433
|
+
"type": "Header",
|
|
434
|
+
"props": {
|
|
435
|
+
"backgroundColor": "#ffffff",
|
|
436
|
+
"borderLeft": "1px none #ffffff",
|
|
437
|
+
"borderRight": "1px none #ffffff",
|
|
438
|
+
"borderTop": "1px none #ffffff",
|
|
439
|
+
"borderBottom": "1px none #ffffff",
|
|
440
|
+
"paddingTop": "0px",
|
|
441
|
+
"paddingBottom": "0px",
|
|
442
|
+
"paddingLeft": "0px",
|
|
443
|
+
"paddingRight": "0px",
|
|
444
|
+
"cols": [
|
|
445
|
+
12
|
|
446
|
+
]
|
|
447
|
+
},
|
|
448
|
+
"children": [
|
|
449
|
+
{
|
|
450
|
+
"id": "98d909a48",
|
|
451
|
+
"type": "Column",
|
|
452
|
+
"props": {},
|
|
453
|
+
"children": [
|
|
454
|
+
{
|
|
455
|
+
"id": "8298cb99a",
|
|
456
|
+
"type": "TextSet",
|
|
457
|
+
"props": {
|
|
458
|
+
"list": [
|
|
459
|
+
{
|
|
460
|
+
"borderWidth": "1px",
|
|
461
|
+
"borderStyle": "none",
|
|
462
|
+
"content": "<p style=\"line-height: 3; text-align: right;\"><span style=\"color: #000000; font-size: 12px; font-family: arial, sans-serif;\"><a style=\"color: #000000;\" href=\"${viewInWebApiUrl}\">Can't see the email? Please <span style=\"color: #3598db;\">click here</span></a></span></p>",
|
|
463
|
+
"paddingLeft": "10px",
|
|
464
|
+
"paddingRight": "10px",
|
|
465
|
+
"paddingTop": "0px",
|
|
466
|
+
"paddingBottom": "0px",
|
|
467
|
+
"borderColor": "#ffffff"
|
|
468
|
+
}
|
|
469
|
+
],
|
|
470
|
+
"containerBackgroundColor": "transparent",
|
|
471
|
+
"paddingLeft": "10px",
|
|
472
|
+
"paddingRight": "10px",
|
|
473
|
+
"paddingTop": "10px",
|
|
474
|
+
"paddingBottom": "10px"
|
|
475
|
+
},
|
|
476
|
+
"children": []
|
|
477
|
+
}
|
|
478
|
+
]
|
|
479
|
+
}
|
|
480
|
+
]
|
|
481
|
+
}
|
|
482
|
+
Footer = {
|
|
483
|
+
"id": generate_UUID(9),
|
|
484
|
+
"type": "Footer",
|
|
485
|
+
"props": {
|
|
486
|
+
"backgroundColor": "#ffffff",
|
|
487
|
+
"borderLeft": "1px none #ffffff",
|
|
488
|
+
"borderRight": "1px none #ffffff",
|
|
489
|
+
"borderTop": "1px none #ffffff",
|
|
490
|
+
"borderBottom": "1px none #ffffff",
|
|
491
|
+
"paddingTop": "0px",
|
|
492
|
+
"paddingBottom": "0px",
|
|
493
|
+
"paddingLeft": "0px",
|
|
494
|
+
"paddingRight": "0px",
|
|
495
|
+
"cols": [
|
|
496
|
+
12
|
|
497
|
+
]
|
|
498
|
+
},
|
|
499
|
+
"children": [
|
|
500
|
+
{
|
|
501
|
+
"id": "b3bcabad7",
|
|
502
|
+
"type": "Column",
|
|
503
|
+
"props": {},
|
|
504
|
+
"children": [
|
|
505
|
+
Subscribe
|
|
506
|
+
]
|
|
507
|
+
}
|
|
508
|
+
]
|
|
509
|
+
}
|
|
400
510
|
|
|
401
511
|
|
|
402
512
|
def genSection(block_list: list):
|
|
@@ -411,14 +521,64 @@ def genSection(block_list: list):
|
|
|
411
521
|
return section
|
|
412
522
|
|
|
413
523
|
|
|
524
|
+
def genSchemaAnalysis(schema):
|
|
525
|
+
"""
|
|
526
|
+
递归遍历嵌套字典结构,提取所有层级的type和对应的id
|
|
527
|
+
相同type的id将以列表形式汇总(按遍历顺序追加)
|
|
528
|
+
|
|
529
|
+
Args:
|
|
530
|
+
data: 嵌套字典/列表结构(支持单节点字典或children列表)
|
|
531
|
+
|
|
532
|
+
Returns:
|
|
533
|
+
dict: key为type值,value为id列表(即使只有一个id也保持列表格式)
|
|
534
|
+
:param schema:
|
|
535
|
+
"""
|
|
536
|
+
result = {}
|
|
537
|
+
|
|
538
|
+
def recursive_parse(node):
|
|
539
|
+
if isinstance(node, dict):
|
|
540
|
+
if 'type' in node and 'id' in node:
|
|
541
|
+
type_val = node['type']
|
|
542
|
+
id_val = node['id']
|
|
543
|
+
result.setdefault(type_val, []).append(id_val)
|
|
544
|
+
|
|
545
|
+
if 'children' in node and isinstance(node['children'], list):
|
|
546
|
+
for child in node['children']:
|
|
547
|
+
recursive_parse(child)
|
|
548
|
+
elif isinstance(node, list):
|
|
549
|
+
for item in node:
|
|
550
|
+
recursive_parse(item)
|
|
551
|
+
|
|
552
|
+
recursive_parse(schema if isinstance(schema,dict) else schema.value)
|
|
553
|
+
schemaAnalysis = {"schema": result}
|
|
554
|
+
return json.dumps(schemaAnalysis, indent=4, ensure_ascii=False)
|
|
555
|
+
|
|
556
|
+
|
|
414
557
|
def genAllBlockSchema():
|
|
415
558
|
"""
|
|
416
559
|
获取所有的BlockSchema
|
|
417
560
|
:return:
|
|
418
561
|
"""
|
|
419
|
-
temp_list = [i.value for i in BlockSchema if i
|
|
562
|
+
temp_list = [i.value for i in BlockSchema if i not in (BlockSchema.Section, BlockSchema.Footer, BlockSchema.Header)]
|
|
563
|
+
return temp_list
|
|
564
|
+
|
|
565
|
+
|
|
566
|
+
def genAllBlockSchemaList():
|
|
567
|
+
"""
|
|
568
|
+
获取所有的BlockSchema
|
|
569
|
+
:return:
|
|
570
|
+
"""
|
|
571
|
+
temp_list = [i.name for i in BlockSchema if i not in (BlockSchema.Section, BlockSchema.Footer, BlockSchema.Header)]
|
|
420
572
|
return temp_list
|
|
421
573
|
|
|
422
574
|
|
|
423
575
|
if __name__ == '__main__':
|
|
424
|
-
print(
|
|
576
|
+
# print(BlockSchema.Footer.value)
|
|
577
|
+
# print(genAllBlockSchemaList())
|
|
578
|
+
# [BlockSchema.Logo, BlockSchema.Social]
|
|
579
|
+
# [BlockSchema.Logo, BlockSchema.Link, BlockSchema.Image, BlockSchema.ImageSet, BlockSchema.Video,
|
|
580
|
+
# BlockSchema.TimerCountdown, BlockSchema.Commodity, BlockSchema.Discount, BlockSchema.TextSet,
|
|
581
|
+
# BlockSchema.ImageText,
|
|
582
|
+
# BlockSchema.Button, BlockSchema.Divider, BlockSchema.Social, BlockSchema.HTMLCode]
|
|
583
|
+
|
|
584
|
+
print(genSchemaAnalysis(BlockSchema.Section))
|
|
@@ -3,7 +3,6 @@ import time
|
|
|
3
3
|
from smartpush.base.request_base import RequestBase
|
|
4
4
|
from smartpush.base.url_enum import *
|
|
5
5
|
from smartpush.email.schema import *
|
|
6
|
-
from smartpush.utils import ListDictUtils
|
|
7
6
|
|
|
8
7
|
|
|
9
8
|
def gen_universal_request_param(universalId, schema, **kwargs):
|
|
@@ -34,7 +33,7 @@ class UniversalContent(RequestBase):
|
|
|
34
33
|
|
|
35
34
|
# 创建universal
|
|
36
35
|
def create_universal(self, requestParam):
|
|
37
|
-
result = self.request(method=URL.saveUniversalContent.method, path=URL.saveUniversalContent.url,
|
|
36
|
+
result = self.request(method=URL.UniversalContent.saveUniversalContent.method, path=URL.UniversalContent.saveUniversalContent.url,
|
|
38
37
|
data=requestParam)
|
|
39
38
|
return result
|
|
40
39
|
|
|
@@ -49,7 +48,7 @@ class UniversalContent(RequestBase):
|
|
|
49
48
|
requestParam = {'universalName': universa_name}
|
|
50
49
|
if blockType_list and type(blockType_list) == list:
|
|
51
50
|
requestParam.update(blockType=blockType_list)
|
|
52
|
-
result = self.request(method=URL.queryUniversalContent.method, path=URL.queryUniversalContent.url,
|
|
51
|
+
result = self.request(method=URL.UniversalContent.queryUniversalContent.method, path=URL.UniversalContent.queryUniversalContent.url,
|
|
53
52
|
data=requestParam)
|
|
54
53
|
return result
|
|
55
54
|
|
|
@@ -73,7 +72,7 @@ class UniversalContent(RequestBase):
|
|
|
73
72
|
"data": universal_list,
|
|
74
73
|
"type": _type
|
|
75
74
|
}
|
|
76
|
-
result = self.request(method=URL.updateUniversalContent.method, path=URL.updateUniversalContent.url,
|
|
75
|
+
result = self.request(method=URL.UniversalContent.updateUniversalContent.method, path=URL.UniversalContent.updateUniversalContent.url,
|
|
77
76
|
data=requestParam)
|
|
78
77
|
return result
|
|
79
78
|
|
|
@@ -85,7 +84,7 @@ class UniversalContent(RequestBase):
|
|
|
85
84
|
:return:
|
|
86
85
|
"""
|
|
87
86
|
requestParam = {'universalId': universalId}
|
|
88
|
-
result = self.request(method=URL.deleteUniversalContent.method, path=URL.deleteUniversalContent.url,
|
|
87
|
+
result = self.request(method=URL.UniversalContent.deleteUniversalContent.method, path=URL.UniversalContent.deleteUniversalContent.url,
|
|
89
88
|
params=requestParam)
|
|
90
89
|
return result
|
|
91
90
|
|
|
@@ -98,19 +97,18 @@ class UniversalContent(RequestBase):
|
|
|
98
97
|
:return:
|
|
99
98
|
"""
|
|
100
99
|
requestParam = {"type": _type, "campaignId": campaignId, "universalIds": universalIds}
|
|
101
|
-
result = self.request(method=URL.updateCampaignUsed.method, path=URL.updateCampaignUsed.url,
|
|
100
|
+
result = self.request(method=URL.UniversalContent.updateCampaignUsed.method, path=URL.UniversalContent.updateCampaignUsed.url,
|
|
102
101
|
data=requestParam)
|
|
103
102
|
return result
|
|
104
103
|
|
|
105
104
|
def assert_block_in_the_section(self, section_universa_name, block_universa=None):
|
|
106
105
|
"""
|
|
107
106
|
判断收藏的block是否在该section中
|
|
107
|
+
:param block_universa:
|
|
108
108
|
:param section_universa_name:
|
|
109
|
-
:param _id:
|
|
110
|
-
:param block_universa_id:
|
|
111
|
-
:param block_universa_name:
|
|
112
109
|
:return:
|
|
113
110
|
"""
|
|
111
|
+
section = {}
|
|
114
112
|
result = self.query_universal(universa_name=section_universa_name)
|
|
115
113
|
if result:
|
|
116
114
|
section = result['resultData']['datas'][0]
|
|
@@ -182,3 +180,26 @@ if __name__ == '__main__':
|
|
|
182
180
|
raise
|
|
183
181
|
finally:
|
|
184
182
|
universal.delete_universal(sectionUniversalId)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
if __name__ == '__main__':
|
|
186
|
+
sectionUniversalId = generate_UUID()
|
|
187
|
+
sectionUniversalName = gen_universal_name(BlockSchema.Section)
|
|
188
|
+
print(sectionUniversalName)
|
|
189
|
+
block_list = [BlockSchema.Logo] # 添加block
|
|
190
|
+
|
|
191
|
+
block_dict = {}
|
|
192
|
+
result_list = []
|
|
193
|
+
blockUniversalNameList = []
|
|
194
|
+
for block in block_list:
|
|
195
|
+
name = block.name
|
|
196
|
+
block_schema = get_universal_schema(block, _id=generate_UUID(9), universalId=generate_UUID(),
|
|
197
|
+
universalName=gen_universal_name(block))
|
|
198
|
+
result_list.append(block_schema)
|
|
199
|
+
print(result_list)
|
|
200
|
+
vars['result_list'] = result_list
|
|
201
|
+
section_schema = get_universal_schema(genSection(result_list), _id=generate_UUID(9), universalId=sectionUniversalId,
|
|
202
|
+
universalName=sectionUniversalName)
|
|
203
|
+
universal_request_param = gen_universal_request_param(sectionUniversalId, section_schema)
|
|
204
|
+
vars['universal_request_param'] = universal_request_param
|
|
205
|
+
print(universal_request_param)
|
smartpush/flow/MockFlow.py
CHANGED
|
@@ -102,16 +102,13 @@ def check_flow(host_domain, cookies, mock_domain="", **kwargs):
|
|
|
102
102
|
split_node: list,有拆分节点时需填,结构:如: ["false", "true"],即走到拆分节点限制不满足分支再走满足分支
|
|
103
103
|
get_email_content: bool,默认false, 提取邮件内容,用于断言邮箱内是否送达
|
|
104
104
|
"""
|
|
105
|
-
# todo: 还差邮件校验部分,后续补充
|
|
106
105
|
is_split_steps = kwargs.get("split_steps", "all")
|
|
107
106
|
# 步骤1 - 所需字段:split_steps、host_domain、cookies、flow_id、pulsar
|
|
108
107
|
if is_split_steps == "one" or is_split_steps == "all":
|
|
109
|
-
#
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
kwargs["old_flow_counts"] = old_flow_counts
|
|
114
|
-
print(f"触发前节点数据: {old_flow_counts}")
|
|
108
|
+
# 提取版本号
|
|
109
|
+
_, old_versions, _ = get_current_flow(host_domain=host_domain, cookies=cookies,
|
|
110
|
+
flow_id=kwargs["flow_id"],
|
|
111
|
+
splits=kwargs.get("split_node", None))
|
|
115
112
|
# 更新flow
|
|
116
113
|
if kwargs.get("update_flow_params", False):
|
|
117
114
|
global_flow.update_flow(host_domain=host_domain, cookies=cookies,
|
|
@@ -124,6 +121,14 @@ def check_flow(host_domain, cookies, mock_domain="", **kwargs):
|
|
|
124
121
|
# 启动flow
|
|
125
122
|
global_flow.start_flow(host_domain=host_domain, cookies=cookies, flow_id=kwargs["flow_id"],
|
|
126
123
|
version=old_versions, draft_snap_version=draft_snap_version)
|
|
124
|
+
|
|
125
|
+
# 触发前提取flow数据,后续做对比
|
|
126
|
+
old_flow_counts, _, _ = get_current_flow(host_domain=host_domain, cookies=cookies,
|
|
127
|
+
flow_id=kwargs["flow_id"],
|
|
128
|
+
splits=kwargs.get("split_node", None))
|
|
129
|
+
time.sleep(3)
|
|
130
|
+
kwargs["old_flow_counts"] = old_flow_counts
|
|
131
|
+
print(f"触发前节点数据: {old_flow_counts}")
|
|
127
132
|
# 触发flow
|
|
128
133
|
mock_pulsar(mock_domain=mock_domain, pulsar=kwargs["pulsar"], limit=kwargs.get("limit", 1))
|
|
129
134
|
if is_split_steps == "one":
|
smartpush/utils/ListDictUtils.py
CHANGED
|
@@ -70,3 +70,16 @@ def all_in_list(list_a, list_b):
|
|
|
70
70
|
# 支持列表、元组、集合等可迭代类型
|
|
71
71
|
print(f"判断对象【{list_a}】在 {list_b}")
|
|
72
72
|
return set(list_a).issubset(set(list_b))
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def check_values_in_list_set(a, b):
|
|
76
|
+
"""
|
|
77
|
+
使用集合检查列表a中的所有值是否都在列表b中(效率更高)
|
|
78
|
+
"""
|
|
79
|
+
set_b = set(b)
|
|
80
|
+
missing_values = [x for x in a if x not in set_b]
|
|
81
|
+
|
|
82
|
+
if not missing_values:
|
|
83
|
+
return [True, "匹配成功"]
|
|
84
|
+
else:
|
|
85
|
+
return [False, "匹配失败,不匹配数据" + str(missing_values)]
|
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
from datetime import date, datetime, timedelta, timezone
|
|
2
|
+
import calendar
|
|
3
|
+
import random
|
|
4
|
+
from zoneinfo import ZoneInfo
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class DateUtils:
|
|
8
|
+
"""日期生成与处理工具类"""
|
|
9
|
+
|
|
10
|
+
# ==================== 1. 获取当前日期/时间 ====================
|
|
11
|
+
@staticmethod
|
|
12
|
+
def get_current_date() -> date:
|
|
13
|
+
"""获取当前日期(date对象,仅年月日)"""
|
|
14
|
+
return date.today()
|
|
15
|
+
|
|
16
|
+
@staticmethod
|
|
17
|
+
def get_current_datetime(tz: str | None = None) -> datetime:
|
|
18
|
+
"""获取当前datetime对象(可选时区)
|
|
19
|
+
:param tz: 时区标识(如"Asia/Shanghai"、"America/New_York")
|
|
20
|
+
"""
|
|
21
|
+
if tz:
|
|
22
|
+
return datetime.now(ZoneInfo(tz))
|
|
23
|
+
return datetime.now()
|
|
24
|
+
|
|
25
|
+
@staticmethod
|
|
26
|
+
def get_current_datetime_to_str() -> str:
|
|
27
|
+
"""获取当前datetime对象(可选时区)
|
|
28
|
+
"""
|
|
29
|
+
return str(DateUtils.get_current_datetime())
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@staticmethod
|
|
33
|
+
def get_utc_now() -> datetime:
|
|
34
|
+
"""获取UTC标准时间(带时区信息)"""
|
|
35
|
+
return datetime.utcnow().replace(tzinfo=timezone.utc)
|
|
36
|
+
|
|
37
|
+
# ==================== 2. 生成指定日期/时间 ====================
|
|
38
|
+
@staticmethod
|
|
39
|
+
def get_specified_date(year: int, month: int, day: int) -> date | None:
|
|
40
|
+
"""生成指定日期(date对象),无效日期返回None"""
|
|
41
|
+
try:
|
|
42
|
+
return date(year, month, day)
|
|
43
|
+
except ValueError as e:
|
|
44
|
+
print(f"❌ 无效日期:{year}-{month}-{day},错误:{e}")
|
|
45
|
+
return None
|
|
46
|
+
|
|
47
|
+
@staticmethod
|
|
48
|
+
def get_specified_datetime(
|
|
49
|
+
year: int, month: int, day: int,
|
|
50
|
+
hour: int = 0, minute: int = 0, second: int = 0,
|
|
51
|
+
tz: str | None = None
|
|
52
|
+
) -> datetime | None:
|
|
53
|
+
"""生成指定datetime对象(可选时区),无效参数返回None"""
|
|
54
|
+
try:
|
|
55
|
+
dt = datetime(year, month, day, hour, minute, second)
|
|
56
|
+
if tz:
|
|
57
|
+
dt = dt.replace(tzinfo=ZoneInfo(tz))
|
|
58
|
+
return dt
|
|
59
|
+
except ValueError as e:
|
|
60
|
+
print(f"❌ 无效日期时间:{year}-{month}-{day} {hour}:{minute}:{second},错误:{e}")
|
|
61
|
+
return None
|
|
62
|
+
|
|
63
|
+
@staticmethod
|
|
64
|
+
def str_to_date(date_str: str, format_str: str = "%Y-%m-%d") -> date | None:
|
|
65
|
+
"""字符串转date对象
|
|
66
|
+
:param date_str: 日期字符串(如"2025-01-01")
|
|
67
|
+
:param format_str: 字符串格式(默认"%Y-%m-%d")
|
|
68
|
+
"""
|
|
69
|
+
try:
|
|
70
|
+
return datetime.strptime(date_str, format_str).date()
|
|
71
|
+
except ValueError as e:
|
|
72
|
+
print(f"❌ 日期字符串格式错误:{date_str},预期格式:{format_str},错误:{e}")
|
|
73
|
+
return None
|
|
74
|
+
|
|
75
|
+
# ==================== 3. 生成日期范围 ====================
|
|
76
|
+
@staticmethod
|
|
77
|
+
def get_last_n_days(n: int, include_today: bool = True) -> list[date]:
|
|
78
|
+
"""获取近N天的日期列表
|
|
79
|
+
:param include_today: 是否包含今天(默认True)
|
|
80
|
+
"""
|
|
81
|
+
today = date.today()
|
|
82
|
+
if include_today:
|
|
83
|
+
return [today - timedelta(days=i) for i in range(n)]
|
|
84
|
+
return [today - timedelta(days=i+1) for i in range(n)]
|
|
85
|
+
|
|
86
|
+
@staticmethod
|
|
87
|
+
def get_date_range(
|
|
88
|
+
start_date: date | str,
|
|
89
|
+
end_date: date | str,
|
|
90
|
+
format_str: str = "%Y-%m-%d"
|
|
91
|
+
) -> list[date]:
|
|
92
|
+
"""获取起止日期之间的所有日期(包含两端)"""
|
|
93
|
+
# 处理字符串输入
|
|
94
|
+
if isinstance(start_date, str):
|
|
95
|
+
start_date = DateUtils.str_to_date(start_date, format_str)
|
|
96
|
+
if isinstance(end_date, str):
|
|
97
|
+
end_date = DateUtils.str_to_date(end_date, format_str)
|
|
98
|
+
|
|
99
|
+
if not start_date or not end_date or start_date > end_date:
|
|
100
|
+
print("❌ 起止日期无效或起始日期大于结束日期")
|
|
101
|
+
return []
|
|
102
|
+
|
|
103
|
+
delta = end_date - start_date
|
|
104
|
+
return [start_date + timedelta(days=i) for i in range(delta.days + 1)]
|
|
105
|
+
|
|
106
|
+
@staticmethod
|
|
107
|
+
def get_days_in_month(year: int, month: int) -> list[date]:
|
|
108
|
+
"""获取指定年月的所有日期"""
|
|
109
|
+
try:
|
|
110
|
+
days_count = calendar.monthrange(year, month)[1] # 获取当月天数
|
|
111
|
+
return [date(year, month, day) for day in range(1, days_count + 1)]
|
|
112
|
+
except ValueError as e:
|
|
113
|
+
print(f"❌ 无效年月:{year}-{month},错误:{e}")
|
|
114
|
+
return []
|
|
115
|
+
|
|
116
|
+
# ==================== 4. 生成随机日期 ====================
|
|
117
|
+
@staticmethod
|
|
118
|
+
def get_random_date(
|
|
119
|
+
start_date: date | str,
|
|
120
|
+
end_date: date | str,
|
|
121
|
+
format_str: str = "%Y-%m-%d"
|
|
122
|
+
) -> date | None:
|
|
123
|
+
"""生成指定范围内的随机日期"""
|
|
124
|
+
date_range = DateUtils.get_date_range(start_date, end_date, format_str)
|
|
125
|
+
if not date_range:
|
|
126
|
+
return None
|
|
127
|
+
return random.choice(date_range)
|
|
128
|
+
|
|
129
|
+
@staticmethod
|
|
130
|
+
def get_random_datetime(
|
|
131
|
+
start_dt: datetime | str,
|
|
132
|
+
end_dt: datetime | str,
|
|
133
|
+
format_str: str = "%Y-%m-%d %H:%M:%S"
|
|
134
|
+
) -> datetime | None:
|
|
135
|
+
"""生成指定范围内的随机datetime"""
|
|
136
|
+
# 处理字符串输入
|
|
137
|
+
if isinstance(start_dt, str):
|
|
138
|
+
try:
|
|
139
|
+
start_dt = datetime.strptime(start_dt, format_str)
|
|
140
|
+
except ValueError:
|
|
141
|
+
print(f"❌ 起始时间格式错误:{start_dt}")
|
|
142
|
+
return None
|
|
143
|
+
if isinstance(end_dt, str):
|
|
144
|
+
try:
|
|
145
|
+
end_dt = datetime.strptime(end_dt, format_str)
|
|
146
|
+
except ValueError:
|
|
147
|
+
print(f"❌ 结束时间格式错误:{end_dt}")
|
|
148
|
+
return None
|
|
149
|
+
|
|
150
|
+
if start_dt >= end_dt:
|
|
151
|
+
print("❌ 起始时间大于等于结束时间")
|
|
152
|
+
return None
|
|
153
|
+
|
|
154
|
+
delta = end_dt - start_dt
|
|
155
|
+
random_seconds = random.randint(0, int(delta.total_seconds()))
|
|
156
|
+
return start_dt + timedelta(seconds=random_seconds)
|
|
157
|
+
|
|
158
|
+
# ==================== 5. 日期计算 ====================
|
|
159
|
+
@staticmethod
|
|
160
|
+
def add_days(base_date: date | datetime, days: int) -> date | datetime:
|
|
161
|
+
"""日期加N天"""
|
|
162
|
+
return base_date + timedelta(days=days)
|
|
163
|
+
|
|
164
|
+
@staticmethod
|
|
165
|
+
def subtract_days(base_date: date | datetime, days: int) -> date | datetime:
|
|
166
|
+
"""日期减N天"""
|
|
167
|
+
return base_date - timedelta(days=days)
|
|
168
|
+
|
|
169
|
+
@staticmethod
|
|
170
|
+
def get_month_start(year: int | None = None, month: int | None = None) -> date:
|
|
171
|
+
"""获取指定年月的月初日期(默认当前年月)"""
|
|
172
|
+
year = year or date.today().year
|
|
173
|
+
month = month or date.today().month
|
|
174
|
+
return date(year, month, 1)
|
|
175
|
+
|
|
176
|
+
@staticmethod
|
|
177
|
+
def get_month_end(year: int | None = None, month: int | None = None) -> date:
|
|
178
|
+
"""获取指定年月的月末日期(默认当前年月)"""
|
|
179
|
+
year = year or date.today().year
|
|
180
|
+
month = month or date.today().month
|
|
181
|
+
days_count = calendar.monthrange(year, month)[1]
|
|
182
|
+
return date(year, month, days_count)
|
|
183
|
+
|
|
184
|
+
@staticmethod
|
|
185
|
+
def get_quarter_start(year: int | None = None, quarter: int | None = None) -> date:
|
|
186
|
+
"""获取指定年季度的起始日期(默认当前年当前季度)
|
|
187
|
+
:param quarter: 季度(1-4)
|
|
188
|
+
"""
|
|
189
|
+
year = year or date.today().year
|
|
190
|
+
quarter = quarter or (date.today().month - 1) // 3 + 1
|
|
191
|
+
start_month = (quarter - 1) * 3 + 1
|
|
192
|
+
return date(year, start_month, 1)
|
|
193
|
+
|
|
194
|
+
# ==================== 6. 日期格式化 ====================
|
|
195
|
+
@staticmethod
|
|
196
|
+
def date_to_str(date_obj: date | datetime, format_str: str = "%Y-%m-%d") -> str:
|
|
197
|
+
"""日期/时间对象转字符串"""
|
|
198
|
+
return date_obj.strftime(format_str)
|
|
199
|
+
|
|
200
|
+
# ==================== 7. 时区转换 ====================
|
|
201
|
+
@staticmethod
|
|
202
|
+
def convert_timezone(dt: datetime, from_tz: str, to_tz: str) -> datetime:
|
|
203
|
+
"""转换时区
|
|
204
|
+
:param dt: 原始时间对象(若无时区信息,需指定from_tz)
|
|
205
|
+
:param from_tz: 原始时区
|
|
206
|
+
:param to_tz: 目标时区
|
|
207
|
+
"""
|
|
208
|
+
if not dt.tzinfo:
|
|
209
|
+
dt = dt.replace(tzinfo=ZoneInfo(from_tz))
|
|
210
|
+
return dt.astimezone(ZoneInfo(to_tz))
|
|
211
|
+
|
|
212
|
+
# ==================== 原有基础功能(省略,保持之前的方法) ====================
|
|
213
|
+
# (注:若需完整代码,需包含之前的get_current_date、get_specified_date等方法)
|
|
214
|
+
|
|
215
|
+
# ==================== 新增:时间戳相关功能 ====================
|
|
216
|
+
@staticmethod
|
|
217
|
+
def get_current_timestamp(precision: str = "millisecond") -> int:
|
|
218
|
+
"""获取当前时间戳
|
|
219
|
+
:param precision: 精度("second"=秒级,"millisecond"=毫秒级)
|
|
220
|
+
:return: 时间戳整数
|
|
221
|
+
"""
|
|
222
|
+
now = datetime.now()
|
|
223
|
+
if precision == "millisecond":
|
|
224
|
+
return int(now.timestamp() * 1000)
|
|
225
|
+
return int(now.timestamp())
|
|
226
|
+
|
|
227
|
+
@staticmethod
|
|
228
|
+
def get_utc_timestamp(precision: str = "second") -> int:
|
|
229
|
+
"""获取当前UTC时间戳
|
|
230
|
+
:param precision: 精度("second"=秒级,"millisecond"=毫秒级)
|
|
231
|
+
:return: UTC时间戳整数
|
|
232
|
+
"""
|
|
233
|
+
utc_now = datetime.utcnow()
|
|
234
|
+
if precision == "millisecond":
|
|
235
|
+
return int(utc_now.timestamp() * 1000)
|
|
236
|
+
return int(utc_now.timestamp())
|
|
237
|
+
|
|
238
|
+
@staticmethod
|
|
239
|
+
def timestamp_to_datetime(
|
|
240
|
+
timestamp: int,
|
|
241
|
+
precision: str = "second",
|
|
242
|
+
tz: str | None = None
|
|
243
|
+
) -> datetime | None:
|
|
244
|
+
"""时间戳转datetime对象(支持时区)
|
|
245
|
+
:param timestamp: 时间戳(整数)
|
|
246
|
+
:param precision: 时间戳精度("second"=秒级,"millisecond"=毫秒级)
|
|
247
|
+
:param tz: 时区(如"Asia/Shanghai",None则为本地时区)
|
|
248
|
+
:return: datetime对象(带时区信息,若指定tz)
|
|
249
|
+
"""
|
|
250
|
+
try:
|
|
251
|
+
# 转换毫秒级时间戳为秒级
|
|
252
|
+
ts = timestamp / 1000 if precision == "millisecond" else timestamp
|
|
253
|
+
dt = datetime.fromtimestamp(ts)
|
|
254
|
+
# 设置时区
|
|
255
|
+
if tz:
|
|
256
|
+
dt = dt.replace(tzinfo=ZoneInfo(tz))
|
|
257
|
+
return dt
|
|
258
|
+
except ValueError as e:
|
|
259
|
+
print(f"❌ 无效时间戳:{timestamp},错误:{e}")
|
|
260
|
+
return None
|
|
261
|
+
|
|
262
|
+
@staticmethod
|
|
263
|
+
def datetime_to_timestamp(
|
|
264
|
+
dt: datetime,
|
|
265
|
+
precision: str = "second"
|
|
266
|
+
) -> int | None:
|
|
267
|
+
"""datetime对象转时间戳
|
|
268
|
+
:param dt: datetime对象(若带时区,会自动转换为UTC时间戳)
|
|
269
|
+
:param precision: 精度("second"=秒级,"millisecond"=毫秒级)
|
|
270
|
+
:return: 时间戳整数
|
|
271
|
+
"""
|
|
272
|
+
try:
|
|
273
|
+
ts = dt.timestamp()
|
|
274
|
+
return int(ts * 1000) if precision == "millisecond" else int(ts)
|
|
275
|
+
except ValueError as e:
|
|
276
|
+
print(f"❌ 无效datetime对象:{dt},错误:{e}")
|
|
277
|
+
return None
|
|
278
|
+
|
|
279
|
+
@staticmethod
|
|
280
|
+
def timestamp_to_str(
|
|
281
|
+
timestamp: int,
|
|
282
|
+
format_str: str = "%Y-%m-%d %H:%M:%S",
|
|
283
|
+
precision: str = "second",
|
|
284
|
+
tz: str | None = None
|
|
285
|
+
) -> str | None:
|
|
286
|
+
"""时间戳转格式化字符串
|
|
287
|
+
:param timestamp: 时间戳(整数)
|
|
288
|
+
:param format_str: 输出格式(默认"%Y-%m-%d %H:%M:%S")
|
|
289
|
+
:param precision: 时间戳精度("second"=秒级,"millisecond"=毫秒级)
|
|
290
|
+
:param tz: 时区(如"Asia/Shanghai")
|
|
291
|
+
:return: 格式化日期字符串
|
|
292
|
+
"""
|
|
293
|
+
dt = DateUtils.timestamp_to_datetime(timestamp, precision, tz)
|
|
294
|
+
if dt:
|
|
295
|
+
return dt.strftime(format_str)
|
|
296
|
+
return None
|
|
297
|
+
|
|
298
|
+
@staticmethod
|
|
299
|
+
def str_to_timestamp(
|
|
300
|
+
date_str: str,
|
|
301
|
+
format_str: str = "%Y-%m-%d %H:%M:%S",
|
|
302
|
+
precision: str = "second",
|
|
303
|
+
tz: str | None = None
|
|
304
|
+
) -> int | None:
|
|
305
|
+
"""格式化字符串转时间戳
|
|
306
|
+
:param date_str: 日期字符串(如"2025-01-01 12:00:00")
|
|
307
|
+
:param format_str: 输入格式(默认"%Y-%m-%d %H:%M:%S")
|
|
308
|
+
:param precision: 输出精度("second"=秒级,"millisecond"=毫秒级)
|
|
309
|
+
:param tz: 时区(如"Asia/Shanghai",None则为本地时区)
|
|
310
|
+
:return: 时间戳整数
|
|
311
|
+
"""
|
|
312
|
+
try:
|
|
313
|
+
dt = datetime.strptime(date_str, format_str)
|
|
314
|
+
if tz:
|
|
315
|
+
dt = dt.replace(tzinfo=ZoneInfo(tz))
|
|
316
|
+
return DateUtils.datetime_to_timestamp(dt, precision)
|
|
317
|
+
except ValueError as e:
|
|
318
|
+
print(f"❌ 日期字符串格式错误:{date_str},错误:{e}")
|
|
319
|
+
return None
|
|
320
|
+
|
|
321
|
+
@staticmethod
|
|
322
|
+
def get_random_timestamp(
|
|
323
|
+
start_ts: int,
|
|
324
|
+
end_ts: int,
|
|
325
|
+
precision: str = "second"
|
|
326
|
+
) -> int | None:
|
|
327
|
+
"""生成指定时间戳范围内的随机时间戳
|
|
328
|
+
:param start_ts: 起始时间戳
|
|
329
|
+
:param end_ts: 结束时间戳
|
|
330
|
+
:param precision: 精度("second"=秒级,"millisecond"=毫秒级)
|
|
331
|
+
:return: 随机时间戳
|
|
332
|
+
"""
|
|
333
|
+
if start_ts >= end_ts:
|
|
334
|
+
print("❌ 起始时间戳大于等于结束时间戳")
|
|
335
|
+
return None
|
|
336
|
+
# 毫秒级需处理范围
|
|
337
|
+
multiplier = 1000 if precision == "millisecond" else 1
|
|
338
|
+
random_ts = random.randint(start_ts * multiplier, end_ts * multiplier)
|
|
339
|
+
return random_ts if precision == "millisecond" else random_ts // multiplier
|
|
@@ -4,19 +4,20 @@ smartpush/account/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU
|
|
|
4
4
|
smartpush/account/operate_account.py,sha256=nzJLLAEwNElavZeWVqnA_MSGTBzQrSrknmezYBwtvWs,1525
|
|
5
5
|
smartpush/base/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
6
|
smartpush/base/faker_data.py,sha256=TOd5EKVImxZpsKEW_dtKa2iqiUGqU7OBkOM8pvqKVUc,24643
|
|
7
|
-
smartpush/base/request_base.py,sha256=
|
|
8
|
-
smartpush/base/url_enum.py,sha256=
|
|
7
|
+
smartpush/base/request_base.py,sha256=eMF6zw0QKzIMQhg3LNqutaCNTQJ5lsrHuohkaadOl64,2677
|
|
8
|
+
smartpush/base/url_enum.py,sha256=7JRE61JPvwaGfnbZHhFohhxqS758CSiAPk-wwlLyy78,2232
|
|
9
9
|
smartpush/crowd/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
10
|
-
smartpush/crowd/crowd.py,sha256=
|
|
10
|
+
smartpush/crowd/crowd.py,sha256=7FNtTVfW8Okf-RU1n5O9olFv7aSKxBVHigZ6zi9MFKw,21107
|
|
11
11
|
smartpush/email/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
12
|
-
smartpush/email/
|
|
13
|
-
smartpush/email/
|
|
12
|
+
smartpush/email/activity.py,sha256=LXssDWgxnPEcqnmlr7kiSVP4YbNhP4SDJep6Qc49bcM,3481
|
|
13
|
+
smartpush/email/schema.py,sha256=cB9WAjdLbesfnMGnu6gB7JJU0Ja4WJB8-s_jz5DaCYg,30927
|
|
14
|
+
smartpush/email/universal_content.py,sha256=TwbhvtybbegkUzuvh9WE3SOnPs15PQu-mXQUCz2iMuw,10180
|
|
14
15
|
smartpush/export/__init__.py,sha256=D9GbWcmwnetEndFDty5XbVienFK1WjqV2yYcQp3CM84,99
|
|
15
16
|
smartpush/export/basic/ExcelExportChecker.py,sha256=YqWmDGSFadQdK2vNJ070Qvad9ZtqEwiQyPkOemlACfs,21508
|
|
16
17
|
smartpush/export/basic/GetOssUrl.py,sha256=zxNZj6x7Ph9N3P5k82pLpBFjZxKrDfbgqS2fTYyhvso,8467
|
|
17
18
|
smartpush/export/basic/ReadExcel.py,sha256=SbVRBKd4Y1UPUXXnvyQ3J6WtZLinwTCYpBos1D6_wtU,8921
|
|
18
19
|
smartpush/export/basic/__init__.py,sha256=6tcrS-2NSlsJo-UwEsnGUmwCf7jgOsh_UEbM0FD-gYE,70
|
|
19
|
-
smartpush/flow/MockFlow.py,sha256=
|
|
20
|
+
smartpush/flow/MockFlow.py,sha256=L-T_x8hieHs1nPDsP2BUSa6q4pVuiURVN2xP9njakzs,7943
|
|
20
21
|
smartpush/flow/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
21
22
|
smartpush/flow/global_flow.py,sha256=D1hCpO5fsWTEfl4yNpi3O8PFhz_UmKLHSIQhKtypkvU,2817
|
|
22
23
|
smartpush/flow/history_flow.py,sha256=Gds1FI6i9ue-n1WwpNgL2JlQ2PhSR8VK7dvZ9JemZKw,4854
|
|
@@ -27,11 +28,12 @@ smartpush/form/form_before.py,sha256=CCvAC_2yWPlnQGtjEA8LPLy9853Nq3nNjcL2GewFWIs
|
|
|
27
28
|
smartpush/form/form_client_operation.py,sha256=gg-5uHXCyMa_ypBSYPYFVxXdwZdYBJsNtUCqayknMBw,303
|
|
28
29
|
smartpush/utils/DataTypeUtils.py,sha256=BC7ioztO3vAfKd1EOoNvXdVuXYY8qjNskV1DP7LhW-M,1082
|
|
29
30
|
smartpush/utils/EmailUtlis.py,sha256=DAHd73bJ8hiJCLEXtD0xcwxPD7SOPSmBB7Jvlf6gN6s,11201
|
|
30
|
-
smartpush/utils/ListDictUtils.py,sha256=
|
|
31
|
+
smartpush/utils/ListDictUtils.py,sha256=6jIngO_4vARkKTKRb9ZW4wR7zPkYbc1wiMLF7ySZQXc,3191
|
|
31
32
|
smartpush/utils/StringUtils.py,sha256=n8mo9k0JQN63MReImgv-66JxmmymOGknR8pH2fkQrAo,4139
|
|
32
33
|
smartpush/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
34
|
+
smartpush/utils/date_utils.py,sha256=Xgx2DbmWYri71xXBiaKYTZDeh2a8MFhYns_xB0U2JOA,13159
|
|
33
35
|
smartpush/utils/form_utils.py,sha256=ld-g_Dm_ZlnagQt7imYfUc87bcBRVlTctywuLtzmjXQ,849
|
|
34
|
-
smartpush-1.9.
|
|
35
|
-
smartpush-1.9.
|
|
36
|
-
smartpush-1.9.
|
|
37
|
-
smartpush-1.9.
|
|
36
|
+
smartpush-1.9.9.dist-info/METADATA,sha256=fy3DQCfKtpyrckWnVSd4mNNjxLnRNjeENPVnQ_xAf4o,131
|
|
37
|
+
smartpush-1.9.9.dist-info/WHEEL,sha256=tZoeGjtWxWRfdplE7E3d45VPlLNQnvbKiYnx7gwAy8A,92
|
|
38
|
+
smartpush-1.9.9.dist-info/top_level.txt,sha256=5_CXqu08EfbPaKLjuSAOAqCmGU6shiatwDU_ViBGCmg,10
|
|
39
|
+
smartpush-1.9.9.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|