Package: tonalite
tonaliteThis module simplifies creation of data classes (PEP 557)
from dictionaries.Tonalite is a fork ofdaciteas it wasn't maintain for a log time.InstallationTo install tonalite, simply usepip:$ pip install tonaliteRequirementsMinimum Python version supported bytonaliteis 3.6.Quick startfromdataclassesimportdataclassfromtonaliteimportfrom_dict@dataclassclassUser:name:strage:intis_active:booldata={'name':'John','age':30,'is_active':True,}user=from_dict(data_class=User,data=data)assertuser==User(name='John',age=30,is_active=True)FeaturesTonalite supports following features:nested structures(basic) types checkingoptional fields (i.e.typing.Optional)unionsforward referencescollectionscustom type hooksMotivationPassing plain dictionaries as a data container between your functions or
methods isn't a good practice. Of course you can always create your
custom class instead, but this solution is an overkill if you only want
to merge a few fields within a single object.Fortunately Python has a good solution to this problem - data classes.
Thanks to@dataclassdecorator you can easily create a new custom
type with a list of given fields in a declarative manner. Data classes
support type hints by design.However, even if you are using data classes, you have to create their
instances somehow. In many such cases, your input is a dictionary - it
can be a payload from a HTTP request or a raw data from a database. If
you want to convert those dictionaries into data classes,tonaliteis
your best friend.This library was originally created to simplify creation of type hinted
data transfer objects (DTO) which can cross the boundaries in the
application architecture.It's important to mention thattonaliteis not a data validation library.
There are dozens of awesome data validation projects and it doesn't make
sense to duplicate this functionality withintonalite. If you want to
validate your data first, you should combinetonalitewith one of data
validation library.Please checkUse Casesection for a real-life example.UsageTonalite is based on a single function -tonalite.from_dict. This function
takes 3 parameters:data_class- data class typedata- dictionary of input dataconfig(optional) - configuration of the creation process, instance
oftonalite.ConfigclassConfiguration is a (data) class with following fields:type_hookscastforward_referencescheck_typesstrictstrict_unions_matchThe examples below show all features offrom_dictfunction and usage
of allConfigparameters.Nested structuresYou can pass a data with nested dictionaries and it will create a proper
result.@dataclassclassA:x:stry:int@dataclassclassB:a:Adata={'a':{'x':'test','y':1,}}result=from_dict(data_class=B,data=data)assertresult==B(a=A(x='test',y=1))Optional fieldsWhenever your data class has aOptionalfield and you will not provide
input data for this field, it will take theNonevalue.fromtypingimportOptional@dataclassclassA:x:stry:Optional[int]data={'x':'test',}result=from_dict(data_class=A,data=data)assertresult==A(x='test',y=None)UnionsIf your field can accept multiple types, you should useUnion. Tonalite
will try to match data with provided types one by one. If none will
match, it will raiseUnionMatchErrorexception.fromtypingimportUnion@dataclassclassA:x:str@dataclassclassB:y:int@dataclassclassC:u:Union[A,B]data={'u':{'y':1,},}result=from_dict(data_class=C,data=data)assertresult==C(u=B(y=1))CollectionsTonalite supports fields defined as collections. It works for both - basic
types and data classes.@dataclassclassA:x:stry:int@dataclassclassB:a_list:List[A]data={'a_list':[{'x':'test1','y':1,},{'x':'test2','y':2,}],}result=from_dict(data_class=B,data=data)assertresult==B(a_list=[A(x='test1',y=1),A(x='test2',y=2)])Type hooksYou can useConfig.type_hooksargument if you want to transform the input
data of a data class field with given type into the new value. You have to
pass a following mapping:{Type: callable}, wherecallableis aCallable[[Any], Any].@dataclassclassA:x:strdata={'x':'TEST',}result=from_dict(data_class=A,data=data,config=Config(type_hooks={str:str.lower}))assertresult==A(x='test')If a data class field type is aOptional[T]you can pass both -Optional[T]or justT- as a key intype_hooks. The same with generic
collections, e.g. when a field has typeList[T]you can useList[T]to
transform whole collection orTto transform each item.CastingIt's a very common case that you want to create an instance of a field type
from the input data with just calling your type with the input value. Of
course you can usetype_hooks={T: T}to achieve this goal butcast=[T]is
an easier and more expressive way. It also works with base classes - ifTis a base class of typeS, all fields of typeSwill be also "casted".fromenumimportEnumclassE(Enum):X='x'Y='y'Z='z'@dataclassclassA:e:Edata={'e':'x',}result=from_dict(data_class=A,data=data,config=Config(cast=[E]))# orresult=from_dict(data_class=A,data=data,config=Config(cast=[Enum]))assertresult==A(e=E.X)Forward ReferencesDefinition of forward references can be passed as a{'name': Type}mapping toConfig.forward_references. This dict is passed totyping.get_type_hints()as theglobalnsparam when evaluating each field's type.@dataclassclassX:y:"Y"@dataclassclassY:s:strdata=from_dict(X,{"y":{"s":"text"}},Config(forward_references={"Y":Y}))assertdata==X(Y("text"))Types checkingThere are rare cases whentonalitebuilt-in type checker can not validate
your types (e.g. custom generic class) or you have such functionality
covered by other library and you don't want to validate your types twice.
In such case you can disable type checking withConfig(check_types=False).
By default types checking is enabled.T=TypeVar('T')classX(Generic[T]):pass@dataclassclassA:x:X[str]x=X[str]()assertfrom_dict(A,{'x':x},config=Config(check_types=False))==A(x=x)Strict modeBy defaultfrom_dictignores additional keys (not matching data class field)
in the input data. If you want change this behaviour setConfig.stricttoTrue. In case of unexpected keyfrom_dictwill raiseUnexpectedDataErrorexception.Strict unions matchUnionallows to define multiple possible types for a given field. By defaulttonaliteis trying to find the first matching type for a provided data and it
returns instance of this type. It means that it's possible that there are other
matching types further on theUniontypes list. Withstrict_unions_matchonly a single match is allowed, otherwisetonaliteraisesStrictUnionMatchError.ExceptionsWhenever something goes wrong,from_dictwill raise adequate
exception. There are a few of them:WrongTypeError- raised when a type of a input value does not match
with a type of a data class fieldMissingValueError- raised when you don't provide a value for a
required fieldUnionMatchError- raised when provided data does not match any type
ofUnionForwardReferenceError- raised when undefined forward reference encountered in
dataclassUnexpectedDataError- raised whenstrictmode is enabled and the input
data has not matching keysStrictUnionMatchError- raised whenstrict_unions_matchmode is enabled
and the input data has ambiguousUnionmatchDevelopmentFirst of all - if you want to submit your pull request, thank you very much!
I really appreciate your support.Please remember that every new feature, bug fix or improvement should be tested.
100% code coverage is a must have.We are using a few static code analysis tools to increase the code quality
(black,mypy,pylint). Please make sure that you are not generating any
errors/warnings before you submit your PR. You can find current configuration
in.travis.ymlfile.Last but not least, if you want to introduce new feature, please discuss it
first within an issue.How to startClonetonaliterepository:$ git clone git@github.com:konradhalas/tonalite.gitCreate and activate virtualenv in the way you like:$ python3 -m venv tonalite-env
$ source tonalite-env/bin/activateInstall alltonalitedependencies:$ pip install -e .[dev]To run tests you just have to fire:$ pytestUse caseThere are many cases when we receive "raw" data (Python dicts) as a input to
our system. HTTP request payload is a very common use case. In most web
frameworks we receive request data as a simple dictionary. Instead of
passing this dict down to your "business" code, it's a good idea to create
something more "robust".Following example is a simpleflaskapp - it has single/productsendpoint.
You can use this endpoint to "create" product in your system. Our corecreate_productfunction expects data class as a parameter. Thanks totonalitewe can easily build such data class fromPOSTrequest payload.fromdataclassesimportdataclassfromtypingimportListfromflaskimportFlask,request,Responseimporttonaliteapp=Flask(__name__)@dataclassclassProductVariantData:code:strdescription:str=''stock:int=0@dataclassclassProductData:name:strprice:floatvariants:List[ProductVariantData]defcreate_product(product_data:ProductData)->None:pass# your business logic here@app.route("/products",methods=['POST'])defproducts():product_data=tonalite.from_dict(data_class=ProductData,data=request.get_json(),)create_product(product_data=product_data)returnResponse(status=201)What if we want to validate our data (e.g. check ifcodehas 6 characters)?
Such features are out of scope oftonalitebut we can easily combine it with
one of data validation library. Let's try withmarshmallow.First of all we have to define our data validation schemas:frommarshmallowimportSchema,fields,ValidationErrordefvalidate_code(code):iflen(code)!=6:raiseValidationError('Code must have 6 characters.')classProductVariantDataSchema(Schema):code=fields.Str(required=True,validate=validate_code)description=fields.Str(required=False)stock=fields.Int(required=False)classProductDataSchema(Schema):name=fields.Str(required=True)price=fields.Decimal(required=True)variants=fields.Nested(ProductVariantDataSchema(many=True))And use them within our endpoint:@app.route("/products",methods=['POST'])defproducts():schema=ProductDataSchema()result,errors=schema.load(request.get_json())iferrors:returnResponse(response=json.dumps(errors),status=400,mimetype='application/json',)product_data=tonalite.from_dict(data_class=ProductData,data=result,)create_product(product_data=product_data)returnResponse(status=201)Stilltonalitehelps us to create data class from "raw" dict with validated data.ChangelogFollowtonaliteupdates inCHANGELOG.AuthorsOriginally created by [Konrad Hałas][https://konradhalas.pl].