Python+japrontoを試したときに作成したプロジェクトテンプレートを個人的な備忘録として投稿しておきます。
※長くなるので、ポイントとなる部分のみです。
1. 構成
※htmlはbootstap4を使うように設定しています。
(1) プロジェクトには、以下のフォルダで構成されます。
フォルダ名 | 説明 |
---|---|
├─api | プロジェクトの規定アプリを格納しているフォルダ |
├─apps | 各業務アプリの処理をこのフォルダ内に実装する |
├─config | データベース、ロガーの設定ファイル |
├─static | css, jsなどの静的ファイル |
├─templates | jinja2テンプレート用フォルダ |
(2) 具体的な構成は以下の通り
│ main.py
├─api
│ │ app.py
│ │ db.py
│ │ initlog.py
│ │ settings.py
├─apps
│ ├─admin
│ │ │ views.py
│ ├─auth
│ │ │ views.py
│ ├─index
│ │ │ views.py
│ ├─todos
│ │ │ service.py
│ │ │ views.py
│ ├─userrole
│ │ │ service.py
│ │ │ views.py
│ └─users
│ │ service.py
│ │ views.py
├─config
│ │ database.conf
│ │ logging.conf
├─static
│ ├─css
│ │ bootstrap.min.css
│ │ main.css
│ └─js
│ bootstrap.min.js
│ jquery-3.2.1.min.js
└─templates
│ base.html
├─admin
│ │ index.html
│ ├─userrole
│ │ create.html
│ │ delete.html
│ │ index.html
│ │ update.html
│ └─users
│ create.html
│ delete.html
│ index.html
│ update.html
├─auth
│ login.html
├─index
│ index.html
└─todos
create.html
delete.html
index.html
update.html
2. api
- api/app.py
japrontoの初期化、staticファイルのルーティングなどを行います
from japronto import Application
from api.db import connection
import api.initlog
import os.path
import os
from pathlib import Path
from jinja2 import Environment, FileSystemLoader, Template
# If necessary you can add the content type
CONTENTTYPES = {
'css':'text/css',
'js':'text/javascript',
'pdf':'application/pdf',
'xlsx':'application/vnd.ms-excel',
}
# extension
def get_extension(path):
root, ext = os.path.splitext(path)
return ext
# mimetype
def get_mimetype(path):
ext = get_extension(path)
if (ext[1:] in CONTENTTYPES):
return CONTENTTYPES[ext[1:]]
return "application/octet-stream"
# static files route
def static(request):
mimetype = get_mimetype(request.path[1:])
with open(request.path[1:], 'rb') as static_file:
return request.Response(body=static_file.read(), mime_type=mimetype)
# template engine
template_path=os.path.join(Path(__file__).resolve().parents[1], 'templates')
env = Environment(loader=FileSystemLoader(template_path))
# get jinja2 template
def get_tpl(filename):
return env.get_template(filename)
# decorator
def controller(func):
def inner(*args, **kwargs):
request = args[0]
url=request.path
loginuser = request.is_loggedin
if not loginuser:
return request.Response(headers={'Location': '/login'}, code=302)
elif (not loginuser['userrole']==0) and url.startswith("/admin"):
return request.Response(code=403, text="管理者のみ利用可能です")
else:
return func(*args, **kwargs)
return inner
# create japronto
def createApp():
app = Application()
# setup static files
# https://github.com/squeaky-pl/japronto/issues/43
app.router.add_route('/static/{1}', static)
app.router.add_route('/static/{1}/{2}', static)
app.router.add_route('/static/{1}/{2}/{3}', static)
# database connection extend
app.extend_request(connection, property=True)
return app
app=createApp()
- api/db.py
connection poolの生成などを行います。
import psycopg2
import psycopg2.extras
import psycopg2.pool
from api.settings import config_section
dbconf = config_section('DB')
host=dbconf.get('host')
port=dbconf.get('port')
dbname=dbconf.get('dbname')
user=dbconf.get('user')
password=dbconf.get('password')
minconn=dbconf.get('minconn')
maxconn=dbconf.get('maxconn')
# connection pool
connection_pool = psycopg2.pool.SimpleConnectionPool(
minconn=minconn, maxconn=maxconn,
host=host, port=port,
dbname=dbname, user=user, password=password)
# get cursor
def connection(request):
def do_callback(request):
connection_pool.putconn(request.extra['conn'])
if 'conn' not in request.extra:
request.extra['conn'] = connection_pool.getconn()
request.add_done_callback(do_callback)
return request.extra['conn']
- api/initlog.py
ログの設定
import yaml
import logging
from logging import config, getLogger
# logging setup
config.dictConfig(
yaml.load(open('config/logging.conf', encoding='UTF-8').read(), Loader=yaml.FullLoader))
- api/settings.py
config管理
import os
import configparser
from pathlib import Path
config = configparser.ConfigParser()
config.read('config/database.conf')
def config_section(key):
return config[key]
3. apps
- apps/todos/service.py
サービス層は、crudのapi的な役割を定義する。例として、todoアプリであれば以下のようにする。
def find_all(cur):
sql = "select * from todos"
dict_result = []
cur.execute(sql)
rows = cur.fetchall()
for row in rows:
dict_result.append(dict(row))
return dict_result
def find_by_id(cur, query):
sql = "select * from todos where id = %s "
cur.execute(sql, (query['id'],))
row = cur.fetchone()
return dict(row)
def insertdata(cur, query):
print(query)
sql = "insert into todos (todo) values (%s)"
cnt = cur.execute(sql, (query['todo'], ))
return cnt
def updatedata(cur, query):
print(query)
sql = "update todos set todo = %s where id = %s"
cnt = cur.execute(sql, (query['todo'], query['id'], ))
return cnt
def deletedata(cur, query):
print(query)
sql = "delete from todos where id = %s"
cnt = cur.execute(sql, (query['id'], ))
return cnt
- apps/todos/views.py
Django的にしています。controllerデコレータで、認証チェック、権限チェックが行われ、チェックOKなら、各種コントローラが実行されることになる。
from api.app import app, controller, get_tpl
from psycopg2.extras import DictCursor
from apps.todos.service import find_all, find_by_id, insertdata, updatedata, deletedata
@controller
def index(request):
loginuser = request.is_loggedin
html=get_tpl('/todos/index.html')
result={}
conn = request.connection
with conn.cursor(cursor_factory=DictCursor) as cur:
result=find_all(cur)
return request.Response(text=html.render(data=result, user=loginuser),mime_type='text/html')
@controller
def create(request):
loginuser = request.is_loggedin
html=get_tpl('/todos/create.html')
result={}
if request.method.lower()=="post":
conn = request.connection
with conn.cursor(cursor_factory=DictCursor) as cur:
result=insertdata(cur, request.form)
conn.commit()
return request.Response(text=html.render(data=result, user=loginuser),mime_type='text/html')
@controller
def update(request):
loginuser = request.is_loggedin
html=get_tpl('/todos/update.html')
result={}
conn = request.connection
with conn.cursor(cursor_factory=DictCursor) as cur:
if not request.method.lower()=="post":
result=find_by_id(cur, request.query)
else:
result=updatedata(cur, request.form)
conn.commit()
return request.Response(text=html.render(data=result, user=loginuser),mime_type='text/html')
@controller
def delete(request):
loginuser = request.is_loggedin
html=get_tpl('/todos/delete.html')
result={}
conn = request.connection
with conn.cursor(cursor_factory=DictCursor) as cur:
if not request.method.lower()=="post":
result=find_by_id(cur, request.query)
else:
result=deletedata(cur, request.form)
conn.commit()
return request.Response(text=html.render(data=result, user=loginuser),mime_type='text/html')
app.router.add_route('/todos', index)
app.router.add_route('/todos/create', create)
app.router.add_route('/todos/update', update)
app.router.add_route('/todos/delete', delete)
以下は、プロジェクトに最低限必要なアプリケーション
- apps/admin/views.py
管理者用のトップページ用コントローラ
from api.app import app, controller, get_tpl
@controller
def index(request):
loginuser = request.is_loggedin
html=get_tpl('/admin/index.html')
return request.Response(text=html.render(user=loginuser),mime_type='text/html')
app.router.add_route('/admin', index)
- apps/auth/views.py
ログイン認証用のコントローラ
from api.app import app, controller, get_tpl
from http.cookies import SimpleCookie
from psycopg2.extras import DictCursor
import hashlib
import datetime
import time
sessions = {}
# todo session timeout
timeout_minute=30
def is_user(request, username, password):
conn = request.connection
with conn.cursor(cursor_factory=DictCursor) as cur:
sql = "select a.* from users a where a.username = %s and a.password=%s "
cur.execute(sql, (username, password))
row = cur.fetchone()
if row:
return dict(row)
return None
def create_sessionid(user):
# sessionを生成
session_id = hashlib.sha256(user['username'].encode()).hexdigest()
# sessionは、辞書内に管理する。
sessions[session_id] = user
return session_id
def redirect(request, url, cookies=None):
return request.Response(headers={'Location': url}, code=302, cookies=cookies)
def get_cookie_sessionid(request):
if 'Cookie' in request.headers:
cookies = SimpleCookie()
cookies.load(request.headers['Cookie'])
if 'SessionID' in cookies:
return cookies['SessionID'].value
return None
# login controller
def login(request):
html=get_tpl('/auth/login.html')
if request.method.lower()=="post":
form = request.form
username=form['username']
password=form['password']
user = is_user(request, username, password)
if user:
session_id = create_sessionid(user)
cookies = SimpleCookie()
cookies['SessionID'] = session_id
return redirect(request, '/', cookies)
else:
errmsg = 'ユーザIDまたはパスワードが違います'
return request.Response(text=html.render(), mime_type='text/html')
def is_loggedin(request):
request.extra['authenticateduser']=None
session_id = get_cookie_sessionid(request)
if session_id and (session_id in sessions):
request.extra['authenticateduser']=sessions[session_id]
# 未ログインの状態
return request.extra['authenticateduser']
# logout controller
def logout(request):
session_id = get_cookie_sessionid(request)
sessions.pop(session_id)
return redirect(request, "/login")
app.router.add_route('/login', login)
app.router.add_route('/logout', logout)
# authentication extend
app.extend_request(is_loggedin, property=True)
- apps/index/views.py
ログイン後の初期表示画面用コントローラ
from api.app import app, get_tpl, controller
@controller
def index(request):
loginuser = request.is_loggedin
html=get_tpl('index/index.html')
return request.Response(text=html.render(user=loginuser),mime_type='text/html')
app.router.add_route('/', index)
以下、長くなるので割愛
apps/userrole
apps/users
4. config
- config/databaase.conf
コネクションプールの接続用プロパティ
[DB]
minconn=10
maxconn=50
host=localhost
port=5432
dbname=japrontodb
user=testuser
password=***********************
- config/logging.conf
logger用のプロパティファイル
version: 1
formatters:
custmoFormatter:
format: '%(asctime)s %(levelname)s - %(filename)s %(funcName)s %(lineno)d: %(message)s'
; datefmt: '%Y/%m/%d %H:%M:%S'
loggers:
test:
handlers: [fileRotatingHandler,consoleHandler]
level: DEBUG
qualname: test
propagate: no
file:
handlers: [fileRotatingHandler]
level: DEBUG
qualname: file
propagate: no
console:
handlers: [consoleHandler]
level: DEBUG
qualname: console
propagate: no
handlers:
fileRotatingHandler:
formatter: custmoFormatter
class: logging.handlers.TimedRotatingFileHandler
level: DEBUG
filename: /mnt/c/linux_home/japronto/log/application.log
encoding: utf8
when: 'D'
interval: 1
backupCount: 14
consoleHandler:
class: logging.StreamHandler
level: DEBUG
formatter: custmoFormatter
stream: ext://sys.stdout
root:
level: DEBUG
handlers: [fileRotatingHandler,consoleHandler]
5. static
割愛
6. templates
- templates/index/index.html
トップページ
{% extends 'base.html' %}
{% block content %}
<div class="jumbotron" style="min-height: 500px;">
<h2>Hi, {{ user.username }}, your role is {{ user.userrole }}</h2>
<h1 class="display-4">Welcome to My Project!!</h1>
</div>
{% endblock %}
- templates/auth/login.html
ログイン画面
{% extends 'base.html' %}
{% block content %}
<h1>Log in</h1>
<form method='post' action='/login' enctype='application/x-www-form-urlencoded'>
<div class='form-group' >
<label class='form-label'>username</label>
<input class='form-control' type='text' name='username'/>
</div>
<div class='form-group' >
<label class='form-label'>password</label>
<input class='form-control' type='password' name='password'/>
</div>
<button class='btn btn-primary' type='submit'>submit</button>
</form>
{% endblock %}
- templates/base.html
ページ全体のレイアウト
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta http-equiv="Pragma" content="no-cache">
<meta http-equiv="Cache-Control" content="no-cache">
<link rel="stylesheet" href="/static/css/bootstrap.min.css">
<link rel="stylesheet" href="/static/css/main.css">
<script type="text/javascript" src="/static/js/jquery-3.2.1.min.js"></script>
<script type="text/javascript" src="/static/js/bootstrap.min.js"></script>
<style>
.navi {display: flex; align-items: flex-end; font-size: 1.2em;}
.navi >div> a {padding: 5px;}
.left {display: flex; justify-content: flex-start;}
.right{display: flex; justify-content: flex-end;}
.title {font-family: Meiryo UI; font-size: 1.4em !important; color: white !important; min-width: 200px;}
</style>
</head>
<body>
<div class="container">
<div class="col-md-10 col-md-offset-1">
{% if user %}
<div class="right">logged in: {{ user.username }} </div>
{% endif %}
<header style="background: #263238;padding-left:5px;padding-right:5px;">
<div class="navi">
<div class="navi left" style="width:70%; ">
<div class="title">My Project</div>
{% if user %}
<div>
<a href="/todos">todo</a>
</div>
{% endif %}
</div>
<div class="navi right" style="width:29%; ">
{% if user and user.userrole==0 %}
<div>
<a href="/admin">admin</a>
</div>
{% endif %}
{% if user %}
<div>
<a href="/logout">logout</a>
</div>
{% endif %}
</div>
</div>
</header>
{% block content %}{% endblock %}
</div>
</div>
</body>
</html>
7. main.py
- main.py
from api.app import app
import apps.auth.views
import apps.index.views
import apps.admin.views
import apps.users.views
import apps.userrole.views
import apps.todos.views
if __name__ == '__main__':
app.run(host='127.0.0.1',port=7777)
8. table
create table "public".userrole (
roleid integer not null
, rolename character varying(32) not null
, primary key (roleid)
);
create table "public".users (
id serial primary key
, username character varying(32) not null
, password character varying(128) not null
, email character varying(70) not null
, userrole integer
, active character(1) not null
, created_at timestamp(6) without time zone default CURRENT_TIMESTAMP not null
);
create table "public".todos (
id bigserial primary key
, todo character varying(255) not null
);
9 その他
japronto備忘録(2) CRUDアプリケーションを自動的に生成するに続きます。