commit e817baab555e01de8efe5ce8261ab9243d788f15 Author: Pavel Date: Mon Aug 12 22:22:51 2024 +0300 init diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..d6dec62 --- /dev/null +++ b/.env.example @@ -0,0 +1,14 @@ +SECRET_KEY=secret +DEBUG=1 +CSRF_TRUSTED_ORIGINS=http://localhost:8000 http://127.0.0.1:8000 +ALLOWED_HOSTS=* + +DB_NAME=postgres +DB_USER=postgres +DB_PASSWORD=postgres +DB_HOST=db +DB_PORT=5432 + +REDIS_HOST=redis +REDIS_PORT=6379 +REDIS_PASSWORD=redis \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..89c8bc4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +static +.env +venv +.idea \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..fe07abb --- /dev/null +++ b/Dockerfile @@ -0,0 +1,19 @@ +FROM python:3.11.6-slim + +# set work directory +WORKDIR /app + +# set env variables +ENV PYTHONDONTWRITEBYTECODE 1 +ENV PYTHONUNBUFFERED 1 + +RUN apt-get update +RUN apt-get install -y python3-dev gcc libc-dev libffi-dev +RUN apt-get -y install libpq-dev gcc + +# install dependencies +COPY src/requirements.txt . +RUN pip install --upgrade pip +RUN pip install -r requirements.txt +# copy project +COPY src/. . \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..6d687eb --- /dev/null +++ b/README.md @@ -0,0 +1,13 @@ +# simpliest django(uvicorn)+postgresql+fastapi+redis+nginx docker-compose (ready for production and dev) +To run: +`docker-compose up -d` + +Site available on 8000 port. + +You can make any changes in code, they will appear automatically. If you want to execute something with manage.py use: +``` +docker-compose exec app python3 manage.py migrate +docker-compose exec app python3 manage.py makemigrations +docker-compose exec app python3 manage.py update_admin admin adminpass # create superuser +``` +and so on. diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 0000000..f53ea85 --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,66 @@ +services: + db: + image: postgres:latest + command: postgres -c 'max_connections=200' + environment: + - POSTGRES_USER=${DB_USER} + - POSTGRES_PASSWORD=${DB_PASSWORD} + - POSTGRES_DB=${DB_NAME} + volumes: + - postgresql-data:/var/lib/postgresql/data + restart: on-failure + + nginx: + command: nginx -g "daemon off;" + depends_on: + - app + - api + image: nginx:alpine + restart: on-failure + volumes: + - ./nginx/nginx.conf:/etc/nginx/nginx.conf + - static:/var/www/app/static + ports: + - "8000:8000" + + app: + build: + context: . + dockerfile: Dockerfile + command: bash -c 'while ! Union[str, dict, int, list, None]: + value = self.redis.get(key) + if value is None: + return default + + value = value.decode("utf-8") + with contextlib.suppress(Exception): + value = json.loads(value) + return value + + def delete(self, key: str): + self.redis.delete(key) diff --git a/src/core/__init__.py b/src/core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/core/asgi.py b/src/core/asgi.py new file mode 100644 index 0000000..68ff548 --- /dev/null +++ b/src/core/asgi.py @@ -0,0 +1,31 @@ +""" +ASGI config for core 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/3.2/howto/deployment/asgi/ +""" + +import os + +from django.core.asgi import get_asgi_application +from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "core.settings") +application = get_asgi_application() + +from application.routers.api import router + + +fastapp = FastAPI() + +fastapp.include_router(router) +fastapp.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) diff --git a/src/core/settings.py b/src/core/settings.py new file mode 100644 index 0000000..4b5880a --- /dev/null +++ b/src/core/settings.py @@ -0,0 +1,100 @@ +import os + +from pathlib import Path + +BASE_DIR = Path(__file__).resolve().parent.parent + +SECRET_KEY = os.getenv("SECRET_KEY") + +DEBUG = bool(int(os.getenv("DEBUG", 1))) + +ALLOWED_HOSTS = os.getenv("ALLOWED_HOSTS", "*").split(" ") + +CSRF_TRUSTED_ORIGINS = os.getenv("CSRF_TRUSTED_ORIGINS", "http://* https://*").split(" ") + +REDIS_HOST = os.getenv("REDIS_HOST") +REDIS_PORT = int(os.getenv("REDIS_PORT")) +REDIS_PASSWORD = os.getenv("REDIS_PASSWORD") + +INSTALLED_APPS = [ + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + 'application' +] + +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 = 'core.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 = 'core.wsgi.application' +ASGI_APPLICATION = "core.asgi.application" + +DATABASES = { + "default": { + "ENGINE": "django.db.backends.postgresql_psycopg2", + "NAME": os.getenv("DB_NAME"), + "USER": os.getenv("DB_USER"), + "PASSWORD": os.getenv("DB_PASSWORD"), + "HOST": os.getenv("DB_HOST"), + "PORT": os.getenv("DB_PORT"), + } +} + +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_L10N = True + +USE_TZ = True + +STATIC_URL = '/static/' +STATIC_ROOT = os.path.join(BASE_DIR, 'static') + +DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' +os.environ["DJANGO_ALLOW_ASYNC_UNSAFE"] = "true" diff --git a/src/core/urls.py b/src/core/urls.py new file mode 100644 index 0000000..22cbb40 --- /dev/null +++ b/src/core/urls.py @@ -0,0 +1,21 @@ +"""core URL Configuration + +The `urlpatterns` list routes URLs to views. For more information please see: + https://docs.djangoproject.com/en/3.2/topics/http/urls/ +Examples: +Function views + 1. Add an import: from my_app import views + 2. Add a URL to urlpatterns: path('', views.home, name='home') +Class-based views + 1. Add an import: from other_app.views import Home + 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') +Including another URLconf + 1. Import the include() function: from django.urls import include, path + 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) +""" +from django.contrib import admin +from django.urls import path + +urlpatterns = [ + path('django/admin/', admin.site.urls), +] diff --git a/src/core/wsgi.py b/src/core/wsgi.py new file mode 100644 index 0000000..135b7a6 --- /dev/null +++ b/src/core/wsgi.py @@ -0,0 +1,16 @@ +""" +WSGI config for core 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/3.2/howto/deployment/wsgi/ +""" + +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'core.settings') + +application = get_wsgi_application() diff --git a/src/helpers/__init__.py b/src/helpers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/helpers/models/__init__.py b/src/helpers/models/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/helpers/models/fields.py b/src/helpers/models/fields.py new file mode 100644 index 0000000..6221132 --- /dev/null +++ b/src/helpers/models/fields.py @@ -0,0 +1,37 @@ +import json + +from django.db import models + + +class JSONField(models.TextField): + """JSONField is a generic textfield that neatly serializes/unserializes + JSON objects seamlessly""" + + def to_python(self, value): + """Convert our string value to JSON after we load it from the DB""" + + if value == "" or value is None: + return None + elif isinstance(value, str): + value = json.loads(value) + else: + raise TypeError("Not valid value type in db") + + return value + + def from_db_value(self, value, expression, connection): + return self.to_python(value) + + def get_prep_value(self, value): + """Convert our JSON object to a string before we save""" + + if isinstance(value, (dict, list)): + value = json.dumps(value) + elif value == "" or value is None: + pass + else: + raise TypeError(f"Not valid value type. ValueType={type(value)}") + return value + + def value_from_object(self, obj): + return json.dumps(super().value_from_object(obj)) diff --git a/src/manage.py b/src/manage.py new file mode 100644 index 0000000..f2a662c --- /dev/null +++ b/src/manage.py @@ -0,0 +1,22 @@ +#!/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', 'core.settings') + try: + from django.core.management import execute_from_command_line + except ImportError as exc: + raise ImportError( + "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?" + ) from exc + execute_from_command_line(sys.argv) + + +if __name__ == '__main__': + main() diff --git a/src/requirements.txt b/src/requirements.txt new file mode 100644 index 0000000..f17f7c1 --- /dev/null +++ b/src/requirements.txt @@ -0,0 +1,5 @@ +uvicorn==0.27.1 +fastapi==0.109.0 +Django==5.0.2 +psycopg2 +redis==4.6.0 \ No newline at end of file