Extend VHC

VHC has been designed with extendability in mind: the goal is to allow people to use the available infrastructure but with custom recipes, drivers and reference file parsers. To do this we use entry-points.

Also the builtin recipes, drivers and parsers are distribute through the same infrastructure.

In the following we will describe the entry point groups used by VHC, naming conventions and the interface for each of the groups. We will also provide some example on how to create new entry points and the associated functions.

For the sake of this documentation we assume that the custom code is implemented into a package with the following structure:

vhc_extra
├── setup.py                 # make the package installable
├── vhc_extra                # the actual code
│   ├── __init__.py          # this is a package
│   ├── drivers.py           # module implementing the driver
│   ├── ref_file_parsers.py  # module implementing new file parsers
│   └── recipe.py            # vhc modifications
├── ...
...

The setup.py can look like:

from setuptools import setup, find_packages
# put here anything else you need
# [...]

entry_points = {
# this will be filled as we go
}

setup(
    name="vch_extra",
    version='0.0.0',
    author="My Name",
    author_email='my@email.me',
    description="VHC extension package",
    long_description="This is a long descriptions",
    packages=find_packages(),
    install_requires=[''],  # put here any extra package vhc_extra depends on
    entry_points=entry_points,
    # any other relevant entry
)

Plug new recipes

A recipe tells vhc what kind of files we expect to work on. To a given recipe, we associate a file name template, used to collect files for vhc and to create the recipe file if not available, and a list of default drivers, were a v_driver.txt file not found.

Recipes are advertised under the vhc.recipes group and each should appear as:

name = module:function

where function has the following signature:

libvhc.recipes.recipe_protopype(name)[source]

Signature for a function used to register a recipe.

Parameters:
name : string

name under which the recipe is advertised

Returns:
string

file name template to use to collect all the relevant file or to guess the recipe name from the files; e.g. for the flat recipe [0-9]*flt.fits is returned

list of strings

list of default drivers to run for the recipe, if no v_driver.txt file is found; e.g. ['common:n_files', 'common:saturation']

Let’s say that you want to add a new recipe type virus_ancillary, you need to do the following:

  1. implement a function according to the prototype (in vhc_extra/vhc_extra/recipe.py)

    def virus_ancillary_recipe(name):
        """File names and drivers available for the virus_ancillary recipe"""
        recipe_match = "*[0-9]*sci.fits"
        default_drivers = common_drivers + ["vhc_extra:do_something",
                                            "do_something_else"]
        return recipe_match, default_drivers
    

Once you have a recipe, you can register it adding the following entry in the entry_points dictionary:

entry_points = {
    'vhc.recipes':
    ['virus_ancillary = vhc_extra.recipe:virus_ancillary_recipe']
}

Now, after installing vhc_extra your recipe will be available to VHC.

Note

If you want to use the machinery provided by the Factory functions to implement new drivers as described in Create and use a drivers you must add a section in the configuration file with the name of your new recipe and the number of expected exposure/dithers:

[virus_ancillary]
# expected number of exposures
n_exposures = 1

Plug new drivers

In Create and use a drivers we have seen some way to write new drivers. Here we illustrate how to advertise new drivers, so that vhc can pick them up.

Drivers are looked for under the vhc.drivers entry point group and, similarly to what seen for the recipes, each entry must be have name of the driver and the position of the callable implementing it. So for the recipe described in Plug new recipes, we need to create two functions to implement the them: lets say that we write two functions vhc_extra/drivers.py:

def do_something(vcheck, path, argv):
    "implement a driver that does something"
    pass

def an_other_driver(vcheck, path, argv):
    """This driver does something else and it also shows that the driver
    and its function names don't have to be the same: how cunning...
    """

After this is done, we tell python where to find them adding to setup.py:

entry_points = {
    'vhc.drivers':
    ['vhc_extra:do_something = vhc_extra.drivers:do_something',
     'do_something_else = vhc_extra.drivers:an_other_driver'
    ]
}

Now whoever installs the vhc_extra package can use the virus_ancillary recipe and the two new drivers just described.

Plug new reference file parsers

Some of the drivers require some reference values or files to run. We store those values or file names in tables in the vhc_config repository and we use a common framework to parse and get values from them. The framework is described and implemented in libvhc.reference_file_parser.

Every file parser is derived from a common ancestor, _BaseParser, already implementing most of the methods necessary. The only method that needs to be implemented to have a working parser is filename().

Now, let’s say that your do_something_else driver need some reference value from a some_reference file somewhere. To avoid hard coding it in the python implementation you can add it in the configuration file:

[virus_ancillary]
# any other configuration
some_reference = /path/to/some_reference.dat

Then in vhc_extra/ref_file_parsers.py we can create the parser:

import libvhc.reference_file_parser as vhcrfp

class MyParser(vhcrfp._BaseParser):
    def filename(self):
        """returns the file name from the configuration"""
        return self.conf.get("vhc_extra", "some_reference")

    def parse_value(self, str_value):
        """parse the extra columns at need"""
        return str_value.split()

Now we need to advertise the parser using the vhc.file_parsers entry point:

entry_points = {
    'vhc.file_parsers':
    ['some_reference = vhc_extra.ref_file_parsers:MyParser']
    }

Once the parser is advertised, it’s possible to get it in your driver with the picker() function:

import libvhc.reference_file_parser as vhcrfp
[...]
parser = vhcrfp.picker('some_reference')
values = parser.get_value(001, 'L', 'U')



Bonus

If you want to do more you can, probably.

The vhc executable simply calls libvhc.vhc.main(). If you want/need to run some code before and/or after vhc, you can wrap the above function and create a console script. E.g. in a entry_point.py module you can define:

import libvhc.vhc as vhc

def main(argv=None):

    # do the setup that you need
    [...]
    # call vhc
    vhc.main(argv=argv)
    # do any cleanup if needed
    [...]

Then add the entry point in your setup.py file to create a new vhc_extra executable when installing your package

entry_points={'console_scripts':
              ['vhc_extra = vhc_extra.entry_point:main']}