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