From 2fa26dfbc2288bdfdea524477371a5ced668ba5c Mon Sep 17 00:00:00 2001 From: Nadav Tasher Date: Sat, 12 Oct 2024 23:08:14 +0300 Subject: [PATCH] Updates to starlette, typing and build system --- Makefile | 90 ++++++++++++++++----- bundles/buildless/configurations/nginx.conf | 0 image/{Dockerfile.template => Dockerfile} | 16 ++-- image/configurations/entrypoint.conf | 4 +- image/configurations/gunicorn.conf | 28 ------- image/requirements.txt | 11 +++ image/src/backend/app.py | 6 +- image/src/backend/utilities/debug.py | 2 +- image/src/backend/utilities/redis.py | 36 ++++++--- image/src/backend/utilities/starlette.py | 71 ++++++---------- image/src/entrypoint.py | 9 +-- resources/scripts/create_dockerfile.py | 28 ------- resources/scripts/create_headless_page.py | 4 +- 13 files changed, 150 insertions(+), 155 deletions(-) delete mode 100644 bundles/buildless/configurations/nginx.conf rename image/{Dockerfile.template => Dockerfile} (60%) delete mode 100644 image/configurations/gunicorn.conf create mode 100644 image/requirements.txt delete mode 100644 resources/scripts/create_dockerfile.py diff --git a/Makefile b/Makefile index f775f00..f38f665 100644 --- a/Makefile +++ b/Makefile @@ -6,67 +6,112 @@ IMAGE_LATEST_TAG ?= $(IMAGE_TAG):latest COPY ?= $(shell which cp) MKDIR ?= $(shell which mkdir) -PYTHON ?= $(shell which python3) DOCKER ?= $(shell which docker) +PYTHON ?= $(shell which python3) + +# Python virtual environment paths +VENV_PATH := .venv +# Python executable paths +PIP := $(VENV_PATH)/bin/pip +YAPF := $(VENV_PATH)/bin/yapf +MYPY := $(VENV_PATH)/bin/mypy +PYLINT := $(VENV_PATH)/bin/pylint +PYTHON := $(VENV_PATH)/bin/python + +# All paths IMAGE_PATH := image BUNDLES_PATH := bundles EXAMPLES_PATH := examples RESOURCES_PATH := resources +# Additional resources TESTS_PATH := $(RESOURCES_PATH)/tests SCRIPTS_PATH := $(RESOURCES_PATH)/scripts +# Source paths BACKEND_PATH := $(IMAGE_PATH)/src/backend FRONTEND_PATH := $(IMAGE_PATH)/src/frontend ENTRYPOINT_PATH := $(IMAGE_PATH)/src/entrypoint.py +REQUIREMENTS_PATH := $(IMAGE_PATH)/requirements.txt +# Bundle paths HEADLESS_BUNDLE_PATH := $(BUNDLES_PATH)/headless BUILDLESS_BUNDLE_PATH := $(BUNDLES_PATH)/buildless INDEPENDENT_BUNDLE_PATH := $(BUNDLES_PATH)/independent +# Headless bundle source paths HEADLESS_BUNDLE_INDEX_PATH := $(HEADLESS_BUNDLE_PATH)/index.html HEADLESS_BUNDLE_TEST_PAGE_PATH := $(HEADLESS_BUNDLE_PATH)/test-page.html +# Buildless bundle source paths BUILDLESS_BUNDLE_BACKEND_PATH := $(BUILDLESS_BUNDLE_PATH)/src/backend BUILDLESS_BUNDLE_FRONTEND_PATH := $(BUILDLESS_BUNDLE_PATH)/src/frontend +# Independent bundle source paths INDEPENDENT_BUNDLE_BACKEND_PATH := $(INDEPENDENT_BUNDLE_PATH)/application/src/backend INDEPENDENT_BUNDLE_FRONTEND_PATH := $(INDEPENDENT_BUNDLE_PATH)/application/src/frontend +# All image sources IMAGE_SOURCES := $(shell find $(IMAGE_PATH) -type f) -PYTHON_SOURCES := $(wildcard $(BACKEND_PATH)/*.py) $(wildcard $(BACKEND_PATH)/*/*.py) $(ENTRYPOINT_PATH) $(wildcard $(EXAMPLES_PATH)/*/application/src/backend/*.py) $(wildcard $(SCRIPTS_PATH)/*.py) + +# All python sources +PYTHON_SOURCES := $(wildcard $(BACKEND_PATH)/*.py) $(wildcard $(BACKEND_PATH)/*/*.py) $(ENTRYPOINT_PATH) $(wildcard $(SCRIPTS_PATH)/*.py) all: bundles image -prerequisites: - $(PYTHON) -m pip install jinja2 yapf +# Linting and checks -format: prerequisites $(PYTHON_SOURCES) - $(PYTHON) -m yapf -i $(PYTHON_SOURCES) --style "{based_on_style: google, column_limit: 400, indent_width: 4}" +checks: format lint typecheck -$(IMAGE_PATH)/Dockerfile-$(IMAGE_TAG): $(SCRIPTS_PATH)/create_dockerfile.py $(IMAGE_PATH)/Dockerfile.template - $(MKDIR) -p $(@D) - $(PYTHON) $(SCRIPTS_PATH)/create_dockerfile.py --python-version $(IMAGE_TAG) < $(IMAGE_PATH)/Dockerfile.template > $@ +lint: $(PYLINT) $(PYTHON_SOURCES) + @# Lint all of the sources + $(PYLINT) -d C0301 -d C0114 -d C0115 -d C0116 $(PYTHON_SOURCES) -image: format $(IMAGE_PATH)/Dockerfile-$(IMAGE_TAG) $(IMAGE_SOURCES) - $(DOCKER) build $(IMAGE_PATH) -f $(IMAGE_PATH)/Dockerfile-$(IMAGE_TAG) -t $(IMAGE_NAME)/$(IMAGE_TAG) -t $(IMAGE_NAME)/$(IMAGE_DATE_TAG) -t $(IMAGE_NAME)/$(IMAGE_LATEST_TAG) +typecheck: $(MYPY) $(PYTHON_SOURCES) + @# Typecheck all of the sources + $(MYPY) --strict --explicit-package-bases --no-implicit-reexport $(PYTHON_SOURCES) -buildx: format $(IMAGE_PATH)/Dockerfile-$(IMAGE_TAG) $(IMAGE_SOURCES) +format: $(YAPF) $(PYTHON_SOURCES) + @# Format the python sources using yapf + $(YAPF) -i $(PYTHON_SOURCES) --style "{based_on_style: google, column_limit: 400, indent_width: 4}" + +# Images + +image: format $(IMAGE_SOURCES) + @# Build the image + $(DOCKER) build --build-arg PYTHON_VERSION=$(IMAGE_TAG) $(IMAGE_PATH) -t $(IMAGE_NAME)/$(IMAGE_TAG) -t $(IMAGE_NAME)/$(IMAGE_DATE_TAG) -t $(IMAGE_NAME)/$(IMAGE_LATEST_TAG) + +buildx: format $(IMAGE_SOURCES) + @# Create the build context $(DOCKER) buildx create --use - $(DOCKER) buildx build $(IMAGE_PATH) --push --platform linux/386,linux/amd64,linux/arm64/v8 -f $(IMAGE_PATH)/Dockerfile-$(IMAGE_TAG) -t $(IMAGE_NAME)/$(IMAGE_DATE_TAG) -t $(IMAGE_NAME)/$(IMAGE_LATEST_TAG) -clean: - $(RM) $(IMAGE_PATH)/Dockerfile-* + @# Build the image + $(DOCKER) buildx build --build-arg PYTHON_VERSION=$(IMAGE_TAG) $(IMAGE_PATH) --push --platform linux/386,linux/amd64,linux/arm64/v8 -t $(IMAGE_NAME)/$(IMAGE_DATE_TAG) -t $(IMAGE_NAME)/$(IMAGE_LATEST_TAG) -bundles: headless buildless independent +# Bundles +bundles: headless buildless independent headless: $(HEADLESS_BUNDLE_INDEX_PATH) $(HEADLESS_BUNDLE_TEST_PAGE_PATH) - buildless: $(BUILDLESS_BUNDLE_BACKEND_PATH)/app.py $(BUILDLESS_BUNDLE_BACKEND_PATH)/worker.py $(BUILDLESS_BUNDLE_FRONTEND_PATH)/index.html $(BUILDLESS_BUNDLE_FRONTEND_PATH)/application/application.css $(BUILDLESS_BUNDLE_FRONTEND_PATH)/application/application.js - independent: $(INDEPENDENT_BUNDLE_BACKEND_PATH)/app.py $(INDEPENDENT_BUNDLE_BACKEND_PATH)/worker.py $(INDEPENDENT_BUNDLE_FRONTEND_PATH)/index.html $(INDEPENDENT_BUNDLE_FRONTEND_PATH)/application/application.css $(INDEPENDENT_BUNDLE_FRONTEND_PATH)/application/application.js +# Local prerequisites + +$(VENV_PATH): $(REQUIREMENTS_PATH) + @# Create a new virtual environment + python3 -m venv $(VENV_PATH) + + @# Install some dependencies + $(PIP) install -r $(REQUIREMENTS_PATH) jinja2 yapf mypy pylint + +$(YAPF): $(VENV_PATH) +$(MYPY): $(VENV_PATH) +$(PYLINT): $(VENV_PATH) +$(PYTHON): $(VENV_PATH) + +# Targets to make + $(INDEPENDENT_BUNDLE_BACKEND_PATH)/%.py: $(BACKEND_PATH)/%.py $(MKDIR) -p $(@D) $(COPY) $^ $@ @@ -99,6 +144,8 @@ $(HEADLESS_BUNDLE_TEST_PAGE_PATH): $(TESTS_PATH)/test-page.html $(SCRIPTS_PATH)/ $(MKDIR) -p $(@D) $(PYTHON) $(SCRIPTS_PATH)/create_headless_page.py --base-directory $(FRONTEND_PATH) < $(TESTS_PATH)/test-page.html > $@ +# Tests + test: image $(DOCKER) run --rm -p 80:80 -p 443:443 -e DEBUG=1 -e REDIS=$(REDIS) -v $(abspath $(TESTS_PATH)/test-page.html):/application/frontend/index.html:ro -v /tmp/test:/opt $(IMAGE_NAME)/$(IMAGE_TAG) @@ -115,4 +162,9 @@ test-independent: independent $(DOCKER) compose --project-directory $(INDEPENDENT_BUNDLE_PATH) up --build test-independent-build: independent - $(MAKE) -C $(INDEPENDENT_BUNDLE_PATH) \ No newline at end of file + $(MAKE) -C $(INDEPENDENT_BUNDLE_PATH) + +# Cleanups + +clean: + $(RM) -r $(VENV_PATH) $(IMAGE_PATH)/Dockerfile-* \ No newline at end of file diff --git a/bundles/buildless/configurations/nginx.conf b/bundles/buildless/configurations/nginx.conf deleted file mode 100644 index e69de29..0000000 diff --git a/image/Dockerfile.template b/image/Dockerfile similarity index 60% rename from image/Dockerfile.template rename to image/Dockerfile index 341c910..f99117e 100644 --- a/image/Dockerfile.template +++ b/image/Dockerfile @@ -1,18 +1,16 @@ +# The default python version is 3.10 +ARG PYTHON_VERSION=3.10 + # Select the base image -FROM python:{{PYTHON_VERSION}}-slim-bookworm AS python +FROM python:${PYTHON_VERSION}-slim-bookworm -# Make the debian frontend non-interactive for apt installations -ENV DEBIAN_FRONTEND=noninteractive +# Copy the requirements file +COPY requirements.txt /tmp/requirements.txt # Upgrade pip and install dependencies -RUN pip install --upgrade \ - pip ipython \ - uvicorn==0.30.3 gunicorn==22.0.0 \ - starlette==0.38.2 websockets==12.0 python-multipart==0.0.9 \ - redis==5.0.8 hiredis==3.0.0 munch==4.0.0 rednest==0.5.0 runtypes==0.6.1 guardify==0.2.3 +RUN pip install -U -r /tmp/requirements.txt pip ipython # Copy default configurations -COPY configurations/gunicorn.conf /etc/gunicorn/gunicorn.conf COPY configurations/entrypoint.conf /etc/entrypoint/entrypoint.conf # Copy the sources diff --git a/image/configurations/entrypoint.conf b/image/configurations/entrypoint.conf index 5e4f959..6c8c17e 100644 --- a/image/configurations/entrypoint.conf +++ b/image/configurations/entrypoint.conf @@ -3,11 +3,11 @@ command=python worker.py directory=/application/backend [http] -command=gunicorn --workers 4 --forwarded-allow-ips * --worker-class uvicorn.workers.UvicornWorker --bind 0.0.0.0:80 app:app +command=gunicorn --workers 4 --forwarded-allow-ips * --worker-class uvicorn.workers.UvicornWorker --access-logfile /dev/stdout --bind 0.0.0.0:80 app:app directory=/application/backend [https] -command=gunicorn --workers 4 --forwarded-allow-ips * --worker-class uvicorn.workers.UvicornWorker --bind 0.0.0.0:443 --certfile=/etc/ssl/private/server.crt --keyfile=/etc/ssl/private/server.key app:app +command=gunicorn --workers 4 --forwarded-allow-ips * --worker-class uvicorn.workers.UvicornWorker --access-logfile /dev/stdout --bind 0.0.0.0:443 --certfile /etc/ssl/private/server.crt --keyfile /etc/ssl/private/server.key app:app directory=/application/backend [include] diff --git a/image/configurations/gunicorn.conf b/image/configurations/gunicorn.conf deleted file mode 100644 index f531649..0000000 --- a/image/configurations/gunicorn.conf +++ /dev/null @@ -1,28 +0,0 @@ -[loggers] -keys=root, gunicorn.error - -[handlers] -keys=console - -[formatters] -keys=redis - -[logger_root] -level=INFO -handlers=console - -[logger_gunicorn.error] -level=INFO -handlers=console -propagate=0 -qualname=gunicorn.error - -[handler_console] -class=StreamHandler -formatter=redis -args=(sys.stdout, ) - -[formatter_redis] -format=%(process)d:G %(asctime)s * %(message)s -datefmt=%d %b %Y %H:%M:%S.000 -class=logging.Formatter \ No newline at end of file diff --git a/image/requirements.txt b/image/requirements.txt new file mode 100644 index 0000000..b908145 --- /dev/null +++ b/image/requirements.txt @@ -0,0 +1,11 @@ +uvicorn==0.30.3 +gunicorn==22.0.0 +starlette==0.38.2 +websockets==12.0 +python-multipart==0.0.9 +redis==5.0.8 +hiredis==3.0.0 +munch==4.0.0 +rednest==0.5.1 +runtypes==0.6.1 +guardify==0.2.3 \ No newline at end of file diff --git a/image/src/backend/app.py b/image/src/backend/app.py index 5c1423b..d968260 100644 --- a/image/src/backend/app.py +++ b/image/src/backend/app.py @@ -6,7 +6,7 @@ # Import the router from utilities.redis import wait_for_redis_sync, broadcast_sync, broadcast_async, receive_sync, receive_async, redict -from utilities.starlette import router +from utilities.starlette import WebSocket, router # Wait for redis to ping back before operating on database wait_for_redis_sync() @@ -25,7 +25,7 @@ def click_request() -> str: logging.info("User clicked - count is now %d", DATABASE.count) # Return the ping count - return "Click count is %d" % DATABASE.count + return f"Click count is {DATABASE.count}" @router.post("/api/relay") @@ -39,7 +39,7 @@ async def relay_request(message: str, sender: Optional[Email] = None) -> None: @router.socket("/socket/relay") -async def relay_socket(websocket) -> None: +async def relay_socket(websocket: WebSocket) -> None: # Accept the websocket await websocket.accept() diff --git a/image/src/backend/utilities/debug.py b/image/src/backend/utilities/debug.py index 19d5776..ee4e0de 100644 --- a/image/src/backend/utilities/debug.py +++ b/image/src/backend/utilities/debug.py @@ -1,4 +1,4 @@ import os # Get debug state -DEBUG = bool(int(os.environ.get("DEBUG", 0))) \ No newline at end of file +DEBUG = bool(int(os.environ.get("DEBUG", 0))) diff --git a/image/src/backend/utilities/redis.py b/image/src/backend/utilities/redis.py index 2e5e7f7..f36ce14 100644 --- a/image/src/backend/utilities/redis.py +++ b/image/src/backend/utilities/redis.py @@ -1,10 +1,13 @@ import os import time import json -import munch +import typing import asyncio import contextlib +# Import munch utility +import munch + # Import redis utilities import redis import redis.asyncio @@ -23,14 +26,21 @@ REDIS_ASYNC = redis.asyncio.Redis.from_url(REDIS_URL, decode_responses=True) # Patch the dictionary copy type +# pylint: disable-next=protected-access Dictionary._COPY_TYPE = munch.Munch + # Create wrapper functions for databases -relist = lambda name: List(REDIS_SYNC, name) -redict = lambda name: Dictionary(REDIS_SYNC, name) +def relist(name: str) -> List: + return List(REDIS_SYNC, name) + + +def redict(name: str) -> Dictionary: + return Dictionary(REDIS_SYNC, name) -def wait_for_redis_sync(): +# Functions to wait for redis +def wait_for_redis_sync() -> None: # Initialize ping response ping_response = None @@ -45,7 +55,7 @@ def wait_for_redis_sync(): time.sleep(1) -async def wait_for_redis_async(): +async def wait_for_redis_async() -> None: # Initialize ping response ping_response = None @@ -60,22 +70,22 @@ async def wait_for_redis_async(): await asyncio.sleep(1) -def broadcast_sync(channel=GLOBAL_CHANNEL, redis=REDIS_SYNC, **parameters): +def broadcast_sync(channel: str = GLOBAL_CHANNEL, connection: redis.Redis = REDIS_SYNC, **parameters: typing.Any) -> None: # Publish to channel - redis.publish(channel, json.dumps(parameters)) + connection.publish(channel, json.dumps(parameters)) -async def broadcast_async(channel=GLOBAL_CHANNEL, redis=REDIS_ASYNC, **parameters): +async def broadcast_async(channel: str = GLOBAL_CHANNEL, connection: redis.Redis = REDIS_ASYNC, **parameters: typing.Any) -> None: # Publish to channel - await redis.publish(channel, json.dumps(parameters)) + await connection.publish(channel, json.dumps(parameters)) -def receive_sync(channel=GLOBAL_CHANNEL, redis=REDIS_SYNC, count=0): +def receive_sync(channel: str = GLOBAL_CHANNEL, connection: redis.Redis = REDIS_SYNC, count: int = 0) -> munch.Munch: # Count messages received = 0 # Create Pub / Sub subscriber - with redis.pubsub() as subscriber: + with connection.pubsub() as subscriber: # Subscribe to channel subscriber.subscribe(channel) @@ -102,12 +112,12 @@ def receive_sync(channel=GLOBAL_CHANNEL, redis=REDIS_SYNC, count=0): received += 1 -async def receive_async(channel=GLOBAL_CHANNEL, redis=REDIS_ASYNC, count=0): +async def receive_async(channel: str = GLOBAL_CHANNEL, connection: redis.Redis = REDIS_ASYNC, count: int = 0) -> munch.Munch: # Count messages received = 0 # Create Pub / Sub subscriber - async with redis.pubsub() as subscriber: + async with connection.pubsub() as subscriber: # Subscribe to channel await subscriber.subscribe(channel) diff --git a/image/src/backend/utilities/starlette.py b/image/src/backend/utilities/starlette.py index 2a5477a..369d3c5 100644 --- a/image/src/backend/utilities/starlette.py +++ b/image/src/backend/utilities/starlette.py @@ -5,7 +5,7 @@ from utilities.debug import DEBUG # Import starlette utilities -from starlette.routing import Mount, Route, WebSocketRoute +from starlette.routing import BaseRoute, Mount, Route, WebSocketRoute from starlette.requests import Request from starlette.responses import Response, JSONResponse, PlainTextResponse from starlette.websockets import WebSocket @@ -28,35 +28,13 @@ MIMETYPE_SIMPLE_FORM = "application/x-www-form-urlencoded" MIMETYPE_MULTIPART_FORM = "multipart/form-data" +# Function type for decorators +Function = typing.TypeVar("Function", bound=typing.Callable[..., typing.Any]) -def gather_types(types): - # Fetch all of the required types - required_types = { - # Create a key: value without prefix - key[len(PREFIX_REQUIRED):]: types.pop(key) - # For all keys in options - for key in list(types) - # That start with the prefix - if key.startswith(PREFIX_REQUIRED) - } - - # Fetch all of the optional types - optional_types = { - # Create a key: value without prefix - key[len(PREFIX_OPTIONAL):]: types.pop(key) - # For all keys in options - for key in list(types) - # That start with the prefix - if key.startswith(PREFIX_OPTIONAL) - } - - # Return the type dicts and the remaining types - return required_types, optional_types, types - - -async def gather_parameters(request_or_websocket: typing.Union[Request, WebSocket]): + +async def gather_parameters(request_or_websocket: typing.Union[Request, WebSocket]) -> typing.Dict[str, typing.Any]: # Create a dictionary to store all of the paremters - parameters = dict() + parameters: typing.Dict[str, typing.Any] = {} # Update the request parameters using the path parameters for key, value in request_or_websocket.path_params.items(): @@ -111,16 +89,18 @@ async def gather_parameters(request_or_websocket: typing.Union[Request, WebSocke return parameters -class Router(object): +class Router: + + def __init__(self, root: typing.Optional[str] = None) -> None: + # Initialize root directory + self.root: typing.Optional[str] = root - def __init__(self, static_directory: typing.Optional[str] = None) -> None: # Initialize routes - self.routes = [] - self.static_directory = static_directory + self.routes: typing.List[BaseRoute] = [] - def socket(self, path, cast=True): + def socket(self, path: str, cast: bool = True) -> typing.Callable[[Function], Function]: # Create a decorator function - def decorator(function): + def decorator(function: Function) -> Function: # Make sure the function is a coroutine function assert inspect.iscoroutinefunction(function), "Socket routes must be async" @@ -149,11 +129,11 @@ async def endpoint(websocket: WebSocket) -> None: return function # Return the decorator - return decorator + return typing.cast(Function, decorator) - def route(self, path, methods, cast=True): + def route(self, path: str, methods: typing.List[str], cast: bool = True) -> typing.Callable[[Function], Function]: # Create a decorator function - def decorator(function): + def decorator(function: Function) -> Function: # Create the request endpoint function async def endpoint(request: Request) -> Response: @@ -186,21 +166,21 @@ async def endpoint(request: Request) -> Response: return function # Return the decorator - return decorator + return typing.cast(Function, decorator) - def get(self, path): + def get(self, path: str) -> typing.Callable[[Function], Function]: return self.route(path, methods=["GET"]) - def post(self, path): + def post(self, path: str) -> typing.Callable[[Function], Function]: return self.route(path, methods=["POST"]) - def put(self, path): + def put(self, path: str) -> typing.Callable[[Function], Function]: return self.route(path, methods=["PUT"]) - def delete(self, path): + def delete(self, path: str) -> typing.Callable[[Function], Function]: return self.route(path, methods=["DELETE"]) - def initialize(self): + def initialize(self) -> Starlette: # Create exception handler exception_handlers = { # When any exception occurs, return an exception string @@ -211,8 +191,9 @@ def initialize(self): routes = list(self.routes) # Create the static files route as needed - if self.static_directory is not None: - routes.append(Mount(path="/", app=StaticFiles(directory=self.static_directory, html=True))) + if self.root is not None: + # This route should be last as it is a fallback route + routes.append(Mount(path="/", app=StaticFiles(directory=self.root, html=True))) # Initialize the starlette application return Starlette(debug=DEBUG, routes=routes, exception_handlers=exception_handlers) diff --git a/image/src/entrypoint.py b/image/src/entrypoint.py index 54661fd..876e1f0 100644 --- a/image/src/entrypoint.py +++ b/image/src/entrypoint.py @@ -1,9 +1,9 @@ #!/usr/bin/env python import os +import sys import glob import shlex -import signal import logging import argparse import subprocess @@ -14,7 +14,6 @@ # Create the argument parser parser = argparse.ArgumentParser() -parser.add_argument("-t", "--timeout", type=int, default=10) parser.add_argument("-c", "--configuration", type=str, default="/etc/entrypoint/entrypoint.conf") parser.add_argument("programs", type=str, nargs="*") @@ -59,12 +58,12 @@ program_configuration = configuration[name] # Create the processes - for worker in range(int(program_configuration.get("replication", 1))): + for worker in range(int(program_configuration.get("replication", "1"))): # Create the process using the values process = subprocess.Popen(shlex.split(program_configuration["command"]), stdin=devnull, cwd=program_configuration.get("directory")) # Add the process to the dictionary - processes[process.pid] = ("%s_%d" % (name, worker + 1), process) + processes[process.pid] = (f"{name}_{worker + 1}", process) # Log the startup logging.info("Started worker %d for %s", worker + 1, name) @@ -114,4 +113,4 @@ os.close(devnull) # Exit with the error code - exit(exit_code) + sys.exit(exit_code) diff --git a/resources/scripts/create_dockerfile.py b/resources/scripts/create_dockerfile.py deleted file mode 100644 index f9dde29..0000000 --- a/resources/scripts/create_dockerfile.py +++ /dev/null @@ -1,28 +0,0 @@ -import sys -import jinja2 -import argparse - - -def main(): - # Create argument parser - parser = argparse.ArgumentParser() - parser.add_argument("--python-version", action="store", required=True, help="Python version to use for creating the Dockerfile") - - # Parse the arguments - arguments = parser.parse_args() - - # Read the stdin - template = sys.stdin.read() - - # Create the jinja template - jinja_template = jinja2.Template(template) - - # Render the template - dockerfile = jinja_template.render({"PYTHON_VERSION": arguments.python_version}) - - # Write the dockerfile to stdout - sys.stdout.write(dockerfile) - - -if __name__ == "__main__": - main() diff --git a/resources/scripts/create_headless_page.py b/resources/scripts/create_headless_page.py index 2882af5..5ef1654 100644 --- a/resources/scripts/create_headless_page.py +++ b/resources/scripts/create_headless_page.py @@ -5,7 +5,7 @@ import argparse -def substitute_with_file(content, base_path, file_path, content_type): +def substitute_with_file(content: str, base_path: str, file_path: str, content_type: str) -> str: # Open the file for reading and read contents with open(os.path.join(base_path, file_path.lstrip("/")), "rb") as file: data = file.read() @@ -14,7 +14,7 @@ def substitute_with_file(content, base_path, file_path, content_type): return content.replace(f'"{file_path}"', f'"data:{content_type};base64,{base64.b64encode(data).decode()}"') -def main(): +def main() -> None: # Create argument parser parser = argparse.ArgumentParser() parser.add_argument("--base-directory", action="store", required=True, help="Directory to use for rendering the template")