├── README.rst ├── bin └── scadmin.py ├── scandium ├── __init__.py ├── core.py └── tpl │ └── project_template │ ├── project_name │ ├── __init__.py │ ├── settings.pyt │ ├── static │ │ └── icons │ │ │ ├── icon.ico │ │ │ ├── icon128x128.png │ │ │ ├── icon16x16.png │ │ │ ├── icon248x248.png │ │ │ └── icon32x32.png │ ├── templates │ │ └── index.html │ └── views.pyt │ ├── runapp.pyt │ └── setup.pyt ├── setup.py └── tests └── __init__.py /README.rst: -------------------------------------------------------------------------------- 1 | ======== 2 | Scandium 3 | ======== 4 | 5 | Introduction 6 | ============ 7 | 8 | Scandium is a replacement for Appcelerator's `Titanium Desktop `__ that enables deployment of Python web applications on the desktop. 9 | 10 | A scandium app is basically just a `Flask `__ app inside a chromeless QtWebKit widget. 11 | 12 | Installation 13 | ============ 14 | 15 | Scandium depends on: 16 | 17 | - twisted 18 | - flask 19 | - qt4reactor 20 | - PySide 21 | - py2exe 22 | 23 | The most hassle-free installation method is to download and install twisted, qt4reactor, PySide and py2exe separately. Setuptools will install Flask for you. 24 | 25 | 26 | 27 | Creating a Project 28 | ================== 29 | 30 | Running ``scadmin.py startproject projectname`` will produce the following in the current directory::: 31 | 32 | - scandium-projectname 33 | | - runapp.py 34 | | - setup.py 35 | \ - projectname 36 | | - __init__.py 37 | | - settings.py 38 | | - static 39 | | | - css 40 | | | - icons 41 | | \ - js 42 | | - templates 43 | | \ - index.html 44 | \ views.py 45 | 46 | Getting Started 47 | =============== 48 | 49 | If you've got your dependencies in order, ``python runapp.py`` will generate a Scandium 'hello world' application from a blank project. 50 | 51 | Add views, templates and static content in the obvious places. 52 | 53 | Configuration 54 | ------------- 55 | 56 | You can override the default configuration by modifying ``settings.py``. Available settings and defaults are as follows: 57 | 58 | DEBUG 59 | ^^^^^ 60 | 61 | Debug flag for the Scandium app. Enables 62 | 63 | Default: ``True`` 64 | 65 | 66 | FLASK_DEBUG 67 | ^^^^^^^^^^^ 68 | 69 | Sets the debug flag on the Flask app. Enables in-browser tracebacks etc. 70 | 71 | Default: ``True`` 72 | 73 | HTTP_PORT 74 | ^^^^^^^^^ 75 | 76 | Port to listen for HTTP connections. 77 | 78 | Default: ``8080`` 79 | 80 | STATIC_RESOURCE 81 | ^^^^^^^^^^^^^^^ 82 | 83 | Defines where to find static resources. Can be a filepath or a tuple of (package, directoryname). You must use the latter format if you want to deploy your application using a compressed or bundled distributable. 84 | The default value for this setting is defined in settings.py because the project name is required. 85 | 86 | Default: ``(projectname, 'static')`` 87 | 88 | TEMPLATE_RESOURCE 89 | ^^^^^^^^^^^^^^^^^ 90 | 91 | Defines where to find templates. Can be a filepath or a tuple of (package, directoryname). You must use the latter format if you want to deploy your application using a compressed or bundled distributable. 92 | The default value for this setting is defined in settings.py because the project name is required. 93 | 94 | Default: ``(projectname, 'templates')`` 95 | 96 | ALLOW_DEFERREDS 97 | ^^^^^^^^^^^^^^^ 98 | 99 | If enabled, views may return twisted deferred objects. The response will be returned to the browser when the deferred fires. 100 | 101 | Default: ``False`` 102 | 103 | 104 | ICON_RESOURCE 105 | ^^^^^^^^^^^^^ 106 | 107 | Defines the image to use for the application icon. Can be a filepath or a tuple of (package, directoryname). You must use the latter format if you want to deploy your application using a compressed or bundled distributable. 108 | 109 | Default: None 110 | 111 | WINDOW_TITLE 112 | ^^^^^^^^^^^^ 113 | 114 | Title to be displayed in the browser window. Adding a ```` tag to your HTML page won't affect this. 115 | 116 | Default: ``"Scandium Browser"`` 117 | 118 | WINDOW_GEOMETRY 119 | ^^^^^^^^^^^^^^^ 120 | 121 | Size and position for the application window, specified as ``(x, y, width, height)``. 122 | 123 | Default: ``(100, 100, 800, 500)`` 124 | 125 | 126 | Custom Settings 127 | --------------- 128 | 129 | It is possible to define custom settings in ``settings.py`` for use in your web application code. Just do ``from projectname import sc`` and reference the settings using ``sc.conf.SETTING_NAME``. 130 | 131 | 132 | Building with py2exe 133 | ==================== 134 | 135 | The template project layout includes a ``setup.py`` file that will generate an executable using ``py2exe`` when invoked. Running ``python setup.py py2exe`` will generate a ``dist`` directory containing::: 136 | 137 | - projectname.exe 138 | - QtGui4.dll 139 | - QtNetwork4.dll 140 | - QtCore4.dll 141 | \ - imageformats 142 | | - qgif4.dll 143 | | - qjpeg4.dll 144 | | - qsvg4.dll 145 | ... 146 | 147 | The Qt4 DLLs and ``imageformats`` formats directory need to be there for image processing support. I can't figure out how to embed them inside the executable, so you need to distribute this whole directory. 148 | 149 | Target machines must have the Microsoft Visual C++ Redistributable installed, available from http://www.microsoft.com/en-us/download/confirmation.aspx?id=29 150 | -------------------------------------------------------------------------------- /bin/scadmin.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import re 4 | import os 5 | import sys 6 | import stat 7 | import errno 8 | import shutil 9 | 10 | from optparse import OptionParser 11 | 12 | import scandium 13 | 14 | 15 | def make_writeable(filename): 16 | """ 17 | Make sure that the file is writeable. 18 | Useful if our source is read-only. 19 | """ 20 | if sys.platform.startswith('java'): 21 | # On Jython there is no os.access() 22 | return 23 | if not os.access(filename, os.W_OK): 24 | st = os.stat(filename) 25 | new_permissions = stat.S_IMODE(st.st_mode) | stat.S_IWUSR 26 | os.chmod(filename, new_permissions) 27 | 28 | 29 | def handle_template(template, subdir): 30 | """ 31 | Determines where the app or project templates are. 32 | Use scandium.__path__[0] as the default because we don't 33 | know into which directory Scandium has been installed. 34 | """ 35 | if template is None: 36 | return os.path.join(scandium.__path__[0], 'tpl', subdir) 37 | else: 38 | if template.startswith('file://'): 39 | template = template[7:] 40 | expanded_template = os.path.expanduser(template) 41 | expanded_template = os.path.normpath(expanded_template) 42 | if os.path.isdir(expanded_template): 43 | return expanded_template 44 | raise Exception("couldn't handle project template %s." % template) 45 | 46 | 47 | def create(options, project_name, target=None): 48 | 49 | # If it's not a valid directory name. 50 | if not re.search(r'^[_a-zA-Z]\w*$', project_name): 51 | # Provide a smart error message, depending on the error. 52 | if not re.search(r'^[_a-zA-Z]', project_name): 53 | message = ('make sure the name begins ' 54 | 'with a letter or underscore') 55 | else: 56 | message = 'use only numbers, letters and underscores' 57 | raise Exception("%r is not a valid project name. Please %s." % 58 | (project_name, message)) 59 | 60 | # if some directory is given, make sure it's nicely expanded 61 | if target is None: 62 | top_dir = os.path.join(os.getcwd(), "scandium-%s" % project_name) 63 | try: 64 | os.makedirs(top_dir) 65 | except OSError, e: 66 | if e.errno == errno.EEXIST: 67 | message = "'%s' already exists" % top_dir 68 | else: 69 | message = e 70 | raise Exception(message) 71 | else: 72 | top_dir = os.path.abspath(os.path.expanduser(target)) 73 | if not os.path.exists(top_dir): 74 | raise Exception("Destination directory '%s' does not " 75 | "exist, please create it first." % top_dir) 76 | 77 | template_dir = handle_template(None, "project_template") 78 | prefix_length = len(template_dir) + 1 79 | 80 | for root, dirs, files in os.walk(template_dir): 81 | path_rest = root[prefix_length:] 82 | relative_dir = path_rest.replace("project_name", project_name) 83 | if relative_dir: 84 | target_dir = os.path.join(top_dir, relative_dir) 85 | if not os.path.exists(target_dir): 86 | os.mkdir(target_dir) 87 | 88 | for dirname in dirs[:]: 89 | if dirname.startswith('.'): 90 | dirs.remove(dirname) 91 | 92 | for filename in files: 93 | if filename.endswith(('.pyo', '.pyc', '.py.class')): 94 | # Ignore some files as they cause various breakages. 95 | continue 96 | old_path = os.path.join(root, filename) 97 | new_path = os.path.join(top_dir, relative_dir, \ 98 | filename.replace("project_name", \ 99 | project_name)) 100 | if os.path.exists(new_path): 101 | raise Exception("%s already exists, overlaying a " 102 | "project or app into an existing " 103 | "directory won't replace conflicting " 104 | "files" % new_path) 105 | 106 | # Render .pyt files using string subsitutions 107 | context = dict({'project_name': project_name}, **options.__dict__) 108 | 109 | with open(old_path, 'rb') as template_file: 110 | content = template_file.read() 111 | if filename.endswith("pyt"): 112 | while re.search("{{.*}}", content): 113 | sub = re.search("{{(.*?)}}", content).group(1) 114 | content = re.sub("{{%s}}" % sub, context.get(sub, ""), \ 115 | content) 116 | new_path = new_path[:-1] # drop the 't' from the file ext 117 | with open(new_path, 'wb') as new_file: 118 | new_file.write(content) 119 | 120 | try: 121 | shutil.copymode(old_path, new_path) 122 | make_writeable(new_path) 123 | except OSError: 124 | print "Notice: Couldn't set permission bits on %s. "\ 125 | "You're probably using an uncommon filesystem setup. "\ 126 | "No problem.\n" % new_path 127 | 128 | 129 | if __name__ == "__main__": 130 | 131 | usage = "%s startproject [options] []" % \ 132 | sys.argv[0] 133 | parser = OptionParser(usage=usage) 134 | parser.add_option("-a", "--author", 135 | dest="author", 136 | default="", 137 | help="Application Author") 138 | parser.add_option("-e", "--email", 139 | dest="email", 140 | default="", 141 | help="Email Address") 142 | parser.add_option("-V", "--version", 143 | dest="version", 144 | default="", 145 | help="Application Version") 146 | parser.add_option("-k", "--keywords", 147 | dest="keywords", 148 | default="", 149 | help="Application Keywords") 150 | parser.add_option("-d", "--description", 151 | dest="description", 152 | default="", 153 | help="Application Description") 154 | parser.add_option("-l", "--license", 155 | dest="license", 156 | default="", 157 | help="Application License") 158 | 159 | options, args = parser.parse_args(sys.argv[1:]) 160 | if args[0] != "startproject" or len(args) not in (2, 3): 161 | parser.error("Unrecognized input.") 162 | 163 | create(options, *args[1:]) -------------------------------------------------------------------------------- /scandium/__init__.py: -------------------------------------------------------------------------------- 1 | from PySide import QtGui 2 | app = QtGui.QApplication([]) 3 | import qt4reactor 4 | qt4reactor.install() 5 | 6 | __version__ = "0.0.0" 7 | 8 | 9 | def Scandium(*args, **kwargs): 10 | from .core import Harness 11 | return Harness(*args, **kwargs) -------------------------------------------------------------------------------- /scandium/core.py: -------------------------------------------------------------------------------- 1 | from twisted.internet import reactor, defer 2 | from twisted.python.failure import Failure 3 | from twisted.web import server, resource 4 | from twisted.web.wsgi import WSGIResource 5 | 6 | from werkzeug.wsgi import SharedDataMiddleware 7 | from jinja2.loaders import FileSystemLoader, PackageLoader 8 | from flask import Flask 9 | 10 | from PySide import QtGui 11 | from PySide.QtWebKit import QWebView, QWebSettings 12 | from PySide.QtCore import QUrl, QByteArray 13 | 14 | import pkgutil 15 | 16 | 17 | # See http://stackoverflow.com/questions/6690639/how-to-configure-static-serving-in-twisted-with-django 18 | class SharedRoot(resource.Resource): 19 | "Root resource that combines the two sites/entry points" 20 | WSGI = None 21 | 22 | def getChild(self, child, request): 23 | request.prepath.pop() 24 | request.postpath.insert(0, child) 25 | return self.WSGI 26 | 27 | def render(self, request): 28 | return self.WSGI.render(request) 29 | 30 | 31 | class Browser(QWebView): 32 | "Web browser" 33 | def __init__(self, url, title=None, geometry=None, icon=None): 34 | super(Browser, self).__init__() 35 | 36 | self.setGeometry(*geometry) 37 | self.setWindowTitle(title) 38 | 39 | if icon: 40 | pixmap = QtGui.QPixmap() 41 | if type(icon) == tuple: # package, not filepath 42 | img_data = pkgutil.get_data(*icon) 43 | else: 44 | with open(icon) as fh: 45 | img_data = fh.read() 46 | pixmap.loadFromData(QByteArray(img_data)) 47 | self.setWindowIcon(QtGui.QIcon(pixmap)) 48 | 49 | self.load(QUrl(url)) 50 | 51 | def closeEvent(self, event): 52 | event.accept() 53 | reactor.stop() 54 | 55 | 56 | class Config(object): 57 | """ 58 | Scandium Config object, handles configuration defaults and customization. 59 | """ 60 | DEBUG = True 61 | FLASK_DEBUG = True 62 | HTTP_PORT = 8080 63 | STATIC_RESOURCE = None 64 | TEMPLATE_RESOURCE = None 65 | ALLOW_DEFERREDS = True 66 | ICON_RESOURCE = None 67 | WINDOW_TITLE = "Scandium Browser" 68 | WINDOW_GEOMETRY = (100, 100, 800, 500) 69 | 70 | def update(self, settings_module): 71 | for setting in dir(settings_module): 72 | if setting == setting.upper(): 73 | setattr(self, setting, getattr(settings_module, setting)) 74 | 75 | 76 | class Harness(): 77 | """ 78 | Main Scandium object 79 | """ 80 | def __init__(self): 81 | self.conf = Config() 82 | 83 | def start(self): 84 | root = SharedRoot() 85 | root.WSGI = WSGIResource(reactor, reactor.getThreadPool(), self.app) 86 | self.webserver = server.Site(root) 87 | 88 | reactor.listenTCP(self.conf.HTTP_PORT, self.webserver) 89 | reactor.callLater(0, self.browser.show) 90 | reactor.run() 91 | 92 | @property 93 | def app(self): 94 | if not hasattr(self, '_app'): 95 | self._app = self._create_app() 96 | return self._app 97 | 98 | @property 99 | def browser(self): 100 | if not hasattr(self, '_browser'): 101 | self._browser = self._create_browser() 102 | return self._browser 103 | 104 | def _create_app(self): 105 | app = Flask(__name__) 106 | app.debug = self.conf.FLASK_DEBUG 107 | 108 | if not self.conf.STATIC_RESOURCE: 109 | raise Exception('STATIC_RESOURCE setting not configured.') 110 | if not self.conf.TEMPLATE_RESOURCE: 111 | raise Exception('TEMPLATE_RESOURCE setting not configured.') 112 | 113 | app.wsgi_app = SharedDataMiddleware(app.wsgi_app, { 114 | '/': self.conf.STATIC_RESOURCE 115 | }) 116 | if type(self.conf.TEMPLATE_RESOURCE) == tuple: # package, not filepath 117 | app.jinja_loader = PackageLoader(*self.conf.TEMPLATE_RESOURCE) 118 | else: 119 | app.jinja_loader = FileSystemLoader(self.conf.TEMPLATE_RESOURCE) 120 | if self.conf.ALLOW_DEFERREDS: 121 | self._enable_deferreds(app) 122 | return app 123 | 124 | def _create_browser(self): 125 | browser = Browser('http://localhost:%d/' % self.conf.HTTP_PORT, \ 126 | icon=self.conf.ICON_RESOURCE, 127 | title=self.conf.WINDOW_TITLE, 128 | geometry=self.conf.WINDOW_GEOMETRY) 129 | 130 | devextras = QWebSettings.WebAttribute.DeveloperExtrasEnabled 131 | browser.settings().setAttribute(devextras, self.conf.DEBUG) 132 | return browser 133 | 134 | def _enable_deferreds(self, app): 135 | import Queue 136 | import functools 137 | 138 | #From the comments here: 139 | #http://www.saltycrane.com/blog/2008/10/cant-block-deferred-twisted 140 | def block_on(d): 141 | "Block until a deferred fires" 142 | q = Queue.Queue() 143 | d.addBoth(q.put) 144 | ret = q.get() 145 | if isinstance(ret, Failure): 146 | ret.raiseException() 147 | else: 148 | return ret 149 | 150 | def routeMaybeDeferred(rule, **options): 151 | """ 152 | A routing method that allows the view function to return a 153 | deferred, and if so blocks for it to complete. 154 | 155 | This is a hack: we should really be using something like 156 | https://github.com/twisted/klein, but klein won't work with the 157 | Qt reactor. 158 | """ 159 | def decorator(f): 160 | blocking = lambda func=None, *args, **kw: \ 161 | block_on(defer.maybeDeferred(func, *args, **kw)) 162 | fn = functools.partial(blocking, func=f) 163 | fn.__name__ = f.__name__ # partials don't inherit __name__ 164 | endpoint = options.pop('endpoint', None) 165 | app.add_url_rule(rule, endpoint, fn, **options) 166 | return fn 167 | return decorator 168 | app.route = routeMaybeDeferred 169 | -------------------------------------------------------------------------------- /scandium/tpl/project_template/project_name/__init__.py: -------------------------------------------------------------------------------- 1 | from scandium import Scandium 2 | 3 | # Initialization 4 | sc = Scandium() 5 | 6 | # Configuration 7 | import settings 8 | sc.conf.update(settings) -------------------------------------------------------------------------------- /scandium/tpl/project_template/project_name/settings.pyt: -------------------------------------------------------------------------------- 1 | # Put configuration options in here 2 | 3 | STATIC_RESOURCE = ('{{project_name}}', 'static') # Default static location 4 | TEMPLATE_RESOURCE = ('{{project_name}}', 'templates') # Default template location -------------------------------------------------------------------------------- /scandium/tpl/project_template/project_name/static/icons/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattbennett/scandium/837184f14d03c27ddc3e2d8df5ab7cf614ba5e74/scandium/tpl/project_template/project_name/static/icons/icon.ico -------------------------------------------------------------------------------- /scandium/tpl/project_template/project_name/static/icons/icon128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattbennett/scandium/837184f14d03c27ddc3e2d8df5ab7cf614ba5e74/scandium/tpl/project_template/project_name/static/icons/icon128x128.png -------------------------------------------------------------------------------- /scandium/tpl/project_template/project_name/static/icons/icon16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattbennett/scandium/837184f14d03c27ddc3e2d8df5ab7cf614ba5e74/scandium/tpl/project_template/project_name/static/icons/icon16x16.png -------------------------------------------------------------------------------- /scandium/tpl/project_template/project_name/static/icons/icon248x248.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattbennett/scandium/837184f14d03c27ddc3e2d8df5ab7cf614ba5e74/scandium/tpl/project_template/project_name/static/icons/icon248x248.png -------------------------------------------------------------------------------- /scandium/tpl/project_template/project_name/static/icons/icon32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattbennett/scandium/837184f14d03c27ddc3e2d8df5ab7cf614ba5e74/scandium/tpl/project_template/project_name/static/icons/icon32x32.png -------------------------------------------------------------------------------- /scandium/tpl/project_template/project_name/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Hello Scandium! 4 | 5 | 6 | Hello Scandium! 7 | 8 | -------------------------------------------------------------------------------- /scandium/tpl/project_template/project_name/views.pyt: -------------------------------------------------------------------------------- 1 | from flask import render_template 2 | from {{project_name}} import sc 3 | 4 | 5 | @sc.app.route("/") 6 | def index(): 7 | return render_template('index.html') -------------------------------------------------------------------------------- /scandium/tpl/project_template/runapp.pyt: -------------------------------------------------------------------------------- 1 | from {{project_name}} import sc 2 | from {{project_name}} import views 3 | sc.start() -------------------------------------------------------------------------------- /scandium/tpl/project_template/setup.pyt: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | from py2exe.build_exe import py2exe as build_exe 3 | from distutils.sysconfig import get_python_lib 4 | import fnmatch 5 | import py2exe 6 | import sys 7 | import os 8 | 9 | # If run without args, build executables, in quiet mode. 10 | if len(sys.argv) == 1: 11 | sys.argv.append("py2exe") 12 | sys.argv.append("-q") 13 | 14 | ################################################################ 15 | # Customize these variables 16 | 17 | NAME = "{{project_name}}" 18 | VERSION = "{{version}}" 19 | DESCRIPTION = "{{description}}" 20 | COMPANY_NAME = "{{company_name}}" 21 | LICENSE = "{{license}}" 22 | 23 | # Fiddle with these variables if you use Python modules that 24 | # py2exe can't find, or you change the location of static 25 | # and template data. 26 | 27 | INCLUDES = ['jinja2.ext', 'PySide.QtNetwork'] 28 | EXCLUDES = ["Tkconstants", "Tkinter", "tcl"] 29 | PACKAGES = find_packages(exclude=("tests",)) 30 | PACKAGE_DATA_DIRS = ('static', 'templates') 31 | 32 | 33 | ################################################################ 34 | # A program using PySide 35 | 36 | # The manifest will be inserted as resource into {{project_name}}.exe. This 37 | # gives the controls the Windows XP appearance (if run on XP ;-) and 38 | # ensures the Visual C++ Redistributable Package DLLs get found. 39 | # 40 | # Another option would be to store it in a file named 41 | # {{project_name}}.exe.manifest, and copy it with the data_files option into 42 | # the dist-dir. 43 | # 44 | manifest_template = ''' 45 | 46 | 47 | 53 | {{project_name}} Program 54 | 55 | 56 | 64 | 65 | 66 | 67 | 68 | 76 | 77 | 78 | 79 | ''' 80 | 81 | RT_MANIFEST = 24 82 | 83 | 84 | # Extention to embed package_data in py2exe's distributable 85 | # See: http://crazedmonkey.com/blog/python/pkg_resources-with-py2exe.html 86 | class MediaCollector(build_exe): 87 | def copy_extensions(self, extensions): 88 | build_exe.copy_extensions(self, extensions) 89 | 90 | def collect_media(path): 91 | for root, _, filenames in os.walk(path): 92 | for fname in fnmatch.filter(filenames, '*'): 93 | parent = os.path.join(self.collect_dir, root) 94 | if not os.path.exists(parent): 95 | self.mkpath(parent) 96 | self.copy_file(os.path.join(root, fname), \ 97 | os.path.join(parent, fname)) 98 | self.compiled_files.append(os.path.join(root, fname)) 99 | for dname in PACKAGE_DATA_DIRS: 100 | collect_media(os.path.join(NAME, dname)) 101 | collect_media(os.path.join(NAME, dname)) 102 | 103 | 104 | # Create Windows Application target 105 | # 106 | class Target: 107 | def __init__(self, **kw): 108 | self.__dict__.update(kw) 109 | # for the versioninfo resources 110 | self.version = VERSION 111 | self.company_name = COMPANY_NAME 112 | self.description = DESCRIPTION 113 | self.copyright = LICENSE 114 | self.name = NAME 115 | 116 | app = Target( 117 | # what to build 118 | script = "runapp.py", 119 | other_resources = [(RT_MANIFEST, 1, manifest_template % dict(prog=NAME))], 120 | icon_resources = [(1, "%s/static/icons/icon.ico" % NAME)], 121 | dest_base = NAME 122 | ) 123 | 124 | 125 | # Qt4 uses plugins for image processing. These cannot be bundled into the 126 | # executable, so we copy them into the application directory, along with 127 | # the Qt DLL files, which we then exclude from the bundle. 128 | path = os.path.join(get_python_lib(), 'PySide', 'plugins', 'imageformats') 129 | imageformats = [] 130 | for dll in os.listdir(path): 131 | imageformats.append(os.path.join(path, dll)) 132 | 133 | path = os.path.join(get_python_lib(), 'PySide') 134 | qt = [] 135 | for dll in ("QtCore4.dll", "QtGui4.dll", "QtNetwork4.dll"): 136 | qt.append(os.path.join(path, dll)) 137 | 138 | DATA_FILES = [('imageformats', imageformats), ('', qt)] 139 | 140 | 141 | ################################################################ 142 | 143 | setup( 144 | cmdclass = {'py2exe': MediaCollector}, 145 | data_files = DATA_FILES, 146 | include_package_data=True, 147 | options = {"py2exe": {"compressed": 1, 148 | "optimize": 1, 149 | "ascii": 0, 150 | "bundle_files": 1, 151 | "packages": PACKAGES, 152 | "includes": INCLUDES, 153 | "excludes": EXCLUDES, 154 | # exclude the Qt4 DLLs to ensure the data_files version gets used, otherwise image processing will fail 155 | "dll_excludes": ['msvcp90.dll', 'w9xpopen.exe', "QtCore4.dll", "QtGui4.dll", "QtNetwork4.dll"]}}, 156 | zipfile = None, 157 | windows = [app], 158 | ) -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | from setuptools import setup, find_packages 3 | 4 | 5 | def find_package_data(package, *paths): 6 | data = {package: []} 7 | for path in paths: 8 | for root, _, filenames in os.walk(os.path.join(package, path)): 9 | for fname in filenames: 10 | pkg_path = os.path.join(os.path.relpath(root, package), fname) 11 | data[package].append(pkg_path) 12 | return data 13 | 14 | NAME = "scandium" 15 | VERSION = '0.0.1' 16 | PACKAGES = find_packages(exclude="tests") 17 | PACKAGE_DATA_DIRS = ('tpl',) 18 | PACKAGE_DATA = find_package_data(NAME, *PACKAGE_DATA_DIRS) 19 | 20 | AUTHOR = "Matt Bennett" 21 | EMAIL = "matt.bennett@inmarsat.com" 22 | KEYWORDS = "scandium web desktop application titanium" 23 | DESCRIPTION = "Titanium Desktop replacement. A toolkit for transformation of" \ 24 | " python webapps into desktop applications under QtWebKit." 25 | LICENSE = "BSD" 26 | 27 | 28 | setup( 29 | name = NAME, 30 | version = VERSION, 31 | author = AUTHOR, 32 | author_email = EMAIL, 33 | description = DESCRIPTION, 34 | license = LICENSE, 35 | keywords = KEYWORDS, 36 | packages = PACKAGES, 37 | package_data = PACKAGE_DATA, 38 | include_package_data = True, 39 | zip_safe = False, 40 | classifiers = [ 41 | # 42 | ], 43 | dependency_links = [ 44 | # setuptools will install from list link, but do so as a zipped egg. 45 | # py2exe can't import qt4reactor from a zip, so building your scandium 46 | # into an exe will fail. Installing qt4reactor manually is recommended. 47 | #"https://github.com/ghtdak/qtreactor/zipball/master#egg=qt4reactor-1.0" 48 | ], 49 | scripts = ['bin/scadmin.py'], 50 | install_requires = [ 51 | "twisted", # manual install recommended 52 | "qt4reactor>=1.0", # manual install recommended 53 | "flask", 54 | "PySide", # manual install recommended 55 | ] 56 | ) -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattbennett/scandium/837184f14d03c27ddc3e2d8df5ab7cf614ba5e74/tests/__init__.py --------------------------------------------------------------------------------