Skip to content

Commit e024c82

Browse files
committed
34 | Update error messages
1 parent 6a0cb2d commit e024c82

File tree

9 files changed

+223
-44
lines changed

9 files changed

+223
-44
lines changed

.github/workflows/test-lib-building.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ jobs:
2525
id: build
2626

2727
- name: Install built library
28-
run: pip install dist/*.whl
28+
run: pip install "$(ls dist/*.whl | head -n 1)[optional]"
2929

3030
- name: Verify library usage - Part I
3131
run: |

docs/changelog.rst

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,20 @@ Changelog
44
All notable changes to this project will be documented in this file.
55

66

7+
[0.2.0] - 2025-04-07
8+
--------------------
9+
10+
Added
11+
^^^^^
12+
- getErrorMessages
13+
14+
Changed
15+
^^^^^^^
16+
- Updated error handling: The first error for each field is now returned in a combined format,
17+
enabling more detailed and flexible error handling on the frontend. :doc:`Check it out <guides/frontend_validation>`
18+
- Errors received through external_api request get logged.
19+
20+
721
[0.1.2] - 2025-03-29
822
--------------------
923

docs/conf.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
project = "flask-inputfilter"
22
copyright = "2025, Leander Cain Slotosch"
33
author = "Leander Cain Slotosch"
4-
release = "0.1.2"
4+
release = "0.2.0"
55

66
extensions = ["sphinx_rtd_theme"]
77

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
Frontend Validation
2+
===================
3+
4+
Keeping frontend and backend validations synchronized can take a lot of time and
5+
may lead to unexpected behavior if not maintained properly.
6+
7+
With ``flask_inputfilter`` you can easily implement an extra route to keep up with the validation and
8+
use the same ``InputFilter`` definition both for the frontend and backend validation.
9+
10+
11+
Example implementation
12+
~~~~~~~~~~~~~~~~~~~~~~~
13+
14+
.. code-block:: python
15+
16+
from flask import Response, Flask
17+
from flask_inputfilter import InputFilter
18+
from flask_inputfilter.Condition import ExactlyOneOfCondition
19+
from flask_inputfilter.Enum import RegexEnum
20+
from flask_inputfilter.Filter import StringTrimFilter, ToIntegerFilter, ToNullFilter
21+
from flask_inputfilter.Validator import IsIntegerValidator, IsStringValidator, RegexValidator
22+
23+
app = Flask(__name__)
24+
25+
class UpdateZipcodeInputFilter(InputFilter):
26+
def __init__(self):
27+
super().__init__()
28+
29+
self.add(
30+
'id',
31+
required=True,
32+
filters=[ToIntegerFilter(), ToNullFilter()],
33+
validators=[
34+
IsIntegerValidator()
35+
]
36+
)
37+
38+
self.add(
39+
'zipcode',
40+
filters=[StringTrimFilter()],
41+
validators=[
42+
RegexValidator(
43+
pattern=RegexEnum.POSTAL_CODE.value,
44+
error_message='The zipcode is not in the correct format.'
45+
)
46+
]
47+
)
48+
49+
@app.route('/form-update-zipcode', methods=['POST'])
50+
@UpdateZipcodeInputFilter.validate()
51+
def updateZipcode():
52+
return Response(status=200)
53+
54+
This basic implementation allows you to validate the form in the frontend through the route ``/form-update-zipcode``.
55+
If the validation is successful, it returns an empty response with the status code 200.
56+
If it fails, it returns an response with the status code 400 and the corresponding errors in json format.
57+
58+
If the validation for the zipcode fails, the response would be:
59+
60+
.. code-block:: python
61+
62+
{
63+
"zipcode": "The zipcode is not in the correct format."
64+
}
65+
66+
Validation errors of conditions can be found in the ``_condition`` field.

docs/guides/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,4 @@ Guides
66
:glob:
77

88
create_own_components
9+
frontend_validation

flask_inputfilter/InputFilter.py

Lines changed: 70 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import json
12
import re
23
from typing import Any, Callable, Dict, List, Optional, Tuple, Union
34

@@ -26,7 +27,7 @@ class InputFilter:
2627
"__global_validators",
2728
"__data",
2829
"__validated_data",
29-
"__error_message",
30+
"__errors",
3031
)
3132

3233
def __init__(self, methods: Optional[List[str]] = None) -> None:
@@ -37,7 +38,7 @@ def __init__(self, methods: Optional[List[str]] = None) -> None:
3738
self.__global_validators: List[BaseValidator] = []
3839
self.__data: Dict[str, Any] = {}
3940
self.__validated_data: Dict[str, Any] = {}
40-
self.__error_message: str = ""
41+
self.__errors: Dict[str, str] = {}
4142

4243
@final
4344
def add(
@@ -269,10 +270,10 @@ def clear(self) -> None:
269270
self.__global_validators.clear()
270271
self.__data.clear()
271272
self.__validated_data.clear()
272-
self.__error_message = ""
273+
self.__errors.clear()
273274

274275
@final
275-
def getErrorMessage(self) -> str:
276+
def getErrorMessage(self, field_name: str) -> str:
276277
"""
277278
Retrieves and returns a predefined error message.
278279
@@ -286,7 +287,23 @@ def getErrorMessage(self) -> str:
286287
Returns:
287288
str: A string representing the predefined error message.
288289
"""
289-
return self.__error_message
290+
return self.__errors.get(field_name)
291+
292+
@final
293+
def getErrorMessages(self) -> Dict[str, str]:
294+
"""
295+
Retrieves all error messages associated with the fields in the
296+
input filter.
297+
298+
This method aggregates and returns a dictionary of error messages
299+
where the keys represent field names, and the values are their
300+
respective error messages.
301+
302+
Returns:
303+
Dict[str, str]: A dictionary containing field names as keys and
304+
their corresponding error messages as values.
305+
"""
306+
return self.__errors
290307

291308
@final
292309
def getValue(self, name: str) -> Any:
@@ -516,8 +533,8 @@ def isValid(self) -> bool:
516533
try:
517534
self.validateData(self.__data)
518535

519-
except (ValidationError, Exception) as e:
520-
self.__error_message = str(e)
536+
except ValidationError as e:
537+
self.__errors = e.args[0]
521538
return False
522539

523540
return True
@@ -549,6 +566,7 @@ def validateData(
549566
"""
550567
validated_data = self.__validated_data
551568
data = data or self.__data
569+
errors = {}
552570

553571
for field_name, field_info in self.__fields.items():
554572
value = data.get(field_name)
@@ -562,30 +580,38 @@ def validateData(
562580
external_api = field_info.external_api
563581
copy = field_info.copy
564582

565-
if copy:
566-
value = validated_data.get(copy)
567-
568-
if external_api:
569-
value = self.__callExternalApi(
570-
external_api, fallback, validated_data
571-
)
583+
try:
584+
if copy:
585+
value = validated_data.get(copy)
572586

573-
value = self.__applyFilters(filters, value)
587+
if external_api:
588+
value = self.__callExternalApi(
589+
external_api, fallback, validated_data
590+
)
574591

575-
value = self.__validateField(validators, fallback, value) or value
592+
value = self.__applyFilters(filters, value)
593+
value = (
594+
self.__validateField(validators, fallback, value) or value
595+
)
596+
value = self.__applySteps(steps, fallback, value) or value
597+
value = self.__checkForRequired(
598+
field_name, required, default, fallback, value
599+
)
576600

577-
value = self.__applySteps(steps, fallback, value) or value
601+
validated_data[field_name] = value
578602

579-
value = self.__checkForRequired(
580-
field_name, required, default, fallback, value
581-
)
603+
except ValidationError as e:
604+
errors[field_name] = str(e)
582605

583-
validated_data[field_name] = value
606+
try:
607+
self.__checkConditions(validated_data)
608+
except ValidationError as e:
609+
errors["_condition"] = str(e)
584610

585-
self.__checkConditions(validated_data)
611+
if errors:
612+
raise ValidationError(errors)
586613

587614
self.__validated_data = validated_data
588-
589615
return validated_data
590616

591617
@classmethod
@@ -626,7 +652,11 @@ def wrapper(
626652
g.validated_data = input_filter.validateData()
627653

628654
except ValidationError as e:
629-
return Response(status=400, response=str(e))
655+
return Response(
656+
status=400,
657+
response=json.dumps(e.args[0]),
658+
mimetype="application/json",
659+
)
630660

631661
return f(*args, **kwargs)
632662

@@ -724,8 +754,14 @@ def __callExternalApi(
724754
Raised if the external API call does not succeed and no
725755
fallback value is provided.
726756
"""
757+
import logging
758+
727759
import requests
728760

761+
logger = logging.getLogger(__name__)
762+
763+
data_key = config.data_key
764+
729765
requestData = {
730766
"headers": {},
731767
"params": {},
@@ -753,25 +789,22 @@ def __callExternalApi(
753789
response = requests.request(**requestData)
754790

755791
if response.status_code != 200:
756-
raise ValidationError(
757-
f"External API call failed with "
758-
f"status code {response.status_code}"
792+
logger.error(
793+
f"External_api request inside of InputFilter "
794+
f"failed: {response.text}"
759795
)
796+
raise
760797

761798
result = response.json()
762799

763-
data_key = config.data_key
764800
if data_key:
765801
return result.get(data_key)
766802

767803
return result
768-
except Exception as e:
804+
except Exception:
769805
if fallback is None:
770-
self.__error_message = str(e)
771-
772806
raise ValidationError(
773-
f"External API call failed for field "
774-
f"'{config.data_key}'."
807+
f"External API call failed for field " f"'{data_key}'."
775808
)
776809

777810
return fallback
@@ -829,7 +862,9 @@ def __checkForRequired(
829862

830863
raise ValidationError(f"Field '{field_name}' is required.")
831864

832-
def __checkConditions(self, validated_data: dict) -> None:
865+
def __checkConditions(self, validated_data: Dict[str, Any]) -> None:
833866
for condition in self.__conditions:
834867
if not condition.check(validated_data):
835-
raise ValidationError(f"Condition '{condition}' not met.")
868+
raise ValidationError(
869+
f"Condition '{condition.__class__.__name__}' not met."
870+
)

setup.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,11 @@
22

33
setup(
44
name="flask_inputfilter",
5-
version="0.1.2",
5+
version="0.2.0",
66
license="MIT",
77
author="Leander Cain Slotosch",
88
author_email="[email protected]",
9-
description="A library to filter and validate input data in "
9+
description="A library to easily filter and validate input data in "
1010
"Flask applications",
1111
long_description=open("README.rst").read(),
1212
long_description_content_type="text/x-rst",
@@ -16,10 +16,14 @@
1616
),
1717
install_requires=[
1818
"flask>=2.1",
19-
"pillow>=8.0.0",
20-
"requests>=2.22.0",
2119
"typing_extensions>=3.6.2",
2220
],
21+
extras_require={
22+
"optional": [
23+
"pillow>=8.0.0",
24+
"requests>=2.22.0",
25+
],
26+
},
2327
classifiers=[
2428
"Operating System :: OS Independent",
2529
"Programming Language :: Python :: 3.14",

0 commit comments

Comments
 (0)