Files
tamigo-cli/tamigo.py
2026-03-11 21:17:15 +01:00

331 lines
12 KiB
Python

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