#!/usr/bin/env python
#
# License: BSD
# https://raw.github.com/robotics-in-py/rocon_app_platform/license/LICENSE
#
#################################################################################
from __future__ import division, print_function
import copy
import os
import tarfile
import tempfile
import rocon_python_utils
import rocon_uri
import rospkg
from .exceptions import *
from .rapp import Rapp
import logging
import sys
logger = logging.getLogger('indexer')
logger.addHandler(logging.StreamHandler(sys.stderr))
[docs]class RappIndexer(object):
__slots__ = ['raw_data_path', 'raw_data', 'invalid_data', 'package_whitelist', 'package_blacklist', 'rospack', 'packages_path', 'source']
def __init__(self, raw_data=None, package_whitelist=None, package_blacklist=[], packages_path=None, source=None):
self.packages_path = packages_path
self.raw_data_path = {}
self.raw_data = {}
self.invalid_data = {}
self.package_whitelist = package_whitelist
self.package_blacklist = package_blacklist
self.source = source
self.rospack = rospkg.RosPack()
if raw_data is not None:
self.raw_data = raw_data
else:
self.update_index(package_whitelist, package_blacklist)
def __str__(self):
ret = '-------------------------------\n'
for rapp_name, rapp in self.raw_data.items():
ret += str(rapp_name) + '\n'
for attr_name, attr_path in rapp.raw_data.items():
ret += ' ' + str(attr_name) + ' : ' + str(attr_path) + '\n'
ret += '--------------------------------'
return ret
[docs] def update_index(self, package_whitelist=None, package_blacklist=[]):
'''
Crawls rocon apps from ROS_PACKAGE_PATH and generates raw_data dictionary.
:param package_whitelist: list of target package list
:type package_whitelist: [str]
:param package_blacklist: list of blacklisted package
:type package_blacklist: [str]
'''
self.raw_data_path, _invalid_path = rocon_python_utils.ros.resource_index_from_package_exports('rocon_app', self.packages_path, package_whitelist, package_blacklist)
raw_data = {}
invalid_data = {}
for resource_name, (path, catkin_package) in self.raw_data_path.items():
try:
r = Rapp(resource_name, self.rospack)
r.load_rapp_yaml_from_file(path)
r.package = catkin_package
raw_data[resource_name] = r
except InvalidRappFieldException as irfe:
invalid_data[resource_name] = str(irfe)
except InvalidRappException as ire:
invalid_data[resource_name] = str(ire)
except RappResourceNotExistException as e:
invalid_data[resource_name] = str(e)
self.raw_data = raw_data
self.invalid_data = invalid_data
self.package_whitelist = package_whitelist
self.package_blacklist = package_blacklist
def get_package_whitelist_blacklist(self):
return self.package_whitelist, self.package_blacklist
[docs] def get_raw_rapp(self, rapp_name):
'''
returns rapp instance of given name
:param rapp_name: rapp name
:type rapp_name: str
:returns: rapp
:rtype: rocon_app_utilities.Rapp
:raises: RappNotExistException: the given rapp name does not exist
'''
if rapp_name not in self.raw_data:
raise RappNotExistException(str(rapp_name) + ' does not exist')
return self.raw_data[rapp_name]
[docs] def get_rapp(self, rapp_name):
'''
returns complete rapp instance which includes inherited attributes from its parent
:param rapp name: Rapp name
:type rapp_name: str
:returns: rapp instance
:rtype: rocon_app_utilities.rapp.Rapp
:raises: RappNotExistException: the given rapp name does not exist
'''
if rapp_name not in self.raw_data:
raise RappNotExistException(str(rapp_name) + ' does not exist')
rapp = self._resolve(rapp_name)
rapp.load_rapp_specs_from_file()
return rapp
[docs] def get_compatible_rapps(self, uri=rocon_uri.default_uri_string, ancestor_share_check=False):
'''
returns all rapps which are compatible with given URI
:param uri: Rocon URI
:type uri: str
:returns: a dict of compatible rapps, a dict of incompatible rapps, a dict of invalid rapps
:rtype: {resource_name:rocon_app_utilities.Rapp}, {resource_name:rocon_app_utilities.Rapp}, {resource_name:str}
'''
compatible_rapps = {}
incompatible_rapps = {}
invalid_rapps = {}
for resource_name, rapp in self.raw_data.items():
if not rapp.is_implementation:
continue
try:
if rapp.is_compatible(uri):
compatible_rapps[resource_name] = rapp
else:
incompatible_rapps[resource_name] = rapp
except rocon_uri.exceptions.RoconURIValueError as e:
invalid_rapps[resource_name] = str(e)
resolved_compatible_rapps, invalid_compatible = self._resolve_rapplist(compatible_rapps, ancestor_share_check)
resolved_incompatible_rapps, invalid_incompatible = self._resolve_rapplist(incompatible_rapps, ancestor_share_check)
invalid_rapps.update(invalid_compatible)
invalid_rapps.update(invalid_incompatible)
for resource_name, rapp in resolved_compatible_rapps.items():
try:
rapp.load_rapp_specs_from_file()
except RappResourceNotExistException as e:
invalid_rapps[resource_name] = str(e)
except RappMalformedException as e:
invalid_rapps[resource_name] = str(e)
for resource_name in invalid_rapps:
if resource_name in resolved_compatible_rapps:
del resolved_compatible_rapps[resource_name]
if hasattr(self, 'invalid_data'):
invalid_rapps.update(self.invalid_data)
return resolved_compatible_rapps, resolved_incompatible_rapps, invalid_rapps
def _resolve_rapplist(self, rapps, ancestor_share_check):
'''
resolve full spec of given dict of rapps
:param rapps: list of rapps
:type dict
:returns: resolved rapps, invalid rapps
:rtypes: {}, {}
'''
resolved = {}
used_ancestors = {}
invalid = {}
for resource_name, unused_rapp in rapps.items():
try:
resolved_rapp = self._resolve(resource_name)
ancestor_name = resolved_rapp.ancestor_name
if ancestor_share_check and ancestor_name in used_ancestors:
invalid[resource_name] = "ancestor has already been taken by other rapp"
else:
resolved[resource_name] = resolved_rapp
used_ancestors[ancestor_name] = resource_name
except ParentRappNotFoundException as e:
invalid[resource_name] = str('parent rapp not found (is it in your rapp_package_whitelist?) [%s]' % str(e.parent_name))
except RappInvalidChainException as e:
invalid[resource_name] = str(e)
return resolved, invalid
def _resolve(self, rapp_name):
'''
resolve the rapp instance with its parent specification and return a runnable rapp
:param rapp name: Rapp name
:type rapp_name: str
:returns: fully resolved rapp
:rtype: rocon_app_utilities.Rapp
'''
rapp = copy.deepcopy(self.raw_data[rapp_name]) # Not to currupt original data
parent_name = rapp.parent_name
stack = []
stack.append(rapp.resource_name)
rapp, ancestor_name = self._resolve_recursive(rapp, parent_name, stack)
rapp.ancestor_name = ancestor_name
return rapp
def _resolve_recursive(self, rapp, parent_name, stack):
'''
Internal method of _resolve
:raises: RappInvalidChainException: Rapp is implmentation child but does not have parent
:raises: ParentRappNotFoundException: Its parent does not exist
'''
if rapp.is_ancestor and rapp.is_implementation:
return rapp, stack.pop()
if not parent_name:
raise RappInvalidChainException('Invalid Rapp Chain from [' + str(rapp) + ']')
if not parent_name in self.raw_data:
raise ParentRappNotFoundException(rapp.resource_name, parent_name)
if parent_name in stack:
raise RappCyclicChainException(stack)
parent = self.raw_data[parent_name]
rapp.inherit(parent)
stack.append(parent.resource_name)
return self._resolve_recursive(rapp, parent.parent_name, stack)
[docs] def to_dot(self):
'''
returns the dot graph format. Not Implemented Yet.
'''
raise NotImplementedError()
# TODO
pass
[docs] def merge(self, other_indexer):
'''
Updates this index with the rapps from the other_indexer.
:param other_indexer: the other inder
:type other_indexer: rocon_app_utilities.RappIndexer
'''
self.raw_data.update(other_indexer.raw_data)
self.raw_data_path.update(other_indexer.raw_data_path)
# Cleanup 'invalid' invalid data before merge
self.invalid_data = {k: v for k, v in self.invalid_data.items() if k not in self.raw_data}
self.invalid_data.update(other_indexer.invalid_data)
[docs] def write_tarball(self, filename_prefix):
'''
Writes the index to a gzipped tarball.
:param filename_prefix: the pathname of the archive with out the suffix '.index.tar.gz'
:type filename_prefix: str
'''
RESOURCE_KEYS = ['icon', 'public_interface', 'public_parameters', 'launch']
logger.debug("write_tarball() to '%s...'" % filename_prefix)
added = set([])
with tarfile.open('%s.index.tar.gz' % filename_prefix, 'w:gz') as tar:
for rapp in self.raw_data.values():
# add package.xml file
rapp_package_filename = os.path.normpath(rapp.package.filename)
if rapp_package_filename not in added:
logger.debug("write_tarball() add package.xml '%s" % rapp_package_filename)
tar.add(rapp_package_filename)
added.add(rapp_package_filename)
# add .rapp file
rapp_filename = os.path.normpath(rapp.filename)
if rapp_filename not in added:
logger.debug("write_tarball() add .rapp file '%s" % rapp_filename)
tar.add(rapp_filename)
added.add(rapp_filename)
for value in [v for k, v in rapp.yaml_data.items() if k in RESOURCE_KEYS]:
logger.debug("write_index() value: %s" % str(value))
if value and os.path.exists(value):
normed_path = os.path.normpath(value)
logger.debug("write_index() add resource '%s" % str(normed_path))
tar.add(normed_path)
added.add(normed_path)
else:
logger.debug("write_index() path does not exist %s" % str(value))
def read_tarball(name=None, fileobj=None, package_whitelist=None, package_blacklist=[]):
'''
Reads an index from a gzipped tarball.
:param name: the pathname of the archive
:type name: str
:param fileobj: alternative to a file object opened for name
:type fileobj: file
:param package_whitelist: list of target package list
:type package_whitelist: [str]
:param package_blacklist: list of blacklisted package
:type package_blacklist: [str]
:returns: the index
:rtype: rocon_app_utilities.RappIndexer
'''
# TODO avoid unpacking
logger.debug('read_tarball(name=%s, fileobj=%s)' % (name, fileobj))
tempdir = tempfile.mkdtemp(suffix='_unpacked', prefix='rapp_index_')
try:
logger.debug("read_tarball() unpack to '%s'" % tempdir)
with tarfile.open(name=name, fileobj=fileobj, mode='r:gz') as tar:
tar.extractall(tempdir)
index = RappIndexer(packages_path=tempdir, package_whitelist=package_whitelist, package_blacklist=package_blacklist)
finally:
#shutil.rmtree(tempdir)
pass
return index