django-custom-storage 0.3.2__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.
@@ -0,0 +1,22 @@
1
+ """
2
+ See PEP 386 (https://peps.python.org/pep-0386/)
3
+ """
4
+
5
+ __version__ = "0.3.2"
6
+ __version_info__ = tuple(
7
+ int(i) if i.isdigit() else i for i in __version__.split(".")
8
+ )
9
+ __license__ = "MIT"
10
+ __title__ = "custom_storage"
11
+
12
+ __author__ = "DLRSP"
13
+ __copyright__ = "Copyright 2010-present DLRSP"
14
+
15
+ # Version synonym
16
+ VERSION = __version_info__
17
+
18
+ # Header encoding (see RFC5987)
19
+ HTTP_HEADER_ENCODING = "iso-8859-1"
20
+
21
+ # Default datetime input and output formats
22
+ ISO_8601 = "iso-8601"
custom_storage/apps.py ADDED
@@ -0,0 +1,11 @@
1
+ # coding: utf-8
2
+ from django.apps import AppConfig
3
+
4
+
5
+ class CustomStorageConfig(AppConfig):
6
+ default_auto_field = "django.db.models.AutoField"
7
+ name = "custom_storage"
8
+ verbose_name = "Custom Storage"
9
+
10
+ def ready(self):
11
+ from . import settings # noqa: F401
@@ -0,0 +1,201 @@
1
+ import datetime
2
+ import logging
3
+ import os
4
+ import sys
5
+
6
+ import django
7
+ from django.conf import settings
8
+ from django.core.exceptions import ImproperlyConfigured
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+ # todo: https://overtag.dk/v2/blog/a-settings-pattern-for-reusable-django-apps/
13
+ # todo: https://docs.djangoproject.com/en/5.1/topics/settings/
14
+
15
+
16
+ def setting(name, default=None):
17
+ """
18
+ Helper function to get a Django setting by name. If setting doesn't exists
19
+ it will return a default.
20
+
21
+ :param name: Name of setting
22
+ :type name: str
23
+ :param default: Value if setting is unfound
24
+ :returns: Setting's value
25
+ """
26
+ return getattr(settings, name, default)
27
+
28
+
29
+ # overwrite settings for local DEBUG mode
30
+ if settings.DEBUG:
31
+ settings.configure(COMPRESS_ENABLED=False)
32
+ settings.configure(COMPRESS_OFFLINE=False)
33
+ settings.configure(STATIC_URL="/static/")
34
+ settings.configure(COMPRESS_ROOT=settings.STATIC_ROOT)
35
+ settings.configure(COMPRESS_URL=settings.STATIC_URL)
36
+
37
+ # todo: check how tow
38
+ # settings.configure(STORAGES=settings.STORAGES["default"]["BACKEND"]("django.contrib.staticfiles.storage.StaticFilesStorage"))
39
+ settings.STORAGES["default"][
40
+ "BACKEND"
41
+ ] = "django.contrib.staticfiles.storage.StaticFilesStorage"
42
+ settings.STATICFILES_FINDERS = (
43
+ "django.contrib.staticfiles.finders.FileSystemFinder",
44
+ "django.contrib.staticfiles.finders.AppDirectoriesFinder",
45
+ )
46
+ if os.name == "nt":
47
+ MEDIA_ROOT = os.path.join(settings.WORK_DIR, "mediaroot")
48
+ STATIC_ROOT = os.path.join(settings.WORK_DIR, "staticroot")
49
+
50
+ # STORAGE Configurations
51
+ AWS_S3_STATIC_URL = getattr(settings, "AWS_S3_STATIC_URL", settings.STATIC_URL)
52
+ if not AWS_S3_STATIC_URL.endswith("/"):
53
+ raise ImproperlyConfigured(
54
+ "The STATIC_URL and AWS_S3_STATIC_URL settings must have a trailing slash."
55
+ )
56
+ AWS_S3_STATIC_ROOT = getattr(
57
+ settings, "AWS_S3_STATIC_ROOT", settings.STATIC_ROOT
58
+ )
59
+ AWS_S3_STATIC_LOCATION = getattr(settings, "AWS_S3_STATIC_LOCATION", "static")
60
+
61
+ AWS_S3_MEDIA_URL = getattr(settings, "AWS_S3_MEDIA_URL", settings.MEDIA_URL)
62
+ if not AWS_S3_MEDIA_URL.endswith("/"):
63
+ raise ImproperlyConfigured(
64
+ "The MEDIA_URL and AWS_S3_MEDIA_URL settings must have a trailing slash."
65
+ )
66
+ AWS_S3_MEDIA_ROOT = getattr(settings, "AWS_S3_MEDIA_ROOT", settings.MEDIA_ROOT)
67
+ AWS_S3_MEDIA_LOCATION = getattr(settings, "AWS_S3_MEDIA_LOCATION", "media")
68
+
69
+ AWS_S3_COMPRESS_ROOT = getattr(
70
+ settings, "AWS_S3_COMPRESS_ROOT", AWS_S3_STATIC_ROOT
71
+ )
72
+ settings.COMPRESS_ROOT = AWS_S3_COMPRESS_ROOT
73
+
74
+ # Add option to FORCE_LOCAL_STORAGE
75
+ # https://www.mslinn.com/django/1300-django-aws-control.html
76
+ if "--force-local-storage" in sys.argv:
77
+ # print("AWS datastore disabled; using local storage for assets instead.")
78
+ settings.FORCE_LOCAL_STORAGE = True
79
+ sys.argv.remove("--force-local-storage")
80
+ else:
81
+ # print("Using AWS datastore for assets.")
82
+ settings.FORCE_LOCAL_STORAGE = False
83
+
84
+ # settings.STORAGES = {
85
+ # "default": {
86
+ # "BACKEND": "custom_storage.storage.MediaRootCachedS3Storage",
87
+ # "OPTIONS": {},
88
+ # },
89
+ # "staticfiles": {
90
+ # "BACKEND": "custom_storage.storage.StaticRootCachedS3Storage",
91
+ # "OPTIONS": {},
92
+ # },
93
+ # "compressor": {
94
+ # "BACKEND": "compressor.storage.CompressorFileStorage",
95
+ # "OPTIONS": {},
96
+ # },
97
+ # "local": {
98
+ # "BACKEND": "django.core.files.storage.FileSystemStorage",
99
+ # "OPTIONS": {},
100
+ # },
101
+ # }
102
+
103
+ # DEFAULT_FILE_STORAGE was deprecated in Django 4.2 and removed in 5.1; reading
104
+ # or assigning it raises under -W error::DeprecationWarning. Derive the default
105
+ # backend from the modern STORAGES setting instead (honoring a legacy override
106
+ # only on Django < 4.2, where the alias is still the source of truth).
107
+ if django.VERSION < (4, 2):
108
+ _default_backend = setting(
109
+ "DEFAULT_FILE_STORAGE", settings.STORAGES["default"]["BACKEND"]
110
+ )
111
+ else:
112
+ _default_backend = settings.STORAGES["default"]["BACKEND"]
113
+ settings.THUMBNAIL_DEFAULT_STORAGE = setting(
114
+ "THUMBNAIL_DEFAULT_STORAGE", _default_backend
115
+ )
116
+
117
+ if settings.FORCE_LOCAL_STORAGE:
118
+ settings.STORAGES["default"][
119
+ "BACKEND"
120
+ ] = "django.core.files.storage.FileSystemStorage"
121
+ else:
122
+ if os.getenv("RUN_COMPRESS", True):
123
+ settings.STORAGES["staticfiles"][
124
+ "BACKEND"
125
+ ] = "custom_storage.storage.StaticRootCachedS3Storage"
126
+
127
+ settings.COMPRESS_OUTPUT_DIR = setting(
128
+ "COMPRESS_OUTPUT_DIR", "compressed_static"
129
+ )
130
+ settings.COMPRESS_STORAGE = setting(
131
+ "COMPRESS_STORAGE", settings.STORAGES["staticfiles"]["BACKEND"]
132
+ )
133
+
134
+ # AWS S3 Configurations
135
+ settings.AWS_S3_FILE_OVERWRITE = setting("AWS_S3_FILE_OVERWRITE", False)
136
+ settings.AWS_DEFAULT_ACL = setting("AWS_DEFAULT_ACL", "public-read")
137
+ settings.AWS_QUERYSTRING_AUTH = setting("AWS_QUERYSTRING_AUTH", False)
138
+ settings.AWS_FILE_EXPIRE = setting("AWS_FILE_EXPIRE", 61)
139
+ settings.AWS_PRELOAD_METADATA = setting("AWS_PRELOAD_METADATA", True)
140
+
141
+ two_months = datetime.timedelta(days=settings.AWS_FILE_EXPIRE)
142
+ date_two_months_later = datetime.date.today() + two_months
143
+ expires = date_two_months_later.strftime("%A, %d %B %Y 20:00:00 GMT")
144
+ settings.AWS_S3_OBJECT_PARAMETERS = setting(
145
+ "AWS_S3_OBJECT_PARAMETERS",
146
+ {
147
+ "Expires": expires,
148
+ "CacheControl": "max-age=%d" % (int(two_months.total_seconds()),),
149
+ },
150
+ )
151
+
152
+ # Compress Configurations
153
+ settings.COMPRESS_ENABLED = setting("COMPRESS_ENABLED", True)
154
+ settings.COMPRESS_OFFLINE = setting("COMPRESS_OFFLINE", True)
155
+ settings.STATICFILES_FINDERS = setting(
156
+ "STATICFILES_FINDERS",
157
+ (
158
+ "django.contrib.staticfiles.finders.FileSystemFinder",
159
+ "django.contrib.staticfiles.finders.AppDirectoriesFinder",
160
+ "compressor.finders.CompressorFinder",
161
+ ),
162
+ )
163
+ settings.COMPRESS_CSS_HASHING_METHOD = setting(
164
+ "COMPRESS_CSS_HASHING_METHOD", None
165
+ )
166
+ settings.COMPRESS_CSS_FILTERS = setting(
167
+ "COMPRESS_CSS_FILTERS",
168
+ [
169
+ "compressor.filters.css_default.CssAbsoluteFilter",
170
+ # 'compressor.filters.css_default.CssRelativeFilter',
171
+ "compressor.filters.cssmin.CSSMinFilter",
172
+ ],
173
+ )
174
+ settings.COMPRESS_JS_FILTERS = setting(
175
+ "COMPRESS_JS_FILTERS",
176
+ [
177
+ "compressor.filters.jsmin.JSMinFilter",
178
+ ],
179
+ )
180
+ settings.KEEP_COMMENTS_ON_MINIFYING = setting(
181
+ "KEEP_COMMENTS_ON_MINIFYING", False
182
+ )
183
+ settings.HTML_MINIFY = setting("HTML_MINIFY", True)
184
+
185
+ # # overwrite settings for local DEBUG mode
186
+ # if settings.DEBUG:
187
+ # settings.COMPRESS_ENABLED = False
188
+ # settings.COMPRESS_OFFLINE = False
189
+ # settings.STATIC_URL = "/static/"
190
+ # settings.COMPRESS_ROOT = settings.STATIC_ROOT
191
+ # settings.COMPRESS_URL = settings.STATIC_URL
192
+ # settings.STORAGES["default"][
193
+ # "BACKEND"
194
+ # ] = "django.contrib.staticfiles.storage.StaticFilesStorage"
195
+ # settings.STATICFILES_FINDERS = (
196
+ # "django.contrib.staticfiles.finders.FileSystemFinder",
197
+ # "django.contrib.staticfiles.finders.AppDirectoriesFinder",
198
+ # )
199
+ # if os.name == "nt":
200
+ # MEDIA_ROOT = os.path.join(settings.WORK_DIR, "mediaroot")
201
+ # STATIC_ROOT = os.path.join(settings.WORK_DIR, "staticroot")
@@ -0,0 +1,104 @@
1
+ from django.conf import settings
2
+ from django.utils.module_loading import import_string
3
+ from storages.backends.s3 import S3Storage
4
+
5
+ from .settings import (
6
+ AWS_S3_MEDIA_LOCATION,
7
+ AWS_S3_MEDIA_URL,
8
+ AWS_S3_STATIC_LOCATION,
9
+ AWS_S3_STATIC_URL,
10
+ )
11
+
12
+ # Django deprecated django.core.files.storage.get_storage_class in 4.2 and
13
+ # removed it in 5.1. Always resolve via import_string so we never trigger
14
+ # RemovedInDjango51Warning (which is fatal under -W error::DeprecationWarning)
15
+ # nor break on Django 5.1+. Kept as a module-level name for backward compat.
16
+ def get_storage_class(import_path):
17
+ return import_string(import_path)
18
+
19
+
20
+ class StaticRootCachedS3Storage(S3Storage):
21
+ """
22
+ S3 storage backend that saves the files locally, too.
23
+
24
+ The defaults for ``location`` and ``base_url`` are ``AWS_S3_STATIC_LOCATION`` and
25
+ ``AWS_S3_STATIC_URL``.
26
+
27
+ The defaults for ``local_storage`` is ``STORAGE['compressor']['BACKENDS']``
28
+
29
+ """
30
+
31
+ def __init__(
32
+ self, location=None, base_url=None, local_storage=None, *args, **kwargs
33
+ ):
34
+ super().__init__(*args, **kwargs)
35
+ if location is None:
36
+ self.location = AWS_S3_STATIC_LOCATION
37
+ if base_url is None:
38
+ self.base_url = AWS_S3_STATIC_URL
39
+ if local_storage is None:
40
+ self.local_storage = get_storage_class(
41
+ settings.STORAGES["compressor"]["BACKEND"]
42
+ )()
43
+ # self.location = "static"
44
+ # self.local_storage = get_storage_class("compressor.storage.CompressorFileStorage")()
45
+
46
+ def save(self, name, content, max_length=None):
47
+ self.local_storage._save(name, content)
48
+ super().save(name, self.local_storage._open(name))
49
+ return name
50
+
51
+
52
+ class MediaRootCachedS3Storage(S3Storage):
53
+ """
54
+ S3 storage backend that saves the files locally, too.
55
+
56
+ The defaults for ``location`` and ``base_url`` are ``AWS_S3_MEDIA_LOCATION`` and
57
+ ``AWS_S3_MEDIA_URL``.
58
+
59
+ The defaults for ``local_storage`` is ``STORAGE['local']['BACKENDS']``
60
+
61
+ """
62
+
63
+ def __init__(
64
+ self, location=None, base_url=None, local_storage=None, *args, **kwargs
65
+ ):
66
+ super().__init__(*args, **kwargs)
67
+ if location is None:
68
+ self.location = AWS_S3_MEDIA_LOCATION
69
+ if base_url is None:
70
+ self.base_url = AWS_S3_MEDIA_URL
71
+ if local_storage is None:
72
+ self.local_storage = get_storage_class(
73
+ settings.STORAGES["local"]["BACKEND"]
74
+ )()
75
+ # self.location = "media"
76
+ # self.local_storage = get_storage_class("django.core.files.storage.FileSystemStorage")()
77
+
78
+ def save(self, name, content, max_length=None):
79
+ self.local_storage._save(name, content)
80
+ super().save(name, self.local_storage._open(name))
81
+ return name
82
+
83
+ def delete(self, name):
84
+ self.local_storage.delete(name)
85
+ super().delete(name)
86
+
87
+ # Delete optimized image if exist
88
+ for extension in ("webp", "avif"):
89
+ print(f"{name}.{extension}")
90
+ self.local_storage.delete(f"{name}.{extension}")
91
+ super().delete(f"{name}.{extension}")
92
+
93
+
94
+ class PublicMediaS3Boto3Storage(S3Storage):
95
+ location = "media"
96
+ default_acl = "public-read"
97
+ file_overwrite = False
98
+
99
+
100
+ class PrivateMediaS3Boto3Storage(S3Storage):
101
+ location = "private"
102
+ default_acl = "private"
103
+ file_overwrite = False
104
+ custom_domain = False
@@ -0,0 +1,123 @@
1
+ #
2
+ # This file is autogenerated by pip-compile with Python 3.10
3
+ # by the following command:
4
+ #
5
+ # pip-compile --allow-unsafe --generate-hashes --output-file=requirements/py310-django32.txt requirements/runtime.in
6
+ #
7
+ asgiref==3.11.1 \
8
+ --hash=sha256:5f184dc43b7e763efe848065441eac62229c9f7b0475f41f80e207a114eda4ce \
9
+ --hash=sha256:e8667a091e69529631969fd45dc268fa79b99c92c5fcdda727757e52146ec133
10
+ # via django
11
+ boto3==1.43.36 \
12
+ --hash=sha256:42942dde254673abcbc9e6e60017c88341a4f49d99d24e1f2e290fb38138c26f \
13
+ --hash=sha256:587d7ee92a12e440ad12b0e7f11f3358f0c4d65b19f64726efc94aaf194aff28
14
+ # via django-storages
15
+ botocore==1.43.36 \
16
+ --hash=sha256:3c65fdc39ed01d8dfde1e961b34038aed03c459f8ddf80717a12ac006475e49d \
17
+ --hash=sha256:4cae47d1b2d426316b85a0087d9e69e048f13bc003b5177d74639fe9dfd28205
18
+ # via
19
+ # boto3
20
+ # s3transfer
21
+ django==3.2.25 \
22
+ --hash=sha256:7ca38a78654aee72378594d63e51636c04b8e28574f5505dff630895b5472777 \
23
+ --hash=sha256:a52ea7fcf280b16f7b739cec38fa6d3f8953a5456986944c3ca97e79882b4e38
24
+ # via
25
+ # -r requirements/runtime.in
26
+ # django-appconf
27
+ # django-storages
28
+ django-appconf==1.2.0 \
29
+ --hash=sha256:15a88d60dd942d6059f467412fe4581db632ef03018a3c183fb415d6fc9e5cec \
30
+ --hash=sha256:b81bce5ef0ceb9d84df48dfb623a32235d941c78cc5e45dbb6947f154ea277f4
31
+ # via django-compressor
32
+ django-compressor==4.4 \
33
+ --hash=sha256:1b0acc9cfba9f69bc38e7c41da9b0d70a20bc95587b643ffef9609cf46064f67 \
34
+ --hash=sha256:6e2b0c0becb9607f5099c2546a824c5b84a6918a34bc37a8a622ffa250313596
35
+ # via -r requirements/runtime.in
36
+ django-storages[s3]==1.14.6 \
37
+ --hash=sha256:11b7b6200e1cb5ffcd9962bd3673a39c7d6a6109e8096f0e03d46fab3d3aabd9 \
38
+ --hash=sha256:7a25ce8f4214f69ac9c7ce87e2603887f7ae99326c316bc8d2d75375e09341c9
39
+ # via -r requirements/runtime.in
40
+ jmespath==1.1.0 \
41
+ --hash=sha256:472c87d80f36026ae83c6ddd0f1d05d4e510134ed462851fd5f754c8c3cbb88d \
42
+ --hash=sha256:a5663118de4908c91729bea0acadca56526eb2698e83de10cd116ae0f4e97c64
43
+ # via
44
+ # boto3
45
+ # botocore
46
+ python-dateutil==2.9.0.post0 \
47
+ --hash=sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3 \
48
+ --hash=sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427
49
+ # via botocore
50
+ pytz==2026.2 \
51
+ --hash=sha256:04156e608bee23d3792fd45c94ae47fae1036688e75032eea2e3bf0323d1f126 \
52
+ --hash=sha256:0e60b47b29f21574376f218fe21abc009894a2321ea16c6754f3cad6eb7cdd6a
53
+ # via django
54
+ rcssmin==1.1.1 \
55
+ --hash=sha256:271e3d2f8614a6d4637ed8fff3d90007f03e2a654cd9444f37d888797662ba72 \
56
+ --hash=sha256:35da6a6999e9e2c5b0e691b42ed56cc479373e0ecab33ef5277dfecce625e44a \
57
+ --hash=sha256:42576d95dfad53d77df2e68dfdec95b89b10fad320f241f1af3ca1438578254a \
58
+ --hash=sha256:4f9400b4366d29f5f5446f58e78549afa8338e6a59740c73115e9f6ac413dc64 \
59
+ --hash=sha256:705c9112d0ed54ea40aecf97e7fd29bdf0f1c46d278a32d8f957f31dde90778a \
60
+ --hash=sha256:79421230dd67c37ec61ed9892813d2b839b68f2f48ef55c75f976e81701d60b4 \
61
+ --hash=sha256:868215e1fd0e92a6122e0ed5973dfc7bb8330fe1e92274d05b2585253b38c0ca \
62
+ --hash=sha256:8a26fec3c1e6b7a3765ccbaccc20fbb5c0ed3422cc381e01a2607f08d7621c44 \
63
+ --hash=sha256:8fcfd10ae2a1c4ce231a33013f2539e07c3836bf17cc945cc25cc30bf8e68e45 \
64
+ --hash=sha256:908fe072efd2432fb0975a61124609a8e05021367f6a3463d45f5e3e74c4fdda \
65
+ --hash=sha256:914e589f40573035006913861ed2adc28fbe70082a8b6bff5be7ee430b7b5c2e \
66
+ --hash=sha256:a04d58a2a21e9a089306d3f99c4b12bf5b656a79c198ef2321e80f8fd9afab06 \
67
+ --hash=sha256:a417735d4023d47d048a6288c88dbceadd20abaaf65a11bb4fda1e8458057019 \
68
+ --hash=sha256:c30f8bc839747b6da59274e0c6e4361915d66532e26448d589cb2b1846d7bf11 \
69
+ --hash=sha256:c7278c1c25bb90d8e554df92cfb3b6a1195004ead50f764653d3093933ee0877 \
70
+ --hash=sha256:c7728e3b546b1b6ea08cab721e8e21409dbcc11b881d0b87d10b0be8930af2a2 \
71
+ --hash=sha256:cf74d7ea5e191f0f344b354eed8b7c83eeafbd9a97bec3a579c3d26edf11b005 \
72
+ --hash=sha256:d0afc6e7b64ef30d6dcde88830ec1a237b9f16a39f920a8fd159928684ccf8db \
73
+ --hash=sha256:d4e263fa9428704fd94c2cb565c7519ca1d225217943f71caffe6741ab5b9df1 \
74
+ --hash=sha256:e923c105100ab70abde1c01d3196ddd6b07255e32073685542be4e3a60870c8e \
75
+ --hash=sha256:ee386bec6d62f8c814d65c011d604a7c82d24aa3f718facd66e850eea8d6a5a1 \
76
+ --hash=sha256:f15673e97f0a68b4c378c4d15b088fe96d60bc106d278c88829923118833c20f \
77
+ --hash=sha256:f7a1fcdbafaacac0530da04edca4a44303baab430ea42e7d59aece4b3f3e9a51
78
+ # via django-compressor
79
+ rjsmin==1.2.1 \
80
+ --hash=sha256:113132a40ce7d03b2ced4fac215f0297338ed1c207394b739266efab7831988b \
81
+ --hash=sha256:122aa52bcf7ad9f12728d309012d1308c6ecfe4d6b09ea867a110dcad7b7728c \
82
+ --hash=sha256:145c6af8df42d8af102d0d39a6de2e5fa66aef9e38947cfb9d65377d1b9940b2 \
83
+ --hash=sha256:1f982be8e011438777a94307279b40134a3935fc0f079312ee299725b8af5411 \
84
+ --hash=sha256:3453ee6d5e7a2723ec45c2909e2382371783400e8d51952b692884c6d850a3d0 \
85
+ --hash=sha256:35827844d2085bd59d34214dfba6f1fc42a215c455887437b07dbf9c73019cc1 \
86
+ --hash=sha256:35f21046504544e2941e04190ce24161255479133751550e36ddb3f4af0ecdca \
87
+ --hash=sha256:5d67ec09da46a492186e35cabca02a0d092eda5ef5b408a419b99ee4acf28d5c \
88
+ --hash=sha256:747bc9d3bc8a220f40858e6aad50b2ae2eb7f69c924d4fa3803b81be1c1ddd02 \
89
+ --hash=sha256:7dd58b5ed88233bc61dc80b0ed87b93a1786031d9977c70d335221ef1ac5581a \
90
+ --hash=sha256:812af25c08d6a5ae98019a2e1b47ebb47f7469abd351670c353d619eaeae4064 \
91
+ --hash=sha256:8a6710e358c661dcdcfd027e67de3afd72a6af4c88101dcf110de39e9bbded39 \
92
+ --hash=sha256:8c340e251619c97571a5ade20f147f1f7e8664f66a2d6d7319e05e3ef6a4423c \
93
+ --hash=sha256:99c074cd6a8302ff47118a9c3d086f89328dc8e5c4b105aa1f348fb85c765a30 \
94
+ --hash=sha256:b8464629a18fe69f70677854c93a3707976024b226a0ce62707c618f923e1346 \
95
+ --hash=sha256:bbd7a0abaa394afd951f5d4e05249d306fec1c9674bfee179787674dddd0bdb7 \
96
+ --hash=sha256:bc5bc2f94e59bc81562c572b7f1bdd6bcec4f61168dc68a2993bad2d355b6e19 \
97
+ --hash=sha256:bd1faedc425006d9e86b23837d164f01d105b7a8b66b767a9766d0014773db2a \
98
+ --hash=sha256:ca90630b84fe94bb07739c3e3793e87d30c6ee450dde08653121f0d9153c8d0d \
99
+ --hash=sha256:d332e44a1b21ad63401cc7eebc81157e3d982d5fb503bb4faaea5028068d71e9 \
100
+ --hash=sha256:eb770aaf637919b0011c4eb87b9ac6317079fb9800eb17c90dda05fc9de4ebc3 \
101
+ --hash=sha256:f0895b360dccf7e2d6af8762a52985e3fbaa56778de1bf6b20dbc96134253807 \
102
+ --hash=sha256:f7cd33602ec0f393a0058e883284496bb4dbbdd34e0bbe23b594c8933ddf9b65
103
+ # via django-compressor
104
+ s3transfer==0.19.0 \
105
+ --hash=sha256:777cc2415536f1debadb5c2ef7779275d0fc0fe0e042411cdd6caebeb2685262 \
106
+ --hash=sha256:ce436931687addc4c1712d52d40b32f53e88315723f107ffa20ba82b05a0f685
107
+ # via boto3
108
+ six==1.17.0 \
109
+ --hash=sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274 \
110
+ --hash=sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81
111
+ # via python-dateutil
112
+ sqlparse==0.5.5 \
113
+ --hash=sha256:12a08b3bf3eec877c519589833aed092e2444e68240a3577e8e26148acc7b1ba \
114
+ --hash=sha256:e20d4a9b0b8585fdf63b10d30066c7c94c5d7a7ec47c889a2d83a3caa93ff28e
115
+ # via django
116
+ typing-extensions==4.15.0 \
117
+ --hash=sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466 \
118
+ --hash=sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548
119
+ # via asgiref
120
+ urllib3==2.7.0 \
121
+ --hash=sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c \
122
+ --hash=sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897
123
+ # via botocore