This commit is contained in:
1
server-python/.python-version
Normal file
1
server-python/.python-version
Normal file
@@ -0,0 +1 @@
|
||||
3.12.2
|
||||
140
server-python/app.py
Normal file
140
server-python/app.py
Normal file
@@ -0,0 +1,140 @@
|
||||
import json
|
||||
import os
|
||||
from contextlib import asynccontextmanager
|
||||
from datetime import datetime, timezone
|
||||
from typing import Literal
|
||||
|
||||
import motor.motor_asyncio
|
||||
from bson import json_util
|
||||
from bson.objectid import ObjectId
|
||||
from dotenv import find_dotenv, load_dotenv
|
||||
from fastapi import FastAPI, Request
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.responses import FileResponse
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from prettytable import PrettyTable
|
||||
from pydantic import BaseModel
|
||||
|
||||
env_file = find_dotenv(".env")
|
||||
load_dotenv("../.env")
|
||||
|
||||
IS_PROD = os.getenv("PRODUCTION", "") == "true"
|
||||
|
||||
|
||||
class Config:
|
||||
MONGO_SERVER: str = os.environ.get("MONGO_SERVER", "")
|
||||
MONGO_DB: str = os.environ.get("MONGO_DB", "")
|
||||
|
||||
|
||||
class Document(BaseModel):
|
||||
date: int
|
||||
value: float
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
table = PrettyTable()
|
||||
table.field_names = ["VARIABLE", "VALUE"]
|
||||
table.add_rows(
|
||||
[
|
||||
[env_variable, os.environ.get(env_variable)]
|
||||
for env_variable in ["MONGO_SERVER", "MONGO_DB"]
|
||||
]
|
||||
)
|
||||
print(table)
|
||||
yield
|
||||
|
||||
|
||||
app = FastAPI(openapi_url=("/openapi.json" if not IS_PROD else ""), lifespan=lifespan)
|
||||
|
||||
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"],
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
client = motor.motor_asyncio.AsyncIOMotorClient(Config.MONGO_SERVER, 27017)
|
||||
db = client[Config.MONGO_DB]
|
||||
|
||||
|
||||
def object_id_from_date(date: int) -> ObjectId:
|
||||
timestamp = datetime.fromtimestamp(date, tz=timezone.utc)
|
||||
object_id = ObjectId.from_datetime(timestamp)
|
||||
return object_id
|
||||
|
||||
|
||||
@app.get(
|
||||
"/type/{type}/startDate/{start_date}/endDate/{end_date}/sample/{sample}",
|
||||
tags=["Retrieve sensor data"],
|
||||
)
|
||||
async def get_sensor_data(
|
||||
type: Literal["temperature", "humidity"],
|
||||
start_date: int,
|
||||
end_date: int,
|
||||
sample: int,
|
||||
) -> list[Document]:
|
||||
collection = db["dht22"]
|
||||
pipeline = [
|
||||
{
|
||||
"$match": {
|
||||
"$and": [
|
||||
{
|
||||
"_id": {
|
||||
"$gt": object_id_from_date(start_date),
|
||||
"$lt": object_id_from_date(end_date),
|
||||
}
|
||||
},
|
||||
{"type": type},
|
||||
{"value": {"$ne": "NaN"}},
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"$group": {
|
||||
"_id": {
|
||||
"$toDate": {
|
||||
"$subtract": [
|
||||
{"$toLong": {"$toDate": "$_id"}},
|
||||
{
|
||||
"$mod": [
|
||||
{"$toLong": {"$toDate": "$_id"}},
|
||||
1000 * 60 * sample,
|
||||
]
|
||||
},
|
||||
]
|
||||
}
|
||||
},
|
||||
"value": {"$avg": "$value"},
|
||||
"date": {"$min": {"$toDate": "$_id"}},
|
||||
}
|
||||
},
|
||||
{
|
||||
"$project": {
|
||||
"_id": 0,
|
||||
"value": {"$round": ["$value", 1]},
|
||||
"date": {"$toLong": "$date"},
|
||||
}
|
||||
},
|
||||
{"$sort": {"date": 1}},
|
||||
]
|
||||
|
||||
docs = [doc async for doc in collection.aggregate(pipeline)]
|
||||
return json.loads(json_util.dumps(docs))
|
||||
|
||||
|
||||
index = FileResponse(path="../dist/index.html", headers={"Cache-Control": "no-cache"})
|
||||
|
||||
|
||||
@app.middleware("http")
|
||||
async def add_default_404(request: Request, call_next):
|
||||
response = await call_next(request)
|
||||
if response.status_code == 404:
|
||||
return index
|
||||
else:
|
||||
return response
|
||||
|
||||
|
||||
app.mount("/", StaticFiles(directory="../dist", html=True), name="dist")
|
||||
16
server-python/generate_schema.py
Normal file
16
server-python/generate_schema.py
Normal file
@@ -0,0 +1,16 @@
|
||||
import json
|
||||
|
||||
from app import app
|
||||
from fastapi.openapi.utils import get_openapi
|
||||
|
||||
with open("openapi.json", "w") as f:
|
||||
json.dump(
|
||||
get_openapi(
|
||||
title=app.title,
|
||||
version=app.version,
|
||||
openapi_version=app.openapi_version,
|
||||
description=app.description,
|
||||
routes=app.routes,
|
||||
),
|
||||
f,
|
||||
)
|
||||
81
server-python/openapi.json
Normal file
81
server-python/openapi.json
Normal file
@@ -0,0 +1,81 @@
|
||||
{
|
||||
"openapi": "3.1.0",
|
||||
"info": { "title": "FastAPI", "version": "0.1.0" },
|
||||
"paths": {
|
||||
"/type/{type}/startDate/{start_date}/endDate/{end_date}/sample/{sample}": {
|
||||
"get": {
|
||||
"tags": ["Retrieve sensor data"],
|
||||
"summary": "Get Sensor Data",
|
||||
"operationId": "get_sensor_data_type__type__startDate__start_date__endDate__end_date__sample__sample__get",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "type",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": { "enum": ["temperature", "humidity"], "type": "string", "title": "Type" }
|
||||
},
|
||||
{
|
||||
"name": "start_date",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": { "type": "integer", "title": "Start Date" }
|
||||
},
|
||||
{ "name": "end_date", "in": "path", "required": true, "schema": { "type": "integer", "title": "End Date" } },
|
||||
{ "name": "sample", "in": "path", "required": true, "schema": { "type": "integer", "title": "Sample" } }
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": { "$ref": "#/components/schemas/Document" },
|
||||
"title": "Response Get Sensor Data Type Type Startdate Start Date Enddate End Date Sample Sample Get"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation Error",
|
||||
"content": { "application/json": { "schema": { "$ref": "#/components/schemas/HTTPValidationError" } } }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
"schemas": {
|
||||
"Document": {
|
||||
"properties": {
|
||||
"date": { "type": "integer", "title": "Date" },
|
||||
"value": { "type": "number", "title": "Value" }
|
||||
},
|
||||
"type": "object",
|
||||
"required": ["date", "value"],
|
||||
"title": "Document"
|
||||
},
|
||||
"HTTPValidationError": {
|
||||
"properties": {
|
||||
"detail": { "items": { "$ref": "#/components/schemas/ValidationError" }, "type": "array", "title": "Detail" }
|
||||
},
|
||||
"type": "object",
|
||||
"title": "HTTPValidationError"
|
||||
},
|
||||
"ValidationError": {
|
||||
"properties": {
|
||||
"loc": {
|
||||
"items": { "anyOf": [{ "type": "string" }, { "type": "integer" }] },
|
||||
"type": "array",
|
||||
"title": "Location"
|
||||
},
|
||||
"msg": { "type": "string", "title": "Message" },
|
||||
"type": { "type": "string", "title": "Error Type" }
|
||||
},
|
||||
"type": "object",
|
||||
"required": ["loc", "msg", "type"],
|
||||
"title": "ValidationError"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
1209
server-python/poetry.lock
generated
Normal file
1209
server-python/poetry.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
22
server-python/pyproject.toml
Normal file
22
server-python/pyproject.toml
Normal file
@@ -0,0 +1,22 @@
|
||||
[tool.poetry]
|
||||
name = "sensor-web-api"
|
||||
version = "0.1.0"
|
||||
description = ""
|
||||
authors = ["Your Name <you@example.com>"]
|
||||
readme = "README.md"
|
||||
package-mode = false
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.12"
|
||||
motor = "^3.4.0"
|
||||
fastapi = "^0.111.0"
|
||||
python-dotenv = "^1.0.1"
|
||||
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
ruff = "^0.4.4"
|
||||
prettytable = "^3.10.0"
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core"]
|
||||
build-backend = "poetry.core.masonry.api"
|
||||
Reference in New Issue
Block a user