mirror of
https://github.com/Alexander-D-Karpov/akarpov
synced 2024-11-29 02:53:43 +03:00
added file preview generation for Docker, base model to generate cropped images
This commit is contained in:
parent
359e2929fc
commit
91b851805f
|
@ -1,33 +1,10 @@
|
|||
from django.db.models.signals import post_save, pre_delete, pre_save
|
||||
from django.db.models.signals import pre_delete, pre_save
|
||||
from django.dispatch import receiver
|
||||
|
||||
from akarpov.blog.models import Post, PostRating, Tag
|
||||
from akarpov.common.tasks import crop_model_image
|
||||
from akarpov.blog.models import PostRating, Tag
|
||||
from akarpov.utils.generators import generate_hex_color
|
||||
|
||||
|
||||
@receiver(pre_save, sender=Post)
|
||||
def post_on_save(sender, instance: Post, **kwargs):
|
||||
if instance.id:
|
||||
previous = Post.objects.get(id=instance.id)
|
||||
if (
|
||||
previous.image != instance.image
|
||||
and kwargs["update_fields"] != frozenset({"image_cropped"})
|
||||
and instance
|
||||
):
|
||||
if instance.image:
|
||||
crop_model_image.apply_async(
|
||||
kwargs={
|
||||
"pk": instance.pk,
|
||||
"app_label": "blog",
|
||||
"model_name": "Post",
|
||||
},
|
||||
countdown=2,
|
||||
)
|
||||
else:
|
||||
instance.image_cropped = None
|
||||
|
||||
|
||||
@receiver(pre_save, sender=Tag)
|
||||
def tag_create(sender, instance: Tag, **kwargs):
|
||||
if instance.id is None:
|
||||
|
@ -71,17 +48,3 @@ def post_rating_delete(sender, instance: PostRating, **kwargs):
|
|||
post.rating += 1
|
||||
post.rating_down -= 1
|
||||
post.save()
|
||||
|
||||
|
||||
@receiver(post_save, sender=Post)
|
||||
def post_on_create(sender, instance: Post, created, **kwargs):
|
||||
if created:
|
||||
if instance.image:
|
||||
crop_model_image.apply_async(
|
||||
kwargs={
|
||||
"pk": instance.pk,
|
||||
"app_label": "blog",
|
||||
"model_name": "Post",
|
||||
},
|
||||
countdown=2,
|
||||
)
|
||||
|
|
|
@ -1,13 +1,73 @@
|
|||
import os
|
||||
|
||||
from django.db import models
|
||||
|
||||
from akarpov.common.tasks import crop_model_image
|
||||
from akarpov.utils.files import user_file_upload_mixin
|
||||
from akarpov.utils.generators import generate_charset
|
||||
|
||||
|
||||
def create_cropped_model_image(sender, instance, created, **kwargs):
|
||||
model = sender
|
||||
if created:
|
||||
if instance.image:
|
||||
crop_model_image.apply_async(
|
||||
kwargs={
|
||||
"pk": instance.pk,
|
||||
"app_label": model._meta.app_label,
|
||||
"model_name": model._meta.model_name,
|
||||
},
|
||||
countdown=2,
|
||||
)
|
||||
|
||||
|
||||
def update_cropped_model_image(sender, instance, **kwargs):
|
||||
model = sender
|
||||
if instance.id:
|
||||
previous = model.objects.get(id=instance.id)
|
||||
if previous.image != instance.image:
|
||||
# delete previous cropped image
|
||||
if instance.image_cropped:
|
||||
if os.path.isfile(instance.image_cropped.path):
|
||||
os.remove(instance.image_cropped.path)
|
||||
# run task to create new cropped image
|
||||
if kwargs["update_fields"] != frozenset({"image_cropped"}) and instance:
|
||||
if instance.image:
|
||||
crop_model_image.apply_async(
|
||||
kwargs={
|
||||
"pk": instance.pk,
|
||||
"app_label": model._meta.app_label,
|
||||
"model_name": model._meta.model_name,
|
||||
},
|
||||
countdown=2,
|
||||
)
|
||||
else:
|
||||
instance.image_cropped = None
|
||||
|
||||
|
||||
def delete_cropped_model_image(sender, instance, **kwargs):
|
||||
print(instance.image_cropped)
|
||||
if instance.image_cropped:
|
||||
if os.path.isfile(instance.image_cropped.path):
|
||||
os.remove(instance.image_cropped.path)
|
||||
|
||||
|
||||
class BaseImageModel(models.Model):
|
||||
"""
|
||||
stores user's images in their media folder, creates, updates and deletes preview
|
||||
requires celery to run
|
||||
"""
|
||||
|
||||
image = models.ImageField(upload_to=user_file_upload_mixin, blank=True)
|
||||
image_cropped = models.ImageField(upload_to="cropped/", blank=True)
|
||||
|
||||
@classmethod
|
||||
def __init_subclass__(cls, **kwargs):
|
||||
super().__init_subclass__(**kwargs)
|
||||
models.signals.pre_save.connect(update_cropped_model_image, sender=cls)
|
||||
models.signals.post_save.connect(create_cropped_model_image, sender=cls)
|
||||
models.signals.post_delete.connect(delete_cropped_model_image, sender=cls)
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
from django.contrib import admin
|
||||
|
||||
from akarpov.files.models import BaseFile, Folder
|
||||
from akarpov.files.models import File, Folder
|
||||
|
||||
admin.site.register(BaseFile)
|
||||
admin.site.register(File)
|
||||
admin.site.register(Folder)
|
||||
|
|
|
@ -0,0 +1,108 @@
|
|||
# Generated by Django 4.1.7 on 2023-03-24 13:50
|
||||
|
||||
import akarpov.utils.files
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import django.utils.timezone
|
||||
import model_utils.fields
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("shortener", "0001_initial"),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
("files", "0006_alter_basefile_slug"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="File",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("slug", models.SlugField(blank=True, max_length=20, unique=True)),
|
||||
(
|
||||
"created",
|
||||
model_utils.fields.AutoCreatedField(
|
||||
default=django.utils.timezone.now,
|
||||
editable=False,
|
||||
verbose_name="created",
|
||||
),
|
||||
),
|
||||
(
|
||||
"modified",
|
||||
model_utils.fields.AutoLastModifiedField(
|
||||
default=django.utils.timezone.now,
|
||||
editable=False,
|
||||
verbose_name="modified",
|
||||
),
|
||||
),
|
||||
("name", models.CharField(max_length=100)),
|
||||
("description", models.TextField(blank=True)),
|
||||
("private", models.BooleanField(default=True)),
|
||||
("preview", models.FileField(blank=True, upload_to="file/previews/")),
|
||||
(
|
||||
"file",
|
||||
models.FileField(
|
||||
upload_to=akarpov.utils.files.user_file_upload_mixin
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"abstract": False,
|
||||
},
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="folder",
|
||||
name="parent",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="children",
|
||||
to="files.folder",
|
||||
),
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name="BaseFile",
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="file",
|
||||
name="folder",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="files",
|
||||
to="files.folder",
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="file",
|
||||
name="short_link",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
to="shortener.link",
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="file",
|
||||
name="user",
|
||||
field=models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="files",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
]
|
|
@ -14,7 +14,7 @@
|
|||
from akarpov.utils.files import user_file_upload_mixin
|
||||
|
||||
|
||||
class BaseFile(TimeStampedModel, ShortLink):
|
||||
class File(TimeStampedModel, ShortLink):
|
||||
"""model to store user's files"""
|
||||
|
||||
name = CharField(max_length=100)
|
||||
|
@ -27,6 +27,7 @@ class BaseFile(TimeStampedModel, ShortLink):
|
|||
"files.Folder", related_name="files", blank=True, null=True, on_delete=CASCADE
|
||||
)
|
||||
|
||||
preview = FileField(blank=True, upload_to="file/previews/")
|
||||
file = FileField(blank=False, upload_to=user_file_upload_mixin)
|
||||
|
||||
def get_absolute_url(self):
|
||||
|
@ -41,7 +42,9 @@ class Folder(TimeStampedModel, ShortLink):
|
|||
slug = SlugField(max_length=20, blank=True)
|
||||
|
||||
user = ForeignKey("users.User", related_name="files_folders", on_delete=CASCADE)
|
||||
parent = ForeignKey("self", blank=True, related_name="children", on_delete=CASCADE)
|
||||
parent = ForeignKey(
|
||||
"self", null=True, blank=True, related_name="children", on_delete=CASCADE
|
||||
)
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse("files:folder", kwargs={"slug": self.slug})
|
||||
|
|
|
@ -76,5 +76,5 @@ def _font_points_to_pixels(pt):
|
|||
def create_preview(file_path: str) -> str:
|
||||
# TODO: add text image generation/code image
|
||||
manager = PreviewManager(cache_path, create_folder=True)
|
||||
path_to_preview_image = manager.get_jpeg_preview(file_path)
|
||||
path_to_preview_image = manager.get_pdf_preview(file_path)
|
||||
return path_to_preview_image
|
||||
|
|
16
akarpov/files/signals.py
Normal file
16
akarpov/files/signals.py
Normal file
|
@ -0,0 +1,16 @@
|
|||
from django.db.models.signals import post_save
|
||||
from django.dispatch import receiver
|
||||
|
||||
from akarpov.files.models import File
|
||||
from akarpov.files.tasks import generate_file_review
|
||||
|
||||
|
||||
@receiver(post_save, sender=File)
|
||||
def post_on_create(sender, instance: File, created, **kwargs):
|
||||
if created:
|
||||
generate_file_review.apply_async(
|
||||
kwargs={
|
||||
"pk": instance.pk,
|
||||
},
|
||||
countdown=2,
|
||||
)
|
19
akarpov/files/tasks.py
Normal file
19
akarpov/files/tasks.py
Normal file
|
@ -0,0 +1,19 @@
|
|||
from celery import shared_task
|
||||
from django.core.files import File
|
||||
|
||||
from akarpov.files.models import File as FileModel
|
||||
from akarpov.files.services.preview import create_preview
|
||||
|
||||
|
||||
@shared_task()
|
||||
def generate_file_review(pk: int):
|
||||
file = FileModel.objects.get(pk=pk)
|
||||
pth = create_preview(file.file)
|
||||
with open(pth, "rb") as f:
|
||||
file.preview.save(
|
||||
pth.split("/")[-1],
|
||||
File(f.read()),
|
||||
save=False,
|
||||
)
|
||||
file.save(update_fields=["preview"])
|
||||
return pk
|
|
@ -1,11 +1,11 @@
|
|||
from django.views.generic import DetailView
|
||||
|
||||
from akarpov.files.models import BaseFile, Folder
|
||||
from akarpov.files.models import File, Folder
|
||||
|
||||
|
||||
class FileView(DetailView):
|
||||
template_name = "files/view.html"
|
||||
model = BaseFile
|
||||
model = File
|
||||
slug_field = "slug"
|
||||
|
||||
|
||||
|
|
|
@ -1,41 +0,0 @@
|
|||
from django.db.models.signals import post_save, pre_save
|
||||
from django.dispatch import receiver
|
||||
|
||||
from akarpov.common.tasks import crop_model_image
|
||||
from akarpov.test_platform.models import Form
|
||||
|
||||
|
||||
@receiver(post_save, sender=Form)
|
||||
def form_on_create(sender, instance: Form, created, **kwargs):
|
||||
if created:
|
||||
if instance.image:
|
||||
crop_model_image.apply_async(
|
||||
kwargs={
|
||||
"pk": instance.pk,
|
||||
"app_label": "test_platform",
|
||||
"model_name": "Form",
|
||||
},
|
||||
countdown=2,
|
||||
)
|
||||
|
||||
|
||||
@receiver(pre_save, sender=Form)
|
||||
def form_on_save(sender, instance: Form, **kwargs):
|
||||
if instance.id:
|
||||
previous = Form.objects.get(id=instance.id)
|
||||
if (
|
||||
previous.image != instance.image
|
||||
and kwargs["update_fields"] != frozenset({"image_cropped"})
|
||||
and instance
|
||||
):
|
||||
if instance.image:
|
||||
crop_model_image.apply_async(
|
||||
kwargs={
|
||||
"pk": instance.pk,
|
||||
"app_label": "test_platform",
|
||||
"model_name": "Form",
|
||||
},
|
||||
countdown=2,
|
||||
)
|
||||
else:
|
||||
instance.image_cropped = None
|
|
@ -1,43 +0,0 @@
|
|||
from django.db.models.signals import post_save, pre_save
|
||||
from django.dispatch import receiver
|
||||
|
||||
from akarpov.common.tasks import crop_model_image
|
||||
from akarpov.users.models import User
|
||||
|
||||
|
||||
@receiver(pre_save, sender=User)
|
||||
def on_change(sender, instance: User, **kwargs):
|
||||
if instance.id is None: # new object will be created
|
||||
pass
|
||||
else:
|
||||
previous = User.objects.get(id=instance.id)
|
||||
if previous.image != instance.image and kwargs["update_fields"] != frozenset(
|
||||
{"image_cropped"}
|
||||
):
|
||||
if instance.image:
|
||||
crop_model_image.apply_async(
|
||||
kwargs={
|
||||
"pk": instance.pk,
|
||||
"app_label": "users",
|
||||
"model_name": "User",
|
||||
},
|
||||
countdown=2,
|
||||
)
|
||||
else:
|
||||
instance.image_cropped = None
|
||||
instance.save()
|
||||
|
||||
|
||||
@receiver(post_save, sender=User)
|
||||
def post_on_create(sender, instance: User, created, **kwargs):
|
||||
if created:
|
||||
if instance.image:
|
||||
if instance.image:
|
||||
crop_model_image.apply_async(
|
||||
kwargs={
|
||||
"pk": instance.pk,
|
||||
"app_label": "users",
|
||||
"model_name": "User",
|
||||
},
|
||||
countdown=2,
|
||||
)
|
|
@ -16,6 +16,7 @@ ARG BUILD_ENVIRONMENT=local
|
|||
ARG APP_HOME=/app
|
||||
|
||||
ENV PYTHONUNBUFFERED 1
|
||||
ENV DRAWIO_VERSION 15.7.3
|
||||
ENV PYTHONDONTWRITEBYTECODE 1
|
||||
ENV BUILD_ENV ${BUILD_ENVIRONMENT}
|
||||
|
||||
|
@ -24,16 +25,21 @@ WORKDIR ${APP_HOME}
|
|||
# Install required system dependencies
|
||||
RUN apt-get update && \
|
||||
apt-get install -y build-essential libpq-dev gettext libmagic-dev libjpeg-dev zlib1g-dev && \
|
||||
# Dependencies for file preview generation
|
||||
# apt-get install -y poppler-utils libfile-mimeinfo-perl libimage-exiftool-perl ghostscript libsecret-1-0 zlib1g-dev libjpeg-dev imagemagick libmagic1 webp libreoffice inkscape ffmpeg xvfb && \
|
||||
apt-get purge -y --auto-remove -o APT:AutoRemove:RecommendsImportant=false && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN curl -sSL https://install.python-poetry.org | python3 -
|
||||
# Dependencies for file preview generation
|
||||
# RUN curl -LO https://github.com/jgraph/drawio-desktop/releases/download/v${DRAWIO_VERSION}/drawio-x86_64-${DRAWIO_VERSION}.AppImage && mv drawio-x86_64-${DRAWIO_VERSION}.AppImage /usr/local/bin/drawio
|
||||
ENV PATH="/root/.local/bin:$PATH"
|
||||
|
||||
RUN poetry config virtualenvs.create false
|
||||
|
||||
COPY ./pyproject.toml ./poetry.lock /app/
|
||||
RUN poetry install --no-root --only main
|
||||
RUN preview --check-dependencies
|
||||
|
||||
|
||||
COPY ./compose/production/django/entrypoint /entrypoint
|
||||
|
|
BIN
models.png
Normal file
BIN
models.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.5 MiB |
222
poetry.lock
generated
222
poetry.lock
generated
|
@ -285,6 +285,48 @@ d = ["aiohttp (>=3.7.4)"]
|
|||
jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"]
|
||||
uvloop = ["uvloop (>=0.15.2)"]
|
||||
|
||||
[[package]]
|
||||
name = "cairocffi"
|
||||
version = "1.5.0"
|
||||
description = "cffi-based cairo bindings for Python"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "cairocffi-1.5.0.tar.gz", hash = "sha256:d105b49009d9b4970a459e38ff030cb5dfc8c8ee231e867d28f77ee9df44495e"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
cffi = ">=1.1.0"
|
||||
|
||||
[package.extras]
|
||||
doc = ["sphinx", "sphinx_rtd_theme"]
|
||||
test = ["flake8", "isort", "numpy", "pikepdf", "pytest"]
|
||||
xcb = ["xcffib (>=0.3.2)"]
|
||||
|
||||
[[package]]
|
||||
name = "cairosvg"
|
||||
version = "2.7.0"
|
||||
description = "A Simple SVG Converter based on Cairo"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.5"
|
||||
files = [
|
||||
{file = "CairoSVG-2.7.0-py3-none-any.whl", hash = "sha256:17cb96423a896258848322a95c80160e714a58f1af3dd73b8e1750994519b9f9"},
|
||||
{file = "CairoSVG-2.7.0.tar.gz", hash = "sha256:ac4dc7c1d38b3a15717db2633a3a383012e0be664c727c911637e6af6a49293c"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
cairocffi = "*"
|
||||
cssselect2 = "*"
|
||||
defusedxml = "*"
|
||||
pillow = "*"
|
||||
tinycss2 = "*"
|
||||
|
||||
[package.extras]
|
||||
doc = ["sphinx", "sphinx-rtd-theme"]
|
||||
test = ["flake8", "isort", "pytest"]
|
||||
|
||||
[[package]]
|
||||
name = "celery"
|
||||
version = "5.2.7"
|
||||
|
@ -767,6 +809,26 @@ test = ["hypothesis (>=1.11.4,!=3.79.2)", "iso8601", "pretend", "pytest (>=6.2.0
|
|||
test-randomorder = ["pytest-randomly"]
|
||||
tox = ["tox"]
|
||||
|
||||
[[package]]
|
||||
name = "cssselect2"
|
||||
version = "0.7.0"
|
||||
description = "CSS selectors for Python ElementTree"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "cssselect2-0.7.0-py3-none-any.whl", hash = "sha256:fd23a65bfd444595913f02fc71f6b286c29261e354c41d722ca7a261a49b5969"},
|
||||
{file = "cssselect2-0.7.0.tar.gz", hash = "sha256:1ccd984dab89fc68955043aca4e1b03e0cf29cad9880f6e28e3ba7a74b14aa5a"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
tinycss2 = "*"
|
||||
webencodings = "*"
|
||||
|
||||
[package.extras]
|
||||
doc = ["sphinx", "sphinx_rtd_theme"]
|
||||
test = ["flake8", "isort", "pytest"]
|
||||
|
||||
[[package]]
|
||||
name = "decorator"
|
||||
version = "5.1.1"
|
||||
|
@ -1539,6 +1601,24 @@ files = [
|
|||
[package.dependencies]
|
||||
python-dateutil = ">=2.4"
|
||||
|
||||
[[package]]
|
||||
name = "ffmpeg-python"
|
||||
version = "0.2.0"
|
||||
description = "Python bindings for FFmpeg - with complex filtering support"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
files = [
|
||||
{file = "ffmpeg-python-0.2.0.tar.gz", hash = "sha256:65225db34627c578ef0e11c8b1eb528bb35e024752f6f10b78c011f6f64c4127"},
|
||||
{file = "ffmpeg_python-0.2.0-py3-none-any.whl", hash = "sha256:ac441a0404e053f8b6a1113a77c0f452f1cfc62f6344a769475ffdc0f56c23c5"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
future = "*"
|
||||
|
||||
[package.extras]
|
||||
dev = ["Sphinx (==2.1.0)", "future (==0.17.1)", "numpy (==1.16.4)", "pytest (==4.6.1)", "pytest-mock (==1.10.4)", "tox (==3.12.1)"]
|
||||
|
||||
[[package]]
|
||||
name = "filelock"
|
||||
version = "3.9.0"
|
||||
|
@ -1622,6 +1702,17 @@ files = [
|
|||
{file = "funcy-1.18.tar.gz", hash = "sha256:15448d19a8ebcc7a585afe7a384a19186d0bd67cbf56fb42cd1fd0f76313f9b2"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "future"
|
||||
version = "0.18.3"
|
||||
description = "Clean single-source support for Python 3 and 2"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
|
||||
files = [
|
||||
{file = "future-0.18.3.tar.gz", hash = "sha256:34a17436ed1e96697a86f9de3d15a3b0be01d8bc8de9c1dffd59fb8234ed5307"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gunicorn"
|
||||
version = "20.1.0"
|
||||
|
@ -2527,6 +2618,37 @@ nodeenv = ">=0.11.1"
|
|||
pyyaml = ">=5.1"
|
||||
virtualenv = ">=20.10.0"
|
||||
|
||||
[[package]]
|
||||
name = "preview-generator"
|
||||
version = "0.29"
|
||||
description = "A library for generating preview (thumbnails, text or json overview) for file-based content"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">= 3.7"
|
||||
files = [
|
||||
{file = "preview_generator-0.29.tar.gz", hash = "sha256:343978dace795b4b9da862d96f2e4a9c088e58a255c32a97ecd8729d4947377a"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
cairosvg = {version = "*", optional = true, markers = "extra == \"all\""}
|
||||
ffmpeg-python = {version = "*", optional = true, markers = "extra == \"all\""}
|
||||
filelock = "*"
|
||||
pyexifinfo = "*"
|
||||
python-magic = "*"
|
||||
Wand = "*"
|
||||
xvfbwrapper = {version = "*", optional = true, markers = "extra == \"all\""}
|
||||
|
||||
[package.extras]
|
||||
3d = ["vtk"]
|
||||
all = ["cairosvg", "ffmpeg-python", "xvfbwrapper", "xvfbwrapper"]
|
||||
cairosvg = ["cairosvg"]
|
||||
dev = ["ImageHash", "black", "flake8", "isort", "mypy", "pre-commit", "pytest", "pytest-dotenv"]
|
||||
drawio = ["xvfbwrapper"]
|
||||
raw = ["rawpy"]
|
||||
scribus = ["xvfbwrapper"]
|
||||
testing = ["ImageHash", "pytest", "pytest-dotenv"]
|
||||
video = ["ffmpeg-python"]
|
||||
|
||||
[[package]]
|
||||
name = "prometheus-client"
|
||||
version = "0.16.0"
|
||||
|
@ -2716,6 +2838,31 @@ files = [
|
|||
{file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pydotplus"
|
||||
version = "2.0.2"
|
||||
description = "Python interface to Graphviz's Dot language"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
files = [
|
||||
{file = "pydotplus-2.0.2.tar.gz", hash = "sha256:91e85e9ee9b85d2391ead7d635e3d9c7f5f44fd60a60e59b13e2403fa66505c4"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
pyparsing = ">=2.0.1"
|
||||
|
||||
[[package]]
|
||||
name = "pyexifinfo"
|
||||
version = "0.4.0"
|
||||
description = "Simple Metadata extraction using Exiftool"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
files = [
|
||||
{file = "pyexifinfo-0.4.0.tar.gz", hash = "sha256:578b34b3c593fe77bbe6b62588f9f2ec679dca63f7d486148c9a6ff1fdd4bdc9"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyflakes"
|
||||
version = "3.0.1"
|
||||
|
@ -2840,6 +2987,21 @@ files = [
|
|||
[package.dependencies]
|
||||
pylint = ">=1.7"
|
||||
|
||||
[[package]]
|
||||
name = "pyparsing"
|
||||
version = "3.0.9"
|
||||
description = "pyparsing module - Classes and methods to define and execute parsing grammars"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.6.8"
|
||||
files = [
|
||||
{file = "pyparsing-3.0.9-py3-none-any.whl", hash = "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc"},
|
||||
{file = "pyparsing-3.0.9.tar.gz", hash = "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
diagrams = ["jinja2", "railroad-diagrams"]
|
||||
|
||||
[[package]]
|
||||
name = "pyrsistent"
|
||||
version = "0.19.3"
|
||||
|
@ -3461,6 +3623,25 @@ files = [
|
|||
{file = "text_unidecode-1.3-py2.py3-none-any.whl", hash = "sha256:1311f10e8b895935241623731c2ba64f4c455287888b18189350b67134a822e8"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tinycss2"
|
||||
version = "1.2.1"
|
||||
description = "A tiny CSS parser"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "tinycss2-1.2.1-py3-none-any.whl", hash = "sha256:2b80a96d41e7c3914b8cda8bc7f705a4d9c49275616e886103dd839dfc847847"},
|
||||
{file = "tinycss2-1.2.1.tar.gz", hash = "sha256:8cff3a8f066c2ec677c06dbc7b45619804a6938478d9d73c284b29d14ecb0627"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
webencodings = ">=0.4"
|
||||
|
||||
[package.extras]
|
||||
doc = ["sphinx", "sphinx_rtd_theme"]
|
||||
test = ["flake8", "isort", "pytest"]
|
||||
|
||||
[[package]]
|
||||
name = "tomli"
|
||||
version = "2.0.1"
|
||||
|
@ -3659,6 +3840,22 @@ platformdirs = ">=2.4,<4"
|
|||
docs = ["furo (>=2022.12.7)", "proselint (>=0.13)", "sphinx (>=6.1.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=22.12)"]
|
||||
test = ["covdefaults (>=2.2.2)", "coverage (>=7.1)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23)", "pytest (>=7.2.1)", "pytest-env (>=0.8.1)", "pytest-freezegun (>=0.4.2)", "pytest-mock (>=3.10)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)"]
|
||||
|
||||
[[package]]
|
||||
name = "wand"
|
||||
version = "0.6.11"
|
||||
description = "Ctypes-based simple MagickWand API binding for Python"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
files = [
|
||||
{file = "Wand-0.6.11-py2.py3-none-any.whl", hash = "sha256:1b77e25439ace57f665d1ccc6cff2766fad0834005b89ae3e7aaf3ba12b124b0"},
|
||||
{file = "Wand-0.6.11.tar.gz", hash = "sha256:b661700da9f8f1e931e52726e4fc643a565b9514f5883d41b773e3c37c9fa995"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
doc = ["Sphinx (>=5.3.0)"]
|
||||
test = ["pytest (>=7.2.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "watchdog"
|
||||
version = "2.3.1"
|
||||
|
@ -3743,6 +3940,18 @@ files = [
|
|||
{file = "wcwidth-0.2.6.tar.gz", hash = "sha256:a5220780a404dbe3353789870978e472cfe477761f06ee55077256e509b156d0"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "webencodings"
|
||||
version = "0.5.1"
|
||||
description = "Character encoding aliases for legacy web content"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
files = [
|
||||
{file = "webencodings-0.5.1-py2.py3-none-any.whl", hash = "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78"},
|
||||
{file = "webencodings-0.5.1.tar.gz", hash = "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "werkzeug"
|
||||
version = "2.2.3"
|
||||
|
@ -3862,7 +4071,18 @@ files = [
|
|||
{file = "wrapt-1.15.0.tar.gz", hash = "sha256:d06730c6aed78cee4126234cf2d071e01b44b915e725a6cb439a879ec9754a3a"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "xvfbwrapper"
|
||||
version = "0.2.9"
|
||||
description = "run headless display inside X virtual framebuffer (Xvfb)"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
files = [
|
||||
{file = "xvfbwrapper-0.2.9.tar.gz", hash = "sha256:bcf4ae571941b40254faf7a73432dfc119ad21ce688f1fdec533067037ecfc24"},
|
||||
]
|
||||
|
||||
[metadata]
|
||||
lock-version = "2.0"
|
||||
python-versions = "^3.11"
|
||||
content-hash = "f9a99f36ec97a450398a9bf31df6970377ae436a6f3e0500acf0eaf08dc4199f"
|
||||
content-hash = "8bb4acf1ceb7b54ad1864fe625ca1aa68d4cc371ce45170bede728ad84fa2c84"
|
||||
|
|
|
@ -72,6 +72,8 @@ django-active-link = "^0.1.8"
|
|||
channels = "^4.0.0"
|
||||
django-upload-validator = "^1.1.6"
|
||||
markdown = "^3.4.3"
|
||||
preview-generator = {extras = ["all"], version = "^0.29"}
|
||||
pydotplus = "^2.0.2"
|
||||
|
||||
|
||||
[build-system]
|
||||
|
|
Loading…
Reference in New Issue
Block a user