From e00284bc7e2a25b72fad0d43736daecc22ccabf3 Mon Sep 17 00:00:00 2001 From: lalanza808 Date: Mon, 13 Feb 2023 08:26:57 -0800 Subject: [PATCH] Initial commit --- .gitignore | 129 ++++++++++++++++++++++++++ LICENSE | 21 +++++ Makefile | 17 ++++ README.md | 2 + app.py | 7 ++ docker-compose.yaml | 16 ++++ env-example | 16 ++++ myapp/__init__.py | 0 myapp/cli.py | 30 ++++++ myapp/config.py | 33 +++++++ myapp/factory.py | 37 ++++++++ myapp/filters.py | 15 +++ myapp/forms.py | 8 ++ myapp/library/__init__.py | 0 myapp/library/cache.py | 35 +++++++ myapp/library/market.py | 16 ++++ myapp/models.py | 38 ++++++++ myapp/routes/__init__.py | 0 myapp/routes/api.py | 11 +++ myapp/routes/meta.py | 8 ++ myapp/static/css/main.css | 0 myapp/static/images/monero-logo.png | Bin 0 -> 11912 bytes myapp/static/js/main.js | 0 myapp/templates/includes/footer.html | 7 ++ myapp/templates/includes/head.html | 20 ++++ myapp/templates/includes/header.html | 9 ++ myapp/templates/includes/scripts.html | 9 ++ myapp/templates/index.html | 37 ++++++++ requirements.txt | 14 +++ 29 files changed, 535 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 Makefile create mode 100644 README.md create mode 100644 app.py create mode 100644 docker-compose.yaml create mode 100644 env-example create mode 100644 myapp/__init__.py create mode 100644 myapp/cli.py create mode 100644 myapp/config.py create mode 100644 myapp/factory.py create mode 100644 myapp/filters.py create mode 100644 myapp/forms.py create mode 100644 myapp/library/__init__.py create mode 100644 myapp/library/cache.py create mode 100644 myapp/library/market.py create mode 100644 myapp/models.py create mode 100644 myapp/routes/__init__.py create mode 100644 myapp/routes/api.py create mode 100644 myapp/routes/meta.py create mode 100644 myapp/static/css/main.css create mode 100644 myapp/static/images/monero-logo.png create mode 100644 myapp/static/js/main.js create mode 100644 myapp/templates/includes/footer.html create mode 100644 myapp/templates/includes/head.html create mode 100644 myapp/templates/includes/header.html create mode 100644 myapp/templates/includes/scripts.html create mode 100644 myapp/templates/index.html create mode 100644 requirements.txt 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 0000000000000000000000000000000000000000..bf7ab3261b16e36f6ca9dbfa07290f3b2d4b5d5b GIT binary patch literal 11912 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 literal 0 HcmV?d00001 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