├── .gitignore ├── LICENSE ├── Procfile ├── README.md ├── databin ├── __init__.py ├── core.py ├── cors.py ├── default_settings.py ├── model.py ├── parsers │ ├── __init__.py │ ├── psql.py │ ├── simple.py │ └── util.py ├── static │ └── style.css ├── templates │ ├── index.html │ ├── layout.html │ └── view.html ├── util.py └── web.py ├── requirements.txt └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.egg-info 3 | development.db -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013, Friedrich Lindenberg 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a 4 | copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included 12 | in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 15 | OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 17 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 18 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 19 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 20 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: gunicorn databin.web:app 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | databin 2 | ======= 3 | 4 | databin is a simple tool that allows you to share tables via the web just like other paste bins would let you share code and text. 5 | 6 | The code is based on SQLAlchemy, Flask and sniffing glue. If you want to install it, make sure you have ``foreman`` installed (e.g. 7 | via brew or apt), then create a ``virtualenv`` before you: 8 | 9 | pip install -r requirements.txt 10 | python setup.py develop 11 | foreman start 12 | 13 | And remember: tables are for friends. 14 | 15 | -------------------------------------------------------------------------------- /databin/__init__.py: -------------------------------------------------------------------------------- 1 | # shut up useless SA warning: 2 | import warnings; 3 | warnings.filterwarnings('ignore', 'Unicode type received non-unicode bind param value.') 4 | from sqlalchemy.exc import SAWarning 5 | warnings.filterwarnings('ignore', category=SAWarning) 6 | -------------------------------------------------------------------------------- /databin/core.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from flask import Flask 4 | from flask.ext.sqlalchemy import SQLAlchemy 5 | 6 | from databin import default_settings 7 | 8 | logging.basicConfig(level=logging.INFO) 9 | 10 | app = Flask(__name__) 11 | app.config.from_object(default_settings) 12 | app.config.from_envvar('DATABIN_SETTINGS', silent=True) 13 | 14 | db = SQLAlchemy(app) 15 | -------------------------------------------------------------------------------- /databin/cors.py: -------------------------------------------------------------------------------- 1 | from datetime import timedelta 2 | from flask import make_response, request, current_app 3 | from functools import update_wrapper 4 | 5 | 6 | def crossdomain(origin=None, methods=None, headers=None, 7 | max_age=8640000, attach_to_all=True, 8 | automatic_options=True): 9 | if methods is not None: 10 | methods = ', '.join(sorted(x.upper() for x in methods)) 11 | if headers is not None and not isinstance(headers, basestring): 12 | headers = ', '.join(x.upper() for x in headers) 13 | if not isinstance(origin, basestring): 14 | origin = ', '.join(origin) 15 | if isinstance(max_age, timedelta): 16 | max_age = max_age.total_seconds() 17 | 18 | def get_methods(): 19 | if methods is not None: 20 | return methods 21 | 22 | options_resp = current_app.make_default_options_response() 23 | return options_resp.headers['allow'] 24 | 25 | def decorator(f): 26 | def wrapped_function(*args, **kwargs): 27 | if automatic_options and request.method == 'OPTIONS': 28 | resp = current_app.make_default_options_response() 29 | else: 30 | resp = make_response(f(*args, **kwargs)) 31 | if not attach_to_all and request.method != 'OPTIONS': 32 | return resp 33 | 34 | h = resp.headers 35 | 36 | h['Access-Control-Allow-Origin'] = origin 37 | h['Access-Control-Allow-Methods'] = get_methods() 38 | h['Access-Control-Max-Age'] = str(max_age) 39 | if headers is not None: 40 | h['Access-Control-Allow-Headers'] = headers 41 | return resp 42 | 43 | f.provide_automatic_options = False 44 | return update_wrapper(wrapped_function, f) 45 | return decorator 46 | -------------------------------------------------------------------------------- /databin/default_settings.py: -------------------------------------------------------------------------------- 1 | 2 | import os 3 | 4 | DEBUG = True 5 | ASSETS_DEBUG = True 6 | SECRET_KEY = os.environ.get('SECRET', 'key') 7 | SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL', 'sqlite:///development.db') 8 | -------------------------------------------------------------------------------- /databin/model.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from formencode import Schema, validators, Invalid, FancyValidator 3 | 4 | from databin.core import db 5 | from databin.util import make_key 6 | from databin.parsers import get_parsers 7 | 8 | 9 | class ValidFormat(FancyValidator): 10 | 11 | def _to_python(self, value, state): 12 | for key, name in get_parsers(): 13 | if value == key: 14 | return value 15 | raise Invalid('Not a valid format', value, None) 16 | 17 | 18 | class PasteSchema(Schema): 19 | description = validators.String(min=0, max=255) 20 | format = ValidFormat() 21 | force_header = validators.StringBool(empty=False) 22 | data = validators.String(min=10, max=255000) 23 | 24 | 25 | class Paste(db.Model): 26 | __tablename__ = 'paste' 27 | 28 | id = db.Column(db.Integer, primary_key=True) 29 | key = db.Column(db.Unicode()) 30 | source_ip = db.Column(db.Unicode()) 31 | description = db.Column(db.Unicode()) 32 | format = db.Column(db.Unicode()) 33 | data = db.Column(db.Unicode()) 34 | force_header = db.Column(db.Boolean()) 35 | created_at = db.Column(db.DateTime, default=datetime.utcnow) 36 | 37 | @classmethod 38 | def create(cls, data, source_ip): 39 | obj = cls() 40 | data = PasteSchema().to_python(data) 41 | while True: 42 | obj.key = make_key() 43 | if cls.by_key(obj.key) is None: 44 | break 45 | obj.source_ip = source_ip 46 | obj.description = data.get('description') 47 | obj.format = data.get('format') 48 | obj.force_header = data.get('force_header') 49 | obj.data = data.get('data') 50 | db.session.add(obj) 51 | db.session.commit() 52 | return obj 53 | 54 | @classmethod 55 | def by_key(cls, key): 56 | q = db.session.query(cls).filter_by(key=key) 57 | return q.first() 58 | 59 | def to_dict(self): 60 | return { 61 | 'id': self.id, 62 | 'key': self.key, 63 | 'source_ip': self.source_ip, 64 | 'description': self.description, 65 | 'format': self.format, 66 | 'force_header': self.force_header, 67 | 'data': self.data, 68 | 'created_at': self.created_at 69 | } 70 | 71 | @classmethod 72 | def all(cls): 73 | return db.session.query(cls) 74 | 75 | db.create_all() 76 | -------------------------------------------------------------------------------- /databin/parsers/__init__.py: -------------------------------------------------------------------------------- 1 | from databin.parsers.util import ParseException 2 | from databin.parsers.simple import parse_csv, parse_tsv, parse_ssv 3 | from databin.parsers.psql import parse_psql 4 | 5 | PARSERS = [ 6 | ('Excel copy & paste', 'excel', parse_tsv), 7 | ('psql Shell', 'psql', parse_psql), 8 | ('mysql Shell', 'mysql', parse_psql), 9 | ('Comma-Separated Values', 'csv', parse_csv), 10 | ('Tab-Separated Values', 'tsv', parse_tsv), 11 | ('Space-Separated Values', 'ssv', parse_ssv) 12 | ] 13 | 14 | 15 | def parse(format, data): 16 | for name, key, func in PARSERS: 17 | if key == format: 18 | return func(data) 19 | raise ParseException() 20 | 21 | 22 | def get_parsers(): 23 | for name, key, func in PARSERS: 24 | yield (key, name) 25 | -------------------------------------------------------------------------------- /databin/parsers/psql.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | def clean(cell): 4 | return cell.strip() 5 | 6 | 7 | def parse_psql(data): 8 | header, rows = False, [] 9 | for row in data.split('\n'): 10 | if set(row.strip()) == set(('+', '-')): 11 | header = True 12 | continue 13 | cells = map(clean, row.split('|')) 14 | rows.append(cells) 15 | return header, rows 16 | -------------------------------------------------------------------------------- /databin/parsers/simple.py: -------------------------------------------------------------------------------- 1 | from StringIO import StringIO 2 | import csv 3 | 4 | 5 | def parse_cell(cell): 6 | try: 7 | return cell.decode('utf-8') 8 | except: 9 | return cell 10 | 11 | 12 | def parse_csv(data, delimiter=','): 13 | databuf = StringIO(data.encode('utf-8')) 14 | rows = [] 15 | for row in csv.reader(databuf, delimiter=delimiter): 16 | rows.append([parse_cell(c) for c in row]) 17 | return False, rows 18 | 19 | 20 | def parse_tsv(data): 21 | return parse_csv(data, delimiter='\t') 22 | 23 | 24 | def parse_ssv(data): 25 | return parse_csv(data, delimiter=' ') 26 | -------------------------------------------------------------------------------- /databin/parsers/util.py: -------------------------------------------------------------------------------- 1 | 2 | class ParseException(Exception): 3 | pass -------------------------------------------------------------------------------- /databin/static/style.css: -------------------------------------------------------------------------------- 1 | 2 | body, label { 3 | font-family: 'OpenSansRegular', Helvetica, sans-serif; 4 | font-size: 15px; 5 | } 6 | 7 | h1, table th, strong { 8 | font-weight: normal; 9 | font-family: 'OpenSansBold', Helvetica, sans-serif; 10 | } 11 | 12 | h1 small { 13 | font-family: 'OpenSansRegular', Helvetica, sans-serif; 14 | } 15 | 16 | h1 .title, a { 17 | color: #CF19B9; 18 | } 19 | 20 | a:hover { 21 | text-decoration: none; 22 | } 23 | 24 | .toplink { 25 | padding: 1.6em 0 0 0; 26 | } 27 | 28 | .hidden { 29 | display: none; 30 | } 31 | 32 | .claim { 33 | font-size: 1.3em; 34 | line-height: 1.5em; 35 | padding: 0.5em 0 1em 0; 36 | color: #666; 37 | } 38 | 39 | textarea { 40 | font-size: 14px; 41 | font-family: monospace; 42 | } 43 | 44 | .airlock { 45 | background-color: #f9f9f9; 46 | border-bottom: 1px solid #ddd; 47 | padding: 1em 0 2em 0; 48 | } 49 | 50 | .tablewrap { 51 | width: 100%; 52 | overflow: auto; 53 | } 54 | 55 | table, table th, table td { 56 | font-size: 13px; 57 | } 58 | 59 | .table-striped tbody tr:nth-child(odd) td, .table-striped tbody tr:nth-child(odd) th { 60 | background-color: #fff; 61 | } 62 | 63 | footer { 64 | padding: 2em 0 2em 0; 65 | } 66 | 67 | footer ul { 68 | list-style-type: none; 69 | float: right; 70 | } 71 | 72 | footer ul li { 73 | display: inline-block; 74 | } -------------------------------------------------------------------------------- /databin/templates/index.html: -------------------------------------------------------------------------------- 1 | {%extends "layout.html" %} 2 | 3 | {% block content %} 4 |
5 |
6 |

7 | databin helps you to share tabular data — a few rows from Excel or a result from a SQL prompt — with others. 8 |

9 | 10 | 11 | 12 | 15 | 16 |
17 | 18 |
19 |
20 | 21 |
22 | 23 |
24 |
25 |
26 | 27 |
28 | 33 |
34 |
35 |
36 |
37 | 41 |
42 |
43 |
44 |
45 | 46 |
47 |
48 |
49 |
50 |
51 | {% endblock %} 52 | 53 | -------------------------------------------------------------------------------- /databin/templates/layout.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | databin: tables are for friends 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 |
25 |
26 | 27 |

databin a paste for tables

28 |
29 |
30 | {% block content %} 31 | - No Content - 32 | {% endblock %} 33 |
34 |
35 | 44 | 45 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /databin/templates/view.html: -------------------------------------------------------------------------------- 1 | {%extends "layout.html" %} 2 | 3 | {% block content %} 4 |
5 |
6 | {% if not table %} 7 |
Sorry, we were unable to generate the table. Please try again.
8 |
{{paste.data}}
9 | {% else %} 10 | 14 |

15 | {{paste.description}} 16 |

17 |
18 | 19 | {% for row in table %} 20 | 21 | {% if loop.first and has_header %} 22 | {% for cell in row %} 23 | 24 | {% endfor %} 25 | {% else %} 26 | {% for cell in row %} 27 | 28 | {% endfor %} 29 | {% endif %} 30 | 31 | {% endfor %} 32 |
{{cell}}{{cell}}
33 |
34 | {% endif %} 35 |
36 |
37 | {% endblock %} 38 | 39 | -------------------------------------------------------------------------------- /databin/util.py: -------------------------------------------------------------------------------- 1 | from flask import make_response, request 2 | from StringIO import StringIO 3 | from uuid import uuid4 4 | from hashlib import sha1 5 | import csv 6 | import json 7 | 8 | 9 | FORMATS = { 10 | 'application/json': 'json', 11 | 'text/csv': 'csv', 12 | 'text/html': 'html' 13 | } 14 | 15 | 16 | def response_format(url_format): 17 | if url_format is not None and url_format in FORMATS.values(): 18 | return url_format 19 | best_mime = request.accept_mimetypes.best_match(FORMATS.keys(), 20 | default='text/html') 21 | return FORMATS.get(best_mime) 22 | 23 | 24 | def make_it_cache(res, etag): 25 | res.headers['Cache-Control'] = 'public; max-age: 8640000' 26 | res.headers['ETag'] = etag 27 | return res 28 | 29 | 30 | def generate_etag(key, format): 31 | buf = '%s//%s//%s' % (key, format, request.args.get('callback')) 32 | return '"%s"' % sha1(buf).hexdigest() 33 | 34 | 35 | def make_csv(table): 36 | data = StringIO() 37 | writer = csv.writer(data) 38 | for row in table: 39 | writer.writerow([v.encode('utf-8') for v in row]) 40 | res = make_response(data.getvalue()) 41 | res.headers['Content-Type'] = 'text/csv; encoding=utf-8' 42 | return res 43 | 44 | 45 | def make_json(table, has_header): 46 | data = {'header': [], 'rows': list(table)} 47 | if has_header and len(data['rows']): 48 | data['header'] = data['rows'][0] 49 | data['rows'] = data['rows'][1:] 50 | data = json.dumps(data) 51 | if 'callback' in request.args: 52 | data = '%s && %s(%s);' % (request.args.get('callback'), 53 | request.args.get('callback'), data) 54 | res = make_response(data) 55 | res.headers['Content-Type'] = 'application/json; encoding=utf-8' 56 | return res 57 | 58 | 59 | def make_key(): 60 | return uuid4().hex[:6] 61 | -------------------------------------------------------------------------------- /databin/web.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from flask import render_template, request, redirect, url_for 4 | from flask import make_response 5 | from werkzeug.exceptions import NotFound, HTTPException 6 | from formencode import Invalid, htmlfill 7 | 8 | from databin.core import app 9 | from databin.model import Paste 10 | from databin.util import make_it_cache, make_csv, make_json 11 | from databin.util import generate_etag, response_format 12 | from databin.cors import crossdomain 13 | from databin.parsers import get_parsers, ParseException, parse 14 | 15 | log = logging.getLogger(__name__) 16 | 17 | class NotModified(HTTPException): 18 | code = 304 19 | 20 | 21 | def get_paste(key, format): 22 | paste = Paste.by_key(key) 23 | if paste is None: 24 | raise NotFound('No such table: %s' % key) 25 | etag = generate_etag(key, format) 26 | if request.if_none_match and request.if_none_match == etag: 27 | raise NotModified() 28 | has_header, table = False, None 29 | try: 30 | has_header, table = parse(paste.format, paste.data) 31 | except ParseException, pe: 32 | log.exception(pe) 33 | has_header = has_header or paste.force_header 34 | return paste, table, has_header, etag 35 | 36 | 37 | @app.route("/t/.") 38 | @app.route("/t/") 39 | @crossdomain(origin='*') 40 | def view(key, format=None): 41 | format = response_format(format) 42 | paste, table, has_header, etag = get_paste(key, format) 43 | if format == 'json': 44 | res = make_json(table, has_header) 45 | elif format == 'csv': 46 | res = make_csv(table) 47 | else: 48 | html = render_template('view.html', 49 | paste=paste.to_dict(), 50 | has_header=has_header, 51 | table=table) 52 | res = make_response(html) 53 | return make_it_cache(res, etag) 54 | 55 | 56 | @app.route("/", methods=['POST']) 57 | def post(): 58 | try: 59 | paste = Paste.create(request.form, request.remote_addr) 60 | return redirect(url_for('view', key=paste.key)) 61 | except Invalid, inv: 62 | return htmlfill.render(index(), auto_insert_errors=False, 63 | defaults=request.form, 64 | errors=inv.unpack_errors()) 65 | 66 | 67 | @app.route("/") 68 | def index(): 69 | return render_template('index.html', 70 | parsers=get_parsers()) 71 | 72 | 73 | if __name__ == "__main__": 74 | app.run(port=5000) 75 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | psycopg2 2 | Flask==0.9 3 | Flask-SQLAlchemy==0.16 4 | FormEncode==1.2.6 5 | Jinja2==2.7 6 | MarkupSafe==0.18 7 | Pygments==1.6 8 | SQLAlchemy==0.8.1 9 | Werkzeug==0.8.3 10 | bpython==0.12 11 | gunicorn==0.17.4 12 | wsgiref==0.1.2 13 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | setup(name='databin', 4 | version='0.1', 5 | description="A pastebin for tables", 6 | long_description="", 7 | classifiers=[], 8 | keywords='pastebin data tables', 9 | author='Friedrich Lindenberg', 10 | author_email='friedrich@pudo.org', 11 | url='http://pudo.org', 12 | license='MIT', 13 | packages=find_packages(exclude=['ez_setup', 'examples', 'tests']), 14 | include_package_data=True, 15 | zip_safe=False, 16 | install_requires=[ 17 | # -*- Extra requirements: -*- 18 | ], 19 | entry_points=""" 20 | # -*- Entry points: -*- 21 | """, 22 | ) 23 | --------------------------------------------------------------------------------