smartpush 1.9.8__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.
@@ -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
@@ -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 URL(Enum):
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
- :type:表单报告
13
- """
14
- pageFormReportDetail = '/formReport/detail/pageFormReportDetail', 'POST' # 获取表单收集数据
15
- getFormReportDetail = '/formReport/getFormReportDetail', 'POST' # 获取表单报告数据(曝光/点击)
16
- getFormPerformanceTrend = 'formReport/getFormPerformanceTrend', 'POST'
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
- :type 群组
20
- """
21
- editCrowdPackage = '/crowdPackage/editCrowdPackage', 'POST'
22
- crowdPersonListInPackage = '/crowdPackage/crowdPersonList', 'POST'
23
- crowdPackageDetail = '/crowdPackage/detail', 'POST'
24
- crowdPackageList = '/crowdPackage/list', 'POST'
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
- :type 素材收藏
35
- """
36
- saveUniversalContent = "/universalContent/saveUniversalContent", 'POST'
37
- deleteUniversalContent = "/universalContent/deleteUniversalContent", 'GET'
38
- updateUniversalContent = "/universalContent/updateUniversalContent", 'POST'
39
- queryUniversalContent = "/universalContent/query", 'POST'
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
- @property
44
- def method(self):
45
- return self.value[1]
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
- @property
48
- def url(self):
49
- return self.value[0]
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.method)
54
- print(URL.getFormReportDetail.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
@@ -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
 
@@ -86,7 +86,7 @@ class CrowdList(RequestBase):
86
86
  :return:
87
87
  """
88
88
  requestParam = {"page": page, "pageSize": pageSize}
89
- 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,
90
90
  data=requestParam)
91
91
  resultData = result['resultData']
92
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 != BlockSchema.Section]
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(json.dumps(genAllBlockSchema()))
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)
@@ -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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: smartpush
3
- Version: 1.9.8
3
+ Version: 1.9.9
4
4
  Summary: 用于smartpush自动化测试工具包
5
5
  Author: lulu、felix、long
6
6
 
@@ -4,13 +4,14 @@ 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=mg0dSm6ucPdwqSUn_tQzynYWYKf0_vX7kcGJWSsX3e0,2455
8
- smartpush/base/url_enum.py,sha256=GC8lXuXflKFl3xbeOIzS3vgE7BF2Zo5H9pn6okgHPyQ,1627
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=-UrOLAhWaYXUk3V2ezy4xkYDxJAmKD1KtccSrPm5PQY,21059
10
+ smartpush/crowd/crowd.py,sha256=7FNtTVfW8Okf-RU1n5O9olFv7aSKxBVHigZ6zi9MFKw,21107
11
11
  smartpush/email/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
12
- smartpush/email/schema.py,sha256=Gf9tYNP9UjkbXpPbh_uAcAx7QZ5RNs-E7GPVp1BU9d8,23478
13
- smartpush/email/universal_content.py,sha256=By8ZzhPEvTYuktPfh_wRwRSYnntwoV5qJd3mXzkrSg4,9063
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
@@ -30,8 +31,9 @@ smartpush/utils/EmailUtlis.py,sha256=DAHd73bJ8hiJCLEXtD0xcwxPD7SOPSmBB7Jvlf6gN6s
30
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.8.dist-info/METADATA,sha256=ocrGVX8kR0kE0c9y61_E9wck8yNGFExrAuK9XApli2c,131
35
- smartpush-1.9.8.dist-info/WHEEL,sha256=tZoeGjtWxWRfdplE7E3d45VPlLNQnvbKiYnx7gwAy8A,92
36
- smartpush-1.9.8.dist-info/top_level.txt,sha256=5_CXqu08EfbPaKLjuSAOAqCmGU6shiatwDU_ViBGCmg,10
37
- smartpush-1.9.8.dist-info/RECORD,,
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,,