django-nativemojo 0.1.10__py3-none-any.whl → 0.1.16__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- django_nativemojo-0.1.16.dist-info/METADATA +138 -0
- django_nativemojo-0.1.16.dist-info/RECORD +302 -0
- mojo/__init__.py +1 -1
- mojo/apps/account/management/__init__.py +5 -0
- mojo/apps/account/management/commands/__init__.py +6 -0
- mojo/apps/account/management/commands/serializer_admin.py +651 -0
- mojo/apps/account/migrations/0004_user_avatar.py +20 -0
- mojo/apps/account/migrations/0005_group_last_activity.py +18 -0
- mojo/apps/account/migrations/0006_add_device_tracking_models.py +72 -0
- mojo/apps/account/migrations/0007_delete_userdevicelocation.py +16 -0
- mojo/apps/account/migrations/0008_userdevicelocation.py +33 -0
- mojo/apps/account/migrations/0009_geolocatedip_subnet.py +18 -0
- mojo/apps/account/migrations/0010_group_avatar.py +20 -0
- mojo/apps/account/migrations/0011_user_org_registereddevice_pushconfig_and_more.py +118 -0
- mojo/apps/account/migrations/0012_remove_pushconfig_apns_key_file_and_more.py +21 -0
- mojo/apps/account/migrations/0013_pushconfig_test_mode_alter_pushconfig_apns_enabled_and_more.py +28 -0
- mojo/apps/account/migrations/0014_notificationdelivery_data_payload_and_more.py +48 -0
- mojo/apps/account/models/__init__.py +2 -0
- mojo/apps/account/models/device.py +281 -0
- mojo/apps/account/models/group.py +319 -15
- mojo/apps/account/models/member.py +29 -5
- mojo/apps/account/models/push/__init__.py +4 -0
- mojo/apps/account/models/push/config.py +112 -0
- mojo/apps/account/models/push/delivery.py +93 -0
- mojo/apps/account/models/push/device.py +66 -0
- mojo/apps/account/models/push/template.py +99 -0
- mojo/apps/account/models/user.py +369 -19
- mojo/apps/account/rest/__init__.py +2 -0
- mojo/apps/account/rest/device.py +39 -0
- mojo/apps/account/rest/group.py +9 -0
- mojo/apps/account/rest/push.py +187 -0
- mojo/apps/account/rest/user.py +100 -6
- mojo/apps/account/services/__init__.py +1 -0
- mojo/apps/account/services/push.py +363 -0
- mojo/apps/aws/migrations/0001_initial.py +206 -0
- mojo/apps/aws/migrations/0002_emaildomain_can_recv_emaildomain_can_send_and_more.py +28 -0
- mojo/apps/aws/migrations/0003_mailbox_is_domain_default_mailbox_is_system_default_and_more.py +31 -0
- mojo/apps/aws/migrations/0004_s3bucket.py +39 -0
- mojo/apps/aws/migrations/0005_alter_emaildomain_region_delete_s3bucket.py +21 -0
- mojo/apps/aws/models/__init__.py +19 -0
- mojo/apps/aws/models/email_attachment.py +99 -0
- mojo/apps/aws/models/email_domain.py +218 -0
- mojo/apps/aws/models/email_template.py +132 -0
- mojo/apps/aws/models/incoming_email.py +197 -0
- mojo/apps/aws/models/mailbox.py +288 -0
- mojo/apps/aws/models/sent_message.py +175 -0
- mojo/apps/aws/rest/__init__.py +7 -0
- mojo/apps/aws/rest/email.py +33 -0
- mojo/apps/aws/rest/email_ops.py +183 -0
- mojo/apps/aws/rest/messages.py +32 -0
- mojo/apps/aws/rest/s3.py +64 -0
- mojo/apps/aws/rest/send.py +101 -0
- mojo/apps/aws/rest/sns.py +403 -0
- mojo/apps/aws/rest/templates.py +19 -0
- mojo/apps/aws/services/__init__.py +32 -0
- mojo/apps/aws/services/email.py +390 -0
- mojo/apps/aws/services/email_ops.py +548 -0
- mojo/apps/docit/__init__.py +6 -0
- mojo/apps/docit/markdown_plugins/syntax_highlight.py +25 -0
- mojo/apps/docit/markdown_plugins/toc.py +12 -0
- mojo/apps/docit/migrations/0001_initial.py +113 -0
- mojo/apps/docit/migrations/0002_alter_book_modified_by_alter_page_modified_by.py +26 -0
- mojo/apps/docit/migrations/0003_alter_book_group.py +20 -0
- mojo/apps/docit/models/__init__.py +17 -0
- mojo/apps/docit/models/asset.py +231 -0
- mojo/apps/docit/models/book.py +227 -0
- mojo/apps/docit/models/page.py +319 -0
- mojo/apps/docit/models/page_revision.py +203 -0
- mojo/apps/docit/rest/__init__.py +10 -0
- mojo/apps/docit/rest/asset.py +17 -0
- mojo/apps/docit/rest/book.py +22 -0
- mojo/apps/docit/rest/page.py +22 -0
- mojo/apps/docit/rest/page_revision.py +17 -0
- mojo/apps/docit/services/__init__.py +11 -0
- mojo/apps/docit/services/docit.py +315 -0
- mojo/apps/docit/services/markdown.py +44 -0
- mojo/apps/fileman/README.md +8 -8
- mojo/apps/fileman/backends/base.py +76 -70
- mojo/apps/fileman/backends/filesystem.py +86 -86
- mojo/apps/fileman/backends/s3.py +409 -108
- mojo/apps/fileman/migrations/0001_initial.py +106 -0
- mojo/apps/fileman/migrations/0002_filemanager_parent_alter_filemanager_max_file_size.py +24 -0
- mojo/apps/fileman/migrations/0003_remove_file_fileman_fil_upload__c4bc35_idx_and_more.py +25 -0
- mojo/apps/fileman/migrations/0004_remove_file_original_filename_and_more.py +39 -0
- mojo/apps/fileman/migrations/0005_alter_file_upload_token.py +18 -0
- mojo/apps/fileman/migrations/0006_file_download_url_filemanager_forever_urls.py +23 -0
- mojo/apps/fileman/migrations/0007_remove_filemanager_forever_urls_and_more.py +22 -0
- mojo/apps/fileman/migrations/0008_file_category.py +18 -0
- mojo/apps/fileman/migrations/0009_rename_file_path_file_storage_file_path.py +18 -0
- mojo/apps/fileman/migrations/0010_filerendition.py +33 -0
- mojo/apps/fileman/migrations/0011_alter_filerendition_original_file.py +19 -0
- mojo/apps/fileman/models/__init__.py +1 -5
- mojo/apps/fileman/models/file.py +240 -58
- mojo/apps/fileman/models/manager.py +427 -31
- mojo/apps/fileman/models/rendition.py +118 -0
- mojo/apps/fileman/renderer/__init__.py +111 -0
- mojo/apps/fileman/renderer/audio.py +403 -0
- mojo/apps/fileman/renderer/base.py +205 -0
- mojo/apps/fileman/renderer/document.py +404 -0
- mojo/apps/fileman/renderer/image.py +222 -0
- mojo/apps/fileman/renderer/utils.py +297 -0
- mojo/apps/fileman/renderer/video.py +304 -0
- mojo/apps/fileman/rest/__init__.py +1 -18
- mojo/apps/fileman/rest/upload.py +22 -32
- mojo/apps/fileman/signals.py +58 -0
- mojo/apps/fileman/tasks.py +254 -0
- mojo/apps/fileman/utils/__init__.py +40 -16
- mojo/apps/incident/migrations/0005_incidenthistory.py +39 -0
- mojo/apps/incident/migrations/0006_alter_incident_state.py +18 -0
- mojo/apps/incident/migrations/0007_event_uid.py +18 -0
- mojo/apps/incident/migrations/0008_ticket_ticketnote.py +55 -0
- mojo/apps/incident/migrations/0009_incident_status.py +18 -0
- mojo/apps/incident/migrations/0010_event_country_code.py +18 -0
- mojo/apps/incident/migrations/0011_incident_country_code.py +18 -0
- mojo/apps/incident/migrations/0012_alter_incident_status.py +18 -0
- mojo/apps/incident/models/__init__.py +2 -0
- mojo/apps/incident/models/event.py +35 -0
- mojo/apps/incident/models/history.py +36 -0
- mojo/apps/incident/models/incident.py +3 -1
- mojo/apps/incident/models/ticket.py +62 -0
- mojo/apps/incident/reporter.py +21 -1
- mojo/apps/incident/rest/__init__.py +1 -0
- mojo/apps/incident/rest/event.py +7 -1
- mojo/apps/incident/rest/ticket.py +43 -0
- mojo/apps/jobs/__init__.py +489 -0
- mojo/apps/jobs/adapters.py +24 -0
- mojo/apps/jobs/cli.py +616 -0
- mojo/apps/jobs/daemon.py +370 -0
- mojo/apps/jobs/examples/sample_jobs.py +376 -0
- mojo/apps/jobs/examples/webhook_examples.py +203 -0
- mojo/apps/jobs/handlers/__init__.py +5 -0
- mojo/apps/jobs/handlers/webhook.py +317 -0
- mojo/apps/jobs/job_engine.py +734 -0
- mojo/apps/jobs/keys.py +203 -0
- mojo/apps/jobs/local_queue.py +363 -0
- mojo/apps/jobs/management/__init__.py +3 -0
- mojo/apps/jobs/management/commands/__init__.py +3 -0
- mojo/apps/jobs/manager.py +1327 -0
- mojo/apps/jobs/migrations/0001_initial.py +97 -0
- mojo/apps/jobs/migrations/0002_alter_job_max_retries_joblog.py +39 -0
- mojo/apps/jobs/models/__init__.py +6 -0
- mojo/apps/jobs/models/job.py +441 -0
- mojo/apps/jobs/rest/__init__.py +2 -0
- mojo/apps/jobs/rest/control.py +466 -0
- mojo/apps/jobs/rest/jobs.py +421 -0
- mojo/apps/jobs/scheduler.py +571 -0
- mojo/apps/jobs/services/__init__.py +6 -0
- mojo/apps/jobs/services/job_actions.py +465 -0
- mojo/apps/jobs/settings.py +209 -0
- mojo/apps/logit/migrations/0004_alter_log_level.py +18 -0
- mojo/apps/logit/models/log.py +7 -1
- mojo/apps/metrics/__init__.py +8 -1
- mojo/apps/metrics/redis_metrics.py +198 -0
- mojo/apps/metrics/rest/__init__.py +3 -0
- mojo/apps/metrics/rest/categories.py +266 -0
- mojo/apps/metrics/rest/helpers.py +48 -0
- mojo/apps/metrics/rest/permissions.py +99 -0
- mojo/apps/metrics/rest/values.py +277 -0
- mojo/apps/metrics/utils.py +19 -2
- mojo/decorators/auth.py +6 -1
- mojo/decorators/http.py +47 -3
- mojo/helpers/aws/__init__.py +45 -0
- mojo/helpers/aws/ec2.py +804 -0
- mojo/helpers/aws/iam.py +748 -0
- mojo/helpers/aws/inbound_email.py +309 -0
- mojo/helpers/aws/kms.py +413 -0
- mojo/helpers/aws/s3.py +451 -11
- mojo/helpers/aws/ses.py +483 -0
- mojo/helpers/aws/ses_domain.py +959 -0
- mojo/helpers/aws/sns.py +461 -0
- mojo/helpers/crypto/__init__.py +1 -1
- mojo/helpers/crypto/utils.py +15 -0
- mojo/helpers/dates.py +18 -0
- mojo/helpers/location/__init__.py +2 -0
- mojo/helpers/location/countries.py +262 -0
- mojo/helpers/location/geolocation.py +196 -0
- mojo/helpers/logit.py +37 -0
- mojo/helpers/redis/__init__.py +2 -0
- mojo/helpers/redis/adapter.py +606 -0
- mojo/helpers/redis/client.py +48 -0
- mojo/helpers/redis/pool.py +225 -0
- mojo/helpers/request.py +8 -0
- mojo/helpers/response.py +14 -2
- mojo/helpers/settings/__init__.py +2 -0
- mojo/helpers/{settings.py → settings/helper.py} +1 -37
- mojo/helpers/settings/parser.py +132 -0
- mojo/middleware/auth.py +1 -1
- mojo/middleware/cors.py +40 -0
- mojo/middleware/logging.py +131 -12
- mojo/middleware/mojo.py +10 -0
- mojo/models/rest.py +494 -65
- mojo/models/secrets.py +98 -3
- mojo/serializers/__init__.py +106 -0
- mojo/serializers/core/__init__.py +90 -0
- mojo/serializers/core/cache/__init__.py +121 -0
- mojo/serializers/core/cache/backends.py +518 -0
- mojo/serializers/core/cache/base.py +102 -0
- mojo/serializers/core/cache/disabled.py +181 -0
- mojo/serializers/core/cache/memory.py +287 -0
- mojo/serializers/core/cache/redis.py +533 -0
- mojo/serializers/core/cache/utils.py +454 -0
- mojo/serializers/core/manager.py +550 -0
- mojo/serializers/core/serializer.py +475 -0
- mojo/serializers/examples/settings.py +322 -0
- mojo/serializers/formats/csv.py +393 -0
- mojo/serializers/formats/localizers.py +509 -0
- mojo/serializers/{models.py → simple.py} +38 -15
- mojo/serializers/suggested_improvements.md +388 -0
- testit/client.py +1 -1
- testit/helpers.py +35 -4
- testit/runner.py +23 -6
- django_nativemojo-0.1.10.dist-info/METADATA +0 -96
- django_nativemojo-0.1.10.dist-info/RECORD +0 -194
- mojo/apps/metrics/rest/db.py +0 -0
- mojo/apps/notify/README.md +0 -91
- mojo/apps/notify/README_NOTIFICATIONS.md +0 -566
- mojo/apps/notify/admin.py +0 -52
- mojo/apps/notify/handlers/example_handlers.py +0 -516
- mojo/apps/notify/handlers/ses/__init__.py +0 -25
- mojo/apps/notify/handlers/ses/bounce.py +0 -0
- mojo/apps/notify/handlers/ses/complaint.py +0 -25
- mojo/apps/notify/handlers/ses/message.py +0 -86
- mojo/apps/notify/management/commands/__init__.py +0 -1
- mojo/apps/notify/management/commands/process_notifications.py +0 -370
- mojo/apps/notify/mod +0 -0
- mojo/apps/notify/models/__init__.py +0 -12
- mojo/apps/notify/models/account.py +0 -128
- mojo/apps/notify/models/attachment.py +0 -24
- mojo/apps/notify/models/bounce.py +0 -68
- mojo/apps/notify/models/complaint.py +0 -40
- mojo/apps/notify/models/inbox.py +0 -113
- mojo/apps/notify/models/inbox_message.py +0 -173
- mojo/apps/notify/models/outbox.py +0 -129
- mojo/apps/notify/models/outbox_message.py +0 -288
- mojo/apps/notify/models/template.py +0 -30
- mojo/apps/notify/providers/aws.py +0 -73
- mojo/apps/notify/rest/ses.py +0 -0
- mojo/apps/notify/utils/__init__.py +0 -2
- mojo/apps/notify/utils/notifications.py +0 -404
- mojo/apps/notify/utils/parsing.py +0 -202
- mojo/apps/notify/utils/render.py +0 -144
- mojo/apps/tasks/README.md +0 -118
- mojo/apps/tasks/__init__.py +0 -11
- mojo/apps/tasks/manager.py +0 -489
- mojo/apps/tasks/rest/__init__.py +0 -2
- mojo/apps/tasks/rest/hooks.py +0 -0
- mojo/apps/tasks/rest/tasks.py +0 -62
- mojo/apps/tasks/runner.py +0 -174
- mojo/apps/tasks/tq_handlers.py +0 -14
- mojo/helpers/aws/setup_email.py +0 -0
- mojo/helpers/redis.py +0 -10
- mojo/models/meta.py +0 -262
- mojo/ws4redis/README.md +0 -174
- mojo/ws4redis/__init__.py +0 -2
- mojo/ws4redis/client.py +0 -283
- mojo/ws4redis/connection.py +0 -327
- mojo/ws4redis/exceptions.py +0 -32
- mojo/ws4redis/redis.py +0 -183
- mojo/ws4redis/servers/base.py +0 -86
- mojo/ws4redis/servers/django.py +0 -171
- mojo/ws4redis/servers/uwsgi.py +0 -63
- mojo/ws4redis/settings.py +0 -45
- mojo/ws4redis/utf8validator.py +0 -128
- mojo/ws4redis/websocket.py +0 -403
- {django_nativemojo-0.1.10.dist-info → django_nativemojo-0.1.16.dist-info}/LICENSE +0 -0
- {django_nativemojo-0.1.10.dist-info → django_nativemojo-0.1.16.dist-info}/NOTICE +0 -0
- {django_nativemojo-0.1.10.dist-info → django_nativemojo-0.1.16.dist-info}/WHEEL +0 -0
- /mojo/apps/{notify → aws}/__init__.py +0 -0
- /mojo/apps/{notify/handlers → aws/migrations}/__init__.py +0 -0
- /mojo/apps/{notify/management → docit/markdown_plugins}/__init__.py +0 -0
- /mojo/apps/{notify/providers → docit/migrations}/__init__.py +0 -0
- /mojo/apps/{notify/rest → fileman/migrations}/__init__.py +0 -0
- /mojo/{ws4redis/servers → apps/jobs/examples}/__init__.py +0 -0
- /mojo/apps/{fileman/models/render.py → jobs/migrations/__init__.py} +0 -0
- /mojo/{serializers → rest}/openapi.py +0 -0
- /mojo/{apps/fileman/rest/__init__ → serializers/formats/__init__.py} +0 -0
@@ -0,0 +1,222 @@
|
|
1
|
+
import os
|
2
|
+
import io
|
3
|
+
from typing import Dict, Optional, Tuple, Union, BinaryIO
|
4
|
+
from PIL import Image, ImageOps
|
5
|
+
import mimetypes
|
6
|
+
import logging
|
7
|
+
|
8
|
+
from mojo.apps.fileman.models import File, FileRendition
|
9
|
+
from mojo.apps.fileman.renderer.base import BaseRenderer, RenditionRole
|
10
|
+
|
11
|
+
logger = logging.getLogger(__name__)
|
12
|
+
|
13
|
+
class ImageRenderer(BaseRenderer):
|
14
|
+
"""
|
15
|
+
Renderer for image files
|
16
|
+
|
17
|
+
Creates various renditions like thumbnails, square crops, and resized versions
|
18
|
+
"""
|
19
|
+
|
20
|
+
# Image file categories
|
21
|
+
supported_categories = ['image']
|
22
|
+
|
23
|
+
# Default rendition definitions with sizes and options
|
24
|
+
default_renditions = {
|
25
|
+
RenditionRole.THUMBNAIL: {'width': 150, 'height': 150, 'mode': 'contain'},
|
26
|
+
RenditionRole.THUMBNAIL_SM: {'width': 32, 'height': 32, 'mode': 'contain'},
|
27
|
+
RenditionRole.THUMBNAIL_MD: {'width': 64, 'height': 64, 'mode': 'contain'},
|
28
|
+
RenditionRole.THUMBNAIL_LG: {'width': 300, 'height': 300, 'mode': 'contain'},
|
29
|
+
RenditionRole.SQUARE_SM: {'width': 100, 'height': 100, 'mode': 'crop'}
|
30
|
+
}
|
31
|
+
|
32
|
+
# Default output format settings
|
33
|
+
default_format = 'JPEG'
|
34
|
+
default_quality = 85
|
35
|
+
|
36
|
+
def __init__(self, file: File):
|
37
|
+
super().__init__(file)
|
38
|
+
# Map of file extensions to PIL format names
|
39
|
+
self.format_map = {
|
40
|
+
'.jpg': 'JPEG',
|
41
|
+
'.jpeg': 'JPEG',
|
42
|
+
'.png': 'PNG',
|
43
|
+
'.gif': 'GIF',
|
44
|
+
'.bmp': 'BMP',
|
45
|
+
'.webp': 'WEBP',
|
46
|
+
'.tiff': 'TIFF',
|
47
|
+
}
|
48
|
+
|
49
|
+
def _download_original(self) -> Union[str, None]:
|
50
|
+
"""
|
51
|
+
Download the original file to a temporary location
|
52
|
+
|
53
|
+
Returns:
|
54
|
+
str: Path to the downloaded file, or None if download failed
|
55
|
+
"""
|
56
|
+
try:
|
57
|
+
file_manager = self.file.file_manager
|
58
|
+
backend = file_manager.backend
|
59
|
+
|
60
|
+
# Get file extension
|
61
|
+
_, ext = os.path.splitext(self.file.filename)
|
62
|
+
temp_path = self.get_temp_path(ext)
|
63
|
+
|
64
|
+
# Download file from storage
|
65
|
+
backend.download(self.file.storage_file_path, temp_path)
|
66
|
+
return temp_path
|
67
|
+
except Exception as e:
|
68
|
+
logger.error(f"Failed to download original file: {str(e)}")
|
69
|
+
return None
|
70
|
+
|
71
|
+
def _get_output_format(self, source_path: str, options: Dict = None) -> Tuple[str, str]:
|
72
|
+
"""
|
73
|
+
Determine the output format and file extension
|
74
|
+
|
75
|
+
Args:
|
76
|
+
source_path: Path to the source image
|
77
|
+
options: Additional options for format selection
|
78
|
+
|
79
|
+
Returns:
|
80
|
+
Tuple[str, str]: (PIL format name, file extension)
|
81
|
+
"""
|
82
|
+
_, ext = os.path.splitext(source_path.lower())
|
83
|
+
|
84
|
+
# Use specified format if provided
|
85
|
+
if options and 'format' in options:
|
86
|
+
format_name = options['format'].upper()
|
87
|
+
if format_name == 'JPEG':
|
88
|
+
return format_name, '.jpg'
|
89
|
+
return format_name, f".{options['format'].lower()}"
|
90
|
+
|
91
|
+
# Use original format if supported
|
92
|
+
if ext in self.format_map:
|
93
|
+
return self.format_map[ext], ext
|
94
|
+
|
95
|
+
# Default to JPEG
|
96
|
+
return self.default_format, '.jpg'
|
97
|
+
|
98
|
+
def _process_image(self, source_path: str, width: int, height: int,
|
99
|
+
mode: str = 'contain', options: Dict = None) -> Tuple[BinaryIO, str, str]:
|
100
|
+
"""
|
101
|
+
Process image to create a rendition
|
102
|
+
|
103
|
+
Args:
|
104
|
+
source_path: Path to the source image
|
105
|
+
width: Target width
|
106
|
+
height: Target height
|
107
|
+
mode: Resize mode ('contain', 'crop', 'stretch')
|
108
|
+
options: Additional processing options
|
109
|
+
|
110
|
+
Returns:
|
111
|
+
Tuple[BinaryIO, str, str]: (Image data, format, mime type)
|
112
|
+
"""
|
113
|
+
options = options or {}
|
114
|
+
quality = options.get('quality', self.default_quality)
|
115
|
+
|
116
|
+
try:
|
117
|
+
# Open the image
|
118
|
+
with Image.open(source_path) as img:
|
119
|
+
# Convert to RGB if RGBA (unless PNG or format with alpha support)
|
120
|
+
if img.mode == 'RGBA' and options.get('format', '').upper() != 'PNG':
|
121
|
+
img = img.convert('RGB')
|
122
|
+
|
123
|
+
# Process based on mode
|
124
|
+
if mode == 'crop':
|
125
|
+
# Square crop (centered)
|
126
|
+
img = ImageOps.fit(img, (width, height), Image.Resampling.LANCZOS)
|
127
|
+
elif mode == 'contain':
|
128
|
+
# Resize to fit within dimensions while maintaining aspect ratio
|
129
|
+
img.thumbnail((width, height), Image.Resampling.LANCZOS)
|
130
|
+
elif mode == 'stretch':
|
131
|
+
# Stretch to fill dimensions
|
132
|
+
img = img.resize((width, height), Image.Resampling.LANCZOS)
|
133
|
+
|
134
|
+
# Determine output format
|
135
|
+
format_name, extension = self._get_output_format(source_path, options)
|
136
|
+
|
137
|
+
# Save to buffer
|
138
|
+
buffer = io.BytesIO()
|
139
|
+
if format_name == 'JPEG':
|
140
|
+
img.save(buffer, format=format_name, quality=quality, optimize=True)
|
141
|
+
else:
|
142
|
+
img.save(buffer, format=format_name)
|
143
|
+
|
144
|
+
buffer.seek(0)
|
145
|
+
mime_type = mimetypes.guess_type(f"file{extension}")[0]
|
146
|
+
|
147
|
+
return buffer, extension, mime_type
|
148
|
+
except Exception as e:
|
149
|
+
logger.error(f"Image processing error: {str(e)}")
|
150
|
+
raise
|
151
|
+
|
152
|
+
def create_rendition(self, role: str, options: Dict = None) -> Optional[FileRendition]:
|
153
|
+
"""
|
154
|
+
Create an image rendition for the specified role
|
155
|
+
|
156
|
+
Args:
|
157
|
+
role: The role of the rendition
|
158
|
+
options: Additional options for creating the rendition
|
159
|
+
|
160
|
+
Returns:
|
161
|
+
FileRendition: The created rendition, or None if creation failed
|
162
|
+
"""
|
163
|
+
try:
|
164
|
+
# Get rendition settings
|
165
|
+
settings = self.default_renditions.get(role, {})
|
166
|
+
if options:
|
167
|
+
settings.update(options)
|
168
|
+
|
169
|
+
# Default dimensions and mode
|
170
|
+
width = settings.get('width', 150)
|
171
|
+
height = settings.get('height', 150)
|
172
|
+
mode = settings.get('mode', 'contain')
|
173
|
+
|
174
|
+
# Download the original file
|
175
|
+
source_path = self._download_original()
|
176
|
+
if not source_path:
|
177
|
+
return None
|
178
|
+
|
179
|
+
try:
|
180
|
+
# Process the image
|
181
|
+
buffer, extension, mime_type = self._process_image(
|
182
|
+
source_path, width, height, mode, settings
|
183
|
+
)
|
184
|
+
|
185
|
+
# Generate output filename
|
186
|
+
name, _ = os.path.splitext(self.file.storage_filename)
|
187
|
+
filename = f"{name}_renditions/{role}{extension}"
|
188
|
+
|
189
|
+
# Save to storage
|
190
|
+
file_manager = self.file.file_manager
|
191
|
+
backend = file_manager.backend
|
192
|
+
storage_path = os.path.join(
|
193
|
+
os.path.dirname(self.file.storage_file_path),
|
194
|
+
filename
|
195
|
+
)
|
196
|
+
|
197
|
+
# Upload to storage
|
198
|
+
# print(storage_path)
|
199
|
+
backend.save(buffer, storage_path, mime_type)
|
200
|
+
|
201
|
+
# Get file size
|
202
|
+
file_size = buffer.getbuffer().nbytes
|
203
|
+
|
204
|
+
# Create rendition record
|
205
|
+
rendition = self._create_rendition_object(
|
206
|
+
role=role,
|
207
|
+
filename=filename,
|
208
|
+
storage_path=storage_path,
|
209
|
+
content_type=mime_type,
|
210
|
+
category='image',
|
211
|
+
file_size=file_size
|
212
|
+
)
|
213
|
+
|
214
|
+
return rendition
|
215
|
+
finally:
|
216
|
+
# Clean up temporary file
|
217
|
+
if os.path.exists(source_path):
|
218
|
+
os.unlink(source_path)
|
219
|
+
|
220
|
+
except Exception as e:
|
221
|
+
logger.error(f"Failed to create image rendition '{role}': {str(e)}")
|
222
|
+
return None
|
@@ -0,0 +1,297 @@
|
|
1
|
+
import os
|
2
|
+
import mimetypes
|
3
|
+
import tempfile
|
4
|
+
import logging
|
5
|
+
import shutil
|
6
|
+
import hashlib
|
7
|
+
from typing import Dict, List, Tuple, Optional, Union, Any
|
8
|
+
|
9
|
+
logger = logging.getLogger(__name__)
|
10
|
+
|
11
|
+
# Additional MIME type mappings that might not be in the standard library
|
12
|
+
ADDITIONAL_MIME_TYPES = {
|
13
|
+
'.webp': 'image/webp',
|
14
|
+
'.heic': 'image/heic',
|
15
|
+
'.heif': 'image/heif',
|
16
|
+
'.webm': 'video/webm',
|
17
|
+
'.mkv': 'video/x-matroska',
|
18
|
+
'.m4a': 'audio/mp4',
|
19
|
+
'.flac': 'audio/flac',
|
20
|
+
}
|
21
|
+
|
22
|
+
# Register additional MIME types
|
23
|
+
for ext, type_name in ADDITIONAL_MIME_TYPES.items():
|
24
|
+
mimetypes.add_type(type_name, ext)
|
25
|
+
|
26
|
+
# Map of file categories by MIME type prefix
|
27
|
+
CATEGORY_MAP = {
|
28
|
+
'image/': 'image',
|
29
|
+
'video/': 'video',
|
30
|
+
'audio/': 'audio',
|
31
|
+
'text/': 'document',
|
32
|
+
'application/pdf': 'pdf',
|
33
|
+
'application/msword': 'document',
|
34
|
+
'application/vnd.openxmlformats-officedocument.wordprocessingml': 'document',
|
35
|
+
'application/vnd.ms-excel': 'spreadsheet',
|
36
|
+
'application/vnd.openxmlformats-officedocument.spreadsheetml': 'spreadsheet',
|
37
|
+
'application/vnd.ms-powerpoint': 'presentation',
|
38
|
+
'application/vnd.openxmlformats-officedocument.presentationml': 'presentation',
|
39
|
+
'application/zip': 'archive',
|
40
|
+
'application/x-rar-compressed': 'archive',
|
41
|
+
'application/x-7z-compressed': 'archive',
|
42
|
+
'application/x-tar': 'archive',
|
43
|
+
'application/gzip': 'archive',
|
44
|
+
}
|
45
|
+
|
46
|
+
def get_file_category(mime_type: str) -> str:
|
47
|
+
"""
|
48
|
+
Determine the category of a file based on its MIME type.
|
49
|
+
|
50
|
+
Args:
|
51
|
+
mime_type: The MIME type of the file
|
52
|
+
|
53
|
+
Returns:
|
54
|
+
str: The category of the file (image, video, audio, document, etc.)
|
55
|
+
"""
|
56
|
+
if not mime_type:
|
57
|
+
return 'unknown'
|
58
|
+
|
59
|
+
# Check for exact matches
|
60
|
+
if mime_type in CATEGORY_MAP:
|
61
|
+
return CATEGORY_MAP[mime_type]
|
62
|
+
|
63
|
+
# Check for prefix matches
|
64
|
+
for prefix, category in CATEGORY_MAP.items():
|
65
|
+
if prefix.endswith('/'):
|
66
|
+
if mime_type.startswith(prefix):
|
67
|
+
return category
|
68
|
+
|
69
|
+
# Default category
|
70
|
+
return 'other'
|
71
|
+
|
72
|
+
def get_file_info(file_path: str) -> Dict[str, Any]:
|
73
|
+
"""
|
74
|
+
Get information about a file
|
75
|
+
|
76
|
+
Args:
|
77
|
+
file_path: Path to the file
|
78
|
+
|
79
|
+
Returns:
|
80
|
+
Dict with file information (size, mime_type, category, etc.)
|
81
|
+
"""
|
82
|
+
if not os.path.exists(file_path):
|
83
|
+
return {
|
84
|
+
'exists': False,
|
85
|
+
'size': 0,
|
86
|
+
'mime_type': None,
|
87
|
+
'category': 'unknown',
|
88
|
+
}
|
89
|
+
|
90
|
+
file_size = os.path.getsize(file_path)
|
91
|
+
mime_type, _ = mimetypes.guess_type(file_path)
|
92
|
+
category = get_file_category(mime_type)
|
93
|
+
|
94
|
+
return {
|
95
|
+
'exists': True,
|
96
|
+
'size': file_size,
|
97
|
+
'mime_type': mime_type,
|
98
|
+
'category': category,
|
99
|
+
'extension': os.path.splitext(file_path)[1].lower(),
|
100
|
+
}
|
101
|
+
|
102
|
+
def create_temp_directory() -> str:
|
103
|
+
"""
|
104
|
+
Create a temporary directory for file processing
|
105
|
+
|
106
|
+
Returns:
|
107
|
+
str: Path to the temporary directory
|
108
|
+
"""
|
109
|
+
return tempfile.mkdtemp()
|
110
|
+
|
111
|
+
def create_temp_file(suffix: str = '') -> str:
|
112
|
+
"""
|
113
|
+
Create a temporary file for processing
|
114
|
+
|
115
|
+
Args:
|
116
|
+
suffix: Optional suffix for the temp file (e.g., '.jpg')
|
117
|
+
|
118
|
+
Returns:
|
119
|
+
str: Path to the temporary file
|
120
|
+
"""
|
121
|
+
fd, path = tempfile.mkstemp(suffix=suffix)
|
122
|
+
os.close(fd)
|
123
|
+
return path
|
124
|
+
|
125
|
+
def cleanup_temp_files(paths: List[str]):
|
126
|
+
"""
|
127
|
+
Clean up temporary files and directories
|
128
|
+
|
129
|
+
Args:
|
130
|
+
paths: List of paths to clean up
|
131
|
+
"""
|
132
|
+
for path in paths:
|
133
|
+
if not path:
|
134
|
+
continue
|
135
|
+
try:
|
136
|
+
if os.path.isdir(path):
|
137
|
+
shutil.rmtree(path, ignore_errors=True)
|
138
|
+
elif os.path.exists(path):
|
139
|
+
os.unlink(path)
|
140
|
+
except Exception as e:
|
141
|
+
logger.warning(f"Failed to clean up temporary path {path}: {str(e)}")
|
142
|
+
|
143
|
+
def calculate_file_hash(file_path: str, algorithm: str = 'md5') -> str:
|
144
|
+
"""
|
145
|
+
Calculate hash for a file
|
146
|
+
|
147
|
+
Args:
|
148
|
+
file_path: Path to the file
|
149
|
+
algorithm: Hash algorithm to use (md5, sha1, sha256)
|
150
|
+
|
151
|
+
Returns:
|
152
|
+
str: Hex digest of the hash
|
153
|
+
"""
|
154
|
+
if algorithm == 'md5':
|
155
|
+
hash_obj = hashlib.md5()
|
156
|
+
elif algorithm == 'sha1':
|
157
|
+
hash_obj = hashlib.sha1()
|
158
|
+
elif algorithm == 'sha256':
|
159
|
+
hash_obj = hashlib.sha256()
|
160
|
+
else:
|
161
|
+
raise ValueError(f"Unsupported hash algorithm: {algorithm}")
|
162
|
+
|
163
|
+
with open(file_path, 'rb') as f:
|
164
|
+
for chunk in iter(lambda: f.read(4096), b''):
|
165
|
+
hash_obj.update(chunk)
|
166
|
+
|
167
|
+
return hash_obj.hexdigest()
|
168
|
+
|
169
|
+
def calculate_dimensions(original_width: int, original_height: int,
|
170
|
+
target_width: int, target_height: int,
|
171
|
+
mode: str = 'contain') -> Tuple[int, int]:
|
172
|
+
"""
|
173
|
+
Calculate dimensions for resizing an image or video
|
174
|
+
|
175
|
+
Args:
|
176
|
+
original_width: Original width
|
177
|
+
original_height: Original height
|
178
|
+
target_width: Target width
|
179
|
+
target_height: Target height
|
180
|
+
mode: Resize mode ('contain', 'cover', 'stretch')
|
181
|
+
|
182
|
+
Returns:
|
183
|
+
Tuple[int, int]: (new_width, new_height)
|
184
|
+
"""
|
185
|
+
if mode == 'stretch':
|
186
|
+
return target_width, target_height
|
187
|
+
|
188
|
+
# Calculate aspect ratios
|
189
|
+
original_ratio = original_width / original_height
|
190
|
+
target_ratio = target_width / target_height
|
191
|
+
|
192
|
+
if mode == 'contain':
|
193
|
+
# Fit within target dimensions while maintaining aspect ratio
|
194
|
+
if original_ratio > target_ratio:
|
195
|
+
# Width is the limiting factor
|
196
|
+
new_width = target_width
|
197
|
+
new_height = int(new_width / original_ratio)
|
198
|
+
else:
|
199
|
+
# Height is the limiting factor
|
200
|
+
new_height = target_height
|
201
|
+
new_width = int(new_height * original_ratio)
|
202
|
+
else: # cover
|
203
|
+
# Fill target dimensions while maintaining aspect ratio
|
204
|
+
if original_ratio > target_ratio:
|
205
|
+
# Height is the limiting factor
|
206
|
+
new_height = target_height
|
207
|
+
new_width = int(new_height * original_ratio)
|
208
|
+
else:
|
209
|
+
# Width is the limiting factor
|
210
|
+
new_width = target_width
|
211
|
+
new_height = int(new_width / original_ratio)
|
212
|
+
|
213
|
+
return new_width, new_height
|
214
|
+
|
215
|
+
def get_video_duration(video_path: str) -> Optional[float]:
|
216
|
+
"""
|
217
|
+
Get the duration of a video file in seconds
|
218
|
+
|
219
|
+
Args:
|
220
|
+
video_path: Path to the video file
|
221
|
+
|
222
|
+
Returns:
|
223
|
+
float: Duration in seconds, or None if duration couldn't be determined
|
224
|
+
"""
|
225
|
+
try:
|
226
|
+
import subprocess
|
227
|
+
|
228
|
+
# Use ffprobe to get video duration
|
229
|
+
cmd = [
|
230
|
+
"ffprobe",
|
231
|
+
"-v", "error",
|
232
|
+
"-show_entries", "format=duration",
|
233
|
+
"-of", "default=noprint_wrappers=1:nokey=1",
|
234
|
+
video_path
|
235
|
+
]
|
236
|
+
|
237
|
+
result = subprocess.run(cmd,
|
238
|
+
stdout=subprocess.PIPE,
|
239
|
+
stderr=subprocess.PIPE,
|
240
|
+
text=True,
|
241
|
+
check=True)
|
242
|
+
|
243
|
+
duration = float(result.stdout.strip())
|
244
|
+
return duration
|
245
|
+
except Exception as e:
|
246
|
+
logger.error(f"Failed to get video duration: {str(e)}")
|
247
|
+
return None
|
248
|
+
|
249
|
+
def get_audio_duration(audio_path: str) -> Optional[float]:
|
250
|
+
"""
|
251
|
+
Get the duration of an audio file in seconds
|
252
|
+
|
253
|
+
Args:
|
254
|
+
audio_path: Path to the audio file
|
255
|
+
|
256
|
+
Returns:
|
257
|
+
float: Duration in seconds, or None if duration couldn't be determined
|
258
|
+
"""
|
259
|
+
return get_video_duration(audio_path) # Use the same ffprobe method
|
260
|
+
|
261
|
+
def get_image_dimensions(image_path: str) -> Optional[Tuple[int, int]]:
|
262
|
+
"""
|
263
|
+
Get dimensions of an image
|
264
|
+
|
265
|
+
Args:
|
266
|
+
image_path: Path to the image file
|
267
|
+
|
268
|
+
Returns:
|
269
|
+
Tuple[int, int]: (width, height), or None if dimensions couldn't be determined
|
270
|
+
"""
|
271
|
+
try:
|
272
|
+
from PIL import Image
|
273
|
+
with Image.open(image_path) as img:
|
274
|
+
return img.size
|
275
|
+
except Exception as e:
|
276
|
+
logger.error(f"Failed to get image dimensions: {str(e)}")
|
277
|
+
return None
|
278
|
+
|
279
|
+
def format_filesize(size_in_bytes: int) -> str:
|
280
|
+
"""
|
281
|
+
Format a file size in bytes to a human-readable string
|
282
|
+
|
283
|
+
Args:
|
284
|
+
size_in_bytes: Size in bytes
|
285
|
+
|
286
|
+
Returns:
|
287
|
+
str: Human-readable file size (e.g., "1.2 MB")
|
288
|
+
"""
|
289
|
+
if size_in_bytes < 1024:
|
290
|
+
return f"{size_in_bytes} B"
|
291
|
+
|
292
|
+
for unit in ['KB', 'MB', 'GB', 'TB', 'PB']:
|
293
|
+
size_in_bytes /= 1024.0
|
294
|
+
if size_in_bytes < 1024.0:
|
295
|
+
return f"{size_in_bytes:.1f} {unit}"
|
296
|
+
|
297
|
+
return f"{size_in_bytes:.1f} PB"
|