from django.utils.functional import cached_property from .query import SIGsAPIModelManager, SIGsAPIRelationModelManager import inspect MULTIPLE_OBJECTS_RETURNED = "MultipleObjectsReturned" DOES_NOT_EXIST = "DoesNotExist" class ObjectDoesNotExist(Exception): """Objeto não encontrad""" pass class MultipleObjectsReturned(Exception): """Mais de um objeto foi encontrado onde só um era esperado""" pass class SIGsAPIOptions: """ Essa classe será usada como atributo _meta dos SIGsAPIModels """ def __init__(self, model, servico = None, recurso = None, path = None, api_query_fields_list = [], pk_fields = []): self.model = model self.servico = servico self.recurso = recurso self.path = path self.fields = {} self.name = model.__class__.__name__ self.api_query_fields_list = api_query_fields_list self.pk_fields = pk_fields or [] def set_attribute(self, attribute_name, attribute): self.fields[attribute_name] = attribute def get_attribute(self, attribute_name): return self.fields[attribute_name] def get_field(self, field_name): return self.get_attribute(field_name) def _has_contribute_to_class(value): # Only call contribute_to_class() if it's bound. return not inspect.isclass(value) and hasattr(value, "contribute_to_sigs_api_class") class SIGsAPIModelBase(type): """ Metaclass dos SIGsModel Essa metaclasse foi criada para realizar algumas operações no momento da criação das classes que herdam de SIGsModel. Basicamente, os objetivos são: a) Inserir o Manager (.objects) b) Transformar os atributos do tipo SIGsAPIField em propriedades calculadas Args: type (_type_): _description_ """ def __new__(cls, name, bases, attrs, **kwargs): """ Método que cria uma nova classe dessa metaclasse Por exemplo, se uma classe for definida assim: ``` class TheClass(MyParentClass, metaclass = SIGsAPIModelBase) my_attr = 32 def a_method(self): return my_attr + 5 ``` No momento em que essa classe estiver sendo criada, esse método será executado e poderá alterar o nome, as bases, os atributos e métodos dessa classe. Assim, antes de criarmos a classe, vamos alterar seus atributos para alcançarmos os dois objetivos acima definidos. Código fortemente inspirado em django.db.models.base.ModelBase ! Args: name (_type_): Nome da nova classe (no exemplo, "TheClass") bases (_type_): Classes das quais ela herda (no exemplo, [MyParentClass,]) attrs (_type_): Os atributos da classe, no exemplo, [my_attr, a_method] Returns: _type_: A nova classe! """ # Salva o metodo new da metaclasse default do pyton, para chamálo depois super_new = super().__new__ # esse código limita a execução desse código para classes onde ao menos um dos # parents é uma instância de SIGsAPIModelBase! parents = [b for b in bases if isinstance(b, SIGsAPIModelBase)] if not parents: return super_new(cls, name, bases, attrs) # Removemos alguns atributos especiais da classe e colocamos no array new_attrs # Os atributos especiais são ___module___ e __classcell__ module = attrs.pop("__module__") new_attrs = {"__module__": module} # esse dicionário guarda os novos atributos classcell = attrs.pop("__classcell__", None) if classcell is not None: new_attrs["__classcell__"] = classcell # Retiramos, se houver, o atributo Meta, porque vamos precisar modificá-lo attr_meta = attrs.pop("Meta", None) # Agora vamos separar os demais atributos. Aqueles que tiverem a propriedade is_sigs_api_field # serão substituídos, os demais incluídos. fields_attrs = {} # Esse dicionário guarda os atributos especiais for obj_name, obj in attrs.items(): if _has_contribute_to_class(obj): # esse método verifica se é um atributo especial fields_attrs[obj_name] = obj else: new_attrs[obj_name] = obj # Aqui chamamos o método new da metaclasse default do python new_class = super_new(cls, name, bases, new_attrs) # ESSE AQUI FOI O CÓDIGO QUE EU NÃO COPIEI DO DJANGO # Aqui vamos realizar o segundo objetivo options = { 'model': new_class, 'servico': attr_meta.servico, 'recurso': attr_meta.recurso, } if hasattr(attr_meta, "path"): options['path'] = attr_meta.path if hasattr(attr_meta, "pk_fields"): options['pk_fields'] = attr_meta.pk_fields if hasattr(attr_meta, "api_query_fields_list"): options['api_query_fields_list'] = attr_meta.api_query_fields_list _meta = SIGsAPIOptions(**options ) setattr(new_class, '_meta', _meta) # Aqui vamos realizar o primeiro objetivo, que adicionar o objeto "objects" à objects = SIGsAPIModelManager(the_class = new_class) setattr(new_class, 'objects', objects) # ADicionando as classes de erro DoesNotExist e MultipleObjectsReturned setattr(new_class, DOES_NOT_EXIST, type( DOES_NOT_EXIST, ( ObjectDoesNotExist, ), { '__module__': module, "__qualname__": "{base}.{additive}".format(base = new_class.__qualname__, additive = DOES_NOT_EXIST), } )) setattr(new_class, MULTIPLE_OBJECTS_RETURNED, type( MULTIPLE_OBJECTS_RETURNED, ( MultipleObjectsReturned, ), { '__module__': module, "__qualname__": "{base}.{additive}".format(base = new_class.__qualname__, additive = MULTIPLE_OBJECTS_RETURNED), } )) # Aqui adicionamos os atributos especiais à classe # to the class. for field_name, field in fields_attrs.items(): field.contribute_to_sigs_api_class(new_class, field_name) # Por fim, retornamos a nova classe criada. return new_class class SIGsAPIModel(metaclass = SIGsAPIModelBase): def __init__(self, object, *args, **kwargs): self._object = object def serializable_value(self, field_name): """ Return the value of the field name for this instance. If the field is a foreign key, return the id value instead of the object. If there's no Field object with this name on the model, return the model attribute's value. Used to serialize a field's value (in the serializer, or form output, for example). Normally, you would just access the attribute directly and not use this method. """ try: field = self._meta.get_field(field_name) except: return getattr(self, field_name) return getattr(self, field.property_name) class SIGsAPIModelDetalhe(metaclass = SIGsAPIModelBase): def __init__(self, object, *args, **kwargs): self._object = object