CheeseAPI 1.3.2__tar.gz → 1.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.
@@ -1,4 +1,4 @@
1
- import time, os, inspect, socket, multiprocessing, signal, http, ipaddress, datetime, traceback
1
+ import time, os, inspect, socket, multiprocessing, signal, http, ipaddress, datetime, traceback, threading
2
2
  from typing import TYPE_CHECKING, Dict, Tuple, List
3
3
 
4
4
  import asyncio, uvloop, setproctitle, websockets
@@ -10,6 +10,7 @@ from CheeseAPI.exception import Route_404_Exception, Route_405_Exception
10
10
  if TYPE_CHECKING:
11
11
  from CheeseAPI.app import App
12
12
  from CheeseAPI.protocol import HttpProtocol, WebsocketProtocol
13
+ from CheeseAPI.schedule import ScheduleTask
13
14
 
14
15
  class Handle:
15
16
  def __init__(self, app: 'App'):
@@ -149,7 +150,6 @@ class Handle:
149
150
  logger.error(f'''
150
151
  {logger.encode(traceback.format_exc()[:-1])}''')
151
152
  finally:
152
- logger.destroy()
153
153
  os.killpg(os.getpid(), signal.SIGKILL)
154
154
 
155
155
  def worker_beforeStarting(self):
@@ -243,18 +243,38 @@ class Handle:
243
243
  return
244
244
 
245
245
  timer = datetime.datetime.now()
246
+ tasks: List[ScheduleTask] = []
246
247
 
247
248
  for task in self._app.scheduler.tasks.values():
248
- triggeredTimer = task.startTimer + task.timer * task.total_repetition_num
249
-
250
- if (self._timer < triggeredTimer < timer or self._timer > triggeredTimer + task.timer) and task.active and task.is_unexpired:
251
- self._app._managers_['schedules'][task.key] = {
252
- **self._app._managers_['schedules'][task.key],
253
- 'total_repetition_num': task.total_repetition_num + 1
254
- }
255
- await task.fn()
256
- if task.is_expired and task.auto_remove:
257
- self._app.scheduler.remove(task.key)
249
+ if task.inactive or task.expired:
250
+ continue
251
+
252
+ if task.mode == 'multiprocessing':
253
+ if task.key not in self._app.scheduler._taskHandlers:
254
+ self._app.scheduler._taskHandlers[task.key] = multiprocessing.Process(target = self._app.scheduler._processHandle, args = (task.key, ), name = f'{setproctitle.getproctitle()}:SchedulerTask:{task.key}', daemon = True)
255
+ self._app.scheduler._taskHandlers[task.key].start()
256
+ elif task.mode == 'threading':
257
+ if task.key not in self._app.scheduler._taskHandlers:
258
+ self._app.scheduler._taskHandlers[task.key] = threading.Thread(target = self._app.scheduler._processHandle, args = (task.key, ), name = f'{setproctitle.getproctitle()}:SchedulerTask:{task.key}', daemon = True)
259
+ self._app.scheduler._taskHandlers[task.key].start()
260
+ elif task.mode == 'asyncio':
261
+ triggeredTimer = task.startTimer + task.timer * task.total_repetition_num
262
+
263
+ if (self._timer < triggeredTimer < timer or self._timer > triggeredTimer + task.timer):
264
+ tasks.append(task)
265
+
266
+ for task in tasks:
267
+ self._app._managers_['schedules'][task.key] = {
268
+ **self._app._managers_['schedules'][task.key],
269
+ 'total_repetition_num': task.total_repetition_num + 1,
270
+ 'lastTimer': timer
271
+ }
272
+
273
+ await asyncio.gather(*[ task.fn() for task in tasks ])
274
+
275
+ for task in self._app.scheduler.tasks.values():
276
+ if task.expired and task.auto_remove:
277
+ self._app.scheduler.remove(task.key)
258
278
 
259
279
  self._timer = timer
260
280
 
@@ -37,7 +37,7 @@ class Request:
37
37
  self._method = 'WEBSOCKET'
38
38
 
39
39
  def _parseBody(self):
40
- if 'Content-Type' not in self.headers:
40
+ if 'Content-Type' not in self.headers or self.body is None:
41
41
  return
42
42
 
43
43
  if 'application/json' in self.headers['Content-Type']:
@@ -360,6 +360,7 @@ class BaseResponse:
360
360
  'Transfer-Encoding': 'chunked',
361
361
  **headers
362
362
  }
363
+ self.headers['Content-Type'] += '; charset=utf-8'
363
364
  self.body: str | bytes | Callable | AsyncIterator = self.status.description if body is None else body
364
365
  self._transfering: bool = False
365
366
 
@@ -464,7 +465,7 @@ class JsonResponse(BaseResponse):
464
465
 
465
466
  def __init__(self, body: dict | list = {}, status: http.HTTPStatus | int = http.HTTPStatus.OK, headers: Dict[str, str] = {}):
466
467
  super().__init__(json.dumps(body), status, {
467
- 'Content-Type': 'application/json; charset=utf-8',
468
+ 'Content-Type': 'application/json',
468
469
  **headers
469
470
  })
470
471
 
@@ -1,7 +1,7 @@
1
- import uuid, datetime
2
- from typing import TYPE_CHECKING, Callable, Dict, overload
1
+ import uuid, datetime, multiprocessing, threading, time
2
+ from typing import TYPE_CHECKING, Callable, Dict, overload, Literal
3
3
 
4
- import dill
4
+ import dill, setproctitle
5
5
 
6
6
  if TYPE_CHECKING:
7
7
  from CheeseAPI.app import App
@@ -9,7 +9,7 @@ if TYPE_CHECKING:
9
9
  class ScheduleTask:
10
10
  def __init__(self, app: 'App', key: str):
11
11
  self._app: 'App' = app
12
- self.key = key
12
+ self._key: str = key
13
13
 
14
14
  def reset(self):
15
15
  '''
@@ -21,6 +21,14 @@ class ScheduleTask:
21
21
  'total_repetition_num': 0
22
22
  }
23
23
 
24
+ @property
25
+ def key(self) -> str:
26
+ '''
27
+ 【只读】
28
+ '''
29
+
30
+ return self._key
31
+
24
32
  @property
25
33
  def timer(self) -> datetime.timedelta:
26
34
  '''
@@ -52,7 +60,7 @@ class ScheduleTask:
52
60
  '''
53
61
  自定义的开始时间。若未设置,则为当前时间。
54
62
 
55
- 若设置后`self.is_expired and self.auto_remove`,则该任务会被立刻删除。
63
+ 若设置后`self.expired and self.auto_remove`,则该任务会被立刻删除。
56
64
  '''
57
65
 
58
66
  return self._app._managers_['schedules'][self.key]['startTimer']
@@ -64,7 +72,7 @@ class ScheduleTask:
64
72
  'startTimer': value
65
73
  }
66
74
 
67
- if self.auto_remove and self.is_expired:
75
+ if self.auto_remove and self.expired:
68
76
  del self._app._managers_['schedules'][self.key]
69
77
 
70
78
  @property
@@ -72,7 +80,7 @@ class ScheduleTask:
72
80
  '''
73
81
  任务过期后是否自动删除。
74
82
 
75
- 若设置后`self.is_expired and self.auto_remove`,则该任务会被立刻删除。
83
+ 若设置后`self.expired and self.auto_remove`,则该任务会被立刻删除。
76
84
  '''
77
85
 
78
86
  return self._app._managers_['schedules'][self.key]['auto_remove']
@@ -84,7 +92,7 @@ class ScheduleTask:
84
92
  'auto_remove': value
85
93
  }
86
94
 
87
- if self.auto_remove and self.is_expired:
95
+ if self.auto_remove and self.expired:
88
96
  del self._app._managers_['schedules'][self.key]
89
97
 
90
98
  @property
@@ -102,15 +110,20 @@ class ScheduleTask:
102
110
  'active': value
103
111
  }
104
112
 
105
- if self.auto_remove and self.is_expired:
106
- del self._app._managers_['schedules'][self.key]
113
+ @property
114
+ def inactive(self) -> bool:
115
+ '''
116
+ 【只读】 是否未激活。
117
+ '''
118
+
119
+ return not self._app._managers_['schedules'][self.key]['active']
107
120
 
108
121
  @property
109
122
  def expected_repetition_num(self) -> int:
110
123
  '''
111
124
  期望的重复次数;0代表无限次数。
112
125
 
113
- 若设置后`self.is_expired and self.auto_remove`,则该任务会被立刻删除。
126
+ 若设置后`self.expired and self.auto_remove`,则该任务会被立刻删除。
114
127
  '''
115
128
 
116
129
  return self._app._managers_['schedules'][self.key]['expected_repetition_num']
@@ -122,8 +135,13 @@ class ScheduleTask:
122
135
  'expected_repetition_num': value
123
136
  }
124
137
 
125
- if self.auto_remove and self.is_expired:
126
- del self._app._managers_['schedules'][self.key]
138
+ @property
139
+ def mode(self) -> Literal['multiprocessing', 'threading', 'asyncio']:
140
+ '''
141
+ 【只读】 运行的模式是进程、线程还是协程。
142
+ '''
143
+
144
+ return self._app._managers_['schedules'][self.key]['mode']
127
145
 
128
146
  @property
129
147
  def total_repetition_num(self) -> int:
@@ -144,7 +162,7 @@ class ScheduleTask:
144
162
  return self.expected_repetition_num - self.total_repetition_num
145
163
 
146
164
  @property
147
- def is_unexpired(self) -> bool:
165
+ def unexpired(self) -> bool:
148
166
  '''
149
167
  【只读】 任务是否未过期。
150
168
  '''
@@ -155,19 +173,56 @@ class ScheduleTask:
155
173
  return self.startTimer + self.timer * (self.expected_repetition_num + 1) >= datetime.datetime.now()
156
174
 
157
175
  @property
158
- def is_expired(self) -> bool:
176
+ def expired(self) -> bool:
159
177
  '''
160
178
  【只读】 任务是否过期。
161
179
  '''
162
180
 
163
- return not self.is_unexpired
181
+ return not self.unexpired
182
+
183
+ @property
184
+ def lastTimer(self) -> datetime.datetime | None:
185
+ '''
186
+ 【只读】 任务上一次的触发时间;`None`代表从未触发过。
187
+ '''
188
+
189
+ return self._app._managers_['schedules'][self.key]['lastTimer']
164
190
 
165
191
  class Scheduler:
166
192
  def __init__(self, app: 'App'):
167
193
  self._app: 'App' = app
194
+ self._taskHandlers: Dict[str, multiprocessing.Process | threading.Thread] = {}
195
+
196
+ def _processHandle(self, key: str):
197
+ task: ScheduleTask = self._app.scheduler.tasks[key]
198
+ if task.mode == 'multiprocessing':
199
+ setproctitle.setproctitle(f'{setproctitle.getproctitle()}:SchedulerTask:{task.key}')
200
+ lastTimer = datetime.datetime.now()
201
+
202
+ while True:
203
+ task: ScheduleTask = self._app.scheduler.tasks.get(key)
204
+ if not task or task.expired or task.inactive:
205
+ break
206
+
207
+ _timer = datetime.datetime.now()
208
+
209
+ triggeredTimer = task.startTimer + task.timer * task.total_repetition_num
210
+ if (lastTimer < triggeredTimer <= _timer or lastTimer > triggeredTimer + task.timer) and task.active:
211
+ task.fn()
212
+
213
+ self._app._managers_['schedules'][task.key] = {
214
+ **self._app._managers_['schedules'][task.key],
215
+ 'total_repetition_num': task.total_repetition_num + 1,
216
+ 'lastTimer': _timer
217
+ }
218
+
219
+ lastTimer = _timer
220
+ time.sleep(self._app.server.intervalTime)
221
+
222
+ del self._taskHandlers[key]
168
223
 
169
224
  @overload
170
- def add(self, timer: datetime.timedelta, fn: Callable, *, key: str | None = None, startTimer: datetime.datetime | None = None, expected_repetition_num: int = 0, auto_remove: bool = False):
225
+ def add(self, timer: datetime.timedelta, fn: Callable, *, key: str | None = None, startTimer: datetime.datetime | None = None, expected_repetition_num: int = 0, auto_remove: bool = False, mode: Literal['multiprocessing', 'threading', 'asyncio'] = 'multiprocessing'):
171
226
  '''
172
227
  通过函数添加一个任务。
173
228
 
@@ -193,10 +248,12 @@ class Scheduler:
193
248
  - expected_repetition_num: 期望的重复次数,0代表无限重复。
194
249
 
195
250
  - auto_remove: 若`expected_repetition_num > 0`且当前计划过期,是否自动删除该计划。
251
+
252
+ - mode: 运行的模式是进程、线程还是协程。
196
253
  '''
197
254
 
198
255
  @overload
199
- def add(self, timer: datetime.timedelta, *, key: str | None = None, startTimer: datetime.datetime | None = None, expected_repetition_num: int = 0, auto_remove: bool = False):
256
+ def add(self, timer: datetime.timedelta, *, key: str | None = None, startTimer: datetime.datetime | None = None, expected_repetition_num: int = 0, auto_remove: bool = False, mode: Literal['multiprocessing', 'threading', 'asyncio'] = 'multiprocessing'):
200
257
  '''
201
258
  通过装饰器添加一个任务。
202
259
 
@@ -221,13 +278,15 @@ class Scheduler:
221
278
  - expected_repetition_num: 期望的重复次数,0代表无限重复。
222
279
 
223
280
  - auto_remove: 若`expected_repetition_num > 0`且当前计划过期,是否自动删除该计划。
281
+
282
+ - mode: 运行的模式是进程、线程还是协程。
224
283
  '''
225
284
 
226
- def add(self, timer: datetime.timedelta, fn: Callable | None = None, *, key: str | None = None, startTimer: datetime.datetime | None = None, expected_repetition_num: int = 0, auto_remove: bool = False):
227
- if not key:
285
+ def add(self, timer: datetime.timedelta, fn: Callable | None = None, *, key: str | None = None, startTimer: datetime.datetime | None = None, expected_repetition_num: int = 0, auto_remove: bool = False, mode: Literal['multiprocessing', 'threading', 'asyncio'] = 'multiprocessing'):
286
+ if key is None:
228
287
  key = str(uuid.uuid4())
229
288
 
230
- if not startTimer:
289
+ if startTimer is None:
231
290
  startTimer = datetime.datetime.now()
232
291
 
233
292
  if fn:
@@ -238,7 +297,9 @@ class Scheduler:
238
297
  'expected_repetition_num': expected_repetition_num,
239
298
  'total_repetition_num': 0,
240
299
  'auto_remove': auto_remove,
241
- 'active': True
300
+ 'active': True,
301
+ 'mode': mode,
302
+ 'lastTimer': None
242
303
  }
243
304
  return
244
305
 
@@ -250,7 +311,9 @@ class Scheduler:
250
311
  'expected_repetition_num': expected_repetition_num,
251
312
  'total_repetition_num': 0,
252
313
  'auto_remove': auto_remove,
253
- 'active': True
314
+ 'active': True,
315
+ 'mode': mode,
316
+ 'lastTimer': None
254
317
  }
255
318
  return fn
256
319
  return wrapper
@@ -33,3 +33,7 @@ class Server:
33
33
  '''
34
34
 
35
35
  return self._intervalTime
36
+
37
+ @intervalTime.setter
38
+ def intervalTime(self, value: float):
39
+ self._intervalTime = value
@@ -1,17 +1,18 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: CheeseAPI
3
- Version: 1.3.2
3
+ Version: 1.4.0
4
4
  Summary: 一款web协程框架。
5
5
  Project-URL: Source, https://github.com/CheeseUnknown/CheeseAPI
6
6
  Author-email: Cheese Unknown <cheese@cheese.ren>
7
7
  License-File: LICENSE
8
8
  Keywords: API,BackEnd
9
- Requires-Dist: cheeselog==1.0.*
9
+ Requires-Dist: cheeselog==1.1.*
10
10
  Requires-Dist: cheesesignal==1.1.*
11
11
  Requires-Dist: dill
12
12
  Requires-Dist: email-validator
13
13
  Requires-Dist: httptools
14
14
  Requires-Dist: pydantic
15
+ Requires-Dist: setproctitle
15
16
  Requires-Dist: uvloop
16
17
  Requires-Dist: websockets
17
18
  Requires-Dist: xmltodict
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "CheeseAPI"
7
- version = "1.3.2"
7
+ version = "1.4.0"
8
8
  description = "一款web协程框架。"
9
9
  readme = "README.md"
10
10
  license-files = { paths = [ "LICENSE" ] }
@@ -14,7 +14,7 @@ authors = [
14
14
  keywords = [ 'API', 'BackEnd' ]
15
15
 
16
16
  dependencies = [
17
- "CheeseLog==1.0.*",
17
+ "CheeseLog==1.1.*",
18
18
  "xmltodict",
19
19
  "websockets",
20
20
  "uvloop",
@@ -22,7 +22,8 @@ dependencies = [
22
22
  "CheeseSignal==1.1.*",
23
23
  "dill",
24
24
  "pydantic",
25
- "email-validator"
25
+ "email-validator",
26
+ "setproctitle"
26
27
  ]
27
28
 
28
29
  [project.urls]
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