From 502a7488e8071bb641d0343af2a14b68e483421a Mon Sep 17 00:00:00 2001
From: Claudio Sousa <claudio.sousa@gmail.com>
Date: Thu, 14 Mar 2019 13:21:24 +0000
Subject: [PATCH] Merged in NRRPLT-7268_csv_api (pull request #10)

[NRRPLT-7268] VC uses the new proxy call to retrieve CSVs

* [NRRPLT-7268] VC uses the new proxy call to retrieve CSVs

* [NRRPLT-7268] missing config validation

* [NRRPLT-7268] returning csv content as string

Approved-by: Manos Angelidis <angelidis@fortiss.org>
Approved-by: Luc Guyot <luc.guyot@epfl.ch>
---
 .../hbp_nrp_virtual_coach/config.py           |   3 +-
 .../hbp_nrp_virtual_coach/simulation.py       |  12 +-
 .../tests/test_simulation.py                  |  20 ++-
 .../tests/test_virtual_coach.py               | 108 ++++++++++++-
 .../hbp_nrp_virtual_coach/virtual_coach.py    | 148 +++++++++++++++++-
 5 files changed, 275 insertions(+), 16 deletions(-)

diff --git a/hbp_nrp_virtual_coach/hbp_nrp_virtual_coach/config.py b/hbp_nrp_virtual_coach/hbp_nrp_virtual_coach/config.py
index 7295a03..53d8425 100644
--- a/hbp_nrp_virtual_coach/hbp_nrp_virtual_coach/config.py
+++ b/hbp_nrp_virtual_coach/hbp_nrp_virtual_coach/config.py
@@ -85,7 +85,8 @@ class Config(dict):
         self.__validate('proxy', ['staging', 'dev', 'local', environment])
         self.__validate('proxy-services', ['experiment-list', 'available-servers', 'server-info',
                                            'experiment-clone', 'experiment-delete',
-                                           'storage-authentication', 'storage-experiment-list'])
+                                           'storage-authentication', 'storage-experiment-list',
+                                           'csv-files', 'experiment-file'])
         self.__validate('simulation-services', ['create', 'state', 'reset'])
         self.__validate('simulation-scripts', ['state-machine', 'transfer-function', 'brain',
                                                'sdf-world'])
diff --git a/hbp_nrp_virtual_coach/hbp_nrp_virtual_coach/simulation.py b/hbp_nrp_virtual_coach/hbp_nrp_virtual_coach/simulation.py
index ab56251..8a6152f 100644
--- a/hbp_nrp_virtual_coach/hbp_nrp_virtual_coach/simulation.py
+++ b/hbp_nrp_virtual_coach/hbp_nrp_virtual_coach/simulation.py
@@ -40,18 +40,20 @@ class Simulation(object):
     Provides an interface to launch or control a simulation instance.
     """
 
-    def __init__(self, http_client, config):
+    def __init__(self, http_client, config, vc):
         """
         Initialize a simulation interface and default logger.
 
         :param http_client: A HTTP client.
         :param config: A loaded Virtual Coach config.
+        :param vc: The virtual coach instance.
         """
         assert isinstance(config, Config)
         assert isinstance(http_client, HTTPClient)
 
         self.__http_client = http_client
         self.__config = config
+        self.__vc = vc
 
         self.__server = None
         self.__server_info = None
@@ -857,3 +859,11 @@ class Simulation(object):
                 "Unable to reset simulation, HTTP status %s" % status_code)
         self.__logger.info('Reset completed. The simulation has been paused and will not be started'
                            ' automatically.')
+
+    def get_last_run_csv_file(self, file_name):
+        """
+        Retrieves a csv file content for the last simulation run.
+
+        :param file_name: The name of csv file for which to retrieve the content.
+        """
+        return self.__vc.get_last_run_csv_file(self.__experiment_id, file_name)
diff --git a/hbp_nrp_virtual_coach/hbp_nrp_virtual_coach/tests/test_simulation.py b/hbp_nrp_virtual_coach/hbp_nrp_virtual_coach/tests/test_simulation.py
index f51d830..2bf450f 100644
--- a/hbp_nrp_virtual_coach/hbp_nrp_virtual_coach/tests/test_simulation.py
+++ b/hbp_nrp_virtual_coach/hbp_nrp_virtual_coach/tests/test_simulation.py
@@ -39,13 +39,10 @@ import json
 from StringIO import StringIO
 
 
-
 class TestSimulation(unittest.TestCase):
 
-    
     def setUp(self):
-        self._sim = Simulation(RequestsClient({'Authorization': 'token'}), Config('local'))
-
+        self._sim = Simulation(RequestsClient({'Authorization': 'token'}), Config('local'), None)
 
     def setUpForLaunch(self):
         get_response = (None,
@@ -56,8 +53,8 @@ class TestSimulation(unittest.TestCase):
         self._sim._Simulation__http_client.post = Mock(return_value=post_response)
 
     def test_init_asserts(self):
-        self.assertRaises(AssertionError, Simulation, None, Config('local'))
-        self.assertRaises(AssertionError, Simulation, RequestsClient({'Authorization': 'token'}), None)
+        self.assertRaises(AssertionError, Simulation, None, Config('local'), None)
+        self.assertRaises(AssertionError, Simulation, RequestsClient({'Authorization': 'token'}), None, None)
 
     def test_launch_asserts(self):
         self.assertRaises(AssertionError, self._sim.launch, None, 'conf', 'server', None)
@@ -77,7 +74,7 @@ class TestSimulation(unittest.TestCase):
         self._sim._Simulation__http_client.get.assert_called_once()
         self._sim._Simulation__http_client.post.assert_not_called()
         mocked_traceback.print_exec.assert_called_once()
-    
+
     @patch('hbp_nrp_virtual_coach.simulation.traceback')
     def test_failed_create_conflict(self, mocked_traceback):
         self.setUpForLaunch()
@@ -476,7 +473,7 @@ class TestSimulation(unittest.TestCase):
         self._sim._Simulation__headers = {}
 
         self._sim._Simulation__http_client.put = Mock(return_value=(httplib.OK, None))
- 
+
 
         self._sim.save_transfer_functions()
         self._sim._Simulation__http_client.put.assert_called_once(
@@ -634,3 +631,10 @@ class TestSimulation(unittest.TestCase):
         self.assertRaises(ValueError, self._sim.reset, 'foo')
         self.assertRaises(Exception, self._sim.reset, 'full')
         self._sim.start.assert_called_once()
+
+    def test_get_csv_last_run_file(self):
+        self._sim._Simulation__vc = Mock()
+        self._sim._Simulation__vc.get_last_run_csv_file = Mock()
+        self._sim._Simulation__experiment_id = 'experiment_id'
+        self._sim.get_last_run_csv_file('file_name')
+        self._sim._Simulation__vc.get_last_run_csv_file.assert_called_once_with('experiment_id', 'file_name')
diff --git a/hbp_nrp_virtual_coach/hbp_nrp_virtual_coach/tests/test_virtual_coach.py b/hbp_nrp_virtual_coach/hbp_nrp_virtual_coach/tests/test_virtual_coach.py
index 9b18051..9f25da1 100644
--- a/hbp_nrp_virtual_coach/hbp_nrp_virtual_coach/tests/test_virtual_coach.py
+++ b/hbp_nrp_virtual_coach/hbp_nrp_virtual_coach/tests/test_virtual_coach.py
@@ -29,7 +29,7 @@ from hbp_nrp_virtual_coach.virtual_coach import VirtualCoach
 
 from bbp_client.oidc.client import BBPOIDCClient
 
-from mock import Mock, patch
+from mock import Mock, patch, MagicMock
 import unittest
 import requests
 import getpass
@@ -57,7 +57,7 @@ class TestVirtualCoach(unittest.TestCase):
                 raise ImportError('no ROS for tests')
             return realimport(name, globals, locals, fromlist, level)
         builtins.__import__ = rospy_import_fail
-        
+
 
         self._vc = VirtualCoach(environment='local', storage_username='nrpuser')
 
@@ -134,14 +134,14 @@ class TestVirtualCoach(unittest.TestCase):
     def test_no_login_credentials(self):
         self.assertRaises(Exception, VirtualCoach)
 
-    
+
     @patch('hbp_nrp_virtual_coach.virtual_coach.VirtualCoach._VirtualCoach__get_storage_token')
     @patch('getpass.getpass', return_value='password')
     def test_storage_auth(self, mock_getpass, mock_storage_login):
         # mocked storage authentication, ensure called if username provided
         VirtualCoach(environment='local', storage_username='nrpuser')
         mock_storage_login.assert_called_once_with('nrpuser', 'password')
-    
+
     @patch('bbp_client.oidc.client.BBPOIDCClient.implicit_auth')
     def test_oidc_auth(self, mock_oidc_login):
         # mocked OIDC authentication, ensure called if username provided
@@ -453,7 +453,7 @@ mock-server-5
         request.return_value = Request()
         self._vc._VirtualCoach__storage_username = None
         self.assertRaises(ValueError, self._vc.clone_cloned_experiment, 'exp_id')
-        
+
         vc_oidc_username = VirtualCoach(environment='dev', oidc_username='youknowwho')
         vc_oidc_username._VirtualCoach__http_client._OIDCHTTPClient__oidc_client.request = Mock(return_value=[{'status': 477}, 'something'])
         self.assertRaises(Exception, vc_oidc_username.clone_cloned_experiment, 'exp_id')
@@ -511,3 +511,101 @@ mock-server-5
         mock_request.return_value = Response()
         content = self._vc._VirtualCoach__get_storage_token('user', 'pass')
         self.assertEqual(content, 'token')
+
+    def __mock_csv_files_response(self):
+        class Response(object):
+            status_code = 200
+            content = """
+            [
+                { "name": "csv1", "folder": "folder1", "size":3, "uuid": "uuid1" },
+                { "name": "csv2", "folder": "folder1", "size":2, "uuid": "uuid2" },
+                { "name": "csv3", "folder": "folder1", "size":4, "uuid": "uuid3" },
+                { "name": "csv1", "folder": "folder2", "size":5, "uuid": "uuid4" }
+            ]
+            """
+
+        return Response()
+
+    @patch('requests.get')
+    @patch('sys.stdout', new_callable=StringIO)
+    def test_print_runs(self, mock_stdout,mock_request):
+        mock_request.return_value  = self.__mock_csv_files_response()
+
+        content = self._vc.print_runs('exp_id')
+        csv_runs = """
++--------+---------+-------+
+| Run id |  Date   | Bytes |
++========+=========+=======+
+|      0 | folder1 |     9 |
++--------+---------+-------+
+|      1 | folder2 |     5 |
++--------+---------+-------+
+        """
+        self.assertEqual(mock_stdout.getvalue().strip(), csv_runs.strip())
+
+    @patch('requests.get')
+    @patch('sys.stdout', new_callable=StringIO)
+    def test_print_csv_run_files(self, mock_stdout,mock_request):
+        mock_request.return_value  = self.__mock_csv_files_response()
+
+        self._vc.print_run_csv_files('exp_id', 0)
+        csv_run_files = """
++------+------+
+| File | Size |
++======+======+
+| csv1 |    3 |
++------+------+
+| csv2 |    2 |
++------+------+
+| csv3 |    4 |
++------+------+
+"""
+        self.assertEqual(mock_stdout.getvalue().strip(), csv_run_files.strip())
+
+    @patch('requests.get')
+    @patch('sys.stdout', new_callable=StringIO)
+    def test_print_csv_last_run_files(self, mock_stdout,mock_request):
+        mock_request.return_value  = self.__mock_csv_files_response()
+
+        self._vc.print_last_run_csv_files('exp_id')
+        csv_run_files = """
++------+------+
+| File | Size |
++======+======+
+| csv1 |    5 |
++------+------+
+"""
+        self.assertEqual(mock_stdout.getvalue().strip(), csv_run_files.strip())
+
+    def __mock_csv_file_response(self):
+        class Response(object):
+            status_code = 200
+            content ='a,b,c\n1,2,3'
+
+        return Response()
+
+    @patch('requests.get')
+    def test_get_run_csv_file(self, mock_request):
+        mock_request.side_effect = [
+            self.__mock_csv_files_response(),
+            self.__mock_csv_file_response()
+        ]
+        self._vc._VirtualCoach__get_csv_file_content = MagicMock(side_effect=self._vc._VirtualCoach__get_csv_file_content)
+        csv_content = self._vc.get_run_csv_file('exp_id', 0, 'csv1')
+
+        self.assertEqual(csv_content, 'a,b,c\n1,2,3')
+        self._vc._VirtualCoach__get_csv_file_content.assert_called_once_with('exp_id', u'uuid1')
+
+    @patch('requests.get')
+    def test_get_last_run_csv_file(self, mock_request):
+        mock_request.side_effect = [
+            self.__mock_csv_files_response(),
+            self.__mock_csv_file_response()
+        ]
+
+        self._vc._VirtualCoach__get_csv_file_content = MagicMock(side_effect=self._vc._VirtualCoach__get_csv_file_content)
+        csv_content = self._vc.get_last_run_csv_file('exp_id', 'csv1')
+        self.assertEqual(csv_content, 'a,b,c\n1,2,3')
+        self._vc._VirtualCoach__get_csv_file_content.assert_called_once_with('exp_id', u'uuid4')
+
+
diff --git a/hbp_nrp_virtual_coach/hbp_nrp_virtual_coach/virtual_coach.py b/hbp_nrp_virtual_coach/hbp_nrp_virtual_coach/virtual_coach.py
index af88278..221be57 100644
--- a/hbp_nrp_virtual_coach/hbp_nrp_virtual_coach/virtual_coach.py
+++ b/hbp_nrp_virtual_coach/hbp_nrp_virtual_coach/virtual_coach.py
@@ -31,6 +31,7 @@ from hbp_nrp_virtual_coach.requests_client import RequestsClient
 from hbp_nrp_virtual_coach.oidc_http_client import OIDCHTTPClient
 
 from datetime import datetime, timedelta
+from collections import defaultdict
 from dateutil import parser, tz
 import json
 import requests
@@ -279,7 +280,7 @@ class VirtualCoach(object):
 
         # attempt to launch the simulation on all server targets, on success return an interface
         # to the simulation
-        sim = Simulation(self.__http_client, self.__config)
+        sim = Simulation(self.__http_client, self.__config, self)
         for server in servers:
             try:
                 if sim.launch(experiment_id, str(experiment_conf), str(server),
@@ -427,3 +428,148 @@ class VirtualCoach(object):
             raise Exception('Error when getting server list, Status Code: %d. Error: %s'
                             % (status_code, response))
         return json.loads(response)
+
+    def __get_available_CSV_files(self, experiment_id):
+        """
+        Internal helper to retrieve the list of CSV files available for an experiment
+
+        :param experiment_id: The experiment id for which to retrieve the list of CSV files
+        """
+        response = requests.get(self.__config['proxy-services']['csv-files'] % (experiment_id,),
+                                headers=self.__http_headers)
+
+        if response.status_code != httplib.OK:
+            raise Exception('Error when getting CSV files Status Code: %d. Error: %s'
+                            % (response.status_code, response))
+        csv_files = json.loads(response.content)
+        distinct_runs = defaultdict(dict)
+        for csv_file in csv_files:
+            distinct_runs[csv_file['folder']][csv_file['name']] = csv_file
+        return distinct_runs
+
+    def print_runs(self, exp_id):
+        """
+        Prints the list of simulation runs that generated CSV files
+
+        :param exp_id: The experiment id for which to retrieve the list of CSV simulation runs
+        """
+        csv_files = self.__get_available_CSV_files(exp_id)
+
+        table = Texttable()
+        table.header(['Run id', 'Date', 'Bytes'])
+        table.set_cols_align(['r', 'c', 'r'])
+
+        for i, run_date in enumerate(sorted(csv_files.keys())):
+            run_size = sum(file['size'] for file in csv_files[run_date].values())
+            table.add_row([i, run_date, run_size])
+
+        logger.info('List of simulation runs')
+        print table.draw()
+
+    def print_run_csv_files(self, exp_id, run_id):
+        """
+        Prints the list of CSV files for a given run
+
+        :param exp_id: The experiment id for which to retrieve the list of CSV files
+        :param run_id: The run id for which to retrieve the list of CSV files
+        """
+        csv_files = self.__get_available_CSV_files(exp_id)
+
+        table = Texttable()
+        table.header(['File', 'Size'])
+        table.set_cols_align(['l', 'r'])
+
+        sorted_runs = sorted(csv_files.keys())
+        if not 0 <= run_id < len(sorted_runs):
+            raise Exception('Could not find run %i, %i runs were found' %
+                            (run_id, len(sorted_runs)))
+
+        for csv_file in csv_files[sorted_runs[run_id]].values():
+            table.add_row([csv_file['name'], csv_file['size']])
+
+        logger.info('Run %i list of files.', run_id)
+        print table.draw()
+
+    def print_last_run_csv_files(self, exp_id):
+        """
+        Prints the list of CSV files for the last run
+
+        :param exp_id: The experiment id for which to retrieve the list of CSV files
+        """
+        csv_files = self.__get_available_CSV_files(exp_id)
+
+        table = Texttable()
+        table.header(['File', 'Size'])
+        table.set_cols_align(['l', 'r'])
+
+        sorted_runs = sorted(csv_files.keys())
+        if not sorted_runs:
+            raise Exception('Could not find any run')
+
+        for csv_file in csv_files[sorted_runs[-1]].values():
+            table.add_row([csv_file['name'], csv_file['size']])
+
+        logger.info('Last run list of files')
+        print table.draw()
+
+    def __get_csv_file_content(self, exp_id, file_uuid):
+        """
+        Internal helper method to retrieve a CSV file content
+
+        :param exp_id: The experiment id for which to retrieve the CSV file content
+        :param file_uuid: The file uuid for which to retrieve the content
+        """
+        logger.info('Retrieving CSV file.')
+
+        response = requests.get(self.__config['proxy-services']['experiment-file'] %
+                                    (exp_id, file_uuid),
+                                headers=self.__http_headers)
+
+        if response.status_code != httplib.OK:
+            raise Exception('Error when getting CSV file Status Code: %d. Error: %s'
+                            % (response.status_code, response))
+
+        return response.content
+
+    def get_run_csv_file(self, exp_id, run_id, file_name):
+        """
+        Retrieves a CSV file content
+
+        :param exp_id: The experiment id
+        :param run_id: The run id
+        :param file_uuid: The file uuid
+        """
+        csv_files = self.__get_available_CSV_files(exp_id)
+        sorted_runs = sorted(csv_files.keys())
+        if not 0 <= run_id < len(sorted_runs):
+            raise Exception('Could not find run %i, %i runs were found' %
+                            (run_id, len(sorted_runs)))
+
+        if file_name not in csv_files[sorted_runs[run_id]]:
+            file_names = ', '.join(f['name'] for f in csv_files[sorted_runs[run_id]].values())
+            raise Exception('Could not find file \'%s\' in run %i, available file names are: %s' %
+                            (file_name, run_id, file_names))
+
+        file_uuid = csv_files[sorted_runs[run_id]][file_name]['uuid']
+        return self.__get_csv_file_content(exp_id, file_uuid)
+
+    def get_last_run_csv_file(self, exp_id, file_name):
+        """
+        Retrieves a CSV file content for the last run
+
+        :param exp_id: The experiment id
+        :param file_name: The file name
+        """
+
+        csv_files = self.__get_available_CSV_files(exp_id)
+        sorted_runs = sorted(csv_files.keys())
+        if not sorted_runs:
+            raise Exception('Could not find any run')
+
+        if file_name not in csv_files[sorted_runs[-1]]:
+            file_names = ', '.join(file['name'] for file in csv_files[sorted_runs[-1]].values())
+            raise Exception('Could not find file \'%s\' in last run, available file names are: %s' %
+                            (file_name, file_names))
+
+        file_uuid = csv_files[sorted_runs[-1]][file_name]['uuid']
+        return self.__get_csv_file_content(exp_id, file_uuid)
-- 
GitLab