Skip to content

Commit 359c5ec

Browse files
authored
Updates to analytics-host to support multiple AnalysisGroups (#84)
* Updates to analytics-host to support multiple `AnalysisGroup` - This update modifies `analytics-host` to accept multiple device/device archive slugs to create `AnalysisGroup` from them * Fixed `find_analysis_group` to accomodate list input from argparse * Updated pytables version to 3.5.1 to address incomptability with latest versions of pandas/numpy (pandas-dev/pandas#24839) * Added a new template 'multiple_source_info' to get information similar to the 'basic_info' template but for a sequence of device/block slugs input to `analytics-host`
1 parent 3a4ea29 commit 359c5ec

File tree

5 files changed

+249
-5
lines changed

5 files changed

+249
-5
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
"""An extension of 'basic_info' template that prints information for multiple AnalysisGroup"""
2+
3+
from __future__ import unicode_literals, absolute_import
4+
import sys
5+
from io import open, StringIO
6+
from past.builtins import basestring
7+
from .analysis_template import AnalysisTemplate
8+
9+
10+
class MultipleSourceInfoReport(AnalysisTemplate):
11+
"""Extension of 'basic_info' AnalysisTemplate that just prints info for multiple sources
12+
13+
If you pass the argument streams as True, the report will also include a
14+
list of all data streams in the AnalysisGroup. This AnalysisTemplate can
15+
be directly viewed on stdout without needing to be saved to a file but can
16+
also optionally generate a text file (.txt) with the metadata that it
17+
prints.
18+
19+
Args:
20+
group (list): A list of AnalysisGroups we wish to analyze
21+
streams (bool): Include stream summary information as well
22+
in the report. This defaults to False if not passed.
23+
"""
24+
25+
def __init__(self, groups, streams=False):
26+
self._groups = groups
27+
self.standalone = True
28+
self.include_streams = streams
29+
30+
def run(self, output_path, file_handler=None):
31+
"""Render this report to output_path.
32+
33+
If this report is a standalone html file, the output path will have
34+
.html appended to it and be a single file.
35+
36+
If this report is not standalone, the output will be folder that is
37+
created at output_path.
38+
39+
If bundle is True and the report is not standalone, it will be zipped
40+
into a file at output_path.zip. Any html or directory that was
41+
created as an intermediary before zipping will be deleted before this
42+
function returns.
43+
44+
Args:
45+
output_path (str): the path to the folder that we wish
46+
to create.
47+
48+
Returns:
49+
str: The path to the actual file or directory created. This
50+
may differ from the file you pass in output_path by an
51+
extension or the addition of a subdirectory.
52+
"""
53+
54+
out = StringIO()
55+
56+
if output_path is not None:
57+
if output_path.endswith('.txt'):
58+
output_path = output_path[:-4]
59+
60+
output_path = output_path + ".txt"
61+
62+
for _group in self._groups:
63+
out.write("Source Info\n")
64+
out.write("-----------\n")
65+
66+
new_line = '\n' + ' ' * 31
67+
for key in sorted(_group.source_info):
68+
if len(key) > 27:
69+
key = key[:27] + '...'
70+
71+
val = _group.source_info[key]
72+
if isinstance(val, basestring):
73+
val = val.encode('utf-8').decode('utf-8')
74+
else:
75+
val = str(val)
76+
out.write('{0:30s} {1}\n'.format(key, val.replace('\n', new_line)))
77+
78+
out.write("\nProperties\n")
79+
out.write("----------\n")
80+
for key in sorted(_group.properties):
81+
if len(key) > 27:
82+
key = key[:27] + '...'
83+
84+
val = _group.properties[key]
85+
if isinstance(val, basestring):
86+
val = val.encode('utf-8').decode('utf-8')
87+
else:
88+
val = str(val)
89+
out.write('{0:30s} {1}\n'.format(key, val.replace('\n', new_line)))
90+
91+
if self.include_streams:
92+
out.write("\nStream Summaries\n")
93+
out.write("----------------\n")
94+
95+
for slug in sorted(_group.streams):
96+
if _group.stream_empty(slug):
97+
continue
98+
99+
name = _group.get_stream_name(slug)
100+
101+
if len(name) > 37:
102+
name = name[:37] + '...'
103+
104+
out.write('{:40s} {:s}\n'.format(name, slug))
105+
106+
out.write("\nStream Counts\n")
107+
out.write("-------------\n")
108+
109+
for slug in sorted(_group.streams):
110+
if _group.stream_empty(slug):
111+
continue
112+
113+
counts = _group.stream_counts[slug]
114+
115+
out.write('{:s} {: 6d} points {: 6d} events\n'.format(slug, counts.get('points'), counts.get('events')))
116+
117+
encoded = out.getvalue().encode('utf-8')
118+
119+
if file_handler is None:
120+
with open(output_path, "wb") as outfile:
121+
outfile.write(output_path, encoded)
122+
else:
123+
file_handler(output_path, encoded)
124+
125+
return [output_path]

iotile_analytics_interactive/iotile_analytics/interactive/scripts/analytics_host.py

+24-4
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ def build_args():
121121
parser.add_argument('--web-push-slug', type=str, default=None, help="Override the source slug given in the analysisgroup and force it to be this")
122122
parser.add_argument('--token', type=str, default=None, help="Token for authentication to iotile cloud (instead of a password)")
123123
parser.add_argument('-d', '--domain', default=DOMAIN_NAME, help="Domain to use for remote queries, defaults to https://iotile.cloud")
124-
parser.add_argument('analysis_group', default=None, nargs='?', help="The slug or path of the object you want to perform analysis on")
124+
parser.add_argument('analysis_group', default=None, nargs='*', help="The slug or path of the object you want to perform analysis on")
125125

126126
return parser
127127

@@ -214,11 +214,26 @@ def print_report_details(report):
214214
except ValidationError:
215215
print("Error parsing docstring for report.")
216216

217+
def find_analysis_groups(args):
218+
"""Parse through the list of options for analysis_group and build a list"""
219+
groups = args.analysis_group
217220

218-
def find_analysis_group(args):
221+
all_groups = []
222+
all_logins = True
223+
for _group in groups:
224+
logged_in, group = find_analysis_group(args, _group)
225+
all_groups.append(group)
226+
all_logins = all_logins and logged_in
227+
228+
if len(all_groups) == 1:
229+
return all_logins, all_groups[0]
230+
231+
return all_logins, all_groups
232+
233+
def find_analysis_group(args, group_in):
219234
"""Find an analysis group by name."""
220235

221-
group = args.analysis_group
236+
group = group_in
222237
is_cloud = False
223238

224239
if group.startswith('d--'):
@@ -337,6 +352,11 @@ def build_file_handler(output_path, standalone, bundle, web_push, label, group,
337352
if label is None:
338353
label = input("Enter a label for the report: ")
339354

355+
356+
# if there are multiple AnalysisGroup, push to the first one by default
357+
if isinstance(group, list):
358+
group = group[0]
359+
340360
if slug is None and group is not None:
341361
slug = group.source_info.get('slug')
342362

@@ -422,7 +442,7 @@ def main(argv=None):
422442
return 0
423443

424444
report_args = split_args(args.arg)
425-
logged_in, group = find_analysis_group(args)
445+
logged_in, group = find_analysis_groups(args)
426446

427447

428448
check_arguments(report_obj, report_args, confirm=not args.no_confirm)

iotile_analytics_interactive/setup.py

+1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
entry_points={
1818
'console_scripts': ['analytics-host = iotile_analytics.interactive.scripts.analytics_host:cmdline_main'],
1919
'iotile_analytics.live_report': ['basic_info = iotile_analytics.interactive.reports.info_report:SourceInfoReport',
20+
'multiple_source_info = iotile_analytics.interactive.reports.multiple_source_info:MultipleSourceInfoReport',
2021
'stream_overview = iotile_analytics.interactive.reports.stream_report:StreamOverviewReport']
2122
},
2223
include_package_data=True,

iotile_analytics_interactive/test/test_analytics_host.py

+98
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,92 @@
4444
"----------\n")
4545

4646

47+
MULTIPLE_SOURCES_ARCHIVE = (
48+
"Source Info\n"
49+
"-----------\n"
50+
"block 1\n"
51+
"created_by tim\n"
52+
"created_on 2017-10-27T21:23:36Z\n"
53+
"description Test archive for analysis purposes\n"
54+
"id 25\n"
55+
"org arch-internal\n"
56+
"pid\n"
57+
"sg saver-v1-0-0\n"
58+
"slug b--0001-0000-0000-04e7\n"
59+
"title Archive: Test Saver Device (04e7)\n\n"
60+
"Properties\n"
61+
"----------\n"
62+
"CargoDescription S.O 80287\n"
63+
"Country USA\n"
64+
"Customer Test Customer\n"
65+
"LoadingType 7310037\n"
66+
"Message Ship Date:06/23/2016\n"
67+
" PAL:1206-40\n"
68+
" Test Calibration:03/29/2018\n"
69+
" C3684\n"
70+
"Notes Ship Date:06/23/2016\n"
71+
" PAL:1206-40\n"
72+
" Test Calibration:03/29/2018\n"
73+
" C3684\n"
74+
"Product/Tool Type Big Box\n"
75+
"ProjectID TP\n"
76+
"Region A\n"
77+
"S/N 0540-949\n"
78+
"Sales Order No 15\n"
79+
"Ship From America\n"
80+
"Ship To The World\n"
81+
"Ship Via Fedex\n"
82+
"ShipFrom KLA ISRAEL\n"
83+
"ShipTo SMNC\n"
84+
"ShipVia AIR/AIR TRUCK\n"
85+
"System No 1\n"
86+
"Transport Type Air\n"
87+
"TransportType AIR/AIR TRUCK\n"
88+
"TripID 31\n"
89+
"Source Info\n"
90+
"-----------\n"
91+
"block 1\n"
92+
"created_by tim\n"
93+
"created_on 2017-10-27T21:23:36Z\n"
94+
"description Test archive for analysis purposes\n"
95+
"id 25\n"
96+
"org arch-internal\n"
97+
"pid\n"
98+
"sg saver-v1-0-0\n"
99+
"slug b--0001-0000-0000-04e7\n"
100+
"title Archive: Test Saver Device (04e7)\n\n"
101+
"Properties\n"
102+
"----------\n"
103+
"CargoDescription S.O 80287\n"
104+
"Country USA\n"
105+
"Customer Test Customer\n"
106+
"LoadingType 7310037\n"
107+
"Message Ship Date:06/23/2016\n"
108+
" PAL:1206-40\n"
109+
" Test Calibration:03/29/2018\n"
110+
" C3684\n"
111+
"Notes Ship Date:06/23/2016\n"
112+
" PAL:1206-40\n"
113+
" Test Calibration:03/29/2018\n"
114+
" C3684\n"
115+
"Product/Tool Type Big Box\n"
116+
"ProjectID TP\n"
117+
"Region A\n"
118+
"S/N 0540-949\n"
119+
"Sales Order No 15\n"
120+
"Ship From America\n"
121+
"Ship To The World\n"
122+
"Ship Via Fedex\n"
123+
"ShipFrom KLA ISRAEL\n"
124+
"ShipTo SMNC\n"
125+
"ShipVia AIR/AIR TRUCK\n"
126+
"System No 1\n"
127+
"Transport Type Air\n"
128+
"TransportType AIR/AIR TRUCK\n"
129+
"TripID 31"
130+
)
131+
132+
47133
def test_info_report_archive_stdout(shipping, capsys):
48134
"""Make sure we can render info to stdout from an archive."""
49135

@@ -192,3 +278,15 @@ def test_rendering_report(water_meter, tmpdir):
192278

193279
retval = main(['-t', 'stream_overview', slug, '-d', domain, '--no-verify', '-o', str(tmpdir.join('report')), '-c', '-a', 'stream=%s' % stream])
194280
assert retval == 0
281+
282+
def test_multiple_analysis_group_inputs(shipping, capsys):
283+
"""Test whether we pass multiple slugs as arguments to AnalysisTemplate"""
284+
285+
domain, _cloud = shipping
286+
slug1 = 'b--0001-0000-0000-04e7'
287+
slug2 = 'b--0001-0000-0000-04e7'
288+
289+
CloudSession(user='[email protected]', password='test', domain=domain, verify=False)
290+
retval = main(['-t', 'multiple_source_info', slug1, slug2, '-d', domain, '--no-verify', '-c'])
291+
assert retval == 0
292+

tox.ini

+1-1
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ deps=
2424
pyOpenSSL
2525
pytest-logging
2626
pytest-localserver
27-
tables==3.4.2
27+
tables==3.5.1
2828
./iotile_analytics_core
2929
./iotile_analytics_interactive
3030
./iotile_analytics_offline

0 commit comments

Comments
 (0)