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
mojo/helpers/aws/s3.py CHANGED
@@ -1,23 +1,22 @@
1
- from rest import settings
1
+ from mojo.helpers.settings import settings
2
+ from mojo.helpers import logit
2
3
  from objict import objict
3
4
  import boto3
4
5
  import botocore
5
6
  from urllib.parse import urlparse
6
7
  import io
7
- import sys
8
- from medialib import utils
9
- import threading
10
8
  import tempfile
11
- import logging
12
- from typing import Optional, Union, BinaryIO, Dict, List, Any
9
+ import json
10
+ from typing import Optional, Union, BinaryIO, Dict, List, Any, Tuple
13
11
 
14
- logger = logging.getLogger(__name__)
12
+ logger = logit.get_logger(__name__)
15
13
 
16
14
  class S3Config:
17
15
  """S3 configuration holder with lazy initialization of clients and resources."""
18
- def __init__(self, key: str, secret: str):
16
+ def __init__(self, key: str, secret: str, region_name: str):
19
17
  self.key = key
20
18
  self.secret = secret
19
+ self.region_name = region_name
21
20
  self._resource = None
22
21
  self._client = None
23
22
 
@@ -26,7 +25,8 @@ class S3Config:
26
25
  if self._resource is None:
27
26
  self._resource = boto3.resource('s3',
28
27
  aws_access_key_id=self.key,
29
- aws_secret_access_key=self.secret)
28
+ aws_secret_access_key=self.secret,
29
+ region_name=self.region_name)
30
30
  return self._resource
31
31
 
32
32
  @property
@@ -34,11 +34,36 @@ class S3Config:
34
34
  if self._client is None:
35
35
  self._client = boto3.client('s3',
36
36
  aws_access_key_id=self.key,
37
- aws_secret_access_key=self.secret)
37
+ aws_secret_access_key=self.secret,
38
+ region_name=self.region_name)
38
39
  return self._client
39
40
 
41
+ @staticmethod
42
+ def get_bucket(bucket_name):
43
+ if not bucket_name:
44
+ raise ValueError("Bucket name cannot be empty")
45
+ return S3.resource.Bucket(bucket_name)
46
+
47
+ @staticmethod
48
+ def list_all_buckets():
49
+ """
50
+ List all S3 buckets.
51
+
52
+ Returns:
53
+ List of bucket names
54
+ """
55
+ try:
56
+ response = S3.client.list_buckets()
57
+ return [
58
+ {"id": b["Name"], "name": b["Name"], "created": b["CreationDate"].timestamp()}
59
+ for b in response.get("Buckets", [])
60
+ ]
61
+ except botocore.exceptions.ClientError as e:
62
+ logger.error(f"Failed to list buckets: {e}")
63
+ return []
64
+
40
65
  # Initialize the global S3 configuration
41
- S3 = S3Config(settings.AWS_KEY, settings.AWS_SECRET)
66
+ S3 = S3Config(settings.AWS_KEY, settings.AWS_SECRET, settings.AWS_REGION)
42
67
 
43
68
 
44
69
  class S3Item:
@@ -266,3 +291,418 @@ def delete(url: str) -> None:
266
291
  def path(url: str) -> str:
267
292
  """Extract the path component from a URL."""
268
293
  return urlparse(url).path
294
+
295
+
296
+ class S3Bucket:
297
+ """
298
+ Simple interface for S3 bucket management.
299
+
300
+ This class provides methods to create, configure, and manage S3 buckets
301
+ with sensible defaults.
302
+ """
303
+
304
+ def __init__(self, name: str):
305
+ """
306
+ Initialize a bucket manager for the specified bucket.
307
+
308
+ Args:
309
+ name: The name of the S3 bucket
310
+ """
311
+ self.name = name
312
+ self.exists = self._check_exists()
313
+
314
+ def _check_exists(self) -> bool:
315
+ """Check if the bucket exists."""
316
+ try:
317
+ S3.client.head_bucket(Bucket=self.name)
318
+ return True
319
+ except botocore.exceptions.ClientError as e:
320
+ if e.response['Error']['Code'] == '404':
321
+ return False
322
+ # If it's a different error (e.g., 403 forbidden), still return False
323
+ # but log the error
324
+ logger.warning(f"Error checking bucket existence: {e}")
325
+ return False
326
+
327
+ def create(self, region: Optional[str] = None, public_access: bool = False) -> bool:
328
+ """
329
+ Create the S3 bucket with optional configuration.
330
+
331
+ Args:
332
+ region: AWS region for the bucket. If None, uses configured region.
333
+ public_access: Whether to allow public access to the bucket.
334
+
335
+ Returns:
336
+ True if bucket was created, False if it already exists
337
+ """
338
+ if self.exists:
339
+ logger.info(f"Bucket {self.name} already exists")
340
+ return False
341
+
342
+ # Use configured region if none specified
343
+ if region is None:
344
+ region = S3.region_name
345
+
346
+ create_params = {'Bucket': self.name}
347
+
348
+ # Add region configuration if specified
349
+ if region and region != 'us-east-1':
350
+ create_params['CreateBucketConfiguration'] = {
351
+ 'LocationConstraint': region
352
+ }
353
+
354
+ try:
355
+ S3.client.create_bucket(**create_params)
356
+ self.exists = True
357
+
358
+ # Configure public access blocking based on the public_access parameter
359
+ if not public_access:
360
+ S3.client.put_public_access_block(
361
+ Bucket=self.name,
362
+ PublicAccessBlockConfiguration={
363
+ 'BlockPublicAcls': True,
364
+ 'IgnorePublicAcls': True,
365
+ 'BlockPublicPolicy': True,
366
+ 'RestrictPublicBuckets': True
367
+ }
368
+ )
369
+ return True
370
+ except botocore.exceptions.ClientError as e:
371
+ logger.error(f"Failed to create bucket {self.name}: {e}")
372
+ return False
373
+
374
+ def delete(self, force: bool = False) -> bool:
375
+ """
376
+ Delete the bucket.
377
+
378
+ Args:
379
+ force: If True, delete all objects in the bucket first
380
+
381
+ Returns:
382
+ True if successfully deleted, False otherwise
383
+ """
384
+ if not self.exists:
385
+ logger.info(f"Bucket {self.name} does not exist")
386
+ return False
387
+
388
+ try:
389
+ if force:
390
+ self.delete_all_objects()
391
+
392
+ S3.client.delete_bucket(Bucket=self.name)
393
+ self.exists = False
394
+ return True
395
+ except botocore.exceptions.ClientError as e:
396
+ logger.error(f"Failed to delete bucket {self.name}: {e}")
397
+ return False
398
+
399
+ def delete_all_objects(self) -> int:
400
+ """
401
+ Delete all objects in the bucket.
402
+
403
+ Returns:
404
+ Number of objects deleted
405
+ """
406
+ count = 0
407
+ try:
408
+ # List objects in the bucket
409
+ paginator = S3.client.get_paginator('list_objects_v2')
410
+
411
+ for page in paginator.paginate(Bucket=self.name):
412
+ if 'Contents' not in page:
413
+ continue
414
+
415
+ # Delete objects in batches of 1000 (S3 API limit)
416
+ objects = [{'Key': obj['Key']} for obj in page['Contents']]
417
+ if objects:
418
+ S3.client.delete_objects(
419
+ Bucket=self.name,
420
+ Delete={'Objects': objects}
421
+ )
422
+ count += len(objects)
423
+
424
+ return count
425
+ except botocore.exceptions.ClientError as e:
426
+ logger.error(f"Failed to delete objects in bucket {self.name}: {e}")
427
+ return count
428
+
429
+ def set_policy(self, policy: Union[Dict, str]) -> bool:
430
+ """
431
+ Set a bucket policy.
432
+
433
+ Args:
434
+ policy: Policy document as dict or JSON string
435
+
436
+ Returns:
437
+ True if successful, False otherwise
438
+ """
439
+ if not self.exists:
440
+ logger.warning(f"Bucket {self.name} does not exist")
441
+ return False
442
+
443
+ try:
444
+ # Convert dict to JSON string if needed
445
+ policy_str = policy if isinstance(policy, str) else json.dumps(policy)
446
+
447
+ S3.client.put_bucket_policy(
448
+ Bucket=self.name,
449
+ Policy=policy_str
450
+ )
451
+ return True
452
+ except botocore.exceptions.ClientError as e:
453
+ logger.error(f"Failed to set policy for bucket {self.name}: {e}")
454
+ return False
455
+
456
+ def make_public(self) -> bool:
457
+ """
458
+ Make the bucket publicly readable.
459
+
460
+ Returns:
461
+ True if successful, False otherwise
462
+ """
463
+ if not self.exists:
464
+ logger.warning(f"Bucket {self.name} does not exist")
465
+ return False
466
+
467
+ try:
468
+ # Remove public access block
469
+ S3.client.put_public_access_block(
470
+ Bucket=self.name,
471
+ PublicAccessBlockConfiguration={
472
+ 'BlockPublicAcls': False,
473
+ 'IgnorePublicAcls': False,
474
+ 'BlockPublicPolicy': False,
475
+ 'RestrictPublicBuckets': False
476
+ }
477
+ )
478
+
479
+ # Set public read policy
480
+ policy = {
481
+ "Version": "2012-10-17",
482
+ "Statement": [
483
+ {
484
+ "Sid": "PublicReadGetObject",
485
+ "Effect": "Allow",
486
+ "Principal": "*",
487
+ "Action": "s3:GetObject",
488
+ "Resource": f"arn:aws:s3:::{self.name}/*"
489
+ }
490
+ ]
491
+ }
492
+
493
+ return self.set_policy(policy)
494
+ except botocore.exceptions.ClientError as e:
495
+ logger.error(f"Failed to make bucket {self.name} public: {e}")
496
+ return False
497
+
498
+ def enable_website(self, index_document: str = 'index.html',
499
+ error_document: Optional[str] = None) -> bool:
500
+ """
501
+ Configure the bucket for static website hosting.
502
+
503
+ Args:
504
+ index_document: Default index document
505
+ error_document: Custom error document
506
+
507
+ Returns:
508
+ True if successful, False otherwise
509
+ """
510
+ if not self.exists:
511
+ logger.warning(f"Bucket {self.name} does not exist")
512
+ return False
513
+
514
+ try:
515
+ website_config = {
516
+ 'IndexDocument': {'Suffix': index_document}
517
+ }
518
+
519
+ if error_document:
520
+ website_config['ErrorDocument'] = {'Key': error_document}
521
+
522
+ S3.client.put_bucket_website(
523
+ Bucket=self.name,
524
+ WebsiteConfiguration=website_config
525
+ )
526
+ return True
527
+ except botocore.exceptions.ClientError as e:
528
+ logger.error(f"Failed to configure website for bucket {self.name}: {e}")
529
+ return False
530
+
531
+ def get_website_url(self) -> str:
532
+ """
533
+ Get the website URL for this bucket.
534
+
535
+ Returns:
536
+ The website URL
537
+ """
538
+ region = S3.region_name
539
+
540
+ # Special URL format for us-east-1
541
+ if region == 'us-east-1':
542
+ return f"http://{self.name}.s3-website-{region}.amazonaws.com"
543
+ else:
544
+ return f"http://{self.name}.s3-website.{region}.amazonaws.com"
545
+
546
+ def list_objects(self, prefix: str = '', max_keys: int = 1000) -> List[Dict]:
547
+ """
548
+ List objects in the bucket.
549
+
550
+ Args:
551
+ prefix: Filter objects by prefix
552
+ max_keys: Maximum number of keys to return
553
+
554
+ Returns:
555
+ List of object metadata dictionaries
556
+ """
557
+ if not self.exists:
558
+ logger.warning(f"Bucket {self.name} does not exist")
559
+ return []
560
+
561
+ try:
562
+ response = S3.client.list_objects_v2(
563
+ Bucket=self.name,
564
+ Prefix=prefix,
565
+ MaxKeys=max_keys
566
+ )
567
+
568
+ return response.get('Contents', [])
569
+ except botocore.exceptions.ClientError as e:
570
+ logger.error(f"Failed to list objects in bucket {self.name}: {e}")
571
+ return []
572
+
573
+ def enable_versioning(self) -> bool:
574
+ """
575
+ Enable versioning on the bucket.
576
+
577
+ Returns:
578
+ True if successful, False otherwise
579
+ """
580
+ if not self.exists:
581
+ logger.warning(f"Bucket {self.name} does not exist")
582
+ return False
583
+
584
+ try:
585
+ S3.client.put_bucket_versioning(
586
+ Bucket=self.name,
587
+ VersioningConfiguration={'Status': 'Enabled'}
588
+ )
589
+ return True
590
+ except botocore.exceptions.ClientError as e:
591
+ logger.error(f"Failed to enable versioning for bucket {self.name}: {e}")
592
+ return False
593
+
594
+
595
+ def make_path_public(self, prefix: str):
596
+ # Get the current bucket policy (if any)
597
+ try:
598
+ current_policy = json.loads(S3.client.get_bucket_policy(Bucket=self.name)["Policy"])
599
+ statements = current_policy.get("Statement", [])
600
+ except S3.client.exceptions.from_code('NoSuchBucketPolicy'):
601
+ current_policy = {"Version": "2012-10-17", "Statement": []}
602
+ statements = []
603
+
604
+ # Check if our public-read rule for the prefix already exists
605
+ public_read_sid = f"AllowPublicReadForPrefix_{prefix.strip('/')}"
606
+ already_exists = any(
607
+ stmt.get("Sid") == public_read_sid for stmt in statements
608
+ )
609
+
610
+ if already_exists:
611
+ print(f"Policy for prefix '{prefix}' already exists.")
612
+ return
613
+
614
+ # Construct the public read statement for the given prefix
615
+ new_statement = {
616
+ "Sid": public_read_sid,
617
+ "Effect": "Allow",
618
+ "Principal": "*",
619
+ "Action": "s3:GetObject",
620
+ "Resource": f"arn:aws:s3:::{self.name}/{prefix}*"
621
+ }
622
+
623
+ # Add and apply the new policy
624
+ current_policy["Statement"].append(new_statement)
625
+ S3.client.put_bucket_policy(
626
+ Bucket=self.name,
627
+ Policy=json.dumps(current_policy)
628
+ )
629
+
630
+ def make_private(self):
631
+ try:
632
+ S3.client.put_public_access_block(
633
+ Bucket=self.name,
634
+ PublicAccessBlockConfiguration={
635
+ 'BlockPublicAcls': True,
636
+ 'IgnorePublicAcls': True,
637
+ 'BlockPublicPolicy': True,
638
+ 'RestrictPublicBuckets': True
639
+ }
640
+ )
641
+ return True
642
+ except botocore.exceptions.ClientError as e:
643
+ logger.error(f"Failed to make bucket {self.name} private: {e}")
644
+ return False
645
+
646
+
647
+ def enable_cors(self):
648
+ try:
649
+ S3.client.put_bucket_cors(
650
+ Bucket=self.name,
651
+ CORSConfiguration={
652
+ 'CORSRules': [
653
+ {
654
+ 'AllowedHeaders': ['*'],
655
+ 'AllowedMethods': ['GET', 'HEAD', 'PUT', 'POST', 'DELETE'],
656
+ 'AllowedOrigins': ['*'],
657
+ 'ExposeHeaders': ['ETag', 'x-amz-version-id'],
658
+ 'MaxAgeSeconds': 3000
659
+ }
660
+ ]
661
+ }
662
+ )
663
+ return True
664
+ except botocore.exceptions.ClientError as e:
665
+ logger.error(f"Failed to enable CORS for bucket {self.name}: {e}")
666
+ return False
667
+
668
+ def enable_lifecycle(self):
669
+ try:
670
+ S3.client.put_bucket_lifecycle_configuration(
671
+ Bucket=self.name,
672
+ LifecycleConfiguration={
673
+ 'Rules': [
674
+ {
675
+ 'Expiration': {'Days': 30},
676
+ 'ID': 'DeleteAfter30Days',
677
+ 'Filter': {'Prefix': 'logs/'},
678
+ 'Status': 'Enabled'
679
+ }
680
+ ]
681
+ }
682
+ )
683
+ return True
684
+ except botocore.exceptions.ClientError as e:
685
+ logger.error(f"Failed to enable lifecycle for bucket {self.name}: {e}")
686
+ return False
687
+
688
+ def get_url(self, key: str, presigned: bool = False,
689
+ expires: int = 3600) -> str:
690
+ """
691
+ Get a URL for an object in the bucket.
692
+
693
+ Args:
694
+ key: Object key
695
+ presigned: Whether to generate a presigned URL
696
+ expires: Expiration time in seconds for presigned URLs
697
+
698
+ Returns:
699
+ URL for the object
700
+ """
701
+ if presigned:
702
+ return S3.client.generate_presigned_url(
703
+ 'get_object',
704
+ Params={'Bucket': self.name, 'Key': key},
705
+ ExpiresIn=expires
706
+ )
707
+ else:
708
+ return f"https://{self.name}.s3.amazonaws.com/{key}"