import requests import questionary from rich.console import Console from rich.table import Table from rich.theme import Theme from rich.panel import Panel from rich.text import Text import json import os import re from datetime import datetime, timedelta from dotenv import load_dotenv # Load environment variables from .env if it exists load_dotenv() # Fallout Pip-Boy Style Theme fallout_theme = Theme({ "info": "bold green", "warning": "bold yellow", "error": "bold red", "success": "bold green", "dim": "dim green", "highlight": "bold #33ff33", "table.header": "bold #33ff33", "table.title": "bold #33ff33", }) console = Console(theme=fallout_theme) # Questionary custom style custom_style = questionary.Style([ ('qmark', 'fg:#33ff33 bold'), # token in front of the question ('question', 'fg:#33ff33 bold'), # question text ('answer', 'fg:#33ff33'), # submitted answer text behind the question ('pointer', 'fg:#33ff33 bold'), # pointer used in select input ('highlighted', 'fg:#000000 bg:#33ff33 bold'), # pointed-at choice in select and checkbox ('selected', 'fg:#33ff33'), # pointed-at choice in select and checkbox ('separator', 'fg:#004400'), # separator in lists ('instruction', 'fg:#004400 italic'), # user instructions for checkbox, reorder, etc. ('text', 'fg:#33ff33'), # plain text ('disabled', 'fg:#004400 italic') # disabled choices for select and checkbox ]) PIPO_ART = """ _____ __ __ _____ _____ ____ |_ _| /\ | \/ |_ _/ ____|/ __ \ | | / \ | \ / | | || | __| | | | | | / /\ \ | |\/| | | || | |_ | | | | _| |_ / ____ \| | | |_| || |__| | |__| | |_____/_/ \_\_| |_|_____\_____|\____/ [bold #33ff33]ROBCO INDUSTRIES (TM) TERMLINK PROTOCOL[/bold #33ff33] """ BASE_URL = "https://api.tamigo.com" def print_header(): console.print(PIPO_ART, style="highlight") console.print("[dim]-------------------------------------------[/dim]") console.print("[highlight]WELCOME TO TAMIGO-OS v1.0.0[/highlight]") console.print("[dim]-------------------------------------------[/dim]\n") def parse_tamigo_date(date_str): """ Parses Tamigo date formats: - /Date(1600898400000+0200)/ - 2023-10-27T08:00:00 """ if not date_str: return None # Handle /Date(1600898400000+0200)/ match = re.search(r"/Date\((\d+)([+-]\d+)?\)/", date_str) if match: ms = int(match.group(1)) # Convert ms to seconds return datetime.fromtimestamp(ms / 1000.0) # Handle ISO format try: # datetime.fromisoformat handles T and optional Z/+offset in Python 3.7+ return datetime.fromisoformat(date_str.replace("Z", "+00:00")) except: return None class TamigoClient: def __init__(self): self.session_token = None self.user_info = None self.employee_id = None def login(self, email, password): urls = [ f"{BASE_URL}/login/application", f"{BASE_URL}/Login", f"{BASE_URL}/login", ] for url in urls: payload = { "Email": email, "Password": password, "Name": "TamigoCLI", "Key": password } try: headers = { "Content-Type": "application/json", "Accept": "application/json" } response = requests.post(url, json=payload, headers=headers, timeout=15) if response.status_code == 200: try: data = response.json() self.session_token = data.get("SessionToken") or data.get("securitytoken") or data.get("Token") if self.session_token: self.user_info = data self.employee_id = data.get("EmployeeId") return True except json.JSONDecodeError: text = response.text.strip().strip('"') if text and len(text) > 20: self.session_token = text self.user_info = {"Email": email} return True except: continue return False def get_employee_actual_shifts(self, start_date_dt, end_date_dt): if not self.session_token: return None headers = { "x-tamigo-token": self.session_token, "securitytoken": self.session_token, "Accept": "application/json" } all_shifts = [] days_diff = (end_date_dt - start_date_dt).days for i in range(0, days_diff + 1, 60): target_date = (end_date_dt - timedelta(days=i)).strftime("%Y-%m-%d") current_dt = end_date_dt - timedelta(days=i) if current_dt < start_date_dt - timedelta(days=60): break url = f"{BASE_URL}/actualshifts/past/{target_date}" try: response = requests.get(url, headers=headers) if response.status_code == 200: data = response.json() if isinstance(data, list): all_shifts.extend(data) elif response.status_code == 401: response = requests.get(url, params={"securitytoken": self.session_token}) if response.status_code == 200: all_shifts.extend(response.json()) except: pass start_str = start_date_dt.strftime("%Y-%m-%d") end_str = end_date_dt.strftime("%Y-%m-%d") url_period = f"{BASE_URL}/shifts/period/{start_str}/{end_str}" try: response = requests.get(url_period, headers=headers) if response.status_code == 200: data = response.json() if isinstance(data, list): all_shifts.extend(data) except: pass return all_shifts def calculate_checkins(client): range_choice = questionary.select( "SELECT TEMPORAL RANGE:", choices=[ "Last 365 days", "Last 30 days", "This Month", "This Year", "Custom range..." ], style=custom_style ).ask() end_date_dt = datetime.now() if range_choice == "Last 365 days": start_date_dt = end_date_dt - timedelta(days=365) elif range_choice == "Last 30 days": start_date_dt = end_date_dt - timedelta(days=30) elif range_choice == "This Month": start_date_dt = end_date_dt.replace(day=1) elif range_choice == "This Year": start_date_dt = end_date_dt.replace(month=1, day=1) else: while True: start_str = questionary.text("ENTER START DATE (YYYY-MM-DD):", default=(datetime.now() - timedelta(days=30)).strftime("%Y-%m-%d"), style=custom_style).ask() end_str = questionary.text("ENTER END DATE (YYYY-MM-DD):", default=datetime.now().strftime("%Y-%m-%d"), style=custom_style).ask() try: start_date_dt = datetime.strptime(start_str, "%Y-%m-%d") end_date_dt = datetime.strptime(end_str, "%Y-%m-%d") if start_date_dt > end_date_dt: console.print("[error]ERROR: INVALID CHRONOLOGY. START MUST PRECEDE END.[/error]") continue break except ValueError: console.print("[error]ERROR: INVALID DATA FORMAT. USE YYYY-MM-DD.[/error]") with console.status(f"[highlight]INITIALIZING DATA RETRIEVAL...[/highlight]"): data = client.get_employee_actual_shifts(start_date_dt, end_date_dt) if data: work_days = {} requested_start = start_date_dt.date() requested_end = end_date_dt.date() for item in data: raw_date = item.get("Date") or item.get("StartTime") or item.get("CheckInTime") dt = parse_tamigo_date(raw_date) if dt: item_date = dt.date() if not (requested_start <= item_date <= requested_end): continue date_str = dt.strftime("%Y-%m-%d") is_absent = item.get("IsAbsent", False) hours = (item.get("ActualShiftHours") or item.get("CheckInOutShiftHours") or item.get("Hours") or 0) if hours == 0 and item.get("StartTime") and item.get("EndTime"): st = parse_tamigo_date(item.get("StartTime")) et = parse_tamigo_date(item.get("EndTime")) if st and et: hours = (et - st).total_seconds() / 3600.0 has_actual = False if hours > 0 or item.get("CheckInTime") or item.get("ActualStartTime") or (item.get("StartTime") and not is_absent): has_actual = True if has_actual and not is_absent: if date_str not in work_days: work_days[date_str] = {"hours": float(hours), "text": item.get("ActualShiftText") or item.get("ActivityName") or "WORKED"} else: work_days[date_str]["hours"] += float(hours) if not work_days: console.print("[warning]WARNING: NO LOG ENTRIES DETECTED FOR SPECIFIED PARAMETERS.[/warning]") return all_dates = sorted(work_days.keys(), reverse=True) table = Table(title="LOG ENTRIES: ACTUAL WORKED SHIFTS", show_header=True, header_style="table.header", box=None) table.add_column("DATE", style="highlight") table.add_column("HOURS", justify="right") table.add_column("DETAILS", style="dim") for day in all_dates: info = work_days[day] table.add_row(day, f"{info['hours']:.2f}", str(info['text']).upper()) console.print(table) summary_text = f"PERIOD: {start_date_dt.strftime('%Y-%m-%d')} TO {end_date_dt.strftime('%Y-%m-%d')}\n" summary_text += f"DAYS WORKED: {len(all_dates)}\n" summary_text += f"TOTAL HOURS: {sum(d['hours'] for d in work_days.values()):.2f}" console.print(Panel(summary_text, title="SUMMARY STATISTICS", border_style="highlight", expand=False)) else: console.print("[error]FATAL ERROR: DATA RETRIEVAL FAILURE.[/error]") def show_profile(client): if client.user_info: console.print_json(data=client.user_info) else: console.print("[error]ERROR: ACCESS DENIED. SESSION TOKEN NOT FOUND.[/error]") def main(): print_header() client = TamigoClient() email = os.getenv("TAMIGO_EMAIL") if not email: email = questionary.text("ENTER IDENTIFIER (EMAIL):", style=custom_style).ask() password = os.getenv("TAMIGO_PASSWORD") if not password: password = questionary.password("ENTER ACCESS KEY:", style=custom_style).ask() if not email or not password: return with console.status("[highlight]AUTHENTICATING WITH VAULT-TEC CLOUD...[/highlight]"): success = client.login(email, password) if success: console.print("[success]AUTHENTICATION SUCCESSFUL. ACCESS GRANTED.[/success]") menu(client) else: console.print("[error]CRITICAL FAILURE: INVALID CREDENTIALS.[/error]") def menu(client): while True: choice = questionary.select( "SELECT ACTION:", choices=[ "Calculate actual work days", "Show profile info", "Logout and Exit" ], style=custom_style ).ask() if choice == "Calculate actual work days": calculate_checkins(client) elif choice == "Show profile info": show_profile(client) else: console.print("[highlight]SYSTEM SHUTDOWN INITIATED...[/highlight]") break if __name__ == "__main__": main()