|
1 | 1 | """This module is a CRUD interface between resource managers and the sqlalchemy ORM""" |
2 | 2 | import logging |
3 | | -from typing import TYPE_CHECKING, Any, Iterable, List, Optional, Tuple, Type |
| 3 | +from typing import TYPE_CHECKING, Any, Iterable, List, Optional, Tuple, Type, Union |
4 | 4 |
|
5 | 5 | from sqlalchemy import delete, func, select |
6 | 6 | from sqlalchemy.exc import DBAPIError, IntegrityError, MissingGreenlet, NoResultFound |
|
44 | 44 |
|
45 | 45 | log = logging.getLogger(__name__) |
46 | 46 |
|
| 47 | +ModelTypeOneOrMany = Union[TypeModel, list[TypeModel]] |
| 48 | + |
47 | 49 |
|
48 | 50 | class SqlalchemyDataLayer(BaseDataLayer): |
49 | 51 | """Sqlalchemy data layer""" |
@@ -134,9 +136,77 @@ def prepare_id_value(self, col: InstrumentedAttribute, value: Any) -> Any: |
134 | 136 |
|
135 | 137 | return value |
136 | 138 |
|
| 139 | + async def link_relationship_object( |
| 140 | + self, |
| 141 | + obj: TypeModel, |
| 142 | + relation_name: str, |
| 143 | + related_data: Optional[ModelTypeOneOrMany], |
| 144 | + ): |
| 145 | + """ |
| 146 | + Links target object with relationship object or objects |
| 147 | +
|
| 148 | + :param obj: |
| 149 | + :param relation_name: |
| 150 | + :param related_data: |
| 151 | + """ |
| 152 | + # todo: relation name may be different? |
| 153 | + setattr(obj, relation_name, related_data) |
| 154 | + |
| 155 | + async def check_object_has_relationship_or_raise(self, obj: TypeModel, relation_name: str): |
| 156 | + """ |
| 157 | + Checks that there is relationship with relation_name in obj |
| 158 | +
|
| 159 | + :param obj: |
| 160 | + :param relation_name: |
| 161 | + """ |
| 162 | + try: |
| 163 | + hasattr(obj, relation_name) |
| 164 | + except MissingGreenlet: |
| 165 | + raise InternalServerError( |
| 166 | + detail=( |
| 167 | + f"Error of loading the {relation_name!r} relationship. " |
| 168 | + f"Please add this relationship to include query parameter explicitly." |
| 169 | + ), |
| 170 | + parameter="include", |
| 171 | + ) |
| 172 | + |
| 173 | + async def get_related_data_to_link( |
| 174 | + self, |
| 175 | + related_model: TypeModel, |
| 176 | + relationship_info: RelationshipInfo, |
| 177 | + relationship_in: Union[ |
| 178 | + BaseJSONAPIRelationshipDataToOneSchema, |
| 179 | + BaseJSONAPIRelationshipDataToManySchema, |
| 180 | + ], |
| 181 | + ) -> Optional[ModelTypeOneOrMany]: |
| 182 | + """ |
| 183 | + Retrieves object or objects to link from database |
| 184 | +
|
| 185 | + :param related_model: |
| 186 | + :param relationship_info: |
| 187 | + :param relationship_in: |
| 188 | + """ |
| 189 | + if not relationship_in.data: |
| 190 | + return {True: [], False: None}[relationship_info.many] |
| 191 | + |
| 192 | + if relationship_info.many: |
| 193 | + assert isinstance(relationship_in, BaseJSONAPIRelationshipDataToManySchema) |
| 194 | + return await self.get_related_objects_list( |
| 195 | + related_model=related_model, |
| 196 | + related_id_field=relationship_info.id_field_name, |
| 197 | + ids=[r.id for r in relationship_in.data], |
| 198 | + ) |
| 199 | + |
| 200 | + assert isinstance(relationship_in, BaseJSONAPIRelationshipDataToOneSchema) |
| 201 | + return await self.get_related_object( |
| 202 | + related_model=related_model, |
| 203 | + related_id_field=relationship_info.id_field_name, |
| 204 | + id_value=relationship_in.data.id, |
| 205 | + ) |
| 206 | + |
137 | 207 | async def apply_relationships(self, obj: TypeModel, data_create: BaseJSONAPIItemInSchema) -> None: |
138 | 208 | """ |
139 | | - TODO: move generic code to another method |
| 209 | + Handles relationships passed in request |
140 | 210 |
|
141 | 211 | :param obj: |
142 | 212 | :param data_create: |
@@ -167,45 +237,15 @@ async def apply_relationships(self, obj: TypeModel, data_create: BaseJSONAPIItem |
167 | 237 | continue |
168 | 238 |
|
169 | 239 | relationship_info: RelationshipInfo = field.field_info.extra["relationship"] |
170 | | - |
171 | | - # ... |
172 | 240 | related_model = get_related_model_cls(type(obj), relation_name) |
| 241 | + related_data = await self.get_related_data_to_link( |
| 242 | + related_model=related_model, |
| 243 | + relationship_info=relationship_info, |
| 244 | + relationship_in=relationship_in, |
| 245 | + ) |
173 | 246 |
|
174 | | - if relationship_info.many: |
175 | | - assert isinstance(relationship_in, BaseJSONAPIRelationshipDataToManySchema) |
176 | | - |
177 | | - related_data = [] |
178 | | - if relationship_in.data: |
179 | | - related_data = await self.get_related_objects_list( |
180 | | - related_model=related_model, |
181 | | - related_id_field=relationship_info.id_field_name, |
182 | | - ids=[r.id for r in relationship_in.data], |
183 | | - ) |
184 | | - else: |
185 | | - assert isinstance(relationship_in, BaseJSONAPIRelationshipDataToOneSchema) |
186 | | - |
187 | | - if relationship_in.data: |
188 | | - related_data = await self.get_related_object( |
189 | | - related_model=related_model, |
190 | | - related_id_field=relationship_info.id_field_name, |
191 | | - id_value=relationship_in.data.id, |
192 | | - ) |
193 | | - else: |
194 | | - setattr(obj, relation_name, None) |
195 | | - continue |
196 | | - try: |
197 | | - hasattr(obj, relation_name) |
198 | | - except MissingGreenlet: |
199 | | - raise InternalServerError( |
200 | | - detail=( |
201 | | - f"Error of loading the {relation_name!r} relationship. " |
202 | | - f"Please add this relationship to include query parameter explicitly." |
203 | | - ), |
204 | | - parameter="include", |
205 | | - ) |
206 | | - |
207 | | - # todo: relation name may be different? |
208 | | - setattr(obj, relation_name, related_data) |
| 247 | + await self.check_object_has_relationship_or_raise(obj, relation_name) |
| 248 | + await self.link_relationship_object(obj, relation_name, related_data) |
209 | 249 |
|
210 | 250 | async def create_object(self, data_create: BaseJSONAPIItemInSchema, view_kwargs: dict) -> TypeModel: |
211 | 251 | """ |
|
0 commit comments