Files
tkb_timeshift/claude_n8n/tools/mock_api_server.py
Docker Config Backup 8793ac4f59 Add Claude N8N toolkit with Docker mock API server
- Added comprehensive N8N development tools collection
- Added Docker-containerized mock API server for testing
- Added complete documentation and setup guides
- Added mock API server with health checks and data endpoints
- Tools include workflow analyzers, debuggers, and controllers

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-06-17 21:23:46 +02:00

439 lines
16 KiB
Python

"""
Mock API Server for N8N Testing
This module provides a REST API server that serves data from text files.
N8N workflows can call this API to get consistent test data stored in files.
"""
import json
import logging
import os
import random
import threading
import time
from datetime import datetime
from pathlib import Path
from typing import Dict, List, Any, Optional, Union
from flask import Flask, jsonify, request, Response
from werkzeug.serving import make_server
logger = logging.getLogger(__name__)
class MockAPIServer:
"""REST API server that serves data from text files."""
def __init__(self, data_dir: str = "api_data", host: str = "0.0.0.0", port: int = 5000):
"""
Initialize the mock API server.
Args:
data_dir: Directory containing data files
host: Host to bind the server to
port: Port to run the server on
"""
self.data_dir = Path(data_dir)
self.data_dir.mkdir(exist_ok=True)
self.host = host
self.port = port
self.app = Flask(__name__)
self.server = None
self.server_thread = None
self.is_running = False
# Setup routes
self._setup_routes()
# Create example data file if it doesn't exist
self._create_example_data()
def _setup_routes(self):
"""Setup API routes."""
@self.app.route('/health', methods=['GET'])
def health_check():
"""Health check endpoint."""
return jsonify({
'status': 'healthy',
'timestamp': datetime.now().isoformat(),
'data_dir': str(self.data_dir),
'available_endpoints': self._get_available_endpoints()
})
@self.app.route('/data/<path:filename>', methods=['GET'])
def get_data(filename):
"""Get data from a specific file."""
return self._serve_data_file(filename)
@self.app.route('/data', methods=['GET'])
def list_data_files():
"""List available data files."""
files = []
for file_path in self.data_dir.glob('*.json'):
files.append({
'name': file_path.stem,
'filename': file_path.name,
'size': file_path.stat().st_size,
'modified': datetime.fromtimestamp(file_path.stat().st_mtime).isoformat(),
'url': f"http://{self.host}:{self.port}/data/{file_path.name}"
})
return jsonify({
'files': files,
'count': len(files)
})
@self.app.route('/matrix', methods=['GET'])
def get_matrix_data():
"""Get Matrix chat data (example endpoint)."""
return self._serve_data_file('matrix_messages.json')
@self.app.route('/random/<path:filename>', methods=['GET'])
def get_random_data(filename):
"""Get random item from data file."""
data = self._load_data_file(filename)
if not data:
return jsonify({'error': 'File not found or empty'}), 404
if isinstance(data, list) and data:
return jsonify(random.choice(data))
else:
return jsonify(data)
@self.app.route('/paginated/<path:filename>', methods=['GET'])
def get_paginated_data(filename):
"""Get paginated data from file."""
page = int(request.args.get('page', 1))
per_page = int(request.args.get('per_page', 10))
data = self._load_data_file(filename)
if not data:
return jsonify({'error': 'File not found or empty'}), 404
if isinstance(data, list):
start_idx = (page - 1) * per_page
end_idx = start_idx + per_page
paginated_data = data[start_idx:end_idx]
return jsonify({
'data': paginated_data,
'page': page,
'per_page': per_page,
'total': len(data),
'total_pages': (len(data) + per_page - 1) // per_page
})
else:
return jsonify({
'data': data,
'page': 1,
'per_page': 1,
'total': 1,
'total_pages': 1
})
@self.app.route('/upload', methods=['POST'])
def upload_data():
"""Upload new data to a file."""
try:
filename = request.args.get('filename')
if not filename:
return jsonify({'error': 'filename parameter required'}), 400
data = request.get_json()
if not data:
return jsonify({'error': 'JSON data required'}), 400
filepath = self.data_dir / f"{filename}.json"
with open(filepath, 'w') as f:
json.dump(data, f, indent=2)
return jsonify({
'message': f'Data uploaded successfully to {filename}.json',
'filename': f"{filename}.json",
'size': filepath.stat().st_size
})
except Exception as e:
return jsonify({'error': str(e)}), 500
@self.app.errorhandler(404)
def not_found(error):
return jsonify({'error': 'Endpoint not found'}), 404
@self.app.errorhandler(500)
def internal_error(error):
return jsonify({'error': 'Internal server error'}), 500
def _serve_data_file(self, filename: str) -> Response:
"""Serve data from a specific file."""
# Add .json extension if not present
if not filename.endswith('.json'):
filename += '.json'
data = self._load_data_file(filename)
if data is None:
return jsonify({'error': f'File {filename} not found'}), 404
# Add some variation to timestamps if present
varied_data = self._add_timestamp_variation(data)
return jsonify(varied_data)
def _load_data_file(self, filename: str) -> Optional[Union[Dict, List]]:
"""Load data from a JSON file."""
filepath = self.data_dir / filename
if not filepath.exists():
logger.warning(f"Data file not found: {filepath}")
return None
try:
with open(filepath, 'r', encoding='utf-8') as f:
return json.load(f)
except Exception as e:
logger.error(f"Error loading data file {filepath}: {e}")
return None
def _add_timestamp_variation(self, data: Union[Dict, List]) -> Union[Dict, List]:
"""Add slight variations to timestamps to simulate real data."""
if isinstance(data, dict):
return self._vary_dict_timestamps(data)
elif isinstance(data, list):
return [self._vary_dict_timestamps(item) if isinstance(item, dict) else item for item in data]
else:
return data
def _vary_dict_timestamps(self, data: Dict) -> Dict:
"""Add variation to timestamps in a dictionary."""
varied_data = data.copy()
# Common timestamp fields to vary
timestamp_fields = ['timestamp', 'origin_server_ts', 'created_at', 'updated_at', 'time', 'date']
for key, value in varied_data.items():
if key in timestamp_fields and isinstance(value, (int, float)):
# Add random variation of ±5 minutes for timestamp fields
variation = random.randint(-300000, 300000) # ±5 minutes in milliseconds
varied_data[key] = value + variation
elif key == 'age' and isinstance(value, (int, float)):
# Add small random variation to age fields
variation = random.randint(-1000, 1000)
varied_data[key] = max(0, value + variation)
elif isinstance(value, dict):
varied_data[key] = self._vary_dict_timestamps(value)
elif isinstance(value, list):
varied_data[key] = [
self._vary_dict_timestamps(item) if isinstance(item, dict) else item
for item in value
]
return varied_data
def _get_available_endpoints(self) -> List[str]:
"""Get list of available API endpoints."""
endpoints = [
'/health',
'/data',
'/data/<filename>',
'/random/<filename>',
'/paginated/<filename>',
'/upload',
'/matrix'
]
# Add endpoints for each data file
for file_path in self.data_dir.glob('*.json'):
endpoints.append(f'/data/{file_path.name}')
return endpoints
def _create_example_data(self):
"""Create example data files if they don't exist."""
# Create matrix messages example
matrix_file = self.data_dir / 'matrix_messages.json'
if not matrix_file.exists():
matrix_example = [
{
"chunk": [
{
"type": "m.room.message",
"room_id": "!xZkScMybPseErYMJDz:matrix.klas.chat",
"sender": "@signal_37c02a2c-31a2-4937-88f2-3f6be48afcdc:matrix.klas.chat",
"content": {
"body": "Viděli jsme dopravní nehodu",
"m.mentions": {},
"msgtype": "m.text"
},
"origin_server_ts": 1749927752871,
"unsigned": {
"membership": "join",
"age": 668
},
"event_id": "$k2t8Uj8K9tdtXfuyvnxezL-Gijqb0Bw4rucgZ0rEOgA",
"user_id": "@signal_37c02a2c-31a2-4937-88f2-3f6be48afcdc:matrix.klas.chat",
"age": 668
},
{
"type": "m.room.message",
"room_id": "!xZkScMybPseErYMJDz:matrix.klas.chat",
"sender": "@signal_961d5a74-062f-4f22-88bd-e192a5e7d567:matrix.klas.chat",
"content": {
"body": "BTC fee je: 1 sat/vByte",
"m.mentions": {},
"msgtype": "m.text"
},
"origin_server_ts": 1749905152683,
"unsigned": {
"membership": "join",
"age": 22623802
},
"event_id": "$WYbz0dB8f16PxL9_j0seJbO1tFaSGiiWeDaj8yGEfC8",
"user_id": "@signal_961d5a74-062f-4f22-88bd-e192a5e7d567:matrix.klas.chat",
"age": 22623802
}
],
"start": "t404-16991_0_0_0_0_0_0_0_0_0",
"end": "t395-16926_0_0_0_0_0_0_0_0_0"
}
]
with open(matrix_file, 'w', encoding='utf-8') as f:
json.dump(matrix_example, f, indent=2, ensure_ascii=False)
logger.info(f"Created example matrix messages file: {matrix_file}")
# Create simple test data example
test_file = self.data_dir / 'test_data.json'
if not test_file.exists():
test_data = {
"message": "Hello from mock API",
"timestamp": int(time.time() * 1000),
"items": [
{"id": 1, "name": "Item 1"},
{"id": 2, "name": "Item 2"},
{"id": 3, "name": "Item 3"}
]
}
with open(test_file, 'w') as f:
json.dump(test_data, f, indent=2)
logger.info(f"Created example test data file: {test_file}")
def start(self, debug: bool = False, threaded: bool = True) -> bool:
"""
Start the API server.
Args:
debug: Enable debug mode
threaded: Run in a separate thread
Returns:
True if server started successfully
"""
try:
if self.is_running:
logger.warning("Server is already running")
return False
self.server = make_server(self.host, self.port, self.app, threaded=True)
if threaded:
self.server_thread = threading.Thread(target=self.server.serve_forever, daemon=True)
self.server_thread.start()
else:
self.server.serve_forever()
self.is_running = True
logger.info(f"Mock API server started on http://{self.host}:{self.port}")
logger.info(f"Health check: http://{self.host}:{self.port}/health")
logger.info(f"Data files: http://{self.host}:{self.port}/data")
return True
except Exception as e:
logger.error(f"Failed to start server: {e}")
return False
def stop(self):
"""Stop the API server."""
if self.server and self.is_running:
self.server.shutdown()
if self.server_thread:
self.server_thread.join(timeout=5)
self.is_running = False
logger.info("Mock API server stopped")
else:
logger.warning("Server is not running")
def add_data_file(self, filename: str, data: Union[Dict, List]) -> str:
"""
Add a new data file.
Args:
filename: Name of the file (without .json extension)
data: Data to store
Returns:
Path to the created file
"""
if not filename.endswith('.json'):
filename += '.json'
filepath = self.data_dir / filename
with open(filepath, 'w', encoding='utf-8') as f:
json.dump(data, f, indent=2, ensure_ascii=False)
logger.info(f"Added data file: {filepath}")
return str(filepath)
def get_server_info(self) -> Dict[str, Any]:
"""Get information about the server."""
return {
'host': self.host,
'port': self.port,
'is_running': self.is_running,
'data_dir': str(self.data_dir),
'base_url': f"http://{self.host}:{self.port}",
'health_url': f"http://{self.host}:{self.port}/health",
'data_files_count': len(list(self.data_dir.glob('*.json'))),
'available_endpoints': self._get_available_endpoints()
}
def create_mock_api_server(data_dir: str = "api_data", host: str = "0.0.0.0", port: int = 5000):
"""Create a mock API server instance."""
return MockAPIServer(data_dir, host, port)
# CLI functionality for standalone usage
if __name__ == "__main__":
import argparse
parser = argparse.ArgumentParser(description="Mock API Server for N8N Testing")
parser.add_argument("--host", default="0.0.0.0", help="Host to bind to")
parser.add_argument("--port", type=int, default=5000, help="Port to bind to")
parser.add_argument("--data-dir", default="api_data", help="Directory for data files")
parser.add_argument("--debug", action="store_true", help="Enable debug mode")
args = parser.parse_args()
# Setup logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
# Create and start server
server = MockAPIServer(args.data_dir, args.host, args.port)
try:
print(f"Starting Mock API Server on http://{args.host}:{args.port}")
print(f"Data directory: {args.data_dir}")
print("Press Ctrl+C to stop")
server.start(debug=args.debug, threaded=False)
except KeyboardInterrupt:
print("\nShutting down server...")
server.stop()
print("Server stopped")