readmes added comments and some links to the openapi docs mds :)
All checks were successful
checks-impure / test (pull_request) Successful in 29s
checks / test (pull_request) Successful in 3m29s

This commit is contained in:
Georg-Stahn
2024-01-16 22:25:05 +01:00
parent 01e98d363b
commit 3052015a51
15 changed files with 215 additions and 97 deletions

View File

@@ -0,0 +1,37 @@
**init\_**.py:
```bash
usage: clan webui [-h] [--port PORT] [--host HOST] [--populate] [--emulate] [--no-open] [--dev]
[--dev-port DEV_PORT] [--dev-host DEV_HOST] [--reload]
[--log-level {critical,error,warning,info,debug,trace}]
[sub_url]
positional arguments:
sub_url Sub URL to open in the browser
options:
-h, --help show this help message and exit
--port PORT Port to listen on
--host HOST Host to listen on
--populate Populate the database with dummy data
--emulate Emulate two entities c1 and c2 + dlg and ap
--no-open Don't open the browser
--dev Run in development mode
--dev-port DEV_PORT Port to listen on for the dev server
--dev-host DEV_HOST Host to listen on
--reload Don't reload on changes
--log-level {critical,error,warning,info,debug,trace}
Log level
```
In this folder are some basic files:
- config.py
- to configer basic value for the server and the emulation
- ip/host
- ports
- emuplate_fast.py
- some api call that emulate the behavoir
- extra servers with api calls are emulated here
In the subfolder <webui> is the backend impplemented.

View File

@@ -1,14 +1,18 @@
# Imports
import argparse import argparse
import logging import logging
import sys import sys
from types import ModuleType from types import ModuleType
from typing import Optional from typing import Optional
# Custom imports
from . import webui from . import webui
from .custom_logger import setup_logging from .custom_logger import setup_logging
# Setting up the logger
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
# Trying to import argcomplete module, if not present, set it to None
argcomplete: Optional[ModuleType] = None argcomplete: Optional[ModuleType] = None
try: try:
import argcomplete # type: ignore[no-redef] import argcomplete # type: ignore[no-redef]
@@ -16,44 +20,59 @@ except ImportError:
pass pass
# Function to create the main argument parser
def create_parser(prog: Optional[str] = None) -> argparse.ArgumentParser: def create_parser(prog: Optional[str] = None) -> argparse.ArgumentParser:
# Creating the main argument parser with a description
parser = argparse.ArgumentParser(prog=prog, description="cLAN tool") parser = argparse.ArgumentParser(prog=prog, description="cLAN tool")
# Adding a debug argument to enable debug logging
parser.add_argument( parser.add_argument(
"--debug", "--debug",
help="Enable debug logging", help="Enable debug logging",
action="store_true", action="store_true",
) )
# Adding subparsers for different commands
subparsers = parser.add_subparsers() subparsers = parser.add_subparsers()
# Adding a subparser for the "webui" command
parser_webui = subparsers.add_parser("webui", help="start webui") parser_webui = subparsers.add_parser("webui", help="start webui")
# Registering additional arguments for the "webui" command
webui.register_parser(parser_webui) webui.register_parser(parser_webui)
# Using argcomplete for shell autocompletion if available
if argcomplete: if argcomplete:
argcomplete.autocomplete(parser) argcomplete.autocomplete(parser)
# If no command-line arguments provided, print the help message
if len(sys.argv) == 1: if len(sys.argv) == 1:
parser.print_help() parser.print_help()
return parser return parser
# this will be the entrypoint under /bin/clan (see pyproject.toml config) # this will be the entrypoint under /bin/clan (see pyproject.toml config)
### Main entry point function
def main() -> None: def main() -> None:
# Creating the main argument parser
parser = create_parser() parser = create_parser()
# Parsing command-line arguments
args = parser.parse_args() args = parser.parse_args()
# Setting up logging based on the debug flag
if args.debug: if args.debug:
setup_logging(logging.DEBUG) setup_logging(logging.DEBUG)
log.debug("Debug log activated") log.debug("Debug log activated")
else: else:
setup_logging(logging.INFO) setup_logging(logging.INFO)
# If the parsed arguments do not have the "func" attribute, exit
if not hasattr(args, "func"): if not hasattr(args, "func"):
return return
# Calling the function associated with the specified command
args.func(args) args.func(args)
# Entry point for script execution
if __name__ == "__main__": if __name__ == "__main__":
main() main()

View File

@@ -1,4 +1,15 @@
# CORS configuration
cors_url = [
"http://localhost",
"http://127.0.0.1",
"http://0.0.0.0",
"http://[::]",
]
cors_ports = [2979, 3000]
# host for the server, frontend, backend and emulators
host = "127.0.0.1" host = "127.0.0.1"
# used for emmulation and population for testing
port_dlg = 7000 port_dlg = 7000
port_ap = 7500 port_ap = 7500
_port_client_base = 8000 _port_client_base = 8000

View File

@@ -1,19 +1,24 @@
# Importing necessary modules and packages
import sys import sys
import time import time
import urllib import urllib
from datetime import datetime from datetime import datetime
# Importing FastAPI and related components
from fastapi import FastAPI from fastapi import FastAPI
from fastapi.responses import HTMLResponse, JSONResponse from fastapi.responses import HTMLResponse, JSONResponse
# Importing configuration and schemas from the clan_cli package
import clan_cli.config as config import clan_cli.config as config
from clan_cli.webui.schemas import Resolution from clan_cli.webui.schemas import Resolution
# Creating FastAPI instances for different applications
app_dlg = FastAPI(swagger_ui_parameters={"tryItOutEnabled": True}) app_dlg = FastAPI(swagger_ui_parameters={"tryItOutEnabled": True})
app_ap = FastAPI(swagger_ui_parameters={"tryItOutEnabled": True}) app_ap = FastAPI(swagger_ui_parameters={"tryItOutEnabled": True})
app_c1 = FastAPI(swagger_ui_parameters={"tryItOutEnabled": True}) app_c1 = FastAPI(swagger_ui_parameters={"tryItOutEnabled": True})
app_c2 = FastAPI(swagger_ui_parameters={"tryItOutEnabled": True}) app_c2 = FastAPI(swagger_ui_parameters={"tryItOutEnabled": True})
# List of FastAPI instances and their associated ports
apps = [ apps = [
(app_dlg, config.port_dlg), (app_dlg, config.port_dlg),
(app_ap, config.port_ap), (app_ap, config.port_ap),
@@ -22,7 +27,7 @@ apps = [
] ]
#### HEALTHCHECK # Healthcheck endpoints for different applications
@app_c1.get("/") @app_c1.get("/")
async def root_c1() -> str: async def root_c1() -> str:
return "C1 is alive" return "C1 is alive"
@@ -63,6 +68,7 @@ async def healthcheck_ap() -> str:
return "200 OK" return "200 OK"
# Function for performing health checks on a given URL with retries
def get_health(*, url: str, max_retries: int = 20, delay: float = 0.2) -> str | None: def get_health(*, url: str, max_retries: int = 20, delay: float = 0.2) -> str | None:
for attempt in range(max_retries): for attempt in range(max_retries):
try: try:
@@ -74,13 +80,10 @@ def get_health(*, url: str, max_retries: int = 20, delay: float = 0.2) -> str |
return None return None
#### CONSUME SERVICE # Service consumption emulation for c1 which returns a gif1
# TODO send_msg???
@app_c1.get("/v1/print_daemon1", response_class=HTMLResponse) @app_c1.get("/v1/print_daemon1", response_class=HTMLResponse)
async def consume_service_from_other_entity_c1() -> HTMLResponse: async def consume_service_from_other_entity_c1() -> HTMLResponse:
# HTML content for the response
html_content = """ html_content = """
<html> <html>
<body> <body>
@@ -104,6 +107,7 @@ async def deregister_c1() -> JSONResponse:
@app_c2.get("/v1/print_daemon2", response_class=HTMLResponse) @app_c2.get("/v1/print_daemon2", response_class=HTMLResponse)
async def consume_service_from_other_entity_c2() -> HTMLResponse: async def consume_service_from_other_entity_c2() -> HTMLResponse:
# Similar HTML content for the response
html_content = """ html_content = """
<html> <html>
<body> <body>
@@ -127,7 +131,9 @@ async def deregister_c2() -> JSONResponse:
@app_ap.get("/ap_list_of_services", response_class=JSONResponse) @app_ap.get("/ap_list_of_services", response_class=JSONResponse)
async def ap_list_of_services() -> JSONResponse: async def ap_list_of_services() -> JSONResponse:
# Sample list of services as a JSON response
res = [ res = [
# Service 1
{ {
"uuid": "bdd640fb-0667-1ad1-1c80-317fa3b1799d", "uuid": "bdd640fb-0667-1ad1-1c80-317fa3b1799d",
"service_name": "Carlos Printing0", "service_name": "Carlos Printing0",
@@ -150,6 +156,7 @@ async def ap_list_of_services() -> JSONResponse:
}, },
"usage": [{"times_consumed": 2, "consumer_entity_did": "did:sov:test:120"}], "usage": [{"times_consumed": 2, "consumer_entity_did": "did:sov:test:120"}],
}, },
# Service 2 (similar structure)
{ {
"uuid": "23b8c1e9-3924-56de-3eb1-3b9046685257", "uuid": "23b8c1e9-3924-56de-3eb1-3b9046685257",
"service_name": "Carlos Printing1", "service_name": "Carlos Printing1",
@@ -217,7 +224,6 @@ async def ap_list_of_services() -> JSONResponse:
"usage": [{"times_consumed": 2, "consumer_entity_did": "did:sov:test:120"}], "usage": [{"times_consumed": 2, "consumer_entity_did": "did:sov:test:120"}],
}, },
] ]
# resp = json.dumps(obj=res)
return JSONResponse(content=res, status_code=200) return JSONResponse(content=res, status_code=200)

View File

@@ -0,0 +1,6 @@
Here are the files found for the backend of the service.
- the backed is using a sql light db with sqlachremy
- this is done in
- sql\_\*.py, schema.py, tags.py
- subfolder: routers which also contains the apicall defenitions

View File

@@ -2,25 +2,55 @@ import argparse
import logging import logging
from typing import Callable, NoReturn, Optional from typing import Callable, NoReturn, Optional
# Get the logger for this module
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
# Initialize variables for server startup and potential ImportError
start_server: Optional[Callable] = None start_server: Optional[Callable] = None
ServerImportError: Optional[ImportError] = None ServerImportError: Optional[ImportError] = None
# Try importing the start_server function from the server module
try: try:
from .server import start_server from .server import start_server
except ImportError as e: except ImportError as e:
# If ImportError occurs, log the exception and store it in ServerImportError
log.exception(e) log.exception(e)
ServerImportError = e ServerImportError = e
# Function to be called when FastAPI is not installed
##########################################################################################
# usage: clan webui [-h] [--port PORT] [--host HOST] [--populate] [--emulate] [--no-open] [--dev]
# [--dev-port DEV_PORT] [--dev-host DEV_HOST] [--reload]
# [--log-level {critical,error,warning,info,debug,trace}]
# [sub_url]
#
# positional arguments:
# sub_url Sub URL to open in the browser
#
# options:
# -h, --help show this help message and exit
# --port PORT Port to listen on
# --host HOST Host to listen on
# --populate Populate the database with dummy data
# --emulate Emulate two entities c1 and c2 + dlg and ap
# --no-open Don't open the browser
# --dev Run in development mode
# --dev-port DEV_PORT Port to listen on for the dev server
# --dev-host DEV_HOST Host to listen on
# --reload Don't reload on changes
# --log-level {critical,error,warning,info,debug,trace}
# Log level
##########################################################################################
def fastapi_is_not_installed(_: argparse.Namespace) -> NoReturn: def fastapi_is_not_installed(_: argparse.Namespace) -> NoReturn:
assert ServerImportError is not None assert ServerImportError is not None
print( print(
f"Dependencies for the webserver is not installed. The webui command has been disabled ({ServerImportError})" f"Dependencies for the webserver are not installed. The webui command has been disabled ({ServerImportError})"
) )
exit(1) exit(1)
# Function to register command-line arguments for the webserver
def register_parser(parser: argparse.ArgumentParser) -> None: def register_parser(parser: argparse.ArgumentParser) -> None:
parser.add_argument("--port", type=int, default=2979, help="Port to listen on") parser.add_argument("--port", type=int, default=2979, help="Port to listen on")
parser.add_argument( parser.add_argument(
@@ -69,10 +99,10 @@ def register_parser(parser: argparse.ArgumentParser) -> None:
type=str, type=str,
default="/", default="/",
nargs="?", nargs="?",
help="Sub url to open in the browser", help="Sub URL to open in the browser",
) )
# Set the args.func variable in args # Set the args.func variable in args based on whether FastAPI is installed
if start_server is None: if start_server is None:
parser.set_defaults(func=fastapi_is_not_installed) parser.set_defaults(func=fastapi_is_not_installed)
else: else:

View File

@@ -1,55 +1,46 @@
# Imports
import logging import logging
from contextlib import asynccontextmanager
from typing import Any
# import for sql # Import FastAPI components and SQLAlchemy related modules
from fastapi import FastAPI from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
from fastapi.routing import APIRoute from fastapi.routing import APIRoute
from fastapi.staticfiles import StaticFiles from fastapi.staticfiles import StaticFiles
from sqlalchemy.exc import SQLAlchemyError from sqlalchemy.exc import SQLAlchemyError
# Import configs
from ..config import cors_ports, cors_url
# Import custom modules and classes
from ..errors import ClanError from ..errors import ClanError
from . import sql_models from . import sql_models
from .assets import asset_path from .assets import asset_path
from .error_handlers import clan_error_handler, sql_error_handler from .error_handlers import clan_error_handler, sql_error_handler
from .routers import endpoints, health, root, socket_manager2 # sql router hinzufügen from .routers import endpoints, health, root
from .sql_db import engine from .sql_db import engine
from .tags import tags_metadata from .tags import tags_metadata
cors_url = [
"http://localhost",
"http://127.0.0.1",
"http://0.0.0.0",
"http://[::]",
]
cors_ports = [2979, 3000]
cors_whitelist = [] cors_whitelist = []
for u in cors_url: for u in cors_url:
for p in cors_ports: for p in cors_ports:
cors_whitelist.append(f"{u}:{p}") cors_whitelist.append(f"{u}:{p}")
# Logging setup # Logging setup
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@asynccontextmanager # Function to set up and configure the FastAPI application
async def lifespan(app: FastAPI) -> Any:
await socket_manager2.brd.connect()
yield
await socket_manager2.brd.disconnect()
def setup_app() -> FastAPI: def setup_app() -> FastAPI:
# bind sql engine # Uncomment the following line to drop existing tables during startup (if needed)
# TODO comment aut and add flag to run with pupulated data rm *.sql run pytest with marked then start clan webui
# https://docs.pytest.org/en/7.1.x/example/markers.html
# sql_models.Base.metadata.drop_all(engine) # sql_models.Base.metadata.drop_all(engine)
# Create tables in the database using SQLAlchemy
sql_models.Base.metadata.create_all(bind=engine) sql_models.Base.metadata.create_all(bind=engine)
app = FastAPI(lifespan=lifespan, swagger_ui_parameters={"tryItOutEnabled": True}) # Initialize FastAPI application with lifespan management
app = FastAPI(swagger_ui_parameters={"tryItOutEnabled": True})
# Configure CORS middleware
app.add_middleware( app.add_middleware(
CORSMiddleware, CORSMiddleware,
allow_origins=cors_whitelist, allow_origins=cors_whitelist,
@@ -58,31 +49,35 @@ def setup_app() -> FastAPI:
allow_headers=["*"], allow_headers=["*"],
) )
# Include routers for various endpoints and components
app.include_router(health.router) app.include_router(health.router)
# sql methodes
app.include_router(endpoints.router) app.include_router(endpoints.router)
app.include_router(socket_manager2.router) # Needs to be last in registration due to wildcard route
# Needs to be last in register. Because of wildcard route
app.include_router(root.router) app.include_router(root.router)
# Add custom exception handlers
app.add_exception_handler(ClanError, clan_error_handler) # type: ignore app.add_exception_handler(ClanError, clan_error_handler) # type: ignore
app.add_exception_handler(SQLAlchemyError, sql_error_handler) # type: ignore app.add_exception_handler(SQLAlchemyError, sql_error_handler) # type: ignore
# Mount the "static" route for serving static files
app.mount("/static", StaticFiles(directory=asset_path()), name="static") app.mount("/static", StaticFiles(directory=asset_path()), name="static")
# Add tag descriptions to the OpenAPI schema # Add tag descriptions to the OpenAPI schema
app.openapi_tags = tags_metadata app.openapi_tags = tags_metadata
# Assign operation IDs to API routes
for route in app.routes: for route in app.routes:
if isinstance(route, APIRoute): if isinstance(route, APIRoute):
route.operation_id = route.name # in this case, 'read_items' route.operation_id = route.name # in this case, 'read_items'
log.debug(f"Registered route: {route}") log.debug(f"Registered route: {route}")
# Log registered exception handlers
for i in app.exception_handlers.items(): for i in app.exception_handlers.items():
log.debug(f"Registered exception handler: {i}") log.debug(f"Registered exception handler: {i}")
return app return app
# Create an instance of the FastAPI application
app = setup_app() app = setup_app()

View File

@@ -0,0 +1 @@
In the <endpoints.py> are the api endpoints implemented which could be used of the user/s.

View File

@@ -27,11 +27,21 @@ router = APIRouter()
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
# API Endpoints for all tables
# see the default api documentation under:
### pkgs/clan-cli/tests/openapi_client/docs/DefaultApi.md
######################### #########################
# # # #
# Service # # Service #
# # # #
######################### #########################
# see the corresponding documentation under:
### pkgs/clan-cli/tests/openapi_client/docs/Service.md
### pkgs/clan-cli/tests/openapi_client/docs/ServiceCreate.md
### pkgs/clan-cli/tests/openapi_client/docs/ServiceUsageCreate.md
### pkgs/clan-cli/tests/openapi_client/docs/ServicesApi.md
@router.post("/api/v1/service", response_model=Service, tags=[Tags.services]) @router.post("/api/v1/service", response_model=Service, tags=[Tags.services])
def create_service( def create_service(
service: ServiceCreate, db: Session = Depends(sql_db.get_db) service: ServiceCreate, db: Session = Depends(sql_db.get_db)
@@ -134,6 +144,10 @@ def delete_service(
# Entity # # Entity #
# # # #
######################### #########################
# see the corresponding documentation under:
### pkgs/clan-cli/tests/openapi_client/docs/Entity.md
### pkgs/clan-cli/tests/openapi_client/docs/EntityCreate.md
### pkgs/clan-cli/tests/openapi_client/docs/EntitiesApi.md
@router.post("/api/v1/entity", response_model=Entity, tags=[Tags.entities]) @router.post("/api/v1/entity", response_model=Entity, tags=[Tags.entities])
def create_entity( def create_entity(
entity: EntityCreate, db: Session = Depends(sql_db.get_db) entity: EntityCreate, db: Session = Depends(sql_db.get_db)
@@ -298,6 +312,9 @@ def get_rpc_by_role(db: Session, role: Role, path: str) -> Any:
# Resolution # # Resolution #
# # # #
######################### #########################
# see the corresponding documentation under:
### pkgs/clan-cli/tests/openapi_client/docs/Resolution.md
### pkgs/clan-cli/tests/openapi_client/docs/ResolutionApi.md
@router.get( @router.get(
"/api/v1/resolutions", response_model=List[Resolution], tags=[Tags.resolutions] "/api/v1/resolutions", response_model=List[Resolution], tags=[Tags.resolutions]
) )
@@ -312,6 +329,8 @@ def get_all_resolutions(
# Repository # # Repository #
# # # #
######################### #########################
# see the corresponding documentation under:
### pkgs/clan-cli/tests/openapi_client/docs/RepositoriesApi.md
@router.get( @router.get(
"/api/v1/repositories", tags=[Tags.repositories], response_model=List[Service] "/api/v1/repositories", tags=[Tags.repositories], response_model=List[Service]
) )
@@ -326,6 +345,10 @@ def get_all_repositories(
# Eventmessage # # Eventmessage #
# # # #
######################### #########################
# see the corresponding documentation under:
### pkgs/clan-cli/tests/openapi_client/docs/Eventmessage.md
### pkgs/clan-cli/tests/openapi_client/docs/EventmessageCreate.md
### pkgs/clan-cli/tests/openapi_client/docs/EventmessageApi.md
@router.post( @router.post(
"/api/v1/event_message", response_model=Eventmessage, tags=[Tags.eventmessages] "/api/v1/event_message", response_model=Eventmessage, tags=[Tags.eventmessages]
) )

View File

@@ -1,52 +0,0 @@
# Requires: `starlette`, `uvicorn`, `jinja2`
# Run with `uvicorn example:app`
import logging
import os
import anyio
from broadcaster import Broadcast
from fastapi import APIRouter, WebSocket
from fastapi.responses import HTMLResponse
log = logging.getLogger(__name__)
router = APIRouter()
brd = Broadcast("memory://")
@router.get("/ws2_example")
async def get() -> HTMLResponse:
html = open(f"{os.getcwd()}/webui/routers/messenger.html").read()
return HTMLResponse(html)
@router.websocket("/ws2")
async def chatroom_ws(websocket: WebSocket) -> None:
await websocket.accept()
async with anyio.create_task_group() as task_group:
# run until first is complete
async def run_chatroom_ws_receiver() -> None:
await chatroom_ws_receiver(websocket=websocket)
task_group.cancel_scope.cancel()
task_group.start_soon(run_chatroom_ws_receiver)
log.warning("Started chatroom_ws_sender")
await chatroom_ws_sender(websocket)
async def chatroom_ws_receiver(websocket: WebSocket) -> None:
async for message in websocket.iter_text():
log.warning(f"Received message: {message}")
await brd.publish(channel="chatroom", message=message)
async def chatroom_ws_sender(websocket: WebSocket) -> None:
async with brd.subscribe(channel="chatroom") as subscriber:
if subscriber is None:
log.error("Subscriber is None")
return
async for event in subscriber: # type: ignore
await websocket.send_text(event.message)

View File

@@ -1,3 +1,4 @@
# Imports
import logging import logging
from datetime import datetime from datetime import datetime
from typing import List from typing import List
@@ -7,14 +8,23 @@ from pydantic import BaseModel, Field, validator
from . import sql_models from . import sql_models
from .db_types import Role, Status from .db_types import Role, Status
# Set logger
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
# create basemodel
class Machine(BaseModel): class Machine(BaseModel):
name: str name: str
status: Status status: Status
### Create database schema for sql
# each section will represent an own table
# Entity, Service, Resolution, Eventmessages
# The relation between them is as follows:
# one Entity can have many Services
######################### #########################
# # # #
# Entity # # Entity #

View File

@@ -1,3 +1,4 @@
# Imports
import argparse import argparse
import logging import logging
import multiprocessing as mp import multiprocessing as mp
@@ -10,7 +11,6 @@ from pathlib import Path
from threading import Thread from threading import Thread
from typing import Iterator from typing import Iterator
# XXX: can we dynamically load this using nix develop?
import uvicorn import uvicorn
from pydantic import AnyUrl, IPvAnyAddress from pydantic import AnyUrl, IPvAnyAddress
from pydantic.tools import parse_obj_as from pydantic.tools import parse_obj_as
@@ -19,9 +19,11 @@ import clan_cli.config as config
from clan_cli.emulate_fastapi import apps, get_health from clan_cli.emulate_fastapi import apps, get_health
from clan_cli.errors import ClanError from clan_cli.errors import ClanError
# Setting up logging
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
# Function to open the browser for a specified URL
def open_browser(base_url: AnyUrl, sub_url: str) -> None: def open_browser(base_url: AnyUrl, sub_url: str) -> None:
for i in range(5): for i in range(5):
try: try:
@@ -33,6 +35,7 @@ def open_browser(base_url: AnyUrl, sub_url: str) -> None:
_open_browser(url) _open_browser(url)
# Helper function to open a web browser for a given URL using available browsers
def _open_browser(url: AnyUrl) -> subprocess.Popen: def _open_browser(url: AnyUrl) -> subprocess.Popen:
for browser in ("firefox", "iceweasel", "iceape", "seamonkey"): for browser in ("firefox", "iceweasel", "iceape", "seamonkey"):
if shutil.which(browser): if shutil.which(browser):
@@ -52,6 +55,7 @@ def _open_browser(url: AnyUrl) -> subprocess.Popen:
raise ClanError("No browser found") raise ClanError("No browser found")
# Context manager to spawn the Node.js development server
@contextmanager @contextmanager
def spawn_node_dev_server(host: IPvAnyAddress, port: int) -> Iterator[None]: def spawn_node_dev_server(host: IPvAnyAddress, port: int) -> Iterator[None]:
log.info("Starting node dev server...") log.info("Starting node dev server...")
@@ -78,6 +82,7 @@ def spawn_node_dev_server(host: IPvAnyAddress, port: int) -> Iterator[None]:
proc.terminate() proc.terminate()
# Main function to start the server
def start_server(args: argparse.Namespace) -> None: def start_server(args: argparse.Namespace) -> None:
with ExitStack() as stack: with ExitStack() as stack:
headers: list[tuple[str, str]] = [] headers: list[tuple[str, str]] = []
@@ -115,6 +120,7 @@ def start_server(args: argparse.Namespace) -> None:
sql_models.Base.metadata.drop_all(engine) sql_models.Base.metadata.drop_all(engine)
if args.populate: if args.populate:
# pre populate the server with some test data
test_dir = Path(__file__).parent.parent.parent / "tests" test_dir = Path(__file__).parent.parent.parent / "tests"
if not test_dir.is_dir(): if not test_dir.is_dir():

View File

@@ -1,3 +1,4 @@
# Imports
from typing import List, Optional from typing import List, Optional
from sqlalchemy import func from sqlalchemy import func
@@ -7,6 +8,8 @@ from sqlalchemy.sql.expression import true
from ..errors import ClanError from ..errors import ClanError
from . import schemas, sql_models from . import schemas, sql_models
# Functions to manipulate the tables of the database
######################### #########################
# # # #

View File

@@ -1,3 +1,4 @@
# Imports
from typing import Generator from typing import Generator
from sqlalchemy import create_engine from sqlalchemy import create_engine
@@ -7,6 +8,8 @@ from sqlalchemy.orm import Session, declarative_base, sessionmaker
URL = "sqlite:///./sql_app.db" URL = "sqlite:///./sql_app.db"
# Create db engine
engine = create_engine(URL, connect_args={"check_same_thread": False}) engine = create_engine(URL, connect_args={"check_same_thread": False})
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)

View File

@@ -4,15 +4,15 @@ from sqlalchemy.orm import relationship
from .db_types import Role from .db_types import Role
from .sql_db import Base from .sql_db import Base
# Relationsship example
# https://dev.to/freddiemazzilli/flask-sqlalchemy-relationships-exploring-relationship-associations-igo
# SQLAlchemy model for the "entities" table
class Entity(Base): class Entity(Base):
__tablename__ = "entities" __tablename__ = "entities"
## Queryable body ## ## Queryable body ##
# Primary Key
did = Column(String, primary_key=True, index=True) did = Column(String, primary_key=True, index=True)
# Indexed Columns
name = Column(String, index=True, unique=True) name = Column(String, index=True, unique=True)
ip = Column(String, index=True) ip = Column(String, index=True)
network = Column(String, index=True) network = Column(String, index=True)
@@ -21,67 +21,85 @@ class Entity(Base):
stop_health_task = Column(Boolean) stop_health_task = Column(Boolean)
## Non queryable body ## ## Non queryable body ##
# In here we deposit: Not yet defined stuff # JSON field for additional non-queryable data
other = Column(JSON) other = Column(JSON)
## Relations ## ## Relations ##
# One-to-Many relationship with "services" table
services = relationship("Service", back_populates="entity") services = relationship("Service", back_populates="entity")
# One-to-Many relationship with "entity_roles" table
roles = relationship("EntityRoles", back_populates="entity") roles = relationship("EntityRoles", back_populates="entity")
# One-to-Many relationship with "service_usage" table
consumes = relationship("ServiceUsage", back_populates="consumer_entity") consumes = relationship("ServiceUsage", back_populates="consumer_entity")
# SQLAlchemy model for the "entity_roles" table
class EntityRoles(Base): class EntityRoles(Base):
__tablename__ = "entity_roles" __tablename__ = "entity_roles"
## Queryable body ## ## Queryable body ##
# Primary Key
id = Column(Integer, primary_key=True, autoincrement=True) id = Column(Integer, primary_key=True, autoincrement=True)
# Foreign Key
entity_did = Column(String, ForeignKey("entities.did")) entity_did = Column(String, ForeignKey("entities.did"))
# Enum field for role
role = Column(Enum(Role), index=True, nullable=False) # type: ignore role = Column(Enum(Role), index=True, nullable=False) # type: ignore
## Relations ## ## Relations ##
# Many-to-One relationship with "entities" table
entity = relationship("Entity", back_populates="roles") entity = relationship("Entity", back_populates="roles")
# SQLAlchemy model for the "service_usage" table
class ServiceUsage(Base): class ServiceUsage(Base):
__tablename__ = "service_usage" __tablename__ = "service_usage"
## Queryable body ## ## Queryable body ##
# Primary Key
id = Column(Integer, primary_key=True, autoincrement=True) id = Column(Integer, primary_key=True, autoincrement=True)
# Foreign Key
consumer_entity_did = Column(String, ForeignKey("entities.did")) consumer_entity_did = Column(String, ForeignKey("entities.did"))
# Many-to-One relationship with "entities" table
consumer_entity = relationship("Entity", back_populates="consumes") consumer_entity = relationship("Entity", back_populates="consumes")
times_consumed = Column(Integer, index=True, nullable=False) times_consumed = Column(Integer, index=True, nullable=False)
service_uuid = Column(String, ForeignKey("services.uuid")) service_uuid = Column(String, ForeignKey("services.uuid"))
# Many-to-One relationship with "services" table
service = relationship("Service", back_populates="usage") service = relationship("Service", back_populates="usage")
# SQLAlchemy model for the "services" table
class Service(Base): class Service(Base):
__tablename__ = "services" __tablename__ = "services"
# Queryable body # Queryable body
# Primary Key
uuid = Column(Text(length=36), primary_key=True, index=True) uuid = Column(Text(length=36), primary_key=True, index=True)
service_name = Column(String, index=True) service_name = Column(String, index=True)
service_type = Column(String, index=True) service_type = Column(String, index=True)
endpoint_url = Column(String, index=True) endpoint_url = Column(String, index=True)
## Non queryable body ## ## Non queryable body ##
# In here we deposit: Action # JSON fields for additional non-queryable data
other = Column(JSON) other = Column(JSON)
status = Column(JSON, index=True) status = Column(JSON, index=True)
action = Column(JSON, index=True) action = Column(JSON, index=True)
## Relations ## ## Relations ##
# One entity can have many services # One-to-Many relationship with "entities" table
entity = relationship("Entity", back_populates="services") entity = relationship("Entity", back_populates="services")
entity_did = Column(String, ForeignKey("entities.did")) entity_did = Column(String, ForeignKey("entities.did"))
# One-to-Many relationship with "service_usage" table
usage = relationship("ServiceUsage", back_populates="service") usage = relationship("ServiceUsage", back_populates="service")
# SQLAlchemy model for the "eventmessages" table
class Eventmessage(Base): class Eventmessage(Base):
__tablename__ = "eventmessages" __tablename__ = "eventmessages"
## Queryable body ## ## Queryable body ##
# Primary Key
id = Column(Integer, primary_key=True, autoincrement=True) id = Column(Integer, primary_key=True, autoincrement=True)
timestamp = Column(Integer, unique=True, index=True) timestamp = Column(Integer, unique=True, index=True)
group = Column(Integer, index=True) group = Column(Integer, index=True)
@@ -91,8 +109,10 @@ class Eventmessage(Base):
des_did = Column(String, index=True) des_did = Column(String, index=True)
## Non queryable body ## ## Non queryable body ##
# In here we deposit: Network, Roles, Visible, etc. # JSON field for additional non-queryable data
msg = Column(JSON) msg = Column(JSON)
## Relations ## ## Relations ##
# One-to-Many relationship with "entities" table
# One entity can send many messages # One entity can send many messages
# (Note: The comment is incomplete and can be extended based on the relationship)