Repository: ManiacalLabs/BiblioPixel
Pull request #196 (2017-05-24 12:23:58)

Load projects from URLs.
I swore I'd do no new development but, well, I ended up having downtime waiting for something and just started to sketch.

Suspect you will like it, however - everything else flows through, but now you can specify a URL instead of a file path, so you can directly run a project file you checked into github somewhere.

There's one tiny tweak.  You can still enter "raw JSON" for the `bibliopixel run` command - but now you have to use the `-j` flag to do so.  Before the program "guessed" as to whether you were using JSON or not, but I think now there are more options, explicit is better than implicit.

adammhaile: I should scold you for not finishing Wiki changes and instead doing this... but Ooooo shiny!
Since now you could do:
bibliopixel run https://raw.githubusercontent.com/adammhaile/BiblioPixelProjects/master/cube.json
Which is f*&$ing awesome. 

+1

rec: You can even use `bibliopixel run https://github.com/adammhaile/BiblioPixelProjects/master/blob/cube.json` - in other words, the "naïve" URL that you get from github by defailt.



rec: The advantage of your not paying me is that you can't stop me from doing fun things!  :-D

Patch:
diff --git a/bibliopixel/main/run.py b/bibliopixel/main/run.py
index 1acca9b..883b4be 100644
--- a/bibliopixel/main/run.py
+++ b/bibliopixel/main/run.py
@@ -1,5 +1,4 @@
 from bibliopixel.project import project
-from bibliopixel.util import opener
 
 HELP = """
 Run a project description file.
@@ -7,11 +6,15 @@ Run a project description file.
 
 
 def run(args, settings):
-    data = args.name and opener.opener(args.name).read() or {}
-    project.run(data)
+    project.run(args.name, not args.json)
 
 
 def set_parser(parser):
     parser.set_defaults(run=run)
-    parser.description = 'Run specified BiblioPixel config from file'
-    parser.add_argument('name', nargs='?', help='Path to config json file')
+    parser.description = 'Run specified BiblioPixel project from file or URL.'
+    parser.add_argument(
+        'name', nargs='?',
+        help='Path project file - can be a URL or file system location')
+    parser.add_argument(
+        '-j', '--json', action='store_true',
+        help='Enter JSON directly as a command line argument.')
diff --git a/bibliopixel/project/project.py b/bibliopixel/project/project.py
index 5ca9876..dfec014 100644
--- a/bibliopixel/project/project.py
+++ b/bibliopixel/project/project.py
@@ -5,7 +5,7 @@ from .. animation import runner
 from .. import data_maker
 from .. layout import gen_matrix
 from .. led.multimap import MultiMapBuilder
-from .. util import opener
+from .. util import files
 
 
 def make_led(driver, led, maker=None):
@@ -57,15 +57,7 @@ def project_to_runnable(project):
     return project_to_animation(**project).start
 
 
-def run(s):
-    try:
-        project = json.load(opener.opener(s))
-    except:
-        project = json.loads(s)
-
+def run(s, is_filename=True):
+    project = files.read_json(s, is_filename)
     runnable = project_to_runnable(project)
     runnable()
-
-
-if __name__ == '__main__':
-    run(*sys.argv[1:])
diff --git a/bibliopixel/util/datafile.py b/bibliopixel/util/datafile.py
index 63eef4d..72733a8 100644
--- a/bibliopixel/util/datafile.py
+++ b/bibliopixel/util/datafile.py
@@ -1,5 +1,5 @@
 import configparser, json
-from . import opener
+from . import files
 
 
 def load_config(fp):
@@ -20,7 +20,7 @@ def dump_config(data, fp):
 
 
 class DataFile(object):
-    def __init__(self, filename, is_json=None, open=opener.opener):
+    def __init__(self, filename, is_json=None, open=files.opener):
         self.filename = filename
 
         if is_json is None:
diff --git a/bibliopixel/util/files.py b/bibliopixel/util/files.py
new file mode 100644
index 0000000..d41d429
--- /dev/null
+++ b/bibliopixel/util/files.py
@@ -0,0 +1,49 @@
+import json, os.path
+
+GITHUB_BASE = 'github.com'
+GITHUB_RAW = 'raw.githubusercontent.com'
+
+
+def munge_url(url):
+    if not (url.endswith('.json') and ('://%s/' % GITHUB_BASE) in url):
+        return url
+
+    parts = url.split('/')
+    assert parts[0] in 'https:', 'http:'
+    assert not parts[1]
+    assert parts[2] == GITHUB_BASE
+    assert parts[5] == 'blob'
+    parts[2] = GITHUB_RAW
+    parts.pop(5)
+    return '/'.join(parts)
+
+
+def opener(fname, mode='r', *args, **kwds):
+    requests = None
+    if ':' in fname:
+        try:
+            import requests
+        except:
+            pass
+
+    if not requests:
+        return open(os.path.expanduser(fname), mode, *args, **kwds)
+
+    assert not (set(mode) & set('wax')), 'Cannot write to remote file ' + fname
+    url = munge_url(fname)
+    response = requests.get(url)
+    if response.ok:
+        response.read = lambda: response.text
+        return response
+
+    raise IOError('%s: Can\'t open URL %s' % (response.status_code, url))
+
+
+def read_json(s, is_filename=True):
+    data = opener(s).read() if is_filename else s
+
+    try:
+        return json.loads(data)
+    except ValueError as e:
+        e.args = ['%s\nin filename %s' % (e.args[0], s)]
+        raise
diff --git a/bibliopixel/util/opener.py b/bibliopixel/util/opener.py
deleted file mode 100644
index 0f80324..0000000
--- a/bibliopixel/util/opener.py
+++ /dev/null
@@ -1,5 +0,0 @@
-import os.path
-
-
-def opener(fname, *args):
-    return open(os.path.expanduser(fname), *args)


Test patch:
diff --git a/test/project_test.py b/test/project_test.py
index 9b6f86b..1dc0b4d 100644
--- a/test/project_test.py
+++ b/test/project_test.py
@@ -89,7 +89,7 @@ PROJECT_SIM = """
 class ProjectTest(unittest.TestCase):
     @long_test
     def test_simple(self):
-        project.run(PROJECT)
+        project.run(PROJECT, False)
 
     @long_test
     def test_file(self):
@@ -97,12 +97,12 @@ class ProjectTest(unittest.TestCase):
 
     @long_test
     def test_multi(self):
-        project.run(PROJECT_MULTI)
+        project.run(PROJECT_MULTI, False)
 
     @long_test
     def test_shared(self):
-        project.run(PROJECT_SHARED)
+        project.run(PROJECT_SHARED, False)
 
     @long_test
     def test_sim(self):
-        project.run(PROJECT_SIM)
+        project.run(PROJECT_SIM, False)
diff --git a/test/simpixel_project.json b/test/simpixel_project.json
new file mode 100644
index 0000000..d3518c1
--- /dev/null
+++ b/test/simpixel_project.json
@@ -0,0 +1,57 @@
+{
+    "driver": {
+        "typename": "simpixel",
+        "num": 1024
+    },
+
+    "led": {
+        "typename": "matrix",
+        "width": 32,
+        "height": 32
+    },
+
+    "animation": {
+        "typename": "sequence",
+        "animations": [
+            {
+                "animation":
+                "BiblioPixelAnimations.matrix.MatrixRain.MatrixRainBow",
+                "run": {
+                    "amt": 1,
+                    "fps": 20,
+                    "seconds": 8
+                }
+            },
+            {
+                "animation": {
+                    "typename": "BiblioPixelAnimations.matrix.Text.ScrollText",
+                    "xPos": 16,
+                    "font_scale": 2,
+                    "text": "BiblioPixel Demo"
+                },
+                "run": {
+                    "amt": 1,
+                    "fps": 30,
+                    "seconds": 8,
+                    "until_complete": true
+                }
+            },
+            {
+                "animation": "BiblioPixelAnimations.matrix.bloom.Bloom",
+                "run": {
+                    "amt": 3,
+                    "fps": 60,
+                    "seconds": 8
+                }
+            },
+            {
+                "animation": "BiblioPixelAnimations.matrix.circlepop.CirclePop",
+                "run": {
+                    "amt": 1,
+                    "fps": 30,
+                    "seconds": 8
+                }
+            }
+        ]
+    }
+}