|
"""
|
|
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)
|