move into /app dir and add new db handler + wrappers

This commit is contained in:
Logan Cusano
2025-05-11 20:33:57 -04:00
parent 163153d7f6
commit 67925c120a
8 changed files with 840 additions and 30 deletions

4
.gitignore vendored
View File

@@ -1 +1,3 @@
*.venv *.venv
*__pycache__/
*.bat

View File

@@ -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

View File

@@ -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
View 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
View 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
View 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}")

View File

@@ -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
) )

View File

@@ -1,2 +1,4 @@
websockets websockets
quart quart
motor
fastapi