|
1 | 1 | from enum import Enum, auto
|
2 |
| -from typing import Union |
| 2 | +from typing import Dict, List, Union |
3 | 3 |
|
4 | 4 | from labelbox import utils
|
5 | 5 | from labelbox.exceptions import InvalidAttributeError
|
@@ -239,11 +239,74 @@ class EntityMeta(type):
|
239 | 239 | of the Entity class object so they can be referenced for example like:
|
240 | 240 | Entity.Project.
|
241 | 241 | """
|
| 242 | + # Maps Entity name to Relationships for all currently defined Entities |
| 243 | + relationship_mappings: Dict[str, List[Relationship]] = {} |
242 | 244 |
|
243 | 245 | def __init__(cls, clsname, superclasses, attributedict):
|
244 | 246 | super().__init__(clsname, superclasses, attributedict)
|
| 247 | + cls.validate_cached_relationships() |
245 | 248 | if clsname != "Entity":
|
246 | 249 | setattr(Entity, clsname, cls)
|
| 250 | + EntityMeta.relationship_mappings[utils.snake_case( |
| 251 | + cls.__name__)] = cls.relationships() |
| 252 | + |
| 253 | + @staticmethod |
| 254 | + def raise_for_nested_cache(first: str, middle: str, last: List[str]): |
| 255 | + raise TypeError( |
| 256 | + "Cannot cache a relationship to an Entity with its own cached relationship(s). " |
| 257 | + f"`{first}` caches `{middle}` which caches `{last}`") |
| 258 | + |
| 259 | + @staticmethod |
| 260 | + def cached_entities(entity_name: str): |
| 261 | + """ |
| 262 | + Return all cached entites for a given Entity name |
| 263 | + """ |
| 264 | + cached_entities = EntityMeta.relationship_mappings.get(entity_name, []) |
| 265 | + return { |
| 266 | + entity.name: entity for entity in cached_entities if entity.cache |
| 267 | + } |
| 268 | + |
| 269 | + def validate_cached_relationships(cls): |
| 270 | + """ |
| 271 | + Graphql doesn't allow for infinite nesting in queries. |
| 272 | + This function checks that cached relationships result in valid queries. |
| 273 | + * It does this by making sure that a cached relationship do not |
| 274 | + reference any entity with its own cached relationships. |
| 275 | +
|
| 276 | + This check is performed by looking to see if this entity caches |
| 277 | + any entities that have their own cached fields. If this entity |
| 278 | + that we are checking has any cached fields then we also check |
| 279 | + all currently defined entities to see if they cache this entity. |
| 280 | +
|
| 281 | + A two way check is necessary because checks are performed as classes are being defined. |
| 282 | + As opposed to after all objects have been created. |
| 283 | + """ |
| 284 | + # All cached relationships |
| 285 | + cached_rels = [r for r in cls.relationships() if r.cache] |
| 286 | + |
| 287 | + # Check if any cached entities have their own cached fields |
| 288 | + for rel in cached_rels: |
| 289 | + nested = cls.cached_entities(rel.name) |
| 290 | + if nested: |
| 291 | + cls.raise_for_nested_cache(utils.snake_case(cls.__name__), |
| 292 | + rel.name, list(nested.keys())) |
| 293 | + |
| 294 | + # If the current Entity (cls) has any cached relationships (cached_rels) |
| 295 | + # then no other defined Entity (entities in EntityMeta.relationship_mappings) can cache this Entity. |
| 296 | + if cached_rels: |
| 297 | + # For all currently defined Entities |
| 298 | + for entity_name in EntityMeta.relationship_mappings: |
| 299 | + # Get all cached ToOne relationships |
| 300 | + rels = cls.cached_entities(entity_name) |
| 301 | + # Check if the current Entity (cls) is referenced by the Entity with `entity_name` |
| 302 | + rel = rels.get(utils.snake_case(cls.__name__)) |
| 303 | + # If rel exists and is cached then raise an exception |
| 304 | + # This means `entity_name` caches `cls` which cached items in `cached_rels` |
| 305 | + if rel and rel.cache: |
| 306 | + cls.raise_for_nested_cache( |
| 307 | + utils.snake_case(entity_name), |
| 308 | + utils.snake_case(cls.__name__), |
| 309 | + [entity.name for entity in cached_rels]) |
247 | 310 |
|
248 | 311 |
|
249 | 312 | class Entity(metaclass=EntityMeta):
|
|
0 commit comments