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.
@@ -0,0 +1,115 @@
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
@@ -0,0 +1,72 @@
1
+ from typing import Any
2
+ from ..web.settings import get_config
3
+ from ..logging import print_info
4
+ from .common import Object, chainMap
5
+ from .watch import AppTask
6
+ from aiohttp import web
7
+ from ..web import app
8
+ from pathlib import Path
9
+ from .m2d import create_models, create_tables
10
+ from ..orm import create_engine
11
+ import os
12
+
13
+
14
+ def run_app(**config_kwargs: Any):
15
+ config = Object(config_kwargs)
16
+ print_info("Current mode: ", config.mode)
17
+ app_config = get_config(config.environment)
18
+ # print(environment, host, port, mode, path, entry)
19
+ # return
20
+ if config.mode == 'production':
21
+ App = app.create(env=config.environment, config=app_config, entry_model=config.entry)
22
+ App.run()
23
+ else:
24
+ aux_app = web.Application(
25
+ logger=None,
26
+ )
27
+ defaults = {"host": "0.0.0.0", "port": 9001, "path": None}
28
+ conf_server = app_config.get("server", {})
29
+ env_server = {"host": config.host, "port": config.port, "path": config.path}
30
+ server = chainMap(defaults, conf_server, env_server)
31
+ app_config["server"] = server
32
+ aux_port = int(server.port) + 1
33
+ task = AppTask(Path(".").resolve(), app_config, entry=config.entry, env=config.environment)
34
+ aux_app.cleanup_ctx.append(task.cleanup_ctx)
35
+ web.run_app(
36
+ aux_app,
37
+ access_log=None,
38
+ host=config.host,
39
+ port=aux_port,
40
+ print=None,
41
+ shutdown_timeout=0.1,
42
+ )
43
+
44
+
45
+ async def run_gen(**config_kwargs: Any):
46
+ config = Object(config_kwargs)
47
+ configs = get_config(config.environment)
48
+ db_configs = configs.get("database")
49
+ if db_configs is None:
50
+ raise KeyError(
51
+ f"Can't find the database config in conf.{config.environment}.yaml"
52
+ )
53
+ db_config = db_configs.get(config.database)
54
+ if db_config is None:
55
+ raise KeyError(
56
+ f"Can't find the database config key ->{config.database} in conf.{config.environment}.yaml"
57
+ )
58
+ sa = await create_engine(**db_config)
59
+ if sa.pool is None:
60
+ return
61
+ model_path = os.path.join(os.path.abspath("."), config.path)
62
+ tables = None
63
+ if config.table != "ALL":
64
+ tables = config.table.split(",")
65
+
66
+ if config.create == "model":
67
+ await create_models(sa, config.path, tables=tables)
68
+ print_info(f"Model save at:{model_path}")
69
+ elif config.create == "table":
70
+ if model_path is None:
71
+ raise ValueError("Need package name or model name.")
72
+ await create_tables(sa, model_path, tables=tables)
@@ -0,0 +1,74 @@
1
+
2
+
3
+ from typing import Any
4
+ from ..web.settings import get_config
5
+ from ..logging import print_info
6
+ from .common import Object, chainMap
7
+ from ..utils.watch import AppTask
8
+ from aiohttp import web
9
+ from ..web import app
10
+ from pathlib import Path
11
+ from ..utils.m2d import create_models, create_tables
12
+ from ..orm import create_engine
13
+ import os
14
+
15
+
16
+ def run_app(**config_kwargs: Any):
17
+ config = Object(config_kwargs)
18
+ print_info("Current mode: ", config.mode)
19
+ app_config = get_config(config.environment)
20
+ # print(environment, host, port, mode, path, entry)
21
+ # return
22
+ if config.mode == 'production':
23
+ App = app.create(env=config.environment, config=app_config, entry_model=config.entry)
24
+ App.run()
25
+ else:
26
+ aux_app = web.Application(
27
+ logger=None,
28
+ )
29
+ defaults = {"host": "0.0.0.0", "port": 9001, "path": None}
30
+ conf_server = app_config.get("server", {})
31
+ env_server = {"host": config.host, "port": config.port, "path": config.path}
32
+ server = chainMap(defaults, conf_server, env_server)
33
+ app_config["server"] = server
34
+ aux_port = int(server.port) + 1
35
+ task = AppTask(Path(".").resolve(), app_config, entry=config.entry, env=config.environment)
36
+ aux_app.cleanup_ctx.append(task.cleanup_ctx)
37
+ web.run_app(
38
+ aux_app,
39
+ access_log=None,
40
+ host=config.host,
41
+ port=aux_port,
42
+ print=None,
43
+ shutdown_timeout=0.1,
44
+ )
45
+
46
+
47
+ async def run_gen(**config_kwargs: Any):
48
+ config = Object(config_kwargs)
49
+ configs = get_config(config.environment)
50
+ db_configs = configs.get("database")
51
+ if db_configs is None:
52
+ raise KeyError(
53
+ f"Can't find the database config in conf.{config.environment}.yaml"
54
+ )
55
+ db_config = db_configs.get(config.database)
56
+ if db_config is None:
57
+ raise KeyError(
58
+ f"Can't find the database config key ->{config.database} in conf.{config.environment}.yaml"
59
+ )
60
+ sa = await create_engine(**db_config)
61
+ if sa.pool is None:
62
+ return
63
+ model_path = os.path.join(os.path.abspath("."), config.path)
64
+ tables = None
65
+ if config.table != "ALL":
66
+ tables = config.table.split(",")
67
+
68
+ if config.create == "model":
69
+ await create_models(sa, config.path, tables=tables)
70
+ print_info(f"Model save at:{model_path}")
71
+ elif config.create == "table":
72
+ if model_path is None:
73
+ raise ValueError("Need package name or model name.")
74
+ await create_tables(sa, model_path, tables=tables)
@@ -0,0 +1,57 @@
1
+ import asyncio
2
+ from ..logging import warning
3
+
4
+ EADDRINUSE: int
5
+
6
+
7
+ class Object(dict):
8
+ # __setattr__ = dict.__setitem__
9
+ # __getattr__ = dict.__getitem__
10
+ def __setattr__(self, key, value):
11
+ self[key] = value
12
+
13
+ def __getattr__(self, key):
14
+ return self.get(key, None)
15
+
16
+ def __delattr__(self, key):
17
+ try:
18
+ del self[key]
19
+ except KeyError:
20
+ raise AttributeError(f"'DotDict' object has no attribute '{key}'")
21
+
22
+ def __getstate__(self):
23
+ """to fix pickle.dumps"""
24
+ return dict(self)
25
+
26
+ def __setstate__(self, state):
27
+ self.update(state)
28
+
29
+
30
+ def chainMap(*dicts):
31
+ merged_dict = Object()
32
+ for d in dicts:
33
+ for key, value in d.items():
34
+ if key not in merged_dict or value is not None:
35
+ merged_dict[key] = value
36
+ return merged_dict
37
+
38
+
39
+ async def check_port_open(port: int, delay: float = 1) -> None:
40
+ loop = asyncio.get_running_loop()
41
+ # the "s = socket.socket; s.bind" approach sometimes says a port is in use when it's not
42
+ # this approach replicates aiohttp so should always give the same answer
43
+ for i in range(5, 0, -1):
44
+ try:
45
+ server = await loop.create_server(
46
+ asyncio.Protocol, host="0.0.0.0", port=port
47
+ )
48
+ except OSError as e:
49
+ if e.errno != EADDRINUSE:
50
+ raise
51
+ warning("port %d is already in use, waiting %d...", port, i)
52
+ await asyncio.sleep(delay)
53
+ else:
54
+ server.close()
55
+ await server.wait_closed()
56
+ return
57
+ raise Exception("The port {} is already is use".format(port))
cloudoll/utils/m2d.py ADDED
@@ -0,0 +1,311 @@
1
+ import enum
2
+ import os
3
+ import re
4
+ from ..logging import print_info, print_warn
5
+ import importlib
6
+
7
+
8
+ def snake_to_camel(snake_str):
9
+ components = snake_str.split("_")
10
+ # camel_str = components[0] + ''.join(x.title() for x in components[1:])
11
+ # x.capitalize() //first
12
+ # x.title() //all
13
+ # return camel_str[0].upper() + camel_str[1:]
14
+ return "".join(x.title() for x in components[0:])
15
+
16
+
17
+ async def create_model(pool, table_name) -> str:
18
+ """
19
+ Create table
20
+ :params table name
21
+ """
22
+ print_info(f"create model from {table_name}")
23
+ # rows = await pool.all(f"show full COLUMNS from `{table_name}`", None)
24
+ rows = await get_table_cols(pool, table_name)
25
+ # print(rows)
26
+ tb = f"\nclass {snake_to_camel(table_name)}(Model):\n\n"
27
+ tb += f"\t__table__ = '{table_name}'\n\n"
28
+ for f in rows:
29
+ fields = get_col(f, pool.driver)
30
+ # print("fields",fields)
31
+ name = fields["name"]
32
+ column_type = fields["column_type"]
33
+ values = []
34
+ if fields["primary_key"]:
35
+ values.append("primary_key=True")
36
+ if fields["charset"]:
37
+ values.append(f"charset='{fields['charset']}'")
38
+ if fields["max_length"] and column_type != "tinyint":
39
+ values.append(
40
+ f"max_length=({fields['max_length']})"
41
+ if isinstance(fields["max_length"], str) and "," in fields["max_length"]
42
+ else f"max_length={fields['max_length']}"
43
+ )
44
+ if fields["scale_length"]:
45
+ values.append(f"scale_length={fields['scale_length']}")
46
+ if fields["default"]:
47
+ values.append(f"default='{fields['default']}'")
48
+ if fields["auto_increment"]:
49
+ values.append("auto_increment=True")
50
+ if fields["NOT_NULL"]:
51
+ values.append("not_null=True")
52
+ if fields["created_generated"]:
53
+ values.append("created_generated=True")
54
+ if fields["update_generated"]:
55
+ values.append("update_generated=True")
56
+ if fields["comment"]:
57
+ values.append(f"comment='{fields['comment']}'")
58
+ if "unsigned" in column_type:
59
+ column_type = column_type.replace(" unsigned", "")
60
+ values.append("unsigned=True")
61
+ name = re.sub(r"\s", "", name)
62
+ tb += f"\t{name} = models.{ColTypes[column_type].value}Field({', '.join(values)})\n"
63
+ tb += "\n"
64
+ return tb
65
+
66
+
67
+ async def get_table_cols(pool, table_name):
68
+ if pool.driver == "mysql":
69
+ return await pool.all(f"show full COLUMNS from `{table_name}`", None)
70
+ elif pool.driver == "postgres":
71
+ pri_row = await pool.one(
72
+ f"SELECT column_name from information_schema.key_column_usage WHERE table_name = '{table_name}'",
73
+ None,
74
+ )
75
+ sql = f"""SELECT column_name Field,
76
+ column_default Default,
77
+ data_type column_type,
78
+ is_nullable Null,
79
+ numeric_precision num_length,
80
+ numeric_scale scale_length,
81
+ character_maximum_length str_length,
82
+ datetime_precision date_length,
83
+ col_description('{table_name}'::regclass, ordinal_position) Comment
84
+ FROM information_schema.columns WHERE table_name ='{table_name}'"""
85
+
86
+ rows = await pool.all(sql, None)
87
+ for row in rows:
88
+ row["Key"] = "PRI" if pri_row and pri_row["column_name"] == row["field"] else None
89
+
90
+ # print("rows", rows)
91
+
92
+ return rows
93
+ else:
94
+ raise ValueError("Database not support.")
95
+
96
+
97
+ async def get_all_tables(pool):
98
+ if pool.driver == "mysql":
99
+ return await pool.all("show tables", None)
100
+ elif pool.driver == "postgres":
101
+ return await pool.all(
102
+ "SELECT table_name FROM information_schema.tables WHERE table_schema = 'public'",
103
+ None,
104
+ )
105
+ else:
106
+ raise ValueError("Database not support.")
107
+
108
+
109
+ async def create_models(pool, save_path: str = None, tables: list = None):
110
+ """
111
+ Create models
112
+ :params tables
113
+ :params save_path
114
+ """
115
+ if tables and len(tables) > 0:
116
+ tbs = tables
117
+ else:
118
+ # result = await pool.all("show tables", None)
119
+ all_tables = await get_all_tables(pool)
120
+ tbs = [list(c.values())[0] for c in all_tables]
121
+ content = ""
122
+ import_line = "from cloudoll.orm.model import models, Model\n\n"
123
+ for t in tbs:
124
+ content += await create_model(pool, t)
125
+ if save_path:
126
+ first_append = True
127
+ if os.path.exists(save_path):
128
+ with open(save_path, "r", encoding="utf-8") as f:
129
+ t = f.readlines(5)
130
+ first_append = import_line not in t
131
+ with open(save_path, "a", encoding="utf-8") as f:
132
+ if first_append:
133
+ content = import_line + content
134
+ f.write(content)
135
+ else:
136
+ return content
137
+
138
+
139
+ async def create_table(pool, models: list, tables: list = None):
140
+ for model in models:
141
+ # print(table.__name__)
142
+ if model.__name__ == "Model":
143
+ continue
144
+ tb = model.__table__
145
+
146
+ if tables and tb not in tables:
147
+ continue
148
+
149
+ if tb.startswith("v_"):
150
+ print_warn(f"{tb} look like a view so skip.")
151
+ continue
152
+
153
+ # sql = f"DROP TABLE IF EXISTS `{tb}`;\n"
154
+ sql = ""
155
+ sql += f"CREATE TABLE `{tb}` (\n"
156
+
157
+ # labels = get_filed(model)
158
+ labels = model.__fields__
159
+ sqls = []
160
+ for f in labels:
161
+ lb = getattr(model, f)
162
+ row = get_col_sql(lb)
163
+ sqls.append(row)
164
+ sql += ",\n".join(sqls)
165
+ sql += ") ENGINE=InnoDB;"
166
+
167
+ print_info(f"create table {tb} ...\n\n")
168
+ # print(sql)
169
+ await pool.query(sql, None)
170
+
171
+
172
+ async def create_tables(pool, model_name: str = None, tables: list = None):
173
+
174
+ # parts = model_name.split('.')
175
+ # package_name = '.'.join(parts[:-1]) or '.'
176
+ # module_name = parts[-1]
177
+ # print(module_name,package_name)
178
+ # models = importlib.import_module(module_name,package_name)
179
+ # module_classes = [cls for cls in vars(module_name).values() if isinstance(cls, type)]
180
+
181
+ name = os.path.basename(model_name)[:-3]
182
+ # 创建模块规范
183
+ module_spec = importlib.util.spec_from_file_location(name, model_name)
184
+ # 创建模块对象
185
+ module = importlib.util.module_from_spec(module_spec)
186
+ # 执行模块并加载其内容
187
+ module_spec.loader.exec_module(module)
188
+ # 获取模块中的所有实体
189
+ module_classes = [cls for cls in vars(module).values() if isinstance(cls, type)]
190
+
191
+ # print(module_classes)
192
+ await create_table(pool, models=module_classes, tables=tables)
193
+
194
+
195
+ def get_col(field, driver="mysql"):
196
+ if driver == "mysql":
197
+ fields = {
198
+ "name": field["Field"],
199
+ "column_type": None,
200
+ "primary_key": field["Key"] == "PRI",
201
+ "default": field["Default"],
202
+ "charset": field["Collation"],
203
+ "max_length": None,
204
+ "scale_length":None,
205
+ "auto_increment": field["Extra"] == "auto_increment",
206
+ "NOT_NULL": field["Null"] == "NO",
207
+ "created_generated": "DEFAULT_GENERATED" == field["Extra"],
208
+ "update_generated": "on update" in field["Extra"],
209
+ "comment": field["Comment"],
210
+ }
211
+ field_type = field["Type"]
212
+ t = re.match(r"(\w+)[(](.*?)[)]", field_type)
213
+ if not t:
214
+ fields["column_type"] = field_type
215
+ else:
216
+ fields["column_type"] = t.groups()[0]
217
+ fields["max_length"] = t.groups()[1]
218
+ return fields
219
+ elif driver == "postgres" or driver == "postgresql":
220
+ fields = {
221
+ "name": field["field"],
222
+ "column_type": field["column_type"].replace(" ", "_"),
223
+ "primary_key": field["Key"] == "PRI",
224
+ "default": field["default"],
225
+ "charset": None,
226
+ "max_length": field["num_length"]
227
+ or field["str_length"]
228
+ or field["date_length"],
229
+ "scale_length": field["scale_length"],
230
+ "auto_increment": None,
231
+ "NOT_NULL": field["null"] == "NO",
232
+ "created_generated": None,
233
+ "update_generated": None,
234
+ "comment": field["comment"],
235
+ }
236
+ return fields
237
+
238
+
239
+ def get_col_sql(field):
240
+ sql = f"`{field.name}` {field.column_type}"
241
+
242
+ if field.max_length:
243
+ if isinstance(field.max_length, tuple):
244
+ sql += f"{field.max_length}"
245
+ elif field.scale_length:
246
+ sql += f"({field.max_length},{field.scale_length})"
247
+ else:
248
+ sql += f"({field.max_length})"
249
+
250
+ if field.charset:
251
+ # _ci 不区分大小写 _cs Yes
252
+ cs = field.charset.split("_")[0]
253
+ sql += f" CHARACTER SET {cs} COLLATE {field.charset}"
254
+ if field.primary_key:
255
+ sql += " PRIMARY KEY"
256
+ if field.auto_increment:
257
+ # todo: postgres SERIAL
258
+ sql += " AUTO_INCREMENT"
259
+ if field.NOT_NULL:
260
+ sql += " NOT NULL"
261
+ if field.default:
262
+ sql += " DEFAULT " + (
263
+ field.default if "(" in field.default else f"'{field.default}'"
264
+ )
265
+ # else:
266
+ # sql += " DEFAULT NULL"
267
+ # print(field.name, field.update_generated)
268
+ if field.update_generated:
269
+ sql += " ON UPDATE " + (
270
+ field.default if "(" in field.default else f"'{field.default}'"
271
+ )
272
+ if field.comment:
273
+ sql += f" COMMENT '{field.comment}'"
274
+
275
+ return sql
276
+
277
+
278
+ def get_filed(model):
279
+ return [
280
+ attr
281
+ for attr in dir(model)
282
+ if not callable(getattr(model, attr)) and not attr.startswith("__")
283
+ ]
284
+
285
+
286
+ class ColTypes(enum.Enum):
287
+ char = "Char"
288
+ varchar = "VarChar"
289
+ text = "Text"
290
+ longtext = "LongText"
291
+ mediumtext = "MediumText"
292
+ tinyint = "Boolean"
293
+ smallint = "Integer"
294
+ mediuint = "Integer"
295
+ int = "Integer"
296
+ bigint = "BigInteger"
297
+ float = "Float"
298
+ double = "Double"
299
+ decimal = "Decimal"
300
+ numeric = "Numeric"
301
+ date = "Date"
302
+ datetime = "Datetime"
303
+ timestamp = "Timestamp"
304
+ json = "Json"
305
+ # pg
306
+ timestamp_with_time_zone = "Timestamp"
307
+ timestamp_without_time_zone = "Timestamp"
308
+ character_varying = "VarChar"
309
+ character = "Char"
310
+ integer = "Integer"
311
+ boolean = "Boolean"