diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..c801a94
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,2 @@
+# Auto detect text files and perform LF normalization
+# text=auto
diff --git a/README.md b/README.md
deleted file mode 100644
index e69de29..0000000
diff --git a/readme b/readme
new file mode 100644
index 0000000..09b0832
--- /dev/null
+++ b/readme
@@ -0,0 +1,2 @@
+this is a python project
+learning python
\ No newline at end of file
diff --git a/www/__pycache__/apis.cpython-37.pyc b/www/__pycache__/apis.cpython-37.pyc
new file mode 100644
index 0000000..c8f835c
Binary files /dev/null and b/www/__pycache__/apis.cpython-37.pyc differ
diff --git a/www/__pycache__/apis.cpython-38.pyc b/www/__pycache__/apis.cpython-38.pyc
new file mode 100644
index 0000000..356a6b9
Binary files /dev/null and b/www/__pycache__/apis.cpython-38.pyc differ
diff --git a/www/__pycache__/config.cpython-37.pyc b/www/__pycache__/config.cpython-37.pyc
new file mode 100644
index 0000000..e831e95
Binary files /dev/null and b/www/__pycache__/config.cpython-37.pyc differ
diff --git a/www/__pycache__/config.cpython-38.pyc b/www/__pycache__/config.cpython-38.pyc
new file mode 100644
index 0000000..84d7847
Binary files /dev/null and b/www/__pycache__/config.cpython-38.pyc differ
diff --git a/www/__pycache__/config_default.cpython-37.pyc b/www/__pycache__/config_default.cpython-37.pyc
new file mode 100644
index 0000000..4a981ac
Binary files /dev/null and b/www/__pycache__/config_default.cpython-37.pyc differ
diff --git a/www/__pycache__/config_default.cpython-38.pyc b/www/__pycache__/config_default.cpython-38.pyc
new file mode 100644
index 0000000..394ce30
Binary files /dev/null and b/www/__pycache__/config_default.cpython-38.pyc differ
diff --git a/www/__pycache__/config_override.cpython-37.pyc b/www/__pycache__/config_override.cpython-37.pyc
new file mode 100644
index 0000000..198f176
Binary files /dev/null and b/www/__pycache__/config_override.cpython-37.pyc differ
diff --git a/www/__pycache__/config_override.cpython-38.pyc b/www/__pycache__/config_override.cpython-38.pyc
new file mode 100644
index 0000000..216d0da
Binary files /dev/null and b/www/__pycache__/config_override.cpython-38.pyc differ
diff --git a/www/__pycache__/coroweb.cpython-37.pyc b/www/__pycache__/coroweb.cpython-37.pyc
new file mode 100644
index 0000000..4c31d58
Binary files /dev/null and b/www/__pycache__/coroweb.cpython-37.pyc differ
diff --git a/www/__pycache__/coroweb.cpython-38.pyc b/www/__pycache__/coroweb.cpython-38.pyc
new file mode 100644
index 0000000..956c6a9
Binary files /dev/null and b/www/__pycache__/coroweb.cpython-38.pyc differ
diff --git a/www/__pycache__/handlers.cpython-37.pyc b/www/__pycache__/handlers.cpython-37.pyc
new file mode 100644
index 0000000..d89ba96
Binary files /dev/null and b/www/__pycache__/handlers.cpython-37.pyc differ
diff --git a/www/__pycache__/handlers.cpython-38.pyc b/www/__pycache__/handlers.cpython-38.pyc
new file mode 100644
index 0000000..27963a3
Binary files /dev/null and b/www/__pycache__/handlers.cpython-38.pyc differ
diff --git a/www/__pycache__/models.cpython-37.pyc b/www/__pycache__/models.cpython-37.pyc
new file mode 100644
index 0000000..c8117c5
Binary files /dev/null and b/www/__pycache__/models.cpython-37.pyc differ
diff --git a/www/__pycache__/models.cpython-38.pyc b/www/__pycache__/models.cpython-38.pyc
new file mode 100644
index 0000000..2b5d70c
Binary files /dev/null and b/www/__pycache__/models.cpython-38.pyc differ
diff --git a/www/__pycache__/orm.cpython-37.pyc b/www/__pycache__/orm.cpython-37.pyc
new file mode 100644
index 0000000..8c4f5f0
Binary files /dev/null and b/www/__pycache__/orm.cpython-37.pyc differ
diff --git a/www/__pycache__/orm.cpython-38.pyc b/www/__pycache__/orm.cpython-38.pyc
new file mode 100644
index 0000000..e83e23c
Binary files /dev/null and b/www/__pycache__/orm.cpython-38.pyc differ
diff --git a/www/apis.py b/www/apis.py
new file mode 100644
index 0000000..45c23de
--- /dev/null
+++ b/www/apis.py
@@ -0,0 +1,41 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+
+__author__ = 'Michael Liao'
+
+'''
+JSON API definition.
+'''
+
+import json, logging, inspect, functools
+
+class APIError(Exception):
+ '''
+ the base APIError which contains error(required), data(optional) and message(optional).
+ '''
+ def __init__(self, error, data='', message=''):
+ super(APIError, self).__init__(message)
+ self.error = error
+ self.data = data
+ self.message = message
+
+class APIValueError(APIError):
+ '''
+ Indicate the input value has error or invalid. The data specifies the error field of input form.
+ '''
+ def __init__(self, field, message=''):
+ super(APIValueError, self).__init__('value:invalid', field, message)
+
+class APIResourceNotFoundError(APIError):
+ '''
+ Indicate the resource was not found. The data specifies the resource name.
+ '''
+ def __init__(self, field, message=''):
+ super(APIResourceNotFoundError, self).__init__('value:notfound', field, message)
+
+class APIPermissionError(APIError):
+ '''
+ Indicate the api has no permission.
+ '''
+ def __init__(self, message=''):
+ super(APIPermissionError, self).__init__('permission:forbidden', 'permission', message)
\ No newline at end of file
diff --git a/www/app.py b/www/app.py
index 26439e5..ef8003e 100644
--- a/www/app.py
+++ b/www/app.py
@@ -1,3 +1,6 @@
+
+from config import configs
+
import logging; logging.basicConfig(level=logging.INFO)
# logging是Python 的日志记录工具,level表示设置根记录器级别去指定 level.
# 日志级别:
@@ -7,25 +10,153 @@ import logging; logging.basicConfig(level=logging.INFO)
# INFO 20
# DEBUG 10
# NOTSET 0
-
+# __auther__ = 'lzj'
# 导入 logging 模块并使用';'对其全局配置
# basicConfig 配置了 level 信息,level 配置为 INFO 信息,即只输出 INFO 级别的信息
+import asyncio, os, json, time
+from datetime import datetime
-import asyncio
from aiohttp import web
+from jinja2 import Environment, FileSystemLoader
+import orm
+from coroweb import add_routes, add_static
-async def index(request):
- return web.Response(body=b'
fuck
', headers={'content-type': 'text/html'})
+def init_jinja2(app, **kw):
+ logging.info('init jinja2...')
+ options = dict(
+ autoescape = kw.get('autoescape', True),
+ block_start_string = kw.get('block_start_string', '{%'),
+ block_end_string = kw.get('block_end_string', '%}'),
+ variable_start_string = kw.get('variable_start_string', '{{'),
+ variable_end_string = kw.get('variable_end_string', '}}'),
+ auto_reload = kw.get('auto_reload', True)
+ )
+ path = kw.get('path', None)
+ if path is None:
+ path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'templates')
+ logging.info('set jinja2 template path: %s' % path)
+ env = Environment(loader=FileSystemLoader(path), **options)
+ filters = kw.get('filters', None)
+ if filters is not None:
+ for name, f in filters.items():
+ env.filters[name] = f
+ app['__templating__'] = env
+async def logger_factory(app, handler):
+ async def logger(request):
+ logging.info('Request: %s %s' % (request.method, request.path))
+ # await asyncio.sleep(0.3)
+ return (await handler(request))
+ return logger
-def init():
- # 创建 web.Application ,web app的骨架
- app = web.Application()
- app.router.add_get('/', index)
- web.run_app(app, host='127.0.0.1', port=9000)
+async def data_factory(app, handler):
+ async def parse_data(request):
+ if request.method == 'POST':
+ if request.content_type.startswith('application/json'):
+ request.__data__ = await request.json()
+ logging.info('request json: %s' % str(request.__data__))
+ elif request.content_type.startswith('application/x-www-form-urlencoded'):
+ request.__data__ = await request.post()
+ logging.info('request form: %s' % str(request.__data__))
+ return (await handler(request))
+ return parse_data
+async def response_factory(app, handler):
+ async def response(request):
+ logging.info('Response handler...')
+ r = await handler(request)
+ if isinstance(r, web.StreamResponse):
+ return r
+ if isinstance(r, bytes):
+ resp = web.Response(body=r)
+ resp.content_type = 'application/octet-stream'
+ return resp
+ if isinstance(r, str):
+ if r.startswith('redirect:'):
+ return web.HTTPFound(r[9:])
+ resp = web.Response(body=r.encode('utf-8'))
+ resp.content_type = 'text/html;charset=utf-8'
+ return resp
+ if isinstance(r, dict):
+ template = r.get('__template__')
+ if template is None:
+ resp = web.Response(body=json.dumps(r, ensure_ascii=False, default=lambda o: o.__dict__).encode('utf-8'))
+ resp.content_type = 'application/json;charset=utf-8'
+ return resp
+ else:
+ resp = web.Response(body=app['__templating__'].get_template(template).render(**r).encode('utf-8'))
+ resp.content_type = 'text/html;charset=utf-8'
+ return resp
+ if isinstance(r, int) and r >= 100 and r < 600:
+ return web.Response(text=r)
+ if isinstance(r, tuple) and len(r) == 2:
+ t, m = r
+ if isinstance(t, int) and t >= 100 and t < 600:
+ return web.Response(text=t,body=str(m))
+ # default:
+ resp = web.Response(body=str(r).encode('utf-8'))
+ resp.content_type = 'text/plain;charset=utf-8'
+ return resp
+ return response
-if __name__ == '__main__':
- init()
+def datetime_filter(t):
+ delta = int(time.time() - t)
+ if delta < 60:
+ return u'1分钟前'
+ if delta < 3600:
+ return u'%s分钟前' % (delta // 60)
+ if delta < 86400:
+ return u'%s小时前' % (delta // 3600)
+ if delta < 604800:
+ return u'%s天前' % (delta // 86400)
+ dt = datetime.fromtimestamp(t)
+ return u'%s年%s月%s日' % (dt.year, dt.month, dt.day)
+
+# async def index(request):
+# return web.Response(body=b'....vv
', headers={'content-type': 'text/html'})
+# def init():
+# # 创建 web.Application ,web app的骨架
+# app = web.Application()
+# app.router.add_get('/', index)
+# web.run_app(app, host='127.0.0.1', port=9000)
+
+async def init(loop):
+ await orm.create_pool(loop=loop, host='127.0.0.1', port=3306, user='root', password='123456', db='demo')
+ app = web.Application(loop=loop, middlewares=[
+ logger_factory, response_factory
+ ])
+ init_jinja2(app, filters=dict(datetime=datetime_filter))
+ add_routes(app, 'handlers')
+ add_static(app)
+ srv = await loop.create_server(app.make_handler(), '127.0.0.1', 9001)
+ logging.info('server started at http://127.0.0.1:9001...')
+ return srv
+
+loop = asyncio.get_event_loop()
+loop.run_until_complete(init(loop))
+loop.run_forever()
+
+# 新版写法,会报错。
+# async def init_db(app):
+# # If on Linux, use another user instead of 'root'
+# await orm.create_pool(
+# host=configs.db.host,
+# port=configs.db.port,
+# user=configs.db.user,
+# password=configs.db.password,
+# db=configs.db.db
+#
+# )
+#
+#
+# app = web.Application(middlewares=[
+# logger_factory,
+# response_factory
+# ])
+# init_jinja2(app, filters=dict(datatime=datetime_filter))
+# add_routes(app, 'handlers')
+# add_static(app)
+# app.on_startup.append(init_db)
+# web.run_app(app, host='localhost', port=9000)
\ No newline at end of file
diff --git a/www/config.py b/www/config.py
new file mode 100644
index 0000000..04317ae
--- /dev/null
+++ b/www/config.py
@@ -0,0 +1,49 @@
+
+import config_default
+
+class Dict(dict):
+ '''
+ Simple dict but support access as x.y style.
+ '''
+ def __init__(self, names=(), values=(), **kw):
+ super(Dict, self).__init__(**kw)
+ # zip 可以把 names 和 values 合成为一个列表 ,这里为了方便给 self 赋值
+ for k, v in zip(names, values):
+ self[k] = v
+
+ def __getattr__(self, key):
+ try:
+ return self[key]
+ except KeyError:
+ raise AttributeError(r"'Dict' object has no attribute '%s'" % key)
+
+ def __setattr__(self, key, value):
+ self[key] = value
+
+def merge(defaults, override):
+ r = {}
+ for k, v in defaults.items():
+ if k in override:
+ if isinstance(v, dict):
+ r[k] = merge(v, override[k])
+ else:
+ r[k] = override[k]
+ else:
+ r[k] = v
+ return r
+
+def toDict(d):
+ D = Dict()
+ for k, v in d.items():
+ D[k] = toDict(v) if isinstance(v, dict) else v
+ return D
+
+configs = config_default.configs
+
+try:
+ import config_override
+ configs = merge(configs, config_override.configs)
+except ImportError:
+ pass
+
+configs = toDict(configs)
\ No newline at end of file
diff --git a/www/config_default.py b/www/config_default.py
new file mode 100644
index 0000000..8e03338
--- /dev/null
+++ b/www/config_default.py
@@ -0,0 +1,14 @@
+
+configs = {
+ 'debug': True,
+ 'db': {
+ 'host': '127.0.0.1',
+ 'port': 3306,
+ 'user': 'root',
+ 'password': '123456',
+ 'db': 'demo'
+ },
+ 'session': {
+ 'secret': 'demo'
+ }
+}
\ No newline at end of file
diff --git a/www/config_override.py b/www/config_override.py
new file mode 100644
index 0000000..5ac4d15
--- /dev/null
+++ b/www/config_override.py
@@ -0,0 +1,6 @@
+
+configs = {
+ 'db': {
+ 'host': '127.0.0.1'
+ }
+}
\ No newline at end of file
diff --git a/www/coroweb.py b/www/coroweb.py
new file mode 100644
index 0000000..944cd6b
--- /dev/null
+++ b/www/coroweb.py
@@ -0,0 +1,220 @@
+import asyncio, os, inspect, logging, functools
+
+from urllib import parse
+
+from aiohttp import web
+
+from apis import APIError
+
+def get(path):
+ # 函数通过@get()的装饰就附带了URL信息。
+ '''
+ Define decorator @get('/path')
+ '''
+ # wrapper装饰器:
+ # 装饰器本质上是一个Python函数,它可以让其他函数在不需要做任何代码变动的前提下增加额外功能,
+ # 装饰器的返回值也是一个函数对象。它经常用于有切面需求的场景,比如:插入日志、性能测试、事务处理、缓存、权限校验等场景。
+ # 装饰器是解决这类问题的绝佳设计,有了装饰器,我们就可以抽离出大量与函数功能本身无关的雷同代码并继续重用。
+ # 可变参数*args和关键字参数**kwargs,有了这两个参数,装饰器就可以用于任意目标函数
+ def decorator(func):
+ # 返回了一个partial对象,这个对象对update_wrapper进行了包装,固定了wrapped,assigned,updated三个参数。
+ # wraps本省就是一个装饰器,因为它返回的是一个“函数”即partial对象,这个对象接收函数作为参数,同时以函数作为返回值。
+ # 把wrapped函数的属性拷贝到wrapper函数中。
+ # wrapped是被装饰的原函数
+ # wrapper是被装饰器装饰后的新函数。
+ @functools.wraps(func)
+ def wrapper(*args, **kw):
+ return func(*args, **kw)
+ wrapper.__method__ = 'GET'
+ wrapper.__route__ = path
+ return wrapper
+ return decorator
+
+def post(path):
+
+ '''
+ Define decorator @post('/path')
+ '''
+ def decorator(func):
+ @functools.wraps(func)
+ def wrapper(*args, **kw):
+ return func(*args, **kw)
+ wrapper.__method__ = 'POST'
+ wrapper.__route__ = path
+ return wrapper
+ return decorator
+
+
+def get_required_kw_args(fn):
+ args = []
+ params = inspect.signature(fn).parameters
+ for name, param in params.items():
+ if param.kind == inspect.Parameter.KEYWORD_ONLY and param.default == inspect.Parameter.empty:
+ # 在 args 里加上仅包含关键字(keyword)的参数, 且不包括默认值, 然后返回 args
+ args.append(name)
+ return tuple(args)
+ # 所以这个函数的作用和名称一样, 得到需要的关键字参数, 下面同理
+
+def get_named_kw_args(fn):
+ args = []
+ params = inspect.signature(fn).parameters
+ for name, param in params.items():
+ if param.kind == inspect.Parameter.KEYWORD_ONLY:
+ args.append(name)
+ return tuple(args)
+
+def has_named_kw_args(fn):
+ params = inspect.signature(fn).parameters
+ for _, param in params.items():
+ if param.kind == inspect.Parameter.KEYWORD_ONLY:
+ return True
+
+def has_var_kw_arg(fn):
+ params = inspect.signature(fn).parameters
+ for _, param in params.items():
+ if param.kind == inspect.Parameter.VAR_KEYWORD:
+ return True
+
+def has_request_arg(fn):
+ sig = inspect.signature(fn)
+ params = sig.parameters
+ found = False
+ for name, param in params.items():
+ if name == 'request':
+ found = True
+ continue
+ if found and (param.kind != inspect.Parameter.VAR_POSITIONAL and param.kind != inspect.Parameter.KEYWORD_ONLY and param.kind != inspect.Parameter.VAR_KEYWORD):
+ raise ValueError('request parameter must be the last named parameter in function: %s%s' % (fn.__name__, str(sig)))
+ return found
+
+# RequestHandler目的就是从URL函数中分析其需要接收的参数,从request中获取必要的参数
+# 调用URL函数,然后把结果转换为web.Response对象
+class RequestHandler(object):
+ def __init__(self, app, fn):
+ self._app = app
+ self._func = fn
+ self._has_request_arg = has_request_arg(fn)
+ self._has_var_kw_arg = has_var_kw_arg(fn)
+ self._has_named_kw_args = has_named_kw_args(fn)
+ self._named_kw_args = get_named_kw_args(fn)
+ self._required_kw_args = get_required_kw_args(fn)
+
+ async def __call__(self, request):
+ kw = None
+ if self._has_var_kw_arg or self._has_named_kw_args or self._required_kw_args:
+ # 只要获取到了var 姓名 或者需要的关键字之一,就判断是不是post请求
+ if request.method == 'POST':
+ # 如果没有内容类型 text/html等之类的参数
+ if not request.content_type:
+ return web.HTTPBadRequest(reason='Missing Content-Type.')
+ ct = request.content_type.lower() #全转小写
+ # 如果内容类型是以json开头(json文件)
+ if ct.startswith('application/json'):
+ params = await request.json()
+ # inspect库中的检查是否是对象的方法
+ if not isinstance(params, dict):
+ return web.HTTPBadRequest(reason='JSON body must be object.')
+ kw = params
+ #判断内容类型开头是不是原生表单或者表单请求
+ elif ct.startswith('application/x-www-form-urlencoded') or ct.startswith('multipart/form-data'):
+ params = await request.post()
+ kw = dict(**params)
+ else:
+ # 不支持的类型
+ return web.HTTPBadRequest(reason='Unsupported Content-Type: %s' % request.content_type)
+ # 判断是不是get请求
+ if request.method == 'GET':
+ qs = request.query_string
+ if qs:
+ kw = dict()
+ for k, v in parse.parse_qs(qs, True).items():
+ kw[k] = v[0]
+ # 都没有获取到任何信息
+ if kw is None:
+ kw = dict(**request.match_info)
+ else:
+ if not self._has_var_kw_arg and self._named_kw_args:
+ # remove all unamed kw:
+ copy = dict()
+ for name in self._named_kw_args:
+ if name in kw:
+ copy[name] = kw[name]
+ kw = copy
+ # check named arg:
+ for k, v in request.match_info.items():
+ if k in kw:
+ logging.warning('Duplicate arg name in named arg and kw args: %s' % k)
+ kw[k] = v
+ if self._has_request_arg:
+ kw['request'] = request
+ # check required kw:
+ if self._required_kw_args:
+ for name in self._required_kw_args:
+ if not name in kw:
+ return web.HTTPBadRequest(reason='Missing argument: %s' % name)
+ logging.info('call with args: %s' % str(kw))
+ try:
+ r = await self._func(**kw)
+ return r
+ except APIError as e:
+ return dict(error=e.error, data=e.data, message=e.message)
+ return RequestHandler.__call__()
+
+def add_static(app):
+ path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'static')
+ app.router.add_static('/static/', path)
+ logging.info('add static %s => %s' % ('/static/', path))
+
+
+# 改变静态网址下路径
+def add_static(app):
+ path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'static')
+ app.router.add_static('/static/', path)
+ logging.info('add static %s => %s' % ('/static/', path))
+
+# add_route函数,用来注册一个URL处理函数
+def add_route(app, fn):
+ # getattr() 函数用于返回一个对象属性值。
+ # 类型
+ # 路径
+ method = getattr(fn, '__method__', None)
+ path = getattr(fn, '__route__', None)
+ if path is None or method is None:
+ raise ValueError('@get or @post not defined in %s.' % str(fn))
+ # iscoroutinefunction():Return True if the object is a 协程函数
+ # isgeneratorfunction():Return True if the object is a Python 生成器函数
+ if not asyncio.iscoroutinefunction(fn) and not inspect.isgeneratorfunction(fn):
+ # asyncio.coroutine():
+ # 用来标记基于生成器的协程的装饰器。
+ # 此装饰器使得旧式的基于生成器的协程能与 async/await 代码相兼容
+ # 此装饰器不应该被用于 async def 协程
+ fn = asyncio.coroutine(fn)
+ #日志内容,inspect.signature(fn).parameters获取函数参数的参数名,参数的属性,参数的默认值
+ logging.info('add route %s %s => %s(%s)' % (method, path, fn.__name__, ', '.join(inspect.signature(fn).parameters.keys())))
+ app.router.add_route(method, path, RequestHandler(app, fn))
+
+# 把很多次add_route()注册的调用
+def add_routes(app, module_name):
+ # Python rfind() 返回字符串最后一次出现的位置,如果没有匹配项则返回 -1。
+ n = module_name.rfind('.')#对应python文件存为handles,使用rfind找到handles.py文件
+ if n == (-1):#如果没有匹配项,返回(-1)
+ # __import__() 函数用于动态加载类和函数 。
+ # 如果一个模块经常变化就可以使用 __import__() 来动态载入。
+ # globals() 函数会以字典类型返回当前位置的全部全局变量。
+ # locals() 函数会以字典类型返回当前位置的全部局部变量。
+ mod = __import__(module_name, globals(), locals())
+ else:#找到了,返回字符串出现的最后一次位置。
+ name = module_name[n+1:]
+ mod = getattr(__import__(module_name[:n], globals(), locals(), [name]), name)
+ for attr in dir(mod):
+ if attr.startswith('_'):
+ continue
+ fn = getattr(mod, attr)
+ # callable() 函数用于检查一个对象是否是可调用的。如果返回 True,object 仍然可能调用失败;但如果返回 False,调用对象 object 绝对不会成功。
+ # 对于函数、方法、lambda 函式、 类以及实现了 __call__ 方法的类实例, 它都返回 True。
+ if callable(fn):#如果找到了对应的处理方法。
+ method = getattr(fn, '__method__', None)
+ path = getattr(fn, '__route__', None)
+ if method and path:
+ # logging.info('add rosute ')
+ add_route(app, fn)
\ No newline at end of file
diff --git a/www/handlers.py b/www/handlers.py
new file mode 100644
index 0000000..7eb1f8b
--- /dev/null
+++ b/www/handlers.py
@@ -0,0 +1,16 @@
+ # url handlers
+
+import re, time, json, logging, hashlib, base64, asyncio
+
+from coroweb import get, post
+
+from models import User, Comment, Blog, next_id
+
+@get('/')
+async def index(request):
+
+ users = await User.findAll()
+ return {
+ '__template__': 'test.html',
+ 'users': users
+ }
\ No newline at end of file
diff --git a/www/models.py b/www/models.py
new file mode 100644
index 0000000..bfd0368
--- /dev/null
+++ b/www/models.py
@@ -0,0 +1,48 @@
+
+import time, uuid
+
+from orm import Model, StringField, BooleanField, FloatField, TextField
+
+def next_id():
+ # uuid.uuid4() 可以生成一个随机的 UUID , 目的是区别不同事务(大概)
+ # hex 可以把自身返回为一个16进制整数 , 所以这个函数就是生成各种 id ,里面还包含时间
+ return '%015d%s000' % (int(time.time() * 1000), uuid.uuid4().hex)
+
+class User(Model):
+ __table__ = 'users'
+ # varchar 为 MySQL 里的数据类型
+ id = StringField(primary_key=True, default=next_id, ddl='varchar(50)')
+ email = StringField(ddl='varchar(50)')
+ password = StringField(ddl='varchar(50)') #有改动passwd改成password
+ # admin = BooleanField()
+ name = StringField(ddl='varchar(50)')
+ image = StringField(ddl='varchar(500)')
+ # created_at = FloatField(default=time.time)
+ # time.time 可以设置当前日期和时间, 把日期和时间储存为 float 类型 , 记录到 create_at 里
+
+class Blog(Model):
+ __table__ = 'blogs'
+
+ id = StringField(primary_key=True, default=next_id, ddl='varchar(50)')
+ user_id = StringField(ddl='varchar(50)')
+ user_name = StringField(ddl='varchar(50)')
+ user_image = StringField(ddl='varchar(500)')
+ name = StringField(ddl='varchar(50)')
+ summary = StringField(ddl='varchar(200)')
+ content = TextField()
+ created_at = FloatField(default=time.time)
+
+class Comment(Model):
+ __table__ = 'comments'
+
+ id = StringField(primary_key=True, default=next_id, ddl='varchar(50)')
+ blog_id = StringField(ddl='varchar(50)')
+ user_id = StringField(ddl='varchar(50)')
+ user_name = StringField(ddl='varchar(50)')
+ user_image = StringField(ddl='varchar(500)')
+ content = TextField()
+ created_at = FloatField(default=time.time)
+
+# 这些属性可以按照自己的需要进行增减
+
+# 之后对数据库的测试在 www 文件夹的 test_sql.py 里
\ No newline at end of file
diff --git a/www/orm.py b/www/orm.py
new file mode 100644
index 0000000..ed64743
--- /dev/null
+++ b/www/orm.py
@@ -0,0 +1,281 @@
+
+import asyncio, logging, aiomysql
+
+# 创建基本日志函数
+def log(sql, args=()):
+ logging.info('SQL: %s' % sql)
+
+# 创建连接池函数
+async def create_pool(loop, **kw):
+ logging.info('create database connection pool...')
+ # 声明 __pool 为全局变量
+ global __pool
+ # 使用这些基本参数来创建连接池
+ # await 和 async 是联动的(异步IO)
+ # 连接池是一种标准技术,用于在内存中维护长时间运行的连接,以便有效地重用,
+ # 并未应用程序可能同时使用的连接总数提供管理。特别是对于服务器端Web应用程序,
+ # 连接池是内存中维护活动数据库连接池的标准方法,这些活动数据库连接在请求之间重复使用。
+ # 使用这些基本参数来创建连接池
+ # await 和 async 是联动的(异步IO)
+ __pool = await aiomysql.create_pool(
+ host=kw.get('host', 'localhost'),
+ port=kw.get('port', 3306),
+ user=kw['user'],
+ password=kw['password'],
+ db=kw['db'],
+ charset=kw.get('charset', 'utf8'),
+ autocommit=kw.get('autocommit', True),
+ maxsize=kw.get('maxsize', 10),
+ minsize=kw.get('minsize', 1),
+ loop=loop
+ )
+
+async def select(sql, args, size=None):
+ log(sql, args)
+ global __pool
+
+
+ # 防止多个程序同时执行,达到异步效果
+ with (await __pool) as conn:
+
+ # 'aiomysql.DictCursor'要求返回字典格式
+ cur = await conn.cursor(aiomysql.DictCursor)
+ # cursor 游标实例可以调用 execute 来执行一条单独的 SQL 语句
+ await cur.execute(sql.replace('?', '%s'), args or())
+ # size 为空时为 False,上面定义了初始值为 None ,具体得看传入的参数有没有定义 size
+ if size:
+ # fetchmany 可以获取行数为 size 的多行查询结果集,返回一个列表
+ rs = await cur.fetchmany(size)
+ else:
+ # fetchall 可以获取一个查询结果的所有(剩余)行,返回一个列表
+ rs = await cur.fetchall()
+ # close() ,立即关闭 cursor ,从这一时刻起该 cursor 将不再可用
+ await cur.close()
+ # 日志:提示返回了多少行
+ logging.info('rows returned: %s' % len(rs))
+ # select 函数给我们从 SQL 返回了一个列表
+ return rs
+
+# execute :执行
+async def execute(sql, args):
+ log(sql)
+ global __pool
+ with (await __pool) as conn:
+ try:
+ cur = await conn.cursor()
+ await cur.execute(sql.replace('?', '%s'),args)
+ # rowcount 获取行数,应该表示的是该函数影响的行数
+ affected = cur.rowcount
+ await cur.close()
+ except BaseException as _:
+ # except BaseException as e:
+ # 将错误抛出,BaseEXception 是异常不用管
+ raise
+ # 返回行数
+ return affected
+
+
+
+def create_args_string(num):
+ L = []
+ for _ in range(num):
+ L.append('?')
+ return ', '.join(L)
+
+# Model 是一个基类,所以先定义 ModelMetaclass ,再在定义 Model 时使用 metaclass 参数
+class ModelMetaclass(type):
+ # __new__()方法接收到的参数依次是:
+ # cls:当前准备创建的类的对象 class
+ # name:类的名字 str
+ # bases:类继承的父类集合 Tuple
+ # attrs:类的方法集合
+ def __new__(cls, name, bases, attrs):
+ # 排除 Model 类本身,返回它自己
+ if name=='Model':
+ return type.__new__(cls, name, bases, attrs)
+ # 获取 table 名称
+ tableName = attrs.get('__table__', None) or name
+ # 日志:找到名为 name 的 model
+ logging.info('found model: %s (table: %s)' % (name, tableName))
+ # 获取 所有的 Field 和主键名
+ mappings = dict()
+ fields = []
+ primaryKey = None
+ # attrs.items 取决于 __new__ 传入的 attrs 参数
+ for k,v in attrs.items():
+ # isinstance 函数:如果 v 和 Field 类型相同则返回 True ,不相同则 False
+ if isinstance(v, Field):
+ logging.info(' found mapping: %s ==> %s' % (k,v))
+ mappings[k] = v
+ # 这里的 v.primary_key 我理解为 :只要 primary_key 为 True 则这个 field 为主键
+ if v.primary_key:
+ # 找到主键,如果主键 primaryKey 有值时,返回一个错误
+ if primaryKey:
+ raise RuntimeError('Duplicate primary key for field: %s' % k)
+ # 然后直接给主键赋值
+ primaryKey = k
+ else:
+ # 没找到主键的话,直接在 fields 里加上 k
+ fields.append(k)
+ if not primaryKey:
+ # 如果主键为 None 就报错
+ raise RuntimeError('Primary key not found.')
+ for k in mappings.keys():
+ # pop :如果 key 存在于字典中则将其移除并返回其值,否则返回 default
+ attrs.pop(k)
+
+ escaped_fields = list(map(lambda f: '`%s`' % f, fields))
+ attrs['__mappings__'] = mappings # 保存属性和列的映射关系
+ attrs['__table__'] = tableName # table 名
+ attrs['__primary_key__'] = primaryKey # 主键属性名
+ attrs['__fields__'] = fields # 除主键外的属性名
+ # 构造默认的 SELECT, INSERT, UPDAT E和 DELETE 语句
+ attrs['__select__'] = 'select `%s`, %s from `%s`' % (primaryKey, ', '.join(escaped_fields), tableName)
+ attrs['__insert__'] = 'insert into `%s` (%s, `%s`) values (%s)' % (tableName, ', '.join(escaped_fields), primaryKey, create_args_string(len(escaped_fields) + 1))
+ attrs['__update__'] = 'update `%s` set %s where `%s`=?' % (tableName, ', '.join(map(lambda f: '`%s`=?' % (mappings.get(f).name or f), fields)), primaryKey)
+ attrs['__delete__'] = 'delete from `%s` where `%s`=?' % (tableName, primaryKey)
+ return type.__new__(cls, name, bases, attrs)
+
+# metaclass 参数提示 Model 要通过上面的 __new__ 来创建
+class Model(dict, metaclass=ModelMetaclass):
+ def __init__(self, **kw):
+ # super 用来引用父类? 引用了 ModelMetaclass ? super 文档:
+ super(Model, self).__init__(**kw)
+ # 返回参数为 key 的自身属性, 如果出错则报具体错误
+ def __getattr__(self, key):
+ try:
+ return self[key]
+ except KeyError:
+ raise AttributeError(r"'Model' object has no attribute '%s'" % key)
+ # 设置自身属性
+ def __setattr__(self, key, value):
+ self[key] = value
+ # 通过属性返回想要的值
+ def getValue(self, key):
+ return getattr(self, key, None)
+ #
+ def getValueOrDefault(self, key):
+ value = getattr(self, key, None)
+ if value is None:
+ # 如果 value 为 None,定位某个键; value 不为 None 就直接返回
+ field = self.__mappings__[key]
+ if field.default is not None:
+ # 如果 field.default 不是 None : 就把它赋值给 value
+ value = field.default() if callable(field.default) else field.default
+ logging.debug('using default value for %s: %s' % (key,str(value)))
+ setattr(self, key, value)
+ return value
+
+ # *** 往 Model 类添加 class 方法,就可以让所有子类调用 class 方法
+ @classmethod
+ async def findAll(cls, where=None, args=None, **kw):
+ ## find objects by where clause
+ sql = [cls.__select__]
+ # where 默认值为 None
+ # 如果 where 有值就在 sql 加上字符串 'where' 和 变量 where
+ if where:
+ sql.append('where')
+ sql.append(where)
+ if args is None:
+ # args 默认值为 None
+ # 如果 findAll 函数未传入有效的 where 参数,则将 '[]' 传入 args
+ args = []
+
+ orderBy = kw.get('orderBy', None)
+ if orderBy:
+ # get 可以返回 orderBy 的值,如果失败就返回 None ,这样失败也不会出错
+ # oederBy 有值时给 sql 加上它,为空值时什么也不干
+ sql.append('order by')
+ sql.append(orderBy)
+ # 开头和上面 orderBy 类似
+ limit = kw.get('limit', None)
+ if limit is not None:
+ sql.append('limit')
+ if isinstance(limit, int):
+ # 如果 limit 为整数
+ sql.append('?')
+ args.append(limit)
+ elif isinstance(limit, tuple) and len(limit) == 2:
+ # 如果 limit 是元组且里面只有两个元素
+ sql.append('?, ?')
+ # extend 把 limit 加到末尾
+ args.extend(limit)
+ else:
+ raise ValueError('Invalid limit value: %s' % str(limit))
+ rs = await select(' '.join(sql), args)
+ # 返回选择的列表里的所有值 ,完成 findAll 函数
+ return [cls(**r) for r in rs]
+
+ @classmethod
+ async def findNumber(cls, selectField, where=None, args=None):
+ ## find number by select and where
+ #找到选中的数及其位置
+ sql = ['select %s _num_ from `%s`' % (selectField, cls.__table__)]
+ if where:
+ sql.append('where')
+ sql.append(where)
+ rs = await select(' '.join(sql), args, 1)
+ if len(rs) == 0:
+ # 如果 rs 内无元素,返回 None ;有元素就返回某个数
+ return None
+ return rs[0]['_num_']
+
+ @classmethod
+ async def find(cls, pk):
+ ## find object by primary key
+ # 通过主键找对象
+ rs = await select('%s where `%s`=?' % (cls.__select__, cls.__primary_key__), [pk], 1)
+ if len(rs) == 0:
+ return None
+ return cls(**rs[0])
+
+ # *** 往 Model 类添加实例方法,就可以让所有子类调用实例方法
+ async def save(self):
+ args = list(map(self.getValueOrDefault, self.__fields__))
+ args.append(self.getValueOrDefault(self.__primary_key__))
+ rows = await execute(self.__insert__, args)
+ if rows != 1:
+ logging.warning('failed to insert record: affected rows: %s' % rows)
+
+ async def update(self):
+ args = list(map(self.getValue, self.__fields__))
+ args.append(self.getValue(self.__primary_key__))
+ rows = await execute(self.__update__, args)
+ if rows != 1:
+ logging.warning('failed to update by primary key: affected rows: %s' % rows)
+
+ async def remove(self):
+ args = [self.getValue(self.__primary_key__)]
+ rows = await execute(self.__delete__, args)
+ if rows != 1:
+ logging.warning('failed to remove by primary key: affected rows: %s' % rows)
+
+# 定义 Field
+class Field(object):
+ def __init__(self, name, column_type, primary_key, default):
+ self.name = name
+ self.column_type = column_type
+ self.primary_key = primary_key
+ self.default = default
+ def __str__(self):
+ return '<%s, %s:%s>' % (self.__class__.__name__, self.column_type, self.name)
+# 定义 Field 子类及其子类的默认值
+class StringField(Field):
+ def __init__(self, name=None, primary_key=False, default=None, ddl='varchar(100)'):
+ super().__init__(name, ddl, primary_key, default)
+
+class BooleanField(Field):
+ def __init__(self, name=None, default=False):
+ super().__init__(name, 'boolean', False, default)
+
+class IntegerField(Field):
+ def __init__(self, name=None, primary_key=False, default=0):
+ super().__init__(name, 'bigint', primary_key, default)
+
+class FloatField(Field):
+ def __init__(self, name=None, primary_key=False, default=0):
+ super().__init__(name, 'real', primary_key,default)
+
+class TextField(Field):
+ def __init__(self, name=None, default=None):
+ super().__init__(name, 'text', False, default)
diff --git a/www/templates/test.html b/www/templates/test.html
new file mode 100644
index 0000000..1e9b5b2
--- /dev/null
+++ b/www/templates/test.html
@@ -0,0 +1,13 @@
+
+
+
+
+ Test users - Moe Python Webapp
+
+
+ FUCK
+ {% for u in users %}
+ {{ u.name }} / {{ u.email }} /{{u.password}}
+ {% endfor %}
+
+
\ No newline at end of file
diff --git a/www/test.py b/www/test.py
new file mode 100644
index 0000000..dc53718
--- /dev/null
+++ b/www/test.py
@@ -0,0 +1,22 @@
+import asyncio
+import orm
+from models import User
+
+
+async def test(loop):
+ global i
+ i=1
+ await orm.create_pool(user='root', password='123456', db='demo',loop=loop)
+ a = User(name='Administrator', email='admin@example.com', password='123456', image='about:blank',id=i)
+ i+=1
+ x = User(name='gjj', email='gjj@example.com', password='123456', image='about:blank',id=i)
+ i += 1
+ t = User(name='roro', email='roro@example.com', password='123456789', image='about:blank',id=i)
+ await a.save()
+ await x.save()
+ await t.save()
+
+
+loop = asyncio.get_event_loop()
+loop.run_until_complete(test(loop))
+