first commit
This commit is contained in:
@@ -0,0 +1,334 @@
|
||||
"""
|
||||
ZynkTime - Time reporting application for Kleer API.
|
||||
|
||||
This module provides FastAPI endpoints for time reporting and a CLI interface.
|
||||
"""
|
||||
import logging
|
||||
import sys
|
||||
from datetime import datetime
|
||||
|
||||
import fastapi
|
||||
import uvicorn
|
||||
from fastapi import File, HTTPException, UploadFile
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.responses import HTMLResponse, FileResponse
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
|
||||
from api_client import KleerAPI
|
||||
from cli import run_cli
|
||||
from config import Config
|
||||
from models import TimeReportResponse
|
||||
from services import (
|
||||
create_time_event,
|
||||
extract_work_time,
|
||||
get_user_by_name,
|
||||
parse_csv,
|
||||
parse_xlsx,
|
||||
validate_date,
|
||||
)
|
||||
|
||||
# Configure logging
|
||||
logging.basicConfig(level=logging.INFO, format='%(levelname)s: %(message)s')
|
||||
|
||||
# Initialize FastAPI app
|
||||
app = fastapi.FastAPI(
|
||||
title="ZynkTime API",
|
||||
description="Time reporting API for Kleer",
|
||||
version="1.0.0"
|
||||
)
|
||||
|
||||
# Add CORS middleware
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"],
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
# Mount static files
|
||||
app.mount("/static", StaticFiles(directory="static"), name="static")
|
||||
|
||||
|
||||
@app.get("/", response_class=HTMLResponse)
|
||||
async def root():
|
||||
"""Serve the web UI."""
|
||||
return FileResponse("static/index.html")
|
||||
|
||||
|
||||
@app.get("/api")
|
||||
def api_info():
|
||||
"""API information endpoint."""
|
||||
return {
|
||||
"name": "ZynkTime API",
|
||||
"version": "1.0.0",
|
||||
"endpoints": {
|
||||
"projects": "/projects",
|
||||
"report_time": "/report-time"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@app.get("/health")
|
||||
def health_check():
|
||||
"""Health check endpoint."""
|
||||
try:
|
||||
Config.validate_config()
|
||||
return {"status": "healthy", "api_key_configured": True}
|
||||
except ValueError:
|
||||
return {"status": "unhealthy", "api_key_configured": False}
|
||||
|
||||
|
||||
@app.get("/projects")
|
||||
def list_projects():
|
||||
"""
|
||||
List all active projects for the configured user.
|
||||
|
||||
Returns:
|
||||
Dictionary of project names to project information
|
||||
|
||||
Raises:
|
||||
HTTPException: If user not found or API request fails
|
||||
"""
|
||||
try:
|
||||
kleer_api = KleerAPI()
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
users_data = kleer_api.get_user_info()
|
||||
username = Config.get_username()
|
||||
|
||||
try:
|
||||
user_id = get_user_by_name(users_data, username)
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=404, detail=str(e))
|
||||
|
||||
projects = kleer_api.get_projects(user_id=user_id)
|
||||
return projects
|
||||
|
||||
|
||||
@app.post("/report-time", response_model=TimeReportResponse)
|
||||
async def report_time_from_csv(
|
||||
project_name: str,
|
||||
file: UploadFile = File(...)
|
||||
):
|
||||
"""
|
||||
Upload a CSV file and report time to a specified Kleer project.
|
||||
|
||||
Args:
|
||||
project_name: Name of the project to report time to
|
||||
file: CSV file containing time entries
|
||||
|
||||
Returns:
|
||||
TimeReportResponse with summary of reported time
|
||||
|
||||
Raises:
|
||||
HTTPException: If validation fails or API request fails
|
||||
"""
|
||||
# Initialize API client
|
||||
try:
|
||||
kleer_api = KleerAPI()
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
# Get user ID
|
||||
users_data = kleer_api.get_user_info()
|
||||
username = Config.get_username()
|
||||
|
||||
try:
|
||||
user_id = get_user_by_name(users_data, username)
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=404, detail=str(e))
|
||||
|
||||
# Get project info
|
||||
projects = kleer_api.get_projects(user_id=user_id)
|
||||
project_info = projects.get(project_name)
|
||||
|
||||
if not project_info:
|
||||
available_projects = ", ".join(projects.keys())
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail=f"Project '{project_name}' not found. Available projects: {available_projects}"
|
||||
)
|
||||
|
||||
# Process file content
|
||||
content = await file.read()
|
||||
filename = (file.filename or "").lower()
|
||||
content_type = (file.content_type or "").lower()
|
||||
|
||||
try:
|
||||
if filename.endswith(".xlsx") or "spreadsheetml" in content_type:
|
||||
rows = parse_xlsx(content)
|
||||
else:
|
||||
rows = parse_csv(content.decode('utf-8-sig'))
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=400, detail=f"Failed to parse file: {str(e)}")
|
||||
|
||||
work_time = extract_work_time(rows)
|
||||
|
||||
if not work_time:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="No valid work time found in the uploaded CSV."
|
||||
)
|
||||
|
||||
# Submit events
|
||||
total_work_time = 0.0
|
||||
failed_dates = []
|
||||
|
||||
for date, total_time in work_time.items():
|
||||
try:
|
||||
event = create_time_event(
|
||||
user_id=user_id,
|
||||
project_id=project_info["id"],
|
||||
activity=project_info["activity"],
|
||||
date=date,
|
||||
hours=total_time
|
||||
)
|
||||
kleer_api.put_event(event)
|
||||
total_work_time += total_time
|
||||
except Exception as e:
|
||||
logging.error(f"Failed to report time for {date}: {e}")
|
||||
failed_dates.append(date)
|
||||
|
||||
# Check if any submissions failed
|
||||
if failed_dates:
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail=f"Failed to report time for dates: {', '.join(failed_dates)}"
|
||||
)
|
||||
|
||||
return TimeReportResponse(
|
||||
message="Time reported successfully.",
|
||||
reported_days=len(work_time),
|
||||
total_hours=total_work_time,
|
||||
project_name=project_name,
|
||||
user_name=username
|
||||
)
|
||||
|
||||
@app.get("/salary")
|
||||
def get_salary_info():
|
||||
"""
|
||||
Placeholder endpoint for salary information.
|
||||
|
||||
Returns:
|
||||
Static message indicating feature is under development.
|
||||
"""
|
||||
# Initialize API client
|
||||
try:
|
||||
kleer_api = KleerAPI()
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
# Get user ID
|
||||
users_data = kleer_api.get_user_info()
|
||||
username = Config.get_username()
|
||||
|
||||
try:
|
||||
user_id = get_user_by_name(users_data, username)
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=404, detail=str(e))
|
||||
|
||||
|
||||
|
||||
return {"message": "Salary information feature is under development."}
|
||||
|
||||
|
||||
def main():
|
||||
"""Main entry point for the application."""
|
||||
if len(sys.argv) > 1 and sys.argv[1] == 'cli':
|
||||
# Run CLI mode
|
||||
run_cli()
|
||||
else:
|
||||
# Run API server
|
||||
print("Starting ZynkTime FastAPI server...")
|
||||
print("To run CLI mode, use: python main.py cli")
|
||||
print(f"Server will be available at http://{Config.SERVER_HOST}:{Config.SERVER_PORT}")
|
||||
print("API documentation available at /docs")
|
||||
|
||||
uvicorn.run(
|
||||
app,
|
||||
host=Config.SERVER_HOST,
|
||||
port=Config.SERVER_PORT
|
||||
)
|
||||
|
||||
|
||||
def _get_user_id(kleer_api: KleerAPI) -> int:
|
||||
"""Resolve configured username to user ID using API data."""
|
||||
users_data = kleer_api.get_user_info()
|
||||
username = Config.get_username()
|
||||
|
||||
try:
|
||||
return get_user_by_name(users_data, username)
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=404, detail=str(e))
|
||||
|
||||
|
||||
def _validate_date_range(start_date: str, end_date: str) -> None:
|
||||
"""Validate date strings and ordering."""
|
||||
if not (validate_date(start_date) and validate_date(end_date)):
|
||||
raise HTTPException(status_code=400, detail="Dates must be in YYYY-MM-DD format")
|
||||
|
||||
if datetime.fromisoformat(start_date) > datetime.fromisoformat(end_date):
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="Start date must be on or before end date"
|
||||
)
|
||||
|
||||
|
||||
@app.get("/events")
|
||||
def list_events(start_date: str, end_date: str):
|
||||
"""
|
||||
List events for the configured user between two dates (inclusive).
|
||||
"""
|
||||
_validate_date_range(start_date, end_date)
|
||||
|
||||
try:
|
||||
kleer_api = KleerAPI()
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
user_id = _get_user_id(kleer_api)
|
||||
|
||||
events = kleer_api.get_events(user_id=user_id, start_date=start_date, end_date=end_date)
|
||||
return {
|
||||
"user_id": user_id,
|
||||
"start_date": start_date,
|
||||
"end_date": end_date,
|
||||
"events": events.get("event-readables", events)
|
||||
}
|
||||
|
||||
|
||||
@app.post("/approve-events")
|
||||
def approve_events(start_date: str, end_date: str):
|
||||
"""
|
||||
Approve events for the configured user in a date range.
|
||||
"""
|
||||
_validate_date_range(start_date, end_date)
|
||||
|
||||
try:
|
||||
kleer_api = KleerAPI()
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
user_id = _get_user_id(kleer_api)
|
||||
|
||||
approval_response = kleer_api.approve_events(
|
||||
user_id=user_id,
|
||||
start_date=start_date,
|
||||
end_date=end_date
|
||||
)
|
||||
|
||||
return {
|
||||
"message": "Events approved",
|
||||
"user_id": user_id,
|
||||
"start_date": start_date,
|
||||
"end_date": end_date,
|
||||
"approval": approval_response
|
||||
}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user