├── MANIFEST.in ├── docs ├── docs_requirements.txt ├── source │ ├── katportalclient.rst │ ├── index.rst │ └── conf.py └── Makefile ├── katportalclient ├── test │ ├── __init__.py │ └── test_client.py ├── __init__.py └── request.py ├── tox.ini ├── .gitignore ├── setup.cfg ├── README.md ├── LICENSE ├── pyproject.toml ├── examples ├── get_sb_id_with_capture_block.py ├── get_latest_sensor_value.py ├── sensor_subarray_lookup.py ├── example_userlogs_usage.py ├── get_schedule_block_info.py ├── basic_websocket_subscription.py ├── get_sensor_info.py ├── get_sensor_history.py └── get_target_descriptions.py ├── setup.py ├── Jenkinsfile └── CHANGELOG.md /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include *.md -------------------------------------------------------------------------------- /docs/docs_requirements.txt: -------------------------------------------------------------------------------- 1 | sphinx>=1.2.3, <2.0 2 | docutils>=0.12, <1.0 3 | sphinx_rtd_theme>=0.1.5, <1.0 4 | numpydoc>=0.5, <1.0 5 | -------------------------------------------------------------------------------- /katportalclient/test/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2015 SKA South Africa (http://ska.ac.za/) 2 | # BSD license - see COPYING for details 3 | """Root of katportalclient test package.""" 4 | -------------------------------------------------------------------------------- /docs/source/katportalclient.rst: -------------------------------------------------------------------------------- 1 | katportalclient 2 | =============== 3 | 4 | :mod:`client` 5 | ------------- 6 | .. automodule:: katportalclient.client 7 | :members: 8 | :undoc-members: 9 | :show-inheritance: 10 | 11 | :mod:`request` 12 | -------------- 13 | .. automodule:: katportalclient.request 14 | :members: 15 | :undoc-members: 16 | :show-inheritance: 17 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py{27, 36},docs 3 | 4 | [testenv] 5 | passenv = test_flags 6 | commands = 7 | coverage run --source={env:test_flags} -m nose --xunit-file=nosetests_{envname}.xml 8 | coverage xml -o coverage_{envname}.xml 9 | coverage html 10 | coverage report -m --skip-covered 11 | deps = 12 | coverage<5.0.0 # see https://github.com/nedbat/coveragepy/issues/716 13 | mock 14 | nose 15 | nosexcover 16 | 17 | [testenv:docs] 18 | # Python interpreter which will be used for creating the virtual environment. 19 | basepython = python3.6 20 | # whitelisting non-virtualenv commands. 21 | whitelist_externals = make 22 | changedir = docs 23 | commands = make html 24 | deps = 25 | sphinx>=1.2.3, <2.0 26 | docutils>=0.12, <1.0 27 | sphinx_rtd_theme>=0.1.5, <1.0 28 | numpydoc>=0.5, <1.0 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### Based on github gist: https://gist.github.com/octocat/9257657 ### 2 | 3 | # Compiled source # 4 | ################### 5 | *.com 6 | *.class 7 | *.dll 8 | *.exe 9 | *.o 10 | *.so 11 | *.pyc 12 | 13 | # Packages # 14 | ############ 15 | # it's better to unpack these files and commit the raw source 16 | # git has its own built in compression methods 17 | *.7z 18 | *.dmg 19 | *.gz 20 | *.iso 21 | *.jar 22 | *.rar 23 | *.tar 24 | *.zip 25 | *.eggs 26 | *.egg-info 27 | 28 | # Logs and databases # 29 | ###################### 30 | *.log 31 | *.sql 32 | *.sqlite 33 | 34 | # OS generated files # 35 | ###################### 36 | .DS_Store 37 | .DS_Store? 38 | ._* 39 | .Spotlight-V100 40 | .Trashes 41 | ehthumbs.db 42 | Thumbs.db 43 | 44 | # Other # 45 | ######### 46 | version.txt 47 | dump.rdb 48 | build 49 | 50 | # Testing # 51 | ########### 52 | nosetests*.xml 53 | *.coverage 54 | coverage*.xml 55 | .tox 56 | -------------------------------------------------------------------------------- /katportalclient/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2015 SKA South Africa (http://ska.ac.za/) 2 | # BSD license - see COPYING for details 3 | """Root of katportalclient package.""" 4 | from __future__ import absolute_import 5 | 6 | from .client import ( 7 | KATPortalClient, ScheduleBlockNotFoundError, SensorNotFoundError, 8 | SensorHistoryRequestError, ScheduleBlockTargetsParsingError, 9 | SubarrayNumberUnknown, SensorLookupError, InvalidResponseError, 10 | create_jwt_login_token, SensorSample, SensorSampleValueTime) 11 | from .request import JSONRPCRequest 12 | 13 | # BEGIN VERSION CHECK 14 | # Get package version when locally imported from repo or via -e develop install 15 | try: 16 | import katversion as _katversion 17 | except ImportError: # pragma: no cover 18 | import time as _time 19 | __version__ = "0.0+unknown.{}".format(_time.strftime('%Y%m%d%H%M')) 20 | else: # pragma: no cover 21 | __version__ = _katversion.get_version(__path__[0]) 22 | # END VERSION CHECK 23 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [nosetests] 2 | verbosity = 3 3 | detailed-errors = 1 4 | with-xunit = 1 5 | 6 | [flake8] 7 | max-line-length = 90 8 | max-doc-length = 90 9 | docstring-convention = numpy 10 | statistics = True 11 | show-source = True 12 | ; Print the total number of errors. 13 | count = True 14 | ; max-complexity = 10 15 | ; List of checks to ignore 16 | ; D401 First line should be in imperative mood; try rephrasing 17 | ; W503: line break before binary operator (This is incompartible with Black, 18 | ; See: https://black.readthedocs.io/en/stable/the_black_code_style.html#line-breaks-binary-operators) 19 | ignore = D401,W503 20 | exclude = *.egg/*,build,dist,__pycache__,.mypy_cache,.pytest_cache,__init__.py,docs/*.py 21 | ; C errors are not selected by default, so add them to your selection 22 | select = B,C,D,E,F,I,W 23 | 24 | [pydocstyle] 25 | convention = numpy 26 | match = .*\.py 27 | inherit = false 28 | ; List of checks to ignore 29 | ; D401: First line should be in imperative mood. According to https://github.com/google/styleguide/blob/gh-pages/pyguide.md this rule is disabled. 30 | add-ignore = D401 31 | -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | Welcome to the katportalclient's documentation 2 | ========================================== 3 | 4 | A client for simple access to **katportal**, via websocket and HTTP connections. 5 | The HTTP methods allow once-off requests, like the current list of schedule blocks. 6 | For continuous updates, use the Pub/Sub methods, which work over a websocket. 7 | 8 | Dependencies 9 | ------------ 10 | Details can be found in `setup.py` but basically it is only: 11 | 12 | - `katversion `_ 13 | - `tornado `_ is used as the web framework and for its asynchronous functionality. 14 | 15 | **Note:** ``setup.py`` depends on ``katversion``, so make sure that is installed before 16 | installing the package. 17 | 18 | Install 19 | ------- 20 | 21 | ``pip install katportalclient`` 22 | 23 | Example usage 24 | ------------- 25 | 26 | See the `examples` folder for code that demonstrates some usage scenarios. 27 | 28 | Contents 29 | -------- 30 | 31 | .. toctree:: 32 | :maxdepth: 1 33 | 34 | Core API 35 | 36 | 37 | Indices and tables 38 | ------------------ 39 | 40 | * :ref:`genindex` 41 | * :ref:`modindex` 42 | * :ref:`search` 43 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | katportalclient 2 | =============== 3 | 4 | [![Doc Status](https://readthedocs.org/projects/katportalclient/badge/?version=latest)](http://katportalclient.readthedocs.io/en/latest) 5 | [![PyPI Version](https://img.shields.io/pypi/v/katportalclient.svg)](https://pypi.python.org/pypi/katportalclient) 6 | [![Python Versions](https://img.shields.io/pypi/pyversions/katportalclient.svg)](https://pypi.python.org/pypi/katportalclient/) 7 | 8 | A client for simple access to **katportal**, via websocket and HTTP connections. 9 | The HTTP methods allow once-off requests, like the current list of schedule blocks. 10 | For continuous updates, use the Pub/Sub methods, which work over a websocket. 11 | 12 | Dependencies 13 | ------------ 14 | Details can be found in `setup.py` but basically it is only: 15 | 16 | - [katversion](https://pypi.org/project/katversion/) 17 | - [tornado](http://www.tornadoweb.org) is used as the web framework and for its asynchronous functionality. 18 | 19 | **Note:** `setup.py` depends on katversion, so make sure that is installed before installing the package. 20 | 21 | Install 22 | ------- 23 | 24 | ```bash 25 | pip install katportalclient 26 | ``` 27 | 28 | Example usage 29 | ------------- 30 | 31 | See the `examples` folder for code that demonstrates some usage scenarios. 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 SKA South Africa 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, 8 | this list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | * Neither the name of the SKA South Africa project nor the names of its 15 | contributors may be used to endorse or promote products derived from this 16 | software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 19 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 20 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 22 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 23 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 24 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 25 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 26 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | # Project configuration file 2 | 3 | # NOTE: you have to use single-quoted strings in TOML for regular expressions. 4 | # It's the equivalent of r-strings in Python. Multiline strings are treated as 5 | # verbose regular expressions by Black. Use [ ] to denote a significant space 6 | # character. 7 | 8 | [tool.black] 9 | # See: https://docs.google.com/document/d/1aZoIyR9tz5rCWr2qJKuMTmKp2IzHlFjrCFrpDDHFypM/edit#heading=h.95alkrtwtub0 10 | line-length = 90 11 | target-version = ['py27', 'py36'] 12 | include = '\.pyi?$' 13 | exclude = ''' 14 | /( 15 | \.eggs 16 | | \.git 17 | | \.__pycache__ 18 | | \.mypy_cache 19 | | \.tox 20 | | \.venv 21 | | \.env 22 | | _build 23 | | buck-out 24 | | build 25 | | dist 26 | )/ 27 | ''' 28 | 29 | [tool.isort] 30 | force_grid_wrap = 0 31 | include_trailing_comma = true 32 | indent = 4 33 | line_length = 90 34 | multi_line_output = 3 35 | # Having imports glued together is physically painful. ;) 36 | lines_between_types = 1 37 | # Imports sections 38 | sections = ['FUTURE', 'STDLIB', 'THIRDPARTY', 'FIRSTPARTY', 'LOCALFOLDER'] 39 | # A list of known imports that will be forced to display within their specified category. 40 | known_first_party = [ 41 | 'astrokat', 'katcapture', 'katcomp', 'katconf', 'katcore', 'katcorelib', 'katcp', 42 | 'katdeploy', 'katgui', 'katlogger', 'katmisc', 'katnodeman', 'katobs', 'katopc', 43 | 'katpoint', 'katportal', 'katportalclient', 'katproxy', 'katscripts', 'katsdisp', 44 | 'katsdpcatalogues', 'katsdpcontroller', 'katsdpscripts', 'katsdpservices', 45 | 'katsdptelstate', 'katsim', 'katuilib', 'katusescripts', 'katversion', 'mkat_tango', 'mkatstore', 46 | 'nosekatreport', 'pylibmodbus', 'spead', 'tango_simlib' 47 | ] 48 | -------------------------------------------------------------------------------- /katportalclient/request.py: -------------------------------------------------------------------------------- 1 | # Copyright 2015 SKA South Africa (http://ska.ac.za/) 2 | # BSD license - see COPYING for details 3 | """Module defining the JSON-RPC request class used by websocket client.""" 4 | 5 | import uuid 6 | 7 | import ujson as json 8 | 9 | from builtins import object, str 10 | 11 | 12 | class JSONRPCRequest(object): 13 | """ 14 | Class with structure following the JSON-RPC standard. 15 | 16 | Parameters 17 | ---------- 18 | method: str 19 | Name of the remote procedure to call. 20 | params: list 21 | List of parameters to be used for the remote procedure call. 22 | """ 23 | 24 | id = '' 25 | method = '' 26 | params = None 27 | 28 | def __init__(self, method, params): 29 | self.jsonrpc = '2.0' 30 | self.id = str(uuid.uuid4().hex[:10]) 31 | self.method = method 32 | self.params = params 33 | 34 | def __call__(self): 35 | """Return object's attribute dictionary in JSON form.""" 36 | return json.dumps(self.__dict__) 37 | 38 | def __repr__(self): 39 | """Return a human readable string of the object""" 40 | return "{_class}: id: {_id}, method: {method}, params: {params}".format( 41 | _class=self.__class__, 42 | _id=self.id, 43 | method=self.method, 44 | params=self.params) 45 | 46 | def method_and_params_hash(self): 47 | """Return a hash for the methods and params attributes for easy comparison""" 48 | # cast self.params to string because we can only hash immutable objects 49 | # if params is a list or set or anything like that, this will raise a 50 | # TypeError 51 | return hash((self.method, str(self.params))) 52 | -------------------------------------------------------------------------------- /examples/get_sb_id_with_capture_block.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Copyright (c) 2019 National Research Foundation (South African Radio Astronomy Observatory) 3 | # BSD license - see LICENSE for details 4 | """Simple example demonstrating queries of schedule block IDs using 5 | a given valid capture block ID. 6 | """ 7 | from __future__ import print_function 8 | 9 | import logging 10 | import argparse 11 | 12 | import tornado.gen 13 | 14 | from katportalclient import KATPortalClient 15 | 16 | 17 | logger = logging.getLogger('katportalclient.example') 18 | logger.setLevel(logging.INFO) 19 | 20 | 21 | @tornado.gen.coroutine 22 | def main(): 23 | # Change URL to point to a valid portal node. 24 | portal_client = KATPortalClient('http://{}/api/client'. 25 | format(args.host), 26 | on_update_callback=None, logger=logger) 27 | 28 | for capture_block_id in args.capture_block_ids: 29 | schedule_blocks = yield portal_client.sb_ids_by_capture_block(capture_block_id) 30 | print("\nSchedule block ID(s) for the Capture block ID {}:\n{}\n".format(capture_block_id, schedule_blocks)) 31 | # ./get_sb_id_with_capture_block.py --host portal.mkat.karoo.kat.ac.za 1556745649 32 | # Example output: 33 | # Schedule block IDs for Capture block ID 1556745649: 34 | # [u'20190501-0001'] 35 | 36 | 37 | if __name__ == '__main__': 38 | parser = argparse.ArgumentParser( 39 | description="Download schedule block ID of a given capture block ID and print to stdout.") 40 | parser.add_argument( 41 | '--host', 42 | default='127.0.0.1', 43 | help="hostname or IP of the portal server (default: %(default)s).") 44 | parser.add_argument( 45 | 'capture_block_ids', 46 | metavar='capture-block-id', 47 | nargs='+', 48 | help="capture block ID used to request associated schedule block ID(s).") 49 | parser.add_argument( 50 | '-v', '--verbose', 51 | dest='verbose', action="store_true", 52 | default=False, 53 | help="provide extremely verbose output.") 54 | args = parser.parse_args() 55 | if args.verbose: 56 | logger.setLevel(logging.DEBUG) 57 | else: 58 | logger.setLevel(logging.WARNING) 59 | 60 | # Start up the tornado IO loop. 61 | # Only a single function to run once, so use run_sync() instead of start() 62 | io_loop = tornado.ioloop.IOLoop.current() 63 | io_loop.run_sync(main) 64 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Copyright (c) 2015 National Research Foundation (South African Radio Astronomy Observatory) 3 | # BSD license - see LICENSE for details 4 | 5 | from os import path 6 | from setuptools import setup, find_packages 7 | from sys import version_info 8 | 9 | PYTHON2 = (2,) <= version_info < (3,) 10 | 11 | this_directory = path.abspath(path.dirname(__file__)) 12 | 13 | files = {'Readme': 'README.md', 'Changelog': 'CHANGELOG.md'} 14 | 15 | long_description = "" 16 | for name, filename in files.items(): 17 | if name != 'Readme': 18 | long_description += "# {}\n".format(name) 19 | open_kwargs = {'encoding': 'utf8'} if not PYTHON2 else {} 20 | with open(path.join(this_directory, filename), **open_kwargs) as _f: 21 | file_contents = _f.read() 22 | long_description += file_contents + "\n\n" 23 | 24 | setup( 25 | name="katportalclient", 26 | description="A client for katportal.", 27 | long_description=long_description, 28 | long_description_content_type='text/markdown', 29 | author="MeerKAT CAM Team", 30 | author_email="cam@ska.ac.za", 31 | packages=find_packages(), 32 | include_package_data=True, 33 | scripts=[], 34 | url='https://github.com/ska-sa/katportalclient', 35 | classifiers=[ 36 | "Development Status :: 4 - Beta", 37 | "Intended Audience :: Developers", 38 | "License :: OSI Approved :: BSD License", 39 | "Operating System :: OS Independent", 40 | "Programming Language :: Python", 41 | "Programming Language :: Python :: 2", 42 | "Programming Language :: Python :: 2.7", 43 | "Programming Language :: Python :: 3", 44 | "Programming Language :: Python :: 3.3", 45 | "Programming Language :: Python :: 3.4", 46 | "Programming Language :: Python :: 3.5", 47 | "Programming Language :: Python :: 3.6", 48 | "Programming Language :: Python :: 3.7", 49 | "Topic :: Software Development :: Libraries :: Python Modules", 50 | "Topic :: Scientific/Engineering :: Astronomy" 51 | ], 52 | platforms=["OS Independent"], 53 | keywords="meerkat kat ska", 54 | python_requires=">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, <4", 55 | setup_requires=["katversion"], 56 | use_katversion=True, 57 | install_requires=[ 58 | "future", 59 | "futures; python_version<'3'", 60 | "tornado>=4.0, <7.0; python_version>='3'", 61 | "tornado>=4.0, <5.0; python_version<'3'", 62 | 'ujson==2.0.3; python_version<"3"', 63 | 'ujson==4.3.0; python_version>"3"', 64 | ], 65 | zip_safe=False, 66 | test_suite="nose.collector", 67 | ) 68 | -------------------------------------------------------------------------------- /examples/get_latest_sensor_value.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Copyright (c) 2018 National Research Foundation (South African Radio Astronomy Observatory) 3 | # BSD license - see LICENSE for details 4 | """Simple example demonstrating retrieving the latest sensor value. 5 | 6 | This example gets lists of sensor names, then returns the 7 | latest value of each specific sensor. It uses HTTP access to katportal. 8 | """ 9 | from __future__ import print_function 10 | 11 | import logging 12 | import argparse 13 | 14 | import tornado.gen 15 | 16 | from katportalclient import KATPortalClient 17 | from katportalclient.client import SensorNotFoundError 18 | 19 | 20 | logger = logging.getLogger('katportalclient.example') 21 | logger.setLevel(logging.INFO) 22 | 23 | 24 | @tornado.gen.coroutine 25 | def main(): 26 | # Change URL to point to a valid portal node. 27 | portal_client = KATPortalClient('http://{}/api/client'.format(args.host), 28 | on_update_callback=None, logger=logger) 29 | 30 | # Get the names of sensors matching the patterns 31 | # See examples/get_sensor_info.py for details on sensor name pattern matching 32 | sensor_names = yield portal_client.sensor_names(args.sensors) 33 | print("\nMatching sensor names for pattern {}: {}".format(args.sensors, sensor_names)) 34 | 35 | # Fetch the readings for the sensors found. 36 | if len(sensor_names) == 0: 37 | print("No matching sensors found!") 38 | else: 39 | for sensor_name in sensor_names: 40 | try: 41 | sensor_value = yield portal_client.sensor_value(sensor_name, 42 | include_value_ts=True) 43 | except SensorNotFoundError as exc: 44 | print("\n", exc) 45 | continue 46 | print("\nValue for sensor {}:".format(sensor_name)) 47 | print(sensor_value) 48 | 49 | 50 | if __name__ == '__main__': 51 | parser = argparse.ArgumentParser( 52 | description="Download latest sensor value and print to stdout.") 53 | parser.add_argument( 54 | '--host', 55 | default='127.0.0.1', 56 | help="hostname or IP of the portal server (default: %(default)s).") 57 | parser.add_argument( 58 | 'sensors', 59 | metavar='sensor', 60 | nargs='+', 61 | help="list of sensor names or filter strings to request data for") 62 | parser.add_argument( 63 | '-v', '--verbose', 64 | dest='verbose', action="store_true", 65 | default=False, 66 | help="provide extremely verbose output.") 67 | args = parser.parse_args() 68 | if args.verbose: 69 | logger.setLevel(logging.DEBUG) 70 | else: 71 | logger.setLevel(logging.WARNING) 72 | 73 | # Start up the tornado IO loop. 74 | # Only a single function to run once, so use run_sync() instead of start() 75 | io_loop = tornado.ioloop.IOLoop.current() 76 | io_loop.run_sync(main) 77 | -------------------------------------------------------------------------------- /examples/sensor_subarray_lookup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Copyright (c) 2017 National Research Foundation (South African Radio Astronomy Observatory) 3 | # BSD license - see LICENSE for details 4 | """Simple example demonstrating the use of the sensor_subarray_lookup method. 5 | This method gets the full sensor name based on a generic component and sensor 6 | name, for a given subarray. This method will return a failed katcp response if 7 | the given subarray is not in the 'active' or 'initialising' state. 8 | 9 | This example uses HTTP access to katportal, not websocket access. 10 | 11 | """ 12 | from __future__ import print_function 13 | 14 | import logging 15 | import argparse 16 | 17 | import tornado.gen 18 | 19 | from katportalclient import KATPortalClient, SensorLookupError 20 | 21 | logger = logging.getLogger('katportalclient.example') 22 | logger.setLevel(logging.INFO) 23 | 24 | 25 | @tornado.gen.coroutine 26 | def main(): 27 | # Change URL to point to a valid portal node. Subarray can be 1 to 4. 28 | # Note: if on_update_callback is set to None, then we cannot use the 29 | # KATPortalClient.connect() method (i.e. no websocket access). 30 | portal_client = KATPortalClient( 31 | 'http://{host}/api/client/{sub_nr}'.format(**vars(args)), 32 | on_update_callback=None, 33 | logger=logger) 34 | 35 | lookup_args = vars(args) 36 | try: 37 | name = yield portal_client.sensor_subarray_lookup( 38 | component=lookup_args['component'], 39 | sensor=lookup_args['sensor'], 40 | return_katcp_name=lookup_args['return_katcp_name']) 41 | print("Lookup result: ", name) 42 | except SensorLookupError as exc: 43 | print("Lookup failed!", exc) 44 | 45 | 46 | if __name__ == '__main__': 47 | parser = argparse.ArgumentParser( 48 | description="Returns a full sensor name based on a generic component " 49 | "and sensor.") 50 | parser.add_argument( 51 | '--host', 52 | default='127.0.0.1', 53 | help="hostname or IP of the portal server (default: %(default)s).") 54 | parser.add_argument( 55 | '-n', 56 | '--sub-nr', 57 | dest='sub_nr', 58 | default='1', 59 | help="The subarray that the component is assigned to.") 60 | parser.add_argument( 61 | '-c', 62 | '--component', 63 | dest='component', 64 | help="Component containing the sensor to be looked up.") 65 | parser.add_argument( 66 | '-s', '--sensor', dest='sensor', help="The sensor to be looked up.") 67 | parser.add_argument( 68 | '-k', 69 | '--return-katcp-name', 70 | default=False, 71 | dest='return_katcp_name', 72 | action='store_true', 73 | help="Whether to return the katcp name or the Python normalised name.") 74 | parser.add_argument( 75 | '-v', 76 | '--verbose', 77 | dest='verbose', 78 | action="store_true", 79 | default=False, 80 | help="provide extremely verbose output.") 81 | args = parser.parse_args() 82 | 83 | if args.verbose: 84 | logger.setLevel(logging.DEBUG) 85 | else: 86 | logger.setLevel(logging.WARNING) 87 | # Start up the tornado IO loop. 88 | # Only a single function to run once, so use run_sync() instead of start() 89 | io_loop = tornado.ioloop.IOLoop.current() 90 | io_loop.run_sync(main) 91 | -------------------------------------------------------------------------------- /examples/example_userlogs_usage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Copyright (c) 2017 National Research Foundation (South African Radio Astronomy Observatory) 3 | # BSD license - see LICENSE for details 4 | """Simple example demonstrating userlogs queries. 5 | 6 | This example gets lists of tags and userlogs in various ways. 7 | It uses HTTP access to katportal. 8 | """ 9 | from __future__ import print_function 10 | 11 | import time 12 | import logging 13 | import argparse 14 | 15 | import tornado.gen 16 | 17 | from katportalclient import KATPortalClient 18 | 19 | 20 | logger = logging.getLogger('katportalclient.example') 21 | logger.setLevel(logging.INFO) 22 | 23 | 24 | @tornado.gen.coroutine 25 | def main(): 26 | # Change URL to point to a valid portal node. 27 | # If you are not interested in any subarray specific information 28 | # (e.g. schedule blocks), then the number can be omitted, as below. 29 | # Note: if on_update_callback is set to None, then we cannot use the 30 | # KATPortalClient.connect() method (i.e. no websocket access). 31 | portal_client = KATPortalClient('http://{}/api/client'.format(args.host), 32 | on_update_callback=None, logger=logger) 33 | 34 | # Login so that we know which user to create userlogs for! 35 | yield portal_client.login(username="user@example.com", password="password") 36 | tags = yield portal_client.userlog_tags() 37 | userlogs = yield portal_client.userlogs() 38 | 39 | print("There are %s userlog tags." % len(tags)) 40 | print("==============================") 41 | print("Here is a list of userlogs for today:") 42 | print(userlogs) 43 | 44 | # To create an userlog use the following code 45 | # To add tags, make an array of tag id's 46 | userlog_tags_to_add = [tags[0].get('id'), tags[1].get('id')] 47 | userlog_content = "This is where you would put the content of the userlog!" 48 | # Start time and end times needs to be in this format 'YYYY-MM-DD HH:mm:ss' 49 | # All times are in UTC 50 | start_time = time.strftime('%Y-%m-%d 00:00:00') 51 | end_time = time.strftime('%Y-%m-%d 23:59:59') 52 | 53 | userlog_created = yield portal_client.create_userlog( 54 | content=userlog_content, 55 | tag_ids=userlog_tags_to_add, 56 | start_time=start_time, 57 | end_time=end_time) 58 | 59 | print("==============================") 60 | print("Created a userlog! This is the new userlog: ") 61 | print(userlog_created) 62 | print("==============================") 63 | 64 | # To edit an existing userlog, user modify_userlog with the modified userlog 65 | # Here we are modifying the userlog we created using create_userlog 66 | userlog_created['content'] = 'This content is edited by katportalclient!' 67 | userlog_created['end_time'] = userlog_created['start_time'] 68 | result = yield portal_client.modify_userlog(userlog_created) 69 | print("==============================") 70 | print("Edited userlog! Result: ") 71 | print(result) 72 | 73 | # Remember to logout when you are done! 74 | print("==============================") 75 | print("Logging out!") 76 | yield portal_client.logout() 77 | 78 | 79 | if __name__ == '__main__': 80 | parser = argparse.ArgumentParser( 81 | description="Download userlogs and tags and print to stdout.") 82 | parser.add_argument( 83 | '--host', 84 | default='127.0.0.1', 85 | help="hostname or IP of the portal server (default: %(default)s).") 86 | parser.add_argument( 87 | '-v', '--verbose', 88 | dest='verbose', action="store_true", 89 | default=False, 90 | help="provide extremely verbose output.") 91 | args = parser.parse_args() 92 | if args.verbose: 93 | logger.setLevel(logging.DEBUG) 94 | else: 95 | logger.setLevel(logging.WARNING) 96 | 97 | # Start up the tornado IO loop. 98 | # Only a single function to run once, so use run_sync() instead of start() 99 | io_loop = tornado.ioloop.IOLoop.current() 100 | io_loop.run_sync(main) 101 | -------------------------------------------------------------------------------- /Jenkinsfile: -------------------------------------------------------------------------------- 1 | pipeline { 2 | 3 | agent { 4 | label 'cambase_bionic' 5 | } 6 | 7 | environment { 8 | KATPACKAGE = "${(env.JOB_NAME - env.JOB_BASE_NAME) - '-multibranch/'}" 9 | } 10 | 11 | stages { 12 | stage ('Checkout SCM') { 13 | steps { 14 | checkout([ 15 | $class: 'GitSCM', 16 | branches: [[name: "refs/heads/${env.BRANCH_NAME}"]], 17 | extensions: [[$class: 'LocalBranch']], 18 | userRemoteConfigs: scm.userRemoteConfigs, 19 | doGenerateSubmoduleConfigurations: false, 20 | submoduleCfg: [] 21 | ]) 22 | } 23 | } 24 | 25 | stage ('Static analysis') { 26 | steps { 27 | sh "pylint ./${KATPACKAGE} --output-format=parseable --exit-zero > pylint.out" 28 | sh "lint_diff.sh -r ${KATPACKAGE}" 29 | } 30 | 31 | post { 32 | always { 33 | recordIssues(tool: pyLint(pattern: 'pylint.out')) 34 | } 35 | } 36 | } 37 | 38 | stage ('Install & Unit Tests') { 39 | options { 40 | timestamps() 41 | timeout(time: 30, unit: 'MINUTES') 42 | } 43 | 44 | environment { 45 | test_flags = "${KATPACKAGE}" 46 | } 47 | 48 | parallel { 49 | stage ('py27') { 50 | steps { 51 | echo "Running nosetests on Python 2.7" 52 | sh 'tox -e py27' 53 | } 54 | } 55 | 56 | stage ('py36') { 57 | steps { 58 | echo "Running nosetests on Python 3.6" 59 | sh 'tox -e py36' 60 | } 61 | } 62 | stage ('Generate documentation.') { 63 | options { 64 | timestamps() 65 | timeout(time: 30, unit: 'MINUTES') 66 | } 67 | 68 | steps { 69 | echo "Generating Sphinx documentation." 70 | sh 'tox -e docs' 71 | } 72 | } 73 | } 74 | 75 | post { 76 | always { 77 | junit 'nosetests_*.xml' 78 | cobertura ( 79 | coberturaReportFile: 'coverage_*.xml', 80 | failNoReports: true, 81 | failUnhealthy: true, 82 | failUnstable: true, 83 | autoUpdateHealth: true, 84 | autoUpdateStability: true, 85 | zoomCoverageChart: true, 86 | // Ideally test coverage should be > 80% 87 | /* 88 | lineCoverageTargets: '80, 80, 80', 89 | conditionalCoverageTargets: '80, 80, 80', 90 | classCoverageTargets: '80, 80, 80', 91 | fileCoverageTargets: '80, 80, 80', 92 | */ 93 | ) 94 | archiveArtifacts '*.xml' 95 | } 96 | } 97 | } 98 | 99 | stage ('Build & publish packages') { 100 | when { 101 | branch 'master' 102 | } 103 | 104 | steps { 105 | sh 'fpm -s python -t deb .' 106 | sh 'python setup.py bdist_wheel' 107 | sh 'mv *.deb dist/' 108 | archiveArtifacts 'dist/*' 109 | 110 | // Trigger downstream publish job 111 | build job: 'ci.publish-artifacts', parameters: [ 112 | string(name: 'job_name', value: "${env.JOB_NAME}"), 113 | string(name: 'build_number', value: "${env.BUILD_NUMBER}")] 114 | } 115 | } 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /examples/get_schedule_block_info.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Copyright (c) 2016 National Research Foundation (South African Radio Astronomy Observatory) 3 | # BSD license - see LICENSE for details 4 | """Simple example demonstrating schedule block information queries. 5 | 6 | This example uses HTTP access to katportal, not websocket access. It uses a 7 | specific subarray when initialising the KATPortalClient, as schedule blocks 8 | are assigned to specific subarrays. 9 | """ 10 | from __future__ import print_function 11 | 12 | import logging 13 | import argparse 14 | 15 | import tornado.gen 16 | 17 | from katportalclient import KATPortalClient 18 | 19 | 20 | logger = logging.getLogger('katportalclient.example') 21 | logger.setLevel(logging.INFO) 22 | 23 | 24 | @tornado.gen.coroutine 25 | def main(): 26 | # Change URL to point to a valid portal node. Subarray can be 1 to 4. 27 | # Note: if on_update_callback is set to None, then we cannot use the 28 | # KATPortalClient.connect() method (i.e. no websocket access). 29 | portal_client = KATPortalClient('http://{}/api/client/{}'. 30 | format(args.host, args.sub_nr), 31 | on_update_callback=None, logger=logger) 32 | 33 | # Get the IDs of schedule blocks assigned to the subarray specified above. 34 | sb_ids = yield portal_client.schedule_blocks_assigned() 35 | print("\nSchedule block IDs on subarray {}\n{}".format(args.sub_nr, sb_ids)) 36 | # Example output: 37 | # Schedule block IDs on subarray 1: 38 | # [u'20161010-0001', u'20161010-0002', u'20161010-0003'] 39 | 40 | # Fetch the details for one of the schedule blocks found. 41 | if len(sb_ids) > 0: 42 | sb_detail = yield portal_client.schedule_block_detail(sb_ids[0]) 43 | print("\nDetail for SB {}:\n{}\n".format(sb_ids[0], sb_detail)) 44 | # Example output: 45 | # Detail for SB 20161010-0001: 46 | # {u'antennas_dry_run_alloc': u'm011', 47 | # u'id_code': u'20170208-0001', 48 | # u'obs_readiness': u'READY_TO_EXECUTE', 49 | # u'owner': u'CAM', 50 | # u'actual_end_time': None, 51 | # u'antennas_alloc': None, 52 | # u'instruction_set': u'run-obs-script ~/scripts/cam/basic-script.py ' 53 | # u'-t 20 -m 360 --proposal-id=CAM_AQF ' 54 | # u'--program-block-id=CAM_basic_script', 55 | # u'controlled_resources_alloc': None, 56 | # u'targets': u'[{"track_start_offset":73.0233476162,"target":"PKS 0023-26 ' 57 | # u'| J0025-2602 | OB-238, radec, 0:25:49.16, -26:02:12.6, ' 58 | # u'(1410.0 8400.0 -1.694 2.107 -0.4043)","track_duration":20.0}]', 59 | # u'scheduled_time': u'2017-02-08 11:53:34.000Z', 60 | # u'lead_operator_priority': None, 61 | # u'id': 156, 62 | # u'antenna_spec': u'm011', 63 | # u'state': u'SCHEDULED', 64 | # u'config_label': u'', 65 | # u'pb_id': None, 66 | # u'type': u'OBSERVATION', 67 | # u'actual_start_time': None, 68 | # u'description': u'Track for m011', 69 | # u'verification_state': u'VERIFIED', 70 | # u'sb_order': None, 71 | # u'sub_nr': 1, 72 | # u'desired_start_time': None, 73 | # u'sb_sequence': None, 74 | # u'expected_duration_seconds': 400, 75 | # u'action_time': u'2017-02-08 11:53:34.000Z', 76 | # u'controlled_resources_spec': u'', 77 | # u'controlled_resources_dry_run_alloc': u'', 78 | # u'notes': u'(Cloned from 20170123-0017) ', 79 | # u'outcome': u'UNKNOWN', 80 | # u'data_quality': None} 81 | 82 | 83 | if __name__ == '__main__': 84 | parser = argparse.ArgumentParser( 85 | description="Download schedule block info for a subarray and print to stdout.") 86 | parser.add_argument( 87 | '--host', 88 | default='127.0.0.1', 89 | help="hostname or IP of the portal server (default: %(default)s).") 90 | parser.add_argument( 91 | '-s', '--sub_nr', 92 | default='1', 93 | type=int, 94 | help="subarray number (1, 2, 3, or 4) to request schedule for " 95 | "(default: %(default)s).") 96 | parser.add_argument( 97 | '-v', '--verbose', 98 | dest='verbose', action="store_true", 99 | default=False, 100 | help="provide extremely verbose output.") 101 | args = parser.parse_args() 102 | if args.verbose: 103 | logger.setLevel(logging.DEBUG) 104 | else: 105 | logger.setLevel(logging.WARNING) 106 | 107 | # Start up the tornado IO loop. 108 | # Only a single function to run once, so use run_sync() instead of start() 109 | io_loop = tornado.ioloop.IOLoop.current() 110 | io_loop.run_sync(main) 111 | -------------------------------------------------------------------------------- /examples/basic_websocket_subscription.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Copyright (c) 2016 National Research Foundation (South African Radio Astronomy Observatory) 3 | # BSD license - see LICENSE for details 4 | """Simple example demonstrating websocket subscription. 5 | 6 | This example connects to katportal via a websocket. It subscribes to 7 | the specified sensor group names, and keeps printing out the published messages 8 | received every few seconds. 9 | """ 10 | from __future__ import print_function 11 | 12 | import argparse 13 | import logging 14 | import uuid 15 | 16 | import tornado.gen 17 | 18 | from builtins import str 19 | 20 | from katportalclient import KATPortalClient 21 | 22 | 23 | logger = logging.getLogger('katportalclient.example') 24 | logger.setLevel(logging.INFO) 25 | 26 | 27 | def on_update_callback(msg_dict): 28 | """Handler for every JSON message published over the websocket.""" 29 | print("GOT message:") 30 | for key, value in list(msg_dict.items()): 31 | if key == 'msg_data': 32 | print('\tmsg_data:') 33 | for data_key, data_value in list(msg_dict['msg_data'].items()): 34 | print("\t\t{}: {}".format(data_key, data_value)) 35 | else: 36 | print("\t{}: {}".format(key, value)) 37 | 38 | 39 | @tornado.gen.coroutine 40 | def main(): 41 | # Change URL to point to a valid portal node. 42 | # If you are not interested in any subarray specific information 43 | # (e.g. schedule blocks), then the number can be omitted, as below. 44 | portal_client = KATPortalClient('http://{}/api/client'.format(args.host), 45 | on_update_callback, logger=logger) 46 | 47 | # First connect to the websocket, before subscribing. 48 | yield portal_client.connect() 49 | 50 | # Use a namespace with a unique name when subscribing to avoid a 51 | # clash with existing namespaces. 52 | namespace = 'namespace_' + str(uuid.uuid4()) 53 | 54 | # Subscribe to the namespace (async call) - no messages will be received yet, 55 | # as this is a new namespace. 56 | result = yield portal_client.subscribe(namespace) 57 | print("Subscription result: {} identifier(s).".format(result)) 58 | 59 | # Set the sampling strategies for the sensors of interest, on our custom 60 | # namespace. In this example, we are interested in a number of patterns, 61 | # e.g. any sensor with "mode" in the name. The response messages will 62 | # be published to our namespace every 5 seconds. 63 | result = yield portal_client.set_sampling_strategies( 64 | namespace, args.sensors, 65 | 'period 5.0') 66 | print("\nSet sampling strategies result: {}.\n".format(result)) 67 | 68 | # Example: 69 | # ./basic_websocket_subscription.py --host 127.0.0.1 pos_actual_pointm anc_mean_wind 70 | # Subscription result: 1 identifier(s). 71 | # GOT message: 72 | # msg_pattern: namespace_93f3e645-1818-4913-ae13-f4fbc6eacf31:* 73 | # msg_channel: namespace_93f3e645-1818-4913-ae13-f4fbc6eacf31:m011_pos_actual_pointm_azim 74 | # msg_data: 75 | # status: nominal 76 | # timestamp: 1486038182.71 77 | # value: -185.0 78 | # name: m011_pos_actual_pointm_azim 79 | # received_timestamp: 1486050055.89 80 | # GOT message: 81 | # msg_pattern: namespace_93f3e645-1818-4913-ae13-f4fbc6eacf31:* 82 | # msg_channel: namespace_93f3e645-1818-4913-ae13-f4fbc6eacf31:m011_pos_actual_pointm_elev 83 | # msg_data: 84 | # status: nominal 85 | # timestamp: 1486038182.71 86 | # value: 15.0 87 | # name: m011_pos_actual_pointm_elev 88 | # received_timestamp: 1486050055.89 89 | # ... 90 | # GOT message: 91 | # msg_pattern: namespace_93f3e645-1818-4913-ae13-f4fbc6eacf31:* 92 | # msg_channel: namespace_93f3e645-1818-4913-ae13-f4fbc6eacf31:anc_mean_wind_speed 93 | # msg_data: 94 | # status: nominal 95 | # timestamp: 1486050057.07 96 | # value: 4.9882065556 97 | # name: anc_mean_wind_speed 98 | # received_timestamp: 1486050057.13 99 | # ... 100 | # 101 | # The IOLoop will continue to run until the program is aborted. 102 | # Push Ctrl+C to stop. 103 | 104 | 105 | if __name__ == '__main__': 106 | parser = argparse.ArgumentParser( 107 | description="Subscribe to websocket and print messages to stdout for " 108 | "matching sensor names.") 109 | parser.add_argument( 110 | '--host', 111 | default='127.0.0.1', 112 | help="hostname or IP of the portal server (default: %(default)s).") 113 | parser.add_argument( 114 | 'sensors', 115 | metavar='sensor', 116 | nargs='+', 117 | help="list of sensor names or filter strings to request data for " 118 | "(examples: wind_speed azim elev)") 119 | parser.add_argument( 120 | '-v', '--verbose', 121 | dest='verbose', action="store_true", 122 | default=False, 123 | help="provide extremely verbose output.") 124 | args = parser.parse_args() 125 | if args.verbose: 126 | logger.setLevel(logging.DEBUG) 127 | else: 128 | logger.setLevel(logging.WARNING) 129 | 130 | # Start up the tornado IO loop: 131 | io_loop = tornado.ioloop.IOLoop.current() 132 | io_loop.add_callback(main) 133 | io_loop.start() 134 | -------------------------------------------------------------------------------- /examples/get_sensor_info.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Copyright (c) 2016 National Research Foundation (South African Radio Astronomy Observatory) 3 | # BSD license - see LICENSE for details 4 | """Simple example demonstrating sensor information queries. 5 | 6 | This example gets lists of sensor names in various ways, and gets the 7 | detailed atttributes of a specific sensor. It uses HTTP access to katportal, 8 | not websocket access. 9 | """ 10 | from __future__ import print_function 11 | 12 | import logging 13 | import argparse 14 | 15 | import tornado.gen 16 | 17 | from katportalclient import KATPortalClient 18 | 19 | 20 | logger = logging.getLogger('katportalclient.example') 21 | logger.setLevel(logging.INFO) 22 | 23 | 24 | @tornado.gen.coroutine 25 | def main(): 26 | # Change URL to point to a valid portal node. 27 | # If you are not interested in any subarray specific information 28 | # (e.g. schedule blocks), then the number can be omitted, as below. 29 | # Note: if on_update_callback is set to None, then we cannot use the 30 | # KATPortalClient.connect() method (i.e. no websocket access). 31 | portal_client = KATPortalClient('http://{}/api/client'.format(args.host), 32 | on_update_callback=None, logger=logger) 33 | 34 | # Get the names of sensors matching the patterns 35 | sensor_names = yield portal_client.sensor_names(args.sensors) 36 | print("\nMatching sensor names for pattern {}: {}".format(args.sensors, sensor_names)) 37 | 38 | # Get the names of sensors matching a pattern 39 | # pattern = 'anc_w.*_device_status' 40 | # sensor_names = yield portal_client.sensor_names(pattern) 41 | # print "\nMatching sensor names for pattern {} : {}".format(pattern, sensor_names) 42 | # Example output: 43 | # Matching sensor names for pattern ['anc_w.*_device_status']: 44 | # [u'anc_wind_device_status', u'anc_weather_device_status'] 45 | 46 | # Get the names of sensors matching a pattern 47 | # pattern = 'anc_(mean|gust)_wind_speed' 48 | # sensor_names = yield portal_client.sensor_names(pattern) 49 | # print "\nMatching sensor names for pattern {} : {}".format(pattern, sensor_names) 50 | # Example output: 51 | # Matching sensor names for pattern ['anc_(mean|gust)_wind_speed']: 52 | # [u'anc_mean_wind_speed', u'anc_gust_wind_speed'] 53 | 54 | # Get the names of sensors matching a list of patterns 55 | # pattern = 'm01[12]_pos_request_base_azim' 56 | # sensor_names = yield portal_client.sensor_names(pattern) 57 | # print "\nMatching sensor names for pattern {} : {}".format(pattern, sensor_names) 58 | # Example output (if sensors is 'm01[12]_pos_request_base_azim'): 59 | # Matching sensor names for pattern ['m01[12]_pos_request_base_azim']: 60 | # [u'm011_pos_request_base_azim', u'm011_pos_request_base_azim'] 61 | 62 | # Fetch the details for the sensors found. 63 | if len(sensor_names) == 0: 64 | print("No matching sensors found!") 65 | else: 66 | for sensor_name in sensor_names: 67 | sensor_detail = yield portal_client.sensor_detail(sensor_name) 68 | print("\nDetail for sensor {}:".format(sensor_name)) 69 | for key in sorted(sensor_detail): 70 | print(" {}: {}".format(key, sensor_detail[key])) 71 | # Example output: 72 | # Detail for sensor m011_pos_request_base_azim: 73 | # component: m011 74 | # description: Requested target azimuth 75 | # katcp_name: m011.pos.request-base-azim 76 | # name: m011_pos_request_base_azim 77 | # params: [-195.0, 370.0] 78 | # site: deva 79 | # systype: mkat 80 | # type: float 81 | # units: deg 82 | 83 | # Example: ./get_sensor_info.py --host devx.camlab.kat.ac.za anc_(mean|gust)_wind_speed 84 | # 85 | # Matching sensor names: [u'anc_mean_wind_speed', u'anc_gust_wind_speed'] 86 | # 87 | # Detail for sensor anc_mean_wind_speed: 88 | # {'name': u'anc_mean_wind_speed', u'systype': u'mkat', 'component': u'anc', 89 | # u'site': u'deva', u'katcp_name': u'anc.mean_wind_speed', u'params': u'[]', 90 | # u'units': u'', u'type': u'float', 91 | # u'description': u"Mean of ['wind.wind-speed', 'weather.wind-speed'] 92 | # in (600 * 1.0s) window"} 93 | # 94 | # Another Example: ./get_sensor_info.py --host devx.camlab.kat.ac.za anc_.*_wind_speed 95 | # 96 | # Matching sensor names for pattern ['anc_.*_wind_speed']: 97 | # [u'anc_asc_wind_speed', u'anc_gust_wind_speed', u'anc_mean_wind_speed', 98 | # u'anc_wind_wind_speed', u'anc_asccombo_wind_speed_2', 99 | # u'anc_weather_wind_speed'] 100 | 101 | 102 | if __name__ == '__main__': 103 | parser = argparse.ArgumentParser( 104 | description="Download sensor info and print to stdout.") 105 | parser.add_argument( 106 | '--host', 107 | default='127.0.0.1', 108 | help="hostname or IP of the portal server (default: %(default)s).") 109 | parser.add_argument( 110 | 'sensors', 111 | metavar='sensor', 112 | nargs='+', 113 | help="list of sensor names or filter strings to request data for") 114 | parser.add_argument( 115 | '-v', '--verbose', 116 | dest='verbose', action="store_true", 117 | default=False, 118 | help="provide extremely verbose output.") 119 | args = parser.parse_args() 120 | if args.verbose: 121 | logger.setLevel(logging.DEBUG) 122 | else: 123 | logger.setLevel(logging.WARNING) 124 | 125 | # Start up the tornado IO loop. 126 | # Only a single function to run once, so use run_sync() instead of start() 127 | io_loop = tornado.ioloop.IOLoop.current() 128 | io_loop.run_sync(main) 129 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = build 9 | 10 | # Internal variables. 11 | PAPEROPT_a4 = -D latex_paper_size=a4 12 | PAPEROPT_letter = -D latex_paper_size=letter 13 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source 14 | # the i18n builder cannot share the environment and doctrees with the others 15 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source 16 | 17 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext 18 | 19 | help: 20 | @echo "Please use \`make ' where is one of" 21 | @echo " html to make standalone HTML files" 22 | @echo " dirhtml to make HTML files named index.html in directories" 23 | @echo " singlehtml to make a single large HTML file" 24 | @echo " pickle to make pickle files" 25 | @echo " json to make JSON files" 26 | @echo " htmlhelp to make HTML files and a HTML help project" 27 | @echo " qthelp to make HTML files and a qthelp project" 28 | @echo " devhelp to make HTML files and a Devhelp project" 29 | @echo " epub to make an epub" 30 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 31 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 32 | @echo " text to make text files" 33 | @echo " man to make manual pages" 34 | @echo " texinfo to make Texinfo files" 35 | @echo " info to make Texinfo files and run them through makeinfo" 36 | @echo " gettext to make PO message catalogs" 37 | @echo " changes to make an overview of all changed/added/deprecated items" 38 | @echo " linkcheck to check all external links for integrity" 39 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 40 | 41 | clean: 42 | -rm -rf $(BUILDDIR)/* 43 | 44 | html: 45 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 46 | @echo 47 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 48 | 49 | dirhtml: 50 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 51 | @echo 52 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 53 | 54 | singlehtml: 55 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 56 | @echo 57 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 58 | 59 | pickle: 60 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 61 | @echo 62 | @echo "Build finished; now you can process the pickle files." 63 | 64 | json: 65 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 66 | @echo 67 | @echo "Build finished; now you can process the JSON files." 68 | 69 | htmlhelp: 70 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 71 | @echo 72 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 73 | ".hhp project file in $(BUILDDIR)/htmlhelp." 74 | 75 | qthelp: 76 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 77 | @echo 78 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 79 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 80 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/katportal.qhcp" 81 | @echo "To view the help file:" 82 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/katportal.qhc" 83 | 84 | devhelp: 85 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 86 | @echo 87 | @echo "Build finished." 88 | @echo "To view the help file:" 89 | @echo "# mkdir -p $$HOME/.local/share/devhelp/katportal" 90 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/katportal" 91 | @echo "# devhelp" 92 | 93 | epub: 94 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 95 | @echo 96 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 97 | 98 | latex: 99 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 100 | @echo 101 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 102 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 103 | "(use \`make latexpdf' here to do that automatically)." 104 | 105 | latexpdf: 106 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 107 | @echo "Running LaTeX files through pdflatex..." 108 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 109 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 110 | 111 | text: 112 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 113 | @echo 114 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 115 | 116 | man: 117 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 118 | @echo 119 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 120 | 121 | texinfo: 122 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 123 | @echo 124 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 125 | @echo "Run \`make' in that directory to run these through makeinfo" \ 126 | "(use \`make info' here to do that automatically)." 127 | 128 | info: 129 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 130 | @echo "Running Texinfo files through makeinfo..." 131 | make -C $(BUILDDIR)/texinfo info 132 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 133 | 134 | gettext: 135 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 136 | @echo 137 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 138 | 139 | changes: 140 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 141 | @echo 142 | @echo "The overview file is in $(BUILDDIR)/changes." 143 | 144 | linkcheck: 145 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 146 | @echo 147 | @echo "Link check complete; look for any errors in the above output " \ 148 | "or in $(BUILDDIR)/linkcheck/output.txt." 149 | 150 | doctest: 151 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 152 | @echo "Testing of doctests in the sources finished, look at the " \ 153 | "results in $(BUILDDIR)/doctest/output.txt." 154 | -------------------------------------------------------------------------------- /examples/get_sensor_history.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Copyright (c) 2016 National Research Foundation (South African Radio Astronomy Observatory) 3 | # BSD license - see LICENSE for details 4 | """Simple example demonstrating sensor information and history queries. 5 | 6 | This example gets lists of sensor names in various ways, and gets the 7 | detailed atttributes of a specific sensor. It also gets the time history 8 | samples for a few sensors. 9 | """ 10 | from __future__ import print_function 11 | 12 | import logging 13 | import argparse 14 | import time 15 | 16 | import tornado.gen 17 | 18 | from builtins import range 19 | from datetime import datetime 20 | 21 | from katportalclient import KATPortalClient 22 | 23 | 24 | logger = logging.getLogger('katportalclient.example') 25 | 26 | 27 | @tornado.gen.coroutine 28 | def main(): 29 | # Change URL to point to a valid portal node. 30 | # If you are not interested in any subarray specific information 31 | # (e.g. schedule blocks), then the number can be omitted, as below. 32 | # Note: if on_update_callback is set to None, then we cannot use the 33 | # KATPortalClient.connect() and subscribe() methods here. 34 | portal_client = KATPortalClient('http://{host}/api/client'.format(**vars(args)), 35 | on_update_callback=None, logger=logger) 36 | 37 | # Get the names of sensors matching the patterns 38 | sensor_names = yield portal_client.sensor_names(args.sensors) 39 | print("\nMatching sensor names: {}".format(sensor_names)) 40 | # Example output (if sensors is 'm01[12]_pos_request_base'): 41 | # Matching sensor names: [u'm011_pos_request_base_azim', 42 | # u'm012_pos_request_base_ra', u'm012_pos_request_base_dec', 43 | # u'm011_pos_request_base_ra', u'm012_pos_request_base_elev', 44 | # u'm011_pos_request_base_dec', u'm012_pos_request_base_azim', 45 | # u'm011_pos_request_base_elev'] 46 | 47 | # Fetch the details for the sensors found. 48 | for sensor_name in sensor_names: 49 | sensor_detail = yield portal_client.sensor_detail(sensor_name) 50 | print("\nDetail for sensor {}:".format(sensor_name)) 51 | for key in sorted(sensor_detail): 52 | print(" {}: {}".format(key, sensor_detail[key])) 53 | # Example output: 54 | # Detail for sensor m011_pos_request_base_azim: 55 | # component: m011 56 | # description: Requested target azimuth 57 | # katcp_name: m011.pos.request-base-azim 58 | # name: m011_pos_request_base_azim 59 | # params: [-195.0, 370.0] 60 | # site: deva 61 | # systype: mkat 62 | # type: float 63 | # units: deg 64 | 65 | num_sensors = len(sensor_names) 66 | if num_sensors == 0: 67 | print("\nNo matching sensors found - no history to request!") 68 | else: 69 | print ("\nRequesting history for {} sensors, from {} to {}" 70 | .format( 71 | num_sensors, 72 | datetime.utcfromtimestamp( 73 | args.start).strftime('%Y-%m-%dT%H:%M:%SZ'), 74 | datetime.utcfromtimestamp(args.end).strftime('%Y-%m-%dT%H:%M:%SZ'))) 75 | value_time = args.include_value_time 76 | if len(sensor_names) == 1: 77 | # Request history for just a single sensor - result is 78 | # sample_time, value, status 79 | # If value timestamp is also required, then add the additional argument: 80 | # include_value_time=True 81 | # result is then sample_time, value_time, value, status 82 | history = yield portal_client.sensor_history( 83 | sensor_names[0], args.start, args.end, 84 | include_value_ts=value_time) 85 | histories = {sensor_names[0]: history} 86 | else: 87 | # Request history for all the sensors - result is sample_time, value, status 88 | # If value timestamp is also required, then add the additional argument: 89 | # include_value_time=True 90 | # result is then sample_time, value_time, value, status 91 | histories = yield portal_client.sensors_histories(sensor_names, args.start, 92 | args.end, 93 | include_value_ts=value_time) 94 | 95 | print("Found {} sensors.".format(len(histories))) 96 | for sensor_name, history in list(histories.items()): 97 | num_samples = len(history) 98 | print("History for: {} ({} samples)".format(sensor_name, num_samples)) 99 | if num_samples > 0: 100 | for count in range(0, num_samples, args.decimate): 101 | item = history[count] 102 | if count == 0: 103 | print("\tindex,{}".format(",".join(item._fields))) 104 | print("\t{},{}".format(count, item.csv())) 105 | 106 | # Example: ./get_sensor_history.py -s 1522756324 -e 1522759924 sys_watchdogs_sys 107 | # Matching sensor names: [u'sys_watchdogs_sys'] 108 | # Detail for sensor sys_watchdogs_sys: 109 | # attributes: {u'component': u'sys', u'original_name': u'sys.watchdogs.sys', u'params': u'[0, 4294967296]', u'description': u'Count of watchdogs received from component sys on 10.8.67.220:2025', u'type': u'integer'} 110 | # component: sys 111 | # name: sys_watchdogs_sys 112 | # Requesting history for 1 sensors, from 2018-04-03T11:52:08Z to 2018-04-03T12:52:08Z 113 | # Found 1 sensors. 114 | # History for: sys_watchdogs_sys (360 samples) 115 | # index,sample_time,value,status 116 | # 0,1522756329.5110459328,42108,nominal 117 | # 1,1522756339.511122942,42109,nominal 118 | # 2,1522756349.5113239288,42110,nominal 119 | # 3,1522756359.5115270615,42111,nominal 120 | # 4,1522756369.5126268864,42112,nominal 121 | # 5,1522756379.5129699707,42113,nominal 122 | # 6,1522756389.513215065,42114,nominal 123 | # 7,1522756399.514425993,42115,nominal 124 | # 8,1522756409.5146770477,42116,nominal 125 | # 9,1522756419.5149009228,42117,nominal 126 | 127 | 128 | if __name__ == '__main__': 129 | parser = argparse.ArgumentParser( 130 | description="Downloads sample histories and prints to stdout.") 131 | parser.add_argument( 132 | '--host', 133 | default='127.0.0.1', 134 | help="hostname or IP of the portal server (default: %(default)s).") 135 | parser.add_argument( 136 | '-s', '--start', 137 | default=time.time() - 3600, 138 | type=int, 139 | help="start time of sample query [sec since UNIX epoch] (default: 1h ago).") 140 | parser.add_argument( 141 | '-e', '--end', 142 | type=int, 143 | default=time.time(), 144 | help="end time of sample query [sec since UNIX epoch] (default: now).") 145 | parser.add_argument( 146 | '-d', '--decimate', 147 | type=int, 148 | metavar='N', 149 | default=1, 150 | help="decimation level - only every Nth sample is output (default: %(default)s).") 151 | parser.add_argument( 152 | 'sensors', 153 | metavar='sensor', 154 | nargs='+', 155 | help="list of sensor names or filter strings to request data for") 156 | parser.add_argument( 157 | '-v', '--verbose', 158 | dest='verbose', action="store_true", 159 | default=False, 160 | help="provide extremely verbose output.") 161 | parser.add_argument( 162 | '-i', '--include-value-time', 163 | dest="include_value_time", action="store_false", 164 | help="include value timestamp") 165 | 166 | args = parser.parse_args() 167 | if args.verbose: 168 | logger.setLevel(logging.DEBUG) 169 | else: 170 | logger.setLevel(logging.WARNING) 171 | 172 | # Start up the tornado IO loop. 173 | # Only a single function to run once, so use run_sync() instead of start() 174 | io_loop = tornado.ioloop.IOLoop.current() 175 | io_loop.run_sync(main) 176 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [v0.2.2](https://github.com/ska-sa/katportalclient/tree/v0.2.2) (2020-04-30) 4 | 5 | [Full Changelog](https://github.com/ska-sa/katportalclient/compare/v0.2.1...v0.2.2) 6 | 7 | **Merged pull requests:** 8 | 9 | - Generate documentation as part of CI build. [\#61](https://github.com/ska-sa/katportalclient/pull/61) ([mmphego](https://github.com/mmphego)) 10 | - Install latest ujson to fix float truncation [\#59](https://github.com/ska-sa/katportalclient/pull/59) ([lanceWilliams](https://github.com/lanceWilliams)) 11 | - Changed build agent from Ubuntu Trusty to Bionic [\#58](https://github.com/ska-sa/katportalclient/pull/58) ([mmphego](https://github.com/mmphego)) 12 | - Automate code linting and reject PR if fails. [\#57](https://github.com/ska-sa/katportalclient/pull/57) ([mmphego](https://github.com/mmphego)) 13 | 14 | ## [v0.2.1](https://github.com/ska-sa/katportalclient/tree/v0.2.1) (2019-09-13) 15 | 16 | [Full Changelog](https://github.com/ska-sa/katportalclient/compare/v0.2.0...v0.2.1) 17 | 18 | **Merged pull requests:** 19 | 20 | - fix sensor detail method [\#56](https://github.com/ska-sa/katportalclient/pull/56) ([tockards](https://github.com/tockards)) 21 | 22 | ## [v0.2.0](https://github.com/ska-sa/katportalclient/tree/v0.2.0) (2019-09-11) 23 | 24 | [Full Changelog](https://github.com/ska-sa/katportalclient/compare/v0.1.1...v0.2.0) 25 | 26 | **Merged pull requests:** 27 | 28 | - Fix quoting of sensor filters in URLs [\#53](https://github.com/ska-sa/katportalclient/pull/53) ([bmerry](https://github.com/bmerry)) 29 | - correct url for katstore [\#51](https://github.com/ska-sa/katportalclient/pull/51) ([tockards](https://github.com/tockards)) 30 | - Address PR Comments \#31 [\#47](https://github.com/ska-sa/katportalclient/pull/47) ([tockards](https://github.com/tockards)) 31 | - Merge master into new-katstore-integration feature [\#46](https://github.com/ska-sa/katportalclient/pull/46) ([tockards](https://github.com/tockards)) 32 | - MT-409: Test katportalclient against katstore64 [\#37](https://github.com/ska-sa/katportalclient/pull/37) ([xinyuwu](https://github.com/xinyuwu)) 33 | - Update katstore feature branch with master [\#36](https://github.com/ska-sa/katportalclient/pull/36) ([ajoubertza](https://github.com/ajoubertza)) 34 | - Feature/new katstore integration [\#31](https://github.com/ska-sa/katportalclient/pull/31) ([tockards](https://github.com/tockards)) 35 | 36 | ## [v0.1.1](https://github.com/ska-sa/katportalclient/tree/v0.1.1) (2019-08-30) 37 | 38 | [Full Changelog](https://github.com/ska-sa/katportalclient/compare/v0.1.0...v0.1.1) 39 | 40 | **Merged pull requests:** 41 | 42 | - add license and long\_description to setup.py [\#50](https://github.com/ska-sa/katportalclient/pull/50) ([tockards](https://github.com/tockards)) 43 | 44 | ## [v0.1.0](https://github.com/ska-sa/katportalclient/tree/v0.1.0) (2019-08-29) 45 | 46 | [Full Changelog](https://github.com/ska-sa/katportalclient/compare/98c0d8fea2ffdb32e9fbf96e1de57653e98cd3a5...v0.1.0) 47 | 48 | **Closed issues:** 49 | 50 | - Mixed messages on code reuse [\#28](https://github.com/ska-sa/katportalclient/issues/28) 51 | 52 | **Merged pull requests:** 53 | 54 | - add initial changelog [\#48](https://github.com/ska-sa/katportalclient/pull/48) ([tockards](https://github.com/tockards)) 55 | - Make katportalclient work on newer versions of Tornado [\#45](https://github.com/ska-sa/katportalclient/pull/45) ([bmerry](https://github.com/bmerry)) 56 | - Capture block ID usage in examples [\#44](https://github.com/ska-sa/katportalclient/pull/44) ([bngcebetsha](https://github.com/bngcebetsha)) 57 | - Request a list of sb\_ids associated with a given capture block ID [\#43](https://github.com/ska-sa/katportalclient/pull/43) ([bngcebetsha](https://github.com/bngcebetsha)) 58 | - Trigger downstream publish [\#42](https://github.com/ska-sa/katportalclient/pull/42) ([sw00](https://github.com/sw00)) 59 | - Doc strings updated [\#41](https://github.com/ska-sa/katportalclient/pull/41) ([rohanschwartz](https://github.com/rohanschwartz)) 60 | - Add multiple sensor readings request [\#40](https://github.com/ska-sa/katportalclient/pull/40) ([lanceWilliams](https://github.com/lanceWilliams)) 61 | - Add Python 3 compatibility [\#38](https://github.com/ska-sa/katportalclient/pull/38) ([ajoubertza](https://github.com/ajoubertza)) 62 | - Increase HTTP request timeout to 60 seconds [\#35](https://github.com/ska-sa/katportalclient/pull/35) ([ajoubertza](https://github.com/ajoubertza)) 63 | - Add `sensor\_value` function that returns latest sensor reading [\#34](https://github.com/ska-sa/katportalclient/pull/34) ([SKAJohanVenter](https://github.com/SKAJohanVenter)) 64 | - Revert new katstore changes on master [\#32](https://github.com/ska-sa/katportalclient/pull/32) ([ajoubertza](https://github.com/ajoubertza)) 65 | - User/bulelani/cb 1824/add test for subarray sensor lookup [\#30](https://github.com/ska-sa/katportalclient/pull/30) ([bxaia](https://github.com/bxaia)) 66 | - Update license details to BSD [\#29](https://github.com/ska-sa/katportalclient/pull/29) ([ajoubertza](https://github.com/ajoubertza)) 67 | - Allow sensor subarray lookup for component names [\#27](https://github.com/ska-sa/katportalclient/pull/27) ([ajoubertza](https://github.com/ajoubertza)) 68 | - added a sensor\_subarray\_lookup method that calls a katportal endpoint… [\#26](https://github.com/ska-sa/katportalclient/pull/26) ([bxaia](https://github.com/bxaia)) 69 | - Fix sensor\_detail request if duplicates found [\#25](https://github.com/ska-sa/katportalclient/pull/25) ([ajoubertza](https://github.com/ajoubertza)) 70 | - Improved usage of katpoint for better clarity [\#24](https://github.com/ska-sa/katportalclient/pull/24) ([fjoubert](https://github.com/fjoubert)) 71 | - added example usage of the katpoint target and antenna objects [\#23](https://github.com/ska-sa/katportalclient/pull/23) ([fjoubert](https://github.com/fjoubert)) 72 | - Added auth, userlogs, removed future targets details and many unit tests [\#22](https://github.com/ska-sa/katportalclient/pull/22) ([fjoubert](https://github.com/fjoubert)) 73 | - Reconnection logic + resending jsonrpc requests on reconnect [\#21](https://github.com/ska-sa/katportalclient/pull/21) ([fjoubert](https://github.com/fjoubert)) 74 | - Improve basic websocket subscription example [\#20](https://github.com/ska-sa/katportalclient/pull/20) ([ajoubertza](https://github.com/ajoubertza)) 75 | - Methods and example of how to get schedule block targets and target details from our catalogues [\#19](https://github.com/ska-sa/katportalclient/pull/19) ([fjoubert](https://github.com/fjoubert)) 76 | - User/lize/cb 1498 fix retrieve sensor data [\#18](https://github.com/ska-sa/katportalclient/pull/18) ([lvdheever](https://github.com/lvdheever)) 77 | - User/lize/cb 1498 [\#17](https://github.com/ska-sa/katportalclient/pull/17) ([lvdheever](https://github.com/lvdheever)) 78 | - Allow simultaneous sensor history requests [\#16](https://github.com/ska-sa/katportalclient/pull/16) ([ajoubertza](https://github.com/ajoubertza)) 79 | - Allow historical sensor queries [\#15](https://github.com/ska-sa/katportalclient/pull/15) ([ajoubertza](https://github.com/ajoubertza)) 80 | - Add sensor list and sensor detail functions [\#14](https://github.com/ska-sa/katportalclient/pull/14) ([ajoubertza](https://github.com/ajoubertza)) 81 | - jenkinsfile tweaks to always checkout the correct head of the branch;… [\#13](https://github.com/ska-sa/katportalclient/pull/13) ([fjoubert](https://github.com/fjoubert)) 82 | - Methods to get schedule block info [\#12](https://github.com/ska-sa/katportalclient/pull/12) ([ajoubertza](https://github.com/ajoubertza)) 83 | - Request sitemap from portal webserver on initialisation [\#11](https://github.com/ska-sa/katportalclient/pull/11) ([ajoubertza](https://github.com/ajoubertza)) 84 | - Flake8, docstring and copyright fixes [\#10](https://github.com/ska-sa/katportalclient/pull/10) ([ajoubertza](https://github.com/ajoubertza)) 85 | - improved debug logging call [\#9](https://github.com/ska-sa/katportalclient/pull/9) ([fjoubert](https://github.com/fjoubert)) 86 | - better local branch checkout in Jenkinsfile [\#8](https://github.com/ska-sa/katportalclient/pull/8) ([fjoubert](https://github.com/fjoubert)) 87 | - Indentation fix [\#7](https://github.com/ska-sa/katportalclient/pull/7) ([bxaia](https://github.com/bxaia)) 88 | - User/bulelani/jenkinsfile archive artifacts [\#6](https://github.com/ska-sa/katportalclient/pull/6) ([fjoubert](https://github.com/fjoubert)) 89 | - Jenkinsfile & local version [\#5](https://github.com/ska-sa/katportalclient/pull/5) ([fjoubert](https://github.com/fjoubert)) 90 | -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # katportalclient documentation build configuration file, created by 4 | # sphinx-quickstart on Thu Feb 19 12:42:35 2015. 5 | # 6 | # This file is execfile()d with the current directory set to its containing dir. 7 | # 8 | # Note that not all possible configuration values are present in this 9 | # autogenerated file. 10 | # 11 | # All configuration values have a default; values that are commented out 12 | # serve to show the default. 13 | 14 | import sys, os 15 | import sphinx_rtd_theme 16 | 17 | from katportalclient import __version__ 18 | 19 | # If extensions (or modules to document with autodoc) are in another directory, 20 | # add these directories to sys.path here. If the directory is relative to the 21 | # documentation root, use os.path.abspath to make it absolute, like shown here. 22 | sys.path.insert(0, os.path.abspath('../../katportalclient')) 23 | 24 | # -- General configuration ----------------------------------------------------- 25 | 26 | # If your documentation needs a minimal Sphinx version, state it here. 27 | #needs_sphinx = '1.0' 28 | 29 | # Add any Sphinx extension module names here, as strings. They can be extensions 30 | # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. 31 | extensions = [ 32 | 'sphinx.ext.autodoc', 33 | 'sphinx.ext.autosummary', 34 | #'sphinx.ext.intersphinx', 35 | 'sphinx.ext.todo', 36 | 'sphinx.ext.coverage', 37 | 'sphinx.ext.viewcode', 38 | #'sphinx.ext.graphviz', 39 | #'sphinx.ext.inheritance_diagram', 40 | 'numpydoc', 41 | ] 42 | 43 | # To get rid of WARNING: toctree contains reference to nonexisting document 44 | # Also see: https://github.com/phn/pytpm/issues/3#issuecomment-12133978 45 | #numpydoc_show_class_members = False 46 | 47 | numpydoc_show_inherited_class_members = False 48 | numpydoc_class_members_toctree = False 49 | 50 | # Add any paths that contain templates here, relative to this directory. 51 | templates_path = ['_templates'] 52 | 53 | # The suffix of source filenames. 54 | source_suffix = '.rst' 55 | 56 | # The encoding of source files. 57 | #source_encoding = 'utf-8-sig' 58 | 59 | # The master toctree document. 60 | master_doc = 'index' 61 | 62 | # General information about the project. 63 | project = u'katportalclient' 64 | copyright = u'2015, SKA SA - CAM' 65 | 66 | # The version info for the project you're documenting, acts as replacement for 67 | # |version| and |release|, also used in various other places throughout the 68 | # built documents. 69 | # 70 | # The short X.Y version. 71 | version = __version__ 72 | # The full version, including alpha/beta/rc tags. 73 | release = __version__ 74 | 75 | # The language for content autogenerated by Sphinx. Refer to documentation 76 | # for a list of supported languages. 77 | #language = None 78 | 79 | # There are two options for replacing |today|: either, you set today to some 80 | # non-false value, then it is used: 81 | #today = '' 82 | # Else, today_fmt is used as the format for a strftime call. 83 | #today_fmt = '%B %d, %Y' 84 | 85 | # List of patterns, relative to source directory, that match files and 86 | # directories to ignore when looking for source files. 87 | exclude_patterns = [] 88 | 89 | # The reST default role (used for this markup: `text`) to use for all documents. 90 | #default_role = None 91 | 92 | # If true, '()' will be appended to :func: etc. cross-reference text. 93 | #add_function_parentheses = True 94 | 95 | # If true, the current module name will be prepended to all description 96 | # unit titles (such as .. function::). 97 | #add_module_names = True 98 | 99 | # If true, sectionauthor and moduleauthor directives will be shown in the 100 | # output. They are ignored by default. 101 | #show_authors = False 102 | 103 | # The name of the Pygments (syntax highlighting) style to use. 104 | pygments_style = 'sphinx' 105 | 106 | # A list of ignored prefixes for module index sorting. 107 | #modindex_common_prefix = [] 108 | 109 | 110 | # -- Options for HTML output --------------------------------------------------- 111 | 112 | # The theme to use for HTML and HTML Help pages. See the documentation for 113 | # a list of builtin themes. 114 | html_theme = 'sphinx_rtd_theme' 115 | 116 | # Theme options are theme-specific and customize the look and feel of a theme 117 | # further. For a list of options available for each theme, see the 118 | # documentation. 119 | #html_theme_options = {} 120 | 121 | # Add any paths that contain custom themes here, relative to this directory. 122 | html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] 123 | 124 | # The name for this set of Sphinx documents. If None, it defaults to 125 | # " v documentation". 126 | #html_title = None 127 | 128 | # A shorter title for the navigation bar. Default is the same as html_title. 129 | #html_short_title = None 130 | 131 | # The name of an image file (relative to this directory) to place at the top 132 | # of the sidebar. 133 | #html_logo = None 134 | 135 | # The name of an image file (within the static path) to use as favicon of the 136 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 137 | # pixels large. 138 | #html_favicon = None 139 | 140 | # Add any paths that contain custom static files (such as style sheets) here, 141 | # relative to this directory. They are copied after the builtin static files, 142 | # so a file named "default.css" will overwrite the builtin "default.css". 143 | html_static_path = ['_static'] 144 | 145 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 146 | # using the given strftime format. 147 | #html_last_updated_fmt = '%b %d, %Y' 148 | 149 | # If true, SmartyPants will be used to convert quotes and dashes to 150 | # typographically correct entities. 151 | #html_use_smartypants = True 152 | 153 | # Custom sidebar templates, maps document names to template names. 154 | #html_sidebars = {} 155 | 156 | # Additional templates that should be rendered to pages, maps page names to 157 | # template names. 158 | #html_additional_pages = {} 159 | 160 | # If false, no module index is generated. 161 | #html_domain_indices = True 162 | 163 | # If false, no index is generated. 164 | #html_use_index = True 165 | 166 | # If true, the index is split into individual pages for each letter. 167 | #html_split_index = False 168 | 169 | # If true, links to the reST sources are added to the pages. 170 | #html_show_sourcelink = True 171 | 172 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 173 | #html_show_sphinx = True 174 | 175 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 176 | #html_show_copyright = True 177 | 178 | # If true, an OpenSearch description file will be output, and all pages will 179 | # contain a tag referring to it. The value of this option must be the 180 | # base URL from which the finished HTML is served. 181 | #html_use_opensearch = '' 182 | 183 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 184 | #html_file_suffix = None 185 | 186 | # Output file base name for HTML help builder. 187 | htmlhelp_basename = 'katportalclientdoc' 188 | 189 | 190 | # -- Options for LaTeX output -------------------------------------------------- 191 | 192 | latex_elements = { 193 | # The paper size ('letterpaper' or 'a4paper'). 194 | #'papersize': 'letterpaper', 195 | 196 | # The font size ('10pt', '11pt' or '12pt'). 197 | #'pointsize': '10pt', 198 | 199 | # Additional stuff for the LaTeX preamble. 200 | #'preamble': '', 201 | } 202 | 203 | # Grouping the document tree into LaTeX files. List of tuples 204 | # (source start file, target name, title, author, documentclass [howto/manual]). 205 | latex_documents = [ 206 | ('index', 'katportalclient.tex', u'katportalclient Documentation', 207 | u'SKA SA - CAM', 'manual'), 208 | ] 209 | 210 | # The name of an image file (relative to this directory) to place at the top of 211 | # the title page. 212 | #latex_logo = None 213 | 214 | # For "manual" documents, if this is true, then toplevel headings are parts, 215 | # not chapters. 216 | #latex_use_parts = False 217 | 218 | # If true, show page references after internal links. 219 | #latex_show_pagerefs = False 220 | 221 | # If true, show URL addresses after external links. 222 | #latex_show_urls = False 223 | 224 | # Documents to append as an appendix to all manuals. 225 | #latex_appendices = [] 226 | 227 | # If false, no module index is generated. 228 | #latex_domain_indices = True 229 | 230 | 231 | # -- Options for manual page output -------------------------------------------- 232 | 233 | # One entry per manual page. List of tuples 234 | # (source start file, name, description, authors, manual section). 235 | man_pages = [ 236 | ('index', 'katportalclient', u'katportalclient Documentation', 237 | [u'SKA SA - CAM'], 1) 238 | ] 239 | 240 | # If true, show URL addresses after external links. 241 | #man_show_urls = False 242 | 243 | 244 | # -- Options for Texinfo output ------------------------------------------------ 245 | 246 | # Grouping the document tree into Texinfo files. List of tuples 247 | # (source start file, target name, title, author, 248 | # dir menu entry, description, category) 249 | texinfo_documents = [ 250 | ('index', 'katportalclient', u'katportalclient Documentation', 251 | u'SKA SA - CAM', 'katportalclient', 'One line description of project.', 252 | 'Miscellaneous'), 253 | ] 254 | 255 | # Documents to append as an appendix to all manuals. 256 | #texinfo_appendices = [] 257 | 258 | # If false, no module index is generated. 259 | #texinfo_domain_indices = True 260 | 261 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 262 | #texinfo_show_urls = 'footnote' 263 | 264 | 265 | # Example configuration for intersphinx: refer to the Python standard library. 266 | intersphinx_mapping = {'http://docs.python.org/': None} 267 | -------------------------------------------------------------------------------- /examples/get_target_descriptions.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Copyright (c) 2017 National Research Foundation (South African Radio Astronomy Observatory) 3 | # BSD license - see LICENSE for details 4 | """Simple example demonstrating getting future pointing information by supplying 5 | a schedule block id code. 6 | 7 | This example uses HTTP access to katportal, not websocket access. 8 | """ 9 | from __future__ import print_function 10 | 11 | import logging 12 | import argparse 13 | 14 | import katpoint 15 | import tornado.gen 16 | 17 | from katportalclient import KATPortalClient 18 | 19 | 20 | logger = logging.getLogger('katportalclient.example') 21 | logger.setLevel(logging.INFO) 22 | 23 | 24 | @tornado.gen.coroutine 25 | def main(): 26 | # Change URL to point to a valid portal node. Subarray can be 1 to 4. 27 | # Note: if on_update_callback is set to None, then we cannot use the 28 | # KATPortalClient.connect() method (i.e. no websocket access). 29 | portal_client = KATPortalClient('http://{host}/api/client/1'.format(**vars(args)), 30 | on_update_callback=None, logger=logger) 31 | 32 | results = yield portal_client.future_targets( 33 | '{sb_id_code}'.format(**vars(args))) 34 | print(results) 35 | 36 | # Example output: 37 | # [ 38 | # { 39 | # "track_start_offset":39.8941187859, 40 | # "target":"PKS 0023-26 | J0025-2602 | OB-238, radec, 0:25:49.16, -26:02:12.6, (1410.0 8400.0 -1.694 2.107 -0.4043)", 41 | # "track_duration":20.0 42 | # }, 43 | # { 44 | # "track_start_offset":72.5947952271, 45 | # "target":"PKS 0043-42 | J0046-4207, radec, 0:46:17.75, -42:07:51.5, (400.0 2000.0 3.12 -0.7)", 46 | # "track_duration":20.0 47 | # }, 48 | # { 49 | # "track_start_offset":114.597304821, 50 | # "target":"PKS 0408-65 | J0408-6545, radec, 4:08:20.38, -65:45:09.1, (1410.0 8400.0 -3.708 3.807 -0.7202)", 51 | # "track_duration":20.0 52 | # } 53 | # ] 54 | 55 | # Create the antenna object that is used as the reference observer for 56 | # the coordinate calculations. 57 | # The parameters for the antenna object is as follows: 58 | # name : string or :class:`Antenna` object 59 | # Name of antenna, or full description string or existing antenna object 60 | # latitude : string or float, optional 61 | # Geodetic latitude, either in 'D:M:S' string format or float in radians 62 | # longitude : string or float, optional 63 | # Longitude, either in 'D:M:S' string format or a float in radians 64 | # altitude : string or float, optional 65 | # Altitude above WGS84 geoid, in metres 66 | # diameter : string or float, optional 67 | # Dish diameter, in metres 68 | # delay_model : :class:`DelayModel` object or equivalent, optional 69 | # Delay model for antenna, either as a direct object, a file-like object 70 | # representing a parameter file, or a string or sequence of float params. 71 | # The first three parameters form an East-North-Up offset from WGS84 72 | # reference position, in metres. 73 | # pointing_model : :class:`PointingModel` object or equivalent, optional 74 | # Pointing model for antenna, either as a direct object, a file-like 75 | # object representing a parameter file, or a string or sequence of 76 | # float parameters from which the :class:`PointingModel` object can 77 | # be instantiated 78 | # beamwidth : string or float, optional 79 | # Full width at half maximum (FWHM) average beamwidth, as a multiple of 80 | # lambda / D (wavelength / dish diameter). This depends on the dish 81 | # illumination pattern, and ranges from 1.03 for a uniformly illuminated 82 | # circular dish to 1.22 for a Gaussian-tapered circular dish (the 83 | # default). 84 | 85 | # Antenna description string has format "name, latitude, longitude, altitude" 86 | antenna = katpoint.Antenna("MeerKAT, -30:42:39.8, 21:26:38.0, 1035") 87 | 88 | # The default timestamp is "now", otherwise pass in a Unix timestamp as a float, or 89 | # a string of the form "YYYY-MM-DD HH:MM:SS.SSS" or "YYYY/MM/DD HH:MM:SS.SSS" 90 | timestamp_for_calcs = katpoint.Timestamp() 91 | 92 | for future_target in results: 93 | # Create the katpoint.Target object 94 | # The body argument is the full description string as specified in the 95 | # "target" attribute of the future_targets results. 96 | # The antenna parameter is a katpoint.Antenna object that defines 97 | # the reference observer for the coordinates calculation. 98 | # An alternative to specifying the antenna in the target object 99 | # would be to pass the antenna object as a parameter to any of 100 | # the calculation methods. 101 | 102 | # Please see the katpoint.Target class docstring for a detailed 103 | # explanation of the usage and input parameters. 104 | target = katpoint.Target(body=future_target['target'], 105 | antenna=antenna) 106 | print("-" * 80) 107 | # Short human-friendly string representation of target object. 108 | print("Target: {}".format(target)) 109 | # Complete string representation of target object, sufficient to 110 | # reconstruct it. 111 | print("\tdescription: {}".format(target.description)) 112 | # Type of target body, as a string tag. 113 | print("\tbody_type: {}".format(target.body_type)) 114 | # Calculate target (az, el) coordinates as seen from antenna at 115 | # time(s). 116 | print("\tazel: {}".format(target.azel(timestamp=timestamp_for_calcs))) 117 | # Calculate target's apparent (ra, dec) coordinates as seen from antenna at time(s). 118 | # This calculates the *apparent topocentric position* of the target for 119 | # the epoch-of-date in equatorial coordinates. Take note that this is 120 | # *not* the "star-atlas" position of the target, but the position as is 121 | # actually seen from the antenna at the given times. The difference is on 122 | # the order of a few arcminutes. These are the coordinates that a telescope 123 | # with an equatorial mount would use to track the target. Some targets are 124 | # unable to provide this (due to a limitation of pyephem), notably 125 | # stationary (*azel*) targets, and provide the *astrometric geocentric 126 | # position* instead. 127 | print("\tapparent_radec: {}".format( 128 | target.apparent_radec(timestamp=timestamp_for_calcs))) 129 | # Calculate target's astrometric (ra, dec) coordinates as seen from antenna at time(s). 130 | # This calculates the J2000 *astrometric geocentric position* of the 131 | # target, in equatorial coordinates. This is its star atlas position for 132 | # the epoch of J2000. 133 | print("\tastrometric_radec: {}".format( 134 | target.astrometric_radec(timestamp=timestamp_for_calcs))) 135 | # Calculate target's galactic (l, b) coordinates as seen from antenna at time(s). 136 | # This calculates the galactic coordinates of the target, based on the 137 | # J2000 *astrometric* equatorial coordinates. This is its position relative 138 | # to the Galactic reference frame for the epoch of J2000. 139 | print("\tgalactic: {}".format(target.galactic(timestamp=timestamp_for_calcs))) 140 | # Calculate parallactic angle on target as seen from antenna at time(s). 141 | # This calculates the *parallactic angle*, which is the position angle of 142 | # the observer's vertical on the sky, measured from north toward east. 143 | # This is the angle between the great-circle arc connecting the celestial 144 | # North pole to the target position, and the great-circle arc connecting 145 | # the zenith above the antenna to the target, or the angle between the 146 | # *hour circle* and *vertical circle* through the target, at the given 147 | # timestamp(s). 148 | print("\tparallactic_angle: {}".format( 149 | target.parallactic_angle(timestamp=timestamp_for_calcs))) 150 | 151 | # Example output: 152 | # -------------------------------------------------------------------------------- 153 | # Target: PKS 0023-26 (J0025-2602, OB-238), tags=radec, 0:25:49.16 -26:02:12.6, flux defined for 1410 - 8400 MHz 154 | # description: PKS 0023-26 | J0025-2602 | OB-238, radec, 0:25:49.16, -26:02:12.6, (1410.0 8400.0 -1.694 2.107 -0.4043) 155 | # body_type: radec 156 | # azel: (2.6852309703826904, -0.07934337109327316) 157 | # apparent_radec: (0.11626834312746936, -0.4528470669175264) 158 | # astrometric_radec: (0.11265809433414732, -0.45442846845967694) 159 | # galactic: (0.737839670058081, -1.4690447007394833) 160 | # parallactic_angle: -0.201351834408 161 | # -------------------------------------------------------------------------------- 162 | # Target: PKS 0043-42 (J0046-4207), tags=radec, 0:46:17.75 -42:07:51.5, flux defined for 400 - 2000 MHz 163 | # description: PKS 0043-42 | J0046-4207, radec, 0:46:17.75, -42:07:51.5, (400.0 2000.0 3.12 -0.7) 164 | # body_type: radec 165 | # azel: (2.6755282878875732, -0.3695283532142639) 166 | # apparent_radec: (0.20537825871631094, -0.7337815110666293) 167 | # astrometric_radec: (0.20200368040530206, -0.7353241823440498) 168 | # galactic: (5.351322507432435, -1.3083084615323348) 169 | # parallactic_angle: -0.249510196222 170 | # -------------------------------------------------------------------------------- 171 | # Target: 3C 48 (J0137+3309), tags=radec, 1:37:41.30 33:09:35.1, flux defined for 1408 - 23780 MHz 172 | # description: 3C 48 | J0137+3309, radec, 1:37:41.30, 33:09:35.1, (1408.0 23780.0 2.465 -0.004 -0.1251) 173 | # body_type: radec 174 | # azel: (2.0180811882019043, 0.821528434753418) 175 | # apparent_radec: (0.43045833891817115, 0.5802468926346598) 176 | # astrometric_radec: (0.4262457643630985, 0.5787468166381896) 177 | # galactic: (2.338087971687879, -0.5012483563409715) 178 | # parallactic_angle: -0.455535950384 179 | # -------------------------------------------------------------------------------- 180 | # Target: PKS 0316+16 (J0318+1628, CTA 21), tags=radec, 3:18:57.80 16:28:32.7, flux defined for 1410 - 8400 MHz 181 | # description: PKS 0316+16 | J0318+1628 | CTA 21, radec, 3:18:57.80, 16:28:32.7, (1410.0 8400.0 1.717 0.2478 -0.1594) 182 | # body_type: radec 183 | # azel: (1.7292250394821167, 0.38660863041877747) 184 | # apparent_radec: (0.8723107486945224, 0.28858452929189576) 185 | # astrometric_radec: (0.8681413143524127, 0.2875560842354557) 186 | # galactic: (2.9083411725795085, -0.5863595073035497) 187 | # parallactic_angle: -0.433835448811 188 | # -------------------------------------------------------------------------------- 189 | # Target: For A (J0322-3712, Fornax A, NGC 1316, PKS 0320-37), tags=radec, 3:22:41.51 -37:12:33.4, flux defined for 1200 - 2000 MHz 190 | # description: For A | J0322-3712 | Fornax A | NGC 1316 | PKS 0320-37, radec, 3:22:41.51, -37:12:33.4, (1200.0 2000.0 2.097) 191 | # body_type: radec 192 | # azel: (2.1072041988372803, -0.4763931930065155) 193 | # apparent_radec: (0.8872067982786673, -0.6484923060219283) 194 | # astrometric_radec: (0.8844099646425649, -0.6494244095113811) 195 | # galactic: (4.191667715692746, -0.9894384083155107) 196 | # parallactic_angle: -0.455723041705 197 | # -------------------------------------------------------------------------------- 198 | # Target: PKS 0408-65 (J0408-6545), tags=radec, 4:08:20.38 -65:45:09.1, flux defined for 1410 - 8400 MHz 199 | # description: PKS 0408-65 | J0408-6545, radec, 4:08:20.38, -65:45:09.1, (1410.0 8400.0 -3.708 3.807 -0.7202) 200 | # body_type: radec 201 | # azel: (2.3527166843414307, -0.9555976390838623) 202 | # apparent_radec: (1.0841209870004445, -1.14695824889687) 203 | # astrometric_radec: (1.0835862116596364, -1.1475981012312526) 204 | # galactic: (4.8632537597104, -0.7134665360827386) 205 | # parallactic_angle: -0.78112011371 206 | # -------------------------------------------------------------------------------- 207 | # Target: PKS 0410-75 (J0408-7507), tags=radec, 4:08:49.07 -75:07:13.7, flux defined for 200 - 12000 MHz 208 | # description: PKS 0410-75 | J0408-7507, radec, 4:08:49.07, -75:07:13.7, (200.0 12000.0 1.362 0.7345 -0.254) 209 | # body_type: radec 210 | # azel: (2.589509963989258, -1.0602085590362549) 211 | # apparent_radec: (1.083933909716081, -1.3104608809263385) 212 | # astrometric_radec: (1.0856726073362912, -1.3110995759307191) 213 | # galactic: (5.043567016155924, -0.6305471158619853) 214 | # parallactic_angle: -0.981741957496 215 | 216 | 217 | if __name__ == '__main__': 218 | parser = argparse.ArgumentParser( 219 | description="Gets the target list and target pointing descriptions " 220 | "from katportal catalogues.") 221 | parser.add_argument( 222 | '--host', 223 | default='127.0.0.1', 224 | help="hostname or IP of the portal server (default: %(default)s).") 225 | parser.add_argument( 226 | '-s', '--sb-id-code', 227 | default=None, 228 | dest='sb_id_code', 229 | help="The schedule block id code to load the future targets list from") 230 | parser.add_argument( 231 | '-v', '--verbose', 232 | dest='verbose', action="store_true", 233 | default=False, 234 | help="provide extremely verbose output.") 235 | args = parser.parse_args() 236 | if args.verbose: 237 | logger.setLevel(logging.DEBUG) 238 | else: 239 | logger.setLevel(logging.WARNING) 240 | # Start up the tornado IO loop. 241 | # Only a single function to run once, so use run_sync() instead of start() 242 | io_loop = tornado.ioloop.IOLoop.current() 243 | io_loop.run_sync(main) 244 | -------------------------------------------------------------------------------- /katportalclient/test/test_client.py: -------------------------------------------------------------------------------- 1 | # Copyright 2015 SKA South Africa (http://ska.ac.za/) 2 | # BSD license - see COPYING for details 3 | """Tests for katportalclient.""" 4 | from __future__ import print_function 5 | 6 | from future import standard_library 7 | standard_library.install_aliases() # noqa: E402 8 | import logging 9 | import io 10 | import time 11 | from urllib.parse import quote_plus 12 | 13 | import mock 14 | import ujson as json # noqa: E402 15 | 16 | from builtins import bytes, zip 17 | from functools import partial 18 | from past.builtins import basestring 19 | 20 | from tornado import concurrent, gen 21 | from tornado.httpclient import HTTPResponse, HTTPRequest 22 | from tornado.test.websocket_test import WebSocketBaseTestCase, TestWebSocketHandler 23 | from tornado.testing import gen_test 24 | from tornado.web import Application 25 | 26 | from katportalclient import ( 27 | KATPortalClient, JSONRPCRequest, ScheduleBlockNotFoundError, InvalidResponseError, 28 | SensorNotFoundError, SensorLookupError, SensorHistoryRequestError, 29 | ScheduleBlockTargetsParsingError, create_jwt_login_token, SensorSample, 30 | SensorSampleValueTime) 31 | 32 | 33 | LOGGER_NAME = 'test_portalclient' 34 | NEW_WEBSOCKET_DELAY = 0.05 35 | SITEMAP_URL = 'http://dummy.for.sitemap/api/client/3' 36 | 37 | 38 | # Example JSON text for sensor request responses 39 | sensor_json = { 40 | "anc_weather_wind_speed": """{"name":"anc_weather_wind_speed", 41 | "component": "anc", 42 | "attributes": {"description":"Wind speed", 43 | "systype":"mkat", 44 | "site":"deva", 45 | "original_name":"anc.weather.wind-speed", 46 | "params":"[0.0, 70.0]", 47 | "units":"m\/s", 48 | "type":"float"}}""", 49 | 50 | "anc_mean_wind_speed": """{ "name" : "anc_mean_wind_speed", 51 | "component": "anc", 52 | "attributes": {"description":"Mean wind speed", 53 | "systype":"mkat", 54 | "site":"deva", 55 | "original_name":"anc.mean-wind-speed", 56 | "params":"[0.0, 70.0]", 57 | "units":"m\/s", 58 | "type":"float"}}""", 59 | 60 | "anc_gust_wind_speed": """{"name" :"anc_gust_wind_speed", 61 | "component": "anc", 62 | "attributes":{"description":"Gust wind speed", 63 | "systype":"mkat", 64 | "site":"deva", 65 | "original_name":"anc.gust-wind-speed", 66 | "params":"[0.0, 70.0]", 67 | "units":"m\/s", 68 | "type":"float"}}""", 69 | 70 | "anc_gust_wind_speed2": """{"name" :"anc_gust_wind_speed2", 71 | "component": "anc", 72 | "attributes":{"description":"Gust wind speed2", 73 | "systype":"mkat", 74 | "site":"deva", 75 | "original_name":"anc.gust-wind-speed2", 76 | "params":"[0.0, 72.0]", 77 | "units":"m\/s", 78 | "type":"float"}}""", 79 | 80 | "anc_wind_device_status": """{"name" :"anc_wind_device_status", 81 | "component": "anc", 82 | "attributes":{"description":"Overall status of wind system", 83 | "systype":"mkat", 84 | "site":"deva", 85 | "original_name":"anc.wind.device-status", 86 | "params":"['ok', 'degraded', 'fail']", 87 | "units":"", 88 | "type":"discrete"}}""", 89 | 90 | "anc_weather_device_status": """{"name" :"anc_weather_device_status", 91 | "component": "anc", 92 | "attributes":{"description":"Overall status of weather system", 93 | "systype":"mkat", 94 | "site":"deva", 95 | "original_name":"anc.weather.device-status", 96 | "params":"['ok', 'degraded', 'fail']", 97 | "units":"", 98 | "type":"discrete"}}""", 99 | 100 | "regex_error": """{"error":"invalid regular expression: quantifier operand invalid\n"}""", 101 | 102 | "invalid_response": """{"error":"prepared invalid response"}""" 103 | } 104 | 105 | 106 | sensor_data1 = """{ 107 | "url": "/katstore/api/query/?start_time=1523249984&end_time=now&interval=0&sensor=anc_mean_wind_speed&minmax=false&buffer_only=false&additional_fields=false", 108 | "sensor_name": "anc_mean_wind_speed", 109 | "title": "Sensors Query", 110 | "data": [ 111 | { 112 | "value": 91474, 113 | "sample_time": 1523249992.0250809193, 114 | "status" : "error" 115 | }, 116 | { 117 | "value": 91475, 118 | "sample_time": 1523250002.0252408981, 119 | "status" : "error" 120 | }, 121 | { 122 | "value": 91476, 123 | "sample_time": 1523250012.0289709568, 124 | "status" : "error" 125 | }, 126 | { 127 | "value": 91477, 128 | "sample_time": 1523250022.0292000771, 129 | "status" : "error" 130 | } 131 | ] 132 | }""" 133 | 134 | sensor_data2 = """{ 135 | "url": "/katstore/api/query/?start_time=1523249984&end_time=now&interval=0&sensor=anc_gust_wind_speed&minmax=false&buffer_only=false&additional_fields=false", 136 | "sensor_name": "anc_gust_wind_speed", 137 | "title": "Sensors Query", 138 | "data": [ 139 | { 140 | "value": 91475, 141 | "sample_time": 1523250002.0252408981, 142 | "status" : "error" 143 | }, 144 | { 145 | "value": 91476, 146 | "sample_time": 1523250012.0289709568, 147 | "status" : "error" 148 | }, 149 | { 150 | "value": 91477, 151 | "sample_time": 1523250022.0292000771, 152 | "status" : "error" 153 | } 154 | ] 155 | }""" 156 | 157 | sensor_data3 = """{ 158 | "url": "/katstore/api/query/?start_time=1523249984&end_time=now&interval=0&sensor=sys_watchdogs_sys&minmax=false&buffer_only=false&additional_fields=false", 159 | "sensor_name": "sys_watchdogs_sys", 160 | "title": "Sensors Query", 161 | "data": [ 162 | { 163 | "value": 91474, 164 | "sample_time": 1523249992.0250809193, 165 | "value_time": 1523249991.0250809193, 166 | "status" : "error" 167 | }, 168 | { 169 | "value": 91475, 170 | "sample_time": 1523250002.0252408981, 171 | "value_time": 1523249991.0250809193, 172 | "status" : "error" 173 | }, 174 | { 175 | "value": 91477, 176 | "sample_time": 1523250022.0292000771, 177 | "value_time": 1523249991.0250809193, 178 | "status" : "error" 179 | } 180 | ] 181 | }""" 182 | 183 | sensor_data4 = """{ 184 | "url": "/katstore/api/query/?start_time=1523249984&end_time=now&interval=0&sensor=anc_mean_wind_speed&minmax=false&buffer_only=false&additional_fields=false", 185 | "sensor_name": "anc_mean_wind_speed", 186 | "title": "Sensors Query", 187 | "data": [ 188 | { 189 | "value": "91474", 190 | "sample_time": 1523249992.0250809193, 191 | "value_time": 1523249991.0250809193, 192 | "status" : "error" 193 | }, 194 | { 195 | "value": "91475", 196 | "sample_time": 1523250002.0252408981, 197 | "value_time": 1523249991.0250809193, 198 | "status" : "error" 199 | }, 200 | { 201 | "value": "91477", 202 | "sample_time": 1523250022.0292000771, 203 | "value_time": 1523249991.0250809193, 204 | "status" : "error" 205 | }, 206 | { 207 | "value": "91477", 208 | "sample_time": 1523250021.0292000771, 209 | "value_time": 1523249990.0250809193, 210 | "status" : "error" 211 | } 212 | ] 213 | }""" 214 | 215 | sensor_data_fail = """{ 216 | "url": "/katstore/api/query/?start_time=1523249984&end_time=now&interval=0&sensor=anc_mean_wind_speed&minmax=false&buffer_only=false&additional_fields=false", 217 | "sensor_name": "anc_mean_wind_speed", 218 | "title": "Sensors Query", 219 | "data": [] 220 | }""" 221 | 222 | # Keep a reference to the last test websocket handler instantiated, so that it 223 | # can be used in tests that require injecting data from the server side. 224 | # (yes, it is hacky) 225 | test_websocket = None 226 | 227 | 228 | class TestWebSocket(TestWebSocketHandler): 229 | """Web socket test server.""" 230 | 231 | def open(self): 232 | global test_websocket 233 | test_websocket = self 234 | print("WebSocket opened") 235 | 236 | def on_message(self, message): 237 | """Fake the typical replies depending on the type of method called.""" 238 | reply = {} 239 | message = json.loads(message) 240 | reply['id'] = message['id'] 241 | if message['method'] == 'pubsub-test': 242 | reply['id'] = 'redis-pubsub' 243 | reply['result'] = {'value': 'blahdieblah'} 244 | elif message['method'] == 'add': 245 | x, y = message['params'] 246 | reply['result'] = x + y 247 | elif message['method'] in ('subscribe', 'unsubscribe'): 248 | reply['result'] = len(message['params'][1]) 249 | elif message['method'] in ('set_sampling_strategy', 250 | 'set_sampling_strategies'): 251 | reply['result'] = {} 252 | filters = message['params'][1] 253 | if not isinstance(filters, list): 254 | filters = [filters] 255 | reply['result'][filters[0]] = { 256 | 'success': True, 257 | 'info': message['params'][2] 258 | } 259 | else: 260 | reply['result'] = 'unknown' 261 | self.write_message(json.dumps(reply)) 262 | 263 | def on_close(self): 264 | global test_websocket 265 | test_websocket = None 266 | print("WebSocket closed") 267 | 268 | 269 | class TestKATPortalClient(WebSocketBaseTestCase): 270 | 271 | def setUp(self): 272 | super(TestKATPortalClient, self).setUp() 273 | 274 | self.websocket_url = 'ws://localhost:%d/test' % self.get_http_port() 275 | 276 | # Mock the asynchronous HTTP client, with our sitemap 277 | http_sync_client_patcher = mock.patch('tornado.httpclient.HTTPClient') 278 | self.addCleanup(http_sync_client_patcher.stop) 279 | self.mock_http_sync_client = http_sync_client_patcher.start() 280 | 281 | def mock_fetch(url): 282 | sitemap = {'client': 283 | {'websocket': self.websocket_url, 284 | 'historic_sensor_values': r"http://0.0.0.0/katstore", 285 | 'schedule_blocks': r"http://0.0.0.0/sb", 286 | 'subarray_sensor_values': r"http://0.0.0.0/sensor-list", 287 | 'target_descriptions': r"http://0.0.0.0/sources", 288 | 'sub_nr': '3', 289 | 'authorization': r"http://0.0.0.0/katauth", 290 | 'userlogs': r"http://0.0.0.0/katcontrol/userlogs", 291 | 'subarray': r"http:/0.0.0.0/katcontrol/subarray", 292 | 'monitor': r"http:/0.0.0.0/katmonitor", 293 | } 294 | } 295 | body_buffer = StringIO.StringIO(json.dumps(sitemap)) 296 | return HTTPResponse(HTTPRequest(url), 200, buffer=body_buffer) 297 | 298 | self.mock_http_sync_client().fetch.side_effect = mock_fetch 299 | 300 | # Mock the async HTTP client for other HTTP requests 301 | http_async_client_patcher = mock.patch( 302 | 'tornado.httpclient.AsyncHTTPClient') 303 | self.addCleanup(http_async_client_patcher.stop) 304 | self.mock_http_async_client = http_async_client_patcher.start() 305 | # Set up a fetcher that just knows about the sitemap 306 | self.mock_http_async_client().fetch.side_effect = self.mock_async_fetcher('') 307 | 308 | def on_update_callback(msg, self): 309 | self.logger.info("Client got update message: '{}'".format(msg)) 310 | self.on_update_callback_call_count += 1 311 | 312 | self.on_update_callback_call_count = 0 313 | on_msg_callback = partial(on_update_callback, self=self) 314 | self._portal_client = KATPortalClient( 315 | SITEMAP_URL, 316 | on_msg_callback, 317 | io_loop=self.io_loop) 318 | 319 | def tearDown(self): 320 | yield self.close(self._portal_client) 321 | super(TestKATPortalClient, self).tearDown() 322 | 323 | def get_app(self): 324 | # Create application object with handlers 325 | self.logger = logging.getLogger(LOGGER_NAME) 326 | self.logger.setLevel(logging.INFO) 327 | self.close_future = gen.Future() 328 | self.application = Application([ 329 | ('/test', TestWebSocket, dict(close_future=self.close_future)), 330 | ]) 331 | self.application.logger = self.logger 332 | return self.application 333 | 334 | @gen_test 335 | def test_connect(self): 336 | self.assertIsNotNone(self._portal_client) 337 | yield self._portal_client.connect() 338 | self.assertTrue(self._portal_client.is_connected) 339 | self.assertTrue(self._portal_client._heart_beat_timer.is_running()) 340 | self.assertFalse(self._portal_client._disconnect_issued) 341 | 342 | @gen_test 343 | def test_reconnect(self): 344 | self.assertIsNotNone(self._portal_client) 345 | yield self._portal_client.connect() 346 | self.assertTrue(self._portal_client.is_connected) 347 | self.assertTrue(self._portal_client._heart_beat_timer.is_running()) 348 | connect_future = gen.Future() 349 | self._portal_client._connect = mock.MagicMock( 350 | return_value=connect_future) 351 | connect_future.set_result(None) 352 | yield test_websocket.close() 353 | yield gen.sleep(NEW_WEBSOCKET_DELAY) # give ioloop time to open new websocket 354 | self._portal_client._connect.assert_called_with(reconnecting=True) 355 | 356 | @gen_test 357 | def test_resend_subscriptions_and_strategies_after_reconnect(self): 358 | self.assertIsNotNone(self._portal_client) 359 | yield self._portal_client.connect() 360 | self.assertTrue(self._portal_client.is_connected) 361 | self.assertTrue(self._portal_client._heart_beat_timer.is_running()) 362 | resend_future = gen.Future() 363 | self._portal_client._resend_subscriptions_and_strategies = mock.MagicMock( 364 | return_value=resend_future) 365 | resend_future.set_result(None) 366 | yield test_websocket.close() 367 | yield gen.sleep(NEW_WEBSOCKET_DELAY) # give ioloop time to open new websocket 368 | self._portal_client._resend_subscriptions_and_strategies.assert_called_once() 369 | 370 | # test another reconnect if resending the strategies did not work on a 371 | # reconnect 372 | resend_future2 = gen.Future() 373 | resend_future2.set_result(None) 374 | self._portal_client._resend_subscriptions_and_strategies = mock.MagicMock( 375 | return_value=resend_future2) 376 | self._portal_client._resend_subscriptions_and_strategies.side_effect = Exception( 377 | 'some exception was thrown while _resend_subscriptions_and_strategies') 378 | self._portal_client._connect_later = mock.MagicMock() 379 | yield test_websocket.close() 380 | yield gen.sleep(NEW_WEBSOCKET_DELAY) # give ioloop time to open new websocket 381 | self._portal_client._connect_later.assert_called_once() 382 | 383 | @gen_test 384 | def test_server_redis_reconnect_message(self): 385 | self.assertIsNotNone(self._portal_client) 386 | yield self._portal_client.connect() 387 | self.assertTrue(self._portal_client.is_connected) 388 | self.assertTrue(self._portal_client._heart_beat_timer.is_running()) 389 | resend_future = gen.Future() 390 | self._portal_client._resend_subscriptions = mock.MagicMock( 391 | return_value=resend_future) 392 | self._portal_client._websocket_message('{"id": "redis-reconnect"}') 393 | resend_future.set_result(None) 394 | self._portal_client._resend_subscriptions.assert_called_once() 395 | 396 | @gen_test 397 | def test_resend_subscriptions(self): 398 | self.assertIsNotNone(self._portal_client) 399 | yield self._portal_client.connect() 400 | self.assertTrue(self._portal_client.is_connected) 401 | self.assertTrue(self._portal_client._heart_beat_timer.is_running()) 402 | send_future = gen.Future() 403 | send_future.set_result('Test _send success') 404 | self._portal_client._send = mock.MagicMock( 405 | return_value=send_future) 406 | self._portal_client._ws_jsonrpc_cache = [ 407 | JSONRPCRequest(method='subscribe', params='test params1'), 408 | JSONRPCRequest(method='subscribe', params='test params2'), 409 | JSONRPCRequest(method='not_subscribe', params='test params3'), 410 | JSONRPCRequest(method='unsubscribe', params='test params4'), 411 | JSONRPCRequest(method='subscribe', params='test params5'), 412 | JSONRPCRequest(method='subscribe', params='test params6')] 413 | yield self._portal_client._resend_subscriptions() 414 | # only subscribes must be resent! 415 | self.assertEquals(self._portal_client._send.call_count, 4) 416 | 417 | @gen_test 418 | def test_resend_subscriptions_and_strategies(self): 419 | self.assertIsNotNone(self._portal_client) 420 | yield self._portal_client.connect() 421 | self.assertTrue(self._portal_client.is_connected) 422 | self.assertTrue(self._portal_client._heart_beat_timer.is_running()) 423 | send_future = gen.Future() 424 | send_future.set_result('Test _send success') 425 | self._portal_client._send = mock.MagicMock( 426 | return_value=send_future) 427 | self._portal_client._ws_jsonrpc_cache = [ 428 | JSONRPCRequest(method='subscribe', params='test params1'), 429 | JSONRPCRequest(method='subscribe', params='test params2'), 430 | JSONRPCRequest(method='not_subscribe', params='test params3'), 431 | JSONRPCRequest(method='unsubscribe', params='test params4'), 432 | JSONRPCRequest(method='set_sampling_strategy', 433 | params='test params5'), 434 | JSONRPCRequest(method='set_sampling_strategies', params='test params6')] 435 | yield self._portal_client._resend_subscriptions_and_strategies() 436 | self.assertEquals(self._portal_client._send.call_count, 6) 437 | 438 | @gen_test 439 | def test_disconnect(self): 440 | self.assertIsNotNone(self._portal_client) 441 | yield self._portal_client.connect() 442 | self.assertTrue(self._portal_client.is_connected) 443 | self._portal_client.disconnect() 444 | self.assertFalse(self._portal_client.is_connected) 445 | self.assertFalse(self._portal_client._heart_beat_timer.is_running()) 446 | self.assertEquals(self._portal_client._ws_jsonrpc_cache, []) 447 | self.assertTrue(self._portal_client._disconnect_issued) 448 | 449 | @gen_test 450 | def test_add(self): 451 | yield self._portal_client.connect() 452 | result = yield self._portal_client.add(8, 67) 453 | self.assertEqual(result, 8 + 67) 454 | 455 | @gen_test 456 | def test_add_when_not_connected(self): 457 | with self.assertRaises(Exception): 458 | yield self._portal_client.add(8, 67) 459 | 460 | @gen_test 461 | def test_cache_jsonrpc_request(self): 462 | req1 = JSONRPCRequest('test1', 'test_params1') 463 | req2 = JSONRPCRequest('test2', ['test_params2', 'test_params2']) 464 | req3 = JSONRPCRequest('subscribe', ['namespace', 'sub_strings']) 465 | req4 = JSONRPCRequest('unsubscribe', ['namespace', 'sub_strings']) 466 | req5 = JSONRPCRequest('set_sampling_strategy', 467 | ['namespace', 'sensor_name', 'strategy_and_params']) 468 | req6 = JSONRPCRequest('set_sampling_strategy', 469 | ['namespace', 'sensor_name', 'none']) 470 | req7 = JSONRPCRequest('set_sampling_strategies', 471 | ['namespace', 'sensor_name', 'strategy_and_params']) 472 | req8 = JSONRPCRequest('set_sampling_strategies', 473 | ['namespace', 'sensor_name', 'none']) 474 | self._portal_client._cache_jsonrpc_request(req1) 475 | self.assertEquals(len(self._portal_client._ws_jsonrpc_cache), 1) 476 | self.assertEquals(self._portal_client._ws_jsonrpc_cache[0].id, req1.id) 477 | self._portal_client._cache_jsonrpc_request(req2) 478 | self.assertEquals(len(self._portal_client._ws_jsonrpc_cache), 2) 479 | self.assertEquals(self._portal_client._ws_jsonrpc_cache[0].id, req1.id) 480 | self.assertEquals(self._portal_client._ws_jsonrpc_cache[1].id, req2.id) 481 | # test no duplicates are added 482 | self._portal_client._cache_jsonrpc_request(req1) 483 | self.assertEquals(len(self._portal_client._ws_jsonrpc_cache), 2) 484 | self.assertEquals(self._portal_client._ws_jsonrpc_cache[0].id, req1.id) 485 | self.assertEquals(self._portal_client._ws_jsonrpc_cache[1].id, req2.id) 486 | # test subscriptions are added 487 | self._portal_client._cache_jsonrpc_request(req3) 488 | self.assertEquals(len(self._portal_client._ws_jsonrpc_cache), 3) 489 | self.assertEquals(self._portal_client._ws_jsonrpc_cache[0].id, req1.id) 490 | self.assertEquals(self._portal_client._ws_jsonrpc_cache[1].id, req2.id) 491 | self.assertEquals(self._portal_client._ws_jsonrpc_cache[2].id, req3.id) 492 | # test no duplicate subscriptions are added 493 | self._portal_client._cache_jsonrpc_request(req3) 494 | self.assertEquals(len(self._portal_client._ws_jsonrpc_cache), 3) 495 | self.assertEquals(self._portal_client._ws_jsonrpc_cache[0].id, req1.id) 496 | self.assertEquals(self._portal_client._ws_jsonrpc_cache[1].id, req2.id) 497 | self.assertEquals(self._portal_client._ws_jsonrpc_cache[2].id, req3.id) 498 | # test that an unsubscribe removes a subscribe message 499 | self._portal_client._cache_jsonrpc_request(req4) 500 | self.assertEquals(len(self._portal_client._ws_jsonrpc_cache), 2) 501 | self.assertEquals(self._portal_client._ws_jsonrpc_cache[0].id, req1.id) 502 | self.assertEquals(self._portal_client._ws_jsonrpc_cache[1].id, req2.id) 503 | # test set_sampling_strategy and set_sampling_strategies are added 504 | self._portal_client._cache_jsonrpc_request(req5) 505 | self._portal_client._cache_jsonrpc_request(req7) 506 | self.assertEquals(len(self._portal_client._ws_jsonrpc_cache), 4) 507 | self.assertEquals(self._portal_client._ws_jsonrpc_cache[2].id, req5.id) 508 | self.assertEquals(self._portal_client._ws_jsonrpc_cache[3].id, req7.id) 509 | # test set_sampling_strategy and set_sampling_strategies are not 510 | # duplicated 511 | self._portal_client._cache_jsonrpc_request(req5) 512 | self._portal_client._cache_jsonrpc_request(req7) 513 | self.assertEquals(len(self._portal_client._ws_jsonrpc_cache), 4) 514 | self.assertEquals(self._portal_client._ws_jsonrpc_cache[2].id, req5.id) 515 | self.assertEquals(self._portal_client._ws_jsonrpc_cache[3].id, req7.id) 516 | # test that set_sampling_strategy and set_sampling_strategies are removed 517 | # when a strategy of none is given 518 | self._portal_client._cache_jsonrpc_request(req6) 519 | self._portal_client._cache_jsonrpc_request(req8) 520 | self.assertEquals(len(self._portal_client._ws_jsonrpc_cache), 2) 521 | self.assertEquals(self._portal_client._ws_jsonrpc_cache[0].id, req1.id) 522 | self.assertEquals(self._portal_client._ws_jsonrpc_cache[1].id, req2.id) 523 | # test cache is cleared on a disconnect 524 | self._portal_client.disconnect() 525 | self.assertEquals(len(self._portal_client._ws_jsonrpc_cache), 0) 526 | 527 | @gen_test 528 | def test_subscribe(self): 529 | self._portal_client._cache_jsonrpc_request = mock.MagicMock() 530 | yield self._portal_client.connect() 531 | result = yield self._portal_client.subscribe('planets', ['jupiter', 'm*']) 532 | self.assertEqual(result, 2) 533 | self._portal_client._cache_jsonrpc_request.assert_called_once() 534 | 535 | @gen_test 536 | def test_unsubscribe(self): 537 | self._portal_client._cache_jsonrpc_request = mock.MagicMock() 538 | yield self._portal_client.connect() 539 | result = yield self._portal_client.unsubscribe( 540 | 'alpha', ['a*', 'b*', 'c*', 'd', 'e']) 541 | self.assertEqual(result, 5) 542 | self._portal_client._cache_jsonrpc_request.assert_called_once() 543 | 544 | @gen_test 545 | def test_set_sampling_strategy(self): 546 | self._portal_client._cache_jsonrpc_request = mock.MagicMock() 547 | yield self._portal_client.connect() 548 | result = yield self._portal_client.set_sampling_strategy( 549 | 'ants', 'mode', 'period 1') 550 | self.assertTrue(isinstance(result, dict)) 551 | self.assertTrue('mode' in list(result.keys())) 552 | self._portal_client._cache_jsonrpc_request.assert_called_once() 553 | 554 | @gen_test 555 | def test_set_sampling_strategies(self): 556 | self._portal_client._cache_jsonrpc_request = mock.MagicMock() 557 | yield self._portal_client.connect() 558 | result = yield self._portal_client.set_sampling_strategies( 559 | 'ants', ['mode', 'sensors_ok', 'ap_connected'], 'event-rate 1 5') 560 | self.assertTrue(isinstance(result, dict)) 561 | self.assertTrue('mode' in list(result.keys())) 562 | self._portal_client._cache_jsonrpc_request.assert_called_once() 563 | 564 | @gen_test 565 | def test_on_update_callback(self): 566 | yield self._portal_client.connect() 567 | # Fake a redis publish update to make sure the callback is invoked 568 | req = JSONRPCRequest('pubsub-test', []) 569 | self._portal_client._ws.write_message(req()) 570 | yield gen.sleep(0.2) # Give pubsub message chance to be received 571 | self.assertEqual(self.on_update_callback_call_count, 1) 572 | 573 | @gen_test 574 | def test_init_with_websocket_url(self): 575 | """Test backwards compatibility initialising directly with a websocket URL.""" 576 | test_client = KATPortalClient(self.websocket_url, None) 577 | yield test_client.connect() 578 | self.assertTrue(test_client.is_connected) 579 | 580 | @gen_test 581 | def test_sitemap_includes_expected_endpoints(self): 582 | sitemap = self._portal_client.sitemap 583 | self.assertTrue(sitemap['websocket'].startswith('ws://')) 584 | self.assertTrue( 585 | sitemap['historic_sensor_values'].startswith('http://')) 586 | self.assertTrue(sitemap['schedule_blocks'].startswith('http://')) 587 | self.assertTrue(sitemap['capture_blocks'].startswith('http://')) 588 | self.assertTrue(sitemap['sub_nr'] == '3') 589 | 590 | @gen_test 591 | def test_schedule_blocks_assigned_list_valid(self): 592 | """Test schedule block IDs are correctly extracted from JSON text.""" 593 | schedule_block_base_url = self._portal_client.sitemap[ 594 | 'schedule_blocks'] 595 | 596 | self.mock_http_async_client().fetch.side_effect = self.mock_async_fetcher( 597 | valid_response=r""" 598 | {"result": 599 | "[{\"id_code\":\"20160908-0004\",\"owner\":\"CAM\",\"type\":\"OBSERVATION\",\"sub_nr\":1}, 600 | {\"id_code\":\"20160908-0005\",\"owner\":\"CAM\",\"type\":\"OBSERVATION\",\"sub_nr\":3}, 601 | {\"id_code\":\"20160908-0006\",\"owner\":\"CAM\",\"type\":\"OBSERVATION\",\"sub_nr\":2}, 602 | {\"id_code\":\"20160908-0007\",\"owner\":\"CAM\",\"type\":\"OBSERVATION\",\"sub_nr\":4}, 603 | {\"id_code\":\"20160908-0008\",\"owner\":\"CAM\",\"type\":\"OBSERVATION\",\"sub_nr\":3} 604 | ]" 605 | }""", 606 | invalid_response="""{"result":null}""", 607 | starts_with=schedule_block_base_url) 608 | 609 | sb_ids = yield self._portal_client.schedule_blocks_assigned() 610 | 611 | # Verify that only the 2 schedule blocks for subarray 3 are returned 612 | self.assertTrue(len(sb_ids) == 2, 613 | "Expect exactly 2 schedule block IDs") 614 | self.assertIn('20160908-0005', sb_ids) 615 | self.assertIn('20160908-0008', sb_ids) 616 | 617 | @gen_test 618 | def test_schedule_blocks_assigned_list_empty(self): 619 | """Test with no schedule block IDs on a subarray.""" 620 | schedule_block_base_url = self._portal_client.sitemap[ 621 | 'schedule_blocks'] 622 | 623 | self.mock_http_async_client().fetch.side_effect = self.mock_async_fetcher( 624 | valid_response=r""" 625 | {"result": 626 | "[{\"id_code\":\"20160908-0004\",\"owner\":\"CAM\",\"type\":\"OBSERVATION\",\"sub_nr\":1}, 627 | {\"id_code\":\"20160908-0005\",\"owner\":\"CAM\",\"type\":\"OBSERVATION\",\"sub_nr\":2} 628 | ]" 629 | }""", 630 | invalid_response="""{"result":null}""", 631 | starts_with=schedule_block_base_url) 632 | 633 | sb_ids = yield self._portal_client.schedule_blocks_assigned() 634 | 635 | # Verify that there are no schedule blocks (since tests work on 636 | # subarray 3) 637 | self.assertTrue(len(sb_ids) == 0, "Expect no schedule block IDs") 638 | 639 | @gen_test 640 | def test_schedule_block_detail(self): 641 | """Test schedule block detail is correctly extracted from JSON text.""" 642 | schedule_block_base_url = self._portal_client.sitemap[ 643 | 'schedule_blocks'] 644 | schedule_block_id = "20160908-0005" 645 | 646 | self.mock_http_async_client().fetch.side_effect = self.mock_async_fetcher( 647 | valid_response=r""" 648 | {"result": 649 | {"id_code":"20160908-0005", 650 | "owner":"CAM", 651 | "actual_end_time":null, 652 | "id":5, 653 | "scheduled_time":"2016-09-08 09:18:53.000Z", 654 | "priority":"LOW", 655 | "state":"SCHEDULED", 656 | "config_label":"", 657 | "type":"OBSERVATION", 658 | "expected_duration_seconds":89, 659 | "description":"a test schedule block", 660 | "sub_nr":3, 661 | "desired_start_time":null, 662 | "actual_start_time":null, 663 | "resource_alloc":null 664 | } 665 | }""", 666 | invalid_response="""{"result":null}""", 667 | starts_with=schedule_block_base_url, 668 | contains=schedule_block_id) 669 | 670 | with self.assertRaises(ScheduleBlockNotFoundError): 671 | yield self._portal_client.schedule_block_detail("20160908-bad") 672 | 673 | sb_valid = yield self._portal_client.schedule_block_detail(schedule_block_id) 674 | self.assertTrue(sb_valid['id_code'] == schedule_block_id) 675 | self.assertTrue(sb_valid['sub_nr'] == 3) 676 | self.assertIn('description', sb_valid) 677 | self.assertIn('desired_start_time', sb_valid) 678 | self.assertIn('scheduled_time', sb_valid) 679 | self.assertIn('actual_start_time', sb_valid) 680 | self.assertIn('actual_end_time', sb_valid) 681 | self.assertIn('expected_duration_seconds', sb_valid) 682 | self.assertIn('state', sb_valid) 683 | 684 | @gen_test 685 | def test_sb_ids_by_capture_block_valid(self): 686 | """Test SB IDs are extracted for valid capture block ID.""" 687 | capture_block_base_url = self._portal_client.sitemap[ 688 | 'capture_blocks'] 689 | capture_block_id = "1556092846" 690 | 691 | self.mock_http_async_client().fetch.side_effect = self.mock_async_fetcher( 692 | valid_response=r""" 693 | {"result":["20190424-0009", "20190424-0010"]}""", 694 | invalid_response=r"""{"result":null}""", 695 | starts_with=capture_block_base_url) 696 | sb_ids = yield self._portal_client.sb_ids_by_capture_block(capture_block_id) 697 | # Verify that sb_id has been returned to the list 698 | self.assertTrue(len(sb_ids) == 2, 699 | "Expect exactly 2 schedule block IDs") 700 | self.assertIn('20190424-0009', sb_ids) 701 | self.assertIn('20190424-0010', sb_ids) 702 | 703 | @gen_test 704 | def test_sb_ids_by_capture_block_empty(self): 705 | """Test no SB IDs are extracted for unused capture block ID.""" 706 | capture_block_base_url = self._portal_client.sitemap[ 707 | 'capture_blocks'] 708 | capture_block_id = "123456" 709 | 710 | self.mock_http_async_client().fetch.side_effect = self.mock_async_fetcher( 711 | valid_response=r"""{"result":[]}""", 712 | invalid_response=r"""{"result":null}""", 713 | starts_with=capture_block_base_url) 714 | sb_ids = yield self._portal_client.sb_ids_by_capture_block(capture_block_id) 715 | # Verify that empty list returned 716 | self.assertTrue(len(sb_ids) == 0, 717 | "Expect no schedule block IDs") 718 | 719 | @gen_test 720 | def test_sensor_names_single_sensor_valid(self): 721 | """Test single sensor name is correctly extracted from JSON text.""" 722 | history_base_url = self._portal_client.sitemap[ 723 | 'historic_sensor_values'] 724 | sensor_name_filter = 'anc_weather_wind_speed' 725 | 726 | self.mock_http_async_client().fetch.side_effect = self.mock_async_fetcher( 727 | valid_response=('{"data": [%s]}' % sensor_json['anc_weather_wind_speed']), 728 | invalid_response='[]', 729 | starts_with=history_base_url, 730 | contains=sensor_name_filter) 731 | 732 | sensors = yield self._portal_client.sensor_names(sensor_name_filter) 733 | 734 | self.assertTrue(len(sensors) == 1, "Expect exactly 1 sensor") 735 | self.assertTrue(sensors[0] == sensor_name_filter) 736 | 737 | @gen_test 738 | def test_sensor_names_multiple_sensors_valid(self): 739 | """Test multiple sensors correctly extracted from JSON text.""" 740 | history_base_url = self._portal_client.sitemap[ 741 | 'historic_sensor_values'] 742 | sensor_name_filter = 'anc_w.*_device_status' 743 | 744 | self.mock_http_async_client().fetch.side_effect = self.mock_async_fetcher( 745 | valid_response='{"data":[%s, %s]}' % (sensor_json['anc_wind_device_status'], 746 | sensor_json['anc_weather_device_status']), 747 | invalid_response='[]', 748 | starts_with=history_base_url, 749 | contains=quote_plus(sensor_name_filter)) 750 | 751 | sensors = yield self._portal_client.sensor_names(sensor_name_filter) 752 | 753 | self.assertTrue(len(sensors) == 2, "Expect exactly 2 sensors") 754 | self.assertTrue(sensors[0] == 'anc_weather_device_status') 755 | self.assertTrue(sensors[1] == 'anc_wind_device_status') 756 | 757 | @gen_test 758 | def test_sensor_names_no_duplicate_sensors(self): 759 | """Test no duplicates if filters request duplicate sensors.""" 760 | history_base_url = self._portal_client.sitemap[ 761 | 'historic_sensor_values'] 762 | sensor_name_filters = [ 763 | 'anc_weather_wind_speed', 'anc_weather_wind_speed'] 764 | 765 | self.mock_http_async_client().fetch.side_effect = self.mock_async_fetcher( 766 | valid_response=('{"data":[%s]}' % sensor_json['anc_weather_wind_speed']), 767 | invalid_response='[]', 768 | starts_with=history_base_url, 769 | contains=sensor_name_filters[0]) 770 | 771 | sensors = yield self._portal_client.sensor_names(sensor_name_filters) 772 | 773 | self.assertTrue(len(sensors) == 1, "Expect exactly 1 sensor") 774 | self.assertTrue(sensors[0] == sensor_name_filters[0]) 775 | 776 | @gen_test 777 | def test_sensor_names_empty_list(self): 778 | """Test with sensor name that does not exist.""" 779 | history_base_url = self._portal_client.sitemap[ 780 | 'historic_sensor_values'] 781 | 782 | self.mock_http_async_client().fetch.side_effect = self.mock_async_fetcher( 783 | valid_response='[]', 784 | invalid_response='[{}]'.format( 785 | sensor_json['anc_weather_wind_speed']), 786 | starts_with=history_base_url) 787 | 788 | sensors = yield self._portal_client.sensor_names('non_existant_sensor') 789 | 790 | self.assertTrue(len(sensors) == 0, "Expect exactly 0 sensors") 791 | 792 | @gen_test 793 | def test_sensor_names_exception_for_invalid_regex(self): 794 | """Test that invalid regex raises exception.""" 795 | history_base_url = self._portal_client.sitemap[ 796 | 'historic_sensor_values'] 797 | 798 | self.mock_http_async_client().fetch.side_effect = self.mock_async_fetcher( 799 | valid_response=sensor_json['regex_error'], 800 | invalid_response=sensor_json['invalid_response'], 801 | starts_with=history_base_url) 802 | 803 | with self.assertRaises(SensorNotFoundError): 804 | yield self._portal_client.sensor_names('*bad') 805 | 806 | @gen_test 807 | def test_sensor_detail(self): 808 | """Test sensor's attributes are correctly extracted from JSON text.""" 809 | history_base_url = self._portal_client.sitemap['historic_sensor_values'] 810 | sensor_name = 'anc_weather_wind_speed' 811 | self.mock_http_async_client().fetch.side_effect = self.mock_async_fetcher( 812 | valid_response=('{{"data":[{}]}}'.format( sensor_json['anc_weather_wind_speed'])), 813 | invalid_response=sensor_json['invalid_response'], 814 | starts_with=history_base_url, 815 | contains=sensor_name) 816 | 817 | with self.assertRaises(SensorNotFoundError): 818 | yield self._portal_client.sensor_detail('invalid_sensor_name') 819 | 820 | sensor_detail = yield self._portal_client.sensor_detail(sensor_name) 821 | 822 | self.assertTrue(sensor_detail['name'] == sensor_name) 823 | self.assertTrue(sensor_detail['description'] == "Wind speed") 824 | self.assertTrue(sensor_detail['params'] == "[0.0, 70.0]") 825 | self.assertTrue(sensor_detail['units'] == "m/s") 826 | self.assertTrue(sensor_detail['type'] == "float") 827 | self.assertTrue(sensor_detail['component'] == "anc") 828 | self.assertTrue(sensor_detail['katcp_name'] == "anc.weather.wind-speed") 829 | 830 | @gen_test 831 | def test_sensor_detail_for_multiple_sensors_but_exact_match(self): 832 | """Test sensor detail request with many matches, but one exact match. 833 | 834 | In this case, there is a sensor name that also happens to be a prefix 835 | for other sensor names. E.g. if we have "sensor_foo", and "sensor_foo2", 836 | the details of "sensor_foo" must be available, even though there are 837 | multiple sensors that match that pattern. 838 | """ 839 | history_base_url = self._portal_client.sitemap[ 840 | 'historic_sensor_values'] 841 | sensor_name_filter = 'anc_gust_wind_speed' 842 | 843 | self.mock_http_async_client().fetch.side_effect = self.mock_async_fetcher( 844 | valid_response='{{"data" : [{}, {}]}}'.format (sensor_json['anc_gust_wind_speed2'], 845 | sensor_json['anc_gust_wind_speed']), 846 | invalid_response="[]", 847 | starts_with=history_base_url, 848 | contains=sensor_name_filter) 849 | 850 | sensor_detail = yield self._portal_client.sensor_detail('anc_gust_wind_speed') 851 | self.assertTrue(sensor_detail['name'] == 'anc_gust_wind_speed') 852 | 853 | @gen_test 854 | def test_sensor_detail_exception_for_multiple_sensors(self): 855 | """Test exception raised if sensor name is not unique for detail request.""" 856 | history_base_url = self._portal_client.sitemap[ 857 | 'historic_sensor_values'] 858 | sensor_name_filter = 'anc_w.*_device_status' 859 | 860 | self.mock_http_async_client().fetch.side_effect = self.mock_async_fetcher( 861 | valid_response='[{}, {}]'.format(sensor_json['anc_wind_device_status'], 862 | sensor_json['anc_weather_device_status']), 863 | invalid_response="[]", 864 | starts_with=history_base_url, 865 | contains=sensor_name_filter) 866 | 867 | with self.assertRaises(SensorNotFoundError): 868 | yield self._portal_client.sensor_detail(sensor_name_filter) 869 | 870 | @gen_test 871 | def test_sensor_value_invalid_results(self): 872 | """test that we handle the monitor server returning an invalid string""" 873 | self.mock_http_async_client().fetch.side_effect = self.mock_async_fetcher('') 874 | with self.assertRaises(InvalidResponseError): 875 | yield self._portal_client.sensor_value("INVALID_SENSOR") 876 | 877 | @gen_test 878 | def test_sensor_value_no_results(self): 879 | """Test that we handle no matches""" 880 | self.mock_http_async_client().fetch.side_effect = self.mock_async_fetcher('[]') 881 | with self.assertRaises(SensorNotFoundError): 882 | yield self._portal_client.sensor_value("INVALID_SENSOR") 883 | 884 | @gen_test 885 | def test_sensor_value_multiple_results_one_match(self): 886 | """Test that we handle multiple results correctly with one match""" 887 | 888 | mon_response = ('[{"status":"nominal",' 889 | '"name":"tfrmon_tfr_m018_l_band_offset","component":"tfrmon",' 890 | '"value":43680.0,' 891 | '"value_ts":1530713112,"time":1531302437},' 892 | '{"status":"nominal",' 893 | '"name":"some_other_sample","component":"tfrmon","value":43680.0,' 894 | '"value_ts":111.111,"time":222.222}]') 895 | 896 | self.mock_http_async_client().fetch.side_effect = self.mock_async_fetcher(mon_response) 897 | result = yield self._portal_client.sensor_value("tfrmon_tfr_m018_l_band_offset") 898 | expected_result = SensorSample(sample_time=1531302437, value=43680.0, 899 | status='nominal') 900 | assert result == expected_result 901 | 902 | self.mock_http_async_client().fetch.side_effect = self.mock_async_fetcher(mon_response) 903 | result = yield self._portal_client.sensor_value("tfrmon_tfr_m018_l_band_offset", 904 | include_value_ts=True) 905 | expected_result = SensorSampleValueTime(sample_time=1531302437, 906 | value_time=1530713112, 907 | value=43680.0, status='nominal') 908 | assert result == expected_result 909 | 910 | @gen_test 911 | def test_sensor_value_multiple_results_no_match(self): 912 | """Test that we handle multiple results correctly with no matches""" 913 | 914 | mon_response = ('[{"status":"nominal",' 915 | '"name":"some_other_sample","component":"anc",' 916 | '"value":43680.0,' 917 | '"value_ts":1530713112.9800000191,"time":1531302437},' 918 | '{"status":"nominal",' 919 | '"name":"some_other_sample1","component":"anc","value":43680.0,' 920 | '"value_ts":111.111,"time":222.222}]') 921 | 922 | self.mock_http_async_client().fetch.side_effect = self.mock_async_fetcher(mon_response) 923 | with self.assertRaises(SensorNotFoundError): 924 | yield self._portal_client.sensor_value( 925 | "tfrmon_tfr_m018_l_band_offset_average") 926 | 927 | @gen_test 928 | def test_sensor_value_one_result(self): 929 | """Test that we can handle single result""" 930 | mon_response = ('[{"status":"nominal",' 931 | '"name":"some_other_sample","component":"anc","value":43680.0,' 932 | '"value_ts":111.111,"time":222.222}]') 933 | 934 | self.mock_http_async_client().fetch.side_effect = self.mock_async_fetcher(mon_response) 935 | expected_result = SensorSample(sample_time=222.222, value=43680.0, status='nominal') 936 | res = yield self._portal_client.sensor_value( 937 | "tfrmon_tfr_m018_l_band_offset_average") 938 | assert res == expected_result 939 | 940 | self.mock_http_async_client().fetch.side_effect = self.mock_async_fetcher(mon_response) 941 | expected_result = SensorSampleValueTime(sample_time=222.222, value_time=111.111, 942 | value=43680.0, status=u'nominal') 943 | res = yield self._portal_client.sensor_value( 944 | "tfrmon_tfr_m018_l_band_offset_average", 945 | include_value_ts=True) 946 | assert res == expected_result 947 | 948 | @gen_test 949 | def test_sensor_values_invalid_results(self): 950 | """test that we handle the monitor server returning an invalid string""" 951 | self.mock_http_async_client().fetch.side_effect = self.mock_async_fetcher('') 952 | with self.assertRaises(InvalidResponseError): 953 | yield self._portal_client.sensor_values("INVALID_FILTER") 954 | 955 | @gen_test 956 | def test_sensor_values_no_results(self): 957 | """Test that we handle no matches""" 958 | self.mock_http_async_client().fetch.side_effect = self.mock_async_fetcher('[]') 959 | with self.assertRaises(SensorNotFoundError): 960 | yield self._portal_client.sensor_values("INVALID_FILTER") 961 | 962 | @gen_test 963 | def test_sensor_values_one_filter_multiple_matches(self): 964 | """Test that we handle multiple matches correctly with one filter""" 965 | 966 | mon_response = ('[{"status":"nominal",' 967 | '"name":"tfrmon_tfr_m018_l_band_offset","component":"tfrmon",' 968 | '"value":43680.0,' 969 | '"value_ts":1530713112,"time":1531302437},' 970 | '{"status":"nominal",' 971 | '"name":"some_other_sample","component":"tfrmon","value":43680.0,' 972 | '"value_ts":111.111,"time":222.222}]') 973 | 974 | self.mock_http_async_client().fetch.side_effect = self.mock_async_fetcher(mon_response) 975 | result = yield self._portal_client.sensor_values("ARBITRARY_FILTER") 976 | expected_result = { 977 | "tfrmon_tfr_m018_l_band_offset": SensorSample(sample_time=1531302437, 978 | value=43680.0, 979 | status='nominal'), 980 | "some_other_sample": SensorSample(sample_time=222.222, 981 | value=43680.0, 982 | status='nominal')} 983 | assert result == expected_result 984 | 985 | self.mock_http_async_client().fetch.side_effect = self.mock_async_fetcher(mon_response) 986 | result = yield self._portal_client.sensor_values("ARBITRARY_FILTER", 987 | include_value_ts=True) 988 | expected_result = { 989 | "tfrmon_tfr_m018_l_band_offset": SensorSampleValueTime(sample_time=1531302437, 990 | value_time=1530713112, 991 | value=43680.0, 992 | status='nominal'), 993 | "some_other_sample": SensorSampleValueTime(sample_time=222.222, 994 | value_time=111.111, 995 | value=43680.0, 996 | status='nominal')} 997 | assert result == expected_result 998 | 999 | @gen_test 1000 | def test_sensor_values_multiple_filters_multiple_matches(self): 1001 | """Test that we handle multiple matches correctly with multiple filters""" 1002 | 1003 | mon_response_0 = ('[{"status":"nominal",' 1004 | '"name":"some_other_sample_0",' 1005 | '"component":"anc","value":43480.0,' 1006 | '"value_ts":110.111,"time":220.222}]') 1007 | mon_response_1 = ('[{"status":"nominal",' 1008 | '"name":"some_other_sample_1",' 1009 | '"component":"anc","value":43580.0,' 1010 | '"value_ts":111.111,"time":221.222}]') 1011 | 1012 | self.mock_http_async_client().fetch.side_effect = self.mock_async_fetchers( 1013 | valid_responses=[mon_response_0, mon_response_1], 1014 | invalid_responses=['1error', '2error'], 1015 | containses=["sample_0", "sample_1"] 1016 | ) 1017 | 1018 | expected_result = {"some_other_sample_0": SensorSample(sample_time=220.222, 1019 | value=43480.0, 1020 | status='nominal'), 1021 | "some_other_sample_1": SensorSample(sample_time=221.222, 1022 | value=43580.0, 1023 | status='nominal')} 1024 | res = yield self._portal_client.sensor_values(["some_other_sample_0", 1025 | "some_other_sample_1"]) 1026 | assert res == expected_result, res 1027 | 1028 | @gen_test 1029 | def test_sensor_history_single_sensor_without_value_time(self): 1030 | """Test that time ordered data without value_time is received for a 1031 | single sensor request. 1032 | """ 1033 | history_base_url = self._portal_client.sitemap[ 1034 | 'historic_sensor_values'] 1035 | sensor_name = 'anc_mean_wind_speed' 1036 | 1037 | self.mock_http_async_client().fetch.side_effect = self.mock_async_fetcher( 1038 | valid_response=sensor_data1, 1039 | invalid_response='error', 1040 | starts_with=history_base_url, 1041 | contains=sensor_name) 1042 | 1043 | samples = yield self._portal_client.sensor_history( 1044 | sensor_name, start_time_sec=0, end_time_sec=time.time(), 1045 | include_value_ts=False) 1046 | # expect exactly 4 samples 1047 | self.assertTrue(len(samples) == 4) 1048 | 1049 | # ensure time order is increasing 1050 | time_sec = 0.0 1051 | for sample in samples: 1052 | self.assertGreater(sample.sample_time, time_sec) 1053 | time_sec = sample.sample_time 1054 | # Ensure sample contains sample_time, value, status 1055 | self.assertEqual(len(sample), 3) 1056 | 1057 | @gen_test 1058 | def test_sensor_history_single_sensor_with_value_time(self): 1059 | """Test that time ordered data with value_time is received for a single sensor request.""" 1060 | history_base_url = self._portal_client.sitemap[ 1061 | 'historic_sensor_values'] 1062 | sensor_name = 'anc_mean_wind_speed' 1063 | self.mock_http_async_client().fetch.side_effect = self.mock_async_fetcher( 1064 | valid_response=sensor_data3, 1065 | invalid_response='error', 1066 | starts_with=history_base_url, 1067 | contains=sensor_name) 1068 | 1069 | samples = yield self._portal_client.sensor_history( 1070 | sensor_name, start_time_sec=0, end_time_sec=time.time(), include_value_ts=True) 1071 | # expect exactly 3 samples 1072 | self.assertTrue(len(samples) == 3) 1073 | 1074 | # ensure time order is increasing 1075 | time_sec = 0 1076 | for sample in samples: 1077 | self.assertGreater(sample.sample_time, time_sec) 1078 | time_sec = sample.sample_time 1079 | # Ensure sample contains sample_time, value_time, value, status 1080 | self.assertEqual(len(sample), 4) 1081 | # Ensure value_time 1082 | self.assertGreater(sample.sample_time, sample.value_time) 1083 | 1084 | @gen_test 1085 | def test_sensor_history_single_sensor_valid_times(self): 1086 | """Test that time ordered data is received for a single sensor request.""" 1087 | history_base_url = self._portal_client.sitemap[ 1088 | 'historic_sensor_values'] 1089 | sensor_name = 'anc_mean_wind_speed' 1090 | self.mock_http_async_client().fetch.side_effect = self.mock_async_fetcher( 1091 | valid_response=sensor_data3, 1092 | invalid_response='error', 1093 | starts_with=history_base_url, 1094 | contains=sensor_name) 1095 | 1096 | samples = yield self._portal_client.sensor_history( 1097 | sensor_name, start_time_sec=0, end_time_sec=time.time()) 1098 | # expect exactly 3 samples 1099 | self.assertTrue(len(samples) == 3) 1100 | 1101 | # ensure time order is increasing 1102 | time_sec = 0 1103 | for sample in samples: 1104 | self.assertGreater(sample[0], time_sec) 1105 | time_sec = sample[0] 1106 | 1107 | @gen_test 1108 | def test_sensor_history_single_sensor_invalid_times(self): 1109 | """Test that no data is received for a single sensor request.""" 1110 | history_base_url = self._portal_client.sitemap[ 1111 | 'historic_sensor_values'] 1112 | sensor_name = 'anc_mean_wind_speed' 1113 | 1114 | self.mock_http_async_client().fetch.side_effect = self.mock_async_fetcher( 1115 | valid_response=sensor_data_fail, 1116 | invalid_response='error', 1117 | starts_with=history_base_url, 1118 | contains=sensor_name) 1119 | 1120 | samples = yield self._portal_client.sensor_history( 1121 | sensor_name, start_time_sec=0, end_time_sec=100) 1122 | # expect no samples 1123 | self.assertTrue(len(samples) == 0) 1124 | 1125 | @gen_test 1126 | def test_sensor_history_multiple_sensors_valid_times(self): 1127 | """Test that time ordered data is received for a multiple sensor request.""" 1128 | history_base_url = self._portal_client.sitemap[ 1129 | 'historic_sensor_values'] 1130 | sensor_name_filter = 'anc_.*_wind_speed' 1131 | sensor_names = ['anc_gust_wind_speed', 'anc_mean_wind_speed'] 1132 | # complicated way to define the behaviour for the 3 expected HTTP requests 1133 | # - 1st call gives sensor list 1134 | # - 2nd call provides the sample history for sensor 0 1135 | # - 3rd call provides the sample history for sensor 1 1136 | self.mock_http_async_client().fetch.side_effect = self.mock_async_fetchers( 1137 | valid_responses=[ 1138 | '{{"data" : [{}, {}]}}'.format(sensor_json[sensor_names[0]], 1139 | sensor_json[sensor_names[1]]), 1140 | sensor_data2, sensor_data1 1141 | ], 1142 | invalid_responses=['1error', '2error', '3error'], 1143 | starts_withs=history_base_url, 1144 | containses=[ 1145 | quote_plus(sensor_name_filter), 1146 | sensor_names[0], 1147 | sensor_names[1]]) 1148 | 1149 | histories = yield self._portal_client.sensors_histories(sensor_name_filter, 1150 | start_time_sec=0, 1151 | end_time_sec=time.time()) 1152 | # expect exactly 2 lists of samples 1153 | self.assertTrue(len(histories) == 2) 1154 | # expect keys to match the 2 sensor names 1155 | self.assertIn(sensor_names[0], list(histories.keys())) 1156 | self.assertIn(sensor_names[1], list(histories.keys())) 1157 | # expect 3 samples for 1st, and 4 samples for 2nd 1158 | self.assertTrue(len(histories[sensor_names[0]]) == 3) 1159 | self.assertTrue(len(histories[sensor_names[1]]) == 4) 1160 | 1161 | # ensure time order is increasing, per sensor 1162 | for sensor in histories: 1163 | time_sec = 0 1164 | for sample in histories[sensor]: 1165 | self.assertGreater(sample[0], time_sec) 1166 | time_sec = sample[0] 1167 | 1168 | @gen_test 1169 | def test_sensor_history_multiple_sensor_futures(self): 1170 | """Test multiple sensor requests in list of futures.""" 1171 | history_base_url = self._portal_client.sitemap[ 1172 | 'historic_sensor_values'] 1173 | sensor_names = ['anc_mean_wind_speed', 'anc_gust_wind_speed'] 1174 | 1175 | # complicated way to define the behaviour for the 2 expected HTTP requests 1176 | # - 1st call provides the sample history for sensor 0 1177 | # - 2nd call provides the sample history for sensor 1 1178 | self.mock_http_async_client().fetch.side_effect = self.mock_async_fetchers( 1179 | valid_responses=[ 1180 | sensor_data4, 1181 | sensor_data3], 1182 | invalid_responses=['1error', '2error'], 1183 | starts_withs=history_base_url, 1184 | containses=[ 1185 | sensor_names[0], 1186 | sensor_names[1]]) 1187 | 1188 | futures = [] 1189 | futures.append(self._portal_client.sensor_history( 1190 | sensor_names[0], start_time_sec=0, end_time_sec=time.time())) 1191 | futures.append(self._portal_client.sensor_history( 1192 | sensor_names[1], start_time_sec=0, end_time_sec=time.time())) 1193 | yield futures 1194 | histories = {} 1195 | for future, sensor_name in zip(futures, sensor_names): 1196 | histories[sensor_name] = future.result() 1197 | # expect exactly 2 lists of samples 1198 | self.assertTrue(len(histories) == 2) 1199 | # expect keys to match the 2 sensor names 1200 | self.assertIn(sensor_names[0], list(histories.keys())) 1201 | self.assertIn(sensor_names[1], list(histories.keys())) 1202 | # expect 4 samples for 1st, and 3 samples for 2nd 1203 | self.assertTrue(len(histories[sensor_names[0]]) == 4) 1204 | self.assertTrue(len(histories[sensor_names[1]]) == 3) 1205 | 1206 | # ensure time order is increasing, per sensor 1207 | for sensor in histories: 1208 | time_sec = 0 1209 | for sample in histories[sensor]: 1210 | self.assertGreater(sample[0], time_sec) 1211 | time_sec = sample[0] 1212 | 1213 | @gen_test 1214 | def test_future_targets(self): 1215 | sb_base_url = self._portal_client.sitemap['schedule_blocks'] 1216 | sb_id_code_1 = "20160908-0005" 1217 | sb_id_code_2 = "20160908-0006" 1218 | sb_id_code_3 = "20160908-0007" 1219 | 1220 | self.mock_http_async_client().fetch.side_effect = self.mock_async_fetchers( 1221 | valid_responses=[ 1222 | r""" 1223 | {"result": 1224 | {"id_code":"20160908-0005", 1225 | "targets":"" 1226 | } 1227 | }""", 1228 | r""" 1229 | {"result": 1230 | {"id_code":"20160908-0006", 1231 | "targets":"" 1232 | } 1233 | }""", 1234 | r""" 1235 | {"result": 1236 | {"id_code":"20160908-0007", 1237 | "targets":"[{\"key\": \"some json body\"}]" 1238 | } 1239 | }"""], 1240 | invalid_responses=[ 1241 | """{"result":null}""", 1242 | """{"result":null}""", 1243 | """{"result":null}"""], 1244 | starts_withs=sb_base_url, 1245 | containses=[sb_id_code_1, sb_id_code_2, sb_id_code_3]) 1246 | 1247 | with self.assertRaises(ScheduleBlockTargetsParsingError): 1248 | yield self._portal_client.future_targets(sb_id_code_1) 1249 | with self.assertRaises(ScheduleBlockNotFoundError): 1250 | yield self._portal_client.future_targets('bad sb id code') 1251 | targets_list = yield self._portal_client.future_targets(sb_id_code_3) 1252 | self.assertEquals(targets_list, [{u'key': u'some json body'}]) 1253 | 1254 | def test_create_jwt_login_token(self): 1255 | """Test that our jwt encoding works as expected""" 1256 | test_token = create_jwt_login_token( 1257 | email='test@test.test', password='testpassword') 1258 | # test tokens for this test is generated using a the email, password combination 1259 | # and the standard JWT standard RFC 7519, see http://jwt.io 1260 | self.assertEquals( 1261 | test_token, 1262 | b'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6InRlc3RAdGVzdC' 1263 | b'50ZXN0In0.aI9/c3tgy5kaKUMfeVHn/3CWLddz4lZI4yFAqHq/JH0=') 1264 | test_token2 = create_jwt_login_token( 1265 | email='random text should also work, you never know!', 1266 | password='some PeOpl3 have WEIRD pa$$words?') 1267 | self.assertEquals( 1268 | test_token2, 1269 | b'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6InJhbmRvbSB0ZX' 1270 | b'h0IHNob3VsZCBhbHNvIHdvcmssIHlvdSBuZXZlciBrbm93ISJ9.H1aItCXEZfNO' 1271 | b'5CUP3vwKefqdEMBVpnNfMRYah5jPCAA=') 1272 | 1273 | @gen_test 1274 | def test_login(self): 1275 | """Test the login procedure. 1276 | 1. Verify username, password and role. 1277 | 2. Login with the resulting session id token 1278 | 3. Check that the session id is included as an Authorization header in 1279 | subsequent calls""" 1280 | auth_base_url = self._portal_client.sitemap['authorization'] 1281 | authorized_fetch_future = gen.Future() 1282 | self._portal_client.authorized_fetch = mock.MagicMock( 1283 | return_value=authorized_fetch_future) 1284 | auth_fetch_result = HTTPResponse( 1285 | HTTPRequest(auth_base_url), 200, 1286 | buffer=buffer_bytes_io( 1287 | '{"session_id": "token generated by katportal", "user_id": "123"}')) 1288 | authorized_fetch_future.set_result(auth_fetch_result) 1289 | 1290 | yield self._portal_client.login('testusername@test.org', 'testpass') 1291 | self._portal_client.authorized_fetch.assert_called_with( 1292 | auth_token='token generated by katportal', 1293 | url=self._portal_client.sitemap['authorization'] + '/user/login', 1294 | body='', method='POST') 1295 | self.assertEquals(self._portal_client._session_id, 1296 | 'token generated by katportal') 1297 | self.assertEquals(self._portal_client._current_user_id, '123') 1298 | 1299 | # Test a failed login 1300 | authorized_fetch_fail_future = gen.Future() 1301 | auth_fetch_fail_result = HTTPResponse( 1302 | HTTPRequest(auth_base_url), 200, 1303 | buffer=buffer_bytes_io('{"logged_in": "False"}')) 1304 | self._portal_client.authorized_fetch = mock.MagicMock( 1305 | return_value=authorized_fetch_fail_future) 1306 | authorized_fetch_fail_future.set_result(auth_fetch_fail_result) 1307 | self._portal_client.authorized_fetch.set_result(auth_fetch_fail_result) 1308 | yield self._portal_client.login('fail username', 'fail pass') 1309 | # test tokens for this test is generated using a the email, password combination 1310 | # and the standard JWT standard RFC 7519, see http://jwt.io 1311 | self._portal_client.authorized_fetch.assert_called_with( 1312 | auth_token=b'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImZ' 1313 | b'haWwgdXNlcm5hbWUifQ.IWU7Asuevn8Skm+qU7GJPuhLFoCvG47A' 1314 | b'M7lyRQfAbT0=', 1315 | url='http://0.0.0.0/katauth/user/verify/read_only') 1316 | self.assertEquals(self._portal_client._session_id, None) 1317 | self.assertEquals(self._portal_client._current_user_id, None) 1318 | 1319 | @gen_test 1320 | def test_logout(self): 1321 | """Test logout procedure 1322 | 1. Login 1323 | 2. Logout 1324 | 3. Check if _session_id and _current_user_id has been cleared""" 1325 | auth_base_url = self._portal_client.sitemap['authorization'] 1326 | # login 1327 | authorized_fetch_future = gen.Future() 1328 | self._portal_client.authorized_fetch = mock.MagicMock( 1329 | return_value=authorized_fetch_future) 1330 | auth_fetch_result = HTTPResponse( 1331 | HTTPRequest(auth_base_url), 200, 1332 | buffer=buffer_bytes_io( 1333 | '{"session_id": "token generated by katportal", "user_id": "123"}')) 1334 | authorized_fetch_future.set_result(auth_fetch_result) 1335 | 1336 | yield self._portal_client.login('testusername@test.org', 'testpass') 1337 | self._portal_client.authorized_fetch.assert_called_with( 1338 | auth_token='token generated by katportal', 1339 | url=self._portal_client.sitemap['authorization'] + '/user/login', 1340 | body='', method='POST') 1341 | self.assertEquals(self._portal_client._session_id, 1342 | 'token generated by katportal') 1343 | self.assertEquals(self._portal_client._current_user_id, '123') 1344 | 1345 | # logout 1346 | yield self._portal_client.logout() 1347 | self.assertEquals(self._portal_client._session_id, None) 1348 | self.assertEquals(self._portal_client._current_user_id, None) 1349 | self._portal_client.authorized_fetch.assert_called_with( 1350 | auth_token='token generated by katportal', 1351 | url=self._portal_client.sitemap['authorization'] + '/user/logout', 1352 | body='{}', method='POST') 1353 | 1354 | @gen_test 1355 | def test_userlog_tags(self): 1356 | """Test userlogs tags listing""" 1357 | base_url = self._portal_client.sitemap['userlogs'] + '/tags' 1358 | 1359 | self.mock_http_async_client().fetch.side_effect = self.mock_async_fetchers( 1360 | valid_responses=[r"""[{ 1361 | "activated": "True", 1362 | "slug": "", 1363 | "name": "m047", 1364 | "id": "1" 1365 | },{ 1366 | "activated": "True", 1367 | "slug": "", 1368 | "name": "m046", 1369 | "id": "2" 1370 | }]"""], 1371 | invalid_responses=['error'], 1372 | starts_withs=base_url) 1373 | 1374 | tags = yield self._portal_client.userlog_tags() 1375 | self.assertEquals(len(tags), 2) 1376 | self.assertEquals(tags[0]['id'], '1') 1377 | self.assertEquals(tags[1]['id'], '2') 1378 | 1379 | @gen_test 1380 | def test_userlogs(self): 1381 | """Test userlogs listing""" 1382 | # fake a login 1383 | self._portal_client._session_id = 'some token' 1384 | self._portal_client._current_user_id = 1 1385 | # then list userlogs 1386 | 1387 | userlogs_base_url = self._portal_client.sitemap['userlogs'] 1388 | authorized_fetch_future = gen.Future() 1389 | self._portal_client.authorized_fetch = mock.MagicMock( 1390 | return_value=authorized_fetch_future) 1391 | auth_fetch_result = HTTPResponse( 1392 | HTTPRequest(userlogs_base_url), 200, 1393 | buffer=buffer_bytes_io( 1394 | r""" 1395 | [{ 1396 | "other_metadata": "[]", 1397 | "user_id": "1", 1398 | "attachments": "[]", 1399 | "tags": "[]", 1400 | "timestamp": "2017-02-07 08:47:22", 1401 | "start_time": "2017-02-07 00:00:00", 1402 | "modified": "", 1403 | "content": "katportalclient userlog creation content!", 1404 | "parent_id": "", 1405 | "user": {"email": "cam@ska.ac.za", "id": 1, "name": "CAM"}, 1406 | "attachment_count": "0", 1407 | "id": "40", 1408 | "end_time": "2017-02-07 23:59:59" 1409 | },{ 1410 | "other_metadata": [], 1411 | "user_id": 2, 1412 | "attachments": [], 1413 | "tags": "[]", 1414 | "timestamp": "2017-02-07 01:00:00", 1415 | "start_time": "2017-02-07 00:00:00", 1416 | "modified": "", 1417 | "content": "katportalclient userlog creation content 2!", 1418 | "parent_id": "", 1419 | "user": {"email": "cam2@ska.ac.za", "id": 2, "name": "CAM2"}, 1420 | "attachment_count": "0", 1421 | "id": "41", 1422 | "end_time": "2017-02-07 23:59:59" 1423 | }] 1424 | """)) 1425 | authorized_fetch_future.set_result(auth_fetch_result) 1426 | 1427 | userlogs = yield self._portal_client.userlogs() 1428 | self.assertEquals(len(userlogs), 2) 1429 | self.assertEquals(userlogs[0]['id'], '40') 1430 | self.assertEquals(userlogs[1]['id'], '41') 1431 | 1432 | @gen_test 1433 | def test_create_userlog(self): 1434 | """Test userlog creation""" 1435 | # fake a login 1436 | self._portal_client._session_id = 'some token' 1437 | self._portal_client._current_user_id = 1 1438 | # then list userlogs 1439 | 1440 | userlogs_base_url = self._portal_client.sitemap['userlogs'] 1441 | authorized_fetch_future = gen.Future() 1442 | self._portal_client.authorized_fetch = mock.MagicMock( 1443 | return_value=authorized_fetch_future) 1444 | auth_fetch_result = HTTPResponse( 1445 | HTTPRequest(userlogs_base_url), 200, 1446 | buffer=buffer_bytes_io( 1447 | r""" 1448 | { 1449 | "other_metadata": "[]", 1450 | "user_id": "1", 1451 | "attachments": "[]", 1452 | "tags": "[]", 1453 | "timestamp": "2017-02-07 08:47:22", 1454 | "start_time": "2017-02-07 00:00:00", 1455 | "modified": "", 1456 | "content": "test content", 1457 | "parent_id": "", 1458 | "user": {"email": "cam@ska.ac.za", "id": 1, "name": "CAM"}, 1459 | "attachment_count": "0", 1460 | "id": "40", 1461 | "end_time": "2017-02-07 23:59:59" 1462 | } 1463 | """)) 1464 | authorized_fetch_future.set_result(auth_fetch_result) 1465 | 1466 | userlog = yield self._portal_client.create_userlog( 1467 | content='test content', 1468 | tag_ids=[1, 2, 3], 1469 | start_time='2017-02-07 08:47:22', 1470 | end_time='2017-02-07 08:47:22') 1471 | self.assertEquals( 1472 | userlog, 1473 | {u'other_metadata': u'[]', u'user_id': u'1', u'attachments': u'[]', 1474 | u'tags': u'[]', u'timestamp': u'2017-02-07 08:47:22', 1475 | u'start_time': u'2017-02-07 00:00:00', u'modified': u'', 1476 | u'content': u'test content', 1477 | u'parent_id': u'', 1478 | u'user': {u'email': u'cam@ska.ac.za', u'name': u'CAM', u'id': 1}, 1479 | u'attachment_count': u'0', u'id': u'40', 1480 | u'end_time': u'2017-02-07 23:59:59'}) 1481 | 1482 | self._portal_client.authorized_fetch.assert_called_once_with( 1483 | auth_token='some token', 1484 | body=mock.ANY, 1485 | method='POST', 1486 | url=self._portal_client.sitemap['userlogs']) 1487 | call_kwargs = self._portal_client.authorized_fetch.call_args[1] 1488 | actual_body_dict = json.loads(call_kwargs['body']) 1489 | expected_body_dict = { 1490 | "content": "test content", 1491 | "tag_ids": [1, 2, 3], 1492 | "start_time": "2017-02-07 08:47:22", 1493 | "user": self._portal_client._current_user_id, 1494 | "end_time": "2017-02-07 08:47:22"} 1495 | self.assertDictEqual(actual_body_dict, expected_body_dict) 1496 | 1497 | @gen_test 1498 | def test_modify_userlog(self): 1499 | """Test userlog modification""" 1500 | # fake a login 1501 | self._portal_client._session_id = 'some token' 1502 | self._portal_client._current_user_id = 1 1503 | # then list userlogs 1504 | 1505 | userlogs_base_url = self._portal_client.sitemap['userlogs'] 1506 | userlog_fetch_future = gen.Future() 1507 | self._portal_client.authorized_fetch = mock.MagicMock( 1508 | return_value=userlog_fetch_future) 1509 | fetch_result = HTTPResponse( 1510 | HTTPRequest(userlogs_base_url), 200, 1511 | buffer=buffer_bytes_io( 1512 | r""" 1513 | { 1514 | "other_metadata": "[]", 1515 | "user_id": "1", 1516 | "attachments": "[]", 1517 | "tags": "[]", 1518 | "timestamp": "2017-02-07 08:47:22", 1519 | "start_time": "2017-02-07 00:00:00", 1520 | "modified": "", 1521 | "content": "test content modified", 1522 | "parent_id": "", 1523 | "user": {"email": "cam@ska.ac.za", "id": 1, "name": "CAM"}, 1524 | "attachment_count": "0", 1525 | "id": "40", 1526 | "end_time": "2017-02-07 23:59:59" 1527 | } 1528 | """)) 1529 | userlog_fetch_future.set_result(fetch_result) 1530 | 1531 | userlog_to_modify = { 1532 | 'other_metadata': [], 1533 | 'user_id': 1, 1534 | 'attachments': [], 1535 | 'tags': '[]', 1536 | 'timestamp': '2017-02-07 08:47:22', 1537 | 'start_time': '2017-02-07 00:00:00', 1538 | 'modified': '', 1539 | 'content': 'katportalclient userlog modified content!', 1540 | 'parent_id': '', 1541 | 'user': {'email': 'cam@ska.ac.za', 'id': 1, 'name': 'CAM'}, 1542 | 'attachment_count': 0, 1543 | 'id': 40, 1544 | 'end_time': '2017-02-07 23:59:59' 1545 | } 1546 | userlog = yield self._portal_client.modify_userlog(userlog_to_modify, [1, 2, 3]) 1547 | self.assertEquals( 1548 | userlog, 1549 | {u'other_metadata': u'[]', u'user_id': u'1', u'attachments': u'[]', 1550 | u'tags': u'[]', u'timestamp': u'2017-02-07 08:47:22', 1551 | u'start_time': u'2017-02-07 00:00:00', u'modified': u'', 1552 | u'content': u'test content modified', 1553 | u'parent_id': u'', 1554 | u'user': {u'email': u'cam@ska.ac.za', u'name': u'CAM', u'id': 1}, 1555 | u'attachment_count': u'0', u'id': u'40', 1556 | u'end_time': u'2017-02-07 23:59:59'}) 1557 | 1558 | self._portal_client.authorized_fetch.assert_called_once_with( 1559 | auth_token='some token', 1560 | body=mock.ANY, 1561 | method='POST', 1562 | url='{}/{}'.format( 1563 | self._portal_client.sitemap['userlogs'], userlog_to_modify['id'])) 1564 | call_kwargs = self._portal_client.authorized_fetch.call_args[1] 1565 | actual_body_dict = json.loads(call_kwargs['body']) 1566 | self.assertDictEqual(actual_body_dict, userlog_to_modify) 1567 | 1568 | # Test bad tags attribute 1569 | with self.assertRaises(ValueError): 1570 | userlog_to_modify['tags'] = 'random nonsense' 1571 | userlog = yield self._portal_client.modify_userlog(userlog_to_modify) 1572 | 1573 | @gen_test 1574 | def test_sensor_subarray_lookup(self): 1575 | """Test sensor subarray lookup is correctly extracted.""" 1576 | lookup_base_url = (self._portal_client.sitemap['subarray'] + 1577 | '/3/sensor-lookup/cbf/device_status/0') 1578 | sensor_name_filter = 'device_status' 1579 | expected_sensor_name = 'cbf_3_device_status' 1580 | 1581 | self.mock_http_async_client().fetch.side_effect = self.mock_async_fetcher( 1582 | valid_response='{"result":"cbf_3_device_status"}', 1583 | invalid_response=['error'], 1584 | starts_with=lookup_base_url, 1585 | contains=sensor_name_filter) 1586 | sensor = yield self._portal_client.sensor_subarray_lookup( 1587 | 'cbf', sensor_name_filter, False) 1588 | self.assertTrue(sensor == expected_sensor_name) 1589 | 1590 | @gen_test 1591 | def test_sensor_subarray_katcp_name_lookup(self): 1592 | """Test sensor subarray lookup returns the correct katcp name.""" 1593 | lookup_base_url = (self._portal_client.sitemap['subarray'] + 1594 | '/3/sensor-lookup/cbf/device-status/1') 1595 | sensor_name_filter = 'device-status' 1596 | expected_sensor_name = 'cbf_3.device-status' 1597 | 1598 | self.mock_http_async_client().fetch.side_effect = self.mock_async_fetcher( 1599 | valid_response='{"result":"cbf_3.device-status"}', 1600 | invalid_response=['error'], 1601 | starts_with=lookup_base_url, 1602 | contains=sensor_name_filter) 1603 | sensor = yield self._portal_client.sensor_subarray_lookup( 1604 | 'cbf', sensor_name_filter, True) 1605 | self.assertTrue(sensor == expected_sensor_name) 1606 | 1607 | @gen_test 1608 | def test_sensor_subarray_invalid_sensor_lookup(self): 1609 | """Test that sensor subarray lookup can correctly handle an invalid sensor name.""" 1610 | lookup_base_url = (self._portal_client.sitemap['subarray'] + 1611 | '/3/sensor-lookup/anc/device_status/0') 1612 | sensor_name_filter = 'device_status' 1613 | self.mock_http_async_client().fetch.side_effect = self.mock_async_fetcher( 1614 | valid_response='{"error":"SensorLookupError: Could not lookup the sensor ' 1615 | 'on component. Could not determine component."}', 1616 | invalid_response='[]', 1617 | starts_with=lookup_base_url, 1618 | contains=sensor_name_filter) 1619 | with self.assertRaises(SensorLookupError): 1620 | yield self._portal_client.sensor_subarray_lookup( 1621 | 'anc', sensor_name_filter, False) 1622 | 1623 | 1624 | def mock_async_fetchers(self, valid_responses, invalid_responses, starts_withs=None, 1625 | ends_withs=None, containses=None): 1626 | """Allows definition of multiple HTTP async fetchers.""" 1627 | num_calls = len(valid_responses) 1628 | if starts_withs is None or isinstance(starts_withs, basestring): 1629 | starts_withs = [starts_withs] * num_calls 1630 | if ends_withs is None or isinstance(ends_withs, basestring): 1631 | ends_withs = [ends_withs] * num_calls 1632 | if containses is None or isinstance(containses, basestring): 1633 | containses = [containses] * num_calls 1634 | assert(len(invalid_responses) == num_calls) 1635 | assert(len(starts_withs) == num_calls) 1636 | assert(len(ends_withs) == num_calls) 1637 | assert(len(containses) == num_calls) 1638 | mock_fetches = [self.mock_async_fetcher(v, i, s, e, c) 1639 | for v, i, s, e, c in zip( 1640 | valid_responses, invalid_responses, 1641 | starts_withs, ends_withs, containses)] 1642 | # flip order so that poping effectively goes from first to last input 1643 | mock_fetches.reverse() 1644 | 1645 | def mock_fetch(url): 1646 | if url == SITEMAP_URL: 1647 | # Don't consume from the list, because it's not the request 1648 | # we're looking for. 1649 | single_fetch = mock_fetches[-1] 1650 | else: 1651 | single_fetch = mock_fetches.pop() 1652 | return single_fetch(url) 1653 | 1654 | return mock_fetch 1655 | 1656 | 1657 | def mock_async_fetcher(self, valid_response, invalid_response=None, starts_with=None, 1658 | ends_with=None, contains=None): 1659 | """Returns a mock HTTP async fetch function, depending on the conditions.""" 1660 | 1661 | def mock_fetch(url, method="GET", body=None): 1662 | if url == SITEMAP_URL: 1663 | sitemap = {'client': 1664 | {'websocket': self.websocket_url, 1665 | 'historic_sensor_values': r"http://0.0.0.0/history", 1666 | 'schedule_blocks': r"http://0.0.0.0/sb", 1667 | 'capture_blocks': r"http://0.0.0.0/cb", 1668 | 'subarray_sensor_values': r"http://0.0.0.0/sensor-list", 1669 | 'target_descriptions': r"http://0.0.0.0/sources", 1670 | 'sub_nr': '3', 1671 | 'authorization': r"http://0.0.0.0/katauth", 1672 | 'userlogs': r"http://0.0.0.0/katcontrol/userlogs", 1673 | 'subarray': r"http:/0.0.0.0/katcontrol/subarray", 1674 | 'monitor': r"http:/0.0.0.0/katmonitor", 1675 | } 1676 | } 1677 | response = json.dumps(sitemap) 1678 | else: 1679 | start_ok = starts_with is None or url.startswith(starts_with) 1680 | end_ok = ends_with is None or url.endswith(ends_with) 1681 | contains_ok = contains is None or contains in url 1682 | 1683 | if (start_ok and end_ok and contains_ok) or invalid_response is None: 1684 | response = valid_response 1685 | else: 1686 | response = invalid_response 1687 | 1688 | body_buffer = buffer_bytes_io(response) 1689 | result = HTTPResponse(HTTPRequest(url), 200, buffer=body_buffer) 1690 | future = concurrent.Future() 1691 | future.set_result(result) 1692 | return future 1693 | 1694 | 1695 | return mock_fetch 1696 | 1697 | 1698 | def buffer_bytes_io(message): 1699 | """Return a string as BytesIO, in Python 2 and 3.""" 1700 | return io.BytesIO(bytes(message, encoding='utf-8')) 1701 | --------------------------------------------------------------------------------