Просмотр исходного кода

Adding code and structure

master
Dimitar Veljanovski 1 год назад
Сommit
618ad0ff74
12 измененных файлов: 528 добавлений и 0 удалений
  1. +39
    -0
      Config/CacheProvider.py
  2. +51
    -0
      Config/Common.py
  3. +26
    -0
      Config/Constants.py
  4. +35
    -0
      Config/DB/Models.py
  5. +13
    -0
      Config/DB/Schemas.py
  6. +11
    -0
      Config/DB/__init__.py
  7. +181
    -0
      Config/RouteProvider.py
  8. +25
    -0
      Config/__init__.py
  9. +105
    -0
      Routes/Users/Crud.py
  10. +18
    -0
      Routes/Users/__init__.py
  11. +19
    -0
      app.py
  12. +5
    -0
      passenger_wsgi.py

+ 39
- 0
Config/CacheProvider.py Просмотреть файл

@@ -0,0 +1,39 @@
from functools import wraps
from flask import request, jsonify
import time

class Cache:
def __init__(self):
"""
Simple caching mechanism that will only cache data if the user is not sending any GET / POST arguments in the request.
Use in combination with the RouteProvider class to be able to include in a request without extra instantiation and reduce performance cost.
"""
self.cache = {}

def __call__(self, timeout):
"""
Can be expanded to include request payload based caching - make a caching hash which includes the request.args and request.form data.
Keep in mind that it is memory taxing given a dynamic request type.
Caching large payload routes with thousands of possible arguments could cause a memory overflow and crash the server.
"""
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
cache_key = f"{request.method}:{request.path}"
if (cache_key in self.cache and time.time() <= self.cache[cache_key]["date_expires"]) and not (
request.form.keys() or request.args.keys()
):
return jsonify(self.cache[cache_key]["data"])
else:
data = func(*args, **kwargs)
expiration_time = time.time() + timeout
self.cache[cache_key] = {
"date_created": time.time(),
"date_expires": expiration_time,
"data": data
}
return jsonify(data)
return wrapper
return decorator

_cache = Cache()

+ 51
- 0
Config/Common.py Просмотреть файл

@@ -0,0 +1,51 @@
from Config import db, uri
from sqlalchemy.orm import sessionmaker, scoped_session
import sqlalchemy, datetime

class SQL:
def __init__(self):
self.engine = sqlalchemy.create_engine(uri)
self.Session = scoped_session(sessionmaker(bind=self.engine))
pass

def query(self, query, params):
session = self.Session()
result = session.execute(query, params).fetchall()
ret = []
for row in result:
ret.append(dict(row))

return ret


def str_to_date(str, format="%Y-%m-%d %H:%M:%S"):
example = datetime.datetime(2021, 6, 2, 9, 39)
if str == None:
return False
if type(str) == type(example):
return str
return datetime.datetime.strptime(str, format)

def dict_is_xor(_dict, keys):
exists = False
for key in keys:
if key in _dict and exists == False:
exists = key
continue
if key in _dict and exists != False:
return False
return exists

def crud_routes(request, instance):
method = request.method
if method == "POST":
return instance.create(request)
elif method == "GET":
return instance.read(request)
elif method == "PUT":
return instance.update(request)
elif method == "DELETE":
return instance.delete(request)

+ 26
- 0
Config/Constants.py Просмотреть файл

@@ -0,0 +1,26 @@
constants = {
"local" : {
"mysql": {
"db_name": "DATABASE NAME",
"host": "DATABASE HOST",
"user": "DATABASE USER",
"password": "DATABASE PASSWORD",
},
"static_root": "/path/to/static/folder/root"
},
"remote" : {
"mysql" : {
"db_name": "DATABASE NAME",
"host": "DATABASE HOST",
"user": "DATABASE USER",
"password": "DATABASE PASSWORD",
},
"static_root": "/path/to/static/folder/root"
},
"selector" : "remote"
}

# KEEP IN MIND. ONLY DECLARE YOUR PASSWORD IN CONSTANTS DURING DEVELOPMENT.
# ONCE IN PRODUCTION, USE ENVIRONMENTAL VARIABLES

constants = constants[constants["selector"]]

+ 35
- 0
Config/DB/Models.py Просмотреть файл

@@ -0,0 +1,35 @@
from Config import db, app
import datetime

def connect_table_to_key(db, collection_name, foreign_keys):
return db.relationship(collection_name, foreign_keys=foreign_keys)

class Users(db.Model):
__tablename__ = 'Users'

id = db.Column(db.Integer, primary_key=True)
admin_id = db.Column(db.Integer, db.ForeignKey('Users.id'), nullable=True)
email = db.Column(db.String(256), unique=True, nullable=False)
password = db.Column(db.String(256), nullable=False)
sign_up_date = db.Column(db.DateTime, nullable=False)
archived = db.Column(db.Integer, nullable=False, default=0)
name = db.Column(db.String(64), nullable=False)
address = db.Column(db.String(256), nullable=False)
profile_picture_path = db.Column(db.String(512), nullable=False)
notification_token = db.Column(db.String(512), nullable=False, default="NA")

Admin = connect_table_to_key(db, 'Users', foreign_keys=[admin_id])

__struct__ = {
"id" : "Number",
"admin_id" : "Number",
"email": "String",
"password": "String",
"sign_up_date": "String",
"archived": "Number",
"name": "String",
"address": "String",
"profile_picture_path": "String",
"notification_token": "String"
}
__unique__ = ["id", "email"]

+ 13
- 0
Config/DB/Schemas.py Просмотреть файл

@@ -0,0 +1,13 @@
from flask_sqlalchemy import SQLAlchemy
from flask_marshmallow import Marshmallow
from marshmallow import fields
from Config.DB.Models import Users

ma = Marshmallow()

class UsersSchema(ma.SQLAlchemyAutoSchema):
admin = fields.Nested('self', attribute='Admin', many=False, default=None)
class Meta:
model = Users
exclude = ['password', 'notification_token']

+ 11
- 0
Config/DB/__init__.py Просмотреть файл

@@ -0,0 +1,11 @@
from Config.DB.Models import Users
from Config.DB.Schemas import UsersSchema

class Tables:
Users = Users
# ADD REST OF THE TABLES HERE

class Schemas:
User = UsersSchema()
Users = UsersSchema(many=True)
# ADD REST OF THE SCHEMAS HERE

+ 181
- 0
Config/RouteProvider.py Просмотреть файл

@@ -0,0 +1,181 @@
from flask_jwt_extended import get_jwt_identity
from Config.DB import Schemas, Tables
from Config import constants, db
from flask import jsonify
from functools import wraps
import random, json

class RouteProvider:
def __init__(self):
self.auth_user = None
self.schemas = Schemas()
self.tables = Tables()
self.db = db
self.constants = constants

@classmethod
def access_controller(cls, access_level=["*"]):
"""
Based on the access_level array, checks if the authenticated user request has access to the requested route.
Might require recoding if you use a different users authentication implementation
"""
def decorator(fn):
@wraps(fn)
def wrapper(self, *args, **kwargs):
current_user = get_jwt_identity()
try:
current_user = json.loads(current_user)
except Exception as e:
print(current_user)
return cls._abort(418, "I'm a teapot")
if not current_user:
return cls._abort(401, "User is not authenticated to access this route")
tables = Tables()
user = tables.Users.query.filter_by(id=current_user["id"]).first()
if user is None:
return cls._abort(403, "Something went wrong when trying to authenticate your access. Please try to log in again.")
if "*" not in access_level:
schemas = Schemas()
user_schemafied = schemas.User.dump(user)
if user_schemafied["role"]["value"] not in access_level:
return cls._abort(403, "Please check your access level..")
self.auth_user = user
return fn(self, *args, **kwargs)
return wrapper
return decorator

@staticmethod
def _abort(code, message):
"""
The function is used to provide an error back to the user with an appropriate code and message (msg).
"""
return jsonify({"msg": message, "code": code}), code

def validate(self, keys, data):
"""
The function validates the request - checks if all the required keys are in the data dictionary
"""
for key in keys:
if key not in data:
return False
return True

def check_constraint(self, data, table):
"""
Checking constraints - making sure that the new entry does not break uniqueness constraints
"""
for key in table.__unique__:
if key not in data:
continue
res = table.query.filter_by(**{key: data[key]}).first()
if res is not None and key != "id":
if "id" in data and int(data["id"]) == res.id:
continue
return f"Conflict: {key} '{data[key]}' already exists. Please use a different value."
return True

def save_file(self, files, file_key, static_suffix="/", name=None):
"""
The request object and file key must be passed.
If static_suffix is not passed, the function assumes static_root as save path.
If name is not passed, the system will generate one randomly.
Make sure the root location has been granted 755 privilege and is owned by the same
user who executes and starts the server
"""
if name is None:
name = self.get_random_alphanumerical()
if file_key not in files:
return False

file = files[file_key]
extension = self.get_extension(file)
name = name + "." + extension
print(self.constants)
file.save(self.constants["static_root"] + static_suffix + name)

return static_suffix + name

def get_random_alphanumerical(self, _len = 16):
"""
Provides a truely random alphanumerical string with _len number of characters
"""
asciiCodes = []
alphanumerical = ""
asciiCodes += random.sample(range(97, 122), int(round(0.375 * _len)))
asciiCodes += random.sample(range(65, 90), int(round(0.375 * _len)))
asciiCodes += random.sample(range(48, 57), int(round(0.25 * _len)))
random.shuffle(asciiCodes)
for char in asciiCodes:
alphanumerical += chr(char)
return alphanumerical
def get_random_numerical(self, _len = 16):
"""
Provides a truely random numerical string with _len number of characters
"""
asciiCodes = []
alphanumerical = ""
asciiCodes += random.sample(range(48, 57), _len)
random.shuffle(asciiCodes)
for char in asciiCodes:
alphanumerical += chr(char)
return alphanumerical

def generate_secret_key(self, length):
"""
Generates a secret key with length number of characters. The secret key consists of all lower case letters.
"""
key = ""
for x in range(length):
rand = random.randint(97, 122)
key += chr(rand)
return key

def get_extension(self, _f):
"""
Provides the extension of the _f file
"""
ext = str(_f.filename.split(".")[len(_f.filename.split(".")) - 1])
return ext

def get_hash_info(self, args):
"""
Extracts the hash information out of the requests and provides info about enabled / key / type
"""
return {
"enable_hash" : False if "enable_hash" not in args or args["enable_hash"] != "true" else True,
"hash_key" : "id" if "hash_key" not in args else args["hash_key"],
"hash_type" : True if "hash_type" not in args or args["hash_type"] == "cbht" else False
}

def build_params(self, keys, args):
"""
Using the table structure (keys), checks if each key is in the request arguments (args).
If the key is in args, then it is properly parsed and formatted and returned in the params dictionary
"""
params = dict()
for key in keys:
if key in args:
params[key] = args[key] if keys[key] != "Integer" else int(args[key])
return params

def hash_query_results(self, array, col_key, cbht=True):
"""
Creates a hash table of closed or open bucket type from a specific array with N dictionaries where the key exists inside each dictrionary.
"""
if type(array) != list:
array = [array]
if len(array) == 0:
return []
ret = [None for _ in range(max(array,key=lambda x: x[col_key])[col_key]+1)]
for item in array:
if cbht:
ret[item[col_key]] = item
else:
if ret[item[col_key]] is None:
ret[item[col_key]] = [item]
else:
ret[item[col_key]].append(item)
return ret

+ 25
- 0
Config/__init__.py Просмотреть файл

@@ -0,0 +1,25 @@
# THIRD PARTY
from flask import Flask
from flask_cors import CORS
from flask_sqlalchemy import SQLAlchemy
from flask_jwt_extended import JWTManager, create_access_token
from flask_marshmallow import Marshmallow

# CONSTANTS IMPORT
from .Constants import constants

app = Flask(__name__, template_folder='../templates') # App initialization

# DB URI
uri = "mysql+pymysql://"+constants["mysql"]["user"]+":"+constants["mysql"]["password"]+"@"+constants["mysql"]["host"]+"/"+constants["mysql"]["db_name"]+"?&autocommit=false"

# APP CONFIGURATIONS
app.config["SQLALCHEMY_DATABASE_URI"] = uri
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
app.config["JWT_SECRET_KEY"] = "use-a-secret-key"

# INSTANCES
cors = CORS(app)
db = SQLAlchemy(app)
jwt = JWTManager(app)
ma = Marshmallow(app)

+ 105
- 0
Routes/Users/Crud.py Просмотреть файл

@@ -0,0 +1,105 @@
from Config.RouteProvider import RouteProvider
from flask import jsonify
from flask_jwt_extended import jwt_required
from datetime import datetime

class UsersCRUD(RouteProvider):
def __init__(self):
super().__init__()

@jwt_required()
@RouteProvider.access_controller(access_level=["Administrator", "System Administrator"])
def create(self, request):
data = request.form
files = request.files
required_keys = ["email", "password", "name", "address"]
required_files = ["profile_picture"]

if not self.validate(required_keys, data):
return self._abort(400, "Incorrectly formatted request. Please make sure that all the required fields are entered.")

if not self.validate(required_files, files):
return self._abort(400, "Incorrectly formatted request. Please make sure that all the required files are attached.")
constraint = self.check_constraint(data, self.tables.Users)
if self.check_constraint(data, self.tables.Users) is not True:
return self._abort(409, constraint)
user = self.tables.Users()
[setattr(user, key, data[key]) for key in required_keys]
profile_picture_path = self.save_file(files, "profile_picture", "/users/")
user.profile_picture_path = profile_picture_path
user.sign_up_date = datetime.utcnow()
self.db.session.add(user)
self.db.session.commit()

user = self.tables.Users.query.filter_by(email = data["email"]).first()
return jsonify({ "user" : self.schemas.User.dump(user) })

@jwt_required()
@RouteProvider.access_controller(access_level=["*"])
def read(self, request):
params = self.build_params(self.tables.Users.__struct__, request.args)
query_result = self.tables.Users.query.filter_by(**params).all()
return jsonify({ "users" : self.schemas.Users.dump(query_result), "args" : params })

@jwt_required()
@RouteProvider.access_controller(access_level=["*"])
def update(self, request):
data = request.form
files = request.files
required_keys = ["id"]
updatable_keys = []
[updatable_keys.append(key) for key in self.tables.Users.__struct__ if key not in ["sign_up_date", "id"]]
required_files = ["profile_picture"]

if not self.validate(required_keys, data):
return self._abort(400, "Incorrectly formatted request. Please make sure that all the required fields are entered.")
user = self.tables.Users.query.filter_by(id = data["id"]).first()
if user is None:
return self._abort(404, "User not found")

constraint = self.check_constraint(data, self.tables.Users)
if constraint is not True:
return self._abort(409, constraint)

[setattr(user, key, data[key]) for key in updatable_keys if key in data]
if "profile_picture" in files:
profile_picture_path = self.save_file(files, "profile_picture", "/users/")
user.profile_picture_path = profile_picture_path

if data["admin_id"] == 'null':
user.admin_id = None

self.db.session.commit()
user = self.tables.Users.query.filter_by(id = data["id"]).first()
return jsonify({ "user" : self.schemas.User.dump(user) })

@jwt_required()
@RouteProvider.access_controller(access_level=["Administrator", "System Administrator"])
def delete(self, request):
params = self.build_params(self.tables.Users.__struct__, request.args)
if "id" not in params:
return self._abort(400, "Missing params info: 'id'")

user = self.tables.Users.query.filter_by(id = params["id"]).first()
if user is None:
return self._abort(404, f'User with ID \'{params["id"]}\' is not found')

user.archived = 1 if user.archived == 0 else 0

if user.archived == 1:
user.archived = 0
else:
user.archived = 1

user = self.tables.Users.query.filter_by(**params).first()

return jsonify({ "user" : self.schemas.User.dump(user) })


users_crud = UsersCRUD()

+ 18
- 0
Routes/Users/__init__.py Просмотреть файл

@@ -0,0 +1,18 @@
# THIRD PARTY
from flask import request, Blueprint

# CONFIGURATION
from Config.Common import crud_routes

# ROUTES
from Routes.Users.Crud import users_crud

# Initialize a user blueprint
users_api = Blueprint("users_api", __name__)

# DEFINE YOUR ROUTES HERE
#
# EXAMPLE USAGE
@users_api.route("/crud", methods=["GET", "POST", "PUT", "DELETE"])
def users_crud_routes():
return crud_routes(request, users_crud)

+ 19
- 0
app.py Просмотреть файл

@@ -0,0 +1,19 @@
# FROM THIRD PARTY
from flask_requests import request
from flask import jsonify

# CONFIGURATIONS
from Config import app

# BLUEPRINTS / ROUTES
from Routes.Users import users_api

app.register_blueprint(users_api, url_prefix="/users/")

# PUBLIC META ROUTE - USED TO GIVE THE FRONTEND APPLICATION INFORMATION ABOUT NEW VERSION RELEASE ETC.
@app.route("/meta", methods=["GET"])
def meta():
return jsonify({ "version" : "0.0.0" })

if __name__ == "__main__":
app.run()

+ 5
- 0
passenger_wsgi.py Просмотреть файл

@@ -0,0 +1,5 @@
# FOR PRODUCTION USE
from app import app as application

if __name__ == "__main__":
application.run()

Загрузка…
Отмена
Сохранить