From 82ce85a44803aa1eb79fc69b9860398a2d0120ee Mon Sep 17 00:00:00 2001 From: Hartmnt Date: Tue, 28 Mar 2023 21:57:09 +0000 Subject: [PATCH] Implement build_number_generator --- build_number_generator/__init__.py | 15 +++ .../controller/build_number.py | 95 +++++++++++++++++++ build_number_generator/database.py | 47 +++++++++ build_number_generator/schema.sql | 18 ++++ 4 files changed, 175 insertions(+) create mode 100644 build_number_generator/__init__.py create mode 100644 build_number_generator/controller/build_number.py create mode 100644 build_number_generator/database.py create mode 100644 build_number_generator/schema.sql diff --git a/build_number_generator/__init__.py b/build_number_generator/__init__.py new file mode 100644 index 0000000..73b24bc --- /dev/null +++ b/build_number_generator/__init__.py @@ -0,0 +1,15 @@ +from flask import Flask +import os + +app_path = os.path.dirname(__file__) +app = Flask(__name__, root_path=app_path, instance_path=os.path.join(app_path, "instance"), instance_relative_config=True) + +app.config.from_pyfile("config.py") + +app.app_context() + +with app.app_context(): + from . import database + database.init() + + from .controller import build_number diff --git a/build_number_generator/controller/build_number.py b/build_number_generator/controller/build_number.py new file mode 100644 index 0000000..c5e7668 --- /dev/null +++ b/build_number_generator/controller/build_number.py @@ -0,0 +1,95 @@ +from flask import Flask, jsonify, current_app, request +from build_number_generator.database import get_database + +import hashlib + +def hash_token(token: str) -> str: + digest = hashlib.blake2b(digest_size=64) + digest.update(str.encode(token)) + return digest.hexdigest() + + +def fail(status_message: str, status_code: int = 400) -> tuple: + response = {} + response["status"] = status_code + response["message"] = status_message + return response, status_code + + +@current_app.errorhandler(405) +def method_not_allowed(e) -> tuple: + return fail("Method not allowed", 405) + + +@current_app.route("/build-number", methods=["POST"]) +@current_app.route("/build-number/", methods=["POST"]) +@current_app.route("/build-number//", methods=["POST"]) +def build_number(series=None, commit=None): + + token = request.form.get("token") + + if not token: + return fail("Unautherized", 401) + + if not hash_token(token) in current_app.config["AUTHORIZED_HASHES"]: + return fail("Unautherized", 401) + + if not series: + return fail("Required parameter not provided: series") + + if series.count(".") != 1: + return fail("Invalid parameter: series. Format: .") + + major, minor = series.split(".") + + if not major.isnumeric() or not minor.isnumeric(): + return fail("Invalid parameter: series. Format: .") + + major = int(major) + minor = int(minor) + + if not commit: + return fail("Required parameter not provided: commit hash") + + if len(commit) < 8: + return fail("Invalid parameter: commit") + + response = {} + + db = get_database() + + # Find or insert series + result = db.execute("SELECT * FROM series WHERE name='%i.%i'" % (major, minor)) + row = result.fetchone() + + if row is not None: + series_id = row["id"] + else: + cursor = db.execute("INSERT INTO series VALUES (NULL, '%i.%i', NULL)" % (major, minor)) + series_id = cursor.lastrowid + + # Find or insert build number + result = db.execute("SELECT * FROM build WHERE series_id=%i AND commit_hash='%s'" % (series_id, commit)) + row = result.fetchone() + + if row is not None: + build_number = row["build_number"] + + response["message"] = "Known commit" + else: + result = db.execute("SELECT build_number FROM build WHERE series_id=%i ORDER BY build_number DESC LIMIT 1" % (series_id)) + row = result.fetchone() + build_number = (row["build_number"] + 1) if row is not None else 1 + cursor = db.execute("INSERT INTO build VALUES (NULL, %i, '%s', %i, NULL)" % (series_id, commit, build_number)) + + response["message"] = "New build number created" + + db.commit() + + response["stauts"] = 200 + response["commit_hash"] = commit + response["series"] = "%i.%i" % (major, minor) + response["series_id"] = series_id + response["build_number"] = build_number + + return response, 200 diff --git a/build_number_generator/database.py b/build_number_generator/database.py new file mode 100644 index 0000000..4d751dd --- /dev/null +++ b/build_number_generator/database.py @@ -0,0 +1,47 @@ +import sqlite3 +import os + +import click +from flask import current_app, g + + +def get_database() -> sqlite3.Connection: + """Gets the database connection for the current request. If this function is called for the first time for the given request, + this function also connects to the database, whereas later calls will simply reuse that connection""" + if "database" not in g: + filename = os.path.join(current_app.instance_path, current_app.config["DATABASE"]) + g.database = sqlite3.connect( + filename, detect_types=sqlite3.PARSE_DECLTYPES + ) + g.database.row_factory = sqlite3.Row + + return g.database + + +def close_database(_ = None) -> None: + """If a connection to the database has been established, this function closes it again""" + database: sqlite3.Connection = g.pop("database", None) + + if database is not None: + database.close() + + +def init_database() -> None: + """This function (re)creates the database from scratch. If the database existed + before, its contents will be dropped""" + database: sqlite3.Connection = get_database() + + with current_app.open_resource("schema.sql") as file: + database.executescript(file.read().decode("utf-8")) + + +@click.command("init-db") +def init_db_command() -> None: + """Clear existing data and create new tables.""" + init_database() + click.echo("Database initialized") + + +def init() -> None: + current_app.teardown_appcontext(close_database) + current_app.cli.add_command(init_db_command) diff --git a/build_number_generator/schema.sql b/build_number_generator/schema.sql new file mode 100644 index 0000000..baa146b --- /dev/null +++ b/build_number_generator/schema.sql @@ -0,0 +1,18 @@ +DROP TABLE IF EXISTS series; +DROP TABLE IF EXISTS build; + +CREATE TABLE series ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + created_on DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE build ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + series_id INTEGER NOT NULL, + commit_hash TEXT NOT NULL, + build_number INTEGER NOT NULL, + created_on DEFAULT CURRENT_TIMESTAMP, + UNIQUE(series_id, commit_hash), + FOREIGN KEY (series_id) REFERENCES series (id) +);