move into /app dir and add new db handler + wrappers
This commit is contained in:
4
.gitignore
vendored
4
.gitignore
vendored
@@ -1 +1,3 @@
|
|||||||
*.venv
|
*.venv
|
||||||
|
*__pycache__/
|
||||||
|
*.bat
|
||||||
@@ -11,7 +11,7 @@ COPY requirements.txt .
|
|||||||
RUN pip install --no-cache-dir -r requirements.txt
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
# Copy the server code into the container
|
# Copy the server code into the container
|
||||||
COPY server.py .
|
COPY ./app .
|
||||||
|
|
||||||
# Make port 8765 available to the world outside this container
|
# Make port 8765 available to the world outside this container
|
||||||
EXPOSE 8765
|
EXPOSE 8765
|
||||||
|
|||||||
2
Makefile
2
Makefile
@@ -13,5 +13,7 @@ build:
|
|||||||
run: build
|
run: build
|
||||||
docker run -it --rm \
|
docker run -it --rm \
|
||||||
--name $(SERVER_CONTAINER_NAME) \
|
--name $(SERVER_CONTAINER_NAME) \
|
||||||
|
-e DB_NAME=$(DB_NAME) \
|
||||||
|
-e MONGO_URL=$(MONGO_URL) \
|
||||||
--network host \
|
--network host \
|
||||||
$(SERVER_IMAGE)
|
$(SERVER_IMAGE)
|
||||||
|
|||||||
251
app/internal/db_handler.py
Normal file
251
app/internal/db_handler.py
Normal file
@@ -0,0 +1,251 @@
|
|||||||
|
import motor.motor_asyncio
|
||||||
|
import asyncio
|
||||||
|
from typing import Optional, Dict, Any, List
|
||||||
|
|
||||||
|
class MongoHandler:
|
||||||
|
"""
|
||||||
|
A basic asynchronous handler for MongoDB operations using motor.
|
||||||
|
Designed to be used with asyncio.
|
||||||
|
"""
|
||||||
|
def __init__(self, db_name: str, collection_name: str, mongo_uri: str = "mongodb://localhost:27017/"):
|
||||||
|
"""
|
||||||
|
Initializes the MongoDB handler.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
db_name (str): The name of the database to connect to.
|
||||||
|
collection_name (str): The name of the collection to use.
|
||||||
|
mongo_uri (str): The MongoDB connection string URI.
|
||||||
|
Defaults to the standard local URI.
|
||||||
|
"""
|
||||||
|
self.mongo_uri = mongo_uri
|
||||||
|
self.db_name = db_name
|
||||||
|
self.collection_name = collection_name
|
||||||
|
self._client: Optional[motor.motor_asyncio.AsyncIOMotorClient] = None
|
||||||
|
self._db: Optional[motor.motor_asyncio.AsyncIOMotorDatabase] = None
|
||||||
|
self._collection: Optional[motor.motor_asyncio.AsyncIOMotorCollection] = None
|
||||||
|
|
||||||
|
async def connect(self):
|
||||||
|
"""Establishes an asynchronous connection to MongoDB."""
|
||||||
|
if self._client is None:
|
||||||
|
try:
|
||||||
|
self._client = motor.motor_asyncio.AsyncIOMotorClient(self.mongo_uri)
|
||||||
|
# The ismaster command is cheap and does not require auth.
|
||||||
|
# It is used to confirm that the client can connect to the deployment.
|
||||||
|
await self._client.admin.command('ismaster')
|
||||||
|
self._db = self._client[self.db_name]
|
||||||
|
self._collection = self._db[self.collection_name]
|
||||||
|
print(f"Connected to MongoDB: Database '{self.db_name}', Collection '{self.collection_name}'")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Failed to connect to MongoDB at {self.mongo_uri}: {e}")
|
||||||
|
self._client = None # Ensure client is None if connection fails
|
||||||
|
raise # Re-raise the exception after printing
|
||||||
|
|
||||||
|
async def close(self):
|
||||||
|
"""Closes the MongoDB connection."""
|
||||||
|
if self._client:
|
||||||
|
self._client.close()
|
||||||
|
self._client = None
|
||||||
|
self._db = None
|
||||||
|
self._collection = None
|
||||||
|
print("MongoDB connection closed.")
|
||||||
|
|
||||||
|
async def __aenter__(self):
|
||||||
|
"""Allows using the handler with async with."""
|
||||||
|
await self.connect()
|
||||||
|
return self
|
||||||
|
|
||||||
|
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
||||||
|
"""Ensures the connection is closed when exiting async with."""
|
||||||
|
await self.close()
|
||||||
|
|
||||||
|
async def insert_one(self, document: Dict[str, Any]) -> Any:
|
||||||
|
"""
|
||||||
|
Inserts a single document into the collection.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
document (Dict[str, Any]): The document to insert.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Any: The result of the insert operation (InsertOneResult).
|
||||||
|
"""
|
||||||
|
if self._collection is None:
|
||||||
|
raise RuntimeError("MongoDB connection not established. Call connect() first or use async with.")
|
||||||
|
print(f"Inserting document into '{self.collection_name}'...")
|
||||||
|
result = await self._collection.insert_one(document)
|
||||||
|
print(f"Inserted document with ID: {result.inserted_id}")
|
||||||
|
return result
|
||||||
|
|
||||||
|
async def find_one(self, query: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
||||||
|
"""
|
||||||
|
Finds a single document matching the query.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
query (Dict[str, Any]): The query document.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Optional[Dict[str, Any]]: The found document, or None if not found.
|
||||||
|
"""
|
||||||
|
if self._collection is None:
|
||||||
|
raise RuntimeError("MongoDB connection not established. Call connect() first or use async with.")
|
||||||
|
print(f"Finding one document in '{self.collection_name}' with query: {query}")
|
||||||
|
document = await self._collection.find_one(query)
|
||||||
|
return document
|
||||||
|
|
||||||
|
async def find(self, query: Dict[str, Any] = None) -> List[Dict[str, Any]]:
|
||||||
|
"""
|
||||||
|
Finds multiple documents matching the query.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
query (Dict[str, Any], optional): The query document. Defaults to None (find all).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List[Dict[str, Any]]: A list of matching documents.
|
||||||
|
"""
|
||||||
|
if self._collection is None:
|
||||||
|
raise RuntimeError("MongoDB connection not established. Call connect() first or use async with.")
|
||||||
|
if query is None:
|
||||||
|
query = {}
|
||||||
|
print(f"Finding documents in '{self.collection_name}' with query: {query}")
|
||||||
|
# Use list comprehension to iterate through the cursor asynchronously
|
||||||
|
documents = [doc async for doc in self._collection.find(query)]
|
||||||
|
print(f"Found {len(documents)} documents.")
|
||||||
|
return documents
|
||||||
|
|
||||||
|
async def update_one(self, query: Dict[str, Any], update: Dict[str, Any], upsert: bool = False) -> Any:
|
||||||
|
"""
|
||||||
|
Updates a single document matching the query.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
query (Dict[str, Any]): The query document.
|
||||||
|
update (Dict[str, Any]): The update operations to apply.
|
||||||
|
upsert (bool): If True, insert a new document if no match is found.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Any: The result of the update operation (UpdateResult).
|
||||||
|
"""
|
||||||
|
if self._collection is None:
|
||||||
|
raise RuntimeError("MongoDB connection not established. Call connect() first or use async with.")
|
||||||
|
print(f"Updating one document in '{self.collection_name}' with query: {query}, update: {update}")
|
||||||
|
result = await self._collection.update_one(query, update, upsert=upsert)
|
||||||
|
print(f"Matched {result.matched_count}, Modified {result.modified_count}, Upserted ID: {result.upserted_id}")
|
||||||
|
return result
|
||||||
|
|
||||||
|
async def delete_one(self, query: Dict[str, Any]) -> Any:
|
||||||
|
"""
|
||||||
|
Deletes a single document matching the query.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
query (Dict[str, Any]): The query document.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Any: The result of the delete operation (DeleteResult).
|
||||||
|
"""
|
||||||
|
if self._collection is None:
|
||||||
|
raise RuntimeError("MongoDB connection not established. Call connect() first or use async with.")
|
||||||
|
print(f"Deleting one document from '{self.collection_name}' with query: {query}")
|
||||||
|
result = await self._collection.delete_one(query)
|
||||||
|
print(f"Deleted count: {result.deleted_count}")
|
||||||
|
return result
|
||||||
|
|
||||||
|
async def delete_many(self, query: Dict[str, Any]) -> Any:
|
||||||
|
"""
|
||||||
|
Deletes multiple documents matching the query.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
query (Dict[str, Any]): The query document.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Any: The result of the delete operation (DeleteResult).
|
||||||
|
"""
|
||||||
|
if self._collection is None:
|
||||||
|
raise RuntimeError("MongoDB connection not established. Call connect() first or use async with.")
|
||||||
|
print(f"Deleting many documents from '{self.collection_name}' with query: {query}")
|
||||||
|
result = await self._collection.delete_many(query)
|
||||||
|
print(f"Deleted count: {result.deleted_count}")
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
# --- Example Usage ---
|
||||||
|
async def example_mongo_usage():
|
||||||
|
"""Demonstrates how to use the MongoHandler."""
|
||||||
|
# Ensure you have a MongoDB server running, default is localhost:27017
|
||||||
|
db_name = "radio_app_db"
|
||||||
|
collection_name = "channels"
|
||||||
|
|
||||||
|
# Using async with ensures the connection is closed automatically
|
||||||
|
async with MongoHandler(db_name, collection_name) as mongo:
|
||||||
|
# --- Insert Example ---
|
||||||
|
print("\n--- Inserting a document ---")
|
||||||
|
channel_data = {
|
||||||
|
"_id": "channel_3", # You can specify _id or let MongoDB generate one
|
||||||
|
"name": "Emergency Services",
|
||||||
|
"frequency_khz": 453000,
|
||||||
|
"location": "Countywide",
|
||||||
|
"avail_on_nodes": ["client-xyz987"],
|
||||||
|
"description": "Monitor for emergency broadcasts."
|
||||||
|
}
|
||||||
|
try:
|
||||||
|
insert_result = await mongo.insert_one(channel_data)
|
||||||
|
print(f"Insert successful: {insert_result.inserted_id}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Insert failed: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
# --- Find One Example ---
|
||||||
|
print("\n--- Finding one document ---")
|
||||||
|
query = {"_id": "channel_3"}
|
||||||
|
found_channel = await mongo.find_one(query)
|
||||||
|
if found_channel:
|
||||||
|
print("Found document:", found_channel)
|
||||||
|
else:
|
||||||
|
print("Document not found.")
|
||||||
|
|
||||||
|
# --- Find Many Example ---
|
||||||
|
print("\n--- Finding all documents ---")
|
||||||
|
all_channels = await mongo.find() # Empty query finds all
|
||||||
|
print("All documents:", all_channels)
|
||||||
|
|
||||||
|
# --- Update Example ---
|
||||||
|
print("\n--- Updating a document ---")
|
||||||
|
update_query = {"_id": "channel_3"}
|
||||||
|
update_data = {"$set": {"location": "Statewide", "avail_on_nodes": ["client-xyz987", "client-newnode1"]}}
|
||||||
|
update_result = await mongo.update_one(update_query, update_data)
|
||||||
|
print(f"Update successful: Matched {update_result.matched_count}, Modified {update_result.modified_count}")
|
||||||
|
|
||||||
|
print("\n--- Finding the updated document ---")
|
||||||
|
updated_channel = await mongo.find_one(update_query)
|
||||||
|
print("Updated document:", updated_channel)
|
||||||
|
|
||||||
|
# --- Delete Example ---
|
||||||
|
print("\n--- Deleting a document ---")
|
||||||
|
delete_query = {"_id": "channel_3"}
|
||||||
|
delete_result = await mongo.delete_one(delete_query)
|
||||||
|
print(f"Delete successful: Deleted count {delete_result.deleted_count}")
|
||||||
|
|
||||||
|
print("\n--- Verifying deletion ---")
|
||||||
|
deleted_channel = await mongo.find_one(delete_query)
|
||||||
|
if deleted_channel:
|
||||||
|
print("Document still found (deletion failed).")
|
||||||
|
else:
|
||||||
|
print("Document successfully deleted.")
|
||||||
|
|
||||||
|
# --- Insert another for delete_many example ---
|
||||||
|
temp_doc1 = {"_id": "temp_1", "tag": "temp"}
|
||||||
|
temp_doc2 = {"_id": "temp_2", "tag": "temp"}
|
||||||
|
await mongo.insert_one(temp_doc1)
|
||||||
|
await mongo.insert_one(temp_doc2)
|
||||||
|
|
||||||
|
# --- Delete Many Example ---
|
||||||
|
print("\n--- Deleting many documents ---")
|
||||||
|
delete_many_query = {"tag": "temp"}
|
||||||
|
delete_many_result = await mongo.delete_many(delete_many_query)
|
||||||
|
print(f"Delete many successful: Deleted count {delete_many_result.deleted_count}")
|
||||||
|
|
||||||
|
|
||||||
|
# To run the example usage:
|
||||||
|
# 1. Ensure you have a MongoDB server running locally on the default port (27017).
|
||||||
|
# 2. Save the code as mongodb_handler.py.
|
||||||
|
# 3. Run from your terminal: python -m asyncio mongodb_handler.py
|
||||||
|
if __name__ == "__main__":
|
||||||
|
# Running the example directly requires running within an asyncio loop
|
||||||
|
asyncio.run(example_mongo_usage())
|
||||||
300
app/internal/db_wrappers.py
Normal file
300
app/internal/db_wrappers.py
Normal file
@@ -0,0 +1,300 @@
|
|||||||
|
import os
|
||||||
|
import typing
|
||||||
|
import asyncio
|
||||||
|
from uuid import uuid4
|
||||||
|
from typing import Optional, List, Dict, Any
|
||||||
|
from enum import Enum
|
||||||
|
from internal.db_handler import MongoHandler
|
||||||
|
|
||||||
|
# Init vars
|
||||||
|
DB_NAME = os.getenv("DB_NAME", "default_db")
|
||||||
|
MONGO_URL = os.getenv("MONGO_URL", "mongodb://10.10.202.4:27017/")
|
||||||
|
|
||||||
|
SYSTEM_DB_COLLECTION_NAME = "radio_systems"
|
||||||
|
|
||||||
|
# --- Types
|
||||||
|
class DemodTypes(str, Enum):
|
||||||
|
P25 = "P25"
|
||||||
|
DMR = "DMR"
|
||||||
|
ANALOG = "NBFM"
|
||||||
|
|
||||||
|
class TalkgroupTag:
|
||||||
|
"""Represents a talkgroup tag."""
|
||||||
|
def __init__(self, talkgroup: str, tagDec: int):
|
||||||
|
self.talkgroup = talkgroup
|
||||||
|
self.tagDec = tagDec
|
||||||
|
|
||||||
|
# Add a method to convert to a dictionary, useful for sending as JSON
|
||||||
|
def to_dict(self) -> Dict[str, Any]:
|
||||||
|
return {"talkgroup": self.talkgroup, "tagDec": self.tagDec}
|
||||||
|
|
||||||
|
class System:
|
||||||
|
"""
|
||||||
|
A basic data model for a channel/system entry in a radio system.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self,
|
||||||
|
_id: str,
|
||||||
|
_type: DemodTypes,
|
||||||
|
name: str,
|
||||||
|
frequency_khz: List[int],
|
||||||
|
location: str,
|
||||||
|
avail_on_nodes: List[str],
|
||||||
|
description: Optional[str] = "",
|
||||||
|
tags: Optional[List[TalkgroupTag]] = None,
|
||||||
|
whitelist: Optional[List[int]] = None):
|
||||||
|
"""
|
||||||
|
Initializes a System object.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
_id: A unique identifier for the entry (e.g., MongoDB ObjectId string).
|
||||||
|
_type: The demodulation type (P25, NBFM, etc.).
|
||||||
|
name: The name of the channel/system.
|
||||||
|
frequency_khz: The frequency in kilohertz.
|
||||||
|
location: The geographical location or coverage area.
|
||||||
|
avail_on_nodes: A list of node identifiers where this is available.
|
||||||
|
description: A brief description.
|
||||||
|
"""
|
||||||
|
self._id: str = _id
|
||||||
|
self.type: DemodTypes = _type
|
||||||
|
self.name: str = name
|
||||||
|
self.frequency_khz: List[int] = frequency_khz
|
||||||
|
self.location: str = location
|
||||||
|
self.avail_on_nodes: List[str] = avail_on_nodes
|
||||||
|
self.description: str = description or ""
|
||||||
|
self.tags: List[TalkgroupTag] = tags or None
|
||||||
|
self.whitelist: List[int] = whitelist or None
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
"""
|
||||||
|
Provides a developer-friendly string representation of the object.
|
||||||
|
"""
|
||||||
|
# Use self.type.value for string representation of the enum
|
||||||
|
return (f"System(_id='{self._id}', type='{self.type.value}', name='{self.name}', "
|
||||||
|
f"frequency_khz={self.frequency_khz}, location='{self.location}', "
|
||||||
|
f"avail_on_nodes={self.avail_on_nodes}, description='{self.description}',"
|
||||||
|
f" tags='{self.tags}', whitelist='{self.whitelist}')")
|
||||||
|
|
||||||
|
def to_dict(self) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Converts the System object to a dictionary suitable for MongoDB.
|
||||||
|
Converts the DemodTypes enum to its string value.
|
||||||
|
"""
|
||||||
|
return {
|
||||||
|
"_id": self._id,
|
||||||
|
"type": self.type.value, # Store the enum value (string)
|
||||||
|
"name": self.name,
|
||||||
|
"frequency_khz": self.frequency_khz,
|
||||||
|
"location": self.location,
|
||||||
|
"avail_on_nodes": self.avail_on_nodes,
|
||||||
|
"description": self.description,
|
||||||
|
"tags": self.tags,
|
||||||
|
"whitelist": self.whitelist,
|
||||||
|
}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_dict(cls, data: Dict[str, Any]) -> "System":
|
||||||
|
"""
|
||||||
|
Creates a System object from a dictionary (e.g., from MongoDB).
|
||||||
|
Converts the 'type' string back to a DemodTypes enum member.
|
||||||
|
"""
|
||||||
|
# Ensure required keys exist and handle potential type mismatches if necessary
|
||||||
|
# Convert the type string back to the DemodTypes enum
|
||||||
|
system_type = DemodTypes(data.get("type")) if data.get("type") else None # Handle missing or invalid type
|
||||||
|
|
||||||
|
if system_type is None:
|
||||||
|
# Handle error: could raise an exception or return None/default
|
||||||
|
# For this example, let's raise an error if type is missing/invalid
|
||||||
|
raise ValueError(f"Invalid or missing 'type' in document data: {data}")
|
||||||
|
|
||||||
|
|
||||||
|
return cls(
|
||||||
|
_id=data.get("_id"),
|
||||||
|
_type=system_type,
|
||||||
|
name=data.get("name", ""), # Provide default empty string if name is missing
|
||||||
|
frequency_khz=data.get("frequency_khz", 0), # Provide default 0 if missing
|
||||||
|
location=data.get("location", ""),
|
||||||
|
avail_on_nodes=data.get("avail_on_nodes", []), # Provide default empty list
|
||||||
|
description=data.get("description", ""),
|
||||||
|
tags=data.get("tags", None),
|
||||||
|
whitelist=data.get("whitelist", None)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# --- System class ---
|
||||||
|
class SystemDbController():
|
||||||
|
def __init__(self):
|
||||||
|
# Init the handler
|
||||||
|
self.db_h = MongoHandler(DB_NAME, SYSTEM_DB_COLLECTION_NAME, MONGO_URL)
|
||||||
|
|
||||||
|
async def create_system(self, system_data: Dict[str, Any]) -> Optional[System]:
|
||||||
|
"""
|
||||||
|
Creates a new system entry in the database.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
system_data: A dictionary containing the data for the new system.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The created System object if successful, None otherwise.
|
||||||
|
"""
|
||||||
|
print("\n--- Creating a document ---")
|
||||||
|
try:
|
||||||
|
# Check if the data to be inserted has an ID
|
||||||
|
if not system_data.get("_id"):
|
||||||
|
system_data['_id'] = uuid4()
|
||||||
|
|
||||||
|
inserted_result = None
|
||||||
|
inserted_id = None
|
||||||
|
async with self.db_h as db:
|
||||||
|
insert_result = await self.db_h.insert_one(system_data)
|
||||||
|
inserted_id = insert_result.inserted_id
|
||||||
|
|
||||||
|
if inserted_id:
|
||||||
|
print(f"Insert successful with ID: {inserted_id}")
|
||||||
|
# Fetch the inserted document to get the complete data including the generated _id
|
||||||
|
query = {"_id": inserted_id}
|
||||||
|
|
||||||
|
inserted_doc = None
|
||||||
|
async with self.db_h as db:
|
||||||
|
inserted_doc = await db.find_one(query)
|
||||||
|
|
||||||
|
if inserted_doc:
|
||||||
|
# Convert the fetched dictionary back to a System object
|
||||||
|
return System.from_dict(inserted_doc)
|
||||||
|
else:
|
||||||
|
print("Insert acknowledged but no ID returned.")
|
||||||
|
return None
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Create failed: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def find_system(self, query: Dict[str, Any]) -> Optional[System]:
|
||||||
|
"""
|
||||||
|
Finds a single system entry in the database.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
query: A dictionary representing the query criteria.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A System object if found, None otherwise.
|
||||||
|
"""
|
||||||
|
print("\n--- Finding one document ---")
|
||||||
|
try:
|
||||||
|
found_doc = None
|
||||||
|
async with self.db_h as db:
|
||||||
|
found_doc = await db.find_one(query)
|
||||||
|
|
||||||
|
if found_doc:
|
||||||
|
print("Found document (raw dict):", found_doc)
|
||||||
|
# Convert the dictionary result to a System object
|
||||||
|
return System.from_dict(found_doc)
|
||||||
|
else:
|
||||||
|
print("Document not found.")
|
||||||
|
return None
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Find failed: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def find_systems(self, query: Dict[str, Any]) -> Optional[List[System]]:
|
||||||
|
"""
|
||||||
|
Finds one or more system entries in the database.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
query: A dictionary representing the query criteria.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A list of System object(s) if found, None otherwise.
|
||||||
|
"""
|
||||||
|
print("\n--- Finding documents ---")
|
||||||
|
try:
|
||||||
|
found_docs = None
|
||||||
|
async with self.db_h as db:
|
||||||
|
found_docs = await db.find(query)
|
||||||
|
|
||||||
|
if found_docs:
|
||||||
|
print("Found document (raw dict):", found_docs)
|
||||||
|
# Convert the dictionary results to a System object
|
||||||
|
converted_systems = []
|
||||||
|
for doc in found_docs:
|
||||||
|
converted_systems.append(System.from_dict(doc))
|
||||||
|
|
||||||
|
print("YURB", found_docs, converted_systems)
|
||||||
|
|
||||||
|
return converted_systems if len(converted_systems) > 0 else None
|
||||||
|
else:
|
||||||
|
print("Document not found.")
|
||||||
|
return None
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Find failed: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def find_all_systems(self, query: Dict[str, Any] = {}) -> List[System]:
|
||||||
|
"""
|
||||||
|
Finds multiple system entries in the database.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
query: A dictionary representing the query criteria (default is empty to find all).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A list of System objects.
|
||||||
|
"""
|
||||||
|
print("\n--- Finding multiple documents ---")
|
||||||
|
try:
|
||||||
|
found_docs = None
|
||||||
|
async with self.db_h as db:
|
||||||
|
found_docs = await db.find(query)
|
||||||
|
|
||||||
|
if found_docs:
|
||||||
|
print(f"Found {len(found_docs)} documents (raw dicts).")
|
||||||
|
# Convert the list of dictionaries to a list of System objects
|
||||||
|
return [System.from_dict(doc) for doc in found_docs]
|
||||||
|
else:
|
||||||
|
print("No documents found.")
|
||||||
|
return []
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Find all failed: {e}")
|
||||||
|
return []
|
||||||
|
|
||||||
|
async def update_system(self, query: Dict[str, Any], update_data: Dict[str, Any]) -> Optional[int]:
|
||||||
|
"""
|
||||||
|
Updates a single system entry in the database.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
query: A dictionary representing the query criteria to find the document.
|
||||||
|
update_data: A dictionary representing the update operations (e.g., using $set).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The number of modified documents if successful, None otherwise.
|
||||||
|
"""
|
||||||
|
print("\n--- Updating a document ---")
|
||||||
|
try:
|
||||||
|
update_result = None
|
||||||
|
async with self.db_h as db:
|
||||||
|
update_result = await db.update_one(query, update_data)
|
||||||
|
|
||||||
|
print(f"Update result: Matched {update_result.matched_count}, Modified {update_result.modified_count}")
|
||||||
|
return update_result.modified_count
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Update failed: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def delete_system(self, query: Dict[str, Any]) -> Optional[int]:
|
||||||
|
"""
|
||||||
|
Deletes a single system entry from the database.
|
||||||
|
Args:
|
||||||
|
query: A dictionary representing the query criteria to find the document to delete.
|
||||||
|
Returns:
|
||||||
|
The number of deleted documents if successful, None otherwise.
|
||||||
|
"""
|
||||||
|
print("\n--- Deleting a document ---")
|
||||||
|
try:
|
||||||
|
delete_result = None
|
||||||
|
async with self.db_h as db:
|
||||||
|
delete_result = await self.db_h.delete_one(query)
|
||||||
|
|
||||||
|
print(f"Delete result: Deleted count {delete_result.deleted_count}")
|
||||||
|
return delete_result.deleted_count
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Delete failed: {e}")
|
||||||
|
return None
|
||||||
259
app/routers/systems.py
Normal file
259
app/routers/systems.py
Normal file
@@ -0,0 +1,259 @@
|
|||||||
|
from quart import Blueprint, jsonify, request, abort
|
||||||
|
from internal.db_wrappers import System, SystemDbController
|
||||||
|
from werkzeug.exceptions import HTTPException
|
||||||
|
|
||||||
|
systems_bp = Blueprint('systems', __name__)
|
||||||
|
db_h = SystemDbController()
|
||||||
|
|
||||||
|
|
||||||
|
@systems_bp.route("/", methods=['POST'])
|
||||||
|
async def create_system_route():
|
||||||
|
"""API endpoint to create a new system."""
|
||||||
|
print("\n--- Handling POST /systems ---")
|
||||||
|
try:
|
||||||
|
# In Quart, you need to explicitly get the JSON request body
|
||||||
|
request_data = await request.get_json()
|
||||||
|
|
||||||
|
if not request_data:
|
||||||
|
abort(400, "Request body must be JSON") # Bad Request
|
||||||
|
|
||||||
|
if '_id' in request_data:
|
||||||
|
id_search_result = await db_h.find_system({"_id": request_data["_id"]})
|
||||||
|
if id_search_result:
|
||||||
|
# If _id is provided and exists, return conflict
|
||||||
|
abort(409, f"System with ID '{request_data['_id']}' already exists")
|
||||||
|
|
||||||
|
# Check if name exists (optional, depending on requirements)
|
||||||
|
if 'name' in request_data:
|
||||||
|
name_search_result = await db_h.find_system({"name": request_data["name"]})
|
||||||
|
if name_search_result:
|
||||||
|
abort(409, f"System with name '{request_data['name']}' already exists")
|
||||||
|
|
||||||
|
# Check if frequency_khz exists (optional, depending on requirements)
|
||||||
|
if 'frequency_khz' in request_data:
|
||||||
|
freq_search_result = await db_h.find_system({"frequency_khz": request_data["frequency_khz"]})
|
||||||
|
if freq_search_result:
|
||||||
|
abort(409, f"System with frequency '{request_data['frequency_khz']}' already exists")
|
||||||
|
|
||||||
|
created_system = await db_h.create_system(request_data)
|
||||||
|
|
||||||
|
if created_system:
|
||||||
|
print("Created new system:", created_system)
|
||||||
|
return jsonify(created_system), 201
|
||||||
|
else:
|
||||||
|
abort(500, "Failed to create system in the database.")
|
||||||
|
except HTTPException:
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error creating system: {e}")
|
||||||
|
# Catch any other unexpected errors
|
||||||
|
abort(500, f"Internal server error: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
@systems_bp.route('/', methods=['GET'])
|
||||||
|
async def list_systems_route():
|
||||||
|
"""API endpoint to get a list of all systems."""
|
||||||
|
print("\n--- Handling GET /systems ---")
|
||||||
|
try:
|
||||||
|
all_systems = await db_h.find_all_systems()
|
||||||
|
|
||||||
|
return jsonify([system.to_dict() for system in all_systems]), 200 # 200 OK status code
|
||||||
|
except HTTPException:
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error listing systems: {e}")
|
||||||
|
abort(500, f"Internal server error: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
@systems_bp.route('/<string:system_id>', methods=['GET'])
|
||||||
|
async def get_system_route(system_id: str):
|
||||||
|
"""API endpoint to get details for a specific system by ID."""
|
||||||
|
print(f"\n--- Handling GET /systems/{system_id} ---")
|
||||||
|
try:
|
||||||
|
# Fix the query dictionary syntax
|
||||||
|
system = await db_h.find_system({'_id': system_id})
|
||||||
|
|
||||||
|
if system:
|
||||||
|
# system is a System object, jsonify will convert it
|
||||||
|
return jsonify(system.to_dict()), 200 # 200 OK
|
||||||
|
else:
|
||||||
|
# If system is None, it means the document was not found
|
||||||
|
abort(404, f"System with ID '{system_id}' not found") # 404 Not Found
|
||||||
|
except HTTPException:
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error getting system details for ID {system_id}: {e}")
|
||||||
|
abort(500, f"Internal server error: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
@systems_bp.route('/client/<string:client_id>', methods=['GET'])
|
||||||
|
async def get_system_by_client_route(client_id: str):
|
||||||
|
"""API endpoint to get details for a specific system by ID."""
|
||||||
|
print(f"\n--- Handling GET /systems/client/{client_id} ---")
|
||||||
|
try:
|
||||||
|
# Fix the query dictionary syntax
|
||||||
|
systems = await db_h.find_systems({'avail_on_nodes': client_id})
|
||||||
|
|
||||||
|
if systems:
|
||||||
|
# system is a System object, jsonify will convert it
|
||||||
|
return jsonify([system.to_dict() for system in systems]), 200 # 200 OK
|
||||||
|
else:
|
||||||
|
# If system is None, it means the document was not found
|
||||||
|
abort(404, f"Client with ID '{client_id}' not found") # 404 Not Found
|
||||||
|
except HTTPException:
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error getting system details for client ID {client_id}: {e}")
|
||||||
|
abort(500, f"Internal server error: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
@systems_bp.route('/<string:system_id>', methods=['PUT'])
|
||||||
|
async def update_system_route(system_id: str, updated_system_data):
|
||||||
|
try:
|
||||||
|
update_system = await db_h.update_system({"_id", system_id}, updated_system_data)
|
||||||
|
|
||||||
|
if update_system:
|
||||||
|
print("Updated system:", update_system)
|
||||||
|
return jsonify(update_system), 201
|
||||||
|
else:
|
||||||
|
abort(500, "Failed to update system in the database.")
|
||||||
|
|
||||||
|
except HTTPException:
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error updating system: {e}")
|
||||||
|
# Catch any other unexpected errors
|
||||||
|
abort(500, f"Internal server error: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
@systems_bp.route('/<string:system_id>', methods=['DELETE'])
|
||||||
|
async def delete_system_route(system_id: str):
|
||||||
|
try:
|
||||||
|
deleted_system = await db_h.delete_system({"_id", system_id})
|
||||||
|
|
||||||
|
if deleted_system:
|
||||||
|
print("Deleted system:", deleted_system)
|
||||||
|
return jsonify(deleted_system), 201
|
||||||
|
else:
|
||||||
|
abort(500, "Failed to delete system in the database.")
|
||||||
|
|
||||||
|
except HTTPException:
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error deleting system: {e}")
|
||||||
|
# Catch any other unexpected errors
|
||||||
|
abort(500, f"Internal server error: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
@systems_bp.route('/<string:system_id>/assign', methods=['POST'])
|
||||||
|
async def assign_client_to_system_route(system_id: str):
|
||||||
|
"""
|
||||||
|
API endpoint to assign a client ID to a system's available_on_nodes list.
|
||||||
|
Uses MongoDB $addToSet to add the client ID if not already present.
|
||||||
|
Expects JSON body: {"client_id": "..."}
|
||||||
|
"""
|
||||||
|
print(f"\n--- Handling POST /systems/{system_id}/assign ---")
|
||||||
|
try:
|
||||||
|
request_data = await request.get_json()
|
||||||
|
|
||||||
|
if not request_data or 'client_id' not in request_data:
|
||||||
|
abort(400, "Request body must contain 'client_id'")
|
||||||
|
|
||||||
|
client_id = request_data['client_id']
|
||||||
|
|
||||||
|
if not isinstance(client_id, str) or not client_id:
|
||||||
|
abort(400, "'client_id' must be a non-empty string")
|
||||||
|
|
||||||
|
# First, check if the system exists
|
||||||
|
existing_system = await db_h.find_system({"_id": system_id})
|
||||||
|
if existing_system is None:
|
||||||
|
abort(404, f"System with ID '{system_id}' not found")
|
||||||
|
|
||||||
|
# Use $addToSet to add the client_id to the avail_on_nodes array
|
||||||
|
# $addToSet only adds the element if it's not already in the array
|
||||||
|
update_query = {"_id": system_id}
|
||||||
|
update_data = {"$addToSet": {"avail_on_nodes": client_id}}
|
||||||
|
|
||||||
|
update_result = await db_h.update_system(update_query, update_data)
|
||||||
|
|
||||||
|
if update_result > 0:
|
||||||
|
print(f"Client '{client_id}' assigned to system '{system_id}'.")
|
||||||
|
status = "client_assigned"
|
||||||
|
else:
|
||||||
|
print(f"Client '{client_id}' was already assigned to system '{system_id}'.")
|
||||||
|
status = "already_assigned"
|
||||||
|
|
||||||
|
updated_system = await db_h.find_system({"_id": system_id})
|
||||||
|
if updated_system:
|
||||||
|
return jsonify({
|
||||||
|
"status": status,
|
||||||
|
"system": updated_system.to_dict() # Return dict representation
|
||||||
|
}), 200 # 200 OK
|
||||||
|
else:
|
||||||
|
# Should not happen if update_result.matched_count was 1, but handle defensively
|
||||||
|
print(f"Update matched but couldn't fetch updated system {system_id}.")
|
||||||
|
abort(500, "Failed to fetch system state after assignment attempt.")
|
||||||
|
|
||||||
|
except HTTPException:
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error during system assignment: {e}")
|
||||||
|
abort(500, f"Internal server error: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
@systems_bp.route('/<string:system_id>/dismiss', methods=['POST'])
|
||||||
|
async def dismiss_client_from_system_route(system_id: str):
|
||||||
|
"""
|
||||||
|
API endpoint to dismiss (remove) a client ID from a system's available_on_nodes list.
|
||||||
|
Uses MongoDB $pull to remove the client ID if present.
|
||||||
|
Expects JSON body: {"client_id": "..."}
|
||||||
|
"""
|
||||||
|
print(f"\n--- Handling POST /systems/{system_id}/deassign ---")
|
||||||
|
try:
|
||||||
|
request_data = await request.get_json()
|
||||||
|
|
||||||
|
if not request_data or 'client_id' not in request_data:
|
||||||
|
abort(400, "Request body must contain 'client_id'")
|
||||||
|
|
||||||
|
client_id = request_data['client_id']
|
||||||
|
|
||||||
|
if not isinstance(client_id, str) or not client_id:
|
||||||
|
abort(400, "'client_id' must be a non-empty string")
|
||||||
|
|
||||||
|
# First, check if the system exists
|
||||||
|
existing_system = await db_h.find_system({"_id": system_id})
|
||||||
|
if existing_system is None:
|
||||||
|
abort(404, f"System with ID '{system_id}' not found")
|
||||||
|
|
||||||
|
# Use $pull to remove the client_id from the avail_on_nodes array
|
||||||
|
# $pull removes all occurrences of the value
|
||||||
|
update_query = {"_id": system_id}
|
||||||
|
update_data = {"$pull": {"avail_on_nodes": client_id}}
|
||||||
|
|
||||||
|
update_result = await db_h.update_system(update_query, update_data)
|
||||||
|
|
||||||
|
if update_result > 0:
|
||||||
|
print(f"Client '{client_id}' dismissed from system '{system_id}'.")
|
||||||
|
status = "client_deassigned"
|
||||||
|
else:
|
||||||
|
print(f"Client '{client_id}' was not found in avail_on_nodes for system '{system_id}'.")
|
||||||
|
status = "not_assigned"
|
||||||
|
# Note: update_result.matched_count will be 1 even if modified_count is 0
|
||||||
|
|
||||||
|
# Optionally fetch the updated document to return its current state
|
||||||
|
updated_system = await db_h.find_system({"_id": system_id})
|
||||||
|
if updated_system:
|
||||||
|
return jsonify({
|
||||||
|
"status": status,
|
||||||
|
"system": updated_system.to_dict() # Return dict representation
|
||||||
|
}), 200 # 200 OK
|
||||||
|
else:
|
||||||
|
# Should not happen if update_result.matched_count was 1, but handle defensively
|
||||||
|
print(f"Update matched but couldn't fetch updated system {system_id}.")
|
||||||
|
abort(500, "Failed to fetch system state after de-assignment attempt.")
|
||||||
|
|
||||||
|
except HTTPException:
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error during system de-assignment: {e}")
|
||||||
|
abort(500, f"Internal server error: {e}")
|
||||||
@@ -2,7 +2,8 @@ import asyncio
|
|||||||
import websockets
|
import websockets
|
||||||
import json
|
import json
|
||||||
import uuid
|
import uuid
|
||||||
from quart import Quart, jsonify, request # Import necessary Quart components
|
from quart import Quart, jsonify, request
|
||||||
|
from routers.systems import systems_bp
|
||||||
|
|
||||||
# --- WebSocket Server Components ---
|
# --- WebSocket Server Components ---
|
||||||
# Dictionary to store active clients: {client_id: websocket}
|
# Dictionary to store active clients: {client_id: websocket}
|
||||||
@@ -46,7 +47,6 @@ async def send_command_to_all_clients(command_name, *args):
|
|||||||
print(f"Failed to send to client {client_id}: connection closed.")
|
print(f"Failed to send to client {client_id}: connection closed.")
|
||||||
await unregister_client(client_id)
|
await unregister_client(client_id)
|
||||||
|
|
||||||
|
|
||||||
async def websocket_server_handler(websocket):
|
async def websocket_server_handler(websocket):
|
||||||
"""Handles incoming WebSocket connections and messages from clients."""
|
"""Handles incoming WebSocket connections and messages from clients."""
|
||||||
client_id = None
|
client_id = None
|
||||||
@@ -85,10 +85,10 @@ channels = {
|
|||||||
"channel_1": {
|
"channel_1": {
|
||||||
"id": "channel_1",
|
"id": "channel_1",
|
||||||
"name": "Local News Radio",
|
"name": "Local News Radio",
|
||||||
"frequency_list_khz": [98500],
|
"frequency_list_khz": ["98500"],
|
||||||
"decode_mode": "p25",
|
"decode_mode": "P25",
|
||||||
"location": "Cityville",
|
"location": "Cityville",
|
||||||
"tags": None,
|
"tags": [],
|
||||||
"tag_whitelist": [1,2,3],
|
"tag_whitelist": [1,2,3],
|
||||||
"avail_on_nodes": ["client-abc123", "client-def456"], # List of client IDs that can tune this channel
|
"avail_on_nodes": ["client-abc123", "client-def456"], # List of client IDs that can tune this channel
|
||||||
"description": "Your source for local news and weather."
|
"description": "Your source for local news and weather."
|
||||||
@@ -97,9 +97,9 @@ channels = {
|
|||||||
"id": "channel_2",
|
"id": "channel_2",
|
||||||
"name": "Music Mix FM",
|
"name": "Music Mix FM",
|
||||||
"frequency_list_khz": [101300],
|
"frequency_list_khz": [101300],
|
||||||
"decode_mode": "p25",
|
"decode_mode": "P25",
|
||||||
"location": "Townsville",
|
"location": "Townsville",
|
||||||
"tags": None,
|
"tags": [],
|
||||||
"tag_whitelist": [6,7,8],
|
"tag_whitelist": [6,7,8],
|
||||||
"avail_on_nodes": ["client-def456", "client-ghi789"],
|
"avail_on_nodes": ["client-def456", "client-ghi789"],
|
||||||
"description": "Playing the hits, all day long."
|
"description": "Playing the hits, all day long."
|
||||||
@@ -117,7 +117,7 @@ websocket_server_instance = None
|
|||||||
async def startup_websocket_server():
|
async def startup_websocket_server():
|
||||||
"""Starts the WebSocket server when the Quart app starts."""
|
"""Starts the WebSocket server when the Quart app starts."""
|
||||||
global websocket_server_instance
|
global websocket_server_instance
|
||||||
websocket_server_address = "localhost"
|
websocket_server_address = "0.0.0.0"
|
||||||
websocket_server_port = 8765
|
websocket_server_port = 8765
|
||||||
|
|
||||||
# Start the WebSocket server task
|
# Start the WebSocket server task
|
||||||
@@ -138,31 +138,25 @@ async def shutdown_websocket_server():
|
|||||||
print("WebSocket server shut down.")
|
print("WebSocket server shut down.")
|
||||||
|
|
||||||
|
|
||||||
|
app.register_blueprint(systems_bp, url_prefix="/systems")
|
||||||
|
|
||||||
@app.route('/')
|
@app.route('/')
|
||||||
async def index():
|
async def index():
|
||||||
return "Welcome to the Radio App Server API!"
|
return "Welcome to the Radio App Server API!"
|
||||||
|
|
||||||
@app.route('/channels', methods=['GET'])
|
|
||||||
async def get_channels():
|
|
||||||
"""API endpoint to get a list of all channels."""
|
|
||||||
# Return a list of channel IDs and names for a summary view
|
|
||||||
channel_summary = [{"id": ch["id"], "name": ch["name"]} for ch in channels.values()]
|
|
||||||
return jsonify(channel_summary)
|
|
||||||
|
|
||||||
@app.route('/channels/<channel_id>', methods=['GET'])
|
|
||||||
async def get_channel_details(channel_id):
|
|
||||||
"""API endpoint to get details for a specific channel."""
|
|
||||||
channel = channels.get(channel_id)
|
|
||||||
if channel:
|
|
||||||
return jsonify(channel)
|
|
||||||
else:
|
|
||||||
return jsonify({"error": "Channel not found"}), 404
|
|
||||||
|
|
||||||
@app.route('/clients', methods=['GET'])
|
@app.route('/clients', methods=['GET'])
|
||||||
async def list_clients():
|
async def list_clients():
|
||||||
"""API endpoint to list currently connected client IDs."""
|
"""API endpoint to list currently connected client IDs."""
|
||||||
return jsonify(list(active_clients.keys()))
|
return jsonify(list(active_clients.keys()))
|
||||||
|
|
||||||
|
@app.route('/request_token', methods=['POST'])
|
||||||
|
async def request_token():
|
||||||
|
"""API endpoint to list currently connected client IDs."""
|
||||||
|
# TODO - Add DB logic
|
||||||
|
return jsonify({
|
||||||
|
"token": "MTE5NjAwNTM2ODYzNjExMjk3Nw.GuCMXg.24iNNofNNumq46FIj68zMe9RmQgugAgfrvelEA"
|
||||||
|
})
|
||||||
|
|
||||||
# --- Example API endpoint to trigger a WebSocket command ---
|
# --- Example API endpoint to trigger a WebSocket command ---
|
||||||
# This is a simple example. More complex logic might be needed
|
# This is a simple example. More complex logic might be needed
|
||||||
# to validate commands or arguments before sending.
|
# to validate commands or arguments before sending.
|
||||||
@@ -176,8 +170,8 @@ async def api_send_command(client_id, command_name):
|
|||||||
if client_id not in active_clients:
|
if client_id not in active_clients:
|
||||||
return jsonify({"error": f"Client {client_id} not found"}), 404
|
return jsonify({"error": f"Client {client_id} not found"}), 404
|
||||||
|
|
||||||
if command_name not in ["print_message", "set_status", "run_task"]: # Basic validation
|
# if command_name not in ["join_server", "leave_server", "set_status", "run_task"]: # Basic validation
|
||||||
return jsonify({"error": f"Unknown command: {command_name}"}), 400
|
# return jsonify({"error": f"Unknown command: {command_name}"}), 400
|
||||||
|
|
||||||
try:
|
try:
|
||||||
request_data = await request.get_json()
|
request_data = await request.get_json()
|
||||||
@@ -200,7 +194,7 @@ if __name__ == "__main__":
|
|||||||
# We removed asyncio.run(main()) and the main() function itself.
|
# We removed asyncio.run(main()) and the main() function itself.
|
||||||
print("Starting Quart API server...")
|
print("Starting Quart API server...")
|
||||||
app.run(
|
app.run(
|
||||||
host="localhost",
|
host="0.0.0.0",
|
||||||
port=5000,
|
port=5000,
|
||||||
debug=False # Set to True for development
|
debug=False # Set to True for development
|
||||||
)
|
)
|
||||||
@@ -1,2 +1,4 @@
|
|||||||
websockets
|
websockets
|
||||||
quart
|
quart
|
||||||
|
motor
|
||||||
|
fastapi
|
||||||
Reference in New Issue
Block a user