Python+ResponderでWEBアプリケーションを構築する。

PythonのWEBフレームワーク[「Responder」](https://python-responder.org/en/latest/index.html)を使ったWEBアプリケーションを作成してみます。
「Quick Start!」, 「Feature Tour」を参考に、私自身に最低限必要なものだけを取り上げています。

- 動作環境

| カテゴリ         |    　    |
|:-----------------|:-------------------|
| os       |        windows10 home        |
| python           |       Python 3.7.3       |
|responder|1.3.1 |

- install

pipでインストールするだけです。

```cmd:cmd.prompt
pip install responder
```

- 以下、すべて「run.py」でWEBサーバを起動します。

```cmd:cmd.prompt
python run.py
```

##1.hello world

```run.py
import responder

api = responder.API()

@api.route("/")
def hello_world(req, resp):
    resp.text = "hello, world!"

if __name__ == '__main__':
    api.run()
```

##2.Template Engine

- directory / file

``` 
root
│  run.py
│  
└─templates
        hello.html
```
templateは、「templates」フォルダの下に作成する。

- run.py

```run.py
import responder

api = responder.API()

@api.route("/html/{who}")
def hello_html(req, resp, *, who):
    resp.content = api.template('hello.html', who=who)

if __name__ == '__main__':
    api.run()
```

- hello.html

```hello.html
<h1>Hello {{ who }}</h1>
```

- 確認

[http://127.0.0.1:5042/html/john](http://127.0.0.1:5042/html/john)にアクセスする


##3.Returning JSON

- [Quick Start](https://python-responder.org/en/latest/quickstart.html#run-the-server)では、以下のようになっている。

```run.py
import responder

api = responder.API()

@api.route("/json/{who}")
def hello_to(req, resp, *, who):
    resp.media = {"hello": who}

if __name__ == '__main__':
    api.run()
```

- しかし、日本語だとunicodeで表示されてしまう。回避策として、json.dumpsを使う。

```run.py
import responder
import json

api = responder.API()

@api.route("/")
def hello_to(req, resp):
    # resp.media = {"hello": "斎藤さん"}
    resp.headers = {"Content-Type": "application/json; charset=utf-8"} 
    resp.content = json.dumps({"hello": "斎藤さん"}, ensure_ascii=False)

if __name__ == '__main__':
    api.run()
```

##4.File Upload

ここでは、staticファイルを参照する方法も載せておきます。

- ファイル・フォルダ構成

```
fileupload
│  run.py            ・・・(1)
└─templates
        index.html   ・・・(2)
        layout.html  ・・・(3)
        success.html ・・・(4)
static
└─css
       main.css      ・・・(5)
```

###(1) run.py

```python:run.py
from pathlib import Path
import responder
import requests

def parentdir(path='.', layer=0):
    return Path(path).resolve().parents[layer]

BASE_DIR = parentdir(__file__,1)

# ・・※1
api = responder.API(
    static_dir=str(BASE_DIR.joinpath('static')),
)

@api.route("/")
async def greet_world(req, resp):
    resp.content = api.template('index.html')

@api.route("/upload")
async def upload_file(req, resp):
    @api.background.task
    def process_data(data):
        file=data['file']
        f = open('./{}'.format(data['file']['filename']), 'wb')
        f.write(file['content'])
        f.close()

    data = await req.media(format='files')
    process_data(data)

    resp.content = api.template('success.html')

if __name__ == '__main__':
    api.run()
```
※1・・staticファイルを使用するときは、APIを定義するときに「static_dir」を指定します。


###(2) index.html

```html:index.html
{% extends "layout.html" %}
{% block content %}
  <h1>File Upload With Responder</h1>
  <form id="fileupload" action="/upload" method="post" enctype="multipart/form-data">
      <input name="file" type="file"/>
      <button type="submit">送信</button>
  </form>
{% endblock %}
```

###(3) layout.html

```html:layout.html
<!doctype html>
<html>
<head>
<meta charset="utf-8"/>
<title>{{ title }}</title>
<link rel="shortcut icon" href="">
<link href="/static/css/main.css" rel="stylesheet" type="text/css" media="all">
</head>
<body>
{% block content %}
<!-- ここにメインコンテンツを書く -->
{% endblock %}
</body>
</html>
```

###(4) success.html

```html:success.html
{% extends "layout.html" %}
{% block content %}
  <h1>File Upload With Responder(Result)</h1>
  <p>ファイルをアップロードしました。</p>
  <a href="#" onclick="history.back(-1); return false;">戻る</a>
{% endblock %}
```

###(5) main.css

```css:main.css
body {
    background: rgb(50, 54, 57);
    color: white;
  }
```
- darkmode風にしています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/246741/92f4beb5-62c7-bceb-0697-6591d60af9d1.png)

##5.ユーザ認証

- ファイル・フォルダ構成

```
userauth
│  run.py             ・・・・(1)
│  
└─templates
        layout.html   ・・・・(2)
        login.html    ・・・・(3)
        logout.html   ・・・・(4)
        top.html      ・・・・(5)
static
└─css
       main.css
```

###(1)run.py

```python:run.py
# -*- coding: utf-8 -*-
from pathlib import Path
import responder

def parentdir(path='.', layer=0):
    return Path(path).resolve().parents[layer]

BASE_DIR = parentdir(__file__,1)

api = responder.API(
    static_dir=str(BASE_DIR.joinpath('static')),
)

users = {
    "admin": "password",
    "guest": "password"
}

@api.route(before_request=True)
def prepare_response(req, resp):
    # 静的ファイルはOK
    if req.url.path.startswith('/static/'):
        return
    # パスがログインページならOK
    if req.url.path == '/login':
        return
    # セッションにusernameが保存されている場合、OK
    if req.session.get('username') is not None:
        return
    # 上記以外はすべて認証が必要
    api.redirect(resp, '/login')

@api.route("/")
async def hello(req, resp):
    api.redirect(resp, "/login")

def isauthenticated(username, password):
    if not username in users:
        return False    
    if users.get(username) != password:
        return False
    return True

@api.route("/login")
class login:
    async def on_get(self, req, resp):   # or on_get..
        # getの場合
        resp.content = api.template('login.html')

    async def on_post(self, req, resp):   # or on_get..
        data = await req.media()
        # postの場合
        errmessages=[]
        username=data.get('username')
        password=data.get('password')
        if not isauthenticated(username, password):
            errmessages.append('ユーザ名またはパスワードに誤りがあります')
            loginredirect="/top"
            resp.content = api.template('login.html', errmessages=errmessages)
            return

        resp.session['username']=username
        api.redirect(resp, "/top")

@api.route("/top")
async def top(req, resp):
    authenticateduser = req.session.get('username')
    resp.content = api.template('top.html', authenticateduser=authenticateduser)

@api.route("/logout")
async def logout(req, resp):
    req.session.pop('username')
    resp.content = api.template('logout.html')

if __name__ == '__main__':
    api.run()

```
###(2)layout.html

```html:layout.html
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta charset="utf-8">
<title></title>
<meta name="description" content="">
<meta name="author" content="">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="/static/css/main.css" rel="stylesheet" type="text/css" media="all">
<link rel="shortcut icon" href="">
</head>
<body>
    <main>
        <div id="content">
            {% for errmessage in errmessages %}
                <p class="error">{{ errmessage }}</p>
            {% endfor %} 
            {% block content %}
            {% endblock %}
        </div>
        <nav>
            {% if authenticateduser %}
            <a href="/logout" style="color:white;">ログアウト</a>
            {% endif %}
        </nav>
    </main>
</body>
</html>
```

###(3)login.html

```html:login.html
{% extends "layout.html" %}
{% block content %}
<form id="loginForm" action="/login" method="post">
  <h1 style="color: white;">ログイン認証</h1>
  <label>username:</label>
  <input type="text" name="username" placeholder="username" value="" required>
  <br/>
  <label>password:</label>
  <input type="password" name="password" placeholder="password" value="" required>
  <br/>
  <br/>
  <input type="submit" value="ログイン">
</form>

{% endblock %}
```

###(4)logout.html

```html:logout.html
{% extends "layout.html" %}
{% block content %}

<h1>ログアウト</h1>

<p>ご利用ありがとうございました。</p>
<a href="/login" style="color:white;">ログイン</a>

{% endblock %}
```

###(5)top.html

```html:top.html
{% extends "layout.html" %}
{% block content %}

<h1>Hello {{ authenticateduser }} さん</h1>

{% endblock %}
```

##6.データベース接続

[python+responderでRest APIを作る](https://qiita.com/t-iguchi/items/07e776a1550f01c08a6b)にて。

##7.その他

まだまだ他にもあります。本家サイトを参考してください。


- **2019.05.03 追記**

##8.download file

サーバ上のファイル（CSV, PDF）をダウンロードします。

- ファイル・フォルダ構成

```
filedownload
   run.py     ・・・・(1)
   sample.pdf
   所属.csv
```

###(1).run.py

```python:run.py
import responder
import urllib.parse
from pathlib import Path

api = responder.API()

def parentdir(path='.', layer=0):
    return Path(path).resolve().parents[layer]

BASE_DIR = parentdir(__file__,0)

CONTENTTYPES = {
 'plain':'text/plain', 
 'csv':'text/csv',
 'html':'text/html',
 'js':'text/javascript',
 'json':'application/json',
 'pdf':'application/pdf',
 'xlsx':'application/vnd.ms-excel',
 'ppt':'application/vnd.ms-powerpoint',
 'docx':'application/msword',
 'jpeg':'image/jpeg',
 'png':'image/png',
 'gif':'image/gif',
 'bmp':'image/bmp',
 'zip':'application/zip',
 'tar':'application/x-tar'
}

def readf(filename):
    path=BASE_DIR.joinpath(filename)
    with open(path,'rb') as f:
        dt = f.read()
    return dt

@api.route("/csv/{filename}")
def hellocsv(req, resp, *, filename):
    downloadfilename=urllib.parse.unquote(filename, 'utf-8')
    resp.headers['Content-Type'] = CONTENTTYPES.get('csv')
    resp.headers['Content-Disposition'] = 'attachment; filename='+ filename
    resp.content=readf(downloadfilename)

@api.route("/pdf/{filename}")
def hellopdf(req, resp, *, filename):
    downloadfilename=urllib.parse.unquote(filename, 'utf-8')
    resp.headers['Content-Type'] = CONTENTTYPES.get('pdf')
    resp.headers['Content-Disposition'] = 'attachment; filename='+ filename
    resp.content=readf(downloadfilename)

if __name__ == '__main__':
    api.run()
```

###(2).確認

[http://127.0.0.1:5042/csv/確認.csv](http://127.0.0.1:5042/csv/%E6%89%80%E5%B1%9E.csv)
[http://127.0.0.1:5042/pdf/sample.pdf](http://127.0.0.1:5042/pdf/sample.pdf)