A simple, slim and useful, zero-dependency utility for parsing dictionaries or dictionary-like objects.
It's particularly useful for parsing incoming request data in REST APIs & web applications, for example in the case
of Flask, parsing form data from request.form, query string arguments fromrequest.args or JSON data from
request.json.
The dictparse design takes inspiration from Python's own argparse library, similar to the ArgumentParser class
, taking input as a dictionary or dictionary-like object, enforcing rules, types, applying functions, default values and
returning a NameSpace, with values mapped to attributes.
pip install dictparseThe following code is a Python program that takes takes some data in the form of a dictionary and parses it:
>>> from dictparse import DictionaryParser
>>> parser = DictionaryParser()
>>> parser.add_param("name", str, required=True)
>>> params = parser.parse_dict({"name": "FooBar"})
>>> params.name
'FooBar'The first step is to create the DictionaryParser object
>>> from dictparse import DictionaryParser
>>> parser = DictionaryParser(description="Create a new user")Adding parameters to the parser is done by making calls to the add_param method. These calls tell the
DictionaryParser how to handle the values passed in and turn them into the desired output, enforcing rules
, changing types and transforming values based on the arguments passed to the add_param method.
>>> parser = DictionaryParser()
>>> parser.add_param("name", str, required=True)
>>> parser.add_param("language", str, choices=["python", "javascript", "rust"])
>>> parser.add_param("tags", str, action=lambda x: x.split(","))
>>> params = parser.parse_dict({"name": "FooBar", "language": "python", "tags": "foo,bar,baz"})
>>> params.name
'FooBar'
>>> params.language
'python'
>>> params.tags
['foo', 'bar', 'baz']
>>> params.to_dict()
{'name': 'FooBar', 'language': 'python', 'tags': ['foo', 'bar', 'baz']}If the parser does not find a value matching the name, the default value is None
DictionaryParser.add_param(
name: str,
type_: Optional[Union[Type[str], Type[int], Type[float], Type[bool], Type[list], Type[dict], Type[set], Type[tuple]]] = None,
dest: Optional[str] = None,
required: Optional[bool] = False,
choices: Optional[Union[list, set, tuple]] = None,
action: Optional[Callable] = None,
description: Optional[str] = None,
default: Optional[Any] = None,
regex: Optional[str] = None
) -> Nonename: The parameter name (required - See note below)type_: The common parameter type (The parser will attempt to convert the parameter value to the given type)dest: The destination name of the parameter (See note below)required: IfTrue, enforce a value for the parameter must existschoices: A list, set, or tuple of possible choicesaction: A function to apply to the value (Applied after any type conversion)description: A description of the parameterdefault: A default value for the parameter if not foundregex: A regular expression to match against (Sets the parameter toNoneif the match is negative)
Note - The
nameanddestparameters must comply with standard Python variable naming conventions (only start with a letter or underscore & only contain alpha-numeric characters), not be a Python keyword and not start and end with a double underscore (dunder)
After creating the parser and adding parameters to it, data can be parsed by calling the parse_dict method, passing
in the data to be parsed. This returns a NameSpace object.
DictionaryParser.parse_dict(
data: Dict[str, Any],
strict: Optional[bool] = False,
action: Optional[Callable] = None
) -> NameSpace:data: A dictionary or dictionary-like objectstrict: IfTrue, raises an exception if any parameters not added to the parser are receivedaction: A function to apply to all parameters (after any type conversion and after action passed toadd_param)
A NameSpace object is returned when calling parse_dict and contains the parsed data after applying your rules
defined when adding arguments.
Parameters can be accessed as attributes of the NameSpace using dot notation:
>>> parser = DictionaryParser()
>>> parser.add_param("age", int, required=True)
>>> params = parser.parse_dict({"age": 30})
>>> params.age
30A standard AttributeError will be raised if an attribute is accessed without being added to the parser:
>>> params.foo
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'NameSpace' object has no attribute 'foo'if the dest parameter is supplied when adding a parameter in add_param, the value can only be accessed by using the
dest value:
>>> parser = DictionaryParser()
>>> parser.add_param("bar", str, dest="foo")
>>> params = parser.parse_dict({"bar": "bar"})
>>> params.foo
'bar'The NameSpace object has the following available methods:
NameSpace.get(
name: str,
default: Optional[Any] = None
) -> Union[None, Any]:Calling the get method on the NameSpace and passing in a key works in the same way as calling get on a dictionary
, returning either the value for the parameter requested or None if the NameSpace does not have that attribute.
An optional default value can be supplied using the default parameter to be returned if the attribute does not exist.
>>> parser = DictionaryParser()
>>> parser.add_param("age", int, required=True)
>>> parser.add_param("weight", int)
>>> params = parser.parse_dict({"age": 30, "height": 1.9})
>>> params.weight
None
>>> params.get("age")
30
>>> params.get("foo", 42)
42NameSpace.to_dict() -> dictReturns a dictionary of the parsed parameters.
>>> parser = DictionaryParser()
>>> parser.add_param("one", str)
>>> parser.add_param("two", int)
>>> parser.add_param("three", list)
>>> params = parser.parse_dict({"one": "one", "two": 2, "three": [1, 2, 3]})
>>> params.to_dict()
{'one': 'one', 'two': 2, 'three': [1, 2, 3]}to_dict() accepts an optional parameter exclude, a list of keys to exclude from the returned dictionary
>>> from dictparse import DictionaryParser
>>> parser = DictionaryParser()
>>> parser.add_param("csrf_token", str, required=True)
>>> parser.add_param("name", str)
>>> parser.add_param("email", str)
>>> params = parser.parse_dict({"csrf_token": "xxyyzz112233", "name": "foo", "email": "foo@bar.com"})
>>> params.to_dict(exclude=["csrf_token"])
{'name': 'foo', 'email': 'foo@bar.com'}Returns a Param object
>>> from dictparse import DictionaryParser
>>> parser = DictionaryParser()
>>> parser.add_param("names", list, default=[])
>>> params = parser.parse_dict({"names": ["foo", "bar"]})
>>> names = params.get_param("names")
>>> names.name
'names'
>>> names.value
['foo', 'bar']
>>> names.default
[]Param objects are hold all data associated with the parameter, as can be seen below in the Param.__init__ method:
class Param(object):
def __init__(
self,
name: str,
type_: Optional[Union[Type[str], Type[int], Type[float], Type[bool], Type[list], Type[dict], Type[set], Type[tuple]]] = None,
dest: Optional[str] = None,
required: Optional[bool] = False,
choices: Optional[Union[list, set, tuple]] = None,
action: Optional[Callable] = None,
description: Optional[str] = None,
default: Optional[Any] = None,
regex: Optional[str] = None,
value: Optional[Any] = None
):Note - The
NameSpacewill be assigned the value fordestif supplied inadd_param
>>> from dictparse import DictionaryParser
>>> parser = DictionaryParser()
>>> parser.add_param("foo", str, dest="bar")
>>> params = parser.parse_dict({"foo": 42})
>>> param = params.get_param("bar")
>>> param.name
'foo'
>>> param.dest
'bar'
>>> param.value
'42'An example of parsing JSON data sent in a POST request to a Flask route:
from app.users import create_user
from flask import Flask, request
from respond import JSONResponse
from dictparse import DictionaryParser
def create_app():
app = Flask(__name__)
@app.route("/", methods=["POST"])
def post():
parser = DictionaryParser(description="Create a new user")
parser.add_param("name", str, required=True)
parser.add_param("age", int)
parser.add_param("password", str, required=True, action=lambda x: x.encode("utf-8"))
parser.add_param("interests", list, action=lambda x: [i.strip() for i in x])
parser.add_param("level", float, default=1.5)
parser.add_param("stage", str, choices=["alpha", "beta"])
try:
params = parser.parse_dict(request.get_json())
except Exception as e:
return JSONResponse.bad_request(str(e))
user = create_user(
name=params.name,
age=params.age,
password=params.password,
interests=params.interests,
level=params.level,
stage=params.stage
)
return JSONResponse.created(user.to_dict())
return app
if __name__ == "__main__":
app = create_app()
app.run()Exceptions will be raised in the following scenarios:
Raised when a parameter cannot be parsed to the type declared in add_param
from dictparse import DictionaryParser
from dictparse.exceptions import ParserTypeError
parser = DictionaryParser()
parser.add_param("age", int)
try:
params = parser.parse_dict({"age": "thirty"})
except ParserTypeError as e:
print(e) # Invalid value 'thirty' for parameter 'age', expected 'int' not 'str'ParserTypeError contains the following attributes:
param: The parameter name (str)value: The parameter value (Any)expected: The expected type (type)
Raised when parse_dict is called and a parameter is required, but not found
from dictparse import DictionaryParser
from dictparse.exceptions import ParserRequiredParameterError
parser = DictionaryParser()
parser.add_param("name", str)
parser.add_param("email", str, required=True)
try:
params = parser.parse_dict({"name": "John Doe"})
except ParserRequiredParameterError as e:
print(e) # Missing required parameter 'email'ParserRequiredParameterErrorhas a single attributeparam, the name of the parameter (str)
Raised when parse_dict is called and parses a value not defined in the choices parameter of add_param
from dictparse import DictionaryParser
from dictparse.exceptions import ParserInvalidChoiceError
parser = DictionaryParser()
parser.add_param("name", str)
parser.add_param("language", str, choices=["python", "bash"])
try:
params = parser.parse_dict({"name": "John Doe", "language": "javascript"})
except ParserInvalidChoiceError as e:
print(e) # Parameter 'language' must be one of '['python', 'bash']', not 'javascript'ParserInvalidChoiceError has the following 3 attributes:
param: The parameter name (str)value: The parameter value (Any)choices: The available choices added viaadd_param(list|set|tuple)
Raised calling parse_dict with strict set to True
The strict parameter enforces the parser to only accept parameters that have been added to the parser
from dictparse import DictionaryParser
from dictparse.exceptions import ParserInvalidParameterError
parser = DictionaryParser()
parser.add_param("name", str)
parser.add_param("language", str, choices=["python", "bash"])
try:
params = parser.parse_dict({"name": "John Doe", "language": "python", "email": "jdoe@gmail.com"}, strict=True)
except ParserInvalidParameterError as e:
print(e) # Invalid parameter 'email'ParserInvalidParameterError has a single attribute param, the name of the parameter (str)
If an invalid data type for data is passed to parse_dict (such as a list or string), it raises a
ParserInvalidDataTypeError
from dictparse import DictionaryParser
from dictparse.exceptions import ParserInvalidDataTypeError
parser = DictionaryParser()
parser.add_param("name", str)
try:
params = parser.parse_dict([{"name", "John Doe"}])
except ParserInvalidDataTypeError as e:
print(e) # Invalid type for 'data', must be a dict or dict-like object, not 'list'
try:
params = parser.parse_dict("foo")
except ParserInvalidDataTypeError as e:
print(e) # Invalid type for 'data', must be a dict or dict-like object, not 'str'A test suite is available in the tests directory with 100% coverage (15/Sep/2020)
Name Stmts Miss Cover
---------------------------------------------
dictparse/__init__.py 1 0 100%
dictparse/exceptions.py 37 0 100%
dictparse/parser.py 106 0 100%
tests/__init__.py 0 0 100%
tests/test_parser.py 310 0 100%
---------------------------------------------
TOTAL 454 0 100%