Skip to content

Commit 11f92c9

Browse files
Merge pull request #785 from adamtheturtle/success-add
Initial passing test for adding a target
2 parents 542d83b + c76e255 commit 11f92c9

File tree

10 files changed

+314
-0
lines changed

10 files changed

+314
-0
lines changed

dev-requirements.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,7 @@ pyroma==2.4 # Packaging best practices checker
1414
pytest==3.8.0 # Test runners
1515
PyYAML==3.13
1616
vulture==0.29
17+
VWS-Python-Mock==2018.9.10.6
1718
yapf==0.24.0 # Automatic formatting for Python
19+
pytest-cov==2.6.0 # Measure code coverage
20+
codecov==2.0.15 # Upload coverage data

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
requests==2.19.1

src/vws/__init__.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
11
"""
22
A library for Vuforia Web Services.
33
"""
4+
5+
from .vws import VWS
6+
7+
__all__ = [
8+
'VWS',
9+
]

src/vws/_authorization.py

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
"""
2+
Authorization helpers.
3+
"""
4+
5+
import base64
6+
import email.utils
7+
import hashlib
8+
import hmac
9+
10+
11+
def compute_hmac_base64(key: bytes, data: bytes) -> bytes:
12+
"""
13+
Return the Base64 encoded HMAC-SHA1 hash of the given `data` using the
14+
provided `key`.
15+
"""
16+
hashed = hmac.new(key=key, msg=None, digestmod=hashlib.sha1)
17+
hashed.update(msg=data)
18+
return base64.b64encode(s=hashed.digest())
19+
20+
21+
def rfc_1123_date() -> str:
22+
"""
23+
Return the date formatted as per RFC 2616, section 3.3.1, rfc1123-date, as
24+
described in
25+
https://library.vuforia.com/articles/Training/Using-the-VWS-API.
26+
"""
27+
return email.utils.formatdate(None, localtime=False, usegmt=True)
28+
29+
30+
def authorization_header( # pylint: disable=too-many-arguments
31+
access_key: bytes,
32+
secret_key: bytes,
33+
method: str,
34+
content: bytes,
35+
content_type: str,
36+
date: str,
37+
request_path: str,
38+
) -> bytes:
39+
"""
40+
Return an `Authorization` header which can be used for a request made to
41+
the VWS API with the given attributes.
42+
43+
See https://library.vuforia.com/articles/Training/Using-the-VWS-API.
44+
45+
Args:
46+
access_key: A VWS server or client access key.
47+
secret_key: A VWS server or client secret key.
48+
method: The HTTP method which will be used in the request.
49+
content: The request body which will be used in the request.
50+
content_type: The `Content-Type` header which is expected by
51+
endpoint. This does not necessarily have to match the
52+
`Content-Type` sent in the headers. In particular, for the query
53+
API, this must be set to `multipart/form-data` but the header must
54+
include the boundary.
55+
date: The current date which must exactly match the date sent in the
56+
`Date` header.
57+
request_path: The path to the endpoint which will be used in the
58+
request.
59+
60+
Returns:
61+
Return an `Authorization` header which can be used for a request made
62+
to the VWS API with the given attributes.
63+
"""
64+
hashed = hashlib.md5()
65+
hashed.update(content)
66+
content_md5_hex = hashed.hexdigest()
67+
68+
components_to_sign = [
69+
method,
70+
content_md5_hex,
71+
content_type,
72+
date,
73+
request_path,
74+
]
75+
string_to_sign = '\n'.join(components_to_sign)
76+
signature = compute_hmac_base64(
77+
key=secret_key,
78+
data=bytes(
79+
string_to_sign,
80+
encoding='utf-8',
81+
),
82+
)
83+
auth_header = b'VWS %s:%s' % (access_key, signature)
84+
return auth_header

src/vws/vws.py

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
"""
2+
Tools for interacting with Vuforia APIs.
3+
"""
4+
5+
import base64
6+
import io
7+
import json
8+
from typing import Union
9+
from urllib.parse import urljoin
10+
11+
import requests
12+
from requests import Response
13+
14+
from vws._authorization import authorization_header, rfc_1123_date
15+
16+
17+
def _target_api_request(
18+
server_access_key: bytes,
19+
server_secret_key: bytes,
20+
method: str,
21+
content: bytes,
22+
request_path: str,
23+
base_vws_url: str,
24+
) -> Response:
25+
"""
26+
Make a request to the Vuforia Target API.
27+
28+
This uses `requests` to make a request against https://vws.vuforia.com.
29+
The content type of the request will be `application/json`.
30+
31+
Args:
32+
server_access_key: A VWS server access key.
33+
server_secret_key: A VWS server secret key.
34+
method: The HTTP method which will be used in the request.
35+
content: The request body which will be used in the request.
36+
request_path: The path to the endpoint which will be used in the
37+
request.
38+
39+
base_vws_url: The base URL for the VWS API.
40+
41+
Returns:
42+
The response to the request made by `requests`.
43+
"""
44+
date = rfc_1123_date()
45+
content_type = 'application/json'
46+
47+
signature_string = authorization_header(
48+
access_key=server_access_key,
49+
secret_key=server_secret_key,
50+
method=method,
51+
content=content,
52+
content_type=content_type,
53+
date=date,
54+
request_path=request_path,
55+
)
56+
57+
headers = {
58+
'Authorization': signature_string,
59+
'Date': date,
60+
'Content-Type': content_type,
61+
}
62+
63+
url = urljoin(base=base_vws_url, url=request_path)
64+
65+
response = requests.request(
66+
method=method,
67+
url=url,
68+
headers=headers,
69+
data=content,
70+
)
71+
72+
return response
73+
74+
75+
class VWS:
76+
"""
77+
An interface to Vuforia Web Services APIs.
78+
"""
79+
80+
def __init__(
81+
self,
82+
server_access_key: str,
83+
server_secret_key: str,
84+
base_vws_url: str = 'https://vws.vuforia.com',
85+
) -> None:
86+
"""
87+
Args:
88+
server_access_key: A VWS server access key.
89+
server_secret_key: A VWS server secret key.
90+
base_vws_url: The base URL for the VWS API.
91+
"""
92+
self._server_access_key = server_access_key.encode()
93+
self._server_secret_key = server_secret_key.encode()
94+
self._base_vws_url = base_vws_url
95+
96+
def add_target(
97+
self,
98+
name: str,
99+
width: Union[int, float],
100+
image: io.BytesIO,
101+
) -> str:
102+
"""
103+
Add a target to a Vuforia Web Services database.
104+
105+
Args:
106+
name: The name of the target.
107+
width: The width of the target.
108+
image: The image of the target.
109+
110+
Returns:
111+
The target ID of the new target.
112+
"""
113+
image_data = image.getvalue()
114+
image_data_encoded = base64.b64encode(image_data).decode('ascii')
115+
metadata_encoded = None
116+
117+
data = {
118+
'name': name,
119+
'width': width,
120+
'image': image_data_encoded,
121+
'application_metadata': metadata_encoded,
122+
}
123+
124+
content = bytes(json.dumps(data), encoding='utf-8')
125+
126+
response = _target_api_request(
127+
server_access_key=self._server_access_key,
128+
server_secret_key=self._server_secret_key,
129+
method='POST',
130+
content=content,
131+
request_path='/targets',
132+
base_vws_url=self._base_vws_url,
133+
)
134+
135+
return str(response.json()['target_id'])

tests/conftest.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
"""
2+
Configuration, plugins and fixtures for `pytest`.
3+
"""
4+
5+
from typing import Iterator
6+
7+
import pytest
8+
from mock_vws import MockVWS
9+
10+
from vws import VWS
11+
12+
pytest_plugins = [ # pylint: disable=invalid-name
13+
'tests.fixtures.images',
14+
]
15+
16+
17+
@pytest.fixture()
18+
def client() -> Iterator[VWS]:
19+
"""
20+
Yield a VWS client which connects to a mock.
21+
"""
22+
with MockVWS() as mock:
23+
vws_client = VWS(
24+
server_access_key=mock.server_access_key,
25+
server_secret_key=mock.server_secret_key,
26+
)
27+
28+
yield vws_client

tests/data/high_quality_image.jpg

15.5 KB
Loading

tests/fixtures/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
"""
2+
Common fixtures.
3+
"""

tests/fixtures/images.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
"""
2+
Fixtures for images.
3+
"""
4+
5+
import io
6+
7+
import pytest
8+
9+
10+
@pytest.fixture()
11+
def high_quality_image() -> io.BytesIO:
12+
"""
13+
Return an image file which is expected to have a 'success' status when
14+
added to a target and a high tracking rating.
15+
16+
At the time of writing, this image gains a tracking rating of 5.
17+
"""
18+
path = 'tests/data/high_quality_image.jpg'
19+
with open(path, 'rb') as high_quality_image_file:
20+
return io.BytesIO(high_quality_image_file.read())

tests/test_add_target.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
"""
2+
Tests for helper function for adding a target to a Vuforia database.
3+
"""
4+
5+
import io
6+
7+
from vws import VWS
8+
9+
10+
class TestSuccess:
11+
"""
12+
Tests for successfully adding a target.
13+
"""
14+
15+
def test_add_target(
16+
self,
17+
client: VWS,
18+
high_quality_image: io.BytesIO,
19+
) -> None:
20+
"""
21+
No exception is raised when adding one target.
22+
"""
23+
client.add_target(name='x', width=1, image=high_quality_image)
24+
25+
def test_add_two_targets(
26+
self,
27+
client: VWS,
28+
high_quality_image: io.BytesIO,
29+
) -> None:
30+
"""
31+
No exception is raised when adding two targets with different names.
32+
"""
33+
client.add_target(name='x', width=1, image=high_quality_image)
34+
client.add_target(name='a', width=1, image=high_quality_image)

0 commit comments

Comments
 (0)