Mashup Award6で小飼弾さんの 404 API Not Found賞を授賞しました
先日作ったPythonのFlask製アプリ、オンラインコード共有ツール codetype.orgが審査員特別賞を授賞しました。
授賞作品の中では一番地味だと言えるサービスです。
TwitterのOAuthを利用してログインをし、プログラミングのコードを貼り付けてオンライン上で実行結果がわかる・共有できるというものです。
もともと9月に女性エンジニアとしてMashup Caravan Girls Talkでプレゼンをさせて頂いたのですが
そのデモアプリとして動く物を1週間くらいでざっくり作ったのがきっかけです。
その当時は弾さんのAPIは提供企業として正式なものではありませんでした。
それがプレゼンから1ヶ月後くらいの10月半ばに、弾さんのAPIが特別審査員として正式に提供することになったようです。
真面目に授賞できるだなんて思っていなくて、私自身でもすごくビックリしていますし、嬉しいのと恥ずかしいのが入り交じっている感覚です。
弾さんから弾さんの著書 & 弾言に弾さんのサインも頂きました。あリがとうございます!!
授賞式の様子。恐縮しすぎて笑顔がおかしい。
source: http://weekly.ascii.jp/elem/000/000/029/29236/
システム的にも雑な部分がたくさんあります。
さくらの500円サーバーでマルチドメインとして動いているため、全然費用がかかっていないです。
弾さんのllevalのAPIから返ってくるレスポンスをごそっとjson形式のままDBに保存しています。
制作/開発時間
今回のサービスは学習込みで全部で30時間くらいかけて作りました。
- ざっくりとしたサンプルベースの雛形5時間
- OAuth対応、lleval対応、ユーザ登録、syntax highlight対応、 10時間
- テンプレート化 10時間
- fork機能、レイアウト微調整、Twitterにpost機能、バナー作成、その他細かいところ 5時間
codetypeの主な機能
- Twitterのアカウントでログインができる
- コードが貼りつけて公開できる
- 実行した結果がオンライン上で確認できる
- http://codetype.org/aNfJJREpTu などオリジナルのURLでシェアできる
- コメントができる
- 既存のコードをベースにフォークできる
- ユーザ名は非公開
あたらしいことにチャレンジしたこと
- Pythonでなにか作りたい
- OAuthの仕組みを利用してなにか作りたい
- 個人ではじめてきちんとサービスとして公開した
- コンテスト的なものに初めて応募
コードも晒します
create_table.sql
CREATE TABLE users ( id integer primary key autoincrement, twitter_id int not null, name string not null, oauth_token string not null, oauth_secret string not null, date string null ); CREATE TABLE entries ( id integer primary key autoincrement, title string null, text string not null, author string not null, hash string not null, tag string null, lang string null, date string null, dan string null ); CREATE TABLE comments ( id integer primary key autoincrement, text string not null, entry_id int null, user_id int null, date string null );
~/src/flaskr/flaskr.py
# -*- coding: utf-8 -*- """ Flaskr ~~~~~~ A microblog example application written as Flask tutorial with Flask and sqlite3. :copyright: (c) 2010 by Armin Ronacher. :license: BSD, see LICENSE for more details. """ from __future__ import with_statement import sqlite3 from contextlib import closing from flask import Flask, request, session, g, redirect, url_for, abort, render_template, flash, escape from flaskext.oauth import OAuth from markdown import markdown import random import string import datetime import sys import os from pprint import pprint import urllib from flask import json oauth = OAuth() twitter = oauth.remote_app('twitter', base_url='http://api.twitter.com/1/', request_token_url='http://api.twitter.com/oauth/request_token', access_token_url='http://api.twitter.com/oauth/access_token', authorize_url='http://api.twitter.com/oauth/authorize', consumer_key='[consumer_key]', consumer_secret='[consumer_secret]' ) # configuration DATABASE = '/home/acotie/src/flaskr/flaskr.db' DEBUG = True SECRET_KEY = '[SECRET_KEY]' USERNAME = '' PASSWORD = '' # create our little application :) app = Flask(__name__) app.config.from_object(__name__) app.config.from_envvar('FLASKR_SETTINGS', silent=True) default_lang = { 'perl': 'pl', 'perl6': 'p6', 'awk': 'awk', 'basic': 'bas', 'brainfuck': 'bf', 'c': 'c', 'elisp': 'el', 'lisp': 'lsp', 'scheme': 'scm', 'haskell': 'hs', 'io': 'io', 'javascript': 'js', 'lua': 'lua', 'm4': 'm4', 'ocaml': 'ml', 'php': 'php', 'python': 'py', 'python3': 'py3', 'ruby': 'rb', 'ruby19': 'rb19', 'postscript': 'ps', 'tcl': 'tcl', } def query_db(query, args=(), one=False): cur = g.db.execute(query, args) rv = [dict((cur.description[idx][0], value) for idx, value in enumerate(row)) for row in cur.fetchall()] return (rv[0] if rv else None) if one else rv def random_str(self, length = 10): ret = "" for i in range(length): ret += random.choice(string.ascii_letters) return ret def connect_db(): """Returns a new connection to the database.""" return sqlite3.connect(app.config['DATABASE']) def init_db(): """Creates the database tables.""" with closing(connect_db()) as db: with app.open_resource('schema.sql') as f: db.cursor().executescript(f.read()) db.commit() @app.before_request def before_request(): """Make sure we are connected to the database each request.""" g.db = connect_db() g.user = None if 'user_id' in session: g.user = query_db('select * from users where twitter_id = ?', [session['user_id']], one=True) @app.after_request def after_request(response): """Closes the database again at the end of the request.""" g.db.close() return response @twitter.tokengetter def get_twitter_token(): #return session.get('twitter_token') user = g.user if user is not None: return user.oauth_token, user.oauth_secret @app.route('/') def show_entries(): cur = g.db.execute('select title, text, author, tag, lang, hash, date from entries order by id desc limit 10') entries = [dict(title=row[0], text=row[1], author=row[2], tag=row[3], lang=row[4], hash=row[5], date=row[6]) for row in cur.fetchall()] return render_template('index.html', entries=entries, default_lang=default_lang, ) @app.route('/recent/<int:page>') def show_entries_pager(page): if page is None: page = 1 of_value = 1 of_value = page * 10 print of_value entries = query_db('select title, text, author, tag, lang, hash, date from entries order by id desc LIMIT 10 OFFSET ?', [of_value], one=False) if entries is None: print 'No such entry' abort(404) return render_template('recent.html', entries=entries, default_lang=default_lang, ) @app.route('/<hash>') def entry(hash): dan = '' entry = query_db('select * from entries where hash = ?', [hash], one=True) if entry is None: print 'No such entry' abort(404) comment = query_db('select * from comments where entry_id = ?', [entry['id']], one=False) if comment is None: print 'No such comment' if entry['dan']: dan = json.loads( entry['dan'] ) print dan return render_template('entry.html', entry=entry, comment=comment, dan=dan, default_lang=default_lang, ) @app.route('/<hash>/fork') def entry_fork(hash): dan = '' entry = query_db('select * from entries where hash = ?', [hash], one=True) if entry is None: print 'No such entry' abort(404) comment = query_db('select * from comments where entry_id = ?', [entry['id']], one=False) if comment is None: print 'No such comment' if entry['dan']: dan = json.loads( entry['dan'] ) print dan return render_template('entry_fork.html', entry=entry, comment=comment, dan=dan, default_lang=default_lang) @app.route('/tag/<tag>') def entry_tag(tag): entry = query_db('select * from entries where tag = ?', [tag], one=False) if entry is None: print 'No such entry' abort(404) return render_template('entry_tag.html', entry=entry, tags=tag ) @app.route('/about') def about_entry(): return render_template('about.html') @app.route('/blog') def blog_entry(): return render_template('blog.html') @app.route('/contact') def contact_entry(): return render_template('contact.html') @app.route('/add', methods=['POST']) def add_entry(): if not session.get('logged_in'): abort(401) if not g.user: abort(401) name = session.get('username',None) hash = random_str(20) # dan the api code = request.form['text'] lang = request.form['lang'] url = 'http://api.dan.co.jp/lleval.cgi' params = urllib.urlencode({'c':'', 's':code, 'l':lang}) f = urllib.urlopen(url + '?' + params) # paramsはc=&s=code&l=lang dan = f.read() g.db.execute("insert into entries (title, text, author, tag, lang, hash, date, dan) values (?, ?, ?, ?, ?, ?, ?, ?)", [request.form['title'], request.form['text'], name, request.form['tag'], request.form['lang'], hash, datetime.datetime.now().strftime("%Y/%m/%d %H:%M:%S"), dan ]) g.db.commit() flash('New entry was successfully posted') return redirect( url_for('entry', hash=hash) ) @app.route('/add_c/<hash>', methods=['POST']) def add_comment(hash): if not session.get('logged_in'): abort(401) if not g.user: abort(401) entry = query_db('select * from entries where hash = ?', [hash], one=True) if entry is None: print 'No such entry' abort(404) g.db.execute("insert into comments (text, entry_id, user_id, date) values (?, ?, ?, ?)", [request.form['comment'], entry['id'], g.user['id'], datetime.datetime.now().strftime("%Y/%m/%d %H:%M:%S")]) g.db.commit() flash('New entry was successfully posted') return redirect( url_for('entry', hash=hash) ) @app.route('/login', methods=['GET', 'POST']) def login(): if g.user: print "OK" session['logged_in'] = True session['user_id'] = g.user['twitter_id'] session['username'] = g.user['name'] flash('You were logged in') return redirect(url_for('show_entries')) else: print "NO" print g.user print(url_for("oauth_authorized")) return twitter.authorize(callback=url_for("oauth_authorized")) @app.route('/logout') def logout(): session.pop('logged_in', None) session.pop('username', None) flash('You were logged out') return redirect(url_for('show_entries')) @app.route('/oauth-authorized') @twitter.authorized_handler def oauth_authorized(resp): next_url = url_for('show_entries') if resp is None: flash(u'You denied the request to sign in.') return redirect(next_url) else: #success user = g.db.execute('select name from users where twitter_id = ?', [int(resp['user_id'])]).fetchone() if user is None: g.db.execute("insert into users (twitter_id, name, oauth_token, oauth_secret, date) values (?, ?, ?, ?, ?)", [int(resp['user_id']), resp['screen_name'], resp['oauth_token'], resp['oauth_token_secret'], datetime.datetime.now()]) g.db.commit() session['twitter_token'] = ( resp['oauth_token'], resp['oauth_token_secret'] ) session['logged_in'] = True session['username'] = resp['screen_name'] session['user_id'] = int(resp['user_id']) flash(resp['screen_name'] + ' were signed in') return redirect(next_url) if __name__ == '__main__': app.run(host='codetype.org', port=5001)
さくらのマルチドメインの公開ディレクトリの~/www/codetype/以下に.htaccessを置きます。
~/www/codetype/.htaccess
#AddHandler cgi-script-debug .cgi DirectoryIndex index.html .ht RewriteEngine on RewriteBase / RewriteCond %{REQUEST_URI} ^/ RewriteRule ^(.*)$ http://codetype.org:5001/$1 [L,P]
さくらのサーバーは定期的にpythonやperlなど立ち上げっぱなしだとサーバー側のプロセスをkillしてしまう為
プロセスが落ちたら立ち上げるように無限ループさせるスクリプトを書いています。
問題はシェルのプロセス自体やscreenなどもkillされる恐れもあります。必要であれば別途、死活監視用スクリプトが必要かと思います。
~/codetype.sh
#!/usr/local/bin/bash while true do echo 'starting Flaskr server...' python ~/src/flaskr/flaskr.py done
構造やレイアウトなどはこちらにまとめています。
https://gist.github.com/730666
謝辞
今回アプリを作る上で参考にさせて頂いたサイト
以下の4サイトにお世話になりました。本当にありがとうございます!!
MA運営事務局の方々、弾さん、知り合いの方々
今回のきっかけを作っていただいた、プレゼンしませんか?と声を掛けてくださったMashup Award運営事務局の山本さん、山田さん、
仕事終わってからカフェなどで資料thonに付き合ってくださった方々、温かい声を掛けてくださった方々。
本当に感謝の気持ちでいっぱいです。心からお礼を申し上げます。
授賞式でしか味わえないような感動や刺激を沢山受けました。