diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..79c3c361a394cc3056e1c6218d01576c8b6db3c3 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,18 @@ +FROM python:3.9.7-slim + +RUN apt-get update && apt-get install --no-install-recommends -y bash nano mc + +ENV PYTHONUNBUFFERED 1 + +RUN mkdir /src +WORKDIR /src +COPY . /src/ +RUN pip install -r requirements.txt + +CMD ["uvicorn", "main:app", "--proxy-headers", "--host", "0.0.0.0", "--port", "8000"] + +# build the image like this: +# docker build -t adex-fastapi:latest . + +# log into the container +# docker exec -it adex-fastapi sh diff --git a/database/__init__.py b/database/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/database/crud.py b/database/crud.py new file mode 100644 index 0000000000000000000000000000000000000000..ff883d3da6db5a758630a416ed9bebb92dde27ff --- /dev/null +++ b/database/crud.py @@ -0,0 +1,31 @@ +from sqlalchemy.orm import Session + +from .models import Star +from utils import timeit + +@timeit +def get_stars(db: Session, skip: int = 0, limit: int = 1000): + return db.query(Star).offset(skip).limit(limit).all() + +@timeit +def get_rectangle(db: Session, ra_min: float = 0.0, ra_max: float = 1.0, dec_min: float = 0.0, dec_max: float = 1.0, j_mag: int = 10000, limit: int = 1000): + list = db.query(Star).filter( + Star.ra > ra_min, + Star.ra < ra_max, + Star.dec > dec_min, + Star.dec < dec_max, + Star.j_mag < j_mag + ).limit(limit).all() + print("retrieved "+str(len(list)) + ' stars') + return list + +@timeit +def get_rectangle_query(stars, ra_min: float = 0.0, ra_max: float = 1.0, dec_min: float = 0.0, dec_max: float = 1.0, j_mag: int = 10000, limit: int = 1000): + query = stars.select().where( + Star.ra > ra_min, + Star.ra < ra_max, + Star.dec > dec_min, + Star.dec < dec_max, + Star.j_mag < j_mag + ).limit(limit) + return query \ No newline at end of file diff --git a/database/database.py b/database/database.py new file mode 100644 index 0000000000000000000000000000000000000000..14c98a92d9275eadeb5c013b1bf574a6f3a2481c --- /dev/null +++ b/database/database.py @@ -0,0 +1,39 @@ +import os +import sqlalchemy +from sqlalchemy import create_engine +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.orm import sessionmaker + +# Note that we are using databases package as it uses asyncpg +# as an interface to talk to PostgreSQL database +import databases + +# dev machine on SURFSara +# DATABASE_URL = "postgresql://postgres:secret@145.38.187.31/ucac4" + +# mintbox docker +# DATABASE_URL = "postgresql://postgres:secret@postgres-ucac4/ucac4" + +# mintbox +# DATABASE_URL = "postgresql://postgres:secret@192.168.178.37/ucac4" + +# localhost +# DATABASE_URL = "postgresql://postgres:postgres@localhost/ucac4" + +# read from environment +DATABASE_URL = os.environ.get('DATABASE_URL', 'postgresql://postgres:postgres@localhost/ucac4') +print('DATABASE_URL = '+DATABASE_URL) + +my_database = databases.Database(DATABASE_URL) # asyncpg +metadata = sqlalchemy.MetaData() # asyncpg + +engine = create_engine( + DATABASE_URL, pool_size=3, max_overflow=0 +) + +metadata.create_all(engine) # asyncpg + +SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) + +Base = declarative_base() + diff --git a/database/models.py b/database/models.py new file mode 100644 index 0000000000000000000000000000000000000000..a8e02bb67d3c6036475912bfc3eaeff1d2afaaf1 --- /dev/null +++ b/database/models.py @@ -0,0 +1,14 @@ +from sqlalchemy import Column, Integer, String, Float, DateTime + +from .database import Base + +# sql alchemy model +class Star(Base): + __tablename__ = "stars" + zone = Column(Integer, index=True) + mpos1 = Column(Integer, primary_key=True, index=True) + ra = Column(Float, index=True) + dec = Column(Float, index=True) + j_mag = Column(Integer, index=True) + v_mag = Column(Integer, index=True) + diff --git a/database/schemas.py b/database/schemas.py new file mode 100644 index 0000000000000000000000000000000000000000..b272d1627f92e05eb0137a2a4ac88b5835cf1fa5 --- /dev/null +++ b/database/schemas.py @@ -0,0 +1,18 @@ +from datetime import datetime + +from pydantic import BaseModel +from typing import Optional + +# Pydantic models help you define request payload models +# and response models in Python Class object notation + +class Star(BaseModel): + zone: int + mpos1: int + ra: float + dec: float + j_mag : Optional[float] + v_mag : Optional[float] + + class Config: + orm_mode = True \ No newline at end of file diff --git a/main.py b/main.py new file mode 100644 index 0000000000000000000000000000000000000000..dd6548cab01c0e233e7cad5119aff84b6dba9171 --- /dev/null +++ b/main.py @@ -0,0 +1,42 @@ +import uvicorn +from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware +from database.database import my_database + +from routers import stars + +# https://fastapi.tiangolo.com/tutorial/sql-databases/ + +app = FastAPI( + title="ADEX backend", + description="ADEX backend FastAPI", + version="0.0.1", + contact={ + "name": "Nico Vermaas", + "email": "vermaas@astron.nl", + }, + license_info={ + "name": "Apache 2.0", + "url": "https://www.apache.org/licenses/LICENSE-2.0.html", + },) + +app.include_router(stars.router) + +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +@app.on_event("startup") +async def startup(): + await my_database.connect() + +@app.on_event("shutdown") +async def shutdown(): + await my_database.disconnect() + +if __name__ == "__main__": + uvicorn.run(app, host="0.0.0.0", port=8000) \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..0722f0fe9eb465af0ada6c205d143f95c1297dc1 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,17 @@ +anyio==3.6.1 +asyncpg==0.26.0 +click==8.1.3 +colorama==0.4.5 +databases==0.6.1 +fastapi==0.79.0 +greenlet==1.1.2 +gunicorn==20.1.0 +h11==0.13.0 +idna==3.3 +pydantic==1.9.2 +sniffio==1.2.0 +SQLAlchemy==1.4.40 +starlette==0.19.1 +typing_extensions==4.3.0 +uvicorn==0.18.2 +psycopg2-binary==2.9.3 diff --git a/routers/__init__.py b/routers/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/routers/stars.py b/routers/stars.py new file mode 100644 index 0000000000000000000000000000000000000000..a5087d6b1b9697aa8c59e7ca74bf553b9d4aa031 --- /dev/null +++ b/routers/stars.py @@ -0,0 +1,43 @@ +from typing import List + +from fastapi import APIRouter, Query, Depends +from sqlalchemy.orm import Session +from utils import timeit +from database import crud, models, schemas +from database.database import SessionLocal, engine +from database.database import my_database +from database.models import Star + +router = APIRouter(tags=["stars"],) + +# Dependency +def get_db(): + db = SessionLocal() + try: + yield db + finally: + db.close() + + +# http://127.0.0.1:8000/stars/ +# http://127.0.0.1:8000/stars/?skip=100&limit=100 +@router.get("/stars/", tags=["stars"], response_model=List[schemas.Star]) +async def get_stars(skip: int = 0, limit: int = 1000, db: Session = Depends(get_db)): + items = crud.get_stars(db, skip=skip, limit=limit) + return items + +@router.get("/stars_rectangle/", tags=["stars"], response_model=List[schemas.Star]) +async def get_stars_rectangle(ra_min: float = 0.0, ra_max: float = 1.0, + dec_min: float = 0.0, dec_max: float = 1.0, + j_mag: int = 10000, limit: int = 1000, db: Session = Depends(get_db)): + items = crud.get_rectangle(db, ra_min=ra_min, ra_max=ra_max, dec_min=dec_min, dec_max=dec_max, j_mag=j_mag, limit=limit) + return items + +@timeit +@router.get("/stars_rectangle_async/", tags=["stars"], response_model=List[schemas.Star]) +async def get_stars_rectangle_async(ra_min: float = 0.0, ra_max: float = 1.0, + dec_min: float = 0.0, dec_max: float = 1.0, + j_mag: int = 10000, limit: int = 1000): + + query = crud.get_rectangle_query(Star.__table__, ra_min=ra_min, ra_max=ra_max, dec_min=dec_min, dec_max=dec_max, j_mag=j_mag, limit=limit) + return await my_database.fetch_all(query) \ No newline at end of file diff --git a/run.bat b/run.bat new file mode 100644 index 0000000000000000000000000000000000000000..76ea2dfabb42ced29be00701422d3be1adb8f576 --- /dev/null +++ b/run.bat @@ -0,0 +1 @@ +uvicorn main:app --reload \ No newline at end of file diff --git a/utils.py b/utils.py new file mode 100644 index 0000000000000000000000000000000000000000..7aa0aa258040959cff4e562ff906a53877d11ff0 --- /dev/null +++ b/utils.py @@ -0,0 +1,23 @@ +""" +common helper functions +""" +import logging; +from datetime import * +import time + +logger = logging.getLogger(__name__) + +# this is a decorator that can be put in front (around) a function all to measure its execution time +def timeit(method): + def timed(*args, **kw): + ts = time.time() + result = method(*args, **kw) + te = time.time() + if 'log_time' in kw: + name = kw.get('log_name', method.__name__.upper()) + kw['log_time'][name] = int((te - ts) * 1000) + else: + print('execution time: %r %2.2f ms' % \ + (method.__name__, (te - ts) * 1000)) + return result + return timed \ No newline at end of file