[docs]classPydanticMeta:""" The ``PydanticMeta`` class is used to configure metadata for generating the pydantic Model. Usage: .. code-block:: python3 class Foo(Model): ... class PydanticMeta: exclude = ("foo", "baa") computed = ("count_peanuts", ) """#: If not empty, only fields this property contains will be in the pydantic modelinclude:Tuple[str,...]=()#: Fields listed in this property will be excluded from pydantic modelexclude:Tuple[str,...]=("Meta",)#: Computed fields can be listed here to use in pydantic modelcomputed:Tuple[str,...]=()#: Use backward relations without annotations - not recommended, it can be huge data#: without controlbackward_relations:bool=True#: Maximum recursion level allowedmax_recursion:int=3#: Allow cycles in recursion - This can result in HUGE data - Be careful!#: Please use this with ``exclude``/``include`` and sane ``max_recursion``allow_cycles:bool=False#: If we should exclude raw fields (the ones have _id suffixes) of relationsexclude_raw_fields:bool=True#: Sort fields alphabetically.#: If not set (or ``False``) then leave fields in declaration ordersort_alphabetically:bool=False#: Allows user to specify custom config for generated modelmodel_config:Optional[ConfigDict]=None
def_br_it(val:str)->str:returnval.replace("\n","<br/>").strip()def_cleandoc(obj:Any)->str:return_br_it(inspect.cleandoc(obj.__doc__or""))def_pydantic_recursion_protector(cls:"Type[Model]",*,stack:tuple,exclude:Tuple[str,...]=(),include:Tuple[str,...]=(),computed:Tuple[str,...]=(),name=None,allow_cycles:bool=False,sort_alphabetically:Optional[bool]=None,)->Optional[Type[PydanticModel]]:""" It is an inner function to protect pydantic model creator against cyclic recursion """ifnotallow_cyclesandclsin(c[0]forcinstack[:-1]):returnNonecaller_fname=stack[0][1]prop_path=[caller_fname]# It stores the fields in the hierarchylevel=1for_,parent_fname,parent_max_recursioninstack[1:]:# Check recursion levelprop_path.insert(0,parent_fname)iflevel>=parent_max_recursion:# This is too verbose, Do we even need a way of reporting truncated models?# tortoise.logger.warning(# "Recursion level %i has reached for model %s",# level,# parent_cls.__qualname__ + "." + ".".join(prop_path),# )returnNonelevel+=1returnpydantic_model_creator(cls,exclude=exclude,include=include,computed=computed,name=name,_stack=stack,allow_cycles=allow_cycles,sort_alphabetically=sort_alphabetically,)
[docs]defpydantic_model_creator(cls:"Type[Model]",*,name=None,exclude:Tuple[str,...]=(),include:Tuple[str,...]=(),computed:Tuple[str,...]=(),optional:Tuple[str,...]=(),allow_cycles:Optional[bool]=None,sort_alphabetically:Optional[bool]=None,_stack:tuple=(),exclude_readonly:bool=False,meta_override:Optional[Type]=None,model_config:Optional[ConfigDict]=None,validators:Optional[Dict[str,Any]]=None,module:str=__name__,)->Type[PydanticModel]:""" Function to build `Pydantic Model <https://pydantic-docs.helpmanual.io/usage/models/>`__ off Tortoise Model. :param _stack: Internal parameter to track recursion :param cls: The Tortoise Model :param name: Specify a custom name explicitly, instead of a generated name. :param exclude: Extra fields to exclude from the provided model. :param include: Extra fields to include from the provided model. :param computed: Extra computed fields to include from the provided model. :param optional: Extra optional fields for the provided model. :param allow_cycles: Do we allow any cycles in the generated model? This is only useful for recursive/self-referential models. A value of ``False`` (the default) will prevent any and all backtracking. :param sort_alphabetically: Sort the parameters alphabetically instead of Field-definition order. The default order would be: * Field definition order + * order of reverse relations (as discovered) + * order of computed functions (as provided). :param exclude_readonly: Build a subset model that excludes any readonly fields :param meta_override: A PydanticMeta class to override model's values. :param model_config: A custom config to use as pydantic config. :param validators: A dictionary of methods that validate fields. :param module: The name of the module that the model belongs to. Note: Created pydantic model uses config_class parameter and PydanticMeta's config_class as its Config class's bases(Only if provided!), but it ignores ``fields`` config. pydantic_model_creator will generate fields by include/exclude/computed parameters automatically. """# Fully qualified class namefqname=cls.__module__+"."+cls.__qualname__postfix=""defget_name()->str:# If arguments are specified (different from the defaults), we append a hash to the# class name, to make it unique# We don't check by stack, as cycles get explicitly renamed.# When called later, include is explicitly set, so fence passes.nonlocalpostfixis_default=(exclude==()andinclude==()andcomputed==()andsort_alphabeticallyisNoneandallow_cyclesisNoneandnotexclude_readonly)hashval=f"{fqname};{exclude};{include};{computed};{_stack}:{sort_alphabetically}:{allow_cycles}:{exclude_readonly}"postfix=(":"+b32encode(sha3_224(hashval.encode("utf-8")).digest()).decode("utf-8").lower()[:6]ifnotis_defaultelse"")returnfqname+postfix# We need separate model class for different exclude, include and computed parameters_name=nameorget_name()has_submodel=False# Get settings and defaultsmeta=getattr(cls,"PydanticMeta",PydanticMeta)defget_param(attr:str)->Any:ifmeta_override:returngetattr(meta_override,attr,getattr(meta,attr,getattr(PydanticMeta,attr)))returngetattr(meta,attr,getattr(PydanticMeta,attr))default_include:Tuple[str,...]=tuple(get_param("include"))default_exclude:Tuple[str,...]=tuple(get_param("exclude"))default_computed:Tuple[str,...]=tuple(get_param("computed"))default_config:Optional[ConfigDict]=get_param("model_config")backward_relations:bool=bool(get_param("backward_relations"))max_recursion:int=int(get_param("max_recursion"))exclude_raw_fields:bool=bool(get_param("exclude_raw_fields"))_sort_fields:bool=(bool(get_param("sort_alphabetically"))ifsort_alphabeticallyisNoneelsesort_alphabetically)_allow_cycles:bool=bool(get_param("allow_cycles")ifallow_cyclesisNoneelseallow_cycles)# Update parameters with defaultsinclude=tuple(include)+default_includeexclude=tuple(exclude)+default_excludecomputed=tuple(computed)+default_computedannotations=get_annotations(cls)pconfig=PydanticModel.model_config.copy()ifdefault_config:pconfig.update(default_config)ifmodel_config:pconfig.update(model_config)if"title"notinpconfig:pconfig["title"]=nameorcls.__name__if"extra"notinpconfig:pconfig["extra"]="forbid"properties:Dict[str,Any]={}# Get model descriptionmodel_description=cls.describe(serializable=False)# Field map we usefield_map:Dict[str,dict]={}pk_raw_field:str=""deffield_map_update(keys:tuple,is_relation=True)->None:nonlocalpk_raw_fieldforkeyinkeys:fds=model_description[key]ifisinstance(fds,dict):fds=[fds]forfdinfds:n=fd["name"]ifkey=="pk_field":pk_raw_field=n# Include or exclude fieldif(includeandnnotininclude)orninexclude:continue# Remove raw fieldsraw_field=fd.get("raw_field",None)ifraw_fieldisnotNoneandexclude_raw_fieldsandraw_field!=pk_raw_field:field_map.pop(raw_field,None)field_map[n]=fd# Update field definitions from descriptionifnotexclude_readonly:field_map_update(("pk_field",),is_relation=False)field_map_update(("data_fields",),is_relation=False)ifnotexclude_readonly:included_fields:tuple=("fk_fields","o2o_fields","m2m_fields",)ifbackward_relations:included_fields=(*included_fields,"backward_fk_fields","backward_o2o_fields",)field_map_update(included_fields)# Add possible computed fieldsfield_map.update({k:{"field_type":callable,"function":getattr(cls,k),"description":None,}forkincomputed})# Sort field map (Python 3.7+ has guaranteed ordered dictionary keys)if_sort_fields:# Sort Alphabeticallyfield_map={k:field_map[k]forkinsorted(field_map)}else:# Sort to definition orderfield_map={k:field_map[k]forkintuple(cls._meta.fields_map.keys())+computedifkinfield_map}# Process fieldsforfname,fdescinfield_map.items():comment=""json_schema_extra:Dict[str,Any]={}fconfig:Dict[str,Any]={"json_schema_extra":json_schema_extra,}field_type=fdesc["field_type"]field_default=fdesc.get("default")is_optional_field=fnameinoptionaldefget_submodel(_model:"Type[Model]")->Optional[Type[PydanticModel]]:"""Get Pydantic model for the submodel"""nonlocalexclude,_name,has_submodelif_model:new_stack=_stack+((cls,fname,max_recursion),)# Get pydantic schema for the submodelprefix_len=len(fname)+1pmodel=_pydantic_recursion_protector(_model,exclude=tuple(str(v[prefix_len:])forvinexcludeifv.startswith(fname+".")),include=tuple(str(v[prefix_len:])forvinincludeifv.startswith(fname+".")),computed=tuple(str(v[prefix_len:])forvincomputedifv.startswith(fname+".")),stack=new_stack,allow_cycles=_allow_cycles,sort_alphabetically=sort_alphabetically,)else:pmodel=None# If the result is None it has been excluded and we need to exclude the fieldifpmodelisNone:exclude+=(fname,)else:has_submodel=True# We need to rename if there are duplicate instances of this modelifclsin(c[0]forcin_stack):_name=nameorget_name()returnpmodel# Foreign keys and OneToOne fields are embedded schemasis_to_one_relation=Falseif(field_typeisrelational.ForeignKeyFieldInstanceorfield_typeisrelational.OneToOneFieldInstanceorfield_typeisrelational.BackwardOneToOneRelation):is_to_one_relation=Truemodel=get_submodel(fdesc["python_type"])ifmodel:iffdesc.get("nullable"):json_schema_extra["nullable"]=Trueiffdesc.get("nullable")orfield_defaultisnotNone:model=Optional[model]# type: ignoreproperties[fname]=model# Backward FK and ManyToMany fields are list of embedded schemaselif(field_typeisrelational.BackwardFKRelationorfield_typeisrelational.ManyToManyFieldInstance):model=get_submodel(fdesc["python_type"])ifmodel:properties[fname]=List[model]# type: ignore# Computed fields as methodseliffield_typeiscallable:func=fdesc["function"]annotation=get_annotations(cls,func).get("return",None)comment=_cleandoc(func)ifannotationisnotNone:properties[fname]=computed_field(return_type=annotation,description=comment)(func)# Json fieldseliffield_typeisJSONField:properties[fname]=Any# Any other tortoise fieldselse:annotation=annotations.get(fname,None)if"readOnly"infdesc["constraints"]:json_schema_extra["readOnly"]=fdesc["constraints"]["readOnly"]delfdesc["constraints"]["readOnly"]fconfig.update(fdesc["constraints"])ptype=fdesc["python_type"]iffdesc.get("nullable"):json_schema_extra["nullable"]=Trueifis_optional_fieldorfield_defaultisnotNoneorfdesc.get("nullable"):ptype=Optional[ptype]ifnot(exclude_readonlyandjson_schema_extra.get("readOnly")isTrue):properties[fname]=annotationorptypeiffnameinpropertiesandnotisinstance(properties[fname],tuple):fconfig["title"]=fname.replace("_"," ").title()description=commentor_br_it(fdesc.get("docstring")orfdesc["description"]or"")ifdescription:fconfig["description"]=descriptionftype=properties[fname]ifisinstance(ftype,PydanticDescriptorProxy):continueifis_optional_fieldor(field_defaultisnotNoneandnotcallable(field_default)):properties[fname]=(ftype,Field(default=field_default,**fconfig))else:if(j:=fconfig.get("json_schema_extra"))and((j.get("nullable")andnotis_to_one_relationandfield_typenotin(IntField,TextField))or(exclude_readonlyandj.get("readOnly"))):fconfig["default_factory"]=lambda:Noneproperties[fname]=(ftype,Field(**fconfig))# Here we endure that the name is unique, but complete objects are still labeled verbatimifnothas_submodeland_stack:_name=nameorf"{fqname}.leaf"else:_name=nameorget_name()# Here we de-dup to ensure that a uniquely named object is a unique object# This fixes some Pydantic constraints.if_namein_MODEL_INDEX:return_MODEL_INDEX[_name]# Creating Pydantic class for the properties generated beforeproperties["model_config"]=pconfigmodel=create_model(_name,__base__=PydanticModel,__module__=module,__validators__=validators,**properties,)# Copy the Model docstring overmodel.__doc__=_cleandoc(cls)# Store the base classmodel.model_config["orig_model"]=cls# type: ignore# Store model reference so we can de-dup it later on if needed._MODEL_INDEX[_name]=modelreturnmodel
[docs]defpydantic_queryset_creator(cls:"Type[Model]",*,name=None,exclude:Tuple[str,...]=(),include:Tuple[str,...]=(),computed:Tuple[str,...]=(),allow_cycles:Optional[bool]=None,sort_alphabetically:Optional[bool]=None,)->Type[PydanticListModel]:""" Function to build a `Pydantic Model <https://pydantic-docs.helpmanual.io/usage/models/>`__ list off Tortoise Model. :param cls: The Tortoise Model to put in a list. :param name: Specify a custom name explicitly, instead of a generated name. The list generated name is currently naive and merely adds a "s" to the end of the singular name. :param exclude: Extra fields to exclude from the provided model. :param include: Extra fields to include from the provided model. :param computed: Extra computed fields to include from the provided model. :param allow_cycles: Do we allow any cycles in the generated model? This is only useful for recursive/self-referential models. A value of ``False`` (the default) will prevent any and all backtracking. :param sort_alphabetically: Sort the parameters alphabetically instead of Field-definition order. The default order would be: * Field definition order + * order of reverse relations (as discovered) + * order of computed functions (as provided). """submodel=pydantic_model_creator(cls,exclude=exclude,include=include,computed=computed,allow_cycles=allow_cycles,sort_alphabetically=sort_alphabetically,name=name,)lname=nameorf"{submodel.__name__}_list"# Creating Pydantic class for the properties generated beforemodel=create_model(lname,__base__=PydanticListModel,root=(List[submodel],Field(default_factory=list)),# type: ignore)# Copy the Model docstring overmodel.__doc__=_cleandoc(cls)# The title of the model to hide the hash postfixmodel.model_config["title"]=nameorf"{submodel.model_config['title']}_list"model.model_config["submodel"]=submodel# type: ignorereturnmodel