From 8262aa72abdaf0c44f03ec9788233e28032b498c Mon Sep 17 00:00:00 2001 From: Jakob Scheid Date: Wed, 29 Apr 2026 19:04:39 +0200 Subject: [PATCH] Add basic API structure --- docs/CHANGELOG.md | 7 + src/jcloud_deployment_server/__main__.py | 155 +++++++++++++++++++++++ 2 files changed, 162 insertions(+) create mode 100644 docs/CHANGELOG.md create mode 100644 src/jcloud_deployment_server/__main__.py diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md new file mode 100644 index 0000000..f38471b --- /dev/null +++ b/docs/CHANGELOG.md @@ -0,0 +1,7 @@ +# Changelog + +## [Unreleased] + +### Added + +- Add basic API structure \ No newline at end of file diff --git a/src/jcloud_deployment_server/__main__.py b/src/jcloud_deployment_server/__main__.py new file mode 100644 index 0000000..3023cd9 --- /dev/null +++ b/src/jcloud_deployment_server/__main__.py @@ -0,0 +1,155 @@ +# Copyright 2026 jCloud Services GbR + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from fastapi import FastAPI +import os +import uvicorn +import argparse +import ipaddress +import jcloud_config_parser +import logging +import pathlib + +def existing_file(path: str) -> pathlib.Path: + ''' + Checks whether a path is a file and returns it if it exists. + + :param path: The path + :type path: str + + :raises argparse.ArgumentTypeError: If the path is a directory or does not exist. + + :return: The path + :rtype: pathlib.Path + ''' + + if os.path.isdir(path): + raise argparse.ArgumentTypeError(f'\'{path}\': Is a directory') + if not os.path.isfile(path): + raise argparse.ArgumentTypeError(f'\'{path}\': No such file or directory') + return pathlib.Path(path) + +# parse arguments + +argument_parser = argparse.ArgumentParser(description='The jCloud deployment server.') +argument_parser.add_argument('-c', '--config', type=existing_file, default=pathlib.Path(os.path.dirname(__file__) + '/../../config/'), help='The directory containing the configuration files.') +argument_parser.add_argument('-H', '--host', type=ipaddress.ip_address, default=None, help='The host to listen') +argument_parser.add_argument('-p', '--port', type=int, default=None, help='The port to listen') +args = argument_parser.parse_args() + +# load default configuration +with open(os.path.dirname(__file__) + '/../../default_configuration.conf') as f: + default_configuration = jcloud_config_parser.ini.INIConfiguration.from_string(f.read()) + f.close() + +# load configuration +configuration = jcloud_config_parser.ini.INIConfiguration.from_string( + (args.config / 'server.conf').read_text(), + default=default_configuration, + ignore_errors=True +) + +logfile = configuration.logging.logfile +if os.path.isdir(logfile): + logfile = None +if not logfile: + logfile = None + +LOGLEVEL = logging._nameToLevel.get(configuration.logging.loglevel.upper(), logging._nameToLevel.get(default_configuration.logging.loglevel.upper(), logging.INFO)) + +# set up logging +LOGGING_FORMAT = '%(asctime)s [%(levelname)s] %(name)s: %(message)s' +logging.basicConfig(level=LOGLEVEL, format=LOGGING_FORMAT) +logger = logging.getLogger('api') +logger.propagate = False +logger.handlers = list() + +formatter = logging.Formatter(LOGGING_FORMAT) +if logfile is not None: + logger_file_handler = logging.FileHandler(logfile) + logger_file_handler.setFormatter(formatter) + logger.addHandler(logger_file_handler) + +def validate_host(host: str) -> bool: + ''' + Checks whether the address is a valid address (``ip:port``), e. g. ``0.0.0.0:3000``. + + :param addr: The address + :type addr: str + + :return: ``True`` if the address is valid, otherwise ``False``. + :rtype: bool + ''' + + try: + ipaddress.ip_address(host) + + return True + except Exception: + return False + +# host +host = args.host or configuration.server.host +if not validate_host(host): + logger.error(f'Error in configuration: [server]host: \'{host}\' is not a valid host, using default value.') + host = default_configuration.server.host + +# port +port = args.port or configuration.server.port +if not port.isdigit(): + logger.error(f'Error in configuration: [server]port: \'{port}\' is not an integer, using default value.') + port = default_configuration.server.port +else: + if not (0 <= int(port) <= 65535): + logger.error(f'Error in configuration: [server]port: {port} is not between 0 and 65535, using default value.') + port = default_configuration.server.port +port = int(port) + +# Initialize FastAPI +app = FastAPI( + docs_url=configuration.docs.swagger, + redoc_url=configuration.docs.redoc, + openapi_url=configuration.docs.openapi +) + +if __name__ == '__main__': + uvicorn.run( + app, + host = host, + port = port, + server_header = False, + log_config = { + 'version': 1, + 'disable_existing_loggers': True, + 'handlers': { + 'uvicorn_handler': { + 'class': 'logging.FileHandler', + 'filename': logfile, + 'formatter': 'uvicorn_formatter', + }, + }, + 'formatters': { + 'uvicorn_formatter': { + 'format': LOGGING_FORMAT + }, + }, + 'loggers': { + 'uvicorn': { + 'handlers': ['uvicorn_handler'], + 'level': LOGLEVEL, + 'propagate': False + }, + }, + } + ) \ No newline at end of file