Skip to content

Commit

Permalink
Direct ArchFXFlexibleDictionaryReport upload (#8)
Browse files Browse the repository at this point in the history
`streamer/report` API endpoint requires very particular request structure, fully derivable from `ArchFXFlexibleDictionaryReport` object.

Let's ensure request correctness by providing a dedicated API call.
  • Loading branch information
dmaone authored Aug 3, 2021
1 parent 55c6195 commit 52f1963
Show file tree
Hide file tree
Showing 6 changed files with 64 additions and 2 deletions.
4 changes: 4 additions & 0 deletions RELEASE.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

All major changes in each released version of the archfx-cloud plugin are listed here.

## 0.12.0

- Support `ArchFXFlexibleDictionaryReport` direct upload

## 0.11.0

- Support Django SimpleJWT authentication variety
Expand Down
3 changes: 3 additions & 0 deletions archfx_cloud/api/connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,9 @@ def refresh_token(self):
self._destroy_tokens()
return False

def __call__(self, id):
return self.resource_class(session=self.session, base_url=self.url(id))

def __getattr__(self, item):
"""
Instead of raising an attribute error, the undefined attribute will
Expand Down
15 changes: 15 additions & 0 deletions archfx_cloud/reports/flexible_dictionary.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""A flexible dictionary based report format suitable for msgpack and json serialization."""

import datetime
from io import BytesIO
from typing import List, Union
import pytz
import msgpack
Expand Down Expand Up @@ -115,6 +116,20 @@ def write(self, file_path: str):
with open(file_path, "wb") as outfile:
outfile.write(self.encode())

def upload(self, cloud):
"""Uploads this report into ArchFX cloud
Args:
cloud: an instance of archfx_cloud.api.connection.Api. Must be authenticated.
Returns:
int: The number of new readings that were accepted by the cloud as novel.
"""
return cloud("streamer/report").upload_fp(
("report.mp", BytesIO(self.encode())),
timestamp=self.sent_timestamp,
)['count']


def _encode_datetime(obj):
"""Pack a datetime into an isoformat string."""
Expand Down
3 changes: 3 additions & 0 deletions tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,9 @@ def test_upload_fp_content_type(self, m):
resp = api.test.upload_fp(BytesIO(b"test")) # "No mock address" means content-type is broken!
self.assertEqual(resp['result'], 'ok')

request_body = m.request_history[0]._request.body
self.assertIn(b'Content-Disposition: form-data; name="file"; filename=', request_body)

@requests_mock.Mocker()
def test_get_list(self, m):
payload = {
Expand Down
39 changes: 38 additions & 1 deletion tests/test_flexible_report.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
from datetime import datetime
from datetime import datetime, timezone
import unittest

import dateutil.parser
import msgpack
import requests_mock

from archfx_cloud.api.connection import Api
from archfx_cloud.reports.flexible_dictionary import ArchFXFlexibleDictionaryReport
from archfx_cloud.reports.report import ArchFXDataPoint

Expand Down Expand Up @@ -149,3 +151,38 @@ def test_report_usage(self):
assert report_data[0].get('extra_data') == {'foo': 5, 'bar': 'foobar'}
assert report_data[1].get('extra_data') == {'foo': 6, 'bar': 'foobar'}
assert report_data[2].get('extra_data') == {}

@requests_mock.Mocker()
def test_upload(self, m):
"""Testing that we can upload a FlexibleDictionaryReport."""
report = ArchFXFlexibleDictionaryReport.FromReadings(
device='d--1234',
data=[
ArchFXDataPoint(
timestamp=datetime(2021, 1, 20, tzinfo=timezone.utc),
stream='0001-5030',
value=2.0,
summary_data={'foo': 5, 'bar': 'foobar'},
raw_data=None,
reading_id=1000
),
],
report_id=1003,
streamer=0xff,
sent_timestamp=datetime(2021, 1, 20, tzinfo=timezone.utc),
)
sent_time_str = report.sent_timestamp.replace(":", "%3A").replace("+", "%2B")
m.post(
f"http://archfx.test/api/v1/streamer/report/?timestamp={sent_time_str}",
json={"count": 1},
)


api = Api(domain='http://archfx.test')
resp = report.upload(api)
self.assertEqual(resp, 1)

request = m.request_history[0]._request
self.assertIn('multipart/form-data; boundary=', request.headers["Content-Type"])
self.assertIn(b'Content-Disposition: form-data; name="file"; filename=', request.body)
self.assertIn(b'filename="report.mp"', request.body) # Check filename correctness
2 changes: 1 addition & 1 deletion version.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
version = '0.11.0'
version = '0.12.0'

0 comments on commit 52f1963

Please sign in to comment.