photo-objects 0.0.1__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.
Files changed (49) hide show
  1. photo_objects-0.0.1/LICENSE +21 -0
  2. photo_objects-0.0.1/PKG-INFO +79 -0
  3. photo_objects-0.0.1/README.md +33 -0
  4. photo_objects-0.0.1/photo_objects/__init__.py +0 -0
  5. photo_objects-0.0.1/photo_objects/django/__init__.py +10 -0
  6. photo_objects-0.0.1/photo_objects/django/admin.py +6 -0
  7. photo_objects-0.0.1/photo_objects/django/api/__init__.py +3 -0
  8. photo_objects-0.0.1/photo_objects/django/api/album.py +57 -0
  9. photo_objects-0.0.1/photo_objects/django/api/auth.py +49 -0
  10. photo_objects-0.0.1/photo_objects/django/api/photo.py +136 -0
  11. photo_objects-0.0.1/photo_objects/django/api/utils.py +191 -0
  12. photo_objects-0.0.1/photo_objects/django/apps.py +41 -0
  13. photo_objects-0.0.1/photo_objects/django/context_processors.py +9 -0
  14. photo_objects-0.0.1/photo_objects/django/forms.py +140 -0
  15. photo_objects-0.0.1/photo_objects/django/management/__init__.py +0 -0
  16. photo_objects-0.0.1/photo_objects/django/management/commands/__init__.py +0 -0
  17. photo_objects-0.0.1/photo_objects/django/management/commands/create-initial-admin-account.py +31 -0
  18. photo_objects-0.0.1/photo_objects/django/migrations/0001_initial.py +51 -0
  19. photo_objects-0.0.1/photo_objects/django/migrations/__init__.py +0 -0
  20. photo_objects-0.0.1/photo_objects/django/models.py +117 -0
  21. photo_objects-0.0.1/photo_objects/django/objsto.py +82 -0
  22. photo_objects-0.0.1/photo_objects/django/signals.py +26 -0
  23. photo_objects-0.0.1/photo_objects/django/tests/__init__.py +0 -0
  24. photo_objects-0.0.1/photo_objects/django/tests/test_album.py +278 -0
  25. photo_objects-0.0.1/photo_objects/django/tests/test_auth.py +91 -0
  26. photo_objects-0.0.1/photo_objects/django/tests/test_photo.py +289 -0
  27. photo_objects-0.0.1/photo_objects/django/tests/test_utils.py +18 -0
  28. photo_objects-0.0.1/photo_objects/django/tests/utils.py +55 -0
  29. photo_objects-0.0.1/photo_objects/django/urls.py +81 -0
  30. photo_objects-0.0.1/photo_objects/django/views/__init__.py +0 -0
  31. photo_objects-0.0.1/photo_objects/django/views/api/__init__.py +3 -0
  32. photo_objects-0.0.1/photo_objects/django/views/api/album.py +54 -0
  33. photo_objects-0.0.1/photo_objects/django/views/api/auth.py +26 -0
  34. photo_objects-0.0.1/photo_objects/django/views/api/photo.py +98 -0
  35. photo_objects-0.0.1/photo_objects/django/views/api/utils.py +10 -0
  36. photo_objects-0.0.1/photo_objects/django/views/ui/__init__.py +2 -0
  37. photo_objects-0.0.1/photo_objects/django/views/ui/album.py +110 -0
  38. photo_objects-0.0.1/photo_objects/django/views/ui/photo.py +120 -0
  39. photo_objects-0.0.1/photo_objects/django/views/ui/utils.py +10 -0
  40. photo_objects-0.0.1/photo_objects/django/views/utils.py +4 -0
  41. photo_objects-0.0.1/photo_objects/error.py +2 -0
  42. photo_objects-0.0.1/photo_objects/img.py +94 -0
  43. photo_objects-0.0.1/photo_objects.egg-info/PKG-INFO +79 -0
  44. photo_objects-0.0.1/photo_objects.egg-info/SOURCES.txt +47 -0
  45. photo_objects-0.0.1/photo_objects.egg-info/dependency_links.txt +1 -0
  46. photo_objects-0.0.1/photo_objects.egg-info/requires.txt +2 -0
  47. photo_objects-0.0.1/photo_objects.egg-info/top_level.txt +1 -0
  48. photo_objects-0.0.1/pyproject.toml +38 -0
  49. photo_objects-0.0.1/setup.cfg +4 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Toni Kangas
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,79 @@
1
+ Metadata-Version: 2.1
2
+ Name: photo-objects
3
+ Version: 0.0.1
4
+ Summary: Application for storing photos in S3 compatible object-storage.
5
+ Author: Toni Kangas
6
+ License: MIT License
7
+
8
+ Copyright (c) 2024 Toni Kangas
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
+ SOFTWARE.
27
+
28
+ Project-URL: Homepage, https://github.com/kangasta/photo-objects
29
+ Project-URL: Repository, https://github.com/kangasta/photo-objects.git
30
+ Classifier: Environment :: Web Environment
31
+ Classifier: Framework :: Django
32
+ Classifier: Framework :: Django :: 5.0
33
+ Classifier: Intended Audience :: Developers
34
+ Classifier: License :: OSI Approved :: MIT License
35
+ Classifier: Operating System :: OS Independent
36
+ Classifier: Programming Language :: Python
37
+ Classifier: Programming Language :: Python :: 3
38
+ Classifier: Programming Language :: Python :: 3 :: Only
39
+ Classifier: Topic :: Internet :: WWW/HTTP
40
+ Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
41
+ Requires-Python: >=3.10
42
+ Description-Content-Type: text/markdown
43
+ License-File: LICENSE
44
+ Requires-Dist: minio~=7.2
45
+ Requires-Dist: pillow~=10.4
46
+
47
+ # Photo Objects
48
+
49
+ [![CI](https://github.com/kangasta/photo-objects/actions/workflows/ci.yml/badge.svg)](https://github.com/kangasta/photo-objects/actions/workflows/ci.yml)
50
+
51
+ Application for storing photos in S3 compatible object-storage.
52
+
53
+ ## Testing
54
+
55
+ Check and automatically fix formatting with:
56
+
57
+ ```bash
58
+ pycodestyle --exclude back/api/settings.py,*/migrations/*.py back photo_objects
59
+ autopep8 -aaar --in-place --exclude back/api/settings.py,*/migrations/*.py back photo_objects
60
+ ```
61
+
62
+ Run static analysis with:
63
+
64
+ ```bash
65
+ pylint -E --enable=invalid-name,unused-import,useless-object-inheritance back/api photo_objects
66
+ ```
67
+
68
+ Run integration tests (in the `api` directory) with:
69
+
70
+ ```bash
71
+ python3 runtests.py
72
+ ```
73
+
74
+ Get test coverage with:
75
+
76
+ ```bash
77
+ coverage run --branch --source photo_objects runtests.py
78
+ coverage report -m
79
+ ```
@@ -0,0 +1,33 @@
1
+ # Photo Objects
2
+
3
+ [![CI](https://github.com/kangasta/photo-objects/actions/workflows/ci.yml/badge.svg)](https://github.com/kangasta/photo-objects/actions/workflows/ci.yml)
4
+
5
+ Application for storing photos in S3 compatible object-storage.
6
+
7
+ ## Testing
8
+
9
+ Check and automatically fix formatting with:
10
+
11
+ ```bash
12
+ pycodestyle --exclude back/api/settings.py,*/migrations/*.py back photo_objects
13
+ autopep8 -aaar --in-place --exclude back/api/settings.py,*/migrations/*.py back photo_objects
14
+ ```
15
+
16
+ Run static analysis with:
17
+
18
+ ```bash
19
+ pylint -E --enable=invalid-name,unused-import,useless-object-inheritance back/api photo_objects
20
+ ```
21
+
22
+ Run integration tests (in the `api` directory) with:
23
+
24
+ ```bash
25
+ python3 runtests.py
26
+ ```
27
+
28
+ Get test coverage with:
29
+
30
+ ```bash
31
+ coverage run --branch --source photo_objects runtests.py
32
+ coverage report -m
33
+ ```
File without changes
@@ -0,0 +1,10 @@
1
+ from enum import Enum
2
+
3
+
4
+ class Size(Enum):
5
+ TINY = "xs"
6
+ SMALL = "sm"
7
+ MEDIUM = "md"
8
+ LARGE = "lg"
9
+ HUGE = "xl"
10
+ ORIGINAL = "og"
@@ -0,0 +1,6 @@
1
+ from django.contrib import admin
2
+
3
+ from .models import Album, Photo
4
+
5
+ admin.site.register(Album)
6
+ admin.site.register(Photo)
@@ -0,0 +1,3 @@
1
+ from .album import *
2
+ from .auth import *
3
+ from .photo import *
@@ -0,0 +1,57 @@
1
+ from django.db.models.deletion import ProtectedError
2
+ from django.http import HttpRequest
3
+
4
+ from photo_objects.django.forms import CreateAlbumForm, ModifyAlbumForm
5
+ from photo_objects.django.models import Album
6
+
7
+ from .auth import check_album_access
8
+ from .utils import (
9
+ FormValidationFailed,
10
+ JsonProblem,
11
+ check_permissions,
12
+ parse_input_data,
13
+ )
14
+
15
+
16
+ def get_albums(request: HttpRequest):
17
+ if not request.user.is_authenticated:
18
+ return Album.objects.filter(visibility=Album.Visibility.PUBLIC)
19
+ else:
20
+ return Album.objects.all()
21
+
22
+
23
+ def create_album(request: HttpRequest):
24
+ check_permissions(request, 'photo_objects.add_album')
25
+ data = parse_input_data(request)
26
+
27
+ f = CreateAlbumForm(data)
28
+ if not f.is_valid():
29
+ raise FormValidationFailed(f)
30
+
31
+ return f.save()
32
+
33
+
34
+ def modify_album(request: HttpRequest, album_key: str):
35
+ check_permissions(request, 'photo_objects.change_album')
36
+ album = check_album_access(request, album_key)
37
+ data = parse_input_data(request)
38
+
39
+ f = ModifyAlbumForm({**album.to_json(), **data}, instance=album)
40
+ if not f.is_valid():
41
+ raise FormValidationFailed(f)
42
+
43
+ return f.save()
44
+
45
+
46
+ def delete_album(request: HttpRequest, album_key: str):
47
+ check_permissions(request, 'photo_objects.delete_album')
48
+ album = check_album_access(request, album_key)
49
+
50
+ try:
51
+ album.delete()
52
+ except ProtectedError:
53
+ raise JsonProblem(
54
+ f"Album with {album_key} key can not be deleted because it "
55
+ "contains photos.",
56
+ 409,
57
+ )
@@ -0,0 +1,49 @@
1
+ from django.http import HttpRequest
2
+
3
+ from photo_objects.django import Size
4
+ from photo_objects.django.models import Album, Photo
5
+
6
+ from photo_objects.django.api.utils import (
7
+ AlbumNotFound,
8
+ InvalidSize,
9
+ PhotoNotFound,
10
+ Unauthorized,
11
+ join_key,
12
+ )
13
+
14
+
15
+ def check_album_access(request: HttpRequest, album_key: str):
16
+ try:
17
+ album = Album.objects.get(key=album_key)
18
+ except Album.DoesNotExist:
19
+ raise AlbumNotFound(album_key)
20
+
21
+ if not request.user.is_authenticated:
22
+ if album.visibility != Album.Visibility.PUBLIC:
23
+ raise AlbumNotFound(album_key)
24
+
25
+ return album
26
+
27
+
28
+ def check_photo_access(
29
+ request: HttpRequest,
30
+ album_key: str,
31
+ photo_key: str,
32
+ size_key: str):
33
+ try:
34
+ size = Size(size_key)
35
+ except ValueError:
36
+ raise InvalidSize(size_key)
37
+
38
+ try:
39
+ photo = Photo.objects.get(key=join_key(album_key, photo_key))
40
+ except Photo.DoesNotExist:
41
+ raise PhotoNotFound(album_key, photo_key)
42
+
43
+ if not request.user.is_authenticated:
44
+ if photo.album.visibility == Album.Visibility.PRIVATE:
45
+ raise AlbumNotFound(album_key)
46
+ if size == Size.ORIGINAL:
47
+ raise Unauthorized()
48
+
49
+ return photo
@@ -0,0 +1,136 @@
1
+ from django.core.files.uploadedfile import UploadedFile
2
+ from django.http import HttpRequest
3
+ from minio.error import S3Error
4
+ from PIL import UnidentifiedImageError
5
+
6
+ from photo_objects.django import objsto
7
+ from photo_objects.django.forms import (
8
+ CreatePhotoForm,
9
+ ModifyPhotoForm,
10
+ UploadPhotosForm,
11
+ slugify,
12
+ )
13
+ from photo_objects.img import photo_details
14
+
15
+ from .auth import check_album_access, check_photo_access
16
+ from .utils import (
17
+ FormValidationFailed,
18
+ JsonProblem,
19
+ check_permissions,
20
+ parse_input_data,
21
+ parse_single_file,
22
+ )
23
+
24
+
25
+ def get_photos(request: HttpRequest, album_key: str):
26
+ album = check_album_access(request, album_key)
27
+ return album.photo_set.all()
28
+
29
+
30
+ def _upload_photo(album_key: str, photo_file: UploadedFile):
31
+ try:
32
+ details = photo_details(photo_file)
33
+ except UnidentifiedImageError:
34
+ raise JsonProblem(
35
+ "Could not open photo file.",
36
+ 400,
37
+ )
38
+
39
+ f = CreatePhotoForm(dict(
40
+ key=f"{album_key}/{slugify(photo_file.name)}",
41
+ album=album_key,
42
+ title="",
43
+ description="",
44
+ **details,
45
+ ))
46
+
47
+ if not f.is_valid():
48
+ raise FormValidationFailed(f)
49
+ photo = f.save()
50
+
51
+ photo_file.seek(0)
52
+ try:
53
+ objsto.put_photo(photo.album.key, photo.filename, "og", photo_file)
54
+ except S3Error:
55
+ # TODO: check that there is no photo entry in the database, if object
56
+ # storage upload fails.
57
+ photo.delete()
58
+ # TODO: logging
59
+ raise JsonProblem(
60
+ "Could not save photo to object storage.",
61
+ 500,
62
+ )
63
+
64
+ return photo
65
+
66
+
67
+ def upload_photo(request: HttpRequest, album_key: str):
68
+ check_permissions(
69
+ request,
70
+ 'photo_objects.add_photo',
71
+ 'photo_objects.change_album')
72
+ photo_file = parse_single_file(request)
73
+ return _upload_photo(album_key, photo_file)
74
+
75
+
76
+ def upload_photos(request: HttpRequest, album_key: str):
77
+ check_permissions(
78
+ request,
79
+ 'photo_objects.add_photo',
80
+ 'photo_objects.change_album')
81
+
82
+ f = UploadPhotosForm(request.POST, request.FILES)
83
+ if not f.is_valid():
84
+ raise FormValidationFailed(f)
85
+
86
+ photo_files = f.cleaned_data["photos"]
87
+ if len(photo_files) < 1:
88
+ f.add_error("photos", "Expected at least one file, got 0.")
89
+ raise FormValidationFailed(f)
90
+
91
+ photos = []
92
+ for photo_file in f.cleaned_data["photos"]:
93
+ try:
94
+ photos.append(_upload_photo(album_key, photo_file))
95
+ except JsonProblem:
96
+ # TODO: include error type in the message
97
+ f.add_error("photos", f"Failed to upload {photo_file.name}.")
98
+
99
+ if not f.is_valid():
100
+ raise FormValidationFailed(f)
101
+
102
+ return photos
103
+
104
+
105
+ def modify_photo(request: HttpRequest, album_key: str, photo_key: str):
106
+ check_permissions(request, 'photo_objects.change_photo')
107
+ photo = check_photo_access(request, album_key, photo_key, 'xs')
108
+ data = parse_input_data(request)
109
+
110
+ f = ModifyPhotoForm({**photo.to_json(), **data}, instance=photo)
111
+
112
+ if not f.is_valid():
113
+ raise FormValidationFailed(f)
114
+
115
+ return f.save()
116
+
117
+
118
+ def delete_photo(request: HttpRequest, album_key: str, photo_key: str):
119
+ check_permissions(request, 'photo_objects.delete_photo')
120
+ photo = check_photo_access(request, album_key, photo_key, 'xs')
121
+
122
+ try:
123
+ objsto.delete_photo(album_key, photo_key)
124
+ except S3Error:
125
+ raise JsonProblem(
126
+ "Could not delete photo from object storage.",
127
+ 500,
128
+ )
129
+
130
+ try:
131
+ photo.delete()
132
+ except Exception:
133
+ raise JsonProblem(
134
+ "Could not delete photo from database.",
135
+ 500,
136
+ )
@@ -0,0 +1,191 @@
1
+ import json
2
+
3
+ from django.forms import ModelForm
4
+ from django.http import HttpRequest, JsonResponse
5
+ from django.core.files.uploadedfile import UploadedFile
6
+ from django.shortcuts import render
7
+ from django.urls import reverse_lazy
8
+
9
+ from photo_objects.error import PhotoObjectsError
10
+ from photo_objects.django.views.utils import BackLink
11
+ from photo_objects.django import Size
12
+
13
+
14
+ APPLICATION_JSON = "application/json"
15
+ APPLICATION_X_WWW_FORM = "application/x-www-form-urlencoded"
16
+ MULTIPART_FORMDATA = "multipart/form-data"
17
+ APPLICATION_PROBLEM = "application/problem+json"
18
+
19
+
20
+ def _pretty_list(in_: list, conjunction: str):
21
+ return f' {conjunction} '.join(
22
+ i for i in (', '.join(in_[:-1]), in_[-1],) if i)
23
+
24
+
25
+ class JsonProblem(PhotoObjectsError):
26
+ def __init__(self, title, status, payload=None, headers=None, errors=None):
27
+ super().__init__(title)
28
+
29
+ self.title = title
30
+ self.status = status
31
+ self.payload = payload or {}
32
+ self.headers = headers
33
+ self.errors = errors
34
+
35
+ @property
36
+ def json_response(self):
37
+ payload = {
38
+ 'title': self.title,
39
+ 'status': self.status,
40
+ **self.payload
41
+ }
42
+
43
+ if self.errors:
44
+ payload['errors'] = self.errors
45
+
46
+ return JsonResponse(
47
+ payload,
48
+ content_type=APPLICATION_PROBLEM,
49
+ status=self.status,
50
+ headers=self.headers
51
+ )
52
+
53
+ def html_response(self, request: HttpRequest):
54
+ return render(request, "photo_objects/problem.html", {
55
+ "title": "Error",
56
+ "back": BackLink(
57
+ f'Back to albums',
58
+ reverse_lazy('photo_objects:list_albums')),
59
+ "problem_title": self.title,
60
+ "status": self.status
61
+ }, status=self.status)
62
+
63
+
64
+ class MethodNotAllowed(JsonProblem):
65
+ def __init__(self, expected: list[str], actual: str):
66
+ expected_human = _pretty_list(expected, "or")
67
+
68
+ super().__init__(
69
+ f"Expected {expected_human} method, got {actual}.",
70
+ 405,
71
+ headers=dict(Allow=', '.join(expected))
72
+ )
73
+
74
+
75
+ class UnsupportedMediaType(JsonProblem):
76
+ def __init__(self, expected: list[str], actual: str):
77
+ expected_human = _pretty_list(expected, "or")
78
+
79
+ super().__init__(
80
+ f"Expected {expected_human} content-type, got {actual}.",
81
+ 415,
82
+ headers={'Accept-Post': ', '.join(expected)}
83
+ )
84
+
85
+
86
+ class Unauthorized(JsonProblem):
87
+ def __init__(self):
88
+ super().__init__(
89
+ "Not authenticated.",
90
+ 401,
91
+ )
92
+
93
+
94
+ class InvalidSize(JsonProblem):
95
+ def __init__(self, actual: str):
96
+ expected = _pretty_list([i.value for i in Size], "or")
97
+
98
+ super().__init__(
99
+ f"Expected {expected} size, got {actual or 'none'}.",
100
+ 400,
101
+ )
102
+
103
+
104
+ class AlbumNotFound(JsonProblem):
105
+ def __init__(self, album_key: str):
106
+ super().__init__(
107
+ f"Album with {album_key} key does not exist.",
108
+ 404,
109
+ )
110
+
111
+
112
+ class PhotoNotFound(JsonProblem):
113
+ def __init__(self, album_key: str, photo_key: str):
114
+ super().__init__(
115
+ f"Photo with {photo_key} key does not exist in {album_key} album.",
116
+ 404,
117
+ )
118
+
119
+
120
+ class FormValidationFailed(JsonProblem):
121
+ def __init__(self, form: ModelForm):
122
+ try:
123
+ resource = form.instance.__class__.__name__
124
+ except AttributeError:
125
+ resource = "Form"
126
+
127
+ super().__init__(
128
+ f"{resource} validation failed.",
129
+ 400,
130
+ errors=form.errors.get_json_data(),
131
+ )
132
+
133
+ self.form = form
134
+
135
+
136
+ def check_permissions(request: HttpRequest, *permissions: str):
137
+ if not request.user.is_authenticated:
138
+ raise Unauthorized()
139
+ if not request.user.has_perms(permissions):
140
+ raise JsonProblem(
141
+ f"Expected {_pretty_list(permissions, 'and')} permissions",
142
+ 403,
143
+ headers=dict(Allow="GET, POST")
144
+ )
145
+
146
+
147
+ def parse_json_body(request: HttpRequest):
148
+ if request.content_type != APPLICATION_JSON:
149
+ raise UnsupportedMediaType(
150
+ [APPLICATION_JSON],
151
+ request.content_type
152
+ )
153
+
154
+ try:
155
+ return json.loads(request.body)
156
+ except BaseException:
157
+ raise JsonProblem(
158
+ "Could not parse JSON data from request body.",
159
+ 400,
160
+ )
161
+
162
+
163
+ def parse_input_data(request: HttpRequest):
164
+ if request.content_type == APPLICATION_JSON:
165
+ return parse_json_body(request)
166
+ elif request.content_type == APPLICATION_X_WWW_FORM:
167
+ return request.POST.dict()
168
+ else:
169
+ raise UnsupportedMediaType(
170
+ [APPLICATION_JSON, APPLICATION_X_WWW_FORM], request.content_type)
171
+
172
+
173
+ def parse_single_file(request: HttpRequest) -> UploadedFile:
174
+ if request.content_type != MULTIPART_FORMDATA:
175
+ raise UnsupportedMediaType(
176
+ [MULTIPART_FORMDATA],
177
+ request.content_type
178
+ )
179
+
180
+ if len(request.FILES) != 1:
181
+ raise JsonProblem(
182
+ f"Expected exactly one file, got {len(request.FILES)}.",
183
+ 400,
184
+ )
185
+
186
+ for _, f in request.FILES.items():
187
+ return f
188
+
189
+
190
+ def join_key(*args):
191
+ return '/'.join(args)
@@ -0,0 +1,41 @@
1
+ from django.apps import AppConfig
2
+ from django.core.checks import Error, register
3
+ from django.conf import settings
4
+
5
+
6
+ class PhotoObjects(AppConfig):
7
+ default_auto_field = 'django.db.models.BigAutoField'
8
+ name = 'photo_objects.django'
9
+ label = 'photo_objects'
10
+
11
+ def ready(self):
12
+ from . import signals
13
+
14
+
15
+ @register()
16
+ def photo_objects_check(app_configs, **kwargs):
17
+ errors = []
18
+
19
+ try:
20
+ conf = settings.PHOTO_OBJECTS_OBJSTO
21
+ except AttributeError:
22
+ errors.append(
23
+ Error(
24
+ 'The PHOTO_OBJECTS_OBJSTO setting must be defined.',
25
+ id='UNDEFINED_SETTING',
26
+ obj='photo_objects',
27
+ )
28
+ )
29
+ return errors
30
+
31
+ for key in ('URL', 'ACCESS_KEY', 'SECRET_KEY',):
32
+ if not conf.get(key):
33
+ errors.append(
34
+ Error(
35
+ f'The PHOTO_OBJECTS_OBJSTO setting must define {key} '
36
+ 'field.',
37
+ id='UNDEFINED_FIELD',
38
+ obj='photo_objects',
39
+ ))
40
+
41
+ return errors
@@ -0,0 +1,9 @@
1
+ from importlib.metadata import version
2
+
3
+
4
+ class Metadata:
5
+ version = version('photo_objects')
6
+
7
+
8
+ def metadata(_):
9
+ return {"photo_objects_metadata": Metadata()}