Skip to content

Commit cade920

Browse files
authored
Merge pull request #18 from photos-network/feature/postman
add limit and offset handling
2 parents bd59921 + f7eeb50 commit cade920

File tree

9 files changed

+156
-143
lines changed

9 files changed

+156
-143
lines changed

config/configuration.json.bak

Lines changed: 0 additions & 29 deletions
This file was deleted.

core/addons/api/__init__.py

Lines changed: 119 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,19 @@
22
import json
33
import logging
44
import os
5+
import pathlib
6+
from datetime import datetime
7+
from typing import Optional
58

69
from aiohttp import web
7-
810
from core.addons.api.dto.details import Details
911
from core.addons.api.dto.location import Location
10-
from core.addons.api.dto.photo import Photo, PhotoEncoder
11-
from core.addons.api.dto.photo_response import PhotoResponse
12-
from core.addons.api.dto.photo_url import PhotoUrl
12+
from core.addons.api.dto.photo import (PhotoDetailsResponse, PhotoEncoder,
13+
PhotoResponse)
14+
from core.addons.api.dto.photo_response import PhotosResponse
15+
from core.base import Session
1316
from core.core import ApplicationCore
17+
from core.persistency.dto.photo import Photo
1418
from core.webserver.request import KEY_USER_ID, RequestView
1519
from core.webserver.status import HTTP_CREATED, HTTP_OK
1620

@@ -28,6 +32,7 @@ async def async_setup(core: ApplicationCore, config: dict) -> bool:
2832

2933
core.http.register_request(APIStatusView())
3034
core.http.register_request(PhotosView())
35+
core.http.register_request(PhotoDetailsView())
3136
core.http.register_request(PhotoView())
3237
core.http.register_request(AlbumView())
3338

@@ -53,93 +58,133 @@ async def head(self, core: ApplicationCore, request: web.Request):
5358
class PhotosView(RequestView):
5459
"""View to handle photos requests."""
5560

56-
url = "/api/photo/"
57-
name = "api:photo:upload"
61+
url = "/v1/photos"
62+
name = "v1:photo"
5863

5964
async def get(self, core: ApplicationCore, request: web.Request) -> web.Response:
6065
"""Get a list of all photo resources."""
66+
_LOGGER.debug(f"GET /v1/photos")
67+
await core.authentication.check_permission(request, "library:read")
68+
6169
user_id = await core.http.get_user_id(request)
6270

6371
if user_id is None:
6472
raise web.HTTPForbidden()
6573

66-
_LOGGER.debug(f"read photos for user_id {user_id}")
67-
user_photos = await core.storage.read_photos(user_id)
68-
_LOGGER.debug(f"iterate through {len(user_photos)} photos.")
74+
limit = 50
75+
if "limit" in request.query:
76+
limit = int(request.query["limit"])
77+
78+
offset = 0
79+
if "offset" in request.query:
80+
offset = int(request.query["offset"])
81+
82+
_LOGGER.debug(f"read {limit} photos for user_id {user_id} beginning with {offset}")
83+
user_photos = await core.storage.read_photos(user_id, offset, limit)
6984

7085
results = []
7186

72-
# iterate through photos
7387
for photo in user_photos:
74-
_LOGGER.debug(f"get additional data for {photo.filepath}")
75-
76-
# photo location
77-
location = None
78-
latitude = await core.storage.read("latitude")
79-
longitude = await core.storage.read("longitude")
80-
if latitude is not None and longitude is not None:
81-
altitude = await core.storage.read("altitude")
82-
if altitude is not None:
83-
location = Location(
84-
latitude=latitude, longitude=longitude, altitude=altitude
85-
)
86-
else:
87-
location = Location(
88-
latitude=latitude, longitude=longitude, altitude="0.0"
89-
)
90-
91-
# photo tags
92-
tags = await core.storage.read("tags")
93-
tags = ["landscape", "sky", "night"]
94-
95-
# add photo to results
9688
results.append(
97-
Photo(
98-
name="DSC_2340-HDR.jpg",
99-
description="",
100-
author="",
101-
created_at="2012-02-09T21:11:53-05:00",
102-
details=Details(
103-
camera="Canon EOS-1D Mark IV",
104-
lens="E 18-200mm F3.5-6.3 OSS",
105-
focal_length="700",
106-
iso="400",
107-
shutter_speed="1/2000",
108-
aperture="6.3",
109-
),
110-
tags=tags,
111-
location=location,
112-
image_urls=[
113-
PhotoUrl(size="1080", url="/data/cache/DSC_2340-HDR_1080.jpg"),
114-
PhotoUrl(size="1600", url="/data/cache/DSC_2340-HDR_1600.jpg"),
115-
PhotoUrl(size="2048", url="/data/cache/DSC_2340-HDR_2048.jpg"),
116-
PhotoUrl(size="full", url="/data/cache/DSC_2340-HDR.jpg"),
117-
],
89+
PhotoResponse(
90+
id=photo.uuid,
91+
name=photo.filename,
92+
image_url=f"{core.config.external_url}/v1/file/{photo.uuid}"
11893
)
11994
)
12095

121-
# key = "latitude"
122-
# _LOGGER.error(f"key/value: {key}/{value}")
123-
124-
# data = request.query
96+
response = PhotosResponse(offset=offset, limit=limit, size=len(results), results=results)
97+
return web.Response(text=json.dumps(response, cls=PhotoEncoder), content_type="application/json")
12598

126-
offset = 0
127-
# if data["size"]:
128-
# offset = data["size"] # integer 0..N
12999

130-
limit = 50
131-
# if data["size"]: # integer Number of records per page.
132-
# limit = data["size"]
100+
class PhotoDetailsView(RequestView):
101+
"""View to handle single photo requests."""
133102

134-
_LOGGER.info(f"loading data for user {user_id}")
103+
url = "/v1/photo/{entity_id}"
104+
name = "v1:photo"
135105

136-
response = PhotoResponse(
137-
offset=offset, limit=limit, size=len(results), results=results
106+
async def get(self, core: ApplicationCore, request: web.Request, entity_id: str) -> web.Response:
107+
"""Return an entity."""
108+
_LOGGER.debug(f"GET /v1/photo/{entity_id}")
109+
110+
# TODO: add user_id to check if user has access to image
111+
photo = await core.storage.read_photo(entity_id)
112+
113+
if photo is None:
114+
raise web.HTTPNotFound
115+
116+
# photo owner
117+
# TODO: get first-/lastname of owner
118+
# owner = await core.authentication
119+
120+
_LOGGER.debug(f"photo {photo.uuid}")
121+
file = os.path.join(photo.directory, photo.filename)
122+
_LOGGER.debug(f"get additional data for {file} / {os.path.exists(file)}")
123+
124+
# photo creation time
125+
fname = pathlib.Path(file)
126+
mtime = datetime.fromtimestamp(fname.stat().st_mtime)
127+
ctime = datetime.fromtimestamp(fname.stat().st_ctime)
128+
129+
# photo location
130+
location = None
131+
latitude = await core.storage.read("latitude")
132+
longitude = await core.storage.read("longitude")
133+
if latitude is not None and longitude is not None:
134+
altitude = await core.storage.read("altitude")
135+
if altitude is not None:
136+
location = Location(latitude=latitude, longitude=longitude, altitude=altitude)
137+
else:
138+
location = Location(latitude=latitude, longitude=longitude, altitude="0.0")
139+
140+
# photo tags
141+
tags = await core.storage.read("tags")
142+
143+
result = PhotoDetailsResponse(
144+
id=photo.uuid,
145+
name=photo.filename,
146+
author=photo.owner,
147+
created_at=ctime.isoformat(),
148+
details=Details(
149+
camera="Nikon Z7",
150+
lens="Nikkor 200mm F1.8",
151+
focal_length="200",
152+
iso="400",
153+
shutter_speed="1/2000",
154+
aperture="4.0",
155+
),
156+
tags=tags,
157+
location=location,
158+
image_url=f"{core.config.external_url}/v1/file/{entity_id}"
138159
)
160+
return web.Response(text=json.dumps(result, cls=PhotoEncoder), content_type="application/json")
139161

140-
return web.Response(
141-
text=json.dumps(response, cls=PhotoEncoder), content_type="application/json"
142-
)
162+
163+
class PhotoView(RequestView):
164+
"""View to handle photo file requests."""
165+
166+
# TODO: enable auth
167+
requires_auth = False
168+
url = "/v1/file/{entity_id}"
169+
name = "v1:file"
170+
171+
async def get(self, core: ApplicationCore, request: web.Request, entity_id: str) -> web.Response:
172+
"""Return an entity."""
173+
_LOGGER.debug(f"GET /v1/file/{entity_id}")
174+
175+
# TODO: parse params max-with / max-height =wmax-width-hmax-height (=w2048-h1024)
176+
# -wmax-width (preserving the aspect ratio)
177+
# -hmax-height (preserving the aspect ratio)
178+
# -c crop images to max-width / max-height
179+
# -d remove exif data
180+
181+
result = Session.query(Photo).filter(Photo.uuid == entity_id).first()
182+
183+
file = os.path.join(result.directory, result.filename)
184+
if os.path.exists(os.path.join(file)):
185+
return web.FileResponse(path=file, status=200)
186+
else:
187+
raise web.HTTPNotFound()
143188

144189
async def post(self, core: ApplicationCore, request: web.Request) -> web.Response:
145190
"""Upload new photo resource."""
@@ -179,30 +224,16 @@ async def post(self, core: ApplicationCore, request: web.Request) -> web.Respons
179224

180225
status_code = HTTP_CREATED if new_entity_created else HTTP_OK
181226

182-
resp = self.json_message(
183-
f"File successfully added with ID: {new_entity_id}", status_code
184-
)
227+
resp = self.json_message(f"File successfully added with ID: {new_entity_id}", status_code)
185228
resp.headers.add("Location", f"/api/photo/{new_entity_id}")
186229

187230
return resp
188231

189-
190-
class PhotoView(RequestView):
191-
"""View to handle single photo requests."""
192-
193-
url = "/api/photo/{entity_id}"
194-
name = "api:photo"
195-
196-
async def get(self, request: web.Request, entity_id) -> web.Response:
197-
"""Return an entity."""
198-
return self.json_message(f"return GET {entity_id}")
199-
200-
async def post(self, request: web.Request, entity_id):
201-
"""Create an entity."""
202-
return self.json_message(f"return POST {entity_id}")
203-
204-
async def delete(self, request: web.Request, entity_id):
232+
async def delete(self, core: ApplicationCore, request: web.Request, entity_id: str):
205233
"""Delete an entity."""
234+
_LOGGER.debug(f"DELETE /v1/file/{entity_id}")
235+
236+
# TODO: delete entity
206237
return self.json_message(f"return DELETE {entity_id}")
207238

208239

core/addons/api/dto/photo.py

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,19 +9,30 @@ def default(self, o):
99
"""Encode all properties."""
1010
return o.__dict__
1111

12+
class PhotoResponse:
13+
"""Photo response object."""
14+
15+
def __init__(
16+
self, id, name, image_url
17+
):
18+
"""Initialize photo response object."""
19+
self.id = id
20+
self.name = name
21+
self.image_url = image_url
22+
1223

13-
class Photo:
24+
class PhotoDetailsResponse:
1425
"""Photo response object."""
1526

1627
def __init__(
17-
self, name, description, author, created_at, details, tags, location, image_urls
28+
self, id, name, owner, created_at, details, tags, location, image_url
1829
):
1930
"""Initialize photo response object."""
31+
self.id = id
2032
self.name = name
21-
self.description = description
22-
self.author = author
33+
self.owner = owner
2334
self.created_at = created_at
2435
self.details = details
2536
self.tags = tags
2637
self.location = location
27-
self.image_urls = image_urls
38+
self.image_url = image_url

core/addons/api/dto/photo_response.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"""photo response"""
22

33

4-
class PhotoResponse:
4+
class PhotosResponse:
55
def __init__(self, offset, limit, size, results):
66
self.offset = offset
77
self.limit = limit

core/addons/api/dto/photo_url.py

Lines changed: 0 additions & 7 deletions
This file was deleted.

core/authentication/__init__.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ def add_client(self, auth_client: AuthenticationClient):
5050

5151
async def revoke_token_handler(self, request: web.Request) -> web.StreamResponse:
5252
"""
53-
Revoke the requeste token and all
53+
Revoke the request token and all associated access tokens [RFC 7009]
5454
5555
See Section 2.1: https://tools.ietf.org/html/rfc7009#section-2.1
5656
"""
@@ -240,12 +240,13 @@ async def authorization_endpoint_post(self, request: web.Request) -> web.StreamR
240240
if "client_id" not in request.query:
241241
_LOGGER.warning("invalid form")
242242
raise web.HTTPFound(f"{redirect_uri}?error=unauthorized_client")
243-
244243
client_id = request.query["client_id"]
244+
_LOGGER.debug(f"client_id {client_id}")
245245

246246
state = None
247247
if "state" in request.query:
248248
state = request.query["state"]
249+
_LOGGER.debug(f"state {state}")
249250

250251
# check if client is known
251252
if not any(client.client_id == client_id for client in self.auth_clients):
@@ -277,6 +278,7 @@ async def authorization_endpoint_post(self, request: web.Request) -> web.StreamR
277278
if credentials_are_valid:
278279
# create an authorization code
279280
authorization_code = self.auth_database.create_authorization_code(username, client_id, request.remote)
281+
_LOGGER.debug(f"authorization_code: {authorization_code}")
280282
if authorization_code is None:
281283
_LOGGER.warning("could not create auth code for client!")
282284
error_reason = "access_denied"
@@ -286,8 +288,10 @@ async def authorization_endpoint_post(self, request: web.Request) -> web.StreamR
286288
raise web.HTTPFound(f"{redirect_uri}?error={error_reason}")
287289

288290
if state is not None:
291+
_LOGGER.debug(f"HTTPFound: {redirect_uri}?code={authorization_code}&state={state}")
289292
redirect_response = web.HTTPFound(f"{redirect_uri}?code={authorization_code}&state={state}")
290293
else:
294+
_LOGGER.debug(f"HTTPFound: {redirect_uri}?code={authorization_code}")
291295
redirect_response = web.HTTPFound(f"{redirect_uri}?code={authorization_code}")
292296

293297
raise redirect_response

0 commit comments

Comments
 (0)