sde_types_market & rework infra

This commit is contained in:
Tom Villette
2023-10-03 18:03:41 +02:00
parent b90e7028b7
commit d8aa735a55
10 changed files with 251 additions and 126 deletions

16
.env.template Normal file
View File

@@ -0,0 +1,16 @@
ESI_CLIENT_ID=
ESI_SECRET_KEY=
ESI_CALLBACK_URL=
ESI_USER_AGENT=
REDIS_URL=
REDIS_PORT=
REDIS_USER=
REDIS_PASSWORD=
REDIS_SSL=
SQLITE_DB_PATH=
POSTGRES_PASSWORD=
POSTGRES_USER=
POSTGRES_DB=

View File

@@ -1,24 +0,0 @@
FROM python:3.11-slim as sde_import
WORKDIR /app
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 static_eve /app/static_eve
COPY --chown=appuser:appuser import_sde.py /app/import_sde.py
RUN python import_sde.py
FROM python:3.11-slim
ENV PYTHONUNBUFFERED=1
ENV PYTHONDONTWRITEBYTECODE=1
RUN adduser -u 5678 --disabled-password --gecos "" appuser
WORKDIR /app
COPY --from=sde_import --chown=appuser:appuser /app/sde.db /app/sde.db
COPY requirements.txt .
RUN python -m pip install --no-cache-dir --upgrade -r requirements.txt
COPY --chown=appuser:appuser eveal /app/eveal
USER appuser
CMD ["uvicorn", "eveal.main:app", "--host", "0.0.0.0", "--port", "8000"]

62
docker-compose.dev.yml Normal file
View File

@@ -0,0 +1,62 @@
version: '3'
services:
eveal:
image: eveal:latest
build:
context: .
dockerfile: Dockerfile
env_file:
- .env
ports:
- 8000:8000
volumes:
- ./eveal:/app/eveal
- ./eveal.db:/app/eveal.db
command: ["uvicorn", "eveal.main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"]
depends_on:
redis:
condition: service_healthy
# db:
# condition: service_healthy
# elasticsearch:
# condition: service_healthy
redis:
image: redis:latest
ports:
- 6379:6379
healthcheck:
test: redis-cli ping
interval: 3s
# db:
# image: postgres:13-alpine
# ports:
# - 5432:5432
# volumes:
# - ./dump_sde.sql:/docker-entrypoint-initdb.d/init_sde.sql
# - mabras_dbdata:/var/lib/postgresql/data
# environment:
# - POSTGRES_PASSWORD
# - POSTGRES_USER
# - POSTGRES_DB
# healthcheck:
# test: pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}
# interval: 10s
# timeout: 3s
# retries: 3
# elasticsearch:
# image: elasticsearch:latest
# ports:
# - 9200:9200
# environment:
# - discovery.type=single-node
# - cluster.name=elasticsearch
# - bootstrap.memory_lock=true
# - "ES_JAVA_OPTS=-Xms512m -Xmx512m"
# healthcheck:
# test: curl -s http://elasticsearch:9200 >/dev/null || exit 1
# interval: 10s
# timeout: 3s
# retries: 3

View File

@@ -1,12 +1,13 @@
import os
from sqlmodel import create_engine, Session
from eveal import models_sde
sqlite_sde_file_name = "sde.db"
sde_engine = create_engine(f"sqlite:///{sqlite_sde_file_name}", echo=True, future=True, connect_args={"check_same_thread": False})
sqlite_file_name = os.getenv("SQLITE_DB_PATH", "eveal.db")
engine = create_engine(f"sqlite:///{sqlite_file_name}", echo=True, future=True, connect_args={"check_same_thread": False})
def get_sde_session():
db = Session(sde_engine)
def get_session():
db = Session(engine)
try:
yield db
finally:

32
eveal/esi.py Normal file
View File

@@ -0,0 +1,32 @@
import datetime
import os
import redis
import pickle
from esy.client import ESIClient
from esy.auth import ESIAuthenticator
class ESICache(object):
def __init__(self, redis_url: str, redis_port: int, db: str):
self._r = redis.Redis(host=redis_url, port=redis_port, db=db)
# self._r = redis.StrictRedis(host=redis_url, port=redis_port, db=db)
def get(self, key):
# return pickle.loads(self._r[key])
return pickle.loads(self._r.get(key))
def set(self, key, data, cached_until: datetime.datetime):
self._r.set(key, pickle.dumps(data), ex=cached_until - datetime.datetime.now(datetime.timezone.utc))
def __contains__(self, item):
# return item in self._r
return self._r.exists(item)
esi_client_id = os.getenv('ESI_CLIENT_ID')
esi_secret_key = os.getenv('ESI_SECRET_KEY')
esi_cache = ESICache(redis_url=os.getenv("REDIS_URL"), redis_port=int(os.getenv("REDIS_PORT")), db="0")
esi_client = ESIClient.get_client(user_agent=os.getenv('ESI_USER_AGENT'), cache=esi_cache)
esi_auth = ESIAuthenticator()

View File

@@ -6,10 +6,10 @@ from typing import List, Annotated, Tuple, Literal
from sqlmodel import SQLModel, Session, select
from eveal.schemas import Evepraisal, PriceReprocess
from eveal.database import sde_engine, get_sde_session
from eveal import models_sde
from eveal.database import engine, get_session
from eveal import models_sde, esi
SQLModel.metadata.create_all(sde_engine) # remove? db should be created by import_sde.py
SQLModel.metadata.create_all(engine) # use alembic?
app = FastAPI()
app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"])
@@ -21,13 +21,13 @@ async def root():
@app.post("/reprocess/")
async def reprocess(ep_items: Evepraisal, ep_mat: Evepraisal, efficiency: float = .55, sde: Session = Depends(get_sde_session)) -> List[PriceReprocess]:
async def reprocess(ep_items: Evepraisal, ep_mat: Evepraisal, efficiency: float = .55, db: Session = Depends(get_session)) -> List[PriceReprocess]:
matprices = {item.typeID: {'sell': item.prices.sell.min, 'buy': item.prices.buy.max} for item in ep_mat.items}
item_reprocess: List[PriceReprocess] = []
for rawitem in ep_items.items:
# item = sde.exec(select(models_sde.Type).where(models_sde.Type.id == rawitem.typeID)).one()
item = sde.get(models_sde.Type, rawitem.typeID)
item = db.get(models_sde.SDEType, rawitem.typeID)
buy_reprocess = sell_reprocess = 0.0
for mat in item.materials.all():
buy_reprocess += matprices[mat.type.id]['buy'] * mat.quantity * efficiency
@@ -43,17 +43,29 @@ async def reprocess(ep_items: Evepraisal, ep_mat: Evepraisal, efficiency: float
@app.get("/sde/types/{sde_type}/")
async def sde_types(sde_type: int | str, sde: Session = Depends(get_sde_session)) -> models_sde.Type:
async def sde_types(sde_type: int | str, db: Session = Depends(get_session)) -> models_sde.SDEType:
try:
item = sde.get(models_sde.Type, int(sde_type))
item = db.get(models_sde.SDEType, int(sde_type))
except ValueError:
item = sde.exec(select(models_sde.Type).where(models_sde.Type.name == sde_type)).one()
item = db.exec(select(models_sde.SDEType).where(models_sde.SDEType.name == sde_type)).one()
return item
@app.post("/sde/types/search")
async def sde_types_search(query: List[Tuple[Literal["id", "name"], int | str]], sde: Session = Depends(get_sde_session)) -> List[models_sde.Type]:
async def sde_types_search(query: List[Tuple[Literal["id", "name"], int | str]], db: Session = Depends(get_session)) -> List[models_sde.SDEType]:
items = []
for key, val in query:
items.extend(sde.exec(select(models_sde.Type).where(getattr(models_sde.Type, key) == val)).all())
items.extend(db.exec(select(models_sde.SDEType).where(getattr(models_sde.SDEType, key) == val)).all())
return items
@app.get("/esi/types/{sde_type}/market/{region_id}/")
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))
if __name__ == "__main__":
import uvicorn
uvicorn.run("eveal.main:app", host="0.0.0.0", port=8000, reload=True)

24
eveal/models_esi.py Normal file
View File

@@ -0,0 +1,24 @@
from datetime import datetime
from typing import Optional, List
from sqlmodel import SQLModel, Field, Relationship
class ESIMarketOrder(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
timestamp: datetime = Field(default_factory=datetime.utcnow, nullable=False)
region_id: int
type_id: int # TODO: link to SDE
location_id: int # TODO: link to SDE
volume_total: int
volume_remain: int
min_volume: int
order_id: int # TODO: use this as PK ? (will lose volume_remain history)
price: float
is_buy_order: bool
duration: int
issued: datetime
range: str # TODO: enum?
system_id: int # TODO: link to SDE

View File

@@ -2,107 +2,107 @@ from typing import Optional, List
from sqlmodel import SQLModel, Field, Relationship
class Icon(SQLModel, table=True):
class SDEIcon(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
description: Optional[str] = None
iconFile: str
categories: List['Category'] = Relationship(back_populates="icon")
groups: List['Group'] = Relationship(back_populates="icon")
marketgroups: List['MarketGroup'] = Relationship(back_populates="icon")
types: List['Type'] = Relationship(back_populates="icon")
categories: List['SDECategory'] = Relationship(back_populates="icon")
groups: List['SDEGroup'] = Relationship(back_populates="icon")
marketgroups: List['SDEMarketGroup'] = Relationship(back_populates="icon")
types: List['SDEType'] = Relationship(back_populates="icon")
class Category(SQLModel, table=True):
class SDECategory(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
icon_id: Optional[int] = Field(default=None, foreign_key="icon.id")
icon: Optional[Icon] = Relationship(back_populates="categories")
icon_id: Optional[int] = Field(default=None, foreign_key="sdeicon.id")
icon: Optional[SDEIcon] = Relationship(back_populates="categories")
name: str
published: bool
groups: List['Group'] = Relationship(back_populates="category")
groups: List['SDEGroup'] = Relationship(back_populates="category")
class Group(SQLModel, table=True):
class SDEGroup(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
anchorable: bool
anchored: bool
category_id: Optional[int] = Field(default=None, foreign_key="category.id")
category: Optional[Category] = Relationship(back_populates="groups")
category_id: Optional[int] = Field(default=None, foreign_key="sdecategory.id")
category: Optional[SDECategory] = Relationship(back_populates="groups")
fittableNonSingletion: bool
icon_id: Optional[int] = Field(default=None, foreign_key="icon.id")
icon: Optional[Icon] = Relationship(back_populates="groups")
icon_id: Optional[int] = Field(default=None, foreign_key="sdeicon.id")
icon: Optional[SDEIcon] = Relationship(back_populates="groups")
name: str
published: bool
useBasePrice: bool
types: List['Type'] = Relationship(back_populates="group")
types: List['SDEType'] = Relationship(back_populates="group")
class MarketGroup(SQLModel, table=True):
class SDEMarketGroup(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
description: Optional[str] = None
hasTypes: bool
icon_id: Optional[int] = Field(default=None, foreign_key="icon.id")
icon: Optional[Icon] = Relationship(back_populates="marketgroups")
icon_id: Optional[int] = Field(default=None, foreign_key="sdeicon.id")
icon: Optional[SDEIcon] = Relationship(back_populates="marketgroups")
name: str
parent_marketgroup_id: Optional[int] = Field(default=None, foreign_key="marketgroup.id")
parent_marketgroup: Optional['MarketGroup'] = Relationship(back_populates="children_marketgroups",
sa_relationship_kwargs={"remote_side": 'MarketGroup.id'}) # workaround for self reference: https://github.com/tiangolo/sqlmodel/issues/127#issuecomment-1224135123
children_marketgroups: List['MarketGroup'] = Relationship(back_populates="parent_marketgroup")
parent_marketgroup_id: Optional[int] = Field(default=None, foreign_key="sdemarketgroup.id")
parent_marketgroup: Optional['SDEMarketGroup'] = Relationship(back_populates="children_marketgroups",
sa_relationship_kwargs={"remote_side": 'SDEMarketGroup.id'}) # workaround for self reference: https://github.com/tiangolo/sqlmodel/issues/127#issuecomment-1224135123
children_marketgroups: List['SDEMarketGroup'] = Relationship(back_populates="parent_marketgroup")
types: List['Type'] = Relationship(back_populates="marketgroup")
types: List['SDEType'] = Relationship(back_populates="marketgroup")
class Type(SQLModel, table=True):
class SDEType(SQLModel, table=True):
id: int = Field(primary_key=True)
group_id: Optional[int] = Field(default=None, foreign_key="group.id")
group: Optional[Group] = Relationship(back_populates="types")
group_id: Optional[int] = Field(default=None, foreign_key="sdegroup.id")
group: Optional[SDEGroup] = Relationship(back_populates="types")
marketgroup_id: Optional[int] = Field(default=None, foreign_key="marketgroup.id")
marketgroup: Optional[MarketGroup] = Relationship(back_populates="types")
marketgroup_id: Optional[int] = Field(default=None, foreign_key="sdemarketgroup.id")
marketgroup: Optional[SDEMarketGroup] = Relationship(back_populates="types")
name: str
published: bool = False
description: Optional[str] = None
basePrice: Optional[float] = None
icon_id: Optional[int] = Field(default=None, foreign_key="icon.id")
icon: Optional[Icon] = Relationship(back_populates="types")
icon_id: Optional[int] = Field(default=None, foreign_key="sdeicon.id")
icon: Optional[SDEIcon] = Relationship(back_populates="types")
volume: Optional[float] = None
portionSize: int
materials: List['TypeMaterial'] = Relationship(back_populates="type",
sa_relationship_kwargs={"foreign_keys": '[TypeMaterial.type_id]'}) # https://github.com/tiangolo/sqlmodel/issues/10#issuecomment-1537445078
material_of: List['TypeMaterial'] = Relationship(back_populates="material_type",
sa_relationship_kwargs={"foreign_keys": '[TypeMaterial.material_type_id]'}) # https://github.com/tiangolo/sqlmodel/issues/10#issuecomment-1537445078
materials: List['SDETypeMaterial'] = Relationship(back_populates="type",
sa_relationship_kwargs={"foreign_keys": '[SDETypeMaterial.type_id]'}) # https://github.com/tiangolo/sqlmodel/issues/10#issuecomment-1537445078
material_of: List['SDETypeMaterial'] = Relationship(back_populates="material_type",
sa_relationship_kwargs={"foreign_keys": '[SDETypeMaterial.material_type_id]'}) # https://github.com/tiangolo/sqlmodel/issues/10#issuecomment-1537445078
class TypeMaterial(SQLModel, table=True):
class SDETypeMaterial(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
type_id: Optional[int] = Field(default=None, foreign_key="type.id")
type: Optional[Type] = Relationship(back_populates="materials",
sa_relationship_kwargs={"primaryjoin": 'TypeMaterial.type_id==Type.id',
type_id: Optional[int] = Field(default=None, foreign_key="sdetype.id")
type: Optional[SDEType] = Relationship(back_populates="materials",
sa_relationship_kwargs={"primaryjoin": 'SDETypeMaterial.type_id==SDEType.id',
'lazy': 'joined'}) # workaround: https://github.com/tiangolo/sqlmodel/issues/10#issuecomment-1002835506
material_type_id: Optional[int] = Field(default=None, foreign_key="type.id")
material_type: Optional[Type] = Relationship(back_populates="material_of",
sa_relationship_kwargs={"primaryjoin": 'TypeMaterial.material_type_id==Type.id',
material_type_id: Optional[int] = Field(default=None, foreign_key="sdetype.id")
material_type: Optional[SDEType] = Relationship(back_populates="material_of",
sa_relationship_kwargs={"primaryjoin": 'SDETypeMaterial.material_type_id==SDEType.id',
'lazy': 'joined'}) # workaround: https://github.com/tiangolo/sqlmodel/issues/10#issuecomment-1002835506
quantity: int

View File

@@ -1,14 +1,14 @@
import yaml
from eveal.database import sde_engine, sqlite_sde_file_name
from eveal.database import engine, sqlite_file_name
from sqlmodel import Session, SQLModel
from eveal import models_sde
import os
try:
os.remove(sqlite_sde_file_name)
os.remove(sqlite_file_name)
except:
pass
SQLModel.metadata.create_all(sde_engine)
SQLModel.metadata.create_all(engine)
print("Importing SDE data...")
@@ -16,9 +16,9 @@ print("Importing icons...")
with open("static_eve/sde/fsd/iconIDs.yaml", "r", encoding="utf-8") as f:
icons = yaml.safe_load(f)
with Session(sde_engine) as db:
with Session(engine) as db:
for id, icon in icons.items():
db.add(models_sde.Icon(id=id, **icon))
db.add(models_sde.SDEIcon(id=id, **icon))
db.commit()
@@ -26,11 +26,11 @@ print("Importing categories...")
with open("static_eve/sde/fsd/categoryIDs.yaml", "r", encoding="utf-8") as f:
categories = yaml.safe_load(f)
with Session(sde_engine) as db:
with Session(engine) as db:
for id, category in categories.items():
if category["published"] == False:
continue
db.add(models_sde.Category(id=id,
db.add(models_sde.SDECategory(id=id,
icon_id=category['iconID'] if 'iconID' in category else None,
name=category['name']['en'],
published=category['published']))
@@ -41,11 +41,11 @@ print("Importing groups...")
with open("static_eve/sde/fsd/groupIDs.yaml", "r", encoding="utf-8") as f:
groups = yaml.safe_load(f)
with Session(sde_engine) as db:
with Session(engine) as db:
for id, group in groups.items():
if group["published"] == False:
continue
db.add(models_sde.Group(id=id,
db.add(models_sde.SDEGroup(id=id,
anchorable=group['anchorable'],
anchored=group['anchored'],
category_id=group['categoryID'],
@@ -62,9 +62,9 @@ print("Importing marketgroups...")
with open("static_eve/sde/fsd/marketGroups.yaml", "r", encoding="utf-8") as f:
marketgroups = yaml.safe_load(f)
with Session(sde_engine) as db:
with Session(engine) as db:
for id, marketgroup in marketgroups.items():
db.add(models_sde.MarketGroup(id=id,
db.add(models_sde.SDEMarketGroup(id=id,
description=marketgroup['descriptionID']['en'] if 'descriptionID' in marketgroup else None,
hasTypes=marketgroup['hasTypes'],
icon_id=marketgroup['iconID'] if 'iconID' in marketgroup else None,
@@ -78,11 +78,11 @@ print("Importing types...")
with open("static_eve/sde/fsd/typeIDs.yaml", "r", encoding="utf-8") as f:
types = yaml.safe_load(f)
with Session(sde_engine) as db:
with Session(engine) as db:
for id, type in types.items():
if type["published"] == False:
continue
db.add(models_sde.Type(id=id,
db.add(models_sde.SDEType(id=id,
group_id=type['groupID'],
marketgroup_id=type['marketGroupID'] if 'marketGroupID' in type else None,
name=type['name']['en'],
@@ -100,10 +100,10 @@ print("Importing materials...")
with open("static_eve/sde/fsd/typeMaterials.yaml", "r", encoding="utf-8") as f:
materials = yaml.safe_load(f)
with Session(sde_engine) as db:
with Session(engine) as db:
for id, material in materials.items():
for mat in material['materials']:
db.add(models_sde.TypeMaterial(type_id=id,
db.add(models_sde.SDETypeMaterial(type_id=id,
material_id=mat['materialTypeID'],
quantity=mat['quantity']
))

View File

@@ -2,3 +2,5 @@ fastapi
httpx
uvicorn[standard]
sqlmodel
esy
redis