cloudoll 2.2.37__tar.gz → 2.3.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.
Files changed (49) hide show
  1. {cloudoll-2.2.37 → cloudoll-2.3.0}/PKG-INFO +13 -2
  2. cloudoll-2.3.0/cloudoll/__init__.py +1 -0
  3. cloudoll-2.3.0/cloudoll/cli.py +76 -0
  4. {cloudoll-2.2.37 → cloudoll-2.3.0}/cloudoll/logging/__init__.py +2 -2
  5. {cloudoll-2.2.37 → cloudoll-2.3.0}/cloudoll/orm/__init__.py +1 -1
  6. {cloudoll-2.2.37 → cloudoll-2.3.0}/cloudoll/orm/model.py +3 -3
  7. cloudoll-2.3.0/cloudoll/utils/cli_main.py +72 -0
  8. cloudoll-2.3.0/cloudoll/utils/common.py +51 -0
  9. {cloudoll-2.2.37 → cloudoll-2.3.0}/cloudoll/utils/watch.py +38 -26
  10. cloudoll-2.3.0/cloudoll/web/__init__.py +51 -0
  11. cloudoll-2.2.37/cloudoll/web/__init__.py → cloudoll-2.3.0/cloudoll/web/core.py +135 -196
  12. cloudoll-2.3.0/cloudoll/web/http.py +79 -0
  13. {cloudoll-2.2.37 → cloudoll-2.3.0}/cloudoll/web/settings.py +4 -4
  14. {cloudoll-2.2.37 → cloudoll-2.3.0}/cloudoll.egg-info/PKG-INFO +13 -2
  15. {cloudoll-2.2.37 → cloudoll-2.3.0}/cloudoll.egg-info/SOURCES.txt +2 -0
  16. {cloudoll-2.2.37 → cloudoll-2.3.0}/setup.py +0 -3
  17. cloudoll-2.2.37/cloudoll/__init__.py +0 -1
  18. cloudoll-2.2.37/cloudoll/cli.py +0 -137
  19. cloudoll-2.2.37/cloudoll/utils/common.py +0 -17
  20. cloudoll-2.2.37/cloudoll/web/http.py +0 -139
  21. {cloudoll-2.2.37 → cloudoll-2.3.0}/LICENSE +0 -0
  22. {cloudoll-2.2.37 → cloudoll-2.3.0}/README.md +0 -0
  23. {cloudoll-2.2.37 → cloudoll-2.3.0}/cloudoll/__main__.py +0 -0
  24. {cloudoll-2.2.37 → cloudoll-2.3.0}/cloudoll/error/__init__.py +0 -0
  25. {cloudoll-2.2.37 → cloudoll-2.3.0}/cloudoll/error/errors.py +0 -0
  26. {cloudoll-2.2.37 → cloudoll-2.3.0}/cloudoll/mail/__init__.py +0 -0
  27. {cloudoll-2.2.37 → cloudoll-2.3.0}/cloudoll/mail/smtp.py +0 -0
  28. {cloudoll-2.2.37 → cloudoll-2.3.0}/cloudoll/orm/base.py +0 -0
  29. {cloudoll-2.2.37 → cloudoll-2.3.0}/cloudoll/orm/err.py +0 -0
  30. {cloudoll-2.2.37 → cloudoll-2.3.0}/cloudoll/orm/field.py +0 -0
  31. {cloudoll-2.2.37 → cloudoll-2.3.0}/cloudoll/orm/mysql.py +0 -0
  32. {cloudoll-2.2.37 → cloudoll-2.3.0}/cloudoll/orm/parse.py +0 -0
  33. {cloudoll-2.2.37 → cloudoll-2.3.0}/cloudoll/orm/postgres.py +0 -0
  34. {cloudoll-2.2.37 → cloudoll-2.3.0}/cloudoll/robot/__init__.py +0 -0
  35. {cloudoll-2.2.37 → cloudoll-2.3.0}/cloudoll/robot/dingtalk.py +0 -0
  36. {cloudoll-2.2.37 → cloudoll-2.3.0}/cloudoll/robot/feishu.py +0 -0
  37. {cloudoll-2.2.37 → cloudoll-2.3.0}/cloudoll/robot/slack.py +0 -0
  38. {cloudoll-2.2.37 → cloudoll-2.3.0}/cloudoll/utils/__init__.py +0 -0
  39. {cloudoll-2.2.37 → cloudoll-2.3.0}/cloudoll/utils/m2d.py +0 -0
  40. {cloudoll-2.2.37 → cloudoll-2.3.0}/cloudoll/utils/patch_loop.py +0 -0
  41. {cloudoll-2.2.37 → cloudoll-2.3.0}/cloudoll/utils/snowflake.py +0 -0
  42. {cloudoll-2.2.37 → cloudoll-2.3.0}/cloudoll/web/exception.py +0 -0
  43. {cloudoll-2.2.37 → cloudoll-2.3.0}/cloudoll/web/html.py +0 -0
  44. {cloudoll-2.2.37 → cloudoll-2.3.0}/cloudoll/web/jwt.py +0 -0
  45. {cloudoll-2.2.37 → cloudoll-2.3.0}/cloudoll.egg-info/dependency_links.txt +0 -0
  46. {cloudoll-2.2.37 → cloudoll-2.3.0}/cloudoll.egg-info/entry_points.txt +0 -0
  47. {cloudoll-2.2.37 → cloudoll-2.3.0}/cloudoll.egg-info/requires.txt +0 -0
  48. {cloudoll-2.2.37 → cloudoll-2.3.0}/cloudoll.egg-info/top_level.txt +0 -0
  49. {cloudoll-2.2.37 → cloudoll-2.3.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: cloudoll
3
- Version: 2.2.37
3
+ Version: 2.3.0
4
4
  Summary: 辅助快速创建可分布的微服务。
5
5
  Home-page: https://gitee.com/chuchur/cloudoll-py
6
6
  Author: chuchur
@@ -29,6 +29,17 @@ Requires-Dist: aiohttp_session[secure]
29
29
  Requires-Dist: PyYAML
30
30
  Requires-Dist: click
31
31
  Requires-Dist: envyaml
32
+ Dynamic: author
33
+ Dynamic: author-email
34
+ Dynamic: classifier
35
+ Dynamic: description
36
+ Dynamic: description-content-type
37
+ Dynamic: home-page
38
+ Dynamic: license
39
+ Dynamic: license-file
40
+ Dynamic: requires-dist
41
+ Dynamic: requires-python
42
+ Dynamic: summary
32
43
 
33
44
 
34
45
  # cloudoll 云端玩具
@@ -0,0 +1 @@
1
+ __version__ = "2.3.0"
@@ -0,0 +1,76 @@
1
+ import click
2
+ import asyncio
3
+ import os
4
+ import sys
5
+ from .logging import print_error
6
+ from . import __version__
7
+ from typing import Any
8
+ from .utils.cli_main import run_app, run_gen
9
+ sys.path.append(os.path.abspath("."))
10
+
11
+
12
+ @click.group()
13
+ @click.version_option(__version__, "-V", "--version", prog_name="cloudoll")
14
+ def cli() -> None:
15
+ pass
16
+
17
+
18
+ life_cycle = ["on_startup", "on_shutdown", "on_cleanup", "cleanup_ctx"]
19
+
20
+
21
+ @cli.command()
22
+ @click.option("-p", "--path", help="Model's relative path,save models or create tables", default="models.py",)
23
+ @click.option("-c", "--create", help="For create model or table, model / table", default="model")
24
+ @click.option("-t", "--table", help="Table's name or Model's name, split by `,` or 'ALL'", required=True,)
25
+ @click.option("-env", "--environment", help="Environment, local / test / prod", default="local")
26
+ @click.option("-db", "--database", help="Database name, pick the database in conf.{env}.yaml", default="mysql",)
27
+ def gen(**config: Any) -> None:
28
+ """Help to create models or tables."""
29
+
30
+ # click.echo(create)
31
+ async def start():
32
+ try:
33
+ sa = await run_gen(**config)
34
+ except Exception as e:
35
+ # traceback.print_exc()
36
+ print_error('Error: %s', e)
37
+ click.echo("input `cloudoll --help` for more helps.")
38
+ finally:
39
+ await sa.close()
40
+
41
+ asyncio.run(start())
42
+
43
+
44
+ @cli.command()
45
+ @click.option("-env", "--environment", help="Environment, local / test / prod", default="local")
46
+ @click.option("-p", "--port", help="Server's port", default=None)
47
+ @click.option("-h", "--host", help="Server's host", default=None)
48
+ @click.option("-m", "--mode", help="development or production mode", default='development')
49
+ @click.option("-path", "--path", help="Unix file system path to serve on. Specifying a path will cause hostname and port arguments to be ignored.",)
50
+ @click.option("-e", "--entry", help="Entry point model name. delfault name app", default="app")
51
+ def start(**config: Any) -> None:
52
+ """Start a services."""
53
+ try:
54
+ active_config = {k: v for k, v in config.items() if v is not None}
55
+ run_app(**active_config)
56
+ except Exception as e:
57
+ print_error('Error: %s', e)
58
+ # traceback.format_exc()
59
+ sys.exit(2)
60
+
61
+
62
+ _project_existing = click.Path(dir_okay=True)
63
+
64
+
65
+ @cli.command()
66
+ @click.argument("project-name", type=_project_existing, required=True)
67
+ def create(project_name) -> None:
68
+ full_path = os.path.join(os.path.abspath("."), project_name)
69
+ # log = logging.getLogger('cloudoll.create')
70
+ if os.path.exists(full_path):
71
+ print_error(f"Project name `{project_name}` already exist.")
72
+ # return None
73
+
74
+
75
+ if __name__ == "__main__":
76
+ cli()
@@ -42,10 +42,10 @@ class ColorAnsi:
42
42
  _FORMATS = {
43
43
  # 终端输出格式
44
44
  # "console_format": "%(log_color)s[%(levelname)s]-%(asctime)s-%(pathname)s-%(module)s-%(funcName)s-[line:%(lineno)d]: %(message)s",
45
- "console_format": "%(log_color)s%(asctime)s %(levelname)s %(funcName)s %(message)s",
45
+ "console_format": "%(log_color)s%(asctime)s %(levelname)s %(message)s",
46
46
  # 日志输出格式
47
47
  # "log_format": "[%(levelname)s]-%(asctime)s-%(pathname)s-%(module)s-%(funcName)s-[line:%(lineno)d]: %(message)s",
48
- "log_format": "%(asctime)s %(levelname)s %(funcName)s %(message)s",
48
+ "log_format": "%(asctime)s %(levelname)s %(message)s",
49
49
  }
50
50
 
51
51
  _LOG_SIZE = 1024 * 1024 * 1 # 1MB 日志最大1MB
@@ -44,7 +44,7 @@ async def create_engine(**kw):
44
44
  url = f"{driver}://{configs['username']}:{configs['password']}@{configs['host']}:{configs['port']}/{configs['db']}"
45
45
  return await aioredis.from_url(url, **query)
46
46
  else:
47
- print_error("Not suport this database type.")
47
+ raise "Not suport this database type."
48
48
 
49
49
 
50
50
  class Postgres(MeteBase):
@@ -1,5 +1,5 @@
1
1
  from .field import Field, Function, Expression
2
- from ..logging import print_warn
2
+ from ..logging import warning
3
3
  from functools import reduce
4
4
  import operator
5
5
  import copy
@@ -35,13 +35,13 @@ class ModelMetaclass(type):
35
35
  # mappings[k] = v
36
36
  if v.primary_key:
37
37
  if primary_key:
38
- print_warn(f"Duplicate primary key for {table_name}")
38
+ warning(f"Duplicate primary key for {table_name}")
39
39
  primary_key = k
40
40
  # else:
41
41
  fields.append(k)
42
42
 
43
43
  if not primary_key:
44
- print_warn(f"{table_name} Missing primary key")
44
+ warning(f"{table_name} Missing primary key")
45
45
 
46
46
  # for k in mappings.keys():
47
47
  # attrs.pop(k)
@@ -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,51 @@
1
+ import asyncio
2
+ class Object(dict):
3
+ # __setattr__ = dict.__setitem__
4
+ # __getattr__ = dict.__getitem__
5
+ def __setattr__(self, key, value):
6
+ self[key] = value
7
+
8
+ def __getattr__(self, key):
9
+ return self.get(key, None)
10
+
11
+ def __delattr__(self, key):
12
+ try:
13
+ del self[key]
14
+ except KeyError:
15
+ raise AttributeError(f"'DotDict' object has no attribute '{key}'")
16
+
17
+ def __getstate__(self):
18
+ '''to fix pickle.dumps '''
19
+ return dict(self)
20
+
21
+ def __setstate__(self, state):
22
+ self.update(state)
23
+
24
+ def chainMap(*dicts):
25
+ merged_dict = Object()
26
+ for d in dicts:
27
+ for key, value in d.items():
28
+ if key not in merged_dict or value is not None:
29
+ merged_dict[key] = value
30
+ return merged_dict
31
+
32
+
33
+ async def check_port_open(port: int, delay: float = 1) -> None:
34
+ loop = asyncio.get_running_loop()
35
+ # the "s = socket.socket; s.bind" approach sometimes says a port is in use when it's not
36
+ # this approach replicates aiohttp so should always give the same answer
37
+ for i in range(5, 0, -1):
38
+ try:
39
+ server = await loop.create_server(
40
+ asyncio.Protocol, host="0.0.0.0", port=port
41
+ )
42
+ except OSError as e:
43
+ if e.errno != EADDRINUSE:
44
+ raise
45
+ warning("port %d is already in use, waiting %d...", port, i)
46
+ await asyncio.sleep(delay)
47
+ else:
48
+ server.close()
49
+ await server.wait_closed()
50
+ return
51
+ raise Exception("The port {} is already is use".format(port))
@@ -1,6 +1,6 @@
1
- from typing import AsyncIterator, Iterable, Optional, Tuple, Union
1
+ from typing import AsyncIterator, Optional, Union
2
2
  from pathlib import Path
3
- from ..web import Application, app, check_port_open
3
+ from ..web import Application, app
4
4
  import asyncio
5
5
  import os
6
6
  import signal
@@ -8,12 +8,13 @@ import sys
8
8
  from contextlib import suppress
9
9
  from multiprocessing import Process
10
10
  from watchfiles import awatch, DefaultFilter
11
- from ..logging import print_info, print_warn
11
+ from ..logging import info, warning, error
12
12
  import contextlib
13
- from typing import Any, Iterator, Optional, NoReturn
14
- from typing import TYPE_CHECKING, Optional, Sequence, Union
13
+ from typing import Iterator, Optional
14
+ from typing import Optional, Union
15
15
  from aiohttp import web
16
-
16
+ import traceback
17
+ from .common import check_port_open
17
18
 
18
19
  class CloudollFilter(DefaultFilter):
19
20
  def __init__(self, ignore_dirs: tuple = None) -> None:
@@ -89,9 +90,7 @@ def mian_app(tty_path, config, entry, env):
89
90
  loop = asyncio.new_event_loop()
90
91
  runner = loop.run_until_complete(create_main_app(config, entry, env))
91
92
  try:
92
- loop.run_until_complete(
93
- start_main_app(runner, config["server"]["port"])
94
- )
93
+ loop.run_until_complete(start_main_app(runner, config["server"]["port"]))
95
94
  loop.run_forever()
96
95
  except KeyboardInterrupt:
97
96
  pass
@@ -109,6 +108,8 @@ async def create_main_app(config, entry, env):
109
108
  async def start_main_app(runner, port):
110
109
  await runner.setup()
111
110
  site = web.TCPSite(runner, host="0.0.0.0", port=port)
111
+ info(f"Server running on http://0.0.0.0:{port}")
112
+ info("(Press CTRL+C to quit)")
112
113
  await site.start()
113
114
 
114
115
 
@@ -123,26 +124,23 @@ class AppTask(WatchTask):
123
124
 
124
125
  async def _run(self) -> None:
125
126
  assert self._app is not None
127
+ self._start_dev_server()
126
128
  try:
127
- self._start_dev_server()
128
-
129
129
  async for changes in self._awatch:
130
130
  self._reloads += 1
131
131
  if any(f.endswith(".py") for _, f in changes):
132
- print_info(f"{len(changes)} changes, restarting server")
132
+ info(f"{len(changes)} changes, restarting server")
133
133
  await self._stop_dev_server()
134
134
  self._start_dev_server()
135
135
  await asyncio.sleep(1)
136
136
  except Exception as exc:
137
- # exception(exc)
137
+ traceback.print_exc()
138
+
138
139
  raise Exception("error running dev server")
139
140
 
140
141
  def _start_dev_server(self) -> None:
141
142
  act = "Start" if self._reloads == 0 else "Restart"
142
- print_info(f"{act}ing dev server")
143
- print_info(
144
- f"Server Running on http://{self._config['server']['host']}:{self._config['server']['port']}"
145
- )
143
+ info(f"{act}ing dev server")
146
144
 
147
145
  try:
148
146
  tty_path = os.ttyname(sys.stdin.fileno())
@@ -153,26 +151,40 @@ class AppTask(WatchTask):
153
151
  # on windows, without a windows machine I've no idea what else to do here
154
152
  tty_path = None
155
153
 
156
- self._process = Process(
157
- target=mian_app, args=(tty_path, self._config, self._entry, self._env)
158
- )
159
- self._process.start()
154
+ # for key, value in self._config.items():
155
+ # try:
156
+ # pickle.dumps(value)
157
+ # except Exception as e:
158
+ # print(f"Serialization error on key '{key}': {e} {value}")
159
+
160
+ # import pickle
161
+ # try:
162
+ # pickle.dumps(b) # 检查是否可序列化
163
+ # except Exception as e:
164
+ # print("Serialization error:", e)
165
+ try:
166
+ self._process = Process(
167
+ target=mian_app, args=(tty_path, self._config, self._entry, self._env)
168
+ )
169
+ self._process.start()
170
+ except Exception as e:
171
+ error(f"Error starting server: {e}")
160
172
 
161
173
  async def _stop_dev_server(self) -> None:
162
174
  if self._process.is_alive():
163
- print_info("stopping server process...")
175
+ info("stopping server process...")
164
176
  if self._process.pid:
165
- print_info("sending SIGINT")
177
+ info("sending SIGINT")
166
178
  os.kill(self._process.pid, signal.SIGINT)
167
179
  self._process.join(5)
168
180
  if self._process.exitcode is None:
169
- print_warn("process has not terminated, sending SIGKILL")
181
+ warning("process has not terminated, sending SIGKILL")
170
182
  self._process.kill()
171
183
  self._process.join(1)
172
184
  else:
173
- print_info("process stopped")
185
+ info("process stopped")
174
186
  else:
175
- print_warn(
187
+ warning(
176
188
  "server process already dead, exit code: %s", self._process.exitcode
177
189
  )
178
190
 
@@ -0,0 +1,51 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+
4
+ __author__ = "chuchur/chuchur.com"
5
+ from typing import Tuple
6
+
7
+ from .settings import get_config
8
+ from . import jwt
9
+ from .core import (
10
+ Application,
11
+ app,
12
+ Response,
13
+ WebSocketResponse,
14
+ WebSocket,
15
+ WebStream,
16
+ WSMsgType,
17
+ View,
18
+ routes,
19
+ get,
20
+ put,
21
+ delete,
22
+ post,
23
+ render_json,
24
+ render_error,
25
+ render_view,
26
+ middleware,
27
+ redirect,
28
+ )
29
+
30
+ __all__: Tuple[str, ...] = (
31
+ "Application",
32
+ "app",
33
+ "WebSocket",
34
+ "Response",
35
+ "WebSocketResponse",
36
+ "WebStream",
37
+ "WSMsgType",
38
+ "View",
39
+ "routes",
40
+ "get",
41
+ "put",
42
+ "delete",
43
+ "post",
44
+ "render_json",
45
+ "render_error",
46
+ "render_view",
47
+ "middleware",
48
+ "redirect",
49
+ "jwt",
50
+ "get_config",
51
+ )