add sql cli and explain it in the readme
This commit is contained in:
		
							
								
								
									
										49
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										49
									
								
								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. 
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -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
									
								
							
							
						
						
									
										63
									
								
								capsulflask/cli.py
									
									
									
									
									
										Normal 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')
 | 
			
		||||
@ -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()
 | 
			
		||||
    ))
 | 
			
		||||
  
 | 
			
		||||
 | 
			
		||||
@ -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');
 | 
			
		||||
 | 
			
		||||
		Reference in New Issue
	
	Block a user