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/web/core.py ADDED
@@ -0,0 +1,557 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+
4
+ __author__ = "chuchur/chuchur.com"
5
+
6
+ import argparse
7
+ import asyncio
8
+ import hashlib
9
+ import os
10
+ import base64
11
+ import importlib
12
+ import inspect
13
+ import json
14
+ from errno import EADDRINUSE
15
+ from urllib import parse
16
+ from aiohttp import web, hdrs
17
+ from aiohttp.web import Response
18
+ from aiohttp.web_ws import WebSocketResponse, StreamResponse, WSMsgType
19
+ from aiohttp.web_response import LooseHeaders
20
+ from aiohttp_session import (
21
+ get_session,
22
+ setup,
23
+ redis_storage,
24
+ memcached_storage,
25
+ cookie_storage,
26
+ )
27
+ from .settings import get_config
28
+ import aiomcache
29
+ from redis import asyncio as aioredis
30
+ from ..logging import warning, info
31
+ from ..orm.model import Model
32
+ from . import jwt
33
+ from decimal import Decimal
34
+ from datetime import datetime, date
35
+ from ..utils.common import chainMap, Object
36
+ from ..orm import create_engine, parse_coon
37
+ import uuid
38
+ from typing import Optional, Iterable, Callable, Awaitable
39
+
40
+
41
+ class RequestHandler(object):
42
+ def __init__(self, fn):
43
+ self.fn = fn
44
+
45
+ async def __call__(self, request):
46
+ info(f"{request.method} {request.path}")
47
+ return await _render_result(request, self.fn)
48
+
49
+
50
+ async def _set_session_route(request):
51
+ params = dict()
52
+ # match
53
+ rt = request.match_info
54
+ for k, v in rt.items():
55
+ params[k] = v
56
+ request.params = Object(params)
57
+ session = await get_session(request)
58
+ # session = await new_session(request)
59
+ request.session = session
60
+
61
+
62
+ def _auto_reg_module(module_dir: str):
63
+ module_path = os.path.join(os.path.abspath("."), module_dir)
64
+ if not os.path.exists(module_path):
65
+ info(f"{module_dir} not detected")
66
+ return
67
+ for filename in os.listdir(module_dir):
68
+ if filename.startswith("__") or filename.startswith("."):
69
+ continue
70
+ full_path = os.path.join(os.path.abspath("."), module_dir, filename)
71
+ if os.path.isdir(full_path):
72
+ _auto_reg_module(module_dir + os.sep + filename)
73
+ elif filename.endswith(".py"):
74
+ module_name = (
75
+ module_dir.replace(os.sep, ".")
76
+ + "."
77
+ + filename[:-3].replace(os.sep, ".")
78
+ )
79
+ importlib.import_module(module_name)
80
+
81
+
82
+ async def _render_result(request, func):
83
+ content_type = request.content_type
84
+ # 获取函数参数的名称和默认值
85
+ props = inspect.getfullargspec(func)
86
+ args = list(props.args)
87
+ if "self" in args:
88
+ args.remove("self")
89
+
90
+ if len(args) == 2 and content_type == "multipart/form-data":
91
+ await _set_session_route(request)
92
+ multipart = await request.multipart()
93
+ field = await multipart.next()
94
+ result = await func(request, field)
95
+ elif len(args) == 1:
96
+ await _set_session_route(request)
97
+ if content_type == "multipart/form-data":
98
+ data = await request.post()
99
+ elif content_type == "application/json":
100
+ data = await request.json()
101
+ else:
102
+ data = await request.post()
103
+ query_string = request.query_string
104
+ body = {}
105
+ for k in data:
106
+ body[k] = data[k]
107
+ qs = {}
108
+ if query_string:
109
+ for k, v in parse.parse_qs(query_string, True).items():
110
+ qs[k] = v[0]
111
+ request.qs = Object(qs)
112
+ request.body = Object(body)
113
+ result = await func(request)
114
+ else:
115
+ result = await func()
116
+ try:
117
+ if isinstance(result, Response):
118
+ return result
119
+ if isinstance(result, StreamResponse):
120
+ return result
121
+ if isinstance(result, WebSocketResponse): # maybe catch error
122
+ return result
123
+ if "content_type" in result and "text/html" in result["content_type"]:
124
+ return result
125
+ except:
126
+ pass
127
+
128
+ return render_json(result)
129
+
130
+
131
+ def _sa_ignore_hash(method, path):
132
+ md5 = hashlib.md5()
133
+ hashstr = f"{method}{path}"
134
+ md5.update(hashstr.encode("utf-8"))
135
+ return md5.hexdigest()
136
+
137
+
138
+ def _parse_int(num):
139
+ return eval(num) if isinstance(num, str) else num
140
+
141
+
142
+ def _sa_ignore_middleware():
143
+ async def set_ignore(ctx, handler):
144
+ hashstr = _sa_ignore_hash(ctx.method, ctx.path)
145
+ ctx.is_sa_ignore = hashstr in ctx.app.ignore_paths
146
+ return await handler(ctx)
147
+
148
+ return set_ignore
149
+
150
+
151
+ class Application(object):
152
+ def __init__(self):
153
+ self._loop = None
154
+ self.env = None
155
+ self.app = None
156
+ self._route_table = web.RouteTableDef()
157
+ self._middleware = []
158
+ self.config = {}
159
+ self.clean_up = False
160
+
161
+ def _load_life_cycle(self, entry_model=None):
162
+ try:
163
+ if not entry_model:
164
+ return
165
+ entry = importlib.import_module(entry_model, ".")
166
+
167
+ life_cycle = ["on_startup", "on_shutdown", "on_cleanup", "on_task"]
168
+ for cycle in life_cycle:
169
+ if hasattr(entry, cycle):
170
+ cy = getattr(self, cycle)
171
+ cy.append(getattr(entry, cycle))
172
+ except ImportError:
173
+ warning(f"Entry model:{entry_model} can not find.")
174
+
175
+ def _init_parse(self):
176
+ try:
177
+ parser = argparse.ArgumentParser(description="Cloudapp parse")
178
+
179
+ parser.add_argument("-host", type=str, help="Server Host", required=False)
180
+ parser.add_argument("-port", type=int, help="Server Port", required=False)
181
+ parser.add_argument("-env", type=str, help="Environment", required=False)
182
+ self.args = parser.parse_args()
183
+ except:
184
+ pass
185
+
186
+ def create(self, env: str = None, entry_model: str = None, config=None):
187
+ # self.init_parse()
188
+ self.env = env
189
+ loop = asyncio.get_event_loop()
190
+ if loop is None:
191
+ loop = asyncio.new_event_loop()
192
+ self._loop = loop
193
+ if not config:
194
+ config = get_config(env or "local")
195
+ self.config = config
196
+
197
+ sa_ignore_mid = _sa_ignore_middleware()
198
+ sa_ignore_mid.__middleware_version__ = 1
199
+ self._middleware.append(sa_ignore_mid)
200
+
201
+ # middlewares
202
+ _auto_reg_module("middlewares")
203
+ info("auto register middlewares")
204
+
205
+ conf_server = config.get("server")
206
+ client_max_size = 1024**2 * 2
207
+ if conf_server is not None:
208
+ client_max_size = conf_server.get("client_max_size", client_max_size)
209
+ self.app = web.Application(
210
+ logger=None,
211
+ loop=loop,
212
+ middlewares=self._middleware,
213
+ client_max_size=_parse_int(client_max_size),
214
+ )
215
+
216
+ # database
217
+ self.app.ignore_paths = set()
218
+ self.app.db = Object()
219
+ self.app.on_startup.append(self._init_database)
220
+ self.app.on_cleanup.append(self._close_database)
221
+ self.app.config = config
222
+ self.app.env = env
223
+ self.app.jwt_encode = self.jwt_encode
224
+ self.app.jwt_decode = self.jwt_decode
225
+ # session
226
+ self.app.on_startup.append(self._init_session)
227
+ # router:
228
+ _auto_reg_module("controllers")
229
+ info("auto register controllers")
230
+
231
+ self.app.add_routes(self._route_table)
232
+
233
+ # load life
234
+ entry = conf_server.get("entry", entry_model)
235
+ self._load_life_cycle(entry)
236
+
237
+ # static
238
+ if conf_server is not None:
239
+ conf_st = conf_server.get("static")
240
+ if conf_st:
241
+ temp = os.path.join(os.path.abspath("."), "static")
242
+ self.app.router.add_static(**conf_st, path=temp)
243
+ info("Suggest using nginx or others instead.")
244
+
245
+ temp = os.path.join(os.path.abspath("."), "templates")
246
+ if os.path.exists(temp):
247
+ from jinja2 import Environment, FileSystemLoader
248
+
249
+ self.env = Environment(loader=FileSystemLoader(temp), autoescape=True)
250
+
251
+ return self
252
+
253
+ async def release(self):
254
+ try:
255
+ await self._close_database(self.app)
256
+ except:
257
+ pass
258
+
259
+ async def _close_database(self, apps):
260
+ if apps is None or self.clean_up:
261
+ return
262
+ self.clean_up = True
263
+
264
+ for db in apps.db:
265
+ info(f"release database {db}.")
266
+ await apps.db[db].close()
267
+
268
+ # close for session
269
+ if "redis" in apps:
270
+ info(f"release redis session.")
271
+ await apps.redis.close()
272
+ if "memcached" in apps:
273
+ info(f"release memcached session")
274
+ apps.memcached.close()
275
+
276
+ async def _init_database(self, apps):
277
+ conf_db = self.config.get("database")
278
+ if conf_db:
279
+ for db_key in conf_db:
280
+ apps.db[db_key] = await create_engine(**conf_db[db_key])
281
+
282
+ async def _init_session(self, apps):
283
+ config = self.config or {}
284
+ sess = config.get("session", {})
285
+
286
+ max_age = sess.get("max_age")
287
+ httponly = sess.get("httponly")
288
+ cookie_name = sess.get("key", "CLOUDOLL_SESSION")
289
+ secure = sess.get("secure")
290
+
291
+ # redis
292
+ redis_conf = sess.get("redis")
293
+ mcache_conf = sess.get("memcached")
294
+
295
+ if redis_conf:
296
+ redis_url = redis_conf.get("url")
297
+ qs = {}
298
+ if not redis_url:
299
+ cfg, qs = parse_coon(redis_url)
300
+ redis_url = f"{cfg['type']}://{cfg['username']}:{cfg['password']}@{cfg['host']}:{cfg['port']}/{cfg['db']}"
301
+
302
+ redis = await aioredis.from_url(redis_url, **qs)
303
+ apps.redis = redis
304
+ storage = redis_storage.RedisStorage(
305
+ redis,
306
+ cookie_name=cookie_name,
307
+ max_age=_parse_int(max_age),
308
+ httponly=httponly,
309
+ secure=secure,
310
+ )
311
+ setup(apps, storage)
312
+ info("start redis session.")
313
+ elif mcache_conf:
314
+ host = mcache_conf.get("host")
315
+ port = mcache_conf.get("port", 11211)
316
+
317
+ mc = aiomcache.Client(host, port)
318
+ apps.memcached = mc
319
+ storage = memcached_storage.MemcachedStorage(
320
+ mc,
321
+ cookie_name=cookie_name,
322
+ max_age=_parse_int(max_age),
323
+ httponly=httponly,
324
+ secure=secure,
325
+ )
326
+ setup(apps, storage)
327
+ info("start memcached session.")
328
+ else:
329
+ dig = hashlib.sha256(cookie_name.encode()).digest()
330
+ fernet_key = base64.urlsafe_b64encode(dig)
331
+ secret_key = base64.urlsafe_b64decode(fernet_key)
332
+
333
+ # fernet_key = fernet.Fernet.generate_key()
334
+ # secret_key = base64.urlsafe_b64decode(fernet_key)
335
+
336
+ storage = cookie_storage.EncryptedCookieStorage(
337
+ secret_key,
338
+ cookie_name=cookie_name,
339
+ max_age=_parse_int(max_age),
340
+ httponly=httponly,
341
+ )
342
+ setup(apps, storage)
343
+ info("start local cookie.")
344
+
345
+ def run(self, **kw):
346
+ """
347
+ run app
348
+ :params prot default 9001
349
+ :params host default 127.0.0.1
350
+ """
351
+ defaults = {"host": "0.0.0.0", "port": 9001, "path": None}
352
+ conf = self.config.get("server", {})
353
+ conf = chainMap(defaults, conf, kw)
354
+ info(f"Server running on http://{conf['host']}:{conf['port']}")
355
+ info("(Press CTRL+C to quit.)")
356
+ web.run_app(
357
+ self.app,
358
+ loop=self._loop,
359
+ host="0.0.0.0",
360
+ port=conf["port"],
361
+ path=conf["path"],
362
+ access_log=None,
363
+ print=None,
364
+ )
365
+
366
+ def add_router(self, path, method, name, sa_ignore):
367
+ def inner(handler):
368
+ handler = RequestHandler(handler)
369
+ self.router.add_route(method, path, handler, name=name)
370
+ return handler
371
+
372
+ if sa_ignore:
373
+ hashstr = _sa_ignore_hash(method, path)
374
+ self.app.ignore_paths.add(hashstr)
375
+ return inner
376
+
377
+ def add_middleware(self, func):
378
+ func.__middleware_version__ = 1
379
+ self._middleware.append(func)
380
+ return func
381
+
382
+ def jwt_encode(self, payload):
383
+ jwt_conf = self.config.get("jwt", {})
384
+ key = jwt_conf.get("key")
385
+ exp = jwt_conf.get("exp")
386
+ if not key or not exp:
387
+ raise KeyError("Please set jwt key or exp...")
388
+ return jwt.encode(payload, key, exp)
389
+
390
+ def jwt_decode(self, token):
391
+ jwt_conf = self.config.get("jwt", {})
392
+ key = jwt_conf.get("key")
393
+ return jwt.decode(token, key)
394
+
395
+ @property
396
+ def route_table(self):
397
+ return self._route_table
398
+
399
+ @property
400
+ def router(self):
401
+ return self.app.router
402
+
403
+ @property
404
+ def middlewares(self):
405
+ return self.app.middlewares
406
+
407
+ @property
408
+ def on_startup(self):
409
+ return self.app.on_startup
410
+
411
+ @property
412
+ def on_shutdown(self):
413
+ return self.app.on_shutdown
414
+
415
+ @property
416
+ def on_cleanup(self):
417
+ return self.app.on_cleanup
418
+
419
+ @property
420
+ def on_task(self):
421
+ return self.app.cleanup_ctx
422
+
423
+
424
+ class View(web.View):
425
+ async def _iter(self) -> StreamResponse:
426
+ request = self.request
427
+ if request.method not in hdrs.METH_ALL:
428
+ self._raise_allowed_methods()
429
+ func: Optional[Callable[[], Awaitable[StreamResponse]]]
430
+ func = getattr(self, request.method.lower(), None)
431
+ if func is None:
432
+ self._raise_allowed_methods()
433
+ return await _render_result(request, func)
434
+
435
+
436
+ app = Application()
437
+
438
+
439
+ class JsonEncoder(json.JSONEncoder):
440
+ def default(self, obj):
441
+ if isinstance(obj, datetime) or isinstance(obj, date):
442
+ return obj.__str__()
443
+ elif isinstance(obj, Decimal):
444
+ return str(obj)
445
+ elif isinstance(obj, set):
446
+ return list(obj)
447
+ elif isinstance(obj, Model):
448
+ return obj.__dict__
449
+ elif isinstance(obj, bytes):
450
+ return obj.decode("utf-8")
451
+ elif isinstance(obj, uuid.UUID) or isinstance(obj, Exception):
452
+ return str(obj)
453
+ else:
454
+ return super(JsonEncoder, self).default(obj)
455
+
456
+
457
+ async def WebSocket(
458
+ request,
459
+ timeout: float = 10.0,
460
+ receive_timeout: Optional[float] = None,
461
+ autoclose: bool = True,
462
+ autoping: bool = True,
463
+ heartbeat: Optional[float] = None,
464
+ protocols: Iterable[str] = (),
465
+ compress: bool = True,
466
+ max_msg_size: int = 4 * 1024 * 1024,
467
+ ) -> WebSocketResponse:
468
+ ws = WebSocketResponse(
469
+ timeout=timeout,
470
+ receive_timeout=receive_timeout,
471
+ autoclose=autoclose,
472
+ autoping=autoping,
473
+ heartbeat=heartbeat,
474
+ protocols=protocols,
475
+ compress=compress,
476
+ max_msg_size=max_msg_size,
477
+ )
478
+ await ws.prepare(request)
479
+ return ws
480
+
481
+
482
+ async def WebStream(
483
+ request,
484
+ status: int = 200,
485
+ reason: Optional[str] = None,
486
+ headers: Optional[LooseHeaders] = None,
487
+ ):
488
+ stream = StreamResponse(status=status, reason=reason, headers=headers)
489
+ await stream.prepare(request)
490
+ return stream
491
+
492
+
493
+ def get(path: str, name=None, sa_ignore=False):
494
+ return app.add_router(path, "GET", name, sa_ignore)
495
+
496
+
497
+ def post(path: str, name=None, sa_ignore=False):
498
+ return app.add_router(path, "POST", name, sa_ignore)
499
+
500
+
501
+ def put(path: str, name=None, sa_ignore=False):
502
+ return app.add_router(path, "PUT", name, sa_ignore)
503
+
504
+
505
+ def delete(path: str, name=None, sa_ignore=False):
506
+ return app.add_router(path, "DELETE", name, sa_ignore)
507
+
508
+
509
+ def routes(path: str, sa_ignore=False):
510
+ # todo: sa_ignore
511
+ return app.route_table.view(path)
512
+
513
+
514
+ def render_error(msg, status=500) -> Response:
515
+ return render_json({"message": msg, "code": status}, status=status)
516
+
517
+
518
+ def render_json(data, **kw) -> Response:
519
+ res = {}
520
+ if isinstance(data, list):
521
+ res["data"] = data
522
+ elif isinstance(data, dict):
523
+ res.update(data)
524
+ elif isinstance(data, tuple):
525
+ data = dict(data)
526
+ res.update(data)
527
+ else:
528
+ res["data"] = data
529
+ status = kw.get("status", 200)
530
+ if status == 200:
531
+ res["message"] = kw.get("message", "OK")
532
+ res["code"] = kw.get("code", 200)
533
+
534
+ res["timestamp"] = int(datetime.now().timestamp() * 1000)
535
+ text = json.dumps(res, ensure_ascii=False, cls=JsonEncoder)
536
+ return web.json_response(text=text, **kw)
537
+
538
+
539
+ def middleware(func):
540
+ return app.add_middleware(func)
541
+
542
+
543
+ def render(**kw) -> Response:
544
+ return Response(**kw)
545
+
546
+
547
+ def render_view(template=None, *args, **kw) -> Response:
548
+ body = None
549
+ if app.env is not None:
550
+ body = app.env.get_template(template).render(*args)
551
+ view = render(body=body, **kw)
552
+ view.content_type = "text/html;charset=utf-8"
553
+ return view
554
+
555
+
556
+ def redirect(urlpath):
557
+ return web.HTTPFound(location=urlpath)
@@ -0,0 +1,63 @@
1
+ from aiohttp.web_exceptions import *
2
+
3
+
4
+ __all__ = (
5
+ "HTTPException",
6
+ "HTTPError",
7
+ "HTTPRedirection",
8
+ "HTTPSuccessful",
9
+ "HTTPOk",
10
+ "HTTPCreated",
11
+ "HTTPAccepted",
12
+ "HTTPNonAuthoritativeInformation",
13
+ "HTTPNoContent",
14
+ "HTTPResetContent",
15
+ "HTTPPartialContent",
16
+ "HTTPMove",
17
+ "HTTPMultipleChoices",
18
+ "HTTPMovedPermanently",
19
+ "HTTPFound",
20
+ "HTTPSeeOther",
21
+ "HTTPNotModified",
22
+ "HTTPUseProxy",
23
+ "HTTPTemporaryRedirect",
24
+ "HTTPPermanentRedirect",
25
+ "HTTPClientError",
26
+ "HTTPBadRequest",
27
+ "HTTPUnauthorized",
28
+ "HTTPPaymentRequired",
29
+ "HTTPForbidden",
30
+ "HTTPNotFound",
31
+ "HTTPMethodNotAllowed",
32
+ "HTTPNotAcceptable",
33
+ "HTTPProxyAuthenticationRequired",
34
+ "HTTPRequestTimeout",
35
+ "HTTPConflict",
36
+ "HTTPGone",
37
+ "HTTPLengthRequired",
38
+ "HTTPPreconditionFailed",
39
+ "HTTPRequestEntityTooLarge",
40
+ "HTTPRequestURITooLong",
41
+ "HTTPUnsupportedMediaType",
42
+ "HTTPRequestRangeNotSatisfiable",
43
+ "HTTPExpectationFailed",
44
+ "HTTPMisdirectedRequest",
45
+ "HTTPUnprocessableEntity",
46
+ "HTTPFailedDependency",
47
+ "HTTPUpgradeRequired",
48
+ "HTTPPreconditionRequired",
49
+ "HTTPTooManyRequests",
50
+ "HTTPRequestHeaderFieldsTooLarge",
51
+ "HTTPUnavailableForLegalReasons",
52
+ "HTTPServerError",
53
+ "HTTPInternalServerError",
54
+ "HTTPNotImplemented",
55
+ "HTTPBadGateway",
56
+ "HTTPServiceUnavailable",
57
+ "HTTPGatewayTimeout",
58
+ "HTTPVersionNotSupported",
59
+ "HTTPVariantAlsoNegotiates",
60
+ "HTTPInsufficientStorage",
61
+ "HTTPNotExtended",
62
+ "HTTPNetworkAuthenticationRequired",
63
+ )
cloudoll/web/html.py ADDED
@@ -0,0 +1,93 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+
4
+ __author__ = "chuchur/chuchur.com"
5
+ from html.parser import HTMLParser
6
+
7
+
8
+ class parser(HTMLParser):
9
+ def __init__(self, tags: dict = None):
10
+ """
11
+ 标签属性查找
12
+ :params tags :{ "div":{ "id" : "list" ,"class": "table" }}
13
+ """
14
+ HTMLParser.__init__(self)
15
+ self._text = []
16
+ self._tags = tags
17
+ self._images = []
18
+ self._videos = []
19
+ self._finds = []
20
+
21
+ def handle_starttag(self, tag, attrs):
22
+ # print('<%s>' % tag)
23
+ # if self._tags:
24
+ # for t in self._tags:
25
+ # if tag == t:
26
+ # s = attrs
27
+ pass
28
+
29
+ def handle_endtag(self, tag):
30
+ # print('</%s>' % tag)
31
+
32
+ pass
33
+
34
+ def handle_startendtag(self, tag, attrs):
35
+ """
36
+ 处理单闭合标签
37
+ """
38
+ if tag == "img":
39
+ src = [v for k, v in attrs if k == "src"]
40
+ if len(src):
41
+ self._images.append(src[0])
42
+ if tag == "video":
43
+ src = [v for k, v in attrs if k == "src"]
44
+ if len(src):
45
+ self._videos.append(src[0])
46
+ # print('<%s/>' % tag)
47
+
48
+ pass
49
+
50
+ def handle_data(self, data):
51
+ self._text.append(data)
52
+ # print(data)
53
+ pass
54
+
55
+ def handle_comment(self, data):
56
+ # print('<!--', data, '-->')
57
+ pass
58
+
59
+ def handle_entityref(self, name):
60
+ # print('&%s;' % name)
61
+ pass
62
+
63
+ def handle_charref(self, name):
64
+ # print('&#%s;' % name)
65
+ pass
66
+
67
+ @property
68
+ def text(self):
69
+ return "".join(self._text)
70
+
71
+ @text.setter
72
+ def text(self, value):
73
+ self._text = value
74
+
75
+ @property
76
+ def images(self):
77
+ return self._images
78
+
79
+ @images.setter
80
+ def images(self, value):
81
+ self._images = value
82
+
83
+ @property
84
+ def videos(self):
85
+ return self._videos
86
+
87
+ @videos.setter
88
+ def videos(self, value):
89
+ self._videos = value
90
+
91
+ def parser(self, code: str):
92
+ self.feed(code)
93
+ return self