Skip to content

Commit 4929c44

Browse files
authored
feature: image procesing modeling
* chore(image_processing): move image processing to its own app * chore(image_processing): wrap image_processing and divide into domains * chore(image_processing): move applied transformation handling to the client layer * chore(image_processing_api): move get_local_transformer to utils * feature(image_processing): create image processing models * feature(image_processing): persist local processing procedures * chore(places): update local image task with user
1 parent 71e46fe commit 4929c44

35 files changed

Lines changed: 723 additions & 251 deletions

.pre-commit-config.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ repos:
2626
- id: mypy
2727
args: [--strict, --disable-error-code=misc]
2828
additional_dependencies: [
29+
Pillow,
2930
django-stubs,
3031
django,
3132
whitenoise,
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,13 @@
55
from PIL import Image as PImage
66
from PIL import ImageFilter
77

8-
from apps.images.constants import (
8+
from apps.image_processing.api.constants import (
99
ImageTransformations,
1010
TransformationFilterBlurFilter,
1111
TransformationFilterDither,
1212
TransformationFilterThumbnailResampling,
1313
)
14-
from apps.images.processing.data_models import (
14+
from apps.image_processing.src.data_models import (
1515
InternalImageTransformationFilters,
1616
InternalTransformationFiltersBlackAndWhite,
1717
InternalTransformationFiltersBlur,
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
import logging
2+
from dataclasses import asdict
3+
from io import BytesIO
4+
5+
from django.core.files import File
6+
7+
from apps.image_processing.api.data_models import ImageTransformationDefinition
8+
from apps.image_processing.api.utils import (
9+
get_local_transformer,
10+
get_transformation_dataclasses,
11+
)
12+
from apps.image_processing.models import (
13+
Image,
14+
ImageTransformation,
15+
ProcessedImage,
16+
TransformationBatch,
17+
)
18+
from apps.image_processing.src.data_models import (
19+
InternalImageTransformationResult,
20+
InternalTransformationManagerSaveResult,
21+
)
22+
from apps.image_processing.src.managers import ImageLocalManager
23+
24+
logger = logging.getLogger(__name__)
25+
26+
27+
def image_processing_save_procedure(
28+
user_id: int,
29+
image_file: File[bytes],
30+
transformer: str,
31+
transformations: list[ImageTransformationDefinition],
32+
transformations_applied: list[InternalImageTransformationResult],
33+
) -> None:
34+
image = Image.objects.create(user_id=user_id, file=image_file)
35+
transformation_batch = TransformationBatch(
36+
input_image=image,
37+
transformer=transformer,
38+
)
39+
transformation_batch.full_clean()
40+
transformation_batch.save()
41+
42+
image_transformations = []
43+
for transformation in transformations:
44+
filters = asdict(transformation.filters) if transformation.filters else None
45+
image_transformations.append(
46+
ImageTransformation(
47+
identifier=transformation.identifier,
48+
transformation=transformation.transformation,
49+
filters=filters,
50+
batch=transformation_batch,
51+
)
52+
)
53+
ImageTransformation.objects.bulk_create(image_transformations)
54+
55+
image_transformations_dict = {
56+
transformation.identifier: transformation
57+
for transformation in image_transformations
58+
}
59+
processed_images = []
60+
for transformation_applied in transformations_applied:
61+
bytes_image = BytesIO(transformation_applied.image.tobytes())
62+
bytes_image.seek(0)
63+
processed_images.append(
64+
ProcessedImage(
65+
identifier=transformation_applied.identifier,
66+
file=File(bytes_image, name=f"{transformation_applied.identifier}.png"),
67+
transformation=image_transformations_dict[
68+
transformation_applied.identifier
69+
],
70+
)
71+
)
72+
ProcessedImage.objects.bulk_create(processed_images)
73+
74+
75+
def image_local_transform(
76+
user_id: int,
77+
image_path: str,
78+
transformations: list[ImageTransformationDefinition],
79+
parent_folder: str,
80+
is_chain: bool = False,
81+
) -> list[InternalTransformationManagerSaveResult]:
82+
"""
83+
Transforms a local image using specified transformations and saves the results.
84+
85+
This function processes an image located at the specified path using a list of
86+
image transformations. The transformed images are saved under the specified
87+
parent folder. The function returns a list of results indicating the paths
88+
where the transformed images were saved.
89+
90+
Args:
91+
user_id (int): The user_id who is performing the transformation.
92+
image_path (str): The path to the local image file to be transformed.
93+
transformations (list[ImageTransformationDefinition]): A list of
94+
transformation definitions to apply to the image.
95+
parent_folder (str): The name of the parent folder where transformed
96+
images will be saved.
97+
is_chain (bool, optional): A flag indicating whether to use a chain
98+
transformer. Defaults to False.
99+
100+
Returns:
101+
list[InternalTransformationManagerSaveResult]: A list containing the
102+
results of the transformation, including the paths of the saved images.
103+
"""
104+
transformations_data = get_transformation_dataclasses(transformations)
105+
transformer = get_local_transformer(
106+
transformations=transformations_data, is_chain=is_chain
107+
)
108+
image_manager = ImageLocalManager(image_path, transformer=transformer)
109+
transformations_applied = image_manager.apply_transformations()
110+
images_save = image_manager.save(
111+
parent_folder=parent_folder, transformations=transformations_applied
112+
)
113+
image_processing_save_procedure(
114+
user_id=user_id,
115+
image_file=image_manager.get_file(),
116+
transformer=transformer.name,
117+
transformations=transformations,
118+
transformations_applied=transformations_applied,
119+
)
120+
return images_save
File renamed without changes.
Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
from unittest.mock import MagicMock, patch
2+
3+
import pytest
4+
from django.contrib.auth import get_user_model
5+
6+
from apps.image_processing.api.constants import ImageTransformations
7+
from apps.image_processing.api.data_models import (
8+
ImageTransformationDefinition,
9+
TransformationFiltersThumbnail,
10+
)
11+
from apps.image_processing.api.services import (
12+
image_local_transform,
13+
image_processing_save_procedure,
14+
)
15+
from apps.image_processing.models import (
16+
Image,
17+
ImageTransformation,
18+
ProcessedImage,
19+
TransformationBatch,
20+
)
21+
from apps.image_processing.src.data_models import InternalImageTransformationResult
22+
from apps.image_processing.src.managers import ImageLocalManager
23+
24+
User = get_user_model()
25+
26+
27+
@pytest.fixture
28+
def mock_get_transformation_dataclasses():
29+
with patch(
30+
"apps.image_processing.api.services.get_transformation_dataclasses"
31+
) as mock:
32+
mock.return_value = [MagicMock(), MagicMock()]
33+
yield mock
34+
35+
36+
@pytest.fixture
37+
def mock_get_local_transformer():
38+
with patch("apps.image_processing.api.services.get_local_transformer") as mock:
39+
mock_transformer_instance = MagicMock()
40+
mock.return_value = mock_transformer_instance
41+
yield mock
42+
43+
44+
@pytest.fixture
45+
def mock_image_local_manager():
46+
with patch("apps.image_processing.api.services.ImageLocalManager") as mock:
47+
mock_manager_instance = MagicMock()
48+
mock_manager_instance.apply_transformations = MagicMock()
49+
mock_manager_instance.get_file.return_value = 1
50+
mock_manager_instance.save.return_value = {
51+
"transformed_image.png": "/path/to/transformed_image.png"
52+
}
53+
mock.return_value = mock_manager_instance
54+
yield mock
55+
56+
57+
@patch("apps.image_processing.api.services.image_processing_save_procedure")
58+
def test_image_local_transform(
59+
mock_image_processing_save_procedure,
60+
mock_get_transformation_dataclasses,
61+
mock_get_local_transformer,
62+
mock_image_local_manager,
63+
):
64+
user_id = 1
65+
image_path = "/fake/image.jpg"
66+
transformations = [
67+
ImageTransformationDefinition(
68+
identifier="test_thumb",
69+
transformation=ImageTransformations.THUMBNAIL,
70+
filters=TransformationFiltersThumbnail(size=(100, 100)),
71+
)
72+
]
73+
parent_folder = "test_parent"
74+
75+
expected_save_result = {"transformed_image.png": "/path/to/transformed_image.png"}
76+
77+
result = image_local_transform(user_id, image_path, transformations, parent_folder)
78+
79+
mock_get_transformation_dataclasses.assert_called_once_with(transformations)
80+
mock_get_local_transformer.assert_called_once_with(
81+
transformations=mock_get_transformation_dataclasses.return_value, is_chain=False
82+
)
83+
84+
mock_image_local_manager.assert_called_once_with(
85+
image_path, transformer=mock_get_local_transformer.return_value
86+
)
87+
88+
mock_instance = mock_image_local_manager.return_value
89+
mock_instance.apply_transformations.assert_called_once()
90+
mock_instance.save.assert_called_once_with(
91+
parent_folder=parent_folder,
92+
transformations=mock_instance.apply_transformations.return_value,
93+
)
94+
95+
mock_image_processing_save_procedure.assert_called_once_with(
96+
user_id=user_id,
97+
image_file=1,
98+
transformer=mock_get_local_transformer().name,
99+
transformations=transformations,
100+
transformations_applied=mock_instance.apply_transformations.return_value,
101+
)
102+
assert result == expected_save_result
103+
104+
105+
@pytest.mark.django_db
106+
def test_image_processing_save_procedure_integration(user, temp_image_file):
107+
user_id = user.id
108+
109+
transformer_name = "chain"
110+
transformations_defs = [
111+
ImageTransformationDefinition(
112+
identifier="thumb_integration",
113+
transformation=ImageTransformations.THUMBNAIL,
114+
filters=TransformationFiltersThumbnail(size=(100, 100)),
115+
),
116+
ImageTransformationDefinition(
117+
identifier="resize_integration",
118+
transformation=ImageTransformations.BLUR,
119+
),
120+
]
121+
mock_applied_image_data = MagicMock()
122+
mock_applied_image_data.tobytes.return_value = b"transformedimagedata"
123+
transformations_applied_results = [
124+
InternalImageTransformationResult(
125+
identifier="thumb_integration", image=mock_applied_image_data
126+
),
127+
InternalImageTransformationResult(
128+
identifier="resize_integration", image=mock_applied_image_data
129+
),
130+
]
131+
132+
local_manage = ImageLocalManager(image_path=temp_image_file)
133+
mock_image_file = local_manage.get_file()
134+
135+
image_processing_save_procedure(
136+
user_id=user_id,
137+
image_file=mock_image_file,
138+
transformer=transformer_name,
139+
transformations=transformations_defs,
140+
transformations_applied=transformations_applied_results,
141+
)
142+
143+
assert Image.objects.count() == 1
144+
saved_image = Image.objects.first()
145+
assert saved_image.user_id == user_id
146+
assert saved_image.file.name == "image_processing/api/test_image.png"
147+
148+
assert TransformationBatch.objects.count() == 1
149+
saved_batch = TransformationBatch.objects.first()
150+
assert saved_batch.input_image == saved_image
151+
assert saved_batch.transformer == transformer_name
152+
153+
assert ImageTransformation.objects.count() == 2
154+
db_transformations = ImageTransformation.objects.order_by("identifier")
155+
assert db_transformations[0].identifier == "resize_integration"
156+
assert db_transformations[0].transformation == ImageTransformations.BLUR
157+
assert db_transformations[0].filters == None
158+
assert db_transformations[0].batch == saved_batch
159+
160+
assert db_transformations[1].identifier == "thumb_integration"
161+
assert db_transformations[1].transformation == ImageTransformations.THUMBNAIL
162+
assert db_transformations[1].filters == {
163+
"size": [100, 100],
164+
"reducing_gap": 2,
165+
"resample": "bicubic",
166+
}
167+
assert db_transformations[1].batch == saved_batch
168+
169+
assert ProcessedImage.objects.count() == 2
170+
db_processed_images = ProcessedImage.objects.order_by("identifier")
171+
assert db_processed_images[0].identifier == "resize_integration"
172+
assert (
173+
db_processed_images[0].file.name
174+
== "image_processing/processed/resize_integration.png"
175+
)
176+
assert db_processed_images[0].transformation == db_transformations[0]
177+
178+
assert db_processed_images[1].identifier == "thumb_integration"
179+
assert (
180+
db_processed_images[1].file.name
181+
== "image_processing/processed/thumb_integration.png"
182+
)
183+
assert db_processed_images[1].transformation == db_transformations[1]

0 commit comments

Comments
 (0)