diff --git a/.env.template b/.env.template index c5f00e2..fb45ed3 100644 --- a/.env.template +++ b/.env.template @@ -12,3 +12,6 @@ POSTGRES_HOST= POSTGRES_PASSWORD= POSTGRES_USER= POSTGRES_DB= + +DRF_SECRET_KEY= +DRF_DEBUG= \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index ee2f1c1..e126bfd 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,9 +6,11 @@ ENV PYTHONDONTWRITEBYTECODE=1 RUN adduser -u 5678 --disabled-password --gecos "" appuser WORKDIR /app +COPY --chown=appuser:appuser manage.py /app/manage.py COPY requirements.txt . RUN python -m pip install --no-cache-dir --upgrade -r requirements.txt -COPY --chown=appuser:appuser eveal /app/eveal +COPY --chown=appuser:appuser mabras /app/mabras +COPY --chown=appuser:appuser api /app/api USER appuser -CMD ["uvicorn", "eveal.main:app", "--host", "0.0.0.0", "--port", "8000"] \ No newline at end of file +CMD ["uvicorn", "mabras.asgi:application", "--host", "0.0.0.0", "--port", "8000"] \ No newline at end of file diff --git a/api/__init__.py b/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/api/admin.py b/api/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/api/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/api/apps.py b/api/apps.py new file mode 100644 index 0000000..66656fd --- /dev/null +++ b/api/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class ApiConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'api' diff --git a/eveal/esi.py b/api/esi.py similarity index 100% rename from eveal/esi.py rename to api/esi.py diff --git a/api/migrations/0001_initial.py b/api/migrations/0001_initial.py new file mode 100644 index 0000000..77f2d3a --- /dev/null +++ b/api/migrations/0001_initial.py @@ -0,0 +1,108 @@ +# Generated by Django 4.2.6 on 2023-10-26 15:51 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='SDECategory', + fields=[ + ('id', models.IntegerField(primary_key=True, serialize=False)), + ('name', models.CharField()), + ('published', models.BooleanField()), + ], + ), + migrations.CreateModel( + name='SDEGroup', + fields=[ + ('id', models.IntegerField(primary_key=True, serialize=False)), + ('name', models.CharField()), + ('published', models.BooleanField()), + ('useBasePrice', models.BooleanField()), + ('fittableNonSingletion', models.BooleanField()), + ('anchored', models.BooleanField()), + ('anchorable', models.BooleanField()), + ('category', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='groups', to='api.sdecategory')), + ], + ), + migrations.CreateModel( + name='SDEIcon', + fields=[ + ('id', models.IntegerField(primary_key=True, serialize=False)), + ('iconFile', models.CharField()), + ('description', models.CharField()), + ], + ), + migrations.CreateModel( + name='SDEMarektGroup', + fields=[ + ('id', models.IntegerField(primary_key=True, serialize=False)), + ('name', models.CharField()), + ('description', models.CharField(default='')), + ('hasTypes', models.BooleanField()), + ('icon', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='api.sdeicon')), + ('parent_marketgroup', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='child_marketgroups', to='api.sdemarektgroup')), + ], + ), + migrations.CreateModel( + name='SDEMetaGroup', + fields=[ + ('id', models.IntegerField(primary_key=True, serialize=False)), + ('name', models.CharField()), + ('iconSuffix', models.CharField()), + ('icon', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='metagroups', to='api.sdeicon')), + ], + ), + migrations.CreateModel( + name='SDEType', + fields=[ + ('id', models.IntegerField(primary_key=True, serialize=False)), + ('name', models.CharField()), + ('description', models.CharField()), + ('published', models.BooleanField()), + ('basePrice', models.FloatField()), + ('volume', models.FloatField()), + ('portionSize', models.IntegerField()), + ('group', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='types', to='api.sdegroup')), + ('icon', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='types', to='api.sdeicon')), + ('marketgroup', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='types', to='api.sdemarektgroup')), + ], + ), + migrations.CreateModel( + name='SDETypeMaterial', + fields=[ + ('id', models.IntegerField(primary_key=True, serialize=False)), + ('quantity', models.IntegerField()), + ('material_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='typematerials_of', to='api.sdetype')), + ('type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='typematerials', to='api.sdetype')), + ], + ), + migrations.AddField( + model_name='sdetype', + name='materials', + field=models.ManyToManyField(related_name='material_of', through='api.SDETypeMaterial', to='api.sdetype'), + ), + migrations.AddField( + model_name='sdetype', + name='metagroup', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='types', to='api.sdemetagroup'), + ), + migrations.AddField( + model_name='sdegroup', + name='icon', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='groups', to='api.sdeicon'), + ), + migrations.AddField( + model_name='sdecategory', + name='icon', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='categories', to='api.sdeicon'), + ), + ] diff --git a/api/migrations/__init__.py b/api/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/api/models.py b/api/models.py new file mode 100644 index 0000000..fa6a91e --- /dev/null +++ b/api/models.py @@ -0,0 +1,4 @@ +from django.db import models +from .models_esi import * +from .models_sde import * + diff --git a/api/models_esi.py b/api/models_esi.py new file mode 100644 index 0000000..beeb308 --- /dev/null +++ b/api/models_esi.py @@ -0,0 +1,2 @@ +from django.db import models + diff --git a/api/models_sde.py b/api/models_sde.py new file mode 100644 index 0000000..8b3ff62 --- /dev/null +++ b/api/models_sde.py @@ -0,0 +1,66 @@ +from django.db import models + + +class SDEIcon(models.Model): + id = models.IntegerField(primary_key=True) + iconFile = models.CharField() + description = models.CharField() + + +class SDECategory(models.Model): + id = models.IntegerField(primary_key=True) + icon = models.ForeignKey(SDEIcon, related_name="categories", null=True, on_delete=models.SET_NULL) + name = models.CharField() + published = models.BooleanField() + + +class SDEGroup(models.Model): + id = models.IntegerField(primary_key=True) + category = models.ForeignKey(SDECategory, related_name="groups", on_delete=models.CASCADE) + name = models.CharField() + published = models.BooleanField() + useBasePrice = models.BooleanField() + fittableNonSingletion = models.BooleanField() + anchored = models.BooleanField() + anchorable = models.BooleanField() + icon = models.ForeignKey(SDEIcon, related_name="groups", null=True, on_delete=models.SET_NULL) + + +class SDEMarektGroup(models.Model): + id = models.IntegerField(primary_key=True) + icon = models.ForeignKey(SDEIcon, null=True, on_delete=models.SET_NULL) + name = models.CharField() + description = models.CharField(default="") + hasTypes = models.BooleanField() + parent_marketgroup = models.ForeignKey("self", null=True, related_name="child_marketgroups", on_delete=models.CASCADE) + + +class SDEMetaGroup(models.Model): + id = models.IntegerField(primary_key=True) + icon = models.ForeignKey(SDEIcon, related_name="metagroups", null=True, on_delete=models.SET_NULL) + name = models.CharField() + iconSuffix = models.CharField() + + +class SDEType(models.Model): + id = models.IntegerField(primary_key=True) + group = models.ForeignKey(SDEGroup, related_name="types", on_delete=models.CASCADE) + marketgroup = models.ForeignKey(SDEMarektGroup, related_name="types", on_delete=models.CASCADE) + metagroup = models.ForeignKey(SDEMetaGroup, related_name="types", on_delete=models.CASCADE) + name = models.CharField() + description = models.CharField() + published = models.BooleanField() + basePrice = models.FloatField() + icon = models.ForeignKey(SDEIcon, related_name="types", null=True, on_delete=models.SET_NULL) + volume = models.FloatField() + portionSize = models.IntegerField() + materials = models.ManyToManyField("self", through="SDETypeMaterial", symmetrical=False, related_name="material_of") + + +class SDETypeMaterial(models.Model): + id = models.IntegerField(primary_key=True) + type = models.ForeignKey(SDEType, on_delete=models.CASCADE, related_name="typematerials") + material_type = models.ForeignKey(SDEType, on_delete=models.CASCADE, related_name="typematerials_of") + + quantity = models.IntegerField() + diff --git a/api/serializers.py b/api/serializers.py new file mode 100644 index 0000000..466c3f6 --- /dev/null +++ b/api/serializers.py @@ -0,0 +1,15 @@ +from django.contrib.auth.models import User, Group +from rest_framework import serializers + + +class UserSerializer(serializers.HyperlinkedModelSerializer): + class Meta: + model = User + fields = ['url', 'username', 'email', 'groups'] + + +class GroupSerializer(serializers.HyperlinkedModelSerializer): + class Meta: + model = Group + fields = ['url', 'name'] + diff --git a/api/tests.py b/api/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/api/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/api/views.py b/api/views.py new file mode 100644 index 0000000..4f59f72 --- /dev/null +++ b/api/views.py @@ -0,0 +1,24 @@ +from django.shortcuts import render +from django.contrib.auth.models import User, Group +from rest_framework import viewsets +from rest_framework import permissions +from api.serializers import UserSerializer, GroupSerializer + + +class UserViewSet(viewsets.ModelViewSet): + """ + API endpoint that allows users to be viewed or edited. + """ + queryset = User.objects.all().order_by('-date_joined') + serializer_class = UserSerializer + permission_classes = [permissions.IsAuthenticated] + + +class GroupViewSet(viewsets.ModelViewSet): + """ + API endpoint that allows groups to be viewed or edited. + """ + queryset = Group.objects.all() + serializer_class = GroupSerializer + permission_classes = [permissions.IsAuthenticated] + diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 2e1512c..46800e9 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -1,22 +1,41 @@ version: '3' services: - eveal: + migrations: image: mabras:local build: context: . dockerfile: Dockerfile + env_file: + - .env + user: "1000:1000" + volumes: + - ./mabras:/app/mabras + - ./api:/app/api + - ./manage.py:/app/manage.py + command: "python manage.py makemigrations;python manage.py migrate" + depends_on: + db: + condition: service_healthy + + eveal: + image: mabras:local env_file: - .env ports: - 8000:8000 + user: "1000:1000" volumes: - - ./eveal:/app/eveal - command: ["uvicorn", "eveal.main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"] + - ./mabras:/app/mabras + - ./api:/app/api + - ./manage.py:/app/manage.py + command: ["uvicorn", "mabras.asgi:application", "--host", "0.0.0.0", "--port", "8000", "--reload"] depends_on: redis: condition: service_healthy db: condition: service_healthy + migrations: + condition: service_completed_successfully # elasticsearch: # condition: service_healthy @@ -38,9 +57,9 @@ services: - .env healthcheck: test: pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB} - interval: 10s + interval: 1s timeout: 3s - retries: 3 + retries: 10 # elasticsearch: # image: elasticsearch:latest diff --git a/eveal/main.py b/eveal/main.py index 26214a7..0ec1a85 100644 --- a/eveal/main.py +++ b/eveal/main.py @@ -1,13 +1,12 @@ -from collections import defaultdict - -from fastapi import FastAPI, Depends, Path, Query +from fastapi import FastAPI, Depends from fastapi.middleware.cors import CORSMiddleware -from typing import List, Annotated, Tuple, Literal, Dict, TypedDict -from sqlmodel import SQLModel, Session, select +from typing import List, Dict +from sqlmodel import SQLModel, Session from eveal.schemas import Evepraisal, PriceReprocess from eveal.database import engine, get_session -from eveal import models_sde, esi +from eveal import models_sde +from api import esi SQLModel.metadata.create_all(engine) # use alembic? @@ -80,7 +79,8 @@ async def sde_types_search(query: List[Dict[str, int | str | None | List]], db: async def sde_types_market(sde_type: int | str, region_id: int | str, db: Session = Depends(get_session)): """Get market orders for a type in a region. example: /esi/types/22291/market/10000002/""" """TODO: use ESIMarketOrder""" - return list(esi.esi_client.Market.get_markets_region_id_orders(order_type="all", type_id=sde_type, region_id=region_id)) + return list( + esi.esi_client.Market.get_markets_region_id_orders(order_type="all", type_id=sde_type, region_id=region_id)) @app.get("/_tools/get_all_mat") diff --git a/import_sde.py b/import_sde.py index bdb8197..a827335 100644 --- a/import_sde.py +++ b/import_sde.py @@ -1,10 +1,3 @@ -import yaml -from eveal.database import engine -from sqlmodel import Session, SQLModel, select -from eveal import models_sde - -SQLModel.metadata.drop_all(engine) # use alembic! -SQLModel.metadata.create_all(engine) print("Importing SDE data...") diff --git a/mabras/__init__.py b/mabras/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/mabras/asgi.py b/mabras/asgi.py new file mode 100644 index 0000000..d714762 --- /dev/null +++ b/mabras/asgi.py @@ -0,0 +1,16 @@ +""" +ASGI config for mabras 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', 'mabras.settings') + +application = get_asgi_application() diff --git a/mabras/settings.py b/mabras/settings.py new file mode 100644 index 0000000..81b72fd --- /dev/null +++ b/mabras/settings.py @@ -0,0 +1,141 @@ +""" +Django settings for mabras project. + +Generated by 'django-admin startproject' using Django 4.2.6. + +For more information on this file, see +https://docs.djangoproject.com/en/4.2/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/4.2/ref/settings/ +""" + +from pathlib import Path +import os + +# Build paths inside the project like this: BASE_DIR / 'subdir'. +BASE_DIR = Path(__file__).resolve().parent.parent + + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/4.2/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = os.getenv("DRF_SECRET_KEY") + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = os.getenv("DRF_DEBUG", False) == "True" + +ALLOWED_HOSTS = [] + + +# Application definition + +REST_FRAMEWORK = { + 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination', + 'PAGE_SIZE': 10, + 'DEFAULT_RENDERER_CLASSES': [ + 'rest_framework.renderers.JSONRenderer', + ], + 'DEFAULT_PARSER_CLASSES': [ + 'rest_framework.parsers.JSONParser', + ], +} + +INSTALLED_APPS = [ + 'api', + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + 'rest_framework' +] + +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 = 'mabras.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 = 'mabras.wsgi.application' + + +# Database +# https://docs.djangoproject.com/en/4.2/ref/settings/#databases + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.postgresql', + "HOST": os.getenv("POSTGRES_HOST"), + "PORT": os.getenv("POSTGRES_PORT", 5432), + "USER": os.getenv("POSTGRES_USER"), + "PASSWORD": os.getenv("POSTGRES_PASSWORD"), + "NAME": os.getenv("POSTGRES_DB"), + } +} + + +# Password validation +# https://docs.djangoproject.com/en/4.2/ref/settings/#auth-password-validators + +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', + }, +] + + +# Internationalization +# https://docs.djangoproject.com/en/4.2/topics/i18n/ + +LANGUAGE_CODE = 'en-us' + +TIME_ZONE = 'UTC' + +USE_I18N = True + +USE_TZ = True + + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/4.2/howto/static-files/ + +STATIC_URL = 'static/' + +# Default primary key field type +# https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field + +DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' diff --git a/mabras/urls.py b/mabras/urls.py new file mode 100644 index 0000000..3d54102 --- /dev/null +++ b/mabras/urls.py @@ -0,0 +1,31 @@ +""" +URL configuration for mabras project. + +The `urlpatterns` list routes URLs to views. For more information please see: + https://docs.djangoproject.com/en/4.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 include, path +from rest_framework import routers +from api import views + +router = routers.DefaultRouter() +router.register(r'users', views.UserViewSet) +router.register(r'groups', views.GroupViewSet) + +# Wire up our API using automatic URL routing. +# Additionally, we include login URLs for the browsable API. +urlpatterns = [ + path('', include(router.urls)), + path('api-auth/', include('rest_framework.urls', namespace='rest_framework')) +] \ No newline at end of file diff --git a/mabras/wsgi.py b/mabras/wsgi.py new file mode 100644 index 0000000..f15aaf8 --- /dev/null +++ b/mabras/wsgi.py @@ -0,0 +1,16 @@ +""" +WSGI config for mabras 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', 'mabras.settings') + +application = get_wsgi_application() diff --git a/manage.py b/manage.py new file mode 100755 index 0000000..3a4a74c --- /dev/null +++ b/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', 'mabras.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/requirements.txt b/requirements.txt index 8c0c8cc..32efb56 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,6 @@ -fastapi -httpx +django +djangorestframework +django-filter +markdown uvicorn[standard] -sqlmodel -esy -redis -psycopg2-binary +psycopg[binary]