From 3b2f2f0469ff349ec346d5045ea27d22bdf90706 Mon Sep 17 00:00:00 2001 From: Lukas Stotz <lukas.stotz@student.reutlingen-university.de> Date: Mon, 14 Apr 2025 16:37:01 +0200 Subject: [PATCH] Restructure backend: Move to app directory, update dependencies, and improve organization --- server/app/__init__.py | 23 ++ server/app/routes/__init__.py | 0 server/app/routes/hl7.py | 28 ++ server/app/routes/xml.py | 27 ++ server/app/services/__init__.py | 0 server/app/services/hl7_service.py | 42 +++ server/app/services/xml_service.py | 35 +++ server/app/utils/__init__.py | 0 server/{ => app/utils}/adt_mapper.py | 0 server/{ => app/utils}/func.py | 0 server/app/utils/hl7_receiver.py | 65 ++++ .../{ => app/utils}/hl7_to_fhir_converter.py | 0 server/app/utils/xml_processor.py | 88 ++++++ server/config.py | 35 +++ server/hl7_receiver.py | 286 ------------------ server/main.py | 97 ------ server/requirements.txt | 8 +- server/run.py | 16 + server/xml_processor.py | 219 -------------- 19 files changed, 364 insertions(+), 605 deletions(-) create mode 100644 server/app/__init__.py create mode 100644 server/app/routes/__init__.py create mode 100644 server/app/routes/hl7.py create mode 100644 server/app/routes/xml.py create mode 100644 server/app/services/__init__.py create mode 100644 server/app/services/hl7_service.py create mode 100644 server/app/services/xml_service.py create mode 100644 server/app/utils/__init__.py rename server/{ => app/utils}/adt_mapper.py (100%) rename server/{ => app/utils}/func.py (100%) create mode 100644 server/app/utils/hl7_receiver.py rename server/{ => app/utils}/hl7_to_fhir_converter.py (100%) create mode 100644 server/app/utils/xml_processor.py create mode 100644 server/config.py delete mode 100644 server/hl7_receiver.py delete mode 100644 server/main.py create mode 100644 server/run.py delete mode 100644 server/xml_processor.py diff --git a/server/app/__init__.py b/server/app/__init__.py new file mode 100644 index 0000000..2282121 --- /dev/null +++ b/server/app/__init__.py @@ -0,0 +1,23 @@ +from flask import Flask +from flask_cors import CORS + +def create_app(config_class=None): + """Create and configure the Flask application.""" + app = Flask(__name__) + + # Configure app + if config_class: + app.config.from_object(config_class) + + # Enable CORS + CORS(app) + + # Initialize extensions + # TODO: Add any Flask extensions here + + # Register blueprints + from app.routes import hl7_bp, xml_bp + app.register_blueprint(hl7_bp) + app.register_blueprint(xml_bp) + + return app diff --git a/server/app/routes/__init__.py b/server/app/routes/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/server/app/routes/hl7.py b/server/app/routes/hl7.py new file mode 100644 index 0000000..a4de22e --- /dev/null +++ b/server/app/routes/hl7.py @@ -0,0 +1,28 @@ +from flask import Blueprint, jsonify, request +from app.services.hl7_service import HL7Service + +hl7_bp = Blueprint('hl7', __name__) +hl7_service = HL7Service() + +@hl7_bp.route('/api/hl7/messages', methods=['GET']) +def get_messages(): + """Get all received HL7 messages.""" + messages = hl7_service.get_messages() + return jsonify(messages) + +@hl7_bp.route('/api/hl7/messages/<message_id>', methods=['GET']) +def get_message(message_id): + """Get a specific HL7 message by ID.""" + message = hl7_service.get_message(message_id) + if message: + return jsonify(message) + return jsonify({'error': 'Message not found'}), 404 + +@hl7_bp.route('/api/hl7/convert/<message_id>', methods=['POST']) +def convert_to_fhir(message_id): + """Convert an HL7 message to FHIR format.""" + try: + fhir_resource = hl7_service.convert_to_fhir(message_id) + return jsonify(fhir_resource) + except Exception as e: + return jsonify({'error': str(e)}), 500 \ No newline at end of file diff --git a/server/app/routes/xml.py b/server/app/routes/xml.py new file mode 100644 index 0000000..262fd97 --- /dev/null +++ b/server/app/routes/xml.py @@ -0,0 +1,27 @@ +from flask import Blueprint, jsonify, request +from app.services.xml_service import XMLService + +xml_bp = Blueprint('xml', __name__) +xml_service = XMLService() + +@xml_bp.route('/api/xml/resources', methods=['GET']) +def get_resources(): + """Get all available XML resources.""" + resources = xml_service.get_resources() + return jsonify(resources) + +@xml_bp.route('/api/xml/resources/<resource_key>', methods=['GET']) +def get_resource(resource_key): + """Get a specific XML resource by key.""" + resource = xml_service.get_resource(resource_key) + if resource: + return jsonify(resource) + return jsonify({'error': 'Resource not found'}), 404 + +@xml_bp.route('/api/xml/resources/<resource_key>/structure', methods=['GET']) +def get_resource_structure(resource_key): + """Get the structure of a specific XML resource.""" + structure = xml_service.get_resource_structure(resource_key) + if structure: + return jsonify(structure) + return jsonify({'error': 'Resource structure not found'}), 404 \ No newline at end of file diff --git a/server/app/services/__init__.py b/server/app/services/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/server/app/services/hl7_service.py b/server/app/services/hl7_service.py new file mode 100644 index 0000000..bb58195 --- /dev/null +++ b/server/app/services/hl7_service.py @@ -0,0 +1,42 @@ +import os +from app.utils.hl7_receiver import HL7Receiver +from app.utils.hl7_to_fhir_converter import HL7ToFHIRConverter + +class HL7Service: + def __init__(self): + self.receiver = HL7Receiver() + self.converter = HL7ToFHIRConverter() + self.messages_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))), + 'received_messages') + + def get_messages(self): + """Get all received HL7 messages.""" + messages = [] + for filename in os.listdir(self.messages_dir): + if filename.endswith('.txt'): + with open(os.path.join(self.messages_dir, filename), 'r') as f: + content = f.read() + messages.append({ + 'id': filename, + 'content': content + }) + return messages + + def get_message(self, message_id): + """Get a specific HL7 message by ID.""" + filepath = os.path.join(self.messages_dir, message_id) + if os.path.exists(filepath): + with open(filepath, 'r') as f: + return { + 'id': message_id, + 'content': f.read() + } + return None + + def convert_to_fhir(self, message_id): + """Convert an HL7 message to FHIR format.""" + message = self.get_message(message_id) + if not message: + raise ValueError(f"Message {message_id} not found") + + return self.converter.convert(message['content']) \ No newline at end of file diff --git a/server/app/services/xml_service.py b/server/app/services/xml_service.py new file mode 100644 index 0000000..b8ac8c6 --- /dev/null +++ b/server/app/services/xml_service.py @@ -0,0 +1,35 @@ +import os +from app.utils.xml_processor import XMLStructureParser + +class XMLService: + def __init__(self): + self.processor = XMLStructureParser() + self.xml_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))), + 'xml_files') + self._resources = {} + + def get_resources(self): + """Get all available XML resources.""" + resources = {} + for filename in os.listdir(self.xml_dir): + if filename.endswith('.xml'): + resource_key = os.path.splitext(filename)[0] + resources[resource_key] = { + 'filename': filename, + 'path': os.path.join(self.xml_dir, filename) + } + return resources + + def get_resource(self, resource_key): + """Get a specific XML resource by key.""" + filepath = os.path.join(self.xml_dir, f"{resource_key}.xml") + if os.path.exists(filepath): + return self.processor.parse_xml_file(filepath) + return None + + def get_resource_structure(self, resource_key): + """Get the structure of a specific XML resource.""" + resource = self.get_resource(resource_key) + if resource: + return resource.get('structure', {}) + return None \ No newline at end of file diff --git a/server/app/utils/__init__.py b/server/app/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/server/adt_mapper.py b/server/app/utils/adt_mapper.py similarity index 100% rename from server/adt_mapper.py rename to server/app/utils/adt_mapper.py diff --git a/server/func.py b/server/app/utils/func.py similarity index 100% rename from server/func.py rename to server/app/utils/func.py diff --git a/server/app/utils/hl7_receiver.py b/server/app/utils/hl7_receiver.py new file mode 100644 index 0000000..6ec8ccc --- /dev/null +++ b/server/app/utils/hl7_receiver.py @@ -0,0 +1,65 @@ +import os +import logging +from watchdog.observers import Observer +from watchdog.events import FileSystemEventHandler +from datetime import datetime + +class HL7MessageHandler(FileSystemEventHandler): + def __init__(self, messages_dir, logs_dir): + self.messages_dir = messages_dir + self.logs_dir = logs_dir + self.setup_logging() + + def setup_logging(self): + """Setup logging configuration.""" + log_file = os.path.join(self.logs_dir, 'hl7_receiver.log') + logging.basicConfig( + filename=log_file, + level=logging.INFO, + format='%(asctime)s - %(levelname)s - %(message)s' + ) + + def on_created(self, event): + """Handle file creation events.""" + if not event.is_directory and event.src_path.endswith('.txt'): + self.process_message(event.src_path) + + def process_message(self, filepath): + """Process a new HL7 message file.""" + try: + with open(filepath, 'r') as f: + content = f.read() + + # Log the received message + logging.info(f"Received HL7 message: {os.path.basename(filepath)}") + + # TODO: Add any additional message processing here + + except Exception as e: + logging.error(f"Error processing message {filepath}: {str(e)}") + +class HL7Receiver: + def __init__(self, messages_dir=None, logs_dir=None): + base_dir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + self.messages_dir = messages_dir or os.path.join(base_dir, 'received_messages') + self.logs_dir = logs_dir or os.path.join(base_dir, 'message_logs') + self.observer = None + self.handler = None + + def start(self): + """Start the HL7 message receiver.""" + # Create directories if they don't exist + os.makedirs(self.messages_dir, exist_ok=True) + os.makedirs(self.logs_dir, exist_ok=True) + + # Initialize and start the observer + self.handler = HL7MessageHandler(self.messages_dir, self.logs_dir) + self.observer = Observer() + self.observer.schedule(self.handler, self.messages_dir, recursive=False) + self.observer.start() + + def stop(self): + """Stop the HL7 message receiver.""" + if self.observer: + self.observer.stop() + self.observer.join() \ No newline at end of file diff --git a/server/hl7_to_fhir_converter.py b/server/app/utils/hl7_to_fhir_converter.py similarity index 100% rename from server/hl7_to_fhir_converter.py rename to server/app/utils/hl7_to_fhir_converter.py diff --git a/server/app/utils/xml_processor.py b/server/app/utils/xml_processor.py new file mode 100644 index 0000000..f6c3170 --- /dev/null +++ b/server/app/utils/xml_processor.py @@ -0,0 +1,88 @@ +import os +import re +from lxml import etree +from watchdog.observers import Observer +from watchdog.events import FileSystemEventHandler + +class XMLStructureParser: + def __init__(self): + self._resources = {} + + def parse_xml_file(self, filepath): + """Parse an XML file and extract its structure.""" + try: + with open(filepath, 'r') as f: + content = f.read() + + # Extract comments using regex + comments = {} + comment_pattern = r'<!--(.*?)-->' + for match in re.finditer(comment_pattern, content): + comment = match.group(1).strip() + line_number = content[:match.start()].count('\n') + 1 + comments[line_number] = comment + + # Parse XML + parser = etree.XMLParser(remove_blank_text=True) + tree = etree.parse(filepath, parser) + root = tree.getroot() + + # Process the XML structure + structure = self._process_element(root, comments) + + # Store the resource + resource_key = os.path.splitext(os.path.basename(filepath))[0] + self._resources[resource_key] = { + 'filename': os.path.basename(filepath), + 'path': filepath, + 'structure': structure + } + + return self._resources[resource_key] + + except Exception as e: + print(f"Error parsing XML file {filepath}: {str(e)}") + return None + + def _process_element(self, element, comments): + """Process an XML element and its children recursively.""" + result = { + 'tag': element.tag, + 'attributes': dict(element.attrib), + 'comments': [], + 'children': [] + } + + # Get element line number + line_number = element.sourceline if hasattr(element, 'sourceline') else None + + # Add comments associated with this element + if line_number in comments: + result['comments'].append(comments[line_number]) + + # Process text content + if element.text and element.text.strip(): + result['text'] = element.text.strip() + + # Process children + for child in element: + child_result = self._process_element(child, comments) + if child_result: + result['children'].append(child_result) + + return result + +class XMLFileWatcher(FileSystemEventHandler): + def __init__(self, xml_dir, processor): + self.xml_dir = xml_dir + self.processor = processor + + def on_created(self, event): + """Handle file creation events.""" + if not event.is_directory and event.src_path.endswith('.xml'): + self.processor.parse_xml_file(event.src_path) + + def on_modified(self, event): + """Handle file modification events.""" + if not event.is_directory and event.src_path.endswith('.xml'): + self.processor.parse_xml_file(event.src_path) \ No newline at end of file diff --git a/server/config.py b/server/config.py new file mode 100644 index 0000000..43f9f2f --- /dev/null +++ b/server/config.py @@ -0,0 +1,35 @@ +import os +from dotenv import load_dotenv + +# Load environment variables from .env file +load_dotenv() + +class Config: + """Base configuration.""" + SECRET_KEY = os.getenv('SECRET_KEY', 'dev') + DEBUG = False + TESTING = False + XML_FILES_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'xml_files') + RECEIVED_MESSAGES_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'received_messages') + MESSAGE_LOGS_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'message_logs') + +class DevelopmentConfig(Config): + """Development configuration.""" + DEBUG = True + +class TestingConfig(Config): + """Testing configuration.""" + TESTING = True + DEBUG = True + +class ProductionConfig(Config): + """Production configuration.""" + DEBUG = False + +# Configuration dictionary +config = { + 'development': DevelopmentConfig, + 'testing': TestingConfig, + 'production': ProductionConfig, + 'default': DevelopmentConfig +} \ No newline at end of file diff --git a/server/hl7_receiver.py b/server/hl7_receiver.py deleted file mode 100644 index 39d75d2..0000000 --- a/server/hl7_receiver.py +++ /dev/null @@ -1,286 +0,0 @@ -import socket -import datetime -import uuid -import os -import json -from pathlib import Path -import hl7 -import logging -from typing import Dict, Any -from flask import Flask, request, jsonify -from flask_cors import CORS -# import threading # Not needed when TCP server is disabled - -app = Flask(__name__) -CORS(app) - -class HL7MessageReceiver: - def __init__(self, host: str = 'localhost', port: int = 6661): - """Initialize the HL7 message receiver. - - Args: - host (str): Host to listen on - port (int): Port to listen on - """ - self.host = host - self.port = port - self.setup_logging() - self.setup_directories() - - def setup_logging(self): - """Set up logging configuration.""" - logging.basicConfig( - level=logging.INFO, - format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', - handlers=[ - logging.FileHandler('hl7_receiver.log'), - logging.StreamHandler() - ] - ) - self.logger = logging.getLogger('HL7Receiver') - - def setup_directories(self): - """Create necessary directories for message storage.""" - # Create base directories - Path('received_messages').mkdir(exist_ok=True) - Path('message_logs').mkdir(exist_ok=True) - - def generate_filename(self, msg_type: str, timestamp: datetime.datetime) -> str: - """Generate a filename based on message type and timestamp. - - Args: - msg_type (str): The HL7 message type - timestamp (datetime.datetime): Message receipt timestamp - - Returns: - str: Generated filename - """ - date_str = timestamp.strftime('%Y%m%d_%H%M%S') - return f"received_messages/{msg_type}_{date_str}.txt" - - def process_message(self, raw_message: str) -> Dict[str, Any]: - """Process received HL7 message. - - Args: - raw_message (str): Raw HL7 message string - - Returns: - Dict[str, Any]: Processed message info - """ - # Parse the HL7 message - try: - # Remove leading/trailing whitespace and ensure proper line endings - raw_message = '\r'.join(line.strip() for line in raw_message.splitlines()) - if not raw_message.endswith('\r'): - raw_message += '\r' - - self.logger.info(f"Attempting to parse message: {raw_message}") - parsed_msg = hl7.parse(raw_message) - - # Extract message type more safely - try: - # Get MSH segment - msh = parsed_msg.segments('MSH')[0] - - # MSH-9 contains the message type in format "ADT^A01" - message_type_field = str(msh[9]) - - # Split the message type field on '^' to get type and trigger - type_parts = message_type_field.split('^') - message_type = type_parts[0] if len(type_parts) > 0 else "UNKNOWN" - trigger_event = type_parts[1] if len(type_parts) > 1 else "" - - # Combine message type and trigger - msg_type = f"{message_type}_{trigger_event}" if trigger_event else message_type - - self.logger.info(f"Extracted message type: {msg_type} from field: {message_type_field}") - - except Exception as e: - self.logger.error(f"Error extracting message type: {str(e)}") - msg_type = "UNKNOWN" - - # Generate timestamp and ID - timestamp = datetime.datetime.now() - msg_id = str(uuid.uuid4()) - - # Create message info dictionary - msg_info = { - 'id': msg_id, - 'timestamp': timestamp.isoformat(), - 'type': msg_type, - 'raw_message': raw_message, - 'parsed_segments': [str(seg) for seg in parsed_msg] - } - - # Save message to file - filename = self.generate_filename(msg_type, timestamp) - with open(filename, 'w', encoding='utf-8') as f: - f.write(f"Message ID: {msg_id}\n") - f.write(f"Timestamp: {timestamp.isoformat()}\n") - f.write(f"Message Type: {msg_type}\n") - f.write("\nRaw Message:\n") - f.write(raw_message) - f.write("\n\nParsed Segments:\n") - for segment in parsed_msg: - f.write(f"{str(segment)}\n") - - # Log message receipt - self.log_message(msg_info) - - self.logger.info(f"Successfully processed message {msg_id} of type {msg_type}") - return msg_info - - except Exception as e: - self.logger.error(f"Error processing message: {str(e)}") - self.logger.error(f"Raw message was: {raw_message}") - raise ValueError(f"Failed to process HL7 message: {str(e)}") - - def log_message(self, msg_info: Dict[str, Any]): - """Log message information to the message log file. - - Args: - msg_info (Dict[str, Any]): Message information to log - """ - log_file = f"message_logs/messages_{datetime.date.today()}.log" - - with open(log_file, 'a', encoding='utf-8') as f: - json.dump(msg_info, f) - f.write('\n') - - # TCP Server functionality commented out - """ - def start_tcp_server(self): - #Start the HL7 message receiver TCP server. - self.logger.info(f"Starting HL7 TCP receiver on {self.host}:{self.port}") - - server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - server_socket.bind((self.host, self.port)) - server_socket.listen(5) - - try: - while True: - client_socket, address = server_socket.accept() - self.logger.info(f"Connection from {address}") - - try: - # Receive message - message = '' - while True: - data = client_socket.recv(4096).decode('utf-8') - if not data: - break - message += data - if '\r' in data: # HL7 messages end with carriage return - break - - if message: - # Process the message - msg_info = self.process_message(message) - - # Send acknowledgment - ack = self.generate_ack(msg_info) - client_socket.send(ack.encode('utf-8')) - - except Exception as e: - self.logger.error(f"Error handling client connection: {str(e)}") - finally: - client_socket.close() - - except KeyboardInterrupt: - self.logger.info("Shutting down HL7 receiver") - server_socket.close() - except Exception as e: - self.logger.error(f"Server error: {str(e)}") - server_socket.close() - raise - """ - - def generate_ack(self, msg_info: Dict[str, Any]) -> str: - """Generate HL7 acknowledgment message. - - Args: - msg_info (Dict[str, Any]): Information about the received message - - Returns: - str: HL7 acknowledgment message - """ - timestamp = datetime.datetime.now().strftime('%Y%m%d%H%M%S') - return ( - f"MSH|^~\\&|RECEIVER|FACILITY|SENDER|FACILITY|{timestamp}||ACK|{msg_info['id']}|P|2.5.1\r" - f"MSA|AA|{msg_info['id']}|Message received successfully\r" - ) - -# Create a global instance of the receiver -receiver = HL7MessageReceiver() - -@app.route('/api/hl7/receive', methods=['POST']) -def receive_hl7(): - """HTTP endpoint to receive HL7 messages.""" - try: - # Get the raw message from the request - raw_message = request.data.decode('utf-8') - if not raw_message: - raw_message = request.form.get('message', '') - - if not raw_message: - return jsonify({ - 'error': 'No HL7 message provided' - }), 400 - - # Process the message - try: - msg_info = receiver.process_message(raw_message) - - # Generate acknowledgment - ack = receiver.generate_ack(msg_info) - - return jsonify({ - 'status': 'success', - 'message': 'HL7 message received and processed successfully', - 'message_info': msg_info, - 'acknowledgment': ack - }) - - except ValueError as ve: - # Handle validation errors with 400 status - return jsonify({ - 'error': str(ve), - 'status': 'error', - 'message': 'Invalid HL7 message format' - }), 400 - - except Exception as e: - # Handle unexpected errors with 500 status - app.logger.error(f"Unexpected error processing request: {str(e)}") - return jsonify({ - 'error': str(e), - 'status': 'error', - 'message': 'Internal server error processing HL7 message' - }), 500 - -@app.route('/api/hl7/messages', methods=['GET']) -def list_messages(): - """List all received HL7 messages.""" - try: - messages = [] - received_dir = Path('received_messages') - if received_dir.exists(): - for file in received_dir.glob('*.txt'): - with open(file, 'r', encoding='utf-8') as f: - messages.append({ - 'filename': file.name, - 'content': f.read() - }) - - return jsonify({ - 'messages': messages - }) - - except Exception as e: - return jsonify({ - 'error': str(e) - }), 500 - -if __name__ == '__main__': - # Start Flask server - app.run(host='0.0.0.0', port=5000) \ No newline at end of file diff --git a/server/main.py b/server/main.py deleted file mode 100644 index 9a1161d..0000000 --- a/server/main.py +++ /dev/null @@ -1,97 +0,0 @@ -from flask import Flask, jsonify, request -from flask_cors import CORS -import os -from xml_processor import XMLWatcher -from typing import Dict, Any -import threading -import logging - -app = Flask(__name__) -cors = CORS(app, origins='*') - -# Global storage for processed XML structures -xml_resources: Dict[str, Dict[str, Any]] = {} - -def on_xml_processed(data: Dict[str, Any]): - """Callback function for when an XML file is processed.""" - resource_type = data["resourceType"] - filename = data["metadata"]["filename"] - key = f"{resource_type}_{filename}" - xml_resources[key] = data - logging.info(f"Processed XML resource: {key}") - -# Initialize XML watcher -xml_dir = os.path.join(os.path.dirname(__file__), "xml_files") -os.makedirs(xml_dir, exist_ok=True) -watcher = XMLWatcher(xml_dir, on_xml_processed) - -# Start watcher in a separate thread -watcher_thread = threading.Thread(target=watcher.start, daemon=True) -watcher_thread.start() - -@app.route("/api/resources", methods=['GET']) -def list_resources(): - """List all available XML resources.""" - return jsonify({ - "resources": [ - { - "key": key, - "resourceType": data["resourceType"], - "filename": data["metadata"]["filename"] - } - for key, data in xml_resources.items() - ] - }) - -@app.route("/api/resources/<resource_key>", methods=['GET']) -def get_resource(resource_key): - """Get a specific XML resource structure by its key.""" - if resource_key not in xml_resources: - return jsonify({"error": "Resource not found"}), 404 - return jsonify(xml_resources[resource_key]) - -@app.route("/api/resources/<resource_key>/structure", methods=['GET']) -def get_resource_structure(resource_key): - """Get the XML structure for a specific resource.""" - if resource_key not in xml_resources: - return jsonify({"error": "Resource not found"}), 404 - - resource = xml_resources[resource_key] - return jsonify({ - "resourceType": resource["resourceType"], - "structure": resource["structure"] - }) - -@app.route("/api/resources/<resource_key>/schema", methods=['GET']) -def get_resource_schema(resource_key): - """Get the schema structure for a specific resource.""" - if resource_key not in xml_resources: - return jsonify({"error": "Resource not found"}), 404 - - def extract_schema(data: Dict[str, Any]) -> Dict[str, Any]: - if isinstance(data, dict): - return { - key: extract_schema(value) if isinstance(value, (dict, list)) - else type(value).__name__ - for key, value in data.items() - } - elif isinstance(data, list): - return [extract_schema(item) for item in data] if data else ["any"] - return type(data).__name__ - - resource = xml_resources[resource_key] - schema = extract_schema(resource["content"]) - return jsonify({ - "resourceType": resource["resourceType"], - "schema": schema - }) - -@app.route("/api/validate", methods=['POST']) -def validate_resource(): - """Validate a resource against its schema.""" - data = request.json - # TODO: Implement validation logic - return jsonify({"valid": True}) - -if __name__ == "__main__": - app.run(debug=True, port=8080) \ No newline at end of file diff --git a/server/requirements.txt b/server/requirements.txt index 24d4832..610918f 100644 --- a/server/requirements.txt +++ b/server/requirements.txt @@ -1,6 +1,8 @@ -Flask==2.0.1 +Flask==3.0.2 flask-cors==4.0.0 -lxml==4.9.3 +python-dotenv==1.0.1 watchdog==3.0.0 +lxml==5.1.0 +requests==2.31.0 hl7==0.4.2 -Werkzeug==2.0.1 \ No newline at end of file +Werkzeug>=3.0.0 \ No newline at end of file diff --git a/server/run.py b/server/run.py new file mode 100644 index 0000000..6bc1da9 --- /dev/null +++ b/server/run.py @@ -0,0 +1,16 @@ +from app import create_app +from config import config +from app.utils.hl7_receiver import HL7Receiver + +# Create Flask application +app = create_app(config['default']) + +# Initialize HL7 receiver +hl7_receiver = HL7Receiver() +hl7_receiver.start() + +if __name__ == '__main__': + try: + app.run(debug=True) + except KeyboardInterrupt: + hl7_receiver.stop() \ No newline at end of file diff --git a/server/xml_processor.py b/server/xml_processor.py deleted file mode 100644 index a6824aa..0000000 --- a/server/xml_processor.py +++ /dev/null @@ -1,219 +0,0 @@ -from watchdog.observers import Observer -from watchdog.events import FileSystemEventHandler -from lxml import etree -import os -import json -from typing import Dict, Any, Optional, List -import logging -import re - -logging.basicConfig(level=logging.INFO) -logger = logging.getLogger(__name__) - -class XMLStructureParser: - """Parses XML files preserving structure, comments, and placeholders.""" - - @staticmethod - def preprocess_fhir_template(content: str) -> str: - """Preprocess FHIR template XML to make it valid XML.""" - # Replace FHIR template patterns like [x] with valid XML names - content = re.sub(r'<(\w+)\[(x|X)\]>', r'<\1_choice>', content) - content = re.sub(r'</(\w+)\[(x|X)\]>', r'</\1_choice>', content) - return content - - @staticmethod - def restore_fhir_template(tag: str) -> str: - """Restore original FHIR template tag names.""" - if tag.endswith('_choice'): - base_name = tag[:-7] - return f"{base_name}[x]" - return tag - - @staticmethod - def extract_comment_info(comment: str) -> Dict[str, str]: - """Extract cardinality and description from a comment.""" - # Extract cardinality - cardinality_match = re.search(r'(0|1)\.\.(\*|0|1)', comment) - cardinality = cardinality_match.group(0) if cardinality_match else '' - - # Remove cardinality and any type info (content between | characters) - desc = re.sub(r'(0|1)\.\.(\*|0|1)\s*', '', comment) - desc = re.sub(r'\|.*?\|', '', desc) - desc = re.sub(r'\[.*?\]', '', desc) - - # Clean up the description - desc = desc.strip() - if desc.startswith('I '): # Remove 'I ' prefix some comments have - desc = desc[2:] - - return { - 'cardinality': cardinality, - 'description': desc - } - - @staticmethod - def parse_xml_file(file_path: str) -> Dict[str, Any]: - """Parse an XML file and return its structure with comments and placeholders.""" - try: - # Read the file content - with open(file_path, 'r', encoding='utf-8') as f: - content = f.read() - - # Store original content for comment positions - original_content = content - - # Preprocess content to make it valid XML - content = XMLStructureParser.preprocess_fhir_template(content) - - # Parse comments first (since they'll be removed by the XML parser) - comments_by_tag = {} - for line in original_content.split('\n'): - if '<!--' in line: - # Get the tag name from the line (if it exists) - tag_match = re.search(r'<(\w+)[\s>]', line) - comment_match = re.search(r'<!--(.*?)-->', line) - - if tag_match and comment_match: - tag = tag_match.group(1) - comment = comment_match.group(1).strip() - - # Store comment if it has cardinality - if re.search(r'(0|1)\.\.(\*|0|1)', comment): - comment_info = XMLStructureParser.extract_comment_info(comment) - comments_by_tag[tag] = comment_info - - # Parse the XML structure - parser = etree.XMLParser(remove_comments=False, remove_blank_text=True) - tree = etree.fromstring(content.encode(), parser) - - def process_element(element) -> Dict[str, Any]: - """Process an XML element and its content.""" - # Skip elements with no tag - if not isinstance(element.tag, str): - return None - - # Get tag name and handle namespaces - tag = element.tag.split('}')[-1] - tag = XMLStructureParser.restore_fhir_template(tag) - - # Get element's text content and check for placeholders - text = element.text.strip() if element.text else "" - placeholder = None - if text: - placeholder_match = re.search(r'\[(.*?)\]', text) - if placeholder_match: - placeholder = placeholder_match.group(0) - - result = { - 'tag': tag, - 'attributes': dict(element.attrib) - } - - # Add comment info if it exists for this tag - if tag in comments_by_tag: - result['comments'] = [comments_by_tag[tag]['cardinality']] - result['description'] = comments_by_tag[tag]['description'] - - if placeholder: - result['placeholder'] = placeholder - - children = [] - for child in element: - child_result = process_element(child) - if child_result: - children.append(child_result) - - if children: - result['children'] = children - - # Only include elements that have: - # 1. A value attribute, or - # 2. A cardinality comment, or - # 3. Children - if ('value' in result['attributes'] or - 'comments' in result or - children): - return result - return None - - # Process the entire tree - structure = process_element(tree) - - return { - 'resourceType': XMLStructureParser.restore_fhir_template( - tree.tag.split('}')[-1] if isinstance(tree.tag, str) else "unknown" - ), - 'structure': structure, - 'metadata': { - 'filename': os.path.basename(file_path), - 'filepath': file_path, - 'namespaces': tree.nsmap if hasattr(tree, 'nsmap') else {} - } - } - - except Exception as e: - logger.error(f"Error parsing XML file {file_path}: {str(e)}") - raise - -class XMLFileHandler(FileSystemEventHandler): - """Handles file system events for XML files.""" - - def __init__(self, watch_directory: str, processed_callback=None): - self.watch_directory = watch_directory - self.processed_callback = processed_callback - self._processed_files = set() - - def on_created(self, event): - if not event.is_directory and event.src_path.endswith('.xml'): - self.process_file(event.src_path) - - def on_modified(self, event): - if not event.is_directory and event.src_path.endswith('.xml'): - self._processed_files.discard(event.src_path) # Allow reprocessing of modified files - self.process_file(event.src_path) - - def process_file(self, file_path: str) -> Optional[Dict[str, Any]]: - """Process an XML file and return its structure.""" - try: - if file_path in self._processed_files: - return None - - logger.info(f"Processing XML file: {file_path}") - - result = XMLStructureParser.parse_xml_file(file_path) - self._processed_files.add(file_path) - - if self.processed_callback: - self.processed_callback(result) - - return result - - except Exception as e: - logger.error(f"Error processing file {file_path}: {str(e)}") - return None - -class XMLWatcher: - """Manages the file system observer for XML files.""" - - def __init__(self, directory: str, processed_callback=None): - self.directory = directory - self.event_handler = XMLFileHandler(directory, processed_callback) - self.observer = Observer() - - def start(self): - """Start watching the directory for XML files.""" - self.observer.schedule(self.event_handler, self.directory, recursive=False) - self.observer.start() - logger.info(f"Started watching directory: {self.directory}") - - # Process existing files - for filename in os.listdir(self.directory): - if filename.endswith('.xml'): - file_path = os.path.join(self.directory, filename) - self.event_handler.process_file(file_path) - - def stop(self): - """Stop watching the directory.""" - self.observer.stop() - self.observer.join() - logger.info("Stopped watching directory") \ No newline at end of file -- GitLab