From b19a1150ec628ef1485d27e6b71c1621cf27b31a Mon Sep 17 00:00:00 2001 From: openSIRP <> Date: Wed, 7 Sep 2022 17:12:49 +0200 Subject: [PATCH] Initial commit --- .gitignore | 162 +++++++++++++++++++++++++++++++++++++++++++ Controllers/Login.py | 41 +++++++++++ Controllers/Post.py | 66 ++++++++++++++++++ Controllers/Tag.py | 22 ++++++ Controllers/User.py | 63 +++++++++++++++++ LICENSE | 16 +++++ Models/Post.py | 23 ++++++ Models/Schema.py | 33 +++++++++ Models/Tag.py | 12 ++++ Models/User.py | 16 +++++ README.md | 3 + app.py | 35 ++++++++++ config.py | 7 ++ requirements.txt | 7 ++ 14 files changed, 506 insertions(+) create mode 100644 .gitignore create mode 100644 Controllers/Login.py create mode 100644 Controllers/Post.py create mode 100644 Controllers/Tag.py create mode 100644 Controllers/User.py create mode 100644 LICENSE create mode 100644 Models/Post.py create mode 100644 Models/Schema.py create mode 100644 Models/Tag.py create mode 100644 Models/User.py create mode 100644 README.md create mode 100644 app.py create mode 100644 config.py create mode 100644 requirements.txt diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5d381cc --- /dev/null +++ b/.gitignore @@ -0,0 +1,162 @@ +# ---> Python +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + diff --git a/Controllers/Login.py b/Controllers/Login.py new file mode 100644 index 0000000..2e775e6 --- /dev/null +++ b/Controllers/Login.py @@ -0,0 +1,41 @@ +import datetime +from flask import request, jsonify +from flask_restful import Resource, abort +from Models.User import User +from app import app, jwt +from flask_jwt_extended import create_access_token, get_jwt_identity, jwt_required, JWTManager, current_user, create_refresh_token + +# Register a callback function that takes whatever object is passed in as the +# identity when creating JWTs and converts it to a JSON serializable format. +@jwt.user_identity_loader +def user_identity_lookup(user): + return user.id + + +# Register a callback function that loads a user from your database whenever +# a protected route is accessed. This should return any python object on a +# successful lookup, or None if the lookup failed for any reason (for example +# if the user has been deleted from the database). +@jwt.user_lookup_loader +def user_lookup_callback(_jwt_header, jwt_data): + identity = jwt_data["sub"] + return User.query.filter_by(id=identity).one_or_none() + +class Login(Resource): + def get(self, ): + user = User.query.filter_by(email=request.json['email']).first_or_404() + + if not user or not user.check_password(request.json['password']): + abort(401, message='Unauthorized') + access_token = create_access_token(identity=user) + refresh_token = create_refresh_token(identity=user) + + return jsonify(access_token=access_token, refresh_token=refresh_token) + + +class Refresh(Resource): + @jwt_required(refresh=True) + def get(self, ): + identity = get_jwt_identity() + access_token = create_access_token(identity=identity) + return jsonify(access_token=access_token) \ No newline at end of file diff --git a/Controllers/Post.py b/Controllers/Post.py new file mode 100644 index 0000000..4592a30 --- /dev/null +++ b/Controllers/Post.py @@ -0,0 +1,66 @@ +from flask import request, jsonify +from Models.Post import Post +from Models.Tag import Tag +from Models.Schema import post_schema, posts_schema +from flask_restful import Resource, abort +from app import db +from flask_jwt_extended import create_access_token, get_jwt_identity, jwt_required, JWTManager, current_user + +class PostListResource(Resource): + @jwt_required() + def get(self): + posts = Post.query.all() + return posts_schema.dump(posts) + + @jwt_required() + def post(self): + tags_array= [] + for tag_id in request.json['tags']: + tags_array.append(Tag.query.filter_by(id=tag_id).first()) + + new_post = Post( + title=request.json['title'], + content=request.json['content'], + author_id=current_user.id, + author=current_user, + tags=tags_array + ) + db.session.add(new_post) + db.session.commit() + return post_schema.dump(new_post) + + +class PostResource(Resource): + @jwt_required() + def get(self, post_id): + post = Post.query.get_or_404(post_id) + return post_schema.dump(post) + + @jwt_required() + def put(self, post_id): + post = Post.query.get_or_404(post_id) + + post.title = request.json['title'] + post.content = request.json['content'] + + db.session.commit() + return post_schema.dump(post) + + @jwt_required() + def patch(self, post_id): + post = Post.query.get_or_404(post_id) + + if 'title' in request.json: + post.title = request.json['title'] + if 'content' in request.json: + post.content = request.json['content'] + + db.session.commit() + return post_schema.dump(post) + + @jwt_required() + def delete(self, post_id): + post = Post.query.get_or_404(post_id) + db.session.delete(post) + db.session.commit() + return '', 204 \ No newline at end of file diff --git a/Controllers/Tag.py b/Controllers/Tag.py new file mode 100644 index 0000000..b74523d --- /dev/null +++ b/Controllers/Tag.py @@ -0,0 +1,22 @@ +from flask import request +from Models.Schema import tag_schema, tags_schema +from Models.Tag import Tag +from flask_restful import Resource, abort +from app import db +from werkzeug.security import generate_password_hash +from flask_jwt_extended import create_access_token, get_jwt_identity, jwt_required, JWTManager, current_user + +class TagListResource(Resource): + @jwt_required() + def get(self): + tags = Tag.query.all() + return tags_schema.dump(tags) + + @jwt_required() + def post(self): + new_tag = Tag( + name=request.json['name'] + ) + db.session.add(new_tag) + db.session.commit() + return tag_schema.dump(new_tag) \ No newline at end of file diff --git a/Controllers/User.py b/Controllers/User.py new file mode 100644 index 0000000..2648d9e --- /dev/null +++ b/Controllers/User.py @@ -0,0 +1,63 @@ +from flask import request +from Models.User import User +from Models.Schema import user_schema, users_schema +from flask_restful import Resource, abort +from app import db +from werkzeug.security import generate_password_hash +from flask_jwt_extended import create_access_token, get_jwt_identity, jwt_required, JWTManager, current_user + +class UserListResource(Resource): + @jwt_required() + def get(self): + users = User.query.all() + return users_schema.dump(users) + + @jwt_required() + def post(self): + new_user = User( + name=request.json['name'], + email=request.json['email'], + password=generate_password_hash(request.json['password']) + ) + db.session.add(new_user) + db.session.commit() + return user_schema.dump(new_user) + + +class UserResource(Resource): + @jwt_required() + def get(self, user_id): + user = User.query.get_or_404(user_id) + return user_schema.dump(post) + + @jwt_required() + def put(self, user_id): + user = User.query.get_or_404(user_id) + + user.name = request.json['name'] + user.email = request.json['email'] + user.password = generate_password_hash(request.json['password']) + + db.session.commit() + return user_schema.dump(post) + + @jwt_required() + def patch(self, user_id): + user = User.query.get_or_404(user_id) + + if 'name' in request.json: + user.name = request.json['name'] + if 'email' in request.json: + user.email = request.json['email'] + if 'password' in request.json: + user.password = generate_password_hash(request.json['password']) + + db.session.commit() + return user_schema.dump(post) + + @jwt_required() + def delete(self, user_id): + user = User.query.get_or_404(user_id) + db.session.delete(user) + db.session.commit() + return '', 204 \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..a4e9dc9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,16 @@ +MIT No Attribution + +Copyright + +Permission is hereby granted, free of charge, to any person obtaining a copy of this +software and associated documentation files (the "Software"), to deal in the Software +without restriction, including without limitation the rights to use, copy, modify, +merge, publish, distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/Models/Post.py b/Models/Post.py new file mode 100644 index 0000000..ba1bb40 --- /dev/null +++ b/Models/Post.py @@ -0,0 +1,23 @@ +from app import db, ma +from Models.User import User +from Models.Tag import Tag + +tags_posts = db.Table('tags_posts_mapping', + db.Column('tag_id', db.Integer, db.ForeignKey('tags.id'), primary_key=True), + db.Column('post_id', db.Integer, db.ForeignKey('post.id'), primary_key=True) +) + + +class Post(db.Model): + __tablename__ = "post" + id = db.Column(db.Integer, primary_key=True) + title = db.Column(db.String(50)) + content = db.Column(db.String(255)) + tags = db.relationship('Tag', secondary=tags_posts, lazy='subquery', + backref=db.backref('posts', lazy=True)) + author_id = db.Column(db.Integer, db.ForeignKey("user.id")) + author = db.relationship("User", backref="posts") + + def __repr__(self): + return '' % self.title + diff --git a/Models/Schema.py b/Models/Schema.py new file mode 100644 index 0000000..eb9d4b8 --- /dev/null +++ b/Models/Schema.py @@ -0,0 +1,33 @@ +from marshmallow_sqlalchemy import SQLAlchemyAutoSchema +from Models.User import User +from Models.Post import Post +from Models.Tag import Tag + +class UserSchema(SQLAlchemyAutoSchema): + class Meta: + model = User + exclude = ("password",) + include_fk = True + include_relationships = True + load_instance = True + +class PostSchema(SQLAlchemyAutoSchema): + class Meta: + model= Post + include_fk = True + include_relationships = True + load_instance = True + +class TagSchema(SQLAlchemyAutoSchema): + class Meta: + model= Tag + include_fk = True + include_relationships = True + load_instance = True + +user_schema = UserSchema() +users_schema = UserSchema(many=True) +post_schema = PostSchema() +posts_schema = PostSchema(many=True) +tag_schema = TagSchema() +tags_schema = TagSchema(many=True) \ No newline at end of file diff --git a/Models/Tag.py b/Models/Tag.py new file mode 100644 index 0000000..959b45f --- /dev/null +++ b/Models/Tag.py @@ -0,0 +1,12 @@ +from app import db, ma + +class Tag(db.Model): + __tablename__ = "tags" + id = db.Column(db.Integer, primary_key=True) + name = db.Column(db.String(50)) + + + def __repr__(self): + return '' % self.name + + diff --git a/Models/User.py b/Models/User.py new file mode 100644 index 0000000..6a7cca7 --- /dev/null +++ b/Models/User.py @@ -0,0 +1,16 @@ +from app import db, ma +from werkzeug.security import check_password_hash + +class User(db.Model): + __tablename__ = "user" + id = db.Column(db.Integer, primary_key=True) + name = db.Column(db.String(50)) + email = db.Column(db.String(255)) + password = db.Column(db.String(255)) + + def __repr__(self): + return '' % self.name + + def check_password(self, password): + return check_password_hash(self.password, password) + diff --git a/README.md b/README.md new file mode 100644 index 0000000..09dc2d9 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# Python-Flask-Template + +Template pour créer une API REST en Python avec Flask, SQLAlchemy, Marshmallow et JWT pour l'authentification \ No newline at end of file diff --git a/app.py b/app.py new file mode 100644 index 0000000..092e629 --- /dev/null +++ b/app.py @@ -0,0 +1,35 @@ +#!/usr/bin/env python +import datetime +import jwt +from flask import Flask, request +from flask_sqlalchemy import SQLAlchemy +from flask_marshmallow import Marshmallow +from flask_restful import Api, abort +from werkzeug.security import generate_password_hash, check_password_hash +from flask_jwt_extended import create_access_token, get_jwt_identity, jwt_required, JWTManager +import config + +app = Flask(__name__) +app.config.from_object('config') +db = SQLAlchemy(app) +ma = Marshmallow(app) +api = Api(app) +jwt = JWTManager(app) + +from Controllers.Post import PostListResource, PostResource +from Controllers.User import UserListResource, UserResource +from Controllers.Tag import TagListResource +from Controllers.Login import Login, Refresh + +api.add_resource(UserListResource, '/v1/user') +api.add_resource(UserResource, '/v1/user/') +api.add_resource(Login, '/v1/login') +api.add_resource(Refresh, '/v1/refresh') +api.add_resource(PostListResource, '/v1/posts') +api.add_resource(PostResource, '/v1/posts/') +api.add_resource(TagListResource, '/v1/tags') +db.create_all() + + +if __name__ == '__main__': + app.run() \ No newline at end of file diff --git a/config.py b/config.py new file mode 100644 index 0000000..b57f369 --- /dev/null +++ b/config.py @@ -0,0 +1,7 @@ +from datetime import timedelta + +DEBUG = True +JWT_SECRET_KEY = "SECRET" +SQLALCHEMY_DATABASE_URI='sqlite:///test.db' +JWT_ACCESS_TOKEN_EXPIRES = timedelta(hours=24) +JWT_REFRESH_TOKEN_EXPIRES = timedelta(days=30) \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..1cd799d --- /dev/null +++ b/requirements.txt @@ -0,0 +1,7 @@ +Flask +Flask-SQLAlchemy +Flask-RESTful +flask-marshmallow +flask-jwt-extended +pyjwt +marshmallow-sqlalchemy