Skip to content

Commit

Permalink
Merge pull request #73 from d-ganchar/release/4.1.0
Browse files Browse the repository at this point in the history
Release/4.1.0
  • Loading branch information
d-ganchar authored May 6, 2021
2 parents 03f83db + 91e0cfb commit 1781064
Show file tree
Hide file tree
Showing 10 changed files with 148 additions and 12 deletions.
10 changes: 7 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,19 @@ Key features
- Easy and beautiful
- Type conversion
- Extensible
- Nested JSON validation
- Header, GET, FORM and nested JSON validation
- Post validation hooks
- Custom error messages
- Supports [Flask-RESTful](https://flask-restful.readthedocs.io/en/latest/)
- Python 2.7 / 3.5 supported up to version 3.0
- Python 2.7 / 3.5 supported up to v3.0

#### How to install:

```
$ pip install flask_request_validator
```

[How to use](https://github.com/d-ganchar/flask_request_validator/wiki)
<p style="text-align:center">
<a href="https://github.com/d-ganchar/flask_request_validator/wiki">How to use</a> |
<a href="https://github.com/d-ganchar/flask_request_validator/wiki#here-are-my-wallets-if-youd-like-to-say-thanks-%EF%B8%8F">say thanks ❤</a>
</p>
2 changes: 2 additions & 0 deletions flask_request_validator/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
)
from .nested_json import JsonParam
from .valid_request import ValidRequest
from .after_param import AbstractAfterParam
from .rules import (
AbstractRule,
CompositeRule,
Expand All @@ -22,4 +23,5 @@
NotEmpty,
Pattern,
Datetime,
Number,
)
17 changes: 17 additions & 0 deletions flask_request_validator/after_param.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
from abc import ABC, abstractmethod

from .valid_request import ValidRequest


class AbstractAfterParam(ABC):
"""
:see: https://github.com/d-ganchar/flask_request_validator/issues/64
"""
@abstractmethod
def validate(self, value: ValidRequest):
"""
Called when all structures are valid and types are converted
(HEADERS, GET, POST has no errors)
:raises AfterParamError:
"""
pass
4 changes: 2 additions & 2 deletions flask_request_validator/error_formatter.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@
from .exceptions import *


def demo_error_formatter(error: Union[InvalidRequestError, InvalidHeadersError]) -> list:
def demo_error_formatter(error: Union[InvalidRequestError, InvalidHeadersError, AfterParamError]) -> list:
"""
Just demo. !!! not supported !!!
"""
if isinstance(error, InvalidHeadersError):
if isinstance(error, (InvalidHeadersError, AfterParamError)):
return [str(error)]

result = []
Expand Down
9 changes: 9 additions & 0 deletions flask_request_validator/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ class RequestError(Exception):
"""


class AfterParamError(RequestError):
pass


class WrongUsageError(RequestError):
pass

Expand Down Expand Up @@ -119,6 +123,11 @@ def __str__(self) -> str:
return 'invalid email address'


class NumberError(RuleError):
def __str__(self) -> str:
return 'expected number'


class ValueDatetimeError(RuleError):
def __init__(self, dt_format: str) -> None:
self.dt_format = dt_format
Expand Down
11 changes: 8 additions & 3 deletions flask_request_validator/rules.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import re
import sys
import numbers
from abc import ABC, abstractmethod
from datetime import datetime
from typing import Iterable
Expand Down Expand Up @@ -163,10 +164,14 @@ def __init__(self, dt_format: str) -> None:
self._dt_format = dt_format

def validate(self, value: str) -> datetime:
"""
:raises ValueDatetimeError:
"""
try:
return datetime.strptime(value, self._dt_format)
except ValueError:
raise ValueDatetimeError(self._dt_format)


class Number(AbstractRule):
def validate(self, value: Any) -> Any:
if not isinstance(value, numbers.Number):
raise NumberError
return value
10 changes: 8 additions & 2 deletions flask_request_validator/validator.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

from flask import request

from .after_param import AbstractAfterParam
from .exceptions import *
from .rules import CompositeRule
from .valid_request import ValidRequest
Expand Down Expand Up @@ -134,7 +135,7 @@ def get_value_from_request(self) -> Any:
return value


def validate_params(*params: Union[JsonParam, Param]):
def validate_params(*params: Union[JsonParam, Param, AbstractAfterParam]):
"""
:raises InvalidHeadersError:
When found invalid headers. Raises before other params validation
Expand All @@ -144,10 +145,12 @@ def validate_params(*params: Union[JsonParam, Param]):
def validate_request(func):
@wraps(func)
def wrapper(*args, **kwargs):
header_params, other_params = (), ()
header_params, other_params, after_params = (), (), ()
for param in params:
if isinstance(param, Param) and param.param_type == HEADER:
header_params += (param, )
elif isinstance(param, AbstractAfterParam):
after_params += (param, )
else:
other_params += (param, )

Expand All @@ -161,6 +164,9 @@ def wrapper(*args, **kwargs):
if type_errors:
raise InvalidRequestError(errors[GET], errors[FORM],
errors[PATH], errors[JSON])
for param in after_params:
param.validate(valid)

args += (valid, )
return func(*args, **kwargs)
return wrapper
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

setup(
name='flask_request_validator',
version='4.0.3',
version='4.1.0',
description='Flask request data validation',
long_description=long_description,
url='https://github.com/d-ganchar/flask_request_validator',
Expand Down
16 changes: 16 additions & 0 deletions tests/test_rules.py
Original file line number Diff line number Diff line change
Expand Up @@ -130,3 +130,19 @@ def test_datetime_rule(self, value, dt_format, dt, err):
self.assertRaises(ValueDatetimeError, rule.validate, value)
else:
self.assertEqual(dt, rule.validate(value))

@parameterized.expand([
(1.0000000000000001e-21, 1.0000000000000001e-21),
(2, 2),
(3.04, 3.04),
('3o1', None),
('', None),
([], None),
({}, None),
])
def test_number_rule(self, value, expected):
rule = Number()
if expected:
self.assertEqual(expected, rule.validate(value))
else:
self.assertRaises(NumberError, rule.validate, value)
79 changes: 78 additions & 1 deletion tests/test_validator.py
Original file line number Diff line number Diff line change
Expand Up @@ -347,7 +347,7 @@ def test_default_value(self):
_app2 = flask.Flask(__name__)


@_app2.errorhandler(InvalidRequestError)
@_app2.errorhandler(RequestError)
def handler(e):
return flask.jsonify(demo_error_formatter(e)), 400

Expand Down Expand Up @@ -482,3 +482,80 @@ def test_json_route_with_error_formatter(self, data, expected, status):
response = client.post('/', data=json.dumps(data), content_type='application/json')
self.assertEqual(response.status, status)
self.assertEqual(response.json, expected)


class ExampleAfterParam(AbstractAfterParam):
def validate(self, value: ValidRequest) -> Any:
errors = []
prev_date = None
for item in value.get_json()['dates']:
date = item.date()
if prev_date and date < prev_date:
errors.append('{d1} < {d2}'.format(d1=date, d2=prev_date))
continue
prev_date = date
if errors:
raise AfterParamError('|'.join(errors))


@_app2.route('/after_param', methods=['POST'])
@validate_params(
JsonParam({
'dates': JsonParam([Datetime('%Y-%m-%d'), ], as_list=True),
}),
ExampleAfterParam(),
)
def after_param(valid: ValidRequest):
return flask.jsonify([str(d.date()) for d in valid.get_json()['dates']])


class TestAfterParam(TestCase):
maxDiff = 2000

@parameterized.expand([
(
['2021-01-01', '2021-01-'],
[{'errors': [
{'list_items': {'1': 'expected a datetime in %Y-%m-%d format'},
'path': 'root|dates'}], 'message': 'invalid JSON parameters'}]
),
(
['2021'],
[{'errors': [{'list_items': {'0': 'expected a datetime in %Y-%m-%d format'},
'path': 'root|dates'}],
'message': 'invalid JSON parameters'}]
),
# valid
(
['2021-01-01', '2021-01-02', '2021-01-03', '2021-01-04'],
['2021-01-01', '2021-01-02', '2021-01-03', '2021-01-04'],
),
])
def test_after_param_rules(self, dates, expected):
with _app2.test_client() as client:
result = client.post(
'/after_param',
data=json.dumps({'dates': dates}),
content_type='application/json',
).json

self.assertEqual(result, expected)

@parameterized.expand([
# valid
(
['2021-01-01', '2021-01-02', '2021-01-03', '2021-01-04'],
['2021-01-01', '2021-01-02', '2021-01-03', '2021-01-04'],
),
# invalid
(['2021-01-01', '2021-01-02', '2021-01-03', '2021-01-01'], ['2021-01-01 < 2021-01-03']),
(['2021-01-10', '2021-01-01', ], ['2021-01-01 < 2021-01-10']),
])
def test_after_params(self, dates, expected):
with _app2.test_client() as client:
data = client.post(
'/after_param',
data=json.dumps({'dates': dates}),
content_type='application/json',
).json
self.assertEqual(data, expected)

0 comments on commit 1781064

Please sign in to comment.