「Python3 + Flask + GitLabのOAuth2認可」サンプルを作成しました
結構時間が経ってしまいましたが、先日の記事「「Python3 + Flask + はてなAPIのOAuth認証」サンプルを作成(移植?)しました - msizの日記」の続きです。
前回はOAuth1認可のサンプルでしたが、今回はOAuth2のサンプルを作成しました。認可側は、将来コードの公開とかに便利かもしれないので、GitLabにしています。(GitHubの方がメジャーだと思いますが、無料アカウントを以前に作ったことがあり、個人で2つ無料アカウントを持てないようなので、念のために避けました)
フレームワークとして Flaskを、OAuth ライブラリとして requests-oauthlibを利用したサンプルプログラムです。
簡単な使い方は以下のとおり。
GitLabにサインアップ・サインインして、OAuth2認可したいアプリを登録
- アプリ登録の手順はGitLabの資料を参照
- アプリを登録するとき、以下の点に注意してください
- 「scopes」設定で「read_user」にチェックを入れる
- 「Redirect URL」に「http://localhost:5000/callback」を含める
- 登録完了後に表示される「Application Id」と「Secret」を控えておく(後で使います)
- python3でvenv環境を作成します
- Flask と requests-oauthlibをvenv環境でインストールします
- 下のPythonコードを保存します (oauth_samle_app.py とします)
- 環境変数 CLIENT_KEY, CLIENT_SECRET に、手順1で登録したアプリの Application Id, Secret を設定して実行します
$ python3 -m venv gitlab-oauth-sample $ cd gitlab-oauth-sample $ source bin/activate # 大抵は、この後プロンプトの表示が変化します。ちなみにvenv環境ぬけるときは「deactivate」を実行 $ pip install Flask requests-oauthlib $ vim oauth_samle_app.py # vimに限らず好きな方法で、Pythonコードを保存 $ env CLIENT_KEY=<YOUR_KEY> CLIENT_SECRET=<YOUR_SECRETE> python oauth_samle_app.py
... で起動してから http://localhost:5000 に Web ブラウザでアクセスして下さい。 うまくいけば、json形式で返された、GitLabでのプロフィールが表示されます。
※なお、Flaskに関してはクイックスタート、チュートリアル(日本語訳)を参考にしています。
(ここから「oauth_samle_app.py」コード)
from requests_oauthlib import OAuth2Session from flask import Flask, request, redirect, session, url_for from flask.json import jsonify import os app = Flask(__name__) # This information is obtained upon registration of a new Gitlab OAuth # application here: https://gitlab.com/oauth/applications/ client_id = os.environ.get("CLIENT_KEY", "<your client key>") client_secret = os.environ.get("CLIENT_SECRET", "<your client secret>") # gitlab api urls and OAuth2 scope authorization_base_url = 'https://gitlab.com/oauth/authorize' token_url = 'https://gitlab.com/oauth/token' user_api_url = 'https://gitlab.com/api/v4/user' scope = ['read_user'] @app.route("/") def demo(): """Step 1: User Authorization. Redirect the user/resource owner to the OAuth provider (i.e. Gitlab) using an URL with a few key OAuth parameters. """ redirect_uri = url_for('.callback', _external=True) app.logger.debug("redirect_uri: {}".format(redirect_uri)) gitlab = OAuth2Session(client_id, redirect_uri=redirect_uri, scope=scope) authorization_url, state = gitlab.authorization_url(authorization_base_url) # State is used to prevent CSRF, keep this for later. session['oauth_state'] = state return redirect(authorization_url) # Step 2: User authorization, this happens on the provider (gitlab.com). @app.route("/callback", methods=["GET"]) def callback(): """ Step 3: Retrieving an access token. The user has been redirected back from the provider to your registered callback URL. With this redirection comes an authorization code included in the redirect URL. We will use that to obtain an access token. """ redirect_uri = url_for('.callback', _external=True) gitlab = OAuth2Session(client_id, state=session['oauth_state'], redirect_uri=redirect_uri) app.logger.debug("request.url: {}".format(request.url)) token = gitlab.fetch_token(token_url, client_secret=client_secret, code=request.args.get('code')) # At this point you can fetch protected resources but lets save # the token and show how this is done from a persisted token # in profile() func. session['oauth_token'] = token return redirect(url_for('.profile')) @app.route("/profile", methods=["GET"]) def profile(): """Fetching a protected resource using an OAuth 2 token. """ gitlab = OAuth2Session(client_id, token=session['oauth_token']) return jsonify(gitlab.get(user_api_url).json()) if __name__ == "__main__": # This allows us to use a plain HTTP callback os.environ['OAUTHLIB_INSECURE_TRANSPORT'] = "1" app.secret_key = os.urandom(24) app.run(host='0.0.0.0', port=5000, debug=True)
「Python3 + Flask + はてなAPIのOAuth認可」サンプルを作成(移植?)しました
gistにあるPythonではてなの OAuth 対応 API を利用する をベースに、Python3 対応の OAuth1 サンプルにしました(記事作成時点で、はてなはOAuth2に対応していない)。
フレームワークとして Flask を、OAuth ライブラリとして requests-oauthlibを利用したサンプルプログラムです。
- (はてな側の準備)はてなに新しいアプリケーションを登録(consumer_keyとconsumer_secretを用意)します(やり方は「[python3][プログラミング]Python3 はてな APIのOAuth認証する(ほぼコピペでOK) - lisz-works」あたりを参照ください)
- ここで作成した「新しいアプリケーション」は削除できるか不明なので、「my-oauth-test」みたいな感じで、いつでも無効にできるような名前にすると無難かと思います
- 権限は read_public だけで構いません(余計な権限はつけない方が無難です)
- python3でvenv環境を作成します
- Flask と requests-oauthlibをvenv環境でインストールします
- 下のPythonコードを保存します (oauth_consumer.py とします)
- 環境変数 CONSUMER_KEY, CONSUMER_SECRET に自分の consumer_key, consumer_secret を設定して実行します
$ python3 -m venv hatena-oauth-sample $ cd hatena-oauth-sample $ source bin/activate # 大抵は、この後プロンプトの表示が変化します。ちなみにvenv環境ぬけるときは「deactivate」を実行 $ pip install Flask requests-oauthlib $ vim oauth_consumer.py # vimに限らず好きな方法で、Pythonコードを保存 $ CONSUMER_KEY=<YOUR_KEY> CONSUMER_SECRET_KEY=<YOUR_SECRETE> python oauth_consumer.py
... で起動してから http://localhost:5000 に Web ブラウザでアクセスして下さい。
OAuth認証が失敗してそうな時は、コマンド実行した端末に表示されているログを確認してください。
また、 Flask のデバッグ・モードを有効にしているので、例外の stacktrace がブラウザに表示された例外画面になった場合は、 ブラウザ上で任意の frame で Python コマンドを実行できます(やり方は、例外画面のメッセージを確認してください)。
作業中に入れたコメントのいくつかは、参考までに残しています。Flask、OAuthはよく分かってないので、お気づきの点などあればコメントしてもらえるとありがたいです。
#!/usr/bin/env python3 # -*- coding: utf-8 -*- # oauth_consumer.py import os import urllib.parse try: import json except ImportError: import simplejson as json import flask from flask import ( session, request, redirect, url_for, render_template_string, ) #import oauth2 as oauth import requests from requests_oauthlib import OAuth1 SECRET_KEY = 'FLASK_SEC_KEY' # used by flask to encrypt session info SCOPE = 'read_public' CONSUMER_KEY = os.environ.get('CONSUMER_KEY', 'HATENA_CONSUMER_KEY') CONSUMER_SECRET = os.environ.get('CONSUMER_SECRET_KEY', 'HATENA_SECRET_KEY') DEBUG = True REQUEST_TOKEN_URL='https://www.hatena.com/oauth/initiate' ACCESS_TOKEN_URL='https://www.hatena.com/oauth/token' AUTHORIZE_URL='https://www.hatena.ne.jp/oauth/authorize' HATENA_USERINFO_URL='http://n.hatena.com/applications/my.json' app = flask.Flask(__name__) app.secret_key = SECRET_KEY app.config['DEBUG'] = DEBUG TEMPLATE = """ <html> <head> <titile></title> </head> <body> {% if user %} <p> Hello {{ user.display_name }}(id:{{ user.url_name }}) <img src="{{ user.profile_image_url }}"> </p> <p><a href="{{ url_for('logout') }}">LOGOUT</a></p> {% else %} <p>Hello GUEST</p> <p><a href="{{ url_for('login') }}">LOGIN</a></p> {% endif %} </body> </html> """ @app.route('/') def index(): app.logger.debug("index() is called") ctx = { 'user': None} access_token = session.get('access_token') if access_token: # access_tokenなどを使ってAPIにアクセスする auth = OAuth1( CONSUMER_KEY, CONSUMER_SECRET, access_token['oauth_token'], # 'resource_owner_key' in OAuth1 param access_token['oauth_token_secret']) # 'resource_owner_secret' r = requests.post(HATENA_USERINFO_URL, auth=auth) if not r.ok: on_oauth_error(r) ctx['user'] = json.loads(r.text) return render_template_string(TEMPLATE, **ctx) # リクエストトークン取得から認証用URLにリダイレクトするための関数 @app.route('/login') def login(): # リクエストトークンの取得 auth = OAuth1( CONSUMER_KEY, CONSUMER_SECRET, callback_uri=url_for('on_auth', _external=True)) # need '_external' flag to avoid rejection from hatena oauth server app.logger.debug("auth: {}".format(vars(auth))) # scope を POST データで指定して、 request_token 取得 URL へ POST 実行 r = requests.post(REQUEST_TOKEN_URL, auth=auth, data='scope={}'.format(urllib.parse.quote(SCOPE))) if not r.ok: on_oauth_error(r) request_token = dict(urllib.parse.parse_qsl(r.text)) # セッションへリクエストトークンを保存しておく app.logger.debug("request_token: {}".format(request_token)) session['request_token'] = request_token # 認証用URLにリダイレクトする return redirect('{}?oauth_token={}'.format( AUTHORIZE_URL, session['request_token']['oauth_token'])) # セッションに保存されたトークンを破棄しログアウトする関数 @app.route('/logout') def logout(): if session.get('access_token'): session.pop('access_token') if session.get('request_token'): session.pop('request_token') return redirect(url_for('index')) # 認証からコールバックされ、アクセストークンを取得するための関数 @app.route('/on-auth') def on_auth(): # login()処理中に session へ保存していた request_token を取得 request_token = session.get('request_token') # 認証用URLからのリダイレクト時に設定された oauth_verifier を取得 oauth_verifier = request.args['oauth_verifier'] # request_token と oauth_verifier を用いてアクセストークンを取得 auth = OAuth1( CONSUMER_KEY, CONSUMER_SECRET, request_token['oauth_token'], # 'resource_owner_key' in OAuth1 param request_token['oauth_token_secret'], # 'resource_owner_secret' verifier=oauth_verifier) r = requests.post(ACCESS_TOKEN_URL, auth=auth) if not r.ok: on_oauth_error(r) access_token = dict(urllib.parse.parse_qsl(r.text)) # セッションに対し、request_token を削除、アクセストークンを記録 if session.get('request_token'): session.pop('request_token') session['access_token'] = access_token return redirect(url_for('index')) def on_oauth_error(res): if not res.ok: app.logger.debug("response header: {}".format(res.headers)) app.logger.debug("response content: {}".format(res.content)) res.raise_for_status() if __name__ == '__main__': app.run(host='0.0.0.0', port=5000, debug=True)
2019-11-30追記:「OAuthは認証ではなく認可」という説明をどこかで 目にしたので、タイトルを変更しました(認証→認可)。
lenovo E130でVirtualboxを実行するとCPU使用率が70-80%になる
タイトルの通りですが、少し前から手元のlenovo E130でVirtualboxを実行するとCPU使用率が70-80%になる現象が起きていました。
先日は間違えて休止状態にしないまま持ち運んだら、リュックサックの外側からでも分かるくらいマシン本体がものすごく熱くなってしまいました。
あと、これは関係あるか分かりませんがVirtualboxしばらく使っていると画面が黒くなって点滅しているマウスカーソルしか表示されないとか、resourceがど~たらというエラーでVirutalboxが再起動できなくなったりFirefoxがサイトへアクセスしなくなったり、いろいろ不便でした。
とりあえず、手元の環境では原因が分かって解決したので、せっかくだからメモ。
原因
LenovoノートPCのカメラ関係のアプリっぽい「vcamsvc.exe」が、Virtualboxを起動するとCPUを50%程度使うようになる(CPUコアのひとつ分を全部使ってたのかも)。
対応
参照サイト: Windows7でCPU使用率が100%のまま下がらない原因と対応 | 雑記帳
- 「コントロールパネル ⇒ 管理ツール ⇒ サービス」でサービスの管理ツール?を起動。
- 一覧から「ThinkVantage Virtual Camera Controller」を右クリックして「停止」。
念のため、右クリックしてプロパティを表示してから、「スタートアップの種類」を「無効」にもしておきました。
調査方法
- タスクマネージャを実行して「パフォーマンス」タブから「リソースモニター」を実行*1。
- リソースモニターで「CPU」タブを選択して、「プロセス」一覧を表示(「プロセス」行しかない場合は一覧が折り畳まれているので、右の方にある小さな丸いボタンを押す)。
- 「CPU」でソートして、CPUを使用しているプロセスを調査。
*1:タスクマネージャの「プロセス」タブの一覧では見つけられませんでした
Packaging Python Projectsの日本語仮訳
少し調べてたんで、はてな記法の練習も兼ねてPackaging Python Projects — Python Packaging User Guideの日本語版の仮訳を投稿。
原文のライセンスはCreative Commons Attribution-ShareAlikeというものですので、必要に応じて参照ください。
当然、日本語の誤記などは私の責任なので、そのあたりは原文の作者へは問合せしないようにしてください。
続きを読むSphinxで日本語文章の不自然な空白をなくしたい
Sphinx拡張機能を使って日本語文章の不自然な空白をなくす
sphinx(日本のユーザ会)で、htmlなどを作成する元ネタrestructuredtextファイルを作成するとき日本語の途中で改行を入れると、htmlなどへ変換したときにその改行箇所で空白文字が入ってしまい、ブラウザで見るときにちょっと見た目が悪くなったりします。
5年くらい前に以下の記事で対応策っぽいものを書いてましたが、数年振りにSphinxを使ってみたら、当時の記事の内容をすっかり忘れていたのでメモ。
msiz.hatenablog.jp
conf.pyの編集(1)
「sphinx-quickstart」を実行したりして作成されるSphinxの設定ファイル「conf.py」で、拡張機能(を実装したPythonモジュール・パッケージ)を探すpathを調整:
... import os import sys sys.path.insert(0, os.path.abspath('exts')) ...
sys.pathに追加しているだけですね。この例の場合、conf.pyのあるディレクトリに「exts」ディレクトリを作成し、そこに追加のPythonファイルを置けば探し出して読み込んでくれる。
なお、「sphinx-quickstart」で作成したconf.pyだと、それっぽい記述がコメントアウトされているので、そこを編集。
拡張機能を実装
今回は「japanese_trunc_whitespace.py」というファイル名にして、「exts」に置きました。
#!/usr/bin/python #from docutils.core import publish_doctree, publish_from_doctree #import sys, codecs, re, optparse from docutils.nodes import paragraph, Text import re def japanese_truncate_whitespace(app, doctree, docname): para_list = get_para_list(doctree) strip_spaces_between_uchars(para_list) strip_spaces_around_uchars_paragraph_children(para_list) # in order no to change literals, select only 'paragraph' nodes # def get_para_list(dtree): para_list = [] for node in dtree.traverse(): if isinstance(node, paragraph): para_list.append(node) return para_list def strip_spaces_between_uchars(para_list): # non-ascii [\n\r\t] non-ascii __RGX = re.compile(r'([^!-~])[\n\r\t]+([^!-~])') # modify text inside Text node # for para in para_list: for node in para.traverse(): if isinstance(node, Text): newtext = node.astext() newtext = __RGX.sub(r"\1\2", newtext) node.parent.replace(node, Text(newtext)) def strip_spaces_around_uchars_paragraph_children(para_list): # non-ascii [\s]* <End-of-TEXT> __RGX1 = re.compile(r'([^!-~])[\s]*$') # <Beginning-of-TEXT> [\s]* non-ascii __RGX2 = re.compile(r'^[\s]*([^!-~])') # modify texts over 2 nodes # (paragraph node can have children of Inline (reference, etc) nodes) # for para in para_list: prev_textnode = Text("") for node in para.traverse(): new_textnode = None if isinstance(node, Text): prevtext = prev_textnode.astext() newtext = node.astext() if __RGX1.search(prevtext) and __RGX2.search(newtext): new_prev_textnode = Text(prev_textnode.astext().rstrip()) new_textnode = Text(newtext.lstrip()) prev_textnode.parent.replace(prev_textnode, new_prev_textnode) node.parent.replace(node, new_textnode) new_prev_textnode.parent = prev_textnode.parent new_textnode.parent = node.parent prev_textnode = new_textnode if new_textnode else node def setup(app): app.add_config_value('japanese_trunc_whitespace', True, True) app.connect("doctree-resolved", japanese_truncate_whitespace)
最後の「setup(app)」で、今回の拡張機能を使用するかを「conf.py」から設定できるようにしています。
conf.pyの編集(2)
conf.pyの適当な場所で、以下の記述を追加
... extensions = [ 'japanese_trunc_whitespace', ... # 他にも使用する拡張機能がある場合は指定 ] japanese_trunc_whitespace = True ...
上手くいけば、「make html」とかで作成したhtmlで、改行箇所の余分な空白がなくなるはず
IBM Cloudでテスト用gitリポジトリを作成
取り急ぎ、個人用のメモなので、分かる人しかわからない書き方をします。
以下の準備はできていることを前提として、説明は省略
なお、手元ではVirtualbox上でCentOSをゲストOSにして作業しているので、それに依存している記述があるかも。
手順など
ここから、手順などのメモ
ローカルの作業用ディレクトリを用意し、必要なファイルを作成
mkdir /tmp/gitweb-app cd /tmp/gitweb-app vim requirements.txt vim manifest.yml
作成するファイルの内容は以下のとおり
- requirements.txt
dulwich
- manifest.yml
applications: - name: dulwich-web routes: # <unique-prefix>は独自の値に変える - route: unique-prefix-dulwich-web.mybluemix.net memory: 32M command: git init $HOME/gitrepo && python -m dulwich.web -l 0.0.0.0 -p $PORT $HOME/gitrepo
IBM CloudのPublic Cloud Foundryにデプロイ
cf push
動作確認
git clone http://unique-prefix-dulwich-web.mybluemix.net gitrepo1 cd gitrepo1 echo test > testfile.txt git add testfile.txt git commit -m "test commit" testfile.txt git push origin master