Advertisement
tmax

Untitled

Sep 19th, 2015
222
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 18.07 KB | None | 0 0
  1. #!/usr/bin/env python
  2.  
  3. import os
  4. import re
  5. import sys
  6. import logging
  7. import httplib
  8. import inspect
  9. import argparse
  10. import requests
  11. import json
  12. import pprint
  13. import time
  14.  
  15. logger = None
  16.  
  17. def configure_logger(name=None, debug=False):
  18.     global logger
  19.     name = name or __name__
  20.     logger = logging.getLogger(name)
  21.     level = logging.DEBUG if debug else logging.INFO
  22.     logger.setLevel(level)
  23.     ch = logging.StreamHandler()
  24.     ch.setLevel(logging.DEBUG)
  25.     datefmt='%c'
  26.     formatter = logging.Formatter("[%(asctime)s %(name)s] %(message)s",  datefmt=datefmt)
  27.     ch.setFormatter(formatter)
  28.     logger.addHandler(ch)
  29.     if debug:
  30.         try:
  31.             import http.client as http_client
  32.         except ImportError:
  33.             # Python 2
  34.             import httplib as http_client
  35.             http_client.HTTPConnection.debuglevel = 1
  36.  
  37.  
  38. def which(program):
  39.     def is_qualified_exe(fpath):
  40.         return len(os.path.split(fpath)[0]) and os.path.isfile(fpath) and os.access(fpath, os.X_OK)
  41.     if is_qualified_exe(program):
  42.         return program
  43.     if sys.platform == "darwin":
  44.         bg1 = "/Applications/%s/%s.app/Contents/MacOS/%s" % (program, program, program)
  45.         bg2 = "/Applications/%s.app/Contents/MacOS/%s" % (program, program)
  46.         for best_guess in (bg1, bg2):
  47.             if is_qualified_exe(best_guess):
  48.                 return best_guess
  49.     for path in os.environ["PATH"].split(os.pathsep):
  50.         path = path.strip('"')
  51.         best_guess = os.path.join(path, program)
  52.         if is_qualified_exe(best_guess):
  53.             return best_guess
  54.     return None
  55.  
  56. def touch(fname, times=None):
  57.     with open(fname, 'a'):
  58.         os.utime(fname, times)
  59.  
  60. class TransformPipeline(object):
  61.     def __init__(self, processors=None, **kw):
  62.         self.output_dir = kw.get("output_dir", '.')
  63.         self.init_kw = kw
  64.         self.processors = self._scan_for_processors(processors)
  65.         self.extmap = {}
  66.         for proc in self.processors:
  67.             self.extmap.update(proc.extmap)
  68.  
  69.     def _scan_for_processors(self, processors):
  70.         processors = processors or list()
  71.         if not processors:
  72.             for (name, cls) in globals().items():
  73.                 if cls in (PrintProcess, PrintCommand):
  74.                     continue
  75.                 if not (inspect.isclass(cls) and issubclass(cls, PrintProcess)):
  76.                     continue
  77.                 processors.append(cls)
  78.         return [proc(**self.init_kw) for proc in processors]
  79.    
  80.     def get_pipeline(self, inputfn, outputfn, level=1):
  81.         inputext = os.path.splitext(inputfn)[-1]
  82.         outputext = os.path.splitext(outputfn)[-1]
  83.         instem = os.path.splitext(os.path.split(inputfn)[-1])[0]
  84.         if inputext not in self.extmap:
  85.             return []
  86.         if outputext in self.extmap[inputext]:
  87.             # we're done!
  88.             return [(self.extmap[inputext][outputext], inputfn, outputfn)]
  89.         res = []
  90.         for _inputext in self.extmap[inputext]:
  91.             _inputfn = os.path.abspath(os.path.join(self.output_dir, instem + _inputext))
  92.             res = self.get_pipeline(_inputfn, outputfn, level=level+1)
  93.             if res:
  94.                 res = [(self.extmap[inputext][_inputext], inputfn, _inputfn)] + res
  95.         return res
  96.  
  97.     def run_pipeline(self, pipeline, **kw):
  98.         for (method, inputfn, outputfn) in pipeline:
  99.             method(inputfn, outputfn, **kw)
  100.             if method.im_self.stale(inputfn, outputfn):
  101.                 msg = "%s -> %s failed (stale)" % (inputfn, outputfn)
  102.                 raise RuntimeError, msg
  103.  
  104.     def transform(self, inputfn, outputfn, **kw):
  105.         outputfn = outputfn
  106.         pipeline = self.get_pipeline(inputfn, outputfn)
  107.         if not pipeline:
  108.             raise RuntimeError, "Could not transform %s -> %s" % (inputfn, outputfn)
  109.         self.run_pipeline(pipeline, **kw)
  110.  
  111. class PrintProcess(object):
  112.     FunctionTemplate = re.compile("transform_(.+)_to_(.+)")
  113.  
  114.     def __init__(self, *args, **kw):
  115.         self.phony = kw.get("phony", False)
  116.         self._extmap = None
  117.  
  118.     @property
  119.     def extmap(self):
  120.         if self._extmap == None:
  121.             self._extmap = {}
  122.             methods = [(self.FunctionTemplate.match(info[0]), info[1]) for info in inspect.getmembers(self) if inspect.ismethod(info[1])]
  123.             methods = [list(match.groups()) + [info] for (match, info) in methods if match]
  124.             extmap = {}
  125.             for (ext1, ext2, method) in methods:
  126.                 ext1 = '.' + ext1
  127.                 ext2 = '.' + ext2
  128.                 self._extmap[ext1] = self._extmap.get(ext1, {})
  129.                 self._extmap[ext1][ext2] = method
  130.         return self._extmap
  131.  
  132.     def transform(self, inputfn, outputfn, **args):
  133.         if self.phony or self.stale(inputfn, outputfn):
  134.             self._transform(inputfn, outputfn, **args)
  135.         return self.stale(inputfn, outputfn)
  136.    
  137.     def _transform(self, inputfn, outputfn, **args):
  138.         touch(outputfn)
  139.  
  140. class PrintCommand(PrintProcess):
  141.     Command = "__command__"
  142.     CommandPath = None
  143.  
  144.     @property
  145.     def command(self, hint=None):
  146.         if not self.CommandPath:
  147.             hint = hint or self.Command
  148.             self.CommandPath = which(hint)
  149.         if not self.CommandPath:
  150.             print "Could not find %s" % hint
  151.         return self.CommandPath
  152.  
  153.     def stale(self, inputfn, outputfn):
  154.         if os.path.exists(inputfn) and os.path.exists(outputfn):
  155.             return os.path.getmtime(inputfn) > os.path.getmtime(outputfn)
  156.         return True
  157.  
  158.     def run(self, args):
  159.         cmd = str.join(' ', [self.command] + args)
  160.         msg = "running: %s" % cmd
  161.         logger.debug(msg)
  162.         os.system(cmd)
  163.        
  164. class Cura_API(PrintCommand):
  165.     Command = "cura"
  166.  
  167.     def _transform(self, inputfn, outputfn, **kw):
  168.         args = []
  169.         if kw.get("profile", None):
  170.             args.append("-i %s" % args["profile"])
  171.         if kw.get("slice", True):
  172.             args.append("-s")
  173.         args.append("-o %s %s" % (outputfn, inputfn))
  174.         msg = "generating '%s' with Cura" % outputfn
  175.         logger.info(msg)
  176.         self.run(args)
  177.  
  178.     def transform_stl_to_gcode(self, *args, **kw):
  179.         self.transform(*args, **kw)
  180.  
  181. class OpenSCAD_API(PrintCommand):
  182.     Command = "openscad"
  183.  
  184.     def _transform(self, inputfn, outputfn, **kw):
  185.         args = []
  186.         if kw.get("make", True):
  187.             args.append("-m make")
  188.         args.append("-o %s %s" % (outputfn, inputfn))
  189.         msg = "generating '%s' with OpenSCAD" % outputfn
  190.         logger.info(msg)
  191.         self.run(args)
  192.  
  193.     def transform_scad_to_dxf(self, *args, **kw):
  194.         self.transform(*args, **kw)
  195.  
  196.     def transform_scad_to_png(self, *args, **kw):
  197.         self.transform(*args, **kw)
  198.  
  199.     def transform_scad_to_stl(self, *args, **kw):
  200.         self.transform(*args, **kw)
  201.  
  202. class OctoPrint_API(PrintProcess):
  203.     def __init__(self, *args, **kw):
  204.         super(OctoPrint_API, self).__init__(*args, phony=True, **kw)
  205.         self._config = kw["config"]
  206.         self._session = None
  207.         self.location = "sdcard" if kw.get("sdcard", False) else "local"
  208.  
  209.     @property
  210.     def key(self):
  211.         return self.config["OctoAPI_KEY"]
  212.  
  213.     @property
  214.     def url(self):
  215.         while self.config["OctoPrint_URL"].endswith('/'):
  216.             self.config["OctoPrint_URL"] = self.config["OctoPrint_URL"][:-1]
  217.         return (self.config["OctoPrint_URL"] + "/api/")
  218.  
  219.     @property
  220.     def session(self):
  221.         if not self._session:
  222.             self._session = requests.Session()
  223.             self._session.headers["X-Api-Key"] = self.key
  224.             self._session.keep_alive = False
  225.         return self._session
  226.  
  227.     @property
  228.     def config(self):
  229.         return self._config
  230.  
  231.     def get(self, url, **args):
  232.         msg = "GET %s %s" % (url, args)
  233.         logger.debug(msg)
  234.         return self.session.get(self.url + url, params=args)
  235.  
  236.     def post(self, url, files=None, data=None):
  237.         msg = "POST %s args=%s files=%s" % (url, args, 0 if not files else len(files))
  238.         logger.debug(msg)
  239.         kw = {}
  240.         if files:
  241.             kw["files"] = files
  242.         if data != None:
  243.             kw["data"] = json.dumps(data)
  244.             kw["headers"] = {"content-type": "application/json"}
  245.         return self.session.post(self.url + url, **kw)
  246.  
  247.     def delete(self, url, **args):
  248.         msg = "DELETE %s %s" % (url, args)
  249.         logger.debug(msg)
  250.         return self.session.delete(self.url + url, params=args)
  251.  
  252.     ## utils
  253.     def check_response(self, res, code, error=False, **kw):
  254.         failure = (res.status_code != code)
  255.         if failure:
  256.             if error:
  257.                 raise RuntimeError, res.text
  258.             msg = "response code=%s: %s" % (res.status_code, res.text)
  259.             logger.debug(msg)
  260.             return None
  261.         try:
  262.             res = res.json()
  263.         except ValueError:
  264.             pass
  265.         return res
  266.  
  267.     def stale(self, inputfn, outputfn, filename=None, **kw):
  268.         stale = True
  269.         filename = filename or os.path.split(inputfn)[-1]
  270.         file_info = self.get_file(filename, error=False)
  271.         if file_info:
  272.             stale = os.path.getmtime(inputfn) > file_info["date"]
  273.         return stale
  274.    
  275.     ## high level
  276.     def has_file(self, filename, **kw):
  277.         info = self.get_file(filename, **kw)
  278.         return bool(info)
  279.    
  280.     def get_file(self, filename, error=False, **kw):
  281.         url = str.join('/', ("files", self.location, filename))
  282.         res = self.get(url, **kw)
  283.         return self.check_response(res, 200, error=error, **kw)
  284.  
  285.     def post_file(self, path, filename, upload=False, error=True, data=None, **kw):
  286.         params = {}
  287.         if upload:
  288.             url = "files/%s" % self.location
  289.             fh = open(path)
  290.             filedata = fh.read()
  291.             params["files"] = {"file": (filename, filedata)}
  292.         else:
  293.             url = "files/%s/%s" % (self.location, filename)
  294.         if data != None:
  295.             params["data"] = data
  296.         res = self.post(url, **params)
  297.         return self.check_response(res, 201, **kw)
  298.    
  299.     def delete_file(self, filename, **kw):
  300.         url = "files/%s/%s" % (self.location, filename)
  301.         res = self.delete(url)
  302.         return self.check_response(res, 204, **kw)
  303.  
  304.     def get_state(self, **kw):
  305.         res = self.get("printer")
  306.         return self.check_response(res, 200, **kw)
  307.  
  308.     def pause_job(self):
  309.         res = self.post("control/job", data={'command': 'pause'})
  310.         return self.check_response(res, 204)
  311.  
  312.     ## transform API
  313.     def _transform(self, inputfn, outputfn, upload=False, select=False, _print=False, **kw):
  314.         filename = os.path.split(inputfn)[-1]
  315.         actions = []
  316.         params = {}
  317.         if os.path.exists(inputfn):
  318.             stale = self.stale(inputfn, outputfn, filename=filename, **kw)
  319.             upload = upload or stale
  320.         if upload:
  321.             actions.append("uploading")
  322.         if _print or select:
  323.             select = True
  324.             actions.append("selecting")
  325.         if _print:
  326.             actions.append("printing")
  327.         msg = "%s %s on OctoPrint" % (str.join(', ', actions), filename)
  328.         logger.info(msg)
  329.         if upload:
  330.             if self.has_file(filename):
  331.                 self.delete_file(filename)
  332.             self.post_file(inputfn, filename=filename, upload=True)
  333.             time.sleep(1)
  334.         else:
  335.             params = {}
  336.             if select:
  337.                 params["command"] = "select"
  338.                 if _print:
  339.                     params["print"] = True
  340.             self.post_file(inputfn, filename=filename, data=params)
  341.  
  342.     def transform_gcode_to_print(self, inputfn, outputfn, **kw):
  343.         kw["_print"] = True
  344.         kw["select"] = True
  345.         self.transform(inputfn, outputfn, **kw)
  346.  
  347.     def transform_gcode_to_upload(self, inputfn, outputfn, **kw):
  348.         self.transform(inputfn, outputfn, **kw)
  349.  
  350.     def transform_gcode_to_select(self, inputfn, outputfn, **kw):
  351.         kw["select"] = True
  352.         self.transform(inputfn, outputfn, **kw)
  353.  
  354. def load_config(fn, error=True):
  355.     try:
  356.         fh = open(fn)
  357.         config = json.loads(fh.read())
  358.         return config
  359.     except:
  360.         if error:
  361.             msg = "Missing config, please run '%s init' first" % sys.argv[0]
  362.             raise RuntimeError, msg
  363.     return {}
  364.  
  365. def save_config(fn, config):
  366.     msg = "Saving config file to '%s'" % fn
  367.     print msg
  368.     fh = open(fn, 'w')
  369.     fh.write(json.dumps(config, sort_keys=True, indent=4))
  370.  
  371. def get_cli():
  372.     def standard_arg_set(sp):
  373.         sp.add_argument('-S', '--openscad_exe', help='path to OpenSCAD')
  374.         sp.add_argument('-C', '--cura_exe', help='path to Cura')
  375.         sp.add_argument('-F', '--cura_profile', help='path to Cura profile, used for slicing')
  376.         sp.add_argument('-o', '--output_dir', help='output directory')
  377.         sp.add_argument('-P', '--export_png', default=False, action="store_true", help='export image of object with OpenSCAD')
  378.         sp.add_argument('-D', '--export_dxf', default=False, action="store_true", help='export DXF of object with OpenSCAD')
  379.         sp.add_argument('input_filename', nargs=1, help='The filename to process')
  380.  
  381.     # global
  382.     parser = argparse.ArgumentParser(description='octoprint command')
  383.     parser.add_argument('-d', '--debug', default=False, action="store_true", help='enable debugging')
  384.     parser.add_argument('-c', '--config', help='path to octocmd configuration file')
  385.     parser.add_argument('-s', '--sdcard', default=False, action="store_true", help='Use sdcard instead of local storage')
  386.  
  387.     subparsers = parser.add_subparsers(help='sub-command help', dest="mode")
  388.     # create the parser for the "init" command
  389.     sp = subparsers.add_parser('init', help='initialize the configuration')
  390.  
  391.     # create the parser for the "status" command
  392.     sp = subparsers.add_parser('status', help='printer status')
  393.  
  394.     # create the parser for the "pause" command
  395.     sp = subparsers.add_parser('pause', help='pause current job')
  396.  
  397.     # create the parser for the "print" command
  398.     sp = subparsers.add_parser('print', help='print a file')
  399.     standard_arg_set(sp)
  400.  
  401.     # create the parser for the "select" command
  402.     sp = subparsers.add_parser('select', help='select a file')
  403.     standard_arg_set(sp)
  404.  
  405.     # create the parser for the "upload" command
  406.     sp = subparsers.add_parser('upload', help='upload a file')
  407.     standard_arg_set(sp)
  408.  
  409.     args = parser.parse_args()
  410.     # post
  411.  
  412.     if not args.config:
  413.         homedir = os.path.expanduser("~")
  414.         args.config = os.path.join(homedir, ".octocmd.conf")
  415.  
  416.     if hasattr(args, "output_dir"):
  417.         if not args.output_dir:
  418.             input_path = os.path.split(args.input_filename[0])
  419.             args.output_dir = input_path[0] or '.'
  420.         if not os.path.exists(args.output_dir):
  421.             os.mkdir(args.output_dir)
  422.  
  423.     return args
  424.  
  425. def process_status(args):
  426.     op = OctoPrint_API(**args.__dict__)
  427.     res = op.get_state(error=True)
  428.  
  429.     def format_status(obj, level=0):
  430.         def header(level):
  431.             return '    ' * level
  432.         rpt = ''
  433.         vals = []
  434.         for key in obj:
  435.             if key == "text":
  436.                 continue
  437.             val = obj[key]
  438.             if type(val) == dict:
  439.                 if vals:
  440.                     rpt += header(vals[0]) + str.join(', ', vals[1:]) + '\n'
  441.                 rpt += '%s%s: ' % (header(level), key)
  442.                 if "text" in obj[key]:
  443.                     rpt += '%s\n' % obj[key]["text"]
  444.                 elif any([type(i) == dict for i in val.values()]):
  445.                     rpt += '\n'
  446.                 rpt += format_status(val, level + 1)
  447.                 vals = []
  448.             else:
  449.                 val = '%s=%s' % (key, val)
  450.                 if not vals:
  451.                     vals.append(level)
  452.                 vals.append(val)
  453.         if vals:
  454.             rpt += str.join(', ', vals[1:]) + '\n'
  455.         return rpt
  456.  
  457.     print format_status(res)
  458.  
  459. def process_init(args):
  460.     def test_config(config):
  461.         op = OctoPrint_API(config=config)
  462.         res = op.get_state()
  463.         return (res != None)
  464.  
  465.     cache = load_config(args.config, error=False)
  466.     complete = False
  467.     while not complete:
  468.         msg = "OctoPrint URL%s: " % ("[%(OctoPrint_URL)s]" % cache if 'OctoPrint_URL' in cache else '')
  469.         url = raw_input(msg)
  470.         url = url or cache.get("OctoPrint_URL", None)
  471.         msg = "OctoPrint API Key%s: " % ("[%(OctoAPI_KEY)s]" % cache if 'OctoAPI_KEY' in cache else '')
  472.         apikey = raw_input(msg)
  473.         apikey = apikey or cache.get("OctoAPI_KEY", None)
  474.         cache = {"OctoAPI_KEY": apikey, "OctoPrint_URL": url}
  475.         if test_config(cache):
  476.             question = "This configuration works, keep it? [Y/n]: "
  477.             default_answer = 'y'
  478.         else:
  479.             question = "This configuration does not work, keep it? [y/N]: "
  480.             default_answer = 'n'
  481.         answer = raw_input(question).strip() or default_answer
  482.         complete = len(answer) and (answer[0].lower() == 'y')
  483.     save_config(args.config, cache)
  484.  
  485. def process_target(args):
  486.     targets = []
  487.     infn = args.input_filename[0]
  488.     stem = os.path.splitext(os.path.split(infn)[-1])[0]
  489.     outfn = stem + "." + args.mode
  490.     targets.append(outfn)
  491.     if args.export_png:
  492.         outfn = stem + ".png"
  493.         targets.append(outfn)
  494.     pipe = TransformPipeline(**args.__dict__)
  495.     for targetfn in targets:
  496.         pipe.transform(infn, targetfn)
  497.  
  498. def process(args):
  499.     if args.mode == "status":
  500.         args.config = load_config(args.config)
  501.         process_status(args)
  502.     elif args.mode == 'pause':
  503.         op = OctoPrint_API(**args.__dict__)
  504.         res = op.pause_job()
  505.     elif args.mode in ("print", "upload", "select"):
  506.         args.config = load_config(args.config)
  507.         process_target(args)
  508.     elif args.mode in ("init"):
  509.         process_init(args)
  510.        
  511. if __name__ == "__main__":
  512.     args = get_cli()
  513.     configure_logger("octocmd", debug=args.debug)
  514.     process(args)
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement