ubuntu-diagrams/diagrams.py

316 lines
10 KiB
Python

from subprocess import Popen, PIPE
import sys
from sys import platform
from os.path import expanduser
from pathlib import Path
import os.path
import zlib
import re
import json
class Example(object):
""" Example handling """
@staticmethod
def get(generator):
scriptdir = os.path.dirname(os.path.abspath(__file__))
example = scriptdir + "/../web/examples/example.%s" % generator
if os.path.isfile(example):
txt = Path(example).read_text()
else:
txt = "no example for %s found" % generator
return txt
class Command(object):
""" a command to be run using the shell environment """
def __init__(self, cmd, versionOption="--version", timeout=5, debug=False):
""" construct me """
self.cmd = cmd
self.timeout = timeout
self.versionOption = versionOption
self.cmdpath = None
self.debug = debug
def call(self, args):
""" call me with the given args"""
return self.docall(self.cmdpath, self.cmd, args)
def callalias(self, alias, args):
""" call me with the given args"""
#print ("callalias cmdpath %s\nalias %s\nargs %s" % (self.cmdpath, alias, args), file=sys.stderr)
return self.docall(self.cmdpath, alias, args)
def docall(self, cmdpath, cmd, args):
""" call with a specific path and command"""
cmdline = "%s%s %s" % (cmdpath, cmd, str(args))
if self.debug:
print ("calling %s" % cmdline, file=sys.stderr)
process = Popen(cmdline, shell=True, stdin=PIPE, stdout=PIPE, stderr=PIPE)
try:
stdout, stderr = process.communicate(timeout=self.timeout)
if self.debug:
if stdout is not None:
print ("stdout: %s" % stdout.decode('utf-8'), file=sys.stderr)
if stderr is not None:
print ("stderr: %s" % stderr.decode('utf-8'), file=sys.stderr)
return stdout, stderr
except Exception:
process.kill()
return None, None
def check(self):
cmdpaths = []
# do we know the cmdpath?
if self.cmdpath is None:
# no we need to try multiple options in a specific order
# prio #1: $HOME/bin
home = expanduser("~")
cmdpaths.append(home + "/bin/")
# prio #2: e.g. Macports
if platform == "darwin":
cmdpaths.append("/opt/local/bin/")
# prio #3: default path / no path
# no path - use default PATH
cmdpaths.append("")
else:
# we know the valid path
cmdpaths.append(self.cmdpath)
for cmdpath in cmdpaths:
stdout, stderr = self.docall(cmdpath, self.cmd, self.versionOption)
stdoutTxt = None
stderrTxt = None
if stdout is not None:
stdoutTxt = stdout.decode("utf-8")
if stderr is not None:
stderrTxt = stderr.decode("utf-8")
if not "not found" in stderrTxt and not "No such file or directory" in stderrTxt:
self.cmdpath = cmdpath
return stdoutTxt, stderrTxt
return None, None
class Generators(object):
generatorDict = {}
""" the available generators """
@staticmethod
def get(generator):
if len(Generators.generatorDict) is 0:
for gen in Generators.generators():
Generators.generatorDict[gen.id] = gen
gen = None
if generator in Generators.generatorDict:
gen = Generators.generatorDict[generator]
return gen
@staticmethod
def generatorIdForAlias(alias):
for gen in Generators.generators():
if alias in gen.aliases:
return gen.id
return None
@staticmethod
def generators():
scriptdir = os.path.dirname(os.path.abspath(__file__))
for plantumlpath in [".", ".."]:
plantumljar = scriptdir + "/" + plantumlpath + "/plantuml.jar";
if os.path.isfile(plantumljar):
break;
if plantumljar is None:
raise Exception("plantuml.jar not found in %s or .. of it", scriptdir)
#else
gens = [
Generator("graphviz", "GraphViz", "dot", "-V", logo="https://graphviz.gitlab.io/_pages/Resources/app.png", url="https://www.graphviz.org/",
aliases=[ 'dot', 'neato', 'twopi', 'circo', 'fdp', 'sfdp', 'patchwork', 'osage' ],
defaultType='png',
outputTypes=['dot', 'xdot', 'ps', 'pdf', 'svg', 'fig', 'png', 'gif', 'jpg', 'json', 'imap', 'cmapx']
),
Generator("mscgen", "Mscgen", "mscgen", "", logo="http://www.mcternan.me.uk/mscgen/img/msc-sig.png", url="http://www.mcternan.me.uk/mscgen/", defaultType='png', outputTypes=['png', 'eps', 'svg', 'ismap']),
Generator("plantuml", "PlantUML", "java -jar " + plantumljar, "-version", aliases=['plantuml'],
logo="https://useblocks.com/assets/img/posts/plantuml_logo.png",
url="https://plantuml.com",
defaultType='png',
download="http://sourceforge.net/projects/plantuml/files/plantuml.jar/download",
outputTypes=['png', 'svg', 'eps', 'pdf', 'vdx', 'xmi', 'scxml', 'html', 'txt', 'utxt',
'latex', 'latex:nopreamble'])
]
return gens
class GenerateResult(object):
def __init__(self, crc32, outputType, path, stdout, stderr):
""" construct me """
self.crc32 = crc32;
self.outputType = outputType;
self.path = path;
self.stdout = stdout
self.stderr = stderr
def errMsg(self):
""" decode my stdout and stderr to an error message"""
msg = ""
if self.stdout is not None:
msg = msg + self.stdout.decode('utf-8')
if self.stderr is not None:
msg = msg + self.stderr.decode('utf-8')
return msg
def asHtml(self):
""" return me as HTML"""
url = '/render/%s/%s' % (self.outputType, self.crc32)
if self.outputType in ['gif', 'jpg', 'png', 'svg']:
return "<img src='%s'>" % url;
elif self.outputType in ['pdf']:
return "<object data='%s' width='640' height='640'></object>" % url;
else:
return "<a href='%s'>%s %s</a>" % (url, self.outputType, self.crc32)
def isValid(self):
""" check if i am valid"""
valid = os.path.isfile(self.path) and not self.errMsg()
return valid
def asJson(self, baseurl):
""" return my result as JSON for the Mediawiki diagrams extension"""
errMsg=self.errMsg();
if errMsg:
jsonTxt="""{
"error": "generating %s failed",
"message": %s
}""" % (self.outputType,json.dumps(errMsg))
else:
jsonTxt = """{
"diagrams": {
"png": {
"url": "%s/png/%s.png"
}
}
}""" % (baseurl,self.crc32)
return jsonTxt
class Generator(object):
""" a diagram generator """
@staticmethod
def getOutputDirectory():
home = expanduser("~")
outputDir = home + "/.diagrams/"
if not os.path.isdir(outputDir):
os.mkdir(outputDir);
return outputDir
def __init__(self, genid, name, cmd, versionOption, logo=None, url=None, download=None, defaultType=None, aliases=None, outputTypes=None, debug=False):
""" construct me """
self.id = genid
self.name = name
self.cmd = cmd
self.gencmd = None
self.logo = logo
self.url = url
self.htmlInfo = None
self.download = download
self.versionOption = versionOption;
if aliases is None:
self.aliases = [cmd]
else:
self.aliases = aliases
self.defaultType = defaultType
self.selectedType = defaultType
self.outputTypes = outputTypes
self.debug = debug
pass
def getHtmlInfo(self):
""" get info on this generator to be displayed via HTML"""
# cache the info since getVersion is a costly process
if self.htmlInfo is None:
version = self.getVersion()
self.htmlInfo = "<a href='%s' title='%s:%s'><img src='%s'/></a>" % (self.url, self.name, version, self.logo)
return self.htmlInfo
def check(self):
""" check my version"""
self.gencmd = Command(self.cmd, self.versionOption, debug=self.debug)
return self.gencmd.check()
def getVersion(self):
stdOutText, stdErrText = self.check()
if stdOutText is None:
stdOutText = ''
if stdErrText is None:
stdErrText = ''
outputText = stdOutText + stdErrText
found = re.search(r'version.*[,)]', outputText)
if found:
version = found.group()
else:
# actually an error message
version = outputText
# invalidate gencmd
self.gencmd = None
return version
@staticmethod
def getHash(txt):
hashValue = zlib.crc32(txt.encode()) & 0xffffffff
hashId = hex(hashValue)
#print ("txt %s\nhashValue %s\nhashId %s" % (txt, hashValue, hashId), file=sys.stderr)
return hashId
def wrap(self,txt):
""" wraot the given text"""
if self.id == "plantuml":
txt="@startuml\n%s\n@enduml\n" % txt
return txt
def generate(self, alias, txt, outputType, renderer, useCached=True):
""" generate """
txt=self.wrap(txt)
hashId = Generator.getHash(txt)
inputPath = "%s%s.%s" % (Generator.getOutputDirectory(), hashId, 'txt')
stdout = None
stderr = None
if not (os.path.isfile(inputPath) and useCached):
text_file = open(inputPath, "wt", encoding='utf-8')
try:
if self.debug:
print ("txt %s" % txt, file=sys.stderr)
text_file.write("%s" % txt)
except Exception as e:
print ("exception %s" % e, file=sys.stderr)
finally:
text_file.close()
outputPath = "%s%s.%s" % (Generator.getOutputDirectory(), hashId, outputType)
if os.path.isfile(outputPath) and useCached:
if self.debug:
print ("cached %s #%s from %s" % (outputType, hashId, outputPath), file=sys.stderr)
#else:
if True:
if self.debug:
print ("generating %s #%s to %s" % (outputType, hashId, outputPath), file=sys.stderr)
if self.id == "graphviz":
args = "-T%s %s -o %s" % (outputType, inputPath, outputPath)
if len(renderer) != 0: #exists and not empty
alias = renderer
else:
alias = "dot"
#print ("renderer %s\nalias %s" % (renderer, alias), file=sys.stderr)
elif self.id == "mscgen":
args = "-T%s -i %s -o %s" % (outputType, inputPath, outputPath)
elif self.id == "plantuml":
args = "-charset UTF-8 -t%s %s" % (outputType, inputPath)
alias = "java -jar /var/diagrams/plantuml.jar"
else:
print ("unknown generator %s" % self.id, file=sys.stderr)
if self.gencmd is None:
self.check()
stdout, stderr = self.gencmd.callalias(alias, args)
result = GenerateResult(hashId, outputType, outputPath, stdout, stderr)
return result