Coverage for mongodb_odm / utils / utils.py: 98%

60 statements  

« prev     ^ index     » next       coverage.py v7.12.0, created at 2025-12-07 08:37 +0000

1import re 1fabcde

2import types 1fabcde

3from typing import Any, Optional, Union 1fabcde

4 

5from mongodb_odm.fields import RelationshipInfo 1fabcde

6from mongodb_odm.utils._internal_models import RelationalFieldInfo 1fabcde

7from pydantic import BaseModel 1fabcde

8from typing_extensions import get_args, get_origin 1fabcde

9 

10UnionType = getattr(types, "UnionType", Union) 1fabcde

11NoneType = type(None) 1fabcde

12pattern = re.compile(r"(?<!^)(?=[A-Z])") 1fabcde

13 

14 

15def camel_to_snake(string: str) -> str: 1fabcde

16 return pattern.sub("_", string).lower() 1fabcde

17 

18 

19def get_database_name(model: Any) -> Optional[Any]: 1fabcde

20 """ 

21 Get the database name if the model ODMConfig has a user-defined database name. 

22 

23 model: Document type 

24 """ 

25 if hasattr(model.ODMConfig, "database"): 1fabcde

26 return model.ODMConfig.database 1fabcde

27 

28 return None 1fabcde

29 

30 

31def convert_model_to_collection(model: Any) -> str: 1fabcde

32 """ 

33 Get the collection name from the model. 

34 Users could define collection names in model ODMConfig. 

35 Otherwise, we will convert the model name to a snake case collection name. 

36 

37 The user defines collection_name as a higher priority here. 

38 

39 model: Document type 

40 """ 

41 if ( 1fab

42 hasattr(model.ODMConfig, "collection_name") 

43 and model.ODMConfig.collection_name is not None 

44 ): 

45 """By default model has ODMConfig in BaseModel""" 1abcde

46 return str(model.ODMConfig.collection_name) 1fabcde

47 else: 

48 return camel_to_snake(model.__name__) 1fabcde

49 

50 

51def _is_union_type(t: Any) -> bool: 1fabcde

52 return t is UnionType or t is Union 1fabcde

53 

54 

55def get_type_from_field(field: Any) -> Any: 1fabcde

56 type_: Any = field.annotation 1fabcde

57 

58 # Resolve Optional fields 

59 if type_ is None: 1fabcde

60 raise ValueError("Missing field type") 1fabcde

61 

62 origin = get_origin(type_) 1fabcde

63 

64 if origin is None: 1fabcde

65 return type_ 1fabcde

66 

67 if _is_union_type(origin): 1fabcde

68 bases = get_args(type_) 1fabcde

69 

70 if len(bases) > 2: 1fabcde

71 raise ValueError("Cannot have a (non-optional) union as a ODM field") 1fabcde

72 # Non optional unions are not allowed 

73 if bases[0] is not NoneType and bases[1] is not NoneType: 1fabcde

74 raise ValueError("Cannot have a (non-optional) union as a ODM field") 1fabcde

75 

76 # Optional unions are allowed 

77 return bases[0] if bases[0] is not NoneType else bases[1] 1fabcde

78 elif origin is list: 1fabcde

79 inner_type_ = get_args(type_)[0] 1fabcde

80 return inner_type_ 1fabcde

81 

82 return origin 

83 

84 

85def get_model_fields(cls: type[BaseModel]) -> dict[str, Any]: 1fabcde

86 return cls.__pydantic_fields__ 1fabcde

87 

88 

89def _get_fields_info( 1fabcde

90 cls: type[BaseModel], fields: list[str] 

91) -> dict[str, RelationalFieldInfo]: 

92 """ 

93 Extract related field information for a model for specific fields. 

94 """ 

95 field_data: dict[str, RelationalFieldInfo] = {} 1fabcde

96 for field in fields: 1fabcde

97 field_type_obj = get_model_fields(cls)[field] 1fabcde

98 if field_type_obj.default.local_field not in get_model_fields(cls): 1fabcde

99 # Check Relationship local_field exists in the model 

100 raise Exception( 1fabcde

101 f'Invalid field "{field_type_obj.default.local_field}" in Relationship' 

102 ) 

103 """ 1abcde

104 Example relationship 

105 class User(Document): 

106 ... 

107 

108 class Course(Document): 

109 ... 

110 author_id: ODMObjectId = Field(...) 

111 author: Optional[User] = Relationship(local_field="author_id") 

112 """ 

113 field_type = get_type_from_field(field_type_obj) 1fabcde

114 field_data[field] = RelationalFieldInfo( 1fabcde

115 model=field_type, # User 

116 local_field=field_type_obj.default.local_field, # author_id 

117 related_field=field_type_obj.default.related_field, # author 

118 ) 

119 

120 return field_data 1fabcde

121 

122 

123def get_relationship_fields_info( 1fabcde

124 cls: type[BaseModel], 

125) -> dict[str, RelationalFieldInfo]: 

126 """ 

127 This function will return all of the related fields with their information. 

128 

129 cls: Document 

130 """ 

131 fields_name = [] 1fabcde

132 for field_name, field_info in get_model_fields(cls).items(): 1fabcde

133 """Get all fields that are related to a specific model.""" 1abcde

134 if type(field_info.default) is RelationshipInfo: 1fabcde

135 fields_name.append(field_name) 1fabcde

136 

137 return _get_fields_info(cls, fields_name) 1fabcde