main.py
Go to the documentation of this file.
1 #!/usr/bin/python
2 '''
3 Fixme: make this into a proper click main
4 '''
5 import os
6 import sys
7 import json
8 from collections import defaultdict
9 import subprocess
10 
11 import click
12 
13 from wirecell import units
14 
15 @click.group("pgraph")
16 @click.pass_context
17 def cli(ctx):
18  '''
19  Wire Cell Signal Processing Features
20  '''
21 
22 class Node (object):
23  def __init__(self, tn, **attrs):
24  if not attrs:
25  print ("Node(%s) with no attributes"%tn)
26 
27  self.tn = tn
28  tn = tn.split(":")
29  self.type = tn[0]
30  try:
31  self.name = tn[1]
32  except IndexError:
33  self.name = ""
34  self.ports = defaultdict(set);
35  self.attrs = attrs
36 
37  @property
38  def display_name(self):
39  if self.name:
40  return "[%s]"%self.name
41  return "(unnamed)"
42 
43  def add_port(self, end, ident):
44  '''
45  Add a port of end "head" or "tail" and ident (number).
46  '''
47  self.ports[end].add(ident)
48 
49  def dot_name(self, port=None):
50  return self.tn.replace(":","_")
51 
52  def dot_label(self):
53  ret = list()
54  if "head" in self.ports:
55  head = "{%s}" % ("|".join(["<in%d>%d"%(num,num) for num in sorted(self.ports["head"])]),)
56  ret.append(head)
57 
58  body = [self.type, self.display_name]
59  for k,v in sorted(self.attrs.items()):
60  if isinstance(v,list):
61  v = "list(%d)"%len(v)
62  if isinstance(v,dict):
63  v = "dict(%d)"%len(v)
64  one = "%s = %s" % (k,v)
65  body.append(one)
66  body = r"\n".join(body)
67  body = r"{%s}" % body
68  ret.append(body)
69 
70  if "tail" in self.ports:
71  tail = "{%s}" % ("|".join(["<out%d>%d"%(num,num) for num in sorted(self.ports["tail"])]),)
72  ret.append(tail)
73 
74  return "{%s}" % ("|".join(ret),)
75 
76 
77 def is_string(x):
78  return type(x) in [type(u""), type("")]
79 def is_list(x):
80  return type(x) in [list]
82  if not is_list(x): return False
83  return all(map(is_string, x))
84 
85 def dotify(edge_dat, attrs):
86  '''
87  Return GraphViz text. If attrs is a dictionary, append to the
88  node a list of its items.
89  '''
90 
91  nodes = dict()
92  def get(edge, end):
93  tn = edge[end]["node"]
94  try:
95  n = nodes[tn]
96  except KeyError:
97  n = Node(tn, **attrs.get(tn, {}))
98  nodes[tn] = n
99  p = edge[end].get("port",0)
100  n.add_port(end, p)
101  return n,p
102 
103 
104  edges = list()
105  for edge in edge_dat:
106  t,tp = get(edge, "tail")
107  h,hp = get(edge, "head")
108  e = '"%s":out%d -> "%s":in%d' % (t.dot_name(),tp, h.dot_name(),hp)
109  edges.append(e);
110 
111  # Try to find any components refereneced.
112  for tn,n in list(nodes.items()):
113  for k,v in n.attrs.items():
114  tocheck = None
115  if is_string(v):
116  tocheck = [v]
117  if is_list_of_string(v):
118  tocheck = v
119  if not tocheck:
120  continue
121  for maybe in tocheck:
122  if maybe not in attrs:
123  continue
124 
125  cn = nodes.get(maybe,None);
126  if cn is None:
127  cn = Node(maybe, **attrs.get(maybe, {}))
128  nodes[maybe] = cn
129 
130  e = '"%s" -> "%s"[style=dashed,color=gray]' % (n.dot_name(), cn.dot_name())
131  edges.append(e)
132 
133 
134 
135  ret = ["digraph pgraph {",
136  "rankdir=LR;",
137  "\tnode[shape=record];"]
138  for nn,node in sorted(nodes.items()):
139  ret.append('\t"%s"[label="%s"];' % (node.dot_name(), node.dot_label()))
140  for e in edges:
141  ret.append("\t%s;" % e)
142  ret.append("}")
143  return '\n'.join(ret);
144 
145 def jsonnet_try_path(path, rel):
146  if not rel:
147  raise RuntimeError('Got invalid filename (empty string).')
148  if rel[0] == '/':
149  full_path = rel
150  else:
151  full_path = os.path.join(path, rel)
152  if full_path[-1] == '/':
153  raise RuntimeError('Attempted to import a directory')
154 
155  if not os.path.isfile(full_path):
156  return full_path, None
157  with open(full_path) as f:
158  return full_path, f.read()
159 
160 
161 def jsonnet_import_callback(path, rel):
162  paths = [path] + os.environ.get("WIRECELL_PATH","").split(":")
163  for maybe in paths:
164  try:
165  full_path, content = jsonnet_try_path(maybe, rel)
166  except RuntimeError:
167  continue
168  if content:
169  return full_path, content
170  raise RuntimeError('File not found')
171 
172 
173 
174 def resolve_path(obj, jpath):
175  '''
176  Select out a part of obj based on a "."-separated path. Any
177  element of the path that looks like an integer will be cast to
178  one assuming it indexes an array.
179  '''
180  jpath = jpath.split('.')
181  for one in jpath:
182  if not one:
183  break
184  try:
185  one = int(one)
186  except ValueError:
187  pass
188  obj = obj[one]
189 
190  return obj
191 
192 def uses_to_params(uses):
193  '''
194  Given a list of nodes, return a dictionary of their "data" entries
195  keyed by 'type' or 'type:name'
196  '''
197  ret = dict()
198  for one in uses:
199  if type(one) != dict:
200  print (type(one),one)
201  tn = one[u"type"]
202  if "name" in one and one['name']:
203  print (one["name"])
204  tn += ":" + one["name"]
205  ret[tn] = one.get("data", {})
206  return ret
207 
208 @cli.command("dotify")
209 @click.option("--jpath", default="",
210  help="A dot-delimited path into the JSON to locate a graph-like object")
211 @click.option("--params/--no-params", default=True,
212  help="Enable/disable the inclusion of contents of configuration parameters")
213 @click.argument("json-file")
214 @click.argument("out-file")
215 @click.pass_context
216 def cmd_dotify(ctx, jpath, params, json_file, out_file):
217  '''
218  Convert a JSON file for a WCT job configuration based on the
219  Pgraph app into a dot file.
220 
221  The JSON file needs to at least contain a list of edges found at
222  the given jpath. Use, eg, "-1" to locate the last element of a
223  configuration sequence which is typically the config for a
224  Pgrapher. If indeed it is, its [jpath].data.edges attribution
225  will be located and the overall JSON data structure will be used
226  as a list of nodes. Otherwise [jpath].edges will be used and
227  [jpath].uses will be used to provide an initial list of node
228  objects.
229  '''
230  if json_file.endswith(".jsonnet"):
231  import _jsonnet
232  jtext = _jsonnet.evaluate_file(json_file, import_callback=jsonnet_import_callback)
233  else:
234  jtext = open(json_file).read()
235 
236  dat = json.loads(jtext)
237  try:
238  cfg = resolve_path(dat, jpath)
239  except Exception:
240  click.echo('failed to resolve path "%s" in object:\n' % (jpath))
241  sys.exit(1)
242 
243  # if cfg["type"] not in ["Pgrapher", "Pnode"]:
244  # click.echo('Object must be of "type" Pgrapher or Pnode, got "%s"' % cfg["type"])
245  # sys.exit(1)
246 
247  if cfg.get("type","") == "Pgrapher": # the Pgrapher app holds edges in "data" attribute
248  print ('Pgrapher object found at jpath: "%s" with %d nodes' % (jpath, len(dat)))
249  edges = cfg["data"]["edges"]
250  uses = dat # if Pgrapher, then original is likely the full config sequence.
251  else:
252  edges = cfg["edges"] # Pnodes have edges as top-level attribute
253  uses = cfg.get("uses", list())
254  attrs = dict()
255  if params:
256  attrs = uses_to_params(uses)
257  dtext = dotify(edges, attrs)
258  ext = os.path.splitext(out_file)[1][1:]
259  dot = "dot -T %s -o %s" % (ext, out_file)
260  proc = subprocess.Popen(dot, shell=True, stdin = subprocess.PIPE)
261  proc.communicate(input=dtext.encode("utf-8"))
262  return
263 
264 def main():
265  cli(obj=dict())
266 
267 if '__main__' == __name__:
268  main()
269 
def display_name(self)
Definition: main.py:38
def is_string(x)
Definition: main.py:77
def is_list_of_string(x)
Definition: main.py:81
int open(const char *, int)
Opens a file descriptor.
Coord add(Coord c1, Coord c2)
Definition: restypedef.cpp:23
def dotify(edge_dat, attrs)
Definition: main.py:85
def dot_label(self)
Definition: main.py:52
def add_port(self, end, ident)
Definition: main.py:43
def uses_to_params(uses)
Definition: main.py:192
def resolve_path(obj, jpath)
Definition: main.py:174
def cmd_dotify(ctx, jpath, params, json_file, out_file)
Definition: main.py:216
def __init__(self, tn, attrs)
Definition: main.py:23
static QInternalList< QTextCodec > * all
Definition: qtextcodec.cpp:63
def cli(ctx)
Definition: main.py:17
def jsonnet_try_path(path, rel)
Definition: main.py:145
def is_list(x)
Definition: main.py:79
def jsonnet_import_callback(path, rel)
Definition: main.py:161
int read(int, char *, size_t)
Read bytes from a file descriptor.
void split(std::string const &s, char c, OutIter dest)
Definition: split.h:35
auto const & get(AssnsNode< L, R, D > const &r)
Definition: AssnsNode.h:115
def dot_name(self, port=None)
Definition: main.py:49