├── .gitignore ├── README.md ├── chapter1 ├── git-diff-config.sh └── zshrc ├── chapter10 ├── app.py ├── config.py ├── model │ ├── __init__.py │ ├── tweet_dao.py │ └── user_dao.py ├── requirements.txt ├── service │ ├── __init__.py │ ├── tweet_service.py │ └── user_service.py ├── setup.py ├── test │ ├── test_model.py │ ├── test_service.py │ └── test_view.py └── view │ └── __init__.py ├── chapter11 ├── app.py ├── config.py ├── model │ ├── __init__.py │ ├── tweet_dao.py │ └── user_dao.py ├── profile_picture_add_column.sql ├── profile_pictures │ └── symbol.png ├── requirements.txt ├── service │ ├── __init__.py │ ├── tweet_service.py │ └── user_service.py ├── setup.py ├── test │ ├── test_model.py │ ├── test_service.py │ └── test_view.py └── view │ └── __init__.py ├── chapter3 └── app.py ├── chapter5 ├── app.py ├── follow_and_unfollow_example.py ├── requirements.txt ├── sign_up_example.py ├── timeline_example.py └── tweet_example.py ├── chapter6 ├── app.py ├── config.py ├── create_table.sql ├── delete_example.sql ├── insert_example.sql ├── join_example.sql ├── select_example.sql ├── select_with_where_example.sql └── update_example.sql ├── chapter7 ├── app.py ├── bcrypt.py ├── config.py ├── decorator_example.py ├── hashlib_example.py └── pyjwt_example.py ├── chapter8 ├── app.py ├── config.py ├── mult.py └── test_endpoints.py ├── chapter9 └── setup.py ├── python_book_img.jpg ├── wecode.png └── wecode_mentors.png /.gitignore: -------------------------------------------------------------------------------- 1 | # Vim 2 | *.*~ 3 | 4 | # Byte-compiled / optimized / DLL files 5 | __pycache__/ 6 | *.py[cod] 7 | *$py.class 8 | 9 | # C extensions 10 | *.so 11 | 12 | # Distribution / packaging 13 | .Python 14 | build/ 15 | develop-eggs/ 16 | dist/ 17 | downloads/ 18 | eggs/ 19 | .eggs/ 20 | lib/ 21 | lib64/ 22 | parts/ 23 | sdist/ 24 | var/ 25 | wheels/ 26 | *.egg-info/ 27 | .installed.cfg 28 | *.egg 29 | MANIFEST 30 | 31 | # PyInstaller 32 | # Usually these files are written by a python script from a template 33 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 34 | *.manifest 35 | *.spec 36 | 37 | # Installer logs 38 | pip-log.txt 39 | pip-delete-this-directory.txt 40 | 41 | # Unit test / coverage reports 42 | htmlcov/ 43 | .tox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | 53 | # Translations 54 | *.mo 55 | *.pot 56 | 57 | # Django stuff: 58 | *.log 59 | local_settings.py 60 | db.sqlite3 61 | 62 | # Flask stuff: 63 | instance/ 64 | .webassets-cache 65 | 66 | # Scrapy stuff: 67 | .scrapy 68 | 69 | # Sphinx documentation 70 | docs/_build/ 71 | 72 | # PyBuilder 73 | target/ 74 | 75 | # Jupyter Notebook 76 | .ipynb_checkpoints 77 | 78 | # pyenv 79 | .python-version 80 | 81 | # celery beat schedule file 82 | celerybeat-schedule 83 | 84 | # SageMath parsed files 85 | *.sage.py 86 | 87 | # Environments 88 | .env 89 | .venv 90 | env/ 91 | venv/ 92 | ENV/ 93 | env.bak/ 94 | venv.bak/ 95 | 96 | # Spyder project settings 97 | .spyderproject 98 | .spyproject 99 | 100 | # Rope project settings 101 | .ropeproject 102 | 103 | # mkdocs documentation 104 | /site 105 | 106 | # mypy 107 | .mypy_cache/ 108 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [wecode.co.kr](https://wecode.co.kr) 2 | 3 | ![](./wecode.png) 4 | 5 | ![](./wecode_mentors.png) 6 | 7 | ![](./python_book_img.jpg) 8 | -------------------------------------------------------------------------------- /chapter1/git-diff-config.sh: -------------------------------------------------------------------------------- 1 | git config --global color.diff.meta 2 | git config --global color.diff.frag 3 | git config --global color.diff.commit 4 | git config --global color.diff.old 5 | git config --global color.diff.new 6 | git config --global color.diff.whitespace "red reverse" 7 | 8 | ## 참고로 저자는 다음과 같은 diff-highlight 색상을 선호한다. 9 | ## 그 이유는 저자는 Solarized light 테마를 사용하고 있는데, 다음 색상의 수정 사항들이 눈에 더 잘 띄 기 때문이다. 10 | #git config --global color.diff-highlight.oldNormal "red" 11 | #git config --global color.diff-highlight.oldHighlight "red 217" git config --global color.diff-highlight.newNormal "green" 12 | #git config --global color.diff-highlight.newHighlight "green 157" 13 | 14 | -------------------------------------------------------------------------------- /chapter1/zshrc: -------------------------------------------------------------------------------- 1 | # Path to your oh-my-zsh installation. export ZSH=$HOME/.oh-my-zsh 2 | ZSH_THEME='agnoster' 3 | 4 | plugins=( 5 | git 6 | osx 7 | autojump 8 | scala 9 | python 10 | pip 11 | github 12 | gnu-utils zsh-syntax-highlighting history-substring-search colored-man-pages 13 | ) 14 | 15 | source $ZSH/oh-my-zsh.sh 16 | source $(brew --prefix autoenv)/activate.sh 17 | source /usr/local/share/zsh-syntax-highlighting/zsh-syntax- highlighting.zsh 18 | -------------------------------------------------------------------------------- /chapter10/app.py: -------------------------------------------------------------------------------- 1 | import config 2 | 3 | from flask import Flask 4 | from sqlalchemy import create_engine 5 | from flask_cors import CORS 6 | 7 | from model import UserDao, TweetDao 8 | from service import UserService, TweetService 9 | from view import create_endpoints 10 | 11 | class Services: 12 | pass 13 | 14 | ################################ 15 | # Create App 16 | ################################ 17 | def create_app(test_config = None): 18 | app = Flask(__name__) 19 | 20 | CORS(app) 21 | 22 | if test_config is None: 23 | app.config.from_pyfile("config.py") 24 | else: 25 | app.config.update(test_config) 26 | 27 | database = create_engine(app.config['DB_URL'], encoding = 'utf-8', max_overflow = 0) 28 | 29 | ## Persistenace Layer 30 | user_dao = UserDao(database) 31 | tweet_dao = TweetDao(database) 32 | 33 | ## Business Layer 34 | services = Services 35 | services.user_service = UserService(user_dao, config) 36 | services.tweet_service = TweetService(tweet_dao) 37 | 38 | ## 엔드포인트들을 생성 39 | create_endpoints(app, services) 40 | 41 | return app 42 | -------------------------------------------------------------------------------- /chapter10/config.py: -------------------------------------------------------------------------------- 1 | db = { 2 | 'user' : 'test', 3 | 'password' : 'test1234', 4 | 'host' : 'localhost', 5 | 'port' : 3306, 6 | 'database' : 'miniter' 7 | } 8 | 9 | DB_URL = f"mysql+mysqlconnector://{db['user']}:{db['password']}@{db['host']}:{db['port']}/{db['database']}?charset=utf8" 10 | JWT_SECRET_KEY = 'SOME_SUPER_SECRET_KEY' 11 | JWT_EXP_DELTA_SECONDS = 7 * 24 * 60 * 60 12 | 13 | test_db = { 14 | 'user' : 'root', 15 | 'password' : 'test1234', 16 | 'host' : 'localhost', 17 | 'port' : 3306, 18 | 'database' : 'miniter_test' 19 | } 20 | 21 | test_config = { 22 | 'DB_URL' : f"mysql+mysqlconnector://{test_db['user']}:{test_db['password']}@{test_db['host']}:{test_db['port']}/{test_db['database']}?charset=utf8", 23 | 'JWT_SECRET_KEY' : 'SOME_SUPER_SECRET_KEY', 24 | 'JWT_EXP_DELTA_SECONDS' : 7 * 24 * 60 * 60 25 | } 26 | -------------------------------------------------------------------------------- /chapter10/model/__init__.py: -------------------------------------------------------------------------------- 1 | from .user_dao import UserDao 2 | from .tweet_dao import TweetDao 3 | 4 | __all__ = [ 5 | 'UserDao', 6 | 'TweetDao' 7 | ] 8 | -------------------------------------------------------------------------------- /chapter10/model/tweet_dao.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import text 2 | 3 | class TweetDao: 4 | def __init__(self, database): 5 | self.db = database 6 | 7 | def insert_tweet(self, user_id, tweet): 8 | return self.db.execute(text(""" 9 | INSERT INTO tweets ( 10 | user_id, 11 | tweet 12 | ) VALUES ( 13 | :id, 14 | :tweet 15 | ) 16 | """), { 17 | 'id' : user_id, 18 | 'tweet' : tweet 19 | }).rowcount 20 | 21 | def get_timeline(self, user_id): 22 | timeline = self.db.execute(text(""" 23 | SELECT 24 | t.user_id, 25 | t.tweet 26 | FROM tweets t 27 | LEFT JOIN users_follow_list ufl ON ufl.user_id = :user_id 28 | WHERE t.user_id = :user_id 29 | OR t.user_id = ufl.follow_user_id 30 | """), { 31 | 'user_id' : user_id 32 | }).fetchall() 33 | 34 | return [{ 35 | 'user_id' : tweet['user_id'], 36 | 'tweet' : tweet['tweet'] 37 | } for tweet in timeline] 38 | -------------------------------------------------------------------------------- /chapter10/model/user_dao.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import text 2 | 3 | class UserDao: 4 | def __init__(self, database): 5 | self.db = database 6 | 7 | def insert_user(self, user): 8 | return self.db.execute(text(""" 9 | INSERT INTO users ( 10 | name, 11 | email, 12 | profile, 13 | hashed_password 14 | ) VALUES ( 15 | :name, 16 | :email, 17 | :profile, 18 | :password 19 | ) 20 | """), user).lastrowid 21 | 22 | def get_user_id_and_password(self, email): 23 | row = self.db.execute(text(""" 24 | SELECT 25 | id, 26 | hashed_password 27 | FROM users 28 | WHERE email = :email 29 | """), {'email' : email}).fetchone() 30 | 31 | return { 32 | 'id' : row['id'], 33 | 'hashed_password' : row['hashed_password'] 34 | } if row else None 35 | 36 | def insert_follow(self, user_id, follow_id): 37 | return self.db.execute(text(""" 38 | INSERT INTO users_follow_list ( 39 | user_id, 40 | follow_user_id 41 | ) VALUES ( 42 | :id, 43 | :follow 44 | ) 45 | """), { 46 | 'id' : user_id, 47 | 'follow' : follow_id 48 | }).rowcount 49 | 50 | def insert_unfollow(self, user_id, unfollow_id): 51 | return self.db.execute(text(""" 52 | DELETE FROM users_follow_list 53 | WHERE user_id = :id 54 | AND follow_user_id = :unfollow 55 | """), { 56 | 'id' : user_id, 57 | 'unfollow' : unfollow_id 58 | }).rowcount 59 | -------------------------------------------------------------------------------- /chapter10/requirements.txt: -------------------------------------------------------------------------------- 1 | asn1crypto==0.24.0 2 | atomicwrites==1.2.1 3 | attrs==18.2.0 4 | bcrypt==3.1.4 5 | certifi==2018.8.24 6 | cffi==1.11.5 7 | click==6.7 8 | cryptography==2.4.2 9 | Flask==1.0.2 10 | idna==2.8 11 | itsdangerous==0.24 12 | Jinja2==2.10 13 | MarkupSafe==1.0 14 | more-itertools==4.3.0 15 | mysql-connector-python==8.0.13 16 | pluggy==0.8.0 17 | protobuf==3.6.1 18 | py==1.7.0 19 | pycparser==2.19 20 | PyJWT==1.7.0 21 | pytest==4.0.1 22 | six==1.11.0 23 | SQLAlchemy==1.2.14 24 | Werkzeug==0.14.1 25 | -------------------------------------------------------------------------------- /chapter10/service/__init__.py: -------------------------------------------------------------------------------- 1 | from .user_service import UserService 2 | from .tweet_service import TweetService 3 | 4 | __all__ = [ 5 | 'UserService', 6 | 'TweetService' 7 | ] 8 | -------------------------------------------------------------------------------- /chapter10/service/tweet_service.py: -------------------------------------------------------------------------------- 1 | 2 | class TweetService: 3 | def __init__(self, tweet_dao): 4 | self.tweet_dao = tweet_dao 5 | 6 | def tweet(self, user_id, tweet): 7 | if len(tweet) > 300: 8 | return None 9 | 10 | return self.tweet_dao.insert_tweet(user_id, tweet) 11 | 12 | def get_timeline(self, user_id): 13 | return self.tweet_dao.get_timeline(user_id) 14 | 15 | -------------------------------------------------------------------------------- /chapter10/service/user_service.py: -------------------------------------------------------------------------------- 1 | import jwt 2 | import bcrypt 3 | 4 | from datetime import datetime, timedelta 5 | 6 | class UserService: 7 | def __init__(self, user_dao, config): 8 | self.user_dao = user_dao 9 | self.config = config 10 | 11 | def create_new_user(self, new_user): 12 | new_user['password'] = bcrypt.hashpw( 13 | new_user['password'].encode('UTF-8'), 14 | bcrypt.gensalt() 15 | ) 16 | 17 | new_user_id = self.user_dao.insert_user(new_user) 18 | 19 | return new_user_id 20 | 21 | def login(self, credential): 22 | email = credential['email'] 23 | password = credential['password'] 24 | user_credential = self.user_dao.get_user_id_and_password(email) 25 | 26 | authorized = user_credential and bcrypt.checkpw(password.encode('UTF-8'), user_credential['hashed_password'].encode('UTF-8')) 27 | 28 | return authorized 29 | 30 | def generate_access_token(self, user_id): 31 | payload = { 32 | 'user_id' : user_id, 33 | 'exp' : datetime.utcnow() + timedelta(seconds = 60 * 60 * 24) 34 | } 35 | token = jwt.encode(payload, self.config.JWT_SECRET_KEY, 'HS256') 36 | 37 | return token.decode('UTF-8') 38 | 39 | def follow(self, user_id, follow_id): 40 | return self.user_dao.insert_follow(user_id, follow_id) 41 | 42 | def unfollow(self, user_id, unfollow_id): 43 | return self.user_dao.insert_unfollow(user_id, unfollow_id) 44 | 45 | def get_user_id_and_password(self, email): 46 | return self.user_dao.get_user_id_and_password(email) 47 | -------------------------------------------------------------------------------- /chapter10/setup.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | from flask_script import Manager 4 | from app import create_app 5 | from flask_twisted import Twisted 6 | from twisted.python import log 7 | 8 | if __name__ == "__main__": 9 | app = create_app() 10 | 11 | twisted = Twisted(app) 12 | log.startLogging(sys.stdout) 13 | 14 | app.logger.info(f"Running the app...") 15 | 16 | manager = Manager(app) 17 | manager.run() 18 | -------------------------------------------------------------------------------- /chapter10/test/test_model.py: -------------------------------------------------------------------------------- 1 | import bcrypt 2 | import pytest 3 | import config 4 | 5 | from model import UserDao, TweetDao 6 | from sqlalchemy import create_engine, text 7 | 8 | 9 | database = create_engine(config.test_config['DB_URL'], encoding= 'utf-8', max_overflow = 0) 10 | 11 | @pytest.fixture 12 | def user_dao(): 13 | return UserDao(database) 14 | 15 | @pytest.fixture 16 | def tweet_dao(): 17 | return TweetDao(database) 18 | 19 | def setup_function(): 20 | ## Create a test user 21 | hashed_password = bcrypt.hashpw( 22 | b"test password", 23 | bcrypt.gensalt() 24 | ) 25 | new_users = [ 26 | { 27 | 'id' : 1, 28 | 'name' : '송은우', 29 | 'email' : 'songew@gmail.com', 30 | 'profile' : 'test profile', 31 | 'hashed_password' : hashed_password 32 | }, { 33 | 'id' : 2, 34 | 'name' : '김철수', 35 | 'email' : 'tet@gmail.com', 36 | 'profile' : 'test profile', 37 | 'hashed_password' : hashed_password 38 | } 39 | ] 40 | database.execute(text(""" 41 | INSERT INTO users ( 42 | id, 43 | name, 44 | email, 45 | profile, 46 | hashed_password 47 | ) VALUES ( 48 | :id, 49 | :name, 50 | :email, 51 | :profile, 52 | :hashed_password 53 | ) 54 | """), new_users) 55 | 56 | ## User 2 의 트윗 미리 생성해 놓기 57 | database.execute(text(""" 58 | INSERT INTO tweets ( 59 | user_id, 60 | tweet 61 | ) VALUES ( 62 | 2, 63 | "Hello World!" 64 | ) 65 | """)) 66 | 67 | def teardown_function(): 68 | database.execute(text("SET FOREIGN_KEY_CHECKS=0")) 69 | database.execute(text("TRUNCATE users")) 70 | database.execute(text("TRUNCATE tweets")) 71 | database.execute(text("TRUNCATE users_follow_list")) 72 | database.execute(text("SET FOREIGN_KEY_CHECKS=1")) 73 | 74 | def get_user(user_id): 75 | row = database.execute(text(""" 76 | SELECT 77 | id, 78 | name, 79 | email, 80 | profile 81 | FROM users 82 | WHERE id = :user_id 83 | """), { 84 | 'user_id' : user_id 85 | }).fetchone() 86 | 87 | return { 88 | 'id' : row['id'], 89 | 'name' : row['name'], 90 | 'email' : row['email'], 91 | 'profile' : row['profile'] 92 | } if row else None 93 | 94 | def get_follow_list(user_id): 95 | rows = database.execute(text(""" 96 | SELECT follow_user_id as id 97 | FROM users_follow_list 98 | WHERE user_id = :user_id 99 | """), { 100 | 'user_id' : user_id 101 | }).fetchall() 102 | 103 | return [int(row['id']) for row in rows] 104 | 105 | def test_insert_user(user_dao): 106 | new_user = { 107 | 'name' : '홍길동', 108 | 'email' : 'hong@test.com', 109 | 'profile' : '서쪽에서 번쩍, 동쪽에서 번쩍', 110 | 'password' : 'test1234' 111 | } 112 | 113 | new_user_id = user_dao.insert_user(new_user) 114 | user = get_user(new_user_id) 115 | 116 | assert user == { 117 | 'id' : new_user_id, 118 | 'name' : new_user['name'], 119 | 'email' : new_user['email'], 120 | 'profile' : new_user['profile'] 121 | } 122 | 123 | def test_get_user_id_and_password(user_dao): 124 | ## get_user_id_and_password 메소드를 호출 하여 유저의 아이디와 비밀번호 해시 값을 읽어들인다. 125 | ## 유저는 이미 setup_function 에서 생성된 유저를 사용한다. 126 | user_credential = user_dao.get_user_id_and_password(email = 'songew@gmail.com') 127 | 128 | ## 먼저 유저 아이디가 맞는지 확인한다. 129 | assert user_credential['id'] == 1 130 | 131 | ## 그리고 유저 비밀번호가 맞는지 bcrypt의 checkpw 메소드를 사용해서 확인 한다. 132 | assert bcrypt.checkpw('test password'.encode('UTF-8'), user_credential['hashed_password'].encode('UTF-8')) 133 | 134 | def test_insert_follow(user_dao): 135 | ## insert_follow 메소드를 사용하여 유저 1이 유저 2를 팔로우 하도록 한다. 136 | ## 유저 1과 2는 setup_function에서 이미 생성 되었다. 137 | user_dao.insert_follow(user_id = 1, follow_id = 2) 138 | 139 | follow_list = get_follow_list(1) 140 | 141 | assert follow_list == [2] 142 | 143 | def test_insert_unfollow(user_dao): 144 | ## insert_follow 메소드를 사용하여 유저 1이 유저 2를 팔로우 한 후 언팔로우 한다. 145 | ## 유저 1과 2는 setup_function에서 이미 생성 되었다. 146 | user_dao.insert_follow(user_id = 1, follow_id = 2) 147 | user_dao.insert_unfollow(user_id = 1, unfollow_id = 2) 148 | 149 | follow_list = get_follow_list(1) 150 | 151 | assert follow_list == [ ] 152 | 153 | def test_insert_tweet(tweet_dao): 154 | tweet_dao.insert_tweet(1, "tweet test") 155 | timeline = tweet_dao.get_timeline(1) 156 | 157 | assert timeline == [ 158 | { 159 | 'user_id' : 1, 160 | 'tweet' : 'tweet test' 161 | } 162 | ] 163 | 164 | def test_timeline(user_dao, tweet_dao): 165 | tweet_dao.insert_tweet(1, "tweet test") 166 | tweet_dao.insert_tweet(2, "tweet test 2") 167 | user_dao.insert_follow(1, 2) 168 | 169 | timeline = tweet_dao.get_timeline(1) 170 | 171 | assert timeline == [ 172 | { 173 | 'user_id' : 2, 174 | 'tweet' : 'Hello World!' 175 | }, 176 | { 177 | 'user_id' : 1, 178 | 'tweet' : 'tweet test' 179 | }, 180 | { 181 | 'user_id' : 2, 182 | 'tweet' : 'tweet test 2' 183 | } 184 | ] 185 | -------------------------------------------------------------------------------- /chapter10/test/test_service.py: -------------------------------------------------------------------------------- 1 | import jwt 2 | import bcrypt 3 | import pytest 4 | import config 5 | 6 | from model import UserDao, TweetDao 7 | from service import UserService, TweetService 8 | from sqlalchemy import create_engine, text 9 | 10 | database = create_engine(config.test_config['DB_URL'], encoding= 'utf-8', max_overflow = 0) 11 | 12 | @pytest.fixture 13 | def user_service(): 14 | return UserService(UserDao(database), config) 15 | 16 | @pytest.fixture 17 | def tweet_service(): 18 | return TweetService(TweetDao(database)) 19 | 20 | def setup_function(): 21 | ## Create a test user 22 | hashed_password = bcrypt.hashpw( 23 | b"test password", 24 | bcrypt.gensalt() 25 | ) 26 | new_users = [ 27 | { 28 | 'id' : 1, 29 | 'name' : '송은우', 30 | 'email' : 'songew@gmail.com', 31 | 'profile' : 'test profile', 32 | 'hashed_password' : hashed_password 33 | }, { 34 | 'id' : 2, 35 | 'name' : '김철수', 36 | 'email' : 'tet@gmail.com', 37 | 'profile' : 'test profile', 38 | 'hashed_password' : hashed_password 39 | } 40 | ] 41 | database.execute(text(""" 42 | INSERT INTO users ( 43 | id, 44 | name, 45 | email, 46 | profile, 47 | hashed_password 48 | ) VALUES ( 49 | :id, 50 | :name, 51 | :email, 52 | :profile, 53 | :hashed_password 54 | ) 55 | """), new_users) 56 | 57 | ## User 2 의 트윗 미리 생성해 놓기 58 | database.execute(text(""" 59 | INSERT INTO tweets ( 60 | user_id, 61 | tweet 62 | ) VALUES ( 63 | 2, 64 | "Hello World!" 65 | ) 66 | """)) 67 | 68 | 69 | def teardown_function(): 70 | database.execute(text("SET FOREIGN_KEY_CHECKS=0")) 71 | database.execute(text("TRUNCATE users")) 72 | database.execute(text("TRUNCATE tweets")) 73 | database.execute(text("TRUNCATE users_follow_list")) 74 | database.execute(text("SET FOREIGN_KEY_CHECKS=1")) 75 | 76 | def get_user(user_id): 77 | row = database.execute(text(""" 78 | SELECT 79 | id, 80 | name, 81 | email, 82 | profile 83 | FROM users 84 | WHERE id = :user_id 85 | """), { 86 | 'user_id' : user_id 87 | }).fetchone() 88 | 89 | return { 90 | 'id' : row['id'], 91 | 'name' : row['name'], 92 | 'email' : row['email'], 93 | 'profile' : row['profile'] 94 | } if row else None 95 | 96 | def get_follow_list(user_id): 97 | rows = database.execute(text(""" 98 | SELECT follow_user_id as id 99 | FROM users_follow_list 100 | WHERE user_id = :user_id 101 | """), { 102 | 'user_id' : user_id 103 | }).fetchall() 104 | 105 | return [int(row['id']) for row in rows] 106 | 107 | def test_create_new_user(user_service): 108 | new_user = { 109 | 'name' : '홍길동', 110 | 'email' : 'hong@test.com', 111 | 'profile' : '동쪽에서 번쩍, 서쪽에서 번쩍', 112 | 'password' : 'test1234' 113 | } 114 | 115 | new_user_id = user_service.create_new_user(new_user) 116 | created_user = get_user(new_user_id) 117 | 118 | assert created_user == { 119 | 'id' : new_user_id, 120 | 'name' : new_user['name'], 121 | 'profile' : new_user['profile'], 122 | 'email' : new_user['email'], 123 | } 124 | 125 | def test_login(user_service): 126 | ## 이미 생성되어 있는 유저의 이메일과 비밀번호를 사용해서 로그인을 시도. 127 | assert user_service.login({ 128 | 'email' : 'songew@gmail.com', 129 | 'password' : 'test password' 130 | }) 131 | 132 | ## 잘못된 비번으로 로그인 했을때 False가 리턴되는지 테스트 133 | assert not user_service.login({ 134 | 'email' : 'songew@gmail.com', 135 | 'password' : 'test1234' 136 | }) 137 | 138 | def test_generate_access_token(user_service): 139 | ## token 생성후 decode 해서 동일한 유저 아이디가 나오는지 테스트 140 | token = user_service.generate_access_token(1) 141 | payload = jwt.decode(token, config.JWT_SECRET_KEY, 'HS256') 142 | 143 | assert payload['user_id'] == 1 144 | 145 | def test_follow(user_service): 146 | user_service.follow(1, 2) 147 | follow_list = get_follow_list(1) 148 | 149 | assert follow_list == [2] 150 | 151 | def test_unfollow(user_service): 152 | user_service.follow(1, 2) 153 | user_service.unfollow(1, 2) 154 | follow_list = get_follow_list(1) 155 | 156 | assert follow_list == [ ] 157 | 158 | def test_tweet(tweet_service): 159 | tweet_service.tweet(1, "tweet test") 160 | timeline = tweet_service.get_timeline(1) 161 | 162 | assert timeline == [ 163 | { 164 | 'user_id' : 1, 165 | 'tweet' : 'tweet test' 166 | } 167 | ] 168 | 169 | def test_timeline(user_service, tweet_service): 170 | tweet_service.tweet(1, "tweet test") 171 | tweet_service.tweet(2, "tweet test 2") 172 | user_service.follow(1, 2) 173 | 174 | timeline = tweet_service.get_timeline(1) 175 | 176 | assert timeline == [ 177 | { 178 | 'user_id' : 2, 179 | 'tweet' : 'Hello World!' 180 | }, 181 | { 182 | 'user_id' : 1, 183 | 'tweet' : 'tweet test' 184 | }, 185 | { 186 | 'user_id' : 2, 187 | 'tweet' : 'tweet test 2' 188 | } 189 | ] 190 | -------------------------------------------------------------------------------- /chapter10/test/test_view.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import bcrypt 3 | import json 4 | import config 5 | 6 | from app import create_app 7 | from sqlalchemy import create_engine, text 8 | 9 | database = create_engine(config.test_config['DB_URL'], encoding= 'utf-8', max_overflow = 0) 10 | 11 | @pytest.fixture 12 | def api(): 13 | app = create_app(config.test_config) 14 | app.config['TESTING'] = True 15 | api = app.test_client() 16 | 17 | return api 18 | def setup_function(): 19 | ## Create a test user 20 | hashed_password = bcrypt.hashpw( 21 | b"test password", 22 | bcrypt.gensalt() 23 | ) 24 | new_users = [ 25 | { 26 | 'id' : 1, 27 | 'name' : '송은우', 28 | 'email' : 'songew@gmail.com', 29 | 'profile' : 'test profile', 30 | 'hashed_password' : hashed_password 31 | }, { 32 | 'id' : 2, 33 | 'name' : '김철수', 34 | 'email' : 'tet@gmail.com', 35 | 'profile' : 'test profile', 36 | 'hashed_password' : hashed_password 37 | } 38 | ] 39 | database.execute(text(""" 40 | INSERT INTO users ( 41 | id, 42 | name, 43 | email, 44 | profile, 45 | hashed_password 46 | ) VALUES ( 47 | :id, 48 | :name, 49 | :email, 50 | :profile, 51 | :hashed_password 52 | ) 53 | """), new_users) 54 | 55 | ## User 2 의 트윗 미리 생성해 놓기 56 | database.execute(text(""" 57 | INSERT INTO tweets ( 58 | user_id, 59 | tweet 60 | ) VALUES ( 61 | 2, 62 | "Hello World!" 63 | ) 64 | """)) 65 | 66 | 67 | def teardown_function(): 68 | database.execute(text("SET FOREIGN_KEY_CHECKS=0")) 69 | database.execute(text("TRUNCATE users")) 70 | database.execute(text("TRUNCATE tweets")) 71 | database.execute(text("TRUNCATE users_follow_list")) 72 | database.execute(text("SET FOREIGN_KEY_CHECKS=1")) 73 | 74 | def test_ping(api): 75 | resp = api.get('/ping') 76 | assert b'pong' in resp.data 77 | 78 | def test_login(api): 79 | resp = api.post( 80 | '/login', 81 | data = json.dumps({'email' : 'songew@gmail.com', 'password' : 'test password'}), 82 | content_type = 'application/json' 83 | ) 84 | assert b"access_token" in resp.data 85 | 86 | def test_unauthorized(api): 87 | # access token이 없이는 401 응답을 리턴하는지를 확인 88 | resp = api.post( 89 | '/tweet', 90 | data = json.dumps({'tweet' : "Hello World!"}), 91 | content_type = 'application/json' 92 | ) 93 | assert resp.status_code == 401 94 | 95 | resp = api.post( 96 | '/follow', 97 | data = json.dumps({'follow' : 2}), 98 | content_type = 'application/json' 99 | ) 100 | assert resp.status_code == 401 101 | 102 | resp = api.post( 103 | '/unfollow', 104 | data = json.dumps({'unfollow' : 2}), 105 | content_type = 'application/json' 106 | ) 107 | assert resp.status_code == 401 108 | 109 | def test_tweet(api): 110 | ## 로그인 111 | resp = api.post( 112 | '/login', 113 | data = json.dumps({'email' : 'songew@gmail.com', 'password' : 'test password'}), 114 | content_type = 'application/json' 115 | ) 116 | resp_json = json.loads(resp.data.decode('utf-8')) 117 | access_token = resp_json['access_token'] 118 | 119 | ## tweet 120 | resp = api.post( 121 | '/tweet', 122 | data = json.dumps({'tweet' : "Hello World!"}), 123 | content_type = 'application/json', 124 | headers = {'Authorization' : access_token} 125 | ) 126 | assert resp.status_code == 200 127 | 128 | ## tweet 확인 129 | resp = api.get(f'/timeline/1') 130 | tweets = json.loads(resp.data.decode('utf-8')) 131 | 132 | assert resp.status_code == 200 133 | assert tweets == { 134 | 'user_id' : 1, 135 | 'timeline' : [ 136 | { 137 | 'user_id' : 1, 138 | 'tweet' : "Hello World!" 139 | } 140 | ] 141 | } 142 | 143 | def test_follow(api): 144 | # 로그인 145 | resp = api.post( 146 | '/login', 147 | data = json.dumps({'email' : 'songew@gmail.com', 'password' : 'test password'}), 148 | content_type = 'application/json' 149 | ) 150 | resp_json = json.loads(resp.data.decode('utf-8')) 151 | access_token = resp_json['access_token'] 152 | 153 | ## 먼저 유저 1의 tweet 확인 해서 tweet 리스트가 비어 있는것을 확인 154 | resp = api.get(f'/timeline/1') 155 | tweets = json.loads(resp.data.decode('utf-8')) 156 | 157 | assert resp.status_code == 200 158 | assert tweets == { 159 | 'user_id' : 1, 160 | 'timeline' : [ ] 161 | } 162 | 163 | # follow 유저 아이디 = 2 164 | resp = api.post( 165 | '/follow', 166 | data = json.dumps({'follow' : 2}), 167 | content_type = 'application/json', 168 | headers = {'Authorization' : access_token} 169 | ) 170 | assert resp.status_code == 200 171 | 172 | ## 이제 유저 1의 tweet 확인 해서 유저 2의 tweet의 리턴 되는것을 확인 173 | resp = api.get(f'/timeline/1') 174 | tweets = json.loads(resp.data.decode('utf-8')) 175 | 176 | assert resp.status_code == 200 177 | assert tweets == { 178 | 'user_id' : 1, 179 | 'timeline' : [ 180 | { 181 | 'user_id' : 2, 182 | 'tweet' : "Hello World!" 183 | } 184 | ] 185 | } 186 | 187 | def test_unfollow(api): 188 | # 로그인 189 | resp = api.post( 190 | '/login', 191 | data = json.dumps({'email' : 'songew@gmail.com', 'password' : 'test password'}), 192 | content_type = 'application/json' 193 | ) 194 | resp_json = json.loads(resp.data.decode('utf-8')) 195 | access_token = resp_json['access_token'] 196 | 197 | # follow 유저 아이디 = 2 198 | resp = api.post( 199 | '/follow', 200 | data = json.dumps({'follow' : 2}), 201 | content_type = 'application/json', 202 | headers = {'Authorization' : access_token} 203 | ) 204 | assert resp.status_code == 200 205 | 206 | ## 이제 유저 1의 tweet 확인 해서 유저 2의 tweet의 리턴 되는것을 확인 207 | resp = api.get(f'/timeline/1') 208 | tweets = json.loads(resp.data.decode('utf-8')) 209 | 210 | assert resp.status_code == 200 211 | assert tweets == { 212 | 'user_id' : 1, 213 | 'timeline' : [ 214 | { 215 | 'user_id' : 2, 216 | 'tweet' : "Hello World!" 217 | } 218 | ] 219 | } 220 | 221 | # unfollow 유저 아이디 = 2 222 | resp = api.post( 223 | '/unfollow', 224 | data = json.dumps({'unfollow' : 2}), 225 | content_type = 'application/json', 226 | headers = {'Authorization' : access_token} 227 | ) 228 | assert resp.status_code == 200 229 | 230 | ## 이제 유저 1의 tweet 확인 해서 유저 2의 tweet이 더 이상 리턴 되지 않는 것을 확인 231 | resp = api.get(f'/timeline/1') 232 | tweets = json.loads(resp.data.decode('utf-8')) 233 | 234 | assert resp.status_code == 200 235 | assert tweets == { 236 | 'user_id' : 1, 237 | 'timeline' : [ ] 238 | } 239 | -------------------------------------------------------------------------------- /chapter10/view/__init__.py: -------------------------------------------------------------------------------- 1 | import jwt 2 | 3 | from flask import request, jsonify, current_app, Response, g 4 | from flask.json import JSONEncoder 5 | from functools import wraps 6 | 7 | ## Default JSON encoder는 set를 JSON으로 변환할 수 없다. 8 | ## 그럼으로 커스텀 엔코더를 작성해서 set을 list로 변환하여 9 | ## JSON으로 변환 가능하게 해주어야 한다. 10 | class CustomJSONEncoder(JSONEncoder): 11 | def default(self, obj): 12 | if isinstance(obj, set): 13 | return list(obj) 14 | 15 | return JSONEncoder.default(self, obj) 16 | 17 | ######################################################### 18 | # Decorators 19 | ######################################################### 20 | def login_required(f): 21 | @wraps(f) 22 | def decorated_function(*args, **kwargs): 23 | access_token = request.headers.get('Authorization') 24 | if access_token is not None: 25 | try: 26 | payload = jwt.decode(access_token, current_app.config['JWT_SECRET_KEY'], 'HS256') 27 | except jwt.InvalidTokenError: 28 | payload = None 29 | 30 | if payload is None: return Response(status=401) 31 | 32 | user_id = payload['user_id'] 33 | g.user_id = user_id 34 | else: 35 | return Response(status = 401) 36 | 37 | return f(*args, **kwargs) 38 | return decorated_function 39 | 40 | def create_endpoints(app, services): 41 | app.json_encoder = CustomJSONEncoder 42 | 43 | user_service = services.user_service 44 | tweet_service = services.tweet_service 45 | 46 | @app.route("/ping", methods=['GET']) 47 | def ping(): 48 | return "pong" 49 | 50 | @app.route("/sign-up", methods=['POST']) 51 | def sign_up(): 52 | new_user = request.json 53 | new_user = user_service.create_new_user(new_user) 54 | 55 | return jsonify(new_user) 56 | 57 | @app.route('/login', methods=['POST']) 58 | def login(): 59 | credential = request.json 60 | authorized = user_service.login(credential) 61 | 62 | if authorized: 63 | user_credential = user_service.get_user_id_and_password(credential['email']) 64 | user_id = user_credential['id'] 65 | token = user_service.generate_access_token(user_id) 66 | 67 | return jsonify({ 68 | 'user_id' : user_id, 69 | 'access_token' : token 70 | }) 71 | else: 72 | return '', 401 73 | 74 | @app.route('/tweet', methods=['POST']) 75 | @login_required 76 | def tweet(): 77 | user_tweet = request.json 78 | tweet = user_tweet['tweet'] 79 | user_id = g.user_id 80 | 81 | result = tweet_service.tweet(user_id, tweet) 82 | if result is None: 83 | return '300자를 초과했습니다', 400 84 | 85 | return '', 200 86 | 87 | @app.route('/follow', methods=['POST']) 88 | @login_required 89 | def follow(): 90 | payload = request.json 91 | user_id = g.user_id 92 | follow_id = payload['follow'] 93 | 94 | user_service.follow(user_id, follow_id) 95 | 96 | return '', 200 97 | 98 | @app.route('/unfollow', methods=['POST']) 99 | @login_required 100 | def unfollow(): 101 | payload = request.json 102 | user_id = g.user_id 103 | unfollow_id = payload['unfollow'] 104 | 105 | user_service.unfollow(user_id, unfollow_id) 106 | 107 | return '', 200 108 | 109 | @app.route('/timeline/', methods=['GET']) 110 | def timeline(user_id): 111 | timeline = tweet_service.get_timeline(user_id) 112 | 113 | return jsonify({ 114 | 'user_id' : user_id, 115 | 'timeline' : timeline 116 | }) 117 | 118 | @app.route('/timeline', methods=['GET']) 119 | @login_required 120 | def user_timeline(): 121 | timeline = tweet_service.get_timeline(g.user_id) 122 | 123 | return jsonify({ 124 | 'user_id' : user_id, 125 | 'timeline' : timeline 126 | }) 127 | -------------------------------------------------------------------------------- /chapter11/app.py: -------------------------------------------------------------------------------- 1 | import config 2 | import boto3 3 | import botocore 4 | 5 | from flask import Flask 6 | from sqlalchemy import create_engine 7 | from flask_cors import CORS 8 | 9 | from model import UserDao, TweetDao 10 | from service import UserService, TweetService 11 | from view import create_endpoints 12 | 13 | class Services: 14 | pass 15 | 16 | ################################ 17 | # Create App 18 | ################################ 19 | def create_app(test_config = None): 20 | app = Flask(__name__) 21 | 22 | CORS(app) 23 | 24 | if test_config is None: 25 | app.config.from_pyfile("config.py") 26 | else: 27 | app.config.update(test_config) 28 | 29 | database = create_engine(app.config['DB_URL'], encoding = 'utf-8', max_overflow = 0) 30 | 31 | ## Persistenace Layer 32 | user_dao = UserDao(database) 33 | tweet_dao = TweetDao(database) 34 | 35 | ## Business Layer 36 | s3_client = boto3.client( 37 | "s3", 38 | aws_access_key_id = app.config['S3_ACCESS_KEY'], 39 | aws_secret_access_key = app.config['S3_SECRET_KEY'] 40 | ) 41 | 42 | services = Services 43 | services.user_service = UserService(user_dao, app.config, s3_client) 44 | services.tweet_service = TweetService(tweet_dao) 45 | 46 | ## 엔드포인트들을 생성 47 | create_endpoints(app, services) 48 | 49 | return app 50 | -------------------------------------------------------------------------------- /chapter11/config.py: -------------------------------------------------------------------------------- 1 | db = { 2 | 'user' : 'root', 3 | 'password' : 'test1234', 4 | 'host' : 'localhost', 5 | 'port' : 3306, 6 | 'database' : 'miniter' 7 | } 8 | 9 | DB_URL = f"mysql+mysqlconnector://{db['user']}:{db['password']}@{db['host']}:{db['port']}/{db['database']}?charset=utf8" 10 | JWT_SECRET_KEY = 'SOME_SUPER_SECRET_KEY' 11 | JWT_EXP_DELTA_SECONDS = 7 * 24 * 60 * 60 12 | 13 | S3_BUCKET = "test-bucket" 14 | S3_ACCESS_KEY = "s3-access-key" 15 | S3_SECRET_KEY = "s3-secrete-key" 16 | S3_BUCKET_URL = f"https://s3.ap-northeast-2.amazonaws.com/{S3_BUCKET}/" 17 | 18 | test_db = { 19 | 'user' : 'root', 20 | 'password' : 'test1234', 21 | 'host' : 'localhost', 22 | 'port' : 3306, 23 | 'database' : 'miniter_test' 24 | } 25 | test_config = { 26 | 'DB_URL' : f"mysql+mysqlconnector://{test_db['user']}:{test_db['password']}@{test_db['host']}:{test_db['port']}/{test_db['database']}?charset=utf8", 27 | 'JWT_SECRET_KEY' : 'SOME_SUPER_SECRET_KEY', 28 | 'JWT_EXP_DELTA_SECONDS' : 7 * 24 * 60 * 60, 29 | 'S3_BUCKET' : "test", 30 | 'S3_ACCESS_KEY' : "test_acces_key", 31 | 'S3_SECRET_KEY' : "test_secret_key", 32 | 'S3_BUCKET_URL' : f"https://s3.ap-northeast-2.amazonaws.com/test/" 33 | } 34 | -------------------------------------------------------------------------------- /chapter11/model/__init__.py: -------------------------------------------------------------------------------- 1 | from .user_dao import UserDao 2 | from .tweet_dao import TweetDao 3 | 4 | __all__ = [ 5 | 'UserDao', 6 | 'TweetDao' 7 | ] 8 | -------------------------------------------------------------------------------- /chapter11/model/tweet_dao.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import text 2 | 3 | class TweetDao: 4 | def __init__(self, database): 5 | self.db = database 6 | 7 | def insert_tweet(self, user_id, tweet): 8 | return self.db.execute(text(""" 9 | INSERT INTO tweets ( 10 | user_id, 11 | tweet 12 | ) VALUES ( 13 | :id, 14 | :tweet 15 | ) 16 | """), { 17 | 'id' : user_id, 18 | 'tweet' : tweet 19 | }).rowcount 20 | 21 | def get_timeline(self, user_id): 22 | timeline = self.db.execute(text(""" 23 | SELECT 24 | t.user_id, 25 | t.tweet 26 | FROM tweets t 27 | LEFT JOIN users_follow_list ufl ON ufl.user_id = :user_id 28 | WHERE t.user_id = :user_id 29 | OR t.user_id = ufl.follow_user_id 30 | """), { 31 | 'user_id' : user_id 32 | }).fetchall() 33 | 34 | return [{ 35 | 'user_id' : tweet['user_id'], 36 | 'tweet' : tweet['tweet'] 37 | } for tweet in timeline] 38 | -------------------------------------------------------------------------------- /chapter11/model/user_dao.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import text 2 | 3 | class UserDao: 4 | def __init__(self, database): 5 | self.db = database 6 | 7 | def insert_user(self, user): 8 | return self.db.execute(text(""" 9 | INSERT INTO users ( 10 | name, 11 | email, 12 | profile, 13 | hashed_password 14 | ) VALUES ( 15 | :name, 16 | :email, 17 | :profile, 18 | :password 19 | ) 20 | """), user).lastrowid 21 | 22 | def get_user_id_and_password(self, email): 23 | row = self.db.execute(text(""" 24 | SELECT 25 | id, 26 | hashed_password 27 | FROM users 28 | WHERE email = :email 29 | """), {'email' : email}).fetchone() 30 | 31 | return { 32 | 'id' : row['id'], 33 | 'hashed_password' : row['hashed_password'] 34 | } if row else None 35 | 36 | def insert_follow(self, user_id, follow_id): 37 | return self.db.execute(text(""" 38 | INSERT INTO users_follow_list ( 39 | user_id, 40 | follow_user_id 41 | ) VALUES ( 42 | :id, 43 | :follow 44 | ) 45 | """), { 46 | 'id' : user_id, 47 | 'follow' : follow_id 48 | }).rowcount 49 | 50 | def insert_unfollow(self, user_id, unfollow_id): 51 | return self.db.execute(text(""" 52 | DELETE FROM users_follow_list 53 | WHERE user_id = :id 54 | AND follow_user_id = :unfollow 55 | """), { 56 | 'id' : user_id, 57 | 'unfollow' : unfollow_id 58 | }).rowcount 59 | 60 | def save_profile_picture(self, profile_pic_path, user_id): 61 | return self.db.execute(text(""" 62 | UPDATE users 63 | SET profile_picture = :profile_pic_path 64 | WHERE id = :user_id 65 | """), { 66 | 'user_id' : user_id, 67 | 'profile_pic_path' : profile_pic_path 68 | }).rowcount 69 | 70 | def get_profile_picture(self, user_id): 71 | row = self.db.execute(text(""" 72 | SELECT profile_picture 73 | FROM users 74 | WHERE id = :user_id 75 | """), { 76 | 'user_id' : user_id 77 | }).fetchone() 78 | 79 | return row['profile_picture'] if row else None 80 | 81 | 82 | -------------------------------------------------------------------------------- /chapter11/profile_picture_add_column.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE users ADD COLUMN profile_picture VARCHAR(255); 2 | -------------------------------------------------------------------------------- /chapter11/profile_pictures/symbol.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rampart81/python-backend-book/6d303ab6349c0ddd5eedada5ad81b8b456c4cc87/chapter11/profile_pictures/symbol.png -------------------------------------------------------------------------------- /chapter11/requirements.txt: -------------------------------------------------------------------------------- 1 | asn1crypto==0.24.0 2 | atomicwrites==1.2.1 3 | attrs==18.2.0 4 | bcrypt==3.1.4 5 | certifi==2018.8.24 6 | cffi==1.11.5 7 | click==6.7 8 | cryptography==2.4.2 9 | Flask==1.0.2 10 | idna==2.8 11 | itsdangerous==0.24 12 | Jinja2==2.10 13 | MarkupSafe==1.0 14 | more-itertools==4.3.0 15 | mysql-connector-python==8.0.13 16 | pluggy==0.8.0 17 | protobuf==3.6.1 18 | py==1.7.0 19 | pycparser==2.19 20 | PyJWT==1.7.0 21 | pytest==4.0.1 22 | six==1.11.0 23 | SQLAlchemy==1.2.14 24 | Werkzeug==0.14.1 25 | -------------------------------------------------------------------------------- /chapter11/service/__init__.py: -------------------------------------------------------------------------------- 1 | from .user_service import UserService 2 | from .tweet_service import TweetService 3 | 4 | __all__ = [ 5 | 'UserService', 6 | 'TweetService' 7 | ] 8 | -------------------------------------------------------------------------------- /chapter11/service/tweet_service.py: -------------------------------------------------------------------------------- 1 | 2 | class TweetService: 3 | def __init__(self, tweet_dao): 4 | self.tweet_dao = tweet_dao 5 | 6 | def tweet(self, user_id, tweet): 7 | if len(tweet) > 300: 8 | return None 9 | 10 | return self.tweet_dao.insert_tweet(user_id, tweet) 11 | 12 | def get_timeline(self, user_id): 13 | return self.tweet_dao.get_timeline(user_id) 14 | 15 | -------------------------------------------------------------------------------- /chapter11/service/user_service.py: -------------------------------------------------------------------------------- 1 | import os 2 | import jwt 3 | import bcrypt 4 | 5 | from datetime import datetime, timedelta 6 | 7 | class UserService: 8 | def __init__(self, user_dao, config, s3_client): 9 | self.user_dao = user_dao 10 | self.config = config 11 | self.s3 = s3_client 12 | 13 | def create_new_user(self, new_user): 14 | new_user['password'] = bcrypt.hashpw( 15 | new_user['password'].encode('UTF-8'), 16 | bcrypt.gensalt() 17 | ) 18 | 19 | new_user_id = self.user_dao.insert_user(new_user) 20 | 21 | return new_user_id 22 | 23 | def login(self, credential): 24 | email = credential['email'] 25 | password = credential['password'] 26 | user_credential = self.user_dao.get_user_id_and_password(email) 27 | 28 | authorized = user_credential and bcrypt.checkpw(password.encode('UTF-8'), user_credential['hashed_password'].encode('UTF-8')) 29 | 30 | return authorized 31 | 32 | def generate_access_token(self, user_id): 33 | payload = { 34 | 'user_id' : user_id, 35 | 'exp' : datetime.utcnow() + timedelta(seconds = 60 * 60 * 24) 36 | } 37 | token = jwt.encode(payload, self.config['JWT_SECRET_KEY'], 'HS256') 38 | 39 | return token.decode('UTF-8') 40 | 41 | def follow(self, user_id, follow_id): 42 | return self.user_dao.insert_follow(user_id, follow_id) 43 | 44 | def unfollow(self, user_id, unfollow_id): 45 | return self.user_dao.insert_unfollow(user_id, unfollow_id) 46 | 47 | def get_user_id_and_password(self, email): 48 | return self.user_dao.get_user_id_and_password(email) 49 | 50 | def save_profile_picture(self, picture, filename, user_id): 51 | self.s3.upload_fileobj( 52 | picture, 53 | self.config['S3_BUCKET'], 54 | filename 55 | ) 56 | 57 | image_url = f"{self.config['S3_BUCKET_URL']}{filename}" 58 | 59 | return self.user_dao.save_profile_picture(image_url, user_id) 60 | 61 | def get_profile_picture(self, user_id): 62 | return self.user_dao.get_profile_picture(user_id) 63 | -------------------------------------------------------------------------------- /chapter11/setup.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | from flask_script import Manager 4 | from app import create_app 5 | from flask_twisted import Twisted 6 | from twisted.python import log 7 | 8 | if __name__ == "__main__": 9 | app = create_app() 10 | 11 | twisted = Twisted(app) 12 | log.startLogging(sys.stdout) 13 | 14 | app.logger.info(f"Running the app...") 15 | 16 | manager = Manager(app) 17 | manager.run() 18 | -------------------------------------------------------------------------------- /chapter11/test/test_model.py: -------------------------------------------------------------------------------- 1 | import bcrypt 2 | import pytest 3 | import config 4 | 5 | from model import UserDao, TweetDao 6 | from sqlalchemy import create_engine, text 7 | 8 | 9 | database = create_engine(config.test_config['DB_URL'], encoding= 'utf-8', max_overflow = 0) 10 | 11 | @pytest.fixture 12 | def user_dao(): 13 | return UserDao(database) 14 | 15 | @pytest.fixture 16 | def tweet_dao(): 17 | return TweetDao(database) 18 | 19 | def setup_function(): 20 | ## Create a test user 21 | hashed_password = bcrypt.hashpw( 22 | b"test password", 23 | bcrypt.gensalt() 24 | ) 25 | new_users = [ 26 | { 27 | 'id' : 1, 28 | 'name' : '송은우', 29 | 'email' : 'songew@gmail.com', 30 | 'profile' : 'test profile', 31 | 'hashed_password' : hashed_password 32 | }, { 33 | 'id' : 2, 34 | 'name' : '김철수', 35 | 'email' : 'tet@gmail.com', 36 | 'profile' : 'test profile', 37 | 'hashed_password' : hashed_password 38 | } 39 | ] 40 | database.execute(text(""" 41 | INSERT INTO users ( 42 | id, 43 | name, 44 | email, 45 | profile, 46 | hashed_password 47 | ) VALUES ( 48 | :id, 49 | :name, 50 | :email, 51 | :profile, 52 | :hashed_password 53 | ) 54 | """), new_users) 55 | 56 | ## User 2 의 트윗 미리 생성해 놓기 57 | database.execute(text(""" 58 | INSERT INTO tweets ( 59 | user_id, 60 | tweet 61 | ) VALUES ( 62 | 2, 63 | "Hello World!" 64 | ) 65 | """)) 66 | 67 | def teardown_function(): 68 | database.execute(text("SET FOREIGN_KEY_CHECKS=0")) 69 | database.execute(text("TRUNCATE users")) 70 | database.execute(text("TRUNCATE tweets")) 71 | database.execute(text("TRUNCATE users_follow_list")) 72 | database.execute(text("SET FOREIGN_KEY_CHECKS=1")) 73 | 74 | def get_user(user_id): 75 | row = database.execute(text(""" 76 | SELECT 77 | id, 78 | name, 79 | email, 80 | profile 81 | FROM users 82 | WHERE id = :user_id 83 | """), { 84 | 'user_id' : user_id 85 | }).fetchone() 86 | 87 | return { 88 | 'id' : row['id'], 89 | 'name' : row['name'], 90 | 'email' : row['email'], 91 | 'profile' : row['profile'] 92 | } if row else None 93 | 94 | def get_follow_list(user_id): 95 | rows = database.execute(text(""" 96 | SELECT follow_user_id as id 97 | FROM users_follow_list 98 | WHERE user_id = :user_id 99 | """), { 100 | 'user_id' : user_id 101 | }).fetchall() 102 | 103 | return [int(row['id']) for row in rows] 104 | 105 | def test_insert_user(user_dao): 106 | new_user = { 107 | 'name' : '홍길동', 108 | 'email' : 'hong@test.com', 109 | 'profile' : '서쪽에서 번쩍, 동쪽에서 번쩍', 110 | 'password' : 'test1234' 111 | } 112 | 113 | new_user_id = user_dao.insert_user(new_user) 114 | user = get_user(new_user_id) 115 | 116 | assert user == { 117 | 'id' : new_user_id, 118 | 'name' : new_user['name'], 119 | 'email' : new_user['email'], 120 | 'profile' : new_user['profile'] 121 | } 122 | 123 | def test_get_user_id_and_password(user_dao): 124 | ## get_user_id_and_password 메소드를 호출 하여 유저의 아이디와 비밀번호 해시 값을 읽어들인다. 125 | ## 유저는 이미 setup_function 에서 생성된 유저를 사용한다. 126 | user_credential = user_dao.get_user_id_and_password(email = 'songew@gmail.com') 127 | 128 | ## 먼저 유저 아이디가 맞는지 확인한다. 129 | assert user_credential['id'] == 1 130 | 131 | ## 그리고 유저 비밀번호가 맞는지 bcrypt의 checkpw 메소드를 사용해서 확인 한다. 132 | assert bcrypt.checkpw('test password'.encode('UTF-8'), user_credential['hashed_password'].encode('UTF-8')) 133 | 134 | def test_insert_follow(user_dao): 135 | ## insert_follow 메소드를 사용하여 유저 1이 유저 2를 팔로우 하도록 한다. 136 | ## 유저 1과 2는 setup_function에서 이미 생성 되었다. 137 | user_dao.insert_follow(user_id = 1, follow_id = 2) 138 | 139 | follow_list = get_follow_list(1) 140 | 141 | assert follow_list == [2] 142 | 143 | def test_insert_unfollow(user_dao): 144 | ## insert_follow 메소드를 사용하여 유저 1이 유저 2를 팔로우 한 후 언팔로우 한다. 145 | ## 유저 1과 2는 setup_function에서 이미 생성 되었다. 146 | user_dao.insert_follow(user_id = 1, follow_id = 2) 147 | user_dao.insert_unfollow(user_id = 1, unfollow_id = 2) 148 | 149 | follow_list = get_follow_list(1) 150 | 151 | assert follow_list == [ ] 152 | 153 | def test_insert_tweet(tweet_dao): 154 | tweet_dao.insert_tweet(1, "tweet test") 155 | timeline = tweet_dao.get_timeline(1) 156 | 157 | assert timeline == [ 158 | { 159 | 'user_id' : 1, 160 | 'tweet' : 'tweet test' 161 | } 162 | ] 163 | 164 | def test_timeline(user_dao, tweet_dao): 165 | tweet_dao.insert_tweet(1, "tweet test") 166 | tweet_dao.insert_tweet(2, "tweet test 2") 167 | user_dao.insert_follow(1, 2) 168 | 169 | timeline = tweet_dao.get_timeline(1) 170 | 171 | assert timeline == [ 172 | { 173 | 'user_id' : 2, 174 | 'tweet' : 'Hello World!' 175 | }, 176 | { 177 | 'user_id' : 1, 178 | 'tweet' : 'tweet test' 179 | }, 180 | { 181 | 'user_id' : 2, 182 | 'tweet' : 'tweet test 2' 183 | } 184 | ] 185 | 186 | def test_save_and_get_profile_picture(user_dao): 187 | ## 먼저 profile picture를 읽어들이도록 하자. 188 | ## 저장한 profile picture 가 없음으로 None이 나와야 한다. 189 | user_id = 1 190 | user_profile_picture = user_dao.get_profile_picture(user_id) 191 | assert user_profile_picture is None 192 | 193 | ## Profile picture url을 저장하자 194 | expected_profile_picture = "https://s3.ap-northeast-2.amazonaws.com/test/profile.jpg" 195 | user_dao.save_profile_picture(expected_profile_picture, user_id) 196 | 197 | ## Profile picture url을 읽어들이자 198 | actual_profile_picture = user_dao.get_profile_picture(user_id) 199 | assert expected_profile_picture == actual_profile_picture 200 | 201 | 202 | 203 | -------------------------------------------------------------------------------- /chapter11/test/test_service.py: -------------------------------------------------------------------------------- 1 | import jwt 2 | import bcrypt 3 | import pytest 4 | import config 5 | 6 | from model import UserDao, TweetDao 7 | from service import UserService, TweetService 8 | from sqlalchemy import create_engine, text 9 | from unittest import mock 10 | 11 | database = create_engine(config.test_config['DB_URL'], encoding= 'utf-8', max_overflow = 0) 12 | 13 | @pytest.fixture 14 | def user_service(): 15 | mock_s3_client = mock.Mock() 16 | return UserService(UserDao(database), config.test_config, mock_s3_client) 17 | 18 | @pytest.fixture 19 | def tweet_service(): 20 | return TweetService(TweetDao(database)) 21 | 22 | def setup_function(): 23 | ## Create a test user 24 | hashed_password = bcrypt.hashpw( 25 | b"test password", 26 | bcrypt.gensalt() 27 | ) 28 | new_users = [ 29 | { 30 | 'id' : 1, 31 | 'name' : '송은우', 32 | 'email' : 'songew@gmail.com', 33 | 'profile' : 'test profile', 34 | 'hashed_password' : hashed_password 35 | }, { 36 | 'id' : 2, 37 | 'name' : '김철수', 38 | 'email' : 'tet@gmail.com', 39 | 'profile' : 'test profile', 40 | 'hashed_password' : hashed_password 41 | } 42 | ] 43 | database.execute(text(""" 44 | INSERT INTO users ( 45 | id, 46 | name, 47 | email, 48 | profile, 49 | hashed_password 50 | ) VALUES ( 51 | :id, 52 | :name, 53 | :email, 54 | :profile, 55 | :hashed_password 56 | ) 57 | """), new_users) 58 | 59 | ## User 2 의 트윗 미리 생성해 놓기 60 | database.execute(text(""" 61 | INSERT INTO tweets ( 62 | user_id, 63 | tweet 64 | ) VALUES ( 65 | 2, 66 | "Hello World!" 67 | ) 68 | """)) 69 | 70 | 71 | def teardown_function(): 72 | database.execute(text("SET FOREIGN_KEY_CHECKS=0")) 73 | database.execute(text("TRUNCATE users")) 74 | database.execute(text("TRUNCATE tweets")) 75 | database.execute(text("TRUNCATE users_follow_list")) 76 | database.execute(text("SET FOREIGN_KEY_CHECKS=1")) 77 | 78 | def get_user(user_id): 79 | row = database.execute(text(""" 80 | SELECT 81 | id, 82 | name, 83 | email, 84 | profile 85 | FROM users 86 | WHERE id = :user_id 87 | """), { 88 | 'user_id' : user_id 89 | }).fetchone() 90 | 91 | return { 92 | 'id' : row['id'], 93 | 'name' : row['name'], 94 | 'email' : row['email'], 95 | 'profile' : row['profile'] 96 | } if row else None 97 | 98 | def get_follow_list(user_id): 99 | rows = database.execute(text(""" 100 | SELECT follow_user_id as id 101 | FROM users_follow_list 102 | WHERE user_id = :user_id 103 | """), { 104 | 'user_id' : user_id 105 | }).fetchall() 106 | 107 | return [int(row['id']) for row in rows] 108 | 109 | def test_create_new_user(user_service): 110 | new_user = { 111 | 'name' : '홍길동', 112 | 'email' : 'hong@test.com', 113 | 'profile' : '동쪽에서 번쩍, 서쪽에서 번쩍', 114 | 'password' : 'test1234' 115 | } 116 | 117 | new_user_id = user_service.create_new_user(new_user) 118 | created_user = get_user(new_user_id) 119 | 120 | assert created_user == { 121 | 'id' : new_user_id, 122 | 'name' : new_user['name'], 123 | 'profile' : new_user['profile'], 124 | 'email' : new_user['email'], 125 | } 126 | 127 | def test_login(user_service): 128 | ## 이미 생성되어 있는 유저의 이메일과 비밀번호를 사용해서 로그인을 시도. 129 | assert user_service.login({ 130 | 'email' : 'songew@gmail.com', 131 | 'password' : 'test password' 132 | }) 133 | 134 | ## 잘못된 비번으로 로그인 했을때 False가 리턴되는지 테스트 135 | assert not user_service.login({ 136 | 'email' : 'songew@gmail.com', 137 | 'password' : 'test1234' 138 | }) 139 | 140 | def test_generate_access_token(user_service): 141 | ## token 생성후 decode 해서 동일한 유저 아이디가 나오는지 테스트 142 | token = user_service.generate_access_token(1) 143 | payload = jwt.decode(token, config.JWT_SECRET_KEY, 'HS256') 144 | 145 | assert payload['user_id'] == 1 146 | 147 | def test_follow(user_service): 148 | user_service.follow(1, 2) 149 | follow_list = get_follow_list(1) 150 | 151 | assert follow_list == [2] 152 | 153 | def test_unfollow(user_service): 154 | user_service.follow(1, 2) 155 | user_service.unfollow(1, 2) 156 | follow_list = get_follow_list(1) 157 | 158 | assert follow_list == [ ] 159 | 160 | def test_tweet(tweet_service): 161 | tweet_service.tweet(1, "tweet test") 162 | timeline = tweet_service.get_timeline(1) 163 | 164 | assert timeline == [ 165 | { 166 | 'user_id' : 1, 167 | 'tweet' : 'tweet test' 168 | } 169 | ] 170 | 171 | def test_timeline(user_service, tweet_service): 172 | tweet_service.tweet(1, "tweet test") 173 | tweet_service.tweet(2, "tweet test 2") 174 | user_service.follow(1, 2) 175 | 176 | timeline = tweet_service.get_timeline(1) 177 | 178 | assert timeline == [ 179 | { 180 | 'user_id' : 2, 181 | 'tweet' : 'Hello World!' 182 | }, 183 | { 184 | 'user_id' : 1, 185 | 'tweet' : 'tweet test' 186 | }, 187 | { 188 | 'user_id' : 2, 189 | 'tweet' : 'tweet test 2' 190 | } 191 | ] 192 | 193 | def test_save_and_get_profile_picture(user_service): 194 | ## 먼저 profile picture를 읽어들이도록 하자. 195 | ## 저장한 profile picture 가 없음으로 None이 나와야 한다. 196 | user_id = 1 197 | user_profile_picture = user_service.get_profile_picture(user_id) 198 | assert user_profile_picture is None 199 | 200 | ## Profile picture url을 저장하자 201 | test_pic = mock.Mock() 202 | filename = "test.png" 203 | user_service.save_profile_picture(test_pic, filename, user_id) 204 | 205 | ## Profile picture url을 읽어들이자 206 | actual_profile_picture = user_service.get_profile_picture(user_id) 207 | assert actual_profile_picture == "https://s3.ap-northeast-2.amazonaws.com/test/test.png" 208 | -------------------------------------------------------------------------------- /chapter11/test/test_view.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import bcrypt 3 | import json 4 | import config 5 | import io 6 | 7 | from app import create_app 8 | from sqlalchemy import create_engine, text 9 | from unittest import mock 10 | 11 | database = create_engine(config.test_config['DB_URL'], encoding= 'utf-8', max_overflow = 0) 12 | 13 | @pytest.fixture 14 | @mock.patch("app.boto3") 15 | def api(mock_boto3): 16 | mock_boto3.client.return_value = mock.Mock() 17 | 18 | app = create_app(config.test_config) 19 | app.config['TESTING'] = True 20 | api = app.test_client() 21 | 22 | return api 23 | 24 | def setup_function(): 25 | ## Create a test user 26 | hashed_password = bcrypt.hashpw( 27 | b"test password", 28 | bcrypt.gensalt() 29 | ) 30 | new_users = [ 31 | { 32 | 'id' : 1, 33 | 'name' : '송은우', 34 | 'email' : 'songew@gmail.com', 35 | 'profile' : 'test profile', 36 | 'hashed_password' : hashed_password 37 | }, { 38 | 'id' : 2, 39 | 'name' : '김철수', 40 | 'email' : 'tet@gmail.com', 41 | 'profile' : 'test profile', 42 | 'hashed_password' : hashed_password 43 | } 44 | ] 45 | database.execute(text(""" 46 | INSERT INTO users ( 47 | id, 48 | name, 49 | email, 50 | profile, 51 | hashed_password 52 | ) VALUES ( 53 | :id, 54 | :name, 55 | :email, 56 | :profile, 57 | :hashed_password 58 | ) 59 | """), new_users) 60 | 61 | ## User 2 의 트윗 미리 생성해 놓기 62 | database.execute(text(""" 63 | INSERT INTO tweets ( 64 | user_id, 65 | tweet 66 | ) VALUES ( 67 | 2, 68 | "Hello World!" 69 | ) 70 | """)) 71 | 72 | 73 | def teardown_function(): 74 | database.execute(text("SET FOREIGN_KEY_CHECKS=0")) 75 | database.execute(text("TRUNCATE users")) 76 | database.execute(text("TRUNCATE tweets")) 77 | database.execute(text("TRUNCATE users_follow_list")) 78 | database.execute(text("SET FOREIGN_KEY_CHECKS=1")) 79 | 80 | def test_ping(api): 81 | resp = api.get('/ping') 82 | assert b'pong' in resp.data 83 | 84 | def test_login(api): 85 | resp = api.post( 86 | '/login', 87 | data = json.dumps({'email' : 'songew@gmail.com', 'password' : 'test password'}), 88 | content_type = 'application/json' 89 | ) 90 | assert b"access_token" in resp.data 91 | 92 | def test_unauthorized(api): 93 | # access token이 없이는 401 응답을 리턴하는지를 확인 94 | resp = api.post( 95 | '/tweet', 96 | data = json.dumps({'tweet' : "Hello World!"}), 97 | content_type = 'application/json' 98 | ) 99 | assert resp.status_code == 401 100 | 101 | resp = api.post( 102 | '/follow', 103 | data = json.dumps({'follow' : 2}), 104 | content_type = 'application/json' 105 | ) 106 | assert resp.status_code == 401 107 | 108 | resp = api.post( 109 | '/unfollow', 110 | data = json.dumps({'unfollow' : 2}), 111 | content_type = 'application/json' 112 | ) 113 | assert resp.status_code == 401 114 | 115 | def test_tweet(api): 116 | ## 로그인 117 | resp = api.post( 118 | '/login', 119 | data = json.dumps({'email' : 'songew@gmail.com', 'password' : 'test password'}), 120 | content_type = 'application/json' 121 | ) 122 | resp_json = json.loads(resp.data.decode('utf-8')) 123 | access_token = resp_json['access_token'] 124 | 125 | ## tweet 126 | resp = api.post( 127 | '/tweet', 128 | data = json.dumps({'tweet' : "Hello World!"}), 129 | content_type = 'application/json', 130 | headers = {'Authorization' : access_token} 131 | ) 132 | assert resp.status_code == 200 133 | 134 | ## tweet 확인 135 | resp = api.get(f'/timeline/1') 136 | tweets = json.loads(resp.data.decode('utf-8')) 137 | 138 | assert resp.status_code == 200 139 | assert tweets == { 140 | 'user_id' : 1, 141 | 'timeline' : [ 142 | { 143 | 'user_id' : 1, 144 | 'tweet' : "Hello World!" 145 | } 146 | ] 147 | } 148 | 149 | def test_follow(api): 150 | # 로그인 151 | resp = api.post( 152 | '/login', 153 | data = json.dumps({'email' : 'songew@gmail.com', 'password' : 'test password'}), 154 | content_type = 'application/json' 155 | ) 156 | resp_json = json.loads(resp.data.decode('utf-8')) 157 | access_token = resp_json['access_token'] 158 | 159 | ## 먼저 유저 1의 tweet 확인 해서 tweet 리스트가 비어 있는것을 확인 160 | resp = api.get(f'/timeline/1') 161 | tweets = json.loads(resp.data.decode('utf-8')) 162 | 163 | assert resp.status_code == 200 164 | assert tweets == { 165 | 'user_id' : 1, 166 | 'timeline' : [ ] 167 | } 168 | 169 | # follow 유저 아이디 = 2 170 | resp = api.post( 171 | '/follow', 172 | data = json.dumps({'follow' : 2}), 173 | content_type = 'application/json', 174 | headers = {'Authorization' : access_token} 175 | ) 176 | assert resp.status_code == 200 177 | 178 | ## 이제 유저 1의 tweet 확인 해서 유저 2의 tweet의 리턴 되는것을 확인 179 | resp = api.get(f'/timeline/1') 180 | tweets = json.loads(resp.data.decode('utf-8')) 181 | 182 | assert resp.status_code == 200 183 | assert tweets == { 184 | 'user_id' : 1, 185 | 'timeline' : [ 186 | { 187 | 'user_id' : 2, 188 | 'tweet' : "Hello World!" 189 | } 190 | ] 191 | } 192 | 193 | def test_unfollow(api): 194 | # 로그인 195 | resp = api.post( 196 | '/login', 197 | data = json.dumps({'email' : 'songew@gmail.com', 'password' : 'test password'}), 198 | content_type = 'application/json' 199 | ) 200 | resp_json = json.loads(resp.data.decode('utf-8')) 201 | access_token = resp_json['access_token'] 202 | 203 | # follow 유저 아이디 = 2 204 | resp = api.post( 205 | '/follow', 206 | data = json.dumps({'follow' : 2}), 207 | content_type = 'application/json', 208 | headers = {'Authorization' : access_token} 209 | ) 210 | assert resp.status_code == 200 211 | 212 | ## 이제 유저 1의 tweet 확인 해서 유저 2의 tweet의 리턴 되는것을 확인 213 | resp = api.get(f'/timeline/1') 214 | tweets = json.loads(resp.data.decode('utf-8')) 215 | 216 | assert resp.status_code == 200 217 | assert tweets == { 218 | 'user_id' : 1, 219 | 'timeline' : [ 220 | { 221 | 'user_id' : 2, 222 | 'tweet' : "Hello World!" 223 | } 224 | ] 225 | } 226 | 227 | # unfollow 유저 아이디 = 2 228 | resp = api.post( 229 | '/unfollow', 230 | data = json.dumps({'unfollow' : 2}), 231 | content_type = 'application/json', 232 | headers = {'Authorization' : access_token} 233 | ) 234 | assert resp.status_code == 200 235 | 236 | ## 이제 유저 1의 tweet 확인 해서 유저 2의 tweet이 더 이상 리턴 되지 않는 것을 확인 237 | resp = api.get(f'/timeline/1') 238 | tweets = json.loads(resp.data.decode('utf-8')) 239 | 240 | assert resp.status_code == 200 241 | assert tweets == { 242 | 'user_id' : 1, 243 | 'timeline' : [ ] 244 | } 245 | 246 | def test_save_and_get_profile_picture(api): 247 | # 로그인 248 | resp = api.post( 249 | '/login', 250 | data = json.dumps({'email' : 'songew@gmail.com', 'password' : 'test password'}), 251 | content_type = 'application/json' 252 | ) 253 | resp_json = json.loads(resp.data.decode('utf-8')) 254 | access_token = resp_json['access_token'] 255 | 256 | # 이미지 파일 업로드 257 | resp = api.post( 258 | '/profile-picture', 259 | content_type = 'multipart/form-data', 260 | headers = {'Authorization' : access_token}, 261 | data = { 'profile_pic' : (io.BytesIO(b'some imagge here'), 'profile.png') } 262 | ) 263 | assert resp.status_code == 200 264 | 265 | # GET 이미지 URL 266 | resp = api.get('/profile-picture/1') 267 | data = json.loads(resp.data.decode('utf-8')) 268 | 269 | assert data['img_url'] == f"{config.test_config['S3_BUCKET_URL']}profile.png" 270 | 271 | 272 | 273 | -------------------------------------------------------------------------------- /chapter11/view/__init__.py: -------------------------------------------------------------------------------- 1 | import jwt 2 | 3 | from flask import request, jsonify, current_app, Response, g, send_file 4 | from flask.json import JSONEncoder 5 | from functools import wraps 6 | from werkzeug.utils import secure_filename 7 | 8 | ## Default JSON encoder는 set를 JSON으로 변환할 수 없다. 9 | ## 그럼으로 커스텀 엔코더를 작성해서 set을 list로 변환하여 10 | ## JSON으로 변환 가능하게 해주어야 한다. 11 | class CustomJSONEncoder(JSONEncoder): 12 | def default(self, obj): 13 | if isinstance(obj, set): 14 | return list(obj) 15 | 16 | return JSONEncoder.default(self, obj) 17 | 18 | ######################################################### 19 | # Decorators 20 | ######################################################### 21 | def login_required(f): 22 | @wraps(f) 23 | def decorated_function(*args, **kwargs): 24 | access_token = request.headers.get('Authorization') 25 | if access_token is not None: 26 | try: 27 | payload = jwt.decode(access_token, current_app.config['JWT_SECRET_KEY'], 'HS256') 28 | except jwt.InvalidTokenError: 29 | payload = None 30 | 31 | if payload is None: return Response(status=401) 32 | 33 | user_id = payload['user_id'] 34 | g.user_id = user_id 35 | else: 36 | return Response(status = 401) 37 | 38 | return f(*args, **kwargs) 39 | return decorated_function 40 | 41 | def create_endpoints(app, services): 42 | app.json_encoder = CustomJSONEncoder 43 | 44 | user_service = services.user_service 45 | tweet_service = services.tweet_service 46 | 47 | @app.route("/ping", methods=['GET']) 48 | def ping(): 49 | return "pong" 50 | 51 | @app.route("/sign-up", methods=['POST']) 52 | def sign_up(): 53 | new_user = request.json 54 | new_user = user_service.create_new_user(new_user) 55 | 56 | return jsonify(new_user) 57 | 58 | @app.route('/login', methods=['POST']) 59 | def login(): 60 | credential = request.json 61 | authorized = user_service.login(credential) 62 | 63 | if authorized: 64 | user_credential = user_service.get_user_id_and_password(credential['email']) 65 | user_id = user_credential['id'] 66 | token = user_service.generate_access_token(user_id) 67 | 68 | return jsonify({ 69 | 'user_id' : user_id, 70 | 'access_token' : token 71 | }) 72 | else: 73 | return '', 401 74 | 75 | @app.route('/tweet', methods=['POST']) 76 | @login_required 77 | def tweet(): 78 | user_tweet = request.json 79 | tweet = user_tweet['tweet'] 80 | user_id = g.user_id 81 | 82 | result = tweet_service.tweet(user_id, tweet) 83 | if result is None: 84 | return '300자를 초과했습니다', 400 85 | 86 | return '', 200 87 | 88 | @app.route('/follow', methods=['POST']) 89 | @login_required 90 | def follow(): 91 | payload = request.json 92 | user_id = g.user_id 93 | follow_id = payload['follow'] 94 | 95 | user_service.follow(user_id, follow_id) 96 | 97 | return '', 200 98 | 99 | @app.route('/unfollow', methods=['POST']) 100 | @login_required 101 | def unfollow(): 102 | payload = request.json 103 | user_id = g.user_id 104 | unfollow_id = payload['unfollow'] 105 | 106 | user_service.unfollow(user_id, unfollow_id) 107 | 108 | return '', 200 109 | 110 | @app.route('/timeline/', methods=['GET']) 111 | def timeline(user_id): 112 | timeline = tweet_service.get_timeline(user_id) 113 | 114 | return jsonify({ 115 | 'user_id' : user_id, 116 | 'timeline' : timeline 117 | }) 118 | 119 | @app.route('/timeline', methods=['GET']) 120 | @login_required 121 | def user_timeline(): 122 | timeline = tweet_service.get_timeline(g.user_id) 123 | 124 | return jsonify({ 125 | 'user_id' : user_id, 126 | 'timeline' : timeline 127 | }) 128 | 129 | @app.route('/profile-picture', methods=['POST']) 130 | @login_required 131 | def upload_profile_picture(): 132 | user_id = g.user_id 133 | 134 | if 'profile_pic' not in request.files: 135 | return 'File is missing', 404 136 | 137 | profile_pic = request.files['profile_pic'] 138 | 139 | if profile_pic.filename == '': 140 | return 'File is missing', 404 141 | 142 | filename = secure_filename(profile_pic.filename) 143 | user_service.save_profile_picture(profile_pic, filename, user_id) 144 | 145 | return '', 200 146 | 147 | @app.route('/profile-picture/', methods=['GET']) 148 | def get_profile_picture(user_id): 149 | profile_picture = user_service.get_profile_picture(user_id) 150 | 151 | if profile_picture: 152 | return jsonify({'img_url' : profile_picture}) 153 | else: 154 | return '', 404 155 | -------------------------------------------------------------------------------- /chapter3/app.py: -------------------------------------------------------------------------------- 1 | from flask import Flask 2 | 3 | app = Flask(__name__) 4 | 5 | @app.route("/ping", methods=['GET']) 6 | def ping(): 7 | return "pong" 8 | -------------------------------------------------------------------------------- /chapter5/app.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, request, jsonify 2 | from flask.json import JSONEncoder 3 | 4 | ## Default JSON encoder는 set를 JSON으로 변환할 수 없다. 5 | ## 그럼으로 커스텀 엔코더를 작성해서 set을 list로 변환하여 6 | ## JSON으로 변환 가능하게 해주어야 한다. 7 | class CustomJSONEncoder(JSONEncoder): 8 | def default(self, obj): 9 | if isinstance(obj, set): 10 | return list(obj) 11 | 12 | return JSONEncoder.default(self, obj) 13 | 14 | app = Flask(__name__) 15 | 16 | app.id_count = 1 17 | app.users = {} 18 | app.tweets = [] 19 | app.json_encoder = CustomJSONEncoder 20 | 21 | @app.route("/ping", methods=['GET']) 22 | def ping(): 23 | return "pong" 24 | 25 | @app.route("/sign-up", methods=['POST']) 26 | def sign_up(): 27 | new_user = request.json 28 | new_user["id"] = app.id_count 29 | app.users[app.id_count] = new_user 30 | app.id_count = app.id_count + 1 31 | 32 | return jsonify(new_user) 33 | 34 | @app.route('/tweet', methods=['POST']) 35 | def tweet(): 36 | payload = request.json 37 | user_id = int(payload['id']) 38 | tweet = payload['tweet'] 39 | 40 | if user_id not in app.users: 41 | return '유저가 존재 하지 않습니다', 400 42 | 43 | if len(tweet) > 300: 44 | return '300자를 초과했습니다', 400 45 | 46 | user_id = int(payload['id']) 47 | 48 | app.tweets.append({ 49 | 'user_id' : user_id, 50 | 'tweet' : tweet 51 | }) 52 | 53 | return '', 200 54 | 55 | @app.route('/follow', methods=['POST']) 56 | def follow(): 57 | payload = request.json 58 | user_id = int(payload['id']) 59 | user_id_to_follow = int(payload['follow']) 60 | 61 | if user_id not in app.users or user_id_to_follow not in app.users: 62 | return '유저가 존재 하지 않습니다', 400 63 | 64 | user = app.users[user_id] 65 | user.setdefault('follow', set()).add(user_id_to_follow) 66 | 67 | return jsonify(user) 68 | 69 | @app.route('/unfollow', methods=['POST']) 70 | def unfollow(): 71 | payload = request.json 72 | user_id = int(payload['id']) 73 | user_id_to_follow = int(payload['unfollow']) 74 | 75 | if user_id not in app.users or user_id_to_follow not in app.users: 76 | return '유저가 존재 하지 않습니다', 400 77 | 78 | user = app.users[user_id] 79 | user.setdefault('follow', set()).discard(user_id_to_follow) 80 | 81 | return jsonify(user) 82 | 83 | @app.route('/timeline/', methods=['GET']) 84 | def timeline(user_id): 85 | if user_id not in app.users: 86 | return '유저가 존재 하지 않습니다', 400 87 | 88 | follow_list = app.users[user_id].get('follow', set()) 89 | follow_list.add(user_id) 90 | timeline = [tweet for tweet in app.tweets if tweet['user_id'] in follow_list] 91 | 92 | return jsonify({ 93 | 'user_id' : user_id, 94 | 'timeline' : timeline 95 | }) 96 | -------------------------------------------------------------------------------- /chapter5/follow_and_unfollow_example.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, jsonify, request 2 | from flask.json import JSONEncoder 3 | 4 | ## Default JSON encoder는 set를 JSON으로 변환할 수 없다. 5 | ## 그럼으로 커스텀 엔코더를 작성해서 set을 list로 변환하여 6 | ## JSON으로 변환 가능하게 해주어야 한다. 7 | class CustomJSONEncoder(JSONEncoder): 8 | def default(self, obj): 9 | if isinstance(obj, set): 10 | return list(obj) 11 | 12 | return JSONEncoder.default(self, obj) 13 | 14 | app = Flask(__name__) 15 | 16 | app.id_count = 1 17 | app.users = {} 18 | app.tweets = [] 19 | app.json_encoder = CustomJSONEncoder 20 | 21 | @app.route('/follow', methods=['POST']) 22 | def follow(): 23 | payload = request.json 24 | user_id = int(payload['id']) 25 | user_id_to_follow = int(payload['follow']) 26 | 27 | if user_id not in app.users or user_id_to_follow not in app.users: 28 | return '유저가 존재 하지 않습니다', 400 29 | 30 | user = app.users[user_id] 31 | user.setdefault('follow', set()).add(user_id_to_follow) 32 | 33 | return jsonify(user) 34 | 35 | @app.route('/unfollow', methods=['POST']) 36 | def unfollow(): 37 | payload = request.json 38 | user_id = int(payload['id']) 39 | user_id_to_follow = int(payload['unfollow']) 40 | 41 | if user_id not in app.users or user_id_to_follow not in app.users: 42 | return '유저가 존재 하지 않습니다', 400 43 | 44 | user = app.users[user_id] 45 | user.setdefault('follow', set()).discard(user_id_to_follow) 46 | 47 | return jsonify(user) 48 | 49 | 50 | -------------------------------------------------------------------------------- /chapter5/requirements.txt: -------------------------------------------------------------------------------- 1 | aiofiles==0.4.0 2 | aiohttp==3.3.2 3 | aniso8601==3.0.2 4 | app==0.0.1 5 | asn1crypto==0.24.0 6 | async-timeout==3.0.0 7 | atomicwrites==1.1.5 8 | attrs==18.1.0 9 | Automat==0.7.0 10 | aws-encryption-sdk==1.3.5 11 | bcrypt==3.1.4 12 | beautifulsoup4==4.6.3 13 | boto3==1.7.72 14 | botocore==1.10.72 15 | bs4==0.0.1 16 | certifi==2018.8.13 17 | cffi==1.11.5 18 | chardet==3.0.4 19 | click==6.7 20 | cryptography==2.3 21 | docutils==0.14 22 | Flask==1.0.2 23 | Flask-Cors==3.0.6 24 | Flask-GraphQL==2.0.0 25 | Flask-SQLAlchemy==2.3.2 26 | graphene==2.1.3 27 | graphql-core==2.1 28 | graphql-relay==0.4.5 29 | graphql-server-core==1.1.1 30 | idna==2.7 31 | idna-ssl==1.1.0 32 | itsdangerous==0.24 33 | Jinja2==2.10 34 | jmespath==0.9.3 35 | MarkupSafe==1.0 36 | multidict==4.3.1 37 | mysqlclient==1.3.13 38 | promise==2.1 39 | pycparser==2.18 40 | PyJWT==1.6.4 41 | python-dateutil==2.7.3 42 | Rx==1.6.1 43 | s3transfer==0.1.13 44 | six==1.11.0 45 | SQLAlchemy==1.2.11 46 | typing==3.6.4 47 | Werkzeug==0.14.1 48 | wrapt==1.10.11 49 | yarl==1.2.6 50 | -------------------------------------------------------------------------------- /chapter5/sign_up_example.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, jsonify, request 2 | 3 | app = Flask(__name__) 4 | app.users = {} 5 | app.id_count = 1 6 | 7 | @app.route("/sign-up", methods=['POST']) 8 | def sign_up(): 9 | new_user = request.json 10 | new_user["id"] = app.id_count 11 | app.users[app.id_count] = new_user 12 | app.id_count = app.id_count + 1 13 | 14 | return jsonify(new_user) 15 | -------------------------------------------------------------------------------- /chapter5/timeline_example.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, jsonify, request 2 | from flask.json import JSONEncoder 3 | 4 | ## Default JSON encoder는 set를 JSON으로 변환할 수 없다. 5 | ## 그럼으로 커스텀 엔코더를 작성해서 set을 list로 변환하여 6 | ## JSON으로 변환 가능하게 해주어야 한다. 7 | class CustomJSONEncoder(JSONEncoder): 8 | def default(self, obj): 9 | if isinstance(obj, set): 10 | return list(obj) 11 | 12 | return JSONEncoder.default(self, obj) 13 | 14 | app = Flask(__name__) 15 | 16 | app.id_count = 1 17 | app.users = {} 18 | app.tweets = [] 19 | app.json_encoder = CustomJSONEncoder 20 | 21 | @app.route('/timeline/', methods=['GET']) 22 | def timeline(user_id): 23 | if user_id not in app.users: 24 | return '유저가 존재 하지 않습니다', 400 25 | 26 | follow_list = app.users[user_id].get('follow', set()) 27 | follow_list.add(user_id) 28 | timeline = [tweet for tweet in app.tweets if tweet['user_id'] in follow_list] 29 | 30 | return jsonify({ 31 | 'user_id' : user_id, 32 | 'timeline' : timeline 33 | }) 34 | -------------------------------------------------------------------------------- /chapter5/tweet_example.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, jsonify, request 2 | 3 | app = Flask(__name__) 4 | app.users = {} 5 | app.id_count = 1 6 | app.tweets = [] 7 | 8 | @app.route('/tweet', methods=['POST']) 9 | def tweet(): 10 | payload = request.json 11 | user_id = int(payload['id']) 12 | tweet = payload['tweet'] 13 | 14 | if user_id not in app.users: 15 | return '유저가 존재 하지 않습니다', 400 16 | 17 | if len(tweet) > 300: 18 | return '300자를 초과했습니다', 400 19 | 20 | user_id = int(payload['id']) 21 | 22 | app.tweets.append({ 23 | 'user_id' : user_id, 24 | 'tweet' : tweet 25 | }) 26 | 27 | return '', 200 28 | -------------------------------------------------------------------------------- /chapter6/app.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, request, jsonify, current_app 2 | from flask.json import JSONEncoder 3 | from sqlalchemy import create_engine, text 4 | 5 | ## Default JSON encoder는 set를 JSON으로 변환할 수 없다. 6 | ## 그럼으로 커스텀 엔코더를 작성해서 set을 list로 변환하여 7 | ## JSON으로 변환 가능하게 해주어야 한다. 8 | class CustomJSONEncoder(JSONEncoder): 9 | def default(self, obj): 10 | if isinstance(obj, set): 11 | return list(obj) 12 | 13 | return JSONEncoder.default(self, obj) 14 | 15 | def get_user(user_id): 16 | user = current_app.database.execute(text(""" 17 | SELECT 18 | id, 19 | name, 20 | email, 21 | profile 22 | FROM users 23 | WHERE id = :user_id 24 | """), { 25 | 'user_id' : user_id 26 | }).fetchone() 27 | 28 | return { 29 | 'id' : user['id'], 30 | 'name' : user['name'], 31 | 'email' : user['email'], 32 | 'profile' : user['profile'] 33 | } if user else None 34 | 35 | def insert_user(user): 36 | return current_app.database.execute(text(""" 37 | INSERT INTO users ( 38 | name, 39 | email, 40 | profile, 41 | hashed_password 42 | ) VALUES ( 43 | :name, 44 | :email, 45 | :profile, 46 | :password 47 | ) 48 | """), user).lastrowid 49 | 50 | def insert_tweet(user_tweet): 51 | return current_app.database.execute(text(""" 52 | INSERT INTO tweets ( 53 | user_id, 54 | tweet 55 | ) VALUES ( 56 | :id, 57 | :tweet 58 | ) 59 | """), user_tweet).rowcount 60 | 61 | def insert_follow(user_follow): 62 | return current_app.database.execute(text(""" 63 | INSERT INTO users_follow_list ( 64 | user_id, 65 | follow_user_id 66 | ) VALUES ( 67 | :id, 68 | :follow 69 | ) 70 | """), user_follow).rowcount 71 | 72 | def insert_unfollow(user_unfollow): 73 | return current_app.database.execute(text(""" 74 | DELETE FROM users_follow_list 75 | WHERE user_id = :id 76 | AND follow_user_id = :unfollow 77 | """), user_unfollow).rowcount 78 | 79 | def get_timeline(user_id): 80 | timeline = current_app.database.execute(text(""" 81 | SELECT 82 | t.user_id, 83 | t.tweet 84 | FROM tweets t 85 | LEFT JOIN users_follow_list ufl ON ufl.user_id = :user_id 86 | WHERE t.user_id = :user_id 87 | OR t.user_id = ufl.follow_user_id 88 | """), { 89 | 'user_id' : user_id 90 | }).fetchall() 91 | 92 | return [{ 93 | 'user_id' : tweet['user_id'], 94 | 'tweet' : tweet['tweet'] 95 | } for tweet in timeline] 96 | 97 | def create_app(test_config = None): 98 | app = Flask(__name__) 99 | 100 | app.json_encoder = CustomJSONEncoder 101 | 102 | if test_config is None: 103 | app.config.from_pyfile("config.py") 104 | else: 105 | app.config.update(test_config) 106 | 107 | database = create_engine(app.config['DB_URL'], encoding = 'utf-8', max_overflow = 0) 108 | app.database = database 109 | 110 | @app.route("/ping", methods=['GET']) 111 | def ping(): 112 | return "pong" 113 | 114 | @app.route("/sign-up", methods=['POST']) 115 | def sign_up(): 116 | new_user = request.json 117 | new_user_id = insert_user(new_user) 118 | new_user = get_user(new_user_id) 119 | 120 | return jsonify(new_user) 121 | 122 | @app.route('/tweet', methods=['POST']) 123 | def tweet(): 124 | user_tweet = request.json 125 | tweet = user_tweet['tweet'] 126 | 127 | if len(tweet) > 300: 128 | return '300자를 초과했습니다', 400 129 | 130 | insert_tweet(user_tweet) 131 | 132 | return '', 200 133 | 134 | @app.route('/follow', methods=['POST']) 135 | def follow(): 136 | payload = request.json 137 | insert_follow(payload) 138 | 139 | return '', 200 140 | 141 | @app.route('/unfollow', methods=['POST']) 142 | def unfollow(): 143 | payload = request.json 144 | insert_unfollow(payload) 145 | 146 | return '', 200 147 | 148 | @app.route('/timeline/', methods=['GET']) 149 | def timeline(user_id): 150 | return jsonify({ 151 | 'user_id' : user_id, 152 | 'timeline' : get_timeline(user_id) 153 | }) 154 | 155 | return app 156 | 157 | -------------------------------------------------------------------------------- /chapter6/config.py: -------------------------------------------------------------------------------- 1 | db = { 2 | 'user' : 'root', 3 | 'password' : 'test1234', 4 | 'host' : 'localhost', 5 | 'port' : 3306, 6 | 'database' : 'miniter' 7 | } 8 | 9 | DB_URL = f"mysql+mysqlconnector://{db['user']}:{db['password']}@{db['host']}:{db['port']}/{db['database']}?charset=utf8" 10 | -------------------------------------------------------------------------------- /chapter6/create_table.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE users( 2 | id INT NOT NULL AUTO_INCREMENT, 3 | name VARCHAR(255) NOT NULL, 4 | email VARCHAR(255) NOT NULL, 5 | hashed_password VARCHAR(255) NOT NULL, 6 | profile VARCHAR(2000) NOT NULL, 7 | created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, 8 | updated_at TIMESTAMP NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP, 9 | PRIMARY KEY (id), 10 | UNIQUE KEY email (email) 11 | ); 12 | 13 | CREATE TABLE users_follow_list( 14 | user_id INT NOT NULL, 15 | follow_user_id INT NOT NULL, 16 | created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, 17 | PRIMARY KEY (user_id, follow_user_id), 18 | CONSTRAINT users_follow_list_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id), 19 | CONSTRAINT users_follow_list_follow_user_id_fkey FOREIGN KEY (follow_user_id) REFERENCES users(id) 20 | ); 21 | 22 | CREATE TABLE tweets( 23 | id INT NOT NULL AUTO_INCREMENT, 24 | user_id INT NOT NULL, 25 | tweet VARCHAR(300) NOT NULL, 26 | created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), 27 | CONSTRAINT tweets_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) 28 | ); 29 | -------------------------------------------------------------------------------- /chapter6/delete_example.sql: -------------------------------------------------------------------------------- 1 | DELETE FROM users WHERE age < 20 2 | -------------------------------------------------------------------------------- /chapter6/insert_example.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO users ( 2 | id, 3 | name, 4 | age, 5 | gender 6 | ) VALUES ( 7 | 1, 8 | "송은우", 9 | 35, 10 | "남자" 11 | ), ( 12 | 2, 13 | "Robert Kelly", 14 | 28, 15 | "남자" 16 | ), ( 17 | 3, 18 | "Cristiano Ronaldo", 19 | 33, 20 | "남자" 21 | ) 22 | -------------------------------------------------------------------------------- /chapter6/join_example.sql: -------------------------------------------------------------------------------- 1 | SELECT 2 | users.name, 3 | user_address.address 4 | FROM users 5 | JOIN user_address ON users.id = user_address.user_id 6 | -------------------------------------------------------------------------------- /chapter6/select_example.sql: -------------------------------------------------------------------------------- 1 | SELECT 2 | id, 3 | name, 4 | age, 5 | gender 6 | FROM users 7 | -------------------------------------------------------------------------------- /chapter6/select_with_where_example.sql: -------------------------------------------------------------------------------- 1 | SELECT 2 | id, 3 | name, 4 | age, 5 | gender 6 | FROM users 7 | WHERE name = "송은우" 8 | -------------------------------------------------------------------------------- /chapter6/update_example.sql: -------------------------------------------------------------------------------- 1 | UPDATE users SET age = 25 WHERE name = "아이유" 2 | -------------------------------------------------------------------------------- /chapter7/app.py: -------------------------------------------------------------------------------- 1 | import jwt 2 | import bcrypt 3 | 4 | from flask import Flask, request, jsonify, current_app, Response, g 5 | from flask.json import JSONEncoder 6 | from sqlalchemy import create_engine, text 7 | from datetime import datetime, timedelta 8 | from functools import wraps 9 | 10 | ## Default JSON encoder는 set를 JSON으로 변환할 수 없다. 11 | ## 그럼으로 커스텀 엔코더를 작성해서 set을 list로 변환하여 12 | ## JSON으로 변환 가능하게 해주어야 한다. 13 | class CustomJSONEncoder(JSONEncoder): 14 | def default(self, obj): 15 | if isinstance(obj, set): 16 | return list(obj) 17 | 18 | return JSONEncoder.default(self, obj) 19 | 20 | def get_user(user_id): 21 | user = current_app.database.execute(text(""" 22 | SELECT 23 | id, 24 | name, 25 | email, 26 | profile 27 | FROM users 28 | WHERE id = :user_id 29 | """), { 30 | 'user_id' : user_id 31 | }).fetchone() 32 | 33 | return { 34 | 'id' : user['id'], 35 | 'name' : user['name'], 36 | 'email' : user['email'], 37 | 'profile' : user['profile'] 38 | } if user else None 39 | 40 | def insert_user(user): 41 | return current_app.database.execute(text(""" 42 | INSERT INTO users ( 43 | name, 44 | email, 45 | profile, 46 | hashed_password 47 | ) VALUES ( 48 | :name, 49 | :email, 50 | :profile, 51 | :password 52 | ) 53 | """), user).lastrowid 54 | 55 | def insert_tweet(user_tweet): 56 | return current_app.database.execute(text(""" 57 | INSERT INTO tweets ( 58 | user_id, 59 | tweet 60 | ) VALUES ( 61 | :id, 62 | :tweet 63 | ) 64 | """), user_tweet).rowcount 65 | 66 | def insert_follow(user_follow): 67 | return current_app.database.execute(text(""" 68 | INSERT INTO users_follow_list ( 69 | user_id, 70 | follow_user_id 71 | ) VALUES ( 72 | :id, 73 | :follow 74 | ) 75 | """), user_follow).rowcount 76 | 77 | def insert_unfollow(user_unfollow): 78 | return current_app.database.execute(text(""" 79 | DELETE FROM users_follow_list 80 | WHERE user_id = :id 81 | AND follow_user_id = :unfollow 82 | """), user_unfollow).rowcount 83 | 84 | def get_timeline(user_id): 85 | timeline = current_app.database.execute(text(""" 86 | SELECT 87 | t.user_id, 88 | t.tweet 89 | FROM tweets t 90 | LEFT JOIN users_follow_list ufl ON ufl.user_id = :user_id 91 | WHERE t.user_id = :user_id 92 | OR t.user_id = ufl.follow_user_id 93 | """), { 94 | 'user_id' : user_id 95 | }).fetchall() 96 | 97 | return [{ 98 | 'user_id' : tweet['user_id'], 99 | 'tweet' : tweet['tweet'] 100 | } for tweet in timeline] 101 | 102 | def get_user_id_and_password(email): 103 | row = current_app.database.execute(text(""" 104 | SELECT 105 | id, 106 | hashed_password 107 | FROM users 108 | WHERE email = :email 109 | """), {'email' : email}).fetchone() 110 | 111 | return { 112 | 'id' : row['id'], 113 | 'hashed_password' : row['hashed_password'] 114 | } if row else None 115 | 116 | ######################################################### 117 | # Decorators 118 | ######################################################### 119 | def login_required(f): 120 | @wraps(f) 121 | def decorated_function(*args, **kwargs): 122 | access_token = request.headers.get('Authorization') 123 | if access_token is not None: 124 | try: 125 | payload = jwt.decode(access_token, current_app.config['JWT_SECRET_KEY'], 'HS256') 126 | except jwt.InvalidTokenError: 127 | payload = None 128 | 129 | if payload is None: return Response(status=401) 130 | 131 | user_id = payload['user_id'] 132 | g.user_id = user_id 133 | g.user = get_user(user_id) if user_id else None 134 | else: 135 | return Response(status = 401) 136 | 137 | return f(*args, **kwargs) 138 | return decorated_function 139 | 140 | def create_app(test_config = None): 141 | app = Flask(__name__) 142 | 143 | app.json_encoder = CustomJSONEncoder 144 | 145 | if test_config is None: 146 | app.config.from_pyfile("config.py") 147 | else: 148 | app.config.update(test_config) 149 | 150 | database = create_engine(app.config['DB_URL'], encoding = 'utf-8', max_overflow = 0) 151 | app.database = database 152 | 153 | @app.route("/ping", methods=['GET']) 154 | def ping(): 155 | return "pong" 156 | 157 | @app.route("/sign-up", methods=['POST']) 158 | def sign_up(): 159 | new_user = request.json 160 | new_user['password'] = bcrypt.hashpw( 161 | new_user['password'].encode('UTF-8'), 162 | bcrypt.gensalt() 163 | ) 164 | 165 | new_user_id = insert_user(new_user) 166 | new_user = get_user(new_user_id) 167 | 168 | return jsonify(new_user) 169 | 170 | @app.route('/login', methods=['POST']) 171 | def login(): 172 | credential = request.json 173 | email = credential['email'] 174 | password = credential['password'] 175 | user_credential = get_user_id_and_password(email) 176 | 177 | if user_credential and bcrypt.checkpw(password.encode('UTF-8'), user_credential['hashed_password'].encode('UTF-8')): 178 | user_id = user_credential['id'] 179 | payload = { 180 | 'user_id' : user_id, 181 | 'exp' : datetime.utcnow() + timedelta(seconds = 60 * 60 * 24) 182 | } 183 | token = jwt.encode(payload, app.config['JWT_SECRET_KEY'], 'HS256') 184 | 185 | return jsonify({ 186 | 'access_token' : token.decode('UTF-8') 187 | }) 188 | else: 189 | return '', 401 190 | 191 | @app.route('/tweet', methods=['POST']) 192 | @login_required 193 | def tweet(): 194 | user_tweet = request.json 195 | user_tweet['id'] = g.user_id 196 | tweet = user_tweet['tweet'] 197 | 198 | if len(tweet) > 300: 199 | return '300자를 초과했습니다', 400 200 | 201 | insert_tweet(user_tweet) 202 | 203 | return '', 200 204 | 205 | @app.route('/follow', methods=['POST']) 206 | @login_required 207 | def follow(): 208 | payload = request.json 209 | payload['id'] = g.user_id 210 | 211 | insert_follow(payload) 212 | 213 | return '', 200 214 | 215 | @app.route('/unfollow', methods=['POST']) 216 | @login_required 217 | def unfollow(): 218 | payload = request.json 219 | payload['id'] = g.user_id 220 | 221 | insert_unfollow(payload) 222 | 223 | return '', 200 224 | 225 | @app.route('/timeline/', methods=['GET']) 226 | def timeline(user_id): 227 | return jsonify({ 228 | 'user_id' : user_id, 229 | 'timeline' : get_timeline(user_id) 230 | }) 231 | 232 | return app 233 | 234 | -------------------------------------------------------------------------------- /chapter7/bcrypt.py: -------------------------------------------------------------------------------- 1 | import bcrypt 2 | 3 | bcrypt.hashpw(b"secrete password", bcrypt.gensalt()) # >>> b'$2b$12$.XIJKgAepSrI5ghrJUaJa.ogLHJHLyY8ikIC.7gDoUMkaMfzNhGo6' 4 | bcrypt.hashpw(b"secrete password", bcrypt.gensalt()).hex() # >>> '243262243132242e6b426f39757a69666e344f563852694a43666b5165445469397448446c4d366635613542396847366d5132446d62744b70357353' 5 | -------------------------------------------------------------------------------- /chapter7/config.py: -------------------------------------------------------------------------------- 1 | db = { 2 | 'user' : 'root', 3 | 'password' : 'test1234', 4 | 'host' : 'localhost', 5 | 'port' : 3306, 6 | 'database' : 'miniter' 7 | } 8 | 9 | DB_URL = f"mysql+mysqlconnector://{db['user']}:{db['password']}@{db['host']}:{db['port']}/{db['database']}?charset=utf8" 10 | -------------------------------------------------------------------------------- /chapter7/decorator_example.py: -------------------------------------------------------------------------------- 1 | from functools import wraps 2 | 3 | def test_decorator(f): 4 | @wraps(f) 5 | def decorated_function(*args, **kwargs): 6 | print("Decorated Function") 7 | return f(*args, **kwargs) 8 | 9 | return decorated_function 10 | 11 | @test_decorator 12 | def func(): 13 | print("Calling func function") 14 | -------------------------------------------------------------------------------- /chapter7/hashlib_example.py: -------------------------------------------------------------------------------- 1 | import hashlib 2 | 3 | m = hashlib.sha256() 4 | m.update(b"test password") 5 | m.hexdigest() # >>> '0b47c69b1033498d5f33f5f7d97bb6a3126134751629f4d0185c115db44c094e' 6 | -------------------------------------------------------------------------------- /chapter7/pyjwt_example.py: -------------------------------------------------------------------------------- 1 | import jwt 2 | 3 | data_to_encode = {'some': 'payload'} 4 | encryption_secret = 'secrete' 5 | algorithm = 'HS256' 6 | encoded = jwt.encode(data_to_encode, encryption_secret, algorithm=algorithm) # >>> 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzb21lIjoicGF5bG9hZCJ9.4twFt5N iznN84AWoo1d7KO1T_yoc0Z6XOpOVswacPZg' 7 | jwt.decode(encoded, encryption_secret, algorithms=[algorithm]) # >>> {'some': 'payload'} 8 | -------------------------------------------------------------------------------- /chapter8/app.py: -------------------------------------------------------------------------------- 1 | import jwt 2 | import bcrypt 3 | 4 | from flask import Flask, request, jsonify, current_app, Response, g 5 | from flask.json import JSONEncoder 6 | from sqlalchemy import create_engine, text 7 | from datetime import datetime, timedelta 8 | from functools import wraps 9 | from flask_cors import CORS 10 | 11 | ## Default JSON encoder는 set를 JSON으로 변환할 수 없다. 12 | ## 그럼으로 커스텀 엔코더를 작성해서 set을 list로 변환하여 13 | ## JSON으로 변환 가능하게 해주어야 한다. 14 | class CustomJSONEncoder(JSONEncoder): 15 | def default(self, obj): 16 | if isinstance(obj, set): 17 | return list(obj) 18 | 19 | return JSONEncoder.default(self, obj) 20 | 21 | def get_user(user_id): 22 | user = current_app.database.execute(text(""" 23 | SELECT 24 | id, 25 | name, 26 | email, 27 | profile 28 | FROM users 29 | WHERE id = :user_id 30 | """), { 31 | 'user_id' : user_id 32 | }).fetchone() 33 | 34 | return { 35 | 'id' : user['id'], 36 | 'name' : user['name'], 37 | 'email' : user['email'], 38 | 'profile' : user['profile'] 39 | } if user else None 40 | 41 | def insert_user(user): 42 | return current_app.database.execute(text(""" 43 | INSERT INTO users ( 44 | name, 45 | email, 46 | profile, 47 | hashed_password 48 | ) VALUES ( 49 | :name, 50 | :email, 51 | :profile, 52 | :password 53 | ) 54 | """), user).lastrowid 55 | 56 | def insert_tweet(user_tweet): 57 | return current_app.database.execute(text(""" 58 | INSERT INTO tweets ( 59 | user_id, 60 | tweet 61 | ) VALUES ( 62 | :id, 63 | :tweet 64 | ) 65 | """), user_tweet).rowcount 66 | 67 | def insert_follow(user_follow): 68 | return current_app.database.execute(text(""" 69 | INSERT INTO users_follow_list ( 70 | user_id, 71 | follow_user_id 72 | ) VALUES ( 73 | :id, 74 | :follow 75 | ) 76 | """), user_follow).rowcount 77 | 78 | def insert_unfollow(user_unfollow): 79 | return current_app.database.execute(text(""" 80 | DELETE FROM users_follow_list 81 | WHERE user_id = :id 82 | AND follow_user_id = :unfollow 83 | """), user_unfollow).rowcount 84 | 85 | def get_timeline(user_id): 86 | timeline = current_app.database.execute(text(""" 87 | SELECT 88 | t.user_id, 89 | t.tweet 90 | FROM tweets t 91 | LEFT JOIN users_follow_list ufl ON ufl.user_id = :user_id 92 | WHERE t.user_id = :user_id 93 | OR t.user_id = ufl.follow_user_id 94 | """), { 95 | 'user_id' : user_id 96 | }).fetchall() 97 | 98 | return [{ 99 | 'user_id' : tweet['user_id'], 100 | 'tweet' : tweet['tweet'] 101 | } for tweet in timeline] 102 | 103 | def get_user_id_and_password(email): 104 | row = current_app.database.execute(text(""" 105 | SELECT 106 | id, 107 | hashed_password 108 | FROM users 109 | WHERE email = :email 110 | """), {'email' : email}).fetchone() 111 | 112 | return { 113 | 'id' : row['id'], 114 | 'hashed_password' : row['hashed_password'] 115 | } if row else None 116 | 117 | ######################################################### 118 | # Decorators 119 | ######################################################### 120 | def login_required(f): 121 | @wraps(f) 122 | def decorated_function(*args, **kwargs): 123 | access_token = request.headers.get('Authorization') 124 | if access_token is not None: 125 | try: 126 | payload = jwt.decode(access_token, current_app.config['JWT_SECRET_KEY'], 'HS256') 127 | except jwt.InvalidTokenError: 128 | payload = None 129 | 130 | if payload is None: return Response(status=401) 131 | 132 | user_id = payload['user_id'] 133 | g.user_id = user_id 134 | g.user = get_user(user_id) if user_id else None 135 | else: 136 | return Response(status = 401) 137 | 138 | return f(*args, **kwargs) 139 | return decorated_function 140 | 141 | def create_app(test_config = None): 142 | app = Flask(__name__) 143 | 144 | CORS(app) 145 | 146 | app.json_encoder = CustomJSONEncoder 147 | 148 | if test_config is None: 149 | app.config.from_pyfile("config.py") 150 | else: 151 | app.config.update(test_config) 152 | 153 | database = create_engine(app.config['DB_URL'], encoding = 'utf-8', max_overflow = 0) 154 | app.database = database 155 | 156 | @app.route("/ping", methods=['GET']) 157 | def ping(): 158 | return "pong" 159 | 160 | @app.route("/sign-up", methods=['POST']) 161 | def sign_up(): 162 | new_user = request.json 163 | new_user['password'] = bcrypt.hashpw( 164 | new_user['password'].encode('UTF-8'), 165 | bcrypt.gensalt() 166 | ) 167 | 168 | new_user_id = insert_user(new_user) 169 | new_user = get_user(new_user_id) 170 | 171 | return jsonify(new_user) 172 | 173 | @app.route('/login', methods=['POST']) 174 | def login(): 175 | credential = request.json 176 | email = credential['email'] 177 | password = credential['password'] 178 | user_credential = get_user_id_and_password(email) 179 | 180 | if user_credential and bcrypt.checkpw(password.encode('UTF-8'), user_credential['hashed_password'].encode('UTF-8')): 181 | user_id = user_credential['id'] 182 | payload = { 183 | 'user_id' : user_id, 184 | 'exp' : datetime.utcnow() + timedelta(seconds = 60 * 60 * 24) 185 | } 186 | token = jwt.encode(payload, app.config['JWT_SECRET_KEY'], 'HS256') 187 | 188 | return jsonify({ 189 | 'user_id' : user_id, 190 | 'access_token' : token.decode('UTF-8') 191 | }) 192 | else: 193 | return '', 401 194 | 195 | @app.route('/tweet', methods=['POST']) 196 | @login_required 197 | def tweet(): 198 | user_tweet = request.json 199 | user_tweet['id'] = g.user_id 200 | tweet = user_tweet['tweet'] 201 | 202 | if len(tweet) > 300: 203 | return '300자를 초과했습니다', 400 204 | 205 | insert_tweet(user_tweet) 206 | 207 | return '', 200 208 | 209 | @app.route('/follow', methods=['POST']) 210 | @login_required 211 | def follow(): 212 | payload = request.json 213 | payload['id'] = g.user_id 214 | 215 | insert_follow(payload) 216 | 217 | return '', 200 218 | 219 | @app.route('/unfollow', methods=['POST']) 220 | @login_required 221 | def unfollow(): 222 | payload = request.json 223 | payload['id'] = g.user_id 224 | 225 | insert_unfollow(payload) 226 | 227 | return '', 200 228 | 229 | @app.route('/timeline/', methods=['GET']) 230 | def timeline(user_id): 231 | return jsonify({ 232 | 'user_id' : user_id, 233 | 'timeline' : get_timeline(user_id) 234 | }) 235 | 236 | @app.route('/timeline', methods=['GET']) 237 | @login_required 238 | def user_timeline(): 239 | user_id = g.user_id 240 | 241 | return jsonify({ 242 | 'user_id' : user_id, 243 | 'timeline' : get_timeline(user_id) 244 | }) 245 | 246 | return app 247 | -------------------------------------------------------------------------------- /chapter8/config.py: -------------------------------------------------------------------------------- 1 | db = { 2 | 'user' : 'test', 3 | 'password' : 'test1234', 4 | 'host' : 'localhost', 5 | 'port' : 3306, 6 | 'database' : 'miniter' 7 | } 8 | 9 | DB_URL = f"mysql+mysqlconnector://{db['user']}:{db['password']}@{db['host']}:{db['port']}/{db['database']}?charset=utf8" 10 | JWT_SECRET_KEY = 'SOME_SUPER_SECRET_KEY' 11 | JWT_EXP_DELTA_SECONDS = 7 * 24 * 60 * 60 12 | 13 | test_db = { 14 | 'user' : 'root', 15 | 'password' : 'test1234', 16 | 'host' : 'localhost', 17 | 'port' : 3306, 18 | 'database' : 'miniter_test' 19 | } 20 | 21 | test_config = { 22 | 'DB_URL' : f"mysql+mysqlconnector://{test_db['user']}:{test_db['password']}@{test_db['host']}:{test_db['port']}/{test_db['database']}?charset=utf8", 23 | 'JWT_SECRET_KEY' : 'SOME_SUPER_SECRET_KEY', 24 | 'JWT_EXP_DELTA_SECONDS' : 7 * 24 * 60 * 60 25 | } 26 | -------------------------------------------------------------------------------- /chapter8/mult.py: -------------------------------------------------------------------------------- 1 | def multiply_by_two(x): 2 | return x * 2 3 | 4 | def test_multiply_by_two(): 5 | assert multiply_by_two(4) == 8 6 | -------------------------------------------------------------------------------- /chapter8/test_endpoints.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import bcrypt 3 | import json 4 | import config 5 | 6 | from app import create_app 7 | from sqlalchemy import create_engine, text 8 | 9 | database = create_engine(config.test_config['DB_URL'], encoding= 'utf-8', max_overflow = 0) 10 | 11 | @pytest.fixture 12 | def api(): 13 | app = create_app(config.test_config) 14 | app.config['TESTING'] = True 15 | api = app.test_client() 16 | 17 | return api 18 | def setup_function(): 19 | ## Create a test user 20 | hashed_password = bcrypt.hashpw( 21 | b"test password", 22 | bcrypt.gensalt() 23 | ) 24 | new_users = [ 25 | { 26 | 'id' : 1, 27 | 'name' : '송은우', 28 | 'email' : 'songew@gmail.com', 29 | 'profile' : 'test profile', 30 | 'hashed_password' : hashed_password 31 | }, { 32 | 'id' : 2, 33 | 'name' : '김철수', 34 | 'email' : 'tet@gmail.com', 35 | 'profile' : 'test profile', 36 | 'hashed_password' : hashed_password 37 | } 38 | ] 39 | database.execute(text(""" 40 | INSERT INTO users ( 41 | id, 42 | name, 43 | email, 44 | profile, 45 | hashed_password 46 | ) VALUES ( 47 | :id, 48 | :name, 49 | :email, 50 | :profile, 51 | :hashed_password 52 | ) 53 | """), new_users) 54 | 55 | ## User 2 의 트윗 미리 생성해 놓기 56 | database.execute(text(""" 57 | INSERT INTO tweets ( 58 | user_id, 59 | tweet 60 | ) VALUES ( 61 | 2, 62 | "Hello World!" 63 | ) 64 | """)) 65 | 66 | 67 | def teardown_function(): 68 | database.execute(text("SET FOREIGN_KEY_CHECKS=0")) 69 | database.execute(text("TRUNCATE users")) 70 | database.execute(text("TRUNCATE tweets")) 71 | database.execute(text("TRUNCATE users_follow_list")) 72 | database.execute(text("SET FOREIGN_KEY_CHECKS=1")) 73 | 74 | def test_ping(api): 75 | resp = api.get('/ping') 76 | assert b'pong' in resp.data 77 | 78 | def test_login(api): 79 | resp = api.post( 80 | '/login', 81 | data = json.dumps({'email' : 'songew@gmail.com', 'password' : 'test password'}), 82 | content_type = 'application/json' 83 | ) 84 | assert b"access_token" in resp.data 85 | 86 | def test_unauthorized(api): 87 | # access token이 없이는 401 응답을 리턴하는지를 확인 88 | resp = api.post( 89 | '/tweet', 90 | data = json.dumps({'tweet' : "Hello World!"}), 91 | content_type = 'application/json' 92 | ) 93 | assert resp.status_code == 401 94 | 95 | resp = api.post( 96 | '/follow', 97 | data = json.dumps({'follow' : 2}), 98 | content_type = 'application/json' 99 | ) 100 | assert resp.status_code == 401 101 | 102 | resp = api.post( 103 | '/unfollow', 104 | data = json.dumps({'unfollow' : 2}), 105 | content_type = 'application/json' 106 | ) 107 | assert resp.status_code == 401 108 | 109 | def test_tweet(api): 110 | ## 로그인 111 | resp = api.post( 112 | '/login', 113 | data = json.dumps({'email' : 'songew@gmail.com', 'password' : 'test password'}), 114 | content_type = 'application/json' 115 | ) 116 | resp_json = json.loads(resp.data.decode('utf-8')) 117 | access_token = resp_json['access_token'] 118 | 119 | ## tweet 120 | resp = api.post( 121 | '/tweet', 122 | data = json.dumps({'tweet' : "Hello World!"}), 123 | content_type = 'application/json', 124 | headers = {'Authorization' : access_token} 125 | ) 126 | assert resp.status_code == 200 127 | 128 | ## tweet 확인 129 | resp = api.get(f'/timeline/1') 130 | tweets = json.loads(resp.data.decode('utf-8')) 131 | 132 | assert resp.status_code == 200 133 | assert tweets == { 134 | 'user_id' : 1, 135 | 'timeline' : [ 136 | { 137 | 'user_id' : 1, 138 | 'tweet' : "Hello World!" 139 | } 140 | ] 141 | } 142 | 143 | def test_follow(api): 144 | # 로그인 145 | resp = api.post( 146 | '/login', 147 | data = json.dumps({'email' : 'songew@gmail.com', 'password' : 'test password'}), 148 | content_type = 'application/json' 149 | ) 150 | resp_json = json.loads(resp.data.decode('utf-8')) 151 | access_token = resp_json['access_token'] 152 | 153 | ## 먼저 유저 1의 tweet 확인 해서 tweet 리스트가 비어 있는것을 확인 154 | resp = api.get(f'/timeline/1') 155 | tweets = json.loads(resp.data.decode('utf-8')) 156 | 157 | assert resp.status_code == 200 158 | assert tweets == { 159 | 'user_id' : 1, 160 | 'timeline' : [ ] 161 | } 162 | 163 | # follow 유저 아이디 = 2 164 | resp = api.post( 165 | '/follow', 166 | data = json.dumps({'id': 1,'follow' : 2}), 167 | content_type = 'application/json', 168 | headers = {'Authorization' : access_token} 169 | ) 170 | assert resp.status_code == 200 171 | 172 | ## 이제 유저 1의 tweet 확인 해서 유저 2의 tweet의 리턴 되는것을 확인 173 | resp = api.get(f'/timeline/1') 174 | tweets = json.loads(resp.data.decode('utf-8')) 175 | 176 | assert resp.status_code == 200 177 | assert tweets == { 178 | 'user_id' : 1, 179 | 'timeline' : [ 180 | { 181 | 'user_id' : 2, 182 | 'tweet' : "Hello World!" 183 | } 184 | ] 185 | } 186 | 187 | def test_unfollow(api): 188 | # 로그인 189 | resp = api.post( 190 | '/login', 191 | data = json.dumps({'email' : 'songew@gmail.com', 'password' : 'test password'}), 192 | content_type = 'application/json' 193 | ) 194 | resp_json = json.loads(resp.data.decode('utf-8')) 195 | access_token = resp_json['access_token'] 196 | 197 | # follow 유저 아이디 = 2 198 | resp = api.post( 199 | '/follow', 200 | data = json.dumps({'id: 1,'follow' : 2}), 201 | content_type = 'application/json', 202 | headers = {'Authorization' : access_token} 203 | ) 204 | assert resp.status_code == 200 205 | 206 | ## 이제 유저 1의 tweet 확인 해서 유저 2의 tweet의 리턴 되는것을 확인 207 | resp = api.get(f'/timeline/1') 208 | tweets = json.loads(resp.data.decode('utf-8')) 209 | 210 | assert resp.status_code == 200 211 | assert tweets == { 212 | 'user_id' : 1, 213 | 'timeline' : [ 214 | { 215 | 'user_id' : 2, 216 | 'tweet' : "Hello World!" 217 | } 218 | ] 219 | } 220 | 221 | # unfollow 유저 아이디 = 2 222 | resp = api.post( 223 | '/unfollow', 224 | data = json.dumps({'id': 1,'unfollow' : 2}), 225 | content_type = 'application/json', 226 | headers = {'Authorization' : access_token} 227 | ) 228 | assert resp.status_code == 200 229 | 230 | ## 이제 유저 1의 tweet 확인 해서 유저 2의 tweet이 더 이상 리턴 되지 않는 것을 확인 231 | resp = api.get(f'/timeline/1') 232 | tweets = json.loads(resp.data.decode('utf-8')) 233 | 234 | assert resp.status_code == 200 235 | assert tweets == { 236 | 'user_id' : 1, 237 | 'timeline' : [ ] 238 | } 239 | -------------------------------------------------------------------------------- /chapter9/setup.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | from flask_script import Manager 4 | from app import create_app 5 | from flask_twisted import Twisted 6 | from twisted.python import log 7 | 8 | if __name__ == "__main__": 9 | app = create_app() 10 | 11 | twisted = Twisted(app) 12 | log.startLogging(sys.stdout) 13 | 14 | app.logger.info(f"Running the app...") 15 | 16 | manager = Manager(app) 17 | manager.run() 18 | -------------------------------------------------------------------------------- /python_book_img.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rampart81/python-backend-book/6d303ab6349c0ddd5eedada5ad81b8b456c4cc87/python_book_img.jpg -------------------------------------------------------------------------------- /wecode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rampart81/python-backend-book/6d303ab6349c0ddd5eedada5ad81b8b456c4cc87/wecode.png -------------------------------------------------------------------------------- /wecode_mentors.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rampart81/python-backend-book/6d303ab6349c0ddd5eedada5ad81b8b456c4cc87/wecode_mentors.png --------------------------------------------------------------------------------