cloudoll 2.3.2__tar.gz → 2.4.0__tar.gz
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.
- {cloudoll-2.3.2 → cloudoll-2.4.0}/PKG-INFO +1 -2
- cloudoll-2.4.0/cloudoll/__init__.py +1 -0
- {cloudoll-2.3.2 → cloudoll-2.4.0}/cloudoll/utils/common.py +9 -3
- cloudoll-2.4.0/cloudoll/web/requests.py +161 -0
- {cloudoll-2.3.2 → cloudoll-2.4.0}/cloudoll.egg-info/PKG-INFO +1 -2
- {cloudoll-2.3.2 → cloudoll-2.4.0}/cloudoll.egg-info/SOURCES.txt +1 -5
- {cloudoll-2.3.2 → cloudoll-2.4.0}/cloudoll.egg-info/requires.txt +0 -1
- {cloudoll-2.3.2 → cloudoll-2.4.0}/setup.py +0 -1
- cloudoll-2.3.2/cloudoll/__init__.py +0 -1
- cloudoll-2.3.2/cloudoll/robot/dingtalk.py +0 -209
- cloudoll-2.3.2/cloudoll/robot/feishu.py +0 -102
- cloudoll-2.3.2/cloudoll/robot/slack.py +0 -115
- cloudoll-2.3.2/cloudoll/utils/__init__.py +0 -0
- cloudoll-2.3.2/cloudoll/web/http.py +0 -79
- {cloudoll-2.3.2 → cloudoll-2.4.0}/LICENSE +0 -0
- {cloudoll-2.3.2 → cloudoll-2.4.0}/README.md +0 -0
- {cloudoll-2.3.2 → cloudoll-2.4.0}/cloudoll/__main__.py +0 -0
- {cloudoll-2.3.2 → cloudoll-2.4.0}/cloudoll/cli.py +0 -0
- {cloudoll-2.3.2 → cloudoll-2.4.0}/cloudoll/error/__init__.py +0 -0
- {cloudoll-2.3.2 → cloudoll-2.4.0}/cloudoll/error/errors.py +0 -0
- {cloudoll-2.3.2 → cloudoll-2.4.0}/cloudoll/logging/__init__.py +0 -0
- {cloudoll-2.3.2 → cloudoll-2.4.0}/cloudoll/mail/__init__.py +0 -0
- {cloudoll-2.3.2 → cloudoll-2.4.0}/cloudoll/mail/smtp.py +0 -0
- {cloudoll-2.3.2 → cloudoll-2.4.0}/cloudoll/orm/__init__.py +0 -0
- {cloudoll-2.3.2 → cloudoll-2.4.0}/cloudoll/orm/base.py +0 -0
- {cloudoll-2.3.2 → cloudoll-2.4.0}/cloudoll/orm/err.py +0 -0
- {cloudoll-2.3.2 → cloudoll-2.4.0}/cloudoll/orm/field.py +0 -0
- {cloudoll-2.3.2 → cloudoll-2.4.0}/cloudoll/orm/model.py +0 -0
- {cloudoll-2.3.2 → cloudoll-2.4.0}/cloudoll/orm/mysql.py +0 -0
- {cloudoll-2.3.2 → cloudoll-2.4.0}/cloudoll/orm/parse.py +0 -0
- {cloudoll-2.3.2 → cloudoll-2.4.0}/cloudoll/orm/postgres.py +0 -0
- {cloudoll-2.3.2/cloudoll/robot → cloudoll-2.4.0/cloudoll/utils}/__init__.py +0 -0
- {cloudoll-2.3.2 → cloudoll-2.4.0}/cloudoll/utils/cli_main.py +0 -0
- {cloudoll-2.3.2 → cloudoll-2.4.0}/cloudoll/utils/m2d.py +0 -0
- {cloudoll-2.3.2 → cloudoll-2.4.0}/cloudoll/utils/patch_loop.py +0 -0
- {cloudoll-2.3.2 → cloudoll-2.4.0}/cloudoll/utils/snowflake.py +0 -0
- {cloudoll-2.3.2 → cloudoll-2.4.0}/cloudoll/utils/watch.py +0 -0
- {cloudoll-2.3.2 → cloudoll-2.4.0}/cloudoll/web/__init__.py +0 -0
- {cloudoll-2.3.2 → cloudoll-2.4.0}/cloudoll/web/core.py +0 -0
- {cloudoll-2.3.2 → cloudoll-2.4.0}/cloudoll/web/exception.py +0 -0
- {cloudoll-2.3.2 → cloudoll-2.4.0}/cloudoll/web/html.py +0 -0
- {cloudoll-2.3.2 → cloudoll-2.4.0}/cloudoll/web/jwt.py +0 -0
- {cloudoll-2.3.2 → cloudoll-2.4.0}/cloudoll/web/settings.py +0 -0
- {cloudoll-2.3.2 → cloudoll-2.4.0}/cloudoll.egg-info/dependency_links.txt +0 -0
- {cloudoll-2.3.2 → cloudoll-2.4.0}/cloudoll.egg-info/entry_points.txt +0 -0
- {cloudoll-2.3.2 → cloudoll-2.4.0}/cloudoll.egg-info/top_level.txt +0 -0
- {cloudoll-2.3.2 → cloudoll-2.4.0}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cloudoll
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.4.0
|
|
4
4
|
Summary: 辅助快速创建可分布的微服务。
|
|
5
5
|
Home-page: https://gitee.com/chuchur/cloudoll-py
|
|
6
6
|
Author: chuchur
|
|
@@ -16,7 +16,6 @@ Requires-Python: >=3.6.0
|
|
|
16
16
|
Description-Content-Type: text/markdown
|
|
17
17
|
License-File: LICENSE
|
|
18
18
|
Requires-Dist: watchfiles
|
|
19
|
-
Requires-Dist: curl_cffi
|
|
20
19
|
Requires-Dist: colorlog
|
|
21
20
|
Requires-Dist: aiomysql
|
|
22
21
|
Requires-Dist: aiopg
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "2.4.0"
|
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
import asyncio
|
|
2
|
+
from ..logging import warning
|
|
3
|
+
|
|
4
|
+
EADDRINUSE: int
|
|
5
|
+
|
|
6
|
+
|
|
2
7
|
class Object(dict):
|
|
3
8
|
# __setattr__ = dict.__setitem__
|
|
4
9
|
# __getattr__ = dict.__getitem__
|
|
@@ -7,20 +12,21 @@ class Object(dict):
|
|
|
7
12
|
|
|
8
13
|
def __getattr__(self, key):
|
|
9
14
|
return self.get(key, None)
|
|
10
|
-
|
|
15
|
+
|
|
11
16
|
def __delattr__(self, key):
|
|
12
17
|
try:
|
|
13
18
|
del self[key]
|
|
14
19
|
except KeyError:
|
|
15
20
|
raise AttributeError(f"'DotDict' object has no attribute '{key}'")
|
|
16
|
-
|
|
21
|
+
|
|
17
22
|
def __getstate__(self):
|
|
18
|
-
|
|
23
|
+
"""to fix pickle.dumps"""
|
|
19
24
|
return dict(self)
|
|
20
25
|
|
|
21
26
|
def __setstate__(self, state):
|
|
22
27
|
self.update(state)
|
|
23
28
|
|
|
29
|
+
|
|
24
30
|
def chainMap(*dicts):
|
|
25
31
|
merged_dict = Object()
|
|
26
32
|
for d in dicts:
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import aiohttp
|
|
2
|
+
import asyncio
|
|
3
|
+
from typing import Optional, Dict, Any, Union
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class AsyncRequest:
|
|
7
|
+
def __init__(
|
|
8
|
+
self,
|
|
9
|
+
timeout: int = 30,
|
|
10
|
+
max_retries: int = 3,
|
|
11
|
+
retry_delay: float = 1.0,
|
|
12
|
+
session: Optional[aiohttp.ClientSession] = None,
|
|
13
|
+
):
|
|
14
|
+
"""
|
|
15
|
+
异步HTTP请求客户端
|
|
16
|
+
|
|
17
|
+
:param timeout: 请求超时时间(秒)
|
|
18
|
+
:param max_retries: 最大重试次数
|
|
19
|
+
:param retry_delay: 重试延迟(秒)
|
|
20
|
+
:param session: 可复用的aiohttp会话
|
|
21
|
+
"""
|
|
22
|
+
self.timeout = aiohttp.ClientTimeout(total=timeout)
|
|
23
|
+
self.max_retries = max_retries
|
|
24
|
+
self.retry_delay = retry_delay
|
|
25
|
+
self._session = session
|
|
26
|
+
|
|
27
|
+
async def request(
|
|
28
|
+
self,
|
|
29
|
+
method: str,
|
|
30
|
+
url: str,
|
|
31
|
+
params: Optional[Dict[str, Any]] = None,
|
|
32
|
+
data: Optional[Union[Dict[str, Any], str, bytes]] = None,
|
|
33
|
+
json_data: Optional[Any] = None,
|
|
34
|
+
headers: Optional[Dict[str, str]] = None,
|
|
35
|
+
cookies: Optional[Dict[str, str]] = None,
|
|
36
|
+
allow_redirects: bool = True,
|
|
37
|
+
raise_for_status: bool = True,
|
|
38
|
+
) -> aiohttp.ClientResponse:
|
|
39
|
+
"""
|
|
40
|
+
执行异步HTTP请求
|
|
41
|
+
|
|
42
|
+
:param method: HTTP方法 (GET, POST, PUT, DELETE等)
|
|
43
|
+
:param url: 请求URL
|
|
44
|
+
:param params: URL查询参数
|
|
45
|
+
:param data: 请求体数据 (表单或原始数据)
|
|
46
|
+
:param json_data: JSON格式的请求体数据
|
|
47
|
+
:param headers: 请求头
|
|
48
|
+
:param cookies: cookies
|
|
49
|
+
:param allow_redirects: 是否允许重定向
|
|
50
|
+
:param raise_for_status: 是否在HTTP错误状态时抛出异常
|
|
51
|
+
:return: aiohttp.ClientResponse
|
|
52
|
+
"""
|
|
53
|
+
session = self._session or aiohttp.ClientSession(timeout=self.timeout)
|
|
54
|
+
last_exception = None
|
|
55
|
+
|
|
56
|
+
for attempt in range(self.max_retries):
|
|
57
|
+
try:
|
|
58
|
+
async with session.request(
|
|
59
|
+
method=method,
|
|
60
|
+
url=url,
|
|
61
|
+
params=params,
|
|
62
|
+
data=data,
|
|
63
|
+
json=json_data,
|
|
64
|
+
headers=headers,
|
|
65
|
+
cookies=cookies,
|
|
66
|
+
allow_redirects=allow_redirects,
|
|
67
|
+
) as response:
|
|
68
|
+
if raise_for_status:
|
|
69
|
+
response.raise_for_status()
|
|
70
|
+
return response
|
|
71
|
+
|
|
72
|
+
except (aiohttp.ClientError, asyncio.TimeoutError) as e:
|
|
73
|
+
last_exception = e
|
|
74
|
+
if attempt < self.max_retries - 1:
|
|
75
|
+
await asyncio.sleep(self.retry_delay * (attempt + 1))
|
|
76
|
+
continue
|
|
77
|
+
|
|
78
|
+
if self._session is None:
|
|
79
|
+
await session.close()
|
|
80
|
+
|
|
81
|
+
raise last_exception or RuntimeError("Request failed")
|
|
82
|
+
|
|
83
|
+
async def get(
|
|
84
|
+
self,
|
|
85
|
+
url: str,
|
|
86
|
+
params: Optional[Dict[str, Any]] = None,
|
|
87
|
+
headers: Optional[Dict[str, str]] = None,
|
|
88
|
+
**kwargs
|
|
89
|
+
) -> aiohttp.ClientResponse:
|
|
90
|
+
"""发送GET请求"""
|
|
91
|
+
return await self.request("GET", url, params=params, headers=headers, **kwargs)
|
|
92
|
+
|
|
93
|
+
async def post(
|
|
94
|
+
self,
|
|
95
|
+
url: str,
|
|
96
|
+
data: Optional[Union[Dict[str, Any], str, bytes]] = None,
|
|
97
|
+
json_data: Optional[Any] = None,
|
|
98
|
+
headers: Optional[Dict[str, str]] = None,
|
|
99
|
+
**kwargs
|
|
100
|
+
) -> aiohttp.ClientResponse:
|
|
101
|
+
"""发送POST请求"""
|
|
102
|
+
return await self.request(
|
|
103
|
+
"POST", url, data=data, json_data=json_data, headers=headers, **kwargs
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
async def put(
|
|
107
|
+
self,
|
|
108
|
+
url: str,
|
|
109
|
+
data: Optional[Union[Dict[str, Any], str, bytes]] = None,
|
|
110
|
+
json_data: Optional[Any] = None,
|
|
111
|
+
headers: Optional[Dict[str, str]] = None,
|
|
112
|
+
**kwargs
|
|
113
|
+
) -> aiohttp.ClientResponse:
|
|
114
|
+
"""发送PUT请求"""
|
|
115
|
+
return await self.request(
|
|
116
|
+
"PUT", url, data=data, json_data=json_data, headers=headers, **kwargs
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
async def delete(
|
|
120
|
+
self, url: str, headers: Optional[Dict[str, str]] = None, **kwargs
|
|
121
|
+
) -> aiohttp.ClientResponse:
|
|
122
|
+
"""发送DELETE请求"""
|
|
123
|
+
return await self.request("DELETE", url, headers=headers, **kwargs)
|
|
124
|
+
|
|
125
|
+
async def close(self) -> None:
|
|
126
|
+
"""关闭客户端会话"""
|
|
127
|
+
if self._session is not None:
|
|
128
|
+
await self._session.close()
|
|
129
|
+
self._session = None
|
|
130
|
+
|
|
131
|
+
async def __aenter__(self):
|
|
132
|
+
return self
|
|
133
|
+
|
|
134
|
+
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
|
135
|
+
await self.close()
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
# 使用示例
|
|
139
|
+
"""
|
|
140
|
+
async def test():
|
|
141
|
+
async with AsyncRequest() as client:
|
|
142
|
+
try:
|
|
143
|
+
# GET请求示例
|
|
144
|
+
response = await client.get(
|
|
145
|
+
"https://httpbin.org/get",
|
|
146
|
+
params={"key": "value"},
|
|
147
|
+
headers={"User-Agent": "AsyncRequest/1.0"},
|
|
148
|
+
)
|
|
149
|
+
print(await response.json())
|
|
150
|
+
|
|
151
|
+
# POST请求示例
|
|
152
|
+
response = await client.post(
|
|
153
|
+
"https://httpbin.org/post",
|
|
154
|
+
json_data={"key": "value"},
|
|
155
|
+
headers={"Content-Type": "application/json"},
|
|
156
|
+
)
|
|
157
|
+
print(await response.json())
|
|
158
|
+
|
|
159
|
+
except Exception as e:
|
|
160
|
+
print(f"Request failed: {e}")
|
|
161
|
+
"""
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cloudoll
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.4.0
|
|
4
4
|
Summary: 辅助快速创建可分布的微服务。
|
|
5
5
|
Home-page: https://gitee.com/chuchur/cloudoll-py
|
|
6
6
|
Author: chuchur
|
|
@@ -16,7 +16,6 @@ Requires-Python: >=3.6.0
|
|
|
16
16
|
Description-Content-Type: text/markdown
|
|
17
17
|
License-File: LICENSE
|
|
18
18
|
Requires-Dist: watchfiles
|
|
19
|
-
Requires-Dist: curl_cffi
|
|
20
19
|
Requires-Dist: colorlog
|
|
21
20
|
Requires-Dist: aiomysql
|
|
22
21
|
Requires-Dist: aiopg
|
|
@@ -23,10 +23,6 @@ cloudoll/orm/model.py
|
|
|
23
23
|
cloudoll/orm/mysql.py
|
|
24
24
|
cloudoll/orm/parse.py
|
|
25
25
|
cloudoll/orm/postgres.py
|
|
26
|
-
cloudoll/robot/__init__.py
|
|
27
|
-
cloudoll/robot/dingtalk.py
|
|
28
|
-
cloudoll/robot/feishu.py
|
|
29
|
-
cloudoll/robot/slack.py
|
|
30
26
|
cloudoll/utils/__init__.py
|
|
31
27
|
cloudoll/utils/cli_main.py
|
|
32
28
|
cloudoll/utils/common.py
|
|
@@ -38,6 +34,6 @@ cloudoll/web/__init__.py
|
|
|
38
34
|
cloudoll/web/core.py
|
|
39
35
|
cloudoll/web/exception.py
|
|
40
36
|
cloudoll/web/html.py
|
|
41
|
-
cloudoll/web/http.py
|
|
42
37
|
cloudoll/web/jwt.py
|
|
38
|
+
cloudoll/web/requests.py
|
|
43
39
|
cloudoll/web/settings.py
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "2.3.2"
|
|
@@ -1,209 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
# -*- coding: utf-8 -*-
|
|
3
|
-
|
|
4
|
-
# docs: https://open.dingtalk.com/document/group/message-types-and-data-format
|
|
5
|
-
__author__ = "chuchur/chuchur.com"
|
|
6
|
-
|
|
7
|
-
import base64
|
|
8
|
-
import hashlib
|
|
9
|
-
import hmac
|
|
10
|
-
from ..logging import print_error
|
|
11
|
-
from curl_cffi import requests as http
|
|
12
|
-
import time
|
|
13
|
-
from urllib import parse
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
class Client(object):
|
|
17
|
-
def __init__(self, **kw):
|
|
18
|
-
self._webhook = kw.get("webhook")
|
|
19
|
-
self._secret = kw.get("secret")
|
|
20
|
-
self._access_token = kw.get("access_token")
|
|
21
|
-
|
|
22
|
-
def _get_sign(self):
|
|
23
|
-
secret = self._secret
|
|
24
|
-
if not secret:
|
|
25
|
-
print_error("缺少secret")
|
|
26
|
-
return None, None
|
|
27
|
-
|
|
28
|
-
# 拼接timestamp和secret
|
|
29
|
-
timestamp = str(round(time.time() * 1000))
|
|
30
|
-
string_to_sign = "{}\n{}".format(timestamp, secret)
|
|
31
|
-
hmac_code = hmac.new(
|
|
32
|
-
secret.encode("utf-8"),
|
|
33
|
-
string_to_sign.encode("utf-8"),
|
|
34
|
-
digestmod=hashlib.sha256,
|
|
35
|
-
).digest()
|
|
36
|
-
|
|
37
|
-
# 对结果进行base64处理
|
|
38
|
-
sign = base64.b64encode(hmac_code).decode("utf-8")
|
|
39
|
-
sign = parse.quote(sign)
|
|
40
|
-
return timestamp, sign
|
|
41
|
-
|
|
42
|
-
def upload(self, type: str, filepath: str):
|
|
43
|
-
"""
|
|
44
|
-
上传媒体
|
|
45
|
-
:params access_token
|
|
46
|
-
:params type 文件类型 image|voice|video|file
|
|
47
|
-
:params filepath 要上传的文件路径
|
|
48
|
-
"""
|
|
49
|
-
access_token = self._access_token
|
|
50
|
-
if not access_token:
|
|
51
|
-
print_error("请配置access_token")
|
|
52
|
-
return False
|
|
53
|
-
url = "https://oapi.dingtalk.com/media/upload?access_token=%s" % access_token
|
|
54
|
-
with open(filepath, "rb") as f:
|
|
55
|
-
files = {"media": f}
|
|
56
|
-
result = http.post(url=url, files=files, data={"type": type})
|
|
57
|
-
res = result.json()
|
|
58
|
-
if res["errcode"] == 0:
|
|
59
|
-
return res
|
|
60
|
-
else:
|
|
61
|
-
print_error(res["errmsg"])
|
|
62
|
-
|
|
63
|
-
def send(self, data: dict):
|
|
64
|
-
"""
|
|
65
|
-
发送除签名外的自定义消息结构体 https://open.dingtalk.com/document/orgapp-server/message-types-and-data-format
|
|
66
|
-
:params data 自定义消息结构体
|
|
67
|
-
"""
|
|
68
|
-
webhook = self._webhook
|
|
69
|
-
if not webhook:
|
|
70
|
-
print_error("缺少webhook")
|
|
71
|
-
return False
|
|
72
|
-
timestamp, sign = self._get_sign()
|
|
73
|
-
if not timestamp or not sign:
|
|
74
|
-
return False
|
|
75
|
-
url = webhook + "×tamp=%s&sign=%s" % (timestamp, sign)
|
|
76
|
-
result = http.post(
|
|
77
|
-
url=url,
|
|
78
|
-
json={**data},
|
|
79
|
-
# data=data
|
|
80
|
-
# data=json.dumps(data)
|
|
81
|
-
)
|
|
82
|
-
if result:
|
|
83
|
-
res = result.json()
|
|
84
|
-
if res["errcode"] == 0:
|
|
85
|
-
print_error("发送成功!")
|
|
86
|
-
return True
|
|
87
|
-
else:
|
|
88
|
-
print_error("发送失败")
|
|
89
|
-
print_error(res["errmsg"])
|
|
90
|
-
return False
|
|
91
|
-
|
|
92
|
-
def send_text(self, text: str):
|
|
93
|
-
"""
|
|
94
|
-
发送文本消息
|
|
95
|
-
:params text 消息内容
|
|
96
|
-
"""
|
|
97
|
-
|
|
98
|
-
return self.send(
|
|
99
|
-
data={
|
|
100
|
-
"msgtype": "text",
|
|
101
|
-
"text": {"content": text},
|
|
102
|
-
}
|
|
103
|
-
)
|
|
104
|
-
|
|
105
|
-
def _send_media(self, type, media_id, **kw):
|
|
106
|
-
"""
|
|
107
|
-
发送媒体
|
|
108
|
-
:params type 媒体类型 image|voice|video|file
|
|
109
|
-
:params media_id 媒体ID
|
|
110
|
-
"""
|
|
111
|
-
data = {"msgtype": type}
|
|
112
|
-
data[type] = {"media_id": media_id}
|
|
113
|
-
if type == "voice":
|
|
114
|
-
data["voice"]["duration"] = kw.get("duration")
|
|
115
|
-
return self.send(data)
|
|
116
|
-
|
|
117
|
-
def send_markdown(self, title, text):
|
|
118
|
-
"""
|
|
119
|
-
发送Markdown消息
|
|
120
|
-
:params title 标题
|
|
121
|
-
:params text markdown结构体
|
|
122
|
-
"""
|
|
123
|
-
return self.send(
|
|
124
|
-
data={"msgtype": "markdown", "markdown": {"title": title, "text": text}}
|
|
125
|
-
)
|
|
126
|
-
|
|
127
|
-
def send_image(self, media_id: str):
|
|
128
|
-
"""
|
|
129
|
-
发送图片消息
|
|
130
|
-
:params media_id 媒体ID
|
|
131
|
-
"""
|
|
132
|
-
return self._send_media("image", media_id)
|
|
133
|
-
|
|
134
|
-
def send_voice(self, media_id: str, duration: str):
|
|
135
|
-
"""
|
|
136
|
-
发送语音消息
|
|
137
|
-
:params media_id 媒体ID
|
|
138
|
-
:params duration 正整数,小于60,表示音频时长。
|
|
139
|
-
"""
|
|
140
|
-
return self._send_media("voice", media_id, duration=duration)
|
|
141
|
-
|
|
142
|
-
def send_file(self, media_id: str):
|
|
143
|
-
"""
|
|
144
|
-
发送文件消息
|
|
145
|
-
:params media_id 媒体ID
|
|
146
|
-
"""
|
|
147
|
-
return self._send_media("file", media_id)
|
|
148
|
-
|
|
149
|
-
def send_link(self, messageUrl, picUrl, title, text):
|
|
150
|
-
"""
|
|
151
|
-
发送链接消息
|
|
152
|
-
:params messageUrl 链接地址
|
|
153
|
-
:params picUrl 链接的小图
|
|
154
|
-
:params title 链接标题 100字内
|
|
155
|
-
:params text 链接副标题 500字内
|
|
156
|
-
"""
|
|
157
|
-
return self.send(
|
|
158
|
-
data={
|
|
159
|
-
"msgtype": "link",
|
|
160
|
-
"link": {
|
|
161
|
-
"picUrl": picUrl,
|
|
162
|
-
"messageUrl": messageUrl,
|
|
163
|
-
"title": title,
|
|
164
|
-
"text": text,
|
|
165
|
-
},
|
|
166
|
-
}
|
|
167
|
-
)
|
|
168
|
-
|
|
169
|
-
def send_card(self, **kw):
|
|
170
|
-
"""
|
|
171
|
-
发送卡片消息
|
|
172
|
-
:params title 标题
|
|
173
|
-
:params text 消息内容 必填
|
|
174
|
-
:params single_title 查看详情(按钮文字) btns 二选一
|
|
175
|
-
:params single_url 查看详情(链接) btns 二选一
|
|
176
|
-
:params btn_orientation 按钮排列顺序。0|1
|
|
177
|
-
:params btns 按钮 [{title:'',actionURL:''}]
|
|
178
|
-
"""
|
|
179
|
-
title = kw.get("title", "")
|
|
180
|
-
text = kw.get("text", "")
|
|
181
|
-
single_title = kw.get("single_title", "")
|
|
182
|
-
single_url = kw.get("single_url", "")
|
|
183
|
-
btn_orientation = kw.get("btn_orientation", 0)
|
|
184
|
-
btns = kw.get("btns", []) or []
|
|
185
|
-
return self.send(
|
|
186
|
-
data={
|
|
187
|
-
"msgtype": "actionCard",
|
|
188
|
-
"actionCard": {
|
|
189
|
-
"title": title,
|
|
190
|
-
"text": text,
|
|
191
|
-
"singleTitle": single_title,
|
|
192
|
-
"singleUrl": single_url,
|
|
193
|
-
"btnOrientation": btn_orientation,
|
|
194
|
-
"btns": btns,
|
|
195
|
-
},
|
|
196
|
-
}
|
|
197
|
-
)
|
|
198
|
-
|
|
199
|
-
def send_freecard(self, links: list):
|
|
200
|
-
"""
|
|
201
|
-
发送卡片消息
|
|
202
|
-
:params links [{ title:'',messageURL:'',picURL:''}]
|
|
203
|
-
"""
|
|
204
|
-
return self.send(
|
|
205
|
-
data={
|
|
206
|
-
"msgtype": "feedCard",
|
|
207
|
-
"feedCard": {"links": links},
|
|
208
|
-
}
|
|
209
|
-
)
|
|
@@ -1,102 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
# -*- coding: utf-8 -*-
|
|
3
|
-
|
|
4
|
-
__author__ = "chuchur/chuchur.com"
|
|
5
|
-
|
|
6
|
-
import hashlib, hmac, time, base64
|
|
7
|
-
from curl_cffi import requests as http
|
|
8
|
-
from ..logging import print_error, print_info
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
class Client(object):
|
|
12
|
-
def __init__(self, **kw):
|
|
13
|
-
self._webhook = kw.get("webhook")
|
|
14
|
-
self._secret = kw.get("secret")
|
|
15
|
-
|
|
16
|
-
def _get_sign(self):
|
|
17
|
-
secret = self._secret
|
|
18
|
-
if not secret:
|
|
19
|
-
print_error("缺少secret")
|
|
20
|
-
return None, None
|
|
21
|
-
# 拼接timestamp和secret
|
|
22
|
-
timestamp = int(time.time())
|
|
23
|
-
string_to_sign = "{}\n{}".format(timestamp, secret)
|
|
24
|
-
hmac_code = hmac.new(
|
|
25
|
-
string_to_sign.encode("utf-8"), digestmod=hashlib.sha256
|
|
26
|
-
).digest()
|
|
27
|
-
|
|
28
|
-
# 对结果进行base64处理
|
|
29
|
-
sign = base64.b64encode(hmac_code).decode("utf-8")
|
|
30
|
-
|
|
31
|
-
return timestamp, sign
|
|
32
|
-
|
|
33
|
-
def send(self, data: dict):
|
|
34
|
-
"""
|
|
35
|
-
发送除签名外的自定义消息结构体
|
|
36
|
-
:params data 自定义消息结构体
|
|
37
|
-
"""
|
|
38
|
-
webhook = self._webhook
|
|
39
|
-
if not webhook:
|
|
40
|
-
print_error("缺少webhook")
|
|
41
|
-
return False
|
|
42
|
-
timestamp, sign = self._get_sign()
|
|
43
|
-
if not timestamp or not sign:
|
|
44
|
-
return False
|
|
45
|
-
result = http.post(
|
|
46
|
-
url=webhook,
|
|
47
|
-
json={"timestamp": timestamp, "sign": sign, **data},
|
|
48
|
-
)
|
|
49
|
-
if result:
|
|
50
|
-
res = result.json()
|
|
51
|
-
if res["StatusCode"] == 0:
|
|
52
|
-
print_info("发送成功")
|
|
53
|
-
return False
|
|
54
|
-
else:
|
|
55
|
-
print_error("发送失败!")
|
|
56
|
-
return True
|
|
57
|
-
|
|
58
|
-
def send_text(self, text: str):
|
|
59
|
-
"""
|
|
60
|
-
发送文本消息
|
|
61
|
-
:params text 消息内容
|
|
62
|
-
"""
|
|
63
|
-
return self.send(
|
|
64
|
-
data={
|
|
65
|
-
"msg_type": "text",
|
|
66
|
-
"content": {"text": text},
|
|
67
|
-
}
|
|
68
|
-
)
|
|
69
|
-
|
|
70
|
-
def send_card(self, **kw):
|
|
71
|
-
"""
|
|
72
|
-
发送卡片消息
|
|
73
|
-
:params content 卡片内容,str
|
|
74
|
-
:params elements 参见 :https://open.feishu.cn/document/ukTMukTMukTM/uMjNwUjLzYDM14yM2ATN
|
|
75
|
-
:params template 标题背景颜色 blue|wathet|truquoise|green|yellow|orange|red|carmine|violet|purple|indigo|grey
|
|
76
|
-
:params i18n 多语言 {'zh_cn':'...'}
|
|
77
|
-
"""
|
|
78
|
-
content = kw.get("content", "")
|
|
79
|
-
i18n = kw.get("i18n", {})
|
|
80
|
-
elements = kw.get("elements", [])
|
|
81
|
-
template = kw.get("template", "blue")
|
|
82
|
-
return self.send(
|
|
83
|
-
data={
|
|
84
|
-
"msg_type": "interactive",
|
|
85
|
-
"card": {
|
|
86
|
-
"config": {
|
|
87
|
-
"wide_screen_mode": True,
|
|
88
|
-
"enable_forward": True,
|
|
89
|
-
"update_multi": True,
|
|
90
|
-
},
|
|
91
|
-
"header": {
|
|
92
|
-
"title": {
|
|
93
|
-
"tag": "plain_text",
|
|
94
|
-
"content": content,
|
|
95
|
-
"i18n": i18n,
|
|
96
|
-
},
|
|
97
|
-
"template": template,
|
|
98
|
-
},
|
|
99
|
-
"elements": elements,
|
|
100
|
-
},
|
|
101
|
-
}
|
|
102
|
-
)
|
|
@@ -1,115 +0,0 @@
|
|
|
1
|
-
from cloudoll.web import http
|
|
2
|
-
|
|
3
|
-
"""
|
|
4
|
-
web_hook: https://infloww.slack.com/apps/A0F7XDUAZ-incoming-webhook?tab=settings&next_id=0
|
|
5
|
-
blocks_info: https://api.slack.com/messaging/composing/layouts
|
|
6
|
-
"""
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
class Header:
|
|
10
|
-
def __init__(self, text: str):
|
|
11
|
-
self._text = text
|
|
12
|
-
|
|
13
|
-
def get_json(self):
|
|
14
|
-
return {"type": "header", "text": {"type": "plain_text", "text": self._text}}
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
class Text:
|
|
18
|
-
def __init__(self, text: str | int):
|
|
19
|
-
self._text = text
|
|
20
|
-
|
|
21
|
-
def get_json(self):
|
|
22
|
-
return {"type": "section", "text": {"type": "mrkdwn", "text": self._text}}
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
class Field:
|
|
26
|
-
def __init__(self, title: str | int, text: str | int):
|
|
27
|
-
self._title = title
|
|
28
|
-
self._text = text
|
|
29
|
-
|
|
30
|
-
def get_json(self):
|
|
31
|
-
return {"type": "mrkdwn", "text": f"{self._title}\n{self._text}"}
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
class Link:
|
|
35
|
-
def __init__(self, title: str | int, url: str | int):
|
|
36
|
-
self._title = title
|
|
37
|
-
self._url = url
|
|
38
|
-
|
|
39
|
-
def get_json(self):
|
|
40
|
-
return {
|
|
41
|
-
"type": "context",
|
|
42
|
-
"elements": [{"type": "mrkdwn", "text": f"<{self._url}|_{self._title}_>"}],
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
class Section:
|
|
47
|
-
def __init__(self):
|
|
48
|
-
self._fileds: list[Field] = []
|
|
49
|
-
|
|
50
|
-
def add(self, field: Field):
|
|
51
|
-
self._fileds.append(field.get_json())
|
|
52
|
-
return self
|
|
53
|
-
|
|
54
|
-
def get_json(self):
|
|
55
|
-
return {"type": "section", "fileds": self._fileds}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
class Elements:
|
|
59
|
-
def __init__(self):
|
|
60
|
-
self._elements: list[Field] = []
|
|
61
|
-
|
|
62
|
-
def add(self, field: Field):
|
|
63
|
-
self._elements.append(field.get_json())
|
|
64
|
-
return self
|
|
65
|
-
|
|
66
|
-
def get_json(self):
|
|
67
|
-
return {"type": "context", "elements": self._elements}
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
class Divider:
|
|
71
|
-
def __init__(self):
|
|
72
|
-
self._fileds: list[Field] = []
|
|
73
|
-
|
|
74
|
-
def get_json(self):
|
|
75
|
-
return {"type": "divider"}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
class Blocks:
|
|
79
|
-
def __init__(self):
|
|
80
|
-
self._blocks = []
|
|
81
|
-
|
|
82
|
-
def add(self, block: Section | Header | Text | Divider | Link):
|
|
83
|
-
self._blocks.append(block)
|
|
84
|
-
return self
|
|
85
|
-
|
|
86
|
-
def add_header(self, text):
|
|
87
|
-
self._blocks.append(Header(text))
|
|
88
|
-
return self
|
|
89
|
-
|
|
90
|
-
def add_link(self, title, url: str):
|
|
91
|
-
self._blocks.append(Link(title, url))
|
|
92
|
-
return self
|
|
93
|
-
|
|
94
|
-
def add_text(self, text):
|
|
95
|
-
self._blocks.append(Text(text))
|
|
96
|
-
return self
|
|
97
|
-
|
|
98
|
-
def add_divider(self):
|
|
99
|
-
self._blocks.append(Divider())
|
|
100
|
-
return self
|
|
101
|
-
|
|
102
|
-
def get_json(self):
|
|
103
|
-
blocks = []
|
|
104
|
-
for block in self._blocks:
|
|
105
|
-
blocks.append(block.get_json())
|
|
106
|
-
return blocks
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
class Slack:
|
|
110
|
-
def __init__(self, url: str):
|
|
111
|
-
self._url = url
|
|
112
|
-
|
|
113
|
-
def send_notify(self, blocks: Blocks):
|
|
114
|
-
res = http.post(url=self._url, json={"blocks": blocks})
|
|
115
|
-
return res
|
|
File without changes
|
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
# -*- coding: utf-8 -*-
|
|
3
|
-
|
|
4
|
-
__author__ = "chuchur/chuchur.com"
|
|
5
|
-
# from aiohttp.web import middleware
|
|
6
|
-
from curl_cffi import requests
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
class Client(object):
|
|
10
|
-
def __init__(self):
|
|
11
|
-
self.session = requests.Session()
|
|
12
|
-
|
|
13
|
-
def requests(self, method, url, **kw) -> requests.Response:
|
|
14
|
-
impersonate = kw.get("impersonate", None)
|
|
15
|
-
if not impersonate:
|
|
16
|
-
kw["impersonate"] = "chrome133a"
|
|
17
|
-
fun = getattr(self.session, method, None)
|
|
18
|
-
result: requests.Response = fun(url, **kw)
|
|
19
|
-
return result
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
http = Client()
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
def get(url, **kw):
|
|
26
|
-
"""
|
|
27
|
-
get 请求
|
|
28
|
-
:params params=dict() 传参
|
|
29
|
-
"""
|
|
30
|
-
return http.requests("get", url, **kw)
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
def post(url, **kw):
|
|
34
|
-
"""
|
|
35
|
-
post 请求
|
|
36
|
-
:params data=dict() or json=dict() 传参
|
|
37
|
-
"""
|
|
38
|
-
return http.requests("post", url, **kw)
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
def put(url, **kw):
|
|
42
|
-
"""
|
|
43
|
-
post 请求
|
|
44
|
-
data=dict()传参
|
|
45
|
-
"""
|
|
46
|
-
return http.requests("put", url, **kw)
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
def delete(url, **kw):
|
|
50
|
-
return http.requests("delete", url, **kw)
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
def head(url, **kw):
|
|
54
|
-
return http.requests("head", url, **kw)
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
def option(url, **kw):
|
|
58
|
-
return http.requests("option", url, **kw)
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
def download(url, savepath=None, **kw):
|
|
62
|
-
"""
|
|
63
|
-
下载文件
|
|
64
|
-
:params url 文件的地址
|
|
65
|
-
:params savepath 保存路径
|
|
66
|
-
"""
|
|
67
|
-
response = http.requests("get", url, **kw)
|
|
68
|
-
if response.status_code == 200:
|
|
69
|
-
# 如果传入了保存路径,保存文件
|
|
70
|
-
if savepath:
|
|
71
|
-
with open(savepath, "wb") as file:
|
|
72
|
-
file.write(response.content)
|
|
73
|
-
print(f"File saved to {savepath}")
|
|
74
|
-
else:
|
|
75
|
-
# 如果没有传入保存路径,返回文件内容
|
|
76
|
-
return response.content
|
|
77
|
-
else:
|
|
78
|
-
print(f"Failed to download the file. Status code: {response.status_code}")
|
|
79
|
-
return None
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|