From 93177768add804308a7dc4c4ada3303a0d5c476c Mon Sep 17 00:00:00 2001 From: Evan Blaudy Date: Tue, 7 Nov 2023 14:40:07 +0100 Subject: [PATCH 1/3] [requirements] upgrade sentry-sdk & sqlalchemy --- setup.cfg | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.cfg b/setup.cfg index 520531698e..21d0f005a7 100644 --- a/setup.cfg +++ b/setup.cfg @@ -70,7 +70,7 @@ install_requires = rq==1.15.1 slackclient==2.9.4 sqlalchemy_utils==0.41.1 - sqlalchemy==2.0.22 + sqlalchemy==2.0.23 ua-parser==0.18.0 werkzeug==3.0.1 @@ -98,7 +98,7 @@ test = monitoring = prometheus-flask-exporter==0.23.0 pygelf==0.4.2 - sentry-sdk==1.33.1 + sentry-sdk==1.34.0 lint = black==23.10.1 From 48e39d916f1b6d0fcc5a24102f78c5b03a7ad7c0 Mon Sep 17 00:00:00 2001 From: Evan Blaudy Date: Thu, 9 Nov 2023 13:43:02 +0100 Subject: [PATCH 2/3] [previews] introduce preview background files --- setup.cfg | 2 + zou/app/blueprints/crud/__init__.py | 10 +- .../crud/preview_background_file.py | 61 ++++ zou/app/blueprints/crud/project.py | 57 +++ zou/app/blueprints/previews/__init__.py | 15 + zou/app/blueprints/previews/resources.py | 334 ++++++++++++++---- zou/app/blueprints/projects/__init__.py | 10 + zou/app/blueprints/projects/resources.py | 101 +++++- zou/app/models/preview_background_file.py | 16 + zou/app/models/project.py | 38 ++ zou/app/services/deletion_service.py | 37 ++ zou/app/services/exception.py | 4 + zou/app/services/files_service.py | 65 ++++ zou/app/services/preview_files_service.py | 4 +- zou/app/services/projects_service.py | 35 ++ zou/app/services/sync_service.py | 151 +++++++- zou/app/services/user_service.py | 3 + zou/app/utils/thumbnail.py | 30 +- .../c49e41f1298b_add_previewbackground.py | 119 +++++++ 19 files changed, 1016 insertions(+), 76 deletions(-) create mode 100644 zou/app/blueprints/crud/preview_background_file.py create mode 100644 zou/app/models/preview_background_file.py create mode 100644 zou/migrations/versions/c49e41f1298b_add_previewbackground.py diff --git a/setup.cfg b/setup.cfg index 21d0f005a7..6009221b9a 100644 --- a/setup.cfg +++ b/setup.cfg @@ -55,6 +55,8 @@ install_requires = ldap3==2.9.1 matterhook==0.2 meilisearch==0.28.4 + numpy==1.24.4 + opencv-python==4.8.1.78 OpenTimelineIO==0.15.0 orjson==3.9.10 pillow==10.1.0 diff --git a/zou/app/blueprints/crud/__init__.py b/zou/app/blueprints/crud/__init__.py index 70c4ef07b6..93aad9662b 100644 --- a/zou/app/blueprints/crud/__init__.py +++ b/zou/app/blueprints/crud/__init__.py @@ -118,7 +118,10 @@ WorkingFilesResource, WorkingFileResource, ) - +from zou.app.blueprints.crud.preview_background_file import ( + PreviewBackgroundFileResource, + PreviewBackgroundFilesResource, +) routes = [ ("/data/persons", PersonsResource), @@ -190,6 +193,11 @@ ("/data/subscriptions/", SubscriptionResource), ("/data/entity-links/", EntityLinksResource), ("/data/entity-links/", EntityLinkResource), + ("/data/preview-background-files", PreviewBackgroundFilesResource), + ( + "/data/preview-background-files/", + PreviewBackgroundFileResource, + ), ] blueprint = Blueprint("/data", "data") diff --git a/zou/app/blueprints/crud/preview_background_file.py b/zou/app/blueprints/crud/preview_background_file.py new file mode 100644 index 0000000000..5434bca1bf --- /dev/null +++ b/zou/app/blueprints/crud/preview_background_file.py @@ -0,0 +1,61 @@ +from zou.app.models.preview_background_file import PreviewBackgroundFile +from zou.app.services.exception import ArgumentsException +from zou.app.services import files_service, deletion_service + +from zou.app.blueprints.crud.base import BaseModelResource, BaseModelsResource + + +class PreviewBackgroundFilesResource(BaseModelsResource): + def __init__(self): + BaseModelsResource.__init__(self, PreviewBackgroundFile) + + def check_read_permissions(self): + return True + + def update_data(self, data): + name = data.get("name", None) + preview_background_file = PreviewBackgroundFile.get_by(name=name) + if preview_background_file is not None: + raise ArgumentsException( + "A preview background file with similar name already exists" + ) + return data + + def post_creation(self, instance): + if instance.is_default: + files_service.reset_default_preview_background_files(instance.id) + files_service.clear_preview_background_file_cache(str(instance.id)) + return instance.serialize() + + +class PreviewBackgroundFileResource(BaseModelResource): + def __init__(self): + BaseModelResource.__init__(self, PreviewBackgroundFile) + + def check_read_permissions(self, instance): + return True + + def update_data(self, data, instance_id): + name = data.get("name", None) + if name is not None: + preview_background_file = PreviewBackgroundFile.get_by(name=name) + if preview_background_file is not None and instance_id != str( + preview_background_file.id + ): + raise ArgumentsException( + "A preview background file with similar name already exists" + ) + return data + + def post_update(self, instance_dict): + if instance_dict["is_default"]: + files_service.reset_default_preview_background_files( + instance_dict["id"] + ) + files_service.clear_preview_background_file_cache(instance_dict["id"]) + return instance_dict + + def post_delete(self, instance_dict): + deletion_service.clear_preview_background_files(instance_dict["id"]) + files_service.clear_preview_background_file_cache(instance_dict["id"]) + return instance_dict diff --git a/zou/app/blueprints/crud/project.py b/zou/app/blueprints/crud/project.py index 9dbeedc43a..103e6d3a44 100644 --- a/zou/app/blueprints/crud/project.py +++ b/zou/app/blueprints/crud/project.py @@ -15,6 +15,7 @@ assets_service, tasks_service, status_automations_service, + files_service, ) from zou.app.utils import events, permissions, fields @@ -79,6 +80,31 @@ def update_data(self, data): ) for task_type_id in data["status_automations"] ] + + if "preview_background_files" in data: + data["preview_background_files"] = [ + files_service.get_preview_background_file_raw( + preview_background_file_id + ) + for preview_background_file_id in data[ + "preview_background_files" + ] + ] + + if data.get("preview_background_file_id") is not None: + preview_background_files_ids = [] + if "preview_background_files" in data: + preview_background_files_ids = [ + str(preview_background_file.id) + for preview_background_file in data[ + "preview_background_files" + ] + ] + if ( + data["preview_background_file_id"] + not in preview_background_files_ids + ): + raise ArgumentsException("Invalid preview_background_file_id") return data def post_creation(self, project): @@ -132,6 +158,37 @@ def pre_update(self, project_dict, data): ) for task_type_id in data["status_automations"] ] + if "preview_background_files" in data: + data["preview_background_files"] = [ + files_service.get_preview_background_file_raw( + preview_background_file_id + ) + for preview_background_file_id in data[ + "preview_background_files" + ] + ] + + if data.get("preview_background_file_id") is not None: + if "preview_background_files" in data: + preview_background_files_ids = [ + str(preview_background_file.id) + for preview_background_file in data[ + "preview_background_files" + ] + ] + else: + preview_background_files_ids = [ + preview_background_file_id + for preview_background_file_id in project_dict[ + "preview_background_files" + ] + ] + if ( + data["preview_background_file_id"] + not in preview_background_files_ids + ): + raise ArgumentsException("Invalid preview_background_file_id") + return data def post_update(self, project_dict): diff --git a/zou/app/blueprints/previews/__init__.py b/zou/app/blueprints/previews/__init__.py index f00448c765..bc71e1beed 100644 --- a/zou/app/blueprints/previews/__init__.py +++ b/zou/app/blueprints/previews/__init__.py @@ -26,6 +26,9 @@ UpdatePreviewPositionResource, ExtractFrameFromPreview, ExtractTileFromPreview, + CreatePreviewBackgroundFileResource, + PreviewBackgroundFileThumbnailResource, + PreviewBackgroundFileResource, ) routes = [ @@ -98,6 +101,18 @@ "/pictures/thumbnails/projects/.png", ProjectThumbnailResource, ), + ( + "/pictures/preview-background-files/", + CreatePreviewBackgroundFileResource, + ), + ( + "/pictures/thumbnails/preview-background-files/.png", + PreviewBackgroundFileThumbnailResource, + ), + ( + "/pictures/preview-background-files/.", + PreviewBackgroundFileResource, + ), ( "/actions/entities//set-main-preview/", LegacySetMainPreviewResource, diff --git a/zou/app/blueprints/previews/resources.py b/zou/app/blueprints/previews/resources.py index 5aef00aecd..d6e0866157 100644 --- a/zou/app/blueprints/previews/resources.py +++ b/zou/app/blueprints/previews/resources.py @@ -5,6 +5,7 @@ from flask_restful import Resource from flask_jwt_extended import jwt_required from flask_fs.errors import FileNotFound +from werkzeug.exceptions import NotFound from zou.app import config from zou.app.mixin import ArgsMixin @@ -33,46 +34,48 @@ from zou.app.services.exception import ( ArgumentsException, PreviewFileNotFoundException, + PreviewBackgroundFileNotFoundException, ) -ALLOWED_PICTURE_EXTENSION = [".jpe", ".jpeg", ".jpg", ".png"] +ALLOWED_PICTURE_EXTENSION = ["jpe", "jpeg", "jpg", "png"] ALLOWED_MOVIE_EXTENSION = [ - ".avi", - ".m4v", - ".mkv", - ".mov", - ".mp4", - ".webm", - ".wmv", + "avi", + "m4v", + "mkv", + "mov", + "mp4", + "webm", + "wmv", ] ALLOWED_FILE_EXTENSION = [ - ".ae", - ".ai", - ".blend", - ".clip", - ".comp", - ".exr", - ".fbx", - ".fla", - ".flv", - ".gif", - ".glb", - ".gltf", - ".hip", - ".ma", - ".mb", - ".mp3", - ".obj", - ".pdf", - ".psd", - ".rar", - ".sbbkp", - ".svg", - ".swf", - ".wav", - ".zip", + "ae", + "ai", + "blend", + "clip", + "comp", + "exr", + "fbx", + "fla", + "flv", + "gif", + "glb", + "gltf", + "hip", + "ma", + "mb", + "mp3", + "obj", + "pdf", + "psd", + "rar", + "sbbkp", + "svg", + "swf", + "wav", + "zip", ] +ALLOWED_PREVIEW_BACKGROUND_EXTENSION = ["hdr"] def send_standard_file( @@ -107,15 +110,26 @@ def send_movie_file(preview_file_id, as_attachment=False, lowdef=False): ) -def send_picture_file(prefix, preview_file_id, as_attachment=False): +def send_picture_file( + prefix, + preview_file_id, + as_attachment=False, + extension="png", + download_name="", +): + if extension == "png": + mimetype = "image/png" + elif extension == "hdr": + mimetype = "image/vnd.radiance" return send_storage_file( file_store.get_local_picture_path, file_store.open_picture, prefix, preview_file_id, - "png", - mimetype="image/png", + extension, + mimetype=mimetype, as_attachment=as_attachment, + download_name=download_name, ) @@ -128,6 +142,7 @@ def send_storage_file( mimetype="application/octet-stream", as_attachment=False, max_age=config.CLIENT_CACHE_MAX_AGE, + download_name="", ): """ Send file from storage. If it's not a local storage, cache the file in @@ -135,15 +150,20 @@ def send_storage_file( """ file_size = None try: - if prefix in ["movies", "original"]: - preview_file = files_service.get_preview_file(preview_file_id) + if prefix in ["movies", "original", "preview-backgrounds"]: + if prefix == "preview-backgrounds": + preview_file = files_service.get_preview_background_file( + preview_file_id + ) + else: + preview_file = files_service.get_preview_file(preview_file_id) if ( preview_file.get("file_size") is not None and preview_file["file_size"] > 0 and preview_file["extension"] == extension ): file_size = preview_file["file_size"] - except PreviewFileNotFoundException: + except NotFound: pass file_path = fs.get_file_path_and_file( config, @@ -155,7 +175,6 @@ def send_storage_file( file_size=file_size, ) - download_name = "" if as_attachment: download_name = names_service.get_preview_file_name(preview_file_id) @@ -216,7 +235,7 @@ def post(self, instance_id): uploaded_file = request.files["file"] file_name_parts = uploaded_file.filename.split(".") - extension = ".%s" % file_name_parts.pop().lower() + extension = file_name_parts.pop().lower() original_file_name = ".".join(file_name_parts) if extension in ALLOWED_PICTURE_EXTENSION: @@ -256,7 +275,7 @@ def post(self, instance_id): preview_file = preview_files_service.update_preview_file( instance_id, { - "extension": extension[1:], + "extension": extension, "original_name": original_file_name, "status": "ready", }, @@ -276,7 +295,7 @@ def save_picture_preview(self, instance_id, uploaded_file): Get uploaded picture, build thumbnails then save everything in the file storage. """ - tmp_folder = current_app.config["TMP_DIR"] + tmp_folder = config.TMP_DIR original_tmp_path = thumbnail_utils.save_file( tmp_folder, instance_id, uploaded_file ) @@ -299,7 +318,7 @@ def save_movie_preview( everything in the file storage. """ no_job = self.get_no_job() - tmp_folder = current_app.config["TMP_DIR"] + tmp_folder = config.TMP_DIR uploaded_movie_path = movie.save_file( tmp_folder, preview_file_id, uploaded_file ) @@ -319,8 +338,8 @@ def save_file_preview(self, instance_id, uploaded_file, extension): """ Get uploaded file then save it in the file storage. """ - tmp_folder = current_app.config["TMP_DIR"] - file_name = instance_id + extension + tmp_folder = config.TMP_DIR + file_name = f"{instance_id}.{extension}" file_path = os.path.join(tmp_folder, file_name) uploaded_file.save(file_path) file_store.add_file("previews", instance_id, file_path) @@ -577,6 +596,7 @@ def get(self, instance_id, extension): abort(403) try: + extension = extension.lower() if extension == "png": return send_picture_file("original", instance_id) elif extension == "pdf": @@ -797,7 +817,7 @@ def post(self, instance_id): self.check_permissions(instance_id) self.prepare_creation(instance_id) - tmp_folder = current_app.config["TMP_DIR"] + tmp_folder = config.TMP_DIR uploaded_file = request.files["file"] thumbnail_path = thumbnail_utils.save_file( tmp_folder, instance_id, uploaded_file @@ -823,9 +843,8 @@ class BasePictureResource(Resource): Base resource to download a thumbnail. """ - def __init__(self, subfolder): - Resource.__init__(self) - self.subfolder = subfolder + def is_exist(self, instance_id): + return False def is_allowed(self, instance_id): return True @@ -893,9 +912,6 @@ def prepare_creation(self, instance_id): class PersonThumbnailResource(BasePictureResource): - def __init__(self): - BasePictureResource.__init__(self, "persons") - def is_exist(self, person_id): return persons_service.get_person(person_id) is not None @@ -920,9 +936,6 @@ def prepare_creation(self, organisation_id): class OrganisationThumbnailResource(BasePictureResource): - def __init__(self): - BasePictureResource.__init__(self, "organisations") - def is_exist(self, organisation_id): return True @@ -943,9 +956,6 @@ def prepare_creation(self, instance_id): class ProjectThumbnailResource(BasePictureResource): - def __init__(self): - BasePictureResource.__init__(self, "projects") - def is_exist(self, project_id): return projects_service.get_project(project_id) is not None @@ -1233,3 +1243,209 @@ def get(self, preview_file_id): ) finally: os.remove(extracted_tile_path) + + +class CreatePreviewBackgroundFileResource(Resource): + """ + Main resource to add a preview background file. It stores the preview background + file and generates a rectangle thumbnail. + """ + + @jwt_required() + def post(self, instance_id): + """ + Main resource to add a preview background file. + --- + tags: + - Preview background file + consumes: + - multipart/form-data + - image/vnd.radiance + parameters: + - in: path + name: instance_id + required: True + type: string + format: UUID + x-example: a24a6ea4-ce75-4665-a070-57453082c25 + - in: formData + name: file + required: True + type: file + responses: + 200: + description: Preview background file added + """ + self.check_permissions(instance_id) + + preview_background_file = files_service.get_preview_background_file( + instance_id + ) + + uploaded_file = request.files["file"] + + file_name_parts = uploaded_file.filename.split(".") + extension = file_name_parts.pop().lower() + original_file_name = ".".join(file_name_parts) + + if extension in ALLOWED_PREVIEW_BACKGROUND_EXTENSION: + metadada = self.save_preview_background_file( + instance_id, uploaded_file, extension + ) + preview_background_file = ( + files_service.update_preview_background_file( + instance_id, + { + "extension": extension, + "original_name": original_file_name, + "file_size": metadada["file_size"], + }, + ) + ) + files_service.clear_preview_background_file_cache(instance_id) + self.emit_preview_background_file_event(preview_background_file) + return preview_background_file, 201 + + else: + current_app.logger.info( + f"Wrong file format, extension: {extension}" + ) + deletion_service.remove_preview_background_file_by_id(instance_id) + abort(400, f"Wrong file format, extension: {extension}") + + def check_permissions(self, instance_id): + """ + Check if user has permissions to add a preview background file. + """ + return permissions.check_admin_permissions() + + def save_preview_background_file( + self, instance_id, uploaded_file, extension + ): + """ + Get uploaded preview background file, build thumbnail then save + everything in the file storage. + """ + try: + tmp_folder = config.TMP_DIR + file_name = f"{instance_id}.{extension}" + preview_background_path = os.path.join(tmp_folder, file_name) + uploaded_file.save(preview_background_path) + file_size = fs.get_file_size(preview_background_path) + file_store.add_picture( + "preview-backgrounds", instance_id, preview_background_path + ) + preview_files_service.clear_variant_from_cache( + instance_id, "preview-backgrounds", extension + ) + if extension == "hdr": + thumbnail_path = thumbnail_utils.turn_hdr_into_thumbnail( + preview_background_path + ) + file_store.add_picture( + "thumbnails", instance_id, thumbnail_path + ) + preview_files_service.clear_variant_from_cache( + instance_id, "thumbnails" + ) + + return { + "preview_file_id": instance_id, + "file_size": file_size, + } + except: + current_app.logger.error( + f"Error while saving preview background file and thumbnail: {instance_id}" + ) + deletion_service.remove_preview_background_file_by_id(instance_id) + abort( + 400, + f"Error while saving preview background file and thumbnail: {instance_id}", + ) + finally: + try: + if os.path.exists(preview_background_path): + os.remove(preview_background_path) + if os.path.exists(thumbnail_path): + os.remove(thumbnail_path) + except: + pass + + def emit_preview_background_file_event(self, preview_background_file): + """ + Emit an event, each time a preview background file is added. + """ + events.emit( + "preview-background-file:update", + {"preview_background_file_id": preview_background_file["id"]}, + ) + events.emit( + "preview-background-file:add-file", + { + "preview_background_file_id": preview_background_file["id"], + "extension": preview_background_file["extension"], + }, + ) + + +class PreviewBackgroundFileResource(Resource): + """ + Main resource to download a preview background file. + """ + + @jwt_required() + def get(self, instance_id, extension): + """ + Download a preview background file. + --- + tags: + - Previews + parameters: + - in: path + name: instance_id + required: True + type: string + format: UUID + x-example: a24a6ea4-ce75-4665-a070-57453082c25 + - in: path + name: extension + required: True + type: string + format: extension + x-example: hdr + responses: + 200: + description: Preview background file downloaded + 404: + description: Preview background file not found + """ + preview_background_file = files_service.get_preview_background_file( + instance_id + ) + + extension = extension.lower() + if preview_background_file["extension"] != extension: + raise PreviewBackgroundFileNotFoundException + + try: + return send_picture_file( + "preview-backgrounds", + instance_id, + extension=extension, + download_name=f"{preview_background_file['original_name']}.{extension}", + ) + except FileNotFound: + current_app.logger.error( + "Preview background file was not found for: %s" % instance_id + ) + raise PreviewBackgroundFileNotFoundException + + +class PreviewBackgroundFileThumbnailResource(BasePictureResource): + def is_exist(self, preview_background_file_id): + return ( + files_service.get_preview_background_file( + preview_background_file_id + ) + is not None + ) diff --git a/zou/app/blueprints/projects/__init__.py b/zou/app/blueprints/projects/__init__.py index 552d9df095..c8e56026cc 100644 --- a/zou/app/blueprints/projects/__init__.py +++ b/zou/app/blueprints/projects/__init__.py @@ -8,6 +8,8 @@ ProductionTeamRemoveResource, ProductionAssetTypeResource, ProductionAssetTypeRemoveResource, + ProductionPreviewBackgroundFileResource, + ProductionPreviewBackgroundFileRemoveResource, ProductionTaskTypeResource, ProductionTaskTypeRemoveResource, ProductionTaskTypesResource, @@ -70,6 +72,14 @@ "/data/projects//settings/status-automations/", ProductionStatusAutomationRemoveResource, ), + ( + "/data/projects//settings/preview-background-files/", + ProductionPreviewBackgroundFileRemoveResource, + ), + ( + "/data/projects//settings/preview-background-files", + ProductionPreviewBackgroundFileResource, + ), ( "/data/projects//metadata-descriptors", ProductionMetadataDescriptorsResource, diff --git a/zou/app/blueprints/projects/resources.py b/zou/app/blueprints/projects/resources.py index f71b0d83d3..c12f4eace0 100644 --- a/zou/app/blueprints/projects/resources.py +++ b/zou/app/blueprints/projects/resources.py @@ -278,7 +278,7 @@ class ProductionTaskTypeResource(Resource, ArgsMixin): @jwt_required() def post(self, project_id): """ - Add an task type linked to a production. + Add a task type linked to a production. --- tags: - Projects @@ -410,7 +410,7 @@ def post(self, project_id): class ProductionTaskStatusRemoveResource(Resource): """ - Allow to remove an task status linked to a production. + Allow to remove a task status linked to a production. """ @jwt_required() @@ -537,6 +537,103 @@ def delete(self, project_id, status_automation_id): return "", 204 +class ProductionPreviewBackgroundFileResource(Resource, ArgsMixin): + """ + Allow to add a preview background file linked to a production. + """ + + @jwt_required() + def get(self, project_id): + """ + Return preview background files linked to a production + --- + tags: + - Projects + parameters: + - in: path + name: project_id + required: true + type: string + format: UUID + x-example: a24a6ea4-ce75-4665-a070-57453082c25 + responses: + 200: + description: Preview background files linked to production + """ + user_service.check_project_access(project_id) + return projects_service.get_project_preview_background_files( + project_id + ) + + @jwt_required() + def post(self, project_id): + """ + Add a preview background file linked to a production. + --- + tags: + - Projects + parameters: + - in: path + name: project_id + required: true + type: string + format: UUID + x-example: a24a6ea4-ce75-4665-a070-57453082c25 + - in: formData + name: preview_background_file_id + required: True + type: string + format: UUID + x-example: a24a6ea4-ce75-4665-a070-57453082c25 + responses: + 201: + description: Preview background file added to production + """ + args = self.get_args([("preview_background_file_id", "", True)]) + + user_service.check_manager_project_access(project_id) + project = projects_service.add_preview_background_file_setting( + project_id, args["preview_background_file_id"] + ) + return project, 201 + + +class ProductionPreviewBackgroundFileRemoveResource(Resource): + """ + Allow to remove a preview background file linked to a production. + """ + + @jwt_required() + def delete(self, project_id, preview_background_file_id): + """ + Remove a preview background file from a production. + --- + tags: + - Projects + parameters: + - in: path + name: project_id + required: true + type: string + format: UUID + x-example: a24a6ea4-ce75-4665-a070-57453082c25 + - in: path + name: preview_background_file_id + required: true + type: string + format: UUID + x-example: a24a6ea4-ce75-4665-a070-57453082c25 + responses: + 204: + description: Preview background file removed from production + """ + user_service.check_manager_project_access(project_id) + projects_service.remove_preview_background_file_setting( + project_id, preview_background_file_id + ) + return "", 204 + + class ProductionMetadataDescriptorsResource(Resource, ArgsMixin): """ Resource to get and create metadata descriptors. It serves to describe diff --git a/zou/app/models/preview_background_file.py b/zou/app/models/preview_background_file.py new file mode 100644 index 0000000000..232e83967a --- /dev/null +++ b/zou/app/models/preview_background_file.py @@ -0,0 +1,16 @@ +from zou.app import db +from zou.app.models.serializer import SerializerMixin +from zou.app.models.base import BaseMixin + + +class PreviewBackgroundFile(db.Model, BaseMixin, SerializerMixin): + """ + Describe a preview background file. + """ + + name = db.Column(db.String(40), nullable=False) + archived = db.Column(db.Boolean(), default=False) + is_default = db.Column(db.Boolean(), default=False, index=True) + original_name = db.Column(db.String(250)) + extension = db.Column(db.String(6)) + file_size = db.Column(db.BigInteger(), default=0) diff --git a/zou/app/models/project.py b/zou/app/models/project.py index 5fcf92e806..a9bd03095b 100644 --- a/zou/app/models/project.py +++ b/zou/app/models/project.py @@ -88,6 +88,18 @@ class ProjectStatusAutomationLink(db.Model): ) +class ProjectPreviewBackgroundFileLink(db.Model): + __tablename__ = "project_preview_background_file_link" + project_id = db.Column( + UUIDType(binary=False), db.ForeignKey("project.id"), primary_key=True + ) + preview_background_file_id = db.Column( + UUIDType(binary=False), + db.ForeignKey("preview_background_file.id"), + primary_key=True, + ) + + class Project(db.Model, BaseMixin, SerializerMixin): """ Describes a production the studio works on. @@ -113,6 +125,12 @@ class Project(db.Model, BaseMixin, SerializerMixin): max_retakes = db.Column(db.Integer, default=0) is_clients_isolated = db.Column(db.Boolean(), default=False) is_preview_download_allowed = db.Column(db.Boolean(), default=False) + default_preview_background_file_id = db.Column( + UUIDType(binary=False), + db.ForeignKey("preview_background_file.id"), + default=None, + index=True, + ) project_status_id = db.Column( UUIDType(binary=False), db.ForeignKey("project_status.id"), index=True @@ -131,6 +149,10 @@ class Project(db.Model, BaseMixin, SerializerMixin): status_automations = db.relationship( "StatusAutomation", secondary="project_status_automation_link" ) + preview_background_files = db.relationship( + "PreviewBackgroundFile", + secondary="project_preview_background_file_link", + ) def set_team(self, person_ids): for person_id in person_ids: @@ -170,6 +192,14 @@ def set_status_automations(self, status_automation_ids): "status_automation_id", ) + def set_preview_background_files(self, preview_background_files_ids): + return self.set_links( + preview_background_files_ids, + ProjectPreviewBackgroundFileLink, + "project_id", + "preview_background_file_id", + ) + @classmethod def create_from_import(cls, data): is_update = False @@ -182,6 +212,9 @@ def create_from_import(cls, data): task_status_ids = data.pop("task_statuses", None) asset_type_ids = data.pop("asset_types", None) status_automation_ids = data.pop("status_automations", None) + preview_background_files_ids = data.pop( + "preview_background_files", None + ) if previous_project is None: previous_project = cls.create(**data) @@ -206,4 +239,9 @@ def create_from_import(cls, data): if status_automation_ids is not None: previous_project.set_status_automations(status_automation_ids) + if preview_background_files_ids is not None: + previous_project.set_preview_background_files( + preview_background_files_ids + ) + return (previous_project, is_update) diff --git a/zou/app/services/deletion_service.py b/zou/app/services/deletion_service.py index 726918e5ee..1a7064e859 100644 --- a/zou/app/services/deletion_service.py +++ b/zou/app/services/deletion_service.py @@ -15,6 +15,7 @@ from zou.app.models.output_file import OutputFile from zou.app.models.person import Person from zou.app.models.playlist import Playlist +from zou.app.models.preview_background_file import PreviewBackgroundFile from zou.app.models.preview_file import PreviewFile from zou.app.models.project import Project from zou.app.models.schedule_item import ScheduleItem @@ -176,6 +177,28 @@ def remove_preview_file(preview_file): return preview_file.serialize() +def remove_preview_background_file_by_id(preview_background_file_id): + """ + Remove all files related to given preview background file, then remove the + preview background file entry from the database. + """ + preview_background_file = PreviewBackgroundFile.get( + preview_background_file_id + ) + return remove_preview_background_file(preview_background_file) + + +def remove_preview_background_file(preview_background_file): + """ + Remove all files related to given preview background file, then remove the + preview background file entry from the database. + """ + + clear_preview_background_files(preview_background_file.id) + preview_background_file.delete() + return preview_background_file.serialize() + + def remove_attachment_file_by_id(attachment_file_id): """ Remove all files related to given attachment file, then remove the @@ -198,6 +221,20 @@ def remove_attachment_file(attachment_file): return attachment_dict +def clear_preview_background_files(preview_background_id): + """ + Remove all files related to given preview background file. + """ + for image_type in [ + "thumbnails", + "preview-backgrounds", + ]: + try: + file_store.remove_picture(image_type, preview_background_id) + except BaseException: + pass + + def clear_picture_files(preview_file_id): """ Remove all files related to given preview file, supposing the original file diff --git a/zou/app/services/exception.py b/zou/app/services/exception.py index e75e451e50..7d1447e04e 100644 --- a/zou/app/services/exception.py +++ b/zou/app/services/exception.py @@ -37,6 +37,10 @@ class TaskNotFoundException(NotFound): pass +class PreviewBackgroundFileNotFoundException(NotFound): + pass + + class DepartmentNotFoundException(NotFound): pass diff --git a/zou/app/services/files_service.py b/zou/app/services/files_service.py index c6eaf12722..70905adea0 100644 --- a/zou/app/services/files_service.py +++ b/zou/app/services/files_service.py @@ -8,9 +8,11 @@ from zou.app.models.output_file import OutputFile from zou.app.models.output_type import OutputType from zou.app.models.preview_file import PreviewFile +from zou.app.models.preview_background_file import PreviewBackgroundFile from zou.app.models.software import Software from zou.app.models.task import Task + from zou.app.services import entities_service from zou.app.services.base_service import ( get_instance, @@ -25,6 +27,7 @@ SoftwareNotFoundException, NoOutputFileException, EntryAlreadyExistsException, + PreviewBackgroundFileNotFoundException, ) from zou.app.utils import cache, fields, events, query as query_utils @@ -839,3 +842,65 @@ def get_preview_files_for_project(project_id, page=-1): .order_by(desc(PreviewFile.updated_at)) ) return query_utils.get_paginated_results(query, page) + + +def clear_preview_background_file_cache(preview_background_file_id): + cache.cache.delete_memoized( + get_preview_background_file, preview_background_file_id + ) + cache.cache.delete_memoized(get_preview_background_files) + + +def get_preview_background_file_raw(preview_background_file_id): + """ + Get preview background file matching given id as an active record. + """ + try: + preview_background = PreviewBackgroundFile.get( + preview_background_file_id + ) + except StatementError: + raise PreviewBackgroundFileNotFoundException + + if preview_background is None: + raise PreviewBackgroundFileNotFoundException + + return preview_background + + +@cache.memoize_function(1200) +def get_preview_background_file(preview_background_file_id): + """ + Get preview background file matching given id as a dictionary. + """ + return get_preview_background_file_raw( + preview_background_file_id + ).serialize() + + +@cache.memoize_function(120) +def get_preview_background_files(): + """ + Get all preview backgrounds files. + """ + return fields.serialize_models(PreviewBackgroundFile.get_all()) + + +def reset_default_preview_background_files(preview_background_file_id): + """ + Set all preview background files as is_default=False except the one matching given id. + """ + PreviewBackgroundFile.query.filter( + PreviewBackgroundFile.id != preview_background_file_id, + PreviewBackgroundFile.is_default == True, + ).update({"is_default": False}) + PreviewBackgroundFile.commit() + + +def update_preview_background_file(preview_background_file_id, data): + preview_background_file = get_preview_background_file_raw( + preview_background_file_id + ) + preview_background_file.update(data) + clear_preview_background_file_cache(preview_background_file_id) + return preview_background_file.serialize() diff --git a/zou/app/services/preview_files_service.py b/zou/app/services/preview_files_service.py index 068c3a9477..6a4250ec42 100644 --- a/zou/app/services/preview_files_service.py +++ b/zou/app/services/preview_files_service.py @@ -313,14 +313,14 @@ def save_variants(preview_file_id, original_picture_path): return variants -def clear_variant_from_cache(preview_file_id, prefix): +def clear_variant_from_cache(preview_file_id, prefix, extension="png"): """ Clear a variant from the cache to force to redownload from object storage. """ if config.FS_BACKEND != "local": file_path = os.path.join( config.TMP_DIR, - "cache-%s-%s.%s" % (prefix, preview_file_id, "png"), + "cache-%s-%s.%s" % (prefix, preview_file_id, extension), ) if os.path.exists(file_path): os.remove(file_path) diff --git a/zou/app/services/projects_service.py b/zou/app/services/projects_service.py index d3b0f18266..369be8b15f 100644 --- a/zou/app/services/projects_service.py +++ b/zou/app/services/projects_service.py @@ -1,5 +1,6 @@ import slugify +from zou.app.models.preview_background_file import PreviewBackgroundFile from zou.app.models.entity import Entity from zou.app.models.entity_type import EntityType from zou.app.models.metadata_descriptor import MetadataDescriptor @@ -25,6 +26,7 @@ MetadataDescriptorNotFoundException, DepartmentNotFoundException, WrongParameterException, + PreviewBackgroundFileNotFoundException, ) from zou.app.utils import fields, events, cache @@ -385,6 +387,34 @@ def remove_status_automation_setting(project_id, status_automation_id): ) +def add_preview_background_file_setting( + project_id, preview_background_file_id +): + """ + Add a preview background file listed in database to the project backgrounds. + """ + return _add_to_list_attr( + project_id, + PreviewBackgroundFile, + preview_background_file_id, + "preview_background_files", + ) + + +def remove_preview_background_file_setting( + project_id, preview_background_file_id +): + """ + Remove a preview background file listed in database from the project backgrounds. + """ + return _remove_from_list_attr( + project_id, + PreviewBackgroundFile, + preview_background_file_id, + "preview_background_files", + ) + + def _add_to_list_attr(project_id, model_class, model_id, list_attr): project = get_project_raw(project_id) model = model_class.get(model_id) @@ -610,6 +640,11 @@ def get_project_status_automations(project_id): return Project.serialize_list(project.status_automations) +def get_project_preview_background_files(project_id): + project = get_project_raw(project_id) + return Project.serialize_list(project.preview_background_files) + + def get_project_fps(project_id): """ Return fps set at project level or default fps if it not set. diff --git a/zou/app/services/sync_service.py b/zou/app/services/sync_service.py index 844f97f64e..e961b397df 100644 --- a/zou/app/services/sync_service.py +++ b/zou/app/services/sync_service.py @@ -24,6 +24,7 @@ from zou.app.models.notification import Notification from zou.app.models.person import Person from zou.app.models.playlist import Playlist +from zou.app.models.preview_background_file import PreviewBackgroundFile from zou.app.models.preview_file import PreviewFile from zou.app.models.project import Project from zou.app.models.project_status import ProjectStatus @@ -36,7 +37,7 @@ from zou.app.models.task_type import TaskType from zou.app.models.time_spent import TimeSpent -from zou.app.services import deletion_service, tasks_service +from zou.app.services import deletion_service, tasks_service, projects_service from zou.app.stores import file_store from flask_fs.backends.local import LocalBackend from zou.app.utils import events @@ -84,6 +85,7 @@ "notification": Notification, "person": Person, "playlist": Playlist, + "preview-background-file": PreviewBackgroundFile, "preview-file": PreviewFile, "project": Project, "project-status": ProjectStatus, @@ -120,6 +122,7 @@ "organisation": "organisations", "person": "persons", "playlist": "playlists", + "preview-background-file": "preview-background-files", "preview-file": "preview-files", "project": "projects", "project-status": "project-status", @@ -175,6 +178,7 @@ ] file_events = [ + "preview-background-file:add-file", "preview-file:add-file", "organisation:set-thumbnail", "person:set-thumbnail", @@ -320,6 +324,14 @@ def run_last_events_files(minutes=0, page_size=50): preview_file = PreviewFile.get(event["data"]["preview_file_id"]) if preview_file is not None: download_preview_from_another_instance(preview_file) + elif event_name in ["preview-background-file"]: + preview_background_file = PreviewBackgroundFile.get( + event["data"]["preview_background_file_id"] + ) + if preview_background_file is not None: + download_preview_background_from_another_instance( + preview_background_file + ) else: download_thumbnail_from_another_instance( event_name, event["data"]["%s_id" % event_name] @@ -603,7 +615,12 @@ def add_file_listeners(event_client): Add new preview event listener. """ gazu.events.add_listener( - event_client, "preview-file:add-file", retrieve_file + event_client, "preview-file:add-file", retrieve_preview_file + ) + gazu.events.add_listener( + event_client, + "preview-background-file:add-file", + retrieve_preview_background_file, ) for model_name in thumbnail_events: gazu.events.add_listener( @@ -613,7 +630,7 @@ def add_file_listeners(event_client): ) -def retrieve_file(data): +def retrieve_preview_file(data): if data.get("sync", False): return try: @@ -629,6 +646,32 @@ def retrieve_file(data): logger.error("Fail to dowonload preview file: %s" % (preview_file_id)) +def retrieve_preview_background_file(data): + if data.get("sync", False): + return + try: + preview_background_file_id = data["preview_background_file_id"] + preview_background_file = PreviewBackgroundFile.get( + preview_background_file_id + ) + download_preview_background_from_another_instance( + preview_background_file + ) + forward_event( + {"name": "preview-background-file:add-file", "data": data} + ) + logger.info( + "Preview background file and related downloaded: %s" + % preview_background_file_id + ) + except gazu.exception.RouteNotFoundException as e: + logger.error("Route not found: %s" % e) + logger.error( + "Fail to dowonload preview background file: %s" + % (preview_background_file_id) + ) + + def get_retrieve_thumbnail(model_name): def retrieve_thumbnail(data): if data.get("sync", False): @@ -766,6 +809,9 @@ def download_files_from_another_instance( download_preview_files_from_another_instance( project=project, pool=pool, number_attemps=number_attemps ) + download_preview_background_files_from_another_instance( + project=project, pool=pool, number_attemps=number_attemps + ) download_attachment_files_from_another_instance( project=project, pool=pool, number_attemps=number_attemps ) @@ -866,6 +912,31 @@ def download_preview_files_from_another_instance( ) +def download_preview_background_files_from_another_instance( + project=None, pool=None, number_attemps=3 +): + """ + Download all preview background files and related. + """ + if project: + project_dict = gazu.project.get_project_by_name(project) + project = projects_service.get_project_raw(project_dict["id"]) + preview_background_files = project.preview_background_files + else: + preview_background_files = PreviewBackgroundFile.query.all() + + for preview_background_file in preview_background_files: + if pool is None: + download_preview_background_from_another_instance( + preview_background_file, number_attemps + ) + else: + pool.apply_async( + download_preview_background_from_another_instance, + (preview_background_file, number_attemps), + ) + + def download_preview_from_another_instance(preview_file, number_attemps=3): """ Download all files link to preview file entry: orginal file and variants. @@ -884,7 +955,7 @@ def download_preview_from_another_instance(preview_file, number_attemps=3): "previews", ]: if not file_store.exists_picture(prefix, preview_file_id): - download_file_from_another_instance( + download_preview_file_from_another_instance( file_store.add_picture, prefix, preview_file_id, @@ -895,7 +966,7 @@ def download_preview_from_another_instance(preview_file, number_attemps=3): if is_movie: for prefix in ["low", "previews"]: if not file_store.exists_movie(prefix, preview_file_id): - download_file_from_another_instance( + download_preview_file_from_another_instance( file_store.add_movie, prefix, preview_file_id, @@ -905,7 +976,7 @@ def download_preview_from_another_instance(preview_file, number_attemps=3): elif is_file: if not file_store.exists_file("previews", preview_file_id): - download_file_from_another_instance( + download_preview_file_from_another_instance( file_store.add_file, "previews", preview_file_id, @@ -914,7 +985,33 @@ def download_preview_from_another_instance(preview_file, number_attemps=3): ) -def download_file_from_another_instance( +def download_preview_background_from_another_instance( + preview_background, number_attemps=3 +): + """ + Download all files link to preview background file entry. + """ + extension = preview_background.extension + + preview_background_file_id = str(preview_background.id) + with app.app_context(): + for prefix in [ + "thumbnails", + "preview-backgrounds", + ]: + if not file_store.exists_picture( + prefix, preview_background_file_id + ): + download_preview_background_file_from_another_instance( + file_store.add_picture, + prefix, + preview_background_file_id, + extension, + number_attemps=number_attemps, + ) + + +def download_preview_file_from_another_instance( save_func, prefix, preview_file_id, extension, number_attemps=3 ): """ @@ -958,6 +1055,46 @@ def download_file_from_another_instance( return path +def download_preview_background_file_from_another_instance( + save_func, prefix, preview_background_file_id, extension, number_attemps=3 +): + """ + Download the preview background file for the given preview from object + storage and store it locally. + """ + if prefix == "preview-backgrounds": + path = f"/pictures/preview-background-files/{preview_background_file_id}.{extension}" + elif prefix == "thumbnails": + path = f"/pictures/thumbnails/preview-background-files/{preview_background_file_id}.png" + + file_path = "/tmp/%s.%s" % (preview_background_file_id, extension) + for attemps_count in range(0, number_attemps): + if attemps_count > 0: + time.sleep(0.5) + try: + response = gazu.client.download(path, file_path) + if response.status_code == 404: + logger.error(f"Not found ({path}).") + elif response.status_code == 500: + logger.error(f"Error while downloading ({path}).") + except Exception: + if attemps_count + 1 == number_attemps: + logger.error(f"Download failed ({path}):") + logger.error(traceback.format_exc()) + if os.path.exists(file_path): + try: + save_func(prefix, preview_background_file_id, file_path) + logger.info(f"Downloaded and uploaded ({path}).") + break + except Exception: + if attemps_count + 1 == number_attemps: + logger.error(f"Upload failed ({path}):") + logger.error(traceback.format_exc()) + finally: + os.remove(file_path) + return path + + def download_attachment_files_from_another_instance( project=None, pool=None, number_attemps=3 ): diff --git a/zou/app/services/user_service.py b/zou/app/services/user_service.py index 7435111d41..c19cfc2675 100644 --- a/zou/app/services/user_service.py +++ b/zou/app/services/user_service.py @@ -26,6 +26,7 @@ shots_service, status_automations_service, tasks_service, + files_service, ) from zou.app.services.exception import ( SearchFilterNotFoundException, @@ -1274,6 +1275,7 @@ def get_context(): task_status_list = tasks_service.get_task_statuses() search_filters = get_filters() search_filter_groups = get_filter_groups() + preview_background_files = files_service.get_preview_background_files() return { "asset_types": asset_types, @@ -1288,4 +1290,5 @@ def get_context(): "task_status": task_status_list, "search_filters": search_filters, "search_filter_groups": search_filter_groups, + "preview_background_files": preview_background_files, } diff --git a/zou/app/utils/thumbnail.py b/zou/app/utils/thumbnail.py index 1acbd49710..83fe9cace3 100644 --- a/zou/app/utils/thumbnail.py +++ b/zou/app/utils/thumbnail.py @@ -1,11 +1,16 @@ import os import shutil import math +import cv2 +import numpy -from zou.app.utils import fs +from pathlib import Path from PIL import Image, ImageFile +from zou.app.utils import fs + + ImageFile.LOAD_TRUNCATED_IMAGES = True Image.MAX_IMAGE_PIXELS = 20000 * 20000 @@ -13,6 +18,7 @@ SQUARE_SIZE = 100, 100 PREVIEW_SIZE = 1200, 0 BIG_SQUARE_SIZE = 400, 400 +BIG_RECTANGLE_SIZE = 300, 200 def save_file(tmp_folder, instance_id, file_to_save): @@ -35,10 +41,7 @@ def convert_jpg_to_png(file_source_path): """ Convert .jpg file located at given path into a .png file with same name. """ - folder_path = os.path.dirname(file_source_path) - file_source_name = os.path.basename(file_source_path) - file_target_name = "%s.png" % file_source_name[:-4] - file_target_path = os.path.join(folder_path, file_target_name) + file_target_path = Path(file_source_path).with_suffix(".png") im = Image.open(file_source_path) if im.mode == "CMYK": @@ -210,3 +213,20 @@ def flat(*nums): Turn into an int tuple an a enumerable of numbers. """ return tuple(int(round(n)) for n in nums) + + +def turn_hdr_into_thumbnail(original_path, size=BIG_RECTANGLE_SIZE): + """ + Turn an HDR file into a thumbnail. + """ + file_target_path = Path(original_path).with_suffix(".png") + hdr = cv2.imread(original_path, flags=cv2.IMREAD_ANYDEPTH) + # Simply clamp values to a 0-1 range for tone-mapping + ldr = numpy.clip(hdr, 0, 1) + # Color space conversion + ldr = ldr ** (1 / 2.2) + # 0-255 remapping for bit-depth conversion + ldr = 255.0 * ldr + cv2.imwrite(str(file_target_path), ldr) + turn_into_thumbnail(file_target_path, size) + return file_target_path diff --git a/zou/migrations/versions/c49e41f1298b_add_previewbackground.py b/zou/migrations/versions/c49e41f1298b_add_previewbackground.py new file mode 100644 index 0000000000..625bd56207 --- /dev/null +++ b/zou/migrations/versions/c49e41f1298b_add_previewbackground.py @@ -0,0 +1,119 @@ +"""Add PreviewBackground + +Revision ID: c49e41f1298b +Revises: 7748d3d22925 +Create Date: 2023-11-09 13:40:21.446542 + +""" +from alembic import op +import sqlalchemy as sa +import sqlalchemy_utils +import sqlalchemy_utils +import uuid + +# revision identifiers, used by Alembic. +revision = "c49e41f1298b" +down_revision = "7748d3d22925" +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table( + "preview_background_file", + sa.Column("name", sa.String(length=40), nullable=False), + sa.Column("archived", sa.Boolean(), nullable=True), + sa.Column("is_default", sa.Boolean(), nullable=True), + sa.Column("original_name", sa.String(length=250), nullable=True), + sa.Column("extension", sa.String(length=6), nullable=True), + sa.Column("file_size", sa.BigInteger(), nullable=True), + sa.Column( + "id", + sqlalchemy_utils.types.uuid.UUIDType(binary=False), + default=uuid.uuid4, + nullable=False, + ), + sa.Column("created_at", sa.DateTime(), nullable=True), + sa.Column("updated_at", sa.DateTime(), nullable=True), + sa.PrimaryKeyConstraint("id"), + ) + with op.batch_alter_table( + "preview_background_file", schema=None + ) as batch_op: + batch_op.create_index( + batch_op.f("ix_preview_background_file_is_default"), + ["is_default"], + unique=False, + ) + + op.create_table( + "project_preview_background_file_link", + sa.Column( + "project_id", + sqlalchemy_utils.types.uuid.UUIDType(binary=False), + default=uuid.uuid4, + nullable=False, + ), + sa.Column( + "preview_background_file_id", + sqlalchemy_utils.types.uuid.UUIDType(binary=False), + default=uuid.uuid4, + nullable=False, + ), + sa.ForeignKeyConstraint( + ["preview_background_file_id"], + ["preview_background_file.id"], + ), + sa.ForeignKeyConstraint( + ["project_id"], + ["project.id"], + ), + sa.PrimaryKeyConstraint("project_id", "preview_background_file_id"), + ) + with op.batch_alter_table("project", schema=None) as batch_op: + batch_op.add_column( + sa.Column( + "default_preview_background_file_id", + sqlalchemy_utils.types.uuid.UUIDType(binary=False), + default=uuid.uuid4, + nullable=True, + ) + ) + batch_op.create_index( + batch_op.f("ix_project_default_preview_background_file_id"), + ["default_preview_background_file_id"], + unique=False, + ) + batch_op.create_foreign_key( + None, + "preview_background_file", + ["default_preview_background_file_id"], + ["id"], + ) + + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table("project", schema=None) as batch_op: + batch_op.drop_constraint( + "project_default_preview_background_file_id_fkey", + type_="foreignkey", + ) + batch_op.drop_index( + batch_op.f("ix_project_default_preview_background_file_id") + ) + batch_op.drop_column("default_preview_background_file_id") + + op.drop_table("project_preview_background_file_link") + with op.batch_alter_table( + "preview_background_file", schema=None + ) as batch_op: + batch_op.drop_index( + batch_op.f("ix_preview_background_file_is_default") + ) + + op.drop_table("preview_background_file") + # ### end Alembic commands ### From 831489ab53a7b6e81817d9a9e45f501b190cf8a2 Mon Sep 17 00:00:00 2001 From: Evan Blaudy Date: Mon, 13 Nov 2023 14:52:59 +0100 Subject: [PATCH 3/3] [requirements] upgrade some libs --- setup.cfg | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.cfg b/setup.cfg index 6009221b9a..6fead76946 100644 --- a/setup.cfg +++ b/setup.cfg @@ -100,10 +100,10 @@ test = monitoring = prometheus-flask-exporter==0.23.0 pygelf==0.4.2 - sentry-sdk==1.34.0 + sentry-sdk==1.35.0 lint = - black==23.10.1 + black==23.11.0 pre-commit==3.5.0 [options.entry_points]