Source code for ferrosoft.apps.dataimport.services.database

import logging
import logging
import re
from abc import ABC, abstractmethod
from dataclasses import dataclass
from typing import Iterable, List, Tuple
from uuid import UUID

from django.db import IntegrityError, models

from ferrosoft.apps.dataimport.services.fixtures import (
    load_converters,
    convert_catalog_files,
)
from ferrosoft.apps.ferrobase.models import Tenant

DATAIMPORT_CONVERTERS = (
    "ferrosoft.apps.dataimport.services.conversion.dataimport_converters"
)


[docs] def import_json(converters: str, catalog: Iterable[str], schema: str) -> list: converters = load_converters(converters) fixtures = convert_catalog_files(catalog, schema, converters) importer = ObjectCreatingFixtureImporter() importer.load_fixtures(fixtures) return fixtures
[docs] class ImportCallback(ABC):
[docs] @abstractmethod def on_object(self, instance): raise NotImplementedError
[docs] @abstractmethod def on_finish(self): return
[docs] class FixtureImporter(ABC):
[docs] def load_fixtures(self, fixtures: Iterable[dict], tenant: Tenant | None = None): raise NotImplementedError
[docs] class ObjectCreatingFixtureImporter(FixtureImporter): """ Importer creating model instances from fixtures. This is trimmed down version of Django management command "loaddata". """
[docs] @dataclass class M2MRelations: field_name: str related_ids: List[UUID]
_ID_PATTERN = r"^[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}$" def __init__(self, callback: ImportCallback | None = None): self.id_pattern = re.compile(self._ID_PATTERN) self._callback = callback
[docs] def load_fixtures(self, fixtures: Iterable[dict], tenant: Tenant | None = None): from django.apps import apps for fixture in fixtures: model_type = apps.get_model(fixture["model"]) fields, m2m = self._transform_fields(fixture["fields"], model_type) model_id = fixture["pk"] existing_model = model_type.objects.filter(pk=model_id) if existing_model: logging.debug("Updating %s %s", model_type, model_id) existing_model.update(**fields) else: logging.debug("Inserting %s %s", model_type, model_id) instance = model_type(id=model_id, **fields) try: instance.save() # Add any M2M relations after saving. if m2m is not None: m2m_set = getattr(instance, m2m.field_name) for ref in m2m.related_ids: m2m_set.add(ref) if self._callback is not None: self._callback.on_object(instance) except IntegrityError: logging.exception("Integrity error while saving instance") if self._callback is not None: self._callback.on_finish()
def _transform_fields(self, fields: dict, model_type) -> Tuple[dict, M2MRelations]: result = {} m2m = None for key, value in fields.items(): # Skip many to many fields. One has to use the set() method. field = model_type._meta.get_field(key) if isinstance(field, models.ManyToManyField): if isinstance(value, list): m2m = self.M2MRelations(key, [UUID(ref) for ref in value]) continue result_key = None if isinstance(value, str) and not key.endswith("_id"): # If the value matches a UUID, append _id to satisfy Django model constructor for foreign keys. match = self.id_pattern.fullmatch(value) if match is not None: result_key = "%s_id" % key result[result_key or key] = value return result, m2m