Skip to content

Commit

Permalink
Merge pull request #3 from midezz/pydantic-support
Browse files Browse the repository at this point in the history
Pydantic support
  • Loading branch information
midezz authored Nov 12, 2021
2 parents 415b0c4 + ac350e5 commit c505b6b
Show file tree
Hide file tree
Showing 23 changed files with 182 additions and 89 deletions.
2 changes: 1 addition & 1 deletion .flake8
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[flake8]
max-line-length = 88
max-line-length = 120
exclude =
venv,
__pycache__,
Expand Down
2 changes: 1 addition & 1 deletion app.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import uvicorn

import models
from src.simplerestapi.main import SimpleApi
from simplerestapi.main import SimpleApi

app = SimpleApi(models, os.environ['DB_URL'])

Expand Down
2 changes: 1 addition & 1 deletion models.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from sqlalchemy import Column, Integer, String, create_engine
from sqlalchemy.ext.declarative import declarative_base

from src.simplerestapi.endpoint import ConstructEndpoint, Endpoint
from simplerestapi.endpoint import ConstructEndpoint, Endpoint

Base = declarative_base(metaclass=ConstructEndpoint)

Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[tool.black]
line-length = 88
line-length = 120
target-version = ['py36']
include = '\.pyi?$'
exclude = '''
Expand Down
3 changes: 2 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@ requests==2.25.1
flake8==3.8.4
pytest==6.2.2
isort==5.7.0
black==20.8b1
black==20.8b1
pydantic-sqlalchemy==0.0.9
6 changes: 3 additions & 3 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,12 @@
requirements = [
'SQLAlchemy>1.3.0,<=1.3.23',
'starlette>0.13.0,<=0.14.2',
'pydantic>1.7,<=1.8',
]

setuptools.setup(
name='simplerestapi',
version='1.0.1',
version='1.0.2',
author='Andrey Nikulin',
author_email='[email protected]',
description='SimpleRestAPI is the library for launch REST API based on your SQLAlchemy models',
Expand All @@ -37,7 +38,6 @@

],
install_requires=requirements,
package_dir={'': 'src'},
packages=setuptools.find_packages(where='src'),
packages=['simplerestapi'],
python_requires='>3.6',
)
File renamed without changes.
13 changes: 3 additions & 10 deletions src/simplerestapi/api.py → simplerestapi/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,17 +45,12 @@ async def get(self, request):
if not params.is_valid():
session.close()
return JSONResponse({'errors': params.errors}, status_code=400)
query = (
session.query(self.model).filter(*params.filters).order_by(params.order_by)
)
query = session.query(self.model).filter(*params.filters).order_by(params.order_by)
offset = (params.page_param - 1) * self.model.ConfigEndpoint.pagination
if params.limit_param is None:
query = query.offset(offset).limit(self.model.ConfigEndpoint.pagination)
else:
if (
params.limit_param <= offset + self.model.ConfigEndpoint.pagination
and params.limit_param > offset
):
if params.limit_param <= offset + self.model.ConfigEndpoint.pagination and params.limit_param > offset:
limit = params.limit_param - offset
query = query.offset(offset).limit(limit)
elif params.limit_param > offset + self.model.ConfigEndpoint.pagination:
Expand Down Expand Up @@ -107,9 +102,7 @@ async def update(self, request):
data = await request.json()
session = Session()
try:
result = (
session.query(self.model).filter_by(**request.path_params).update(data)
)
result = session.query(self.model).filter_by(**request.path_params).update(data)
session.commit()
except Exception:
session.close()
Expand Down
17 changes: 7 additions & 10 deletions src/simplerestapi/endpoint.py → simplerestapi/endpoint.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from sqlalchemy.ext.declarative import DeclarativeMeta, declared_attr

from .api import HANDLER_CLASS_LISTCREATE, GetUpdateDeleteAPI
from .model_validator import ModelValidator
from .router import SimpleApiRouter


Expand Down Expand Up @@ -46,13 +47,13 @@ def get_other_routes(cls):

@classmethod
def get_columns_values(cls, model):
columns = [
c.name
for c in cls.__table__.columns
if c.name not in cls.ConfigEndpoint.exclude_fields
]
columns = [c.name for c in cls.__table__.columns if c.name not in cls.ConfigEndpoint.exclude_fields]
return {column: getattr(model, column) for column in columns}

@classmethod
def validate_model(cls):
return ModelValidator(cls).errors


class ConfigEndpoint:
pagination = 100
Expand All @@ -62,11 +63,7 @@ class ConfigEndpoint:

@classmethod
def get_attrs(cls):
return {
attr: getattr(cls, attr)
for attr in cls.__dict__
if attr.find('__') == -1 and attr != 'get_attrs'
}
return {attr: getattr(cls, attr) for attr in cls.__dict__ if attr.find('__') == -1 and attr != 'get_attrs'}


class ConstructEndpoint(DeclarativeMeta):
Expand Down
5 changes: 5 additions & 0 deletions simplerestapi/exception.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
class ConfigEndpointError(Exception):
def __init__(self, message, errors):
message_errors = message + '\n' + '\n'.join(errors)
super().__init__(message_errors)
self.errors = errors
8 changes: 8 additions & 0 deletions src/simplerestapi/main.py → simplerestapi/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,16 @@

from . import Session
from .endpoint import Endpoint
from .exception import ConfigEndpointError


class SimpleApi:
def __init__(self, models, db, debug=True):
self.model_errors = []
self.models = []
self.routes = []
self.get_models(models)
self.raise_exceptions_by_errors()
self.construct_routes()
self.engine = create_engine(db)
Session.configure(bind=self.engine)
Expand All @@ -29,4 +32,9 @@ def construct_routes(self):
def get_models(self, models):
for _, member in inspect.getmembers(models):
if inspect.isclass(member) and Endpoint in member.__bases__:
self.model_errors += member.validate_model()
self.models.append(member)

def raise_exceptions_by_errors(self):
if self.model_errors:
raise ConfigEndpointError('Your models have errors:', self.model_errors)
43 changes: 43 additions & 0 deletions simplerestapi/model_validator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
from pydantic import BaseModel, validator


class BaseConfigEndpoint(BaseModel):
pagination: int = 100
denied_methods: list = []
path: str = None
exclude_fields: list = []

@validator('denied_methods')
def denied_methods_validate(cls, v):
correct_methods = {'get', 'post', 'delete', 'put', 'patch'}
if len(v) > 0 and set(v) - correct_methods:
raise ValueError('methods must be from list (\'get\', \'post\', \'delete\', \'put\', \'patch\')')

@validator('pagination')
def pagination_validate(cls, v):
if v < 0:
raise ValueError('value must be above or equal 0')


class ModelValidator:
def __init__(self, model):
self.model = model
self.errors = []
self.validate_config_endpoint(model)

def validate_config_endpoint(self, model):
base_config_properties = set(BaseConfigEndpoint.schema()['properties'])
model_config_properties = {key for key in model.ConfigEndpoint.__dict__ if not key.startswith('__')}
config_properties = {key: getattr(model.ConfigEndpoint, key) for key in base_config_properties}
if len(model_config_properties - base_config_properties) > 0:
msg = '\'{0}\': not support parameters'
self.add_error(msg.format(', '.join(model_config_properties - base_config_properties)))
try:
_ = BaseConfigEndpoint(**config_properties)
except Exception as e:
for error in e.errors():
error_msg = f'\'{" ".join(error["loc"])}\': {error["msg"]}'
self.add_error(error_msg)

def add_error(self, error_msg):
self.errors.append(f'Model \'{self.model.__name__}\' has incorrect ConfigEndpoint parameter {error_msg}')
File renamed without changes.
16 changes: 4 additions & 12 deletions src/simplerestapi/url_params.py → simplerestapi/url_params.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,10 @@ def filters(self):
for param, value in self.filter_params.items():
conditions = param.split('__')
if len(conditions) > 1:
criterion = CONDITIONS.get(conditions[1])(
getattr(self.model_class, conditions[0]), value
)
criterion = CONDITIONS.get(conditions[1])(getattr(self.model_class, conditions[0]), value)
res_filters.append(criterion)
else:
criterion = CONDITIONS.get('equal')(
getattr(self.model_class, conditions[0]), value
)
criterion = CONDITIONS.get('equal')(getattr(self.model_class, conditions[0]), value)
res_filters.append(criterion)
return res_filters

Expand All @@ -57,9 +53,7 @@ def valid_filters(self):

def valid_order(self):
if self.order_param:
order_by = (
self.order_param if self.order_param[0] != '-' else self.order_param[1:]
)
order_by = self.order_param if self.order_param[0] != '-' else self.order_param[1:]
if order_by not in self.columns:
self.errors.append(f'Order by \'{self.order_param}\' is not valid')

Expand All @@ -69,9 +63,7 @@ def valid_number_parameter(self, value, param_name):
try:
value = int(value)
except ValueError:
self.errors.append(
f'{param_name} parameter \'{self.limit_param}\' is not correct'
)
self.errors.append(f'{param_name} parameter \'{self.limit_param}\' is not correct')
else:
return value

Expand Down
2 changes: 1 addition & 1 deletion tests/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import pytest

from src.simplerestapi import Session
from simplerestapi import Session


def get_data(model_use):
Expand Down
19 changes: 13 additions & 6 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,26 @@
from sqlalchemy import Column, Integer, create_engine
from sqlalchemy_utils import create_database, drop_database

from src.simplerestapi.endpoint import ConfigEndpoint, Endpoint
from simplerestapi.endpoint import ConfigEndpoint, Endpoint
from tests import models

from .models import Base

ERROR_TEMPLATE = 'Model \'{0}\' has incorrect ConfigEndpoint parameter \'{1}\': {2}'


class TestExampleConfigEndpoint:
denied_methods = ['get', 'delete']
pagination = 20
path = '/test_path'
exclude_fields = ['id']


class ModelTest(models.Base, Endpoint):
id = Column(Integer, primary_key=True)

class ConfigEndpoint:
denied_methods = ['get', 'delete']
pagination = 20
path = '/test_path'
exclude_fields = ['id']

ModelTest.ConfigEndpoint = TestExampleConfigEndpoint()


@pytest.fixture
Expand Down Expand Up @@ -66,3 +72,4 @@ def reset_models_settings():
for _, member in inspect.getmembers(models):
if inspect.isclass(member) and Endpoint in member.__bases__:
setattr(member, 'ConfigEndpoint', ConfigEndpoint())
setattr(ModelTest, 'ConfigEndpoint', TestExampleConfigEndpoint())
4 changes: 2 additions & 2 deletions tests/models.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from sqlalchemy import Column, Integer, String
from sqlalchemy.ext.declarative import declarative_base

from src.simplerestapi.endpoint import ConstructEndpoint, Endpoint
from simplerestapi.endpoint import ConstructEndpoint, Endpoint

Base = declarative_base(metaclass=ConstructEndpoint)

Expand All @@ -17,4 +17,4 @@ class Car(Base, Endpoint):
id = Column(Integer, primary_key=True)
name_model = Column(String)
production = Column(String)
year = Column(Integer)
year = Column(Integer, nullable=False)
Loading

0 comments on commit c505b6b

Please sign in to comment.