示例#1
0
def delete_plant(request: Request,
                 data: PPlantsDeleteRequest,
                 db: Session = Depends(get_db)):
    """tag deleted plant as 'hide' in database"""

    args = data

    record_update: Plant = db.query(Plant).filter_by(
        plant_name=args.plant).first()
    if not record_update:
        logger.error(
            f'Plant to be deleted not found in database: {args.plant}.')
        throw_exception(
            f'Plant to be deleted not found in database: {args.plant}.',
            request=request)
    record_update.hide = True
    db.commit()

    logger.info(message := f'Deleted plant {args.plant}')
    results = {
        'action':
        'Deleted plant',
        'resource':
        'PlantResource',
        'message':
        get_message(message,
                    description=f'Plant name: {args.plant}\nHide: True')
    }

    return results
 def get_plant_by_plant_id(plant_id: int,
                           db: Session,
                           raise_exception: bool = False) -> Plant:
     plant = db.query(Plant).filter(Plant.id == plant_id).first()
     if not plant and raise_exception:
         throw_exception(f'Plant ID not found in database: {plant_id}')
     return plant
示例#3
0
 def get_event_by_event_id(event_id: int,
                           db: Session,
                           raise_exception: bool = False) -> Event:
     event = db.query(Event).filter(Event.id == event_id).first()
     if not event and raise_exception:
         throw_exception(f'Event not found in db: {event_id}')
     return event
示例#4
0
 def get_events_by_plant_id(plant_id: int,
                            db: Session,
                            raise_exception: bool = False) -> List[Event]:
     events = db.query(Event).filter(Event.plant_id == plant_id).all()
     if not events and raise_exception:
         throw_exception(f'No events in db for plant: {plant_id}')
     return events
 def get_taxon_by_taxon_id(taxon_id: int,
                           db: Session,
                           raise_exception: bool = False) -> Taxon:
     taxon = db.query(Taxon).filter(Taxon.id == taxon_id).first()
     if not taxon and raise_exception:
         throw_exception(f'Taxon not found in database: {taxon_id}')
     return taxon
 def get_plant_by_plant_name(plant_name: str,
                             db: Session,
                             raise_exception: bool = False) -> Plant:
     plant = db.query(Plant).filter(Plant.plant_name == plant_name).first()
     if not plant and raise_exception:
         throw_exception(f'Plant not found in database: {plant_name}')
     return plant
 def get_plant_name_by_plant_id(plant_id: int,
                                db: Session,
                                raise_exception: bool = False) -> str:
     plant_name = db.query(
         Plant.plant_name).filter(Plant.id == plant_id).scalar()
     if not plant_name and raise_exception:
         throw_exception(f'Plant not found in database: {plant_id}')
     return plant_name
 def get_plant_id_by_plant_name(plant_name: str,
                                db: Session,
                                raise_exception: bool = False) -> int:
     plant_id = db.query(
         Plant.id).filter(Plant.plant_name == plant_name).scalar()
     if not plant_id and raise_exception:
         throw_exception(f'Plant ID not found in database: {plant_name}')
     return plant_id
示例#9
0
 def get_cat_by_id(category_id: int,
                   db: Session,
                   raise_exception: bool = False) -> PropertyCategory:
     cat = db.query(PropertyCategory).filter(
         PropertyCategory.id == category_id).first()
     if not cat and raise_exception:
         throw_exception(
             f'Property Category not found in database: {category_id}')
     return cat
示例#10
0
 def get_cat_by_name(category_name: str,
                     db: Session,
                     raise_exception: bool = False) -> TraitCategory:
     cat = db.query(TraitCategory).filter(
         TraitCategory.category_name == category_name).first()
     if not cat and raise_exception:
         throw_exception(
             f'Trait Category not found in database: {category_name}')
     return cat
示例#11
0
 def get_by_id(property_value_id: int,
               db: Session,
               raise_exception: bool = False) -> PropertyValue:
     property_obj = db.query(PropertyValue).filter(
         PropertyValue.id == property_value_id).first()
     if not property_obj and raise_exception:
         throw_exception(
             f'No property values found for Property value ID: {property_value_id}'
         )
     return property_obj
示例#12
0
 def get_by_plant_id(plant_id: int,
                     db: Session,
                     raise_exception: bool = False) -> List[PropertyValue]:
     property_obj = db.query(PropertyValue).filter(
         PropertyValue.plant_id == int(plant_id),
         PropertyValue.taxon_id.is_(None)).all()
     if not property_obj and raise_exception:
         throw_exception(
             f'No property values found for Plant ID: {plant_id}')
     return property_obj
示例#13
0
async def upload_images(request: Request, db: Session = Depends(get_db)):
    """upload new image(s)
    todo: switch key in supplied plants list to id"""
    # the ui5 uploader control does somehow not work with the expected form/multipart format expected
    # via fastapi argument files = List[UploadFile] = File(...)
    # therefore, we directly go on the starlette request object
    form = await request.form()
    additional_data = json.loads(form.get('files-data'))
    # noinspection PyTypeChecker
    files: List[UploadFile] = form.getlist('files[]')

    # validate arguments manually as pydantic doesn't trigger here
    try:
        PImageUploadedMetadata(**additional_data)
    except ValidationError as err:
        throw_exception(str(err), request=request)

    # todo: get rid of that key/text/keyword dict
    plants = tuple(
        {
            'key': p,
            'text': p
        } for p in
        additional_data['plants'])  # if 'plants' in additional_data else []
    keywords = tuple({
        'keyword': k,
        'text': k
    } for k in additional_data['keywords'])
    # if 'keywords' in additional_data else []

    # remove duplicates (filename already exists in file system)
    duplicate_filenames = remove_files_already_existing(files, RESIZE_SUFFIX)

    photos: List[Photo] = await _save_image_files(files=files,
                                                  request=request,
                                                  plants=plants,
                                                  keywords=keywords)
    photo_files_ext = _get_pimages_from_photos(photos, db=db)

    msg = get_message(f'Saved {len(files)} images.' +
                      (' Duplicates found.' if duplicate_filenames else ''),
                      message_type=MessageType.WARNING
                      if duplicate_filenames else MessageType.INFORMATION,
                      description=f'Saved: {[p.filename for p in files]}.'
                      f'\nSkipped Duplicates: {duplicate_filenames}.')
    logger.info(msg['message'])
    results = {
        'action': 'Uploaded',
        'resource': 'ImageResource',
        'message': msg,
        'images': photo_files_ext
    }

    return results
示例#14
0
async def _save_image_files(
        files: List[UploadFile],
        request: Request,
        plants: Tuple = (),
        keywords: Tuple = (),
) -> List[Photo]:
    """save the files supplied as starlette uploadfiles on os; assign plants and keywords"""
    photos = []
    for photo_upload in files:
        # save to file system
        path = os.path.join(PATH_ORIGINAL_PHOTOS_UPLOADED,
                            photo_upload.filename)
        logger.info(f'Saving {path}.')

        async with aiofiles.open(path, 'wb') as out_file:
            content = await photo_upload.read()  # async read
            await out_file.write(content)  # async write

        # photo_upload.save(path)  # we can't use object first and then save as this alters file object

        # resize file by lowering resolution if required
        if not config.resizing_size:
            pass
        elif not resizing_required(path, config.resizing_size):
            logger.info(f'No resizing required.')
        else:
            logger.info(f'Saving and resizing {path}.')
            resize_image(path=path,
                         save_to_path=with_suffix(path, RESIZE_SUFFIX),
                         size=config.resizing_size,
                         quality=config.quality)
            path = with_suffix(path, RESIZE_SUFFIX)

        # add to photo directory (cache) and add keywords and plant tags
        # (all the same for each uploaded photo)
        photo = Photo(path_full_local=path, filename=os.path.basename(path))
        photo.tag_authors_plants = [p['key'] for p in plants]
        photo.tag_keywords = [k['keyword'] for k in keywords]
        with lock_photo_directory:
            if p := get_photo_directory(instantiate=False):
                if p in p.photos:
                    throw_exception(
                        f"Already found in PhotoDirectory cache: {photo.path_full_local}",
                        request=request)
                p.photos.append(photo)

        # generate thumbnail image for frontend display and update file's exif tags
        photo.generate_thumbnails()
        photo.write_exif_tags()
        photos.append(photo)
示例#15
0
async def get_plant(request: Request,
                    plant_id: int,
                    db: Session = Depends(get_db)):
    """
    read plant information from db
    currently unused
    """
    plant_obj = db.query(Plant).filter(Plant.id == plant_id).first()
    if not plant_obj:
        throw_exception(f'Plant not found: {plant_id}.', request=request)
    plant = plant_obj.as_dict()

    make_dict_values_json_serializable(plant)
    return plant
示例#16
0
async def delete_image(request: Request, photo: PImageDelete):
    """move the file that should be deleted to another folder (not actually deleted, currently)"""

    old_path = photo.path_full_local
    if not os.path.isfile(old_path):
        logger.error(
            err_msg := f"File selected to be deleted not found: {old_path}")
        throw_exception(err_msg, request=request)

    filename = os.path.basename(old_path)
    new_path = os.path.join(PATH_DELETED_PHOTOS, filename)

    try:
        os.replace(
            src=old_path,
            dst=new_path)  # silently overwrites if privileges are sufficient
    except OSError as e:
        logger.error(err_msg :=
                     f'OSError when moving file {old_path} to {new_path}',
                     exc_info=e)
        throw_exception(err_msg,
                        description=f'Filename: {os.path.basename(old_path)}',
                        request=request)
    logger.info(f'Moved file {old_path} to {new_path}')

    # remove from PhotoDirectory cache
    with lock_photo_directory:
        photo_directory = get_photo_directory(instantiate=False)
        if photo_directory:
            photo_obj = photo_directory.get_photo(photo.path_full_local)
            photo_directory.remove_image_from_directory(photo_obj)

    results = {
        'action':
        'Deleted',
        'resource':
        'ImageResource',
        'message':
        get_message(f'Successfully deleted image',
                    description=f'Filename: {os.path.basename(old_path)}'),
        'photo':
        photo
    }

    # send the photo back to frontend; it will be removed from json model there
    return results
示例#17
0
def rename_plant(request: Request,
                 data: PPlantsRenameRequest,
                 db: Session = Depends(get_db)):
    """we use the put method to rename a plant"""
    args = data

    plant_obj = db.query(Plant).filter(
        Plant.plant_name == args.OldPlantName).first()
    if not plant_obj:
        throw_exception(f"Can't find plant {args.OldPlantName}",
                        request=request)

    if db.query(Plant).filter(Plant.plant_name == args.NewPlantName).first():
        throw_exception(f"Plant already exists: {args.NewPlantName}",
                        request=request)

    # rename plant name
    plant_obj.plant_name = args.NewPlantName
    plant_obj.last_update = datetime.datetime.now()

    # most difficult task: exif tags use plant name not id; we need to change each plant name occurence
    # in images' exif tags
    count_modified_images = rename_plant_in_image_files(
        args.OldPlantName, args.NewPlantName)

    # only after image modifications have gone well, we can commit changes to database
    db.commit()

    create_history_entry(description=f"Renamed to {args.NewPlantName}",
                         db=db,
                         plant_id=plant_obj.id,
                         plant_name=args.OldPlantName,
                         commit=False)

    logger.info(f'Modified {count_modified_images} images.')
    results = {
        'action':
        'Renamed plant',
        'resource':
        'PlantResource',
        'message':
        get_message(f'Renamed {args.OldPlantName} to {args.NewPlantName}',
                    description=f'Modified {count_modified_images} images.')
    }

    return results
示例#18
0
async def update_images(request: Request, modified_ext: PImageUpdated):
    """modify existing image's exif tags"""
    logger.info(
        f"Saving updates for {len(modified_ext.ImagesCollection)} images.")
    with lock_photo_directory:
        directory = get_photo_directory()
        for image_ext in modified_ext.ImagesCollection:
            if not (photo := directory.get_photo(image_ext.path_full_local)):
                throw_exception(
                    f"Can't find image file: {image_ext.path_full_local}",
                    request=request)

            logger.info(
                f'Updating changed image in PhotoDirectory Cache: {photo.path_full_local}'
            )
            photo.tag_keywords = [k.keyword for k in image_ext.keywords]
            photo.tag_authors_plants = [p.key for p in image_ext.plants]
            photo.tag_description = image_ext.description
            photo.write_exif_tags()
示例#19
0
def get_proposals(request: Request,
                  entity_id: ProposalEntity,
                  db: Session = Depends(get_db)):
    """returns proposals for selection tables"""

    results = {}
    if entity_id == ProposalEntity.SOIL:
        results = {'SoilsCollection': [], 'ComponentsCollection': []}
        # soil mixes
        soils = db.query(Soil).all()
        for soil in soils:
            soil_dict = soil.as_dict()
            soil_dict['components'] = [{
                'component_name': c.soil_component.component_name,
                'portion': c.portion
            } for c in soil.soil_to_component_associations]

            results['SoilsCollection'].append(soil_dict)

        # soil components for new mixes
        components = db.query(SoilComponent).all()
        results['ComponentsCollection'] = [{
            'component_name': c.component_name
        } for c in components]

    elif entity_id == ProposalEntity.NURSERY:
        # get distinct nurseries/sources, sorted by last update
        nurseries_tuples = db.query(Plant.nursery_source) \
            .order_by(Plant.last_update.desc()) \
            .distinct(Plant.nursery_source)\
            .filter(Plant.nursery_source.isnot(None)).all()
        if not nurseries_tuples:
            results = {'NurseriesSourcesCollection': []}
        else:
            results = {
                'NurseriesSourcesCollection': [{
                    'name': n[0]
                } for n in nurseries_tuples]
            }

    elif entity_id == ProposalEntity.KEYWORD:
        # return collection of all distinct keywords used in images
        keywords_set = get_distinct_keywords_from_image_files()
        keywords_collection = [{
            'keyword': keyword
        } for keyword in keywords_set]
        results = {'KeywordsCollection': keywords_collection}

    elif entity_id == ProposalEntity.TRAIT_CATEGORY:
        # trait categories
        trait_categories = []
        t: Trait
        for t in TRAIT_CATEGORIES:
            # note: trait categories from config file are created in orm_tables.py if not existing upon start
            trait_category_obj = TraitCategory.get_cat_by_name(
                t, db, raise_exception=True)
            trait_categories.append(trait_category_obj.as_dict())
        results = {'TraitCategoriesCollection': trait_categories}

        # traits
        traits_query: List[Trait] = db.query(Trait).filter(
            Trait.trait_category.has(
                TraitCategory.category_name.in_(TRAIT_CATEGORIES)))
        traits = []
        for t in traits_query:
            t_dict = t.as_dict()
            t_dict['trait_category_id'] = t.trait_category_id
            t_dict['trait_category'] = t.trait_category.category_name
            traits.append(t_dict)
        results['TraitsCollection'] = traits

    else:
        throw_exception(f'Proposal entity {entity_id} not expected.',
                        request=request)

    results.update({
        'action':
        'Get',
        'resource':
        'ProposalResource',
        'message':
        get_message(
            f'Receiving proposal values for entity {entity_id} from backend.')
    })

    return results
示例#20
0
async def update_taxa(request: Request,
                      modified_taxa: PModifiedTaxa,
                      db: Session = Depends(get_db)):
    """two things can be changed in the taxon model, and these are modified in extensions here:
        - modified custom fields
        - traits"""
    modified_taxa = modified_taxa.ModifiedTaxaCollection

    for taxon_modified in modified_taxa:
        taxon: Taxon = db.query(Taxon).filter(
            Taxon.id == taxon_modified.id).first()
        if not taxon:
            logger.error(f'Taxon not found: {taxon.name}. Saving canceled.')
            throw_exception(f'Taxon not found: {taxon.name}. Saving canceled.',
                            request=request)

        taxon.custom_notes = taxon_modified.custom_notes
        update_traits(taxon, taxon_modified.trait_categories, db)

        # changes to images attached to the taxon
        # deleted images
        path_originals_saved = [image.path_original for image in taxon_modified.images] if \
            taxon_modified.images else []
        for image_obj in taxon.images:
            if image_obj.relative_path not in path_originals_saved:
                # don't delete image object, but only the association (image might be assigned to other events)
                db.delete([
                    link for link in taxon.image_to_taxon_associations
                    if link.image.relative_path == image_obj.relative_path
                ][0])

        # newly assigned images
        if taxon_modified.images:
            for image in taxon_modified.images:
                image_obj = db.query(Image).filter(
                    Image.relative_path == image.path_original).first()

                # not assigned to any event, yet
                if not image_obj:
                    image_obj = Image(relative_path=image.path_original)
                    db.add(image_obj)
                    db.flush()  # required to obtain id

                # update link table including the image description
                current_taxon_to_image_link = [
                    t for t in taxon.image_to_taxon_associations
                    if t.image == image_obj
                ]

                # insert link
                if not current_taxon_to_image_link:
                    link = ImageToTaxonAssociation(
                        image_id=image_obj.id,
                        taxon_id=taxon.id,
                        description=image.description)
                    db.add(link)
                    logger.info(
                        f'Image {image_obj.relative_path} assigned to taxon {taxon.name}'
                    )

                # update description
                elif current_taxon_to_image_link[
                        0].description != image.description:
                    current_taxon_to_image_link[
                        0].description = image.description
                    logger.info(
                        f'Update description of link between image {image_obj.relative_path} and taxon'
                        f' {taxon.name}')

    db.commit()

    results = {
        'action': 'Save taxa',
        'resource': 'TaxonResource',
        'message':
        get_message(f'Updated {len(modified_taxa)} taxa in database.')
    }

    logger.info(f'Updated {len(modified_taxa)} taxa in database.')
    return results
示例#21
0
def update_traits(taxon: Taxon,
                  trait_categories: List[PTraitCategoryWithTraits],
                  db: Session):
    """update a taxon's traits (includes deleting and creating traits)"""
    # loop at new traits to update attributes and create new ones
    link: TaxonToTraitAssociation
    new_trait_obj_list = []
    for category_new in trait_categories or []:  # might be None
        # get category object
        category_obj = db.query(TraitCategory).filter(
            TraitCategory.id == category_new.id).first()
        if not category_obj:
            throw_exception(f'Trait Category {category_new.id} not found.')

        # loop at category's traits
        for trait_new in category_new.traits:

            # check if we have no trait id but a same-named trait
            if not trait_new.id:
                trait_obj: Trait = db.query(Trait).filter(
                    Trait.trait == trait_new.trait,
                    Trait.trait_category == category_obj).first()

            # get trait object by id
            else:
                trait_obj: Trait = db.query(Trait).filter(
                    Trait.id == trait_new.id).first()
                if not trait_obj:
                    throw_exception(
                        f"Can't find trait in db although it has an id: {trait_new.id}"
                    )

            # update existing trait's link to taxon (this is where the status attribute lies)
            if trait_obj:
                links_existing = [
                    li for li in trait_obj.taxon_to_trait_associations
                    if li.taxon == taxon
                ]
                if links_existing:
                    links_existing[0].status = trait_new.status
                else:
                    # trait exists, but is not assigned the taxon; create that link
                    link = TaxonToTraitAssociation(taxon=taxon,
                                                   trait=trait_obj,
                                                   status=trait_new.status)
                    db.add(link)
                    # taxon.taxon_to_trait_associations.append(trait_obj)  # commit in calling method

            # altogether new trait
            else:
                logger.info(f"Creating new trait in db for category "
                            f"{category_obj.category_name}: {trait_new.trait}")
                trait_obj = Trait(trait=trait_new.trait,
                                  trait_category=category_obj)
                link = TaxonToTraitAssociation(taxon=taxon,
                                               trait=trait_obj,
                                               status=trait_new.status)
                db.add_all([trait_obj, link])

            # collect traits themselves for identifying deleted links later
            new_trait_obj_list.append(trait_obj)

    # remove deleted traits from taxon links
    for link in taxon.taxon_to_trait_associations:
        if link.trait not in new_trait_obj_list:
            logger.info(
                f"Deleting trait for taxon {taxon.name}: {link.trait.trait}")
            db.delete(link)
示例#22
0
async def modify_events(
    request: Request,
    # todo replace dict with ordinary pydantic schema (also on ui side)
    plants_events_dict: Dict[str, List[PEventNew]] = Body(..., embed=True),
    db: Session = Depends(get_db)):
    """save n events for n plants in database (add, modify, delete)"""
    # frontend submits a dict with events for those plants where at least one event has been changed, added, or
    # deleted. it does, however, always submit all these plants' events

    # loop at the plants and their events
    counts = defaultdict(int)
    new_list = []
    for plant_name, events in plants_events_dict.items():

        plant_obj = Plant.get_plant_by_plant_name(plant_name,
                                                  db,
                                                  raise_exception=True)
        logger.info(
            f'Plant {plant_obj.plant_name} has {len(plant_obj.events)} events in db:'
            f' {[e.id for e in plant_obj.events]}')

        # event might have no id in browser but already in backend from earlier save
        # so try to get eventid  from plant name and date (pseudo-key) to avoid events being deleted
        # note: if we "replace" an event in the browser  (i.e. for a specific date, we delete an event and
        # create a new one, then that event in database will be modified, not deleted and re-created
        for event in [e for e in events if not e.id]:

            event_obj_id = db.query(Event.id).filter(
                Event.plant_id == plant_obj.id,
                Event.date == event.date).scalar()
            if event_obj_id is not None:
                event.id = event_obj_id
                logger.info(
                    f"Identified event without id from browser as id {event.id}"
                )
        event_ids = [e.id for e in events]
        logger.info(
            f'Updating {len(events)} events ({event_ids})for plant {plant_name}'
        )

        # loop at the current plant's database events to find deleted ones
        event_obj: Optional[Event] = None
        for event_obj in plant_obj.events:
            if event_obj.id not in event_ids:
                logger.info(f'Deleting event {event_obj.id}')
                for link in event_obj.image_to_event_associations:
                    db.delete(link)
                db.delete(event_obj)
                counts['Deleted Events'] += 1

        # loop at the current plant's events from frontend to find new events and modify existing ones
        for event in events:
            # new event
            if not event.id:
                # create event record
                logger.info('Creating event.')
                event_obj = Event(date=event.date,
                                  event_notes=event.event_notes,
                                  plant=plant_obj)
                db.add(event_obj)
                counts['Added Events'] += 1

            # update existing event
            else:
                try:
                    logger.info(f'Getting event  {event.id}.')
                    event_obj = Event.get_event_by_event_id(event.id, db)
                    if not event_obj:
                        logger.warning(f'Event not found: {event.id}')
                        continue
                    event_obj.event_notes = event.event_notes
                    event_obj.date = event.date

                except InvalidRequestError as e:
                    db.rollback()
                    logger.error(
                        'Serious error occured at event resource (POST). Rollback. See log.',
                        stack_info=True,
                        exc_info=e)
                    throw_exception(
                        'Serious error occured at event resource (POST). Rollback. See log.',
                        request=request)

            # segments observation, pot, and soil
            if event.observation and not event_obj.observation:
                observation_obj = Observation()
                db.add(observation_obj)
                event_obj.observation = observation_obj
                counts['Added Observations'] += 1
            elif not event.observation and event_obj.observation:
                # 1:1 relationship, so we can delete the observation directly
                db.delete(event_obj.observation)
                event_obj.observation = None
            if event.observation and event_obj.observation:
                event_obj.observation.diseases = event.observation.diseases
                event_obj.observation.observation_notes = event.observation.observation_notes
                # cm to mm
                event_obj.observation.height = event.observation.height * 10 if event.observation.height else None
                event_obj.observation.stem_max_diameter = event.observation.stem_max_diameter * 10 if \
                    event.observation.stem_max_diameter else None

            if not event.pot:
                event_obj.pot_event_type = None
                event_obj.pot = None

            else:
                event_obj.pot_event_type = event.pot_event_type
                # add empty if not existing
                if not event_obj.pot:
                    pot_obj = Pot()
                    db.add(pot_obj)
                    event_obj.pot = pot_obj
                    counts['Added Pots'] += 1

                # pot objects have an id but are not "reused" for other events, so we may change it here
                event_obj.pot.material = event.pot.material
                event_obj.pot.shape_side = event.pot.shape_side
                event_obj.pot.shape_top = event.pot.shape_top
                event_obj.pot.diameter_width = event.pot.diameter_width * 10 if event.pot.diameter_width else None

            if not event.soil:
                event_obj.soil_event_type = None
                # remove soil from event (event to soil is n:1 so we don't delete the soil object but only the
                # assignment)
                if event_obj.soil:
                    event_obj.soil = None

            else:
                event_obj.soil_event_type = event.soil_event_type
                # add soil to event or change it
                if not event_obj.soil or (event.soil and
                                          event.soil.id != event_obj.soil.id):
                    event_obj.soil = get_or_create_soil(
                        event.soil.dict(), counts, db)

            # changes to images attached to the event
            # deleted images
            path_originals_saved = [
                image.path_original for image in event.images
            ] if event.images else []
            for image_obj in event_obj.images:
                if image_obj.relative_path not in path_originals_saved:
                    # don't delete image object, but only the association (image might be assigned to other events)
                    db.delete([
                        link for link in event_obj.image_to_event_associations
                        if link.image.relative_path == image_obj.relative_path
                    ][0])

            # newly assigned images
            if event.images:
                for image in event.images:
                    image_obj = db.query(Image).filter(
                        Image.relative_path == image.path_original).first()

                    # not assigned to any event, yet
                    if not image_obj:
                        image_obj = Image(relative_path=image.path_original)
                        new_list.append(image_obj)

                    # not assigned to that specific event, yet
                    if image_obj not in event_obj.images:
                        event_obj.images.append(image_obj)

    if new_list:
        db.add_all(new_list)
    db.commit()

    logger.info(' Saving Events: ' + (description := ', '.join(
        [f'{key}: {counts[key]}' for key in counts.keys()])))