Type annotations in hug
hug leverages Python3 type annotations for validation and API specification. Within the context of hug, annotations should be set to one of 4 things:
- A cast function, built-in, or your own (str, int, etc) that takes a value casts it and then returns it, raising an exception if it is not in a format that can be cast into the desired type
- A hug type (hug.types.text, hug.types.number, etc.). These are essentially built-in cast functions that provide more contextual information, and good default error messages
- A marshmallow type and/or schema. In hug 2.0.0 Marshmallow is a first class citizen in hug, and all fields and schemas defined with it can be used in hug as type annotations
- A string. When a basic Python string is set as the type annotation it is used by hug to generate documentation, but does not get applied during the validation phase
For example:
import hug @hug.get() def hello(first_name: hug.types.text, last_name: 'Family Name', age: int): print("Hi {0} {1}!".format(first_name, last_name)
is a valid hug endpoint.
Any time a type annotation raises an exception during casting of a type, it is seen as a failure. Otherwise the cast is assumed successful with the returned type replacing the passed-in parameter. By default, all errors are collected in an errors dictionary and returned as the output of the endpoint before the routed function ever gets called. To change how errors are returned you can transform them via the on_invalid
route option, and specify a specific output format for errors by specifying the output_invalid
route option. Or, if you prefer, you can keep hug from handling the validation errors at all by passing in raise_on_invalid=True
to the route.
Built in hug types
hug provides several built-in types for common API use cases:
number
: Validates that a whole number was passed infloat_number
: Validates that a valid floating point number was passed indecimal
: Validates and converts the provided value into a Python Decimal objectuuid
: Validates that the provided value is a valid UUIDtext
: Validates that the provided value is a single string parametermultiple
: Ensures the parameter is passed in as a list (even if only one value is passed in)boolean
: A basic naive HTTP style boolean where no value passed in is seen asFalse
and any value passed in (even if itsfalse
) is seen asTrue
smart_boolean
: A smarter, but more computentionally expensive, boolean that checks the content of the value for common true / false formats (true, True, t, 1) or (false, False, f, 0)delimited_list(delimiter)
: splits up the passed in value based on the provided delimiter and then passes it to the function as a listone_of(values)
: Validates that the passed in value is one of those specifiedmapping(dict_of_passed_in_to_desired_values)
: Likeone_of
, but with a dictionary of acceptable values, to converted value.multi(types)
: Allows passing in multiple acceptable types for a parameter, short circuiting on the first acceptable onein_range(lower, upper, convert=number)
: Accepts a number within a lower and upper bound of acceptable valuesless_than(limit, convert=number)
: Accepts a number within a lower and upper bound of acceptable valuesgreater_than(minimum, convert=number)
: Accepts a value above a given minimumlength(lower, upper, convert=text)
: Accepts a a value that is within a specific length limitshorter_than(limit, convert=text)
: Accepts a text value shorter than the specified length limitlonger_than(limit, convert=text)
: Accepts a value up to the specified limitcut_off(limit, convert=text)
: Cuts off the provided value at the specified index
Extending and creating new hug types
The most obvious way to extend a hug type is to simply inherit from the base type defined in hug.types
and then override __call__
to override how the cast function, or override __init__
to override what parameters the type takes:
import hug class TheAnswer(hug.types.Text): """My new documentation""" def __call__(self, value): value = super().__call__(value) if value != 'fourty-two': raise ValueError('Value is not the answer to everything.') return value
If you simply want to perform additional conversion after a base type is finished, or modify its documentation, the most succinct way is the hug.type
decorator:
import hug @hug.type(extend=hug.types.number) def the_answer(value): """My new documentation""" if value != 42: raise ValueError('Value is not the answer to everything.') return value
Marshmallow integration
Marshmallow is an advanced serialization, deserialization, and validation library. Hug supports using marshmallow fields and schemas as input types.
Here is a simple example of an API that does datetime addition.
import datetime as dt import hug from marshmallow import fields from marshmallow.validate import Range @hug.get('/dateadd', examples="value=1973-04-10&addend=63") def dateadd(value: fields.Date(), addend: fields.Int(validate=Range(min=1))): """Add a value to a date.""" delta = dt.timedelta(days=addend) result = value + delta return {'result': result}