import numpy as np
import pandas as pd
import matplotlib as mpl
import os, subprocess, inspect, io, copy, collections, warnings, numba
import argiope
from string import Template

MODPATH = os.path.dirname(inspect.getfile(argiope))

################################################################################
# CONSTANTS AND DEFINITIONS
################################################################################
class Element:
  """
  The Element meta class.
  """
  def __init__(self, nvert, edges = None, simplices = None ):
    self.nvert = nvert
    self.edges = edges
    self.simplices = simplices

  def __repr__(self):
    return "<{0}D element w. {1} vert.>".format(self.space, self.nvert)

  def split(self, elements, to, at = "labels"):
    """
    Splits elements of the rigth type.
    """
    return

class Element0D(Element):
  space = 0
  pass

class Element1D(Element):
  space = 1
  pass

class Element2D(Element):
  space = 2

  def __init__(self, angles, optimal_angles, *args, **kwargs):
    self.angles = angles
    self.optimal_angles = optimal_angles
    super().__init__(*args, **kwargs)

  def get_surfaces(self):
    return self.edges
  surfaces = property(get_surfaces)

class Element3D(Element):
  space = 3

  def __init__(self, faces, faces_types, *args, **kwargs):
    self.faces = faces
    self.faces_types = faces_types
    super().__init__(*args, **kwargs)

  def get_angles(self):
    return np.concatenate([self.faces[i][ELEMENTS[self.faces_types[i]].angles]
                           for i in range(len(self.faces))])
  angles = property(get_angles)

  def get_optimal_angles(self):
    return  np.concatenate([ELEMENTS[e].optimal_angles
            for e in self.faces_types])
  optimal_angles = property(get_optimal_angles)

  def get_surfaces(self):
    return self.faces
  surfaces = property(get_surfaces)

ELEMENTS = {
    "point1": Element0D(nvert = 1),
    "line2": Element1D(
        nvert =  2),
    "tri3":  Element2D(
        nvert = 3,
        edges = np.array([[0, 1],[1, 2], [2, 0]]),
        simplices = np.array([[0, 1, 2]]),
        angles = np.array([[2, 0, 1], [0, 1, 2], [1, 2, 0]]),
        optimal_angles = np.array([60., 60., 60.])),
    "quad4": Element2D(
        nvert = 4,
        edges = np.array([[0, 1], [1, 2], [2, 3],[3, 0]]),
        simplices = np.array([[0, 1, 3], [1, 2, 3]]),
        angles = np.array([[3, 0, 1], [0, 1, 2], [1, 2, 3], [2, 3, 0]]),
        optimal_angles = np.array([90., 90., 90., 90.])),
    "tetra4": Element3D(
        nvert = 4,
        edges = np.array([[0, 1],
                          [1, 2],
                          [2, 3],
                          [3, 0]]),
        faces = np.array([[0, 1, 2],
                          [0, 3, 1],
                          [1, 3, 2],
                          [2, 3, 0]]),
        faces_types = np.array(["tri3",
                                "tri3",
                                "tri3",
                                "tri3"]),
        simplices = np.array([[0, 1, 2, 3]])),
    "pyra5": Element3D(
        nvert = 5,
        edges = np.array([[0, 1], [1, 2], [2, 3], [3, 0],
                          [0, 4], [1, 4], [2, 4], [3, 4]]),
        faces = [np.array([0, 1, 2, 3]),
                 np.array([0, 1, 4]),
                 np.array([1, 2, 4]),
                 np.array([2, 3, 4]),
                 np.array([3, 0, 4])],
        faces_types = np.array(["quad4",
                               "tri3", "tri3", "tri3", "tri3"]),
        simplices = np.array([[0, 1, 3, 4],  [1, 2, 3, 4]])),
    "prism6": Element3D(
        nvert = 6,
        edges = np.array([[0, 1], [1, 2], [2, 0], [3, 4],
                          [4, 5], [5, 3], [0, 3], [1, 4], [2, 5]]),
        faces = [np.array([0, 1, 2]),
                 np.array([3, 5, 4]),
                 np.array([0, 3, 4, 1]),
                 np.array([1, 4, 5, 2]),
                 np.array([2, 5, 3, 0])],
        faces_types = np.array(["tri3", "tri3",
                               "quad4", "quad4", "quad4"]),
        simplices = np.array([[0, 1, 2, 3],
                              [1, 2, 3, 4],
                              [2, 3, 4, 5]])),
    "hexa8":Element3D(
        nvert = 8,
        edges = np.array([[0, 1], [1, 2], [2, 3], [3, 0],
                          [4, 5], [5, 6], [6, 7], [7, 4],
                          [0, 4], [1, 5], [2, 6], [3, 7]]),
        faces = np.array([[0, 1, 2, 3],
                          [4, 7, 6, 5],
                          [0, 4, 5, 1],
                          [1, 5, 6, 2],
                          [2, 6, 7, 3],
                          [3, 7, 4, 0]]),
        faces_types = np.array(["quad4", "quad4", "quad4", "quad4",
                                "quad4", "quad4"]),
        simplices =np.array([[0, 1, 3, 4],
                            [1, 2, 3, 4],
                            [3, 2, 7, 4],
                            [2, 6, 7, 4],
                            [1, 5, 2, 4],
                            [2, 5, 6, 4]])),

       }

################################################################################

################################################################################
# MESH CLASSES
################################################################################

class Mesh(argiope.utils.Container):
  """
  A single class to handle meshes and associated data. The class constructor only a redirection to the following methods:

  * :func:`Mesh.set_nodes`
  * :func:`Mesh.set_elements`
  * :func:`Mesh.set_fields`

  A simple example is given below.

  .. literalinclude:: examples/mesh/Mesh.py

  For more complex examples, follow the notebook tutorials.
  """
  def __init__(self,**kwargs):
    self.set_nodes(**kwargs)
    self.set_elements(**kwargs)
    self.set_fields(**kwargs)

  def __repr__(self):
    Ne, Nn = 0, 0
    if self.elements is not None : Ne = self.elements.index.size
    if self.nodes is not None: Nn = self.nodes.index.size
    return "<Mesh, {0} nodes, {1} elements, {2} fields>".format(
           Nn, Ne, len(self.fields))

  def set_nodes(self, nlabels = [], coords = [], nsets = {}, **kwargs):
    r"""
    Sets the node data.

    :arg nlabels: node labels. Items be strictly positive and int typed
                  in 1D array-like with shape :math:`(N_n)`.
    :type nlabels: 1D uint typed array-like
    :arg coords: node coordinates. Must be float typed 2D array-like of shape
                 :math:`(N_n \times 3)`.
    :type coords: 2D float typed array-like
    :arg nsets: node sets. Contains boolean array-like of shape :math:`(N_n)`.
    :type nsets: dict
    """
    # DATA PREPROCESSING
    nlabels = np.array(nlabels).astype(np.int64)
    coords = np.array(coords).astype(np.float64)
    if (nlabels < 0).sum() > 0:
      raise ValueError("Node labels must be strictly positive.")
    if len(nlabels) != len(coords):
      raise ValueError("'nlabels' and 'coords' must have the same length")
    if coords.shape[1] != 3:
      raise ValueError("coordinates must be 3 dimensional.")
    # ATTRIBUTES CREATION
    columns = pd.MultiIndex.from_tuples((("coords", "x"),
                                         ("coords", "y"),
                                         ("coords", "z")))
    self.nodes = pd.DataFrame(data = coords,
                              columns = columns,
                              index = nlabels)
    self.nodes.index.name = "node"
    for k, v in nsets.items():
      v = np.array(v)
      if v.dtype != 'bool':
        raise ValueError("Sets must be boolean array-likes.")
      self.nodes["sets", k] = v
    self.nodes["sets", "all"] = True

  def set_elements(self, elabels = None,
                         types = None,
                         stypes = "",
                         conn = None,
                         esets = {},
                         surfaces = {},
                         materials = "",
                         **kwargs):
    """
    Sets the element data.

    :arg elabels: element labels. Items be strictly positive and int typed
                  in 1D array-like with shape :math:`(N_e)`.
    :type elabels: 1D uint typed array-like
    :arg types: element types chosen among argiope specific element types.
    :type types: str typed array-like
    :arg stypes: element types chosen in solver (depends on the chosen solver) specific element types.
    :type stypes: str typed array-like
    :arg conn: connectivity table. In order to deal with non rectangular tables, :math:`0` can be used to fill missing data.
    :type conn: uint typed array-like
    :arg esets: element sets. Contains boolean array-like of shape :math:`(N_e)`.
    :type esets: dict
    :arg surfaces: surfaces. Contains boolean array-like of shape :math:`(N_e, N_s )` with :math:`N_s` being the maximum number of faces on a single element.
    :type surfaces: dict
    :arg materials: material keys. Any number a of materials can be used.
    :type materials: str typed array-like

    """
    # COLUMNS BUILDING
    if elabels is None:
       warnings.warn(
       "Since no element labels where provided, no elements where created",
       Warning)
       self.elements = None
    else:
      columns = pd.MultiIndex.from_tuples([("type", "argiope", "")])
      self.elements = pd.DataFrame(data = types,
                                   columns = columns,
                                   index = elabels)
      self.elements.index.name = "element"
      self.elements.loc[:, ("type", "solver", "")] = stypes
      # Connectivity
      c = pd.DataFrame(conn, index = elabels)
      c.fillna(0, inplace = True)
      c[:] = c.values.astype(np.int32)
      c.columns = pd.MultiIndex.from_product([["conn"],
                                              ["n{0}".format(n) for
                                               n in np.arange(c.shape[1])],
                                              [""]])
      self.elements = self.elements.join(c)
      # Sets
      for k, v in esets.items(): self.elements[("sets", k, "")] = v
      self.elements["sets", "all", ""] = True
      # Surfaces
      for k, v in surfaces.items():
        for fk, vv in v.items():
          self.elements[("surfaces", k, "s{0}".format(fk))] = vv
      # Materials
      self.elements[("materials", "", "") ] = materials
      self.elements.sort_index(axis = 1, inplace = True)

  def set_fields(self, fields = None, **kwargs):
    """
    Sets the fields.
    """
    self.fields = []
    if fields != None:
      for field in fields:
        self.fields.append(field)

  def add_fields(self, fields = None, **kwargs):
    """
    Add the fields into the list of fields.
    """
    if fields != None:
      for field in fields:
        self.fields.append(field)

  def check_elements(self):
    """
    Checks element definitions.
    """
    # ELEMENT TYPE CHECKING
    existing_types = set(self.elements.type.argiope.values.flatten())
    allowed_types = set(ELEMENTS.keys())
    if (existing_types <= allowed_types) == False:
      raise ValueError("Element types {0} not in know elements {1}".format(
                       existing_types - allowed_types, allowed_types))
    print("<Elements: OK>")

  def space(self):
    """
    Returns the dimension of the embedded space of each element.
    """
    return self.elements.type.argiope.map(
           lambda t: ELEMENTS[t].space)

  def nvert(self):
    """
    Returns the number of vertices of eache element according to its type/
    """
    return self.elements.type.argiope.map(
           lambda t: ELEMENTS[t].nvert)

  def split(self, into = "edges", loc = None,
            at = "labels", sort_index = True):
    """
    Returns the decomposition of the elements.

    Inputs:
    * into: must be in ['edges', 'faces', 'simplices', 'angles']
    * loc: None or labels of the chosen elements.
    * at: must be in ['labels', 'coords']
    """
    if type(loc) == type(None):
      elements = self.elements
    else:
      elements = self.elements.loc[loc]
    out = []
    for etype, group in elements.groupby([("type", "argiope", "")]):
      try:
        output_maps = getattr(ELEMENTS[etype], into)
        for om in range(len(output_maps)):
          oshape = len(output_maps[om])
          conn = group.conn
          columns = pd.MultiIndex.from_product([(om,), np.arange(oshape)],
                                                names = [into, "vertex"])
          data = (conn.values[:, output_maps[om]].reshape(len(conn), oshape))
          df = pd.DataFrame(data = data,
                            columns = columns,
                            index = conn.index).stack((0,1))
          out.append(df)
      except:
        print("Can not extract '{0}' from '{1}'".format(into, etype))
    if len(out) != 0:
      out = pd.concat(out)
      out.sort_index(inplace = True)
      if at == "coords":
        data = self.nodes.coords.loc[out.values].values
        out = pd.DataFrame(index = out.index, data = data,
                           columns = ["x", "y", "z"])
      return out

  def centroids_and_volumes(self, sort_index = True):
    """
    Returns a dataframe containing volume and centroids of all the elements.
    """
    elements = self.elements
    out = []
    for etype, group in self.elements.groupby([("type", "argiope", "")]):
      etype_info = ELEMENTS[etype]
      simplices_info = etype_info.simplices
      index = group.index
      simplices_data = self.split(into = "simplices",
                             loc = index,
                             at = "coords")
      simplices = simplices_data.values.reshape(
                  index.size,
                  simplices_info.shape[0],
                  simplices_info.shape[1],
                  3)
      edges = simplices[:,:,1:] - simplices[:,:,:1]
      simplices_centroids = simplices.mean(axis = 2)
      if etype_info.space == 2:
        simplices_volumes = np.linalg.norm(
                  np.cross(edges[:,:,0],
                           edges[:,:,1],
                           axis = 2),
                  axis = 2)/2.
      elif etype_info.space == 3:
        simplices_volumes =  (np.cross(edges[:,:,0],
                                       edges[:,:,1], axis = 2)
                             * edges[:,:, 2]).sum(axis = 2) / 6.
      elements_volumes = simplices_volumes.sum(axis = 1)
      elements_centroids = ((simplices_volumes.reshape(*simplices_volumes.shape, 1)
                          * simplices_centroids).sum(axis = 1)
                          / elements_volumes.reshape(*elements_volumes.shape,1))
      volumes_df = pd.DataFrame(index = index,
                                data = elements_volumes,
                                columns = pd.MultiIndex.from_product(
                                [["volume"], [""]]))
      centroids_df = pd.DataFrame(index = index,
                                data = elements_centroids,
                                columns = pd.MultiIndex.from_product(
                                [["centroid"], ["x", "y", "z"]]))
      out.append(pd.concat([volumes_df, centroids_df], axis = 1))
    out = pd.concat(out)
    if sort_index: out.sort_index(inplace = True)
    return out.sort_index(axis= 1)

  def angles(self, zfill = 3):
    """
    Returns the internal angles of all elements and the associated statistics
    """
    elements = self.elements.sort_index(axis = 1)
    etypes = elements[("type", "argiope")].unique()
    out = []
    for etype in etypes:
      etype_info = ELEMENTS[etype]
      angles_info = etype_info.angles
      loc = elements[("type", "argiope", "")] == etype
      index = elements.loc[loc].index
      angles_data = self.split(into = "angles",
                             loc = loc,
                             at = "coords")
      data = angles_data.values.reshape(index.size,
                                        angles_info.shape[0],
                                        angles_info.shape[1],
                                        3)
      edges = data[:,:,[0,2],:] - data[:,:,1:2,:]
      edges /= np.linalg.norm(edges, axis = 3).reshape(
               index.size, angles_info.shape[0], 2, 1)
      angles = np.degrees(np.arccos((
               edges[:,:,0] * edges[:,:,1]).sum(axis = 2)))

      deviation = angles - etype_info.optimal_angles
      angles_df = pd.DataFrame(index = index,
                               data = angles,
                               columns = pd.MultiIndex.from_product(
      [["angles"], ["a" + "{0}".format(s).zfill(zfill)
              for s in range(angles_info.shape[0])]]))
      deviation_df = pd.DataFrame(index = index,
                               data = deviation,
                               columns = pd.MultiIndex.from_product(
      [["deviation"], ["d" + "{0}".format(s).zfill(zfill)
              for s in range(angles_info.shape[0])]]))

      df = pd.concat([angles_df, deviation_df], axis = 1).sort_index(axis = 1)
      df["stats", "max_angle"] = df.angles.max(axis = 1)
      df["stats", "min_angle"] = df.angles.min(axis = 1)
      df["stats", "max_angular_deviation"] = df.deviation.max(axis = 1)
      df["stats", "min_angular_deviation"] = df.deviation.min(axis = 1)
      df["stats", "max_abs_angular_deviation"] = abs(df.deviation).max(axis = 1)
      df = df.sort_index(axis = 1)
      out.append(df)

    out = pd.concat(out).sort_index(axis = 1)
    return out

  def edges(self, zfill = 3):
    """
    Returns the aspect ratio of all elements.
    """
    edges = self.split("edges", at = "coords").unstack()
    edges["lx"] = edges.x[1]-edges.x[0]
    edges["ly"] = edges.y[1]-edges.y[0]
    edges["lz"] = edges.z[1]-edges.z[0]
    edges["l"] = np.linalg.norm(edges[["lx", "ly", "lz"]], axis = 1)
    edges = (edges.l).unstack()
    edges.columns = pd.MultiIndex.from_product([["length"],
                    ["e" + "{0}".format(s).zfill(zfill)
                    for s in np.arange(edges.shape[1])]])
    edges[("stats", "lmax")] = edges.length.max(axis = 1)
    edges[("stats", "lmin")] = edges.length.min(axis = 1)
    edges[("stats", "aspect_ratio")] = edges.stats.lmax / edges.stats.lmin
    return edges.sort_index(axis = 1)

  def stats(self):
    """
    Returns mesh quality and geometric stats.
    """
    cv = self.centroids_and_volumes()
    angles  = self.angles()
    edges = self.edges()
    return pd.concat([cv , angles[["stats"]], edges[["stats"]] ],
                     axis = 1).sort_index(axis = 1)

  def element_set_to_node_set(self, tag):
    """
    Makes a node set from an element set.
    """
    nodes, elements = self.nodes, self.elements
    loc = (elements.conn[elements[("sets", tag, "")]]
           .stack().stack().unique())
    loc = loc[loc != 0]
    nodes[("sets", tag)] = False
    nodes.loc[loc, ("sets", tag) ] = True

  def node_set_to_surface(self, tag):
    """
    Converts a node set to surface.
    """
    # Create a dummy node with label 0
    nodes = self.nodes.copy()
    dummy = nodes.iloc[0].copy()
    dummy["coords"] *= np.nan
    dummy["sets"] = True
    nodes.loc[0] = dummy
    # Getting element surfaces
    element_surfaces= self.split("surfaces").unstack()
    # killer hack !
    surf = pd.DataFrame(
             nodes.sets[tag].loc[element_surfaces.values.flatten()]
                   .values.reshape(element_surfaces.shape)
                   .prod(axis = 1)
                   .astype(np.bool),
             index = element_surfaces.index).unstack().fillna(False)
    for k in surf.keys():
      self.elements["surfaces", tag, "f{0}".format(k[1]+1) ] = surf.loc[:, k]

  def surface_to_element_sets(self, tag):
    """
    Creates elements sets corresponding to a surface.
    """
    surface = self.elements.surfaces[tag]
    for findex in surface.keys():
      if surface[findex].sum() != 0:
        self.elements[("sets", "_SURF_{0}_FACE{1}"
                     .format(tag, findex[1:]), "")] = surface[findex]

  def to_polycollection(self, *args, **kwargs):
    """
    Returns the mesh as matplotlib polygon collection. (tested only for 2D meshes)
    """
    from matplotlib import collections
    nodes, elements = self.nodes, self.elements.reset_index()
    verts = []
    index = []
    for etype, group in elements.groupby([("type", "argiope", "")]):
      index += list(group.index)
      nvert = ELEMENTS[etype].nvert
      conn = group.conn.values[:, :nvert].flatten()
      coords = nodes.coords[["x", "y"]].loc[conn].values.reshape(
                                            len(group), nvert, 2)
      verts += list(coords)
    verts = np.array(verts)
    verts= verts[np.argsort(index)]
    return collections.PolyCollection(verts, *args,**kwargs )

  def to_triangulation(self):
    """
    Returns the mesh as a matplotlib.tri.Triangulation instance. (2D only)
    """
    from matplotlib.tri import Triangulation
    conn = self.split("simplices").unstack()
    coords = self.nodes.coords.copy()
    node_map  = pd.Series(data = np.arange(len(coords)), index = coords.index)
    conn = node_map.loc[conn.values.flatten()].values.reshape(*conn.shape)
    return Triangulation(coords.x.values, coords.y.values, conn)

  def write_inp(self, *args, **kwargs):
    """
    Exports the mesh to the Abaqus INP format.
    """
    return write_inp(self, *args, **kwargs)

  def fields_metadata(self):
    """
    Returns fields metadata as a dataframe.
    """
    return (pd.concat([f.metadata() for f in self.fields], axis = 1)
            .transpose()
            .sort_values(["step_num", "frame", "label", "position"]))

################################################################################
# FIELDS
################################################################################

class MetaField(argiope.utils.Container):
  """
  A field mother class.

  :param label: field label
  :type label: str
  :param position: physical position
  :type position: in ["node", "element"]
  """
  _positions = ["node", "element"]

  def __init__(self, label = None, position = "node",
               step_num = None, step_label = None,
               part = None,
               frame = None, frame_value = None,
               data = None, **kwargs):
     self.label = label
     self.position = position
     self.step_num = step_num
     self.step_label = step_label
     self.frame = frame
     self.part = part
     self.frame_value = frame_value

     # Data
     self.data =  data
     if hasattr(self, "_columns"):
       self.data.columns = self._columns
     self.data.index.name = position

  def __repr__(self):
    return "<{0} {1} at {2}; step={3} ('{4}'); frame={5} >".format(
    self.__class__.__name__, self.label,
    self.position, self.step_num, self.step_label, self.frame)

  def metadata(self):
    """
    Returns metadata as a dataframe.
    """
    return pd.Series({
           "part": self.part,
           "step_num": self.step_num,
           "step_label": self.step_label,
           "frame": self.frame,
           "frame_value": self.frame_value,
           "label": self.label,
           "position": self.position,
    })

class Field(MetaField):
  pass

class Tensor6Field(MetaField):
  """
  Second order symmetric tensor field.
  """
  _columns = ["v11", "v22", "v33", "v12", "v13", "v23"]

class Tensor4Field(MetaField):
  """
  Second order symmetric 2D tensor field.
  """
  _columns = ["v11", "v22", "v33", "v12"]

class Tensor3Field(MetaField):
  """
  Second order diagonal tensor field.
  """
  _columns = ["v11", "v22", "v12"]

class Vector3Field(MetaField):
  """
  3D vector field.
  """
  _columns = ["v1", "v2", "v3"]

class Vector2Field(MetaField):
  """
  2D vector field.
  """
  _columns = ["v1", "v2"]

class ScalarField(MetaField):
  _columns = ["v"]

################################################################################

################################################################################
# PARSERS
################################################################################
def read_h5(hdfstore, group = ""):
  """
  DEPRECATED
  Reads a mesh saved in the HDF5 format.
  """
  m = Mesh()
  m.elements.data = hdf["elements/connectivity"]
  m.nodes.data    = hdf["nodes/xyz"]
  for key in hdf.keys():
    if key.startswith("/nodes/sets"):
      k = key.replace("/nodes/sets/", "")
      m.nodes.sets[k] = set(hdf[key])
    if key.startswith("/elements/sets"):
      k = key.replace("/elements/sets/", "")
      m.elements.sets[k] = set(hdf[key])
    if key.startswith("/elements/surfaces"):
      k = key.replace("/elements/surfaces/", "")
      m.elements.surfaces[k] = hdf[key]
    if key.startswith("/fields/"):
      if key.endswith("/metadata"):
        tag = key.split("/")[2]
        f = Field()
        f.metadata = hdf["fields/{0}/metadata".format(tag)]
        f.metadata = hdf["fields/{0}/data".format(tag)]
        f.master = m
        m.add_field(tag, f)
  hdf.close()
  return m

def read_msh(path):
  """
  Reads a GMSH MSH file and returns a :class:`Mesh` instance.

  :arg path: path to MSH file.
  :type path: str

  """
  elementMap = { 15:"point1",
                 1:"line2",
                 2:"tri3",
                 3:"quad4",
                 4:"tetra4",
                 5:"hexa8",
                 6:"prism6",
                 7:"pyra4",

               }
  lines = np.array(open(path, "r").readlines())
  locs = {}
  nl = len(lines)
  for i in range(nl):
    line = lines[i].lower().strip()
    if line.startswith("$"):
      if line.startswith("$end"):
        locs[env].append(i)
      else:
        env = line[1:]
        locs[env] = [i]
  nodes = pd.read_csv(
          io.StringIO("\n".join(
                   lines[locs["nodes"][0]+2:locs["nodes"][1]])),
                   sep = " ",
                   names = ["labels", "x", "y", "z"])

  elements = {"labels":[], "etype":[], "conn": [], "tags":[], "sets":{}}

  for line in lines[locs["elements"][0]+2:locs["elements"][1]]:
    d = np.array([int(w) for w in line.split()])
    elements["labels"].append( d[0] )
    elements["etype"].append(elementMap[d[1]] )
    elements["tags"].append( d[3: 3+d[2]] )
    elements["conn"].append(d[3+d[2]:])
  elements["labels"] = np.array(elements["labels"])
  physicalNames = {}
  for line in lines[locs["physicalnames"][0]+2:locs["physicalnames"][1]]:
    w = line.split()
    physicalNames[int(w[1])] = w[2].replace('"', '')
  sets = {}
  tags = np.array([t[0] for t in elements["tags"]])
  for k in physicalNames.keys():
    sets[physicalNames[k]] = np.array([t == k for t in tags])
  """
  for tag, values in sets.items():
    elements["sets"][tag] = elements["labels"][values]
  """
  elements["sets"] = sets
  return Mesh(nlabels = nodes["labels"],
              coords = np.array(nodes[["x", "y", "z"]]),
              elabels = elements["labels"],
              conn = elements["conn"],
              types = elements["etype"],
              esets = elements["sets"])

def read_inp(path):
  """
  Reads Abaqus inp file
  """

  def lineInfo(line):
    out =  {"type": "data"}
    if line[0] == "*":
      if line[1] == "*":
        out["type"] = "comment"
        out["text"] = line[2:]
      else:
        out["type"] = "command"
        words = line[1:].split(",")
        out["value"] = words[0].strip()
        out["options"] = {}
        for word in words[1:]:
          key, value =  [s.strip() for s in word.split("=")]
          out["options"][key] = value
    return out

  def elementMapper(inpeltype):
    if inpeltype == "t3d2": return "Line2"
    if inpeltype[:3] in ["cps", "cpe", "cax"]:
      if inpeltype[3] == "3": return "tri3"
      if inpeltype[3] == "4": return "quad4"
    if inpeltype[:3] in ["c3d"]:
      if inpeltype[3] == "4": return "tetra4"
      if inpeltype[3] == "5": return "pyra5"
      if inpeltype[3] == "6": return "prism6"
      if inpeltype[3] == "8": return "hexa8"

  nlabels      = []
  coords       = []
  nsets        = {}
  elabels      = []
  etypes       = []
  connectivity = []
  esets        = {}
  surfaces     = {}

  # File preprocessing
  lines = np.array([l.strip().lower() for l in open(path).readlines()])
  lines = [line for line in  lines if len(line) != 0]
  # Data processing
  env, setlabel = None, None
  for line in lines:
    d = lineInfo(line)
    if d["type"] == "command":
      env = d["value"]
      # Nodes
      if env == "node":
        opt = d["options"]
        currentset = None
        if "nset" in opt.keys():
          currentset = opt["nset"]
          nsets[currentset] = []

      # Elements
      if env == "element":
        opt = d["options"]
        eltype = elementMapper(opt["type"])
        currentset = None
        if "elset" in opt.keys():
          currentset = opt["elset"]
          esets[currentset] = []

      # Nsets
      if env == "nset":
        opt = d["options"]
        currentset = opt["nset"]
        nsets[currentset] = []

      # Elsets
      if env == "elset":
        opt = d["options"]
        currentset = opt["elset"]
        esets[currentset] = []

      # Surfaces
      if env == "surface":
        opt = d["options"]
        currentsurface = opt["name"]
        if opt["type"] == "element":
          surfaces[currentsurface] = []

    if d["type"] == "data":
      words = line.strip().split(",")
      if env == "node":
        label = int(words[0])
        nlabels.append(label)
        coords.append(
            np.array([np.float64(w) for w in words[1:4]])
                     )
        if currentset != None: nsets[currentset].append(label)

      if env == "element":
        label  = int(words[0])
        elabels.append(label)
        connectivity.append(
            np.array( [np.int32(w) for w in words[1:] if len(w) != 0 ])
                           )
        etypes.append(eltype)
        if currentset != None: esets[currentset].append(label)

      if env == "nset":
        nsets[currentset] += [int(w) for w in words if len(w) != 0]

      if env == "elset":
        esets[currentset] += [int(w) for w in words if len(w) != 0]

      if env == "surface":
        if opt["type"] == "element":
          surfaces[currentsurface].append([w.strip() for w in words])

  surfaces2 = {}
  for tag, surface in surfaces.items():
    surfaces2[tag] = []
    for sdata in surface:
      labels = esets[sdata[0]]
      face = int(sdata[1].split("s")[1].strip())-1
      for label in labels:
        surfaces2[tag].append((label, face))

  return Mesh(nlabels = nlabels,
              coords  = coords,
              nsets   = nsets,
              elabels = elabels,
              etypes  = etypes,
              connectivity = connectivity,
              esets = esets,)
              #surfaces = surfaces2)

################################################################################
# WRITERS
################################################################################

def write_xdmf(mesh, path, dataformat = "XML"):
  """
  Dumps the mesh to XDMF format.
  """
  pattern = Template(open(MODPATH + "/templates/mesh/xdmf.xdmf").read())
  attribute_pattern = Template(open(MODPATH + "/templates/mesh/xdmf_attribute.xdmf").read())
  # MAPPINGS
  cell_map = {
      "tri3":   4,
      "quad4":  5,
      "tetra4": 6,
      "pyra5":  7,
      "prism6": 8,
      "hexa8":  9}
  # REFERENCES
  nodes, elements = mesh.nodes.data, mesh.elements.data
  fields = mesh.fields
  # NUMBERS
  Ne, Nn = len(elements), len(nodes)
  # NODES
  nodes_map = np.arange(nodes.index.max()+1)
  nodes_map[nodes.index] = np.arange(len(nodes.index))
  nodes_map[0] = -1
  # ELEMENTS
  cols = ["n{0}".format(i) for i in range(elements.shape[1]-1)]
  connectivities  = mesh.elements.data[cols].as_matrix()
  connectivities[np.isnan(connectivities)] = 0
  connectivities = connectivities.astype(np.int32)
  connectivities = nodes_map[connectivities]
  labels          = np.array(elements.index)
  etypes          = np.array([cell_map[t] for t in elements.etype])
  lconn           = Ne + (connectivities != -1).sum()
  # FIELDS
  fields_string = ""
  field_data = {}
  for tag, field in fields.items():
      field_data[tag] = {}
      field.data.sort_index(inplace = True)
      fshape = field.data.shape[1]
      if   fshape  == 1: ftype = "Scalar"
      elif fshape  == 3: ftype = "Vector"
      elif fshape  == 2:
        ftype = "Vector"
        # UGLY HACK...
        field = copy.copy(field)
        field.data["v3"] = np.zeros_like(field.data.index)
        fields[tag] = field
        # BACK TO NORMAL
      elif fshape  == 6: ftype = "Tensor6"
      elif fshape  == 4:
        ftype = "Tensor6"
        # UGLY HACK...
        field = copy.copy(field)
        field.data["v13"] = np.zeros_like(field.data.index)
        field.data["v23"] = np.zeros_like(field.data.index)
        fields[tag] = field
        # BACK TO NORMAL
      if field.metadata.position == "Nodal":
        position = "Node"
      if field.metadata.position == "Element":
        position = "Cell"
      field_data[tag]["TAG"]           = tag
      field_data[tag]["ATTRIBUTETYPE"] = ftype
      field_data[tag]["FORMAT"]        = dataformat
      field_data[tag]["FIELD_DIMENSION"] = " ".join([str(l) for l in field.data.shape])
      field_data[tag]["POSITION"]      = position
  if dataformat == "XML":
    #NODES
    nodes_string = "\n".join([11*" " + "{0} {1} {2}".format(
                              n.x,
                              n.y,
                              n.z)
                        for i, n in nodes.iterrows()])
    # ELEMENTS
    elements_string = ""
    for i in range(Ne):
      elements_string += 11*" " + str(etypes[i]) + " "
      c = connectivities[i]
      c = c[np.where(c != -1)]
      elements_string += " ".join([str(i) for i in c]) + "\n"
    elements_strings = elements_string[:-1]
    # FIELDS
    for tag, field in fields.items():
      fdata = field.data.to_csv(sep = " ",
                                index = False,
                                header = False).split("\n")
      fdata = [11 * " " + l for l in fdata]
      fdata = "\n".join(fdata)
      field_data[tag]["DATA"] = fdata
      fields_string += attribute_pattern.substitute(**field_data[tag])
  elif dataformat == "HDF":
    hdf = pd.HDFStore(path + ".h5")
    hdf.put("COORDS", mesh.nodes.data[list("xyz")])
    flatconn = np.zeros(lconn, dtype = np.int32)
    pos = 0
    for i in range(Ne):
      c = connectivities[i]
      c = c[np.where(c != -1)]
      lc = len(c)
      flatconn[pos] = etypes[i]
      flatconn[pos + 1 + np.arange(lc)] = c
      pos += 1 + lc
    hdf.put("CONNECTIVITY", pd.DataFrame(flatconn))
    nodes_string = 11*" " + "{0}.h5:/COORDS/block0_values".format(path)
    elements_string = 11*" " + "{0}.h5:/CONNECTIVITY/block0_values".format(path)
    for tag, field in fields.items():
      fstrings[tag] = fstrings[tag].replace("#DATA",
         11*" " + "{0}.h5:/FIELDS/{1}/block0_values".format(path, tag))
      fields_string += fstrings[tag]
      hdf.put("FIELDS/{0}".format(tag), fields.data)
    hdf.close()
  """
  pattern = pattern.replace("#ELEMENT_NUMBER", str(Ne))
  pattern = pattern.replace("#CONN_DIMENSION", str(lconn))
  pattern = pattern.replace("#CONN_PATH", elements_string)
  pattern = pattern.replace("#NODE_NUMBER", str(Nn))
  pattern = pattern.replace("#NODE_PATH", nodes_string)
  pattern = pattern.replace("#DATAFORMAT", dataformat)
  pattern = pattern.replace("#ATTRIBUTES", fields_string)
  """
  fields_string = "\n".join([attribute_pattern.substitute(**value) for key, value in field_data.items()])
  pattern = pattern.substitute(
     ELEMENT_NUMBER = str(Ne),
     CONN_DIMENSION = str(lconn),
     CONN_PATH      = elements_string,
     NODE_NUMBER    = str(Nn),
     NODE_PATH      = nodes_string,
     DATAFORMAT     = dataformat,
     ATTRIBUTES     = fields_string)
  open(path + ".xdmf", "wb").write(pattern)

def write_inp(mesh, path = None, maxwidth = 40, sections = "solid"):
  """
  Exports the mesh to the INP format.
  """
  def set_to_inp(sets, keyword):
    ss = ""
    for sk in sets.keys():
      labels = sets[sk].loc[sets[sk]].index.values
      labels = list(labels)
      labels.sort()
      if len(labels)!= 0:
        ss += "*{0}, {0}={1}\n".format(keyword, sk)
        ss += argiope.utils.list_to_string(labels) + "\n"
    return ss.strip()

  # DATA
  mesh = mesh.copy()

  # NODES
  nodes_output = (mesh.nodes.coords.to_csv(header = False).split())
  nodes_output = ("\n".join(["  " + s.replace(",", ", ") for s in nodes_output]))

  # NODE SETS
  if "sets" in mesh.nodes.columns.levels[0]:
    nsets = set_to_inp(mesh.nodes.sets, "NSET")
  else:
    nsets = "**"

  # SURFACES
  surf_output = []
  if "surfaces" in mesh.elements.keys():
    sk = mesh.elements.surfaces.keys()
    for sindex in  np.unique(sk.labels[0]):
      slabel = sk.levels[0][sindex]
      surface = mesh.elements.surfaces[slabel]
      if surface.values.sum() != 0:
        mesh.surface_to_element_sets(slabel)
        surf_output.append( "*SURFACE, TYPE=ELEMENT, NAME={0}".format(slabel))
        for findex in surface.keys():
          if surface[findex].sum() != 0:
            surf_output.append("  _SURF_{0}_FACE{1}, S{1}".format(slabel,
                                                                  findex[1:]))
  else:
    surf_output.append("**")

  # ELEMENTS
  elements_output = ""
  for etype, group in mesh.elements.groupby((("type", "solver", ""),)):
    els = group.conn.replace(0, np.nan).to_csv(header = False,
                                               float_format='%.0f').split()
    elements_output += "*ELEMENT, TYPE={0}\n".format(etype)
    elements_output += ("\n".join(["  " + s.strip().strip(",").
                             replace(",", ", ") for s in els]))
    elements_output += "\n"
  elements_output = elements_output.strip()
  el_sets = {}

  # MATERIALS
  section_output = ""
  for material, group in mesh.elements.groupby("materials"):
    slabel = "_MAT_{0}".format(material)
    section_output += "*ELSET, ELSET=_MAT_{0}\n{1}\n".format(
                 material,
                 argiope.utils.list_to_string(group.index.values))

    #mesh.elements[("sets", slabel, "")] = False
    #mesh.elements.loc[group.index, ("sets", slabel, "")] = True
    if sections == "solid":
      section_output += "*SOLID SECTION, ELSET=_MAT_{0}, MATERIAL={0}\n".format(
       material)

  # ELEMENTS SETS
  if "sets" in mesh.elements.columns.levels[0]:
    esets = set_to_inp(mesh.elements.sets.swaplevel(1,0, axis = 1)[""],"ELSET")
  else:
    esets = "**"
  """
  ek = mesh.elements.sets.keys()
  for esindex in  np.unique(ek.labels[0]):
    eslabel = ek.levels[0][esindex]
    eset = mesh.elements.sets[slabel]
  """

  # PATTERN
  pattern = Template(open(MODPATH + "/templates/mesh/inp.inp").read())
  pattern = pattern.substitute(
    NODES     = nodes_output,
    NODE_SETS = nsets,
    ELEMENTS  = elements_output,
    ELEMENT_SETS = esets,
    ELEMENT_SURFACES = "\n".join(surf_output),
    SECTIONS = section_output.strip())
  pattern = pattern.strip()
  if path == None:
    return pattern
  else:
    open(path, "w").write(pattern)

################################################################################
# MESHERS
################################################################################

@numba.jit
def _make_conn(shape):
    """
    Connectivity builder using Numba for speed boost.
    """
    shape = np.array(shape)
    Ne = shape.prod()
    if len(shape) == 2:
        nx, ny = np.array(shape) +1
        conn = np.zeros((Ne, 4), dtype = np.int32)
        counter = 0
        pattern = np.array([0,1,1+nx,nx])
        for j in range(shape[1]):
            for i in range(shape[0]):
                conn[counter] = pattern + 1 + i + j*nx
                counter += 1

    if len(shape) == 3:
        nx, ny, nz  = np.array(shape) +1
        conn = np.zeros((Ne, 8), dtype = np.int32)
        counter = 0
        pattern = np.array([0,1,1+nx,nx,nx*ny,1+nx*ny,1+(nx+1)*ny,(nx+1)*ny])
        for k in range(shape[2]):
            for j in range(shape[1]):
                for i in range(shape[0]):
                    conn[counter] = pattern + 1 + i + j*nx+ k*nx*ny
                    counter += 1
    return conn

def structured_mesh(shape = (2,2,2), dim = (1.,1.,1.)):
    """
    Returns a structured mesh.

    :arg shape: 2 or 3 integers (eg: shape = (10, 10, 10)).
    :type shape: tuple
    :arg dim: 2 or 3 floats (eg: dim = (4., 2., 1.))
    :type dim: tuple

    .. note::

       This function does not use GMSH for mesh generation.

    >>> import argiope as ag
    >>> mesh = ag.mesh.structured_mesh(shape =(10,10,10), dim=(1.,1.,1.)))
    """
    # PREPROCESSING
    shape = np.array(shape)
    dim   = np.array(dim)
    Ne = shape.prod()
    Nn = (shape + 1).prod()
    # LABELS
    nindex = np.arange(Nn) + 1
    eindex = np.arange(Ne) + 1
    # COORDINATES
    coords = [ np.linspace(0., dim[i], shape[i] + 1) for i in range(len(shape))]
    coords = np.array(np.meshgrid(*coords))
    coords = np.array([c.swapaxes(0,1).flatten("F") for c in coords]).T
    if len(shape) == 2:
        c = coords
        coords = np.zeros((Nn, 3))
        coords[:, :2] = c
    # CONNECTIVITY
    conn = _make_conn(shape)
    # MESH INSTANCE
    mesh = Mesh(nlabels = nindex,
                coords  = coords,
                elabels = eindex,
                conn = conn,)
    if len(shape) == 2: mesh.elements[("type", "argiope")] = "quad4"
    if len(shape) == 3: mesh.elements[("type", "argiope")] = "hexa8"
    return mesh


import numpy as np
import pandas as pd
import os, inspect, argiope, subprocess, time
from string import Template

MODPATH = os.path.dirname(inspect.getfile(argiope))

################################################################################
# MODEL DEFINITION
################################################################################
class Model(argiope.utils.Container):
  """
  Model meta class.
  """
  def __init__(self,
               label,
               parts,
               steps,
               materials,
               amplitude = None,
               solver = "abaqus",
               solver_path = "",
               workdir = "./workdir",
               verbose = False):
    self.label       = label
    self.parts       = parts
    self.steps       = steps
    self.amplitude   = amplitude
    self.materials   = materials
    self.solver      = solver
    self.solver_path = solver_path
    self.workdir     = workdir
    self.verbose     = verbose
    self.data = {}

  def make_directories(self):
    """
    Checks if required directories exist and creates them if needed.
    """
    if os.path.isdir(self.workdir) == False: os.mkdir(self.workdir)

  def run_simulation(self):
    """
    Runs the simulation.
    """
    self.make_directories()
    t0 = time.time()
    if self.verbose:
      print('#### RUNNING "{0}" USING SOLVER "{1}"'.format(self.label,
                                               self.solver.upper() ))
    if self.solver == "abaqus":
      command = '{0} job={1} input={1}.inp interactive ask_delete=OFF'.format(
                self.solver_path,
                self.label)

      process = subprocess.Popen(command,
                                 cwd    = self.workdir,
                                 shell  = True,
                                 stdout = subprocess.PIPE,
                                 stderr = subprocess.STDOUT)
      for line in iter(process.stdout.readline, b''):
         line = line.rstrip().decode('utf8')
         print("    ", line)

    t1 = time.time()
    if self.verbose:
      print('  => RAN {0}: DURATION = {1:.2f}s'.format(self.label, t1 - t0))

  def run_postproc(self):
    """
    Runs the post-proc script.
    """
    t0 = time.time()
    if self.verbose:
      print('#### POST-PROCESSING "{0}" USING POST-PROCESSOR "{1}"'.format(self.label,
                                               self.solver.upper()))
    if self.solver == "abaqus":
      command = '{0} viewer noGUI={1}_abqpp.py'.format(self.solver_path, self.label)
      process = subprocess.Popen(
                command,
                cwd    = self.workdir,
                shell  = True,
                stdout = subprocess.PIPE,
                stderr = subprocess.STDOUT)

      for line in iter(process.stdout.readline, b''):
         line = line.rstrip().decode('utf8')
         print("    ", line)
    t1 = time.time()
    if self.verbose:
      print('  => POST-PROCESSED {0}: DURATION = {1:.2f}s >'.format(self.label,
                                                                  t1 - t0))

################################################################################

################################################################################
# PART DEFINITION
################################################################################
class Part(argiope.utils.Container):

  def __init__(self, gmsh_path = "gmsh",
                     file_name = "dummy",
                     workdir = "./",
                     gmsh_space = 2,
                     gmsh_options = "",
                     element_map = None,
                     material_map = None):
    self.gmsh_path  = gmsh_path
    self.file_name  = file_name
    self.workdir    = workdir
    self.gmsh_space = gmsh_space
    self.gmsh_options = gmsh_options
    self.mesh       = None
    self.element_map = element_map
    self.material_map = material_map

  def run_gmsh(self):
    """
    Makes the mesh using gmsh.
    """
    argiope.utils.run_gmsh(gmsh_path = self.gmsh_path,
                           gmsh_space = self.gmsh_space,
                           gmsh_options = self.gmsh_options,
                           name = self.file_name + ".geo",
                           workdir = self.workdir)
    self.mesh = argiope.mesh.read_msh(self.workdir + self.file_name + ".msh")

  def make_mesh(self):
    self.preprocess_mesh()
    self.run_gmsh()
    self.postprocess_mesh()

################################################################################
################################################################################


import pandas as pd
import io, argiope

def read_history_report(path, steps, x_name = None):
  """
  Reads an history output report.
  """
  data = pd.read_csv(path, delim_whitespace = True)
  if x_name != None:
    data[x_name] = data.X
    del data["X"]

  data["step"] = 0
  t = 0.
  for i in range(len(steps)):
    dt = steps[i].duration
    loc = data[data.t == t].index
    if len(loc) == 2:
      data.loc[loc[1]:, "step"] = i
    t += dt
  return data

def read_field_report(path, data_flag = "*DATA", meta_data_flag = "*METADATA"):
  """
  Reads a field output report.
  """
  text = open(path).read()
  mdpos = text.find(meta_data_flag)
  dpos = text.find(data_flag)
  mdata = io.StringIO( "\n".join(text[mdpos:dpos].split("\n")[1:]))
  data = io.StringIO( "\n".join(text[dpos:].split("\n")[1:]))
  data = pd.read_csv(data, index_col = 0)
  data = data.groupby(data.index).mean()
  mdata = pd.read_csv(mdata, sep = "=", header = None, index_col = 0)[1]
  mdata = mdata.to_dict()
  out = {}
  out["step_num"] = int(mdata["step_num"])
  out["step_label"] = mdata["step_label"]
  out["frame"] = int(mdata["frame"])
  out["frame_value"] = float(mdata["frame_value"])
  out["part"] = mdata["instance"]
  position_map = {"NODAL": "node",
                  "ELEMENT_CENTROID": "element",
                  "WHOLE_ELEMENT": "element"}
  out["position"] = position_map[mdata["position"]]
  out["label"] = mdata["label"]
  out["data"] = data
  field_class = getattr(argiope.mesh, mdata["argiope_class"])
  return field_class(**out)


import pickle, gzip, copy, subprocess, argiope, os, inspect

MODPATH = os.path.dirname(inspect.getfile(argiope))
################################################################################
# PICKLE/GZIP RELATED
################################################################################
def load(path):
  """
  Loads a file.
  """
  return pickle.load(gzip.open(path))

################################################################################
# META CLASSES
################################################################################
class Container:
  """
  A container meta class with utilities
  """

  def save(self, path):
    """
    Saves the instance into a compressed serialized file.
    """
    pickle.dump(self, gzip.open(path, "w"), 3)

  def copy(self):
    """
    Returns a copy of self.
    """
    return copy.deepcopy(self)

################################################################################
# SUBPROCESS RELATED
################################################################################
def run_gmsh(gmsh_path = "gmsh", gmsh_space = 3, gmsh_options = "",
             name = "dummy.geo", workdir = "./"):
  p = subprocess.Popen("{0} -{1} {2} {3}".format(gmsh_path, gmsh_space,
                                                 gmsh_options, name),
                       cwd = workdir,
                       shell=True,
                       stdout = subprocess.PIPE)
  trash = p.communicate()

################################################################################
# MISC
################################################################################

def list_to_string(l = range(200), width = 40, indent = "  "):
    """
    Converts a list-like to string with given line width.
    """
    l = [str(v) + "," for v in l]
    counter = 0
    out = "" + indent
    for w in l:
        s = len(w)
        if counter + s > width:
            out += "\n" + indent
            counter = 0
        out += w
        counter += s
    return out.strip(",")

def _equation(nodes = (1, 2), dofs = (1, 1), coefficients = (1., 1.),
              comment = None):
    """
    Returns an Abaqus INP formated string for a given linear equation.
    """
    N = len(nodes)
    if comment == None:
      out = ""
    else:
      out = "**EQUATION: {0}\n".format(comment)
    out+= "*EQUATION\n  {0}\n  ".format(N)
    out += "\n  ".join([ ",".join([ str(nodes[i]),
                                  str(int(dofs[i])),
                                  str(coefficients[i]) ]) for i in range(N)])
    return out

def _unsorted_set(df, label, **kwargs):
    """
    Returns a set as inp string with unsorted option.
    """
    out = "*NSET, NSET={0}, UNSORTED\n".format(label)
    labels = df.index.values
    return out + argiope.utils.list_to_string(labels, **kwargs)


import collections
import requests

from .utils import curry
from .exceptions import AuthenticationError, APIError, ServerError

try:
    basestring
except NameError:
    basestring = str

class PhaxioApi(object):
    VERSION = 1
    BASE_URL = 'https://api.phaxio.com'
    __IMPLEMENTED = ('send', 'testReceive', 'attachPhaxCodeToPdf',
        'createPhaxCode', 'getHostedDocument', 'provisionNumber',
        'releaseNumber', 'numberList', 'faxFile', 'faxList',
        'faxStatus', 'faxCancel', 'accountStatus'
    )

    api_key = None
    api_secret = None

    def __init__(self, key, secret, raise_errors=False):
        """Construct a Phaxio API object

        :param key: Phaxio api key
        :param secret: Phaxio api secret
        :param raise_errors: If set to False (default)
            an api call will return the json response
            as it did previously. If set to True, an api
            call will now raise an appropriate error.
        """
        self.api_key = key
        self.api_secret = secret
        self._raise_errors = raise_errors

    def parse_response(self, response):
        """Parses the API response and raises appropriate
        errors if raise_errors was set to True

        :param response: response from requests http call
        :returns: dictionary of response
        :rtype: dict
        """
        payload = None
        try:
            if isinstance(response.json, collections.Callable):
                payload = response.json()
            else:
                # json isn't callable in old versions of requests
                payload = response.json
        except ValueError:
            # response does not have JSON content
            payload = response.content

        if not self._raise_errors:
            return payload
        else:
            if response.status_code == 401:
                raise AuthenticationError(payload['message'])
            elif response.status_code == 500:
                raise ServerError(payload['message'])
            elif isinstance(payload, dict) and not payload['success']:
                raise APIError(payload['message'])
            else:
                return payload

    def _get(self, method, **kwargs):
        """Builds the url for the specified method and arguments and returns
        the response as a dictionary.
        """

        payload = kwargs.copy()
        payload['api_key'] = self.api_key
        payload['api_secret'] = self.api_secret

        to = payload.pop('to', None)
        if to:
            if isinstance(to, basestring):
                payload['to'] = to
            else:
                # Presumably it's a list or tuple
                for num_i, fax_num in enumerate(to):
                    payload['to[%d]' % num_i] = fax_num

        files = payload.pop('files', [])
        if not isinstance(files, (list, tuple)): files = (files,)

        req_files = {}
        for file_i, f in enumerate(files):
            if isinstance(f, basestring):
                req_files['filename[%d]' % file_i] = open(f, 'rb')
            else:
                f.seek(0)
                req_files['filename[%d]' % file_i] = f

        url = '%s/v%d/%s' % (self.BASE_URL, self.VERSION, method)

        r = requests.post(url, data=payload, files=req_files)

        return self.parse_response(r)

    def __getattribute__(self, name):
        if name in PhaxioApi.__IMPLEMENTED:
            return curry(self._get, name)
        return super(PhaxioApi, self).__getattribute__(name)


import numpy as np
import pandas as pd
import os, inspect, argiope
from string import Template

MODPATH = os.path.dirname(inspect.getfile(argiope))

class Material:
  """
  A material meta class to rule them all.
  """
  _template = "Material"

  def __init__(self, label = "Material", **kwargs):
    self.label = label

  def get_template(self):
    return Template(open(MODPATH + "/templates/materials/{0}.inp".format(
           self._template)).read())

  def write_inp(self):
     """
     Returns the material definition as a string in Abaqus INP format.
     """
     template = self.get_template()
     return template.substitute({"class": self.__class__.__name__,
                                 "label": self.label}).strip()

class Elastic(Material):
   """
   An isotropic elastic material class.
   """
   _template = "Elastic"

   def __init__(self, young_modulus = 1., poisson_ratio = 0.3, **kwargs):
     self.young_modulus = young_modulus
     self.poisson_ratio = poisson_ratio
     super().__init__(**kwargs)

   def write_inp(self):
     """
     Returns the material definition as a string in Abaqus INP format.
     """
     template = self.get_template()
     return template.substitute({"class": self.__class__.__name__,
                                 "label": self.label,
                                 "young_modulus": self.young_modulus,
                                 "poisson_ratio": self.poisson_ratio}).strip()

class _ElasticPlastic(Elastic):
  """
  An elastic plastic material **meta** class.
  """
  _template = "ElasticPlastic"

  def write_inp(self):
    """
    Returns the material definition as a string in Abaqus INP format.
    """
    template = self.get_template()
    plastic_table = self.get_plastic_table()
    return template.substitute({
        "class": self.__class__.__name__,
        "label": self.label,
        "young_modulus": self.young_modulus,
        "poisson_ratio": self.poisson_ratio,
        "plastic_table": (self.get_plastic_table()[["stress", "plastic_strain"]]
                          .to_csv(header = False,
                                  index = False,
                                  sep = ",").strip())}).strip()

class ElasticPerfectlyPlastic(Elastic):
  """
  A elastic perfectly plastic material.
  """
  _template = "ElasticPerfectlyPlastic"

  def __init__(self,  yield_stress = 0.01,
                      **kwargs):
     self.yield_stress = yield_stress
     super().__init__(**kwargs)

  def write_inp(self):
     """
     Returns the material definition as a string in Abaqus INP format.
     """
     template = self.get_template()
     return template.substitute({"class": self.__class__.__name__,
                                 "label": self.label,
                                 "young_modulus": self.young_modulus,
                                 "poisson_ratio": self.poisson_ratio,
                                 "yield_stress":self.yield_stress}).strip()

class ElasticPlasticRateDep(Elastic):
  """
  An elastic plastic rate dependent material.
  """
  _template = "ElasticPlasticRateDep"

  def __init__(self, yield_stress = 100,
                    multiplier = 1.0e-6,
                    exponent = 1.4,
                    **kwargs):
     self.yield_stress = yield_stress
     self.multiplier = multiplier
     self.exponent = exponent
     super().__init__(**kwargs)

  def write_inp(self):
     """
     Returns the material definition as a string in Abaqus INP format.
     """
     template = self.get_template()
     return template.substitute({"class":self.__class__.__name__,
                                 "label":self.label,
                                 "young_modulus":self.young_modulus,
                                 "poisson_ratio":self.poisson_ratio,
                                 "yield_stress":self.yield_stress,
                                 "multiplier":self.multiplier,
                                 "exponent":self.exponent}).strip()

class _PowerLawHardening(_ElasticPlastic):
  """
  A power law hardening meta class.
  """
  def __init__(self,  max_strain = 10,
                      strain_data_points = 100,
                      **kwargs):
     self.max_strain = max_strain
     self.strain_data_points = strain_data_points
     super().__init__(**kwargs)

class Hollomon(_PowerLawHardening):
   """
   An Hollomon material.
   """
   def __init__(self, hardening_exponent = 0.3,
                      yield_stress = 0.01,
                      **kwargs):
     self.hardening_exponent = hardening_exponent
     self.yield_stress = yield_stress
     super().__init__(**kwargs)

   def get_plastic_table(self):
     """
     Calculates the plastic data
     """
     E = self.young_modulus
     sy = self.yield_stress
     n = self.hardening_exponent
     eps_max = self.max_strain
     Np = self.strain_data_points
     ey = sy/E
     s = 10.**np.linspace(0., np.log10(eps_max/ey), Np)
     strain = ey * s
     stress = sy * s**n
     plastic_strain = strain - stress / E
     return pd.DataFrame({"strain": strain,
                          "stress": stress,
                          "plastic_strain": plastic_strain})

class PowerLin(_PowerLawHardening):
   """
   A Power Linear material :
   S = Sy  + K (Ep)**n
   """
   def __init__(self, hardening_exponent = 0.3,
                      yield_stress = 150.,
                      consistency = 200.,
                      **kwargs):
     self.hardening_exponent = hardening_exponent
     self.yield_stress = yield_stress
     self.consistency = consistency
     super().__init__(**kwargs)

   def get_plastic_table(self):
     """
     Calculates the plastic data
     """
     K = self.consistency
     sy = self.yield_stress
     n = self.hardening_exponent
     eps_max = self.max_strain
     Np = self.strain_data_points
     plastic_strain = np.linspace(0., eps_max, Np)
     stress = sy + K * plastic_strain**n
     return pd.DataFrame({"stress": stress,
                          "plastic_strain": plastic_strain})


'''
melting.py

Calculates the melting temperatures of a given nucleotide sequence.

The melting temperature equations, including correction for salt concentrations,
derives from information found on this page:
  https://www.idtdna.com/Calc/Analyzer/Home/Definitions#MeltTemp

The values for the delta H (dH) and delta S (dS) of each nucleotide pair, and
the initial adjustments to dH and dS are derived from:
  Allawi and SantaLucia (1997), Biochemistry 36 : 10581-10594.

Based in large part on code by the following authors:
- Sebastian Bassi <sbassi@genesdigitales.com>
- Greg Singer <singerg@tcd.ie>
- Nicolas Le Novere <lenov@ebi.ac.uk>
- Calvin Morrison <mutantturkey@gmail.com>
'''
from __future__ import division
from math import sqrt, log
import argparse
import sys
from pkg_resources import get_distribution

def _is_sym(seq):
    """Returns True if s is symmetric (same as rev. complement)"""
    comp = {
        'A': 'T',
        'T': 'A',
        'G': 'C',
        'C': 'G'
    }
    return seq == ''.join([comp[i] for i in seq][::-1])

def _overcount(st, p):
    ocu = 0
    x = 0
    while True:
        try:
            i = st.index(p, x)
        except ValueError:
            break
        ocu += 1
        x = i + 1
    return ocu

def _tercorr(st):
    _dh = 0
    _ds = -1.4 if _is_sym(st) else 0
    start = st[0]
    end = st[-1]

    if start == 'G' or start == 'C':
        _dh += 0.1
        _ds -= 2.8
    elif start == 'A' or start == 'T':
        _dh += 2.3
        _ds += 4.1

    if end == 'G' or end == 'C':
        _dh += 0.1
        _ds -= 2.8
    elif end == 'A' or end == 'T':
        _dh += 2.3
        _ds += 4.1
    return _dh, _ds

def temp(s, DNA_c=5000.0, Na_c=10.0, Mg_c=20.0, dNTPs_c=10.0, uncorrected=False):
    '''
    Returns the DNA/DNA melting temp using nearest-neighbor thermodynamics.

    This function returns better results than EMBOSS DAN because it uses updated
    thermodynamics values and takes into account initialization parameters from
    the work of SantaLucia (1998).

    Corrects for mono- and divalent cation concentrations.

    Arguments:
    - DNA_c:   DNA concentration [nM]
    - Na_c:    Na+ concentration [mM]
    - Mg_c:    Mg2+ concentration [mM]
    - dNTPs_c: dNTP concentration [mM]
    - correction: correct for cation concentration?
    '''

    R = 1.987    # Universal gas constant (cal/(K*mol))
    s = s.upper()
    dh, ds = _tercorr(s)
    k = DNA_c * 1e-9

    # Adapted from Table 1 in Allawi and SantaLucia (1997).
    # delta H (kcal/mol)
    dh_coeffs = {"AA": -7.9, "TT": -7.9,
                 "AT": -7.2,
                 "TA": -7.2,
                 "CA": -8.5, "TG": -8.5,
                 "GT": -8.4, "AC": -8.4,
                 "CT": -7.8, "AG": -7.8,
                 "GA": -8.2, "TC": -8.2,
                 "CG": -10.6,
                 "GC": -9.8,
                 "GG": -8.0, "CC": -8.0}

    # delta S (eu)
    ds_coeffs = {"AA": -22.2, "TT": -22.2,
                 "AT": -20.4,
                 "TA": -21.3,
                 "CA": -22.7, "TG": -22.7,
                 "GT": -22.4, "AC": -22.4,
                 "CT": -21.0, "AG": -21.0,
                 "GA": -22.2, "TC": -22.2,
                 "CG": -27.2,
                 "GC": -24.4,
                 "GG": -19.9, "CC": -19.9}

    # Multiplies the number of times each nuc pair is in the sequence by the
    # appropriate coefficient, then returns the sum of all the pairs
    dh = dh + \
        sum(_overcount(s, pair) * coeff for pair, coeff in dh_coeffs.items())
    ds = ds + \
        sum(_overcount(s, pair) * coeff for pair, coeff in ds_coeffs.items())

    fgc = len([filter(lambda x: x == 'G' or x == 'C', s)]) / float(len(s))

    # Melting temperature
    tm = (1000 * dh) / (ds + (R * log(k)))

    if uncorrected:
        return tm - 273.15

    MNa = Na_c * 1e-3
    MMg = Mg_c * 1e-3
    MdNTPs = dNTPs_c * 1e-3

    # Free magnesium concentration
    Ka = 3e4  # association constant in biological buffers
    D = (Ka * MdNTPs - Ka * MMg + 1)**2 + (4 * Ka * MMg)
    Fmg = (-(Ka * MdNTPs - Ka * MMg + 1) + sqrt(D)) / (2 * Ka)

    cation_ratio = sqrt(Fmg) / MNa if MNa > 0 else 7.0

    if cation_ratio < 0.22:
        tm = 1 / (
            (1 / tm) +
            ((4.29 * fgc - 3.95) * log(MNa) + 0.94 * log(MNa)**2) * 1e-5)
    else:
        a = 3.92
        d = 1.42
        g = 8.31
        Fmg = MMg
        if cation_ratio < 6.0:
            a = a * (0.843 - 0.352 * sqrt(MNa) * log(MNa))
            d = d * \
                (1.279 - 4.03 * log(MNa) * 1e-3 - 8.03 * log(MNa)**2 * 1e-3)
            g = g * (0.486 - 0.258 * log(MNa) + 5.25 * log(MNa)**3 * 1e-3)
        tm = 1 / (
            (1 / tm) +
            (a - 0.911 * log(Fmg) + fgc * (6.26 + d * log(Fmg)) +
             1 / (2 * (len(s) - 1)) * (-48.2 + 52.5 * log(Fmg) +
                                       g * log(Fmg)**2)) * 1e-5)

    return tm - 273.15

def nucleotide_sequence(seq):
    non_nucl = set(seq.upper()) - set("ATGC")
    if non_nucl:
        raise ValueError("Non-nucleotide sequence provided")
    return seq

def main(argv=sys.argv[1:]):
    parser = argparse.ArgumentParser('Tm', description="Calculate nucleotide melting temps")

    parser.add_argument(
        "seq",
        help="nucleotide sequence",
        type=nucleotide_sequence
    )

    parser.add_argument(
        "--uncorrected",
        action='store_true',
        default=False,
        help="Do not use monovalent/divalent cation corrections"
    )

    parser.add_argument(
        "-d", "--dna",
        type=float,
        default=5000.0,
        help="DNA concentration (nM)"
    )

    parser.add_argument(
        "--na",
        type=float,
        default=10.0,
        help="Na+ concentration (mM)"
    )

    parser.add_argument(
        "--mg",
        type=float,
        default=20.0,
        help="Mg++ concentration (mM)"
    )

    parser.add_argument(
        "--dntp",
        type=float,
        default=10.0,
        help="Nucleotide triphosphate concentration (mM)"
    )

    parser.add_argument(
        "--version",
        action="version",
        version=get_distribution('melt').version
    )

    args = parser.parse_args(argv)

    Tm = temp(
        args.seq,
        DNA_c=args.dna,
        Na_c=args.na,
        Mg_c=args.mg,
        dNTPs_c=args.dntp,
        uncorrected=args.uncorrected
    )

    print('{0:03.1f}'.format(Tm))


from abaqus import *
from abaqusConstants import *
import displayGroupOdbToolset as dgo

def get_steps(odb):
  """
  Retrieves the steps keys in an odb object
  """
  return odb.steps.keys()

def get_frames(odb, stepKey):
  """
  Retrieves the number of frames in a step.
  """
  return len(odb.steps[stepKey].frames)

def write_xy_report(odb, path, tags, columns, steps):
  """
  Writes a xy_report based on xy data.
  """
  xyData = [session.XYDataFromHistory(name = columns[i],
                    odb = odb,
                    outputVariableName = tags[i],
                    steps = steps)
            for i in xrange(len(tags))]
  session.xyReportOptions.setValues(numDigits=8, numberFormat=SCIENTIFIC)
  session.writeXYReport(fileName=path, appendMode=OFF, xyData=xyData)

def write_field_report(odb, path, label, argiope_class, variable, instance, output_position,
                       step = -1, frame = -1, sortItem='Node Label'):
  """
  Writes a field report and rewrites it in a cleaner format.
  """
  stepKeys = get_steps(odb)
  step = xrange(len(stepKeys))[step]
  frame = xrange(get_frames(odb, stepKeys[step]))[frame]
  nf = NumberFormat(numDigits=9,
                    precision=0,
                    format=SCIENTIFIC)
  session.fieldReportOptions.setValues(
          printTotal=OFF,
          printMinMax=OFF,
          numberFormat=nf)
  leaf = dgo.LeafFromPartInstance(
          partInstanceName = instance)
  session.viewports['Viewport: 1'].odbDisplay.displayGroup.replace(leaf=leaf)
  session.writeFieldReport(
          fileName       = path,
          append         = OFF,
          sortItem       = sortItem,
          odb            = odb,
          step           = step,
          frame          = frame,
          outputPosition = output_position,
          variable       = variable)
  lines = [line.strip() for line in open(path).readlines()]
  isdata = -1
  data = []
  for line in lines:
   if isdata == 1:
     if len(line) == 0:
       isdata -= 1
     else:
       data.append(line)
   elif isdata < 1:
     if line.startswith("--"):
       isdata += 1
  data = "\n".join([",".join(line.split()) for line in data if len(line) != 0])
  # HEADER
  header = str(output_position).lower() + ","
  header += ",".join([v[1] for v in variable[0][2]]) + "\n"
  # METADATA
  metadata = (
          ("label", label),
          ("argiope_class", argiope_class) ,
          ("odb", odb.path),
          ("instance", instance),
          ("position", output_position),
          ("step_num", step),
          ("step_label", stepKeys[step]),
          ("frame", frame),
          ("frame_value", odb.steps[stepKeys[step]].frames[frame].frameValue)
              )
  out = "*METADATA\n{0}\n*DATA\n{1}".format(
        "\n".join(["{0}={1}".format(k, v) for k, v in metadata]),
        header + data)
  open(path, "w").write(out)


import click
import json
import logging
import os
import re
from yaml import YAMLError

from doodledashboard import __about__
from doodledashboard.component import ExternalPackageSource, StaticComponentSource, ComponentConfigLoader, ComponentType
from doodledashboard.configuration import DashboardConfigReader, InvalidConfigurationException
from doodledashboard.dashboard import DashboardRunner, DashboardValidator, ValidationException
from doodledashboard.datafeeds.datafeed import MessageJsonEncoder
from doodledashboard.error_messages import get_error_message
from doodledashboard.notifications.notification import FilteredNotification
from doodledashboard.secrets_store import InvalidSecretsException, try_read_secrets_file, SecretNotFound

def attach_logging(ctx, param, value):
    if value:
        _logger = logging.getLogger("doodledashboard")
        _logger.setLevel(logging.DEBUG)

        ch = logging.StreamHandler()
        ch.setLevel(logging.DEBUG)
        _logger.addHandler(ch)

@click.help_option("-h", "--help")
@click.version_option(__about__.__version__, "-v", "--version", message="%(prog)s v%(version)s")
@click.group()
def cli():
    pass

def is_remote_file(file_path):
    regex = re.compile("^(http|https)://", re.IGNORECASE)
    return True if regex.search(file_path) else False

def read_remote_file(file_path):
    import urllib.request
    with urllib.request.urlopen(file_path) as response:
        return response.read()

def read_file(config_file):
    if is_remote_file(config_file):
        return read_remote_file(config_file)
    else:
        with open(config_file, 'r') as f:
            return f.read()

@cli.command()
@click.argument("dashboards", type=click.Path(), nargs=-1)
@click.option('--once', is_flag=True, help='Loop through notifications once, otherwise will loop indefinitely')
@click.option("--secrets", type=click.Path(exists=True))
@click.option("--verbose", is_flag=True, callback=attach_logging, expose_value=False)
def start(dashboards, once, secrets):
    """Display a dashboard from the dashboard file(s) provided in the DASHBOARDS
       Paths and/or URLs for dashboards (URLs must secrets with http or https)
    """

    if secrets is None:
        secrets = os.path.join(os.path.expanduser("~"), "/.doodledashboard/secrets")

    try:
        loaded_secrets = try_read_secrets_file(secrets)
    except InvalidSecretsException as err:
        click.echo(get_error_message(err, default="Secrets file is invalid"), err=True)
        raise click.Abort()

    read_configs = ["""
    dashboard:
      display:
        type: console
    """]
    for dashboard_file in dashboards:
        read_configs.append(read_file(dashboard_file))

    dashboard_config = DashboardConfigReader(initialise_component_loader(), loaded_secrets)

    try:
        dashboard = read_dashboard_from_config(dashboard_config, read_configs)
    except YAMLError as err:
        click.echo(get_error_message(err, default="Dashboard configuration is invalid"), err=True)
        raise click.Abort()

    try:
        DashboardValidator().validate(dashboard)
    except ValidationException as err:
        click.echo(get_error_message(err, default="Dashboard configuration is invalid"), err=True)
        raise click.Abort()

    explain_dashboard(dashboard)

    click.echo("Dashboard running...")

    while True:
        try:
            DashboardRunner(dashboard).cycle()
        except SecretNotFound as err:
            click.echo(get_error_message(err, default="Datafeed didn't have required secret"), err=True)
            raise click.Abort()

        if once:
            break

@cli.command()
@click.argument("action", type=click.Choice(["datafeeds", "notifications"]))
@click.argument("dashboards", type=click.Path(), nargs=-1)
@click.option("--secrets", type=click.Path(exists=True))
@click.option("--verbose", is_flag=True, callback=attach_logging, expose_value=False)
def view(action, dashboards, secrets):
    """View the output of the datafeeds and/or notifications used in your DASHBOARDS"""

    if secrets is None:
        secrets = os.path.join(os.path.expanduser("~"), "/.doodledashboard/secrets")

    try:
        loaded_secrets = try_read_secrets_file(secrets)
    except InvalidSecretsException as err:
        click.echo(get_error_message(err, default="Secrets file is invalid"), err=True)
        raise click.Abort()

    dashboard_config = DashboardConfigReader(initialise_component_loader(), loaded_secrets)

    read_configs = [read_file(f) for f in dashboards]
    dashboard = read_dashboard_from_config(dashboard_config, read_configs)

    try:
        messages = DashboardRunner(dashboard).poll_datafeeds()
    except SecretNotFound as err:
        click.echo(get_error_message(err, default="Datafeed didn't have required secret"), err=True)
        raise click.Abort()

    cli_output = {"source-data": messages}

    if action == "notifications":
        cli_output["notifications"] = []

        for notification in dashboard.notifications:
            notification_output = notification.create(messages)

            filtered_messages = messages

            if isinstance(notification, FilteredNotification):
                filtered_messages = notification.filter_messages(messages)

            cli_output["notifications"].append({
                "filtered-messages": filtered_messages,
                "notification": str(notification_output)
            })
    json_output = json.dumps(cli_output, sort_keys=True, indent=4, cls=MessageJsonEncoder)
    click.echo(json_output)

@cli.command()
@click.argument("component_type",
                type=click.Choice(["displays", "datafeeds", "filters", "notifications", "all"]),
                default="all")
def list(component_type):
    """List components that are available on your machine"""
    config_loader = initialise_component_loader()
    component_types = sorted({
         "displays": lambda: config_loader.load_by_type(ComponentType.DISPLAY),
         "datafeeds": lambda: config_loader.load_by_type(ComponentType.DATA_FEED),
         "filters": lambda: config_loader.load_by_type(ComponentType.FILTER),
         "notifications": lambda: config_loader.load_by_type(ComponentType.NOTIFICATION)
    }.items(), key=lambda t: t[0])

    def print_ids(creators):
        ids = {c.id_key_value[1] if hasattr(c, "id_key_value") else c.get_id() for c in creators}
        for i in sorted(ids):
            click.echo(" - %s" % i)

    for k, v in component_types:
        if component_type == k or component_type == "all":
            click.echo("Available %s:" % k)
            print_ids(v())

        if component_type == "all":
            click.echo("")

def read_dashboard_from_config(dashboard_config, configs):
    try:
        return dashboard_config.read_yaml(configs)
    except InvalidConfigurationException as err:
        click.echo(get_error_message(err, default="Error parsing configuration file"), err=True)
        raise click.Abort()
    except Exception:
        raise

def initialise_component_loader():
    configs = ComponentConfigLoader()
    configs.add_source(ExternalPackageSource())
    configs.add_source(StaticComponentSource())
    return configs

def explain_dashboard(dashboard):
    display = dashboard.display
    click.echo("Display loaded: %s" % str(display))

    data_sources = dashboard.data_feeds
    click.echo("%s data sources loaded" % len(data_sources))
    for data_source in data_sources:
        click.echo(" - %s" % str(data_source))

    notifications = dashboard.notifications
    click.echo("%s notifications loaded" % len(notifications))
    for notification in notifications:
        click.echo(" - %s" % str(notification))

if __name__ == "__main__":
    cli()


import yaml

from functools import reduce

from doodledashboard.component import ComponentType
from doodledashboard.dashboard import Dashboard
from doodledashboard.notifications.notification import FilteredNotification

class DashboardMerger:

    def __init__(self, default=None):
        self._default_dashboard = default or Dashboard()

    def merge(self, dashboards):
        return reduce(DashboardMerger.dashboard_reduce, dashboards, self._default_dashboard)

    @staticmethod
    def dashboard_reduce(accum_value: Dashboard, x: Dashboard):
        if x.display:
            accum_value.display = x.display

        accum_value.add_data_feeds(x.data_feeds)
        accum_value.add_notifications(x.notifications)

        return accum_value

class ComponentConfigParser:
    """
    Parses a the most common configuration of:
        {
            'type': '<component ID>'
            'options:
                'key1': '<value>'
                'key2': '<value>'
        }
    """

    def __init__(self, section_component_configs):
        self._component_configs = section_component_configs

    def parse(self, config):
        """
            Parses the section of configuration pertaining to a component
            :param config: dict of specific config section
            :return:
        """

        if "type" not in config:
            raise InvalidConfigurationException("The dashboard configuration has not defined a 'type'. %s" % config)

        component_type = config["type"]
        component_config = self._get_config_by_id(self._component_configs, component_type)

        if not component_config:
            raise ComponentNotFoundForType(component_type)

        options = config.get("options", {})
        component = self._parse_item(component_config, options, config)
        component.name = config.get("name", "")
        return component

    def _parse_item(self, component_config, options, root_config):
        return component_config.create(options)

    @staticmethod
    def _get_config_by_id(component_configs, component_id):
        for config_parser in component_configs:
            if config_parser.get_id() == component_id:
                return config_parser
        return None

class NotificationComponentsConfigParser(ComponentConfigParser):

    def __init__(self, notification_configs, filter_parser):
        super().__init__(notification_configs)
        self._filter_parser = filter_parser

    def _parse_item(self, component_config, options, root_config):
        notification = component_config.create(options)

        if "filters" in root_config:
            filters = self._parse_filters(root_config["filters"])
            return FilteredNotification(notification, filters)
        else:
            return notification

    def _parse_filters(self, filters_config):
        return [self._filter_parser.parse(section) for section in filters_config]

class DashboardConfigReader:

    def __init__(self, component_configs_loader, secrets):
        self._dashboard_merger = DashboardMerger()
        self._component_configs_loader = component_configs_loader
        self._secret_store = secrets

        self._initialise_parsers()

    def _initialise_parsers(self):
        display_configs = self._component_configs_loader.load_by_type(ComponentType.DISPLAY)
        self._display_config_section_parser = ComponentConfigParser(display_configs)

        data_feed_configs = self._component_configs_loader.load_by_type(ComponentType.DATA_FEED)
        self._data_feed_config_section_parser = ComponentConfigParser(data_feed_configs)

        filter_configs = self._component_configs_loader.load_by_type(ComponentType.FILTER)
        filter_parser = ComponentConfigParser(filter_configs)

        notification_configs = \
            self._component_configs_loader.load_by_type(ComponentType.NOTIFICATION)
        self._notification_config_section_parser = NotificationComponentsConfigParser(
            notification_configs,
            filter_parser
        )

    def read_yaml(self, yaml_configs):
        dashboards = []

        for yaml_config in yaml_configs:
            try:
                config = yaml.safe_load(yaml_config)
            except yaml.YAMLError as err:
                raise ConfigYamlParsingError(err, yaml_config)

            dashboards.append(self._create_dashboard(config))

        return self._dashboard_merger.merge(dashboards)

    def _create_dashboard(self, config):
        config = config["dashboard"]
        if not config:
            config = {}

        display = None
        if "display" in config:
            try:
                display = self._display_config_section_parser.parse(config["display"])
            except ComponentNotFoundForType as ex:
                raise DisplayNotFound(ex.component_type)

        data_feeds = [
            self._data_feed_config_section_parser.parse(section) for section in config.get("data-feeds", [])
        ]
        for feed in data_feeds:
            feed.secret_store = self._secret_store

        notifications = [
            self._notification_config_section_parser.parse(section) for section in config.get("notifications", [])
        ]

        return Dashboard(display, data_feeds, notifications)

class InvalidConfigurationException(Exception):
    def __init__(self, value):
        self.value = value

    def __str__(self):
        return repr(self.value)

class ComponentNotFoundForType(InvalidConfigurationException):
    def __init__(self, component_type):
        super().__init__("Component not found for type '%s'" % component_type)
        self.component_type = component_type

class EmptyConfiguration(InvalidConfigurationException):
    def __init__(self, configuration_files):
        super().__init__("Configuration files are empty: %s" % configuration_files)
        self.configuration_files = configuration_files

class ConfigYamlParsingError(InvalidConfigurationException):
    def __init__(self, parsing_exception, config):
        super().__init__("Error parsing YAML file %s due to %s" % (config, parsing_exception))
        self.parsing_exception = parsing_exception
        self.config = config

class DisplayNotFound(InvalidConfigurationException):
    def __init__(self, display_id):
        super().__init__("Display %s not found" % display_id)
        self.display_id = display_id


# descriptors.Descriptor
#
# Implements the Descriptor class from which all the descriptors
# in the descriptors package inherit.

from __future__ import print_function, unicode_literals, division

import sys

def with_metaclass(meta, *bases):
    """Armin Ronacher's version of six.with_metaclass.
    http://lucumr.pocoo.org/2013/5/21/porting-to-python-3-redux/

    """
    class metaclass(meta):
        __call__ = type.__call__
        __init__ = type.__init__

        def __new__(cls, name, this_bases, d):
            if this_bases is None:
                return type.__new__(cls, name, (), d)
            return meta(name, bases, d)
    return metaclass(str("temporary_class"), None, {})

class DescriptorMeta(type):
    """The sole purpose of this metaclass is to automatically append a
    call to Descriptor.__set__() to the __set__() method of derived
    classes, so we don't have to manually do that for every Descriptor
    we create. This can be disabled by creating a "no_autoset" class
    attribute in the derived class.

    """
    def __new__(cls, clsname, bases, clsdict):
        clsobj = super(DescriptorMeta, cls).__new__(
            cls, clsname, bases, clsdict)
        if "no_autoset" in clsdict:
            return clsobj
        if hasattr(clsobj, "__set__"):
            setter = getattr(clsobj, "__set__")
            new_setter = cls.add_super__set__(clsobj, setter)
            setattr(clsobj, "__set__", new_setter)
        return clsobj

    def add_super__set__(clsobj, func):
        def wrapper(*args, **kwargs):
            func(*args, **kwargs)
            next_cls = clsobj.__mro__[1]
            if hasattr(next_cls, "__set__"):
                super_setter = getattr(next_cls, "__set__")
                super_setter(*args, **kwargs)
        return wrapper

class Descriptor(with_metaclass(DescriptorMeta, object)):
    """The Descriptor base class from which all other descriptors
    inherit.

    """
    def __init__(self):
        self.field_type = self.__class__.__name__

    def __set__(self, instance, value, name=None):
        if name is None:
            name = self.name
        else:  # __set__ was originally called by a composed descriptor
            self.name = name
        instance.__dict__[name] = value

    def __get__(self, instance, cls):
        try:
            return instance.__dict__[self.name]
        except KeyError:
            raise AttributeError

    def __delete__(self, instance):
        del instance.__dict__[self.name]

    def __repr__(self):
        return "<Descriptor: {} at {}>".format(self.field_type, hex(id(self)))

    def err_msg(self, instance, value):
        """Return an error message for use in exceptions thrown by
        subclasses.

        """
        if not hasattr(self, "name"):
            # err_msg will be called by the composed descriptor
            return ""
        return (
            "Attempted to set the {f_type} attribute {inst}.{attr} to the "
            "{val_type} value {val}, which does not satisfy the condition "
            "{f_type}.".format(
                f_type=self.field_type,
                inst=instance.__class__.__name__,
                attr=self.name,
                val_type=value.__class__.__name__,
                val=value))

    @staticmethod
    def assert_descriptor(obj):
        """Assert that obj is a Descriptor instance."""
        if not isinstance(obj, Descriptor):
            raise TypeError(
                "Cannot combine with a non-Descriptor instance.")

    @staticmethod
    def __create_new(field_type, set_func):
        clsdict = {"__set__": set_func, "no_autoset": True}
        new_desc = type(str(field_type), (Descriptor, ), clsdict)()
        new_desc.field_type = field_type
        return new_desc

    def __and__(self, other):
        """Create a conjunction of this descriptor and another
        descriptor.

        """
        Descriptor.assert_descriptor(other)
        setter1 = self.__class__.__set__
        setter2 = other.__class__.__set__

        def new_set(new_self, instance, value, name=None):
            if name:
                new_self.name = name
            try:
                setter1(self, instance, value, name=new_self.name)
                value = getattr(instance, new_self.name)
                setter2(other, instance, value, name=new_self.name)
                value = getattr(instance, new_self.name)
                super(new_self.__class__, new_self).__set__(
                    instance, value, name)
            except ValueError:
                raise ValueError(new_self.err_msg(instance, value))

        new_field_type = self.field_type + "_AND_" + other.field_type
        new_desc = Descriptor.__create_new(new_field_type, new_set)
        return new_desc

    def __or__(self, other):
        """Create a disjunction of this descriptor and another
        descriptor.

        """
        Descriptor.assert_descriptor(other)
        setter1 = self.__class__.__set__
        setter2 = other.__class__.__set__

        def new_set(new_self, instance, value, name=None):
            if name:
                new_self.name = name
            exceptions = 0
            for setter, caller in zip((setter1, setter2), (self, other)):
                try:
                    setter(caller, instance, value, name=new_self.name)
                    return  # one descriptor satisfied
                except ValueError as e:  # Descriptors throw ValueErrors
                    if Descriptor.exc_thrown_by_descriptor():
                        exceptions += 1
                    else:
                        raise e
            if exceptions >= 2:
                raise ValueError(new_self.err_msg(instance, value))

        new_field_type = self.field_type + "_OR_" + other.field_type
        new_desc = Descriptor.__create_new(new_field_type, new_set)
        return new_desc

    @staticmethod
    def exc_thrown_by_descriptor():
        """Return True if the last exception was thrown by a
        Descriptor instance.

        """
        traceback = sys.exc_info()[2]
        tb_locals = traceback.tb_frame.f_locals
        # relying on naming convention to get the object that threw
        # the exception
        if "self" in tb_locals:
            if not isinstance(tb_locals["self"], Descriptor):
                return False
            return True
        return False


"""
`PyFlot` is a Python interface for the known `Flot` Javascript chart library
"""

__version__ = "0.1"
get_version = lambda : __version__

import time
try:
    import simplejson as json
except ImportError:
    import json

import exception

class BaseOptions(dict):
    "Base implementation for options"

    allowed_options = {}

    def __setitem__(self, key, value):
        """
        __setitem__ should only allow keys within allowed option keys
        and they have to batch the type
        """
        if not key.startswith('__'):
            if key not in self.allowed_options.keys():
                raise TypeError("%s is not a allowed option. Allowed "
                        "options are: %s" % (key, self.allowed_options.keys()))
            if not isinstance(value, self.allowed_options[key]):
                raise TypeError("Option %s got an unexpected object type" % key)
        super(BaseOptions, self).__setitem__(key, value)

    def __init__(self, *args, **kwargs):
        super(BaseOptions, self).__init__()
        for option, value in kwargs.items():
            if value:
                self[option] = value

class GraphOptions(BaseOptions):
    "Option object for graph"

    allowed_options = {
                        'xaxis': dict,
                        'yaxis': dict,
                        'legend': dict,
                        'grid': dict,
    }

class SeriesOptions(BaseOptions):
    """
        Options for Series object. This should be a completition of the
    normal json_data object
    """

    allowed_options  = {
            'label': basestring,
            'color': basestring,
            'xaxis': list,
            'yaxis': list,
            'lines': dict,
            'bars': dict,
            'points': dict,
            'clickable': bool,
            'hoverable': bool,
            'shadowSize': int,
            'highlightColor': basestring,
    }

class Variable(object):
    "Genric Variable Object"

    def contribute_to_class(self, obj, attr_name, data=None):
        if getattr(obj, self._var_name, False):
            raise exception.MultipleAxisException(
                                        "%s already set" % self._var_name)
        setattr(obj, self._var_name, self)
        if data:
            self._set_points(attr_name, data)

class Axis(object):
    "Generic Axis object"

    def __repr__(self):
        return self._var_name

    def __str__(self):
        return self._var_name

class XAxis(Axis):
    "X Axis Object"
    _var_name = '_x'

class YAxis(Axis):
    "Y Axis Object"
    _var_name = '_y'

class LinearVariable(Variable):
    "Linear Variable Object. No adjustment its done to the points"
    def __init__(self, points=None, **kwargs):
        if points is not None:
            self.points = points

    def _set_points(self, attr_name, data):
        self.points = []
        for sample in data:
            try:
                self.points.append(getattr(sample, attr_name))
            except AttributeError:
                s = {}
                s['_x'], s['_y'] = sample
                self.points.append(s[self._var_name])

class TimeVariable(Variable):
    "Time Variable Object. Points content its adjusted"
    def __init__(self, points=None, **kwargs):
        if points is not None:
            self.points = [int(time.mktime(point.timetuple()) * 1000)\
                                                    for point in points]

    def _set_points(self, attr_name, data):
        an = attr_name
        self.points = [int(time.mktime(getattr(s, an).timetuple()) * 1000)\
                                                    for s in data]
        self.points = []
        for sample in data:
            try:
                self.points.append(
                    int(time.mktime(getattr(sample, an).timetuple()) * 1000)
                )
            except AttributeError:
                s = {}
                s['_x'], s['_y'] = sample
                self.points.append(
                    int(time.mktime(s[self._var_name].timetuple()) * 1000)
                )

class XVariable(XAxis, LinearVariable):
    "Linear Variable on X Axis"

class YVariable(YAxis, LinearVariable):
    "Linear Variable on Y Axis"

class TimeXVariable(XAxis, TimeVariable):
    "Time Variable on X Axis"

class TimeYVariable(YAxis, TimeVariable):
    "Time Variable on Y Axis"

class Series(dict):
    "This class represents the actual flot series"

    _options = SeriesOptions()

    def _set_data(self):
        """
        This method will be called to set Series data
        """
        if getattr(self, 'data', False) and not getattr(self, '_x', False) and not getattr(self, '_y', False):
            _x = XVariable()
            _y = YVariable()
            _x.contribute_to_class(self, 'X', self.data)
            _y.contribute_to_class(self, 'Y', self.data)
            self['data'] = zip(self._x.points, self._y.points)
        else:
            for axis in ('_x', '_y'):
                axis_obj = getattr(self, axis, False)
                if not axis_obj:
                    raise exception.MissingAxisException("%s missing" % axis)
                if not getattr(axis_obj, 'points', False):
                    raise exception.MissingDataException()

            self['data'] = zip(self._x.points, self._y.points)

    def __init__(self, data=None, xpoints=None, ypoints=None, options=None, *args, **kwargs):
        """
        Series should be able to get contructed and completed through
        class definition or through kwargs
        """
        #FIXME
        #   There should be a sanitizing method to take care of data, xpoints
        # and ypoints args.
        self._options = SeriesOptions()
        super(Series, self).__init__()

        if 'data' not in dir(self) and data is not None:
            # if not data is defined in Class def try to get is by args
            self.data = data
        if data is None and xpoints is not None and ypoints is not None:
            # xpoints and ypoints were passed directly to Series object
            self.data = zip(xpoints, ypoints)

        # through class definition
        for attr_name in dir(self):
            attr = getattr(self, attr_name)
            if isinstance(attr, Variable):
                attr.contribute_to_class(self, attr_name, self.data)

        # through kwargs
        for name, attr in kwargs.iteritems():
            if isinstance(attr, Variable):
                attr.contribute_to_class(self, name)

        # set series data
        self._set_data()

        # set series options
        for option in dir(self.Meta):
            self._options[option] = getattr(self.Meta, option)
        self.update(self._options)
        # options passed to the construction will overide Meta options
        if options is not None and isinstance(options, SeriesOptions):
            self.update(options)

    @property
    def json_series(self):
        "serializes a Series object"
        return json.dumps(self)

    class Meta:
        pass

class Graph(object):
    "Contains the data object and also the plot options"

    _series = []
    _options = GraphOptions()

    def __init__(self, series=None, options=None, **kwargs):
        "This contructor will be able to receive"
        self._series = []
        # set series
        for attr_name in dir(self):
            attr = getattr(self, attr_name)
            if isinstance(attr, Series):
                self._series.append(attr)
        for arg in kwargs.values():
            if isinstance(arg, Series):
                self._series.append(arg)

        if series and all([isinstance(s, Series) for s in series]):
            self._series.extend(series)

        # set options through Meta
        for option in dir(self.Meta):
            self._options[option] = getattr(self.Meta, option)
        # check for options through argument
        if options and isinstance(options, GraphOptions):
            self._options.update(options)
        self._set_options()

    def _get_axis_mode(self, axis):
        "will get the axis mode for the current series"
        if all([isinstance(getattr(s, axis), TimeVariable) for s in self._series]):
            return 'time'
        return None

    def _set_options(self):
        "sets the graph ploting options"
        # this is aweful
        # FIXME: Axis options should be passed completly by a GraphOption
        if 'xaxis' in self._options.keys():
            self._options['xaxis'].update(
                        {'mode' : self._get_axis_mode(XAxis._var_name)})
        if 'yaxis' in self._options.keys():
            self._options['yaxis'].update(
                        {'mode' : self._get_axis_mode(YAxis._var_name)})

    @property
    def json_data(self):
        "returns its json data serialized"
        return json.dumps(self._series)

    @property
    def options(self):
        "returns its json option serialized"
        return json.dumps(self._options)

    class Meta:
        # meta should contain graph options, eg. xaxis, yaxis, legend
        pass