FAQ | This is a LIVE service | Changelog

Skip to content
Snippets Groups Projects
Commit 56bc0ef7 authored by Monty Dawson's avatar Monty Dawson :coffee:
Browse files

Add photo_client for fetching data from the PhotoAPI

Also fix an issue with dataclasses not being loaded when using python 3.6
parent b20b46a0
No related branches found
No related tags found
1 merge request!2Add photo_client for fetching data from the PhotoAPI
[flake8]
max-line-length=99
exclude=.venv,venv,.tox
exclude=.venv,venv,.tox,env
from oauthlib.oauth2 import BackendApplicationClient
from requests_oauthlib import OAuth2Session
from functools import lru_cache
from requests import auth
class NoSuchPhotoException(Exception):
def __init__(self):
super().__init__('No photo found for query criteria')
class PhotoClient:
"""
A client which allows information to be fetched from the University Photo API.
"""
def __init__(
self,
client_id: str,
client_secret: str,
base_url: str = 'https://api.apps.cam.ac.uk/photo/v1beta1/',
token_url: str = 'https://api.apps.cam.ac.uk/oauth/client_credential/accesstoken'
):
self.client_id = client_id
self.client_secret = client_secret
self.base_url = base_url.rstrip('/')
self.token_url = token_url
@property
@lru_cache()
def session(self):
"""
Lazy-init a OAuth2-wrapped session and fetch a token before it's used.
"""
client = BackendApplicationClient(client_id=self.client_id)
session = OAuth2Session(client=client)
session.fetch_token(
token_url=self.token_url,
auth=auth.HTTPBasicAuth(self.client_id, self.client_secret)
)
return session
def get_transient_image_url(self, photo_id: str):
"""
Returns a transient image url which can be used to by unauthenticated clients
to fetch the url of the given photo id.
"""
response = self.session.get(
f'{self.base_url}/photos/{photo_id}/content', allow_redirects=False
)
if response.status_code == 404:
raise NoSuchPhotoException()
response.raise_for_status()
if not response.headers.get('Location'):
raise RuntimeError('Photo API did not return a transient image url within redirect')
return response.headers['Location']
coverage
pytest
pytest-cov
\ No newline at end of file
pytest-cov
requests_mock
dataclasses
\ No newline at end of file
dataclasses
# for the API clients
requests
requests_oauthlib
from unittest import TestCase
from identitylib.identifiers import Identifier, IdentifierSchemes
from identitylib.identifiers import Identifier, IdentifierSchemes, IdentifierScheme
class IdentifiersTestCase(TestCase):
......@@ -75,3 +75,14 @@ class IdentifiersTestCase(TestCase):
'Invalid identifier scheme barcode.identifiers.lib.cam.ac.uk'
):
Identifier.from_string('VBE@barcode.identifiers.lib.cam.ac.uk')
def test_identifier_schemes_can_be_compared(self):
self.assertTrue(
IdentifierSchemes.PHOTO == IdentifierScheme(
'photo.v1.photo.university.identifiers.cam.ac.uk',
'Photo Identifier',
{
'deprecated': 'photo_id.photo.identifiers.uis.cam.ac.uk',
}
)
)
from unittest import TestCase
from requests import Session, HTTPError
from requests_mock import Mocker
from base64 import b64encode
from identitylib import photo_client
class PhotoClientTestCase(TestCase):
@Mocker()
def setUp(self, request_mock: Mocker):
"""
Setup the photo client here in order to intercept the initial auth request.
"""
auth_request = request_mock.post(
'https://api.apps.cam.ac.uk/oauth/client_credential/accesstoken',
json={
'token_type': 'Bearer',
'access_token': 'fake-token',
}
)
self.client = photo_client.PhotoClient('mock-client-id', 'mock-client-key')
self.assertIsInstance(self.client.session, Session)
self.assertEqual(len(auth_request.request_history), 1)
self.assertEqual(
auth_request.request_history[0].headers['Authorization'],
f'Basic {b64encode(b"mock-client-id:mock-client-key").decode("utf-8")}'
)
@Mocker()
def test_can_fetch_transient_photo_url(self, request_mock: Mocker):
request_mock.get(
f'{self.client.base_url}/photos/test-photo-uuid-123/content',
status_code=302,
headers={
'Location': 'https://storage.google.com/test-photo-uuid-123?expiresIn=10'
}
)
photo_url = self.client.get_transient_image_url('test-photo-uuid-123')
self.assertEqual(photo_url, 'https://storage.google.com/test-photo-uuid-123?expiresIn=10')
@Mocker()
def test_will_throw_if_no_photo_for_transient_url_query(self, request_mock: Mocker):
request_mock.get(
f'{self.client.base_url}/photos/no-photo-available/content', status_code=404,
)
with self.assertRaises(photo_client.NoSuchPhotoException):
self.client.get_transient_image_url('no-photo-available')
@Mocker()
def test_will_throw_if_unexpected_response_for_transient_url(self, request_mock: Mocker):
request_mock.get(
f'{self.client.base_url}/photos/test-photo-uuid/content', status_code=200,
json={'success': True}
)
with self.assertRaisesRegex(
RuntimeError, 'Photo API did not return a transient image url within redirect'
):
self.client.get_transient_image_url('test-photo-uuid')
@Mocker()
def test_will_throw_if_photo_api_fails_for_transient_url_query(self, request_mock: Mocker):
request_mock.get(
f'{self.client.base_url}/photos/something-will-break/content', status_code=500,
json={'success': True}
)
with self.assertRaises(HTTPError):
self.client.get_transient_image_url('something-will-break')
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment