353 lines
9.0 KiB
Python
Raw Normal View History

2025-11-29 00:50:53 +01:00
"""
Tikker API with C Tools Integration
FastAPI endpoints that call C tools for statistics and report generation.
Maintains 100% backwards compatibility with original API interface.
"""
from fastapi import FastAPI, HTTPException, BackgroundTasks, Query
from fastapi.responses import FileResponse, HTMLResponse
from pydantic import BaseModel
from typing import List, Dict, Any, Optional
import logging
import os
from pathlib import Path
from c_tools_wrapper import CToolsWrapper, ToolError
# Configure logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
# Initialize FastAPI app
app = FastAPI(
title="Tikker API",
description="Enterprise keystroke analytics API with C backend",
version="2.0.0"
)
# Initialize C tools wrapper
try:
tools = CToolsWrapper(
tools_dir=os.getenv("TOOLS_DIR", "./build/bin"),
db_path=os.getenv("DB_PATH", "tikker.db")
)
except Exception as e:
logger.error(f"Failed to initialize C tools: {e}")
tools = None
# Pydantic models
class DailyStats(BaseModel):
presses: int
releases: int
repeats: int
total: int
class WordStat(BaseModel):
rank: int
word: str
count: int
percentage: float
class DecoderRequest(BaseModel):
input_file: str
output_file: str
verbose: bool = False
class ReportRequest(BaseModel):
output_file: str = "report.html"
input_dir: str = "logs_plain"
title: str = "Tikker Activity Report"
# Health check endpoint
@app.get("/health")
async def health_check() -> Dict[str, Any]:
"""
Check API and C tools health status.
Returns:
Health status and tool information
"""
if not tools:
raise HTTPException(status_code=503, detail="C tools not initialized")
return tools.health_check()
# Statistics endpoints
@app.get("/api/stats/daily", response_model=DailyStats)
async def get_daily_stats() -> DailyStats:
"""
Get daily keystroke statistics.
Returns:
Daily statistics (presses, releases, repeats, total)
"""
if not tools:
raise HTTPException(status_code=503, detail="C tools not available")
try:
stats = tools.get_daily_stats()
return DailyStats(**stats)
except ToolError as e:
raise HTTPException(status_code=500, detail=str(e))
@app.get("/api/stats/hourly")
async def get_hourly_stats(date: str = Query(..., description="Date in YYYY-MM-DD format")) -> Dict[str, Any]:
"""
Get hourly keystroke statistics for a specific date.
Args:
date: Date in YYYY-MM-DD format
Returns:
Hourly statistics
"""
if not tools:
raise HTTPException(status_code=503, detail="C tools not available")
try:
return tools.get_hourly_stats(date)
except ToolError as e:
raise HTTPException(status_code=500, detail=str(e))
@app.get("/api/stats/weekly")
async def get_weekly_stats() -> Dict[str, Any]:
"""
Get weekly keystroke statistics.
Returns:
Weekly statistics breakdown
"""
if not tools:
raise HTTPException(status_code=503, detail="C tools not available")
try:
return tools.get_weekly_stats()
except ToolError as e:
raise HTTPException(status_code=500, detail=str(e))
@app.get("/api/stats/weekday")
async def get_weekday_stats() -> Dict[str, Any]:
"""
Get weekday comparison statistics.
Returns:
Statistics grouped by day of week
"""
if not tools:
raise HTTPException(status_code=503, detail="C tools not available")
try:
return tools.get_weekday_stats()
except ToolError as e:
raise HTTPException(status_code=500, detail=str(e))
# Word analysis endpoints
@app.get("/api/words/top", response_model=List[WordStat])
async def get_top_words(limit: int = Query(10, ge=1, le=100, description="Number of words to return")) -> List[WordStat]:
"""
Get top N most popular words.
Args:
limit: Number of words to return (1-100)
Returns:
List of words with frequency and rank
"""
if not tools:
raise HTTPException(status_code=503, detail="C tools not available")
try:
words = tools.get_top_words(limit)
return [WordStat(**w) for w in words]
except ToolError as e:
raise HTTPException(status_code=500, detail=str(e))
@app.get("/api/words/find")
async def find_word(word: str = Query(..., description="Word to search for")) -> Dict[str, Any]:
"""
Find statistics for a specific word.
Args:
word: Word to search for
Returns:
Word frequency, rank, and statistics
"""
if not tools:
raise HTTPException(status_code=503, detail="C tools not available")
try:
return tools.find_word(word)
except ToolError as e:
raise HTTPException(status_code=500, detail=str(e))
# Indexing endpoints
@app.post("/api/index")
async def build_index(dir_path: str = Query("logs_plain", description="Directory to index")) -> Dict[str, Any]:
"""
Build word index from text files.
Args:
dir_path: Directory containing text files
Returns:
Indexing results and statistics
"""
if not tools:
raise HTTPException(status_code=503, detail="C tools not available")
try:
return tools.index_directory(dir_path)
except ToolError as e:
raise HTTPException(status_code=500, detail=str(e))
# Decoding endpoints
@app.post("/api/decode")
async def decode_file(request: DecoderRequest, background_tasks: BackgroundTasks) -> Dict[str, Any]:
"""
Decode keystroke token file to readable text.
Args:
request: Decoder request with input/output paths
background_tasks: Background task runner
Returns:
Decoding result
"""
if not tools:
raise HTTPException(status_code=503, detail="C tools not available")
try:
result = tools.decode_file(request.input_file, request.output_file, request.verbose)
return result
except ToolError as e:
raise HTTPException(status_code=500, detail=str(e))
# Report generation endpoints
@app.post("/api/report")
async def generate_report(request: ReportRequest, background_tasks: BackgroundTasks) -> Dict[str, Any]:
"""
Generate HTML activity report.
Args:
request: Report configuration
background_tasks: Background task runner
Returns:
Report generation result
"""
if not tools:
raise HTTPException(status_code=503, detail="C tools not available")
try:
result = tools.generate_report(
output_file=request.output_file,
input_dir=request.input_dir,
title=request.title
)
return result
except ToolError as e:
raise HTTPException(status_code=500, detail=str(e))
@app.get("/api/report/{filename}")
async def get_report(filename: str) -> FileResponse:
"""
Download generated report file.
Args:
filename: Report filename (without path)
Returns:
File response with report content
"""
file_path = Path(filename)
# Security check - prevent directory traversal
if ".." in filename or "/" in filename:
raise HTTPException(status_code=400, detail="Invalid filename")
if not file_path.exists():
raise HTTPException(status_code=404, detail="Report not found")
return FileResponse(path=file_path, filename=filename, media_type="text/html")
# Root endpoint (for backwards compatibility)
@app.get("/")
async def root() -> Dict[str, Any]:
"""
Root API endpoint.
Returns:
API information
"""
return {
"name": "Tikker API",
"version": "2.0.0",
"status": "running",
"backend": "C tools (libtikker)",
"endpoints": {
"health": "/health",
"stats": "/api/stats/daily, /api/stats/hourly, /api/stats/weekly, /api/stats/weekday",
"words": "/api/words/top, /api/words/find",
"operations": "/api/index, /api/decode, /api/report"
}
}
# Backwards compatibility endpoint
@app.get("/api/all-stats")
async def all_stats() -> Dict[str, Any]:
"""
Get all statistics (backwards compatibility endpoint).
Returns:
Comprehensive statistics
"""
if not tools:
raise HTTPException(status_code=503, detail="C tools not available")
try:
daily = tools.get_daily_stats()
weekly = tools.get_weekly_stats()
top_words = tools.get_top_words(10)
return {
"status": "success",
"daily": daily,
"weekly": weekly,
"top_words": top_words,
"backend": "C"
}
except ToolError as e:
raise HTTPException(status_code=500, detail=str(e))
# Exception handlers
@app.exception_handler(ToolError)
async def tool_error_handler(request, exc):
"""Handle C tool errors."""
logger.error(f"C tool error: {exc}")
return HTTPException(status_code=500, detail=str(exc))
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8000)