Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
59 commits
Select commit Hold shift + click to select a range
a31b0de
Quick hacks to serialize data with real image urls
mihow Mar 29, 2023
4f7ddfd
Use dataclass for Deployment
mihow Mar 30, 2023
5343471
Incomplete start of an API implementation
mihow Apr 6, 2023
65b125a
Working API example
mihow Apr 10, 2023
3df9e3a
Quick hacks to serialize data with real image urls
mihow Mar 29, 2023
b1215b1
Use dataclass for Deployment
mihow Mar 30, 2023
fe9d6e2
Incomplete start of an API implementation
mihow Apr 6, 2023
2b7a372
Working API example
mihow Apr 10, 2023
9a21ffd
Merge branch 'api-test' of github.com:RolnickLab/ami-data-manager int…
mihow Apr 11, 2023
bb9675c
Add additional endpoints and supporting changes
mihow Apr 11, 2023
826236c
Fix summary endpoint
mihow Apr 11, 2023
e517a85
Persist docker db
mihow Apr 11, 2023
9069ae9
Debian service for running API server
mihow Apr 11, 2023
31e1e66
Allow monitoring session to be optional
mihow Apr 12, 2023
67a20fd
Standardize list_monitoring_sessions
mihow Apr 12, 2023
bc66cf2
Update method for counting everything
mihow Apr 12, 2023
1c15768
Try serving image crops
mihow Apr 12, 2023
98b3964
Fix show deployments table
mihow Apr 12, 2023
2a9aad0
Merge branch 'api-test' of github.com:RolnickLab/ami-data-manager int…
Apr 12, 2023
65766fb
Update media URL for source images
mihow Apr 12, 2023
758b2ce
Add event data to occurrence list
mihow Apr 13, 2023
53a3b6f
Add capture examples to event list items
mihow Apr 13, 2023
b621b29
Optional URL on list items
mihow Apr 13, 2023
cafb3d6
Hide image base path
mihow Apr 13, 2023
b667f45
Fix group by in PostgreSQL
mihow Apr 13, 2023
5a5342c
Add initial session detail endpoint, no captures
mihow Apr 14, 2023
7fad50b
Fix group by in PostgreSQL
mihow Apr 13, 2023
fb3995b
Merge branch 'api-test' of github.com:RolnickLab/ami-data-manager int…
Apr 15, 2023
b217e69
Add species endpoint
mihow Apr 15, 2023
817db35
Merge branch 'api-test' of github.com:RolnickLab/ami-data-manager int…
mihow Apr 15, 2023
202f5c1
Support fetching ocur. for all events in a dep.
mihow Apr 15, 2023
9d58d49
Renable type hints for settings class
mihow Apr 15, 2023
24593c3
Merge branch 'main' of github.com:RolnickLab/ami-data-manager into ap…
mihow Apr 15, 2023
a249356
Fix list occurrences
mihow Apr 15, 2023
855beb3
Filter species by current deployment
mihow Apr 15, 2023
8de725e
Fix join
mihow Apr 15, 2023
4816025
Add limits & offset from API
mihow Apr 15, 2023
f3d09c0
Only show stats for current deployment
mihow Apr 15, 2023
d73d411
Formatting
mihow Apr 15, 2023
f21efe0
Show total deployments
mihow Apr 15, 2023
1b6f184
Fix num species in summary
mihow Apr 15, 2023
7ecef36
Fix species detection count
mihow Apr 15, 2023
11b61a3
Fix num events count
mihow Apr 15, 2023
58d9ec5
Add height width props to detection
mihow Apr 16, 2023
1739468
Fix occurrence detail path
mihow Apr 17, 2023
ac2618f
Update aggregates before showing deployments
mihow Apr 17, 2023
605ce96
Fix species list
mihow Apr 17, 2023
09ec6ad
Don't create schema for sqlite
mihow Apr 17, 2023
b979be5
Update limits and examples
mihow Apr 17, 2023
fe44049
Fix list of examples
mihow Apr 17, 2023
7a0af66
Fix species examples, but slow
mihow Apr 17, 2023
08e5a3e
Todo
mihow Apr 17, 2023
4ae1550
Increase number of examples
mihow Apr 17, 2023
4061924
Show species occurrences, not detections
mihow Apr 17, 2023
4c69ce5
Fix psql query
mihow Apr 17, 2023
a0718e3
Hacky way to get unique species occurrences
mihow Apr 18, 2023
9378601
Add gunicorn conf for API server
mihow Jul 11, 2023
ad0768b
Merge branch 'main' of https://github.com/RolnickLab/ami-data-manager…
mihow Aug 10, 2023
e14fe1a
Updates for exporting occurrences (#54)
mihow Aug 10, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .flake8
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
[flake8]
max-line-length = 160
ignore = E203, E402, W503
ignore = E203, E402, W503, B008
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ repos:
- id: autoflake
args:
- --in-place
- --imports=sqlalchemy,pydantic
- --imports=trapdata,sqlalchemy,pydantic,fastapi
files: .
types: [file, python]

Expand Down
109 changes: 107 additions & 2 deletions poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ ipython = "^8.11.0"
pytest-cov = "^4.0.0"
pytest-asyncio = "^0.21.0"
pytest = "*"
fastapi = "^0.95.0"
uvicorn = "^0.21.1"


[tool.pytest.ini_options]
Expand Down
21 changes: 21 additions & 0 deletions trapdata/api/ami.service
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
[Unit]

Description=AMI Data Manager API

After=network.target


[Service]

User=debian

Group=www-data

WorkingDirectory=/home/debian/ami-data-manager

ExecStart=/home/debian/miniconda3/bin/gunicorn trapdata.api.main:app --bind 0.0.0.0:8000 --worker-class "uvicorn.workers.UvicornWorker" --log-syslog


[Install]

WantedBy=multi-user.target
39 changes: 39 additions & 0 deletions trapdata/api/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import pathlib
from typing import Any, Dict, List, Optional

from pydantic import BaseSettings, HttpUrl, PostgresDsn, validator
from pydantic.networks import AnyHttpUrl

from trapdata.cli import read_settings
from trapdata.settings import Settings as BaseSettings


class Settings(BaseSettings):
PROJECT_NAME: str = "AMI Data Manager"

SENTRY_DSN: Optional[HttpUrl] = None

API_PATH: str = "/api/v1"

ACCESS_TOKEN_EXPIRE_MINUTES: int = 7 * 24 * 60 # 7 days

BACKEND_CORS_ORIGINS: List[AnyHttpUrl] = []

# The following variables need to be defined in environment

TEST_DATABASE_URL: Optional[PostgresDsn]

SECRET_KEY: str
# END: required environment variables

# STATIC_ROOT: str = "static"

# @validator("STATIC_ROOT")
# def validate_static_root(cls, v):
# path = cls.user_data_path / v
# path.mkdir(parents=True, exist_ok=True)
# return path


# settings = read_settings(SettingsClass=Settings, SECRET_KEY="secret")
settings = Settings(SECRET_KEY="secret")
15 changes: 15 additions & 0 deletions trapdata/api/deps/db.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from typing import Generator

from sqlalchemy import orm

from trapdata.cli import read_settings
from trapdata.db.base import get_session_class

settings = read_settings()


def get_session() -> Generator[orm.Session, None, None]:
Session = get_session_class(db_path=settings.database_url)
with Session() as session:
yield session
session.close()
46 changes: 46 additions & 0 deletions trapdata/api/deps/request_params.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import json
from typing import Callable, Optional, Type

from fastapi import HTTPException, Query
from sqlalchemy import UnaryExpression, asc, desc

from trapdata.api.request_params import RequestParams
from trapdata.db import Base


def parse_react_admin_params(model: Type[Base]) -> Callable:
"""Parses sort and range parameters coming from a react-admin request"""

def inner(
sort_: Optional[str] = Query(
None,
alias="sort",
description='Format: `["field_name", "direction"]`',
example='["id", "ASC"]',
),
range_: Optional[str] = Query(
None,
alias="range",
description="Format: `[start, end]`",
example="[0, 10]",
),
) -> RequestParams:
skip, limit = 0, 10
if range_:
start, end = json.loads(range_)
skip, limit = start, (end - start + 1)

order_by: UnaryExpression = desc(model.id)
if sort_:
sort_column, sort_order = json.loads(sort_)
if sort_order.lower() == "asc":
direction = asc
elif sort_order.lower() == "desc":
direction = desc
else:
raise HTTPException(400, f"Invalid sort direction {sort_order}")
order_by = direction(model.__table__.c[sort_column])

return RequestParams(skip=skip, limit=limit, order_by=order_by)

return inner
87 changes: 87 additions & 0 deletions trapdata/api/factory.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
from fastapi import FastAPI
from fastapi.routing import APIRoute
from fastapi.staticfiles import StaticFiles
from starlette.middleware.cors import CORSMiddleware
from starlette.requests import Request
from starlette.responses import FileResponse, RedirectResponse

from trapdata.api.config import settings
from trapdata.api.views import api_router


def create_app():
description = f"{settings.PROJECT_NAME} API"
app = FastAPI(
title=settings.PROJECT_NAME,
openapi_url=f"{settings.API_PATH}/openapi.json",
docs_url="/docs/",
description=description,
redoc_url="/redoc/",
)
setup_routers(app)
setup_cors_middleware(app)
serve_static_app(app)
return app


def setup_routers(app: FastAPI) -> None:
app.include_router(api_router, prefix=settings.API_PATH)
# The following operation needs to be at the end of this function
use_route_names_as_operation_ids(app)


def serve_static_app(app):
app.mount(
"/static/crops",
StaticFiles(directory=settings.user_data_path / "crops"),
name="crops",
)
app.mount(
"/static/captures",
StaticFiles(directory=settings.image_base_path),
name="captures",
)
app.mount(
"/",
StaticFiles(directory="trapdata/webui/public"),
name="static",
)

@app.middleware("http")
async def _add_404_middleware(request: Request, call_next):
"""Serves static assets on 404"""
response = await call_next(request)
path = request["path"]
if path.startswith(settings.API_PATH) or path.startswith("/docs"):
return response
if response.status_code == 404:
return FileResponse("trapdata/webui/public/index.html")
return response


def setup_cors_middleware(app):
if settings.BACKEND_CORS_ORIGINS:
app.add_middleware(
CORSMiddleware,
allow_origins=[str(origin) for origin in settings.BACKEND_CORS_ORIGINS],
allow_credentials=True,
allow_methods=["*"],
expose_headers=["Content-Range", "Range"],
allow_headers=["Authorization", "Range", "Content-Range"],
)


def use_route_names_as_operation_ids(app: FastAPI) -> None:
"""
Simplify operation IDs so that generated API clients have simpler function
names.

Should be called only after all routes have been added.
"""
route_names = set()
for route in app.routes:
if isinstance(route, APIRoute):
if route.name in route_names:
raise Exception("Route function names should be unique")
route.operation_id = route.name
route_names.add(route.name)
Loading