Skip to content

Commit

Permalink
set up of API #2
Browse files Browse the repository at this point in the history
  • Loading branch information
hcwinsemius committed Jan 14, 2025
1 parent a2b9fa1 commit 7aa7cb3
Show file tree
Hide file tree
Showing 13 changed files with 310 additions and 0 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
.idea
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
Expand Down
3 changes: 3 additions & 0 deletions nodeorc_api/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
"""Backend API for operations and configuration for NodeORC front end."""

__version__ = "0.1.0"
2 changes: 2 additions & 0 deletions nodeorc_api/crud/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
from . import device
from . import disk_management
11 changes: 11 additions & 0 deletions nodeorc_api/crud/device.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from sqlalchemy.orm import Session
from nodeorc import db as models

def get(db: Session):
# there should always only be one device. Hence retrieve the first.
return db.query(models.Device).first()

def update(db: Session, device: models.Device):
db.query(models.Device).update(device.dict())
db.commit()
return db.query(models.Device).first()
8 changes: 8 additions & 0 deletions nodeorc_api/crud/disk_management.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
from sqlalchemy.orm import Session
from nodeorc import db as models

def get(db: Session):
# there should always only be one disk management config. Hence retrieve the first.
dms = db.query(models.DiskManagement)
if dms.count() > 0:
return db.query(models.DiskManagement).first()
8 changes: 8 additions & 0 deletions nodeorc_api/database.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
from nodeorc.db import Session
# Dependency to get the DB session
def get_db():
db = Session()
try:
yield db
finally:
db.close()
28 changes: 28 additions & 0 deletions nodeorc_api/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
from fastapi import FastAPI, Depends, Request
from fastapi.middleware.cors import CORSMiddleware

from nodeorc_api.routers import device, video, disk_management
app = FastAPI()

# origins = ["http://localhost:5173"]
origins = ["*"]

app.add_middleware(
CORSMiddleware,
allow_origins=origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)



app.include_router(device.router)
app.include_router(video.router)
app.include_router(disk_management.router)

@app.get("/")
async def root():
return {"message": "Hello World"}


44 changes: 44 additions & 0 deletions nodeorc_api/routers/device.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
from fastapi import APIRouter, Depends
from nodeorc.db import Session, Device, DeviceStatus, DeviceFormStatus
from typing import List, Dict

from nodeorc_api.schemas.device import DeviceCreate, DeviceResponse
from nodeorc_api.database import get_db
from nodeorc_api import crud

router: APIRouter = APIRouter(prefix="/device", tags=["device"])

@router.get("/", response_model=DeviceResponse, description="Get device information")
async def get_device(db: Session = Depends(get_db)):
device: List[Device] = crud.device.get(db)
return device


@router.get("/statuses/", response_model=List[Dict], description="Get all available status options for devices")
async def get_device_statuses():
return [{"key": status.name, "value": status.value} for status in DeviceStatus]


@router.get("/form_statuses/", response_model=List[Dict], description="Get all available form status options")
async def get_device_form_statuses():
return [{"key": status.name, "value": status.value} for status in DeviceFormStatus]


@router.post("/", response_model=DeviceResponse, status_code=201, description="Update device information")
async def update_device(device: DeviceCreate, db: Session = Depends(get_db)):
# Check if there is already a device
existing_device = crud.device.get(db)
if existing_device:
# Update the existing record's fields
for key, value in device.model_dump(exclude_none=True).items():
setattr(existing_device, key, value)
db.commit()
db.refresh(existing_device) # Refresh to get the updated fields
return existing_device
else:
# Create a new device record if none exists
new_device = Device(**device.model_dump(exclude_none=True, exclude={"id"}))
db.add(new_device)
db.commit()
db.refresh(new_device)
return new_device
37 changes: 37 additions & 0 deletions nodeorc_api/routers/disk_management.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
from fastapi import APIRouter, Depends, Response
from nodeorc.db import Session, DiskManagement
from typing import List, Union

from nodeorc_api.schemas.disk_management import DiskManagementResponse, DiskManagementCreate
from nodeorc_api.database import get_db
from nodeorc_api import crud

router: APIRouter = APIRouter(prefix="/disk_management", tags=["disk_management"])

@router.get("/", response_model=Union[DiskManagementResponse, None], description="Get disk management configuration.")
async def get_disk_management_settings(db: Session = Depends(get_db)):
disk_management: List[DiskManagement] = crud.disk_management.get(db)
return disk_management


@router.post("/", response_model=None, status_code=201, description="Update disk management configuration.")
async def update_device(dm: DiskManagementCreate, db: Session = Depends(get_db)):
# Check if there is already a device
existing_dm = crud.disk_management.get(db)
try:
if existing_dm:
# Update the existing record's fields
for key, value in dm.model_dump(exclude_none=True).items():
setattr(existing_dm, key, value)
db.commit()
db.refresh(existing_dm) # Refresh to get the updated fields
return existing_dm
else:
# Create a new device record if none exists
new_dm = DiskManagement(**dm.model_dump(exclude_none=True, exclude={"id"}))
db.add(new_dm)
db.commit()
db.refresh(new_dm)
return new_dm
except Exception as e:
return Response(f"Error: {e}", status_code=500)
67 changes: 67 additions & 0 deletions nodeorc_api/routers/video.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
from fastapi import Response, Request, APIRouter
from fastapi.responses import StreamingResponse

import cv2

router: APIRouter = APIRouter(prefix="/video", tags=["video"])


@router.get("/feed/", response_class=StreamingResponse, description="Get video stream from user-defined URL")
async def video_feed(request: Request, video_url: str):
async def generate_frames():
if not video_url:
raise ValueError("No video URL provided")
cap = cv2.VideoCapture(video_url)
print(f"VIDEO URL IS: {video_url}")
if not cap.isOpened():
# return Response("Unable to open RTSP stream", status_code=500)
raise RuntimeError("Unable to open RTSP stream")

try:
from collections import deque
frame_buffer = deque(maxlen=10) # Adjust maxlen based on desired buffer size
while True:
if await request.is_disconnected():
break
success, frame = cap.read()
if not success:
break
frame_buffer.append(frame)
# Encode the frame as JPEG
_, buffer = cv2.imencode(".jpg", frame_buffer.pop())

# Yield the frame as part of an MJPEG stream
yield (
b"--frame\r\n"
b"Content-Type: image/jpeg\r\n\r\n" + buffer.tobytes() + b"\r\n"
)
# Add a small delay to avoid overloading the server, probably not required.
# await asyncio.sleep(0.03)
finally:
print("Releasing the video capture object")
cap.release()
del cap

return StreamingResponse(generate_frames(), media_type="multipart/x-mixed-replace; boundary=frame")

@router.head("/feed/", response_class=Response, description="Check if the video feed in the user defined URL is available")
async def check_video_feed(response: Response, video_url: str):
"""
HEAD endpoint to check if the video feed is available.
"""
print(f"VIDEO URL IS: {video_url}")
if not video_url:
raise ValueError("No video URL provided")

try:
# RTSP_URL = "rtsp://nodeorcpi:8554/cam"
cap = cv2.VideoCapture(video_url)

if not cap.isOpened():
return Response("Unable to open RTSP stream", status_code=500)
else:
return Response("Video feed is available", status_code=200)
finally:
cap.release()
del cap

37 changes: 37 additions & 0 deletions nodeorc_api/schemas/device.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import uuid

from pydantic import BaseModel, Field
from typing import Optional
from nodeorc.db import DeviceStatus, DeviceFormStatus

# Pydantic model for responses
class DeviceBase(BaseModel):
name: Optional[str] = Field(default=None, description="Name of the device.")
operating_system: Optional[str] = Field(default=None, description="Operating system of the device.")
processor: Optional[str] = Field(default=None, description="Processor of the device.")
memory: Optional[float] = Field(default=None, description="Memory in GB available on the device.")
status: Optional[DeviceStatus] = Field(default=DeviceStatus.HEALTHY, description="Status of the device.")
form_status: Optional[DeviceFormStatus] = Field(default=DeviceFormStatus.NOFORM, description="Form status of the device.")
nodeorc_version: Optional[str] = Field(default=None, description="Version of nodeorc.")
message: Optional[str] = Field(default=None, description="Error or status message if any.")

class DeviceResponse(DeviceBase):
id: uuid.UUID# = Field(description="Device ID")

class Config:
orm_mode = True

class DeviceCreate(DeviceBase):
pass


class DeviceUpdate(BaseModel):
name: Optional[str]
operating_system: Optional[str]
processor: Optional[str]
memory: Optional[float]
status: Optional[DeviceStatus]
form_status: Optional[DeviceFormStatus]
nodeorc_version: Optional[str]
message: Optional[str]

18 changes: 18 additions & 0 deletions nodeorc_api/schemas/disk_management.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
from datetime import datetime
from pydantic import BaseModel, Field
from typing import Optional


# Pydantic model for responses
class DiskManagementBase(BaseModel):
home_folder: Optional[str] = Field(default=None, description="Home folder of the device.")
min_free_space: Optional[float] = Field(default=None, description="GB of minimum free space required.")
critical_space: Optional[float] = Field(default=None, description="GB of free space critical for the device.")
frequency: Optional[int] = Field(default=None, description="Frequency [s] for checking disk status and cleanup.")

class DiskManagementResponse(DiskManagementBase):
id: int = Field(description="Disk management ID")
created_at: datetime = Field(description="Creation date")

class DiskManagementCreate(DiskManagementBase):
pass
46 changes: 46 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
[build-system]
requires = ["flit_core >=3.4.0,<4"]
build-backend = "flit_core.buildapi"

[project]
name = "nodeorc_api"
authors = [
{name = "Hessel Winsemius", email = "winsemius@rainbowsensing.com"},
]
dependencies = [
# "nodeorc",
"fastapi[standard]"
]
requires-python = ">=3.9" # fix tests to support older versions
readme = "README.md"
classifiers = [
"Development Status :: 3 - Alpha",
"Intended Audience :: Developers",
"Intended Audience :: Science/Research",
"Topic :: Scientific/Engineering :: Hydrology",
"Topic :: Scientific/Engineering :: Image Processing",
"License :: OSI Approved :: GNU Affero General Public License v3",
"License :: OSI Approved :: Image Processing",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11"
]
dynamic = ['version', 'description']

[project.optional-dependencies]
test = [
"pytest",
"pytest-cov",
]

[project.urls]
Source = "https://github.com/localdevices//nodeorc-interface"


[tool.flit.sdist]
include = ["nodeorc_api"]
exclude = ["gcloud_dist", "hack", "helm", "htmlcov", "junit", "tests", ".github"]

[tool.pytest.ini_options]
testpaths = ["tests"]

0 comments on commit 7aa7cb3

Please sign in to comment.