├── .gitignore ├── README.md ├── aurora ├── __init__.py ├── config │ ├── __init__.py │ └── settings.py ├── data │ └── user.sql ├── db.py ├── examples │ ├── __init__.py │ ├── sanic_asyncpg.py │ └── user_orm.py ├── models.py ├── server.py ├── test │ ├── __init__.py │ ├── test_pool.py │ └── test_pool_server.py ├── util │ ├── __init__.py │ └── sanic_jinja.py ├── view.py └── webContent │ ├── static │ ├── css │ │ ├── header.css │ │ ├── index.css │ │ ├── login.css │ │ └── style.css │ └── js │ │ └── message.js │ └── templates │ ├── 404.html │ ├── base │ └── base.html │ ├── entry.html │ ├── index.html │ ├── login.html │ └── modules │ └── head.html └── requirements.txt /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | local_settings.py 55 | 56 | # Flask stuff: 57 | instance/ 58 | .webassets-cache 59 | 60 | # Scrapy stuff: 61 | .scrapy 62 | 63 | # Sphinx documentation 64 | docs/_build/ 65 | 66 | # PyBuilder 67 | target/ 68 | 69 | # IPython Notebook 70 | .ipynb_checkpoints 71 | 72 | # pyenv 73 | .python-version 74 | 75 | # celery beat schedule file 76 | celerybeat-schedule 77 | 78 | # dotenv 79 | .env 80 | 81 | # virtualenv 82 | venv/ 83 | ENV/ 84 | 85 | # Spyder project settings 86 | .spyderproject 87 | 88 | # Rope project settings 89 | .ropeproject 90 | 91 | 92 | 93 | .vscode/ 94 | 95 | .cache/ 96 | 97 | .idea/ 98 | 99 | */__pycache__/ 100 | 101 | local_settings.py 102 | 103 | 104 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # Sanic_demo 3 | 4 | This Project based Sanic 5 | 6 | 7 | 8 | ### requirements 9 | 10 | - uvloop 11 | - sanic 12 | - asyncpg 13 | - jinja2 14 | 15 | 16 | -------------------------------------------------------------------------------- /aurora/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mugbya/aurora/8ecf184afdbeb511f19f83940aa4bd186693a18c/aurora/__init__.py -------------------------------------------------------------------------------- /aurora/config/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mugbya/aurora/8ecf184afdbeb511f19f83940aa4bd186693a18c/aurora/config/__init__.py -------------------------------------------------------------------------------- /aurora/config/settings.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | project settings for Aurora. 4 | 5 | """ 6 | import os 7 | import logging 8 | from logging.config import dictConfig 9 | 10 | # BASE_DIR = os.path.basename(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) 11 | PACKAGE_NAME = os.path.basename(os.getcwd()) 12 | 13 | STATIC_URL = 'webContent/static' 14 | TEMPLATES = "webContent/templates" 15 | 16 | DEBUG = False 17 | 18 | workers = 4 19 | 20 | PORT = 80 21 | 22 | DB_CONFIG = { 23 | 'host': '', 24 | 'user': '', 25 | 'password': '', 26 | 'port': '', 27 | 'database': '' 28 | } 29 | 30 | ''' 31 | 日志集成器 32 | ''' 33 | # from raven import Client 34 | # client = Client('https://******@sentry.io/141953') 35 | 36 | 37 | ''' 38 | python自带日志 39 | ''' 40 | logging_config = dict( 41 | version=1, 42 | formatters={ 43 | 'default': { 44 | 'format': '%(asctime)s %(levelname)-8s %(name)-15s %(message)s' 45 | } 46 | }, 47 | filter={ 48 | }, 49 | handlers={ 50 | 'console': { 51 | 'class': 'logging.StreamHandler', 52 | 'formatter': 'default', 53 | 'level': logging.INFO 54 | }, 55 | 'file': { 56 | 'class': 'logging.FileHandler', 57 | 'filename': '../log/all.log', 58 | 'formatter': 'default', 59 | 'level': logging.INFO 60 | }, 61 | }, 62 | loggers={ 63 | 'sanic': { 64 | 'handlers': ['file'], 65 | 'level': logging.INFO, 66 | "encoding": "utf8" 67 | }, 68 | 'db': { 69 | 'handlers': ['file'], 70 | 'level': logging.INFO, 71 | "encoding": "utf8" 72 | }, 73 | 'view': { 74 | 'handlers': ['file'], 75 | 'level': logging.INFO, 76 | "encoding": "utf8" 77 | }, 78 | } 79 | ) 80 | 81 | dictConfig(logging_config) 82 | 83 | try: 84 | from .local_settings import * 85 | except ImportError: 86 | pass 87 | -------------------------------------------------------------------------------- /aurora/data/user.sql: -------------------------------------------------------------------------------- 1 | CREATE DATABASE ull_user 2 | 3 | CREATE TABLE "users" 4 | ( 5 | id SERIAL PRIMARY KEY, 6 | username VARCHAR(30), 7 | nickname VARCHAR(30), 8 | password VARCHAR(50), 9 | email VARCHAR(100) 10 | ); 11 | 12 | 13 | 14 | INSERT INTO users (username, nickname) VALUES ('a', 'a'); 15 | INSERT INTO users (username, nickname) VALUES ('b', 'b'); 16 | INSERT INTO users (username, nickname) VALUES ('c', 'c'); 17 | INSERT INTO users (username, nickname) VALUES ('d', 'd'); 18 | INSERT INTO users (username, nickname) VALUES ('e', 'e'); 19 | INSERT INTO users (username, nickname) VALUES ('f', 'f'); 20 | INSERT INTO users (username, nickname) VALUES ('g', 'g'); 21 | INSERT INTO users (username, nickname) VALUES ('h', 'h'); 22 | INSERT INTO users (username, nickname) VALUES ('i', 'i'); 23 | INSERT INTO users (username, nickname) VALUES ('j', 'j'); 24 | INSERT INTO users (username, nickname) VALUES ('k', 'k'); 25 | INSERT INTO users (username, nickname) VALUES ('l', 'l'); 26 | INSERT INTO users (username, nickname) VALUES ('m', 'm'); 27 | INSERT INTO users (username, nickname) VALUES ('n', 'n'); 28 | -------------------------------------------------------------------------------- /aurora/db.py: -------------------------------------------------------------------------------- 1 | import asyncpg 2 | from aurora.config import settings 3 | 4 | import logging 5 | 6 | logger = logging.getLogger(__name__) 7 | 8 | ''' 9 | asyncpg 封装 + orm 10 | ''' 11 | 12 | 13 | def log(sql, args=()): 14 | logger.info('SQL: %s' % sql) 15 | 16 | 17 | async def setup_connection(app, loop): 18 | ''' 19 | 创建数据库连接池 20 | 21 | 监听数据库连接数变化 22 | select * from pg_stat_activity; 23 | 24 | :param app: 25 | :param loop: 26 | :return: 27 | ''' 28 | global _pool 29 | logger.info('create database connection pool...') 30 | _pool = await asyncpg.create_pool(**settings.DB_CONFIG) 31 | return _pool 32 | 33 | 34 | async def close_connection(app, loop): 35 | ''' 36 | :param app: 37 | :param loop: 38 | :return: 39 | ''' 40 | await _pool.close() 41 | logger.info('database pool died ') 42 | 43 | 44 | async def select(sql, *args, size=None): 45 | log(sql, args) 46 | async with _pool.acquire() as con: 47 | rs = await con.fetch(sql, *args) 48 | logger.info('rows returned: %s' % len(rs)) 49 | return rs 50 | 51 | 52 | async def execute(sql, *args, autocommit=True): 53 | log(sql) 54 | async with _pool.acquire() as con: 55 | rs = await con.execute(sql, *args) 56 | return rs 57 | 58 | 59 | def create_args_string(num): 60 | L = [] 61 | for n in range(num): 62 | L.append('$' + str(n+1)) 63 | return ', '.join(L) 64 | 65 | 66 | class Field(object): 67 | def __init__(self, name, column_type, primary_key, default): 68 | self.name = name 69 | self.column_type = column_type 70 | self.primary_key = primary_key 71 | self.default = default 72 | 73 | def __str__(self): 74 | return '<%s, %s:%s>' % (self.__class__.__name__, self.column_type, self.name) 75 | 76 | 77 | class StringField(Field): 78 | def __init__(self, name=None, primary_key=False, default=None, ddl='varchar(100)'): 79 | super().__init__(name, ddl, primary_key, default) 80 | 81 | 82 | class BooleanField(Field): 83 | def __init__(self, name=None, default=False): 84 | super().__init__(name, 'boolean', False, default) 85 | 86 | 87 | class IntegerField(Field): 88 | def __init__(self, name=None, primary_key=False, default=0): 89 | super().__init__(name, 'bigint', primary_key, default) 90 | 91 | 92 | class FloatField(Field): 93 | def __init__(self, name=None, primary_key=False, default=0.0): 94 | super().__init__(name, 'real', primary_key, default) 95 | 96 | 97 | class TextField(Field): 98 | def __init__(self, name=None, default=None): 99 | super().__init__(name, 'text', False, default) 100 | 101 | 102 | class ModelMetaclass(type): 103 | def __new__(cls, name, bases, attrs): 104 | if name == 'Model': 105 | return type.__new__(cls, name, bases, attrs) 106 | tableName = attrs.get('__table__', None) or name 107 | logging.info('found model: %s (table: %s)' % (name, tableName)) 108 | mappings = dict() 109 | fields = [] 110 | primaryKey = None 111 | for k, v in attrs.items(): 112 | if isinstance(v, Field): 113 | logging.info(' found mapping: %s ==> %s' % (k, v)) 114 | mappings[k] = v 115 | if v.primary_key: 116 | # 找到主键: 117 | if primaryKey: 118 | raise Exception('Duplicate primary key for field: %s' % k) 119 | primaryKey = k 120 | else: 121 | fields.append(k) 122 | if not primaryKey: 123 | raise Exception('Primary key not found.') 124 | for k in mappings.keys(): 125 | attrs.pop(k) 126 | escaped_fields = list(map(lambda f: '%s' % f, fields)) 127 | attrs['__mappings__'] = mappings # 保存属性和列的映射关系 128 | attrs['__table__'] = tableName 129 | attrs['__primary_key__'] = primaryKey # 主键属性名 130 | attrs['__fields__'] = fields # 除主键外的属性名 131 | attrs['__select__'] = 'select %s, %s from %s' % (primaryKey, ', '.join(escaped_fields), tableName) 132 | attrs['__insert__'] = 'insert into %s (%s) values (%s)' % (tableName, ', '.join(escaped_fields), create_args_string(len(escaped_fields))) 133 | attrs['__update__'] = 'update %s set ' % (tableName, ) 134 | attrs['__delete__'] = 'delete from %s where %s=$1' % (tableName, primaryKey) 135 | return type.__new__(cls, name, bases, attrs) 136 | 137 | 138 | class Model(dict, metaclass=ModelMetaclass): 139 | def __init__(self, **kw): 140 | super(Model, self).__init__(**kw) 141 | 142 | def __getattr__(self, key): 143 | try: 144 | return self[key] 145 | except KeyError: 146 | raise AttributeError(r"'Model' object has no attribute '%s'" % key) 147 | 148 | def __setattr__(self, key, value): 149 | self[key] = value 150 | 151 | def get_primary_key(self, primary_val=None): 152 | ''' 153 | 获取主键值,并处理 154 | :return: 155 | ''' 156 | if not primary_val: 157 | primary_val = self.getValue(self.__primary_key__) 158 | 159 | # 处理主键类型 asyncpg 区分类型 160 | if isinstance(self.__mappings__.get(self.__primary_key__), IntegerField): 161 | return int(primary_val) 162 | else: 163 | return primary_val 164 | 165 | def getValue(self, key): 166 | ''' 167 | 获取属性值 168 | :param key: 169 | :return: 170 | ''' 171 | return getattr(self, key, None) 172 | 173 | def getValueOrDefault(self, key): 174 | value = getattr(self, key, None) 175 | if value is None: 176 | field = self.__mappings__[key] 177 | if field.default is not None: 178 | value = field.default() if callable(field.default) else field.default 179 | logging.debug('using default value for %s: %s' % (key, str(value))) 180 | setattr(self, key, value) 181 | return value 182 | 183 | @classmethod 184 | async def all(cls): 185 | ''' 186 | find all objects 187 | :return: 188 | ''' 189 | sql = [cls.__select__] 190 | rs = await select(' '.join(sql)) 191 | return [cls(**r) for r in rs] 192 | 193 | @classmethod 194 | async def filter(cls, *args, **kw): 195 | ''' 196 | find objects by where clause 197 | :param args: 198 | :param kw: Query parameters 199 | :return: 200 | ''' 201 | sql = [cls.__select__] 202 | if kw: 203 | sql.append('where') 204 | if not args: 205 | args = [] 206 | 207 | # 用占位符构造查询条件 208 | for index, key in enumerate(kw.keys()): 209 | if index == 0: 210 | sql.append(key + '=$1') 211 | args.append(kw.get(key)) 212 | else: 213 | index += 1 214 | sql.append(' and ' + key + '=$' + str(index)) 215 | args.append(kw.get(key)) 216 | 217 | rs = await select(' '.join(sql), *args) 218 | return [cls(**r) for r in rs] 219 | 220 | @classmethod 221 | async def get(cls, *args, **kw): 222 | ''' 223 | 返回对象 224 | :param args: 225 | :param kw: 226 | :return: 227 | ''' 228 | sql = [cls.__select__] 229 | if kw: 230 | sql.append('where') 231 | if not args: 232 | args = [] 233 | 234 | # 用占位符构造查询条件 235 | for index, key in enumerate(kw.keys()): 236 | index += 1 237 | sql.append(key + '=$' + str(index)) 238 | args.append(kw.get(key)) 239 | 240 | res = await select(' '.join(sql), *args) 241 | # TODO 构建对象出错 242 | return [type(cls.__name__, (Model, ), cls(**r)) for r in res] 243 | 244 | @classmethod 245 | async def findAll(cls, where=None, *args, **kw): 246 | ' find objects by where clause. ' 247 | sql = [cls.__select__] 248 | if where: 249 | sql.append('where') 250 | sql.append(where) 251 | if args is None: 252 | args = [] 253 | orderBy = kw.get('orderBy', None) 254 | if orderBy: 255 | sql.append('order by') 256 | sql.append(orderBy) 257 | limit = kw.get('limit', None) 258 | if limit is not None: 259 | sql.append('limit') 260 | if isinstance(limit, int): 261 | sql.append('?') 262 | args.append(limit) 263 | elif isinstance(limit, tuple) and len(limit) == 2: 264 | sql.append('?, ?') 265 | args.extend(limit) 266 | else: 267 | raise ValueError('Invalid limit value: %s' % str(limit)) 268 | rs = await select(' '.join(sql), *args) 269 | return [cls(**r) for r in rs] 270 | 271 | @classmethod 272 | async def findNumber(cls, selectField, where=None, args=None): 273 | ' find number by select and where. ' 274 | sql = ['select %s _num_ from `%s`' % (selectField, cls.__table__)] 275 | if where: 276 | sql.append('where') 277 | sql.append(where) 278 | rs = await select(' '.join(sql), args, 1) 279 | if len(rs) == 0: 280 | return None 281 | return rs[0]['_num_'] 282 | 283 | @classmethod 284 | async def find(cls, pk): 285 | ' find object by primary key. ' 286 | rs = await select('%s where `%s`=?' % (cls.__select__, cls.__primary_key__), [pk], 1) 287 | if len(rs) == 0: 288 | return None 289 | return cls(**rs[0]) 290 | 291 | async def save(self): 292 | args = list(map(self.getValueOrDefault, self.__fields__)) 293 | 294 | rows = await execute(self.__insert__, *args) 295 | if not rows: 296 | logging.warn('failed to insert record: affected rows: %s' % rows) 297 | return False 298 | else: 299 | return True 300 | 301 | async def update(self): 302 | try: 303 | primary_val = self.pop(self.__primary_key__) 304 | 305 | args = list(map(self.getValue, self.keys())) 306 | args.append(self.get_primary_key(primary_val=primary_val)) 307 | 308 | sql = self.__update__ 309 | index = 0 310 | for index, key in enumerate(self.keys()): 311 | index += 1 312 | sql += key + '=$' + str(index) + ', ' 313 | sql = sql[0:-2] + ' where id=$' + str(index+1) 314 | rows = await execute(sql, *args) 315 | 316 | if rows == 'UPDATE 0': 317 | raise Exception('record not exits') 318 | elif rows == 'UPDATE 1': 319 | return True 320 | return False 321 | except Exception as e: 322 | raise Exception(e) 323 | 324 | async def delete(self): 325 | args = [self.get_primary_key()] 326 | rows = await execute(self.__delete__, *args) 327 | if rows == 'DELETE 0': 328 | raise Exception('record not exits') 329 | elif rows == 'DELETE 1': 330 | return True 331 | return False 332 | 333 | 334 | -------------------------------------------------------------------------------- /aurora/examples/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mugbya/aurora/8ecf184afdbeb511f19f83940aa4bd186693a18c/aurora/examples/__init__.py -------------------------------------------------------------------------------- /aurora/examples/sanic_asyncpg.py: -------------------------------------------------------------------------------- 1 | from sanic.response import json 2 | from sanic import Blueprint 3 | from sanic import Sanic 4 | from aurora.config import settings 5 | 6 | from aurora.db import setup_connection, close_connection 7 | 8 | import logging 9 | 10 | logger = logging.getLogger() 11 | 12 | app = Sanic(__name__) 13 | bp = Blueprint('user_v2', url_prefix='/v2/user') 14 | 15 | ''' 16 | /v2/user 旨在 以原生sql实现业务需求 17 | 18 | ''' 19 | 20 | 21 | async def start_connection(app, loop): 22 | ''' 23 | 将数据库连接池放入blueprint 24 | :param app: 25 | :param loop: 26 | :return: 27 | ''' 28 | _pool = await setup_connection(app, loop) 29 | bp.pool = _pool 30 | 31 | 32 | @bp.route('/') 33 | async def index(request): 34 | ''' 35 | 获取所有用户列表 36 | :param request: 37 | :return: 38 | ''' 39 | try: 40 | async with bp.pool.acquire() as conn: 41 | stmt = await conn.prepare('''SELECT id, email FROM users ''') 42 | 43 | results = await stmt.fetch() 44 | 45 | obj_list = [dict(obj) for obj in results] 46 | return json(obj_list) 47 | except Exception as e: 48 | logger.error('index error', str(e)) 49 | return json({'msg': 'fail'}) 50 | 51 | 52 | @bp.route('//') 53 | async def get_user(request, username): 54 | ''' 55 | 以用户名获取指定用户对象 56 | :param request: 57 | :return: 58 | ''' 59 | try: 60 | async with bp.pool.acquire() as conn: 61 | # 使用格式化 62 | # sql = '''SELECT id, email FROM users WHERE nickname='{nickname}' '''.format(nickname=username, ) 63 | # stmt = await conn.prepare(sql) 64 | # results = await stmt.fetch() 65 | 66 | # 使用 asyncpg 提供的占位符 67 | sql = '''SELECT id, email FROM users WHERE nickname=$1 ''' 68 | stmt = await conn.prepare(sql) 69 | results = await stmt.fetch(username) 70 | 71 | obj_list = [dict(obj) for obj in results] 72 | return json(obj_list) 73 | except Exception as e: 74 | logger.error('get_user error', str(e)) 75 | return json({'msg': 'fail'}) 76 | 77 | 78 | @bp.post('/save/') 79 | async def save_user(request): 80 | ''' 81 | 保存user对象 82 | :param request: 83 | :return: 84 | ''' 85 | try: 86 | if request.form: 87 | username = request.parsed_form.get('username', '') 88 | nickname = request.parsed_form.get('nickname', '') 89 | password = request.parsed_form.get('password', '') 90 | email = request.parsed_form.get('email', '') 91 | 92 | async with bp.pool.acquire() as conn: 93 | 94 | # sql = '''INSERT INTO users (username, nickname, password, email) 95 | # VALUES ('{username}', '{nickname}', '{password}', '{email}') '''.format( 96 | # username=username, nickname=nickname, password=password, email=email) 97 | # result = await conn.execute(sql) 98 | 99 | sql = '''INSERT INTO users (username, nickname, password, email) VALUES ($1, $2, $3, $4) ''' 100 | result = await conn.execute(sql, username, nickname, password, email) 101 | if result: 102 | return json({'msg': 'ok'}) 103 | 104 | return json({'msg': 'fail'}) 105 | 106 | except Exception as e: 107 | logger.error('user save error', str(e)) 108 | return json({'msg': 'fail'}) 109 | 110 | 111 | if __name__ == "__main__": 112 | ''' 113 | sanic 启动时创建数据库连接池,服务正常结束时关闭连接池 114 | ''' 115 | app.blueprint(bp) 116 | app.run(host="0.0.0.0", port=settings.PORT, workers=settings.workers, debug=settings.DEBUG, 117 | after_start=start_connection, after_stop=close_connection) 118 | -------------------------------------------------------------------------------- /aurora/examples/user_orm.py: -------------------------------------------------------------------------------- 1 | from sanic.response import json 2 | from sanic import Blueprint 3 | from sanic import Sanic 4 | from aurora.models import User 5 | from aurora.config import settings 6 | 7 | from aurora.db import setup_connection, close_connection 8 | 9 | import logging 10 | logger = logging.getLogger() 11 | 12 | app = Sanic(__name__) 13 | bp = Blueprint('user', url_prefix='/v1/user') 14 | 15 | ''' 16 | /v1/user 旨在 以orm实现业务需求 17 | ''' 18 | 19 | 20 | @bp.route('/') 21 | async def index(request): 22 | ''' 23 | 获取所有用户列表 24 | :param request: 25 | :return: 26 | ''' 27 | try: 28 | obj_list = await User.all() 29 | return json(obj_list) 30 | except Exception as e: 31 | logger.error('index error', str(e)) 32 | return json({'msg': 'fail'}) 33 | 34 | 35 | @bp.route('//') 36 | async def get_user(request, username): 37 | ''' 38 | 以用户名获取指定用户对象 39 | :param request: 40 | :return: 41 | ''' 42 | try: 43 | user_list = await User.filter(nickname=username,) 44 | # user_list = await User.findAll('nickname=$1', username) 45 | 46 | return json(user_list) 47 | except Exception as e: 48 | logger.error('get_user error', str(e)) 49 | return json({'msg': 'fail'}) 50 | 51 | 52 | @bp.post('/save/') 53 | async def save_user(request): 54 | ''' 55 | 保存user对象 56 | :param request: 57 | :return: 58 | ''' 59 | try: 60 | if request.form: 61 | username = request.parsed_form.get('username', '') 62 | nickname = request.parsed_form.get('nickname', '') 63 | password = request.parsed_form.get('password', '') 64 | email = request.parsed_form.get('email', '') 65 | 66 | user = User(username=username, nickname=nickname, password=password, email=email) 67 | res = await user.save() 68 | 69 | if res: 70 | return json({'msg': 'ok'}) 71 | return json({'msg': 'fail'}) 72 | 73 | except Exception as e: 74 | logger.error('user save error', str(e)) 75 | return json({'msg': 'fail'}) 76 | 77 | 78 | @bp.post('/update//') 79 | async def update_user(request, id): 80 | ''' 81 | 更新user对象 82 | 83 | id = request.parsed_form.get('id', '') 84 | username = request.parsed_form.get('username', '') 85 | nickname = request.parsed_form.get('nickname', '') 86 | password = request.parsed_form.get('password', '') 87 | email = request.parsed_form.get('email', '') 88 | 89 | user = User(id=id, username=username, nickname=nickname, password=password, email=email) 90 | 91 | :param request: 92 | :param id: 93 | :return: 94 | ''' 95 | try: 96 | if request.form: 97 | # res = {'id': ['4'], 'email': ['em'], 'username': ['user'], 'nickname': ['ck'], 'password': ['pd']} 98 | res = {} 99 | for key in request.parsed_form.keys(): 100 | res.update({key: request.parsed_form.get(key)}) 101 | 102 | user = User(**res) 103 | await user.update() 104 | return json({'msg': 'ok'}) 105 | return json({'msg': 'fail'}) 106 | 107 | except Exception as e: 108 | logger.error('user save error', str(e)) 109 | return json({'msg': 'fail'}) 110 | 111 | 112 | @bp.post('/del//') 113 | async def del_user(request, id): 114 | ''' 115 | 删除user对象 116 | :param request: 117 | :param id: 118 | :return: 119 | ''' 120 | try: 121 | if request.form: 122 | user = User(id=id) 123 | await user.delete() 124 | return json({'msg': 'ok'}) 125 | return json({'msg': 'fail'}) 126 | 127 | except Exception as e: 128 | logger.error('user save error', str(e)) 129 | return json({'msg': 'fail'}) 130 | 131 | 132 | if __name__ == "__main__": 133 | ''' 134 | sanic 启动时创建数据库连接池,服务正常结束时关闭连接池 135 | ''' 136 | app.blueprint(bp) 137 | app.run(host="0.0.0.0", port=settings.PORT, workers=settings.workers, debug=settings.DEBUG, 138 | after_start=setup_connection, after_stop=close_connection) 139 | -------------------------------------------------------------------------------- /aurora/models.py: -------------------------------------------------------------------------------- 1 | from aurora.db import Model, StringField, BooleanField, FloatField, TextField, IntegerField 2 | 3 | 4 | class User(Model): 5 | __table__ = 'users' 6 | 7 | id = IntegerField(primary_key=True,) 8 | email = StringField(ddl='varchar(50)') 9 | password = StringField(ddl='varchar(50)') 10 | username = StringField(ddl='varchar(50)') 11 | nickname = StringField(ddl='varchar(50)') 12 | -------------------------------------------------------------------------------- /aurora/server.py: -------------------------------------------------------------------------------- 1 | from sanic import Sanic 2 | import asyncio_redis 3 | from aurora.view import bp 4 | from aurora.db import setup_connection, close_connection 5 | from aurora.config import settings 6 | 7 | from sanic_session import RedisSessionInterface 8 | 9 | app = Sanic(__name__) 10 | 11 | app.blueprint(bp) 12 | app.static('/static', settings.STATIC_URL) 13 | 14 | 15 | async def start_connection(app, loop): 16 | ''' 17 | 将数据库连接池放入blueprint 18 | :param app: 19 | :param loop: 20 | :return: 21 | ''' 22 | _data_pool = await setup_connection(app, loop) 23 | bp.pool = _data_pool 24 | 25 | async def startup_redis_pool(): 26 | # redis 27 | _redis_pool = await asyncio_redis.Pool.create(host='127.0.0.1', port=6379, poolsize=10) 28 | bp.redis = _redis_pool 29 | return _redis_pool 30 | 31 | 32 | @app.middleware('request') 33 | async def add_session_to_request(request): 34 | # before each request initialize a session 35 | # using the client's request 36 | await session.open(request) 37 | 38 | 39 | @app.middleware('response') 40 | async def save_session(request, response): 41 | # after each request save the session, 42 | # pass the response to set client cookies 43 | await session.save(request, response) 44 | 45 | 46 | if __name__ == "__main__": 47 | ''' 48 | sanic 启动时创建数据库连接池,服务正常结束时关闭连接池 49 | ''' 50 | session = RedisSessionInterface(redis_getter=startup_redis_pool) 51 | 52 | app.run(host="0.0.0.0", port=settings.PORT, workers=settings.workers, debug=settings.DEBUG, 53 | after_start=start_connection, after_stop=close_connection) 54 | -------------------------------------------------------------------------------- /aurora/test/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mugbya/aurora/8ecf184afdbeb511f19f83940aa4bd186693a18c/aurora/test/__init__.py -------------------------------------------------------------------------------- /aurora/test/test_pool.py: -------------------------------------------------------------------------------- 1 | # coding = utf-8 2 | import requests 3 | from concurrent.futures import ThreadPoolExecutor 4 | import time 5 | import json 6 | import asyncio 7 | 8 | 9 | NUMBERS = range(10) 10 | 11 | 12 | def login(number): 13 | 14 | url = 'http://localhost:8000/test_login/' 15 | payload = {'username': chr(ord('a') + number)} 16 | r = requests.post(url, data=json.dumps(payload)) 17 | return r.text 18 | 19 | 20 | def test_by_futures(): 21 | ''' 22 | 使用 concurrent.futures 测试并发 23 | :return: 24 | ''' 25 | start = time.time() 26 | with ThreadPoolExecutor(max_workers=10) as executor: 27 | for num, result in zip(NUMBERS, executor.map(login, NUMBERS)): 28 | print('login({}) = {}'.format(chr(ord('a') + num), result)) 29 | print('Use requests+ThreadPoolExecutor cost: {}'.format(time.time() - start)) 30 | 31 | 32 | async def run_scraper_tasks(executor): 33 | loop = asyncio.get_event_loop() 34 | blocking_tasks = [] 35 | for num in NUMBERS: 36 | task = loop.run_in_executor(executor, login, num) 37 | task.__num = num 38 | blocking_tasks.append(task) 39 | completed, pending = await asyncio.wait(blocking_tasks) 40 | results = {t.__num: t.result() for t in completed} 41 | for num, result in sorted(results.items(), key=lambda x: x[0]): 42 | print('login({}) = {}'.format(chr(ord('a') + num), result)) 43 | 44 | 45 | def test_by_asyncio(): 46 | ''' 47 | 使用 asyncio 测试并发连接池情况 48 | :return: 49 | ''' 50 | start = time.time() 51 | executor = ThreadPoolExecutor(10) 52 | event_loop = asyncio.get_event_loop() 53 | event_loop.run_until_complete(run_scraper_tasks(executor)) 54 | print('Use asyncio+requests+ThreadPoolExecutor cost: {}'.format(time.time() - start)) 55 | 56 | if __name__ == "__main__": 57 | test_by_asyncio() 58 | test_by_futures() 59 | 60 | 61 | -------------------------------------------------------------------------------- /aurora/test/test_pool_server.py: -------------------------------------------------------------------------------- 1 | from sanic.response import json 2 | from sanic import Blueprint 3 | from sanic import Sanic 4 | from aurora.models import User 5 | from aurora.config import settings 6 | 7 | from aurora.db import setup_connection, close_connection 8 | 9 | import logging 10 | logger = logging.getLogger() 11 | 12 | app = Sanic(__name__) 13 | bp = Blueprint('user', url_prefix='/v1/user') 14 | 15 | ''' 16 | test pool 的服务端代码 17 | ''' 18 | 19 | 20 | @bp.post('/test_login/') 21 | async def test_login(request): 22 | ''' 23 | 服务端测试代码 24 | 测试 并发数据库连接池情况 25 | :param request: 26 | :return: 27 | ''' 28 | if request.method == 'POST': 29 | username = request.json.get('username', '') 30 | user_list = await User.filter(nickname=username) 31 | if user_list: 32 | response = json(user_list) 33 | return response 34 | else: 35 | return json({'msg': 'user not exist'}) 36 | 37 | 38 | if __name__ == "__main__": 39 | ''' 40 | sanic 启动时创建数据库连接池,服务正常结束时关闭连接池 41 | ''' 42 | app.blueprint(bp) 43 | app.run(host="0.0.0.0", port=settings.PORT, workers=settings.workers, debug=settings.DEBUG, 44 | after_start=setup_connection, after_stop=close_connection) 45 | -------------------------------------------------------------------------------- /aurora/util/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mugbya/aurora/8ecf184afdbeb511f19f83940aa4bd186693a18c/aurora/util/__init__.py -------------------------------------------------------------------------------- /aurora/util/sanic_jinja.py: -------------------------------------------------------------------------------- 1 | from sanic.response import html 2 | from jinja2 import Environment, PackageLoader 3 | from config.settings import PACKAGE_NAME, TEMPLATES 4 | 5 | 6 | class SanicJinja2: 7 | env = Environment() 8 | loader = PackageLoader(PACKAGE_NAME, TEMPLATES) 9 | env.loader = loader 10 | 11 | @classmethod 12 | def update_request_context(cls, request, context): 13 | context.setdefault('request', request) 14 | 15 | @classmethod 16 | async def render_string_async(cls, template, request, **context): 17 | cls.update_request_context(request, context) 18 | return await cls.env.get_template(template).render_async(**context) 19 | 20 | @classmethod 21 | async def render_async(cls, template, request, **context): 22 | return html(await cls.render_string_async(template, request, 23 | **context)) 24 | 25 | @classmethod 26 | def render_string(cls, template, request, **context): 27 | cls.update_request_context(request, context) 28 | return cls.env.get_template(template).render(**context) 29 | 30 | @classmethod 31 | def render(cls, template, request, **context): 32 | return html(cls.render_string(template, request, **context)) 33 | 34 | 35 | def render(template, request, **context): 36 | return html(SanicJinja2.render_string(template, request, **context)) 37 | 38 | -------------------------------------------------------------------------------- /aurora/view.py: -------------------------------------------------------------------------------- 1 | import binascii 2 | import os 3 | import logging 4 | from sanic.response import text, html, json, redirect 5 | from sanic.blueprints import Blueprint 6 | from aurora.util.sanic_jinja import render 7 | from aurora.models import User 8 | 9 | bp = Blueprint('view_user') 10 | 11 | 12 | logger = logging.getLogger(__name__) 13 | 14 | 15 | @bp.get('/') 16 | async def index(request): 17 | _token = request.headers.get('token') 18 | user = request['session'].get(_token) 19 | 20 | return render('index.html', request, user=user) 21 | 22 | 23 | @bp.route('/login/', methods=['GET', 'POST']) 24 | async def login(request): 25 | if request.method == 'POST': 26 | username = request.form.get('username', '') 27 | password = request.form.get('password', '') 28 | 29 | user_list = await User.filter(nickname=username, password=password) 30 | if user_list: 31 | _token = binascii.hexlify(os.urandom(16)).decode("utf8") 32 | response = json(_token) 33 | 34 | if not request._cookies.get('session'): 35 | request['session']['session'] = _token 36 | return response 37 | else: 38 | return json({'msg': 'user not exist'}) 39 | else: 40 | return render('login.html', request) 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /aurora/webContent/static/css/header.css: -------------------------------------------------------------------------------- 1 | header { 2 | background: #f5f5f5; 3 | height: 50px; 4 | /*border-bottom: 1px solid #e5e5e5;*/ 5 | /*box-shadow: 0 2px 3px #e5e5e5;*/ 6 | /*display: block;*/ 7 | } 8 | 9 | .navbar-default { 10 | 11 | background-color: #f5f5f5 !important; 12 | border-color: #f5f5f5 !important; 13 | } 14 | 15 | 16 | 17 | .navbar-default .navbar-nav>.active>a, .navbar-default .navbar-nav>.active>a:hover, .navbar-default .navbar-nav>.active>a:focus { 18 | color: #555; 19 | background-color: #f5f5f5 !important; 20 | } -------------------------------------------------------------------------------- /aurora/webContent/static/css/index.css: -------------------------------------------------------------------------------- 1 | /* BACKGROUND GRADIENT */ 2 | /* BACKGROUND SIZE */ 3 | /* BORDER RADIUS */ 4 | /* BOX */ 5 | /* BOX RGBA */ 6 | /* BOX SHADOW */ 7 | /* BOX SIZING */ 8 | /* COLUMNS */ 9 | /* DOUBLE BORDERS */ 10 | /* FLEX */ 11 | /* FLIP */ 12 | /* FONT FACE */ 13 | /* OPACITY */ 14 | /* OUTLINE RADIUS */ 15 | /* RESIZE */ 16 | /* ROTATE*/ 17 | /* TEXT SHADOW */ 18 | /* TRANSFORM */ 19 | /* TRANSITION */ 20 | .question-post-list .qp-item { 21 | position: relative; 22 | padding: 10px 0 10px 140px; 23 | border-bottom: 1px solid #e5e5e5; } 24 | .question-post-list .tag-box { 25 | position: absolute; 26 | top: 10px; 27 | left: 0; 28 | overflow: hidden; 29 | *zoom: 1; } 30 | .question-post-list .tag-box .tag-item { 31 | display: block; 32 | float: left; 33 | width: 40px; 34 | font-size: 12px; 35 | text-align: center; } 36 | .question-post-list .tag-box .tag-item small { 37 | display: block; } 38 | .question-post-list .tag-box .tag-default { 39 | display: inline-block; 40 | width: 40px; 41 | height: 40px; 42 | line-height: 16px; 43 | padding: 4px; 44 | -moz-box-sizing: border-box; 45 | -webkit-box-sizing: border-box; 46 | box-sizing: border-box; } 47 | .question-post-list .tag-box .tag-green { 48 | color: #fff; 49 | background-color: #009a61; } 50 | .question-post-list .tag-box .tag-red { 51 | color: #fff; 52 | background-color: #ad3a37; } 53 | .question-post-list .tag-box .tag-gray { 54 | color: #fff; 55 | background-color: #808b87; } 56 | 57 | .info { 58 | font-size: 12px; 59 | color: #999; } 60 | 61 | .content .title { 62 | display: inline-block; 63 | font-size: 16px; 64 | font-weight: 400; 65 | margin: 6px 0; 66 | color: #252525; 67 | word-break: break-all; } 68 | .content .taglist { 69 | display: inline-block; 70 | list-style: none; 71 | padding: 0; } 72 | .content .tagpopup { 73 | display: inline-block; 74 | padding: 0 5px; 75 | font-size: 12px; 76 | line-height: 16px; 77 | background: #E7F2ED; 78 | color: #017e66; 79 | -moz-border-radius: 5px; 80 | -webkit-border-radius: 5px; 81 | border-radius: 5px; } 82 | 83 | .sidebar-box { 84 | margin: 0 0 20px 0; } 85 | .sidebar-box .title { 86 | font-size: 18px; 87 | color: #252525; 88 | margin: 10px 0; } 89 | .sidebar-box .taglist { 90 | display: block; 91 | padding: 30px 0; 92 | list-style: none; 93 | margin: 0; 94 | border: 1px solid #e5e5e5; 95 | border-width: 1px 0; } 96 | .sidebar-box .taglist .tag { 97 | display: inline-block; 98 | padding: 0 6px; 99 | font-size: 13px; 100 | line-height: 24px; 101 | margin: 0 1px 5px 0; 102 | background: #E7F2ED; 103 | color: #017e66; } 104 | .sidebar-box .taglist .tag a { 105 | color: #017e66; } 106 | .sidebar-box .qalist { 107 | list-style: none; 108 | padding: 0; 109 | margin: 0; } 110 | .sidebar-box .qalist .qa { 111 | display: block; 112 | margin: 0 0 10px; } 113 | .sidebar-box .qalist a { 114 | font-size: 14px; } 115 | .sidebar-box .qalist small { 116 | font-size: 13px; } 117 | 118 | .oauth img { 119 | width: 16px; 120 | } 121 | 122 | /*# sourceMappingURL=index.css.map */ 123 | 124 | -------------------------------------------------------------------------------- /aurora/webContent/static/css/login.css: -------------------------------------------------------------------------------- 1 | .login-box { 2 | width: 240px; 3 | margin:0 auto; 4 | /*margin-top:100px;*/ 5 | 6 | } -------------------------------------------------------------------------------- /aurora/webContent/static/css/style.css: -------------------------------------------------------------------------------- 1 | .container, .navbar-static-top .container, .navbar-fixed-top .container, .navbar-fixed-bottom .container { 2 | width: 940px; 3 | } 4 | 5 | a { 6 | color: #337ab7 !important; 7 | text-decoration: none !important; 8 | } 9 | 10 | .btn { 11 | border: none; 12 | background: #34495e; 13 | color: white; 14 | font-size: 16.5px; 15 | text-decoration: none; 16 | text-shadow: none; 17 | -webkit-box-shadow: none; 18 | -moz-box-shadow: none; 19 | box-shadow: none; 20 | -webkit-transition: 0.25s; 21 | -moz-transition: 0.25s; 22 | -o-transition: 0.25s; 23 | transition: 0.25s; 24 | -webkit-backface-visibility: hidden; 25 | } 26 | 27 | .demo-center { 28 | width: 240px; 29 | padding-bottom: 100px; 30 | margin: 0 auto; 31 | } 32 | 33 | .demo-content { 34 | padding: 60px 0; 35 | } 36 | 37 | .dropdown-menu { 38 | top: 100% !important; 39 | width: 320px !important; 40 | 41 | } 42 | 43 | .message-line{ 44 | padding-left: 15px; 45 | } 46 | 47 | .message-line-brown{ 48 | padding-left: 15px; 49 | color: brown; 50 | } 51 | -------------------------------------------------------------------------------- /aurora/webContent/static/js/message.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by mugbya on 16-10-5. 3 | */ 4 | 5 | $(document).ready(function() { 6 | 7 | setTimeout(requestInventory, 100); 8 | 9 | }); 10 | 11 | // function requestInventory() { 12 | // jQuery.getJSON('//localhost:8888/message/status', {user: $('.user').html()}, 13 | // function(data, status, xhr) { 14 | // $('.message_all').html(data['sum']); 15 | // $('.message').html(data['unread']); 16 | // setTimeout(requestInventory, 0); 17 | // } 18 | // ); 19 | // } 20 | 21 | function requestInventory() { 22 | var domain = window.location.hostname; 23 | var port = window.location.port; 24 | var host = 'ws://' + domain + ':' + port +'/message/status'; 25 | 26 | var websocket = new WebSocket(host); 27 | 28 | websocket.onopen = function (evt) { }; 29 | websocket.onmessage = function(evt) { 30 | var unread = $.parseJSON(evt.data)['unread']; 31 | if (unread > 0){ 32 | var span_node = document.createElement('span'); 33 | var a_node = $('.dropdown-toggle'); 34 | 35 | span_node.innerHTML = '(' + unread + ')'; 36 | a_node.append(span_node); 37 | } 38 | 39 | 40 | var message = $.parseJSON(evt.data)['message']; 41 | var ul_node = $('.dropdown-menu'); 42 | var li_node = document.createElement('li'); 43 | 44 | if(message['status'] != 0){ 45 | li_node.innerHTML = '
' + message['username'] + '评论了你的博客' 46 | + '' + message['data'] + '
'; 47 | }else { 48 | li_node.innerHTML = '
' + message['username'] + '评论了你的博客' 49 | + '' + message['data'] + '
'; 50 | } 51 | 52 | ul_node.prepend(li_node); 53 | 54 | }; 55 | websocket.onerror = function (evt) { }; 56 | } -------------------------------------------------------------------------------- /aurora/webContent/templates/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | HTTP 404 - Repository 6 | 7 | 105 | 106 | 107 |
108 |
109 |
杯具啊
110 | 111 |

杯具啊!

112 | 113 |
HTTP 404……
可能这个页面已经飞走了
114 | 115 |
116 |
117 |
118 | 121 | 122 | 130 | 131 | 140 | 141 | -------------------------------------------------------------------------------- /aurora/webContent/templates/base/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | realtime message example 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | {% block header %}{% endblock %} 16 | 17 | 18 | {% block body %}{% endblock %} 19 | 20 |
21 | {% block footer %}{% endblock %} 22 |
23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /aurora/webContent/templates/entry.html: -------------------------------------------------------------------------------- 1 | {% extends "base/base.html" %} 2 | 3 | {% block header %} 4 | {% module Head() %} 5 | {% end %} 6 | 7 | {% block body %} 8 | 9 |
10 |

11 | {{ blog['title'] }} 12 |

13 |
14 |
15 | 作者: {{ blog['username'] }}      16 |
17 |
18 |
19 | 20 |
21 | {{ blog['content'] }} 22 |
23 | 24 |

回答

25 | 26 |
27 | 28 | {% for comment in blog['comment'] %} 29 |
30 |
31 |

32 | 作者: {{ comment['username'] }}      33 |

34 |
35 | {{ comment['content'] }} 36 |
37 |
38 | {% end %} 39 | 40 |
41 | 42 |
43 | 44 |

撰写答案

45 |
46 | 47 |
48 | 49 |
50 |
51 | 52 |
53 | {% end %} 54 | 55 | {% block footer %} 56 | 57 | {% end %} -------------------------------------------------------------------------------- /aurora/webContent/templates/index.html: -------------------------------------------------------------------------------- 1 | {% extends "base/base.html" %} 2 | 3 | {% block header %} 4 | {% endblock %} 5 | 6 | {% block body %} 7 | 8 |
9 | This is index , I'm {{ user }} 10 | 11 |
12 | {% endblock %} 13 | 14 | {% block footer %} 15 | 16 | {% endblock %} -------------------------------------------------------------------------------- /aurora/webContent/templates/login.html: -------------------------------------------------------------------------------- 1 | {% extends "base/base.html" %} 2 | 3 | 4 | {% block body %} 5 |
6 |
7 |
8 |
9 |

10 | 消息推送示例 11 |

12 |

13 | 用户名: pom 密码: pom 14 |

15 |

16 | 用户名: nash 密码: nash 17 |

18 |
19 |
20 | 21 |
22 | 23 |
24 | 48 |
49 |
50 | 51 | {% endblock %} 52 | 53 | {% block footer %} 54 | 55 | {% endblock %} 56 | -------------------------------------------------------------------------------- /aurora/webContent/templates/modules/head.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 | 50 |
51 |
52 |
53 |
-------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | 2 | # requirement python3.5 3 | 4 | jinja2 5 | 6 | asyncio_redis 7 | 8 | uvloop 9 | 10 | sanic 11 | 12 | requests 13 | 14 | 15 | # 日记记录 16 | # raven 17 | 18 | 19 | # 数据库连接 20 | asyncpg 21 | 22 | # 数据库orm 支持 23 | # peewee-async 24 | # peewee 25 | 26 | # marshmallowpip 27 | 28 | 29 | 30 | --------------------------------------------------------------------------------