From b4fb234e4781092e5d8765dae527a198ab0567aa Mon Sep 17 00:00:00 2001 From: Mustafa Yontar Date: Mon, 1 Feb 2021 14:33:05 +0300 Subject: [PATCH] first commit --- .gitignore | 26 +++++++ Methods.py | 18 +++++ __init__.py | 46 ++++++++++++ jwt.py | 9 +++ resource.py | 212 ++++++++++++++++++++++++++++++++++++++++++++++++++++ views.py | 157 ++++++++++++++++++++++++++++++++++++++ 6 files changed, 468 insertions(+) create mode 100644 .gitignore create mode 100644 Methods.py create mode 100644 __init__.py create mode 100644 jwt.py create mode 100644 resource.py create mode 100644 views.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8ebc9f3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,26 @@ +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +env/ +.venv/ +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +*.egg-info/ +.installed.cfg +*.egg +.idea/ +*.pem +*test.py +trash +mongo_data_dir/* \ No newline at end of file diff --git a/Methods.py b/Methods.py new file mode 100644 index 0000000..08d3cbc --- /dev/null +++ b/Methods.py @@ -0,0 +1,18 @@ +class Get: + method = 'GET' + + +class List: + method = 'GET' + + +class Delete: + method = 'DELETE' + + +class Update: + method = 'PUT' + + +class Create: + method = 'POST' diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..cb12890 --- /dev/null +++ b/__init__.py @@ -0,0 +1,46 @@ +from urllib.parse import urljoin + +from flask import Flask +from mongoengine import Document + +from restapi.views import ApiView +from restapi import Methods + + +class MongoApi: + app: Flask = None + authentication_methods: list = [] + + def __init__(self, app: Flask, authentication_methods): + self.app = app + self.authentication_methods = authentication_methods + + def register_model(self, model: any, uri: str = None, name: str = None) -> None: + if not getattr(model, '_meta').get('methods'): + raise Exception("{} model methods not set".format(model.__name__)) + if not uri: + uri = model.__name__.lower() + if uri[:-1] != "/": + uri = uri + "/" + if uri[0] != "/": + uri = "/" + uri + if not name: + name = model.__name__ + self.app.add_url_rule( + uri, + methods=(method.method for method in getattr(model, '_meta').get('methods') if + method not in (Methods.Get, Methods.Update)), + view_func=ApiView.as_view(name, model=model, authentication_methods=self.authentication_methods)) + + single = [] + if Methods.Update in getattr(model, '_meta').get('methods'): + single.append('PUT') + + if Methods.Get in getattr(model, '_meta').get('methods'): + single.append('GET') + if single: + self.app.add_url_rule( + urljoin(uri, '/'), + methods=single, + view_func=ApiView.as_view("{}_GET_PUT".format(name), model=model, + authentication_methods=self.authentication_methods)) diff --git a/jwt.py b/jwt.py new file mode 100644 index 0000000..b1fe586 --- /dev/null +++ b/jwt.py @@ -0,0 +1,9 @@ +from flask import request +from flask_jwt_extended.utils import verify_token_claims +from flask_jwt_extended.view_decorators import _decode_jwt_from_request + + +def get_jwt_from_request(): + jwt_data, jwt_header = _decode_jwt_from_request(request_type='access') + verify_token_claims(jwt_data) + return jwt_data['identity'] diff --git a/resource.py b/resource.py new file mode 100644 index 0000000..c66ce7e --- /dev/null +++ b/resource.py @@ -0,0 +1,212 @@ +from bson import ObjectId +from flask import request +from mongoengine import Document, ObjectIdField, ListField, \ + EmbeddedDocumentField, ReferenceField, ImageField, FileField, DecimalField, QuerySet +from base64 import b64encode + +from mongoengine.base import BaseField + + +class Resource: + model: Document = None + qs: dict = {} + eqs: dict = {} + build_reference: bool = False + base64_image: bool = True + base64_image_as_full: bool = False + limit: int = 10 + offset: int = 0 + fields: list = [] + ignore_fields: list = [] + mask_fields: dict = {} + meta: dict = {} + as_pk: str = 'id' + query_document: Document = None + mongo_qs = None + + def __init__(self, model: Document): + self.model = model + self.limit = 10 + if 'no_image' in request.args: + self.base64_image = False + if 'with_sub_docs' in request.args: + self.build_reference = request.args.get('with_sub_docs') + if 'thumb' in request.args: + self.base64_image = True + self.base64_image_as_full = False + if 'full_image' in request.args: + self.base64_image = True + self.base64_image_as_full = True + if 'limit' in request.args: + self.limit = int(request.args.get('limit')) + if 'fields' in request.args: + self.fields = request.args.get('fields') + if 'offset' in request.args: + self.offset = int(request.args.get('offset')) + self.meta = getattr(self.model, '_meta') + + if 'ignore_fields' in self.meta: + self.ignore_fields = self.meta.get('ignore_fields') + if 'with_sub_docs' in self.meta: + self.build_reference = self.meta.get('with_sub_docs') + if 'mask_fields' in self.meta: + self.mask_fields = self.meta.get('mask_fields') + if 'document' in self.meta: + self.model = self.meta.get('document') + if 'query' in self.meta: + self.qs = self.meta.get('query') + if 'as_pk' in self.meta: + self.as_pk = self.meta.get('as_pk') + if 'query_document' in self.meta: + self.query_document = self.meta.get('query_document') + + def external_query(self, qs: dict): + self.eqs = qs + + def query(self, qs: dict) -> None: + + self.qs = qs + + def to_qs(self, pk: str = None) -> tuple: + query = {} + if self.qs: + for key, val in self.qs.items(): + if callable(val): + query.update({key: val()}) + else: + query.update({key: val}) + if self.query_document: + query = {} + if self.eqs: + for key, val in self.eqs.items(): + if callable(val): + query.update({key: val()}) + else: + query.update({key: val}) + data = self.query_document.objects.filter(**query) + field_name = "" + for i in self.model._fields_ordered: + try: + if isinstance(getattr(self.model, i).document_type, self.query_document.__class__): + field_name = i + "__in" + except: + pass + query.clear() + query = {field_name: [i.id for i in data]} + for key, val in self.qs.items(): + if callable(val): + query.update({key: val()}) + else: + query.update({key: val}) + print(query) + data = self.model.objects.filter(**query) + if pk: + data.get(**{self.as_pk: pk}) + return data + + def to_json(self, pk: str = None) -> tuple: + + if self.mongo_qs is None: + self.mongo_qs = self.to_qs(pk) + + data = self.mongo_qs + count = data.count() + if pk: + json_data = self.parse_fields(self.mongo_qs.get(**{self.as_pk:pk}),self.model) + else: + data = data[self.offset:self.limit + self.offset] + json_data = [] + for item in data: + item_data = self.parse_fields(item, self.model) + json_data.append(item_data) + return count, json_data + + def parse_field(self, field: BaseField, value: any) -> any: + + if isinstance(field, ObjectIdField): + return str(value) + elif isinstance(field, ReferenceField): + + if self.build_reference: + return self.parse_fields(value, field.document_type) + if value: + return str(value.id) + else: + return value + + elif isinstance(field, ImageField): + if self.base64_image and value: + if self.base64_image_as_full: + file = b64encode(value.read()).decode("utf-8") + return { + "size": value.size, + "upload_date": value.gridout.upload_date, + "format": value.format, + "base64": file + } + else: + file = b64encode(value.thumbnail.read()).decode("utf-8") + + return { + "upload_date": value.gridout.upload_date, + "format": value.format, + "base64": file + } + else: + if value: + return { + "size": value.size + } + else: + return None + elif isinstance(field, DecimalField): + return float(value) + elif isinstance(field, FileField): + return "file" + elif isinstance(field, ListField): + if isinstance(field.field, EmbeddedDocumentField): + return [self.parse_fields(item, field.field.document_type) for item in value] + elif isinstance(field.field, ReferenceField) and self.build_reference: + return [self.parse_fields(item, field.field.document_type) for item in value] + else: + return [self.parse_field(field.field, item) for item in value] + elif isinstance(value, QuerySet): + if value.count() > 0: + return [self.parse_fields(i, value._document) for i in value] + + else: + return value + + def parse_fields(self, item: Document, model: Document) -> dict: + parsed_item = {} + + fields = list(getattr(model, "_fields_ordered")) + if self.model != model: + in_meta = getattr(model,'_meta') + if in_meta.get('ignore_fields'): + self.ignore_fields += in_meta.get('ignore_fields') + for key, val in model.__dict__.items(): + if isinstance(getattr(model, key), property): + fields.append(key) + for i in fields: + if i not in self.ignore_fields: + field = getattr(model, i) + if i not in self.mask_fields: + if not item is None: + value = getattr(item, i) + else: + value = None + else: + value = getattr(item, i) + if self.mask_fields.get(i) == 'all': + value = '*************' + elif self.mask_fields.get(i) == 'email': + value = '{}****@{}***.{}'.format(value[0], value.split("@")[1][0], + '.'.join(value.split("@")[1].split(".")[1:])) + else: + value = '*************' + if not value is None: + parsed_item.update({i: self.parse_field(field, value)}) + else: + parsed_item.update({i: None}) + return parsed_item diff --git a/views.py b/views.py new file mode 100644 index 0000000..f2175ee --- /dev/null +++ b/views.py @@ -0,0 +1,157 @@ +import json +import time + +import mongoengine +from flask import jsonify, request +from flask_views.base import View +from mongoengine import ValidationError, DoesNotExist, InvalidQueryError +from werkzeug.exceptions import Unauthorized, NotFound + +from restapi.resource import Resource + + +class ApiView(View): + model = None + authentication_methods = [] + + def __init__(self, model,authentication_methods): + self.start = time.time() + self.model = model + self.resource = Resource(self.model) + self.authentication_methods = authentication_methods + + def dispatch_request(self, *args, **kwargs): + # keep all the logic in a helper method (_dispatch_request) so that + # it's easy for subclasses to override this method without them also having to copy/paste all the + # authentication logic, etc. + return self._dispatch_request(*args, **kwargs) + + def _dispatch_request(self, *args, **kwargs): + authorized = True if len(self.authentication_methods) == 0 else False + for authentication_method in self.authentication_methods: + if authentication_method().authorized(): + authorized = True + if not authorized: + return {'error': 'Unauthorized'}, '401 Unauthorized' + + try: + return super(ApiView, self).dispatch_request(*args, **kwargs) + except mongoengine.queryset.DoesNotExist as e: + return {'error': 'Empty query: ' + str(e)}, '404 Not Found' + except ValidationError as e: + return e.args[0], '400 Bad Request' + except Unauthorized: + return {'error': 'Unauthorized'}, '401 Unauthorized' + except NotFound as e: + return {'error': str(e)}, '404 Not Found' + + def get(self, *args, **kwargs): + """ + TODO: check permissions + :param args: + :param kwargs: + :return: + """ + if 'pk' in kwargs: + try: + qs = self.resource.to_qs(pk=kwargs.get('pk')) + count, data = self.resource.to_json(pk=kwargs.get('pk')) + response = { + 'response': data, + 'status': True, + 'info': { + 'took': time.time() - self.start + }, + } + except (ValidationError, DoesNotExist) as e: + kwargs.update({'code': 404}) + response = { + "status": False, + "errors": "Object not found" + } + else: + if "query" in request.args and self.model._meta.get('can_query'): + self.resource.external_query(json.loads(request.args.get('query'))) + qs = self.resource.to_qs() + count, data = self.resource.to_json() + response = { + 'response': data, + 'info': { + 'offset': self.resource.offset, + 'limit': self.resource.limit, + 'status': True, + 'total': count, + 'took': time.time() - self.start + }, + } + + return jsonify(response), kwargs.get('code', 200) + + def post(self, *args, **kwargs): + """ + TODO: check permissions + + :param args: + :param kwargs: + :return: + """ + try: + item = self.model(**request.json) + item.validate() + except ValidationError as v: + return jsonify({ + 'status': False, + 'errors': [{"field": field, "error": str(error)} for field, error in v.__dict__.get('errors').items() if + isinstance(error, ValidationError) and field[0] != "_"] + }), 400 + except Exception as e: + return jsonify({ + 'status': False, + 'errors': str(e) + }), 400 + data = item.save() + return self.get(pk=data.id, code=201) + + def put(self, *args, **kwargs): + """ + TODO: check permissions + + :param args: + :param kwargs: + :return: + """ + if "pk" not in kwargs: + return jsonify({ + 'status': False, + "error": "Method not allowed" + }), 403 + else: + try: + self.model.objects(id=kwargs.get('pk')).update(**request.json) + return self.get(pk=kwargs.get('pk')) + except ValidationError as v: + print(v.__dict__) + return jsonify({ + 'status': False, + 'errors': [{"field": v.__dict__.get('field_name'), "error": v.__dict__.get('_message')}] + }), 400 + except InvalidQueryError as e: + return jsonify({ + 'status': False, + 'errors': str(e) + }), 400 + + def has_read_permission(self, request, qs): + return qs + + def has_add_permission(self, request, obj): + return True + + def has_change_permission(self, request, obj): + return True + + def has_delete_permission(self, request, obj): + return True + + def delete(self, *args, **kwargs): + "delete method"