-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathapp.py
191 lines (155 loc) · 5.86 KB
/
app.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
#!/usr/bin/python3
# Copyright (c) BDist Development Team
# Distributed under the terms of the Modified BSD License.
import os
from decimal import Decimal, InvalidOperation
from logging.config import dictConfig
from flask import Flask, flash, jsonify, redirect, render_template, request, url_for
from flask_limiter import Limiter
from flask_limiter.util import get_remote_address
from psycopg.rows import namedtuple_row
from psycopg_pool import ConnectionPool
dictConfig(
{
"version": 1,
"formatters": {
"default": {
"format": "[%(asctime)s] %(levelname)s in %(module)s:%(lineno)s - %(funcName)20s(): %(message)s",
}
},
"handlers": {
"wsgi": {
"class": "logging.StreamHandler",
"stream": "ext://flask.logging.wsgi_errors_stream",
"formatter": "default",
}
},
"root": {"level": "INFO", "handlers": ["wsgi"]},
}
)
RATELIMIT_STORAGE_URI = os.environ.get("RATELIMIT_STORAGE_URI", "memory://")
app = Flask(__name__)
app.config.from_prefixed_env()
log = app.logger
limiter = Limiter(
get_remote_address,
app=app,
default_limits=["200 per day", "50 per hour"],
storage_uri=RATELIMIT_STORAGE_URI,
)
# Use the DATABASE_URL environment variable if it exists, otherwise use the default.
# Use the format postgres://username:password@hostname/database_name to connect to the database.
DATABASE_URL = os.environ.get("DATABASE_URL", "postgres://bank:bank@postgres/bank")
pool = ConnectionPool(
conninfo=DATABASE_URL,
kwargs={
"autocommit": True, # If True don’t start transactions automatically.
"row_factory": namedtuple_row,
},
min_size=4,
max_size=10,
open=True,
# check=ConnectionPool.check_connection,
name="postgres_pool",
timeout=5,
)
def is_decimal(s):
"""Returns True if string is a parseable Decimal number."""
try:
Decimal(s)
return True
except InvalidOperation:
return False
@app.route("/", methods=("GET",))
@app.route("/accounts", methods=("GET",))
@limiter.limit("1 per second")
def account_index():
"""Show all the accounts, most recent first."""
with pool.connection() as conn:
with conn.cursor() as cur:
accounts = cur.execute(
"""
SELECT account_number, branch_name, balance
FROM account
ORDER BY account_number DESC;
""",
{},
).fetchall()
log.debug(f"Found {cur.rowcount} rows.")
return render_template("account/index.html", accounts=accounts)
@app.route("/accounts/<account_number>/update", methods=("GET",))
@limiter.limit("1 per second")
def account_update_view(account_number):
"""Show the page to update the account balance."""
with pool.connection() as conn:
with conn.cursor() as cur:
account = cur.execute(
"""
SELECT account_number, branch_name, balance
FROM account
WHERE account_number = %(account_number)s;
""",
{"account_number": account_number},
).fetchone()
log.debug(f"Found {cur.rowcount} rows.")
# At the end of the `connection()` context, the transaction is committed
# or rolled back, and the connection returned to the pool.
return render_template("account/update.html", account=account)
@app.route("/accounts/<account_number>/update", methods=("POST",))
def account_update_save(account_number):
"""Update the account balance."""
balance = request.form["balance"]
if not balance:
raise ValueError("Balance is required.")
if not is_decimal(balance):
raise ValueError("Balance is required to be decimal.")
if Decimal(balance) < 0:
raise ValueError("Balance is required to be positive.")
with pool.connection() as conn:
with conn.cursor() as cur:
cur.execute(
"""
UPDATE account
SET balance = %(balance)s
WHERE account_number = %(account_number)s;
""",
{"account_number": account_number, "balance": balance},
)
# The result of this statement is persisted immediately by the database
# because the connection is in autocommit mode.
# The connection is returned to the pool at the end of the `connection()` context but,
# because it is not in a transaction state, no COMMIT is executed.
return redirect(url_for("account_index"))
@app.route("/accounts/<account_number>/delete", methods=("POST",))
def account_delete(account_number):
"""Delete the account."""
with pool.connection() as conn:
with conn.cursor() as cur:
with conn.transaction():
# BEGIN is executed, a transaction started
cur.execute(
"""
DELETE FROM depositor
WHERE account_number = %(account_number)s;
""",
{"account_number": account_number},
)
cur.execute(
"""
DELETE FROM account
WHERE account_number = %(account_number)s;
""",
{"account_number": account_number},
)
# These two operations run atomically in the same transaction
# COMMIT is executed at the end of the block.
# The connection is in idle state again.
# The connection is returned to the pool at the end of the `connection()` context
return redirect(url_for("account_index"))
@app.route("/ping", methods=("GET",))
@limiter.exempt
def ping():
log.debug("ping!")
return jsonify({"message": "pong!", "status": "success"})
if __name__ == "__main__":
app.run()