codecov-io: # [Codecov](https://codecov.io/gh/panoptes/panoptes-utils/pull/33?src=pr&el=h1) Report
> Merging [#33](https://codecov.io/gh/panoptes/panoptes-utils/pull/33?src=pr&el=desc) into [develop](https://codecov.io/gh/panoptes/panoptes-utils/commit/59d69b817df5a022263e60ee322c9492db578337?src=pr&el=desc) will **increase** coverage by `0.9%`.
> The diff coverage is `60%`.

[![Impacted file tree graph](https://codecov.io/gh/panoptes/panoptes-utils/pull/33/graphs/tree.svg?width=650&token=YCzESBa7rK&height=150&src=pr)](https://codecov.io/gh/panoptes/panoptes-utils/pull/33?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##           develop      #33     +/-   ##
==========================================
+ Coverage    47.02%   47.92%   +0.9%     
==========================================
  Files           35       37      +2     
  Lines         2520     2654    +134     
  Branches       314      338     +24     
==========================================
+ Hits          1185     1272     +87     
- Misses        1281     1325     +44     
- Partials        54       57      +3
```


| [Impacted Files](https://codecov.io/gh/panoptes/panoptes-utils/pull/33?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [panoptes\_utils/tests/test\_utils.py](https://codecov.io/gh/panoptes/panoptes-utils/pull/33/diff?src=pr&el=tree#diff-cGFub3B0ZXNfdXRpbHMvdGVzdHMvdGVzdF91dGlscy5weQ==) | `98.77% <100%> (ø)` | :arrow_up: |
| [panoptes\_utils/version.py](https://codecov.io/gh/panoptes/panoptes-utils/pull/33/diff?src=pr&el=tree#diff-cGFub3B0ZXNfdXRpbHMvdmVyc2lvbi5weQ==) | `100% <100%> (+100%)` | :arrow_up: |
| [panoptes\_utils/tests/test\_theskyx\_utils.py](https://codecov.io/gh/panoptes/panoptes-utils/pull/33/diff?src=pr&el=tree#diff-cGFub3B0ZXNfdXRpbHMvdGVzdHMvdGVzdF90aGVza3l4X3V0aWxzLnB5) | `92.68% <33.33%> (ø)` | :arrow_up: |
| [panoptes\_utils/utils.py](https://codecov.io/gh/panoptes/panoptes-utils/pull/33/diff?src=pr&el=tree#diff-cGFub3B0ZXNfdXRpbHMvdXRpbHMucHk=) | `46.05% <46.05%> (ø)` | |
| [panoptes\_utils/time.py](https://codecov.io/gh/panoptes/panoptes-utils/pull/33/diff?src=pr&el=tree#diff-cGFub3B0ZXNfdXRpbHMvdGltZS5weQ==) | `76.47% <76.47%> (ø)` | |
| [panoptes\_utils/library.py](https://codecov.io/gh/panoptes/panoptes-utils/pull/33/diff?src=pr&el=tree#diff-cGFub3B0ZXNfdXRpbHMvbGlicmFyeS5weQ==) | `52.94% <87.5%> (+52.94%)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/panoptes/panoptes-utils/pull/33?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/panoptes/panoptes-utils/pull/33?src=pr&el=footer). Last update [59d69b8...155fdfd](https://codecov.io/gh/panoptes/panoptes-utils/pull/33?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).

diff --git a/.travis.yml b/.travis.yml
index d2685fd..5d8985d 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -18,5 +18,6 @@ deploy:
   username: panoptes
   password:
     secure: x4qLLV0mdoXfp12CDjFLaK0AWBxp7K6UfD+3TGgaVpYJbu2hgRDDi9C4iSbS2pHb6NDumiw5viOxpcj9oZnPlIczvC83YoVOqgzSS3gmzpyh0hSsHkZvWOPrTajUV/VqFrGdS3anN1hUEvLrnfQXeZ6+u2jomlbLRueCAAjSkn0dT1XvvoL5ObJUjR/VPZKgVoJmYuWhUYCYBxYxlGEPeRq6jzwlECQduvyk+ZltdLnnlXQT9yQDDrXwN19GweYfnpUwEOlBpVzX9rtiOG+SyZGVRhDzrWaGWWVOGwdCMhiEv+UR69v8cIg4BGTTDEt1tLa050vj8SCTh8c7wY5hC/n9VzDEOROq/pwVrZE0MPEt646ZB0j6+oZ4j6DsHkyMx2V4G/YoqTwnV1zyG3eQL0YU27vtg3TVJYu3Y07zbjaCCJnlzRuRFknXuwqwWkpYl54HlNHL31UNXhN9p4L6WFEWoi6U5MmAwzHK4tWAGAoCgBIGy6MSkX7iLxkLV6IRgvLQntxjppqd3otriuAazntquZwYNgNvU5L0GPAh1icR9Wt8B8lrQ+2V+HuqEEUbOrrgdoa9b06+M1PYg+PvB93s9SSm3Nb/3dShWehWCMbIh3UfwHyJbKPIRcNPZ1Hgx2gx9LUhs0KDws6NswDBogAImafjL5u1zhT4NnNQMuE=
+  distributions: sdist bdist_wheel    
   on:
-    branch: master
+    branch: develop
diff --git a/panoptes_utils/__init__.py b/panoptes_utils/__init__.py
index 6686531..ce4e3d2 100644
--- a/panoptes_utils/__init__.py
+++ b/panoptes_utils/__init__.py
@@ -1,421 +1,10 @@
-import contextlib
-import os
-import shutil
-import signal
-import time
+from .version import __version__
 
-import numpy as np
-from astropy import units as u
-from astropy.coordinates import AltAz
-from astropy.coordinates import ICRS
-from astropy.coordinates import SkyCoord
-from astropy.time import Time
-from astropy.utils import resolve_name
+try:
+    __PANOPTES_SETUP__
+except NameError:
+    __PANOPTES_SETUP__ = False
 
-
-def current_time(flatten=False, datetime=False, pretty=False):
-    """ Convenience method to return the "current" time according to the system.
-
-    Note:
-        If the ``$POCSTIME`` environment variable is set then this will return
-        the time given in the variable. This is used for setting specific times
-        during testing. After checking the value of POCSTIME the environment
-        variable will also be incremented by one second so that subsequent
-        calls to this function will generate monotonically increasing times.
-
-        **Operation of POCS from $POCS/bin/pocs_shell will clear the POCSTIME variable.**
-
-        .. doctest::
-
-            >>> os.environ['POCSTIME'] = '1999-12-31 23:59:59'
-            >>> party_time = current_time(pretty=True)
-            >>> party_time
-            '1999-12-31 23:59:59'
-
-            # Next call is one second later when using $POCSTIME.
-            >>> y2k = current_time(pretty=True)
-            >>> y2k
-            '2000-01-01 00:00:00'
-
-
-    Note:
-        The time returned from this function is **not** timezone aware. All times
-        are UTC.
-
-    .. doctest::
-
-        >>> from panoptes_utils import current_time
-        >>> current_time()                # doctest: +SKIP
-        <Time object: scale='utc' format='datetime' value=2018-10-07 22:29:03.009873>
-
-        >>> current_time(datetime=True)   # doctest: +SKIP
-        datetime.datetime(2018, 10, 7, 22, 29, 26, 594368)
-
-        >>> current_time(pretty=True)     # doctest: +SKIP
-        '2018-10-07 22:29:51'
-
-
-    Returns:
-        astropy.time.Time: Object representing now.
-    """
-
-    pocs_time = os.getenv('POCSTIME')
-
-    if pocs_time is not None and pocs_time > '':
-        _time = Time(pocs_time)
-        # Increment POCSTIME
-        os.environ['POCSTIME'] = (_time + 1 * u.second).isot
-    else:
-        _time = Time.now()
-
-    if flatten:
-        _time = flatten_time(_time)
-
-    if pretty:
-        _time = _time.isot.split('.')[0].replace('T', ' ')
-
-    if datetime:
-        # Add UTC timezone
-        _time = _time.to_datetime()
-
-    return _time
-
-
-def flatten_time(t):
-    """Given an astropy time, flatten to have no extra chars besides integers.
-
-    .. doctest::
-
-        >>> from astropy.time import Time
-        >>> from panoptes_utils import flatten_time
-        >>> t0 = Time('1999-12-31 23:59:59')
-        >>> t0.isot
-        '1999-12-31T23:59:59.000'
-
-        >>> flatten_time(t0)
-        '19991231T235959'
-
-    Args:
-        t (astropy.time.Time): The time to be flattened.
-
-    Returns:
-        str: The flattened string representation of the time.
-    """
-    return t.isot.replace('-', '').replace(':', '').split('.')[0]
-
-
-# This is a streamlined variant of PySerial's serialutil.Timeout.
-class CountdownTimer(object):
-    """Simple timer object for tracking whether a time duration has elapsed.
-
-
-    Args:
-        duration (int or float or astropy.units.Quantity): Amount of time to before time expires.
-            May be numeric seconds or an Astropy time duration (e.g. 1 * u.minute).
-    """
-
-    def __init__(self, duration):
-        if isinstance(duration, u.Quantity):
-            duration = duration.to(u.second).value
-        elif not isinstance(duration, (int, float)):
-            raise ValueError(
-                'duration (%r) is not a supported type: %s' % (duration, type(duration)))
-
-        #: bool: True IFF the duration is zero.
-        assert duration >= 0, "Duration must be non-negative."
-        self.is_non_blocking = (duration == 0)
-
-        self.duration = float(duration)
-        self.restart()
-
-    def expired(self):
-        """Return a boolean, telling if the timeout has expired.
-
-        Returns:
-            bool: If timer has expired.
-        """
-        return self.time_left() <= 0
-
-    def time_left(self):
-        """Return how many seconds are left until the timeout expires.
-
-        Returns:
-            int: Number of seconds remaining in timer, zero if ``is_non_blocking=True``.
-        """
-        if self.is_non_blocking:
-            return 0
-        else:
-            delta = self.target_time - time.monotonic()
-            if delta > self.duration:
-                # clock jumped, recalculate
-                self.restart()
-                return self.duration
-            else:
-                return max(0.0, delta)
-
-    def restart(self):
-        """Restart the timed duration."""
-        self.target_time = time.monotonic() + self.duration
-
-    def sleep(self, max_sleep=None):
-        """Sleep until the timer expires, or for max_sleep, whichever is sooner.
-
-        Args:
-            max_sleep: Number of seconds to wait for, or None.
-        Returns:
-            True if slept for less than time_left(), False otherwise.
-        """
-        remaining = self.time_left()
-        if not remaining:
-            return False
-        if max_sleep and max_sleep < remaining:
-            assert max_sleep > 0
-            time.sleep(max_sleep)
-            return True
-        time.sleep(remaining)
-        return False
-
-
-def listify(obj):
-    """ Given an object, return a list.
-
-    Always returns a list. If obj is None, returns empty list,
-    if obj is list, just returns obj, otherwise returns list with
-    obj as single member.
-
-    Returns:
-        list:   You guessed it.
-    """
-    if obj is None:
-        return []
-    else:
-        return obj if isinstance(obj, (list, type(None))) else [obj]
-
-
-def get_free_space(dir=None):
-    """Return the amoung of freespace in gigabytes for given dir.
-
-    >>> from panoptes_utils import get_free_space
-    >>> get_free_space()        # doctest: +SKIP
-    <Quantity 10.245 Gbyte>
-
-    Args:
-        dir (str, optional): Path to directory. If None defaults to $PANDIR.
-
-    Returns:
-        astropy.units.Quantity: The number of gigabytes avialable in folder.
-
-    """
-    if dir is None:
-        dir = os.getenv('PANDIR')
-
-    _, _, free_space = shutil.disk_usage(dir)
-    free_space = (free_space * u.byte).to(u.gigabyte)
-    return free_space
-
-
-def string_to_params(opts):
-    """Parses a single string into parameters that can be passed to a function.
-
-    A user of the `peas_shell` can supply positional and keyword arguments to the
-    command being called, however the `Cmd` module that is used for the shell does
-    not parse these options but instead passes this as a single string. This utility
-    method does some simple parsing of that string and returns a list of positional
-    parameters and a dictionary of keyword arguments.  A keyword argument is considered
-    anything that contains an equal sign (e.g. `exptime=30`). Any leading `--` to
-    a keyword argument will be stripped during parsing.
-
-    A list of items can be passed by specifying the keyword argument multiple times.
-
-    Note:
-
-        This function will attempt to parse keyword values as floats if possible.
-        If a string is required include a single quote around the value, e.g.
-        `param='42'` will keep the value as the string `'42'`.
-
-
-    >>> from panoptes_utils import string_to_params
-    >>> args, kwargs = string_to_params("parg1 parg2 key1=a_str key2=2 key2='2' key3=03")
-    >>> args
-    ['parg1', 'parg2']
-    >>> kwargs
-    {'key1': 'a_str', 'key2': [2.0, '2'], 'key3': 3.0}
-    >>> isinstance(kwargs['key2'][0], float)
-    True
-    >>> isinstance(kwargs['key2'][1], str)
-    True
-    >>> kwargs['key2'][1] == '2'
-    True
-
-    >>> args, kwargs = string_to_params('--key1=val1 --key1-2=val1-2')
-    >>> kwargs
-    {'key1': 'val1', 'key1-2': 'val1-2'}
-
-    Args:
-        opts (str): A single string containing everything beyond the actual
-            command that is called.
-
-    Returns:
-        tuple(list, dict): Returns a list of positional parameters and a dictionary
-        of keyword arguments. These correspond to the *args and **kwargs that
-        a typical function would receive.
-    """
-    args = []
-    kwargs = {}
-
-    for opt in opts.split(' '):
-        if '=' not in opt:
-            args.append(opt)
-        else:
-            name, value = opt.split('=', maxsplit=1)
-            if name.startswith('--') and len(name) > 2:
-                name = name[2:]
-
-            if "'" in value:
-                # Remove the explict single quotes.
-                value = value.replace("'", "")
-            else:
-                # Make it a number if possible.
-                with contextlib.suppress(ValueError):
-                    value = float(value)
-
-            if name in kwargs:
-                kwargs[name] = listify(kwargs[name])
-                kwargs[name].append(value)
-            else:
-                kwargs[name] = value
-
-    return args, kwargs
-
-
-def load_module(module_name):
-    """Dynamically load a module.
-
-    >>> from panoptes_utils import load_module
-    >>> camera = load_module('pocs.camera.simulator')
-    >>> camera.__package__
-    'pocs.camera'
-
-    Args:
-        module_name (str): Name of module to import.
-
-    Returns:
-        module: an imported module name
-
-    Raises:
-        error.NotFound: If module cannot be imported.
-    """
-    from panoptes_utils import error
-    try:
-        module = resolve_name(module_name)
-    except ImportError:
-        raise error.NotFound(msg=module_name)
-
-    return module
-
-
-def altaz_to_radec(alt=35, az=90, location=None, obstime=None, verbose=False):
-    """Convert alt/az degrees to RA/Dec SkyCoord.
-
-    Args:
-        alt (int, optional): Altitude, defaults to 35
-        az (int, optional): Azimute, defaults to 90 (east)
-        location (None|astropy.coordinates.EarthLocation, required): A valid location.
-        obstime (None, optional): Time for object, defaults to `current_time`
-        verbose (bool, optional): Verbose, default False.
-
-    Returns:
-        astropy.coordinates.SkyCoord: Coordinates corresponding to the AltAz.
-    """
-    assert location is not None
-    if obstime is None:
-        obstime = current_time()
-
-    if verbose:
-        print("Getting coordinates for Alt {} Az {}, from {} at {}".format(
-            alt, az, location, obstime))
-
-    altaz = AltAz(obstime=obstime, location=location, alt=alt * u.deg, az=az * u.deg)
-    return SkyCoord(altaz.transform_to(ICRS))
-
-
-class DelaySigTerm(contextlib.ContextDecorator):
-    """Supports delaying SIGTERM during a critical section.
-
-    This allows one to avoid having SIGTERM interrupt a
-    critical block of code, such as saving to a database.
-
-    Example:
-
-        ..
-        with DelaySigTerm():
-            db.WriteCurrentRecord(record)
-
-    """
-    # TODO(jamessynge): Consider generalizing as DelaySignal(signum).
-
-    def __enter__(self, callback=None):
-        """
-        Args:
-            callback: If not None, called when SIGTERM is handled,
-                with kwargs previously_caught and frame.
-        """
-        self.caught = False
-        self.old_handler = signal.getsignal(signal.SIGTERM)
-        if callback:
-            assert callable(callback)
-            self.callback = callback
-        else:
-            self.callback = None
-
-        def handler(signum, frame):
-            previously_caught = self.caught
-            self.caught = True
-            if self.callback:
-                self.callback(previously_caught=previously_caught, frame=frame)
-
-        signal.signal(signal.SIGTERM, handler)
-        return self
-
-    def __exit__(self, *exc):
-        signal.signal(signal.SIGTERM, self.old_handler)
-        if self.caught:
-            # Send SIGTERM to this process.
-            os.kill(os.getpid(), signal.SIGTERM)
-            # Suppress any exception caught while the context was running.
-            return True
-        return False
-
-
-def get_quantity_value(quantity, unit=None):
-    """ Return the numerical value of a Quantity, optionally converting to unit at the same time.
-
-    If passed something other than a Quantity will simply return the original object.
-
-    Args:
-        quantity (astropy.units.Quantity): Quantity to extract numerical value from.
-        unit (astropy.units.Unit, optional): unit to convert to.
-
-    Returns:
-        float: numerical value of the Quantity after conversion to the specified unit.
-    """
-    if isinstance(quantity, u.Quantity):
-        if unit is not None:
-            quantity = quantity.to(unit)
-        return quantity.value
-    else:
-        return quantity
-
-
-def moving_average(data_set, periods=3):
-    """Moving average.
-
-    Args:
-        data_set (`numpy.array`): An array of values over which to perform the moving average.
-        periods (int, optional): Number of periods.
-
-    Returns:
-        `numpy.array`: An array of the computed averages.
-    """
-    weights = np.ones(periods) / periods
-    return np.convolve(data_set, weights, mode='same')
+if not __PANOPTES_SETUP__:
+    from .time import *
+    from .utils import *
diff --git a/panoptes_utils/library.py b/panoptes_utils/library.py
index c38a2e6..3302e40 100644
--- a/panoptes_utils/library.py
+++ b/panoptes_utils/library.py
@@ -1,9 +1,10 @@
 import ctypes
 
+from astropy.utils import resolve_name
 from panoptes_utils import error
 
 
-def load_library(name, path=None, logger=None):
+def load_c_library(name, path=None, logger=None):
     """Utility function to load a shared/dynamically linked library (.so/.dylib/.dll).
 
     The name and location of the shared library can be manually specified with the library_path
@@ -31,3 +32,28 @@ def load_library(name, path=None, logger=None):
             raise error.NotFound("Cound not find {} library!".format(name))
     # This CDLL loader will raise OSError if the library could not be loaded
     return ctypes.CDLL(path)
+
+
+def load_module(module_name):
+    """Dynamically load a module.
+
+    >>> from panoptes_utils.library import load_module
+    >>> camera = load_module('pocs.camera.simulator')
+    >>> camera.__package__
+    'pocs.camera'
+
+    Args:
+        module_name (str): Name of module to import.
+
+    Returns:
+        module: an imported module name
+
+    Raises:
+        error.NotFound: If module cannot be imported.
+    """
+    try:
+        module = resolve_name(module_name)
+    except ImportError:
+        raise error.NotFound(msg=module_name)
+
+    return module
diff --git a/panoptes_utils/time.py b/panoptes_utils/time.py
new file mode 100644
index 0000000..9c4fc5c
--- /dev/null
+++ b/panoptes_utils/time.py
@@ -0,0 +1,167 @@
+import os
+import time
+from astropy import units as u
+from astropy.time import Time
+
+
+def current_time(flatten=False, datetime=False, pretty=False):
+    """ Convenience method to return the "current" time according to the system.
+
+    Note:
+        If the ``$POCSTIME`` environment variable is set then this will return
+        the time given in the variable. This is used for setting specific times
+        during testing. After checking the value of POCSTIME the environment
+        variable will also be incremented by one second so that subsequent
+        calls to this function will generate monotonically increasing times.
+
+        **Operation of POCS from $POCS/bin/pocs_shell will clear the POCSTIME variable.**
+
+        .. doctest::
+
+            >>> os.environ['POCSTIME'] = '1999-12-31 23:59:59'
+            >>> party_time = current_time(pretty=True)
+            >>> party_time
+            '1999-12-31 23:59:59'
+
+            # Next call is one second later when using $POCSTIME.
+            >>> y2k = current_time(pretty=True)
+            >>> y2k
+            '2000-01-01 00:00:00'
+
+
+    Note:
+        The time returned from this function is **not** timezone aware. All times
+        are UTC.
+
+    .. doctest::
+
+        >>> from panoptes_utils import current_time
+        >>> current_time()                # doctest: +SKIP
+        <Time object: scale='utc' format='datetime' value=2018-10-07 22:29:03.009873>
+
+        >>> current_time(datetime=True)   # doctest: +SKIP
+        datetime.datetime(2018, 10, 7, 22, 29, 26, 594368)
+
+        >>> current_time(pretty=True)     # doctest: +SKIP
+        '2018-10-07 22:29:51'
+
+
+    Returns:
+        astropy.time.Time: Object representing now.
+    """
+
+    pocs_time = os.getenv('POCSTIME')
+
+    if pocs_time is not None and pocs_time > '':
+        _time = Time(pocs_time)
+        # Increment POCSTIME
+        os.environ['POCSTIME'] = (_time + 1 * u.second).isot
+    else:
+        _time = Time.now()
+
+    if flatten:
+        _time = flatten_time(_time)
+
+    if pretty:
+        _time = _time.isot.split('.')[0].replace('T', ' ')
+
+    if datetime:
+        # Add UTC timezone
+        _time = _time.to_datetime()
+
+    return _time
+
+
+def flatten_time(t):
+    """Given an astropy time, flatten to have no extra chars besides integers.
+
+    .. doctest::
+
+        >>> from astropy.time import Time
+        >>> from panoptes_utils import flatten_time
+        >>> t0 = Time('1999-12-31 23:59:59')
+        >>> t0.isot
+        '1999-12-31T23:59:59.000'
+
+        >>> flatten_time(t0)
+        '19991231T235959'
+
+    Args:
+        t (astropy.time.Time): The time to be flattened.
+
+    Returns:
+        str: The flattened string representation of the time.
+    """
+    return t.isot.replace('-', '').replace(':', '').split('.')[0]
+
+
+# This is a streamlined variant of PySerial's serialutil.Timeout.
+class CountdownTimer(object):
+    """Simple timer object for tracking whether a time duration has elapsed.
+
+
+    Args:
+        duration (int or float or astropy.units.Quantity): Amount of time to before time expires.
+            May be numeric seconds or an Astropy time duration (e.g. 1 * u.minute).
+    """
+
+    def __init__(self, duration):
+        if isinstance(duration, u.Quantity):
+            duration = duration.to(u.second).value
+        elif not isinstance(duration, (int, float)):
+            raise ValueError(
+                'duration (%r) is not a supported type: %s' % (duration, type(duration)))
+
+        #: bool: True IFF the duration is zero.
+        assert duration >= 0, "Duration must be non-negative."
+        self.is_non_blocking = (duration == 0)
+
+        self.duration = float(duration)
+        self.restart()
+
+    def expired(self):
+        """Return a boolean, telling if the timeout has expired.
+
+        Returns:
+            bool: If timer has expired.
+        """
+        return self.time_left() <= 0
+
+    def time_left(self):
+        """Return how many seconds are left until the timeout expires.
+
+        Returns:
+            int: Number of seconds remaining in timer, zero if ``is_non_blocking=True``.
+        """
+        if self.is_non_blocking:
+            return 0
+        else:
+            delta = self.target_time - time.monotonic()
+            if delta > self.duration:
+                # clock jumped, recalculate
+                self.restart()
+                return self.duration
+            else:
+                return max(0.0, delta)
+
+    def restart(self):
+        """Restart the timed duration."""
+        self.target_time = time.monotonic() + self.duration
+
+    def sleep(self, max_sleep=None):
+        """Sleep until the timer expires, or for max_sleep, whichever is sooner.
+
+        Args:
+            max_sleep: Number of seconds to wait for, or None.
+        Returns:
+            True if slept for less than time_left(), False otherwise.
+        """
+        remaining = self.time_left()
+        if not remaining:
+            return False
+        if max_sleep and max_sleep < remaining:
+            assert max_sleep > 0
+            time.sleep(max_sleep)
+            return True
+        time.sleep(remaining)
+        return False
\ No newline at end of file
diff --git a/panoptes_utils/utils.py b/panoptes_utils/utils.py
new file mode 100644
index 0000000..c081b62
--- /dev/null
+++ b/panoptes_utils/utils.py
@@ -0,0 +1,231 @@
+import contextlib
+import os
+import shutil
+import signal
+
+import numpy as np
+from astropy import units as u
+from astropy.coordinates import AltAz
+from astropy.coordinates import ICRS
+from astropy.coordinates import SkyCoord
+
+from panoptes_utils.time import current_time
+
+
+def listify(obj):
+    """ Given an object, return a list.
+
+    Always returns a list. If obj is None, returns empty list,
+    if obj is list, just returns obj, otherwise returns list with
+    obj as single member.
+
+    Returns:
+        list:   You guessed it.
+    """
+    if obj is None:
+        return []
+    else:
+        return obj if isinstance(obj, (list, type(None))) else [obj]
+
+
+def get_free_space(dir=None):
+    """Return the amoung of freespace in gigabytes for given dir.
+
+    >>> from panoptes_utils import get_free_space
+    >>> get_free_space()        # doctest: +SKIP
+    <Quantity 10.245 Gbyte>
+
+    Args:
+        dir (str, optional): Path to directory. If None defaults to $PANDIR.
+
+    Returns:
+        astropy.units.Quantity: The number of gigabytes avialable in folder.
+
+    """
+    if dir is None:
+        dir = os.getenv('PANDIR')
+
+    _, _, free_space = shutil.disk_usage(dir)
+    free_space = (free_space * u.byte).to(u.gigabyte)
+    return free_space
+
+
+def string_to_params(opts):
+    """Parses a single string into parameters that can be passed to a function.
+
+    A user of the `peas_shell` can supply positional and keyword arguments to the
+    command being called, however the `Cmd` module that is used for the shell does
+    not parse these options but instead passes this as a single string. This utility
+    method does some simple parsing of that string and returns a list of positional
+    parameters and a dictionary of keyword arguments.  A keyword argument is considered
+    anything that contains an equal sign (e.g. `exptime=30`). Any leading `--` to
+    a keyword argument will be stripped during parsing.
+
+    A list of items can be passed by specifying the keyword argument multiple times.
+
+    Note:
+
+        This function will attempt to parse keyword values as floats if possible.
+        If a string is required include a single quote around the value, e.g.
+        `param='42'` will keep the value as the string `'42'`.
+
+
+    >>> from panoptes_utils import string_to_params
+    >>> args, kwargs = string_to_params("parg1 parg2 key1=a_str key2=2 key2='2' key3=03")
+    >>> args
+    ['parg1', 'parg2']
+    >>> kwargs
+    {'key1': 'a_str', 'key2': [2.0, '2'], 'key3': 3.0}
+    >>> isinstance(kwargs['key2'][0], float)
+    True
+    >>> isinstance(kwargs['key2'][1], str)
+    True
+    >>> kwargs['key2'][1] == '2'
+    True
+
+    >>> args, kwargs = string_to_params('--key1=val1 --key1-2=val1-2')
+    >>> kwargs
+    {'key1': 'val1', 'key1-2': 'val1-2'}
+
+    Args:
+        opts (str): A single string containing everything beyond the actual
+            command that is called.
+
+    Returns:
+        tuple(list, dict): Returns a list of positional parameters and a dictionary
+        of keyword arguments. These correspond to the *args and **kwargs that
+        a typical function would receive.
+    """
+    args = []
+    kwargs = {}
+
+    for opt in opts.split(' '):
+        if '=' not in opt:
+            args.append(opt)
+        else:
+            name, value = opt.split('=', maxsplit=1)
+            if name.startswith('--') and len(name) > 2:
+                name = name[2:]
+
+            if "'" in value:
+                # Remove the explict single quotes.
+                value = value.replace("'", "")
+            else:
+                # Make it a number if possible.
+                with contextlib.suppress(ValueError):
+                    value = float(value)
+
+            if name in kwargs:
+                kwargs[name] = listify(kwargs[name])
+                kwargs[name].append(value)
+            else:
+                kwargs[name] = value
+
+    return args, kwargs
+
+
+def altaz_to_radec(alt=35, az=90, location=None, obstime=None, verbose=False):
+    """Convert alt/az degrees to RA/Dec SkyCoord.
+
+    Args:
+        alt (int, optional): Altitude, defaults to 35
+        az (int, optional): Azimute, defaults to 90 (east)
+        location (None|astropy.coordinates.EarthLocation, required): A valid location.
+        obstime (None, optional): Time for object, defaults to `current_time`
+        verbose (bool, optional): Verbose, default False.
+
+    Returns:
+        astropy.coordinates.SkyCoord: Coordinates corresponding to the AltAz.
+    """
+    assert location is not None
+    if obstime is None:
+        obstime = current_time()
+
+    if verbose:
+        print("Getting coordinates for Alt {} Az {}, from {} at {}".format(
+            alt, az, location, obstime))
+
+    altaz = AltAz(obstime=obstime, location=location, alt=alt * u.deg, az=az * u.deg)
+    return SkyCoord(altaz.transform_to(ICRS))
+
+
+class DelaySigTerm(contextlib.ContextDecorator):
+    """Supports delaying SIGTERM during a critical section.
+
+    This allows one to avoid having SIGTERM interrupt a
+    critical block of code, such as saving to a database.
+
+    Example:
+
+        ..
+        with DelaySigTerm():
+            db.WriteCurrentRecord(record)
+
+    """
+    # TODO(jamessynge): Consider generalizing as DelaySignal(signum).
+
+    def __enter__(self, callback=None):
+        """
+        Args:
+            callback: If not None, called when SIGTERM is handled,
+                with kwargs previously_caught and frame.
+        """
+        self.caught = False
+        self.old_handler = signal.getsignal(signal.SIGTERM)
+        if callback:
+            assert callable(callback)
+            self.callback = callback
+        else:
+            self.callback = None
+
+        def handler(signum, frame):
+            previously_caught = self.caught
+            self.caught = True
+            if self.callback:
+                self.callback(previously_caught=previously_caught, frame=frame)
+
+        signal.signal(signal.SIGTERM, handler)
+        return self
+
+    def __exit__(self, *exc):
+        signal.signal(signal.SIGTERM, self.old_handler)
+        if self.caught:
+            # Send SIGTERM to this process.
+            os.kill(os.getpid(), signal.SIGTERM)
+            # Suppress any exception caught while the context was running.
+            return True
+        return False
+
+
+def get_quantity_value(quantity, unit=None):
+    """ Return the numerical value of a Quantity, optionally converting to unit at the same time.
+
+    If passed something other than a Quantity will simply return the original object.
+
+    Args:
+        quantity (astropy.units.Quantity): Quantity to extract numerical value from.
+        unit (astropy.units.Unit, optional): unit to convert to.
+
+    Returns:
+        float: numerical value of the Quantity after conversion to the specified unit.
+    """
+    if isinstance(quantity, u.Quantity):
+        if unit is not None:
+            quantity = quantity.to(unit)
+        return quantity.value
+    else:
+        return quantity
+
+
+def moving_average(data_set, periods=3):
+    """Moving average.
+
+    Args:
+        data_set (`numpy.array`): An array of values over which to perform the moving average.
+        periods (int, optional): Number of periods.
+
+    Returns:
+        `numpy.array`: An array of the computed averages.
+    """
+    weights = np.ones(periods) / periods
+    return np.convolve(data_set, weights, mode='same')
diff --git a/panoptes_utils/version.py b/panoptes_utils/version.py
index 4c10bae..5c02009 100644
--- a/panoptes_utils/version.py
+++ b/panoptes_utils/version.py
@@ -2,4 +2,4 @@ major = 0
 minor = 0
 patch = 6
 
-__version__ = '{}.{}.{}'.format(major, minor, patch)
+__version__ = '{}.{}.{}-dev'.format(major, minor, patch)

Cleanup  core init file
There are a number of items in `panoptes_utils/__init__.py` that are automatically imported when anything in the directory is imported. This is a problem in cases like the automatic upload of a package to pypi, where a `from panoptes_utils.version import __version__` will require `astropy` and `numpy` be installed.
diff --git a/panoptes_utils/tests/test_theskyx_utils.py b/panoptes_utils/tests/test_theskyx_utils.py
index 39156a0..31a303a 100644
--- a/panoptes_utils/tests/test_theskyx_utils.py
+++ b/panoptes_utils/tests/test_theskyx_utils.py
@@ -18,7 +18,7 @@ def skyx(request):
     # Use `--with-hardware thesky` on cli to run without mock
     Mocket.enable('theskyx',
                   '{}/panoptes-utils/panoptes_utils/tests/data'.format(os.getenv('PANDIR')))
-    if 'theskyx' in pytest.config.getoption('--with-hardware'):
+    if 'theskyx' in request.config.getoption('--with-hardware'):
         Mocket.disable()
 
     theskyx = TheSkyX(connect=False)
@@ -26,13 +26,13 @@ def skyx(request):
     yield theskyx
 
 
-def test_default_connect():
+def test_default_connect(request):
     """Test connection to TheSkyX
 
     If not running with a real connection then use Mocket
     """
     # Use `--with-hardware thesky` on cli to run without mock
-    if 'theskyx' not in pytest.config.getoption('--with-hardware'):
+    if 'theskyx' not in request.config.getoption('--with-hardware'):
         Mocket.enable(
             'theskyx', '{}/panoptes-utils/panoptes_utils/tests/data'.format(os.getenv('PANDIR')))
 
diff --git a/panoptes_utils/tests/test_utils.py b/panoptes_utils/tests/test_utils.py
index 3698830..5e48ef8 100644
--- a/panoptes_utils/tests/test_utils.py
+++ b/panoptes_utils/tests/test_utils.py
@@ -8,9 +8,9 @@ from astropy import units as u
 from panoptes_utils import current_time
 from panoptes_utils import DelaySigTerm
 from panoptes_utils import listify
-from panoptes_utils import load_module
 from panoptes_utils import CountdownTimer
 from panoptes_utils import error
+from panoptes_utils.library import load_module
 
 
 def test_error(capsys):

pip install -e .[all]
parse_log_pytest
requirements.txt
pytest pytest-cov pytest-remotedata>=0.3.1 codecov coverage coveralls mocket pycodestyle==2.3.1 pytest>=3.6 pytest-xdist pytest-mock pytest-asyncio
apt-get update apt-get install -y gcc
3.7
requirements.txt
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
astroplan==0.10.1
astropy==4.3.1
cachetools==5.5.2
certifi @ file:///croot/certifi_1671487769961/work/certifi
charset-normalizer==3.4.1
click==8.1.8
codecov==2.1.13
coverage==6.5.0
coveralls==3.3.1
cycler==0.11.0
decorator==5.1.1
dnspython==2.3.0
docopt==0.6.2
exceptiongroup==1.2.2
execnet==2.0.2
Flask==2.2.5
fonttools==4.38.0
google-api-core==2.24.2
google-auth==2.38.0
google-cloud==0.34.0
google-cloud-core==2.4.3
google-cloud-storage==3.1.0
google-crc32c==1.5.0
google-resumable-media==2.7.2
googleapis-common-protos==1.69.2
httptools==0.6.0
idna==3.10
imageio==2.31.2
importlib-metadata==6.7.0
iniconfig==2.0.0
itsdangerous==2.1.2
Jinja2==3.1.6
kiwisolver==1.4.5
MarkupSafe==2.1.5
matplotlib==3.5.3
mocket==3.12.2
networkx==2.6.3
numpy==1.21.6
oauthlib==3.2.2
packaging==24.0
-e git+https://github.com/panoptes/panoptes-utils.git@59d69b817df5a022263e60ee322c9492db578337#egg=panoptes_utils
Pillow==9.5.0
pluggy==1.2.0
proto-plus==1.26.1
protobuf==4.24.4
psycopg2-binary==2.9.9
pyasn1==0.5.1
pyasn1-modules==0.3.0
pycodestyle==2.3.1
pyerfa==2.0.0.3
pymongo==4.7.3
pyparsing==3.1.4
pyserial==3.5
pytest==7.4.4
pytest-asyncio==0.21.2
pytest-cov==4.1.0
pytest-mock==3.11.1
pytest-remotedata==0.4.1
pytest-xdist==3.5.0
python-dateutil==2.9.0.post0
python-magic==0.4.27
pytz==2025.2
PyWavelets==1.3.0
PyYAML==6.0.1
pyzmq==26.2.1
requests==2.31.0
requests-oauthlib==1.3.1
rsa==4.9
ruamel.yaml==0.18.10
ruamel.yaml.clib==0.2.8
scalpl==0.4.2
scikit-image==0.19.3
scipy==1.7.3
six==1.17.0
tifffile==2021.11.2
tomli==2.0.1
tweepy==4.14.0
typing_extensions==4.7.1
urllib3==2.0.7
Werkzeug==2.2.3
zipp==3.15.0

name: panoptes-utils
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - ca-certificates=2025.2.25=h06a4308_0
  - certifi=2022.12.7=py37h06a4308_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.4.4=h6a678d5_1
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - ncurses=6.4=h6a678d5_0
  - openssl=1.1.1w=h7f8727e_0
  - pip=22.3.1=py37h06a4308_0
  - python=3.7.16=h7a1cb2a_0
  - readline=8.2=h5eee18b_0
  - setuptools=65.6.3=py37h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - wheel=0.38.4=py37h06a4308_0
  - xz=5.6.4=h5eee18b_1
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - astroplan==0.10.1
      - astropy==4.3.1
      - cachetools==5.5.2
      - charset-normalizer==3.4.1
      - click==8.1.8
      - codecov==2.1.13
      - coverage==6.5.0
      - coveralls==3.3.1
      - cycler==0.11.0
      - decorator==5.1.1
      - dnspython==2.3.0
      - docopt==0.6.2
      - exceptiongroup==1.2.2
      - execnet==2.0.2
      - flask==2.2.5
      - fonttools==4.38.0
      - google-api-core==2.24.2
      - google-auth==2.38.0
      - google-cloud==0.34.0
      - google-cloud-core==2.4.3
      - google-cloud-storage==3.1.0
      - google-crc32c==1.5.0
      - google-resumable-media==2.7.2
      - googleapis-common-protos==1.69.2
      - httptools==0.6.0
      - idna==3.10
      - imageio==2.31.2
      - importlib-metadata==6.7.0
      - iniconfig==2.0.0
      - itsdangerous==2.1.2
      - jinja2==3.1.6
      - kiwisolver==1.4.5
      - markupsafe==2.1.5
      - matplotlib==3.5.3
      - mocket==3.12.2
      - networkx==2.6.3
      - numpy==1.21.6
      - oauthlib==3.2.2
      - packaging==24.0
      - pillow==9.5.0
      - pluggy==1.2.0
      - proto-plus==1.26.1
      - protobuf==4.24.4
      - psycopg2-binary==2.9.9
      - pyasn1==0.5.1
      - pyasn1-modules==0.3.0
      - pycodestyle==2.3.1
      - pyerfa==2.0.0.3
      - pymongo==4.7.3
      - pyparsing==3.1.4
      - pyserial==3.5
      - pytest==7.4.4
      - pytest-asyncio==0.21.2
      - pytest-cov==4.1.0
      - pytest-mock==3.11.1
      - pytest-remotedata==0.4.1
      - pytest-xdist==3.5.0
      - python-dateutil==2.9.0.post0
      - python-magic==0.4.27
      - pytz==2025.2
      - pywavelets==1.3.0
      - pyyaml==6.0.1
      - pyzmq==26.2.1
      - requests==2.31.0
      - requests-oauthlib==1.3.1
      - rsa==4.9
      - ruamel-yaml==0.18.10
      - ruamel-yaml-clib==0.2.8
      - scalpl==0.4.2
      - scikit-image==0.19.3
      - scipy==1.7.3
      - six==1.17.0
      - tifffile==2021.11.2
      - tomli==2.0.1
      - tweepy==4.14.0
      - typing-extensions==4.7.1
      - urllib3==2.0.7
      - werkzeug==2.2.3
      - zipp==3.15.0
prefix: /opt/conda/envs/panoptes-utils

panoptes_utils/tests/test_utils.py::test_bad_load_module panoptes_utils/tests/test_utils.py::test_listify panoptes_utils/tests/test_utils.py::test_empty_listify panoptes_utils/tests/test_utils.py::test_pretty_time panoptes_utils/tests/test_utils.py::test_countdown_timer_bad_input panoptes_utils/tests/test_utils.py::test_countdown_timer_non_blocking panoptes_utils/tests/test_utils.py::test_countdown_timer panoptes_utils/tests/test_utils.py::test_delay_of_sigterm_with_nosignal panoptes_utils/tests/test_utils.py::test_delay_of_sigterm_with_handled_signal panoptes_utils/tests/test_utils.py::test_delay_of_sigterm_with_raised_exception panoptes_utils/tests/test_utils.py::test_delay_of_sigterm_with_sigterm
panoptes_utils/tests/test_theskyx_utils.py::test_default_connect panoptes_utils/tests/test_utils.py::test_error
panoptes_utils/__init__.py .travis.yml panoptes_utils/time.py panoptes_utils/version.py panoptes_utils/library.py panoptes_utils/utils.py
panoptes_utils/__init__.py .travis.yml panoptes_utils/time.py panoptes_utils/version.py panoptes_utils/library.py panoptes_utils/utils.py