- Replace eval() with json.loads() in database.py (RCE fix) - Use json.dumps() for safe storage of list fields - Add API key authentication middleware - Remove hardcoded credentials, require env vars - Disable Flask debug mode - Restrict FTP homedir to /app/data with read-only perms - Fix threading: Lock -> RLock, add WAL mode - Fix API calls to use correct DatabaseManager methods - Fix main.py FTP method names - Fix click.click.echo typo - Implement scheduler _run_all_jobs - Add __main__.py for module execution - Pin dependency versions - Use .env vars in docker-compose, read-only DB for FTP - Implement AI text chunking with overlap windows - Add schema validation for AI responses - Skip unsupported file types instead of fallback
210 lines
6.9 KiB
Python
210 lines
6.9 KiB
Python
"""
|
|
REST API for FactsDB service
|
|
Provides endpoints for querying facts and managing the system
|
|
"""
|
|
|
|
from flask import Flask, jsonify, request, Response
|
|
from functools import wraps
|
|
from typing import Dict, Any, List
|
|
import json
|
|
import os
|
|
from .config import Config
|
|
from .database import DatabaseManager
|
|
from .monitoring import get_metrics, get_metrics_json, start_uptime_monitor, increment_fact_extraction, increment_file_processing, increment_error
|
|
|
|
# Start uptime monitoring
|
|
uptime_thread = start_uptime_monitor()
|
|
|
|
def require_api_key(f):
|
|
"""Decorator to require API key authentication"""
|
|
@wraps(f)
|
|
def decorated_function(*args, **kwargs):
|
|
api_key = os.getenv('API_KEY', '')
|
|
if api_key:
|
|
provided_key = request.headers.get('X-API-Key', '')
|
|
if provided_key != api_key:
|
|
return jsonify({'error': 'Unauthorized'}), 401
|
|
return f(*args, **kwargs)
|
|
return decorated_function
|
|
|
|
def create_app() -> Flask:
|
|
"""Create and configure the Flask application"""
|
|
app = Flask(__name__)
|
|
config = Config()
|
|
db_manager = DatabaseManager(config.database)
|
|
|
|
@app.route('/health', methods=['GET'])
|
|
def health():
|
|
"""Health check endpoint"""
|
|
return jsonify({
|
|
'status': 'healthy',
|
|
'service': 'FactsDB'
|
|
})
|
|
|
|
@app.route('/tables', methods=['GET'])
|
|
@require_api_key
|
|
def get_tables():
|
|
"""Get all available tables with record counts"""
|
|
try:
|
|
tables = db_manager.get_all_tables()
|
|
return jsonify({
|
|
'tables': tables,
|
|
'total_tables': len(tables)
|
|
})
|
|
except Exception as e:
|
|
increment_error()
|
|
return jsonify({'error': str(e)}), 500
|
|
|
|
@app.route('/tables/<table_name>', methods=['GET'])
|
|
@require_api_key
|
|
def get_table_data(table_name: str):
|
|
"""Get all facts from a specific table"""
|
|
try:
|
|
facts = db_manager.query_table(table_name)
|
|
return jsonify({
|
|
'table_name': table_name,
|
|
'facts': facts,
|
|
'count': len(facts)
|
|
})
|
|
except Exception as e:
|
|
increment_error()
|
|
return jsonify({'error': str(e)}), 500
|
|
|
|
@app.route('/tables/<table_name>/query', methods=['POST'])
|
|
@require_api_key
|
|
def query_table(table_name: str):
|
|
"""Query facts from a specific table with custom query"""
|
|
try:
|
|
data = request.get_json()
|
|
query = data.get('query', '') if data else ''
|
|
|
|
facts = db_manager.query_table(table_name, query)
|
|
return jsonify({
|
|
'table_name': table_name,
|
|
'facts': facts,
|
|
'count': len(facts)
|
|
})
|
|
except Exception as e:
|
|
increment_error()
|
|
return jsonify({'error': str(e)}), 500
|
|
|
|
@app.route('/tables/<table_name>/count', methods=['GET'])
|
|
@require_api_key
|
|
def get_table_count(table_name: str):
|
|
"""Get record count for a specific table"""
|
|
try:
|
|
count = db_manager.get_table_record_count(table_name)
|
|
return jsonify({
|
|
'table_name': table_name,
|
|
'count': count
|
|
})
|
|
except Exception as e:
|
|
increment_error()
|
|
return jsonify({'error': str(e)}), 500
|
|
|
|
@app.route('/fact/<int:fact_id>', methods=['GET'])
|
|
@require_api_key
|
|
def get_fact(fact_id: int):
|
|
"""Get a specific fact by ID"""
|
|
try:
|
|
with db_manager.get_connection() as conn:
|
|
cursor = conn.execute('''
|
|
SELECT * FROM facts WHERE id = ?
|
|
''', (fact_id,))
|
|
fact = cursor.fetchone()
|
|
|
|
if fact:
|
|
return jsonify(dict(fact))
|
|
else:
|
|
return jsonify({'error': 'Fact not found'}), 404
|
|
except Exception as e:
|
|
increment_error()
|
|
return jsonify({'error': str(e)}), 500
|
|
|
|
@app.route('/search', methods=['GET'])
|
|
@require_api_key
|
|
def search():
|
|
"""Search across all tables"""
|
|
try:
|
|
query = request.args.get('q', '')
|
|
if not query:
|
|
return jsonify({'error': 'Search query required'}), 400
|
|
|
|
# Simple search implementation - in a real system this would be more sophisticated
|
|
results = []
|
|
|
|
# Get all tables
|
|
tables = db_manager.get_all_tables()
|
|
for table in tables:
|
|
table_name = table['table_name']
|
|
facts = db_manager.query_table(table_name)
|
|
for fact in facts:
|
|
# Search in key fields
|
|
search_content = f"{fact.get('title', '')} {fact.get('summary', '')} {fact.get('main_topic', '')}"
|
|
if query.lower() in search_content.lower():
|
|
results.append({
|
|
'table_name': table_name,
|
|
'fact': fact
|
|
})
|
|
|
|
return jsonify({
|
|
'query': query,
|
|
'results': results,
|
|
'count': len(results)
|
|
})
|
|
except Exception as e:
|
|
increment_error()
|
|
return jsonify({'error': str(e)}), 500
|
|
|
|
@app.route('/version', methods=['GET'])
|
|
def version():
|
|
"""Get service version"""
|
|
return jsonify({
|
|
'version': '1.0.0',
|
|
'service': 'FactsDB'
|
|
})
|
|
|
|
@app.route('/metrics', methods=['GET'])
|
|
def metrics():
|
|
"""Prometheus metrics endpoint"""
|
|
try:
|
|
metrics_text = get_metrics()
|
|
return Response(metrics_text, mimetype='text/plain')
|
|
except Exception as e:
|
|
increment_error()
|
|
return jsonify({'error': str(e)}), 500
|
|
|
|
@app.route('/metrics/json', methods=['GET'])
|
|
def metrics_json():
|
|
"""JSON metrics endpoint"""
|
|
try:
|
|
metrics_data = get_metrics_json()
|
|
return jsonify(metrics_data)
|
|
except Exception as e:
|
|
increment_error()
|
|
return jsonify({'error': str(e)}), 500
|
|
|
|
@app.route('/stats', methods=['GET'])
|
|
@require_api_key
|
|
def stats():
|
|
"""Get detailed service statistics"""
|
|
try:
|
|
db_stats = db_manager.get_database_stats()
|
|
metrics_data = get_metrics_json()
|
|
|
|
return jsonify({
|
|
'database_stats': db_stats,
|
|
'metrics': metrics_data['metrics'],
|
|
'timestamp': metrics_data['timestamp']
|
|
})
|
|
except Exception as e:
|
|
increment_error()
|
|
return jsonify({'error': str(e)}), 500
|
|
|
|
return app
|
|
|
|
# Create a default app instance
|
|
app = create_app()
|
|
|
|
if __name__ == '__main__':
|
|
app.run(debug=False, host='0.0.0.0', port=5000) |