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: .. code-block:: bash 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: .. code-block:: python 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_recipe: 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: .. autofunction:: libvhc.recipes.recipe_protopype :noindex: 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``) .. code-block:: python 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: .. code-block:: python 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 :ref:`factories` to implement new drivers as described in :doc:`custom_drivers` you must add a section in the configuration file with the name of your new recipe and the number of expected exposure/dithers: .. code-block:: ini [virus_ancillary] # expected number of exposures n_exposures = 1 .. _plug_driver: Plug new drivers ================ In :doc:`custom_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 :ref:`plug_recipe`, we need to create two functions to implement the them: lets say that we write two functions ``vhc_extra/drivers.py``: .. code-block:: python 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``: .. code-block:: python 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 :mod:`libvhc.reference_file_parser`. Every file parser is derived from a common ancestor, :class:`~libvhc.reference_file_parser._BaseParser`, already implementing most of the methods necessary. The only method that needs to be implemented to have a working parser is :meth:`~libvhc.reference_file_parser._BaseParser.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: .. code-block:: ini [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: .. code-block:: python 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: .. code-block:: python 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 :func:`~libvhc.reference_file_parser.picker` function: .. code-block:: python import libvhc.reference_file_parser as vhcrfp [...] parser = vhcrfp.picker('some_reference') values = parser.get_value(001, 'L', 'U') | | ---- .. rubric:: Bonus If you want to do more you can, probably. The ``vhc`` executable simply calls :func:`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: .. code-block:: python 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 .. code-block:: python entry_points={'console_scripts': ['vhc_extra = vhc_extra.entry_point:main']}