├── .editorconfig
├── .flake8
├── .gitignore
├── .style.yapf
├── LICENSE
├── README.md
├── app
├── __init__.py
├── auth
│ └── auths.py
├── config.py
├── database.py
├── models
│ ├── __init__.py
│ └── user.py
├── routers
│ ├── __init__.py
│ ├── items.py
│ └── users.py
└── util
│ ├── common.py
│ ├── dateencoder.py
│ └── sms.py
├── deploy
├── gunicorn_fast.service
└── test_user.sql
├── gunicorn.py
├── local.py
└── run.py
/.editorconfig:
--------------------------------------------------------------------------------
1 | # https://editorconfig.org/
2 | # https://raw.githubusercontent.com/django/django/master/.editorconfig
3 |
4 | root = true
5 |
6 | [*]
7 | indent_style = space
8 | indent_size = 4
9 | insert_final_newline = true
10 | trim_trailing_whitespace = true
11 | end_of_line = lf
12 | charset = utf-8
13 |
14 | # Docstrings and comments use max_line_length = 79
15 | [*.py]
16 | max_line_length = 119
17 |
18 | # Use 2 spaces for the HTML files
19 | [*.html]
20 | indent_size = 2
21 |
22 | [*.js]
23 | indent_size = 2
24 |
25 | # The JSON files contain newlines inconsistently
26 | [*.json]
27 | indent_size = 2
28 | insert_final_newline = ignore
29 |
30 | [**/admin/js/vendor/**]
31 | indent_style = ignore
32 | indent_size = ignore
33 |
34 | # Minified JavaScript files shouldn't be changed
35 | [**.min.js]
36 | indent_style = ignore
37 | insert_final_newline = ignore
38 |
39 | # Makefiles always use tabs for indentation
40 | [Makefile]
41 | indent_size = 8
42 | indent_style = tab
43 |
44 | # Batch files use tabs for indentation
45 | [*.bat]
46 | indent_style = tab
47 |
48 | [docs/**.txt]
49 | max_line_length = 79
50 |
--------------------------------------------------------------------------------
/.flake8:
--------------------------------------------------------------------------------
1 | [flake8]
2 | ignore = E501,W292,W391
3 | exclude =
4 | .git,
5 | __pycache__,
6 | build,
7 | dist
8 | max-complexity = 10
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | __pycache__*
3 | models.py
4 | node_modules/
5 | dist/
6 | npm-debug.log*
7 | yarn-debug.log*
8 | yarn-error.log*
9 |
10 | # Editor directories and files
11 | .idea
12 | *.suo
13 | *.ntvs*
14 | *.njsproj
15 | *.sln
16 | *.log
17 | *.__pycache__
18 | *.pyc
19 | .vscode
20 |
--------------------------------------------------------------------------------
/.style.yapf:
--------------------------------------------------------------------------------
1 | # https://github.com/google/yapf?utm_source=tuicool&utm_medium=referral
2 |
3 | [style]
4 | based_on_style = pep8
5 | spaces_before_comment = 4
6 | split_before_logical_operator = true
7 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2018 zhiyongma
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in
13 | all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | THE SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 | ---
3 |
4 | **Chinese Documentation**: https://www.cnblogs.com/mazhiyong/
5 |
6 | **Source Code**: https://github.com/zhiyongma/fastproject
7 |
8 | ---
9 |
10 |
11 | ## Requirements
12 |
13 | Python 3.6+
14 |
15 | You should install the requirments below first.
16 |
17 |
18 |
19 | ```console
20 | $ pip install fastapi
21 | $ pip install pymysql
22 | $ pip install sqlalchemy
23 | $ pip install pyjwt
24 | $ pip install bcrypt
25 | $ pip install passlib
26 | $ pip install python-multipart
27 | ```
28 |
29 |
30 |
31 | Then you should create your mysql database. Sample sql in the deploy directory.
32 | You can configure the database connection in config.py.
33 |
34 | You will also need an ASGI server, for production such as Uvicorn or Hypercorn.
35 |
36 |
37 |
38 | ```console
39 | $ pip install uvicorn
40 | ```
41 |
42 |
43 |
44 | For production deployment, you should also install Gunicorn.
45 |
46 |
47 | ```console
48 | $ pip install gunicorn
49 | ```
50 |
51 |
52 |
53 |
54 | ## Example
55 |
56 | ### Local Run
57 |
58 |
59 | ```console
60 | $ uvicorn local:app --reload
61 | ```
62 |
63 |
64 |
65 | ### Server Run
66 |
67 | Run the server with gunicorn.
68 |
69 |
70 |
71 | ```console
72 | $ gunicorn -c /data/fastest/gunicorn.py -e FASTAPI_ENV=production run:app
73 | ```
74 |
75 | * `-c`: gunicorn config.
76 | * `-e`: environment parameter.
77 |
78 |
79 |
80 | Run the server with service.
81 | See the file gunicorn_fast.service in deploy directory. You should put it in /usr/lib/systemd/system for linux os.
82 |
83 |
84 | ```console
85 | $ systemctl start/stop/restart/enable gunicorn_fast.service
86 | ```
87 |
88 |
89 |
90 |
91 | ### Production config
92 | * `-e`: `FASTAPI_ENV=production`.
93 | * `mysql`: configure file `config.py` .
94 | * `gunicorn`: configure file `gunicorn.py` .
95 | * `service`: configure file `gunicorn_fast.service` .
96 |
97 | remarks: the `pidfile` in `gunicorn_fast.service` and `gunicorn.py` should point to the same one file.
98 |
99 | ### API test
100 | ##### `1、user register`
101 |
102 |
103 |
104 |
105 | ##### `2、user login`
106 |
107 |
108 |
109 |
110 | ##### `3、user info`
111 |
112 |
113 |
114 |
115 |
116 | ## License
117 |
118 | This project is licensed under the terms of the MIT license.
119 |
--------------------------------------------------------------------------------
/app/__init__.py:
--------------------------------------------------------------------------------
1 | # -*- encoding: utf-8 -*-
2 | """
3 | Copyright(C) 2018-2019 TEST
4 | All rights reserved
5 |
6 | File : __init__.py
7 | Time : 2020/07/27 15:04:35
8 | Author : mazhiyong
9 | Version : 1.0
10 | """
11 |
12 | import time
13 | import logging
14 | from fastapi import Depends, FastAPI, Header, HTTPException, Request
15 | from .routers import items, users
16 | from app.auth.auths import Auth
17 | from fastapi.responses import JSONResponse
18 |
19 |
20 | def create_app():
21 | """Create and configure an instance of the Flask application."""
22 | app = FastAPI()
23 |
24 | # create logger
25 | logger = logging.getLogger('fastapi')
26 |
27 | async def get_token_header(x_token: str = Header(...)):
28 | if x_token != "fake-super-secret-token":
29 | raise HTTPException(status_code=400, detail="X-Token header invalid")
30 |
31 | app.include_router(users.router)
32 | app.include_router(
33 | items.router,
34 | prefix="/items",
35 | tags=["items"],
36 | dependencies=[Depends(get_token_header)],
37 | responses={404: {"description": "Not found"}},
38 | )
39 |
40 | @app.middleware("http")
41 | async def process_authorization(request: Request, call_next):
42 | """
43 | 在这个函数里统一对访问做权限token校验。
44 | 1、如果是用户注册、登陆,那么不做token校验,由路径操作函数具体验证
45 | 2、如果是其他操作,则需要从header或者cookie中取出token信息,解析出内容
46 | 然后对用户身份进行验证,如果用户不存在则直接返回
47 | 如果用户存在则将用户信息附加到request中,这样在后续的路径操作函数中可以直接使用。
48 | """
49 | start_time = time.time()
50 |
51 | # print(request.url)
52 | # print(request.url.path)
53 |
54 | if request.url.path == '/login' or request.url.path == '/register':
55 | logger.info('no jwt verify.')
56 | else:
57 | logger.info('jwt verify.')
58 |
59 | result = Auth.identifyAll(request)
60 | if result['status'] and result['data']:
61 | user = result['data']['user']
62 |
63 | logger.info('jwt verify success. user: %s ' % user.username)
64 |
65 | # state中记录用户基本信息
66 | request.state.user = user
67 | else:
68 | return JSONResponse(content=result)
69 |
70 | response = await call_next(request)
71 |
72 | process_time = time.time() - start_time
73 | response.headers["X-Process-Time"] = str(process_time)
74 | return response
75 |
76 | return app
77 |
--------------------------------------------------------------------------------
/app/auth/auths.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """
3 | Copyright (C) 2018 PPCMS
4 | All rights reserved
5 |
6 | Filename : auths.py
7 | Description : auths.py
8 |
9 | Created by mazhiyong at 2018-11-01 14:49:36
10 |
11 | 这里采用了JWT的方式进行身份鉴权。
12 | 随着前后端分离的趋势,基于Token的认证方式成为主流,而JWT是基于Token认证方式的一种机制,是实现单点登录认证的一种有效方法。
13 |
14 | 基本校验流程:
15 | 1、用户注册账号。
16 | 2、用户登陆,成功后返回JWT token,数据库中记录token中对应的登陆时间。
17 | 3、用户发起业务请求,header传值字段名为“Authorization”的校验字段,字段值以“JWT”开头,并与token空格隔开。
18 | 4、服务端解析token发起校验,如果解析正确且与数据库中保存的登陆时间一致则认为校验通过
19 |
20 | 注意:
21 | 目前并没有对token的时效性进行校验,不排除日后进行处理。
22 | """
23 |
24 | import logging
25 | import datetime
26 | import jwt
27 | import time
28 | import os
29 | import re
30 | from app.models.user import DBUser
31 | from app import config
32 | from app.util import common
33 | from app.config import get_config
34 | from sqlalchemy.orm import Session
35 | from typing import Optional
36 | from fastapi import HTTPException, status, Response, Depends
37 | from passlib.context import CryptContext
38 | from app.database import get_db_local
39 |
40 |
41 | # create logger
42 | logger = logging.getLogger('fastapi')
43 |
44 | pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
45 |
46 |
47 | class Auth():
48 | @staticmethod
49 | def encode_auth_token(origin_data, exp):
50 | """
51 | 生成认证Token
52 | :param user_id: int
53 | :param login_time: int(timestamp)
54 | :return: string
55 | """
56 | try:
57 | payload = {
58 | 'exp': exp if exp else datetime.datetime.now() + datetime.timedelta(days=7),
59 | 'iat': datetime.datetime.now(),
60 | 'iss': 'paperpie',
61 | 'data': origin_data
62 | }
63 |
64 | # logger.debug('jwt encode secret_key %s', secret_key)
65 |
66 | encoded_jwt = jwt.encode(
67 | payload,
68 | get_config().SECRET_KEY,
69 | get_config().ALGORITHM
70 | )
71 |
72 | return encoded_jwt
73 | except Exception as e:
74 | return e
75 |
76 | @staticmethod
77 | def decode_auth_token(auth_token):
78 | """
79 | 验证Token
80 | :param auth_token:
81 | :return: integer|string
82 | """
83 | try:
84 | secret_key = get_config().SECRET_KEY
85 | algorithm = get_config().ALGORITHM
86 |
87 | payload = jwt.decode(auth_token, secret_key, algorithms=[algorithm], options={'verify_exp': True})
88 | if ('data' in payload):
89 | return payload
90 | else:
91 | return '无效Token'
92 | # raise jwt.InvalidTokenError
93 | except jwt.ExpiredSignatureError:
94 | return 'Token过期'
95 | except jwt.InvalidTokenError:
96 | return '无效Token'
97 |
98 | # 校验密码
99 | @staticmethod
100 | def verify_password(plain_password, hashed_password):
101 | return pwd_context.verify(plain_password, hashed_password)
102 |
103 | # 密码哈希
104 | @staticmethod
105 | def get_password_hash(password):
106 | return pwd_context.hash(password)
107 |
108 | # 用户信息校验:username和password分别校验
109 | @staticmethod
110 | def authenticate_user(username: str, password: str, db: Session):
111 | user = DBUser.get_by_username(db, username)
112 | if not user:
113 | return False
114 | if not Auth.verify_password(password, user.password):
115 | return False
116 | return user
117 |
118 | @staticmethod
119 | def login_authenticate(username: str, password: str, db: Session):
120 | """
121 | 用户登录,登录成功返回token,将登录时间写入数据库;登录失败返回失败原因
122 | """
123 | result = {}
124 |
125 | # 首先校验用户信息
126 | user = Auth.authenticate_user(username, password, db)
127 | if not user:
128 | # close db
129 | db.close()
130 | return common.falseReturn(result, '登录失败')
131 |
132 | # 登陆时间
133 | login_time = int(time.time())
134 | user.login_time = login_time
135 | DBUser.update_login_time(db, user.id, login_time)
136 |
137 | origin_data = {
138 | 'user_id': user.id,
139 | 'login_time': login_time
140 | }
141 |
142 | access_token = Auth.encode_auth_token(origin_data, None).decode()
143 | bearer_token = 'Bearer ' + access_token
144 |
145 | result['user_id'] = user.id
146 | result['username'] = user.username
147 | result['access_token'] = access_token
148 | result['token_type'] = "bearer"
149 |
150 | print(result)
151 |
152 | rsp = common.trueReturn(result, '登录成功')
153 | rsp.set_cookie(key="Bearer", value=bearer_token)
154 | return rsp
155 |
156 |
157 | @staticmethod
158 | def identifyAll(request):
159 | """
160 | 用户鉴权
161 | :return: list
162 | """
163 | auth_header = request.headers.get('Authorization')
164 | logger.info('auth_header %s', auth_header)
165 |
166 | jwt_cookie = request.cookies.get('Bearer')
167 | logger.info('jwt_cookie %s', jwt_cookie)
168 |
169 | if (auth_header or jwt_cookie):
170 | auth_tokenArr = ''
171 |
172 | if(auth_header):
173 | auth_tokenArr = auth_header.split(" ")
174 | # print('auth token from auth_header. ', auth_header)
175 | else:
176 | auth_tokenArr = jwt_cookie.split(" ")
177 | # print('auth token from jwt_cookie. ', jwt_cookie)
178 |
179 | if (not auth_tokenArr or auth_tokenArr[0] != 'Bearer' or len(auth_tokenArr) != 2):
180 | result = common.falseContent('', '请传递正确的验证头信息')
181 | else:
182 | auth_token = auth_tokenArr[1]
183 | payload = Auth.decode_auth_token(auth_token)
184 | if not isinstance(payload, str):
185 | user_id = payload['data']['user_id']
186 | login_time = payload['data']['login_time']
187 |
188 | # get db
189 | db = get_db_local()
190 |
191 | user = DBUser.get_by_user_id(db, user_id)
192 | if (user is None):
193 | result = common.falseContent('', '找不到该用户信息')
194 | else:
195 | if (user.login_time == login_time):
196 | returnUser = {
197 | 'user': user
198 | }
199 | result = common.trueContent(returnUser, '请求成功')
200 | else:
201 | result = common.falseContent('', 'Token已更改,请重新登录获取')
202 |
203 | # close db
204 | db.close()
205 | else:
206 | result = common.falseContent('', payload)
207 | else:
208 | result = common.falseContent('', '没有提供认证token')
209 |
210 | return result
211 |
--------------------------------------------------------------------------------
/app/config.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """
3 | Copyright(C) 2018 PPCMS
4 | All rights reserved
5 |
6 | Filename: config.py
7 | Description: config.py
8 |
9 | Created by mazhiyong at 2018-11-01 14:41:55
10 | """
11 |
12 | import os
13 |
14 |
15 | class Config:
16 | SITE_NAME = u'PPCMS'
17 |
18 | # Consider SQLALCHEMY_COMMIT_ON_TEARDOWN harmful
19 | # SQLALCHEMY_COMMIT_ON_TEARDOWN = True
20 |
21 | SQLALCHEMY_POOL_RECYCLE = 10
22 | SQLALCHEMY_TRACK_MODIFICATIONS = True
23 | SQLALCHEMY_COMMIT_ON_TEARDOWN = True
24 |
25 | # to get a string like this run:
26 | # openssl rand -hex 32
27 | SECRET_KEY = "09d25e094faa6ca2556c818166b7a9563b93f7099f6f0f4caa6cf63b88e8d3e7"
28 | ALGORITHM = "HS256"
29 |
30 | # SQLALCHEMY_MIGRATE_REPO = os.path.join(basedir, 'db_repository')
31 |
32 |
33 | class DevelopmentConfig(Config):
34 | DEBUG = True
35 |
36 | SQLALCHEMY_ECHO = False
37 |
38 | MYSQL_USER = 'pony'
39 | MYSQL_PASS = ''
40 | MYSQL_HOST = 'rm-2zee5e5ytvd02o9e9no.mysql.rds.aliyuncs.com'
41 | MYSQL_PORT = '3306'
42 | MYSQL_DB = 'bookcrawl'
43 |
44 | SQLALCHEMY_DATABASE_URI = 'mysql+pymysql://%s:%s@%s:%s/%s' % (MYSQL_USER, MYSQL_PASS, MYSQL_HOST, MYSQL_PORT, MYSQL_DB)
45 |
46 |
47 | class ProductionConfig(Config):
48 | DEBUG = True
49 |
50 | # mysql configuration
51 | MYSQL_USER = ''
52 | MYSQL_PASS = ''
53 | MYSQL_HOST = ''
54 | MYSQL_PORT = '3306'
55 | MYSQL_DB = ''
56 |
57 | if (len(MYSQL_USER) > 0 and len(MYSQL_PASS) > 0 and len(MYSQL_HOST) > 0 and len(MYSQL_PORT) > 0 and len(MYSQL_DB) > 0):
58 | SQLALCHEMY_DATABASE_URI = 'mysql+pymysql://%s:%s@%s:%s/%s' % (MYSQL_USER, MYSQL_PASS, MYSQL_HOST, MYSQL_PORT, MYSQL_DB)
59 |
60 |
61 | config = {
62 | 'default': DevelopmentConfig,
63 | 'development': DevelopmentConfig,
64 | 'production': ProductionConfig,
65 | }
66 |
67 |
68 | def get_config():
69 | config_name = os.getenv('FASTAPI_ENV') or 'default'
70 | return config[config_name]
71 |
--------------------------------------------------------------------------------
/app/database.py:
--------------------------------------------------------------------------------
1 | # -*- encoding: utf-8 -*-
2 | """
3 | Copyright(C) 2018-2019 TEST
4 | All rights reserved
5 |
6 | File : database.py
7 | Time : 2020/08/01 20:21:48
8 | Author : mazhiyong
9 | Version : 1.0
10 | """
11 |
12 | from sqlalchemy import create_engine
13 | from sqlalchemy.orm import sessionmaker
14 | from sqlalchemy.ext.declarative import declarative_base
15 |
16 | from app.config import get_config
17 |
18 |
19 | # 创建对象的基类:
20 | Base = declarative_base()
21 |
22 | # 初始化数据库连接:
23 | engine = create_engine(get_config().SQLALCHEMY_DATABASE_URI)
24 |
25 | # SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
26 | SessionLocal = sessionmaker(bind=engine)
27 |
28 |
29 | # db Dependency
30 | def get_db():
31 | db = SessionLocal()
32 | try:
33 | yield db
34 | finally:
35 | db.close()
36 |
37 |
38 | def get_db_local():
39 | return SessionLocal()
40 |
--------------------------------------------------------------------------------
/app/models/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zhiyongma/fastproject/1dd2edef68a109d927663143707881dfaad78d64/app/models/__init__.py
--------------------------------------------------------------------------------
/app/models/user.py:
--------------------------------------------------------------------------------
1 | ## -*- encoding: utf-8 -*-
2 | """
3 | Copyright(C) 2018-2019 TEST
4 | All rights reserved
5 |
6 | File : user
7 | Time : 2020/07/02 16:45:40
8 | Author : mazhiyong
9 | Version : 1.0
10 | """
11 |
12 | from sqlalchemy import Column, DateTime, String, text
13 | from sqlalchemy.dialects.mysql import INTEGER, VARCHAR
14 | from sqlalchemy.orm import Session
15 | from app.database import Base
16 | from pydantic import BaseModel
17 | from typing import Optional
18 |
19 |
20 | # Pydantic model
21 | class User(BaseModel):
22 | id: Optional[int] = None
23 | username: str
24 | sex: Optional[str] = None
25 | login_time: Optional[int] = None
26 |
27 | class Config:
28 | orm_mode = True
29 |
30 |
31 | # DB model
32 | class DBUser(Base):
33 | __tablename__ = 'test_user'
34 |
35 | id = Column(INTEGER(64), primary_key=True, comment='编号')
36 | username = Column(String(100))
37 | password = Column(String(100))
38 | sex = Column(VARCHAR(10), server_default=text("''"), comment='性别')
39 | login_time = Column(INTEGER(11), server_default=text("'0'"), comment='登陆时间,主要为了登陆JWT校验使用')
40 | create_date = Column(DateTime, nullable=False, server_default=text("CURRENT_TIMESTAMP"))
41 | update_date = Column(DateTime, nullable=False, server_default=text("CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP"))
42 |
43 | @classmethod
44 | def add(cls, db: Session, data):
45 | db.add(data)
46 | db.commit()
47 | # db.refresh(data)
48 |
49 | @classmethod
50 | def get_by_user_id(cls, db: Session, user_id):
51 | data = db.query(cls).filter_by(id=user_id).first()
52 |
53 | return data
54 |
55 | @classmethod
56 | def get_by_username(cls, db: Session, username):
57 | data = db.query(cls).filter_by(username=username).first()
58 |
59 | return data
60 |
61 | @classmethod
62 | def update(cls, db: Session, username, sex):
63 | db.query(cls).filter_by(username=username).update({cls.sex: sex})
64 |
65 | db.commit()
66 |
67 | @classmethod
68 | def update_login_time(cls, db: Session, user_id, login_time):
69 | db.query(cls).filter_by(id=user_id).update({cls.login_time: login_time})
70 |
71 | db.commit()
72 |
--------------------------------------------------------------------------------
/app/routers/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zhiyongma/fastproject/1dd2edef68a109d927663143707881dfaad78d64/app/routers/__init__.py
--------------------------------------------------------------------------------
/app/routers/items.py:
--------------------------------------------------------------------------------
1 | # -*- encoding: utf-8 -*-
2 | """
3 | Copyright(C) 2018-2019 TEST
4 | All rights reserved
5 |
6 | File : items.py
7 | Time : 2020/07/27 14:32:51
8 | Author : mazhiyong
9 | Version : 1.0
10 | """
11 |
12 | from fastapi import APIRouter, HTTPException
13 |
14 | router = APIRouter()
15 |
16 |
17 | @router.get("/")
18 | async def read_items():
19 | return [{"name": "Item Foo"}, {"name": "item Bar"}]
20 |
21 |
22 | @router.get("/{item_id}")
23 | async def read_item(item_id: str):
24 | return {"name": "Fake Specific Item", "item_id": item_id}
25 |
26 |
27 | @router.put(
28 | "/{item_id}",
29 | tags=["custom"],
30 | responses={403: {"description": "Operation forbidden"}},
31 | )
32 | async def update_item(item_id: str):
33 | if item_id != "foo":
34 | raise HTTPException(status_code=403, detail="You can only update the item: foo")
35 | return {"item_id": item_id, "name": "The Fighters"}
--------------------------------------------------------------------------------
/app/routers/users.py:
--------------------------------------------------------------------------------
1 | # -*- encoding: utf-8 -*-
2 | """
3 | Copyright(C) 2018-2019 TEST
4 | All rights reserved
5 |
6 | File : users.py
7 | Time : 2020/07/27 14:32:44
8 | Author : mazhiyong
9 | Version : 1.0
10 | """
11 |
12 | from fastapi import APIRouter, Depends, Request
13 | from fastapi.security import OAuth2PasswordRequestForm
14 | from sqlalchemy.orm import Session
15 | from app.database import get_db
16 | from app.auth.auths import Auth
17 | from app.models.user import User, DBUser
18 |
19 | router = APIRouter()
20 |
21 |
22 | @router.post("/register", response_model=User)
23 | async def register(request: Request, form_data: OAuth2PasswordRequestForm = Depends(), db: Session = Depends(get_db)):
24 | # 密码加密
25 | password = Auth.get_password_hash(form_data.password)
26 |
27 | db_user = DBUser.get_by_username(db, form_data.username)
28 | if db_user:
29 | return db_user
30 |
31 | db_user = DBUser(username=form_data.username, password=password)
32 | DBUser.add(db, db_user)
33 |
34 | request.session['test'] = "test"
35 |
36 | return db_user
37 |
38 |
39 | @router.post("/login")
40 | async def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends(), db: Session = Depends(get_db)):
41 | return Auth.login_authenticate(form_data.username, form_data.password, db)
42 |
43 |
44 | @router.post("/users/me/", response_model=User)
45 | async def read_users_me(request: Request):
46 | user = request.state.user
47 | return user
48 |
--------------------------------------------------------------------------------
/app/util/common.py:
--------------------------------------------------------------------------------
1 | # -*- encoding: utf-8 -*-
2 | """
3 | Copyright(C) 2018-2019 TEST
4 | All rights reserved
5 |
6 | File : common.py
7 | Time : 2020/08/03 11:46:23
8 | Author : mazhiyong
9 | Version : 1.0
10 | """
11 |
12 | from fastapi.responses import JSONResponse
13 |
14 |
15 | def trueReturn(data, msg):
16 | """ 操作成功结果 """
17 | result = {
18 | "status": True,
19 | "data": data,
20 | "msg": msg
21 | }
22 | return JSONResponse(content=result)
23 |
24 |
25 | def falseReturn(data, msg):
26 | """ 操作成功结果 """
27 | result = {
28 | "status": False,
29 | "data": data,
30 | "msg": msg
31 | }
32 | return JSONResponse(content=result)
33 |
34 |
35 | def trueContent(data, msg):
36 | """ 操作成功结果 """
37 | result = {
38 | "status": True,
39 | "data": data,
40 | "msg": msg
41 | }
42 | return result
43 |
44 |
45 | def falseContent(data, msg):
46 | """ 操作成功结果 """
47 | result = {
48 | "status": False,
49 | "data": data,
50 | "msg": msg
51 | }
52 | return result
53 |
--------------------------------------------------------------------------------
/app/util/dateencoder.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """
3 | Copyright (C) 2018 PPCMS
4 | All rights reserved
5 |
6 | Filename : dateencoder.py
7 | Description : 处理datetime类型数据json转化异常问题.
8 | ex:datetime.datetime is not JSON serializable
9 |
10 | Created by mazhiyong at 2018-11-01 14:42:26
11 | """
12 |
13 | import datetime
14 | import json
15 |
16 |
17 | class DateEncoder(json.JSONEncoder):
18 | def default(self, obj):
19 | if isinstance(obj, datetime.datetime):
20 | print(obj.strftime('%Y-%m-%d %H:%M:%S'))
21 | return obj.strftime('%Y-%m-%d %H:%M:%S')
22 | else:
23 | return json.JSONEncoder.default(self, obj)
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/app/util/sms.py:
--------------------------------------------------------------------------------
1 | # -*- coding: UTF-8 -*-
2 | # Copyright (c) 2014 The CCP project authors. All Rights Reserved.
3 | #
4 | # Use of this source code is governed by a Beijing Speedtong Information Technology Co.,Ltd license
5 | # that can be found in the LICENSE file in the root of the web site.
6 | #
7 | # http://www.yuntongxun.com
8 | #
9 | # An additional intellectual property rights grant can be found
10 | # in the file PATENTS. All contributing project authors may
11 | # be found in the AUTHORS file in the root of the source tree.
12 |
13 | import hashlib
14 | import base64
15 | import datetime
16 | import urllib.request
17 | import json
18 |
19 |
20 | class SMS:
21 | STATUS_SUCCESS = '000000'
22 |
23 | AccountSid = ''
24 | AccountToken = ''
25 | AppId = ''
26 | templateid = ''
27 |
28 | ServerIP = 'app.cloopen.com'
29 | ServerPort = '8883'
30 | SoftVersion = '2013-12-26'
31 | Iflog = True # 是否打印日志
32 | Batch = '' # 时间戳
33 | BodyType = 'xml' # 包体格式,可填值:json 、xml
34 |
35 | def log(self, url, body, data):
36 | print('这是请求的URL:')
37 | print(url)
38 | print('这是请求包体:')
39 | print(body)
40 | print('这是响应包体:')
41 | print(data)
42 | print('********************************')
43 |
44 | # 发送模板短信
45 | # @param to 必选参数 短信接收彿手机号码集合,用英文逗号分开
46 | # @param datas 可选参数 内容数据
47 | # @param tempId 必选参数 模板Id
48 | def sendTemplateSMS(self, to, datas):
49 | nowdate = datetime.datetime.now()
50 | self.Batch = nowdate.strftime("%Y%m%d%H%M%S")
51 |
52 | # 生成sig
53 | signature = self.AccountSid + self.AccountToken + self.Batch
54 | sig = hashlib.md5(signature.encode()).hexdigest().upper()
55 |
56 | # 拼接URL
57 | url = "https://" + self.ServerIP + ":" + self.ServerPort + "/" + self.SoftVersion\
58 | + "/Accounts/" + self.AccountSid + "/SMS/TemplateSMS?sig=" + sig
59 | print(url)
60 |
61 | # 生成auth
62 | src = self.AccountSid + ":" + self.Batch
63 | auth = base64.encodestring(src.encode()).strip()
64 | req = urllib.request.Request(url)
65 | self.setHttpHeader(req)
66 | req.add_header("Authorization", auth)
67 |
68 | # 创建包体
69 | b = ''
70 | for a in datas:
71 | b += '%s' % (a)
72 |
73 | body = '' + b + '%s%s%s\
74 | \
75 | ' % (to, self.templateid, self.AppId)
76 |
77 | if self.BodyType == 'json':
78 | # if this model is Json ..then do next code
79 | b = '['
80 | for a in datas:
81 | b += '"%s",' % (a)
82 | b += ']'
83 | body = '''{"to": "%s", "datas": %s, "templateId": "%s", "appId": "%s"}''' % (
84 | to, b, self.templateid, self.AppId)
85 |
86 | # req.add_data(body)
87 |
88 | data = ''
89 | try:
90 | res = urllib.request.urlopen(req, data=body.encode())
91 | data = res.read()
92 | res.close()
93 |
94 | if self.BodyType == 'json':
95 | # json格式
96 | locations = json.loads(data)
97 | else:
98 | # xml格式
99 | xtj = xmltojson()
100 | locations = xtj.main(data)
101 | if self.Iflog:
102 | self.log(url, body, data)
103 | except Exception as error:
104 | print('Exception------------------------')
105 | print(error)
106 | if self.Iflog:
107 | self.log(url, body, data)
108 | locations = {'172001': '网络错误'}
109 |
110 | # parse result
111 | return locations['statusCode'] # '000000' 为成功,其他为失败
112 |
113 | # 设置包头
114 | def setHttpHeader(self, req):
115 | if self.BodyType == 'json':
116 | req.add_header("Accept", "application/json")
117 | req.add_header("Content-Type", "application/json;charset=utf-8")
118 |
119 | else:
120 | req.add_header("Accept", "application/xml")
121 | req.add_header("Content-Type", "application/xml;charset=utf-8")
122 |
--------------------------------------------------------------------------------
/deploy/gunicorn_fast.service:
--------------------------------------------------------------------------------
1 | [Unit]
2 | Description=Gunicorn fast
3 | After=syslog.target network.target remote-fs.target nss-lookup.target
4 |
5 | [Service]
6 | Type=forking
7 |
8 | PIDFile=/data/log/fast.pid
9 |
10 | ExecStart=/root/.virtualenvs/ppcms/bin/gunicorn -c /data/fastest/gunicorn.py -e FASTAPI_ENV=production run:app
11 | ExecReload=/bin/kill -s HUP $MAINPID
12 | ExecStop=/bin/kill -s QUIT $MAINPID
13 | PrivateTmp=true
14 |
15 | [Install]
16 | WantedBy=multi-user.target
17 |
18 |
--------------------------------------------------------------------------------
/deploy/test_user.sql:
--------------------------------------------------------------------------------
1 | /*
2 | Navicat MySQL Data Transfer
3 |
4 | Source Server : pony
5 | Source Server Type : MySQL
6 | Source Server Version : 80018
7 | Source Host : rm-2zee5e5ytvd02o9e9no.mysql.rds.aliyuncs.com
8 | Source Database : bookcrawl
9 |
10 | Target Server Type : MySQL
11 | Target Server Version : 80018
12 | File Encoding : utf-8
13 |
14 | Date: 08/05/2020 10:15:21 AM
15 | */
16 |
17 | SET NAMES utf8;
18 | SET FOREIGN_KEY_CHECKS = 0;
19 |
20 | -- ----------------------------
21 | -- Table structure for `test_user`
22 | -- ----------------------------
23 | DROP TABLE IF EXISTS `test_user`;
24 | CREATE TABLE `test_user` (
25 | `id` int(64) NOT NULL AUTO_INCREMENT COMMENT '编号',
26 | `username` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
27 | `password` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
28 | `sex` varchar(10) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT '' COMMENT '性别',
29 | `login_time` int(11) DEFAULT '0' COMMENT '登陆时间,主要为了登陆JWT校验使用',
30 | `create_date` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
31 | `update_date` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
32 | PRIMARY KEY (`id`,`username`)
33 | ) ENGINE=InnoDB AUTO_INCREMENT=9 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ROW_FORMAT=COMPACT COMMENT='用户表';
34 |
35 | SET FOREIGN_KEY_CHECKS = 1;
36 |
--------------------------------------------------------------------------------
/gunicorn.py:
--------------------------------------------------------------------------------
1 | # gunicorn.py
2 |
3 | debug = True
4 | daemon = True
5 |
6 | bind = '0.0.0.0:9000' # 绑定ip和端口号
7 | # backlog = 512 # 监听队列
8 | chdir = '/data/fastest' # gunicorn要切换到的目的工作目录
9 | timeout = 30 # 超时
10 | # worker_class = 'gevent' #使用gevent模式,还可以使用sync 模式,默认的是sync模式
11 | work_class = 'uvicorn.workers.UvicornWorker'
12 |
13 | # workers = multiprocessing.cpu_count() * 2 + 1 # 进程数
14 | # threads = 2 #指定每个进程开启的线程数
15 | loglevel = 'debug' # 日志级别,这个日志级别指的是错误日志的级别,而访问日志的级别无法设置
16 | access_log_format = '%(t)s %(p)s %(h)s "%(r)s" %(s)s %(L)s %(b)s %(f)s" "%(a)s"' # 设置gunicorn访问日志格式,错误日志无法设置
17 |
18 | """
19 | 其每个选项的含义如下:
20 | h remote address
21 | l '-'
22 | u currently '-', may be user name in future releases
23 | t date of the request
24 | r status line (e.g. ``GET / HTTP/1.1``)
25 | s status
26 | b response length or '-'
27 | f referer
28 | a user agent
29 | T request time in seconds
30 | D request time in microseconds
31 | L request time in decimal seconds
32 | p process ID
33 | """
34 |
35 | pidfile = "/data/log/fast.pid"
36 | accesslog = "/data/log/gunicorn_fasttest_access.log" # 访问日志文件
37 | errorlog = "/data/log/gunicorn_fasttest_error.log" # 错误日志文件
38 |
--------------------------------------------------------------------------------
/local.py:
--------------------------------------------------------------------------------
1 | # -*- encoding: utf-8 -*-
2 | """
3 | Copyright(C) 2018-2019 PAPERPIE
4 | All rights reserved
5 |
6 | File : run.py
7 | Time : 2019/03/20 16:21:11
8 | Author : mazhiyong
9 | Version : 1.0
10 | """
11 |
12 | from app import create_app
13 |
14 | app = create_app()
15 |
--------------------------------------------------------------------------------
/run.py:
--------------------------------------------------------------------------------
1 | # -*- encoding: utf-8 -*-
2 | """
3 | Copyright(C) 2018-2019 PAPERPIE
4 | All rights reserved
5 |
6 | File : run.py
7 | Time : 2019/03/20 16:21:11
8 | Author : mazhiyong
9 | Version : 1.0
10 | """
11 |
12 | from app import create_app
13 | import logging
14 | from fastapi.logger import logger as fastapi_logger
15 | from logging.handlers import RotatingFileHandler
16 |
17 | app = create_app()
18 |
19 |
20 | # 将日志保存到文件中
21 | formatter = logging.Formatter(
22 | "[%(asctime)s.%(msecs)03d] %(levelname)s [%(thread)d] - %(message)s", "%Y-%m-%d %H:%M:%S")
23 | handler = RotatingFileHandler('/data/log/fastapi.log', backupCount=0)
24 | logging.getLogger().setLevel(logging.NOTSET)
25 | fastapi_logger.addHandler(handler)
26 | handler.setFormatter(formatter)
27 |
28 | fastapi_logger.info('****************** Starting Server *****************')
29 |
--------------------------------------------------------------------------------