From 62029b9acd262e08e2945ba268a0abc269180733 Mon Sep 17 00:00:00 2001 From: Raoul Snyman Date: Fri, 28 Jul 2023 13:03:31 -0700 Subject: [PATCH] Getting things ready to containerize CodeSmigen - Update the Dockerfile to pull the correct package - Update the README.rst with environment variables - Add typing annotations - Refactor config to be loaded via environment variables as well as a file --- Dockerfile | 4 +-- README.rst | 3 +- codesmidgen/__init__.py | 31 +++------------------ codesmidgen/app.py | 3 +- codesmidgen/config.py | 62 +++++++++++++++++++++++++++++++++++++++++ codesmidgen/views.py | 18 ++++++------ 6 files changed, 80 insertions(+), 41 deletions(-) create mode 100644 codesmidgen/config.py diff --git a/Dockerfile b/Dockerfile index 26a736e..ceaf5fe 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ FROM python:3.11 -RUN pip install stickynotes --index-url https://git.snyman.info/packages/raoul/index +RUN pip install --extra-index-url https://git.snyman.info/api/packages/raoul/pypi/simple/ CodeSmidgen hypercorn EXPOSE 8000 -CMD ["hypercorn", "stickynotes.app"] +CMD ["hypercorn", "codesmidgen.app"] diff --git a/README.rst b/README.rst index 9beee72..5efe224 100644 --- a/README.rst +++ b/README.rst @@ -25,7 +25,8 @@ The easiest way to install CodeSmidgen is via Docker and Docker Compose. Here's app: image: git.snyman.info/raoul/codesmidgen:latest env: - - SQLALCHEMY_URL=postgres://codesmidgen:codesmidgen@postgres/codesmidgen + - SMIDGEN_SECRET_KEY=yoursecrethere + - SQLALCHEMY_DATABASE_URL=postgres://codesmidgen:codesmidgen@postgres/codesmidgen restart: unless-stopped ports: - "127.0.0.1:8000:8000" diff --git a/codesmidgen/__init__.py b/codesmidgen/__init__.py index 01bbc6d..e5895e8 100644 --- a/codesmidgen/__init__.py +++ b/codesmidgen/__init__.py @@ -2,48 +2,25 @@ """ CodeSmidgen, yet another paste bin """ -from configparser import ConfigParser -from pathlib import Path - import quart_flask_patch # noqa: F401 from quart import Quart +from codesmidgen.config import get_config from codesmidgen.db import db from codesmidgen.views import views -def read_config(config_path=None): - """ - Read the configuration file and return the values in a dictionary - """ - if config_path: - config_file = config_path / 'codesmidgen.cfg' - else: - config_file = Path(__file__).parent / '..' / 'codesmidgen.cfg' - if not config_file.exists(): - return {} - config_parser = ConfigParser() - config_parser.read(config_file) - config = {} - for option in config_parser.options('codesmidgen'): - config[option.upper()] = config_parser.get('codesmidgen', option) - return config - - -def make_app(config_path=None): +def make_app() -> Quart: """ Create the application object """ app = Quart(__name__) - # Load the config file - config = read_config(config_path) - app.config.update(config) - app.config.update({'SQLALCHEMY_TRACK_MODIFICATIONS': False}) + app.config.update(get_config()) db.init_app(app) app.register_blueprint(views) @app.before_first_request - async def setup_db(): + async def setup_db() -> None: db.create_all() return app diff --git a/codesmidgen/app.py b/codesmidgen/app.py index ef91034..5dad669 100644 --- a/codesmidgen/app.py +++ b/codesmidgen/app.py @@ -2,10 +2,9 @@ """ This is the entry point for the WSGI server """ -from pathlib import Path from codesmidgen import make_app -application = make_app(Path(__file__).parent.parent) +application = make_app() if __name__ == '__main__': application.run(debug=True) diff --git a/codesmidgen/config.py b/codesmidgen/config.py new file mode 100644 index 0000000..05fc038 --- /dev/null +++ b/codesmidgen/config.py @@ -0,0 +1,62 @@ +import json +import os +from configparser import ConfigParser +from pathlib import Path +from typing import Any + +CONFIG_DEFAULTS = { + 'SQLALCHEMY_TRACK_MODIFICATIONS': False +} +CONFIG_PREFIXES = ['SMIDGEN_', 'SQLALCHEMY_'] +STRIPPED_PREFIXES = ['SMIDGEN_'] + + +def read_from_file() -> dict: + """Read the configuration file and return the values in a dictionary""" + config_file = Path(__file__).parent / '..' / 'codesmidgen.cfg' + if not config_file.exists(): + return {} + config_parser = ConfigParser() + config_parser.read(config_file) + config: dict[str, Any] = {} + for option in config_parser.options('codesmidgen'): + config[option.upper()] = config_parser.get('codesmidgen', option) + return config + + +def read_from_envvars() -> dict: + """Read the configuration from environment variables""" + config: dict[str, Any] = {} + for key in sorted(os.environ): + if not any([key.startswith(prefix) for prefix in CONFIG_PREFIXES]): + continue + value = os.environ[key] + try: + value = json.loads(value) + except Exception: + # If the value is not JSON parseable, just leave it as a string + pass + for prefix in STRIPPED_PREFIXES: + key = key.replace(prefix, '') + # Check if there are any "nested" values + if '__' not in key: + config[key] = value + continue + # Navigate the nested values and build the structure + *parents, child = key.split('__') + item = config + for parent in parents: + if parent not in item: + item[parent] = {} + item = item[parent] + item[child] = value + return config + + +def get_config() -> dict: + """Read configuration from files, environment variables, etc.""" + config = {} + config.update(CONFIG_DEFAULTS) + config.update(read_from_file()) + config.update(read_from_envvars()) + return config diff --git a/codesmidgen/views.py b/codesmidgen/views.py index 4e07d5f..1921f07 100644 --- a/codesmidgen/views.py +++ b/codesmidgen/views.py @@ -7,7 +7,7 @@ import secrets import string from datetime import timedelta, datetime -from quart import Blueprint, redirect, request, flash, make_response, current_app, render_template +from quart import Blueprint, Response, redirect, request, flash, make_response, current_app, render_template from pygments import highlight from pygments.formatters.html import HtmlFormatter from pygments.lexers import get_lexer_by_name, get_all_lexers @@ -30,7 +30,7 @@ EXPIRY_DELTAS = { } -def _generate_short_url(): +def _generate_short_url() -> str: """ Encode the URL """ @@ -40,7 +40,7 @@ def _generate_short_url(): @views.route('/', methods=['GET']) -async def index(): +async def index() -> str: """ Add a new sticky note """ @@ -51,7 +51,7 @@ async def index(): @views.route('/notes', methods=['GET']) -async def notes(): +async def notes() -> str: """ Show a list of recent notes """ @@ -64,7 +64,7 @@ async def notes(): @views.route('/about', methods=['GET']) -async def about(): +async def about() -> str: """ Show the about page """ @@ -72,7 +72,7 @@ async def about(): @views.route('/', methods=['POST']) -async def save(): +async def save() -> Response: """ Save a sticky note """ @@ -107,7 +107,7 @@ async def save(): @views.route('/', methods=['GET']) -async def view(note_url): +async def view(note_url: str) -> Response | str: """ Show a sticky note @@ -125,7 +125,7 @@ async def view(note_url): @views.route('/raw/', methods=['GET']) -async def raw(note_url): +async def raw(note_url: str) -> Response | str: """ Show the raw version of a sticky note @@ -139,7 +139,7 @@ async def raw(note_url): @views.route('/pygments.css', methods=['GET']) -async def pygments_css(): +async def pygments_css() -> Response: """ Return the Pygments CSS to the browser """