diff --git a/.DS_Store b/.DS_Store index 1edbe20..1167484 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/.gitignore b/.gitignore index 8ab5b8a..c24e75f 100644 --- a/.gitignore +++ b/.gitignore @@ -2,9 +2,16 @@ .venv # vscode +.DS_Store .vscode # python __pycache__ -# database \ No newline at end of file +# testcode +.pytest_cache +.coverage +cov_html + +# database +migrations \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..1574808 --- /dev/null +++ b/Makefile @@ -0,0 +1,13 @@ +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/Pipfile b/Pipfile index 8f0da77..171a898 100644 --- a/Pipfile +++ b/Pipfile @@ -11,16 +11,13 @@ 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..cdf1a70 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "1b5cad06bc1217f0bcf46064387d2d7de73623503054c8a92ef6f82617638ad7" + "sha256": "23954409be28eb3cce3952ff0bdd9785076fb17adbac0494686b1fc30ac55e53" }, "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,20 +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", @@ -458,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/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 diff --git a/app/.DS_Store b/app/.DS_Store deleted file mode 100644 index 3798787..0000000 Binary files a/app/.DS_Store and /dev/null differ diff --git a/app/__init__.py b/app/__init__.py index 3497d4f..514741c 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -1,40 +1,24 @@ """ - 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") -# 설명할 API에 대한 것 -MA = Marshmallow() +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 -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 """ + return app diff --git a/app/api/__init__.py b/app/api/__init__.py index 615ea49..492fbc0 100644 --- a/app/api/__init__.py +++ b/app/api/__init__.py @@ -1,3 +1,7 @@ +""" + 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 @@ -6,4 +10,4 @@ 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 +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 index 769e143..f2ebae4 100644 --- a/app/api/auth_type.py +++ b/app/api/auth_type.py @@ -1,8 +1,8 @@ -from flask import request -from functools import wraps import jwt +from flask import request, Response +from functools import wraps -SECERET_KEY = "Hello" +SECERET_KEY = "Secret Hellow" ACCESS_TOKEN = { 'Access Token': { 'type': 'apiKey', @@ -18,7 +18,7 @@ }, } -def login_required(f): +def confirm_token(f): @wraps(f) def decorated_function(*args, **kwargs): access_token = request.headers['Authorization'] @@ -27,12 +27,10 @@ def decorated_function(*args, **kwargs): payload = jwt.decode(access_token, SECERET_KEY, "HS256") except jwt.InvalidTokenError: payload = None - - # if payload is None: - # return Response(status=401) - + 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) diff --git a/app/api/database.py b/app/api/database.py index 1bce8ba..c8f0ab2 100644 --- a/app/api/database.py +++ b/app/api/database.py @@ -1,66 +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 marshmallow import ValidationError -from app.constants import STATUS_CODE -from flask_restplus import reqparse +from flask_marshmallow import Marshmallow DB = SQLAlchemy() +MA = Marshmallow() class CRUD: body = '' - status_code = STATUS_CODE.NOT_IMPLEMENTED + status_code = HTTPStatus.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: + self.status_code = HTTPStatus.OK + except IntegrityError as err: 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 + 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 = 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" + self.status_code = HTTPStatus.BAD_REQUEST + return make_response(self.body, self.status_code) + \ 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/models.py b/app/posts/models.py index a0e4280..f45c7be 100644 --- a/app/posts/models.py +++ b/app/posts/models.py @@ -1,33 +1,33 @@ -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 +""" + Posts model file +""" + +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 Post(DB.Model): +class Posts(DB.Model, CRUD): __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) + 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) + 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 + def __init__(self, author_id, title, body): + self.author_id = author_id self.title = title self.body = body - self.author_id = author_id -class PostSchema(Schema): +class PostsSchema(MA.Schema): + not_blank = validate.Length(min=1, error='Field cannot be blank') id = fields.Integer() - name = fields.Str() - title = fields.Str() - body = fields.Str() + 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.Str() + created = fields.String(validate=not_blank) diff --git a/app/posts/views.py b/app/posts/views.py index b7cb785..9a36b41 100644 --- a/app/posts/views.py +++ b/app/posts/views.py @@ -1,86 +1,68 @@ +""" + Posts view file +""" + +from flask_restplus import Namespace, Resource, reqparse, fields +from flask import jsonify, make_response 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.posts.models import Posts, PostsSchema +from app.users.models import Users, UsersSchema 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 +from app.api.auth_type import confirm_token, ACCESS_TOKEN, BASIC_AUTH API = Namespace('Posts', description="Post's REST API") +POSTS_SCHEMA = PostsSchema() -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): +@API.route('s') +class Post(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') + 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(responses=GET, security=ACCESS_TOKEN) - @login_required + @API.doc('get') def get(self): try: - posts_query = Post.query.all() - body = jsonify(POSTS_SCHEMA.dump(posts_query, many=True).data) - if posts_query: + posts = Posts.query.all() + body = jsonify(POSTS_SCHEMA.dump(posts, many=True).data) + if posts: code = HTTPStatus.OK else: code = HTTPStatus.NOT_FOUND except SQLAlchemyError as err: - message = str(err) - body = jsonify({"message": message}) + body = jsonify({'message' : str(err)}) code = HTTPStatus.INTERNAL_SERVER_ERROR return make_response(body, code.value) - @API.expect(POST_FIELDS) + @API.expect(post_field) + @confirm_token + @API.doc('post', security=ACCESS_TOKEN) 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) - + post = Posts(author_id=args_['author_id'], title=args_['title'], body=args_['body']) + return post.add(post, POSTS_SCHEMA) -@API.route('/') +@API.route('/') class PostItem(Resource): - def get(self, seqno): + @confirm_token + @API.doc('get', security=ACCESS_TOKEN) + def get(self, reqno): 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: + post = DB.session.query(Posts).outerjoin( + Users, Users.user_id == Posts.author_id).filter(Posts.id==reqno).first() + body = POSTS_SCHEMA.dump(post).data + if post: code = HTTPStatus.OK else: code = HTTPStatus.NOT_FOUND except SQLAlchemyError as err: - message = str(err) - body = jsonify({"message": message}) + body = jsonify({'message' : str(err)}) code = HTTPStatus.INTERNAL_SERVER_ERROR - return make_response(body, code.value) + return make_response(body, code.value) \ 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_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_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 new file mode 100644 index 0000000..2581501 --- /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) + result = worker.work() + assert result == "work" diff --git a/app/tests/test_request.py b/app/tests/test_request.py index c901074..8c4a7fe 100644 --- a/app/tests/test_request.py +++ b/app/tests/test_request.py @@ -3,5 +3,5 @@ def test_url(flask_app): with flask_app.test_client() as client: - response = client.get('/users') + 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 d6dd823..3a95bc0 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, 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'} 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 : str, user_password : str, user_email : str): + 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 index 3a74a79..132bfbf 100644 --- a/app/users/views.py +++ b/app/users/views.py @@ -1,139 +1,75 @@ - +""" + User views file +""" 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 flask import jsonify, make_response 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 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 BASIC_AUTH, ACCESS_TOKEN, SECERET_KEY -from app.api.auth_type import login_required - -API = Namespace('Users', description="User's REST API") +from app.api.auth_type import SECERET_KEY +API = Namespace('Users', description="User's RESTPlus - 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): +class UsersAuth(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 + 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 }) - @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) + @API.doc('post') + @API.expect(users_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) - + args_ = self.parser.parse_args() + 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 GetUser(Resource): +class UserAuth(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 + 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 }) - @API.doc(responses=POST) - @API.expect(user_field) + @API.doc('post') + @API.expect(user_login_field) def post(self): - args = self.parser.parse_args() + 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')): - #여기서 이제 토큰 발급해서 보내주기 + 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.name + 'user_id' : user.user_id } token = jwt.encode(payload, SECERET_KEY, "HS256") - body = jsonify({"access_token": token.decode('utf-8'), "user": {"id" : user.id}}) + 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: - message = str(err) - body = jsonify({"message": message}) + body = jsonify({'message': str(err)}) 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/images/api_image.png b/images/api_image.png new file mode 100644 index 0000000..286f983 Binary files /dev/null and b/images/api_image.png differ diff --git a/manage.py b/manage.py index f1418c2..ef40f61 100644 --- a/manage.py +++ b/manage.py @@ -1,11 +1,13 @@ """ - 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 +from app.posts.models import Posts, PostsSchema APP = create_app() MANAGER = Manager(APP) @@ -14,7 +16,7 @@ @MANAGER.command def run(): - """ Command application run """ + """ Command Application run """ APP.run() if __name__ == '__main__': 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 ###