photo-objects 0.5.0__py3-none-any.whl → 0.7.0__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.
@@ -17,6 +17,7 @@ from photo_objects.img import photo_details
17
17
  from .auth import check_album_access, check_photo_access
18
18
  from .utils import (
19
19
  FormValidationFailed,
20
+ UploadPhotosFailed,
20
21
  JsonProblem,
21
22
  check_permissions,
22
23
  parse_input_data,
@@ -57,7 +58,7 @@ def _upload_photo(album_key: str, photo_file: UploadedFile):
57
58
  photo.delete()
58
59
 
59
60
  msg = objsto.with_error_code(
60
- "Could not save photo to object storage", e)
61
+ "Could not save photo to object storage.", e)
61
62
  logger.error(f"{msg}: {str(e)}")
62
63
  raise JsonProblem(f"{msg}.", 500) from e
63
64
 
@@ -73,6 +74,17 @@ def upload_photo(request: HttpRequest, album_key: str):
73
74
  return _upload_photo(album_key, photo_file)
74
75
 
75
76
 
77
+ def _join_errors(errors: dict) -> str:
78
+ messages = []
79
+ for _, errs in errors.items():
80
+ for err in errs:
81
+ try:
82
+ messages.append(err.get('message'))
83
+ except AttributeError:
84
+ messages.append(str(err))
85
+ return " ".join(messages) if messages else "Unknown error."
86
+
87
+
76
88
  def upload_photos(request: HttpRequest, album_key: str):
77
89
  check_permissions(
78
90
  request,
@@ -92,12 +104,18 @@ def upload_photos(request: HttpRequest, album_key: str):
92
104
  for photo_file in f.cleaned_data["photos"]:
93
105
  try:
94
106
  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}.")
107
+ except FormValidationFailed as e:
108
+ message = _join_errors(e.form.errors.get_json_data())
109
+ f.add_error(
110
+ "photos",
111
+ f"Failed to upload {photo_file.name}. {message}")
112
+ except JsonProblem as e:
113
+ f.add_error(
114
+ "photos",
115
+ f"Failed to upload {photo_file.name}. {e.title}")
98
116
 
99
117
  if not f.is_valid():
100
- raise FormValidationFailed(f)
118
+ raise UploadPhotosFailed(f, [i.filename for i in photos])
101
119
 
102
120
  return photos
103
121
 
@@ -129,6 +129,12 @@ class FormValidationFailed(JsonProblem):
129
129
  self.form = form
130
130
 
131
131
 
132
+ class UploadPhotosFailed(FormValidationFailed):
133
+ def __init__(self, form: ModelForm, uploaded_photos: list[str]):
134
+ super().__init__(form)
135
+ self.uploaded_photos = uploaded_photos
136
+
137
+
132
138
  def check_permissions(request: HttpRequest, *permissions: str):
133
139
  if not request.user.is_authenticated:
134
140
  raise Unauthorized()
@@ -30,20 +30,25 @@ class PhotoSizeDimensions:
30
30
  max_width: int = None
31
31
  max_height: int = None
32
32
  max_aspect_ratio: float = None
33
+ image_format: str = None
33
34
 
34
35
 
35
36
  DEFAULT_SM = dict(
36
37
  max_width=512,
37
38
  max_height=512,
38
- max_aspect_ratio=1.5
39
+ max_aspect_ratio=1.5,
40
+ image_format='WEBP'
39
41
  )
40
42
  DEFAULT_MD = dict(
41
- max_width=2048,
42
- max_height=2048
43
+ max_width=1024,
44
+ max_height=1024,
45
+ max_aspect_ratio=1.5,
46
+ image_format="JPEG"
43
47
  )
44
48
  DEFAULT_LG = dict(
45
- max_width=4096,
46
- max_height=4096
49
+ max_width=2048,
50
+ max_height=2048,
51
+ image_format='WEBP'
47
52
  )
48
53
 
49
54
 
@@ -1,8 +1,12 @@
1
- from importlib.metadata import version
1
+ from importlib.metadata import PackageNotFoundError, version
2
2
 
3
3
 
4
4
  class Metadata:
5
- version = version('photo_objects')
5
+ def __init__(self):
6
+ try:
7
+ self.version = version('photo_objects')
8
+ except PackageNotFoundError:
9
+ self.version = None
6
10
 
7
11
 
8
12
  def metadata(_):
@@ -195,7 +195,11 @@ class CreatePhotoForm(ModelForm):
195
195
  error_messages = {
196
196
  'album': {
197
197
  'invalid_choice': _('Album with %(value)s key does not exist.')
198
- }
198
+ },
199
+ 'key': {
200
+ 'unique': _(
201
+ 'Photo with this filename already exists in the album.'),
202
+ },
199
203
  }
200
204
 
201
205
 
@@ -2,6 +2,7 @@ from dataclasses import asdict
2
2
  from io import BytesIO
3
3
  import json
4
4
  import mimetypes
5
+ import re
5
6
  import urllib3
6
7
 
7
8
  from minio import Minio, S3Error
@@ -60,8 +61,30 @@ def photo_path(album_key, photo_key, size_key):
60
61
  return f"{size_key}/{album_key}/{photo_key}"
61
62
 
62
63
 
63
- def put_photo(album_key, photo_key, size_key, photo_file):
64
- content_type = mimetypes.guess_type(photo_key)[0]
64
+ def _photo_filename(photo_key: str, image_format: str = None) -> str:
65
+ if image_format:
66
+ filename = re.sub(r'\.[^.]+$', '', photo_key)
67
+ return f"{filename}.{image_format.lower()}"
68
+
69
+ return photo_key
70
+
71
+
72
+ def photo_content_headers(
73
+ photo_key: str,
74
+ image_format: str = None,
75
+ ) -> tuple[str, dict[str, str]]:
76
+ filename = _photo_filename(photo_key, image_format)
77
+
78
+ content_type = mimetypes.guess_type(filename, strict=False)[0]
79
+ headers = {
80
+ "Content-Disposition": f"inline; filename={filename}"
81
+ }
82
+
83
+ return content_type, headers
84
+
85
+
86
+ def put_photo(album_key, photo_key, size_key, photo_file, image_format=None):
87
+ content_type, headers = photo_content_headers(photo_key, image_format)
65
88
 
66
89
  client, bucket = _objsto_access()
67
90
  return client.put_object(
@@ -71,6 +94,7 @@ def put_photo(album_key, photo_key, size_key, photo_file):
71
94
  length=-1,
72
95
  part_size=10 * MEGABYTE,
73
96
  content_type=content_type,
97
+ metadata=headers
74
98
  )
75
99
 
76
100
 
@@ -0,0 +1,60 @@
1
+ from django.contrib.sites.models import Site
2
+
3
+ from photo_objects.django.models import Album, Photo, SiteSettings
4
+
5
+ from .utils import TestCase, create_dummy_photo, temp_static_files
6
+
7
+
8
+ PHOTOS_DIRECTORY = "photos"
9
+
10
+
11
+ class OgMetaTests(TestCase):
12
+ @classmethod
13
+ def setUpTestData(cls):
14
+ album = Album.objects.create(
15
+ key="paris", visibility=Album.Visibility.PUBLIC)
16
+
17
+ create_dummy_photo(album, "tower.jpeg")
18
+
19
+ @temp_static_files
20
+ def test_albums_og_meta(self):
21
+ og_title = '<meta property="og:title" content="Test" />'
22
+
23
+ response = self.client.get("/albums")
24
+ self.assertNotContains(
25
+ response,
26
+ og_title,
27
+ status_code=200,
28
+ html=True)
29
+
30
+ site = Site.objects.get(id=1)
31
+ site.name = "Test"
32
+ site.domain = "test.example.com"
33
+ site.save()
34
+
35
+ response = self.client.get("/albums")
36
+ self.assertNotContains(
37
+ response,
38
+ og_title,
39
+ status_code=200,
40
+ html=True)
41
+
42
+ site_settings = SiteSettings.objects.get(site=site)
43
+ site_settings.description = "Description"
44
+ site_settings.preview_image = Photo.objects.get(key="paris/tower.jpeg")
45
+ site_settings.save()
46
+
47
+ tags = [
48
+ og_title,
49
+ '<meta property="og:description" content="Description" />',
50
+ '<meta property="og:image" content="https://test.example.com/img/paris/tower.jpeg/md"/>', # noqa: E501
51
+ '<meta property="og:url" content="https://test.example.com/albums" />', # noqa: E501
52
+ ]
53
+
54
+ response = self.client.get("/albums")
55
+ for tag in tags:
56
+ self.assertContains(
57
+ response,
58
+ tag,
59
+ status_code=200,
60
+ html=True)
@@ -184,6 +184,15 @@ class PhotoViewTests(TestCase):
184
184
  {filename: file})
185
185
  self.assertStatus(response, 500)
186
186
 
187
+ response = self.client.get(
188
+ "/api/albums/test-photo-a/photos/tower.jpg")
189
+ self.assertStatus(response, 404)
190
+
191
+ response = self.client.get(
192
+ "/api/albums/test-photo-a/photos")
193
+ self.assertStatus(response, 200)
194
+ self.assertEqual(len(response.json()), 0)
195
+
187
196
  def test_get_image_scales_the_image(self):
188
197
  login_success = self.client.login(
189
198
  username='has_permission', password='test')
@@ -1,7 +1,11 @@
1
1
  from dataclasses import dataclass
2
+ from io import StringIO
2
3
  import os
4
+ import shutil
5
+ import tempfile
3
6
 
4
7
  from django.conf import settings
8
+ from django.core.management import call_command
5
9
  from django.test import TestCase as DjangoTestCase, override_settings
6
10
  from django.utils import timezone
7
11
  from django.utils.dateparse import parse_datetime
@@ -120,3 +124,20 @@ def parse_timestamps(data):
120
124
  return Timestamps(
121
125
  created_at=data.get('created_at'),
122
126
  updated_at=data.get('updated_at'))
127
+
128
+
129
+ # Based on https://stackoverflow.com/a/76745063
130
+ def temp_static_files(func):
131
+ '''Decorator that creates a temporary directory, configures that as
132
+ STATIC_ROOT, and collects static files there.'''
133
+ def wrapper(*args, **kwargs):
134
+ static_root = tempfile.mkdtemp(prefix="test_static_")
135
+ with override_settings(STATIC_ROOT=static_root):
136
+ try:
137
+ out = StringIO()
138
+ call_command("collectstatic", "--noinput", stdout=out)
139
+ func(*args, **kwargs)
140
+ finally:
141
+ shutil.rmtree(static_root)
142
+
143
+ return wrapper
@@ -91,14 +91,23 @@ def get_img(request: HttpRequest, album_key: str, photo_key: str):
91
91
  404 if code == "NoSuchKey" else 500,
92
92
  ).json_response
93
93
 
94
- sizes = photo_sizes()
94
+ size_params = getattr(photo_sizes(), size)
95
95
  # TODO: handle error
96
96
  scaled_photo = scale_photo(
97
- original_photo, photo_key, **asdict(getattr(sizes, size)))
97
+ original_photo, photo_key, **asdict(size_params))
98
98
 
99
99
  # TODO: handle error
100
100
  scaled_photo.seek(0)
101
- objsto.put_photo(album_key, photo_key, size, scaled_photo)
101
+ objsto.put_photo(
102
+ album_key,
103
+ photo_key,
104
+ size,
105
+ scaled_photo,
106
+ size_params.image_format)
107
+
108
+ content_type, headers = objsto.photo_content_headers(
109
+ photo_key, size_params.image_format)
102
110
 
103
111
  scaled_photo.seek(0)
104
- return HttpResponse(scaled_photo.read(), content_type=content_type)
112
+ return HttpResponse(
113
+ scaled_photo.read(), content_type=content_type, headers=headers)
@@ -3,7 +3,11 @@ from django.shortcuts import render
3
3
  from django.urls import reverse
4
4
 
5
5
  from photo_objects.django import api
6
- from photo_objects.django.api.utils import AlbumNotFound, FormValidationFailed
6
+ from photo_objects.django.api.utils import (
7
+ AlbumNotFound,
8
+ FormValidationFailed,
9
+ UploadPhotosFailed,
10
+ )
7
11
  from photo_objects.django.forms import ModifyPhotoForm, UploadPhotosForm
8
12
  from photo_objects.django.models import Photo
9
13
  from photo_objects.django.views.utils import (
@@ -24,10 +28,17 @@ def upload_photos(request: HttpRequest, album_key: str):
24
28
  reverse(
25
29
  'photo_objects:show_album',
26
30
  kwargs={"album_key": album_key}))
27
- except FormValidationFailed as e:
31
+ except UploadPhotosFailed as e:
28
32
  form = e.form
33
+ uploaded_count = len(e.uploaded_photos)
34
+ if uploaded_count > 0:
35
+ plural = 's' if uploaded_count != 1 else ''
36
+ info = f"Successfully uploaded {uploaded_count} photo{plural}."
37
+ else:
38
+ info = None
29
39
  else:
30
40
  form = UploadPhotosForm()
41
+ info = None
31
42
 
32
43
  album = api.check_album_access(request, album_key)
33
44
  target = album.title or album.key
@@ -38,6 +49,7 @@ def upload_photos(request: HttpRequest, album_key: str):
38
49
 
39
50
  return render(request, 'photo_objects/photo/upload.html', {
40
51
  "form": form,
52
+ "info": info,
41
53
  "title": "Upload photos",
42
54
  "back": back,
43
55
  "photo": album.cover_photo,
photo_objects/img.py CHANGED
@@ -66,7 +66,10 @@ def _read_camera_setup_and_settings(image: Image) -> dict:
66
66
  raise e
67
67
 
68
68
 
69
- def _image_format(filename):
69
+ def _image_format(image_format, filename):
70
+ if image_format:
71
+ return image_format
72
+
70
73
  image_format = filename.split('.')[-1].upper()
71
74
 
72
75
  if image_format == "JPG":
@@ -131,7 +134,8 @@ def scale_photo(
131
134
  filename,
132
135
  max_width=None,
133
136
  max_height=None,
134
- max_aspect_ratio=None):
137
+ max_aspect_ratio=None,
138
+ image_format=None):
135
139
  image = Image.open(photo_file)
136
140
  width, height = image.size
137
141
 
@@ -164,5 +168,5 @@ def scale_photo(
164
168
  resized = image.resize(new_size, Image.Resampling.LANCZOS)
165
169
 
166
170
  b = BytesIO()
167
- resized.save(b, _image_format(filename))
171
+ resized.save(b, _image_format(image_format, filename))
168
172
  return b
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: photo-objects
3
- Version: 0.5.0
3
+ Version: 0.7.0
4
4
  Summary: Application for storing photos in S3 compatible object-storage.
5
5
  Author: Toni Kangas
6
6
  License: MIT License
@@ -1,23 +1,23 @@
1
1
  photo_objects/__init__.py,sha256=I1508w_ntomEqTFQgC74SurhxVXfCiDWZLRsny2f59g,60
2
2
  photo_objects/config.py,sha256=0-Aeo-z-d_fxx-cjAjxSwPJZUgYaAi7NTodiErlxIXo,861
3
3
  photo_objects/error.py,sha256=7afLYjxM0EaYioxVw_XUqHTvfSMSuQPUwwle0OVlaDY,45
4
- photo_objects/img.py,sha256=8FNxKFBVuSyxO_jA3W3PMag3bZ7sEXYKG78rpgxq71Q,4515
4
+ photo_objects/img.py,sha256=2HVGS2g7rS2hnomozYL92oxrcN6zjDTHvWNr-UAqtGQ,4620
5
5
  photo_objects/utils.py,sha256=sYliXid-bv2EgNA97woRaOnWU75yZFq7fuqzWaseiDg,139
6
6
  photo_objects/django/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
7
  photo_objects/django/admin.py,sha256=ubYfhsWQ85_2moYGZCVneLQ2DVEetbVpvBNisIrtuhw,170
8
8
  photo_objects/django/apps.py,sha256=Apqu6o6fpoxda18NQgKupvQRvTAZxVviIK_-dUR3rck,1444
9
- photo_objects/django/conf.py,sha256=s5lUbSLpjRWKlE2eYqAZmz60q4Oe10-e7JbIERPa6qI,2463
10
- photo_objects/django/context_processors.py,sha256=DLhLZHAvGlrpxYegaLiKEx9X7rpyZUFiAnAVF9jjkFA,165
11
- photo_objects/django/forms.py,sha256=XiCNqjVqPDn_aV4nGM1z5NfLKHd6_ipm7y0YRt3sXgY,6505
9
+ photo_objects/django/conf.py,sha256=ZpeIulEc1tpr8AO52meNKOF30Xf5osbDtDyHvQRtkx4,2593
10
+ photo_objects/django/context_processors.py,sha256=XacUmcYV-4NMMMNXPWrHKdvNd6lfyamisngaVerREiU,306
11
+ photo_objects/django/forms.py,sha256=LcznhXrqPfqRY5b2SwHBbC9-uux1FiZFdQBlHsKX5NU,6649
12
12
  photo_objects/django/models.py,sha256=M40ZFSIX3WgyVXfHQY9SXQOY0s3fpFqfCxNV3CBD5M8,5753
13
- photo_objects/django/objsto.py,sha256=5z4F8pft01WKLBipyE3Ro9Ac9AvWbuV_rG9Iiour90E,3588
13
+ photo_objects/django/objsto.py,sha256=B7DxPWuqFaPFXPLhsHCFlqIzYl7EXLxcHde6zJDe89A,4238
14
14
  photo_objects/django/signals.py,sha256=Q_Swjl_9z6B6zP-97D_ep5zGSAEgmQfwUz0utMDY93A,1624
15
15
  photo_objects/django/urls.py,sha256=pCUTxg4xSd3ZD8BZVjULb9QpJQ03Ek6-sKoVnYG3-OY,1975
16
16
  photo_objects/django/api/__init__.py,sha256=BnEHlm3mwyBy-1xhk-NFasgZa4fjCDjtfkBUoH0puPY,62
17
17
  photo_objects/django/api/album.py,sha256=CJDeGLCuYoxGUDcjssZRpFnToxG_KVE9Ii7NduFW2ks,2003
18
18
  photo_objects/django/api/auth.py,sha256=lS0S1tMVH2uN30g4jlixklv3eMnQ2FbQVQvuRXeMGYo,1420
19
- photo_objects/django/api/photo.py,sha256=Gc4GHmcW0995_alE7Q6mdn7SMt9f1JJ-krv-p-SJi8Y,3870
20
- photo_objects/django/api/utils.py,sha256=kHOdXp-LEaEhL82LCLanQmVpU3jFi8KNSYOgIAv-IVs,5167
19
+ photo_objects/django/api/photo.py,sha256=-lo1g6jfBr884wy-WV2DAEPxzH9V-tFUTRtitmA6i28,4471
20
+ photo_objects/django/api/utils.py,sha256=xilnQndd1DXkNs6K6OqXpzUKkDSZFOG25xX3O2oJggU,5364
21
21
  photo_objects/django/management/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
22
22
  photo_objects/django/management/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
23
23
  photo_objects/django/management/commands/clean-scaled-photos.py,sha256=KJY6phgTCxcmbMUsUfCRQjatvCmKyFninM8zT-tB3Kc,2008
@@ -35,24 +35,25 @@ photo_objects/django/tests/test_album.py,sha256=yjxP_M0bddS9Xpg1d1Wk5OyJsmxmkdYu
35
35
  photo_objects/django/tests/test_auth.py,sha256=hgr1UMVLvSI1x5zY7wTEXSBKfM5E_sNMIFlx8mVWYPY,3928
36
36
  photo_objects/django/tests/test_commands.py,sha256=e3lE1ZhFR39WIq2VSKDNcQHUkSJqSWDYuAcAfu29svs,2955
37
37
  photo_objects/django/tests/test_img.py,sha256=HEAWcr5fpTkzePkhoQ4YrWsDO9TvFOr7my_0LqVbaO4,829
38
- photo_objects/django/tests/test_photo.py,sha256=FzSrtWMQjv_EL-M1zM6rYekw_YGuM87JkMHRcLvB2K8,13386
38
+ photo_objects/django/tests/test_og_meta.py,sha256=Kk5a9KvE88KZ60gLqXSe6rTz5YU-gdjteksYolHd-nw,1804
39
+ photo_objects/django/tests/test_photo.py,sha256=aZCZw7PIXIfIYX3q1lUxn8C53mLRBNK0qp18t7X4ZQ4,13696
39
40
  photo_objects/django/tests/test_utils.py,sha256=zBLv8lkvcLMCaH4D6GR1KZqUe-rPowhcBkQX19-Kshs,2007
40
- photo_objects/django/tests/utils.py,sha256=Yre-l6rlJn7tPeqFnzQdcmBBG-2vwuGzVLKX-2uQhDs,3643
41
+ photo_objects/django/tests/utils.py,sha256=s_3hzQEY4tdja_8406s_SQyRC6fCYI_MBPqvFjC8fB4,4345
41
42
  photo_objects/django/views/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
42
43
  photo_objects/django/views/utils.py,sha256=oP9B6QtHPaukX97rqIlCHxanduktPGFVSCGkGxf4lzI,1691
43
44
  photo_objects/django/views/api/__init__.py,sha256=SxK-b7MgMtD9VRMz46rDV5qtm6AvkRcg3Moa1AWl5pY,108
44
45
  photo_objects/django/views/api/album.py,sha256=EZMOkxYzLSWr9wwXnd4yAO64JtXZq2k3FYohiNMFbGQ,1602
45
46
  photo_objects/django/views/api/auth.py,sha256=EN_ExegzmLN-bhSzu3L9-6UE9qodPd7_ZRLilzrvc8Y,819
46
- photo_objects/django/views/api/photo.py,sha256=FQCqcnPwSaEJ9MVggQF09E4WD1wVFmLZnT_pFFpWGvA,3502
47
+ photo_objects/django/views/api/photo.py,sha256=iWDTBWrzztIKcfW7G6vh1eYTuH2JW129iOLSHjTfXCE,3743
47
48
  photo_objects/django/views/api/utils.py,sha256=uQzKdSKHRAux5OZzqgWQr0gsK_FeweQP0cg_67OWA_Y,264
48
49
  photo_objects/django/views/ui/__init__.py,sha256=N3ro5KggdV-JnfyHwoStX73b3SbVbpcsMuQNlxntVJs,92
49
50
  photo_objects/django/views/ui/album.py,sha256=PmVXAmqVjKLAie1NyB-qXO3eLqOmhIA8PTAGJewgxko,4738
50
51
  photo_objects/django/views/ui/configuration.py,sha256=oX6SV0TFBpbaxfp4cXIdSL41YJhy_aOy30TkBxOpq0M,5065
51
- photo_objects/django/views/ui/photo.py,sha256=Zo-HE-CzMSLFEtDTL1ds1HnXi3ORsIym4iWa_ZMCPpc,6299
52
+ photo_objects/django/views/ui/photo.py,sha256=fVpKI-XqSV8KfoTpDYxceCHR3rJzuVYfmRRQx1I0YIQ,6649
52
53
  photo_objects/django/views/ui/users.py,sha256=nb73cnzuV98QkJb0j8F2hqPgOGFIWpUFTFu6dXMeVwM,722
53
54
  photo_objects/django/views/ui/utils.py,sha256=YV_YcUbX-zUkdFnBlezPChR6aPDhZJ9loSOHBSzF6Cc,273
54
- photo_objects-0.5.0.dist-info/licenses/LICENSE,sha256=V3w6hTjXfP65F4r_mejveHcV5igHrblxao3-2RlfVlA,1068
55
- photo_objects-0.5.0.dist-info/METADATA,sha256=9fsj7gKM1NHpOblwzeKWpDkKh4ygJ_HmfafdEQa1aik,3605
56
- photo_objects-0.5.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
57
- photo_objects-0.5.0.dist-info/top_level.txt,sha256=SZeL8mhf-WMGdhRtTGFvZc3aIRBboQls9O0cFDMGdQ0,14
58
- photo_objects-0.5.0.dist-info/RECORD,,
55
+ photo_objects-0.7.0.dist-info/licenses/LICENSE,sha256=V3w6hTjXfP65F4r_mejveHcV5igHrblxao3-2RlfVlA,1068
56
+ photo_objects-0.7.0.dist-info/METADATA,sha256=wHfCOuVG8iXwYP6V0arHPNUURsF2HV0PvBEBEcmnB0Q,3605
57
+ photo_objects-0.7.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
58
+ photo_objects-0.7.0.dist-info/top_level.txt,sha256=SZeL8mhf-WMGdhRtTGFvZc3aIRBboQls9O0cFDMGdQ0,14
59
+ photo_objects-0.7.0.dist-info/RECORD,,