jwf-common-python 1.0.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.
- jwf_common_python/__init__.py +14 -0
- jwf_common_python/db_utils/__init__.py +6 -0
- jwf_common_python/db_utils/clickhouse.py +124 -0
- jwf_common_python/db_utils/elasticsearch.py +196 -0
- jwf_common_python/db_utils/mysql.py +64 -0
- jwf_common_python/eudic/__init__.py +5 -0
- jwf_common_python/eudic/wordbook.py +211 -0
- jwf_common_python/file_storage/__init__.py +2 -0
- jwf_common_python/file_storage/file_interface.py +29 -0
- jwf_common_python/file_storage/local.py +50 -0
- jwf_common_python/file_storage/tencent_cos.py +65 -0
- jwf_common_python/geography/__init__.py +8 -0
- jwf_common_python/geography/district_amap.py +181 -0
- jwf_common_python/geography/search_amap.py +84 -0
- jwf_common_python/github/__init__.py +5 -0
- jwf_common_python/github/webhooks.py +87 -0
- jwf_common_python/language_utils/__init__.py +34 -0
- jwf_common_python/language_utils/core.py +203 -0
- jwf_common_python/language_utils/files.py +52 -0
- jwf_common_python/language_utils/web.py +117 -0
- jwf_common_python/llm/__init__.py +1 -0
- jwf_common_python/llm/aliyun_qwen.py +10 -0
- jwf_common_python/llm/azure_gpt.py +66 -0
- jwf_common_python/llm/baidu_ernie.py +112 -0
- jwf_common_python/message_queue/__init__.py +7 -0
- jwf_common_python/message_queue/interface.py +49 -0
- jwf_common_python/message_queue/kafka.py +145 -0
- jwf_common_python/message_queue/rabbitmq.py +113 -0
- jwf_common_python/oauth2/__init__.py +0 -0
- jwf_common_python/oauth2/google.py +158 -0
- jwf_common_python/parse_subtitle/__init__.py +6 -0
- jwf_common_python/parse_subtitle/aa.vtt +2364 -0
- jwf_common_python/parse_subtitle/parse_srt_file.py +60 -0
- jwf_common_python/pdf/__init__.py +0 -0
- jwf_common_python/pdf/aaa.py +38 -0
- jwf_common_python/pdf/local_parse.py +259 -0
- jwf_common_python/sms/__init__.py +1 -0
- jwf_common_python/sms/interface.py +7 -0
- jwf_common_python/sms/tencent_sms.py +117 -0
- jwf_common_python/translate/__init__.py +152 -0
- jwf_common_python/translate/_youdao.py +234 -0
- jwf_common_python/translate/microsoft.py +696 -0
- jwf_common_python/translate/tencent.py +194 -0
- jwf_common_python/tts/__init__.py +0 -0
- jwf_common_python/tts/microsoft.py +87 -0
- jwf_common_python/utils/__init__.py +0 -0
- jwf_common_python/utils/image_utils.py +18 -0
- jwf_common_python/utils/tencent_utils.py +51 -0
- jwf_common_python/utils/utils.py +46 -0
- jwf_common_python/video/__init__.py +5 -0
- jwf_common_python/video/merge.py +276 -0
- jwf_common_python/weather/__init__.py +0 -0
- jwf_common_python/weather/amap.py +127 -0
- jwf_common_python/weather/wanwei.py +58 -0
- jwf_common_python/youdao/__init__.py +13 -0
- jwf_common_python/youdao/wordbook.py +88 -0
- jwf_common_python-1.0.0.dist-info/METADATA +669 -0
- jwf_common_python-1.0.0.dist-info/RECORD +60 -0
- jwf_common_python-1.0.0.dist-info/WHEEL +4 -0
- jwf_common_python-1.0.0.dist-info/licenses/LICENSE +201 -0
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from collections.abc import ByteString
|
|
3
|
+
|
|
4
|
+
from jwf_common_python.file_storage.file_interface import Storage
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class LocalStorage(Storage):
|
|
8
|
+
def __init__(self, **kwargs) -> None:
|
|
9
|
+
"""
|
|
10
|
+
1. try to create directory if not exist
|
|
11
|
+
Args:
|
|
12
|
+
storage_home_dir(str): 本地存储的家目录
|
|
13
|
+
|
|
14
|
+
Raises:
|
|
15
|
+
ValueError: _description_
|
|
16
|
+
ValueError: _description_
|
|
17
|
+
"""
|
|
18
|
+
if "storage_home_dir" not in kwargs:
|
|
19
|
+
raise ValueError("storage_home_dir must be provided")
|
|
20
|
+
storage_home_dir = kwargs["storage_home_dir"]
|
|
21
|
+
# 判断目录是否存在,不存在则创建
|
|
22
|
+
if not os.path.exists(storage_home_dir):
|
|
23
|
+
try:
|
|
24
|
+
os.mkdir(storage_home_dir)
|
|
25
|
+
except Exception as e:
|
|
26
|
+
raise ValueError(
|
|
27
|
+
"storage_home_dir does not exist and cannot be created"
|
|
28
|
+
)
|
|
29
|
+
self.storage_home_dir = storage_home_dir
|
|
30
|
+
|
|
31
|
+
def save(self, filename: str, content: ByteString) -> tuple[bool, str]:
|
|
32
|
+
"""保存文件内容到指定文件中.filename是文件名,不是文件全部路径"""
|
|
33
|
+
# 完整路径
|
|
34
|
+
file_path: str = os.path.join(self.storage_home_dir, filename)
|
|
35
|
+
try:
|
|
36
|
+
with open(file_path, "wb") as f:
|
|
37
|
+
f.write(content)
|
|
38
|
+
except Exception as e:
|
|
39
|
+
return False, e
|
|
40
|
+
else:
|
|
41
|
+
return True, file_path
|
|
42
|
+
|
|
43
|
+
def load(self, filename: str) -> tuple[bool, bytes]:
|
|
44
|
+
try:
|
|
45
|
+
with open(os.path.join(self.storage_home_dir, filename), "rb") as f:
|
|
46
|
+
content: bytes = f.read()
|
|
47
|
+
except Exception as e:
|
|
48
|
+
return False, e
|
|
49
|
+
else:
|
|
50
|
+
return True, content
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from collections.abc import ByteString
|
|
3
|
+
|
|
4
|
+
import requests
|
|
5
|
+
|
|
6
|
+
from jwf_common_python.file_storage.file_interface import Storage
|
|
7
|
+
from jwf_common_python.utils.tencent_utils import generate_tencent_key
|
|
8
|
+
|
|
9
|
+
# TODO sdk方式封装
|
|
10
|
+
# TODO 判断文件是否存在
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class TencentCos(Storage):
|
|
14
|
+
def __init__(self, **kwargs) -> None:
|
|
15
|
+
if not {"region", "bucket", "secret_key", "secret_id"}.issubset(kwargs):
|
|
16
|
+
raise ValueError("region,key,secret_key,secret_id must be provided")
|
|
17
|
+
self.bucket = kwargs["bucket"]
|
|
18
|
+
self.region = kwargs["region"]
|
|
19
|
+
self.secret_key = kwargs["secret_key"]
|
|
20
|
+
self.secret_id = kwargs["secret_id"]
|
|
21
|
+
|
|
22
|
+
# api 注明
|
|
23
|
+
def save(self, filename: str, content: ByteString) -> tuple[bool, str]:
|
|
24
|
+
"""只是文件名,而不是文件全部路径"""
|
|
25
|
+
url = f"https://{self.bucket}.cos.{self.region}.myqcloud.com/{filename}"
|
|
26
|
+
authorization: str = generate_tencent_key(
|
|
27
|
+
secret_id=self.secret_id,
|
|
28
|
+
secret_key=self.secret_key,
|
|
29
|
+
key=filename,
|
|
30
|
+
method="put",
|
|
31
|
+
)
|
|
32
|
+
try:
|
|
33
|
+
s = requests.put(
|
|
34
|
+
url=url,
|
|
35
|
+
headers={"Authorization": authorization, "Content-Type": "image/jpeg"},
|
|
36
|
+
data=content,
|
|
37
|
+
)
|
|
38
|
+
except Exception as e:
|
|
39
|
+
print(e)
|
|
40
|
+
return False, e
|
|
41
|
+
logging.info(
|
|
42
|
+
f"tencent cos save file {filename} success. {s.status_code=} {s.content=}"
|
|
43
|
+
)
|
|
44
|
+
return True, filename
|
|
45
|
+
|
|
46
|
+
def load(self, filename: str) -> tuple[bool, bytes]:
|
|
47
|
+
self.key = filename
|
|
48
|
+
url = f"https://{self.bucket}.cos.{self.region}.myqcloud.com/{self.key}"
|
|
49
|
+
authorization: str = generate_tencent_key(
|
|
50
|
+
secret_id=self.secret_id, secret_key=self.secret_key, key=self.key
|
|
51
|
+
)
|
|
52
|
+
try:
|
|
53
|
+
s = requests.get(
|
|
54
|
+
url=url,
|
|
55
|
+
headers={
|
|
56
|
+
"Authorization": authorization,
|
|
57
|
+
},
|
|
58
|
+
)
|
|
59
|
+
except Exception as e:
|
|
60
|
+
logging.error(f"tencent cos read failed {e}")
|
|
61
|
+
return False, b""
|
|
62
|
+
|
|
63
|
+
logging.info(f"tencent cos save file {self.key} success. {s.status_code}")
|
|
64
|
+
# print(s.content)
|
|
65
|
+
return True, s.content
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import requests
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class ChinaDistrictAmap:
|
|
5
|
+
"""获取中国的行政区域
|
|
6
|
+
使用 高德地图 获取中国的行政区域信息
|
|
7
|
+
|
|
8
|
+
city_code: 城市编码。只有市级行政区有citycode。
|
|
9
|
+
- 县/区、城镇/街道 没有自己的citycode,使用所在市的citycode
|
|
10
|
+
- 省级行政区没有citycode
|
|
11
|
+
adcode: 行政区域编码。
|
|
12
|
+
- 省、市、区/县 的adcode是唯一的
|
|
13
|
+
- 城镇/街道 使用所在区/县的adcode
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
def __init__(self, key: str):
|
|
17
|
+
self.key = key
|
|
18
|
+
self.base_url = "https://restapi.amap.com/v3/config/district"
|
|
19
|
+
self.country = "中华人民共和国"
|
|
20
|
+
self._ = {
|
|
21
|
+
"810000": "香港特别行政区",
|
|
22
|
+
"820000": "澳门特别行政区",
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
def _send_request(
|
|
26
|
+
self,
|
|
27
|
+
url: str,
|
|
28
|
+
name: str | None = None,
|
|
29
|
+
adcode: str | None = None,
|
|
30
|
+
citycode: str | None = None,
|
|
31
|
+
) -> dict:
|
|
32
|
+
"""
|
|
33
|
+
name,adcode,citycode 用于校验和解析有效的districts
|
|
34
|
+
- 默认为None, 校验条件是districts长度是1
|
|
35
|
+
- 如果不为None, districts长度大于1,则根据 name,adcode,citycode 过滤,找到唯一值
|
|
36
|
+
"""
|
|
37
|
+
response = requests.get(url)
|
|
38
|
+
if response.status_code != 200:
|
|
39
|
+
raise Exception("请求高德地图API失败")
|
|
40
|
+
res = response.json()
|
|
41
|
+
|
|
42
|
+
# 1. 校验响应状态
|
|
43
|
+
status = res["status"]
|
|
44
|
+
assert status == "1", f"请求高德地图API失败,status: {status}"
|
|
45
|
+
info = res.get("info")
|
|
46
|
+
assert info == "OK", f"请求高德地图API失败,info: {info}"
|
|
47
|
+
infocode = res.get("infocode")
|
|
48
|
+
assert infocode == "10000", f"请求高德地图API失败,infocode: {infocode}"
|
|
49
|
+
# count = res["count"]
|
|
50
|
+
|
|
51
|
+
# 2. 校验响应结构
|
|
52
|
+
districts = res["districts"]
|
|
53
|
+
# 符合条件的有效结果
|
|
54
|
+
response_data_info = []
|
|
55
|
+
if (name is not None) and (adcode is not None) and (citycode is not None):
|
|
56
|
+
# 3个同时不是None,进行过滤,此时districts长度 >= 1
|
|
57
|
+
assert len(districts) >= 1, "districts长度必须大于等于1"
|
|
58
|
+
for district in districts:
|
|
59
|
+
if (
|
|
60
|
+
district["name"] == name
|
|
61
|
+
and district["adcode"] == adcode
|
|
62
|
+
and district["citycode"] == citycode
|
|
63
|
+
):
|
|
64
|
+
response_data_info.append(district)
|
|
65
|
+
else:
|
|
66
|
+
assert len(districts) == 1
|
|
67
|
+
response_data_info = districts
|
|
68
|
+
|
|
69
|
+
# 有效结果只能有1个
|
|
70
|
+
if len(response_data_info) != 1:
|
|
71
|
+
raise Exception(f"未找到符合条件的district, districts: {districts}")
|
|
72
|
+
|
|
73
|
+
# 3. 将实际有效的内容进行提取返回
|
|
74
|
+
return response_data_info[0]
|
|
75
|
+
|
|
76
|
+
def get_privince(self) -> list[dict]:
|
|
77
|
+
"""获取省份
|
|
78
|
+
|
|
79
|
+
"citycode": [],
|
|
80
|
+
"adcode": "410000",
|
|
81
|
+
"name": "河南省",
|
|
82
|
+
"center": "113.753094,34.767052",
|
|
83
|
+
"level": "province",
|
|
84
|
+
"districts": []
|
|
85
|
+
"""
|
|
86
|
+
url = f"{self.base_url}?key={self.key}&keywords={self.country}"
|
|
87
|
+
|
|
88
|
+
china_info = self._send_request(url)
|
|
89
|
+
|
|
90
|
+
# 获取国家信息,判断是否是中国
|
|
91
|
+
china_adcode = china_info["adcode"]
|
|
92
|
+
china_name = china_info["name"]
|
|
93
|
+
level = china_info["level"]
|
|
94
|
+
assert china_adcode == "100000"
|
|
95
|
+
assert china_name == self.country
|
|
96
|
+
assert level == "country"
|
|
97
|
+
|
|
98
|
+
privinces: list[dict] = china_info["districts"]
|
|
99
|
+
|
|
100
|
+
return privinces
|
|
101
|
+
|
|
102
|
+
def get_city(self, privince_name: str, privince_adcode: str) -> list[dict]:
|
|
103
|
+
"""获取指定省份的所有城市
|
|
104
|
+
privince_name: 省份名称
|
|
105
|
+
privince_adcode: 省份行政区域编码
|
|
106
|
+
|
|
107
|
+
但是会跳过:香港特别行政区,澳门特别行政区,这2个属于省单位,但是没有市直接是区
|
|
108
|
+
因为这2个省级单位下面单位直接是 九龙城区 这种区,没有市所以直接跳过
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
返回:
|
|
112
|
+
1. 情况1
|
|
113
|
+
"citycode": "0379", # 城市编码
|
|
114
|
+
"adcode": "410300", # 行政区域编码
|
|
115
|
+
"name": "洛阳市",
|
|
116
|
+
"center": "112.453895,34.619702",
|
|
117
|
+
"level": "city",
|
|
118
|
+
"districts": []
|
|
119
|
+
2. 情况2
|
|
120
|
+
_message : "该省单位下面没有市"
|
|
121
|
+
"""
|
|
122
|
+
|
|
123
|
+
# 跳过对 香港、澳门 下面的区,进行获取区下面(的区)
|
|
124
|
+
if privince_adcode in self._.keys():
|
|
125
|
+
print(f"跳过对 {self._.get(privince_adcode)} 获取下级(市)行政区")
|
|
126
|
+
return [{"_message": "该省单位下面没有市"}]
|
|
127
|
+
|
|
128
|
+
url = f"{self.base_url}?key={self.key}&keywords={privince_name}"
|
|
129
|
+
privince_info = self._send_request(url)
|
|
130
|
+
|
|
131
|
+
# chekc
|
|
132
|
+
privince_adcode_res = privince_info["adcode"]
|
|
133
|
+
privince_name_res = privince_info["name"]
|
|
134
|
+
level = privince_info["level"]
|
|
135
|
+
assert level == "province"
|
|
136
|
+
assert privince_name_res == privince_name
|
|
137
|
+
assert privince_adcode_res == privince_adcode
|
|
138
|
+
|
|
139
|
+
city_info: list[dict] = privince_info["districts"]
|
|
140
|
+
return city_info
|
|
141
|
+
|
|
142
|
+
def get_district(
|
|
143
|
+
self, city_name: str, city_code: str, city_adcode: str
|
|
144
|
+
) -> list[dict]:
|
|
145
|
+
"""获取指定城市的所有区/县
|
|
146
|
+
city_name: 城市名称
|
|
147
|
+
city_code: 城市编码
|
|
148
|
+
city_adcode: 城市行政区域编码
|
|
149
|
+
|
|
150
|
+
返回结果:
|
|
151
|
+
"citycode": "0379",
|
|
152
|
+
"adcode": "410323",
|
|
153
|
+
"name": "新安县",
|
|
154
|
+
"center": "112.13246,34.728909",
|
|
155
|
+
"level": "district",
|
|
156
|
+
"districts": []
|
|
157
|
+
"""
|
|
158
|
+
url = f"{self.base_url}?key={self.key}&keywords={city_name}"
|
|
159
|
+
city_info = self._send_request(
|
|
160
|
+
url, name=city_name, adcode=city_adcode, citycode=city_code
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
# check
|
|
164
|
+
city_name_res = city_info["name"]
|
|
165
|
+
city_code_res = city_info["citycode"]
|
|
166
|
+
city_adcode_res = city_info["adcode"]
|
|
167
|
+
level = city_info["level"]
|
|
168
|
+
|
|
169
|
+
# 如果是香港、澳门,则level为province
|
|
170
|
+
# --- 暂时不对比citycode了
|
|
171
|
+
if city_name in self._.values() and city_adcode in self._.keys():
|
|
172
|
+
assert level == "province"
|
|
173
|
+
else:
|
|
174
|
+
assert level == "city"
|
|
175
|
+
|
|
176
|
+
assert city_name_res == city_name
|
|
177
|
+
assert city_code_res == city_code
|
|
178
|
+
assert city_adcode_res == city_adcode
|
|
179
|
+
|
|
180
|
+
district_info: list[dict] = city_info["districts"]
|
|
181
|
+
return district_info
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import time
|
|
2
|
+
import requests
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class SearchLocationAmap:
|
|
6
|
+
"""高德地图API搜索地理位置
|
|
7
|
+
- 使用2.0版本的API
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
def __init__(self, key: str):
|
|
11
|
+
self.key = key
|
|
12
|
+
self.page_size = 10
|
|
13
|
+
self.types = "汽车服务|汽车销售|汽车维修|摩托车服务|餐饮服务|购物服务|生活服务\
|
|
14
|
+
|体育休闲服务|医疗保健服务|住宿服务|风景名胜|商务住宅|政府机构及社会团体|科教文化服务\
|
|
15
|
+
|交通设施服务|金融保险服务|公司企业|道路附属设施|地名地址信息|公共设施|事件活动|室内设施|虚拟数据|通行设施"
|
|
16
|
+
self.base_url = f"https://restapi.amap.com/v5/place/text?key={self.key}\
|
|
17
|
+
&types={self.types}&city_limit=true&page_size={self.page_size}"
|
|
18
|
+
|
|
19
|
+
def _request(self, url: str):
|
|
20
|
+
response = requests.get(url)
|
|
21
|
+
if response.status_code != 200:
|
|
22
|
+
raise Exception(f"请求失败,状态码:{response.status_code}")
|
|
23
|
+
|
|
24
|
+
res = response.json()
|
|
25
|
+
if res["info"] != "OK":
|
|
26
|
+
raise Exception(f"请求失败,错误信息:{res['info']}")
|
|
27
|
+
return res
|
|
28
|
+
|
|
29
|
+
def search(
|
|
30
|
+
self,
|
|
31
|
+
keyword: str,
|
|
32
|
+
city_name: str,
|
|
33
|
+
) -> list[dict]:
|
|
34
|
+
"""搜索指定城市,指定地理位置.
|
|
35
|
+
keywords: 地点关键字,需要被检索的地点文本信息。
|
|
36
|
+
city_name: 搜索区划, citycode,adcode,cityname;cityname 仅支持城市级别和中文,如“北京市”。
|
|
37
|
+
|
|
38
|
+
返回数据示例:
|
|
39
|
+
{
|
|
40
|
+
"parent": "",
|
|
41
|
+
"address": "通衢路19号",
|
|
42
|
+
"distance": "",
|
|
43
|
+
"pcode": "410000",
|
|
44
|
+
"adcode": "410311",
|
|
45
|
+
"pname": "河南省",
|
|
46
|
+
"cityname": "洛阳市",
|
|
47
|
+
"type": "交通设施服务;火车站;火车站",
|
|
48
|
+
"typecode": "150200",
|
|
49
|
+
"adname": "洛龙区",
|
|
50
|
+
"citycode": "0379",
|
|
51
|
+
"name": "洛阳龙门站",
|
|
52
|
+
"location": "112.456291,34.593842",
|
|
53
|
+
"id": "B017B02U7H"
|
|
54
|
+
},
|
|
55
|
+
"""
|
|
56
|
+
# check
|
|
57
|
+
# 文本总长度不可超过80字符,这里限制为50
|
|
58
|
+
if len(keyword) > 50:
|
|
59
|
+
raise ValueError("keywords长度不能超过50")
|
|
60
|
+
|
|
61
|
+
res = []
|
|
62
|
+
num = 1
|
|
63
|
+
|
|
64
|
+
while True:
|
|
65
|
+
# 1. 获取数据
|
|
66
|
+
url = (
|
|
67
|
+
f"{self.base_url}&keywords={keyword}®ion={city_name}&page_num={num}"
|
|
68
|
+
)
|
|
69
|
+
response = self._request(url)
|
|
70
|
+
|
|
71
|
+
# 2. 更新数据
|
|
72
|
+
pois = response["pois"]
|
|
73
|
+
res.extend(pois)
|
|
74
|
+
|
|
75
|
+
# 3. 判断是否进行下一页的数据获取
|
|
76
|
+
if int(response["count"]) != self.page_size:
|
|
77
|
+
# 本次请求返回的数据个数 不等于 page_size,说明本次请求是最后一页. 数据获取完毕
|
|
78
|
+
break
|
|
79
|
+
else:
|
|
80
|
+
# 存在下一页, 更新num
|
|
81
|
+
num += 1
|
|
82
|
+
# 限制请求次数,避免出现1秒超过3次的限制
|
|
83
|
+
time.sleep(1)
|
|
84
|
+
return res
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""基于原始请求体的 GitHub SHA-256 验签,不记录密钥或签名。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
import hmac
|
|
7
|
+
import re
|
|
8
|
+
from collections.abc import Callable
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _secret_bytes(secret: str | bytes) -> bytes:
|
|
12
|
+
if isinstance(secret, str):
|
|
13
|
+
secret = secret.encode("utf-8")
|
|
14
|
+
if not isinstance(secret, bytes):
|
|
15
|
+
raise TypeError("secret 必须为 str 或 bytes")
|
|
16
|
+
if not secret:
|
|
17
|
+
raise ValueError("secret 不能为空")
|
|
18
|
+
return secret
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def verify_signature(
|
|
22
|
+
payload: bytes, secret: str | bytes, signature_header: str | None
|
|
23
|
+
) -> bool:
|
|
24
|
+
"""校验 X-Hub-Signature-256,返回 bool;密钥必须由调用方传入。
|
|
25
|
+
|
|
26
|
+
payload 必须为原始请求体字节,不能是重新序列化的 JSON。
|
|
27
|
+
签名缺失或格式错误时返回 False,配置/参数类型错误时抛出异常。
|
|
28
|
+
"""
|
|
29
|
+
key = _secret_bytes(secret)
|
|
30
|
+
if not isinstance(payload, bytes):
|
|
31
|
+
raise TypeError("payload 必须为原始请求体 bytes")
|
|
32
|
+
if (
|
|
33
|
+
not isinstance(signature_header, str)
|
|
34
|
+
or re.fullmatch(r"sha256=[0-9a-f]{64}", signature_header) is None
|
|
35
|
+
):
|
|
36
|
+
return False
|
|
37
|
+
expected = "sha256=" + hmac.new(key, payload, hashlib.sha256).hexdigest()
|
|
38
|
+
return hmac.compare_digest(expected, signature_header)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def create_webhook_app(
|
|
42
|
+
secret: str | bytes,
|
|
43
|
+
*,
|
|
44
|
+
path: str = "/hook",
|
|
45
|
+
handler: Callable[[bytes, str | None, str | None], None] | None = None,
|
|
46
|
+
):
|
|
47
|
+
"""返回 Flask 应用,不启动服务器;只有调用此函数才导入 Flask。
|
|
48
|
+
|
|
49
|
+
只接受 POST。验签失败返回 403,成功时依次调用
|
|
50
|
+
handler(payload, event, delivery_id),然后返回 {"status": 200}。
|
|
51
|
+
请求体不强制为 JSON,回调自行解析;回调失败由 Flask 按异常处理,不返回成功。
|
|
52
|
+
"""
|
|
53
|
+
key = _secret_bytes(secret)
|
|
54
|
+
if (
|
|
55
|
+
not isinstance(path, str)
|
|
56
|
+
or not path.startswith("/")
|
|
57
|
+
or "<" in path
|
|
58
|
+
or ">" in path
|
|
59
|
+
):
|
|
60
|
+
raise ValueError("path 必须为以 / 开头的静态路由路径")
|
|
61
|
+
if handler is not None and not callable(handler):
|
|
62
|
+
raise TypeError("handler 必须可调用或为 None")
|
|
63
|
+
try:
|
|
64
|
+
from flask import Flask, jsonify, request
|
|
65
|
+
except ImportError as exc:
|
|
66
|
+
raise ImportError("HTTP 接入需要可选依赖:pip install flask") from exc
|
|
67
|
+
|
|
68
|
+
app = Flask(__name__)
|
|
69
|
+
|
|
70
|
+
def webhook():
|
|
71
|
+
payload = request.get_data(cache=True)
|
|
72
|
+
if not verify_signature(
|
|
73
|
+
payload, key, request.headers.get("X-Hub-Signature-256")
|
|
74
|
+
):
|
|
75
|
+
return jsonify({"error": "invalid_signature"}), 403
|
|
76
|
+
if handler is not None:
|
|
77
|
+
handler(
|
|
78
|
+
payload,
|
|
79
|
+
request.headers.get("X-GitHub-Event"),
|
|
80
|
+
request.headers.get("X-GitHub-Delivery"),
|
|
81
|
+
)
|
|
82
|
+
return jsonify({"status": 200})
|
|
83
|
+
|
|
84
|
+
app.add_url_rule(
|
|
85
|
+
path, endpoint="github_webhook", view_func=webhook, methods=["POST"]
|
|
86
|
+
)
|
|
87
|
+
return app
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""英文文本取词、词表处理与网页取词工具。"""
|
|
2
|
+
|
|
3
|
+
from .core import (
|
|
4
|
+
extract_chapter,
|
|
5
|
+
extract_words,
|
|
6
|
+
filter_words,
|
|
7
|
+
format_words,
|
|
8
|
+
get_chapter_words,
|
|
9
|
+
group_words_by_length,
|
|
10
|
+
is_title_case_word,
|
|
11
|
+
parse_urls,
|
|
12
|
+
sort_words_by_length,
|
|
13
|
+
word_frequencies,
|
|
14
|
+
)
|
|
15
|
+
from .files import generate_file_menu, read_urls, read_word_lists, read_words
|
|
16
|
+
from .web import fetch_words_from_pages
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
"extract_words",
|
|
20
|
+
"filter_words",
|
|
21
|
+
"extract_chapter",
|
|
22
|
+
"get_chapter_words",
|
|
23
|
+
"word_frequencies",
|
|
24
|
+
"format_words",
|
|
25
|
+
"group_words_by_length",
|
|
26
|
+
"sort_words_by_length",
|
|
27
|
+
"is_title_case_word",
|
|
28
|
+
"parse_urls",
|
|
29
|
+
"read_words",
|
|
30
|
+
"read_word_lists",
|
|
31
|
+
"read_urls",
|
|
32
|
+
"generate_file_menu",
|
|
33
|
+
"fetch_words_from_pages",
|
|
34
|
+
]
|