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.
Files changed (276) hide show
  1. django_nativemojo-0.1.16.dist-info/METADATA +138 -0
  2. django_nativemojo-0.1.16.dist-info/RECORD +302 -0
  3. mojo/__init__.py +1 -1
  4. mojo/apps/account/management/__init__.py +5 -0
  5. mojo/apps/account/management/commands/__init__.py +6 -0
  6. mojo/apps/account/management/commands/serializer_admin.py +651 -0
  7. mojo/apps/account/migrations/0004_user_avatar.py +20 -0
  8. mojo/apps/account/migrations/0005_group_last_activity.py +18 -0
  9. mojo/apps/account/migrations/0006_add_device_tracking_models.py +72 -0
  10. mojo/apps/account/migrations/0007_delete_userdevicelocation.py +16 -0
  11. mojo/apps/account/migrations/0008_userdevicelocation.py +33 -0
  12. mojo/apps/account/migrations/0009_geolocatedip_subnet.py +18 -0
  13. mojo/apps/account/migrations/0010_group_avatar.py +20 -0
  14. mojo/apps/account/migrations/0011_user_org_registereddevice_pushconfig_and_more.py +118 -0
  15. mojo/apps/account/migrations/0012_remove_pushconfig_apns_key_file_and_more.py +21 -0
  16. mojo/apps/account/migrations/0013_pushconfig_test_mode_alter_pushconfig_apns_enabled_and_more.py +28 -0
  17. mojo/apps/account/migrations/0014_notificationdelivery_data_payload_and_more.py +48 -0
  18. mojo/apps/account/models/__init__.py +2 -0
  19. mojo/apps/account/models/device.py +281 -0
  20. mojo/apps/account/models/group.py +319 -15
  21. mojo/apps/account/models/member.py +29 -5
  22. mojo/apps/account/models/push/__init__.py +4 -0
  23. mojo/apps/account/models/push/config.py +112 -0
  24. mojo/apps/account/models/push/delivery.py +93 -0
  25. mojo/apps/account/models/push/device.py +66 -0
  26. mojo/apps/account/models/push/template.py +99 -0
  27. mojo/apps/account/models/user.py +369 -19
  28. mojo/apps/account/rest/__init__.py +2 -0
  29. mojo/apps/account/rest/device.py +39 -0
  30. mojo/apps/account/rest/group.py +9 -0
  31. mojo/apps/account/rest/push.py +187 -0
  32. mojo/apps/account/rest/user.py +100 -6
  33. mojo/apps/account/services/__init__.py +1 -0
  34. mojo/apps/account/services/push.py +363 -0
  35. mojo/apps/aws/migrations/0001_initial.py +206 -0
  36. mojo/apps/aws/migrations/0002_emaildomain_can_recv_emaildomain_can_send_and_more.py +28 -0
  37. mojo/apps/aws/migrations/0003_mailbox_is_domain_default_mailbox_is_system_default_and_more.py +31 -0
  38. mojo/apps/aws/migrations/0004_s3bucket.py +39 -0
  39. mojo/apps/aws/migrations/0005_alter_emaildomain_region_delete_s3bucket.py +21 -0
  40. mojo/apps/aws/models/__init__.py +19 -0
  41. mojo/apps/aws/models/email_attachment.py +99 -0
  42. mojo/apps/aws/models/email_domain.py +218 -0
  43. mojo/apps/aws/models/email_template.py +132 -0
  44. mojo/apps/aws/models/incoming_email.py +197 -0
  45. mojo/apps/aws/models/mailbox.py +288 -0
  46. mojo/apps/aws/models/sent_message.py +175 -0
  47. mojo/apps/aws/rest/__init__.py +7 -0
  48. mojo/apps/aws/rest/email.py +33 -0
  49. mojo/apps/aws/rest/email_ops.py +183 -0
  50. mojo/apps/aws/rest/messages.py +32 -0
  51. mojo/apps/aws/rest/s3.py +64 -0
  52. mojo/apps/aws/rest/send.py +101 -0
  53. mojo/apps/aws/rest/sns.py +403 -0
  54. mojo/apps/aws/rest/templates.py +19 -0
  55. mojo/apps/aws/services/__init__.py +32 -0
  56. mojo/apps/aws/services/email.py +390 -0
  57. mojo/apps/aws/services/email_ops.py +548 -0
  58. mojo/apps/docit/__init__.py +6 -0
  59. mojo/apps/docit/markdown_plugins/syntax_highlight.py +25 -0
  60. mojo/apps/docit/markdown_plugins/toc.py +12 -0
  61. mojo/apps/docit/migrations/0001_initial.py +113 -0
  62. mojo/apps/docit/migrations/0002_alter_book_modified_by_alter_page_modified_by.py +26 -0
  63. mojo/apps/docit/migrations/0003_alter_book_group.py +20 -0
  64. mojo/apps/docit/models/__init__.py +17 -0
  65. mojo/apps/docit/models/asset.py +231 -0
  66. mojo/apps/docit/models/book.py +227 -0
  67. mojo/apps/docit/models/page.py +319 -0
  68. mojo/apps/docit/models/page_revision.py +203 -0
  69. mojo/apps/docit/rest/__init__.py +10 -0
  70. mojo/apps/docit/rest/asset.py +17 -0
  71. mojo/apps/docit/rest/book.py +22 -0
  72. mojo/apps/docit/rest/page.py +22 -0
  73. mojo/apps/docit/rest/page_revision.py +17 -0
  74. mojo/apps/docit/services/__init__.py +11 -0
  75. mojo/apps/docit/services/docit.py +315 -0
  76. mojo/apps/docit/services/markdown.py +44 -0
  77. mojo/apps/fileman/README.md +8 -8
  78. mojo/apps/fileman/backends/base.py +76 -70
  79. mojo/apps/fileman/backends/filesystem.py +86 -86
  80. mojo/apps/fileman/backends/s3.py +409 -108
  81. mojo/apps/fileman/migrations/0001_initial.py +106 -0
  82. mojo/apps/fileman/migrations/0002_filemanager_parent_alter_filemanager_max_file_size.py +24 -0
  83. mojo/apps/fileman/migrations/0003_remove_file_fileman_fil_upload__c4bc35_idx_and_more.py +25 -0
  84. mojo/apps/fileman/migrations/0004_remove_file_original_filename_and_more.py +39 -0
  85. mojo/apps/fileman/migrations/0005_alter_file_upload_token.py +18 -0
  86. mojo/apps/fileman/migrations/0006_file_download_url_filemanager_forever_urls.py +23 -0
  87. mojo/apps/fileman/migrations/0007_remove_filemanager_forever_urls_and_more.py +22 -0
  88. mojo/apps/fileman/migrations/0008_file_category.py +18 -0
  89. mojo/apps/fileman/migrations/0009_rename_file_path_file_storage_file_path.py +18 -0
  90. mojo/apps/fileman/migrations/0010_filerendition.py +33 -0
  91. mojo/apps/fileman/migrations/0011_alter_filerendition_original_file.py +19 -0
  92. mojo/apps/fileman/models/__init__.py +1 -5
  93. mojo/apps/fileman/models/file.py +240 -58
  94. mojo/apps/fileman/models/manager.py +427 -31
  95. mojo/apps/fileman/models/rendition.py +118 -0
  96. mojo/apps/fileman/renderer/__init__.py +111 -0
  97. mojo/apps/fileman/renderer/audio.py +403 -0
  98. mojo/apps/fileman/renderer/base.py +205 -0
  99. mojo/apps/fileman/renderer/document.py +404 -0
  100. mojo/apps/fileman/renderer/image.py +222 -0
  101. mojo/apps/fileman/renderer/utils.py +297 -0
  102. mojo/apps/fileman/renderer/video.py +304 -0
  103. mojo/apps/fileman/rest/__init__.py +1 -18
  104. mojo/apps/fileman/rest/upload.py +22 -32
  105. mojo/apps/fileman/signals.py +58 -0
  106. mojo/apps/fileman/tasks.py +254 -0
  107. mojo/apps/fileman/utils/__init__.py +40 -16
  108. mojo/apps/incident/migrations/0005_incidenthistory.py +39 -0
  109. mojo/apps/incident/migrations/0006_alter_incident_state.py +18 -0
  110. mojo/apps/incident/migrations/0007_event_uid.py +18 -0
  111. mojo/apps/incident/migrations/0008_ticket_ticketnote.py +55 -0
  112. mojo/apps/incident/migrations/0009_incident_status.py +18 -0
  113. mojo/apps/incident/migrations/0010_event_country_code.py +18 -0
  114. mojo/apps/incident/migrations/0011_incident_country_code.py +18 -0
  115. mojo/apps/incident/migrations/0012_alter_incident_status.py +18 -0
  116. mojo/apps/incident/models/__init__.py +2 -0
  117. mojo/apps/incident/models/event.py +35 -0
  118. mojo/apps/incident/models/history.py +36 -0
  119. mojo/apps/incident/models/incident.py +3 -1
  120. mojo/apps/incident/models/ticket.py +62 -0
  121. mojo/apps/incident/reporter.py +21 -1
  122. mojo/apps/incident/rest/__init__.py +1 -0
  123. mojo/apps/incident/rest/event.py +7 -1
  124. mojo/apps/incident/rest/ticket.py +43 -0
  125. mojo/apps/jobs/__init__.py +489 -0
  126. mojo/apps/jobs/adapters.py +24 -0
  127. mojo/apps/jobs/cli.py +616 -0
  128. mojo/apps/jobs/daemon.py +370 -0
  129. mojo/apps/jobs/examples/sample_jobs.py +376 -0
  130. mojo/apps/jobs/examples/webhook_examples.py +203 -0
  131. mojo/apps/jobs/handlers/__init__.py +5 -0
  132. mojo/apps/jobs/handlers/webhook.py +317 -0
  133. mojo/apps/jobs/job_engine.py +734 -0
  134. mojo/apps/jobs/keys.py +203 -0
  135. mojo/apps/jobs/local_queue.py +363 -0
  136. mojo/apps/jobs/management/__init__.py +3 -0
  137. mojo/apps/jobs/management/commands/__init__.py +3 -0
  138. mojo/apps/jobs/manager.py +1327 -0
  139. mojo/apps/jobs/migrations/0001_initial.py +97 -0
  140. mojo/apps/jobs/migrations/0002_alter_job_max_retries_joblog.py +39 -0
  141. mojo/apps/jobs/models/__init__.py +6 -0
  142. mojo/apps/jobs/models/job.py +441 -0
  143. mojo/apps/jobs/rest/__init__.py +2 -0
  144. mojo/apps/jobs/rest/control.py +466 -0
  145. mojo/apps/jobs/rest/jobs.py +421 -0
  146. mojo/apps/jobs/scheduler.py +571 -0
  147. mojo/apps/jobs/services/__init__.py +6 -0
  148. mojo/apps/jobs/services/job_actions.py +465 -0
  149. mojo/apps/jobs/settings.py +209 -0
  150. mojo/apps/logit/migrations/0004_alter_log_level.py +18 -0
  151. mojo/apps/logit/models/log.py +7 -1
  152. mojo/apps/metrics/__init__.py +8 -1
  153. mojo/apps/metrics/redis_metrics.py +198 -0
  154. mojo/apps/metrics/rest/__init__.py +3 -0
  155. mojo/apps/metrics/rest/categories.py +266 -0
  156. mojo/apps/metrics/rest/helpers.py +48 -0
  157. mojo/apps/metrics/rest/permissions.py +99 -0
  158. mojo/apps/metrics/rest/values.py +277 -0
  159. mojo/apps/metrics/utils.py +19 -2
  160. mojo/decorators/auth.py +6 -1
  161. mojo/decorators/http.py +47 -3
  162. mojo/helpers/aws/__init__.py +45 -0
  163. mojo/helpers/aws/ec2.py +804 -0
  164. mojo/helpers/aws/iam.py +748 -0
  165. mojo/helpers/aws/inbound_email.py +309 -0
  166. mojo/helpers/aws/kms.py +413 -0
  167. mojo/helpers/aws/s3.py +451 -11
  168. mojo/helpers/aws/ses.py +483 -0
  169. mojo/helpers/aws/ses_domain.py +959 -0
  170. mojo/helpers/aws/sns.py +461 -0
  171. mojo/helpers/crypto/__init__.py +1 -1
  172. mojo/helpers/crypto/utils.py +15 -0
  173. mojo/helpers/dates.py +18 -0
  174. mojo/helpers/location/__init__.py +2 -0
  175. mojo/helpers/location/countries.py +262 -0
  176. mojo/helpers/location/geolocation.py +196 -0
  177. mojo/helpers/logit.py +37 -0
  178. mojo/helpers/redis/__init__.py +2 -0
  179. mojo/helpers/redis/adapter.py +606 -0
  180. mojo/helpers/redis/client.py +48 -0
  181. mojo/helpers/redis/pool.py +225 -0
  182. mojo/helpers/request.py +8 -0
  183. mojo/helpers/response.py +14 -2
  184. mojo/helpers/settings/__init__.py +2 -0
  185. mojo/helpers/{settings.py → settings/helper.py} +1 -37
  186. mojo/helpers/settings/parser.py +132 -0
  187. mojo/middleware/auth.py +1 -1
  188. mojo/middleware/cors.py +40 -0
  189. mojo/middleware/logging.py +131 -12
  190. mojo/middleware/mojo.py +10 -0
  191. mojo/models/rest.py +494 -65
  192. mojo/models/secrets.py +98 -3
  193. mojo/serializers/__init__.py +106 -0
  194. mojo/serializers/core/__init__.py +90 -0
  195. mojo/serializers/core/cache/__init__.py +121 -0
  196. mojo/serializers/core/cache/backends.py +518 -0
  197. mojo/serializers/core/cache/base.py +102 -0
  198. mojo/serializers/core/cache/disabled.py +181 -0
  199. mojo/serializers/core/cache/memory.py +287 -0
  200. mojo/serializers/core/cache/redis.py +533 -0
  201. mojo/serializers/core/cache/utils.py +454 -0
  202. mojo/serializers/core/manager.py +550 -0
  203. mojo/serializers/core/serializer.py +475 -0
  204. mojo/serializers/examples/settings.py +322 -0
  205. mojo/serializers/formats/csv.py +393 -0
  206. mojo/serializers/formats/localizers.py +509 -0
  207. mojo/serializers/{models.py → simple.py} +38 -15
  208. mojo/serializers/suggested_improvements.md +388 -0
  209. testit/client.py +1 -1
  210. testit/helpers.py +35 -4
  211. testit/runner.py +23 -6
  212. django_nativemojo-0.1.10.dist-info/METADATA +0 -96
  213. django_nativemojo-0.1.10.dist-info/RECORD +0 -194
  214. mojo/apps/metrics/rest/db.py +0 -0
  215. mojo/apps/notify/README.md +0 -91
  216. mojo/apps/notify/README_NOTIFICATIONS.md +0 -566
  217. mojo/apps/notify/admin.py +0 -52
  218. mojo/apps/notify/handlers/example_handlers.py +0 -516
  219. mojo/apps/notify/handlers/ses/__init__.py +0 -25
  220. mojo/apps/notify/handlers/ses/bounce.py +0 -0
  221. mojo/apps/notify/handlers/ses/complaint.py +0 -25
  222. mojo/apps/notify/handlers/ses/message.py +0 -86
  223. mojo/apps/notify/management/commands/__init__.py +0 -1
  224. mojo/apps/notify/management/commands/process_notifications.py +0 -370
  225. mojo/apps/notify/mod +0 -0
  226. mojo/apps/notify/models/__init__.py +0 -12
  227. mojo/apps/notify/models/account.py +0 -128
  228. mojo/apps/notify/models/attachment.py +0 -24
  229. mojo/apps/notify/models/bounce.py +0 -68
  230. mojo/apps/notify/models/complaint.py +0 -40
  231. mojo/apps/notify/models/inbox.py +0 -113
  232. mojo/apps/notify/models/inbox_message.py +0 -173
  233. mojo/apps/notify/models/outbox.py +0 -129
  234. mojo/apps/notify/models/outbox_message.py +0 -288
  235. mojo/apps/notify/models/template.py +0 -30
  236. mojo/apps/notify/providers/aws.py +0 -73
  237. mojo/apps/notify/rest/ses.py +0 -0
  238. mojo/apps/notify/utils/__init__.py +0 -2
  239. mojo/apps/notify/utils/notifications.py +0 -404
  240. mojo/apps/notify/utils/parsing.py +0 -202
  241. mojo/apps/notify/utils/render.py +0 -144
  242. mojo/apps/tasks/README.md +0 -118
  243. mojo/apps/tasks/__init__.py +0 -11
  244. mojo/apps/tasks/manager.py +0 -489
  245. mojo/apps/tasks/rest/__init__.py +0 -2
  246. mojo/apps/tasks/rest/hooks.py +0 -0
  247. mojo/apps/tasks/rest/tasks.py +0 -62
  248. mojo/apps/tasks/runner.py +0 -174
  249. mojo/apps/tasks/tq_handlers.py +0 -14
  250. mojo/helpers/aws/setup_email.py +0 -0
  251. mojo/helpers/redis.py +0 -10
  252. mojo/models/meta.py +0 -262
  253. mojo/ws4redis/README.md +0 -174
  254. mojo/ws4redis/__init__.py +0 -2
  255. mojo/ws4redis/client.py +0 -283
  256. mojo/ws4redis/connection.py +0 -327
  257. mojo/ws4redis/exceptions.py +0 -32
  258. mojo/ws4redis/redis.py +0 -183
  259. mojo/ws4redis/servers/base.py +0 -86
  260. mojo/ws4redis/servers/django.py +0 -171
  261. mojo/ws4redis/servers/uwsgi.py +0 -63
  262. mojo/ws4redis/settings.py +0 -45
  263. mojo/ws4redis/utf8validator.py +0 -128
  264. mojo/ws4redis/websocket.py +0 -403
  265. {django_nativemojo-0.1.10.dist-info → django_nativemojo-0.1.16.dist-info}/LICENSE +0 -0
  266. {django_nativemojo-0.1.10.dist-info → django_nativemojo-0.1.16.dist-info}/NOTICE +0 -0
  267. {django_nativemojo-0.1.10.dist-info → django_nativemojo-0.1.16.dist-info}/WHEEL +0 -0
  268. /mojo/apps/{notify → aws}/__init__.py +0 -0
  269. /mojo/apps/{notify/handlers → aws/migrations}/__init__.py +0 -0
  270. /mojo/apps/{notify/management → docit/markdown_plugins}/__init__.py +0 -0
  271. /mojo/apps/{notify/providers → docit/migrations}/__init__.py +0 -0
  272. /mojo/apps/{notify/rest → fileman/migrations}/__init__.py +0 -0
  273. /mojo/{ws4redis/servers → apps/jobs/examples}/__init__.py +0 -0
  274. /mojo/apps/{fileman/models/render.py → jobs/migrations/__init__.py} +0 -0
  275. /mojo/{serializers → rest}/openapi.py +0 -0
  276. /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"