commit e00284bc7e2a25b72fad0d43736daecc22ccabf3 Author: lalanza808 Date: Mon Feb 13 08:26:57 2023 -0800 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b6e4761 --- /dev/null +++ b/.gitignore @@ -0,0 +1,129 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +pip-wheel-metadata/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..041460c --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 lalanza808 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..3c403cd --- /dev/null +++ b/Makefile @@ -0,0 +1,17 @@ +.PHONY: format help + +# Help system from https://marmelab.com/blog/2016/02/29/auto-documented-makefile.html +.DEFAULT_GOAL := help + +help: + @grep -E '^[a-zA-Z0-9_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}' + +setup: ## Establish local environment with dependencies installed + python3 -m venv .venv + .venv/bin/pip install -r requirements.txt + +up: ## Build and run the required containers by fetching binaries + docker-compose -f docker-compose.yaml up -d + +shell: ## Start Flask CLI shell + FLASK_APP=app/app.py FLASK_SECRETS=config.py FLASK_DEBUG=0 FLASK_ENV=production .venv/bin/flask shell diff --git a/README.md b/README.md new file mode 100644 index 0000000..486c901 --- /dev/null +++ b/README.md @@ -0,0 +1,2 @@ +# lza-quart-app +Template project for Quart (Python/Flask) applications. diff --git a/app.py b/app.py new file mode 100644 index 0000000..06a21e1 --- /dev/null +++ b/app.py @@ -0,0 +1,7 @@ +from myapp.factory import create_app + + +app = create_app() + +if __name__ == '__main__': + app.run() diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 0000000..768e7e3 --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,16 @@ +version: '3' +services: + db: + image: postgres:9.6.15-alpine + ports: + - 127.0.0.1:5432:5432 + environment: + POSTGRES_PASSWORD: ${DB_PASS} + POSTGRES_USER: ${DB_USER:-myapp} + POSTGRES_DB: ${DB_NAME:-myapp} + volumes: + - ${DATA_DIR:-./data/postgresql}:/var/lib/postgresql/data + cache: + image: redis:latest + ports: + - 127.0.0.1:6379:6379 diff --git a/env-example b/env-example new file mode 100644 index 0000000..f8ed753 --- /dev/null +++ b/env-example @@ -0,0 +1,16 @@ +DB_PASS=xxxxxxxxxxxxxxxxxxx +DB_USER=myapp +DB_NAME=myapp +DB_HOST=localhost + +XMR_WALLET_PATH=/data/xmr-wallet +XMR_WALLET_PASS=xxxxxxxxxxxxxxxxxxx +XMR_WALLET_RPC_USER=xxxxxxxxxx +XMR_WALLET_RPC_PASS=xxxxxxxxxxxxxxxxxxx +XMR_WALLET_RPC_ENDPOINT=http://localhost:9090 +XMR_DAEMON_URI=http://super.fast.node.xmr.pm:38089 + +SITE_NAME=myapp +SECRET_KEY=xxxxxxxxxxxxxxxxxxx +STATS_TOKEN=xxxxxxxxxxxxxxxxxxxx +SERVER_NAME=localhost:5000 diff --git a/myapp/__init__.py b/myapp/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/myapp/cli.py b/myapp/cli.py new file mode 100644 index 0000000..5000486 --- /dev/null +++ b/myapp/cli.py @@ -0,0 +1,30 @@ +import click +from quart import Blueprint, current_app + +from myapp.models import MyThing +from myapp.factory import db + + +bp = Blueprint('filters', 'filters') + +@bp.cli.command('init') +def init(): + import app.models + db.create_all() + +@bp.cli.command('delete') +@click.argument('thing_id') +def delete(thing_id): + thing = MyThing.query.get(thing_id) + if thing: + db.session.delete(thing) + db.session.commit() + click.echo(f'MyThing {thing.id} was deleted') + else: + click.echo('MyThing ID does not exist') + +@bp.cli.command('list') +def list_things(): + thing = MyThing.query.all() + for i in thing: + click.echo(i.id) diff --git a/myapp/config.py b/myapp/config.py new file mode 100644 index 0000000..7efc5c0 --- /dev/null +++ b/myapp/config.py @@ -0,0 +1,33 @@ +from dotenv import load_dotenv +from secrets import token_urlsafe +from os import getenv + + +load_dotenv() + +# Site meta +SITE_NAME = getenv('SITE_NAME', 'MyApp') +SECRET_KEY = getenv('SECRET_KEY') +STATS_TOKEN = getenv('STATS_TOKEN', token_urlsafe(8)) +SERVER_NAME = getenv('SERVER_NAME', 'localhost:5000') + +# Crypto RPC +XMR_WALLET_PASS = getenv('XMR_WALLET_PASS') +XMR_WALLET_RPC_USER = getenv('XMR_WALLET_RPC_USER') +XMR_WALLET_RPC_PASS = getenv('XMR_WALLET_RPC_PASS') +XMR_WALLET_RPC_ENDPOINT = getenv('XMR_WALLET_RPC_ENDPOINT') +XMR_DAEMON_URI = getenv('XMR_DAEMON_URI') + +# Database +DB_HOST = getenv('DB_HOST', 'localhost') +DB_PORT = getenv('DB_PORT', 5432) +DB_NAME = getenv('DB_NAME', 'myapp') +DB_USER = getenv('DB_USER', 'myapp') +DB_PASS = getenv('DB_PASS') + +# Redis +REDIS_HOST = getenv('REDIS_HOST', 'localhost') +REDIS_PORT = getenv('REDIS_PORT', 6379) + +# Development +TEMPLATES_AUTO_RELOAD = True diff --git a/myapp/factory.py b/myapp/factory.py new file mode 100644 index 0000000..a7c5a53 --- /dev/null +++ b/myapp/factory.py @@ -0,0 +1,37 @@ +import quart.flask_patch +from quart import Quart +from flask_sqlalchemy import SQLAlchemy + +from myapp import config + + +db = SQLAlchemy() + +async def _setup_db(app: Quart): + uri = 'postgresql+psycopg2://{user}:{pw}@{host}:{port}/{db}'.format( + user=config.DB_USER, + pw=config.DB_PASS, + host=config.DB_HOST, + port=config.DB_PORT, + db=config.DB_NAME + ) + app.config['SQLALCHEMY_DATABASE_URI'] = uri + app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False + db = SQLAlchemy(app) + +def create_app(): + app = Quart(__name__) + app.config.from_envvar('QUART_SECRETS') + + + @app.before_serving + async def startup(): + from myapp.routes import meta, api + from myapp import filters + await _setup_db(app) + app.register_blueprint(meta.bp) + app.register_blueprint(api.bp) + app.register_blueprint(filters.bp) + # app.register_blueprint(cli.bp) + + return app diff --git a/myapp/filters.py b/myapp/filters.py new file mode 100644 index 0000000..cb015bd --- /dev/null +++ b/myapp/filters.py @@ -0,0 +1,15 @@ +from datetime import datetime + +from quart import Blueprint, current_app + + +bp = Blueprint('filters', 'filters') + + +@bp.app_template_filter('ts') +def from_ts(v): + return datetime.fromtimestamp(v) + +@bp.app_template_filter('xmr_block_explorer') +def xmr_block_explorer(v): + return f'https://www.exploremonero.com/transaction/{v}' diff --git a/myapp/forms.py b/myapp/forms.py new file mode 100644 index 0000000..8849566 --- /dev/null +++ b/myapp/forms.py @@ -0,0 +1,8 @@ +from flask_wtf import FlaskForm +from wtforms import StringField, BooleanField +from wtforms.validators import DataRequired + + +class Login(FlaskForm): + email = StringField('Email Address:', validators=[DataRequired()], render_kw={"placeholder": "Email", "class": "form-control", "type": "email"}) + password = StringField('Password:', validators=[DataRequired()], render_kw={"placeholder": "Password", "class": "form-control", "type": "password"}) diff --git a/myapp/library/__init__.py b/myapp/library/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/myapp/library/cache.py b/myapp/library/cache.py new file mode 100644 index 0000000..3372afa --- /dev/null +++ b/myapp/library/cache.py @@ -0,0 +1,35 @@ +from json import loads as json_loads +from json import dumps as json_dumps +from datetime import timedelta + +from redis import Redis + +from app.library.market import get_market_data +from app import config + + +class Cache(object): + def __init__(self): + self.redis = Redis(host=config.REDIS_HOST, port=config.REDIS_PORT) + + def store_data(self, item_name, expiration_minutes, data): + self.redis.setex( + item_name, + timedelta(minutes=expiration_minutes), + value=data + ) + + def get_coin_price(self, coin_name): + key_name = f'{coin_name}_price' + data = self.redis.get(key_name) + if data: + return json_loads(data) + else: + d = get_market_data(coin_name) + data = { + key_name: d['market_data']['current_price'], + } + self.store_data(key_name, 4, json_dumps(data)) + return data + +cache = Cache() diff --git a/myapp/library/market.py b/myapp/library/market.py new file mode 100644 index 0000000..1763905 --- /dev/null +++ b/myapp/library/market.py @@ -0,0 +1,16 @@ +from requests import get as r_get + + +def get_market_data(coin_name="monero"): + data = { + 'localization': False, + 'tickers': False, + 'market_data': True, + 'community_data': False, + 'developer_data': False, + 'sparkline': False + } + headers = {'accept': 'application/json'} + url = f'https://api.coingecko.com/api/v3/coins/{coin_name}' + r = r_get(url, headers=headers, data=data) + return r.json() diff --git a/myapp/models.py b/myapp/models.py new file mode 100644 index 0000000..f8f5f56 --- /dev/null +++ b/myapp/models.py @@ -0,0 +1,38 @@ +from datetime import datetime +from uuid import uuid4 + +from sqlalchemy.sql import func + +from myapp.factory import db +from myapp import config + + +def rand_id(): + return uuid4().hex + +class MyThing(db.Model): + __tablename__ = 'swaps' + + # Meta + id = db.Column(db.Integer, primary_key=True) + # id = db.Column(db.String(80), primary_key=True, default=rand_id) # hex based id + date = db.Column(db.DateTime, server_default=func.now()) + my_bool = db.Column(db.Boolean) + my_int = db.Column(db.Integer) + my_str = db.Column(db.String(150)) + completed = db.Column(db.Boolean, default=False) + completed_date = db.Column(db.DateTime, nullable=True) + + def __repr__(self): + return self.id + + def hours_elapsed(self): + now = datetime.utcnow() + if since_completed: + if self.completed_date: + diff = now - self.completed_date + else: + return 0 + else: + diff = now - self.date + return diff.total_seconds() / 60 / 60 diff --git a/myapp/routes/__init__.py b/myapp/routes/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/myapp/routes/api.py b/myapp/routes/api.py new file mode 100644 index 0000000..90b9088 --- /dev/null +++ b/myapp/routes/api.py @@ -0,0 +1,11 @@ +from quart import Blueprint, jsonify + + +bp = Blueprint('api', 'api') + +@bp.route('/api/test') +async def get_prices(): + return jsonify({ + 'test': True, + 'message': 'This is only a test.' + }) diff --git a/myapp/routes/meta.py b/myapp/routes/meta.py new file mode 100644 index 0000000..36cd545 --- /dev/null +++ b/myapp/routes/meta.py @@ -0,0 +1,8 @@ +from quart import Blueprint, render_template + + +bp = Blueprint('meta', 'meta') + +@bp.route('/') +async def index(): + return await render_template('index.html') diff --git a/myapp/static/css/main.css b/myapp/static/css/main.css new file mode 100644 index 0000000..e69de29 diff --git a/myapp/static/images/monero-logo.png b/myapp/static/images/monero-logo.png new file mode 100644 index 0000000..bf7ab32 Binary files /dev/null and b/myapp/static/images/monero-logo.png differ diff --git a/myapp/static/js/main.js b/myapp/static/js/main.js new file mode 100644 index 0000000..e69de29 diff --git a/myapp/templates/includes/footer.html b/myapp/templates/includes/footer.html new file mode 100644 index 0000000..9fc962b --- /dev/null +++ b/myapp/templates/includes/footer.html @@ -0,0 +1,7 @@ + diff --git a/myapp/templates/includes/head.html b/myapp/templates/includes/head.html new file mode 100644 index 0000000..4e3039b --- /dev/null +++ b/myapp/templates/includes/head.html @@ -0,0 +1,20 @@ + + {{ config.SITE_NAME }} + + + + + + + + + + + + + + + + + + diff --git a/myapp/templates/includes/header.html b/myapp/templates/includes/header.html new file mode 100644 index 0000000..07001a0 --- /dev/null +++ b/myapp/templates/includes/header.html @@ -0,0 +1,9 @@ + diff --git a/myapp/templates/includes/scripts.html b/myapp/templates/includes/scripts.html new file mode 100644 index 0000000..c01fef3 --- /dev/null +++ b/myapp/templates/includes/scripts.html @@ -0,0 +1,9 @@ + + +{% with messages = get_flashed_messages() %} + {% if messages %} + {% for message in messages %} +

{{ message }}

+ {% endfor %} + {% endif %} +{% endwith %} diff --git a/myapp/templates/index.html b/myapp/templates/index.html new file mode 100644 index 0000000..a5fcffb --- /dev/null +++ b/myapp/templates/index.html @@ -0,0 +1,37 @@ + + + + {% include 'includes/head.html' %} + + +
+ + + + + + {% include 'includes/footer.html' %} + +
+ + {% include 'includes/scripts.html' %} + + + diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..d170a84 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,14 @@ +Flask +Flask-SQLAlchemy +Flask-WTF +flask-login +gunicorn +Pillow +psycopg2-binary +python-dotenv +qrcode +redis +requests +SQLAlchemy +WTForms +quart