add sql cli and explain it in the readme

This commit is contained in:
forest 2020-05-14 20:05:02 -05:00
parent 8de802aff5
commit 0dc58ed6a8
6 changed files with 119 additions and 12 deletions

View File

@ -42,7 +42,7 @@ nano capsulflask/__init__.py
Run the app
```
FLASK_APP=capsulflask flask run
flask run
```
Run the app in gunicorn
@ -50,8 +50,49 @@ Run the app in gunicorn
.venv/bin/gunicorn --bind 127.0.0.1:5000 capsulflask:app
```
-----
# postgres database schema management
## cli
You can manually mess around with the database like this:
```
flask cli sql -f test.sql
```
```
flask cli sql -c 'SELECT * FROM vms'
```
This one selects the vms table with the column name header:
```
flask cli sql -c "SELECT string_agg(column_name::text, ', ') from information_schema.columns WHERE table_name='vms'; SELECT * from vms"
```
How to modify a payment manually, like if you get a chargeback or to fix customer payment issues:
```
$ flask cli sql -c "SELECT id, created, email, dollars, invalidated from payments"
1, 2020-05-05T00:00:00, forest.n.johnson@gmail.com, 20.00, FALSE
$ flask cli sql -c "UPDATE payments SET invalidated = True WHERE id = 1"
1 rows affected.
$ flask cli sql -c "SELECT id, created, email, dollars, invalidated from payments"
1, 2020-05-05T00:00:00, forest.n.johnson@gmail.com, 20.00, TRUE
```
How you would kick off the scheduled task:
```
flask cli cron-task
```
-----
## postgres database schema management
capsulflask has a concept of a schema version. When the application starts, it will query the database for a table named
`schemaversion` that has one row and one column (`version`). If the `version` it finds is not equal to the `desiredSchemaVersion` variable set in `db.py`, it will run migration scripts from the `schema_migrations` folder one by one until the `schemaversion` table shows the correct version.
@ -62,9 +103,9 @@ For example, the script named `02_up_xyz.sql` should contain code that migrates
In general, for safety, schema version upgrades should not delete data. Schema version downgrades will simply throw an error and exit for now.
-----
# how to setup btcpay server
## how to setup btcpay server
Generate a private key and the accompanying bitpay SIN for the bitpay API client.

1
app.py Normal file
View File

@ -0,0 +1 @@
from capsulflask import app

View File

@ -8,7 +8,7 @@ from flask import Flask
from flask_mail import Mail
from flask import render_template
from capsulflask import virt_model
from capsulflask import virt_model, cli
load_dotenv(find_dotenv())
@ -48,13 +48,14 @@ from capsulflask import db
db.init_app(app)
from capsulflask import auth, landing, console, payment, metrics
from capsulflask import auth, landing, console, payment, metrics, cli
app.register_blueprint(landing.bp)
app.register_blueprint(auth.bp)
app.register_blueprint(console.bp)
app.register_blueprint(payment.bp)
app.register_blueprint(metrics.bp)
app.register_blueprint(cli.bp)
app.add_url_rule("/", endpoint="index")

63
capsulflask/cli.py Normal file
View File

@ -0,0 +1,63 @@
import os
import re
from datetime import datetime
import click
from flask.cli import with_appcontext
from flask import Blueprint
from psycopg2 import ProgrammingError
from capsulflask.db import get_model, my_exec_info_message
bp = Blueprint('cli', __name__)
@bp.cli.command('sql')
@click.option('-f', help='script filename')
@click.option('-c', help='sql command')
@with_appcontext
def sql_script(f, c):
"""Run a sql script against the database. script is run 1 command at a time inside a single transaction."""
model = get_model()
script = ""
if f:
filepath = os.path.join(os.getcwd(), f)
if not os.path.isfile(filepath):
raise f"{filepath} is not a file"
with open(filepath, 'rb') as file:
script = file.read().decode("utf8")
elif c:
script = c
else:
click.echo(f"you must provide sql to run either inline with the -c argument or in a file with the -f argument")
return
commands = re.split(";\\s+", script)
for command in commands:
if command.strip() != "":
model.cursor.execute(command)
if re.match("^\\s*select", command, re.IGNORECASE) is not None:
for row in model.cursor.fetchall():
def format_value(x):
if isinstance(x, bool):
return "TRUE" if x else "FALSE"
if not x :
return "null"
if isinstance(x, datetime):
return x.isoformat()
return f"{x}"
click.echo(", ".join(list(map(format_value, row))))
else:
click.echo(f"{model.cursor.rowcount} rows affected.")
model.connection.commit()
@bp.cli.command('cron-task')
@with_appcontext
def cron_task():
print('a')

View File

@ -146,12 +146,12 @@ class DBModel:
def list_payments_for_account(self, email):
self.cursor.execute("""
SELECT dollars, invalidated, created
SELECT id, dollars, invalidated, created
FROM payments WHERE payments.email = %s""",
(email, )
)
return list(map(
lambda x: dict(dollars=x[0], invalidated=x[1], created=x[2]),
lambda x: dict(id=x[0], dollars=x[1], invalidated=x[2], created=x[3]),
self.cursor.fetchall()
))

View File

@ -49,11 +49,12 @@ CREATE TABLE vm_ssh_public_key (
);
CREATE TABLE payments (
id SERIAL,
email TEXT REFERENCES accounts(email) ON DELETE RESTRICT,
created TIMESTAMP NOT NULL DEFAULT NOW(),
dollars NUMERIC(8, 2) NOT NULL,
invalidated BOOLEAN NOT NULL DEFAULT FALSE,
PRIMARY KEY (email, created)
PRIMARY KEY (email, id)
);
CREATE TABLE login_tokens (
@ -74,8 +75,8 @@ CREATE TABLE payment_sessions (
CREATE TABLE unconfirmed_btcpay_invoices (
id TEXT PRIMARY KEY,
email TEXT REFERENCES accounts(email) ON DELETE RESTRICT,
created TIMESTAMP NOT NULL,
FOREIGN KEY (email, created) REFERENCES payments(email, created) ON DELETE CASCADE
payment_id INTEGER NOT NULL,
FOREIGN KEY (email, payment_id) REFERENCES payments(email, id) ON DELETE CASCADE
);
INSERT INTO os_images (id, template_image_file_name, description)
@ -100,7 +101,7 @@ INSERT INTO accounts (email)
VALUES ('forest.n.johnson@gmail.com');
INSERT INTO payments (email, dollars, created)
VALUES ('forest.n.johnson@gmail.com', 20.00, TO_TIMESTAMP('2020-05-05','YYYY-MM-DD'));
VALUES ('forest.n.johnson@gmail.com', 20.00, TO_TIMESTAMP('2020-05-05','YYYY-MM-DDTHH24-MI-SS'));
INSERT INTO vms (id, email, os, size)
VALUES ('capsul-yi9ffqbjly', 'forest.n.johnson@gmail.com', 'alpine311', 'f1-xx');