diff --git a/Pipfile b/Pipfile index 92883c8..5b6e62f 100644 --- a/Pipfile +++ b/Pipfile @@ -29,7 +29,7 @@ boto3 = "==1.34.162" [dev-packages] codeforlife = {version = "==0.22.12", extras = ["dev"]} -boto3-stubs = {version = "==1.35.71", extras = ["ec2", "s3"]} +boto3-stubs = {version = "==1.35.71", extras = ["essential"]} # codeforlife = {file = "../codeforlife-package-python", editable = true, extras = ["dev"]} # 🚫 Don't add [dev-packages] below that are inherited from the CFL package. diff --git a/Pipfile.lock b/Pipfile.lock index b6299dd..11502e6 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "bd61f40d9a9ef8902128f66bba1113b96771528f34561cf4d3183ab558672ed6" + "sha256": "57ef88cb4cdf3214e3bce25a07a3b7b944ccdd27d908eda1a6c54a1713384bda" }, "pipfile-spec": 6, "requires": { @@ -999,8 +999,7 @@ }, "boto3-stubs": { "extras": [ - "ec2", - "s3" + "essential" ], "hashes": [ "sha256:4abf357250bdb16d1a56489a59bfc385d132a43677956bd984f6578638d599c0", @@ -1543,6 +1542,20 @@ ], "version": "==1.6.1" }, + "mypy-boto3-cloudformation": { + "hashes": [ + "sha256:aba213f3411a65096a8d95633c36e0c57a775ac6ac9ccf1e6fd9bea4002073bc", + "sha256:d1a1500df811ac8ebd459640f5b31c14daac784d8a00fc4f67bc6eb391e7b5a8" + ], + "version": "==1.35.64" + }, + "mypy-boto3-dynamodb": { + "hashes": [ + "sha256:187915c781f352bc79d35b08a094605515ecc54f30107f629972c3358b864a5c", + "sha256:92eac35c49e9f3ff23a4ad6dee5dc54e410e0c49a98b4d93493c7000ebe74568" + ], + "version": "==1.35.60" + }, "mypy-boto3-ec2": { "hashes": [ "sha256:93f9ddadac303d63f34cd4c0a60a682c008d655d3b2cfa74d1234fbde9a0b401", @@ -1550,6 +1563,20 @@ ], "version": "==1.35.70" }, + "mypy-boto3-lambda": { + "hashes": [ + "sha256:00499898236fe423c9292f77644102d4bd6699b3c16b8c4062eb759c022447f5", + "sha256:577a9465ac63ac564efc2755a7e72c28a9d2f496747c1faf242cb13d5017b262" + ], + "version": "==1.35.68" + }, + "mypy-boto3-rds": { + "hashes": [ + "sha256:0850cb5bddda1853c6ba44bb8dc1bf0d303ea4729f8cdf982d0e4d91f08ab2d9", + "sha256:7bfeadfbd361aaf53a5f161c571886d3cadbdf05c15591761280fe6f079ab273" + ], + "version": "==1.35.66" + }, "mypy-boto3-s3": { "hashes": [ "sha256:11a34259983e09d67e4d3a322fd47904a006bbfff19984e4e36a77e30f2014bb", @@ -1557,6 +1584,13 @@ ], "version": "==1.35.69" }, + "mypy-boto3-sqs": { + "hashes": [ + "sha256:61752f1c2bf2efa3815f64d43c25b4a39dbdbd9e472ae48aa18d7c6d2a7a6eb8", + "sha256:9fd6e622ed231c06f7542ba6f8f0eea92046cace24defa95d0d0ce04e7caee0c" + ], + "version": "==1.35.0" + }, "mypy-extensions": { "hashes": [ "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d", diff --git a/application.py b/application.py index 6ff8e28..cdf3864 100644 --- a/application.py +++ b/application.py @@ -7,12 +7,55 @@ import os -from codeforlife.app import StandaloneApplication +# from codeforlife.app import StandaloneApplication from django.core.asgi import get_asgi_application from django.core.wsgi import get_wsgi_application os.environ.setdefault("DJANGO_SETTINGS_MODULE", "settings") +# TODO: delete this +# pylint: disable-all + +import multiprocessing +import typing as t + +from django.core.management import call_command +from gunicorn.app.base import BaseApplication # type: ignore[import-untyped] + + +# pylint: disable-next=abstract-method +class StandaloneApplication(BaseApplication): + """A server for an app in a live environment. + + Based off of: + https://gist.github.com/Kludex/c98ed6b06f5c0f89fd78dd75ef58b424 + https://docs.gunicorn.org/en/stable/custom.html + """ + + def __init__(self, app: t.Callable): + call_command("migrate", interactive=False) + + self.options = { + "bind": "0.0.0.0:8080", + # https://docs.gunicorn.org/en/stable/design.html#how-many-workers + "workers": 1, + "worker_class": "uvicorn.workers.UvicornWorker", + } + self.application = app + super().__init__() + + def load_config(self): + config = { + key: value + for key, value in self.options.items() + if key in self.cfg.settings and value is not None + } + for key, value in config.items(): + self.cfg.set(key.lower(), value) + + def load(self): + return self.application + if __name__ == "__main__": StandaloneApplication(app=get_asgi_application()).run() diff --git a/settings.py b/settings.py index 8f1d0a9..fd5284d 100644 --- a/settings.py +++ b/settings.py @@ -67,97 +67,69 @@ import json import logging +import typing as t import boto3 +from codeforlife.types import JsonDict +if t.TYPE_CHECKING: + from mypy_boto3_s3.client import S3Client -def check_for_pointer_file(S3_APP_BUCKET, S3_APP_KEY): - s3 = boto3.client("s3") - pointer = s3.list_objects( - Bucket=S3_APP_BUCKET, Prefix=f"{S3_APP_KEY}/dbMetadata/pointer" - ) - - if pointer.get("Contents"): - resp = s3.get_object( - Bucket=S3_APP_BUCKET, Key=pointer["Contents"][0]["Key"] - ) - return resp["Body"].read().decode("utf-8") - - return None - - -def construct_db_config(S3_APP_BUCKET, S3_KEY, DB_DATA): - s3 = boto3.client("s3") - - print(f'connecting with bucket: "{S3_APP_BUCKET}", key: "{S3_KEY}"') - logging.info(f'connecting with bucket: "{S3_APP_BUCKET}", key: "{S3_KEY}"') +AWS_S3_APP_BUCKET = os.getenv("aws_s3_app_bucket") +AWS_S3_APP_FOLDER = os.getenv("aws_s3_app_folder") +# AWS_REGION = os.getenv("aws_region") +APP_ID = os.getenv("APP_ID") # type: ignore[assignment] +APP_VERSION = os.getenv("APP_VERSION") # type: ignore[assignment] - cfg = s3.get_object(Bucket=S3_APP_BUCKET, Key=S3_KEY) - config = json.loads(cfg["Body"].read().decode("utf-8")) +def get_databases(): + if AWS_S3_APP_BUCKET and AWS_S3_APP_FOLDER and APP_ID: + key = f"{AWS_S3_APP_FOLDER}/dbMetadata/{APP_ID}/app.dbdata" - if config and config["DBEngine"] == "postgres": - DB_DATA["default"].update( - { - "NAME": config["Database"], - "USER": config["user"], - "PASSWORD": config["password"], - "HOST": config["Endpoint"], - "PORT": config["Port"], - } + print( + f'(print) connecting with bucket: "{AWS_S3_APP_BUCKET}", key: "{key}"' + ) + logging.info( + f'(log) connecting with bucket: "{AWS_S3_APP_BUCKET}", key: "{key}"' ) - return DB_DATA - + s3: "S3Client" = boto3.client("s3") + db_data_object = s3.get_object(Bucket=AWS_S3_APP_BUCKET, Key=key) -def load_db_config(S3_APP_BUCKET, S3_APP_KEY): - s3 = boto3.client("s3") + db_data: JsonDict = json.loads( + db_data_object["Body"].read().decode("utf-8") + ) - default_dict = { + if not db_data or db_data["DBEngine"] != "postgres": + raise ConnectionAbortedError("Invalid database data.") + + name = db_data["Database"] + user = db_data["user"] + password = db_data["password"] + host = db_data["Endpoint"] + port = db_data["Port"] + else: + name = os.getenv("DB_NAME", SERVICE_NAME) + user = os.getenv("DB_USER", "root") + password = os.getenv("DB_PASSWORD", "password") + host = os.getenv("DB_HOST", "localhost") + port = int(os.getenv("DB_PORT", "5432")) + + return { "default": { - # "ENGINE": "django.db.backends.postgresql_psycopg2", "ENGINE": "django.db.backends.postgresql", - "NAME": "", - "USER": "", - "PASSWORD": "", - "HOST": "", - "PORT": "", - "OPTIONS": { - "connect_timeout": 300, - }, + "NAME": name, + "USER": user, + "PASSWORD": password, + "HOST": host, + "PORT": port, + # "OPTIONS": { + # "connect_timeout": 300, + # }, "ATOMIC_REQUESTS": True, } } - link = check_for_pointer_file(S3_APP_BUCKET, S3_APP_KEY) - if link: - return construct_db_config(S3_APP_BUCKET, link, default_dict) - - objs = s3.list_objects( - Bucket=S3_APP_BUCKET, Prefix=f"{S3_APP_KEY}/dbMetadata/" - ) - for config_file in objs.get("Contents", []): - return construct_db_config( - S3_APP_BUCKET, config_file["Key"], default_dict - ) - - -S3_BUCKET = os.getenv("aws_s3_app_bucket") -S3_PREFIX = os.getenv("aws_s3_app_folder") -# AWS_REGION = os.getenv("aws_region") -if S3_BUCKET and S3_PREFIX: - DATABASES = load_db_config(S3_BUCKET, S3_PREFIX) - -# DATABASES = { -# "default": { -# "ENGINE": "django.db.backends.postgresql", -# "NAME": os.getenv("DB_NAME", SERVICE_NAME), -# "HOST": os.getenv("DB_HOST", "localhost"), -# "PORT": int(os.getenv("DB_PORT", "5432")), -# # "USER": os.getenv("DB_USER", "root"), -# # "PASSWORD": os.getenv("DB_PASSWORD", "password"), -# "ATOMIC_REQUESTS": True, -# } -# } +DATABASES = get_databases()