Skip to content

Commit 7c1ce32

Browse files
committed
Pushed basic skeleton, will test it further and update it.
1 parent c643117 commit 7c1ce32

36 files changed

+749
-0
lines changed

Diff for: .gitignore

+3
Original file line numberDiff line numberDiff line change
@@ -99,3 +99,6 @@ ENV/
9999

100100
# mypy
101101
.mypy_cache/
102+
103+
# sqlite files
104+
*.sqlite

Diff for: app/__init__.py

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
from flask import Flask
2+
from flask_marshmallow import Marshmallow
3+
from flask_sqlalchemy import SQLAlchemy
4+
5+
from config import config
6+
7+
db = SQLAlchemy()
8+
ma = Marshmallow()
9+
10+
11+
def create_app(config_name='default'):
12+
app = Flask(__name__, static_folder='./static')
13+
app.config.from_object(config[config_name])
14+
config[config_name].init_app(app)
15+
16+
# Plugins initialization goes here
17+
db.init_app(app)
18+
ma.init_app(app)
19+
20+
from .views import views as views_blueprint
21+
app.register_blueprint(views_blueprint)
22+
23+
from .api.v1 import api_v1 as api_v1_blueprint
24+
app.register_blueprint(api_v1_blueprint, url_prefix='/api/v1')
25+
26+
return app

Diff for: app/api/__init__.py

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from . import v1

Diff for: app/api/v1/__init__.py

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
from flask import Blueprint, g
2+
from flask_httpauth import HTTPTokenAuth
3+
4+
from app.models.user import User
5+
6+
api_v1 = Blueprint('v1', __name__)
7+
auth_v1 = HTTPTokenAuth(scheme='Token')
8+
9+
10+
@auth_v1.verify_token
11+
def verify_token(token):
12+
user = User.verify_auth_token(token)
13+
if user:
14+
g.user = user
15+
g.token = token
16+
return True
17+
return False
18+
19+
20+
from . import auth, father, son, errors

Diff for: app/api/v1/auth/__init__.py

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from . import views

Diff for: app/api/v1/auth/schemas.py

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
from marshmallow import fields
2+
3+
from app import ma
4+
5+
6+
class LoginUserSchema(ma.Schema):
7+
email = fields.Email(required=True)
8+
password = fields.Str(required=True)

Diff for: app/api/v1/auth/views.py

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
from flask import request, abort
2+
from app.api.v1.exceptions import ValidationError
3+
from app.api.v1.utils import json
4+
from app.models.user import User
5+
from .schemas import LoginUserSchema
6+
from .. import api_v1
7+
8+
9+
10+
11+
@api_v1.route('/auth/login', methods=['POST'])
12+
@json
13+
def auth_login():
14+
json_data = request.get_json()
15+
if not json_data:
16+
abort(400)
17+
schema = LoginUserSchema()
18+
result = schema.load(json_data)
19+
if result.errors:
20+
raise ValidationError(result.errors)
21+
user = User.query.filter_by(email=result.data['email']).first()
22+
if user is not None and user.verify_password(result.data['password']):
23+
return {'token': user.generate_auth_token().decode('ascii')}
24+
else:
25+
abort(401)

Diff for: app/api/v1/errors.py

+45
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
from app.api.v1 import api_v1, auth_v1
2+
from app.api.v1.exceptions import ValidationError
3+
from app.api.v1.utils import json
4+
5+
6+
@api_v1.errorhandler(400)
7+
@json
8+
def bad_request(e):
9+
return {'error': 'Bad request, did you include all the data correctly?'}, 400
10+
11+
12+
@api_v1.errorhandler(401)
13+
@json
14+
def unauthorized(e):
15+
return {'error': 'Unauthorized, did not provide proper auth credentials'}, 401
16+
17+
18+
@api_v1.errorhandler(404)
19+
@json
20+
def not_found(e):
21+
return {'error': 'Not found'}, 404
22+
23+
24+
@api_v1.errorhandler(405)
25+
@json
26+
def not_allowed(e):
27+
return {'error': 'Method not allowed'}, 405
28+
29+
30+
@api_v1.errorhandler(500)
31+
@json
32+
def internal_server_error(e):
33+
return {'error': 'Internal server error'}, 500
34+
35+
36+
@api_v1.errorhandler(ValidationError)
37+
@json
38+
def validation_error(e):
39+
return {'error': 'Bad request, did you include all the data correctly?', 'info': e.info}, 400
40+
41+
42+
@auth_v1.error_handler
43+
@json
44+
def auth_error():
45+
return {'error': 'Unauthorized, did not provide proper auth credentials'}, 401

Diff for: app/api/v1/exceptions.py

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
class ValidationError(Exception):
2+
def __init__(self, info):
3+
self.info = info

Diff for: app/api/v1/father/__init__.py

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from . import views

Diff for: app/api/v1/father/schemas.py

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
from app import ma
2+
from app.models.father import Father
3+
4+
5+
class FatherSchema(ma.ModelSchema):
6+
class Meta:
7+
model = Father
8+
dump_only = ('id',)
9+
exclude = ('sons',)

Diff for: app/api/v1/father/views.py

+37
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
from app.api.v1 import api_v1, auth_v1
2+
from app.api.v1.father.schemas import FatherSchema
3+
from app.api.v1.utils import paginate, detail, create, partialupdate, delete
4+
from app.models.father import Father
5+
6+
7+
@api_v1.route('/father', methods=['GET'])
8+
@paginate(FatherSchema)
9+
def list_fathers():
10+
return Father.query
11+
12+
13+
@api_v1.route('/father/<int:id>', methods=['GET'])
14+
@detail(FatherSchema)
15+
def detail_father(id):
16+
return Father.query.get_or_404(id)
17+
18+
19+
@api_v1.route('/father', methods=['POST'])
20+
@auth_v1.login_required
21+
@create(FatherSchema)
22+
def create_father():
23+
pass
24+
25+
26+
@api_v1.route('/father/<int:id>', methods=['PATCH'])
27+
@auth_v1.login_required
28+
@partialupdate(FatherSchema)
29+
def update_father(id):
30+
return Father.query.get_or_404(id)
31+
32+
33+
@api_v1.route('/father/<int:id>', methods=['DELETE'])
34+
@auth_v1.login_required
35+
@delete
36+
def delete_father(id):
37+
return Father.query.get_or_404(id)

Diff for: app/api/v1/son/__init__.py

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from . import views

Diff for: app/api/v1/son/schemas.py

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
from marshmallow import fields
2+
3+
from app import ma
4+
from app.api.v1.father.schemas import FatherSchema
5+
from app.models.son import Son
6+
7+
8+
class SonSchema(ma.ModelSchema):
9+
father = fields.Nested(FatherSchema, dump_only=True)
10+
11+
class Meta:
12+
model = Son
13+
dump_only = ('id',)

Diff for: app/api/v1/son/views.py

+46
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
from flask import request
2+
3+
from app.api.v1 import api_v1, auth_v1
4+
from app.api.v1.son.schemas import SonSchema
5+
from app.api.v1.utils import paginate, detail, create, partialupdate, delete
6+
from app.models.father import Father
7+
from app.models.son import Son
8+
9+
10+
@api_v1.route('/son', methods=['GET'])
11+
@paginate(SonSchema)
12+
def list_sons():
13+
return Son.query
14+
15+
16+
@api_v1.route('/son/<int:id>', methods=['GET'])
17+
@detail(SonSchema)
18+
def detail_son(id):
19+
return Son.query.get_or_404(id)
20+
21+
22+
def pair_with_father(son_object):
23+
request_json = request.get_json()
24+
if request_json['father']:
25+
son_object.father = Father.query.get_or_404(int(request_json['father']))
26+
27+
28+
@api_v1.route('/son', methods=['POST'])
29+
@auth_v1.login_required
30+
@create(SonSchema, after_function=pair_with_father)
31+
def create_son():
32+
pass
33+
34+
35+
@api_v1.route('/son/<int:id>', methods=['PATCH'])
36+
@auth_v1.login_required
37+
@partialupdate(SonSchema)
38+
def update_son(id):
39+
return Son.query.get_or_404(id)
40+
41+
42+
@api_v1.route('/son/<int:id>', methods=['DELETE'])
43+
@auth_v1.login_required
44+
@delete
45+
def delete_son(id):
46+
return Son.query.get_or_404(id)

Diff for: app/api/v1/utils.py

+109
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
from functools import wraps
2+
3+
from flask import jsonify, request, current_app, abort
4+
from marshmallow import ValidationError
5+
from sqlalchemy.exc import IntegrityError
6+
7+
from app import db
8+
9+
10+
def json(f):
11+
@wraps(f)
12+
def wrapped(*args, **kwargs):
13+
rv = f(*args, **kwargs)
14+
code = 200
15+
if isinstance(rv, tuple):
16+
code = rv[1]
17+
rv = rv[0]
18+
return jsonify(rv), code
19+
20+
return wrapped
21+
22+
23+
def paginate(schema):
24+
def inception(f):
25+
@wraps(f)
26+
def wrapped(*args, **kwargs):
27+
s = schema(many=True)
28+
page = request.args.get('page', 1, type=int)
29+
rv = f(*args, **kwargs)
30+
paginator = rv.paginate(page, per_page=current_app.config['RESULTS_PER_API_CALL'], error_out=False)
31+
resulting_data = {'objects': s.dump(paginator.items, many=True).data,
32+
'next_page': paginator.next_num if paginator.has_next else None,
33+
'prev_page': paginator.prev_num if paginator.has_prev else None,
34+
'pages': paginator.pages,
35+
'total_objects': paginator.total}
36+
return jsonify(resulting_data)
37+
38+
return wrapped
39+
40+
return inception
41+
42+
43+
def detail(schema):
44+
def inception(f):
45+
@wraps(f)
46+
def wrapped(*args, **kwargs):
47+
s = schema()
48+
instance = f(*args, **kwargs)
49+
return jsonify(s.dump(instance).data)
50+
51+
return wrapped
52+
53+
return inception
54+
55+
56+
def create(schema, after_function=None):
57+
def inception(f):
58+
@wraps(f)
59+
def wrapped(*args, **kwargs):
60+
s = schema()
61+
raw_data = request.get_json()
62+
result = s.load(raw_data)
63+
if result.errors:
64+
raise ValidationError(result.errors)
65+
new_object = result.data
66+
if after_function is not None:
67+
after_function(new_object)
68+
db.session.add(new_object)
69+
try:
70+
db.session.commit()
71+
return jsonify(s.dump(new_object).data), 201
72+
except IntegrityError:
73+
db.session.rollback()
74+
abort(500)
75+
76+
return wrapped
77+
78+
return inception
79+
80+
81+
def partialupdate(schema, after_function=None):
82+
def inception(f):
83+
@wraps(f)
84+
def wrapped(*args, **kwargs):
85+
s = schema()
86+
raw_data = request.get_json()
87+
instance = f(*args, **kwargs)
88+
result = s.load(raw_data, instance=instance)
89+
if result.errors:
90+
raise ValidationError(result.errors)
91+
new_object = result.data
92+
if after_function is not None:
93+
after_function(new_object)
94+
return jsonify(s.dump(result.data).data)
95+
96+
return wrapped
97+
98+
return inception
99+
100+
101+
def delete(f):
102+
@wraps(f)
103+
def wrapped(*args, **kwargs):
104+
instance = f(*args, **kwargs)
105+
db.session.delete(instance)
106+
db.session.commit()
107+
return '', 204
108+
109+
return wrapped

Diff for: app/models/__init__.py

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from . import father, son, user

Diff for: app/models/father.py

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
from app import db
2+
3+
4+
class Father(db.Model):
5+
__tablename__ = "fathers"
6+
7+
id = db.Column(db.Integer, primary_key=True)
8+
name = db.Column(db.String(200), nullable=False)
9+
10+
sons = db.relationship('Son', backref='father', lazy='dynamic')

Diff for: app/models/son.py

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
from app import db
2+
3+
class Son(db.Model):
4+
__tablename__ = "sons"
5+
6+
id = db.Column(db.Integer, primary_key=True)
7+
name = db.Column(db.String(150))
8+
9+
father_id = db.Column(db.Integer, db.ForeignKey('fathers.id'))

0 commit comments

Comments
 (0)