Initial commit

This commit is contained in:
Benjamin Renard 2014-01-12 01:33:07 +01:00
commit 7004713412
19 changed files with 772 additions and 0 deletions

7
.gitignore vendored Normal file
View file

@ -0,0 +1,7 @@
*.pyc
*.pyo
*~
.*.swp
/*.egg-info
/cache/*
/data/*

10
Makefile Normal file
View file

@ -0,0 +1,10 @@
.PHONY: clean flake8
all: clean flake8
clean:
find -name "*.pyc" | xargs rm -f
rm -rf cache/*
flake8:
flake8 --ignore=E123,E128 --max-line-length=120 mycoserver

24
README Normal file
View file

@ -0,0 +1,24 @@
Install
=======
Debian dependencies:
$ aptitude install python python-mako python-markupsafe python-paste python-pastedeploy python-pastescript \
python-weberror python-webhelpers python-webob
Non-Debian dependencies:
$ git clone git://gitorious.org/biryani/biryani.git biryani1
$ cd biryani1
$ git checkout biryani1
$ python setup.py develop --no-deps --user
Install mycoserver Python egg (from mycoserver root directory):
$ python setup.py develop --no-deps --user
Start server
============
$ paster serve --reload development.ini

53
development.ini Normal file
View file

@ -0,0 +1,53 @@
# EesyVPN Web - Development environment configuration
#
# The %(here)s variable will be replaced with the parent directory of this file.
[DEFAULT]
debug = true
# Uncomment and replace with the address which should receive any error reports
#email_to = you@yourdomain.com
smtp_server = localhost
from_address = myco-server@localhost
dbpass = myP@ssw0rd
[server:main]
use = egg:Paste#http
host = 0.0.0.0
port = 8765
[app:main]
use = egg:MyCoServer
# Logging configuration
[loggers]
keys = root, mycoserver, mycoserver_router
[handlers]
keys = console
[formatters]
keys = generic
[logger_root]
level = DEBUG
handlers = console
[logger_mycoserver]
level = DEBUG
handlers =
qualname = mycoserver
[logger_mycoserver_router]
level = DEBUG
handlers =
qualname = mycoserver.router
[handler_console]
class = StreamHandler
args = (sys.stderr,)
formatter = generic
[formatter_generic]
format = %(asctime)s,%(msecs)03d %(levelname)-5.5s [%(name)s:%(funcName)s line %(lineno)d] %(message)s
datefmt = %H:%M:%S

0
mycoserver/__init__.py Normal file
View file

57
mycoserver/application.py Normal file
View file

@ -0,0 +1,57 @@
# -*- coding: utf-8 -*-
"""Middleware initialization"""
import logging.config
import os
from paste.cascade import Cascade
from paste.urlparser import StaticURLParser
from weberror.errormiddleware import ErrorMiddleware
from . import configuration, context, controllers, templates
import db
def make_app(global_conf, **app_conf):
"""Create a WSGI application and return it
``global_conf``
The inherited configuration for this application. Normally from
the [DEFAULT] section of the Paste ini file.
``app_conf``
The application's local configuration. Normally specified in
the [app:<name>] section of the Paste ini file (where <name>
defaults to main).
"""
logging.config.fileConfig(global_conf['__file__'])
app_ctx = context.Context()
app_ctx.conf = configuration.load_configuration(global_conf, app_conf)
app_ctx.templates = templates.load_templates(app_ctx)
app_ctx.db = db.DB(
app_ctx.conf.get('dbhost','localhost'),
app_ctx.conf.get('dbuser','myco'),
app_ctx.conf.get('dbpass','password'),
app_ctx.conf.get('dbname','myco'),
)
if not app_ctx.db.connect():
logging.error('Failed to connect DB')
app = controllers.make_router()
app = context.make_add_context_to_request(app, app_ctx)
if not app_ctx.conf['debug']:
app = ErrorMiddleware(
app,
error_email=app_ctx.conf['email_to'],
error_log=app_ctx.conf.get('error_log', None),
error_message=app_ctx.conf.get('error_message', 'An internal server error occurred'),
error_subject_prefix=app_ctx.conf.get('error_subject_prefix', 'Web application error: '),
from_address=app_ctx.conf['from_address'],
smtp_server=app_ctx.conf.get('smtp_server', 'localhost'),
)
app = Cascade([StaticURLParser(os.path.join(app_ctx.conf['app_dir'], 'static')), app])
app.ctx = app_ctx
return app

View file

@ -0,0 +1,30 @@
# -*- coding: utf-8 -*-
"""Paste INI configuration"""
import os
from biryani1 import strings
from biryani1.baseconv import (check, default, guess_bool, pipe, struct)
def load_configuration(global_conf, app_conf):
"""Build the application configuration dict."""
app_dir = os.path.dirname(os.path.abspath(__file__))
conf = {}
conf.update(strings.deep_decode(global_conf))
conf.update(strings.deep_decode(app_conf))
conf.update(check(struct(
{
'app_conf': default(app_conf),
'app_dir': default(app_dir),
'cache_dir': default(os.path.join(os.path.dirname(app_dir), 'cache')),
'debug': pipe(guess_bool, default(False)),
'global_conf': default(global_conf),
},
default='drop',
drop_none_values=False,
))(conf))
return conf

24
mycoserver/context.py Normal file
View file

@ -0,0 +1,24 @@
# -*- coding: utf-8 -*-
"""Context loaded and saved in WSGI requests"""
from webob.dec import wsgify
def make_add_context_to_request(app, app_ctx):
"""Return a WSGI middleware that adds context to requests."""
@wsgify
def add_context_to_request(req):
req.ctx = app_ctx
req.ctx.req = req
return req.get_response(app)
return add_context_to_request
class Context(object):
_ = lambda self, message: message
conf = None
templates = None
db = None

70
mycoserver/controllers.py Normal file
View file

@ -0,0 +1,70 @@
# -*- coding: utf-8 -*-
import logging
from webob.dec import wsgify
from . import conv, router, templates, wsgi_helpers
import json
log = logging.getLogger(__name__)
@wsgify
def home(req):
return templates.render(req.ctx, '/home.mako', data={})
@wsgify
def login(req):
params = req.params
log.debug(u'params = {}'.format(params))
inputs = {
'email': params.get('email'),
'password': params.get('password'),
}
log.debug(u'inputs = {}'.format(inputs))
data, errors = conv.inputs_to_login_data(inputs)
if errors is not None:
return wsgi_helpers.bad_request(req.ctx, comment=errors)
log.debug(u'data = {}'.format(data))
login_data=req.ctx.db.login(data['email'],data['password'])
return wsgi_helpers.respond_json(req.ctx,login_data,headers=[('Access-Control-Allow-Origin','*')])
@wsgify
def sync(req):
params = req.params
log.debug(u'params = {}'.format(params))
inputs = {
'email': params.get('email'),
'password': params.get('password'),
'groups': params.get('groups')
}
log.debug(u'inputs = {}'.format(inputs))
data, errors = conv.inputs_to_sync_data(inputs)
if errors is not None or data['groups'] is None:
return wsgi_helpers.bad_request(req.ctx, comment=errors)
data['groups']=json.loads(data['groups'])
log.debug(u'data = {}'.format(data))
login_data=req.ctx.db.login(data['email'],data['password'])
if 'email' in login_data:
ret=req.ctx.db.sync_group(data['email'],data['groups'])
return wsgi_helpers.respond_json(req.ctx,ret,headers=[('Access-Control-Allow-Origin','*')])
else:
return wsgi_helpers.respond_json(
req.ctx,
login_data,
headers=[('Access-Control-Allow-Origin','*')]
)
def make_router():
return router.make_router(
('GET', '^/$', home),
('GET', '^/login$', login),
('GET', '^/sync$', sync),
)

23
mycoserver/conv.py Normal file
View file

@ -0,0 +1,23 @@
# -*- coding: utf-8 -*-
from biryani1.baseconv import cleanup_line, empty_to_none, not_none, pipe, struct
inputs_to_login_data = struct(
{
'email': pipe(cleanup_line, empty_to_none),
'password': pipe(cleanup_line, empty_to_none),
},
default='drop',
drop_none_values=False,
)
inputs_to_sync_data = struct(
{
'email': pipe(cleanup_line, empty_to_none),
'password': pipe(cleanup_line, empty_to_none),
'groups': pipe(empty_to_none),
},
default='drop',
drop_none_values=False,
)

84
mycoserver/db/__init__.py Normal file
View file

@ -0,0 +1,84 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
import json
import logging
log = logging.getLogger(__name__)
import MySQLdb
class DB(object):
def __init__(self,host,user,pwd,db):
self.host = host
self.user = user
self.pwd = pwd
self.db = db
self.con = 0
def connect(self):
if self.con == 0:
try:
con = MySQLdb.connect(self.host,self.user,self.pwd,self.db)
self.con = con
return True
except Exception, e:
log.fatal('Error connecting to database : %s' % e)
return
def do_sql(self,sql):
try:
c=self.con.cursor()
c.execute(sql)
self.con.commit()
return c
except Exception,e:
log.error('Error executing request %s : %s' % (sql,e))
return False
def select(self,sql):
ret=self.do_sql(sql)
if ret!=False:
return ret.fetchall()
return ret
def login(self,email,password):
ret=self.select("SELECT email,name,password FROM users WHERE email='%s' AND password='%s'" % (email,password))
log.debug(ret)
if ret:
if len(ret)==1:
return {
'email': ret[0][0],
'name': ret[0][1]
}
elif len(ret)>=1:
log.warning('Duplicate user %s in database' % email)
elif ret==():
return { 'loginerror': 'Utilisateur inconnu' }
return { 'loginerror': 'Erreur inconnu' }
def sync_group(self,email,groups):
db_groups=self.get_group(email)
json_group=json.dumps(groups)
if db_groups!=False:
if db_groups=={}:
if groups=={}:
return {'groups': {}}
else:
if self.do_sql("INSERT INTO groups (email,groups) VALUES ('%s','%s')" % (email,json_group)):
return {'groups': groups}
elif groups=={}:
return {'groups': db_groups}
else:
if self.do_sql("UPDATE groups SET groups='%s' WHERE email='%s'" % (json_group,email)):
return {'groups': groups}
return {'syncerror': 'Erreur inconnu'}
def get_group(self,email):
ret=self.select("SELECT groups FROM groups WHERE email='%s'" % email)
if ret!=False:
if len(ret)==1:
return json.loads(ret[0][0])
else:
return {}
else:
return False

50
mycoserver/router.py Normal file
View file

@ -0,0 +1,50 @@
# -*- coding: utf-8 -*-
"""Helpers for URLs"""
import logging
import re
from webob.dec import wsgify
from . import wsgi_helpers
log = logging.getLogger(__name__)
def make_router(*routings):
"""Return a WSGI application that dispatches requests to controllers."""
routes = []
for routing in routings:
methods, regex, app = routing[:3]
if isinstance(methods, basestring):
methods = (methods,)
vars = routing[3] if len(routing) >= 4 else {}
routes.append((methods, re.compile(regex), app, vars))
@wsgify
def router(req):
"""Dispatch request to controllers."""
split_path_info = req.path_info.split('/')
assert not split_path_info[0], split_path_info
for methods, regex, app, vars in routes:
if methods is None or req.method in methods:
match = regex.match(req.path_info)
if match is not None:
log.debug(u'URL path = {path} matched controller {controller}'.format(
controller=app, path=req.path_info))
if getattr(req, 'urlvars', None) is None:
req.urlvars = {}
req.urlvars.update(dict(
(name, value.decode('utf-8') if value is not None else None)
for name, value in match.groupdict().iteritems()
))
req.urlvars.update(vars)
req.script_name += req.path_info[:match.end()]
req.path_info = req.path_info[match.end():]
return req.get_response(app)
return wsgi_helpers.not_found(req.ctx)
return router

View file

@ -0,0 +1,44 @@
# -*- coding: utf-8 -*-
"""Mako templates rendering"""
import json
import mako.lookup
import os
from . import helpers
js = lambda x: json.dumps(x, encoding='utf-8', ensure_ascii=False)
def load_templates(ctx):
# Create the Mako TemplateLookup, with the default auto-escaping.
return mako.lookup.TemplateLookup(
default_filters=['h'],
directories=[os.path.join(ctx.conf['app_dir'], 'templates')],
input_encoding='utf-8',
module_directory=os.path.join(ctx.conf['cache_dir'], 'templates'),
)
def render(ctx, template_path, **kw):
return ctx.templates.get_template(template_path).render_unicode(
ctx=ctx,
helpers=helpers,
js=js,
N_=lambda message: message,
req=ctx.req,
**kw).strip()
def render_def(ctx, template_path, def_name, **kw):
return ctx.templates.get_template(template_path).get_def(def_name).render_unicode(
_=ctx.translator.ugettext,
ctx=ctx,
js=js,
N_=lambda message: message,
req=ctx.req,
**kw).strip()

View file

@ -0,0 +1,14 @@
# -*- coding: utf-8 -*-
import os
import random
import datetime
def random_sequence(length):
return [random.random() for idx in xrange(0, length)]
def relative_path(ctx, abs_path):
return os.path.relpath(abs_path, ctx.req.path)

View file

@ -0,0 +1,13 @@
## -*- coding: utf-8 -*-
<%inherit file="/site.mako"/>
<%block name="title_content">MyCo</%block>
<%block name="body_content">
<div class="hero-unit">
<h1>MyCo <small>Gérer vos déponses communes</small></h1>
<p class="muted">Application mobile de gestion de vos dépenses communes.</p>
</div>
</%block>

View file

@ -0,0 +1,23 @@
## -*- coding: utf-8 -*-
<%inherit file="/site.mako"/>
<%block name="body_content">
<div class="alert alert-block alert-error">
<h4 class="alert-heading">Error « ${title} »</h4>
<p>${explanation}</p>
% if comment:
<p>${comment}</p>
% endif
% if message:
<p>${message}</p>
% endif
</div>
</%block>
<%block name="title_content">
${title} - ${parent.title_content()}
</%block>

View file

@ -0,0 +1,36 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><%block name="title_content">MyCo</%block></title>
<link rel="stylesheet" href="https://netdna.bootstrapcdn.com/bootstrap/3.0.3/css/bootstrap.min.css">
<link rel="stylesheet" href="https://netdna.bootstrapcdn.com/bootstrap/3.0.3/css/bootstrap-theme.min.css">
<link rel="stylesheet" href="${helpers.relative_path(ctx, '/css/style.css')}">
<!--[if lt IE 9]>
<script src="https://oss.maxcdn.com/libs/html5shiv/3.7.0/html5shiv.js"></script>
<script src="https://oss.maxcdn.com/libs/respond.js/1.3.0/respond.min.js"></script>
<![endif]-->
</head>
<body>
<div class="navbar navbar-inverse navbar-fixed-top" role="navigation">
<div class="container">
<div class="navbar-header">
<a class="navbar-brand" href="index.html">MyCo</a>
</div>
</div>
</div>
<div class="container">
<%block name="body_content"/>
</div>
<script src="https://code.jquery.com/jquery.js"></script>
<script src="https://netdna.bootstrapcdn.com/bootstrap/3.0.3/js/bootstrap.min.js"></script>
</body>
</html>

170
mycoserver/wsgi_helpers.py Normal file
View file

@ -0,0 +1,170 @@
# -*- coding: utf-8 -*-
import collections
import json
from markupsafe import Markup
from webhelpers.html import tags
import webob.dec
import webob.exc
from . import templates
N_ = lambda message: message
errors_explanation = {
400: N_("Request is faulty"),
401: N_("Access is restricted to authorized persons."),
403: N_("Access is forbidden."),
404: N_("The requested page was not found."),
}
errors_message = {
401: N_("You must login to access this page."),
}
errors_title = {
400: N_("Unable to Access"),
401: N_("Access Denied"),
403: N_("Access Denied"),
404: N_("Unable to Access"),
}
def bad_request(ctx, **kw):
return error(ctx, 400, **kw)
def discard_empty_items(data):
if isinstance(data, collections.Mapping):
# Use type(data) to keep OrderedDicts.
data = type(data)(
(name, discard_empty_items(value))
for name, value in data.iteritems()
if value is not None
)
return data
def error(ctx, code, **kw):
response = webob.exc.status_map[code](headers=kw.pop('headers', None))
if code != 204: # No content
body = kw.pop('body', None)
if body is None:
template_path = kw.pop('template_path', '/http-error.mako')
explanation = kw.pop('explanation', None)
if explanation is None:
explanation = errors_explanation.get(code)
explanation = ctx._(explanation) if explanation is not None else response.explanation
message = kw.pop('message', None)
if message is None:
message = errors_message.get(code)
if message is not None:
message = ctx._(message)
comment = kw.pop('comment', None)
if isinstance(comment, dict):
comment = tags.ul(u'{0} : {1}'.format(key, value) for key, value in comment.iteritems())
elif isinstance(comment, list):
comment = tags.ul(comment)
title = kw.pop('title', None)
if title is None:
title = errors_title.get(code)
title = ctx._(title) if title is not None else response.status
body = templates.render(ctx, template_path,
comment=comment,
explanation=explanation,
message=message,
response=response,
title=title,
**kw)
response.body = body.encode('utf-8') if isinstance(body, unicode) else body
return response
def forbidden(ctx, **kw):
return error(ctx, 403, **kw)
def method_not_allowed(ctx, **kw):
return error(ctx, 405, **kw)
def no_content(ctx, headers=None):
return error(ctx, 204, headers=headers)
def not_found(ctx, **kw):
return error(ctx, 404, **kw)
def redirect(ctx, code=302, location=None, **kw):
assert location is not None
location_str = location.encode('utf-8') if isinstance(location, unicode) else location
response = webob.exc.status_map[code](headers=kw.pop('headers', None), location=location_str)
body = kw.pop('body', None)
if body is None:
template_path = kw.pop('template_path', '/http-error.mako')
explanation = kw.pop('explanation', None)
if explanation is None:
explanation = Markup(u'{0} <a href="{1}">{1}</a>.').format(ctx._(u"You'll be redirected to page"), location)
message = kw.pop('message', None)
if message is None:
message = errors_message.get(code)
if message is not None:
message = ctx._(message)
title = kw.pop('title', None)
if title is None:
title = ctx._("Redirection in progress...")
body = templates.render(ctx, template_path,
comment=kw.pop('comment', None),
explanation=explanation,
message=message,
response=response,
title=title,
**kw)
response.body = body.encode('utf-8') if isinstance(body, unicode) else body
return response
def respond_json(ctx, data, code=None, headers=None, jsonp=None):
"""Return a JSON response.
This function is optimized for JSON following
`Google JSON Style Guide <http://google-styleguide.googlecode.com/svn/trunk/jsoncstyleguide.xml>`_, but will handle
any JSON except for HTTP errors.
"""
if isinstance(data, collections.Mapping):
# Remove null properties as recommended by Google JSON Style Guide.
data = discard_empty_items(data)
error = data.get('error')
else:
error = None
if headers is None:
headers = []
if jsonp:
headers.append(('Content-Type', 'application/javascript; charset=utf-8'))
else:
headers.append(('Content-Type', 'application/json; charset=utf-8'))
if error:
code = code or error['code']
assert isinstance(code, int)
response = webob.exc.status_map[code](headers=headers)
if error.get('code') is None:
error['code'] = code
if error.get('message') is None:
error['message'] = response.title
else:
response = ctx.req.response
if code is not None:
response.status = code
response.headers.update(headers)
text = unicode(json.dumps(data, encoding='utf-8', ensure_ascii=False, indent=2, sort_keys=True))
if jsonp:
text = u'{0}({1})'.format(jsonp, text)
response.text = text
return response
def unauthorized(ctx, **kw):
return error(ctx, 401, **kw)

40
setup.py Executable file
View file

@ -0,0 +1,40 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""MyCO Server web application."""
from setuptools import setup, find_packages
doc_lines = __doc__.split('\n')
setup(
author=u'Benjamin Renard',
author_email=u'brenard@zionetrix.net',
description=doc_lines[0],
entry_points="""
[paste.app_factory]
main = mycoserver.application:make_app
""",
include_package_data=True,
install_requires=[
'Biryani1 >= 0.9dev',
'MarkupSafe >= 0.15',
'WebError >= 0.10',
'WebHelpers >= 1.3',
'WebOb >= 1.1',
],
# keywords='',
# license=u'http://www.fsf.org/licensing/licenses/agpl-3.0.html',
long_description='\n'.join(doc_lines[2:]),
name=u'MyCoServer',
packages=find_packages(),
paster_plugins=['PasteScript'],
setup_requires=['PasteScript >= 1.6.3'],
# url=u'',
version='0.1',
zip_safe=False,
)