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/__init__.py +1 -0
- cloudoll/__main__.py +4 -0
- cloudoll/cli.py +76 -0
- cloudoll/error/__init__.py +0 -0
- cloudoll/error/errors.py +48 -0
- cloudoll/logging/__init__.py +188 -0
- cloudoll/mail/__init__.py +0 -0
- cloudoll/mail/smtp.py +197 -0
- cloudoll/orm/__init__.py +252 -0
- cloudoll/orm/base.py +80 -0
- cloudoll/orm/err.py +2 -0
- cloudoll/orm/field.py +522 -0
- cloudoll/orm/model.py +806 -0
- cloudoll/orm/mysql.py +84 -0
- cloudoll/orm/parse.py +139 -0
- cloudoll/orm/postgres.py +70 -0
- cloudoll/robot/__init__.py +0 -0
- cloudoll/robot/dingtalk.py +209 -0
- cloudoll/robot/feishu.py +102 -0
- cloudoll/robot/slack.py +115 -0
- cloudoll/utils/__init__.py +0 -0
- cloudoll/utils/cli_main.py +72 -0
- cloudoll/utils/cli_mian.py +74 -0
- cloudoll/utils/common.py +57 -0
- cloudoll/utils/m2d.py +311 -0
- cloudoll/utils/patch_loop.py +222 -0
- cloudoll/utils/snowflake.py +86 -0
- cloudoll/utils/watch.py +194 -0
- cloudoll/web/__init__.py +53 -0
- cloudoll/web/core.py +557 -0
- cloudoll/web/exception.py +63 -0
- cloudoll/web/html.py +93 -0
- cloudoll/web/http.py +79 -0
- cloudoll/web/jwt.py +40 -0
- cloudoll/web/requests.py +161 -0
- cloudoll/web/settings.py +22 -0
- cloudoll-2.4.0.dist-info/METADATA +414 -0
- cloudoll-2.4.0.dist-info/RECORD +42 -0
- cloudoll-2.4.0.dist-info/WHEEL +6 -0
- cloudoll-2.4.0.dist-info/entry_points.txt +2 -0
- cloudoll-2.4.0.dist-info/licenses/LICENSE +21 -0
- cloudoll-2.4.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
"""
|
|
2
|
+
from https://github.com/erdewit/nest_asyncio/blob/master/nest_asyncio.py
|
|
3
|
+
"""
|
|
4
|
+
"""Patch asyncio to allow nested event loops."""
|
|
5
|
+
|
|
6
|
+
import asyncio
|
|
7
|
+
import asyncio.events as events
|
|
8
|
+
import os
|
|
9
|
+
import sys
|
|
10
|
+
import threading
|
|
11
|
+
from contextlib import contextmanager, suppress
|
|
12
|
+
from heapq import heappop
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def apply(loop=None):
|
|
16
|
+
"""Patch asyncio to make its event loop reentrant."""
|
|
17
|
+
_patch_asyncio()
|
|
18
|
+
_patch_policy()
|
|
19
|
+
_patch_tornado()
|
|
20
|
+
|
|
21
|
+
loop = loop or asyncio.get_event_loop()
|
|
22
|
+
_patch_loop(loop)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _patch_asyncio():
|
|
26
|
+
"""Patch asyncio module to use pure Python tasks and futures."""
|
|
27
|
+
|
|
28
|
+
def run(main, *, debug=False):
|
|
29
|
+
loop = asyncio.get_event_loop()
|
|
30
|
+
loop.set_debug(debug)
|
|
31
|
+
task = asyncio.ensure_future(main)
|
|
32
|
+
try:
|
|
33
|
+
return loop.run_until_complete(task)
|
|
34
|
+
finally:
|
|
35
|
+
if not task.done():
|
|
36
|
+
task.cancel()
|
|
37
|
+
with suppress(asyncio.CancelledError):
|
|
38
|
+
loop.run_until_complete(task)
|
|
39
|
+
|
|
40
|
+
def _get_event_loop(stacklevel=3):
|
|
41
|
+
loop = events._get_running_loop()
|
|
42
|
+
if loop is None:
|
|
43
|
+
loop = events.get_event_loop_policy().get_event_loop()
|
|
44
|
+
return loop
|
|
45
|
+
|
|
46
|
+
# Use module level _current_tasks, all_tasks and patch run method.
|
|
47
|
+
if hasattr(asyncio, '_nest_patched'):
|
|
48
|
+
return
|
|
49
|
+
if sys.version_info >= (3, 6, 0):
|
|
50
|
+
asyncio.Task = asyncio.tasks._CTask = asyncio.tasks.Task = \
|
|
51
|
+
asyncio.tasks._PyTask
|
|
52
|
+
asyncio.Future = asyncio.futures._CFuture = asyncio.futures.Future = \
|
|
53
|
+
asyncio.futures._PyFuture
|
|
54
|
+
if sys.version_info < (3, 7, 0):
|
|
55
|
+
asyncio.tasks._current_tasks = asyncio.tasks.Task._current_tasks
|
|
56
|
+
asyncio.all_tasks = asyncio.tasks.Task.all_tasks
|
|
57
|
+
if sys.version_info >= (3, 9, 0):
|
|
58
|
+
events._get_event_loop = events.get_event_loop = \
|
|
59
|
+
asyncio.get_event_loop = _get_event_loop
|
|
60
|
+
asyncio.run = run
|
|
61
|
+
asyncio._nest_patched = True
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _patch_policy():
|
|
65
|
+
"""Patch the policy to always return a patched loop."""
|
|
66
|
+
|
|
67
|
+
def get_event_loop(self):
|
|
68
|
+
if self._local._loop is None:
|
|
69
|
+
loop = self.new_event_loop()
|
|
70
|
+
_patch_loop(loop)
|
|
71
|
+
self.set_event_loop(loop)
|
|
72
|
+
return self._local._loop
|
|
73
|
+
|
|
74
|
+
policy = events.get_event_loop_policy()
|
|
75
|
+
policy.__class__.get_event_loop = get_event_loop
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _patch_loop(loop):
|
|
79
|
+
"""Patch loop to make it reentrant."""
|
|
80
|
+
|
|
81
|
+
def run_forever(self):
|
|
82
|
+
with manage_run(self), manage_asyncgens(self):
|
|
83
|
+
while True:
|
|
84
|
+
self._run_once()
|
|
85
|
+
if self._stopping:
|
|
86
|
+
break
|
|
87
|
+
self._stopping = False
|
|
88
|
+
|
|
89
|
+
def run_until_complete(self, future):
|
|
90
|
+
with manage_run(self):
|
|
91
|
+
f = asyncio.ensure_future(future, loop=self)
|
|
92
|
+
if f is not future:
|
|
93
|
+
f._log_destroy_pending = False
|
|
94
|
+
while not f.done():
|
|
95
|
+
self._run_once()
|
|
96
|
+
if self._stopping:
|
|
97
|
+
break
|
|
98
|
+
if not f.done():
|
|
99
|
+
raise RuntimeError(
|
|
100
|
+
'Event loop stopped before Future completed.')
|
|
101
|
+
return f.result()
|
|
102
|
+
|
|
103
|
+
def _run_once(self):
|
|
104
|
+
"""
|
|
105
|
+
Simplified re-implementation of asyncio's _run_once that
|
|
106
|
+
runs handles as they become ready.
|
|
107
|
+
"""
|
|
108
|
+
ready = self._ready
|
|
109
|
+
scheduled = self._scheduled
|
|
110
|
+
while scheduled and scheduled[0]._cancelled:
|
|
111
|
+
heappop(scheduled)
|
|
112
|
+
|
|
113
|
+
timeout = (
|
|
114
|
+
0 if ready or self._stopping
|
|
115
|
+
else min(max(
|
|
116
|
+
scheduled[0]._when - self.time(), 0), 86400) if scheduled
|
|
117
|
+
else None)
|
|
118
|
+
event_list = self._selector.select(timeout)
|
|
119
|
+
self._process_events(event_list)
|
|
120
|
+
|
|
121
|
+
end_time = self.time() + self._clock_resolution
|
|
122
|
+
while scheduled and scheduled[0]._when < end_time:
|
|
123
|
+
handle = heappop(scheduled)
|
|
124
|
+
ready.append(handle)
|
|
125
|
+
|
|
126
|
+
for _ in range(len(ready)):
|
|
127
|
+
if not ready:
|
|
128
|
+
break
|
|
129
|
+
handle = ready.popleft()
|
|
130
|
+
if not handle._cancelled:
|
|
131
|
+
# preempt the current task so that that checks in
|
|
132
|
+
# Task.__step do not raise
|
|
133
|
+
curr_task = curr_tasks.pop(self, None)
|
|
134
|
+
|
|
135
|
+
try:
|
|
136
|
+
handle._run()
|
|
137
|
+
finally:
|
|
138
|
+
# restore the current task
|
|
139
|
+
if curr_task is not None:
|
|
140
|
+
curr_tasks[self] = curr_task
|
|
141
|
+
|
|
142
|
+
handle = None
|
|
143
|
+
|
|
144
|
+
@contextmanager
|
|
145
|
+
def manage_run(self):
|
|
146
|
+
"""Set up the loop for running."""
|
|
147
|
+
self._check_closed()
|
|
148
|
+
old_thread_id = self._thread_id
|
|
149
|
+
old_running_loop = events._get_running_loop()
|
|
150
|
+
try:
|
|
151
|
+
self._thread_id = threading.get_ident()
|
|
152
|
+
events._set_running_loop(self)
|
|
153
|
+
self._num_runs_pending += 1
|
|
154
|
+
if self._is_proactorloop:
|
|
155
|
+
if self._self_reading_future is None:
|
|
156
|
+
self.call_soon(self._loop_self_reading)
|
|
157
|
+
yield
|
|
158
|
+
finally:
|
|
159
|
+
self._thread_id = old_thread_id
|
|
160
|
+
events._set_running_loop(old_running_loop)
|
|
161
|
+
self._num_runs_pending -= 1
|
|
162
|
+
if self._is_proactorloop:
|
|
163
|
+
if (self._num_runs_pending == 0
|
|
164
|
+
and self._self_reading_future is not None):
|
|
165
|
+
ov = self._self_reading_future._ov
|
|
166
|
+
self._self_reading_future.cancel()
|
|
167
|
+
if ov is not None:
|
|
168
|
+
self._proactor._unregister(ov)
|
|
169
|
+
self._self_reading_future = None
|
|
170
|
+
|
|
171
|
+
@contextmanager
|
|
172
|
+
def manage_asyncgens(self):
|
|
173
|
+
if not hasattr(sys, 'get_asyncgen_hooks'):
|
|
174
|
+
# Python version is too old.
|
|
175
|
+
return
|
|
176
|
+
old_agen_hooks = sys.get_asyncgen_hooks()
|
|
177
|
+
try:
|
|
178
|
+
self._set_coroutine_origin_tracking(self._debug)
|
|
179
|
+
if self._asyncgens is not None:
|
|
180
|
+
sys.set_asyncgen_hooks(
|
|
181
|
+
firstiter=self._asyncgen_firstiter_hook,
|
|
182
|
+
finalizer=self._asyncgen_finalizer_hook)
|
|
183
|
+
yield
|
|
184
|
+
finally:
|
|
185
|
+
self._set_coroutine_origin_tracking(False)
|
|
186
|
+
if self._asyncgens is not None:
|
|
187
|
+
sys.set_asyncgen_hooks(*old_agen_hooks)
|
|
188
|
+
|
|
189
|
+
def _check_running(self):
|
|
190
|
+
"""Do not throw exception if loop is already running."""
|
|
191
|
+
pass
|
|
192
|
+
|
|
193
|
+
if hasattr(loop, '_nest_patched'):
|
|
194
|
+
return
|
|
195
|
+
if not isinstance(loop, asyncio.BaseEventLoop):
|
|
196
|
+
raise ValueError('Can\'t patch loop of type %s' % type(loop))
|
|
197
|
+
cls = loop.__class__
|
|
198
|
+
cls.run_forever = run_forever
|
|
199
|
+
cls.run_until_complete = run_until_complete
|
|
200
|
+
cls._run_once = _run_once
|
|
201
|
+
cls._check_running = _check_running
|
|
202
|
+
cls._check_runnung = _check_running # typo in Python 3.7 source
|
|
203
|
+
cls._num_runs_pending = 1 if loop.is_running() else 0
|
|
204
|
+
cls._is_proactorloop = (
|
|
205
|
+
os.name == 'nt' and issubclass(cls, asyncio.ProactorEventLoop))
|
|
206
|
+
if sys.version_info < (3, 7, 0):
|
|
207
|
+
cls._set_coroutine_origin_tracking = cls._set_coroutine_wrapper
|
|
208
|
+
curr_tasks = asyncio.tasks._current_tasks \
|
|
209
|
+
if sys.version_info >= (3, 7, 0) else asyncio.Task._current_tasks
|
|
210
|
+
cls._nest_patched = True
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def _patch_tornado():
|
|
214
|
+
"""
|
|
215
|
+
If tornado is imported before nest_asyncio, make tornado aware of
|
|
216
|
+
the pure-Python asyncio Future.
|
|
217
|
+
"""
|
|
218
|
+
if 'tornado' in sys.modules:
|
|
219
|
+
import tornado.concurrent as tc # type: ignore
|
|
220
|
+
tc.Future = asyncio.Future
|
|
221
|
+
if asyncio.Future not in tc.FUTURES:
|
|
222
|
+
tc.FUTURES += (asyncio.Future,)
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
|
|
2
|
+
import time
|
|
3
|
+
import threading
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
"""
|
|
7
|
+
雪花算法(全局唯一id)
|
|
8
|
+
|
|
9
|
+
# 最早是Twitter公司在其内部用于分布式环境下生成唯一ID
|
|
10
|
+
|
|
11
|
+
# Twitter's Snowflake algorithm implementation which is used to generate distributed IDs.
|
|
12
|
+
|
|
13
|
+
# https://github.com/twitter-archive/snowflake/blob/snowflake-2010/src/main/scala/com/twitter/service/snowflake/IdWorker.scala
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class IdWorker:
|
|
18
|
+
def __init__(self, worker_id, datacenter_id, sequence=0):
|
|
19
|
+
# 位移值
|
|
20
|
+
self.worker_id_bits = 5
|
|
21
|
+
self.datacenter_id_bits = 5
|
|
22
|
+
self.sequence_bits = 12
|
|
23
|
+
|
|
24
|
+
self.max_worker_id = -1 ^ (-1 << self.worker_id_bits)
|
|
25
|
+
self.max_datacenter_id = -1 ^ (-1 << self.datacenter_id_bits)
|
|
26
|
+
|
|
27
|
+
# 参数校验
|
|
28
|
+
if worker_id > self.max_worker_id or worker_id < 0:
|
|
29
|
+
raise ValueError(f'worker_id must be between 0 and {self.max_worker_id}')
|
|
30
|
+
if datacenter_id > self.max_datacenter_id or datacenter_id < 0:
|
|
31
|
+
raise ValueError(f'datacenter_id must be between 0 and {self.max_datacenter_id}')
|
|
32
|
+
|
|
33
|
+
self.worker_id = worker_id
|
|
34
|
+
self.datacenter_id = datacenter_id
|
|
35
|
+
self.sequence = sequence
|
|
36
|
+
self.lock = threading.Lock()
|
|
37
|
+
|
|
38
|
+
# Twitter Epoch (2010-11-04 01:42:54 UTC)
|
|
39
|
+
self.twepoch = 1288834974657
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
self.worker_id_shift = self.sequence_bits
|
|
45
|
+
self.datacenter_id_shift = self.sequence_bits + self.worker_id_bits
|
|
46
|
+
self.timestamp_left_shift = self.sequence_bits + self.worker_id_bits + self.datacenter_id_bits
|
|
47
|
+
self.sequence_mask = -1 ^ (-1 << self.sequence_bits)
|
|
48
|
+
|
|
49
|
+
self.last_timestamp = -1
|
|
50
|
+
|
|
51
|
+
def _time_gen(self):
|
|
52
|
+
return int(time.time() * 1000)
|
|
53
|
+
|
|
54
|
+
def _til_next_millis(self, last_timestamp):
|
|
55
|
+
timestamp = self._time_gen()
|
|
56
|
+
while timestamp <= last_timestamp:
|
|
57
|
+
timestamp = self._time_gen()
|
|
58
|
+
return timestamp
|
|
59
|
+
|
|
60
|
+
def next_id(self):
|
|
61
|
+
with self.lock:
|
|
62
|
+
timestamp = self._time_gen()
|
|
63
|
+
|
|
64
|
+
if timestamp < self.last_timestamp:
|
|
65
|
+
raise Exception(f'Clock moved backwards. Refusing to generate id for {self.last_timestamp - timestamp} milliseconds')
|
|
66
|
+
|
|
67
|
+
if self.last_timestamp == timestamp:
|
|
68
|
+
self.sequence = (self.sequence + 1) & self.sequence_mask
|
|
69
|
+
if self.sequence == 0:
|
|
70
|
+
timestamp = self._til_next_millis(self.last_timestamp)
|
|
71
|
+
else:
|
|
72
|
+
self.sequence = 0
|
|
73
|
+
|
|
74
|
+
self.last_timestamp = timestamp
|
|
75
|
+
|
|
76
|
+
id = ((timestamp - self.twepoch) << self.timestamp_left_shift) | \
|
|
77
|
+
(self.datacenter_id << self.datacenter_id_shift) | \
|
|
78
|
+
(self.worker_id << self.worker_id_shift) | \
|
|
79
|
+
self.sequence
|
|
80
|
+
|
|
81
|
+
return id
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
# eg:
|
|
85
|
+
# worker = IdWorker(worker_id=1, datacenter_id=1)
|
|
86
|
+
# print(worker.next_id())
|
cloudoll/utils/watch.py
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
from typing import AsyncIterator, Optional, Union
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from ..web import Application, app
|
|
4
|
+
import asyncio
|
|
5
|
+
import os
|
|
6
|
+
import signal
|
|
7
|
+
import sys
|
|
8
|
+
from contextlib import suppress
|
|
9
|
+
from multiprocessing import Process
|
|
10
|
+
from watchfiles import awatch, DefaultFilter
|
|
11
|
+
from ..logging import info, warning, error
|
|
12
|
+
import contextlib
|
|
13
|
+
from typing import Iterator, Optional
|
|
14
|
+
from typing import Optional, Union
|
|
15
|
+
from aiohttp import web
|
|
16
|
+
import traceback
|
|
17
|
+
from .common import check_port_open
|
|
18
|
+
|
|
19
|
+
class CloudollFilter(DefaultFilter):
|
|
20
|
+
def __init__(self, ignore_dirs: tuple = None) -> None:
|
|
21
|
+
self.ignore_dirs = self.ignore_dirs + tuple("logs")
|
|
22
|
+
if ignore_dirs:
|
|
23
|
+
self.ignore_dirs = self.ignore_dirs + ignore_dirs
|
|
24
|
+
|
|
25
|
+
super().__init__()
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class WatchTask:
|
|
29
|
+
_app: Application
|
|
30
|
+
_task: "asyncio.Task[None]"
|
|
31
|
+
|
|
32
|
+
def __init__(self, path: Union[Path, str]):
|
|
33
|
+
self._path = path
|
|
34
|
+
|
|
35
|
+
async def start(self, app: Application) -> None:
|
|
36
|
+
self._app = app
|
|
37
|
+
self.stopper = asyncio.Event()
|
|
38
|
+
ignore_dirs = self._config["server"].get("ignore_dirs", [])
|
|
39
|
+
self._awatch = awatch(
|
|
40
|
+
self._path,
|
|
41
|
+
stop_event=self.stopper,
|
|
42
|
+
watch_filter=CloudollFilter(tuple(ignore_dirs)),
|
|
43
|
+
)
|
|
44
|
+
self._task = asyncio.create_task(self._run())
|
|
45
|
+
|
|
46
|
+
async def _run(self) -> None:
|
|
47
|
+
raise NotImplementedError()
|
|
48
|
+
|
|
49
|
+
async def close(self, *args) -> None:
|
|
50
|
+
if self._task:
|
|
51
|
+
self.stopper.set()
|
|
52
|
+
self._task.cancel()
|
|
53
|
+
with suppress(asyncio.CancelledError):
|
|
54
|
+
await self._task
|
|
55
|
+
|
|
56
|
+
async def cleanup_ctx(self, app: Application) -> AsyncIterator[None]:
|
|
57
|
+
await self.start(app)
|
|
58
|
+
yield
|
|
59
|
+
await self.close()
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@contextlib.contextmanager
|
|
63
|
+
def set_tty(tty_path: Optional[str]) -> Iterator[None]:
|
|
64
|
+
try:
|
|
65
|
+
if not tty_path:
|
|
66
|
+
# to match OSError from open
|
|
67
|
+
raise OSError()
|
|
68
|
+
with open(tty_path) as tty:
|
|
69
|
+
sys.stdin = tty
|
|
70
|
+
yield
|
|
71
|
+
except OSError:
|
|
72
|
+
# either tty_path is None (windows) or opening it fails (eg. on pycharm)
|
|
73
|
+
yield
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def mian_app(tty_path, config, entry, env):
|
|
77
|
+
with set_tty(tty_path):
|
|
78
|
+
if sys.version_info >= (3, 11):
|
|
79
|
+
with asyncio.Runner() as runner:
|
|
80
|
+
app_runner = runner.run(create_main_app(config, entry, env))
|
|
81
|
+
try:
|
|
82
|
+
runner.run(start_main_app(app_runner, config["server"]["port"]))
|
|
83
|
+
runner.get_loop().run_forever()
|
|
84
|
+
except KeyboardInterrupt:
|
|
85
|
+
pass
|
|
86
|
+
finally:
|
|
87
|
+
with suppress(asyncio.TimeoutError, KeyboardInterrupt):
|
|
88
|
+
runner.run(app_runner.cleanup())
|
|
89
|
+
else:
|
|
90
|
+
loop = asyncio.new_event_loop()
|
|
91
|
+
runner = loop.run_until_complete(create_main_app(config, entry, env))
|
|
92
|
+
try:
|
|
93
|
+
loop.run_until_complete(start_main_app(runner, config["server"]["port"]))
|
|
94
|
+
loop.run_forever()
|
|
95
|
+
except KeyboardInterrupt:
|
|
96
|
+
pass
|
|
97
|
+
finally:
|
|
98
|
+
with suppress(asyncio.TimeoutError, KeyboardInterrupt):
|
|
99
|
+
loop.run_until_complete(runner.cleanup())
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
async def create_main_app(config, entry, env):
|
|
103
|
+
await check_port_open(config["server"]["port"])
|
|
104
|
+
App = app.create(env=env, config=config, entry_model=entry)
|
|
105
|
+
return web.AppRunner(App.app, shutdown_timeout=0.1)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
async def start_main_app(runner, port):
|
|
109
|
+
await runner.setup()
|
|
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)")
|
|
113
|
+
await site.start()
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class AppTask(WatchTask):
|
|
117
|
+
def __init__(self, watch_path: str, config, entry: None, env: str):
|
|
118
|
+
self._config = config
|
|
119
|
+
self._entry = entry
|
|
120
|
+
self._env = env
|
|
121
|
+
self._reloads = 0
|
|
122
|
+
assert watch_path
|
|
123
|
+
super().__init__(watch_path)
|
|
124
|
+
|
|
125
|
+
async def _run(self) -> None:
|
|
126
|
+
assert self._app is not None
|
|
127
|
+
self._start_dev_server()
|
|
128
|
+
try:
|
|
129
|
+
async for changes in self._awatch:
|
|
130
|
+
self._reloads += 1
|
|
131
|
+
if any(f.endswith(".py") for _, f in changes):
|
|
132
|
+
info(f"{len(changes)} changes, restarting server")
|
|
133
|
+
await self._stop_dev_server()
|
|
134
|
+
self._start_dev_server()
|
|
135
|
+
await asyncio.sleep(1)
|
|
136
|
+
except Exception as exc:
|
|
137
|
+
traceback.print_exc()
|
|
138
|
+
|
|
139
|
+
raise Exception("error running dev server")
|
|
140
|
+
|
|
141
|
+
def _start_dev_server(self) -> None:
|
|
142
|
+
act = "Start" if self._reloads == 0 else "Restart"
|
|
143
|
+
info(f"{act}ing dev server")
|
|
144
|
+
|
|
145
|
+
try:
|
|
146
|
+
tty_path = os.ttyname(sys.stdin.fileno())
|
|
147
|
+
except OSError: # pragma: no branch
|
|
148
|
+
# fileno() always fails with pytest
|
|
149
|
+
tty_path = "/dev/tty"
|
|
150
|
+
except AttributeError:
|
|
151
|
+
# on windows, without a windows machine I've no idea what else to do here
|
|
152
|
+
tty_path = None
|
|
153
|
+
|
|
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}")
|
|
172
|
+
|
|
173
|
+
async def _stop_dev_server(self) -> None:
|
|
174
|
+
if self._process.is_alive():
|
|
175
|
+
info("stopping server process...")
|
|
176
|
+
if self._process.pid:
|
|
177
|
+
info("sending SIGINT")
|
|
178
|
+
os.kill(self._process.pid, signal.SIGINT)
|
|
179
|
+
self._process.join(5)
|
|
180
|
+
if self._process.exitcode is None:
|
|
181
|
+
warning("process has not terminated, sending SIGKILL")
|
|
182
|
+
self._process.kill()
|
|
183
|
+
self._process.join(1)
|
|
184
|
+
else:
|
|
185
|
+
info("process stopped")
|
|
186
|
+
else:
|
|
187
|
+
warning(
|
|
188
|
+
"server process already dead, exit code: %s", self._process.exitcode
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
async def close(self) -> None:
|
|
192
|
+
self.stopper.set()
|
|
193
|
+
await self._stop_dev_server()
|
|
194
|
+
await asyncio.gather(super().close())
|
cloudoll/web/__init__.py
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
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
|
+
render,
|
|
27
|
+
middleware,
|
|
28
|
+
redirect,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
__all__: Tuple[str, ...] = (
|
|
32
|
+
"Application",
|
|
33
|
+
"app",
|
|
34
|
+
"WebSocket",
|
|
35
|
+
"Response",
|
|
36
|
+
"WebSocketResponse",
|
|
37
|
+
"WebStream",
|
|
38
|
+
"WSMsgType",
|
|
39
|
+
"View",
|
|
40
|
+
"routes",
|
|
41
|
+
"get",
|
|
42
|
+
"put",
|
|
43
|
+
"delete",
|
|
44
|
+
"post",
|
|
45
|
+
"render_json",
|
|
46
|
+
"render_error",
|
|
47
|
+
"render_view",
|
|
48
|
+
"render",
|
|
49
|
+
"middleware",
|
|
50
|
+
"redirect",
|
|
51
|
+
"jwt",
|
|
52
|
+
"get_config",
|
|
53
|
+
)
|