First commit

This commit is contained in:
2025-12-19 12:58:58 +01:00
parent cba66667f3
commit 87865e2c6d
26 changed files with 3343 additions and 0 deletions
View File
+108
View File
@@ -0,0 +1,108 @@
"""
Async Worker Module
Provides a persistent async worker thread for executing coroutines.
Solves "Event loop is closed" errors by maintaining a single event loop.
"""
import asyncio
import threading
from typing import Any, Optional
from concurrent.futures import Future
import logging
class AsyncWorker:
"""
Persistent async worker thread for executing coroutines.
This worker maintains a single event loop for the application's lifetime,
solving the "Event loop is closed" error by ensuring all async operations
use the same loop and credentials remain valid.
"""
def __init__(self):
self.loop: Optional[asyncio.AbstractEventLoop] = None
self.thread: Optional[threading.Thread] = None
self.running = False
self.logger = logging.getLogger(__name__)
def start(self):
"""Start the async worker thread."""
if self.running:
return
self.running = True
self.thread = threading.Thread(
target=self._run_loop,
daemon=True,
name="AsyncWorker"
)
self.thread.start()
# Wait for loop to be ready
import time
while self.loop is None:
time.sleep(0.01)
def _run_loop(self):
"""Run the event loop in the worker thread."""
self.loop = asyncio.new_event_loop()
asyncio.set_event_loop(self.loop)
self.logger.info("AsyncWorker event loop started")
try:
self.loop.run_forever()
finally:
self.loop.close()
self.logger.info("AsyncWorker event loop closed")
def submit(self, coro) -> Future:
"""
Submit a coroutine to be executed in the worker loop.
Args:
coro: Coroutine to execute
Returns:
Future that will contain the result
"""
if not self.running:
raise RuntimeError("AsyncWorker not started")
result_future = Future()
def callback():
"""Execute coroutine and set result in future."""
try:
task = asyncio.ensure_future(coro, loop=self.loop)
def done_callback(task_future):
try:
result = task_future.result()
result_future.set_result(result)
except Exception as e:
result_future.set_exception(e)
task.add_done_callback(done_callback)
except Exception as e:
result_future.set_exception(e)
self.loop.call_soon_threadsafe(callback)
return result_future
def stop(self):
"""Stop the async worker thread."""
if not self.running:
return
self.running = False
if self.loop:
self.loop.call_soon_threadsafe(self.loop.stop)
if self.thread:
self.thread.join(timeout=5.0)
self.logger.info("AsyncWorker stopped")
+69
View File
@@ -0,0 +1,69 @@
"""
Logging Utility
Configures logging for the application.
"""
import logging
import os
from datetime import datetime
def setup_logger(name: str = 'AzureKeyVaultManager', log_file: str = None, level=logging.INFO):
"""
Set up and configure a logger.
Args:
name: Logger name
log_file: Optional log file path (defaults to logs/app_{date}.log)
level: Logging level (default: INFO)
Returns:
logging.Logger: Configured logger instance
"""
logger = logging.getLogger(name)
logger.setLevel(level)
# Remove existing handlers
logger.handlers = []
# Create formatter
formatter = logging.Formatter(
'%(asctime)s - %(name)s - %(levelname)s - %(message)s',
datefmt='%Y-%m-%d %H:%M:%S'
)
# Console handler
console_handler = logging.StreamHandler()
console_handler.setLevel(level)
console_handler.setFormatter(formatter)
logger.addHandler(console_handler)
# File handler (optional)
if log_file:
# Create logs directory if it doesn't exist
log_dir = os.path.dirname(log_file)
if log_dir and not os.path.exists(log_dir):
os.makedirs(log_dir)
file_handler = logging.FileHandler(log_file)
file_handler.setLevel(level)
file_handler.setFormatter(formatter)
logger.addHandler(file_handler)
else:
# Default log file in logs directory
log_dir = 'logs'
if not os.path.exists(log_dir):
os.makedirs(log_dir)
log_file = os.path.join(log_dir, f'app_{datetime.now().strftime("%Y%m%d")}.log')
file_handler = logging.FileHandler(log_file)
file_handler.setLevel(level)
file_handler.setFormatter(formatter)
logger.addHandler(file_handler)
return logger
# Create default logger instance
logger = setup_logger()
+37
View File
@@ -0,0 +1,37 @@
"""
Name Sanitization Utility
Sanitizes names for use in Azure Key Vault.
"""
import re
def sanitize_name(name: str) -> str:
"""
Sanitize a name for use in Azure Key Vault.
Key Vault secret names can only contain alphanumeric characters and hyphens.
Args:
name: The name to sanitize
Returns:
str: Sanitized name
Example:
>>> sanitize_name("My App (Production) #2024")
'My-App-Production-2024'
"""
if not name:
return name
# Replace any non-alphanumeric character (except hyphens) with hyphen
sanitized = re.sub(r'[^0-9a-zA-Z-]', '-', name)
# Remove consecutive hyphens
sanitized = re.sub(r'-+', '-', sanitized)
# Remove leading/trailing hyphens
sanitized = sanitized.strip('-')
return sanitized