Skip to content

Commit 21a7cf3

Browse files
committed
Initial passing test for adding a target
1 parent c36d09b commit 21a7cf3

File tree

6 files changed

+248
-8
lines changed

6 files changed

+248
-8
lines changed

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/_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/test_add_target.py

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,6 @@
55
import io
66

77
from vws import VWS
8-
from vws.exceptions import (
9-
BadImage,
10-
Fail,
11-
ImageTooLarge,
12-
MetadataTooLarge,
13-
ProjectInactive,
14-
TargetNameExist,
15-
)
168

179

1810
class TestSuccess:

0 commit comments

Comments
 (0)