Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement blank_none option, satisfying #35 #56

Merged
merged 7 commits into from
Oct 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/python-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ name: Flask-Parameter-Validation Unit Tests

on:
push:
branches: [ "master", "github-ci" ]
branches: [ "master" ]
pull_request:
branches: [ "master" ]

Expand Down
56 changes: 32 additions & 24 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -142,26 +142,27 @@ These can be used in tandem to describe a parameter to validate: `parameter_name
### Validation with arguments to Parameter
Validation beyond type-checking can be done by passing arguments into the constructor of the `Parameter` subclass. The arguments available for use on each type hint are:

| Parameter Name | Type of Argument | Effective On Types | Description |
|-------------------|--------------------------------------------------|------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `default` | any | All, except in `Route` | Specifies the default value for the field, makes non-Optional fields not required |
| `min_str_length` | `int` | `str` | Specifies the minimum character length for a string input |
| `max_str_length` | `int` | `str` | Specifies the maximum character length for a string input |
| `min_list_length` | `int` | `list` | Specifies the minimum number of elements in a list |
| `max_list_length` | `int` | `list` | Specifies the maximum number of elements in a list |
| `min_int` | `int` | `int` | Specifies the minimum number for an integer input |
| `max_int` | `int` | `int` | Specifies the maximum number for an integer input |
| `whitelist` | `str` | `str` | A string containing allowed characters for the value |
| `blacklist` | `str` | `str` | A string containing forbidden characters for the value |
| `pattern` | `str` | `str` | A regex pattern to test for string matches |
| `func` | `Callable[Any] -> Union[bool, tuple[bool, str]]` | All | A function containing a fully customized logic to validate the value. See the [custom validation function](#custom-validation-function) below for usage |
| `datetime_format` | `str` | `datetime.datetime` | Python datetime format string datetime format string ([datetime format codes](https://docs.python.org/3/library/datetime.html#strftime-and-strptime-format-codes)) |
| `comment` | `str` | All | A string to display as the argument description in any generated documentation |
| `alias` | `str` | All but `FileStorage` | An expected parameter name to receive instead of the function name. |
| `json_schema` | `dict` | `dict` | An expected [JSON Schema](https://json-schema.org) which the dict input must conform to |
| `content_types` | `list[str]` | `FileStorage` | Allowed `Content-Type`s |
| `min_length` | `int` | `FileStorage` | Minimum `Content-Length` for a file |
| `max_length` | `int` | `FileStorage` | Maximum `Content-Length` for a file |
| Parameter Name | Type of Argument | Effective On Types | Description |
|-------------------|--------------------------------------------------|------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `default` | any | All, except in `Route` | Specifies the default value for the field, makes non-Optional fields not required |
| `min_str_length` | `int` | `str` | Specifies the minimum character length for a string input |
| `max_str_length` | `int` | `str` | Specifies the maximum character length for a string input |
| `min_list_length` | `int` | `list` | Specifies the minimum number of elements in a list |
| `max_list_length` | `int` | `list` | Specifies the maximum number of elements in a list |
| `min_int` | `int` | `int` | Specifies the minimum number for an integer input |
| `max_int` | `int` | `int` | Specifies the maximum number for an integer input |
| `whitelist` | `str` | `str` | A string containing allowed characters for the value |
| `blacklist` | `str` | `str` | A string containing forbidden characters for the value |
| `pattern` | `str` | `str` | A regex pattern to test for string matches |
| `func` | `Callable[Any] -> Union[bool, tuple[bool, str]]` | All | A function containing a fully customized logic to validate the value. See the [custom validation function](#custom-validation-function) below for usage |
| `datetime_format` | `str` | `datetime.datetime` | Python datetime format string datetime format string ([datetime format codes](https://docs.python.org/3/library/datetime.html#strftime-and-strptime-format-codes)) |
| `comment` | `str` | All | A string to display as the argument description in any generated documentation |
| `alias` | `str` | All but `FileStorage` | An expected parameter name to receive instead of the function name. |
| `json_schema` | `dict` | `dict` | An expected [JSON Schema](https://json-schema.org) which the dict input must conform to |
| `content_types` | `list[str]` | `FileStorage` | Allowed `Content-Type`s |
| `min_length` | `int` | `FileStorage` | Minimum `Content-Length` for a file |
| `max_length` | `int` | `FileStorage` | Maximum `Content-Length` for a file |
| `blank_none` | `bool` | `Optional[str]` | If `True`, an empty string will be converted to `None`, defaults to configured `FPV_BLANK_NONE`, see [Validation Behavior Configuration](#validation-behavior-configuration) for more |

These validators are passed into the `Parameter` subclass in the route function, such as:
* `username: str = Json(default="defaultusername", min_length=5)`
Expand All @@ -183,18 +184,25 @@ def is_odd(val: int):
return val % 2 != 0, "val must be odd"
```

### API Documentation
Using the data provided through parameters, docstrings, and Flask route registrations, Flask Parameter Validation can generate API Documentation in various formats.
To make this easy to use, it comes with a `Blueprint` and the output and configuration options below:
### Configuration Options

#### Format
#### API Documentation Configuration
* `FPV_DOCS_SITE_NAME: str`: Your site's name, to be displayed in the page title, default: `Site`
* `FPV_DOCS_CUSTOM_BLOCKS: array`: An array of dicts to display as cards at the top of your documentation, with the (optional) keys:
* `title: Optional[str]`: The title of the card
* `body: Optional[str] (HTML allowed)`: The body of the card
* `order: int`: The order in which to display this card (out of the other custom cards)
* `FPV_DOCS_DEFAULT_THEME: str`: The default theme to display in the generated webpage

See the [API Documentation](#api-documentation) below for other information on API Documentation generation

#### Validation Behavior Configuration
* `FPV_BLANK_NONE: bool`: Globally override the default `blank_none` behavior for routes in your application, defaults to `False` if unset

### API Documentation
Using the data provided through parameters, docstrings, and Flask route registrations, Flask Parameter Validation can generate API Documentation in various formats.
To make this easy to use, it comes with a `Blueprint` and the output shown below and configuration options [above](#api-documentation-configuration):

#### Included Blueprint
The documentation blueprint can be added using the following code:
```py
Expand Down
23 changes: 17 additions & 6 deletions flask_parameter_validation/parameter_types/parameter.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@
import dateutil.parser as parser
import jsonschema
from jsonschema.exceptions import ValidationError as JSONSchemaValidationError

import flask
from inspect import isclass

class Parameter:

Expand All @@ -31,6 +32,7 @@ def __init__(
comment=None, # str: comment for autogenerated documentation
alias=None, # str: alias for parameter name
json_schema=None, # dict: JSON Schema to check received dicts or lists against
blank_none=None, # bool: Whether blank strings should be converted to None when validating a type of Optional[str]
):
self.default = default
self.min_list_length = min_list_length
Expand All @@ -47,6 +49,7 @@ def __init__(
self.comment = comment
self.alias = alias
self.json_schema = json_schema
self.blank_none = blank_none

def func_helper(self, v):
func_result = self.func(v)
Expand Down Expand Up @@ -156,6 +159,10 @@ def validate(self, value):

def convert(self, value, allowed_types):
"""Some parameter types require manual type conversion (see Query)"""
blank_none = self.blank_none
if blank_none is None: # Default blank_none to False if not provided or set in app config
blank_none = False if "FPV_BLANK_NONE" not in flask.current_app.config else flask.current_app.config["FPV_BLANK_NONE"]

# Datetime conversion
if None in allowed_types and value is None:
return value
Expand Down Expand Up @@ -183,9 +190,13 @@ def convert(self, value, allowed_types):
return date.fromisoformat(str(value))
except ValueError:
raise ValueError("date format does not match ISO 8601")
elif len(allowed_types) == 1 and (issubclass(allowed_types[0], str) or issubclass(allowed_types[0], int) and issubclass(allowed_types[0], Enum)):
if issubclass(allowed_types[0], int):
value = int(value)
returning = allowed_types[0](value)
return returning
elif blank_none and type(None) in allowed_types and str in allowed_types and type(value) is str and len(value) == 0:
return None
elif any(isclass(allowed_type) and (issubclass(allowed_type, str) or issubclass(allowed_type, int) and issubclass(allowed_type, Enum)) for allowed_type in allowed_types):
for allowed_type in allowed_types:
if issubclass(allowed_type, Enum):
if issubclass(allowed_types[0], int):
value = int(value)
returning = allowed_types[0](value)
return returning
return value
26 changes: 20 additions & 6 deletions flask_parameter_validation/parameter_validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ def __call__(self, f):
def nested_func_helper(**kwargs):
"""
Validates the inputs of a Flask route or returns an error. Returns
are wrapped in a dictionary with a flag to let nested_func() know
are wrapped in a dictionary with a flag to let nested_func() know
if it should unpack the resulting dictionary of inputs as kwargs,
or just return the error message.
"""
Expand Down Expand Up @@ -199,12 +199,26 @@ def validate(self, expected_input, all_request_inputs):

# In python3.7+, typing.Optional is used instead of typing.Union[..., None]
if expected_input_type_str.startswith("typing.Optional"):
new_type = expected_input_type.__args__[0]
expected_input_type = new_type
expected_input_type_str = str(new_type)

expected_input_types = expected_input_type.__args__
user_inputs = [user_input]
# If typing.List in optional and user supplied valid list, convert remaining check only for list
for exp_type in expected_input_types:
if any(str(exp_type).startswith(list_hint) for list_hint in list_type_hints):
if type(user_input) is list:
if hasattr(exp_type, "__args__"):
if all(type(inp) in exp_type.__args__ for inp in user_input):
expected_input_type = exp_type
expected_input_types = expected_input_type.__args__
expected_input_type_str = str(exp_type)
user_inputs = user_input
elif int in exp_type.__args__: # Ints from list[str] sources haven't been converted yet, so give it a typecast for good measure
if all(type(int(inp)) in exp_type.__args__ for inp in user_input):
expected_input_type = exp_type
expected_input_types = expected_input_type.__args__
expected_input_type_str = str(exp_type)
user_inputs = user_input
# Prepare expected type checks for unions, lists and plain types
if expected_input_type_str.startswith("typing.Union"):
elif expected_input_type_str.startswith("typing.Union"):
expected_input_types = expected_input_type.__args__
user_inputs = [user_input]
# If typing.List in union and user supplied valid list, convert remaining check only for list
Expand Down
Loading