diff --git a/README.md b/README.md index ca5e41a..a205241 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/app.py b/app.py new file mode 100644 index 0000000..e8cf243 --- /dev/null +++ b/app.py @@ -0,0 +1 @@ +from capsulflask import app \ No newline at end of file diff --git a/capsulflask/__init__.py b/capsulflask/__init__.py index 1e5c1e7..ae52406 100644 --- a/capsulflask/__init__.py +++ b/capsulflask/__init__.py @@ -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") diff --git a/capsulflask/cli.py b/capsulflask/cli.py new file mode 100644 index 0000000..537d488 --- /dev/null +++ b/capsulflask/cli.py @@ -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') \ No newline at end of file diff --git a/capsulflask/db_model.py b/capsulflask/db_model.py index 07f1867..b6ceafd 100644 --- a/capsulflask/db_model.py +++ b/capsulflask/db_model.py @@ -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() )) diff --git a/capsulflask/schema_migrations/02_up_accounts_vms_etc.sql b/capsulflask/schema_migrations/02_up_accounts_vms_etc.sql index 12e1997..1b28535 100644 --- a/capsulflask/schema_migrations/02_up_accounts_vms_etc.sql +++ b/capsulflask/schema_migrations/02_up_accounts_vms_etc.sql @@ -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');