cloudoll 2.4.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.
cloudoll/orm/mysql.py ADDED
@@ -0,0 +1,84 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ from cloudoll.orm import Mysql
5
+
6
+ # for config folder conf.{env}.yaml
7
+
8
+ database:
9
+ mysql:
10
+ type: mysql # important ,default mysql
11
+ host: 127.0.0.1
12
+ port: 3306
13
+ user: root
14
+ password: 123456
15
+ db: test
16
+ charset:utf8mb4
17
+ pool_size:5
18
+
19
+ # to get yaml config
20
+ config = ....(database.mysql)
21
+
22
+ pool = await Mysql().create_engine(**config)
23
+ result = await pool.query("sql")
24
+ """
25
+ __author__ = "chuchur/chuchur.com"
26
+
27
+ from typing import Any
28
+ from aiomysql import create_pool, DictCursor
29
+ from aiomysql.pool import Pool
30
+
31
+ # from aiomysql.cursors import Cursor
32
+ # from aiomysql.connection import Connection
33
+ from cloudoll.logging import print_error
34
+
35
+ # from inspect import isclass, isfunction
36
+ from .base import MeteBase
37
+
38
+
39
+ class AttrDict(dict):
40
+ """Dict that can get attribute by dot, and doesn't raise KeyError"""
41
+
42
+ def __getattr__(self, name):
43
+ try:
44
+ return self[name]
45
+ except KeyError:
46
+ return None
47
+
48
+
49
+ class AttrDictCursor(DictCursor):
50
+ dict_type = AttrDict
51
+
52
+
53
+ class Mysql(MeteBase):
54
+ def __init__(self):
55
+ self.pool: Pool = None
56
+ # self.cursor: Cursor = None
57
+ # self.conn: Connection = None
58
+ # self.__MODELS__ = []
59
+
60
+ # def __call__(cls, *args: Any, **kwds: Any) -> Any:
61
+ # cls.__init__(*args, **kwds)
62
+ # return cls()
63
+
64
+ # @classmethod
65
+ async def create_engine(self, loop=None, **kw):
66
+ try:
67
+ self.pool = await create_pool(
68
+ host=kw.get("host", "localhost"),
69
+ port=kw.get("port", 3306),
70
+ user=kw.get("user"),
71
+ password=str(kw.get("password", "")),
72
+ db=kw.get("db"),
73
+ echo=kw.get("echo", False),
74
+ charset=kw.get("charset", "utf8"),
75
+ autocommit=False, # kw.get("autocommit", False),
76
+ maxsize=kw.get("maxsize", 10),
77
+ minsize=kw.get("pool_size", 5),
78
+ cursorclass=AttrDictCursor,
79
+ connect_timeout=kw.get("connect_timeout", 10),
80
+ loop=loop,
81
+ )
82
+ except Exception as e:
83
+ print_error(f"Database connection failed,the instance : {kw.get('db')}")
84
+ return self
cloudoll/orm/parse.py ADDED
@@ -0,0 +1,139 @@
1
+ from urllib.parse import unquote
2
+ import re
3
+ from typing import Any, Optional, List, cast
4
+ import collections.abc as collections_abc
5
+
6
+ _implicit_encoding = "ascii"
7
+ _implicit_errors = "strict"
8
+
9
+ __ALL__ = "parse_coon"
10
+
11
+
12
+ def parse_coon(url: str):
13
+ pattern = re.compile(
14
+ r"""
15
+ (?P<type>[\w\+]+)://
16
+ (?:
17
+ (?P<username>[^:/]*)
18
+ (?::(?P<password>[^@]*))?
19
+ @)?
20
+ (?:
21
+ (?:
22
+ \[(?P<ipv6host>[^/\?]+)\] |
23
+ (?P<ipv4host>[^/:\?]+)
24
+ )?
25
+ (?::(?P<port>[^/\?]*))?
26
+ )?
27
+ (?:/(?P<db>[^\?]*))?
28
+ (?:\?(?P<query>.*))?
29
+ """,
30
+ re.X,
31
+ )
32
+ match = pattern.match(url)
33
+ if match is not None:
34
+ configs = match.groupdict()
35
+ if configs["username"] is not None:
36
+ configs["username"] = unquote(configs["username"])
37
+
38
+ if configs["password"] is not None:
39
+ configs["password"] = unquote(configs["password"])
40
+
41
+ ipv4host = configs.pop("ipv4host")
42
+ ipv6host = configs.pop("ipv6host")
43
+ configs["host"] = ipv4host or ipv6host
44
+ # name = components.pop("name")
45
+
46
+ if configs["port"]:
47
+ configs["port"] = int(configs["port"])
48
+
49
+ query = {}
50
+ if configs["query"] is not None:
51
+
52
+ for key, value in parse_sql(configs["query"]):
53
+ if key in query:
54
+ query[key] = to_list(query[key])
55
+ cast("List[str]", query[key]).append(value)
56
+ else:
57
+ query[key] = value
58
+
59
+ return configs, query
60
+ else:
61
+ raise ValueError("%s is not a valid database URL" % url)
62
+
63
+
64
+ def to_list(x: Any, default: Optional[List[Any]] = None) -> List[Any]:
65
+ if x is None:
66
+ return default # type: ignore
67
+ if not isinstance(x, collections_abc.Iterable) or isinstance(x, (str, bytes)):
68
+ return [x]
69
+ elif isinstance(x, list):
70
+ return x
71
+ else:
72
+ return list(x)
73
+
74
+
75
+ def parse_sql(
76
+ qs,
77
+ keep_blank_values=False,
78
+ strict_parsing=False,
79
+ encoding="utf-8",
80
+ errors="replace",
81
+ max_num_fields=None,
82
+ separator="&",
83
+ ):
84
+ qs, _coerce_result = _coerce_args(qs)
85
+ separator, _ = _coerce_args(separator)
86
+
87
+ if not separator or (not isinstance(separator, (str, bytes))):
88
+ raise ValueError("Separator must be of type string or bytes.")
89
+
90
+ if max_num_fields is not None:
91
+ num_fields = 1 + qs.count(separator) if qs else 0
92
+ if max_num_fields < num_fields:
93
+ raise ValueError("Max number of fields exceeded")
94
+
95
+ r = []
96
+ query_args = qs.split(separator) if qs else []
97
+ for name_value in query_args:
98
+ if not name_value and not strict_parsing:
99
+ continue
100
+ nv = name_value.split("=", 1)
101
+ if len(nv) != 2:
102
+ if strict_parsing:
103
+ raise ValueError("bad query field: %r" % (name_value,))
104
+ # Handle case of a control-name with no equal sign
105
+ if keep_blank_values:
106
+ nv.append("")
107
+ else:
108
+ continue
109
+ if len(nv[1]) or keep_blank_values:
110
+ name = nv[0].replace("+", " ")
111
+ name = unquote(name, encoding=encoding, errors=errors)
112
+ name = _coerce_result(name)
113
+ value = nv[1].replace("+", " ")
114
+ value = unquote(value, encoding=encoding, errors=errors)
115
+ value = _coerce_result(value)
116
+ r.append((name, value))
117
+ return r
118
+
119
+
120
+ def _encode_result(obj, encoding=_implicit_encoding, errors=_implicit_errors):
121
+ return obj.encode(encoding, errors)
122
+
123
+
124
+ def _noop(obj):
125
+ return obj
126
+
127
+
128
+ def _decode_args(args, encoding=_implicit_encoding, errors=_implicit_errors):
129
+ return tuple(x.decode(encoding, errors) if x else "" for x in args)
130
+
131
+
132
+ def _coerce_args(*args):
133
+ str_input = isinstance(args[0], str)
134
+ for arg in args[1:]:
135
+ if arg and isinstance(arg, str) != str_input:
136
+ raise TypeError("Cannot mix str and non-str arguments")
137
+ if str_input:
138
+ return args + (_noop,)
139
+ return _decode_args(args) + (_encode_result,)
@@ -0,0 +1,70 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ from cloudoll.orm.postgres import Postgres
5
+
6
+ # for config folder conf.{env}.yaml
7
+
8
+ database:
9
+ pg:
10
+ type: postgres # important ,default mysql
11
+ host: 127.0.0.1
12
+ port: 3306
13
+ user: root
14
+ password: 123456
15
+ db: test
16
+ echo: True
17
+ timeout: 60
18
+ pool_size:5
19
+
20
+ # to get yaml config
21
+ config = ....(database.postgres)
22
+
23
+ pool = await Postgres().create_engine(**config)
24
+ result = await pool.query("sql")
25
+ """
26
+ __author__ = "chuchur/chuchur.com"
27
+
28
+ from aiopg import create_pool, Pool, Cursor, Connection
29
+ from ..logging import print_error
30
+ from .base import MeteBase
31
+ from typing import Any
32
+
33
+
34
+ class Postgres(MeteBase):
35
+ def __init__(self):
36
+ self.pool: Pool = None
37
+ self.cursor: Cursor = None
38
+ self.conn: Connection = None
39
+ # self.__MODELS__ = []
40
+
41
+ def __call__(cls, *args: Any, **kwds: Any) -> Any:
42
+ cls.__init__(*args, **kwds)
43
+ return cls()
44
+
45
+ @classmethod
46
+ async def create_engine(self, **kw):
47
+ try:
48
+ host = (kw.get("host", "localhost"),)
49
+ port = (kw.get("port", 5432),)
50
+ user = (kw.get("user"),)
51
+ password = (str(kw.get("password", "")),)
52
+ db = (kw.get("db"),)
53
+ dsn = f"dbname={db[0]} user={user[0]} password={password[0]} host={host[0]} port={port[0]}"
54
+ self.pool = await create_pool(
55
+ dsn=dsn,
56
+ timeout=kw.get("timeout", 10.0),
57
+ enable_json=kw.get("enable_json"),
58
+ enable_hstore=kw.get("enable_hstore"),
59
+ enable_uuid=kw.get("enable_uuid"),
60
+ enable_uuid=kw.get("enable_uuid"),
61
+ pool_recycle=kw.get("pool_recycle", 10),
62
+ echo=kw.get("echo", False),
63
+ minsize=kw.get("pool_size", 5),
64
+ maxsize=kw.get("pool_size", 10),
65
+ )
66
+ except Exception as e:
67
+ # print_error(e)
68
+ print_error(f"Database connection failed,the instance : {kw.get('db')}")
69
+
70
+ return self
File without changes
@@ -0,0 +1,209 @@
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 + "&timestamp=%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
+ )
@@ -0,0 +1,102 @@
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
+ )