diff --git a/solution/.dockerignore b/solution/.dockerignore index d3ae326..9e66e35 100644 --- a/solution/.dockerignore +++ b/solution/.dockerignore @@ -1,5 +1,5 @@ .dockerignore Dockerfile README.md -.venv/ +venv/ __pycache__/ diff --git a/solution/.gitignore b/solution/.gitignore index 808386f..68bc17f 100644 --- a/solution/.gitignore +++ b/solution/.gitignore @@ -1,2 +1,160 @@ -.venv/ -__pycache__/ \ No newline at end of file +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ diff --git a/solution/Dockerfile b/solution/Dockerfile index c763543..f41cea9 100644 --- a/solution/Dockerfile +++ b/solution/Dockerfile @@ -2,11 +2,17 @@ FROM python:3.12.1-alpine3.19 WORKDIR /app -COPY requirements.txt requirements.txt -RUN pip3 install -r requirements.txt +ENV PYTHONDONTWRITEBYTECODE 1 +ENV PYTHONUNBUFFERED 1 +ENV SERVER_PORT=8080 +ENV DJANGO_DEBUG = False + +RUN pip3 install --upgrade pip +COPY requirements/prod.txt . +RUN pip3 install -r prod.txt + +WORKDIR /app COPY . . -ENV SERVER_PORT=8080 - -CMD ["sh", "-c", "exec python3 -m flask run --host=0.0.0.0 --port=$SERVER_PORT"] \ No newline at end of file +CMD ["sh", "-c", "cd pulse && exec python3 manage.py runserver 0.0.0.0:$SERVER_PORT"] diff --git a/solution/app.py b/solution/app.py deleted file mode 100644 index 2ae2cbf..0000000 --- a/solution/app.py +++ /dev/null @@ -1,10 +0,0 @@ -from flask import Flask, request, jsonify - -app = Flask(__name__) - -@app.route('/api/ping', methods=['GET']) -def send(): - return jsonify({"status": "ok"}), 200 - -if __name__ == "__main__": - app.run() diff --git a/solution/pulse/manage.py b/solution/pulse/manage.py new file mode 100755 index 0000000..a9b2b6b --- /dev/null +++ b/solution/pulse/manage.py @@ -0,0 +1,23 @@ +#!/usr/bin/env python +"""Django's command-line utility for administrative tasks.""" +import os +import sys + + +def main(): + """Run administrative tasks.""" + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "pulse.settings") + try: + from django.core.management import execute_from_command_line + except ImportError as exc: + error = ( + "Couldn't import Django. Are you sure it's installed and " + "available on your PYTHONPATH environment variable? Did you " + "forget to activate a virtual environment?", + ) + raise ImportError(error) from exc + execute_from_command_line(sys.argv) + + +if __name__ == "__main__": + main() diff --git a/solution/pulse/ping/__init__.py b/solution/pulse/ping/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/solution/pulse/ping/apps.py b/solution/pulse/ping/apps.py new file mode 100644 index 0000000..70a3495 --- /dev/null +++ b/solution/pulse/ping/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class PingConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "ping" diff --git a/solution/pulse/ping/urls.py b/solution/pulse/ping/urls.py new file mode 100644 index 0000000..f4a7044 --- /dev/null +++ b/solution/pulse/ping/urls.py @@ -0,0 +1,7 @@ +from django.urls import path + +import ping.views + +urlpatterns = [ + path("", ping.views.PingView.as_view(), name="ping"), +] diff --git a/solution/pulse/ping/views.py b/solution/pulse/ping/views.py new file mode 100644 index 0000000..aecfa84 --- /dev/null +++ b/solution/pulse/ping/views.py @@ -0,0 +1,9 @@ +from rest_framework import status +from rest_framework.response import Response +from rest_framework.views import APIView + + +class PingView(APIView): + def get(self, request): + data = {"message": "ok"} + return Response(data, status=status.HTTP_200_OK) diff --git a/solution/pulse/pulse/__init__.py b/solution/pulse/pulse/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/solution/pulse/pulse/asgi.py b/solution/pulse/pulse/asgi.py new file mode 100644 index 0000000..9afa2bd --- /dev/null +++ b/solution/pulse/pulse/asgi.py @@ -0,0 +1,16 @@ +""" +ASGI config for pulse project. + +It exposes the ASGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/4.2/howto/deployment/asgi/ +""" + +import os + +from django.core.asgi import get_asgi_application + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "pulse.settings") + +application = get_asgi_application() diff --git a/solution/pulse/pulse/settings.py b/solution/pulse/pulse/settings.py new file mode 100644 index 0000000..e159905 --- /dev/null +++ b/solution/pulse/pulse/settings.py @@ -0,0 +1,110 @@ +import os +from pathlib import Path + +from dotenv import load_dotenv + +load_dotenv() + +BASE_DIR = Path(__file__).resolve().parent.parent + +SECRET_KEY = os.getenv("DJANGO_SECRET_KEY", "secret_key") + +DEBUG = os.getenv("DJANGO_DEBUG", "true").lower() in ("true", "1", "yes", "y") + +ALLOWED_HOSTS = os.getenv("DJANGO_ALLOWED_HOSTS", "*").split(" ") + +INSTALLED_APPS = [ + "django.contrib.admin", + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.messages", + "django.contrib.staticfiles", + # Third-party apps + "rest_framework", + # Developed apps + "ping.apps.PingConfig", +] + +MIDDLEWARE = [ + "django.middleware.security.SecurityMiddleware", + "django.contrib.sessions.middleware.SessionMiddleware", + "django.middleware.common.CommonMiddleware", + "django.middleware.csrf.CsrfViewMiddleware", + "django.contrib.auth.middleware.AuthenticationMiddleware", + "django.contrib.messages.middleware.MessageMiddleware", + "django.middleware.clickjacking.XFrameOptionsMiddleware", +] + +ROOT_URLCONF = "pulse.urls" + +TEMPLATES = [ + { + "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": [], + "APP_DIRS": True, + "OPTIONS": { + "context_processors": [ + "django.template.context_processors.debug", + "django.template.context_processors.request", + "django.contrib.auth.context_processors.auth", + "django.contrib.messages.context_processors.messages", + ], + }, + }, +] + +WSGI_APPLICATION = "pulse.wsgi.application" + +DATABASES = { + "default": { + "ENGINE": "django.db.backends.sqlite3", + "NAME": BASE_DIR / "db.sqlite3", + }, +} + +AUTH_PASSWORD_VALIDATORS = [ + { + "NAME": ( + "django.contrib.auth.password_validation." + "UserAttributeSimilarityValidator" + ), + }, + { + "NAME": ( + "django.contrib.auth.password_validation.MinimumLengthValidator" + ), + }, + { + "NAME": ( + "django.contrib.auth.password_validation.CommonPasswordValidator" + ), + }, + { + "NAME": ( + "django.contrib.auth.password_validation.NumericPasswordValidator" + ), + }, +] + +LANGUAGE_CODE = "en-us" + +TIME_ZONE = "UTC" + +USE_I18N = True + +USE_TZ = True + +STATIC_URL = "static/" + +DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" + +if DEBUG: + INSTALLED_APPS.append("debug_toolbar") + MIDDLEWARE.append("debug_toolbar.middleware.DebugToolbarMiddleware") +else: + REST_FRAMEWORK = { + "DEFAULT_RENDERER_CLASSES": [ + "rest_framework.renderers.JSONRenderer", + ], + } diff --git a/solution/pulse/pulse/urls.py b/solution/pulse/pulse/urls.py new file mode 100644 index 0000000..340ccd3 --- /dev/null +++ b/solution/pulse/pulse/urls.py @@ -0,0 +1,20 @@ +from django.conf import settings +from django.contrib import admin +from django.urls import include, path + +urlpatterns = [ + # Built-in urls + path("admin/", admin.site.urls), + path( + "api-auth/", include( + "rest_framework.urls", namespace="rest_framework", + ), + ), + + # API + + path("api/ping/", include("ping.urls")), +] + +if settings.DEBUG: + urlpatterns += (path("__debug__/", include("debug_toolbar.urls")),) diff --git a/solution/pulse/pulse/wsgi.py b/solution/pulse/pulse/wsgi.py new file mode 100644 index 0000000..c7934cd --- /dev/null +++ b/solution/pulse/pulse/wsgi.py @@ -0,0 +1,16 @@ +""" +WSGI config for pulse project. + +It exposes the WSGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/4.2/howto/deployment/wsgi/ +""" + +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "pulse.settings") + +application = get_wsgi_application() diff --git a/solution/requirements.txt b/solution/requirements.txt deleted file mode 100644 index 3bd0d17..0000000 --- a/solution/requirements.txt +++ /dev/null @@ -1,7 +0,0 @@ -blinker==1.7.0 -click==8.1.7 -Flask==3.0.1 -itsdangerous==2.1.2 -Jinja2==3.1.3 -MarkupSafe==2.1.4 -Werkzeug==3.0.1 diff --git a/solution/requirements/dev.txt b/solution/requirements/dev.txt new file mode 100644 index 0000000..473bec5 --- /dev/null +++ b/solution/requirements/dev.txt @@ -0,0 +1,4 @@ +django-debug-toolbar==4.3.0 +ruff==0.2.2 + +-r prod.txt diff --git a/solution/requirements/prod.txt b/solution/requirements/prod.txt new file mode 100644 index 0000000..0d4898e --- /dev/null +++ b/solution/requirements/prod.txt @@ -0,0 +1,3 @@ +django==4.2.10 +djangorestframework==3.14.0 +python-dotenv==1.0.1 diff --git a/solution/ruff.toml b/solution/ruff.toml new file mode 100644 index 0000000..0d3bab9 --- /dev/null +++ b/solution/ruff.toml @@ -0,0 +1,17 @@ +exclude = [ + "migrations", + "venv", +] + +line-length = 79 +indent-width = 4 + +[lint] +select = ["ALL"] +ignore = ["D", "ANN", "ARG"] + +[format] +quote-style = "double" +indent-style = "space" +skip-magic-trailing-comma = false +line-ending = "auto" diff --git a/solution/template.env b/solution/template.env new file mode 100644 index 0000000..20eedb5 --- /dev/null +++ b/solution/template.env @@ -0,0 +1,3 @@ +DJANGO_DEBUG = False +SECRET_KEY = fake +DJANGO_ALLOWED_HOSTS = 127.0.0.1