├── .gitignore ├── Procfile ├── README.md ├── migrations ├── README ├── alembic.ini ├── env.py ├── script.py.mako └── versions │ └── 98de763cc1bd_.py ├── requirements.txt ├── restdemo ├── __init__.py ├── app.py ├── config.py ├── model │ ├── __init__.py │ ├── base.py │ ├── demo.py │ ├── tweet.py │ └── user.py ├── resource │ ├── __init__.py │ ├── hello.py │ ├── tweet.py │ └── user.py ├── tests │ ├── __init__.py │ ├── base.py │ ├── test_login.py │ ├── test_tweet.py │ ├── test_user.py │ └── test_user_list.py └── wsgi.py └── tox.ini /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | *.egg-info/ 23 | .installed.cfg 24 | *.egg 25 | 26 | # PyInstaller 27 | # Usually these files are written by a python script from a template 28 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 29 | *.manifest 30 | *.spec 31 | 32 | # Installer logs 33 | pip-log.txt 34 | pip-delete-this-directory.txt 35 | 36 | # Unit test / coverage reports 37 | htmlcov/ 38 | .tox/ 39 | .coverage 40 | .coverage.* 41 | .cache 42 | nosetests.xml 43 | coverage.xml 44 | *,cover 45 | cover/ 46 | .testrepository 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | 55 | # Sphinx documentation 56 | docs/_build/ 57 | 58 | # PyBuilder 59 | target/ 60 | .idea/ 61 | etc/yabgp/yabgp.ini 62 | 63 | # Vagrantfile 64 | .vagrant/ 65 | 66 | AUTHORS 67 | build-stamp 68 | ChangeLog 69 | covhtml/ 70 | doc/build 71 | *.DS_Store 72 | 73 | .vscode/ 74 | .env/ 75 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: gunicorn -w 4 restdemo.wsgi:application -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Flask REST demo 2 | 3 | ## Development 4 | 5 | install requirements 6 | 7 | ``` 8 | $ pip install -r requirements.txt 9 | ``` 10 | 11 | set database url and app 12 | 13 | ``` 14 | $ export DATABASE_URL=mysql+pymysql://root:root@localhost:3306/demo 15 | $ export FLASK_APP="restdemo:create_app()" 16 | ``` 17 | 18 | inital database tables 19 | 20 | ``` 21 | $ flask db init 22 | $ flask db migrate 23 | $ flask db upgrade 24 | ``` 25 | 26 | run the application 27 | 28 | ``` 29 | $ flask run 30 | * Tip: There are .env files present. Do "pip install python-dotenv" to use them. 31 | * Serving Flask app "restdemo:create_app()" 32 | * Environment: production 33 | WARNING: Do not use the development server in a production environment. 34 | Use a production WSGI server instead. 35 | * Debug mode: off 36 | * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit) 37 | 38 | ``` 39 | 40 | ## Testing 41 | 42 | ``` 43 | $ pip install -r requirements.txt 44 | $ python -m unittest discover 45 | ``` 46 | 47 | ## Production 48 | 49 | install requirements 50 | 51 | ``` 52 | $ pip install -r requirements.txt 53 | ``` 54 | 55 | set database url and app 56 | 57 | ``` 58 | $ export DATABASE_URL=mysql+pymysql://root:root@localhost:3306/demo 59 | $ export FLASK_APP="restdemo:create_app()" 60 | ``` 61 | 62 | inital database tables 63 | 64 | ``` 65 | $ flask db init 66 | $ flask db migrate 67 | $ flask db upgrade 68 | ``` 69 | 70 | run the application 71 | 72 | ``` 73 | $ pip install gunicorn 74 | $ gunicorn -w 4 --bind=0.0.0.0:8000 restdemo.wsgi:application 75 | ``` 76 | 77 | -------------------------------------------------------------------------------- /migrations/README: -------------------------------------------------------------------------------- 1 | Generic single-database configuration. -------------------------------------------------------------------------------- /migrations/alembic.ini: -------------------------------------------------------------------------------- 1 | # A generic, single database configuration. 2 | 3 | [alembic] 4 | # template used to generate migration files 5 | # file_template = %%(rev)s_%%(slug)s 6 | 7 | # set to 'true' to run the environment during 8 | # the 'revision' command, regardless of autogenerate 9 | # revision_environment = false 10 | 11 | 12 | # Logging configuration 13 | [loggers] 14 | keys = root,sqlalchemy,alembic 15 | 16 | [handlers] 17 | keys = console 18 | 19 | [formatters] 20 | keys = generic 21 | 22 | [logger_root] 23 | level = WARN 24 | handlers = console 25 | qualname = 26 | 27 | [logger_sqlalchemy] 28 | level = WARN 29 | handlers = 30 | qualname = sqlalchemy.engine 31 | 32 | [logger_alembic] 33 | level = INFO 34 | handlers = 35 | qualname = alembic 36 | 37 | [handler_console] 38 | class = StreamHandler 39 | args = (sys.stderr,) 40 | level = NOTSET 41 | formatter = generic 42 | 43 | [formatter_generic] 44 | format = %(levelname)-5.5s [%(name)s] %(message)s 45 | datefmt = %H:%M:%S 46 | -------------------------------------------------------------------------------- /migrations/env.py: -------------------------------------------------------------------------------- 1 | from __future__ import with_statement 2 | 3 | import logging 4 | from logging.config import fileConfig 5 | 6 | from sqlalchemy import engine_from_config 7 | from sqlalchemy import pool 8 | 9 | from alembic import context 10 | 11 | # this is the Alembic Config object, which provides 12 | # access to the values within the .ini file in use. 13 | config = context.config 14 | 15 | # Interpret the config file for Python logging. 16 | # This line sets up loggers basically. 17 | fileConfig(config.config_file_name) 18 | logger = logging.getLogger('alembic.env') 19 | 20 | # add your model's MetaData object here 21 | # for 'autogenerate' support 22 | # from myapp import mymodel 23 | # target_metadata = mymodel.Base.metadata 24 | from flask import current_app 25 | config.set_main_option('sqlalchemy.url', 26 | current_app.config.get('SQLALCHEMY_DATABASE_URI')) 27 | target_metadata = current_app.extensions['migrate'].db.metadata 28 | 29 | # other values from the config, defined by the needs of env.py, 30 | # can be acquired: 31 | # my_important_option = config.get_main_option("my_important_option") 32 | # ... etc. 33 | 34 | 35 | def run_migrations_offline(): 36 | """Run migrations in 'offline' mode. 37 | 38 | This configures the context with just a URL 39 | and not an Engine, though an Engine is acceptable 40 | here as well. By skipping the Engine creation 41 | we don't even need a DBAPI to be available. 42 | 43 | Calls to context.execute() here emit the given string to the 44 | script output. 45 | 46 | """ 47 | url = config.get_main_option("sqlalchemy.url") 48 | context.configure( 49 | url=url, target_metadata=target_metadata, literal_binds=True 50 | ) 51 | 52 | with context.begin_transaction(): 53 | context.run_migrations() 54 | 55 | 56 | def run_migrations_online(): 57 | """Run migrations in 'online' mode. 58 | 59 | In this scenario we need to create an Engine 60 | and associate a connection with the context. 61 | 62 | """ 63 | 64 | # this callback is used to prevent an auto-migration from being generated 65 | # when there are no changes to the schema 66 | # reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html 67 | def process_revision_directives(context, revision, directives): 68 | if getattr(config.cmd_opts, 'autogenerate', False): 69 | script = directives[0] 70 | if script.upgrade_ops.is_empty(): 71 | directives[:] = [] 72 | logger.info('No changes in schema detected.') 73 | 74 | connectable = engine_from_config( 75 | config.get_section(config.config_ini_section), 76 | prefix='sqlalchemy.', 77 | poolclass=pool.NullPool, 78 | ) 79 | 80 | with connectable.connect() as connection: 81 | context.configure( 82 | connection=connection, 83 | target_metadata=target_metadata, 84 | process_revision_directives=process_revision_directives, 85 | **current_app.extensions['migrate'].configure_args 86 | ) 87 | 88 | with context.begin_transaction(): 89 | context.run_migrations() 90 | 91 | 92 | if context.is_offline_mode(): 93 | run_migrations_offline() 94 | else: 95 | run_migrations_online() 96 | -------------------------------------------------------------------------------- /migrations/script.py.mako: -------------------------------------------------------------------------------- 1 | """${message} 2 | 3 | Revision ID: ${up_revision} 4 | Revises: ${down_revision | comma,n} 5 | Create Date: ${create_date} 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | ${imports if imports else ""} 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = ${repr(up_revision)} 14 | down_revision = ${repr(down_revision)} 15 | branch_labels = ${repr(branch_labels)} 16 | depends_on = ${repr(depends_on)} 17 | 18 | 19 | def upgrade(): 20 | ${upgrades if upgrades else "pass"} 21 | 22 | 23 | def downgrade(): 24 | ${downgrades if downgrades else "pass"} 25 | -------------------------------------------------------------------------------- /migrations/versions/98de763cc1bd_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: 98de763cc1bd 4 | Revises: 5 | Create Date: 2019-04-06 21:16:18.370983 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = '98de763cc1bd' 14 | down_revision = None 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | op.create_table('user', 22 | sa.Column('id', sa.Integer(), nullable=False), 23 | sa.Column('username', sa.String(length=64), nullable=True), 24 | sa.Column('password_hash', sa.String(length=128), nullable=True), 25 | sa.Column('email', sa.String(length=64), nullable=True), 26 | sa.PrimaryKeyConstraint('id'), 27 | sa.UniqueConstraint('email'), 28 | sa.UniqueConstraint('username') 29 | ) 30 | op.create_table('tweet', 31 | sa.Column('id', sa.Integer(), nullable=False), 32 | sa.Column('user_id', sa.Integer(), nullable=True), 33 | sa.Column('body', sa.String(length=140), nullable=True), 34 | sa.Column('created_at', sa.DateTime(), server_default=sa.text('now()'), nullable=True), 35 | sa.ForeignKeyConstraint(['user_id'], ['user.id'], ), 36 | sa.PrimaryKeyConstraint('id') 37 | ) 38 | # ### end Alembic commands ### 39 | 40 | 41 | def downgrade(): 42 | # ### commands auto generated by Alembic - please adjust! ### 43 | op.drop_table('tweet') 44 | op.drop_table('user') 45 | # ### end Alembic commands ### 46 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | alembic==1.0.8 2 | aniso8601==6.0.0 3 | Click==7.0 4 | Flask==1.0.2 5 | Flask-JWT==0.3.2 6 | Flask-Migrate==2.4.0 7 | Flask-RESTful==0.3.7 8 | Flask-SQLAlchemy==2.3.2 9 | gunicorn==19.9.0 10 | itsdangerous==1.1.0 11 | Jinja2==2.10.1 12 | Mako==1.0.8 13 | MarkupSafe==1.1.1 14 | PyJWT==1.4.2 15 | PyMySQL==0.9.3 16 | python-dateutil==2.8.0 17 | python-editor==1.0.4 18 | pytz==2018.9 19 | six==1.12.0 20 | SQLAlchemy==1.3.1 21 | Werkzeug==0.15.1 22 | #psycopg2==2.8.1 -------------------------------------------------------------------------------- /restdemo/__init__.py: -------------------------------------------------------------------------------- 1 | from flask import Flask 2 | from flask_restful import Api 3 | from flask_sqlalchemy import SQLAlchemy 4 | from flask_migrate import Migrate 5 | from flask_jwt import JWT 6 | 7 | db = SQLAlchemy() 8 | migrate = Migrate() 9 | 10 | from restdemo.model.user import User as UserModel 11 | from restdemo.resource.user import User, UserList 12 | from restdemo.resource.hello import Helloworld 13 | from restdemo.resource.tweet import Tweet 14 | from restdemo.config import app_config 15 | 16 | jwt = JWT(None, UserModel.authenticate, UserModel.identity) 17 | 18 | 19 | def create_app(config_name='development'): 20 | 21 | app = Flask(__name__) 22 | api = Api(app) 23 | app.config.from_object(app_config[config_name]) 24 | db.init_app(app) 25 | migrate.init_app(app, db) 26 | jwt.init_app(app) 27 | 28 | api.add_resource(Helloworld, '/') 29 | api.add_resource(User, '/user/') 30 | api.add_resource(UserList, '/users') 31 | api.add_resource(Tweet, '/tweet/') 32 | return app 33 | -------------------------------------------------------------------------------- /restdemo/app.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/udemy-course/flask-rest-demo/00dfe6f27cd0cdbbd1ed64a9af57a57b6f0667c9/restdemo/app.py -------------------------------------------------------------------------------- /restdemo/config.py: -------------------------------------------------------------------------------- 1 | from datetime import timedelta 2 | import os 3 | 4 | config_path = os.path.abspath(os.path.dirname(__file__)) 5 | 6 | 7 | class Config: 8 | 9 | SQLALCHEMY_TRACK_MODIFICATIONS = False 10 | JWT_EXPIRATION_DELTA = timedelta(seconds=300) 11 | JWT_AUTH_URL_RULE = '/auth/login' 12 | JWT_AUTH_HEADER_PREFIX = os.environ.get('JWT_AUTH_HEADER_PREFIX', 'FLASK') 13 | SECRET_KEY = os.environ.get('SECRET_KEY', 'flask123') 14 | SQLALCHEMY_DATABASE_URI = os.environ.get("DATABASE_URL") 15 | 16 | 17 | class TestingConfig(Config): 18 | SQLALCHEMY_DATABASE_URI = 'sqlite:///:memory:' 19 | 20 | 21 | class DevelopmentConfig(Config): 22 | DEBUG = True 23 | 24 | 25 | class ProductionConfig(Config): 26 | pass 27 | 28 | 29 | app_config = { 30 | 'testing': TestingConfig, 31 | 'development': DevelopmentConfig, 32 | 'production': ProductionConfig 33 | } 34 | -------------------------------------------------------------------------------- /restdemo/model/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/udemy-course/flask-rest-demo/00dfe6f27cd0cdbbd1ed64a9af57a57b6f0667c9/restdemo/model/__init__.py -------------------------------------------------------------------------------- /restdemo/model/base.py: -------------------------------------------------------------------------------- 1 | from restdemo import db 2 | 3 | 4 | class Base(db.Model): 5 | 6 | __abstract__ = True 7 | 8 | def as_dict(self): 9 | return { 10 | c.name: getattr(self, c.name) for c in self.__table__.columns 11 | } 12 | 13 | def add(self): 14 | db.session.add(self) 15 | db.session.commit() 16 | 17 | def delete(self): 18 | db.session.delete(self) 19 | db.session.commit() 20 | 21 | def update(self): 22 | db.session.commit() 23 | -------------------------------------------------------------------------------- /restdemo/model/demo.py: -------------------------------------------------------------------------------- 1 | from restdemo import db 2 | 3 | 4 | class Demo(db.Model): 5 | id = db.Column(db.Integer, primary_key=True) 6 | username = db.Column(db.String(64), unique=True) 7 | age = db.Column(db.Integer) 8 | -------------------------------------------------------------------------------- /restdemo/model/tweet.py: -------------------------------------------------------------------------------- 1 | 2 | from sqlalchemy import ForeignKey, func 3 | 4 | from restdemo import db 5 | from restdemo.model.base import Base 6 | 7 | 8 | class Tweet(Base): 9 | 10 | id = db.Column(db.Integer, primary_key=True) 11 | user_id = db.Column(db.Integer, ForeignKey('user.id')) 12 | body = db.Column(db.String(140)) 13 | created_at = db.Column(db.DateTime, server_default=func.now()) 14 | 15 | def __repr__(self): 16 | return "user_id={}, tweet={}".format( 17 | self.user_id, self.body 18 | ) 19 | 20 | def as_dict(self): 21 | t = {c.name: getattr(self, c.name) for c in self.__table__.columns} 22 | t['created_at'] = t['created_at'].isoformat() 23 | return t 24 | -------------------------------------------------------------------------------- /restdemo/model/user.py: -------------------------------------------------------------------------------- 1 | from werkzeug.security import generate_password_hash, check_password_hash 2 | from sqlalchemy.orm import relationship 3 | 4 | from restdemo import db 5 | from restdemo.model.base import Base 6 | 7 | 8 | class User(Base): 9 | 10 | id = db.Column(db.Integer, primary_key=True) 11 | username = db.Column(db.String(64), unique=True) 12 | password_hash = db.Column(db.String(128)) 13 | email = db.Column(db.String(64), unique=True) 14 | 15 | tweet = relationship('Tweet') 16 | 17 | def __repr__(self): 18 | return "id={}, username={}".format( 19 | self.id, self.username 20 | ) 21 | 22 | def set_password(self, password): 23 | self.password_hash = generate_password_hash(password) 24 | 25 | def check_password(self, password): 26 | return check_password_hash(self.password_hash, password) 27 | 28 | @staticmethod 29 | def get_by_username(username): 30 | return db.session.query(User).filter( 31 | User.username == username 32 | ).first() 33 | 34 | @staticmethod 35 | def get_by_id(user_id): 36 | return db.session.query(User).filter( 37 | User.id == user_id 38 | ).first() 39 | 40 | @staticmethod 41 | def get_user_list(): 42 | return db.session.query(User).all() 43 | 44 | @staticmethod 45 | def authenticate(username, password): 46 | user = User.get_by_username(username) 47 | if user: 48 | # check password 49 | if user.check_password(password): 50 | return user 51 | 52 | @staticmethod 53 | def identity(payload): 54 | user_id = payload['identity'] 55 | user = User.get_by_id(user_id) 56 | return user 57 | -------------------------------------------------------------------------------- /restdemo/resource/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/udemy-course/flask-rest-demo/00dfe6f27cd0cdbbd1ed64a9af57a57b6f0667c9/restdemo/resource/__init__.py -------------------------------------------------------------------------------- /restdemo/resource/hello.py: -------------------------------------------------------------------------------- 1 | from flask_restful import Resource 2 | 3 | 4 | class Helloworld(Resource): 5 | 6 | def get(self): 7 | return 'hello world' 8 | -------------------------------------------------------------------------------- /restdemo/resource/tweet.py: -------------------------------------------------------------------------------- 1 | from flask_restful import Resource, reqparse 2 | from flask_jwt import jwt_required, current_identity 3 | 4 | from restdemo.model.user import User as UserModel 5 | from restdemo.model.tweet import Tweet as TweetModel 6 | 7 | 8 | class Tweet(Resource): 9 | parser = reqparse.RequestParser() 10 | parser.add_argument( 11 | 'body', type=str, required=True, 12 | help='body required' 13 | ) 14 | 15 | @jwt_required() 16 | def post(self, username): 17 | if current_identity.username != username: 18 | return {'message': 'please use the right token'} 19 | user = UserModel.get_by_username(username) 20 | if not user: 21 | return {'message': 'user not found'}, 404 22 | data = Tweet.parser.parse_args() 23 | tweet = TweetModel(body=data['body'], user_id=user.id) 24 | tweet.add() 25 | return {'message': 'post success'} 26 | 27 | @jwt_required() 28 | def get(self, username): 29 | user = UserModel.get_by_username(username) 30 | if not user: 31 | return {'message': 'user not found'}, 404 32 | return [t.as_dict() for t in user.tweet] 33 | -------------------------------------------------------------------------------- /restdemo/resource/user.py: -------------------------------------------------------------------------------- 1 | from flask_restful import Resource, reqparse 2 | from flask_jwt import jwt_required 3 | 4 | from restdemo.model.user import User as UserModel 5 | 6 | 7 | def min_length_str(min_length): 8 | def validate(s): 9 | if s is None: 10 | raise Exception('password required') 11 | if not isinstance(s, (int, str)): 12 | raise Exception('password format error') 13 | s = str(s) 14 | if len(s) >= min_length: 15 | return s 16 | raise Exception("String must be at least %i characters long" % min_length) 17 | return validate 18 | 19 | 20 | class User(Resource): 21 | 22 | parser = reqparse.RequestParser() 23 | parser.add_argument( 24 | 'password', type=min_length_str(5), required=True, 25 | help='{error_msg}' 26 | ) 27 | parser.add_argument( 28 | 'email', type=str, required=True, help='required email' 29 | ) 30 | 31 | def get(self, username): 32 | """ 33 | get user detail information 34 | """ 35 | user = UserModel.get_by_username(username) 36 | if user: 37 | return user.as_dict() 38 | return {'message': 'user not found'}, 404 39 | 40 | def post(self, username): 41 | """ create a user""" 42 | data = User.parser.parse_args() 43 | user = UserModel.get_by_username(username) 44 | if user: 45 | return {'message': 'user already exist'} 46 | user = UserModel( 47 | username=username, 48 | email=data['email'] 49 | ) 50 | user.set_password(data['password']) 51 | user.add() 52 | return user.as_dict(), 201 53 | 54 | def delete(self, username): 55 | """delete user""" 56 | user = UserModel.get_by_username(username) 57 | if user: 58 | user.delete() 59 | return {'message': 'user deleted'} 60 | else: 61 | return {'message': 'user not found'}, 404 62 | 63 | def put(self, username): 64 | """update user""" 65 | user = UserModel.get_by_username(username) 66 | if user: 67 | data = User.parser.parse_args() 68 | # user.password_hash = data['password'] 69 | user.email = data['email'] 70 | user.set_password(data['password']) 71 | user.update() 72 | return user.as_dict() 73 | else: 74 | return {'message': "user not found"}, 404 75 | 76 | 77 | class UserList(Resource): 78 | 79 | @jwt_required() 80 | def get(self): 81 | users = UserModel.get_user_list() 82 | return [u.as_dict() for u in users] 83 | -------------------------------------------------------------------------------- /restdemo/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/udemy-course/flask-rest-demo/00dfe6f27cd0cdbbd1ed64a9af57a57b6f0667c9/restdemo/tests/__init__.py -------------------------------------------------------------------------------- /restdemo/tests/base.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from restdemo import create_app, db 4 | 5 | 6 | class TestBase(unittest.TestCase): 7 | 8 | def setUp(self): 9 | self.app = create_app(config_name='testing') 10 | self.client = self.app.test_client 11 | self.user_data = { 12 | 'username': 'test', 13 | 'password': 'test123', 14 | 'email': 'test@test.com' 15 | } 16 | with self.app.app_context(): 17 | db.create_all() 18 | 19 | def tearDown(self): 20 | with self.app.app_context(): 21 | db.session.remove() 22 | db.drop_all() 23 | -------------------------------------------------------------------------------- /restdemo/tests/test_login.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from restdemo.tests.base import TestBase 4 | 5 | 6 | class TestLogin(TestBase): 7 | 8 | def test_login(self): 9 | url = '/user/{}'.format(self.user_data['username']) 10 | self.client().post( 11 | url, 12 | data=self.user_data 13 | ) 14 | url = '/auth/login' 15 | res = self.client().post( 16 | url, 17 | data=json.dumps({'username': 'test', 'password': 'test123'}), 18 | headers={'Content-Type': 'application/json'} 19 | ) 20 | self.assertEqual(res.status_code, 200) 21 | res_data = json.loads(res.get_data(as_text=True)) 22 | self.assertIn('access_token', res_data) 23 | 24 | def test_login_failed(self): 25 | url = '/user/{}'.format(self.user_data['username']) 26 | self.client().post( 27 | url, 28 | data=self.user_data 29 | ) 30 | url = '/auth/login' 31 | res = self.client().post( 32 | url, 33 | data=json.dumps({'username': 'test', 'password': 'wrongpass'}), 34 | headers={'Content-Type': 'application/json'} 35 | ) 36 | self.assertEqual(res.status_code, 401) 37 | res_data = json.loads(res.get_data(as_text=True)) 38 | data = { 39 | "description": "Invalid credentials", 40 | "error": "Bad Request", 41 | "status_code": 401 42 | } 43 | self.assertEqual(res_data, data) 44 | -------------------------------------------------------------------------------- /restdemo/tests/test_tweet.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from restdemo.tests.base import TestBase 4 | 5 | 6 | class TestTweet(TestBase): 7 | 8 | def test_tweet(self): 9 | # create user 10 | url = '/user/{}'.format(self.user_data['username']) 11 | self.client().post( 12 | url, 13 | data=self.user_data 14 | ) 15 | # user login 16 | url = '/auth/login' 17 | res = self.client().post( 18 | url, 19 | data=json.dumps({'username': 'test', 'password': 'test123'}), 20 | headers={'Content-Type': 'application/json'} 21 | ) 22 | res_data = json.loads(res.get_data(as_text=True)) 23 | access_token = '{} {}'.format( 24 | self.app.config['JWT_AUTH_HEADER_PREFIX'], 25 | res_data['access_token'] 26 | ) 27 | # post tweet 28 | url = '/tweet/{}'.format(self.user_data['username']) 29 | res = self.client().post( 30 | url, 31 | data={'body': 'hello world'}, 32 | headers={'Authorization': access_token} 33 | ) 34 | res_data = json.loads(res.get_data(as_text=True)) 35 | self.assertEqual(res.status_code, 200) 36 | self.assertEqual(res_data, {'message': 'post success'}) 37 | 38 | # get tweet 39 | url = '/tweet/{}'.format(self.user_data['username']) 40 | res = self.client().get( 41 | url, 42 | headers={'Authorization': access_token} 43 | ) 44 | res_data = json.loads(res.get_data(as_text=True)) 45 | self.assertEqual(res.status_code, 200) 46 | self.assertEqual(len(res_data), 1) 47 | -------------------------------------------------------------------------------- /restdemo/tests/test_user.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import json 3 | 4 | from restdemo import create_app, db 5 | 6 | 7 | class TestUser(unittest.TestCase): 8 | 9 | def setUp(self): 10 | self.app = create_app(config_name='testing') 11 | self.client = self.app.test_client 12 | self.user_data = { 13 | 'username': 'test', 14 | 'password': 'test123', 15 | 'email': 'test@test.com' 16 | } 17 | with self.app.app_context(): 18 | db.create_all() 19 | 20 | def tearDown(self): 21 | with self.app.app_context(): 22 | db.session.remove() 23 | db.drop_all() 24 | 25 | def test_user_create(self): 26 | url = '/user/{}'.format(self.user_data['username']) 27 | res = self.client().post( 28 | url, 29 | data=self.user_data 30 | ) 31 | self.assertEqual(res.status_code, 201) 32 | res_data = json.loads(res.get_data(as_text=True)) 33 | self.assertEqual(res_data.get('username'), self.user_data['username']) 34 | self.assertEqual(res_data.get('email'), self.user_data['email']) 35 | 36 | res = self.client().post( 37 | url, 38 | data=self.user_data 39 | ) 40 | self.assertEqual(res.status_code, 200) 41 | res_data = json.loads(res.get_data(as_text=True)) 42 | self.assertEqual(res_data.get('message'), 'user already exist') 43 | 44 | def test_user_get(self): 45 | url = '/user/{}'.format(self.user_data['username']) 46 | res = self.client().post( 47 | url, 48 | data=self.user_data 49 | ) 50 | res = self.client().get(url) 51 | res_data = json.loads(res.get_data(as_text=True)) 52 | self.assertEqual(res.status_code, 200) 53 | self.assertEqual(res_data.get('username'), self.user_data['username']) 54 | self.assertEqual(res_data.get('email'), self.user_data['email']) 55 | 56 | def test_user_get_not_exist(self): 57 | url = '/user/{}'.format(self.user_data['username']) 58 | res = self.client().get(url) 59 | res_data = json.loads(res.get_data(as_text=True)) 60 | self.assertEqual(res.status_code, 404) 61 | self.assertEqual(res_data, {'message': 'user not found'}) 62 | 63 | def test_user_delete(self): 64 | url = '/user/{}'.format(self.user_data['username']) 65 | self.client().post( 66 | url, 67 | data=self.user_data 68 | ) 69 | res = self.client().delete(url) 70 | res_data = json.loads(res.get_data(as_text=True)) 71 | self.assertEqual(res.status_code, 200) 72 | self.assertEqual(res_data, {'message': 'user deleted'}) 73 | 74 | def test_user_delete_not_exist(self): 75 | url = '/user/{}'.format(self.user_data['username']) 76 | res = self.client().delete(url) 77 | res_data = json.loads(res.get_data(as_text=True)) 78 | self.assertEqual(res.status_code, 404) 79 | self.assertEqual(res_data, {'message': 'user not found'}) 80 | 81 | def test_user_update(self): 82 | url = '/user/{}'.format(self.user_data['username']) 83 | self.client().post( 84 | url, 85 | data=self.user_data 86 | ) 87 | res = self.client().put( 88 | url, 89 | data={ 90 | 'password': 'newpassword', 91 | 'email': 'newemail@new.com' 92 | } 93 | ) 94 | res_data = json.loads(res.get_data(as_text=True)) 95 | self.assertEqual(res.status_code, 200) 96 | self.assertEqual(res_data['email'], 'newemail@new.com') 97 | 98 | def test_user_update_not_exist(self): 99 | url = '/user/{}'.format(self.user_data['username']) 100 | res = self.client().put( 101 | url, 102 | data={ 103 | 'password': 'newpassword', 104 | 'email': 'newemail@new.com' 105 | } 106 | ) 107 | res_data = json.loads(res.get_data(as_text=True)) 108 | self.assertEqual(res.status_code, 404) 109 | self.assertEqual(res_data, {'message': 'user not found'}) 110 | -------------------------------------------------------------------------------- /restdemo/tests/test_user_list.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from restdemo.tests.base import TestBase 4 | 5 | 6 | class TestUserList(TestBase): 7 | 8 | def test_get_user_list(self): 9 | # create user 10 | url = '/user/{}'.format(self.user_data['username']) 11 | self.client().post( 12 | url, 13 | data=self.user_data 14 | ) 15 | # user login 16 | url = '/auth/login' 17 | res = self.client().post( 18 | url, 19 | data=json.dumps({'username': 'test', 'password': 'test123'}), 20 | headers={'Content-Type': 'application/json'} 21 | ) 22 | res_data = json.loads(res.get_data(as_text=True)) 23 | access_token = '{} {}'.format( 24 | self.app.config['JWT_AUTH_HEADER_PREFIX'], 25 | res_data['access_token'] 26 | ) 27 | # get user list 28 | url = '/users' 29 | res = self.client().get( 30 | url, 31 | headers={'Authorization': access_token} 32 | ) 33 | res_data = json.loads(res.get_data(as_text=True)) 34 | self.assertEqual(res.status_code, 200) 35 | self.assertEqual(len(res_data), 1) 36 | -------------------------------------------------------------------------------- /restdemo/wsgi.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | sys.path.insert(0, os.getcwd()) 5 | 6 | from restdemo import create_app 7 | 8 | # Create an application instance that web servers can use. We store it as 9 | # "application" (the wsgi default) and also the much shorter and convenient 10 | # "app". 11 | application = create_app(config_name='production') 12 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | # Tox (http://tox.testrun.org/) is a tool for running tests 2 | # in multiple virtualenvs. This configuration file will run the 3 | # test suite on all supported python versions. To use it, "pip install tox" 4 | # and then run "tox" from this directory. 5 | 6 | [tox] 7 | # List the environment that will be run by default 8 | minversion = 1.6 9 | envlist = pep8, unittest 10 | skipsdist = True 11 | 12 | [testenv] 13 | setenv = VIRTUAL_ENV={envdir} 14 | LANG=en_US.UTF-8 15 | LANGUAGE=en_US:en 16 | LC_ALL=C 17 | 18 | 19 | [testenv:pep8] 20 | basepython = python3.7 21 | sitepackages = False 22 | deps = flake8 23 | commands = 24 | flake8 {posargs} 25 | 26 | [flake8] 27 | ignore = I101,I100,I201,E402,E722,E731,F811 28 | max-line-length=120 29 | exclude = .venv,.git,.tox,dist,doc,*lib/python*,*egg,build,tools, migrations 30 | 31 | [testenv:unittest] 32 | basepython = python3.7 33 | deps = -r{toxinidir}/requirements.txt 34 | commands = python -m unittest discover --------------------------------------------------------------------------------