from pkg_resources import iter_entry_points
from typeguard import check_argument_types
from typing import Sequence

nodefault = object()

def traverse(obj, target:str, default=nodefault, executable:bool=False, separator:str='.', protect:bool=True):
	"""Traverse down an object, using getattr or getitem.

	If ``executable`` is ``True`` any executable function encountered will be, with no arguments. Traversal will
	continue on the result of that call.  You can change the separator as desired, i.e. to a '/'.

	By default attributes (but not array elements) prefixed with an underscore are taboo.  They will not resolve,
	raising a LookupError.

	Certain allowances are made: if a 'path segment' is numerical, it's treated as an array index. If attribute
	lookup fails, it will re-try on that object using array notation and continue from there.  This makes lookup
	very flexible.
	"""

	# TODO: Support numerical slicing, i.e. ``1:4``, or even just ``:-1`` and things.
	assert check_argument_types()

	value = obj
	remainder = target

	if not target:
		return obj

	while separator:
		name, separator, remainder = remainder.partition(separator)
		numeric = name.lstrip('-').isdigit()

		try:
			if numeric or (protect and name.startswith('_')):
				raise AttributeError()

			value = getattr(value, name)

			if executable and callable(value):
				value = value()

		except AttributeError:
			try:
				value = value[int(name) if numeric else name]

			except (KeyError, TypeError):
				if default is nodefault:
					raise LookupError("Could not resolve '" + target + "' on: " + repr(obj))

				return default

	return value

def load(target:str, namespace:str=None, default=nodefault, executable:bool=False, separators:Sequence[str]=('.', ':'),
		protect:bool=True):
	"""This helper function loads an object identified by a dotted-notation string.

	For example::

		# Load class Foo from example.objects
		load('example.objects:Foo')

		# Load the result of the class method ``new`` of the Foo object
		load('example.objects:Foo.new', executable=True)

	If a plugin namespace is provided simple name references are allowed.  For example::

		# Load the plugin named 'routing' from the 'web.dispatch' namespace
		load('routing', 'web.dispatch')

	The ``executable``, ``protect``, and first tuple element of ``separators`` are passed to the traverse function.
	Providing a namespace does not prevent full object lookup (dot-colon notation) from working.
	"""

	assert check_argument_types()

	if namespace and ':' not in target:
		allowable = dict((i.name,  i) for i in iter_entry_points(namespace))

		if target not in allowable:
			raise LookupError('Unknown plugin "' + target + '"; found: ' + ', '.join(allowable))

		return allowable[target].load()

	parts, _, target = target.partition(separators[1])

	try:
		obj = __import__(parts)
	except ImportError:
		if default is not nodefault:
			return default

		raise

	return traverse(
			obj,
			separators[0].join(parts.split(separators[0])[1:] + target.split(separators[0])),
			default = default,
			executable = executable,
			protect = protect
		) if target else obj


"""Test client."""
from time import sleep

from leicacam.cam import CAM

def run():
    """Run client."""
    cam = CAM()
    print(cam.welcome_msg)
    print(cam.send(b'/cmd:deletelist'))
    sleep(0.1)
    print(cam.receive())
    print(cam.send(b'/cmd:deletelist'))
    sleep(0.1)
    print(cam.wait_for(cmd='cmd', timeout=0.1))
    cam.close()

if __name__ == '__main__':
    run()


#!/usr/bin/env python
"""Generate changelog."""
import os

from pygcgen.main import ChangelogGenerator

def validate_version():
    """Validate version before release."""
    import leicacam
    version_string = leicacam.__version__
    versions = version_string.split('.', 3)
    try:
        for ver in versions:
            int(ver)
    except ValueError:
        print(
            'Only integers are allowed in release version, '
            'please adjust current version {}'.format(version_string))
        return None
    return version_string

def generate():
    """Generate changelog."""
    old_dir = os.getcwd()
    proj_dir = os.path.join(os.path.dirname(__file__), os.pardir)
    os.chdir(proj_dir)
    version = validate_version()
    if not version:
        os.chdir(old_dir)
        return
    print('Generating changelog for version {}'.format(version))
    options = [
        '--user', 'arve0', '--project', 'leicacam', '-v', '--with-unreleased',
        '--future-release', version]
    generator = ChangelogGenerator(options)
    generator.run()
    os.chdir(old_dir)

if __name__ == '__main__':
    generate()


"""Tarjan's algorithm and topological sorting implementation in Python.

by Paul Harrison

Public domain, do with it as you will.

From a blog post by Paul Harrison: http://www.logarithmic.net/pfh/blog/01208083168

Somee cleanup was applied, and Python 3 function annotations (typing module, typeguard validation) supplied.
"""

from collections import defaultdict
from typeguard import check_argument_types
from typing import List, Mapping, Sequence

Graph = Mapping[str, Sequence[str]]

def strongly_connected_components(graph: Graph) -> List:
	"""Find the strongly connected components in a graph using Tarjan's algorithm.

	The `graph` argument should be a dictionary mapping node names to sequences of successor nodes.
	"""

	assert check_argument_types()

	result = []
	stack = []
	low = {}

	def visit(node: str):
		if node in low: return

		num = len(low)
		low[node] = num
		stack_pos = len(stack)
		stack.append(node)

		for successor in graph[node]:
			visit(successor)
			low[node] = min(low[node], low[successor])

		if num == low[node]:
			component = tuple(stack[stack_pos:])
			del stack[stack_pos:]

			result.append(component)

			for item in component:
				low[item] = len(graph)

	for node in graph:
		visit(node)

	return result

def topological_sort(graph: Graph) -> list:
	assert check_argument_types()

	count = defaultdict(lambda: 0)

	for node in graph:
		for successor in graph[node]:
			count[successor] += 1

	result = []
	ready = [node for node in graph if count[node] == 0]

	while ready:
		node = ready.pop(-1)
		result.append(node)

		for successor in graph[node]:
			count[successor] -= 1
			if count[successor] == 0:
				ready.append(successor)

	return result

def robust_topological_sort(graph: Graph) -> list:
	"""Identify strongly connected components then perform a topological sort of those components."""

	assert check_argument_types()

	components = strongly_connected_components(graph)

	node_component = {}
	for component in components:
		for node in component:
			node_component[node] = component

	component_graph = {}
	for component in components:
		component_graph[component] = []

	for node in graph:
		node_c = node_component[node]
		for successor in graph[node]:
			successor_c = node_component[successor]
			if node_c != successor_c:
				component_graph[node_c].append(successor_c)

	return topological_sort(component_graph)


# -*- coding: utf-8 -*-
"""
It would be very difficult to build a FIQL ``Expressions`` without taking into
account the ``Expressions`` part of it.

The ``expression`` module includes the code used for ensuring that any FIQL
``Expression`` created with this package is a valid FIQL ``Expression``.
"""
from __future__ import unicode_literals
from __future__ import absolute_import

from .exceptions import FiqlObjectException
from .operator import Operator

class BaseExpression(object):

    """
    Both ``Constraint`` and ``Expression`` classes extend the
    ``BaseExpression`` class. A FIQL ``Constraint`` is a simple FIQL
    ``Expression``. As such, they share certain attributes.

    Note:

        The parent of any child of ``BaseExpression`` is always an
        ``Expression``. This is a bit contrary to what might be expected as an
        ``Expression`` itself is a child class of ``BaseExpression``.

        This quark is a side effect of the definition of the FIQL
        ``Constraint``. A FIQL ``Constraint`` can not be contained within
        another FIQL ``Constraint`` as a sub-expression. Both a FIQL
        ``Constraint`` and FIQL ``Expression`` can only be sub-expressions of
        an actual FIQL ``Expression``.

    Attributes:
        parent (Expression): The ``Expression`` which contains this object.
    """

    def __init__(self):
        """Initialize instance of ``BaseExpression``."""
        self.parent = None

    def set_parent(self, parent):
        """Set parent ``Expression`` for this object.

        Args:
            parent (Expression): The ``Expression`` which contains this object.

        Raises:
            FiqlObjectException: Parent must be of type ``Expression``.
        """
        if not isinstance(parent, Expression):
            raise FiqlObjectException("Parent must be of %s not %s" % (
                Expression, type(parent)))
        self.parent = parent

    def get_parent(self):
        """Get the parent ``Expression`` for this object.

        Returns:
            Expression: The ``Expression`` which contains this object.

        Raises:
            FiqlObjectException: Parent is ``None``.
        """
        if not isinstance(self.parent, Expression):
            raise FiqlObjectException("Parent must be of %s not %s" % (
                Expression, type(self.parent)))
        return self.parent

class Expression(BaseExpression):

    """
    The ``Expression`` is the largest logical unit of a FIQL ``Expression``. It
    must, like the ``Constraint`` evaluate to ``True`` or ``False``. The
    ``Expression`` can both contain and be contained by an ``Expression``. It,
    unlike the ``Operator`` and ``Constraint``, MUST contain specific
    attributes in order to be valid.

    This class contains the bulk of the logic to ensure that an ``Expression``
    generated by this code is a valid FIQL ``Expression``.

    Note:
        This ``Expression`` class uses a single ``Operator`` to join multiple
        ``Constraints``. This format has the advantage of working cleanly with
        many ORMs and being far more easily converted to the more string
        friendly format of ``Constraint``, ``Operator``, ``Constraint``, etc.
        than the more string friendly format can be converted to the other.

    Attributes:
        elements (list): List of ``Constraint`` and ``Expression`` elements in
            this ``Expression``.
        operator (Operator): The ``Operator`` which relates the elements in
            this ``Expression``.
    """

    def __init__(self):
        """Initialize instance of ``Expression``."""
        super(Expression, self).__init__()
        self.elements = []
        self.operator = None
        # Keep track of which nested fragment we are in.
        self._working_fragment = self
        # Keep track of what was last added.
        self._last_element = None

    def has_constraint(self):
        """Return whether or not the working ``Expression`` has any
        ``Constraints``.

        Returns:
            integer: Number of logical elements within this ``Expression``.
        """
        return len(self.elements)

    def add_operator(self, operator):
        """Add an ``Operator`` to the ``Expression``.

        The ``Operator`` may result in a new ``Expression`` if an ``Operator``
        already exists and is of a different precedence.

        There are three possibilities when adding an ``Operator`` to an
        ``Expression`` depending on whether or not an ``Operator`` already
        exists:

          - No ``Operator`` on the working ``Expression``; Simply set the
            ``Operator`` and return ``self``.
          - ``Operator`` already exists and is higher in precedence; The
            ``Operator`` and last ``Constraint`` belong in a sub-expression of
            the working ``Expression``.
          - ``Operator`` already exists and is lower in precedence; The
            ``Operator`` belongs to the parent of the working ``Expression``
            whether one currently exists or not. To remain in the context of
            the top ``Expression``, this method will return the parent here
            rather than ``self``.

        Args:
            operator (Operator): What we are adding.

        Returns:
            Expression: ``self`` or related ``Expression``.

        Raises:
            FiqlObjectExpression: Operator is not a valid ``Operator``.
        """
        if not isinstance(operator, Operator):
            raise FiqlObjectException("%s is not a valid element type" % (
                operator.__class__))

        if not self._working_fragment.operator:
            self._working_fragment.operator = operator
        elif operator > self._working_fragment.operator:
            last_constraint = self._working_fragment.elements.pop()
            self._working_fragment = self._working_fragment \
                    .create_nested_expression()
            self._working_fragment.add_element(last_constraint)
            self._working_fragment.add_operator(operator)
        elif operator < self._working_fragment.operator:
            if self._working_fragment.parent:
                return self._working_fragment.parent.add_operator(operator)
            else:
                return Expression().add_element(self._working_fragment) \
                        .add_operator(operator)
        return self

    def add_element(self, element):
        """Add an element of type ``Operator``, ``Constraint``, or
        ``Expression`` to the ``Expression``.

        Args:
            element: ``Constraint``, ``Expression``, or ``Operator``.

        Returns:
            Expression: ``self``

        Raises:
            FiqlObjectException: Element is not a valid type.
        """
        if isinstance(element, BaseExpression):
            element.set_parent(self._working_fragment)
            self._working_fragment.elements.append(element)
            return self
        else:
            return self.add_operator(element)

    def create_nested_expression(self):
        """Create a nested ``Expression``, add it as an element to this
        ``Expression``, and return it.

        Returns:
            Expression: The newly created nested ``Expression``.
        """
        sub = Expression()
        self.add_element(sub)
        return sub

    def op_and(self, *elements):
        """Update the ``Expression`` by joining the specified additional
        ``elements`` using an "AND" ``Operator``

        Args:
            *elements (BaseExpression): The ``Expression`` and/or
                ``Constraint`` elements which the "AND" ``Operator`` applies
                to.

        Returns:
            Expression: ``self`` or related ``Expression``.
        """
        expression = self.add_operator(Operator(';'))
        for element in elements:
            expression.add_element(element)
        return expression

    def op_or(self, *elements):
        """Update the ``Expression`` by joining the specified additional
        ``elements`` using an "OR" ``Operator``

        Args:
            *elements (BaseExpression): The ``Expression`` and/or
                ``Constraint`` elements which the "OR" ``Operator`` applies
                to.

        Returns:
            Expression: ``self`` or related ``Expression``.
        """
        expression = self.add_operator(Operator(','))
        for element in elements:
            expression.add_element(element)
        return expression

    def to_python(self):
        """Deconstruct the ``Expression`` instance to a list or tuple
        (If ``Expression`` contains only one ``Constraint``).

        Returns:
            list or tuple: The deconstructed ``Expression``.
        """
        if len(self.elements) == 0:
            return None
        if len(self.elements) == 1:
            return self.elements[0].to_python()
        operator = self.operator or Operator(';')
        return [operator.to_python()] + \
            [elem.to_python() for elem in self.elements]

    def __str__(self):
        """Represent the ``Expression`` instance as a string.

        Returns:
            string: The represented ``Expression``.
        """
        operator = self.operator or Operator(';')
        elements_str = str(operator).join(
            ["{0}".format(elem) for elem in self.elements])
        if self.parent:
            parent_operator = self.parent.operator or Operator(';')
            if parent_operator > operator:
                return "(" + elements_str + ")"
        return elements_str


"""Test client using asyncio."""
import asyncio

from leicacam.async_cam import AsyncCAM

async def run(loop):
    """Run client."""
    cam = AsyncCAM(loop=loop)
    await cam.connect()
    print(cam.welcome_msg)
    await cam.send(b'/cmd:deletelist')
    print(await cam.receive())
    await cam.send(b'/cmd:deletelist')
    print(await cam.wait_for(cmd='cmd', timeout=0.1))
    await cam.send(b'/cmd:deletelist')
    print(await cam.wait_for(cmd='cmd', timeout=0))
    print(await cam.wait_for(cmd='cmd', timeout=0.1))
    print(await cam.wait_for(cmd='test', timeout=0.1))
    cam.close()

if __name__ == '__main__':
    LOOP = asyncio.new_event_loop()
    LOOP.run_until_complete(run(LOOP))
    LOOP.close()


"""Provide an interface to the CAM server."""
from __future__ import print_function

import os
import functools
import logging
import platform
import socket
from collections import OrderedDict
from time import sleep, time

import pydebug

_LOGGER = logging.getLogger(__name__)

def logger(function):
    """Decorate passed in function and log message to module logger."""
    @functools.wraps(function)
    def wrapper(*args, **kwargs):
        """Wrap function."""
        sep = kwargs.get('sep', ' ')
        end = kwargs.get('end', '')  # do not add newline by default
        out = sep.join([repr(x) for x in args])
        out = out + end
        _LOGGER.debug(out)
        return function(*args, **kwargs)
    return wrapper

# debug with `DEBUG=leicacam python script.py`
if platform.system() == 'Windows':
    # monkeypatch
    @logger
    def debug(msg):
        """Debug on Windows."""
        try:
            dbg = os.environ['DEBUG']
            if dbg in ('leicacam', '*'):
                print('leicacam ' + str(msg))
        except KeyError:
            pass
else:
    debug = logger(pydebug.debug('leicacam'))  # pylint: disable=invalid-name

class BaseCAM(object):
    """Base driver for LASAF Computer Assisted Microscopy."""

    # pylint: disable=too-many-instance-attributes

    def __init__(self, host='127.0.0.1', port=8895):
        """Set up instance."""
        self.host = host
        self.port = port
        # prefix for all commands
        self.prefix = [('cli', 'python-leicacam'),
                       ('app', 'matrix')]
        self.prefix_bytes = b'/cli:python-leicacam /app:matrix '
        self.buffer_size = 1024
        self.delay = 0.1  # poll every 100ms when waiting for incomming

    def send(self, commands):
        """Send commands to LASAF through CAM-socket.

        Parameters
        ----------
        commands : list of tuples or bytes string
            Commands as a list of tuples or a bytes string. cam.prefix is
            allways prepended before sending.

        Returns
        -------
        int
            Bytes sent.

        Example
        -------
        ::

            >>> # send list of tuples
            >>> cam.send([('cmd', 'enableall'), ('value', 'true')])

            >>> # send bytes string
            >>> cam.send(b'/cmd:enableall /value:true')

        """
        raise NotImplementedError

    def receive(self):
        """Receive message from socket interface as list of OrderedDict."""
        raise NotImplementedError

    def wait_for(self, cmd, value=None, timeout=60):
        """Hang until command is received.

        If value is supplied, it will hang until ``cmd:value`` is received.

        Parameters
        ----------
        cmd : string
            Command to wait for in bytestring from microscope CAM interface. If
            ``value`` is falsey, value of received command does not matter.
        value : string
            Wait until ``cmd:value`` is received.
        timeout : int
            Minutes to wait for command. If timeout is reached, an empty
            OrderedDict will be returned.

        Returns
        -------
        collections.OrderedDict
            Last received messsage or empty message if timeout is reached.

        """
        raise NotImplementedError

    def _prepare_send(self, commands):
        """Prepare message to be sent.

        Parameters
        ----------
        commands : list of tuples or bytes string
            Commands as a list of tuples or a bytes string. cam.prefix is
            allways prepended before sending.

        Returns
        -------
        string
            Message to be sent.

        """
        if isinstance(commands, bytes):
            msg = self.prefix_bytes + commands
        else:
            msg = tuples_as_bytes(self.prefix + commands)
        debug(b'> ' + msg)
        return msg

def _parse_receive(incomming):
    """Parse received response.

    Parameters
    ----------
    incomming : bytes string
        Incomming bytes from socket server.

    Returns
    -------
    list of OrderedDict
        Received message as a list of OrderedDict.

    """
    debug(b'< ' + incomming)
    # remove terminating null byte
    incomming = incomming.rstrip(b'\x00')
    # split received messages
    # return as list of several messages received
    msgs = incomming.splitlines()
    return [bytes_as_dict(msg) for msg in msgs]

class CAM(BaseCAM):
    """Driver for LASAF Computer Assisted Microscopy."""

    # pylint: disable=too-many-instance-attributes

    def __init__(self, *args, **kwargs):
        """Set up instance."""
        super(CAM, self).__init__(*args, **kwargs)
        self.connect()

    def connect(self):
        """Connect to LASAF through a CAM-socket."""
        self.socket = socket.socket()
        self.socket.connect((self.host, self.port))
        self.socket.settimeout(False)  # non-blocking
        sleep(self.delay)  # wait for response
        self.welcome_msg = self.socket.recv(
            self.buffer_size)  # receive welcome message

    def flush(self):
        """Flush incomming socket messages."""
        debug('flushing incomming socket messages')
        try:
            while True:
                msg = self.socket.recv(self.buffer_size)
                debug(b'< ' + msg)
        except socket.error:
            pass

    def send(self, commands):
        """Send commands to LASAF through CAM-socket.

        Parameters
        ----------
        commands : list of tuples or bytes string
            Commands as a list of tuples or a bytes string. cam.prefix is
            allways prepended before sending.

        Returns
        -------
        int
            Bytes sent.

        Example
        -------
        ::

            >>> # send list of tuples
            >>> cam.send([('cmd', 'enableall'), ('value', 'true')])

            >>> # send bytes string
            >>> cam.send(b'/cmd:enableall /value:true')

        """
        self.flush()  # discard any waiting messages
        msg = self._prepare_send(commands)
        return self.socket.send(msg)

    def receive(self):
        """Receive message from socket interface as list of OrderedDict."""
        try:
            incomming = self.socket.recv(self.buffer_size)
        except socket.error:
            return []

        return _parse_receive(incomming)

    def wait_for(self, cmd, value=None, timeout=60):
        """Hang until command is received.

        If value is supplied, it will hang until ``cmd:value`` is received.

        Parameters
        ----------
        cmd : string
            Command to wait for in bytestring from microscope CAM interface. If
            ``value`` is falsey, value of received command does not matter.
        value : string
            Wait until ``cmd:value`` is received.
        timeout : int
            Minutes to wait for command. If timeout is reached, an empty
            OrderedDict will be returned.

        Returns
        -------
        collections.OrderedDict
            Last received messsage or empty message if timeout is reached.

        """
        wait = time() + timeout * 60
        while True:
            if time() > wait:
                return OrderedDict()
            msgs = self.receive()
            msg = check_messages(msgs, cmd, value=value)
            if msg:
                return msg
            sleep(self.delay)

    def close(self):
        """Close the socket."""
        self.socket.close()

    # convenience methods for commands
    def start_scan(self):
        """Start the matrix scan."""
        cmd = [('cmd', 'startscan')]
        self.send(cmd)
        return self.wait_for(*cmd[0])

    def stop_scan(self):
        """Stop the matrix scan."""
        cmd = [('cmd', 'stopscan')]
        self.send(cmd)
        return self.wait_for(*cmd[0])

    def autofocus_scan(self):
        """Start the autofocus job."""
        cmd = [('cmd', 'autofocusscan')]
        self.send(cmd)
        return self.wait_for(*cmd[0])

    def pause_scan(self):
        """Pause the matrix scan."""
        cmd = [('cmd', 'pausescan')]
        self.send(cmd)
        return self.wait_for(*cmd[0])

    def enable(self, slide=0, wellx=1, welly=1, fieldx=1, fieldy=1):
        """Enable a given scan field."""
        # pylint: disable=too-many-arguments
        cmd = [
            ('cmd', 'enable'),
            ('slide', str(slide)),
            ('wellx', str(wellx)),
            ('welly', str(welly)),
            ('fieldx', str(fieldx)),
            ('fieldy', str(fieldy)),
            ('value', 'true')
        ]
        self.send(cmd)
        return self.wait_for(*cmd[0])

    def disable(self, slide=0, wellx=1, welly=1, fieldx=1, fieldy=1):
        """Disable a given scan field."""
        # pylint: disable=too-many-arguments
        cmd = [
            ('cmd', 'enable'),
            ('slide', str(slide)),
            ('wellx', str(wellx)),
            ('welly', str(welly)),
            ('fieldx', str(fieldx)),
            ('fieldy', str(fieldy)),
            ('value', 'false')
        ]
        self.send(cmd)
        return self.wait_for(*cmd[0])

    def enable_all(self):
        """Enable all scan fields."""
        cmd = [('cmd', 'enableall'), ('value', 'true')]
        self.send(cmd)
        return self.wait_for(*cmd[0])

    def disable_all(self):
        """Disable all scan fields."""
        cmd = [('cmd', 'enableall'), ('value', 'false')]
        self.send(cmd)
        return self.wait_for(*cmd[0])

    def save_template(self, filename="{ScanningTemplate}leicacam.xml"):
        """Save scanning template to filename."""
        cmd = [
            ('sys', '0'),
            ('cmd', 'save'),
            ('fil', str(filename))
        ]
        self.send(cmd)
        return self.wait_for(*cmd[0])

    def load_template(self, filename="{ScanningTemplate}leicacam.xml"):
        """Load scanning template from filename.

        Template needs to exist in database, otherwise it will not load.

        Parameters
        ----------
        filename : str
            Filename to template to load. Filename may contain path also, in
            such case, the basename will be used. '.xml' will be stripped
            from the filename if it exists because of a bug; LASAF implicit
            add '.xml'. If '{ScanningTemplate}' is omitted, it will be added.

        Returns
        -------
        collections.OrderedDict
            Response from LASAF in an ordered dict.

        Example
        -------
        ::

            >>> # load {ScanningTemplate}leicacam.xml
            >>> cam.load_template('leicacam')

            >>> # load {ScanningTemplate}leicacam.xml
            >>> cam.load_template('{ScanningTemplate}leicacam')

            >>> # load {ScanningTemplate}leicacam.xml
            >>> cam.load_template('/path/to/{ScanningTemplate}leicacam.xml')

        """
        basename = os.path.basename(filename)
        if basename[-4:] == '.xml':
            basename = basename[:-4]
        if basename[:18] != '{ScanningTemplate}':
            basename = '{ScanningTemplate}' + basename
        cmd = [
            ('sys', '0'),
            ('cmd', 'load'),
            ('fil', str(basename))
        ]
        self.send(cmd)
        return self.wait_for(*cmd[1])

    def get_information(self, about='stage'):
        """Get information about given keyword. Defaults to stage."""
        cmd = [
            ('cmd', 'getinfo'),
            ('dev', str(about))
        ]
        self.send(cmd)
        return self.wait_for(*cmd[1])

##
# Helper methods
##

def tuples_as_bytes(cmds):
    """Format list of tuples to CAM message with format /key:val.

    Parameters
    ----------
    cmds : list of tuples
        List of commands as tuples.

    Returns
    -------
    bytes
        Sequence of /key:val.

    Example
    -------
    ::

        >>> tuples_as_bytes([('cmd', 'val'), ('cmd2', 'val2')])
        b'/cmd:val /cmd2:val2'

    """
    cmds = OrderedDict(cmds)  # override equal keys
    tmp = []
    for key, val in cmds.items():
        key = str(key)
        val = str(val)
        tmp.append('/' + key + ':' + val)
    return ' '.join(tmp).encode()

def tuples_as_dict(_list):
    """Translate a list of tuples to OrderedDict with key and val as strings.

    Parameters
    ----------
    _list : list of tuples

    Returns
    -------
    collections.OrderedDict

    Example
    -------
    ::

        >>> tuples_as_dict([('cmd', 'val'), ('cmd2', 'val2')])
        OrderedDict([('cmd', 'val'), ('cmd2', 'val2')])

    """
    _dict = OrderedDict()
    for key, val in _list:
        key = str(key)
        val = str(val)
        _dict[key] = val
    return _dict

def bytes_as_dict(msg):
    """Parse CAM message to OrderedDict based on format /key:val.

    Parameters
    ----------
    msg : bytes
        Sequence of /key:val.

    Returns
    -------
    collections.OrderedDict
        With /key:val => dict[key] = val.

    """
    # decode bytes, assume '/' in start
    cmd_strings = msg.decode()[1:].split(r' /')
    cmds = OrderedDict()
    for cmd in cmd_strings:
        unpacked = cmd.split(':')
        # handle string not well formated (ex filenames with c:\)
        if len(unpacked) > 2:
            key = unpacked[0]
            val = ':'.join(unpacked[1:])
        elif len(unpacked) < 2:
            continue
        else:
            key, val = unpacked
        cmds[key] = val
    return cmds

def check_messages(msgs, cmd, value=None):
    """Check if specific message is present.

    Parameters
    ----------
    cmd : string
        Command to check for in bytestring from microscope CAM interface. If
        ``value`` is falsey, value of received command does not matter.
    value : string
        Check if ``cmd:value`` is received.

    Returns
    -------
    collections.OrderedDict
        Correct messsage or None if no correct message if found.

    """
    for msg in msgs:
        if value and msg.get(cmd) == value:
            return msg
        if not value and msg.get(cmd):
            return msg
    return None


# incfile.py
# Copyright (c) 2013-2019 Pablo Acosta-Serafini
# See LICENSE for details
# pylint: disable=C0111,R0914

import os

def incfile(fname, fpointer, lrange="1,6-", sdir=None):
    r"""
    Include a Python source file in a docstring formatted in reStructuredText.

    :param fname: File name, relative to environment variable
                  :bash:`${TRACER_DIR}`
    :type  fname: string

    :param fpointer: Output function pointer. Normally is :code:`cog.out` but
                     :code:`print` or other functions can be used for
                     debugging
    :type  fpointer: function object

    :param lrange: Line range to include, similar to Sphinx
                   `literalinclude <http://sphinx-doc.org/markup/code.html
                   #directive-literalinclude>`_ directive
    :type  lrange: string

    :param sdir: Source file directory. If None the :bash:`${TRACER_DIR}`
                 environment variable is used if it is defined, otherwise
                 the directory where the :code:`docs.support.incfile` module
                 is located is used
    :type  sdir: string

    For example:

    .. code-block:: python

        def func():
            \"\"\"
            This is a docstring. This file shows how to use it:

            .. =[=cog
            .. import docs.support.incfile
            .. docs.support.incfile.incfile('func_example.py', cog.out)
            .. =]=
            .. code-block:: python

                # func_example.py
                if __name__ == '__main__':
                    func()

            .. =[=end=]=
            \"\"\"
            return 'This is func output'
    """
    # Read file
    file_dir = (
        sdir
        if sdir
        else os.environ.get("TRACER_DIR", os.path.abspath(os.path.dirname(__file__)))
    )
    fname = os.path.join(file_dir, fname)
    with open(fname) as fobj:
        lines = fobj.readlines()
    # Parse line specification
    tokens = [item.strip() for item in lrange.split(",")]
    inc_lines = []
    for token in tokens:
        if "-" in token:
            subtokens = token.split("-")
            lmin, lmax = (
                int(subtokens[0]),
                int(subtokens[1]) if subtokens[1] else len(lines),
            )
            for num in range(lmin, lmax + 1):
                inc_lines.append(num)
        else:
            inc_lines.append(int(token))
    # Produce output
    fpointer(".. code-block:: python\n")
    fpointer("\n")
    for num, line in enumerate(lines):
        if num + 1 in inc_lines:
            fpointer("    " + line.replace("\t", "    ") if line.strip() else "\n")
    fpointer("\n")


import os
import json

from django.conf import settings
from django.core.exceptions import ImproperlyConfigured

def locate_package_json():
    """
    Find and return the location of package.json.
    """
    directory = settings.SYSTEMJS_PACKAGE_JSON_DIR
    if not directory:
        raise ImproperlyConfigured(
            "Could not locate 'package.json'. Set SYSTEMJS_PACKAGE_JSON_DIR "
            "to the directory that holds 'package.json'."
        )
    path = os.path.join(directory, 'package.json')
    if not os.path.isfile(path):
        raise ImproperlyConfigured("'package.json' does not exist, tried looking in %s" % path)
    return path

def parse_package_json():
    """
    Extract the JSPM configuration from package.json.
    """
    with open(locate_package_json()) as pjson:
        data = json.loads(pjson.read())
    return data

def find_systemjs_location():
    """
    Figure out where `jspm_packages/system.js` will be put by JSPM.
    """
    location = os.path.abspath(os.path.dirname(locate_package_json()))
    conf = parse_package_json()

    if 'jspm' in conf:
        conf = conf['jspm']

    try:
        conf = conf['directories']
    except TypeError:
        raise ImproperlyConfigured("`package.json` doesn't appear to be a valid json object. "
                                   "Location: %s" % location)
    except KeyError:
        raise ImproperlyConfigured("The `directories` configuarion was not found in package.json. "
                                   "Please check your jspm install and/or configuarion. `package.json` "
                                   "location: %s" % location)

    # check for explicit location, else fall back to the default as jspm does
    jspm_packages = conf['packages'] if 'packages' in conf else 'jspm_packages'
    base = conf['baseURL'] if 'baseURL' in conf else '.'
    return os.path.join(location, base, jspm_packages, 'system.js')


# coding: utf-8
from __future__ import absolute_import, division, print_function

import sys
from datetime import datetime

import six
from represent import ReprHelperMixin
from requests import HTTPError

from .exceptions import (
    YOURLSAPIError, YOURLSHTTPError, YOURLSKeywordExistsError,
    YOURLSNoLoopError, YOURLSNoURLError, YOURLSURLExistsError)
from .log import logger

class ShortenedURL(ReprHelperMixin, object):
    """Represent shortened URL data as returned by the YOURLS API.

    .. attribute:: keyword

       Short URL keyword, e.g. ``abcdef`` for ``http://example.com/abcdef``.

    .. attribute:: url

       Long URL that was shortened.

    .. attribute:: title

       URL page title.

    .. attribute:: date

       :py:class:`~datetime.datetime` of timestamp the URL was shortened.

    .. attribute:: ip

       IP address that originally shortened the URL.

    .. attribute:: clicks

       Number of clicks the shortened URL has received.

    """
    __slots__ = ('shorturl', 'url', 'title', 'date', 'ip', 'clicks', 'keyword')

    def __init__(self, shorturl, url, title, date, ip, clicks, keyword=None):
        self.shorturl = shorturl
        self.url = url
        self.title = title
        self.date = date
        self.ip = ip
        self.clicks = clicks
        self.keyword = keyword

    def _repr_helper_(self, r):
        r.keyword_from_attr('shorturl')
        r.keyword_from_attr('url')
        r.keyword_from_attr('title')
        r.keyword_from_attr('date')
        r.keyword_from_attr('ip')
        r.keyword_from_attr('clicks')
        if self.keyword is not None:
            r.keyword_from_attr('keyword')

    def __eq__(self, other):
        if isinstance(other, ShortenedURL):
            params = ('shorturl', 'url', 'title', 'date', 'ip', 'clicks', 'keyword')
            return all(getattr(self, p) == getattr(other, p) for p in params)
        else:
            return NotImplemented

class DBStats(ReprHelperMixin, object):
    """Represent database statistics as returned by the YOURLS API.

    .. attribute:: total_clicks

       Total number of clicks across all links in the database.

    .. attribute:: total_links

       Total number of links in the database.
    """
    __slots__ = ('total_clicks', 'total_links')

    def __init__(self, total_clicks, total_links):
        self.total_clicks = total_clicks
        self.total_links = total_links

    def _repr_helper_(self, r):
        r.keyword_from_attr('total_clicks')
        r.keyword_from_attr('total_links')

    def __eq__(self, other):
        if isinstance(other, DBStats):
            params = ('total_clicks', 'total_links')
            return all(getattr(self, p) == getattr(other, p) for p in params)
        else:
            return NotImplemented

def _handle_api_error_with_json(http_exc, jsondata, response):
    """Handle YOURLS API errors.

    requests' raise_for_status doesn't show the user the YOURLS json response,
    so we parse that here and raise nicer exceptions.
    """
    if 'code' in jsondata and 'message' in jsondata:
        code = jsondata['code']
        message = jsondata['message']

        if code == 'error:noloop':
            raise YOURLSNoLoopError(message, response=response)
        elif code == 'error:nourl':
            raise YOURLSNoURLError(message, response=response)

    elif 'message' in jsondata:
        message = jsondata['message']
        raise YOURLSHTTPError(message, response=response)

    http_error_message = http_exc.args[0]
    raise YOURLSHTTPError(http_error_message, response=response)

def _validate_yourls_response(response, data):
    """Validate response from YOURLS server."""
    try:
        response.raise_for_status()
    except HTTPError as http_exc:
        # Collect full HTTPError information so we can reraise later if required.
        http_error_info = sys.exc_info()

        # We will reraise outside of try..except block to prevent exception
        # chaining showing wrong traceback when we try and parse JSON response.
        reraise = False

        try:
            jsondata = response.json()
        except ValueError:
            reraise = True
        else:
            logger.debug('Received error {response} with JSON {json}',
                         response=response, json=jsondata)
            _handle_api_error_with_json(http_exc, jsondata, response)

        if reraise:
            six.reraise(*http_error_info)
    else:
        # We have a valid HTTP response, but we need to check what the API says
        # about the request.
        jsondata = response.json()

        logger.debug('Received {response} with JSON {json}', response=response,
                     json=jsondata)

        if {'status', 'code', 'message'} <= set(jsondata.keys()):
            status = jsondata['status']
            code = jsondata['code']
            message = jsondata['message']

            if status == 'fail':
                if code == 'error:keyword':
                    raise YOURLSKeywordExistsError(message, keyword=data['keyword'])
                elif code == 'error:url':
                    url = _json_to_shortened_url(jsondata['url'], jsondata['shorturl'])
                    raise YOURLSURLExistsError(message, url=url)
                else:
                    raise YOURLSAPIError(message)
            else:
                return jsondata
        else:
            # Without status, nothing special needs to be handled.
            return jsondata

def _json_to_shortened_url(urldata, shorturl=None):
    if shorturl is None:
        shorturl = urldata['shorturl']

    if 'date' in urldata:
        date = urldata['date']
    elif 'timestamp' in urldata:
        date = urldata['timestamp']
    else:
        raise YOURLSAPIError("Expected 'date' or 'timestamp' key in JSON response.")

    keyword = urldata.get('keyword', None)

    clicks = int(urldata.get('clicks', '0'))

    url = ShortenedURL(
        shorturl=shorturl,
        url=urldata['url'],
        title=urldata['title'],
        date=datetime.strptime(date, '%Y-%m-%d %H:%M:%S'),
        ip=urldata['ip'],
        clicks=clicks,
        keyword=keyword)

    return url