meringue 1.2.0.dev8__tar.gz → 1.3.0.dev1__tar.gz
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.
- {meringue-1.2.0.dev8 → meringue-1.3.0.dev1}/PKG-INFO +12 -12
- {meringue-1.2.0.dev8 → meringue-1.3.0.dev1}/README.md +6 -5
- {meringue-1.2.0.dev8 → meringue-1.3.0.dev1}/meringue/__init__.py +1 -1
- {meringue-1.2.0.dev8 → meringue-1.3.0.dev1}/meringue/api/handlers.py +2 -1
- {meringue-1.2.0.dev8 → meringue-1.3.0.dev1}/meringue/api/utils.py +1 -0
- {meringue-1.2.0.dev8 → meringue-1.3.0.dev1}/meringue/conf/__init__.py +2 -1
- meringue-1.3.0.dev1/meringue/core/db.py +77 -0
- {meringue-1.2.0.dev8 → meringue-1.3.0.dev1}/meringue/core/query.py +1 -3
- {meringue-1.2.0.dev8 → meringue-1.3.0.dev1}/meringue/protected/fields.py +7 -5
- {meringue-1.2.0.dev8 → meringue-1.3.0.dev1}/meringue/protected/views.py +7 -6
- {meringue-1.2.0.dev8 → meringue-1.3.0.dev1}/meringue/thumbnail/drf_fields.py +24 -14
- {meringue-1.2.0.dev8 → meringue-1.3.0.dev1}/meringue/thumbnail/images.py +1 -1
- {meringue-1.2.0.dev8 → meringue-1.3.0.dev1}/meringue/thumbnail/shortcuts.py +2 -2
- {meringue-1.2.0.dev8 → meringue-1.3.0.dev1}/pyproject.toml +86 -58
- {meringue-1.2.0.dev8 → meringue-1.3.0.dev1}/.gitignore +0 -0
- {meringue-1.2.0.dev8 → meringue-1.3.0.dev1}/AUTHORS +0 -0
- {meringue-1.2.0.dev8 → meringue-1.3.0.dev1}/LICENSE +0 -0
- {meringue-1.2.0.dev8 → meringue-1.3.0.dev1}/meringue/api/__init__.py +0 -0
- {meringue-1.2.0.dev8 → meringue-1.3.0.dev1}/meringue/api/apps.py +0 -0
- {meringue-1.2.0.dev8 → meringue-1.3.0.dev1}/meringue/api/docs/__init__.py +0 -0
- {meringue-1.2.0.dev8 → meringue-1.3.0.dev1}/meringue/api/docs/patchers.py +0 -0
- {meringue-1.2.0.dev8 → meringue-1.3.0.dev1}/meringue/api/docs/views.py +0 -0
- {meringue-1.2.0.dev8 → meringue-1.3.0.dev1}/meringue/api/routers.py +0 -0
- {meringue-1.2.0.dev8 → meringue-1.3.0.dev1}/meringue/conf/default_settings.py +0 -0
- {meringue-1.2.0.dev8 → meringue-1.3.0.dev1}/meringue/core/__init__.py +0 -0
- {meringue-1.2.0.dev8 → meringue-1.3.0.dev1}/meringue/core/apps.py +0 -0
- {meringue-1.2.0.dev8 → meringue-1.3.0.dev1}/meringue/core/locale/en/LC_MESSAGES/django.po +0 -0
- {meringue-1.2.0.dev8 → meringue-1.3.0.dev1}/meringue/core/locale/ru/LC_MESSAGES/django.po +0 -0
- {meringue-1.2.0.dev8 → meringue-1.3.0.dev1}/meringue/core/models.py +0 -0
- {meringue-1.2.0.dev8 → meringue-1.3.0.dev1}/meringue/core/options.py +0 -0
- {meringue-1.2.0.dev8 → meringue-1.3.0.dev1}/meringue/core/templatetags/__init__.py +0 -0
- {meringue-1.2.0.dev8 → meringue-1.3.0.dev1}/meringue/core/templatetags/meringue_base.py +0 -0
- {meringue-1.2.0.dev8 → meringue-1.3.0.dev1}/meringue/core/translation.py +0 -0
- {meringue-1.2.0.dev8 → meringue-1.3.0.dev1}/meringue/core/upload_handlers.py +0 -0
- {meringue-1.2.0.dev8 → meringue-1.3.0.dev1}/meringue/core/utils/__init__.py +0 -0
- {meringue-1.2.0.dev8 → meringue-1.3.0.dev1}/meringue/core/utils/crypt.py +0 -0
- {meringue-1.2.0.dev8 → meringue-1.3.0.dev1}/meringue/core/utils/datetime.py +0 -0
- {meringue-1.2.0.dev8 → meringue-1.3.0.dev1}/meringue/core/utils/frontend.py +0 -0
- {meringue-1.2.0.dev8 → meringue-1.3.0.dev1}/meringue/core/views.py +0 -0
- {meringue-1.2.0.dev8 → meringue-1.3.0.dev1}/meringue/protected/__init__.py +0 -0
- {meringue-1.2.0.dev8 → meringue-1.3.0.dev1}/meringue/protected/apps.py +0 -0
- {meringue-1.2.0.dev8 → meringue-1.3.0.dev1}/meringue/protected/utils.py +0 -0
- {meringue-1.2.0.dev8 → meringue-1.3.0.dev1}/meringue/thumbnail/__init__.py +0 -0
- {meringue-1.2.0.dev8 → meringue-1.3.0.dev1}/meringue/thumbnail/actions.py +0 -0
- {meringue-1.2.0.dev8 → meringue-1.3.0.dev1}/meringue/thumbnail/apps.py +0 -0
- {meringue-1.2.0.dev8 → meringue-1.3.0.dev1}/meringue/thumbnail/constants.py +0 -0
- {meringue-1.2.0.dev8 → meringue-1.3.0.dev1}/meringue/thumbnail/exceptions.py +0 -0
- {meringue-1.2.0.dev8 → meringue-1.3.0.dev1}/meringue/thumbnail/generators.py +0 -0
- {meringue-1.2.0.dev8 → meringue-1.3.0.dev1}/meringue/thumbnail/properties.py +0 -0
- {meringue-1.2.0.dev8 → meringue-1.3.0.dev1}/meringue/thumbnail/storage.py +0 -0
- {meringue-1.2.0.dev8 → meringue-1.3.0.dev1}/meringue/thumbnail/templatetags/__init__.py +0 -0
- {meringue-1.2.0.dev8 → meringue-1.3.0.dev1}/meringue/thumbnail/templatetags/m_thumbnails.py +0 -0
- {meringue-1.2.0.dev8 → meringue-1.3.0.dev1}/meringue/thumbnail/types.py +0 -0
|
@@ -1,22 +1,17 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: meringue
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.3.0.dev1
|
|
4
4
|
Summary: A set of various functionality for a Django based web application.
|
|
5
5
|
Project-URL: Documentation, https://dd.github.io/Meringue
|
|
6
6
|
Project-URL: Repository, https://github.com/dd/Meringue
|
|
7
7
|
Project-URL: Changelog, https://github.com/dd/Meringue/blob/master/CHANGELOG.md
|
|
8
8
|
Project-URL: Bug Tracker, https://github.com/dd/Meringue/issues
|
|
9
9
|
Author-email: Dmitry Dobrynin <dd@tovarisch.engineer>
|
|
10
|
-
License-Expression: LGPL-3.0
|
|
11
|
-
License-File: AUTHORS
|
|
12
|
-
License-File: LICENSE
|
|
13
10
|
Keywords: django,utils
|
|
14
11
|
Classifier: Development Status :: 5 - Production/Stable
|
|
15
12
|
Classifier: Environment :: Plugins
|
|
16
13
|
Classifier: Environment :: Web Environment
|
|
17
14
|
Classifier: Framework :: Django
|
|
18
|
-
Classifier: Framework :: Django :: 1
|
|
19
|
-
Classifier: Framework :: Django :: 1.11
|
|
20
15
|
Classifier: Framework :: Django :: 2
|
|
21
16
|
Classifier: Framework :: Django :: 2.0
|
|
22
17
|
Classifier: Framework :: Django :: 2.1
|
|
@@ -29,6 +24,7 @@ Classifier: Framework :: Django :: 4
|
|
|
29
24
|
Classifier: Framework :: Django :: 4.0
|
|
30
25
|
Classifier: Framework :: Django :: 4.1
|
|
31
26
|
Classifier: Framework :: Django :: 4.2
|
|
27
|
+
Classifier: Framework :: Django :: 5.0
|
|
32
28
|
Classifier: Intended Audience :: Developers
|
|
33
29
|
Classifier: License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)
|
|
34
30
|
Classifier: Natural Language :: English
|
|
@@ -38,6 +34,7 @@ Classifier: Programming Language :: Python :: 3
|
|
|
38
34
|
Classifier: Programming Language :: Python :: 3 :: Only
|
|
39
35
|
Classifier: Programming Language :: Python :: 3.10
|
|
40
36
|
Classifier: Programming Language :: Python :: 3.11
|
|
37
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
41
38
|
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
42
39
|
Classifier: Programming Language :: Python :: Implementation :: PyPy
|
|
43
40
|
Classifier: Topic :: Internet :: WWW/HTTP
|
|
@@ -45,9 +42,11 @@ Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
|
|
|
45
42
|
Classifier: Topic :: Internet :: WWW/HTTP :: WSGI
|
|
46
43
|
Classifier: Topic :: Software Development :: Libraries
|
|
47
44
|
Requires-Python: >=3.10
|
|
48
|
-
Requires-Dist: django
|
|
45
|
+
Requires-Dist: django<6,>=2.0.0
|
|
49
46
|
Provides-Extra: cryptodome
|
|
50
47
|
Requires-Dist: pycryptodome==3.20.0; extra == 'cryptodome'
|
|
48
|
+
Provides-Extra: django-hosts
|
|
49
|
+
Requires-Dist: django-hosts<7,>=5.2; extra == 'django-hosts'
|
|
51
50
|
Provides-Extra: drf
|
|
52
51
|
Requires-Dist: djangorestframework<4,>=3.13; extra == 'drf'
|
|
53
52
|
Provides-Extra: drf-spectacular
|
|
@@ -74,6 +73,9 @@ Description-Content-Type: text/markdown
|
|
|
74
73
|
<a href="https://pypi.org/project/meringue">
|
|
75
74
|
<img src="https://img.shields.io/pypi/pyversions/meringue.svg" alt="PyPI - Python Version" />
|
|
76
75
|
</a>
|
|
76
|
+
<a href="https://pypi.org/project/meringue">
|
|
77
|
+
<img src="https://img.shields.io/pypi/frameworkversions/django/meringue" alt="PyPI - Versions from Framework Classifiers" />
|
|
78
|
+
</a>
|
|
77
79
|
<!-- <a href="https://pypi.org/project/meringue">
|
|
78
80
|
<img src="https://img.shields.io/pypi/format/meringue.svg" alt="PyPI - Format" />
|
|
79
81
|
</a> -->
|
|
@@ -94,7 +96,7 @@ Description-Content-Type: text/markdown
|
|
|
94
96
|
<img src="https://img.shields.io/badge/%F0%9F%A5%9A-Hatch-4051b5.svg" alt="Hatch project" />
|
|
95
97
|
</a>
|
|
96
98
|
<a href="https://squidfunk.github.io/mkdocs-material/" target="_blank">
|
|
97
|
-
<img src="https://img.shields.io/badge
|
|
99
|
+
<img src="https://img.shields.io/badge/-Material_for_MkDocs-526CFE?logo=MaterialForMkDocs&logoColor=white&labelColor=gray" alt="Built with Material for MkDocs" />
|
|
98
100
|
</a>
|
|
99
101
|
<a href="https://github.com/charliermarsh/ruff" target="_blank">
|
|
100
102
|
<img src="https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/charliermarsh/ruff/main/assets/badge/v2.json" alt="linting - Ruff" />
|
|
@@ -110,11 +112,9 @@ Description-Content-Type: text/markdown
|
|
|
110
112
|
</a>
|
|
111
113
|
</p>
|
|
112
114
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
This library is a set of various functionality that I use from project to project.
|
|
115
|
+
A package providing various utilities for Django, such as mixins, form tools, upload handlers, and more.
|
|
116
116
|
|
|
117
|
-
|
|
117
|
+
This library is a collection of reusable components that I frequently use across different projects. Its primary purpose is to clean up and standardize these tools, ensure they are well-tested, and provide clear documentation to make it easy for colleagues to understand and use.
|
|
118
118
|
|
|
119
119
|
However, if someone decides to use this functionality in their project, and even more so to add functionality or change the implementation to a more correct, beautiful or understandable one, I will only be happy, do not worry and feel free to write to me by [mail](mailto:dd@tovarisch.engineer), create an [issue](https://github.com/dd/Meringue/issues) or [pull request](https://github.com/dd/Meringue/pulls) on [github](https://github.com/dd/Meringue).
|
|
120
120
|
|
|
@@ -16,6 +16,9 @@
|
|
|
16
16
|
<a href="https://pypi.org/project/meringue">
|
|
17
17
|
<img src="https://img.shields.io/pypi/pyversions/meringue.svg" alt="PyPI - Python Version" />
|
|
18
18
|
</a>
|
|
19
|
+
<a href="https://pypi.org/project/meringue">
|
|
20
|
+
<img src="https://img.shields.io/pypi/frameworkversions/django/meringue" alt="PyPI - Versions from Framework Classifiers" />
|
|
21
|
+
</a>
|
|
19
22
|
<!-- <a href="https://pypi.org/project/meringue">
|
|
20
23
|
<img src="https://img.shields.io/pypi/format/meringue.svg" alt="PyPI - Format" />
|
|
21
24
|
</a> -->
|
|
@@ -36,7 +39,7 @@
|
|
|
36
39
|
<img src="https://img.shields.io/badge/%F0%9F%A5%9A-Hatch-4051b5.svg" alt="Hatch project" />
|
|
37
40
|
</a>
|
|
38
41
|
<a href="https://squidfunk.github.io/mkdocs-material/" target="_blank">
|
|
39
|
-
<img src="https://img.shields.io/badge
|
|
42
|
+
<img src="https://img.shields.io/badge/-Material_for_MkDocs-526CFE?logo=MaterialForMkDocs&logoColor=white&labelColor=gray" alt="Built with Material for MkDocs" />
|
|
40
43
|
</a>
|
|
41
44
|
<a href="https://github.com/charliermarsh/ruff" target="_blank">
|
|
42
45
|
<img src="https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/charliermarsh/ruff/main/assets/badge/v2.json" alt="linting - Ruff" />
|
|
@@ -52,11 +55,9 @@
|
|
|
52
55
|
</a>
|
|
53
56
|
</p>
|
|
54
57
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
This library is a set of various functionality that I use from project to project.
|
|
58
|
+
A package providing various utilities for Django, such as mixins, form tools, upload handlers, and more.
|
|
58
59
|
|
|
59
|
-
|
|
60
|
+
This library is a collection of reusable components that I frequently use across different projects. Its primary purpose is to clean up and standardize these tools, ensure they are well-tested, and provide clear documentation to make it easy for colleagues to understand and use.
|
|
60
61
|
|
|
61
62
|
However, if someone decides to use this functionality in their project, and even more so to add functionality or change the implementation to a more correct, beautiful or understandable one, I will only be happy, do not worry and feel free to write to me by [mail](mailto:dd@tovarisch.engineer), create an [issue](https://github.com/dd/Meringue/issues) or [pull request](https://github.com/dd/Meringue/pulls) on [github](https://github.com/dd/Meringue).
|
|
62
63
|
|
|
@@ -6,6 +6,7 @@ from rest_framework import views
|
|
|
6
6
|
|
|
7
7
|
from meringue.api.utils import render_error_details
|
|
8
8
|
|
|
9
|
+
|
|
9
10
|
try:
|
|
10
11
|
from rest_framework_simplejwt.exceptions import DetailDictMixin
|
|
11
12
|
except ImportError:
|
|
@@ -24,7 +25,7 @@ def exception_handler(exc, context):
|
|
|
24
25
|
if response is None:
|
|
25
26
|
return response
|
|
26
27
|
|
|
27
|
-
if isinstance(exc,
|
|
28
|
+
if isinstance(exc, Http404 | PermissionDenied):
|
|
28
29
|
# django Http404 and PermissionDenied errors are substituted for drf errors,
|
|
29
30
|
# in `rest_framework.views.exception_handler` method.
|
|
30
31
|
response.data = render_error_details(response.data["detail"])
|
|
@@ -202,7 +202,8 @@ class Settings:
|
|
|
202
202
|
"""
|
|
203
203
|
|
|
204
204
|
if attr not in self.defaults:
|
|
205
|
-
|
|
205
|
+
msg = f"Invalid setting key: '{attr}'"
|
|
206
|
+
raise AttributeError(msg)
|
|
206
207
|
|
|
207
208
|
if attr in self.deprecated_params:
|
|
208
209
|
warnings.warn(self.deprecated_params[attr], DeprecationWarning, stacklevel=2)
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import warnings
|
|
2
|
+
from functools import wraps
|
|
3
|
+
|
|
4
|
+
from django.conf import settings
|
|
5
|
+
from django.db import connections
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class PgAdvisoryLock:
|
|
9
|
+
"""
|
|
10
|
+
A context manager and decorator for using PostgreSQL advisory locks in Django.
|
|
11
|
+
|
|
12
|
+
This class provides a way to use PostgreSQL advisory locks, ensuring that the lock
|
|
13
|
+
is acquired and released correctly. It supports usage as both a context manager
|
|
14
|
+
and a function decorator. The class also checks if the database is PostgreSQL and
|
|
15
|
+
issues a warning if it is not.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
def __init__(self, table: str, value: str, field: str = "id", using: str = "default"):
|
|
19
|
+
"""
|
|
20
|
+
Attributes:
|
|
21
|
+
table: The name of the table to query for the lock ID.
|
|
22
|
+
value: The value to match in the field to retrieve the lock ID.
|
|
23
|
+
field: The name of the field to query for the lock ID.
|
|
24
|
+
using: The database alias to use.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
self.table = table
|
|
28
|
+
self.value = value
|
|
29
|
+
self.field = field
|
|
30
|
+
self.db_name = using
|
|
31
|
+
self.is_postgresql = self._check_postgresql()
|
|
32
|
+
|
|
33
|
+
def __enter__(self):
|
|
34
|
+
self.lock()
|
|
35
|
+
return self
|
|
36
|
+
|
|
37
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
38
|
+
self.unlock()
|
|
39
|
+
return False
|
|
40
|
+
|
|
41
|
+
def __call__(self, func):
|
|
42
|
+
@wraps(func)
|
|
43
|
+
def wrapped(*args, **kwargs):
|
|
44
|
+
with self:
|
|
45
|
+
return func(*args, **kwargs)
|
|
46
|
+
|
|
47
|
+
return wrapped
|
|
48
|
+
|
|
49
|
+
def _check_postgresql(self) -> bool:
|
|
50
|
+
engine = settings.DATABASES[self.db_name]["ENGINE"]
|
|
51
|
+
return "postgresql" in engine
|
|
52
|
+
|
|
53
|
+
def lock(self) -> None:
|
|
54
|
+
"""
|
|
55
|
+
Acquires the advisory lock.
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
if not self.is_postgresql:
|
|
59
|
+
msg = "pg_advisory_lock is only supported with PostgreSQL databases."
|
|
60
|
+
warnings.warn(msg, UserWarning, stacklevel=2)
|
|
61
|
+
return
|
|
62
|
+
|
|
63
|
+
sql = f"SELECT pg_advisory_lock({self.field}) FROM {self.table} WHERE {self.field} = %s" # noqa: S608
|
|
64
|
+
with connections[self.db_name].cursor() as cursor:
|
|
65
|
+
cursor.execute(sql, [self.value])
|
|
66
|
+
|
|
67
|
+
def unlock(self) -> None:
|
|
68
|
+
"""
|
|
69
|
+
Releases the advisory lock.
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
if not self.is_postgresql:
|
|
73
|
+
return
|
|
74
|
+
|
|
75
|
+
sql = f"SELECT pg_advisory_unlock({self.field}) FROM {self.table} WHERE {self.field} = %s" # noqa: S608
|
|
76
|
+
with connections[self.db_name].cursor() as cursor:
|
|
77
|
+
cursor.execute(sql, [self.value])
|
|
@@ -18,12 +18,10 @@ class SortingQuerySet(QuerySet):
|
|
|
18
18
|
The selection for updating sorting can be pre-limited by filtering the list.
|
|
19
19
|
"""
|
|
20
20
|
items = []
|
|
21
|
-
sorting
|
|
22
|
-
for item in self:
|
|
21
|
+
for sorting, item in enumerate(self):
|
|
23
22
|
if item.sorting != sorting:
|
|
24
23
|
item.sorting = sorting
|
|
25
24
|
items.append(item)
|
|
26
|
-
sorting += 1
|
|
27
25
|
|
|
28
26
|
return self.bulk_update(items, ["sorting"])
|
|
29
27
|
|
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
from django.contrib.contenttypes.models import ContentType
|
|
2
|
-
from django.db.models.fields.files import FileField
|
|
3
2
|
from django.db.models.fields.files import FieldFile
|
|
3
|
+
from django.db.models.fields.files import FileField
|
|
4
4
|
from django.db.models.fields.files import ImageField
|
|
5
5
|
from django.db.models.fields.files import ImageFieldFile
|
|
6
6
|
from django.urls import reverse
|
|
7
|
+
|
|
8
|
+
from meringue.conf import m_settings
|
|
9
|
+
|
|
10
|
+
|
|
7
11
|
try:
|
|
8
12
|
from django_hosts.resolvers import reverse as hosts_reverse
|
|
9
13
|
except ImportError:
|
|
10
14
|
hosts_reverse = None
|
|
11
15
|
|
|
12
|
-
from meringue.conf import m_settings
|
|
13
|
-
|
|
14
16
|
|
|
15
17
|
class ProtectedFileMixin:
|
|
16
18
|
@property
|
|
@@ -86,7 +88,7 @@ class ProtectedFileField(FileField):
|
|
|
86
88
|
|
|
87
89
|
def deconstruct(self):
|
|
88
90
|
name, path, args, kwargs = super().deconstruct()
|
|
89
|
-
kwargs[
|
|
91
|
+
kwargs["view_name"] = self.m_protected_view_name
|
|
90
92
|
return name, path, args, kwargs
|
|
91
93
|
|
|
92
94
|
|
|
@@ -122,5 +124,5 @@ class ProtectedImageField(ImageField):
|
|
|
122
124
|
|
|
123
125
|
def deconstruct(self):
|
|
124
126
|
name, path, args, kwargs = super().deconstruct()
|
|
125
|
-
kwargs[
|
|
127
|
+
kwargs["view_name"] = self.m_protected_view_name
|
|
126
128
|
return name, path, args, kwargs
|
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
import mimetypes
|
|
2
2
|
from pathlib import Path
|
|
3
3
|
from urllib.parse import quote
|
|
4
|
-
from urllib.parse import urljoin
|
|
5
4
|
|
|
6
|
-
from django.conf import settings
|
|
7
5
|
from django.contrib.contenttypes.models import ContentType
|
|
8
6
|
from django.core.exceptions import PermissionDenied
|
|
9
7
|
from django.http import FileResponse
|
|
@@ -51,7 +49,10 @@ def x_accel_redirect_view(request, cid, field, pk, disp="inline"):
|
|
|
51
49
|
response["X-Accel-Redirect"] = redirect_url
|
|
52
50
|
return response
|
|
53
51
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
52
|
+
with open(file.path, "rb") as tmp_file:
|
|
53
|
+
response = FileResponse(
|
|
54
|
+
tmp_file,
|
|
55
|
+
as_attachment=disp == "attachment",
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
return response
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import logging
|
|
2
|
-
import
|
|
3
|
-
from pathlib import PurePath
|
|
2
|
+
from pathlib import Path
|
|
4
3
|
|
|
5
4
|
from rest_framework.fields import ImageField
|
|
6
5
|
|
|
@@ -13,7 +12,7 @@ logger = logging.getLogger("meringue.thumbnail")
|
|
|
13
12
|
|
|
14
13
|
|
|
15
14
|
def get_format_from_path(path):
|
|
16
|
-
return FORMATS_BY_EXTENSIONS[
|
|
15
|
+
return FORMATS_BY_EXTENSIONS[path.suffix.lower()]
|
|
17
16
|
|
|
18
17
|
|
|
19
18
|
class BaseImageField(ImageField):
|
|
@@ -53,13 +52,15 @@ class MImageField(BaseImageField):
|
|
|
53
52
|
if not value:
|
|
54
53
|
return None
|
|
55
54
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
55
|
+
path = Path(value.path)
|
|
56
|
+
|
|
57
|
+
if path.exists():
|
|
58
|
+
thumbnail = DefaultThumbnailer(path, job_chain=self.job_chain)
|
|
59
|
+
optimized_image_url = thumbnail.get_image(get_format_from_path(path)).url
|
|
59
60
|
result = optimized_image_url
|
|
60
61
|
|
|
61
62
|
else:
|
|
62
|
-
logger.error(f"File `{
|
|
63
|
+
logger.error(f"File `{path}` not found")
|
|
63
64
|
result = _dummyimage([])
|
|
64
65
|
|
|
65
66
|
return result
|
|
@@ -76,16 +77,23 @@ class MImageSetField(ImageField):
|
|
|
76
77
|
def __init__(
|
|
77
78
|
self,
|
|
78
79
|
size=None,
|
|
79
|
-
dimensions=
|
|
80
|
+
dimensions=(1, 2),
|
|
80
81
|
base_job_chain=None,
|
|
81
82
|
job_chains=None,
|
|
82
83
|
**kwargs,
|
|
83
84
|
):
|
|
84
|
-
if
|
|
85
|
-
msg = "Need to set
|
|
85
|
+
if size and job_chains:
|
|
86
|
+
msg = "Need to set 'size' or 'job_chains' attribute (not both)."
|
|
86
87
|
raise Exception(msg)
|
|
87
88
|
|
|
88
89
|
elif size:
|
|
90
|
+
if not dimensions:
|
|
91
|
+
msg = (
|
|
92
|
+
"If you specify the 'sizes' attribute, "
|
|
93
|
+
"then the 'dimensions' attribute cannot be empty."
|
|
94
|
+
)
|
|
95
|
+
raise Exception(msg)
|
|
96
|
+
|
|
89
97
|
self.job_chains = {}
|
|
90
98
|
|
|
91
99
|
if 1 not in dimensions:
|
|
@@ -147,8 +155,10 @@ class MImageSetField(ImageField):
|
|
|
147
155
|
if not value:
|
|
148
156
|
return None
|
|
149
157
|
|
|
150
|
-
|
|
151
|
-
|
|
158
|
+
path = Path(value.path)
|
|
159
|
+
|
|
160
|
+
if not path.exists():
|
|
161
|
+
logger.error(f"File `{path}` not found")
|
|
152
162
|
return [
|
|
153
163
|
{
|
|
154
164
|
"url": _dummyimage(self.job_chains[1]),
|
|
@@ -156,11 +166,11 @@ class MImageSetField(ImageField):
|
|
|
156
166
|
},
|
|
157
167
|
]
|
|
158
168
|
|
|
159
|
-
original_format = get_format_from_path(
|
|
169
|
+
original_format = get_format_from_path(path)
|
|
160
170
|
result = []
|
|
161
171
|
|
|
162
172
|
for dimension, job_chain in self.job_chains.items():
|
|
163
|
-
thumbnail = DefaultThumbnailer(
|
|
173
|
+
thumbnail = DefaultThumbnailer(path, job_chain=job_chain)
|
|
164
174
|
original = thumbnail.get_image(original_format)
|
|
165
175
|
webp_thumbnail = thumbnail.get_image("WEBP")
|
|
166
176
|
|
|
@@ -131,7 +131,7 @@ class ThumbnailImage(AltersData):
|
|
|
131
131
|
if self.out_format == constants.FORMAT_WEBP:
|
|
132
132
|
return "image/webp"
|
|
133
133
|
|
|
134
|
-
guess_type = mimetypes.guess_type(
|
|
134
|
+
guess_type = mimetypes.guess_type(self.filename)
|
|
135
135
|
return guess_type[0]
|
|
136
136
|
|
|
137
137
|
@property
|
|
@@ -42,8 +42,8 @@ def _dummyimage(job_chain: str) -> str:
|
|
|
42
42
|
height=size[1],
|
|
43
43
|
)
|
|
44
44
|
|
|
45
|
-
return "
|
|
46
|
-
# return "
|
|
45
|
+
return f"{settings.STATIC_URL}images/noise.png"
|
|
46
|
+
# return f"{settings.STATIC_URL}images/none.gif"
|
|
47
47
|
|
|
48
48
|
|
|
49
49
|
def get_thumbnail(
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
[build-system]
|
|
2
|
-
requires = ["hatchling>=1.
|
|
2
|
+
requires = ["hatchling>=1.22.5"]
|
|
3
3
|
build-backend = "hatchling.build"
|
|
4
4
|
|
|
5
5
|
[project]
|
|
@@ -17,8 +17,6 @@ classifiers = [
|
|
|
17
17
|
"Environment :: Plugins",
|
|
18
18
|
"Environment :: Web Environment",
|
|
19
19
|
"Framework :: Django",
|
|
20
|
-
"Framework :: Django :: 1",
|
|
21
|
-
"Framework :: Django :: 1.11",
|
|
22
20
|
"Framework :: Django :: 2",
|
|
23
21
|
"Framework :: Django :: 2.0",
|
|
24
22
|
"Framework :: Django :: 2.1",
|
|
@@ -31,6 +29,7 @@ classifiers = [
|
|
|
31
29
|
"Framework :: Django :: 4.0",
|
|
32
30
|
"Framework :: Django :: 4.1",
|
|
33
31
|
"Framework :: Django :: 4.2",
|
|
32
|
+
"Framework :: Django :: 5.0",
|
|
34
33
|
"Intended Audience :: Developers",
|
|
35
34
|
"License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)",
|
|
36
35
|
"Natural Language :: English",
|
|
@@ -40,6 +39,7 @@ classifiers = [
|
|
|
40
39
|
"Programming Language :: Python :: 3",
|
|
41
40
|
"Programming Language :: Python :: 3.10",
|
|
42
41
|
"Programming Language :: Python :: 3.11",
|
|
42
|
+
"Programming Language :: Python :: 3.12",
|
|
43
43
|
"Programming Language :: Python :: 3 :: Only",
|
|
44
44
|
"Programming Language :: Python :: Implementation :: CPython",
|
|
45
45
|
"Programming Language :: Python :: Implementation :: PyPy",
|
|
@@ -49,7 +49,7 @@ classifiers = [
|
|
|
49
49
|
"Topic :: Software Development :: Libraries",
|
|
50
50
|
]
|
|
51
51
|
dependencies = [
|
|
52
|
-
"Django>=
|
|
52
|
+
"Django>=2.0.0,<6",
|
|
53
53
|
]
|
|
54
54
|
dynamic = ["version"]
|
|
55
55
|
|
|
@@ -58,6 +58,7 @@ modeltranslation = ["django-modeltranslation>=0.17,<0.19"]
|
|
|
58
58
|
drf = ["djangorestframework>=3.13,<4"]
|
|
59
59
|
drf-spectacular = ["drf-spectacular>=0.26.3,<1"]
|
|
60
60
|
cryptodome = ["pycryptodome==3.20.0"]
|
|
61
|
+
django_hosts = ["django_hosts>=5.2,<7"]
|
|
61
62
|
|
|
62
63
|
[project.urls]
|
|
63
64
|
"Documentation" = "https://dd.github.io/Meringue"
|
|
@@ -85,15 +86,17 @@ exclude = [
|
|
|
85
86
|
description = "Dev environment"
|
|
86
87
|
python = "3.12"
|
|
87
88
|
dependencies = [
|
|
88
|
-
"pre-commit==3.
|
|
89
|
-
"ipython==8.
|
|
90
|
-
"django==
|
|
91
|
-
"pytz==
|
|
89
|
+
"pre-commit==3.7.0",
|
|
90
|
+
"ipython==8.23.0",
|
|
91
|
+
"django==5.0.4",
|
|
92
|
+
"pytz==2024.1",
|
|
92
93
|
"django-modeltranslation==0.18.11",
|
|
93
94
|
"pycryptodome==3.20.0",
|
|
94
|
-
"djangorestframework==3.
|
|
95
|
-
"
|
|
96
|
-
"
|
|
95
|
+
"djangorestframework==3.15.1",
|
|
96
|
+
"djangorestframework-simplejwt==5.3.1",
|
|
97
|
+
"drf-spectacular==0.27.2",
|
|
98
|
+
"Pillow==10.3.0",
|
|
99
|
+
"django_hosts==6.0",
|
|
97
100
|
]
|
|
98
101
|
[tool.hatch.envs.default.env-vars]
|
|
99
102
|
DJANGO_SETTINGS_MODULE = "test_project.settings"
|
|
@@ -121,43 +124,48 @@ remove = [
|
|
|
121
124
|
]
|
|
122
125
|
makemessages = "cd meringue/core && django-admin makemessages -l en -l ru --no-obsolete {args}"
|
|
123
126
|
compilemessages = "cd meringue && django-admin compilemessages -l en -l ru {args}"
|
|
127
|
+
changelog-update = [
|
|
128
|
+
"git fetch origin --tags",
|
|
129
|
+
"gitmoji-changelog update \"$(hatch version)\" --preset generic --group-similar-commits",
|
|
130
|
+
]
|
|
131
|
+
release-tag = "git tag -a \"v$(hatch version)\" -m \"v$(hatch version)\""
|
|
124
132
|
|
|
125
133
|
[tool.hatch.envs.lint]
|
|
126
134
|
description = "Lint environment"
|
|
127
135
|
detached = true
|
|
128
|
-
python = "3.
|
|
136
|
+
python = "3.12"
|
|
129
137
|
dependencies = [
|
|
130
|
-
"ruff==0.
|
|
131
|
-
"black==24.
|
|
138
|
+
"ruff==0.4.9",
|
|
139
|
+
"black==24.4.2",
|
|
132
140
|
]
|
|
133
141
|
[tool.hatch.envs.lint.scripts]
|
|
134
142
|
check = [
|
|
135
|
-
"ruff {args:.}",
|
|
143
|
+
"ruff check {args:.}",
|
|
136
144
|
"black --check --diff --exclude=\".*migrations\\/.*$\" {args:.}",
|
|
137
145
|
]
|
|
138
146
|
format = [
|
|
139
|
-
"ruff
|
|
147
|
+
"ruff format {args:.}",
|
|
140
148
|
"black --exclude=\".*migrations\\/.*$\" {args:.}",
|
|
141
149
|
]
|
|
142
150
|
|
|
143
151
|
[tool.hatch.envs.test]
|
|
144
152
|
description = "Tests environment"
|
|
145
153
|
detached = true
|
|
146
|
-
python = "3.
|
|
154
|
+
python = "3.12"
|
|
147
155
|
dependencies = [
|
|
148
|
-
"pytest==
|
|
149
|
-
"pytest-django==4.
|
|
150
|
-
"pytest-cov==
|
|
151
|
-
"Faker==
|
|
152
|
-
"
|
|
153
|
-
"pytz==2023.3.post1",
|
|
154
|
-
"django-modeltranslation==0.18.11",
|
|
156
|
+
"pytest==8.1.1",
|
|
157
|
+
"pytest-django==4.8.0",
|
|
158
|
+
"pytest-cov==5.0.0", # for ci tests with cover
|
|
159
|
+
"Faker==24.7.1",
|
|
160
|
+
"pytz==2024.1",
|
|
155
161
|
"pycryptodome==3.20.0",
|
|
156
|
-
"
|
|
157
|
-
"djangorestframework-simplejwt==5.3.1",
|
|
158
|
-
"drf-spectacular==0.26.4",
|
|
162
|
+
"drf-spectacular==0.27.2",
|
|
159
163
|
"pillow==10.3.0",
|
|
160
|
-
"
|
|
164
|
+
"django==5.0.4",
|
|
165
|
+
"django-modeltranslation==0.18.11",
|
|
166
|
+
"djangorestframework==3.15.1",
|
|
167
|
+
"djangorestframework-simplejwt==5.3.1",
|
|
168
|
+
"django_hosts==6.0",
|
|
161
169
|
]
|
|
162
170
|
[tool.hatch.envs.test.env-vars]
|
|
163
171
|
DJANGO_SETTINGS_MODULE = "test_project.settings"
|
|
@@ -170,15 +178,14 @@ makemigrations = "django-admin makemigrations {args}"
|
|
|
170
178
|
description = "Test matrix environment"
|
|
171
179
|
detached = true
|
|
172
180
|
dependencies = [
|
|
173
|
-
"pytest==
|
|
174
|
-
"pytest-django==4.
|
|
175
|
-
"pytest-cov==
|
|
176
|
-
"Faker==
|
|
177
|
-
"pytz==
|
|
181
|
+
"pytest==8.1.1",
|
|
182
|
+
"pytest-django==4.8.0",
|
|
183
|
+
"pytest-cov==5.0.0", # for ci tests with cover
|
|
184
|
+
"Faker==24.7.1",
|
|
185
|
+
"pytz==2024.1",
|
|
178
186
|
"pycryptodome==3.20.0",
|
|
179
|
-
"drf-spectacular==0.
|
|
187
|
+
"drf-spectacular==0.27.2",
|
|
180
188
|
"pillow==10.3.0",
|
|
181
|
-
"django_hosts==5.2",
|
|
182
189
|
]
|
|
183
190
|
[tool.hatch.envs.mtest.overrides]
|
|
184
191
|
matrix.django.dependencies = [
|
|
@@ -186,6 +193,7 @@ matrix.django.dependencies = [
|
|
|
186
193
|
{ value = "django-modeltranslation=={matrix:modeltranslation}" },
|
|
187
194
|
{ value = "djangorestframework=={matrix:djangorestframework}" },
|
|
188
195
|
{ value = "djangorestframework-simplejwt=={matrix:simplejwt}" },
|
|
196
|
+
{ value = "django_hosts=={matrix:django_hosts}" },
|
|
189
197
|
]
|
|
190
198
|
[tool.hatch.envs.mtest.env-vars]
|
|
191
199
|
DJANGO_SETTINGS_MODULE = "test_project.settings"
|
|
@@ -195,39 +203,56 @@ check = "pytest {args:-q}"
|
|
|
195
203
|
[[tool.hatch.envs.mtest.matrix]]
|
|
196
204
|
python = ["3.10", "3.11"]
|
|
197
205
|
django = ["2.0"]
|
|
198
|
-
modeltranslation = ["0.17.0"]
|
|
206
|
+
modeltranslation = ["0.17.0", "0.18.2"]
|
|
199
207
|
djangorestframework = ["3.13.0"]
|
|
200
208
|
simplejwt = ["5.2.0"]
|
|
209
|
+
django_hosts = ["5.2", "6.0"]
|
|
201
210
|
[[tool.hatch.envs.mtest.matrix]]
|
|
202
211
|
python = ["3.10", "3.11"]
|
|
203
212
|
django = ["3.0"]
|
|
204
|
-
modeltranslation = ["0.17.0"]
|
|
213
|
+
modeltranslation = ["0.17.0", "0.18.2"]
|
|
205
214
|
djangorestframework = ["3.13.0", "3.14.0", "3.15.0"]
|
|
206
215
|
simplejwt = ["5.2.0", "5.3.0"]
|
|
216
|
+
django_hosts = ["5.2", "6.0"]
|
|
207
217
|
[[tool.hatch.envs.mtest.matrix]]
|
|
208
218
|
python = ["3.10", "3.11"]
|
|
209
219
|
django = ["4.0"]
|
|
210
|
-
modeltranslation = ["0.17.0"]
|
|
211
|
-
djangorestframework = ["3.14.0"]
|
|
220
|
+
modeltranslation = ["0.17.0", "0.18.2"]
|
|
221
|
+
djangorestframework = ["3.14.0", "3.15.0"]
|
|
212
222
|
simplejwt = ["5.2.0", "5.3.0"]
|
|
223
|
+
django_hosts = ["5.2", "6.0"]
|
|
224
|
+
[[tool.hatch.envs.mtest.matrix]]
|
|
225
|
+
python = ["3.12"]
|
|
226
|
+
django = ["4.0"]
|
|
227
|
+
modeltranslation = ["0.17.0", "0.18.2"]
|
|
228
|
+
djangorestframework = ["3.14.0", "3.15.0"]
|
|
229
|
+
simplejwt = ["5.3.1"]
|
|
230
|
+
django_hosts = ["6.0"]
|
|
231
|
+
[[tool.hatch.envs.mtest.matrix]]
|
|
232
|
+
python = ["3.10", "3.11", "3.12"]
|
|
233
|
+
django = ["5.0"]
|
|
234
|
+
modeltranslation = ["0.17.0", "0.18.2"]
|
|
235
|
+
djangorestframework = ["3.15.0"]
|
|
236
|
+
simplejwt = ["5.3.1"]
|
|
237
|
+
django_hosts = ["6.0"]
|
|
213
238
|
|
|
214
239
|
[tool.hatch.envs.docs]
|
|
215
240
|
description = "Docs environment"
|
|
216
241
|
detached = true
|
|
217
|
-
python = "3.
|
|
242
|
+
python = "3.12"
|
|
218
243
|
dependencies = [
|
|
219
|
-
"mkdocs[i18n]==1.
|
|
244
|
+
"mkdocs[i18n]==1.6.1",
|
|
220
245
|
"mkdocs-literate-nav==0.6.1",
|
|
221
|
-
"mkdocs-material==9.5.
|
|
222
|
-
"mkdocs-git-revision-date-localized-plugin==1.2.
|
|
223
|
-
"mkdocs-git-authors-plugin==0.
|
|
224
|
-
"mkdocstrings[python]==0.
|
|
225
|
-
"black==24.
|
|
246
|
+
"mkdocs-material==9.5.40",
|
|
247
|
+
"mkdocs-git-revision-date-localized-plugin==1.2.9",
|
|
248
|
+
"mkdocs-git-authors-plugin==0.9.0",
|
|
249
|
+
"mkdocstrings[python]==0.26.1",
|
|
250
|
+
"black==24.10.0",
|
|
226
251
|
"mkdocs-minify-plugin==0.8.0",
|
|
227
252
|
"mkdocs-gen-files==0.5.0",
|
|
228
|
-
"Pygments==2.
|
|
229
|
-
"mike==
|
|
230
|
-
"linkchecker==10.
|
|
253
|
+
"Pygments==2.18.0",
|
|
254
|
+
"mike==2.1.3",
|
|
255
|
+
"linkchecker==10.5.0",
|
|
231
256
|
]
|
|
232
257
|
[tool.hatch.envs.docs.env-vars]
|
|
233
258
|
MERINGUE_MKDOCS_ENABLE_MINIFY = "false"
|
|
@@ -235,7 +260,7 @@ MKDOCS_CONFIG = "mkdocs.yml"
|
|
|
235
260
|
[tool.hatch.envs.docs.scripts]
|
|
236
261
|
build = "mkdocs build --config-file {env:MKDOCS_CONFIG} --clean --strict {args}"
|
|
237
262
|
serve = "mkdocs serve --config-file {env:MKDOCS_CONFIG} --dev-addr localhost:8000 {args}"
|
|
238
|
-
ci-build = "mike deploy --config-file {env:MKDOCS_CONFIG}
|
|
263
|
+
ci-build = "mike deploy --config-file {env:MKDOCS_CONFIG} {args}"
|
|
239
264
|
validate = "linkchecker --config .linkcheckerrc docs/dist"
|
|
240
265
|
build-check = [
|
|
241
266
|
"build --no-directory-urls",
|
|
@@ -244,13 +269,14 @@ build-check = [
|
|
|
244
269
|
|
|
245
270
|
[tool.black]
|
|
246
271
|
line-length = 100
|
|
247
|
-
target-version = ["
|
|
272
|
+
target-version = ["py310"]
|
|
248
273
|
|
|
249
274
|
[tool.ruff]
|
|
250
|
-
target-version = "
|
|
275
|
+
target-version = "py310"
|
|
251
276
|
line-length = 100
|
|
252
277
|
show-fixes = true
|
|
253
278
|
# update-check = true
|
|
279
|
+
[tool.ruff.lint]
|
|
254
280
|
select = [
|
|
255
281
|
"A",
|
|
256
282
|
"B",
|
|
@@ -292,32 +318,34 @@ ignore = [
|
|
|
292
318
|
# "PLC1901", # empty string comparisons
|
|
293
319
|
# "PLW2901", # `for` loop variable overwritten
|
|
294
320
|
"SIM114", # Combine `if` branches using logical `or` operator
|
|
321
|
+
"ISC001", # Disabled as ruff recomendation from warning
|
|
295
322
|
]
|
|
296
323
|
# unfixable = [
|
|
297
324
|
# # Don't touch unused imports
|
|
298
325
|
# "F401",
|
|
299
326
|
# ]
|
|
300
|
-
[tool.ruff.
|
|
327
|
+
[tool.ruff.lint.per-file-ignores]
|
|
301
328
|
"__init__.py" = ["F401", "F403"]
|
|
302
329
|
"test_*.py" = ["S101", "PLR2004", "DTZ001", "RUF012"]
|
|
303
330
|
"*/migrations/*" = ["I", "E", "Q", "RUF"]
|
|
304
331
|
"test_project/*models.py" = ["RUF012"]
|
|
305
332
|
"test_project/*views.py" = ["RUF012"]
|
|
306
|
-
[tool.ruff.flake8-import-conventions]
|
|
307
|
-
[tool.ruff.flake8-import-conventions.extend-aliases]
|
|
333
|
+
[tool.ruff.lint.flake8-import-conventions.extend-aliases]
|
|
308
334
|
"datetime" = "dt"
|
|
309
335
|
# [tool.ruff.flake8-quotes]
|
|
310
336
|
# inline-quotes = "single"
|
|
311
|
-
[tool.ruff.flake8-unused-arguments]
|
|
337
|
+
[tool.ruff.lint.flake8-unused-arguments]
|
|
312
338
|
ignore-variadic-names = true
|
|
313
|
-
[tool.ruff.isort]
|
|
339
|
+
[tool.ruff.lint.isort]
|
|
314
340
|
force-single-line = true
|
|
315
341
|
known-first-party = ["meringue"]
|
|
316
342
|
lines-after-imports = 2
|
|
317
343
|
no-lines-before = ["local-folder"]
|
|
318
344
|
section-order = ["future", "standard-library", "django", "third-party", "first-party", "local-folder"]
|
|
319
|
-
[tool.ruff.isort.sections]
|
|
345
|
+
[tool.ruff.lint.isort.sections]
|
|
320
346
|
django = ["django"]
|
|
347
|
+
[tool.ruff.format]
|
|
348
|
+
exclude = ["*/migrations/*"]
|
|
321
349
|
|
|
322
350
|
[tool.isort]
|
|
323
351
|
known_first_party = ["meringue"]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|