From 6274f60302879a82f6dfe77295ff03d3fe3ad825 Mon Sep 17 00:00:00 2001 From: msnodeve Date: Tue, 23 Jul 2019 10:38:18 +0900 Subject: [PATCH 1/7] =?UTF-8?q?Users=20model,=20orm=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 4 +- Pipfile | 8 +- Pipfile.lock | 92 +----------------- app/.DS_Store | Bin 6148 -> 0 bytes app/__init__.py | 41 +++----- app/api/__init__.py | 9 -- app/api/auth_type.py | 40 -------- app/api/database.py | 69 ++----------- app/constants.py | 51 +++------- app/posts/__init__.py | 0 app/posts/models.py | 33 ------- app/posts/views.py | 86 ----------------- app/tests/__init__.py | 0 app/tests/conftest.py | 10 -- app/tests/test_auth_decorator.py | 35 ------- app/tests/test_practice.py | 22 +++++ app/tests/test_request.py | 7 -- app/users/models.py | 50 ++++------ app/users/views.py | 139 --------------------------- docker-compose.yml | 5 +- manage.py | 5 +- migrations/versions/5ceec2627296_.py | 37 +++++++ migrations/versions/9e7208e2f6e8_.py | 49 ---------- 23 files changed, 120 insertions(+), 672 deletions(-) delete mode 100644 app/.DS_Store delete mode 100644 app/api/__init__.py delete mode 100644 app/api/auth_type.py delete mode 100644 app/posts/__init__.py delete mode 100644 app/posts/models.py delete mode 100644 app/posts/views.py delete mode 100644 app/tests/__init__.py delete mode 100644 app/tests/conftest.py delete mode 100644 app/tests/test_auth_decorator.py create mode 100644 app/tests/test_practice.py delete mode 100644 app/tests/test_request.py delete mode 100644 app/users/views.py create mode 100644 migrations/versions/5ceec2627296_.py delete mode 100644 migrations/versions/9e7208e2f6e8_.py diff --git a/.gitignore b/.gitignore index 8ab5b8a..34cc429 100644 --- a/.gitignore +++ b/.gitignore @@ -2,9 +2,11 @@ .venv # vscode +.DS_Store .vscode # python __pycache__ -# database \ No newline at end of file +# testcode +.pytest_cache \ No newline at end of file diff --git a/Pipfile b/Pipfile index 8f0da77..4209425 100644 --- a/Pipfile +++ b/Pipfile @@ -11,16 +11,12 @@ autopep8 = "*" [packages] flask = "*" +flask-script = "*" flask-sqlalchemy = "*" +flask-marshmallow = "*" flask-migrate = "*" pymysql = "*" flask-restplus = "*" -flask-marshmallow = "*" -marshmallow-jsonapi = "*" -flask-script = "*" -flask-basicauth = "*" -pyjwt = "*" -flask-bcrypt = "*" [requires] python_version = "3.7" diff --git a/Pipfile.lock b/Pipfile.lock index 2ac262a..5f05c72 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "1b5cad06bc1217f0bcf46064387d2d7de73623503054c8a92ef6f82617638ad7" + "sha256": "3d875a7fc9e3de687e5c4c88b0c44181959e440564686a159e76e6a38d7c096e" }, "pipfile-spec": 6, "requires": { @@ -36,60 +36,6 @@ ], "version": "==19.1.0" }, - "bcrypt": { - "hashes": [ - "sha256:0258f143f3de96b7c14f762c770f5fc56ccd72f8a1857a451c1cd9a655d9ac89", - "sha256:0b0069c752ec14172c5f78208f1863d7ad6755a6fae6fe76ec2c80d13be41e42", - "sha256:19a4b72a6ae5bb467fea018b825f0a7d917789bcfe893e53f15c92805d187294", - "sha256:5432dd7b34107ae8ed6c10a71b4397f1c853bd39a4d6ffa7e35f40584cffd161", - "sha256:69361315039878c0680be456640f8705d76cb4a3a3fe1e057e0f261b74be4b31", - "sha256:6fe49a60b25b584e2f4ef175b29d3a83ba63b3a4df1b4c0605b826668d1b6be5", - "sha256:74a015102e877d0ccd02cdeaa18b32aa7273746914a6c5d0456dd442cb65b99c", - "sha256:763669a367869786bb4c8fcf731f4175775a5b43f070f50f46f0b59da45375d0", - "sha256:8b10acde4e1919d6015e1df86d4c217d3b5b01bb7744c36113ea43d529e1c3de", - "sha256:9fe92406c857409b70a38729dbdf6578caf9228de0aef5bc44f859ffe971a39e", - "sha256:a190f2a5dbbdbff4b74e3103cef44344bc30e61255beb27310e2aec407766052", - "sha256:a595c12c618119255c90deb4b046e1ca3bcfad64667c43d1166f2b04bc72db09", - "sha256:c9457fa5c121e94a58d6505cadca8bed1c64444b83b3204928a866ca2e599105", - "sha256:cb93f6b2ab0f6853550b74e051d297c27a638719753eb9ff66d1e4072be67133", - "sha256:d7bdc26475679dd073ba0ed2766445bb5b20ca4793ca0db32b399dccc6bc84b7", - "sha256:ff032765bb8716d9387fd5376d987a937254b0619eff0972779515b5c98820bc" - ], - "version": "==3.1.7" - }, - "cffi": { - "hashes": [ - "sha256:041c81822e9f84b1d9c401182e174996f0bae9991f33725d059b771744290774", - "sha256:046ef9a22f5d3eed06334d01b1e836977eeef500d9b78e9ef693f9380ad0b83d", - "sha256:066bc4c7895c91812eff46f4b1c285220947d4aa46fa0a2651ff85f2afae9c90", - "sha256:066c7ff148ae33040c01058662d6752fd73fbc8e64787229ea8498c7d7f4041b", - "sha256:2444d0c61f03dcd26dbf7600cf64354376ee579acad77aef459e34efcb438c63", - "sha256:300832850b8f7967e278870c5d51e3819b9aad8f0a2c8dbe39ab11f119237f45", - "sha256:34c77afe85b6b9e967bd8154e3855e847b70ca42043db6ad17f26899a3df1b25", - "sha256:46de5fa00f7ac09f020729148ff632819649b3e05a007d286242c4882f7b1dc3", - "sha256:4aa8ee7ba27c472d429b980c51e714a24f47ca296d53f4d7868075b175866f4b", - "sha256:4d0004eb4351e35ed950c14c11e734182591465a33e960a4ab5e8d4f04d72647", - "sha256:4e3d3f31a1e202b0f5a35ba3bc4eb41e2fc2b11c1eff38b362de710bcffb5016", - "sha256:50bec6d35e6b1aaeb17f7c4e2b9374ebf95a8975d57863546fa83e8d31bdb8c4", - "sha256:55cad9a6df1e2a1d62063f79d0881a414a906a6962bc160ac968cc03ed3efcfb", - "sha256:5662ad4e4e84f1eaa8efce5da695c5d2e229c563f9d5ce5b0113f71321bcf753", - "sha256:59b4dc008f98fc6ee2bb4fd7fc786a8d70000d058c2bbe2698275bc53a8d3fa7", - "sha256:73e1ffefe05e4ccd7bcea61af76f36077b914f92b76f95ccf00b0c1b9186f3f9", - "sha256:a1f0fd46eba2d71ce1589f7e50a9e2ffaeb739fb2c11e8192aa2b45d5f6cc41f", - "sha256:a2e85dc204556657661051ff4bab75a84e968669765c8a2cd425918699c3d0e8", - "sha256:a5457d47dfff24882a21492e5815f891c0ca35fefae8aa742c6c263dac16ef1f", - "sha256:a8dccd61d52a8dae4a825cdbb7735da530179fea472903eb871a5513b5abbfdc", - "sha256:ae61af521ed676cf16ae94f30fe202781a38d7178b6b4ab622e4eec8cefaff42", - "sha256:b012a5edb48288f77a63dba0840c92d0504aa215612da4541b7b42d849bc83a3", - "sha256:d2c5cfa536227f57f97c92ac30c8109688ace8fa4ac086d19d0af47d134e2909", - "sha256:d42b5796e20aacc9d15e66befb7a345454eef794fdb0737d1af593447c6c8f45", - "sha256:dee54f5d30d775f525894d67b1495625dd9322945e7fee00731952e0368ff42d", - "sha256:e070535507bd6aa07124258171be2ee8dfc19119c28ca94c9dfb7efd23564512", - "sha256:e1ff2748c84d97b065cc95429814cdba39bcbd77c9c85c89344b317dc0d9cbff", - "sha256:ed851c75d1e0e043cbf5ca9a8e1b13c4c90f3fbd863dacb01c0808e2b5204201" - ], - "version": "==1.12.3" - }, "click": { "hashes": [ "sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13", @@ -105,20 +51,6 @@ "index": "pypi", "version": "==1.1.1" }, - "flask-basicauth": { - "hashes": [ - "sha256:df5ebd489dc0914c224419da059d991eb72988a01cdd4b956d52932ce7d501ff" - ], - "index": "pypi", - "version": "==0.2.0" - }, - "flask-bcrypt": { - "hashes": [ - "sha256:d71c8585b2ee1c62024392ebdbc447438564e2c8c02b4e57b56a4cafd8d13c5f" - ], - "index": "pypi", - "version": "==0.7.1" - }, "flask-marshmallow": { "hashes": [ "sha256:4f507f883838b397638a3a36c7d36ee146b255a49db952f5d9de3f6f4522e8a8", @@ -225,28 +157,6 @@ ], "version": "==2.19.5" }, - "marshmallow-jsonapi": { - "hashes": [ - "sha256:83e33b41e8f411d34a4b515c39f10192919a9d4f7eeccd4d6b49a4030a163374", - "sha256:b0d2cb711ed7b852136fa7cd3457c55fabbf343ada1fc406c67b5167a5a26b0f" - ], - "index": "pypi", - "version": "==0.21.2" - }, - "pycparser": { - "hashes": [ - "sha256:a988718abfad80b6b157acce7bf130a30876d27603738ac39f140993246b25b3" - ], - "version": "==2.19" - }, - "pyjwt": { - "hashes": [ - "sha256:5c6eca3c2940464d106b99ba83b00c6add741c9becaec087fb7ccdefea71350e", - "sha256:8d59a976fb773f3e6a39c85636357c4f0e242707394cadadd9814f5cbaa20e96" - ], - "index": "pypi", - "version": "==1.7.1" - }, "pymysql": { "hashes": [ "sha256:3943fbbbc1e902f41daf7f9165519f140c4451c179380677e6a848587042561a", diff --git a/app/.DS_Store b/app/.DS_Store deleted file mode 100644 index 37987875db6712ef946863187c01f44a2042fbb2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6148 zcmeHK!A`&{SEyizrf#d zW_K|nc<^Ej*-2*J?Cj1q^OD`|003)``W8S105~dPp^RpU&^+maBx$|=&T{#}T5+B8GB0nIubkU>?4)iw zO4{!5f<`B9dh2QbG>*eh-}TM{f84EXAI4GY29ZCM$wA-8C70(x)Q`vQcog*$xsRhJ zI4|*1w^E%$9G%Rac6O&e}UK4wb=5$)(JG<7waqsToaq={Meo?G4yo97|X`I0; zI&($dJHsf9qZ^D0$Xx&NO}G9$x)VPK^gV14%#VPi!5G89pEB?P D8rPWY diff --git a/app/__init__.py b/app/__init__.py index 3497d4f..5b6475b 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -1,40 +1,27 @@ """ - app init + APP을 실행하기 위해 config file """ -from flask import Flask, render_template, jsonify -from flask_restplus import Resource, Api, fields, reqparse -from flask_sqlalchemy import SQLAlchemy -from sqlalchemy.exc import SQLAlchemyError -from sqlalchemy.sql import text -from flask_marshmallow import Marshmallow -from app.api.database import DB -from app.api import REST_API -SQLALCHEMY_DATABASE_URI = \ - ("mysql+pymysql://{USER}:{PASSWORD}@{ADDR}:{PORT}/{NAME}?charset=utf8") +from flask import Flask +from app.api.database import DB, MA +from app.constants import SQLALCHEMY_DATABASE_URI_FORMAT -# 설명할 API에 대한 것 -MA = Marshmallow() -def create_app() -> (Flask): - """ create_app() 함수를 호출해 앱을 초기화 """ - - """ app config part """ - # 나중에 config는 다 빼야 할 것 같다. +def create_app()->(Flask): + """ create_app()을 호출하여 app을 초기화 """ app = Flask(__name__) app.app_context().push() - app.config['SQLALCHEMY_DATABASE_URI'] = SQLALCHEMY_DATABASE_URI.format( - USER="root", - PASSWORD="1234", - ADDR="127.0.0.1", - PORT=3306, - NAME="board" - ) + + app.config['SQLALCHEMY_DATABASE_URI'] = SQLALCHEMY_DATABASE_URI_FORMAT app.config['SQLALCHEMY_ECHO'] = True app.config['DEBUG'] = True + DB.init_app(app) - REST_API.init_app(app) MA.init_app(app) - """ return part """ + @app.route('/') + def root(): + """ main page """ + return "Hello World!" + return app diff --git a/app/api/__init__.py b/app/api/__init__.py deleted file mode 100644 index 615ea49..0000000 --- a/app/api/__init__.py +++ /dev/null @@ -1,9 +0,0 @@ -from flask_restplus import Api -from app.users.views import API as users_api -from app.posts.views import API as posts_api -from app.api.auth_type import ACCESS_TOKEN, BASIC_AUTH - -REST_API = Api(authorizations={**ACCESS_TOKEN, **BASIC_AUTH}) - -REST_API.add_namespace(users_api, '/user') -REST_API.add_namespace(posts_api, '/posts') \ No newline at end of file diff --git a/app/api/auth_type.py b/app/api/auth_type.py deleted file mode 100644 index 769e143..0000000 --- a/app/api/auth_type.py +++ /dev/null @@ -1,40 +0,0 @@ -from flask import request -from functools import wraps -import jwt - -SECERET_KEY = "Hello" -ACCESS_TOKEN = { - 'Access Token': { - 'type': 'apiKey', - 'in': 'header', - 'name': 'Authorization' - } -} -BASIC_AUTH = { - 'Basic Auth': { - 'type': 'basic', - 'in': 'header', - 'name': 'Authorization' - }, -} - -def login_required(f): - @wraps(f) - def decorated_function(*args, **kwargs): - access_token = request.headers['Authorization'] - if access_token is not None: - try: - payload = jwt.decode(access_token, SECERET_KEY, "HS256") - except jwt.InvalidTokenError: - payload = None - - # if payload is None: - # return Response(status=401) - - user_id = payload["user_id"] - # g.user = get_user_info(user_id) if user_id else None - else: - return Response(status=401) - - return f(*args, **kwargs) - return decorated_function \ No newline at end of file diff --git a/app/api/database.py b/app/api/database.py index 1bce8ba..54ca281 100644 --- a/app/api/database.py +++ b/app/api/database.py @@ -1,66 +1,9 @@ -from flask import jsonify -from flask import make_response +""" + Create db +""" + from flask_sqlalchemy import SQLAlchemy -from sqlalchemy.exc import IntegrityError -from marshmallow import ValidationError -from app.constants import STATUS_CODE -from flask_restplus import reqparse +from flask_marshmallow import Marshmallow DB = SQLAlchemy() - -class CRUD: - body = '' - status_code = STATUS_CODE.NOT_IMPLEMENTED - def add(self, resource, schema): - try: - DB.session.add(resource) - DB.session.commit() - query = resource.query.get(resource.id) - self.body = jsonify(schema.dump(resource).data) - self.status_code = STATUS_CODE.CREATED - except IntegrityError as error: - DB.session.rollback() - error_message = str(error) - self.body = jsonify({"error": error_message, "type":"IntegrityError"}) - if "Duplicate entry" in error_message: - self.status_code = 404 - else: - self.status_code = 400 - finally: - response = (self.body, self.status_code.value) - response = make_response(response) - - return response - - def update(self, args, schema): - try: - for key, value in args.items(): - setattr(self, key, value) - DB.session.commit() - self.body = jsonify(schema.dump(self).data) - self.status_code = STATUS_CODE.OK - except IntegrityError as error: - DB.session.rollback() - error_message = str(error) - self.body = jsonify({"error": error_message, "type":"IntegrityError"}) - if "Duplicate entry" in error_message: - self.status_code = STATUS_CODE.CONFLICT - else: - self.status_code = STATUS_CODE.BAD_REQUEST - finally: - response = (self.body, self.status_code.value) - response = make_response(response) - return response - - def delete(self, resource, schema): - DB.session.delete(resource) - DB.session.commit() - self.body = jsonify({"message":"success"}) - self.status_code = STATUS_CODE.OK - response = (self.body, self.status_code.value) - response = make_response(response) - return response - - def select(self, name, password): - - return "test" +MA = Marshmallow() \ No newline at end of file diff --git a/app/constants.py b/app/constants.py index 4d64aac..eb61761 100644 --- a/app/constants.py +++ b/app/constants.py @@ -1,41 +1,12 @@ -from enum import Enum +""" + 상수 클래스 +""" -class STATUS_CODE(Enum): - OK = 200 - CREATED = 201 - NO_CONTENT = 204 - BAD_REQUEST = 400 - UNAUTHORIZED = 401 - FORBIDDEN = 403 - NOT_FOUND = 404 - METHOD_NOT_ALLOWED = 405 - CONFLICT = 409 - INTERNAL_SERVER_ERROR = 500 - NOT_IMPLEMENTED = 501 - BAD_GATEWAY = 502 - -responses = {item.value: item.name for item in STATUS_CODE} - -def support_codes(unsupport_codes): - unsupport_codes = [code.value for code in unsupport_codes] - return {code: name for code, name in responses.items() if code not in unsupport_codes} - -GET = support_codes(unsupport_codes=[ - STATUS_CODE.CREATED, STATUS_CODE.NO_CONTENT, - STATUS_CODE.CONFLICT, - STATUS_CODE.METHOD_NOT_ALLOWED, - STATUS_CODE.NOT_IMPLEMENTED]) -POST = support_codes(unsupport_codes=[ - STATUS_CODE.OK, STATUS_CODE.NO_CONTENT, - STATUS_CODE.METHOD_NOT_ALLOWED, - STATUS_CODE.NOT_IMPLEMENTED]) -PATCH = support_codes(unsupport_codes=[ - STATUS_CODE.CREATED, - STATUS_CODE.NO_CONTENT, - STATUS_CODE.METHOD_NOT_ALLOWED, - STATUS_CODE.NOT_IMPLEMENTED]) -DELETE = support_codes(unsupport_codes=[ - STATUS_CODE.CREATED, - STATUS_CODE.CONFLICT, - STATUS_CODE.METHOD_NOT_ALLOWED, - STATUS_CODE.NOT_IMPLEMENTED]) \ No newline at end of file +SQLALCHEMY_DATABASE_URI = ("mysql+pymysql://{USER}:{PASSWORD}@{ADDR}:{PORT}/{NAME}?charset=utf8") +SQLALCHEMY_DATABASE_URI_FORMAT = SQLALCHEMY_DATABASE_URI.format( + USER="root", + PASSWORD="1234", + ADDR="127.0.0.1", + PORT=3306, + NAME="board" + ) \ No newline at end of file diff --git a/app/posts/__init__.py b/app/posts/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/app/posts/models.py b/app/posts/models.py deleted file mode 100644 index a0e4280..0000000 --- a/app/posts/models.py +++ /dev/null @@ -1,33 +0,0 @@ -from app.api.database import DB, CRUD -from marshmallow import Schema, fields -from flask_sqlalchemy import SQLAlchemy -from marshmallow import validate -from sqlalchemy.sql import text -from app.users.models import Users, UsersSchema - -class Post(DB.Model): - __tablename__ = 'posts' - __table_args__ = {'mysql_collate': 'utf8_general_ci'} - - id = DB.Column(DB.Integer, primary_key=True) - author_id = DB.Column(DB.Integer, DB.ForeignKey(Users.id)) - name = DB.Column(DB.String(255), nullable=False) - title = DB.Column(DB.String(255), nullable=False) - body = DB.Column(DB.String(1024), nullable=False) - author = DB.relationship("Users", uselist=False) - created = DB.Column(DB.TIMESTAMP, server_default=text( - "CURRENT_TIMESTAMP"), nullable=False) - - def __init__(self, name: str, title : str, body : str, author_id: int): - self.name = name - self.title = title - self.body = body - self.author_id = author_id - -class PostSchema(Schema): - id = fields.Integer() - name = fields.Str() - title = fields.Str() - body = fields.Str() - author = fields.Nested(UsersSchema) - created = fields.Str() diff --git a/app/posts/views.py b/app/posts/views.py deleted file mode 100644 index b7cb785..0000000 --- a/app/posts/views.py +++ /dev/null @@ -1,86 +0,0 @@ -from http import HTTPStatus -from flask import jsonify -from flask import make_response -from app.users.models import Users, UsersSchema -from sqlalchemy.exc import SQLAlchemyError -from flask_restplus import Api, Namespace, fields, reqparse, Resource -from app.constants import STATUS_CODE -from app.constants import GET, POST, PATCH, DELETE -from app.posts.models import Post, PostSchema -from app.api.database import DB -from app.api.auth_type import login_required -from app.api.auth_type import BASIC_AUTH, ACCESS_TOKEN, SECERET_KEY - -API = Namespace('Posts', description="Post's REST API") - -POSTS_SCHEMA = PostSchema() - -POST_FIELDS = API.model('Post', { - 'name': fields.String, - 'title': fields.String, - 'body': fields.String, - 'author_id': fields.Integer, -}) - -@API.route('') -class Posts(Resource): - parser = reqparse.RequestParser() - parser.add_argument('name', required=True, type=str, - help="post's name", location='json') - parser.add_argument('title', required=True, type=str, - help="post's title", location='json') - parser.add_argument('body', required=True, type=str, - help="post's body", location='json') - parser.add_argument('author_id', required=True, type=int, - help="post's author", location='json') - - @API.doc(responses=GET, security=ACCESS_TOKEN) - @login_required - def get(self): - try: - posts_query = Post.query.all() - body = jsonify(POSTS_SCHEMA.dump(posts_query, many=True).data) - if posts_query: - code = HTTPStatus.OK - else: - code = HTTPStatus.NOT_FOUND - except SQLAlchemyError as err: - message = str(err) - body = jsonify({"message": message}) - code = HTTPStatus.INTERNAL_SERVER_ERROR - return make_response(body, code.value) - - @API.expect(POST_FIELDS) - def post(self): - args_ = self.parser.parse_args() - post = Post(name=args_['name'], title=args_['title'], body=args_[ - 'body'], author_id=args_['author_id']) - try: - DB.session.add(post) - DB.session.commit() - body = jsonify({"posts": POSTS_SCHEMA.dump(post).data}) - code = HTTPStatus.OK - except SQLAlchemyError as err: - DB.session.rollback() - message = str(err) - body = jsonify({"message": message}) - code = HTTPStatus.INTERNAL_SERVER_ERROR - return make_response(body, code.value) - - -@API.route('/') -class PostItem(Resource): - def get(self, seqno): - try: - post_item = DB.session.query(Post).outerjoin( - Users, Users.id == Post.author_id).filter(Post.id == seqno).first() - body = jsonify({"post": POSTS_SCHEMA.dump(post_item).data}) - if post_item: - code = HTTPStatus.OK - else: - code = HTTPStatus.NOT_FOUND - except SQLAlchemyError as err: - message = str(err) - body = jsonify({"message": message}) - code = HTTPStatus.INTERNAL_SERVER_ERROR - return make_response(body, code.value) diff --git a/app/tests/__init__.py b/app/tests/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/app/tests/conftest.py b/app/tests/conftest.py deleted file mode 100644 index 0391de1..0000000 --- a/app/tests/conftest.py +++ /dev/null @@ -1,10 +0,0 @@ -import pytest -from app import create_app # flask application factory - -@pytest.fixture(scope='session') -def flask_app(): - app = create_app() - app_context = app.app_context() - app_context.push() - yield app - app_context.pop() \ No newline at end of file diff --git a/app/tests/test_auth_decorator.py b/app/tests/test_auth_decorator.py deleted file mode 100644 index 7f855d6..0000000 --- a/app/tests/test_auth_decorator.py +++ /dev/null @@ -1,35 +0,0 @@ -from typing import Callable -from functools import wraps -from app import create_app -from flask import current_app -from flask import request -from datetime import timezone, timedelta, datetime -import jwt - -encoded_jwt = jwt.encode({'exp': datetime.utcnow()}, 'secret', algorithm='HS256') - -class UnAuthorizeError(Exception): - """ 인증 실패 """ - -def requires_auth(f): - @wraps(f) - def decorated(*args, **kwargs): - authorization = request.headers['Authorization'] - if authorization != 'token': # db or jwt.decode - raise UnAuthorizeError("인증이 필요합니다") - return f(*args, **kwargs) - return decorated - -@requires_auth -def something(*args, **kwargs): - # import pdb;pdb.set_trace() - return args, kwargs - -def test_decorator(flask_app): - with flask_app.test_request_context(headers={"Authorization":"adsf"}): - try: - result = something() - assert False - except UnAuthorizeError as err: - message = str(err) - print(message) diff --git a/app/tests/test_practice.py b/app/tests/test_practice.py new file mode 100644 index 0000000..6e68ea0 --- /dev/null +++ b/app/tests/test_practice.py @@ -0,0 +1,22 @@ +import pytest +from unittest import mock + +class Worker: + def work(self): + return "work" + + +def test_practice(): + worker = Worker() + with mock.patch.object(worker, 'work', return_value="mocking!!"): + result = worker.work() + print(result) + + with mock.patch.object(worker, 'work', side_effect=ValueError("mocking!!")): + try: + result = worker.work() + except ValueError as err: + message = str(err) + print(message) + print(result) + assert False \ No newline at end of file diff --git a/app/tests/test_request.py b/app/tests/test_request.py deleted file mode 100644 index c901074..0000000 --- a/app/tests/test_request.py +++ /dev/null @@ -1,7 +0,0 @@ -from http import HTTPStatus -from app import create_app - -def test_url(flask_app): - with flask_app.test_client() as client: - response = client.get('/users') - assert response.status_code == HTTPStatus.OK \ No newline at end of file diff --git a/app/users/models.py b/app/users/models.py index d6dd823..b8a5862 100644 --- a/app/users/models.py +++ b/app/users/models.py @@ -1,42 +1,30 @@ -from app.api.database import DB -from marshmallow import Schema, fields -from flask_sqlalchemy import SQLAlchemy -from marshmallow import validate +""" + Users models file +""" from sqlalchemy.sql import text +from app.api.database import DB, MA +from flask_sqlalchemy import SQLAlchemy +from marshmallow import Schema, fields, validate class Users(DB.Model): __tablename__ = 'users' __table_args__ = {'mysql_collate': 'utf8_general_ci'} id = DB.Column(DB.Integer, primary_key=True) - name = DB.Column(DB.String(255), unique=True, nullable=False) - email = DB.Column(DB.String(50), nullable=False) - password = DB.Column(DB.String(255), nullable=False) - created = DB.Column(DB.TIMESTAMP, server_default=text( - "CURRENT_TIMESTAMP"), nullable=False) - - def __init__(self, name, email, password): - self.name = name - self.email = email - self.password = password + user_id = DB.Column(DB.String(255), unique=True, nullable=False) + user_password = DB.Column(DB.String(255), nullable=False) + user_email = DB.Column(DB.String(255), nullable=False) + created = DB.Column(DB.TIMESTAMP, server_default=text("CURRENT_TIMESTAMP"), nullable=False) + def __init__(self, user_id, user_password, user_email): + self.user_id = user_id + self.user_password = user_password + self.user_email = user_email - -class UsersSchema(Schema): +class UsersSchema(MA.Schema): not_blank = validate.Length(min=1, error='Field cannot be blank') id = fields.Integer(dump_only=True) - email = fields.String(validate=not_blank) - name = fields.String(validate=not_blank) - password = fields.String(validate=not_blank) - created = fields.String(validate=not_blank) - - def get_top_level_links(self, data, many): - if many: - self_link = "/users" - else: - self_link = "/users/{}".format(data['id']) - return {'self': self_link} - - class Meta: - type_ = 'users' - strict = True + user_id = fields.String(validate=not_blank) + user_password = fields.String(validate=not_blank) + user_email = fields.String(validate=not_blank) + created = fields.String(validate=not_blank) \ No newline at end of file diff --git a/app/users/views.py b/app/users/views.py deleted file mode 100644 index 3a74a79..0000000 --- a/app/users/views.py +++ /dev/null @@ -1,139 +0,0 @@ - -import jwt -import datetime -import bcrypt -from http import HTTPStatus -from flask import jsonify -from flask import make_response -from flask import request -from app.users.models import Users, UsersSchema -from sqlalchemy.exc import SQLAlchemyError -from flask_restplus import Api, Namespace, fields, reqparse, Resource -from app.constants import STATUS_CODE -from app.constants import GET, POST, PATCH, DELETE -from app.api.database import DB -from app.api.auth_type import BASIC_AUTH, ACCESS_TOKEN, SECERET_KEY -from app.api.auth_type import login_required - -API = Namespace('Users', description="User's REST API") - -USERS_SCHEMA = UsersSchema() - - -@API.route('/') -@API.param('user_id', 'The user identifier') -class UserItem(Resource): - parser = reqparse.RequestParser() - parser.add_argument('name', required=True, type=str, - help="user's name", location='json') - parser.add_argument('email', required=True, type=str, - help="user's email", location='json') - parser.add_argument('password', required=True, type=str, - help="password", location='json') - - user_field = API.model('Users', { - 'name': fields.String, - 'email': fields.String, - 'password': fields.String - }) - - @API.doc(responses=GET) - def get(self, user_id): - user = Users.query.get_or_404(user_id) - Users.session.close() - user = USERS_SCHEMA.dump(user).data - return user - - @API.expect(user_field) - @API.doc(responses=PATCH) - def patch(self, user_id): - args = self.parser.parse_args() - user = Users.query.get_or_404(user_id) - response = user.update(args, USERS_SCHEMA) - return response - - @API.doc(responses=DELETE) - def delete(self, user_id): - #import pdb; pdb.set_trace() - user = Users.query.get_or_404(user_id) - response = user.delete(user, USERS_SCHEMA) - return response - - -@API.route('s') -class UsersList(Resource): - parser = reqparse.RequestParser() - parser.add_argument('name', required=True, type=str, - help="user's name", location='json') - parser.add_argument('email', required=True, type=str, - help="user's email", location='json') - parser.add_argument('password', required=True, type=str, - help="password", location='json') - - user_field = API.model('Users', { - 'name': fields.String, - 'email': fields.String, - 'password': fields.String - }) - @API.doc(responses=GET, security=ACCESS_TOKEN) - @login_required - def get(self): - users_query = Users.query.all() - results = USERS_SCHEMA.dump(users_query, many=True).data - return results - - @API.expect(user_field) - def post(self): - args = self.parser.parse_args() - temp = args['password'] - hash_pw = bcrypt.hashpw(temp.encode(), bcrypt.gensalt()) - t1 = bcrypt.checkpw(temp.encode(), hash_pw) - user = Users(args['name'], args['email'], hash_pw) - try: - DB.session.add(user) - DB.session.commit() - body = jsonify({"users": USERS_SCHEMA.dump(user).data}) - code = HTTPStatus.OK - except SQLAlchemyError as err: - DB.session.rollback() - message = str(err) - body = jsonify({"message": message}) - code = HTTPStatus.INTERNAL_SERVER_ERROR - return make_response(body, code.value) - - -@API.route('/auth') -class GetUser(Resource): - parser = reqparse.RequestParser() - parser.add_argument('name', required=True, type=str, - help="user's name", location='json') - parser.add_argument('password', required=True, type=str, - help="user's password", location='json') - - user_field = API.model('Auth', { - 'name': fields.String, - 'password': fields.String - }) - - @API.doc(responses=POST) - @API.expect(user_field) - def post(self): - args = self.parser.parse_args() - try: - user = Users.query.filter(Users.name == args['name']).first() - if bcrypt.checkpw(args['password'].encode('utf-8'), user.password.encode('utf-8')): - #여기서 이제 토큰 발급해서 보내주기 - payload = { - 'user_id' : user.name - } - token = jwt.encode(payload, SECERET_KEY, "HS256") - body = jsonify({"access_token": token.decode('utf-8'), "user": {"id" : user.id}}) - if user: - code = HTTPStatus.OK - else: - code = HTTPStatus.NOT_FOUND - except SQLAlchemyError as err: - message = str(err) - body = jsonify({"message": message}) - code = HTTPStatus.INTERNAL_SERVER_ERROR - return make_response(body, code.value) diff --git a/docker-compose.yml b/docker-compose.yml index 1e53041..abbd207 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -11,7 +11,6 @@ services: # 쓰고자하는 서비스 env_file: - ./confs/database/mysql/.env # 환경 변수들을 따로 지정해줘도 되지만 파일로 떼어놓음 volumes: - - 'mysql-data:/var/lib/mysql' - + - mysql:/var/lib/mysql volumes: - mysql-data: \ No newline at end of file + mysql: \ No newline at end of file diff --git a/manage.py b/manage.py index f1418c2..85cbda6 100644 --- a/manage.py +++ b/manage.py @@ -1,11 +1,12 @@ """ - manage file + APP manage file """ from flask_script import Manager from flask_migrate import Migrate, MigrateCommand from app import create_app from app.api.database import DB +from app.users.models import Users, UsersSchema APP = create_app() MANAGER = Manager(APP) @@ -14,7 +15,7 @@ @MANAGER.command def run(): - """ Command application run """ + """ Command Application run """ APP.run() if __name__ == '__main__': diff --git a/migrations/versions/5ceec2627296_.py b/migrations/versions/5ceec2627296_.py new file mode 100644 index 0000000..8cd51ac --- /dev/null +++ b/migrations/versions/5ceec2627296_.py @@ -0,0 +1,37 @@ +"""empty message + +Revision ID: 5ceec2627296 +Revises: +Create Date: 2019-07-23 10:16:27.384161 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '5ceec2627296' +down_revision = None +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('users', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('user_id', sa.String(length=255), nullable=False), + sa.Column('user_password', sa.String(length=255), nullable=False), + sa.Column('user_email', sa.String(length=255), nullable=False), + sa.Column('created', sa.TIMESTAMP(), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=False), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('user_id'), + mysql_collate='utf8_general_ci' + ) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table('users') + # ### end Alembic commands ### diff --git a/migrations/versions/9e7208e2f6e8_.py b/migrations/versions/9e7208e2f6e8_.py deleted file mode 100644 index f2e29af..0000000 --- a/migrations/versions/9e7208e2f6e8_.py +++ /dev/null @@ -1,49 +0,0 @@ -"""empty message - -Revision ID: 9e7208e2f6e8 -Revises: -Create Date: 2019-07-22 11:02:10.139072 - -""" -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision = '9e7208e2f6e8' -down_revision = None -branch_labels = None -depends_on = None - - -def upgrade(): - # ### commands auto generated by Alembic - please adjust! ### - op.create_table('users', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('name', sa.String(length=255), nullable=False), - sa.Column('email', sa.String(length=50), nullable=False), - sa.Column('password', sa.String(length=255), nullable=False), - sa.Column('created', sa.TIMESTAMP(), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=False), - sa.PrimaryKeyConstraint('id'), - sa.UniqueConstraint('name'), - mysql_collate='utf8_general_ci' - ) - op.create_table('posts', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('author_id', sa.Integer(), nullable=True), - sa.Column('name', sa.String(length=255), nullable=False), - sa.Column('title', sa.String(length=255), nullable=False), - sa.Column('body', sa.String(length=1024), nullable=False), - sa.Column('created', sa.TIMESTAMP(), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=False), - sa.ForeignKeyConstraint(['author_id'], ['users.id'], ), - sa.PrimaryKeyConstraint('id'), - mysql_collate='utf8_general_ci' - ) - # ### end Alembic commands ### - - -def downgrade(): - # ### commands auto generated by Alembic - please adjust! ### - op.drop_table('posts') - op.drop_table('users') - # ### end Alembic commands ### From 75f909c7c9c3b2b9187640d8961f90e4dd0eeee2 Mon Sep 17 00:00:00 2001 From: msnodeve Date: Tue, 23 Jul 2019 16:08:23 +0900 Subject: [PATCH 2/7] =?UTF-8?q?Posts=20model,=20view,=20orm=20=EC=A0=81?= =?UTF-8?q?=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/__init__.py | 9 +-- app/api/__init__.py | 12 ++++ app/posts/__init__.py | 0 app/posts/models.py | 33 ++++++++++ app/posts/views.py | 66 +++++++++++++++++++ app/tests/__init__.py | 0 app/users/views.py | 65 ++++++++++++++++++ manage.py | 1 + migrations/versions/bb32b366dab2_.py | 37 +++++++++++ .../{5ceec2627296_.py => d8019933f2c3_.py} | 6 +- 10 files changed, 220 insertions(+), 9 deletions(-) create mode 100644 app/api/__init__.py create mode 100644 app/posts/__init__.py create mode 100644 app/posts/models.py create mode 100644 app/posts/views.py create mode 100644 app/tests/__init__.py create mode 100644 app/users/views.py create mode 100644 migrations/versions/bb32b366dab2_.py rename migrations/versions/{5ceec2627296_.py => d8019933f2c3_.py} (91%) diff --git a/app/__init__.py b/app/__init__.py index 5b6475b..514741c 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -4,6 +4,7 @@ from flask import Flask from app.api.database import DB, MA +from app.api import REST_API from app.constants import SQLALCHEMY_DATABASE_URI_FORMAT @@ -17,11 +18,7 @@ def create_app()->(Flask): app.config['DEBUG'] = True DB.init_app(app) + REST_API.init_app(app) MA.init_app(app) - - @app.route('/') - def root(): - """ main page """ - return "Hello World!" - + return app diff --git a/app/api/__init__.py b/app/api/__init__.py new file mode 100644 index 0000000..352898c --- /dev/null +++ b/app/api/__init__.py @@ -0,0 +1,12 @@ +""" + API config file +""" + +from flask_restplus import Api +from app.users.views import API as users_api +from app.posts.views import API as posts_api + +REST_API = Api() + +REST_API.add_namespace(users_api, '/user') +REST_API.add_namespace(posts_api, '/post') \ No newline at end of file diff --git a/app/posts/__init__.py b/app/posts/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/posts/models.py b/app/posts/models.py new file mode 100644 index 0000000..fd3d532 --- /dev/null +++ b/app/posts/models.py @@ -0,0 +1,33 @@ +""" + Posts model file +""" + +from app.api.database import DB, MA +from marshmallow import Schema, fields, validate +from app.users.models import Users, UsersSchema +from sqlalchemy.sql import text + +class Posts(DB.Model): + __tablename__ = 'posts' + __table_args__ = {'mysql_collate': 'utf8_general_ci'} + + id = DB.Column(DB.Integer, primary_key=True) + author_id = DB.Column(DB.String(255), DB.ForeignKey(Users.user_id)) + title = DB.Column(DB.String(512), nullable=False) + body = DB.Column(DB.String(1024), nullable=False) + author = DB.relationship('Users', uselist=False) + created = DB.Column(DB.TIMESTAMP, server_default=text("CURRENT_TIMESTAMP"), nullable=False) + + def __init__(self, author_id, title, body): + self.author_id = author_id + self.title = title + self.body = body + +class PostsSchema(MA.Schema): + not_blank = validate.Length(min=1, error='Field cannot be blank') + id = fields.Integer() + author_id = fields.String(validate=not_blank) + title = fields.String(validate=not_blank) + body = fields.String(validate=not_blank) + author = fields.Nested(UsersSchema) + created = fields.String(validate=not_blank) diff --git a/app/posts/views.py b/app/posts/views.py new file mode 100644 index 0000000..3dd04f8 --- /dev/null +++ b/app/posts/views.py @@ -0,0 +1,66 @@ +""" + Posts view file +""" + +from flask_restplus import Namespace, Resource, reqparse, fields +from flask import jsonify, make_response +from http import HTTPStatus +from sqlalchemy.exc import SQLAlchemyError +from app.posts.models import Posts, PostsSchema +from app.users.models import Users, UsersSchema +from app.api.database import DB + +API = Namespace('Posts', description="Post's REST API") +POSTS_SCHEMA = PostsSchema() + +@API.route('s') +class Post(Resource): + parser = reqparse.RequestParser() + parser.add_argument('author_id', required=True, type=str, help="Post's author ID", location='json') + parser.add_argument('title', required=True, type=str, help="Post's title", location='json') + parser.add_argument('body', required=True, type=str, help="Post's body", location='json') + + post_field = API.model('Post', { + 'author_id': fields.String, + 'title': fields.String, + 'body': fields.String + }) + + @API.doc('get') + def get(self): + try: + posts = Posts.query.all() + body = jsonify(POSTS_SCHEMA.dump(posts, many=True).data) + code = HTTPStatus.OK + except SQLAlchemyError as err: + body = jsonify({'message' : str(err)}) + code = HTTPStatus.INTERNAL_SERVER_ERROR + return make_response(body, code.value) + + @API.expect(post_field) + @API.doc('post') + def post(self): + args_ = self.parser.parse_args() + post = Posts(author_id=args_['author_id'], title=args_['title'], body=args_['body']) + try: + DB.session.add(post) + DB.session.commit() + body = jsonify({'post', POSTS_SCHEMA.dump(post).data}) + code = HTTPStatus.OK + except SQLAlchemyError as err: + body = jsonify({'message' : str(err)}) + code = HTTPStatus.INTERNAL_SERVER_ERROR + return make_response(body, code.value) + +@API.route('/') +class PostItem(Resource): + def get(self, reqno): + try: + post = DB.session.query(Posts).outerjoin( + Users, Users.user_id == Posts.author_id).filter(Posts.id==reqno).first() + body = jsonify({'post' : POSTS_SCHEMA.dump(post).data}) + code = HTTPStatus.OK + except SQLAlchemyError as err: + body = jsonify({'message' : str(err)}) + code = HTTPStatus.INTERNAL_SERVER_ERROR + return make_response(body, code.value) \ No newline at end of file diff --git a/app/tests/__init__.py b/app/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/users/views.py b/app/users/views.py new file mode 100644 index 0000000..d93dcbf --- /dev/null +++ b/app/users/views.py @@ -0,0 +1,65 @@ +""" + User views file +""" +from http import HTTPStatus +from flask import jsonify, make_response +from sqlalchemy.exc import SQLAlchemyError +from flask_restplus import Namespace, Resource, reqparse, fields +from app.users.models import Users, UsersSchema +from app.api.database import DB + +API = Namespace('Users', description="User's RESTPlus - API") +USERS_SCHEMA = UsersSchema() + +@API.route('s') +class UsersAuth(Resource): + parser = reqparse.RequestParser() + parser.add_argument('user_id', required=True, type=str, help="User's ID", location='json') + parser.add_argument('user_password', required=True, type=str, help="User's PW", location='json') + parser.add_argument('user_email', required=True, type=str, help="User's Email", location='json') + + users_field = API.model('Sign up', { + 'user_id' : fields.String, + 'user_password' : fields.String, + 'user_email' : fields.String + }) + + @API.doc('post') + @API.expect(users_field) + def post(self): + args_ = self.parser.parse_args() + user = Users(args_['user_id'], args_['user_password'], args_['user_email']) + try: + DB.session.add(user) + DB.session.commit() + body = jsonify({'users' : USERS_SCHEMA.dump(user).data}) + code = HTTPStatus.OK + except SQLAlchemyError as err: + DB.session.rollback() + body = jsonify({'message' : str(err)}) + code = HTTPStatus.INTERNAL_SERVER_ERROR + return make_response(body, code.value) + +@API.route('/auth') +class UserAuth(Resource): + parser = reqparse.RequestParser() + parser.add_argument('user_id', required=True, type=str, help="User's ID", location='json') + parser.add_argument('user_password', required=True, type=str, help="User's PW", location='json') + + user_login_field = API.model('Sign in', { + 'user_id' : fields.String, + 'user_password' : fields.String + }) + + @API.doc('post') + @API.expect(user_login_field) + def post(self): + args_ = self.parser.parse_args() + try: + user = Users.query.filter(Users.user_id == args_['user_id']).first() + body = jsonify({'user_id' : user.user_id}) + code = HTTPStatus.OK + except SQLAlchemyError as err: + body = jsonify({'message' : str(err)}) + code = HTTPStatus.INTERNAL_SERVER_ERROR + return make_response(body, code.value) diff --git a/manage.py b/manage.py index 85cbda6..ef40f61 100644 --- a/manage.py +++ b/manage.py @@ -7,6 +7,7 @@ from app import create_app from app.api.database import DB from app.users.models import Users, UsersSchema +from app.posts.models import Posts, PostsSchema APP = create_app() MANAGER = Manager(APP) diff --git a/migrations/versions/bb32b366dab2_.py b/migrations/versions/bb32b366dab2_.py new file mode 100644 index 0000000..889e016 --- /dev/null +++ b/migrations/versions/bb32b366dab2_.py @@ -0,0 +1,37 @@ +"""empty message + +Revision ID: bb32b366dab2 +Revises: d8019933f2c3 +Create Date: 2019-07-23 15:07:27.216578 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = 'bb32b366dab2' +down_revision = 'd8019933f2c3' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('posts', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('author_id', sa.String(length=255), nullable=True), + sa.Column('title', sa.String(length=512), nullable=False), + sa.Column('body', sa.String(length=1024), nullable=False), + sa.Column('created', sa.TIMESTAMP(), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=False), + sa.ForeignKeyConstraint(['author_id'], ['users.user_id'], ), + sa.PrimaryKeyConstraint('id'), + mysql_collate='utf8_general_ci' + ) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table('posts') + # ### end Alembic commands ### diff --git a/migrations/versions/5ceec2627296_.py b/migrations/versions/d8019933f2c3_.py similarity index 91% rename from migrations/versions/5ceec2627296_.py rename to migrations/versions/d8019933f2c3_.py index 8cd51ac..49beca2 100644 --- a/migrations/versions/5ceec2627296_.py +++ b/migrations/versions/d8019933f2c3_.py @@ -1,8 +1,8 @@ """empty message -Revision ID: 5ceec2627296 +Revision ID: d8019933f2c3 Revises: -Create Date: 2019-07-23 10:16:27.384161 +Create Date: 2019-07-23 12:06:26.634913 """ from alembic import op @@ -10,7 +10,7 @@ # revision identifiers, used by Alembic. -revision = '5ceec2627296' +revision = 'd8019933f2c3' down_revision = None branch_labels = None depends_on = None From fef3e918e02857f3271297f320590d7a3d21d0b1 Mon Sep 17 00:00:00 2001 From: msnodeve Date: Tue, 23 Jul 2019 20:48:05 +0900 Subject: [PATCH 3/7] =?UTF-8?q?Users,=20Posts=20=EB=A5=BC=20MVC=20?= =?UTF-8?q?=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 4 +++- Makefile | 7 +++++++ app/api/database.py | 28 ++++++++++++++++++++++++++-- app/posts/models.py | 4 ++-- app/posts/views.py | 22 ++++++++++------------ app/tests/conftest.py | 10 ++++++++++ app/tests/test_models.py | 14 ++++++++++++++ app/tests/test_practice.py | 2 -- app/tests/test_request.py | 7 +++++++ app/users/models.py | 6 +++--- app/users/views.py | 22 ++++++++-------------- 11 files changed, 90 insertions(+), 36 deletions(-) create mode 100644 Makefile create mode 100644 app/tests/conftest.py create mode 100644 app/tests/test_models.py create mode 100644 app/tests/test_request.py diff --git a/.gitignore b/.gitignore index 34cc429..325ce06 100644 --- a/.gitignore +++ b/.gitignore @@ -9,4 +9,6 @@ __pycache__ # testcode -.pytest_cache \ No newline at end of file +.pytest_cache +.coverage +cov_html \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..fdefc8e --- /dev/null +++ b/Makefile @@ -0,0 +1,7 @@ +GREEN=\033[1;32;40m +RED=\033[1;31;40m +NC=\033[0m # No Color + +test: + @bash -c "echo -e \"${GREEN}[pytest 시작]${NC}\"" + pipenv run pytest app/tests --cov-report=html:cov_html --cov-report=term --cov=app \ No newline at end of file diff --git a/app/api/database.py b/app/api/database.py index 54ca281..c8f0ab2 100644 --- a/app/api/database.py +++ b/app/api/database.py @@ -1,9 +1,33 @@ """ Create db """ - +from flask import jsonify +from flask import make_response +from http import HTTPStatus from flask_sqlalchemy import SQLAlchemy +from sqlalchemy.exc import IntegrityError from flask_marshmallow import Marshmallow DB = SQLAlchemy() -MA = Marshmallow() \ No newline at end of file +MA = Marshmallow() + +class CRUD: + body = '' + status_code = HTTPStatus.NOT_IMPLEMENTED + + def add(self, resource, schema): + try: + DB.session.add(resource) + DB.session.commit() + self.body = jsonify(schema.dump(resource).data) + self.status_code = HTTPStatus.OK + except IntegrityError as err: + DB.session.rollback() + err_meg = str(err) + self.body = jsonify({'error' : err_meg, 'type' : 'IntegrityError'}) + if "Duplicate entry" in err_meg: + self.status_code = HTTPStatus.CONFLICT + else: + self.status_code = HTTPStatus.BAD_REQUEST + return make_response(self.body, self.status_code) + \ No newline at end of file diff --git a/app/posts/models.py b/app/posts/models.py index fd3d532..f45c7be 100644 --- a/app/posts/models.py +++ b/app/posts/models.py @@ -2,12 +2,12 @@ Posts model file """ -from app.api.database import DB, MA +from app.api.database import DB, MA, CRUD from marshmallow import Schema, fields, validate from app.users.models import Users, UsersSchema from sqlalchemy.sql import text -class Posts(DB.Model): +class Posts(DB.Model, CRUD): __tablename__ = 'posts' __table_args__ = {'mysql_collate': 'utf8_general_ci'} diff --git a/app/posts/views.py b/app/posts/views.py index 3dd04f8..5113cad 100644 --- a/app/posts/views.py +++ b/app/posts/views.py @@ -31,7 +31,10 @@ def get(self): try: posts = Posts.query.all() body = jsonify(POSTS_SCHEMA.dump(posts, many=True).data) - code = HTTPStatus.OK + if posts: + code = HTTPStatus.OK + else: + code = HTTPStatus.NOT_FOUND except SQLAlchemyError as err: body = jsonify({'message' : str(err)}) code = HTTPStatus.INTERNAL_SERVER_ERROR @@ -42,15 +45,7 @@ def get(self): def post(self): args_ = self.parser.parse_args() post = Posts(author_id=args_['author_id'], title=args_['title'], body=args_['body']) - try: - DB.session.add(post) - DB.session.commit() - body = jsonify({'post', POSTS_SCHEMA.dump(post).data}) - code = HTTPStatus.OK - except SQLAlchemyError as err: - body = jsonify({'message' : str(err)}) - code = HTTPStatus.INTERNAL_SERVER_ERROR - return make_response(body, code.value) + return post.add(post, POSTS_SCHEMA) @API.route('/') class PostItem(Resource): @@ -58,8 +53,11 @@ def get(self, reqno): try: post = DB.session.query(Posts).outerjoin( Users, Users.user_id == Posts.author_id).filter(Posts.id==reqno).first() - body = jsonify({'post' : POSTS_SCHEMA.dump(post).data}) - code = HTTPStatus.OK + body = POSTS_SCHEMA.dump(post).data + if post: + code = HTTPStatus.OK + else: + code = HTTPStatus.NOT_FOUND except SQLAlchemyError as err: body = jsonify({'message' : str(err)}) code = HTTPStatus.INTERNAL_SERVER_ERROR diff --git a/app/tests/conftest.py b/app/tests/conftest.py new file mode 100644 index 0000000..0391de1 --- /dev/null +++ b/app/tests/conftest.py @@ -0,0 +1,10 @@ +import pytest +from app import create_app # flask application factory + +@pytest.fixture(scope='session') +def flask_app(): + app = create_app() + app_context = app.app_context() + app_context.push() + yield app + app_context.pop() \ No newline at end of file diff --git a/app/tests/test_models.py b/app/tests/test_models.py new file mode 100644 index 0000000..3d57d5f --- /dev/null +++ b/app/tests/test_models.py @@ -0,0 +1,14 @@ +from app.posts.models import Posts +from app.users.models import Users + +def test_posts_init(): + test_post = Posts("a", "b", "c") + assert type(test_post.author_id) is str + assert type(test_post.title) is str + assert type(test_post.body) is str + +def test_users_init(): + test_user = Users("a", "b", "c") + assert type(test_user.user_id) is str + assert type(test_user.user_password) is str + assert type(test_user.user_email) is str diff --git a/app/tests/test_practice.py b/app/tests/test_practice.py index 6e68ea0..7920426 100644 --- a/app/tests/test_practice.py +++ b/app/tests/test_practice.py @@ -18,5 +18,3 @@ def test_practice(): except ValueError as err: message = str(err) print(message) - print(result) - assert False \ No newline at end of file diff --git a/app/tests/test_request.py b/app/tests/test_request.py new file mode 100644 index 0000000..8c4a7fe --- /dev/null +++ b/app/tests/test_request.py @@ -0,0 +1,7 @@ +from http import HTTPStatus +from app import create_app + +def test_url(flask_app): + with flask_app.test_client() as client: + response = client.get('/posts') + assert response.status_code == HTTPStatus.OK \ No newline at end of file diff --git a/app/users/models.py b/app/users/models.py index b8a5862..3a95bc0 100644 --- a/app/users/models.py +++ b/app/users/models.py @@ -2,11 +2,11 @@ Users models file """ from sqlalchemy.sql import text -from app.api.database import DB, MA +from app.api.database import DB, MA, CRUD from flask_sqlalchemy import SQLAlchemy from marshmallow import Schema, fields, validate -class Users(DB.Model): +class Users(DB.Model, CRUD): __tablename__ = 'users' __table_args__ = {'mysql_collate': 'utf8_general_ci'} @@ -16,7 +16,7 @@ class Users(DB.Model): user_email = DB.Column(DB.String(255), nullable=False) created = DB.Column(DB.TIMESTAMP, server_default=text("CURRENT_TIMESTAMP"), nullable=False) - def __init__(self, user_id, user_password, user_email): + def __init__(self, user_id : str, user_password : str, user_email : str): self.user_id = user_id self.user_password = user_password self.user_email = user_email diff --git a/app/users/views.py b/app/users/views.py index d93dcbf..0e70b49 100644 --- a/app/users/views.py +++ b/app/users/views.py @@ -18,7 +18,7 @@ class UsersAuth(Resource): parser.add_argument('user_password', required=True, type=str, help="User's PW", location='json') parser.add_argument('user_email', required=True, type=str, help="User's Email", location='json') - users_field = API.model('Sign up', { + users_field = API.model('userRegister', { 'user_id' : fields.String, 'user_password' : fields.String, 'user_email' : fields.String @@ -29,16 +29,7 @@ class UsersAuth(Resource): def post(self): args_ = self.parser.parse_args() user = Users(args_['user_id'], args_['user_password'], args_['user_email']) - try: - DB.session.add(user) - DB.session.commit() - body = jsonify({'users' : USERS_SCHEMA.dump(user).data}) - code = HTTPStatus.OK - except SQLAlchemyError as err: - DB.session.rollback() - body = jsonify({'message' : str(err)}) - code = HTTPStatus.INTERNAL_SERVER_ERROR - return make_response(body, code.value) + return user.add(user, USERS_SCHEMA) @API.route('/auth') class UserAuth(Resource): @@ -46,7 +37,7 @@ class UserAuth(Resource): parser.add_argument('user_id', required=True, type=str, help="User's ID", location='json') parser.add_argument('user_password', required=True, type=str, help="User's PW", location='json') - user_login_field = API.model('Sign in', { + user_login_field = API.model('userLogin', { 'user_id' : fields.String, 'user_password' : fields.String }) @@ -56,9 +47,12 @@ class UserAuth(Resource): def post(self): args_ = self.parser.parse_args() try: - user = Users.query.filter(Users.user_id == args_['user_id']).first() + user = Users.query.filter(Users.user_id == args_['user_id'], Users.user_password == args_['user_password']).first() body = jsonify({'user_id' : user.user_id}) - code = HTTPStatus.OK + if user: + code = HTTPStatus.OK + else: + code = HTTPStatus.NOT_FOUND except SQLAlchemyError as err: body = jsonify({'message' : str(err)}) code = HTTPStatus.INTERNAL_SERVER_ERROR From f5ab64eb0c95e0d7839550d1053d9a4312b9e52c Mon Sep 17 00:00:00 2001 From: msnodeve Date: Wed, 24 Jul 2019 21:32:35 +0900 Subject: [PATCH 4/7] =?UTF-8?q?Authorization,=20Password=20=EC=95=94?= =?UTF-8?q?=ED=98=B8=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Pipfile | 1 + Pipfile.lock | 16 +++++++++---- app/api/__init__.py | 3 ++- app/api/auth_type.py | 38 +++++++++++++++++++++++++++++++ app/posts/views.py | 6 ++++- app/tests/test_crud.py | 16 +++++++++++++ app/tests/test_practice.py | 2 ++ app/users/views.py | 46 ++++++++++++++++++++++++++------------ 8 files changed, 108 insertions(+), 20 deletions(-) create mode 100644 app/api/auth_type.py create mode 100644 app/tests/test_crud.py diff --git a/Pipfile b/Pipfile index 4209425..171a898 100644 --- a/Pipfile +++ b/Pipfile @@ -17,6 +17,7 @@ flask-marshmallow = "*" flask-migrate = "*" pymysql = "*" flask-restplus = "*" +pyjwt = "*" [requires] python_version = "3.7" diff --git a/Pipfile.lock b/Pipfile.lock index 5f05c72..cdf1a70 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "3d875a7fc9e3de687e5c4c88b0c44181959e440564686a159e76e6a38d7c096e" + "sha256": "23954409be28eb3cce3952ff0bdd9785076fb17adbac0494686b1fc30ac55e53" }, "pipfile-spec": 6, "requires": { @@ -157,6 +157,14 @@ ], "version": "==2.19.5" }, + "pyjwt": { + "hashes": [ + "sha256:5c6eca3c2940464d106b99ba83b00c6add741c9becaec087fb7ccdefea71350e", + "sha256:8d59a976fb773f3e6a39c85636357c4f0e242707394cadadd9814f5cbaa20e96" + ], + "index": "pypi", + "version": "==1.7.1" + }, "pymysql": { "hashes": [ "sha256:3943fbbbc1e902f41daf7f9165519f140c4451c179380677e6a848587042561a", @@ -368,10 +376,10 @@ }, "pyparsing": { "hashes": [ - "sha256:530d8bf8cc93a34019d08142593cf4d78a05c890da8cf87ffa3120af53772238", - "sha256:f78e99616b6f1a4745c0580e170251ef1bbafc0d0513e270c4bd281bf29d2800" + "sha256:1873c03321fc118f4e9746baf201ff990ceb915f433f23b395f5580d1840cb2a", + "sha256:9b6323ef4ab914af344ba97510e966d64ba91055d6b9afa6b30799340e89cc03" ], - "version": "==2.4.1" + "version": "==2.4.0" }, "pytest": { "hashes": [ diff --git a/app/api/__init__.py b/app/api/__init__.py index 352898c..492fbc0 100644 --- a/app/api/__init__.py +++ b/app/api/__init__.py @@ -5,8 +5,9 @@ from flask_restplus import Api from app.users.views import API as users_api from app.posts.views import API as posts_api +from app.api.auth_type import ACCESS_TOKEN, BASIC_AUTH -REST_API = Api() +REST_API = Api(authorizations={**ACCESS_TOKEN, **BASIC_AUTH}) REST_API.add_namespace(users_api, '/user') REST_API.add_namespace(posts_api, '/post') \ No newline at end of file diff --git a/app/api/auth_type.py b/app/api/auth_type.py new file mode 100644 index 0000000..f2ebae4 --- /dev/null +++ b/app/api/auth_type.py @@ -0,0 +1,38 @@ +import jwt +from flask import request, Response +from functools import wraps + +SECERET_KEY = "Secret Hellow" +ACCESS_TOKEN = { + 'Access Token': { + 'type': 'apiKey', + 'in': 'header', + 'name': 'Authorization' + } +} +BASIC_AUTH = { + 'Basic Auth': { + 'type': 'basic', + 'in': 'header', + 'name': 'Authorization' + }, +} + +def confirm_token(f): + @wraps(f) + def decorated_function(*args, **kwargs): + access_token = request.headers['Authorization'] + if access_token is not None: + try: + payload = jwt.decode(access_token, SECERET_KEY, "HS256") + except jwt.InvalidTokenError: + payload = None + if payload is None: + return Response(status=401) + user_id = payload["user_id"] + # 원하는 작업 + else: + return Response(status=401) + + return f(*args, **kwargs) + return decorated_function \ No newline at end of file diff --git a/app/posts/views.py b/app/posts/views.py index 5113cad..9a36b41 100644 --- a/app/posts/views.py +++ b/app/posts/views.py @@ -9,6 +9,7 @@ from app.posts.models import Posts, PostsSchema from app.users.models import Users, UsersSchema from app.api.database import DB +from app.api.auth_type import confirm_token, ACCESS_TOKEN, BASIC_AUTH API = Namespace('Posts', description="Post's REST API") POSTS_SCHEMA = PostsSchema() @@ -41,7 +42,8 @@ def get(self): return make_response(body, code.value) @API.expect(post_field) - @API.doc('post') + @confirm_token + @API.doc('post', security=ACCESS_TOKEN) def post(self): args_ = self.parser.parse_args() post = Posts(author_id=args_['author_id'], title=args_['title'], body=args_['body']) @@ -49,6 +51,8 @@ def post(self): @API.route('/') class PostItem(Resource): + @confirm_token + @API.doc('get', security=ACCESS_TOKEN) def get(self, reqno): try: post = DB.session.query(Posts).outerjoin( diff --git a/app/tests/test_crud.py b/app/tests/test_crud.py new file mode 100644 index 0000000..d3e22fd --- /dev/null +++ b/app/tests/test_crud.py @@ -0,0 +1,16 @@ +import pytest +from http import HTTPStatus +from app.users.models import Users +from app.posts.models import Posts +from app.users.views import USERS_SCHEMA +from app.posts.views import POSTS_SCHEMA +from app.api.database import CRUD + +def test_add(): + crud = CRUD() + try: + users = Users('test', 'test','test') + result = crud.add(users, USERS_SCHEMA) + except Exception as err: + print(err) + assert True \ No newline at end of file diff --git a/app/tests/test_practice.py b/app/tests/test_practice.py index 7920426..2581501 100644 --- a/app/tests/test_practice.py +++ b/app/tests/test_practice.py @@ -18,3 +18,5 @@ def test_practice(): except ValueError as err: message = str(err) print(message) + result = worker.work() + assert result == "work" diff --git a/app/users/views.py b/app/users/views.py index 0e70b49..d460553 100644 --- a/app/users/views.py +++ b/app/users/views.py @@ -1,45 +1,57 @@ """ User views file """ +import jwt +import bcrypt from http import HTTPStatus from flask import jsonify, make_response from sqlalchemy.exc import SQLAlchemyError from flask_restplus import Namespace, Resource, reqparse, fields from app.users.models import Users, UsersSchema from app.api.database import DB +from app.api.auth_type import SECERET_KEY API = Namespace('Users', description="User's RESTPlus - API") USERS_SCHEMA = UsersSchema() + @API.route('s') class UsersAuth(Resource): parser = reqparse.RequestParser() - parser.add_argument('user_id', required=True, type=str, help="User's ID", location='json') - parser.add_argument('user_password', required=True, type=str, help="User's PW", location='json') - parser.add_argument('user_email', required=True, type=str, help="User's Email", location='json') + parser.add_argument('user_id', required=True, type=str, + help="User's ID", location='json') + parser.add_argument('user_password', required=True, + type=str, help="User's PW", location='json') + parser.add_argument('user_email', required=True, type=str, + help="User's Email", location='json') users_field = API.model('userRegister', { - 'user_id' : fields.String, - 'user_password' : fields.String, - 'user_email' : fields.String + 'user_id': fields.String, + 'user_password': fields.String, + 'user_email': fields.String }) @API.doc('post') @API.expect(users_field) def post(self): args_ = self.parser.parse_args() - user = Users(args_['user_id'], args_['user_password'], args_['user_email']) + password = args_['user_password'] + hash_pw = bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt()) + user = Users(args_['user_id'], hash_pw, args_['user_email']) return user.add(user, USERS_SCHEMA) + @API.route('/auth') class UserAuth(Resource): parser = reqparse.RequestParser() - parser.add_argument('user_id', required=True, type=str, help="User's ID", location='json') - parser.add_argument('user_password', required=True, type=str, help="User's PW", location='json') + parser.add_argument('user_id', required=True, type=str, + help="User's ID", location='json') + parser.add_argument('user_password', required=True, + type=str, help="User's PW", location='json') user_login_field = API.model('userLogin', { - 'user_id' : fields.String, - 'user_password' : fields.String + 'user_id': fields.String, + 'user_password': fields.String }) @API.doc('post') @@ -47,13 +59,19 @@ class UserAuth(Resource): def post(self): args_ = self.parser.parse_args() try: - user = Users.query.filter(Users.user_id == args_['user_id'], Users.user_password == args_['user_password']).first() - body = jsonify({'user_id' : user.user_id}) + user = Users.query.filter(Users.user_id == args_['user_id']).first() + if bcrypt.checkpw(args_['user_password'].encode('utf-8'), user.user_password.encode('utf-8')): + # token 발급 + payload = { + 'user_id' : user.user_id + } + token = jwt.encode(payload, SECERET_KEY, "HS256") + body = jsonify({'access_token': token.decode('utf-8'),'user': user.id}) if user: code = HTTPStatus.OK else: code = HTTPStatus.NOT_FOUND except SQLAlchemyError as err: - body = jsonify({'message' : str(err)}) + body = jsonify({'message': str(err)}) code = HTTPStatus.INTERNAL_SERVER_ERROR return make_response(body, code.value) From cf124845ff2353b205a767853fabcfff1dc5611a Mon Sep 17 00:00:00 2001 From: msnodeve Date: Thu, 25 Jul 2019 21:41:04 +0900 Subject: [PATCH 5/7] =?UTF-8?q?API=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20?= =?UTF-8?q?=EC=97=85=EB=A1=9C=EB=93=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- images/api_image.png | Bin 0 -> 143878 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 images/api_image.png diff --git a/images/api_image.png b/images/api_image.png new file mode 100644 index 0000000000000000000000000000000000000000..286f9835c29ce10f9a140221aca82a3f91893705 GIT binary patch literal 143878 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%0ASnXNK+hjqymw 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 literal 0 HcmV?d00001 From c2b3ac7c9c04a3d2fce11d447c189f6aae4b3924 Mon Sep 17 00:00:00 2001 From: msnodeve Date: Thu, 25 Jul 2019 21:41:33 +0900 Subject: [PATCH 6/7] =?UTF-8?q?1=EC=B0=A8=20=EB=A6=B4=EB=A6=AC=EC=A6=88=20?= =?UTF-8?q?README.md=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 105 +++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 101 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 1268bd5..3a050b7 100644 --- a/README.md +++ b/README.md @@ -2,9 +2,98 @@ python 개발 환경부터 API, 게시판까지 만들기 ---- -### **What?** + +## Develop Environments + +*** + +- MacBook Pro (13-inch, 2017, Four Thunderbolt 3 Ports) +- Python 3.7 +- vscode +- Docker-Compose version 1.23.2, build 1110ad01 + + + +## Develop tools + +*** + +- pipenv = pip + virtualenv +- Flask-RESTPlus = Flask-RESTful + Swagger +- MySQL docker container +- Docker-compose + + + +## Project Structure + +*** + +```txt +. +├── README.md +├── app +│ ├── __init__.py +│ ├── constans.py +│ ├── api +│ │ ├── __init__.py +│ │ ├── auth_type.py +│ │ └── database.py +│ ├── posts +│ │ ├── __init__.py +│ │ ├── models.py +│ │ └── views.py +│ ├── users +│ │ ├── __init__.py +│ │ ├── models.py +│ │ └── views.py +│ └── tests +├── confs +│ └── database +│ └── mysql +│ └── .env +├── Pipfile +├── Pipfile.lock +├── Makefile +├── docker-compose.yml +├── .gitignore +└── .envrc +``` + + + +## How to run + +*** + +```bash +> docker-compose up -d +> pipenv shell +> pipenv install --dev +> make database +> python manage.py run +``` + + + +## Preview + +*** + +![api_image](/images/api_image.png) + + + +## Release + +- 2019년 7월 25일 1차 릴리즈 v1.0 + + + + + +### What? > 왜 해야 하는가 @@ -13,6 +102,8 @@ python 개발 환경부터 API, 게시판까지 만들기 3. 백 엔드, 프론트 엔드 까지 풀 스택을 목표로 4. 프로페셔널한 개발자가 되기 위해서 +*** + ### **개인적 의견** 안드로이드 클라이언트만 개발하다 보니 RESTful(?) 백 엔드(?) 뭐가 뭐인지 아무것도 이해할 수도 이해할 시간도 이해할 겨를도 없었다. 처음엔 말로만 백 엔드 개발자가 되어야지 그랬지만, 이제는 다르다. 서울에 올라온 만큼 내 롤 모델 개발자 형 밑에서 열심히 공부해 많은 것을 해 보고자 한다. 본인의 입으로 "나는 개발자다"라고 말하고 다니는 이상 모르고 넘어가면 안 될 부분이 상당히 많다고 생각한다. 이 프로젝트도 그러하다. 모르는 사람들은 절대로 모를 것이다. 백 엔드 프로그래머가 되기 위해서는 기본적으로 갖추어야 할 소양이라 생각한다! @@ -34,7 +125,9 @@ python 개발 환경부터 API, 게시판까지 만들기 > 언제 할 것인가 -2019년 7월 11일부터 시작했으며 게시판을 완성할 때까지 계속할 것이다. +2019년 7월 11일부터 시작했으며 게시판 API를 완성할 때까지 계속할 것이다. + +- 2019년 7월 25일 1차 릴리즈 --- @@ -42,4 +135,8 @@ python 개발 환경부터 API, 게시판까지 만들기 > 누가 하는가 -알려주시는 형과 함께하지만, 당연히 혼자 머리를 싸매고 끙끙해야겠지 ! \ No newline at end of file +내가 한다. + + + +이제 시작해보도록 합시다! \ No newline at end of file From e44e17adc3e71f7a1e8b4515569d28be000e7f2d Mon Sep 17 00:00:00 2001 From: msnodeve Date: Thu, 25 Jul 2019 21:42:14 +0900 Subject: [PATCH 7/7] =?UTF-8?q?1=EC=B0=A8=20=EB=A6=B4=EB=A6=AC=EC=A6=88?= =?UTF-8?q?=EB=A5=BC=20=EC=9C=84=ED=95=9C=20=ED=8C=8C=EC=9D=BC=20=EC=A0=95?= =?UTF-8?q?=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .DS_Store | Bin 6148 -> 6148 bytes .gitignore | 5 +++- Makefile | 6 +++++ app/users/views.py | 2 -- migrations/versions/bb32b366dab2_.py | 37 --------------------------- migrations/versions/d8019933f2c3_.py | 37 --------------------------- 6 files changed, 10 insertions(+), 77 deletions(-) delete mode 100644 migrations/versions/bb32b366dab2_.py delete mode 100644 migrations/versions/d8019933f2c3_.py diff --git a/.DS_Store b/.DS_Store index 1edbe2005eb9aedaf74642c21e61b8bdf23be285..1167484c81d6d5e76565ef2ec125b1039f34c275 100644 GIT binary patch delta 200 zcmZoMXfc@J&&aniU^g=(-((&ZYsTY~vsoL|#j2~#jCB-DO=@)%s?E&}bQDaC&1!2o zImA^BZ9NlmE32w&YU`#?{>UQFIAgOQs{^Ah2SYMLK0_HpJVORU2}3SJj%QAOa#Buy z5(5K+00RS~IFMfU9}E~6Ci}6ub8;~FG9)r&Go(&F$0p9&3X-4P#b&F?1=O9%kj_v9 blq~_W@)`0NiqTB_3|75|ZRcioj=%f>@WnS_ delta 47 zcmV+~0MP%0FoZCWPXPlE diff --git a/.gitignore b/.gitignore index 325ce06..c24e75f 100644 --- a/.gitignore +++ b/.gitignore @@ -11,4 +11,7 @@ __pycache__ # testcode .pytest_cache .coverage -cov_html \ No newline at end of file +cov_html + +# database +migrations \ No newline at end of file diff --git a/Makefile b/Makefile index fdefc8e..1574808 100644 --- a/Makefile +++ b/Makefile @@ -2,6 +2,12 @@ GREEN=\033[1;32;40m RED=\033[1;31;40m NC=\033[0m # No Color +database: + @bash -c "echo -e \"${GREEN}[db orm 시작]${NC}\"" + python manage.py db init + python manage.py db migrate + python manage.py db upgrade + test: @bash -c "echo -e \"${GREEN}[pytest 시작]${NC}\"" pipenv run pytest app/tests --cov-report=html:cov_html --cov-report=term --cov=app \ No newline at end of file diff --git a/app/users/views.py b/app/users/views.py index d460553..132bfbf 100644 --- a/app/users/views.py +++ b/app/users/views.py @@ -14,7 +14,6 @@ API = Namespace('Users', description="User's RESTPlus - API") USERS_SCHEMA = UsersSchema() - @API.route('s') class UsersAuth(Resource): parser = reqparse.RequestParser() @@ -40,7 +39,6 @@ def post(self): user = Users(args_['user_id'], hash_pw, args_['user_email']) return user.add(user, USERS_SCHEMA) - @API.route('/auth') class UserAuth(Resource): parser = reqparse.RequestParser() diff --git a/migrations/versions/bb32b366dab2_.py b/migrations/versions/bb32b366dab2_.py deleted file mode 100644 index 889e016..0000000 --- a/migrations/versions/bb32b366dab2_.py +++ /dev/null @@ -1,37 +0,0 @@ -"""empty message - -Revision ID: bb32b366dab2 -Revises: d8019933f2c3 -Create Date: 2019-07-23 15:07:27.216578 - -""" -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision = 'bb32b366dab2' -down_revision = 'd8019933f2c3' -branch_labels = None -depends_on = None - - -def upgrade(): - # ### commands auto generated by Alembic - please adjust! ### - op.create_table('posts', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('author_id', sa.String(length=255), nullable=True), - sa.Column('title', sa.String(length=512), nullable=False), - sa.Column('body', sa.String(length=1024), nullable=False), - sa.Column('created', sa.TIMESTAMP(), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=False), - sa.ForeignKeyConstraint(['author_id'], ['users.user_id'], ), - sa.PrimaryKeyConstraint('id'), - mysql_collate='utf8_general_ci' - ) - # ### end Alembic commands ### - - -def downgrade(): - # ### commands auto generated by Alembic - please adjust! ### - op.drop_table('posts') - # ### end Alembic commands ### diff --git a/migrations/versions/d8019933f2c3_.py b/migrations/versions/d8019933f2c3_.py deleted file mode 100644 index 49beca2..0000000 --- a/migrations/versions/d8019933f2c3_.py +++ /dev/null @@ -1,37 +0,0 @@ -"""empty message - -Revision ID: d8019933f2c3 -Revises: -Create Date: 2019-07-23 12:06:26.634913 - -""" -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision = 'd8019933f2c3' -down_revision = None -branch_labels = None -depends_on = None - - -def upgrade(): - # ### commands auto generated by Alembic - please adjust! ### - op.create_table('users', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('user_id', sa.String(length=255), nullable=False), - sa.Column('user_password', sa.String(length=255), nullable=False), - sa.Column('user_email', sa.String(length=255), nullable=False), - sa.Column('created', sa.TIMESTAMP(), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=False), - sa.PrimaryKeyConstraint('id'), - sa.UniqueConstraint('user_id'), - mysql_collate='utf8_general_ci' - ) - # ### end Alembic commands ### - - -def downgrade(): - # ### commands auto generated by Alembic - please adjust! ### - op.drop_table('users') - # ### end Alembic commands ###