add sqlalchemy model + put create module for webapp
authorymh <ymh.work@gmail.com>
Tue, 14 Oct 2014 05:07:37 +0200
changeset 22 986ee928a866
parent 21 89d235bcbbf3
child 23 16a1925df2df
add sqlalchemy model + put create module for webapp
.hgignore
annot-server/__init__.py
annot-server/database.py
annot-server/models.py
annot-server/server.tac
annot-server/templates/annot.html
annot-server/templates/index.html
annot-server/utils.py
annot-server/webapp.py
annot-server/webapp/__init__.py
annot-server/webapp/templates/annot.html
annot-server/webapp/templates/annotationclient.html
annot-server/webapp/templates/index.html
annot-server/webapp/views.py
client/gulpfile.js
requirements.txt
--- a/.hgignore	Tue Oct 14 18:00:13 2014 +0200
+++ b/.hgignore	Tue Oct 14 05:07:37 2014 +0200
@@ -1,6 +1,7 @@
 syntax: regexp
 
-^utils/pianoroll_test.*
+^utils/pianoroll_test
+^utils/pianoroll_sample_
 ^client/bower_components$
 ^client/build$
 ^client/node_modules$
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/annot-server/__init__.py	Tue Oct 14 05:07:37 2014 +0200
@@ -0,0 +1,4 @@
+#
+# See LICENCE for detail
+# Copyright (c) 2014 IRI
+#
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/annot-server/database.py	Tue Oct 14 05:07:37 2014 +0200
@@ -0,0 +1,54 @@
+#
+# See LICENCE for detail
+# Copyright (c) 2014 IRI
+#
+
+from sqlalchemy import create_engine, MetaData
+from sqlalchemy.orm import scoped_session, sessionmaker
+from sqlalchemy.ext.declarative import declarative_base
+
+import psycopg2.extras
+
+from twisted.python import log
+
+from txpostgres.txpostgres import Connection, ConnectionPool
+
+import config
+
+
+engine = create_engine(config.CONN_STR, convert_unicode=True)
+db_session = scoped_session(sessionmaker(autocommit=False,
+                                         autoflush=False,
+                                         bind=engine))
+
+Base = declarative_base()
+Base.query = db_session.query_property()
+
+def init_db():
+    # import all modules here that might define models so that
+    # they will be registered properly on the metadata.  Otherwise
+    # you will have to import them first before calling init_db()
+    import models
+    Base.metadata.create_all(bind=engine)
+
+
+class DictConnection(Connection):
+
+    @staticmethod
+    def connectionFactory(*args, **kwargs):
+        kwargs['connection_factory'] = psycopg2.extras.DictConnection
+        return psycopg2.connect(*args, **kwargs)
+
+
+class DictConnectionPool(ConnectionPool):
+    connectionFactory = DictConnection
+
+
+def create_connection_pool(conn_string):
+
+    created_connection_pool = DictConnectionPool(None, conn_string)
+
+    d = created_connection_pool.start()
+    d.addErrback(log.err)
+
+    return created_connection_pool, d
--- a/annot-server/models.py	Tue Oct 14 18:00:13 2014 +0200
+++ b/annot-server/models.py	Tue Oct 14 05:07:37 2014 +0200
@@ -8,17 +8,39 @@
 import json
 import uuid
 
-def get_table_create_stmt():
-    return (
-        "CREATE TABLE IF NOT EXISTS annotations ( "
-        "id serial PRIMARY KEY, "
-        "uuid uuid UNIQUE, "
-        "created timestamp default (now() at time zone 'utc') NOT NULL, "
-        "ts timestamptz NOT NULL, "
-        "event varchar(255) NOT NULL, "
-        "channel varchar(255) NOT NULL, "
-        "content json);"
-    )
+from sqlalchemy import Column, Integer, String, DateTime, Table, Index, text
+from sqlalchemy.sql import func
+from sqlalchemy.dialects.postgresql import UUID, JSON
+
+from database import Base, engine
+
+#def get_table_create_stmt():
+#    return (
+#        "CREATE TABLE IF NOT EXISTS annotations ( "
+#        "id serial PRIMARY KEY, "
+#        "uuid uuid UNIQUE, "
+#        "created timestamp default (now() at time zone 'utc') NOT NULL, "
+#        "ts timestamptz NOT NULL, "
+#        "event varchar(255) NOT NULL, "
+#        "channel varchar(255) NOT NULL, "
+#        "content json);"
+#    )
+
+class Annotation(Base):
+    __tablename__ = 'annotations'
+
+    id = Column(Integer, primary_key=True, nullable=False)
+    uuid = Column(UUID, unique=True, nullable=False)
+    created = Column(DateTime, nullable=False, server_default=text("(now() at time zone 'utc')") )
+    ts = Column(DateTime(timezone=True), nullable=False)
+    event = Column(String(255), nullable=False)
+    channel = Column(String(255), nullable=False)
+    content = Column(JSON)
+
+Index('idx_event', Annotation.event)
+Index('idx_channel', Annotation.channel)
+Index('idx_ts', Annotation.ts)
+
 
 def insert_annot_async(params, conn):
 
@@ -31,7 +53,9 @@
     if 'ts' not in params:
         params['ts'] = datetime.utcnow()
 
-    defer = conn.runOperation("INSERT INTO annotations (uuid, ts, event, channel, content) VALUES (%(uuid)s, %(ts)s, %(event)s, %(channel)s, %(content)s)", params)
-    defer.addCallback(lambda _: params)
+    stmt = Annotation.__table__.insert().values(**params).compile(engine)
+
+    defer = conn.runOperation(stmt.string, stmt.params)
+    defer.addCallback(lambda _: stmt.params)
 
     return defer
--- a/annot-server/server.tac	Tue Oct 14 18:00:13 2014 +0200
+++ b/annot-server/server.tac	Tue Oct 14 05:07:37 2014 +0200
@@ -10,21 +10,22 @@
 import psycopg2.extras
 
 from twisted.application import service
+from twisted.internet import reactor
 from twisted.python import log
 from txpostgres import txpostgres
 
 from annotserver import make_service
 import config
-from models import get_table_create_stmt
-from utils import create_connection_pool
+from database import create_connection_pool, init_db, db_session
 
 
 psycopg2.extras.register_uuid()
 
 
+init_db()
 conn, d = create_connection_pool(config.CONN_STR)
 #to do treat error
-d.addCallback(lambda _: conn.runOperation(get_table_create_stmt()))
+#d.addCallback(lambda _: conn.runOperation(get_table_create_stmt()))
 
 #TOODO Log
 #log.startLogging(sys.stdout)
@@ -34,3 +35,5 @@
 
 annotserver = make_service(conn)
 annotserver.setServiceParent(service.IServiceCollection(application))
+
+reactor.addSystemEventTrigger('before', 'shutdown', lambda: db_session.remove())
--- a/annot-server/templates/annot.html	Tue Oct 14 18:00:13 2014 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,75 +0,0 @@
-<!DOCTYPE html>
-<html>
-   <head>
-      <link rel="icon" href="data:;base64,=">
-      <script type="text/javascript">
-         var sock = null;
-         var ellog = null;
-
-         window.onload = function() {
-
-            ellog = document.getElementById('log');
-
-            var wsuri;
-            if (window.location.protocol === "file:") {
-               wsuri = "ws://127.0.0.1:8090/annot";
-            } else {
-               wsuri = "ws://" + window.location.hostname + ":8090/annot";
-            }
-            wsuri = wsuri + "?event=test"
-
-            if ("WebSocket" in window) {
-               sock = new WebSocket(wsuri);
-            } else if ("MozWebSocket" in window) {
-               sock = new MozWebSocket(wsuri);
-            } else {
-               log("Browser does not support WebSocket!");
-               window.location = "http://autobahn.ws/unsupportedbrowser";
-            }
-
-            if (sock) {
-               sock.onopen = function() {
-                  log("Connected to " + wsuri);
-               }
-
-               sock.onclose = function(e) {
-                  log("Connection closed (wasClean = " + e.wasClean + ", code = " + e.code + ", reason = '" + e.reason + "')");
-                  sock = null;
-               }
-
-               sock.onmessage = function(e) {
-                  log("Got message: " + e.data);
-               }
-            }
-         };
-
-         function send() {
-            var msg = document.getElementById('message').value;
-            if (sock) {
-                new_annot = {
-                   categories : [msg],
-                   user : "admin"
-               }
-               sock.send(JSON.stringify(new_annot));
-               log("Sent: " + JSON.stringify(new_annot));
-            } else {
-               log("Not connected.");
-            }
-         };
-
-         function log(m) {
-            ellog.innerHTML += m + '\n';
-            ellog.scrollTop = ellog.scrollHeight;
-         };
-      </script>
-   </head>
-   <body>
-      <h1>OSC websocket Test</h1>
-      <noscript>You must enable JavaScript</noscript>
-      <form>
-         <p>Message: <input id="message" type="text" size="50" maxlength="50" value="Hello, world!"></p>
-      </form>
-      <button onclick='send();'>Send Message</button>
-      <pre id="log" style="height: 20em; overflow-y: scroll; background-color: #faa;"></pre>
-   </body>
-</html>
--- a/annot-server/templates/index.html	Tue Oct 14 18:00:13 2014 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,56 +0,0 @@
-<!DOCTYPE html>
-<html>
-   <head>
-      <link rel="icon" href="data:;base64,=">
-      <script type="text/javascript">
-         var sock = null;
-         var ellog = null;
-
-         window.onload = function() {
-
-            ellog = document.getElementById('log');
-
-            var wsuri;
-            if (window.location.protocol === "file:") {
-               wsuri = "ws://127.0.0.1:8090/broadcast";
-            } else {
-               wsuri = "ws://" + window.location.hostname + ":8090/broadcast";
-            }
-            if ("WebSocket" in window) {
-               sock = new WebSocket(wsuri);
-            } else if ("MozWebSocket" in window) {
-               sock = new MozWebSocket(wsuri);
-            } else {
-               log("Browser does not support WebSocket!");
-               window.location = "http://autobahn.ws/unsupportedbrowser";
-            }
-
-            if (sock) {
-               sock.onopen = function() {
-                  log("Connected to " + wsuri);
-               }
-
-               sock.onclose = function(e) {
-                  log("Connection closed (wasClean = " + e.wasClean + ", code = " + e.code + ", reason = '" + e.reason + "')");
-                  sock = null;
-               }
-
-               sock.onmessage = function(e) {
-                  log("Got message: " + e.data);
-               }
-            }
-         };
-
-         function log(m) {
-            ellog.innerHTML += m + '\n';
-            ellog.scrollTop = ellog.scrollHeight;
-         };
-      </script>
-      <link rel="stylesheet" type="text/css" href="/static/css/base.css">
-   </head>
-   <body>
-      <h1>OSC websocket Test</h1>
-      <noscript>You must enable JavaScript</noscript>
-      <pre id="log"></pre>
-   </body>
-</html>
--- a/annot-server/utils.py	Tue Oct 14 18:00:13 2014 +0200
+++ b/annot-server/utils.py	Tue Oct 14 05:07:37 2014 +0200
@@ -4,33 +4,6 @@
 # Copyright (c) 2014 IRI
 #
 
-import psycopg2.extras
-
-from twisted.python import log
-
-from txpostgres.txpostgres import Connection, ConnectionPool
-
 
 PIANOROLL_CHANNEL = 'PIANOROLL'
 ANNOTATION_CHANNEL = 'ANNOT'
-
-class DictConnection(Connection):
-
-    @staticmethod
-    def connectionFactory(*args, **kwargs):
-        kwargs['connection_factory'] = psycopg2.extras.DictConnection
-        return psycopg2.connect(*args, **kwargs)
-
-
-class DictConnectionPool(ConnectionPool):
-    connectionFactory = DictConnection
-
-
-def create_connection_pool(conn_string):
-
-    created_connection_pool = DictConnectionPool(None, conn_string)
-
-    d = created_connection_pool.start()
-    d.addErrback(log.err)
-
-    return created_connection_pool, d
--- a/annot-server/webapp.py	Tue Oct 14 18:00:13 2014 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,30 +0,0 @@
-
-#
-# See LICENCE for detail
-# Copyright (c) 2014 IRI
-#
-
-import uuid
-
-from flask import Flask, render_template
-
-import config
-
-app = Flask(__name__)
-app.secret_key = str(uuid.uuid4())
-app.config.from_object(config)
-
-@app.route('/')
-def page_home():
-    return render_template('index.html')
-    #return render_template('annotationclient.html', logging=True)
-
-
-@app.route('/annot')
-def page_annot():
-    return render_template('annot.html')
-
-
-@app.route('/annotationclient')
-def page_annotationclient():
-    return render_template('annotationclient.html', logging=True)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/annot-server/webapp/__init__.py	Tue Oct 14 05:07:37 2014 +0200
@@ -0,0 +1,23 @@
+#
+# See LICENCE for detail
+# Copyright (c) 2014 IRI
+#
+
+import uuid
+import json
+
+from flask import Flask
+
+import config
+import database
+
+app = Flask(__name__)
+app.secret_key = str(uuid.uuid4())
+app.debug = config.DEBUG
+app.config.from_object(config)
+
+import webapp.views
+
+@app.teardown_appcontext
+def shutdown_session(exception=None):
+    database.db_session.remove()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/annot-server/webapp/templates/annot.html	Tue Oct 14 05:07:37 2014 +0200
@@ -0,0 +1,75 @@
+<!DOCTYPE html>
+<html>
+   <head>
+      <link rel="icon" href="data:;base64,=">
+      <script type="text/javascript">
+         var sock = null;
+         var ellog = null;
+
+         window.onload = function() {
+
+            ellog = document.getElementById('log');
+
+            var wsuri;
+            if (window.location.protocol === "file:") {
+               wsuri = "ws://127.0.0.1:8090/annot";
+            } else {
+               wsuri = "ws://" + window.location.hostname + ":8090/annot";
+            }
+            wsuri = wsuri + "?event=test"
+
+            if ("WebSocket" in window) {
+               sock = new WebSocket(wsuri);
+            } else if ("MozWebSocket" in window) {
+               sock = new MozWebSocket(wsuri);
+            } else {
+               log("Browser does not support WebSocket!");
+               window.location = "http://autobahn.ws/unsupportedbrowser";
+            }
+
+            if (sock) {
+               sock.onopen = function() {
+                  log("Connected to " + wsuri);
+               }
+
+               sock.onclose = function(e) {
+                  log("Connection closed (wasClean = " + e.wasClean + ", code = " + e.code + ", reason = '" + e.reason + "')");
+                  sock = null;
+               }
+
+               sock.onmessage = function(e) {
+                  log("Got message: " + e.data);
+               }
+            }
+         };
+
+         function send() {
+            var msg = document.getElementById('message').value;
+            if (sock) {
+                new_annot = {
+                   categories : [msg],
+                   user : "admin"
+               }
+               sock.send(JSON.stringify(new_annot));
+               log("Sent: " + JSON.stringify(new_annot));
+            } else {
+               log("Not connected.");
+            }
+         };
+
+         function log(m) {
+            ellog.innerHTML += m + '\n';
+            ellog.scrollTop = ellog.scrollHeight;
+         };
+      </script>
+   </head>
+   <body>
+      <h1>OSC websocket Test</h1>
+      <noscript>You must enable JavaScript</noscript>
+      <form>
+         <p>Message: <input id="message" type="text" size="50" maxlength="50" value="Hello, world!"></p>
+      </form>
+      <button onclick='send();'>Send Message</button>
+      <pre id="log" style="height: 20em; overflow-y: scroll; background-color: #faa;"></pre>
+   </body>
+</html>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/annot-server/webapp/templates/annotationclient.html	Tue Oct 14 05:07:37 2014 +0200
@@ -0,0 +1,102 @@
+<!doctype html>
+<html>
+<head>
+  <meta charset="utf-8">
+  <meta name="viewport" content="width=device-width, initial-scale=1">
+  <title>Mons by IRI</title>
+  <link rel="stylesheet" href="{{ config['STATIC_URL'] }}/css/lib.css">
+  <link rel="stylesheet" href="{{ config['STATIC_URL'] }}/css/app.css">
+</head>
+<body ng-controller="homeCtrl" ng-app="mons" ng-cloak>
+  <div class="container">
+    <div class="row">
+      <div class="col-md-12">
+      <form role="form">
+        <input class="form-control" placeholder="Nom d'utilisateur" ng-model="username"/>
+      </form>
+      </div>
+    </div>
+    <div class="row">
+      <div class="col-md-12">
+        <form role="form">
+          <autocomplete ng-model="yourchoice" data="allCatLabels" attr-input-class="form-control" attr-placeholder="Catégories..." ></autocomplete>
+        </form>
+      </div>
+    </div>
+    <div class="row mons-content">
+        <div class="mons-button hand return" ng-style="returnVisStyle" ng-click="selectLevel()" ng-init="returnVisStyle={visibility:'hidden'}">
+          <div class="content">
+              <div class="table">
+                <div class="table-cell">
+                  <p class="large-cat">RETOUR</p>
+                </div>
+              </div>
+          </div>
+        </div>
+        <div class="mons-button hand send" id="sendButton" ng-click="sendAnnotation(yourchoice)" ng-class="{'success-border':sendBtnSuccess}">
+          <div class="content">
+              <div class="table">
+                <div class="table-cell">
+                  <p class="large-cat">ENVOYER</p>
+                  <p class="normal-cat">la catégorie saisie</p>
+                </div>
+              </div>
+          </div>
+        </div>
+    </div>
+    <div class="mons-content">
+      <div ng-show="!selectedlevel">
+          <div class="mons-button hand" ng-repeat="c in data.categories" style="background-color: {{ '{{' }} c.color {{ '}}' }}"
+                                        ng-click="selectLevel(c.label, c.code, c)" ng-class="{'success-border':c.sendSuccess}">
+            <div class="content">
+                <div class="table">
+                  <div class="table-cell">
+                    <p class="large-cat">{{ '{{' }} c.label {{ '}}' }}</p>
+                    <p class="normal-cat">{{ '{{' }} c.full_label {{ '}}' }}</p>
+                  </div>
+                </div>
+            </div>
+          </div>
+      </div>
+      <div ng-show="selectedlevel">
+          <div class="mons-button hand" ng-repeat="c in selectedlevel" style="background-color: {{ '{{' }} c.color {{ '}}' }}"
+                                        ng-click="sendAnnotation(c.label, c.code, c)" ng-class="{'success-border':c.sendSuccess}">
+            <div class="content">
+                <div class="table">
+                  <div class="table-cell">
+                    <p class="large-cat">{{ '{{' }} c.label {{ '}}' }}</p>
+                    <p class="normal-cat">{{ '{{' }} c.full_label {{ '}}' }}</p>
+                  </div>
+                </div>
+            </div>
+          </div>
+      </div>
+    </div>
+    <footer>
+	  {% if logging %}<div class="row">
+	    <pre id="log" style="height: 20em; overflow-y: scroll; background-color: #faa; text-align: left;"></pre>
+      </div>{% endif %}
+      <div class="row">
+        <div class="col-md-12 text-center">
+            mons vBeta - ©IRI-2014
+        </div>
+      </div>
+    </footer>
+    <div class="row messages">
+      <div class="alert" ng-class="{'alert-success':showSuccessAlert, 'alert-danger':!showSuccessAlert}" role="alert" ng-show="showAlertDiv">{{ '{{' }} alertMessage {{ '}}' }}</div>
+    </div>
+  </div>
+  <script type="text/javascript" src="{{ config['STATIC_URL'] }}/js/lib.js"></script>
+  <!--script type="text/javascript" src="{{ pre_static_path }}static/js/templates.js"></script-->
+  <script type="text/javascript" src="{{ config['STATIC_URL'] }}/js/app.js"></script>
+  <script type="text/javascript">
+    angular.module("mons")
+        .value('context', {
+            {% if logging %}logging: true,{% endif %}
+            urls: {
+                dataUrl: "{{ config['STATIC_URL'] }}/data/categories.json"
+            }
+        });
+  </script>
+</body>
+</html>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/annot-server/webapp/templates/index.html	Tue Oct 14 05:07:37 2014 +0200
@@ -0,0 +1,56 @@
+<!DOCTYPE html>
+<html>
+   <head>
+      <link rel="icon" href="data:;base64,=">
+      <script type="text/javascript">
+         var sock = null;
+         var ellog = null;
+
+         window.onload = function() {
+
+            ellog = document.getElementById('log');
+
+            var wsuri;
+            if (window.location.protocol === "file:") {
+               wsuri = "ws://127.0.0.1:8090/broadcast";
+            } else {
+               wsuri = "ws://" + window.location.hostname + ":8090/broadcast";
+            }
+            if ("WebSocket" in window) {
+               sock = new WebSocket(wsuri);
+            } else if ("MozWebSocket" in window) {
+               sock = new MozWebSocket(wsuri);
+            } else {
+               log("Browser does not support WebSocket!");
+               window.location = "http://autobahn.ws/unsupportedbrowser";
+            }
+
+            if (sock) {
+               sock.onopen = function() {
+                  log("Connected to " + wsuri);
+               }
+
+               sock.onclose = function(e) {
+                  log("Connection closed (wasClean = " + e.wasClean + ", code = " + e.code + ", reason = '" + e.reason + "')");
+                  sock = null;
+               }
+
+               sock.onmessage = function(e) {
+                  log("Got message: " + e.data);
+               }
+            }
+         };
+
+         function log(m) {
+            ellog.innerHTML += m + '\n';
+            ellog.scrollTop = ellog.scrollHeight;
+         };
+      </script>
+      <link rel="stylesheet" type="text/css" href="/static/css/base.css">
+   </head>
+   <body>
+      <h1>OSC websocket Test</h1>
+      <noscript>You must enable JavaScript</noscript>
+      <pre id="log"></pre>
+   </body>
+</html>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/annot-server/webapp/views.py	Tue Oct 14 05:07:37 2014 +0200
@@ -0,0 +1,28 @@
+#
+# See LICENCE for detail
+# Copyright (c) 2014 IRI
+#
+
+from flask import render_template, jsonify, request
+
+from webapp import app
+
+@app.route('/')
+def page_home():
+    return render_template('index.html')
+    #return render_template('annotationclient.html', logging=True)
+
+
+@app.route('/annot')
+def page_annot():
+    return render_template('annot.html')
+
+
+@app.route('/annotationclient')
+def page_annotationclient():
+    return render_template('annotationclient.html', logging=True)
+
+
+@app.route('/api/test', methods=['PUT', 'POST'])
+def new():
+    return jsonify(request.get_json(force=False))
--- a/client/gulpfile.js	Tue Oct 14 18:00:13 2014 +0200
+++ b/client/gulpfile.js	Tue Oct 14 05:07:37 2014 +0200
@@ -3,8 +3,8 @@
 var gutil = require('gulp-util')
 var plugins = require("gulp-load-plugins")({lazy:false});
 
-var templateFolder = '../annot-server/templates/';
-var templateFileDest = '../annot-server/templates/annotationclient.html';
+var templateFolder = '../annot-server/webapp/templates/';
+var templateFileDest = '../annot-server/webapp/templates/annotationclient.html';
 var staticFolder = '../annot-server/static';
 
 var clientBaseName = 'app';
--- a/requirements.txt	Tue Oct 14 18:00:13 2014 +0200
+++ b/requirements.txt	Tue Oct 14 05:07:37 2014 +0200
@@ -1,8 +1,17 @@
 Flask==0.10.1
+Flask-SQLAlchemy==2.0
+Jinja2==2.7.3
+MarkupSafe==0.23
+SQLAlchemy==0.9.8
 Twisted==14.0.2
+Werkzeug==0.9.6
 autobahn==0.9.1
+itsdangerous==0.24
+midi==v0.2.3
 ntplib==0.3.2
 psycopg2==2.5.4
-#c.f. https://confluence.atlassian.com/display/BBKB/abort%3A+certificate+for+bitbucket.org+has+unexpected+fingerprint
--e hg+https://bitbucket.org/IRI/txosc/get/tip.tar.gz#egg=txosc
+six==1.8.0
+txosc==0.2.0
 txpostgres==1.2.0
+wsgiref==0.1.2
+zope.interface==4.1.1