Skip to content
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: 2 additions & 0 deletions DEVELOPMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,7 @@ docker exec -it flask-inputfilter pytest

### Run linting
```bash
docker exec -it flask-inputfilter sh -c "isort ."
docker exec -it flask-inputfilter sh -c "autoflake --in-place --remove-all-unused-imports --ignore-init-module-imports --recursive ."
docker exec -it flask-inputfilter black .
```
118 changes: 118 additions & 0 deletions EXTERNAL_API.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
# External API Functionality in `InputFilter`

This documentation provides a comprehensive overview of the external API functionality available in the `InputFilter` class. It covers the configuration, core methods, and examples of usage for interacting with external APIs.

---

## 1. Overview

The `InputFilter` class includes a mechanism for fetching data from external APIs during the input validation process.
This feature allows dynamic data retrieval based on user inputs, such as validating fields or fetching related data from an external service.

Important to know, the external api functionality runs after all other filters and validators have been executed.
This means that the data fetched from the external API will not be validated or filtered.

---

## 2. Configuration

The external API functionality is configured via the `external_api` parameter in the `add` method. This parameter accepts a dictionary with the following structure:

### `ExternalApiConfig` Fields

| Field | Type | Description |
|------------|--------------------------|-----------------------------------------------------------------------------|
| `url` | `str` | The URL of the external API, with optional placeholders in `{{}}` format. |
| `method` | `str` | The HTTP method to use (e.g., `GET`, `POST`). |
| `params` | `Optional[Dict[str, str]]` | Query parameters for the API, with placeholders allowed. |
| `data_key` | `Optional[str]` | Key in the JSON response to extract the required data. |
| `api_key` | `Optional[str]` | API key for authorization, sent in the `Authorization` header. |

---

## 3. Examples

### 3.1 Basic External API Integration

```python
from flask_inputfilter.InputFilter import InputFilter

class MyInputFilter(InputFilter):
def __init__(self):
super().__init__()

self.add(
"user_id", required=True
)
self.add(
"is_active",
required=True,
external_api={
"url": "https://api.example.com/users/{{user_id}}/status",
"method": "GET",
"data_key": "is_active",
},
)

# Example usage
filter_instance = MyInputFilter()
validated_data = filter_instance.validateData({"user_id": 123})
print(validated_data["is_active"]) # True or False based on API response
```

### 3.2 Using Query Parameters

```python
self.add(
"is_valid",
required=True,
external_api={
"url": "https://api.example.com/validate",
"method": "GET",
"params": {"user": "{{user_id}}", "hash": "{{hash}}"},
"data_key": "is_valid",
},
)
```

This configuration sends the `user_id` and `hash` as query parameters, replacing the placeholders with validated data.

---

### 3.3 Handling Fallback Values

If the external API call fails, a fallback value can be specified:

```python
self.add(
"user_info",
required=True,
fallback={"name": "unknown", "age": 0},
external_api={
"url": "https://api.example.com/user/{{user_id}}",
"method": "GET",
"data_key": "user",
},
)
```

---

## 4. Error Handling

- `ValidationError` is raised when:
- The API call returns a non-200 status code.
- A required field is missing and no fallback/default is provided.
- Validation of the field value fails.

---

## 7. Best Practices

- **Required Fields:** Clearly define required fields and provide fallback values where necessary.
- **Placeholders:** Ensure placeholders in URLs and parameters match the keys in `validated_data`.
- **Fallbacks:** Always provide fallback values for critical fields to avoid disruptions in case of API failure.
- **Security:** Use HTTPS for API calls and secure sensitive data like API keys.
- **Testing:** Mock external API calls during unit testing to avoid dependencies on external systems.

---
15 changes: 13 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,16 @@
The `InputFilter` class is used to validate and filter input data in Flask applications.
It provides a modular way to clean and ensure that incoming data meets expected format and type requirements before being processed.

---

## Installation

```bash
pip install flask-inputfilter
```

---

## Quickstart

To use the `InputFilter` class, you need to create a new class that inherits from it and define the fields you want to validate and filter.
Expand Down Expand Up @@ -72,6 +76,8 @@ def updateZipcode():
zipcode = data.get('zipcode')
```

---

## Options

The `add` method takes the following options:
Expand All @@ -81,16 +87,21 @@ The `add` method takes the following options:
- [`Validator`](src/flask_inputfilter/Validator/README.md)
- [`Default`](#default)
- [`Fallback`](#fallback)
- [`ExternalApi`](EXTERNAL_API.md)

### Required

The `required` option is used to specify if the field is required or not.
If the field is required and not present in the input data, the `validate` method will return a 400 response with the error message.
If the field is required and not present in the input data, the `validate` method will return the `ValidationError` with an error message.

### Default

The `default` option is used to specify a default value to use if the field is not present in the input data.

### Fallback

The `fallback` option is used to specify a fallback value to use if the field is not present in the input data, although it is required or the validation fails.
The `fallback` option is used to specify a fallback value to use if something unexpected happens, for example if the field is required but no value where provides
or the validation fails.

This means that if the field is not required and no value is present, the fallback value will not be used.
In this case you have to use the `default` option.
6 changes: 5 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,8 @@ requires = ["setuptools", "wheel"]
build-backend = "setuptools.build_meta"

[tool.black]
line-length = 90
line-length = 79

[tool.isort]
profile = 'black'
line_length = 79
5 changes: 4 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
autoflake
black
flake8
flask==0.10
isort
pillow==2.0.0
pytest
requests==2.12.0
setuptools
twine
black
6 changes: 4 additions & 2 deletions setup.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
from setuptools import setup, find_packages
from setuptools import find_packages, setup

setup(
name="flask_inputfilter",
version="0.0.3",
author="Leander Cain Slotosch",
author_email="[email protected]",
description="A library to filter and validate input data in" "Flask applications",
description="A library to filter and validate input data in"
"Flask applications",
long_description=open("README.md").read(),
long_description_content_type="text/markdown",
url="https://github.com/LeanderCS/flask-inputfilter",
Expand All @@ -14,6 +15,7 @@
install_requires=[
"Flask>=0.10",
"pillow>=2.0.0",
"requests>=2.12.0",
],
classifiers=[
"Programming Language :: Python :: 3",
Expand Down
3 changes: 2 additions & 1 deletion src/flask_inputfilter/Enum/RegexEnum.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ class RegexEnum(Enum):

ISO_DATE = r"^\d{4}-\d{2}-\d{2}$"
ISO_DATETIME = (
r"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}" r"(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})?$"
r"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}"
r"(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})?$"
)

PHONE_NUMBER = r"^\+?[\d\s\-()]{7,}$"
Expand Down
2 changes: 0 additions & 2 deletions src/flask_inputfilter/Exception/ValidationError.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,3 @@ class ValidationError(Exception):
"""
This class is used to raise an exception when a validation error occurs.
"""

pass
29 changes: 16 additions & 13 deletions src/flask_inputfilter/Filter/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,19 @@ The `Filter` module contains the filters that can be used to filter the input da

The following filters are available in the `Filter` module:

1. [`ArrayExplodeFilter`](src/flask_inputfilter/Filter/ArrayExplodeFilter.py) - Explodes the input string into an array.
2. [`StringTrimFilter`](src/flask_inputfilter/Filter/StringTrimFilter.py) - Trims the whitespace from the beginning and end of the string.
3. [`ToBooleanFilter`](src/flask_inputfilter/Filter/ToBooleanFilter.py) - Converts the string to a boolean value.
4. [`ToCamelCaseFilter`](src/flask_inputfilter/Filter/ToCamelCaseFilter.py) - Converts the string to camel case.
5. [`ToFloatFilter`](src/flask_inputfilter/Filter/ToFloatFilter.py) - Converts the string to a float value.
5. [`ToIntegerFilter`](src/flask_inputfilter/Filter/ToIntegerFilter.py) - Converts the string to an integer value.
6. [`ToLowerFilter`](src/flask_inputfilter/Filter/ToLowerFilter.py) - Converts the string to lowercase.
7. [`ToNullFilter`](src/flask_inputfilter/Filter/ToNullFilter.py) - Converts the string to `None` if it is already `None` or `''` (empty string).
8. [`ToPascaleCaseFilter`](src/flask_inputfilter/Filter/ToPascaleCaseFilter.py) - Converts the string to pascal case.
9. [`ToSnakeCaseFilter`](src/flask_inputfilter/Filter/ToSnakeCaseFilter.py) - Converts the string to snake case.
9. [`ToStringFilter`](src/flask_inputfilter/Filter/ToStringFilter.py) - Converts the input to a string value.
9. [`ToUpperFilter`](src/flask_inputfilter/Filter/ToUpperFilter.py) - Converts the string to uppercase.
10. [`WhitespaceCollapseFilter`](src/flask_inputfilter/Filter/WhitespaceCollapseFilter.py) - Collapses the whitespace in the string.
1. [`ArrayExplodeFilter`](ArrayExplodeFilter.py) - Explodes the input string into an array.
2. [`SlugifyFilter`](SlugifyFilter.py) - Converts the string to a slug.
3. [`StringTrimFilter`](StringTrimFilter.py) - Trims the whitespace from the beginning and end of the string.
4. [`ToAlphaNumericFilter`](ToAlphaNumericFilter.py) - Converts the string to an alphanumeric string.
5. [`ToBooleanFilter`](ToBooleanFilter.py) - Converts the string to a boolean value.
6. [`ToCamelCaseFilter`](ToCamelCaseFilter.py) - Converts the string to camel case.
7. [`ToFloatFilter`](ToFloatFilter.py) - Converts the string to a float value.
8. [`ToIntegerFilter`](ToIntegerFilter.py) - Converts the string to an integer value.
9. [`ToLowerFilter`](ToLowerFilter.py) - Converts the string to lowercase.
10. [`ToNormalizedUnicodeFilter`](ToNormalizedUnicodeFilter.py) - Normalizes the unicode string.
11. [`ToNullFilter`](ToNullFilter.py) - Converts the string to `None` if it is already `None` or `''` (empty string).
12. [`ToPascaleCaseFilter`](ToPascaleCaseFilter.py) - Converts the string to pascal case.
13. [`ToSnakeCaseFilter`](ToSnakeCaseFilter.py) - Converts the string to snake case.
14. [`ToStringFilter`](ToStringFilter.py) - Converts the input to a string value.
15. [`ToUpperFilter`](ToUpperFilter.py) - Converts the string to uppercase.
16. [`WhitespaceCollapseFilter`](WhitespaceCollapseFilter.py) - Collapses the whitespace in the string.
22 changes: 22 additions & 0 deletions src/flask_inputfilter/Filter/SlugifyFilter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import re
from typing import Any, Optional

from ..Filter.BaseFilter import BaseFilter


class SlugifyFilter(BaseFilter):
"""
Filter that converts a string to a slug.
"""

def apply(self, value: Any) -> Optional[str]:

if not isinstance(value, str):
return None

value = value.lower()

value = re.sub(r"[^\w\s-]", "", value)
value = re.sub(r"[\s]+", "-", value)

return value
2 changes: 1 addition & 1 deletion src/flask_inputfilter/Filter/StringTrimFilter.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from ..Filter import BaseFilter
from ..Filter.BaseFilter import BaseFilter


class StringTrimFilter(BaseFilter):
Expand Down
19 changes: 19 additions & 0 deletions src/flask_inputfilter/Filter/ToAlphaNumericFilter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import re
from typing import Any, Optional

from ..Filter.BaseFilter import BaseFilter


class ToAlphaNumericFilter(BaseFilter):
"""
Filter that ensures a string contains only alphanumeric characters.
"""

def apply(self, value: Any) -> Optional[str]:

if not isinstance(value, str):
return None

value = re.sub(r"[^\w]", "", value)

return value
2 changes: 1 addition & 1 deletion src/flask_inputfilter/Filter/ToBooleanFilter.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from typing import Any, Optional

from ..Filter import BaseFilter
from ..Filter.BaseFilter import BaseFilter


class ToBooleanFilter(BaseFilter):
Expand Down
2 changes: 1 addition & 1 deletion src/flask_inputfilter/Filter/ToCamelCaseFilter.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import re
from typing import Any, Optional

from ..Filter import BaseFilter
from ..Filter.BaseFilter import BaseFilter


class ToCamelCaseFilter(BaseFilter):
Expand Down
2 changes: 1 addition & 1 deletion src/flask_inputfilter/Filter/ToFloatFilter.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from ..Filter import BaseFilter
from ..Filter.BaseFilter import BaseFilter


class ToFloatFilter(BaseFilter):
Expand Down
2 changes: 1 addition & 1 deletion src/flask_inputfilter/Filter/ToIntegerFilter.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from typing import Any, Optional

from ..Filter import BaseFilter
from ..Filter.BaseFilter import BaseFilter


class ToIntegerFilter(BaseFilter):
Expand Down
2 changes: 1 addition & 1 deletion src/flask_inputfilter/Filter/ToLowerFilter.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from ..Filter import BaseFilter
from ..Filter.BaseFilter import BaseFilter


class ToLowerFilter(BaseFilter):
Expand Down
33 changes: 33 additions & 0 deletions src/flask_inputfilter/Filter/ToNormalizedUnicodeFilter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import unicodedata
from typing import Any, Optional

from typing_extensions import Literal

from ..Filter.BaseFilter import BaseFilter


class ToNormalizedUnicodeFilter(BaseFilter):
"""
Filter that normalizes a string to a specified Unicode form.
"""

def __init__(
self, form: Literal["NFC", "NFD", "NFKC", "NFKD"] = "NFC"
) -> None:

self.form = form

def apply(self, value: Any) -> Optional[str]:

if not isinstance(value, str):
return None

value = unicodedata.normalize(self.form, value)

value_without_accents = "".join(
char
for char in unicodedata.normalize("NFD", value)
if unicodedata.category(char) != "Mn"
)

return unicodedata.normalize(self.form, value_without_accents)
2 changes: 1 addition & 1 deletion src/flask_inputfilter/Filter/ToNullFilter.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from typing import Any, Optional

from ..Filter import BaseFilter
from ..Filter.BaseFilter import BaseFilter


class ToNullFilter(BaseFilter):
Expand Down
Loading
Loading