enconia-utils 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,71 @@
1
+ """工具函数集合"""
2
+
3
+ from .aliyun_sls_log import send_log_to_sls
4
+ from .feishu_push import FeiShuTalkChatBot, NotifyLevel
5
+ from .id_generator import (
6
+ get_random_short_id,
7
+ get_fix_short_id,
8
+ get_random_long_id,
9
+ get_fix_long_id,
10
+ get_random_uuid,
11
+ get_fix_uuid,
12
+ get_file_md5,
13
+ )
14
+ from .util_date import (
15
+ DateTimeFormat,
16
+ DateMaxMin,
17
+ DateEncoder,
18
+ get_orbit_std_datetime,
19
+ get_orbit_std_datetime_utc,
20
+ get_orbit_std_date,
21
+ get_orbit_std_date_utc,
22
+ get_date_range_by_base,
23
+ get_date_range_list_v1,
24
+ get_date_range_list_v2,
25
+ get_next_day,
26
+ get_next_workday_cn,
27
+ )
28
+ from .util_oss import OssCloudObjectProvider
29
+ from .util_type_mapping import (
30
+ ExtenCons,
31
+ content2file_type,
32
+ file2content_type,
33
+ get_content_type_4_filename,
34
+ )
35
+
36
+ __version__ = "0.1.0"
37
+ __all__ = [
38
+ # aliyun_sls_log
39
+ "send_log_to_sls",
40
+ # feishu_push
41
+ "FeiShuTalkChatBot",
42
+ "NotifyLevel",
43
+ # id_generator
44
+ "get_random_short_id",
45
+ "get_fix_short_id",
46
+ "get_random_long_id",
47
+ "get_fix_long_id",
48
+ "get_random_uuid",
49
+ "get_fix_uuid",
50
+ "get_file_md5",
51
+ # util_date
52
+ "DateTimeFormat",
53
+ "DateMaxMin",
54
+ "DateEncoder",
55
+ "get_orbit_std_datetime",
56
+ "get_orbit_std_datetime_utc",
57
+ "get_orbit_std_date",
58
+ "get_orbit_std_date_utc",
59
+ "get_date_range_by_base",
60
+ "get_date_range_list_v1",
61
+ "get_date_range_list_v2",
62
+ "get_next_day",
63
+ "get_next_workday_cn",
64
+ # util_oss
65
+ "OssCloudObjectProvider",
66
+ # util_type_mapping
67
+ "ExtenCons",
68
+ "content2file_type",
69
+ "file2content_type",
70
+ "get_content_type_4_filename",
71
+ ]
@@ -0,0 +1,133 @@
1
+ """
2
+ 集成阿里云SLS日志服务
3
+
4
+ 使用说明:
5
+ 1. 安装依赖:
6
+ pip install aliyun-log-python-sdk
7
+
8
+ 2. 配置阿里云SLS:
9
+ - 在阿里云控制台创建Project和Logstore
10
+ - 获取AccessKey ID和AccessKey Secret
11
+ - 设置环境变量或修改下方配置:
12
+ ALIBABA_CLOUD_ACCESS_KEY_ID=你的AccessKeyID
13
+ ALIBABA_CLOUD_ACCESS_KEY_SECRET=你的AccessKeySecret
14
+
15
+ 3. 配置信息获取:
16
+ - Endpoint: 在SLS控制台查看,如 cn-hangzhou.log.aliyuncs.com
17
+ - Project: 在SLS控制台创建的项目名称
18
+ - Logstore: 在项目下创建的日志库名称
19
+
20
+ 4. 验证配置:
21
+ - 运行脚本后查看SLS控制台是否收到日志
22
+ - 检查本地日志文件 logs/*.log
23
+
24
+ 5. 查看日志:
25
+ - SLS控制台: https://sls.console.aliyun.com
26
+ - 本地日志: logs/*.log
27
+
28
+ 当前实现:
29
+ - 使用阿里云官方SLS Python SDK写入日志
30
+ - 支持SDK和HTTP API两种发送方式(自动降级)
31
+ - 所有关键操作都会同时记录到本地日志和SLS
32
+ """
33
+ import logging
34
+
35
+ import requests
36
+ import time
37
+ import os
38
+ from datetime import datetime
39
+ from aliyun.log import LogClient, LogItem, PutLogsRequest
40
+
41
+ # 阿里云SLS日志服务配置
42
+ # try:
43
+ # from aliyun.log import LogClient, LogItem, PutLogsRequest
44
+ # SLS_SDK_AVAILABLE = True
45
+ # except ImportError:
46
+ # SLS_SDK_AVAILABLE = False
47
+ # logging.warning("阿里云SLS SDK未安装,将使用HTTP API方式发送日志")
48
+
49
+
50
+ # 阿里云SLS配置信息 - 请根据实际情况修改
51
+ SLS_SDK_AVAILABLE = True
52
+ SLS_ENDPOINT = 'cn-shanghai.log.aliyuncs.com' # 您的日志服务Endpoint
53
+ SLS_PROJECT = 'mineru-logs' # 您的Project名称
54
+ SLS_LOGSTORE = 'mineru-3log' # 您的Logstore名称
55
+ SLS_ACCESS_KEY_ID = os.environ.get('ALIBABA_CLOUD_ACCESS_KEY_ID', 'LTAI5tLtyiy18UAWL626k1wC')
56
+ SLS_ACCESS_KEY_SECRET = os.environ.get('ALIBABA_CLOUD_ACCESS_KEY_SECRET', 'T6VSV1QFn7hsc8wjrOGG8K4meDyXbX')
57
+
58
+ logger = logging.getLogger(__name__)
59
+
60
+ def send_log_to_sls(script_name,level, message, **kwargs):
61
+ """发送日志到阿里云SLS"""
62
+ try:
63
+ if SLS_SDK_AVAILABLE:
64
+ # 使用SDK方式发送日志
65
+ client = LogClient(SLS_ENDPOINT, SLS_ACCESS_KEY_ID, SLS_ACCESS_KEY_SECRET)
66
+
67
+ # 创建日志项
68
+ log_item = LogItem()
69
+ log_item.set_time(int(time.time()))
70
+
71
+ # 添加日志内容
72
+ log_item.set_contents([
73
+ ('level', level),
74
+ ('message', message),
75
+ ('timestamp', datetime.now().isoformat()),
76
+ ('script', script_name),
77
+ ('extra_info', str(kwargs) if kwargs else '')
78
+ ])
79
+
80
+ # 构建请求
81
+ request = PutLogsRequest(SLS_PROJECT, SLS_LOGSTORE, '', '', [log_item])
82
+
83
+ # 发送日志
84
+ response = client.put_logs(request)
85
+
86
+ # put_logs在成功时不会抛出异常,如果失败会抛出异常
87
+ logger.info(f"SLS日志发送成功: {message}")
88
+
89
+ else:
90
+ # 使用HTTP API方式发送日志
91
+ send_log_via_http(script_name,level, message, **kwargs)
92
+
93
+ except Exception as e:
94
+ logger.error(f"SLS日志发送异常: {str(e)}")
95
+ # 如果SDK方式失败,尝试使用HTTP方式
96
+ try:
97
+ send_log_via_http(script_name,level, message, **kwargs)
98
+ except Exception as http_e:
99
+ logger.error(f"HTTP方式也发送失败: {str(http_e)}")
100
+
101
+
102
+ def send_log_via_http(script_name, level, message, **kwargs):
103
+ """通过HTTP API发送日志到SLS"""
104
+ try:
105
+
106
+ # 构建日志数据
107
+ log_data = {
108
+ 'level': level,
109
+ 'message': message,
110
+ 'timestamp': datetime.now().isoformat(),
111
+ 'script': script_name,
112
+ 'extra_info': kwargs if kwargs else {}
113
+ }
114
+
115
+ # 构建请求头
116
+ headers = {
117
+ 'Content-Type': 'application/json',
118
+ 'x-log-apiversion': '0.6.0',
119
+ 'x-log-signaturemethod': 'hmac-sha1'
120
+ }
121
+
122
+ # 发送HTTP请求到SLS API
123
+ url = f"https://{SLS_ENDPOINT}/logstores/{SLS_LOGSTORE}/shards/lb"
124
+
125
+ response = requests.post(url, headers=headers, json=log_data, timeout=5)
126
+
127
+ if response.status_code == 200:
128
+ logger.info(f"HTTP方式SLS日志发送成功: {message}")
129
+ else:
130
+ logger.error(f"HTTP方式SLS日志发送失败: 状态码={response.status_code}, 响应={response.text[:200]}")
131
+
132
+ except Exception as e:
133
+ logger.error(f"HTTP方式SLS日志发送异常: {str(e)}")
@@ -0,0 +1,173 @@
1
+ import requests
2
+ import json
3
+ import time
4
+ import logging
5
+ from enum import Enum
6
+ from typing import Optional, List, Dict
7
+
8
+ try:
9
+ JSONDecodeError = json.decoder.JSONDecodeError
10
+ except AttributeError:
11
+ JSONDecodeError = ValueError
12
+
13
+
14
+ class NotifyLevel(Enum):
15
+ SUCCESS = 'green'
16
+ INFO = 'wathet'
17
+ WARNING = 'yellow'
18
+ DANGER = 'red'
19
+
20
+
21
+ class FeiShuTalkChatBot:
22
+ def __init__(self, webhook, secret=None, pc_slide=False, fail_notice=False):
23
+ '''
24
+ 机器人初始化
25
+ Reference doc: https://open.feishu.cn/document/client-docs/bot-v3/add-custom-bot
26
+
27
+ :param webhook: 飞书群自定义机器人webhook地址
28
+ :param secret: 机器人安全设置页面勾选“加签”时需要传入的密钥
29
+ :param pc_slide: 消息链接打开方式,默认False为浏览器打开,设置为True时为PC端侧边栏打开
30
+ :param fail_notice: 消息发送失败提醒,默认为False不提醒,开发者可以根据返回的消息发送结果自行判断和处理
31
+ '''
32
+ super(FeiShuTalkChatBot, self).__init__()
33
+ self.headers = {'Content-Type': 'application/json; charset=utf-8'}
34
+ self.webhook = webhook
35
+ self.secret = secret
36
+ self.pc_slide = pc_slide
37
+ self.fail_notice = fail_notice
38
+
39
+ def success(self, message, title: str = 'Successfully', ats: Optional[List] = None, action=None):
40
+ self.send_text(title=title, message=message, level=NotifyLevel.SUCCESS.value, ats=ats, action=action)
41
+
42
+ def info(self, message, title: str = 'Notification', ats: Optional[List] = None, action=None):
43
+ self.send_text(title=title, message=message, level=NotifyLevel.INFO.value, ats=ats, action=action)
44
+
45
+ def warning(self, message, title: str = 'Warning', ats: Optional[List] = None, action=None):
46
+ self.send_text(title=title, message=message, level=NotifyLevel.WARNING.value, ats=ats, action=action)
47
+
48
+ def error(self, message, title: str = 'Error', ats: Optional[List] = None, action=None):
49
+ self.send_text(title=title, message=message, level=NotifyLevel.DANGER.value, ats=ats, action=action)
50
+
51
+ def send_text(self, title, message, level, ats: Optional[List] = None, action: Optional[Dict] = None):
52
+ """
53
+ :param title:
54
+ :param message:
55
+ :param level:
56
+ :param ats:
57
+ :param action: { "direction": "DOWN", actions: {} }
58
+ :return:
59
+ """
60
+ data = {
61
+ "msg_type": "interactive",
62
+ "card": {
63
+ "elements": [
64
+ {
65
+ "tag": "hr"
66
+ },
67
+ {
68
+ "tag": "div",
69
+ "text": {
70
+ "tag": "lark_md",
71
+ "content": message
72
+ }
73
+ }
74
+ ],
75
+ "header": {
76
+ "title": {
77
+ "content": title,
78
+ "tag": "plain_text"
79
+ },
80
+ "template": level
81
+ }
82
+ }
83
+ }
84
+
85
+ # Put @ people
86
+ if ats:
87
+ if len(ats) == 1 and ats[0] == "all":
88
+ data["card"]["elements"].append({
89
+ "tag": "div",
90
+ "text": {
91
+ "tag": "lark_md",
92
+ "content": "At <at id=all></at>",
93
+ }
94
+ })
95
+ elif len(ats) > 0:
96
+ at_list_template = []
97
+ for at in ats:
98
+ at_list_template.append({
99
+ "tag": "div",
100
+ "text": {
101
+ "tag": "lark_md",
102
+ "content": "At <at id=" + at + "></at>",
103
+ }
104
+ })
105
+ data["card"]["elements"].extend(at_list_template)
106
+ else:
107
+ raise Exception("Invalid ats.")
108
+
109
+ if action:
110
+ if action["direction"] == "UP":
111
+ direction = "UP"
112
+ else:
113
+ direction = "DOWN"
114
+
115
+ if direction == "DOWN":
116
+ data["card"]["elements"].append({"tag": "hr"})
117
+ data["card"]["elements"].append(action["actions"])
118
+ if direction == "UP":
119
+ data["card"]["elements"].insert(0, action["actions"])
120
+
121
+ return self.post(data)
122
+
123
+ def post(self, data):
124
+ """
125
+ 发送消息(内容UTF-8编码)
126
+ :param data: 消息数据(字典)
127
+ :return: 返回消息发送结果
128
+ """
129
+ try:
130
+ post_data = json.dumps(data)
131
+ response = requests.post(self.webhook, headers=self.headers, data=post_data, verify=False)
132
+
133
+ except requests.exceptions.HTTPError as exc:
134
+ logging.error("消息发送失败, HTTP error: %d, reason: %s" % (exc.response.status_code, exc.response.reason))
135
+ raise
136
+ except requests.exceptions.ConnectionError:
137
+ logging.error("消息发送失败,HTTP connection error!")
138
+ raise
139
+ except requests.exceptions.Timeout:
140
+ logging.error("消息发送失败,Timeout error!")
141
+ raise
142
+ except requests.exceptions.RequestException:
143
+ logging.error("消息发送失败, Request Exception!")
144
+ raise
145
+ else:
146
+ try:
147
+ result = response.json()
148
+ except JSONDecodeError:
149
+ logging.error("服务器响应异常,状态码:%s,响应内容:%s" % (response.status_code, response.text))
150
+ return {'errcode': 500, 'errmsg': '服务器响应异常'}
151
+ else:
152
+ logging.debug('发送结果:%s' % result)
153
+ # 消息发送失败提醒(errcode 不为 0,表示消息发送异常),默认不提醒,开发者可以根据返回的消息发送结果自行判断和处理
154
+ if self.fail_notice and result.get('errcode', True):
155
+ time_now = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(time.time()))
156
+ error_data = {
157
+ "msgtype": "text",
158
+ "text": {
159
+ "content": "[注意-自动通知]飞书机器人消息发送失败,时间:%s,原因:%s,请及时跟进,谢谢!" % (
160
+ time_now, result['errmsg'] if result.get('errmsg', False) else '未知异常')
161
+ },
162
+ "at": {
163
+ "isAtAll": False
164
+ }
165
+ }
166
+ logging.error("消息发送失败,自动通知:%s" % error_data)
167
+ requests.post(self.webhook, headers=self.headers, data=json.dumps(error_data))
168
+ return result
169
+
170
+
171
+ if __name__ == '__main__':
172
+ lark_client = FeiShuTalkChatBot(webhook='https://open.feishu.cn/open-apis/bot/v2/hook/bba5b285-ca56-488c-9142-d8bcbcaee3c1')
173
+ #lark_client.success(message='testtesttesttesttesttest', title='测试标题')
@@ -0,0 +1,114 @@
1
+ import hashlib
2
+ import uuid
3
+ from typing import Optional
4
+
5
+ """
6
+ 版本1 vs 版本2
7
+
8
+ 特性 版本1(8位62进制) 版本2(22位62进制)
9
+ 组合数 ~2.18e14(218万亿) ~3.2e35(远超UUID空间)
10
+ 碰撞阈值 14.77百万(50%概率) 5.2e19(理论极高)
11
+ 适用场景 小规模、临时ID 大规模、高唯一性需求
12
+ 性能开销 低 中等(大整数转换)
13
+ 扩展性 不可扩展 支持更高并发
14
+
15
+ 版本2显著优于版本1:通过扩展ID长度至22位,将碰撞概率降至理论安全极限,适合金融、分布式系统等关键场景。
16
+ 推荐实践:结合数据库唯一索引或Redis缓存,实时检测并重试碰撞ID,确保万无一失
17
+ """
18
+
19
+ printable_char = [
20
+ "0", "1", "2", "3", "4", "5", "6", "7", "8", "9",
21
+ "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w",
22
+ "x", "y", "z",
23
+ "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W",
24
+ "X", "Y", "Z",
25
+ ]
26
+
27
+ """版本2,生成最多 22 位的 ID
28
+ 参考链接:# https://github.com/skorokithakis/shortuuid/tree/master
29
+ 生成思路:根据 UUID3 或者 UUID4 得到的 32 位 16 进制数,转换成 62 进制的数
30
+ """
31
+
32
+
33
+ def get_random_long_id(padding: Optional[int] = 22):
34
+ _tmp_id = get_random_uuid(with_dash=True)
35
+ return _convert_10_2_62(_tmp_id, padding)
36
+
37
+
38
+ def get_fix_long_id(base_str: str, padding: Optional[int] = 22):
39
+ _tmp_id = get_fix_uuid(long_id=base_str, with_dash=True)
40
+ return _convert_10_2_62(_tmp_id, padding)
41
+
42
+
43
+ def _convert_10_2_62(id_16: str, padding: Optional[int]):
44
+ _tmp_id_10 = int(id_16, 16)
45
+
46
+ output = ""
47
+ while _tmp_id_10:
48
+ _tmp_id_10, digit = divmod(_tmp_id_10, 62)
49
+ output += printable_char[digit]
50
+ if padding:
51
+ remainder = max(padding - len(output), 0)
52
+ output += printable_char[0] * remainder
53
+
54
+ return output[::-1]
55
+
56
+
57
+ """版本1,生成 8 位数的短 ID
58
+ 参考链接:https://www.cnblogs.com/shouke/archive/2020/08/02/13423073.html
59
+ 生成思路:根据 UUID3 或者 UUID4 得到的 32 位 16 进制数进行分组,每组 4 个,一共 8 组。对每一组的数字对 62 取余。
60
+ """
61
+
62
+
63
+ def get_random_short_id():
64
+ _tmp_id = get_random_uuid(with_dash=True)
65
+ _buffer = []
66
+ # 循环 8 次,每次取出 4 个
67
+ for i in range(0, 8):
68
+ start = i * 4
69
+ end = i * 4 + 4
70
+ # 将 16 进制的 uuid 转换成 10 进制的数字
71
+ val = int(_tmp_id[start:end], 16)
72
+ _buffer.append(printable_char[val % 62])
73
+ return "".join(_buffer)
74
+
75
+
76
+ def get_fix_short_id(long_id):
77
+ _tmp_id = get_fix_uuid(long_id=long_id, with_dash=True)
78
+ _buffer = []
79
+ for i in range(0, 8):
80
+ start = i * 4
81
+ end = i * 4 + 4
82
+ val = int(_tmp_id[start:end], 16)
83
+ _buffer.append(printable_char[val % 62])
84
+ short_id = "".join(_buffer)
85
+ return short_id
86
+
87
+
88
+ """使用 UUID3 和 UUID4 分别得到固定的和随机的 UUID
89
+ Python 的 UUID3 生成的 ID 是 128 位的十进制数,可以表示为 32 位的十六进制数位。
90
+ Python 的 UUID3 生成的 ID 在给定相同的命名空间和名称的情况下是唯一的,它是通过将命名空间和名称作为输入,应用 MD5 哈希算法生成的。
91
+ 由于 MD5 算法的特性,即使输入的命名空间和名称有微小的变化,生成的 ID 也会有很大的差异,从而保证了唯一性。
92
+ """
93
+
94
+
95
+ def get_random_uuid(with_dash=False):
96
+ if with_dash:
97
+ return str(uuid.uuid4()).replace("-", '')
98
+ else:
99
+ return str(uuid.uuid4())
100
+
101
+
102
+ def get_fix_uuid(long_id, with_dash=False):
103
+ if with_dash:
104
+ return str(uuid.uuid3(uuid.NAMESPACE_DNS, long_id)).replace("-", '')
105
+ else:
106
+ return str(uuid.uuid3(uuid.NAMESPACE_DNS, long_id))
107
+
108
+
109
+ def get_file_md5(filepath):
110
+ with open(filepath, 'rb+') as f:
111
+ md5obj = hashlib.md5()
112
+ md5obj.update(f.read())
113
+ hash_ = md5obj.hexdigest()
114
+ return str(hash_).upper()
@@ -0,0 +1,187 @@
1
+ import datetime
2
+ import json
3
+ from enum import Enum
4
+ from typing import List, Optional
5
+ import pytz
6
+ from deprecated.sphinx import deprecated
7
+
8
+
9
+ class DateTimeFormat(Enum):
10
+ '''Orbit date format
11
+ '''
12
+
13
+ DATETIME_FORMAT = "%Y-%m-%dT%H:%M:%S%z"
14
+ DATE_FORMAT = "%Y-%m-%d"
15
+
16
+
17
+ class DateMaxMin(Enum):
18
+ '''Orbit date time min & max range
19
+ '''
20
+
21
+ DATE_MIN = "1000-01-01"
22
+ DATE_MAX = "2100-12-31"
23
+
24
+ DATETIME_MIN_UTC = "1000-01-01T00:00:00+0000"
25
+ DATETIME_MAX_UTC = "2100-12-31T29:59:59+0000"
26
+
27
+
28
+ class DateEncoder(json.JSONEncoder):
29
+ def default(self, obj):
30
+ if isinstance(obj, datetime.datetime):
31
+ return obj.strftime(DateTimeFormat.DATETIME_FORMAT.value)
32
+ elif isinstance(obj, datetime.date):
33
+ return obj.strftime(DateTimeFormat.DATE_FORMAT.value)
34
+ else:
35
+ return json.JSONEncoder.default(self, obj)
36
+
37
+
38
+ # -------------------------------------------------------------------------------------------------------------
39
+ # Below 2 methods are for getting Orbit standard datetime.
40
+ # -------------------------------------------------------------------------------------------------------------
41
+ @deprecated(version="v1", reason="Use get_orbit_std_datetime instead.")
42
+ def get_orbit_std_datatime():
43
+ current_timezone = datetime.datetime.now(datetime.timezone.utc).astimezone().tzinfo
44
+ return datetime.datetime.now(tz=current_timezone).strftime(DateTimeFormat.DATETIME_FORMAT.value)
45
+
46
+
47
+ @deprecated(version="v1", reason="Use get_orbit_std_datetime_utc instead.")
48
+ def get_orbit_std_datatime_utc():
49
+ return datetime.datetime.now(tz=pytz.timezone('UTC')).strftime(DateTimeFormat.DATETIME_FORMAT.value)
50
+
51
+
52
+ def get_orbit_std_datetime():
53
+ current_timezone = datetime.datetime.now(datetime.timezone.utc).astimezone().tzinfo
54
+ return datetime.datetime.now(tz=current_timezone).strftime(DateTimeFormat.DATETIME_FORMAT.value)
55
+
56
+
57
+ def get_orbit_std_datetime_utc():
58
+ return datetime.datetime.now(tz=pytz.timezone('UTC')).strftime(DateTimeFormat.DATETIME_FORMAT.value)
59
+
60
+
61
+ # -------------------------------------------------------------------------------------------------------------
62
+ # Below 2 methods are for getting Orbit standard date.
63
+ # -------------------------------------------------------------------------------------------------------------
64
+ def get_orbit_std_date():
65
+ return datetime.datetime.now(
66
+ tz=datetime.datetime.now(datetime.timezone.utc).astimezone().tzinfo
67
+ ).strftime(DateTimeFormat.DATE_FORMAT.value)
68
+
69
+
70
+ def get_orbit_std_date_utc():
71
+ return datetime.datetime.now(tz=pytz.timezone('UTC')).strftime(DateTimeFormat.DATE_FORMAT.value)
72
+
73
+
74
+ def get_date_range_by_base(date_range: int,
75
+ base_date: Optional[datetime.datetime] = None,
76
+ direction: str = "backward",
77
+ formatter: str = "%Y-%m-%d") -> List:
78
+ """
79
+ Get date range by datetime.
80
+ :param date_range:
81
+ :param base_date:
82
+ :param direction:
83
+ :param formatter:
84
+ :return: A list of date range.
85
+ """
86
+ if date_range <= 0:
87
+ raise Exception("Param err")
88
+
89
+ if base_date is None:
90
+ base_date = datetime.datetime.now()
91
+
92
+ timedelta = datetime.timedelta(days=date_range - 1)
93
+ if direction == "backward":
94
+ start_date = base_date - timedelta
95
+ end_date = base_date
96
+ elif direction == "forward":
97
+ start_date = base_date
98
+ end_date = base_date + timedelta
99
+ else:
100
+ raise Exception("Param err")
101
+
102
+ return get_date_range_list_v2(start_date.strftime("%Y-%m-%d"), end_date.strftime("%Y-%m-%d"), post_formatter=formatter)
103
+
104
+
105
+ def get_date_range_list_v1(start_date, end_date) -> List:
106
+ """
107
+ This method will return a list in which all date range are included with the very start and ending.
108
+ :param start_date: '2019-06-03'
109
+ :param end_date: '2019-06-05'
110
+ :return:
111
+ """
112
+ datestart = datetime.datetime.strptime(start_date, "%Y-%m-%d")
113
+ dateend = datetime.datetime.strptime(end_date, "%Y-%m-%d")
114
+ dateend += datetime.timedelta(days=1)
115
+
116
+ day_list_with_range = []
117
+ while datestart < dateend:
118
+ day_list_with_range.append(datestart.strftime("%Y-%m-%d"))
119
+ datestart += datetime.timedelta(days=1)
120
+
121
+ return day_list_with_range
122
+
123
+
124
+ def get_date_range_list_v2(start_date, end_date, pre_formatter="%Y-%m-%d", post_formatter="%Y-%m-%d") -> List:
125
+ """
126
+ This method will return a list in which all date range are included with the very start and ending.
127
+ :param post_formatter: Default is %Y-%m-%d
128
+ :param pre_formatter: Default is %Y-%m-%d
129
+ :param start_date: '2019-06-03'
130
+ :param end_date: '2019-06-05'
131
+ :return:
132
+ """
133
+ datestart = datetime.datetime.strptime(start_date, pre_formatter)
134
+ dateend = datetime.datetime.strptime(end_date, pre_formatter)
135
+ dateend += datetime.timedelta(days=1)
136
+
137
+ day_list_with_range = []
138
+ while datestart < dateend:
139
+ day_list_with_range.append(datestart.strftime(post_formatter))
140
+ datestart += datetime.timedelta(days=1)
141
+
142
+ return day_list_with_range
143
+
144
+
145
+ def get_next_day(day: str, interval: int = 1, date_format="%Y-%m-%d") -> str:
146
+ """
147
+ Get the next day by given date
148
+ :param day:
149
+ :param interval:
150
+ :param date_format:
151
+ :return:
152
+ """
153
+
154
+ target_day = datetime.datetime.strptime(day, date_format)
155
+ next_day = target_day + datetime.timedelta(days=interval)
156
+ return next_day.strftime(date_format)
157
+
158
+
159
+ def get_next_workday_cn(workday: str, interval: int = 1, date_format: str = "%Y-%m-%d") -> str:
160
+ """
161
+ Get the next trading date by Chinese calendar
162
+ :param workday:
163
+ :param interval:
164
+ :param date_format:
165
+ :return:
166
+ """
167
+
168
+ try:
169
+ from chinese_calendar import is_workday
170
+ except ImportError:
171
+ raise ValueError(
172
+ "Please install below packages before using this function.\n"
173
+ "- chinese_calendar"
174
+ )
175
+
176
+ # Start to judge
177
+ next_date = datetime.datetime.strptime(
178
+ get_next_day(day=workday, interval=interval, date_format=date_format),
179
+ date_format
180
+ )
181
+ while not is_workday(next_date) or next_date.weekday() in (5, 6):
182
+ next_date = datetime.datetime.strptime(
183
+ get_next_day(day=next_date.strftime(date_format), interval=interval, date_format=date_format),
184
+ date_format
185
+ )
186
+
187
+ return next_date.strftime(date_format)
@@ -0,0 +1,98 @@
1
+ import os
2
+ import re
3
+
4
+ import oss2
5
+ from oss2.credentials import EnvironmentVariableCredentialsProvider
6
+ from .util_type_mapping import get_content_type_4_filename
7
+
8
+
9
+ os.environ['OSS_ACCESS_KEY_ID'] = 'LTAI5tLtyiy18UAWL626k1wC'
10
+ os.environ['OSS_ACCESS_KEY_SECRET'] = 'T6VSV1QFn7hsc8wjrOGG8K4meDyXbX'
11
+
12
+
13
+ class OssCloudObjectProvider:
14
+
15
+ def __init__(self):
16
+ self.oss_endpoint = 'https://oss-cn-shanghai.aliyuncs.com'
17
+ self.region = 'cn-shanghai'
18
+ self.auth = oss2.ProviderAuthV4(EnvironmentVariableCredentialsProvider())
19
+
20
+ def oss_split_path(self, oss_path):
21
+ if not oss_path.startswith('oss://'):
22
+ raise Exception(f"Invalid oss path format: {oss_path}")
23
+ oss_path_re = re.compile(r"(oss://[a-zA-Z\-_0-9]+)/(.+)")
24
+ path_group = oss_path_re.search(oss_path).groups()
25
+ bucket = path_group[0].replace('oss://', '')
26
+ if not bucket:
27
+ raise Exception(f"Invalid oss path format: {oss_path}")
28
+ key = path_group[1]
29
+ if not key:
30
+ raise Exception(f"Invalid oss path format: {oss_path}")
31
+ return bucket, key
32
+
33
+ def copy_oss_obj(self, src_oss_path: str, dst_oss_path: str):
34
+ src_bucket, src_key = self.oss_split_path(src_oss_path)
35
+ dst_bucket, dst_key = self.oss_split_path(dst_oss_path)
36
+ bucket_obj = oss2.Bucket(self.auth, self.oss_endpoint, dst_bucket, region=self.region)
37
+ result = bucket_obj.copy_object(src_bucket, src_key, dst_key)
38
+ if result.status != 200:
39
+ raise Exception(f"Copy failed with status code {result.status}")
40
+
41
+ def check_file_exist(self, oss_path: str) -> bool:
42
+ bucket, key = self.oss_split_path(oss_path)
43
+ bucket_obj = oss2.Bucket(self.auth, self.oss_endpoint, bucket, region=self.region)
44
+ oss_result = bucket_obj.object_exists(key)
45
+ return oss_result
46
+
47
+ def upload_file(self, oss_path: str, local_path: str):
48
+ if not os.path.exists(local_path):
49
+ raise Exception("Local file doesn't exist!")
50
+ bucket, key = self.oss_split_path(oss_path)
51
+ bucket_obj = oss2.Bucket(self.auth, self.oss_endpoint, bucket, region=self.region)
52
+ content_type = get_content_type_4_filename(local_path, text_with_utf8=True)
53
+ result = bucket_obj.put_object_from_file(key, local_path, headers={"Content-Type": content_type})
54
+ if result.status != 200:
55
+ raise Exception(f"Upload failed with status code {result.status}")
56
+
57
+ def download_file(self, oss_path: str, local_path: str, filename: str):
58
+ if not os.path.exists(local_path):
59
+ os.makedirs(local_path)
60
+ bucket, key = self.oss_split_path(oss_path)
61
+ bucket_obj = oss2.Bucket(self.auth, self.oss_endpoint, bucket, region=self.region)
62
+ bucket_obj.get_object_to_file(key, os.path.join(local_path, filename))
63
+
64
+ def get_presign_url_docs(self, oss_path: str, expire_hours: int = 1) -> str:
65
+ bucket, key = self.oss_split_path(oss_path)
66
+ bucket_obj = oss2.Bucket(self.auth, 'http://docs.huanai.tech', bucket, region=self.region, is_cname=True)
67
+ expire_seconds = expire_hours * 60 * 60
68
+ return bucket_obj.sign_url('GET', key, expire_seconds, slash_safe=True)
69
+
70
+ def get_upload_url(self, oss_path: str, expire_hours: int = 1) -> str:
71
+ bucket, key = self.oss_split_path(oss_path)
72
+ bucket_obj = oss2.Bucket(self.auth, self.oss_endpoint, bucket, region=self.region)
73
+ expire_seconds = expire_hours * 60 * 60
74
+ return bucket_obj.sign_url('PUT', key, expire_seconds, slash_safe=True)
75
+
76
+ def list_dir_iter(self, oss_path_pre: str):
77
+ bucket, key_pre = self.oss_split_path(oss_path_pre)
78
+ bucket = oss2.Bucket(self.auth, self.oss_endpoint, bucket, region=self.region)
79
+ for obj in oss2.ObjectIterator(bucket, prefix=key_pre):
80
+ yield obj.key
81
+
82
+ def get_file_content(self, oss_path: str):
83
+ bucket, key = self.oss_split_path(oss_path)
84
+ bucket_obj = oss2.Bucket(self.auth, self.oss_endpoint, bucket, region=self.region)
85
+ return bucket_obj.get_object(key).read()
86
+
87
+ def delete_file_from_oss(self, oss_path: str):
88
+ bucket, key = self.oss_split_path(oss_path)
89
+ bucket_obj = oss2.Bucket(self.auth, self.oss_endpoint, bucket, region=self.region)
90
+ result = bucket_obj.delete_object(key)
91
+ if result.status != 204:
92
+ raise Exception(f"Delete failed with status code {result.status}")
93
+
94
+ # if __name__ == '__main__':
95
+ # oss_client = OssCloudObjectProvider(OSSAPPID, OSSPWD, OSSENDPOINT)
96
+ # # oss_client.upload_file('oss://dev-test-ha/tabel1.pdf', 'table1.pdf')
97
+ # url = oss_client.get_presign_url(f'oss://dev-test-ha/tabel1.pdf')
98
+ # print(url)
@@ -0,0 +1,343 @@
1
+ import threading
2
+ from enum import Enum
3
+
4
+ '''
5
+ Reference link: https://www.lifewire.com/file-extensions-and-mime-types-3469109
6
+ Reference link: https://superuser.com/questions/62852/xlsx-files-are-being-interpreted-as-zip-files-on-download-why
7
+ # Applications
8
+ doc / docx / ppt / pptx / xls / xlsx / pdf / json / js
9
+ # Text Files
10
+ html / htm / txt
11
+ '''
12
+
13
+
14
+ class ExtenCons(Enum):
15
+ '''
16
+ Useful constant variables
17
+ '''
18
+
19
+ EXTEN_OCTET_STREAM = 'application/octet-stream'
20
+ # Application
21
+ EXTEN_APPLI_PDF = 'application/pdf'
22
+ EXTEN_APPLI_XML = 'application/xml'
23
+ EXTEN_APPLI_JSON = 'application/json'
24
+ EXTEN_APPLI_JS = 'application/x-javascript'
25
+ EXTEN_APPLI_DOC = 'application/msword'
26
+ EXTEN_APPLI_DOCX = 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
27
+ EXTEN_APPLI_XLS = 'application/vnd.ms-excel'
28
+ EXTEN_APPLI_XLSX = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
29
+ EXTEN_APPLI_PPT = 'application/vnd.ms-powerpoint'
30
+ EXTEN_APPLI_PPTX = 'application/vnd.openxmlformats-officedocument.presentationml.presentation'
31
+ # Text Files
32
+ EXTEN_TEXT_HTML = 'text/html'
33
+ EXTEN_TEXT_HTM = 'text/html'
34
+ EXTEN_TEXT_TXT = 'text/plain'
35
+ # Text Files with charset
36
+ EXTEN_TEXT_HTML_UTF8 = 'text/html;charset=utf-8'
37
+ EXTEN_TEXT_HTM_UTF8 = 'text/html;charset=utf-8'
38
+ EXTEN_TEXT_TXT_UTF8 = 'text/plain;charset=utf-8'
39
+
40
+
41
+ _content_file_mapping = {
42
+ # MIME Types: Applications
43
+ '*': {'application': 'Binary file', 'mine': 'application/octet-stream'},
44
+ 'pdf': {'application': 'pdf document', 'mine': 'application/pdf'},
45
+ 'json': {'application': 'json document', 'mine': 'application/json'},
46
+ 'evy': {'application': 'Corel Envoy', 'mine': 'application/envoy'},
47
+ 'xml': {'application': 'XML document', 'mine': 'application/xml'},
48
+ # Word related
49
+ 'dot': {'application': 'Word document template', 'mine': 'application/msword'},
50
+ 'doc': {'application': 'Word document', 'mine': 'application/msword'},
51
+ 'docx': {'application': 'docx document', 'mine': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'},
52
+ 'docm': {'application': '', 'mine': 'application/vnd.ms-word.document.macroEnabled.12'}, # Added in 07 Mar, 2024
53
+ 'dotx': {'application': '', 'mine': 'application/vnd.openxmlformats-officedocument.wordprocessingml.template'}, # Added in 07 Mar, 2024
54
+ 'fif': {'application': 'fractal image file', 'mine': 'application/fractals'},
55
+ 'spl': {'application': 'Windows print spool file', 'mine': 'application/futuresplash'},
56
+ 'hta': {'application': 'HTML application', 'mine': 'application/hta'},
57
+ 'acx': {'application': 'Atari ST Program', 'mine': 'application/internet-property-stream'},
58
+ 'hqx': {'application': 'BinHex encoded file', 'mine': 'application/mac-binhex40'},
59
+ 'bin': {'application': 'binary disk image', 'mine': 'application/octet-stream'},
60
+ 'class': {'application': 'Java class file', 'mine': 'application/octet-stream'},
61
+ 'dms': {'application': 'Disk Masher image', 'mine': 'application/octet-stream'},
62
+ 'exe': {'application': 'executable file', 'mine': 'application/octet-stream'},
63
+ 'lha': {'application': 'LHARC compressed archive', 'mine': 'application/octet-stream'},
64
+ 'lzh': {'application': 'LZH compressed file', 'mine': 'application/octet-stream'},
65
+ 'oda': {'application': 'CALS raster image', 'mine': 'application/oda'},
66
+ 'axs': {'application': 'ActiveX script', 'mine': 'application/olescript'},
67
+ 'prf': {'application': 'Outlook profile file', 'mine': 'application/pics-rules'},
68
+ 'p10': {'application': 'certificate request file', 'mine': 'application/pkcs10'},
69
+ 'crl': {'application': 'certificate revocation list file', 'mine': 'application/pkix-crl'},
70
+ 'ai': {'application': 'Adobe Illustrator file', 'mine': 'application/postscript'},
71
+ 'eps': {'application': 'postscript file', 'mine': 'application/postscript'},
72
+ 'ps': {'application': 'postscript file', 'mine': 'application/postscript'},
73
+ 'rtf': {'application': 'rich text format file', 'mine': 'application/rtf'},
74
+ 'setpay': {'application': 'set payment initiation', 'mine': 'application/set-payment-initiation'},
75
+ 'setreg': {'application': 'set registration initiation', 'mine': 'application/set-registration-initiation'},
76
+ # Excel related
77
+ 'xla': {'application': 'Excel Add-in file', 'mine': 'application/vnd.ms-excel'},
78
+ 'xlc': {'application': 'Excel chart', 'mine': 'application/vnd.ms-excel'},
79
+ 'xlm': {'application': 'Excel macro', 'mine': 'application/vnd.ms-excel'},
80
+ 'xls': {'application': 'Excel spreadsheet', 'mine': 'application/vnd.ms-excel'},
81
+ 'xlsx': {'application': 'xlsx document', 'mine': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'},
82
+ 'xlt': {'application': 'Excel template', 'mine': 'application/vnd.ms-excel'},
83
+ 'xlw': {'application': 'Excel worspace', 'mine': 'application/vnd.ms-excel'},
84
+ 'xlam': {'application': '', 'mine': 'application/vnd.ms-excel.addin.macroEnabled.12'}, # Added in 07 Mar, 2024
85
+ 'xlsb': {'application': '', 'mine': 'application/vnd.ms-excel.sheet.binary.macroEnabled.12'}, # Added in 07 Mar, 2024
86
+ 'xlsm': {'application': '', 'mine': 'application/vnd.ms-excel.sheet.macroEnabled.12'}, # Added in 07 Mar, 2024
87
+ 'xltm': {'application': '', 'mine': 'application/vnd.ms-excel.template.macroEnabled.12'}, # Added in 07 Mar, 2024
88
+ 'xltx': {'application': '', 'mine': 'application/vnd.openxmlformats-officedocument.spreadsheetml.template'}, # Added in 07 Mar, 2024
89
+ 'msg': {'application': 'Outlook mail message', 'mine': 'application/vnd.ms-outlook'},
90
+ 'sst': {'application': 'serialized certificate store file', 'mine': 'application/vnd.ms-pkicertstore'},
91
+ 'cat': {'application': 'Windows catalog file', 'mine': 'application/vnd.ms-pkiseccat'},
92
+ 'stl': {'application': 'stereolithography file', 'mine': 'application/vnd.ms-pkistl'},
93
+ # ppt related
94
+ 'pot': {'application': 'PowerPoint template', 'mine': 'application/vnd.ms-powerpoint'},
95
+ 'pps': {'application': 'PowerPoint slide show', 'mine': 'application/vnd.ms-powerpoint'},
96
+ 'ppt': {'application': 'PowerPoint presentation', 'mine': 'application/vnd.ms-powerpoint'},
97
+ 'pptx': {'application': 'pptx document', 'mine': 'application/vnd.openxmlformats-officedocument.presentationml.presentation'},
98
+ 'potm': {'application': '', 'mine': 'application/vnd.ms-powerpoint.template.macroEnabled.12'}, # Added in 07 Mar, 2024
99
+ 'potx': {'application': '', 'mine': 'application/vnd.openxmlformats-officedocument.presentationml.template'}, # Added in 07 Mar, 2024
100
+ 'ppam': {'application': '', 'mine': 'application/vnd.ms-powerpoint.addin.macroEnabled.12'}, # Added in 07 Mar, 2024
101
+ 'ppsm': {'application': '', 'mine': 'application/vnd.ms-powerpoint.slideshow.macroEnabled.12'}, # Added in 07 Mar, 2024
102
+ 'ppsx': {'application': '', 'mine': 'application/vnd.openxmlformats-officedocument.presentationml.slideshow'}, # Added in 07 Mar, 2024
103
+ 'pptm': {'application': '', 'mine': 'application/vnd.ms-powerpoint.presentation.macroEnabled.12'}, # Added in 07 Mar, 2024
104
+ 'mpp': {'application': 'Microsoft Project file', 'mine': 'application/vnd.ms-project'},
105
+ 'wcm': {'application': 'WordPerfect macro', 'mine': 'application/vnd.ms-works'},
106
+ 'wdb': {'application': 'Microsoft Works database', 'mine': 'application/vnd.ms-works'},
107
+ 'wks': {'application': 'Microsoft Works spreadsheet', 'mine': 'application/vnd.ms-works'},
108
+ 'wps': {'application': 'Microsoft Works word processsor document', 'mine': 'application/vnd.ms-works'},
109
+ 'hlp': {'application': 'Windows help file', 'mine': 'application/winhlp'},
110
+ 'bcpio': {'application': 'binary CPIO archive', 'mine': 'application/x-bcpio'},
111
+ 'cdf': {'application': 'computable document format file', 'mine': 'application/x-cdf'},
112
+ 'z': {'application': 'Unix compressed file', 'mine': 'application/x-compress'},
113
+ 'tgz': {'application': 'gzipped tar file', 'mine': 'application/x-compressed'},
114
+ 'cpio': {'application': 'Unix CPIO archive', 'mine': 'application/x-cpio'},
115
+ 'csh': {'application': 'Photoshop custom shapes file', 'mine': 'application/x-csh'},
116
+ 'dcr': {'application': 'Kodak RAW image file', 'mine': 'application/x-director'},
117
+ 'dir': {'application': 'Adobe Director movie', 'mine': 'application/x-director'},
118
+ 'dxr': {'application': 'Macromedia Director movie', 'mine': 'application/x-director'},
119
+ 'dvi': {'application': 'device independent format file', 'mine': 'application/x-dvi'},
120
+ 'gtar': {'application': 'Gnu tar archive', 'mine': 'application/x-gtar'},
121
+ 'gz': {'application': 'Gnu zipped archive', 'mine': 'application/x-gzip'},
122
+ 'hdf': {'application': 'hierarchical data format file', 'mine': 'application/x-hdf'},
123
+ 'ins': {'application': 'internet settings file', 'mine': 'application/x-internet-signup'},
124
+ 'isp': {'application': 'IIS internet service provider settings', 'mine': 'application/x-internet-signup'},
125
+ 'iii': {'application': 'ARC+ architectural file', 'mine': 'application/x-iphone'},
126
+ 'js': {'application': 'JavaScript file', 'mine': 'application/x-javascript'},
127
+ 'latex': {'application': 'LaTex document', 'mine': 'application/x-latex'},
128
+ 'mdb': {'application': 'Microsoft Access database', 'mine': 'application/x-msaccess'},
129
+ 'crd': {'application': 'Windows CardSpace file', 'mine': 'application/x-mscardfile'},
130
+ 'clp': {'application': 'CrazyTalk clip file', 'mine': 'application/x-msclip'},
131
+ 'dll': {'application': 'dynamic link library', 'mine': 'application/x-msdownload'},
132
+ 'm13': {'application': 'Microsoft media viewer file', 'mine': 'application/x-msmediaview'},
133
+ 'm14': {'application': 'Steuer2001 file', 'mine': 'application/x-msmediaview'},
134
+ 'mvb': {'application': 'multimedia viewer book source file', 'mine': 'application/x-msmediaview'},
135
+ 'wmf': {'application': 'Windows meta file', 'mine': 'application/x-msmetafile'},
136
+ 'mny': {'application': 'Microsoft Money file', 'mine': 'application/x-msmoney'},
137
+ 'pub': {'application': 'Microsoft Publisher file', 'mine': 'application/x-mspublisher'},
138
+ 'scd': {'application': 'Turbo Tax tax schedule list', 'mine': 'application/x-msschedule'},
139
+ 'trm': {'application': 'FTR media file', 'mine': 'application/x-msterminal'},
140
+ 'wri': {'application': 'Microsoft Write file', 'mine': 'application/x-mswrite'},
141
+ 'nc': {'application': 'Mastercam numerical control file', 'mine': 'application/x-netcdf'},
142
+ 'pma': {'application': 'MSX computers archive format', 'mine': 'application/x-perfmon'},
143
+ 'pmc': {'application': 'performance monitor counter file', 'mine': 'application/x-perfmon'},
144
+ 'pml': {'application': 'process monitor log file', 'mine': 'application/x-perfmon'},
145
+ 'pmr': {'application': 'Avid persistant media record file', 'mine': 'application/x-perfmon'},
146
+ 'pmw': {'application': 'Pegasus Mail draft stored message', 'mine': 'application/x-perfmon'},
147
+ 'p12': {'application': 'personal information exchange file', 'mine': 'application/x-pkcs12'},
148
+ 'pfx': {'application': 'PKCS #12 certificate file', 'mine': 'application/x-pkcs12'},
149
+ 'p7b': {'application': 'PKCS #7 certificate file', 'mine': 'application/x-pkcs7-certificates'},
150
+ 'spc': {'application': 'software publisher certificate file', 'mine': 'application/x-pkcs7-certificates'},
151
+ 'p7r': {'application': 'certificate request response file', 'mine': 'application/x-pkcs7-certreqresp'},
152
+ 'p7c': {'application': 'PKCS #7 certificate file', 'mine': 'application/x-pkcs7-mime'},
153
+ 'p7m': {'application': 'digitally encrypted message', 'mine': 'application/x-pkcs7-mime'},
154
+ 'p7s': {'application': 'digitally signed email message', 'mine': 'application/x-pkcs7-signature'},
155
+ 'sh': {'application': 'Bash shell script', 'mine': 'application/x-sh'},
156
+ 'shar': {'application': 'Unix shar archive', 'mine': 'application/x-shar'},
157
+ 'swf': {'application': 'Flash file', 'mine': 'application/x-shockwave-flash'},
158
+ 'sit': {'application': 'Stuffit archive file', 'mine': 'application/x-stuffit'},
159
+ 'sv4cpio': {'application': 'system 5 release 4 CPIO file', 'mine': 'application/x-sv4cpio'},
160
+ 'sv4crc': {'application': 'system 5 release 4 CPIO checksum data', 'mine': 'application/x-sv4crc'},
161
+ 'tar': {'application': 'consolidated Unix file archive', 'mine': 'application/x-tar'},
162
+ 'tcl': {'application': 'Tcl script', 'mine': 'application/x-tcl'},
163
+ 'tex': {'application': 'LaTeX source document', 'mine': 'application/x-tex'},
164
+ 'texi': {'application': 'LaTeX info document', 'mine': 'application/x-texinfo'},
165
+ 'texinfo': {'application': 'LaTeX info document', 'mine': 'application/x-texinfo'},
166
+ 'roff': {'application': 'unformatted manual page', 'mine': 'application/x-troff'},
167
+ 't': {'application': 'Turing source code file', 'mine': 'application/x-troff'},
168
+ 'tr': {'application': 'TomeRaider 2 ebook file', 'mine': 'application/x-troff'},
169
+ 'man': {'application': 'Unix manual', 'mine': 'application/x-troff-man'},
170
+ 'me': {'application': 'readme text file', 'mine': 'application/x-troff-me'},
171
+ 'ms': {'application': '3ds Max script file', 'mine': 'application/x-troff-ms'},
172
+ 'ustar': {'application': 'uniform standard tape archive format file', 'mine': 'application/x-ustar'},
173
+ 'src': {'application': 'source code', 'mine': 'application/x-wais-source'},
174
+ 'cer': {'application': 'internet security certificate', 'mine': 'application/x-x509-ca-cert'},
175
+ 'crt': {'application': 'security certificate', 'mine': 'application/x-x509-ca-cert'},
176
+ 'der': {'application': 'DER certificate file', 'mine': 'application/x-x509-ca-cert'},
177
+ 'pko': {'application': 'public key security object', 'mine': 'application/ynd.ms-pkipko'},
178
+ 'zip': {'application': 'zipped file', 'mine': 'application/zip'},
179
+ 'xhtml': {'application': '', 'mine': 'application/xhtml+xml'}, # Added in 07 Mar, 2024
180
+ # MIME Types: Sound Files
181
+ 'au': {'application': 'audio file', 'mine': 'audio/basic'},
182
+ 'snd': {'application': 'sound file', 'mine': 'audio/basic'},
183
+ 'mid': {'application': 'midi file', 'mine': 'audio/mid'},
184
+ 'rmi': {'application': 'media processing server studio', 'mine': 'audio/mid'},
185
+ 'mp3': {'application': 'MP3 file', 'mine': 'audio/mpeg'},
186
+ 'aif': {'application': 'audio interchange file format', 'mine': 'audio/x-aiff'},
187
+ 'aifc': {'application': 'compressed audio interchange file', 'mine': 'audio/x-aiff'},
188
+ 'aiff': {'application': 'audio interchange file format', 'mine': 'audio/x-aiff'},
189
+ 'm3u': {'application': 'media playlist file', 'mine': 'audio/x-mpegurl'},
190
+ 'ra': {'application': 'Real Audio file', 'mine': 'audio/x-pn-realaudio'},
191
+ 'ram': {'application': 'Real Audio metadata file', 'mine': 'audio/x-pn-realaudio'},
192
+ 'wav': {'application': 'WAVE audio file', 'mine': 'audio/x-wav'},
193
+ # MIME Types: Image Files
194
+ 'png': {'application': 'png', 'mine': 'image/png'},
195
+ 'bmp': {'application': 'Bitmap', 'mine': 'image/bmp'},
196
+ 'cod': {'application': 'compiled source code', 'mine': 'image/cis-cod'},
197
+ 'gif': {'application': 'graphic interchange format', 'mine': 'image/gif'},
198
+ 'ief': {'application': 'image file', 'mine': 'image/ief'},
199
+ 'jpe': {'application': 'JPEG image', 'mine': 'image/jpeg'},
200
+ 'jpeg': {'application': 'JPEG image', 'mine': 'image/jpeg'},
201
+ 'jpg': {'application': 'JPEG image', 'mine': 'image/jpeg'},
202
+ 'jfif': {'application': 'JPEG file interchange format', 'mine': 'image/pipeg'},
203
+ 'svg': {'application': 'scalable vector graphic', 'mine': 'image/svg+xml'},
204
+ 'tif': {'application': 'TIF image', 'mine': 'image/tiff'},
205
+ 'tiff': {'application': 'TIF image', 'mine': 'image/tiff'},
206
+ 'ras': {'application': 'Sun raster graphic', 'mine': 'image/x-cmu-raster'},
207
+ 'cmx': {'application': 'Corel metafile exchange image file', 'mine': 'image/x-cmx'},
208
+ 'ico': {'application': 'icon', 'mine': 'image/x-icon'},
209
+ 'pnm': {'application': 'portable any map image', 'mine': 'image/x-portable-anymap'},
210
+ 'pbm': {'application': 'portable bitmap image', 'mine': 'image/x-portable-bitmap'},
211
+ 'pgm': {'application': 'portable graymap image', 'mine': 'image/x-portable-graymap'},
212
+ 'ppm': {'application': 'portable pixmap image', 'mine': 'image/x-portable-pixmap'},
213
+ 'rgb': {'application': 'RGB bitmap', 'mine': 'image/x-rgb'},
214
+ 'xbm': {'application': 'X11 bitmap', 'mine': 'image/x-xbitmap'},
215
+ 'xpm': {'application': 'X11 pixmap', 'mine': 'image/x-xpixmap'},
216
+ 'xwd': {'application': 'X-Windows dump image', 'mine': 'image/x-xwindowdump'},
217
+ # MIME Types: Virtual World Files
218
+ 'flr': {'application': 'Flare decompiled actionscript file', 'mine': 'x-world/x-vrml'},
219
+ 'vrml': {'application': 'VRML file', 'mine': 'x-world/x-vrml'},
220
+ 'wrl': {'application': 'VRML world', 'mine': 'x-world/x-vrml'},
221
+ 'wrz': {'application': 'compressed VRML world', 'mine': 'x-world/x-vrml'},
222
+ 'xaf': {'application': '3ds max XML animation file', 'mine': 'x-world/x-vrml'},
223
+ 'xof': {'application': 'Reality Lab 3D image file', 'mine': 'x-world/x-vrml'},
224
+ # MIME Types: Video Files
225
+ 'mp2': {'application': 'MPEG-2 audio file', 'mine': 'video/mpeg'},
226
+ 'mpa': {'application': 'MPEG-2 audio file', 'mine': 'video/mpeg'},
227
+ 'mpe': {'application': 'MPEG movie file', 'mine': 'video/mpeg'},
228
+ 'mpeg': {'application': 'MPEG movie file', 'mine': 'video/mpeg'},
229
+ 'mpg': {'application': 'MPEG movie file', 'mine': 'video/mpeg'},
230
+ 'mpv2': {'application': 'MPEG-2 video stream', 'mine': 'video/mpeg'},
231
+ 'mp4': {'application': 'MPEG-4', 'mine': 'video/mp4'},
232
+ 'mov': {'application': 'Apple QuickTime movie', 'mine': 'video/quicktime'},
233
+ 'qt': {'application': 'Apple QuickTime movie', 'mine': 'video/quicktime'},
234
+ 'lsf': {'application': 'Logos library system file', 'mine': 'video/x-la-asf'},
235
+ 'lsx': {'application': 'streaming media shortcut', 'mine': 'video/x-la-asf'},
236
+ 'asf': {'application': 'advanced systems format file', 'mine': 'video/x-ms-asf'},
237
+ 'asr': {'application': 'ActionScript remote document', 'mine': 'video/x-ms-asf'},
238
+ 'asx': {'application': 'Microsoft ASF redirector file', 'mine': 'video/x-ms-asf'},
239
+ 'avi': {'application': 'audio video interleave file', 'mine': 'video/x-msvideo'},
240
+ 'movie': {'application': 'Apple QuickTime movie', 'mine': 'video/x-sgi-movie'},
241
+ # MIME Types: Text Files
242
+ 'css': {'application': 'Cascading Style Sheet', 'mine': 'text/css'},
243
+ '323': {'application': 'H.323 internet telephony file', 'mine': 'text/h323'},
244
+ 'htm': {'application': 'HTML file', 'mine': 'text/html'},
245
+ 'html': {'application': 'HTML file', 'mine': 'text/html'},
246
+ 'stm': {'application': 'Exchange streaming media file', 'mine': 'text/html'},
247
+ 'uls': {'application': 'NetMeeting user location service file', 'mine': 'text/iuls'},
248
+ 'bas': {'application': 'BASIC source code file', 'mine': 'text/plain'},
249
+ 'c': {'application': 'C/C++ source code file', 'mine': 'text/plain'},
250
+ 'h': {'application': 'C/C++/Objective C header file', 'mine': 'text/plain'},
251
+ 'txt': {'application': 'text file', 'mine': 'text/plain'},
252
+ 'rtx': {'application': 'rich text file', 'mine': 'text/richtext'},
253
+ 'sct': {'application': 'Scitext continuous tone file', 'mine': 'text/scriptlet'},
254
+ 'tsv': {'application': 'tab separated values file', 'mine': 'text/tab-separated-values'},
255
+ 'htt': {'application': 'hypertext template file', 'mine': 'text/webviewhtml'},
256
+ 'htc': {'application': 'HTML component file', 'mine': 'text/x-component'},
257
+ 'etx': {'application': 'TeX font encoding file', 'mine': 'text/x-setext'},
258
+ 'vcf': {'application': 'vCard file', 'mine': 'text/x-vcard'},
259
+ 'csv': {'application': '', 'mine': 'text/csv'}, # Added in 07 Mar, 2024
260
+ # MIME Types: Mail Message Files
261
+ 'mht': {'application': 'MHTML web archive', 'mine': 'message/rfc822'},
262
+ 'mhtml': {'application': 'MIME HTML file', 'mine': 'message/rfc822'},
263
+ 'nws': {'application': 'Windows Live Mail newsgroup file', 'mine': 'message/rfc822'},
264
+ }
265
+
266
+
267
+ def get_content_type_v1(extension):
268
+ current_type = 'application/octet-stream'
269
+
270
+ try:
271
+ current_type = _content_file_mapping[extension]['mine']
272
+ except Exception as e:
273
+ pass
274
+
275
+ return current_type
276
+
277
+
278
+ class _SingletonType(type):
279
+ _instance_lock = threading.Lock()
280
+
281
+ def __call__(cls, *args, **kwargs):
282
+ if not hasattr(cls, "_instance"):
283
+ with _SingletonType._instance_lock:
284
+ if not hasattr(cls, "_instance"):
285
+ cls._instance = super(_SingletonType, cls).__call__(*args, **kwargs)
286
+ return cls._instance
287
+
288
+
289
+ class _SwitchContentFileType(metaclass=_SingletonType):
290
+ def __init__(self):
291
+ self.content2file_type_mapping = {}
292
+ self.file2content_type_mapping = {}
293
+ # To load all mapping data
294
+ for key, value in _content_file_mapping.items():
295
+ # print(key, value['mine'])
296
+ self.file2content_type_mapping[key] = value['mine']
297
+ if value['mine'] in self.content2file_type_mapping:
298
+ self.content2file_type_mapping[value['mine']].append(key)
299
+ else:
300
+ self.content2file_type_mapping[value['mine']] = [key]
301
+
302
+ def content2file_type(self, content_type):
303
+ current_type = ['*']
304
+ try:
305
+ current_type = self.content2file_type_mapping[str(content_type).strip().lower()]
306
+ except Exception as e:
307
+ pass
308
+ return current_type
309
+
310
+ def file2content_type(self, file_type):
311
+ current_type = 'application/octet-stream'
312
+ try:
313
+ current_type = self.file2content_type_mapping[str(file_type).strip().lower()]
314
+ except Exception as e:
315
+ pass
316
+ return current_type
317
+
318
+
319
+ def content2file_type(content_type):
320
+ switch_content_file_type = _SwitchContentFileType()
321
+ return switch_content_file_type.content2file_type(content_type)
322
+
323
+
324
+ def file2content_type(file_type):
325
+ switch_content_file_type = _SwitchContentFileType()
326
+ return switch_content_file_type.file2content_type(file_type)
327
+
328
+
329
+ # -------------------------------------------------------------------------------------------------------------
330
+ # Below are the recommended methods
331
+ # -------------------------------------------------------------------------------------------------------------
332
+
333
+ def get_content_type_4_filename(filename: str, text_with_utf8=False):
334
+ # If no '.' in filename then get the full filename instead.
335
+ file_extension = filename.split('.')[-1]
336
+
337
+ try:
338
+ extension = _content_file_mapping[file_extension.strip().lower()]['mine']
339
+ if text_with_utf8 and extension.startswith('text/'):
340
+ extension += ';charset=utf-8'
341
+ return extension
342
+ except:
343
+ return 'application/octet-stream'
@@ -0,0 +1,76 @@
1
+ Metadata-Version: 2.4
2
+ Name: enconia-utils
3
+ Version: 0.1.0
4
+ Summary: utils for enconia
5
+ Author-email: "Huanai Technology Co., Ltd" <zhou-jinyuan@foxmail.com>
6
+ License: BSD-3-Clause
7
+ Classifier: Development Status :: 3 - Alpha
8
+ Classifier: Intended Audience :: Developers
9
+ Classifier: License :: OSI Approved :: BSD License
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3.9
12
+ Classifier: Programming Language :: Python :: 3.10
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Requires-Python: >=3.9
15
+ Description-Content-Type: text/markdown
16
+ License-File: LICENSE
17
+ Requires-Dist: requests>=2.25.0
18
+ Requires-Dist: pytz>=2021.1
19
+ Requires-Dist: deprecated>=1.2.0
20
+ Requires-Dist: oss2>=2.15.0
21
+ Requires-Dist: aliyun-log-python-sdk>=0.8.0
22
+ Requires-Dist: chinese_calendar>=1.6.0
23
+ Dynamic: license-file
24
+
25
+ # enconia-utils
26
+
27
+ Enconia 工具包,提供常用的 Python 工具函数集合。
28
+
29
+ ## 安装
30
+
31
+ ```bash
32
+ pip install enconia-utils
33
+ ```
34
+
35
+ ## 功能模块
36
+
37
+ ### ID 生成器 (`id_generator`)
38
+ - `get_random_short_id()` - 生成 8 位随机短 ID
39
+ - `get_random_long_id()` - 生成 22 位随机长 ID
40
+ - `get_fix_short_id(base_str)` - 根据输入字符串生成固定的短 ID
41
+ - `get_fix_long_id(base_str)` - 根据输入字符串生成固定的长 ID
42
+ - `get_file_md5(filepath)` - 计算文件 MD5
43
+
44
+ ### 日期工具 (`util_date`)
45
+ - `get_orbit_std_datetime()` - 获取标准格式的当前日期时间
46
+ - `get_orbit_std_date()` - 获取标准格式的当前日期
47
+ - `get_date_range_list_v2(start, end)` - 获取日期范围列表
48
+ - `get_next_day(day, interval)` - 获取下一天
49
+ - `get_next_workday_cn(day)` - 获取下一个中国工作日
50
+
51
+ ### 飞书推送 (`feishu_push`)
52
+ - `FeiShuTalkChatBot` - 飞书机器人消息推送
53
+
54
+ ### 阿里云 SLS 日志 (`aliyun_sls_log`)
55
+ - `send_log_to_sls()` - 发送日志到阿里云 SLS
56
+
57
+ ### 阿里云 OSS (`util_oss`)
58
+ - `OssCloudObjectProvider` - OSS 文件操作(上传、下载、复制、删除等)
59
+
60
+ ### 文件类型映射 (`util_type_mapping`)
61
+ - `get_content_type_4_filename(filename)` - 根据文件名获取 MIME 类型
62
+ - `file2content_type(ext)` - 扩展名转 MIME 类型
63
+ - `content2file_type(mime)` - MIME 类型转扩展名
64
+
65
+ ## 依赖
66
+
67
+ - requests
68
+ - pytz
69
+ - deprecated
70
+ - oss2
71
+ - aliyun-log-python-sdk
72
+ - chinese_calendar
73
+
74
+ ## License
75
+
76
+ BSD-3-Clause
@@ -0,0 +1,12 @@
1
+ enconia_utils/__init__.py,sha256=0WL5h1iPYVOdk64BMyLhzfDugzIkqfv9-dytW9YkapQ,1626
2
+ enconia_utils/aliyun_sls_log.py,sha256=GplzITXhIavmbuqY1DL723me2z1aai5JmOdOn_vWBzY,4654
3
+ enconia_utils/feishu_push.py,sha256=S1QWRPIImBuYLd4YXSDOFYa3Gnh1qFKybGjNfSVUefM,7089
4
+ enconia_utils/id_generator.py,sha256=nOhb8bVvjuLnHUkM1KVcuRtf1y5Q80Yj1iz4upPkioM,4036
5
+ enconia_utils/util_date.py,sha256=LRxr2UZ547M58OiMlsgx0nYVmqLF0ja0WvshvXiQE1Y,6361
6
+ enconia_utils/util_oss.py,sha256=-tSZTlmrv_TVaCRuRBvi6Qhbvk7ouuk1LWvc3ZBQZfU,4704
7
+ enconia_utils/util_type_mapping.py,sha256=4_BjV7DxqkJzwA2Wk6i4BVK7yVAgMNW8jfcORGJalsI,22821
8
+ enconia_utils-0.1.0.dist-info/licenses/LICENSE,sha256=rvK14ealwAT47qkAKUdC8oIuyMTtqFbBRNOPGY8xdGg,1534
9
+ enconia_utils-0.1.0.dist-info/METADATA,sha256=lHF6keCk0DCMPOX6TL3Ap2QdiOvMN8A2XRVf0Vrp1Ak,2387
10
+ enconia_utils-0.1.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
11
+ enconia_utils-0.1.0.dist-info/top_level.txt,sha256=UuvM6oHnoSut2ivfT3sDExg-jYYWZnZ44uJNAM-f6dY,14
12
+ enconia_utils-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.10.2)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,29 @@
1
+ BSD 3-Clause License
2
+
3
+ Copyright (c) 2026, Huanai Technology Co., Ltd.
4
+ All rights reserved.
5
+
6
+ Redistribution and use in source and binary forms, with or without
7
+ modification, are permitted provided that the following conditions are met:
8
+
9
+ 1. Redistributions of source code must retain the above copyright notice, this
10
+ list of conditions and the following disclaimer.
11
+
12
+ 2. Redistributions in binary form must reproduce the above copyright notice,
13
+ this list of conditions and the following disclaimer in the documentation
14
+ and/or other materials provided with the distribution.
15
+
16
+ 3. Neither the name of the copyright holder nor the names of its
17
+ contributors may be used to endorse or promote products derived from
18
+ this software without specific prior written permission.
19
+
20
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
24
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
26
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
27
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1 @@
1
+ enconia_utils