1919VALIDATE_PARAMS_MAX_DEPTH = 4
2020
2121
22- def is_optional (type_hint : Type ) -> bool :
23- """Check if the type hint is Optional[SomeType]."""
22+ def _is_optional (type_hint : Type ) -> bool :
23+ """Check if the type hint is :data:`~typing.Optional`.
24+
25+ :param type_hint: Type hint to check.
26+ :return: True if the type hint is :data:`~typing.Optional`, False otherwise.
27+
28+ :Example:
29+
30+ .. code-block:: python
31+
32+ from typing import Optional
33+ from flask_utils.decorators import _is_optional
34+
35+ _is_optional(Optional[str]) # True
36+ _is_optional(str) # False
37+
38+ .. versionadded:: 0.2.0
39+ """
2440 return get_origin (type_hint ) is Union and type (None ) in get_args (type_hint )
2541
2642
27- def make_optional (type_hint : Type ) -> Type :
28- """Wrap type hint with Optional if it's not already."""
29- if not is_optional (type_hint ):
43+ def _make_optional (type_hint : Type ) -> Type :
44+ """Wrap type hint with :data:`~typing.Optional` if it's not already.
45+
46+ :param type_hint: Type hint to wrap.
47+ :return: Type hint wrapped with :data:`~typing.Optional`.
48+
49+ :Example:
50+
51+ .. code-block:: python
52+
53+ from typing import Optional
54+ from flask_utils.decorators import _make_optional
55+
56+ _make_optional(str) # Optional[str]
57+ _make_optional(Optional[str]) # Optional[str]
58+
59+ .. versionadded:: 0.2.0
60+ """
61+ if not _is_optional (type_hint ):
3062 return Optional [type_hint ] # type: ignore
3163 return type_hint
3264
3365
34- def is_allow_empty (value : Any , type_hint : Type , allow_empty : bool ) -> bool :
35- """Determine if the value is considered empty and whether it's allowed."""
66+ def _is_allow_empty (value : Any , type_hint : Type , allow_empty : bool ) -> bool :
67+ """Determine if the value is considered empty and whether it's allowed.
68+
69+ :param value: Value to check.
70+ :param type_hint: Type hint to check against.
71+ :param allow_empty: Whether to allow empty values.
72+
73+ :return: True if the value is empty and allowed, False otherwise.
74+
75+ :Example:
76+
77+ .. code-block:: python
78+
79+ from typing import Optional
80+ from flask_utils.decorators import _is_allow_empty
81+
82+ _is_allow_empty(None, str, False) # False
83+ _is_allow_empty("", str, False) # False
84+ _is_allow_empty(None, Optional[str], False) # True
85+ _is_allow_empty("", Optional[str], False) # True
86+ _is_allow_empty("", Optional[str], True) # True
87+ _is_allow_empty("", str, True) # True
88+ _is_allow_empty([], Optional[list], False) # True
89+
90+ .. versionadded:: 0.2.0
91+ """
3692 if value in [None , "" , [], {}]:
3793 # Check if type is explicitly Optional or allow_empty is True
38- if is_optional (type_hint ) or allow_empty :
94+ if _is_optional (type_hint ) or allow_empty :
3995 return True
4096 return False
4197
4298
43- def check_type (value : Any , expected_type : Type , allow_empty : bool = False , curr_depth : int = 0 ) -> bool :
99+ def _check_type (value : Any , expected_type : Type , allow_empty : bool = False , curr_depth : int = 0 ) -> bool :
100+ """Check if the value matches the expected type, recursively if necessary.
101+
102+ :param value: Value to check.
103+ :param expected_type: Expected type.
104+ :param allow_empty: Whether to allow empty values.
105+ :param curr_depth: Current depth of the recursive check.
106+
107+ :return: True if the value matches the expected type, False otherwise.
108+
109+ :Example:
110+
111+ .. code-block:: python
112+
113+ from typing import List, Dict
114+ from flask_utils.decorators import _check_type
115+
116+ _check_type("hello", str) # True
117+ _check_type(42, int) # True
118+ _check_type(42.0, float) # True
119+ _check_type(True, bool) # True
120+ _check_type(["hello", "world"], List[str]) # True
121+ _check_type({"name": "Jules", "city": "Rouen"}, Dict[str, str]) # True
122+
123+ It also works recursively:
124+
125+ .. code-block:: python
126+
127+ from typing import List, Dict
128+ from flask_utils.decorators import _check_type
129+
130+ _check_type(["hello", "world"], List[str]) # True
131+ _check_type(["hello", 42], List[str]) # False
132+ _check_type([{"name": "Jules", "city": "Rouen"},
133+ {"name": "John", "city": "Paris"}], List[Dict[str, str]]) # True
134+ _check_type([{"name": "Jules", "city": "Rouen"},
135+ {"name": "John", "city": 42}], List[Dict[str, str]]) # False
136+
137+ .. versionadded:: 0.2.0
138+ """
139+
44140 if curr_depth >= VALIDATE_PARAMS_MAX_DEPTH :
45141 return True
46- if expected_type is Any or is_allow_empty (value , expected_type , allow_empty ):
142+ if expected_type is Any or _is_allow_empty (value , expected_type , allow_empty ):
47143 return True
48144
49145 if isinstance (value , bool ):
@@ -57,10 +153,10 @@ def check_type(value: Any, expected_type: Type, allow_empty: bool = False, curr_
57153 args = get_args (expected_type )
58154
59155 if origin is Union :
60- return any (check_type (value , arg , allow_empty , (curr_depth + 1 )) for arg in args )
156+ return any (_check_type (value , arg , allow_empty , (curr_depth + 1 )) for arg in args )
61157 elif origin is list :
62158 return isinstance (value , list ) and all (
63- check_type (item , args [0 ], allow_empty , (curr_depth + 1 )) for item in value
159+ _check_type (item , args [0 ], allow_empty , (curr_depth + 1 )) for item in value
64160 )
65161 elif origin is dict :
66162 key_type , val_type = args
@@ -69,7 +165,7 @@ def check_type(value: Any, expected_type: Type, allow_empty: bool = False, curr_
69165 for k , v in value .items ():
70166 if not isinstance (k , key_type ):
71167 return False
72- if not check_type (v , val_type , allow_empty , (curr_depth + 1 )):
168+ if not _check_type (v , val_type , allow_empty , (curr_depth + 1 )):
73169 return False
74170 return True
75171 else :
@@ -80,11 +176,66 @@ def validate_params(
80176 parameters : Dict [Any , Any ],
81177 allow_empty : bool = False ,
82178):
83- """Decorator to validate request JSON body parameters.
179+ """
180+ Decorator to validate request JSON body parameters.
181+
182+ This decorator ensures that the JSON body of a request matches the specified
183+ parameter types and includes all required parameters.
184+
185+ :param parameters: Dictionary of parameters to validate. The keys are parameter names
186+ and the values are the expected types.
187+ :param allow_empty: Allow empty values for parameters. Defaults to False.
188+
189+ :raises flask_utils.errors.badrequest.BadRequestError: If the JSON body is malformed,
190+ the Content-Type header is missing or incorrect, required parameters are missing,
191+ or parameters are of the wrong type.
192+
193+ :Example:
194+
195+ .. code-block:: python
196+
197+ from flask import Flask, request
198+ from typing import List, Dict
199+ from flask_utils.decorators import validate_params
200+ from flask_utils.errors.badrequest import BadRequestError
201+
202+ app = Flask(__name__)
203+
204+ @app.route("/example", methods=["POST"])
205+ @validate_params(
206+ {
207+ "name": str,
208+ "age": int,
209+ "is_student": bool,
210+ "courses": List[str],
211+ "grades": Dict[str, int],
212+ }
213+ )
214+ def example():
215+ \" ""
216+ This route expects a JSON body with the following:
217+ - name: str
218+ - age: int (optional)
219+ - is_student: bool
220+ - courses: list of str
221+ - grades: dict with str keys and int values
222+ \" ""
223+ data = request.get_json()
224+ return data
225+
226+ .. tip::
227+ You can use any of the following types:
228+ * str
229+ * int
230+ * float
231+ * bool
232+ * List
233+ * Dict
234+ * Any
235+ * Optional
236+ * Union
84237
85- Args:
86- parameters (Dict[Any, Any]): Dictionary of parameters to validate.
87- allow_empty (bool, optional): Allow empty values for parameters.
238+ .. versionadded:: 0.2.0
88239 """
89240
90241 def decorator (fn ):
@@ -106,7 +257,7 @@ def wrapper(*args, **kwargs):
106257 raise BadRequestError ("JSON body must be a dict" )
107258
108259 for key , type_hint in parameters .items ():
109- if not is_optional (type_hint ) and key not in data :
260+ if not _is_optional (type_hint ) and key not in data :
110261 raise BadRequestError (f"Missing key: { key } " , f"Expected keys are: { parameters .keys ()} " )
111262
112263 for key in data :
@@ -117,7 +268,7 @@ def wrapper(*args, **kwargs):
117268 )
118269
119270 for key in data :
120- if key in parameters and not check_type (data [key ], parameters [key ], allow_empty ):
271+ if key in parameters and not _check_type (data [key ], parameters [key ], allow_empty ):
121272 raise BadRequestError (f"Wrong type for key { key } ." , f"It should be { parameters [key ]} " )
122273
123274 return fn (* args , ** kwargs )
0 commit comments