Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft: Implement build_number_generator #1

Draft
wants to merge 3 commits into
base: v2
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions build_number_generator/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
from flask import Flask
import os

def create_app() -> None:
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

return app
100 changes: 100 additions & 0 deletions build_number_generator/controller/build_number.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
from flask import Flask, jsonify, current_app, request
from build_number_generator.database import get_database

import hashlib

MIN_COMMIT_HASH_LENGTH = 16

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/<string:series>", methods=["POST"])
@current_app.route("/build-number/<string:series>/<string:commit>", 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. Format: /build-number/<series>/<commit_hash>")

if series.count(".") != 1:
return fail("Invalid parameter: series. Format: <major>.<minor>")

major, minor = series.split(".")

if not major.isnumeric() or not minor.isnumeric():
return fail("Invalid parameter: series. Format: <major>.<minor>")

major = int(major)
minor = int(minor)

if not commit:
return fail("Required parameter not provided. Format: /build-number/<series>/<commit_hash>")

if len(commit) < MIN_COMMIT_HASH_LENGTH:
return fail("Invalid parameter: commit. Must be at least %i characters long" % (MIN_COMMIT_HASH_LENGTH))

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"]
response["message"] = "Known series. "
else:
cursor = db.execute("INSERT INTO series VALUES (NULL, '%i.%i', NULL)" % (major, minor))
series_id = cursor.lastrowid
response["message"] = "Unknown series. "

# 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 hash."
response["stauts"] = 200
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"] += "Unknown commit hash. New build number created."
response["stauts"] = 201

db.commit()

response["commit_hash"] = commit
response["series"] = "%i.%i" % (major, minor)
response["series_id"] = series_id
response["build_number"] = build_number

return response, response["stauts"]
45 changes: 45 additions & 0 deletions build_number_generator/database.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import sqlite3

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:
g.database = sqlite3.connect(
current_app.config["DATABASE"], 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)
18 changes: 18 additions & 0 deletions build_number_generator/schema.sql
Original file line number Diff line number Diff line change
@@ -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)
);
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Flask
23 changes: 23 additions & 0 deletions setup
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
#!/bin/bash

PYTHON_HOST_BIN=python

which $PYTHON_HOST_BIN

if [ $? -ne 0 ]; then
PYTHON_HOST_BIN=python3
fi

which $PYTHON_HOST_BIN

if [ $? -ne 0 ]; then
echo "python executable not found"
exit 1
fi

$PYTHON_HOST_BIN --version

$PYTHON_HOST_BIN -m venv venv
venv/bin/pip install -r requirements.txt

. venv/bin/activate
1 change: 1 addition & 0 deletions test/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
#
2 changes: 2 additions & 0 deletions test/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
pytest
requests
58 changes: 58 additions & 0 deletions test/test_build_number_generator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import pytest
import json

@pytest.fixture()
def app():
from build_number_generator import create_app
from build_number_generator.database import init_database

app = create_app()
app.testing = True

app.config.update({
"DATABASE": "/tmp/test.sqlite",
"AUTHORIZED_HASHES": {
# 'testpassword'
"ebe5a79f942bab98b3c122ceb72a9c23fb21991848cddb9ad4ce8a209c8269c25eb5f5146427afc23ef588de61798b38451282339e2c637c6dec51d635ab50c4"
}
})

with app.app_context():
init_database()

yield app

@pytest.fixture()
def app_context(app):
with app.app_context():
yield app


@pytest.fixture()
def client(app_context):
yield app_context.test_client()


def request(client, url, data=None):
response = client.post(url, data=data, headers={"Content-Type": "application/x-www-form-urlencoded"})
j = json.loads(response.data)
status = response.status_code
return j, status


def test_method_1(client):
response = client.get("/build-number")
j = json.loads(response.data)
status = response.status_code
assert status == 405 and j["status"] == 405


def test_method_2(client):
response = client.get("/build-number")
j = json.loads(response.data)
status = response.status_code
assert status == 405 and j["status"] == 405
#j, status = request(client, "/build-number/asd/asd", "")
#assert status == 400 and j["status"] == 400

#request(client, "/build-number/1.1/abcdefghijklmnopq", "token=testpassword")