Skip to content

Instantly share code, notes, and snippets.

@gcleaves
Created May 21, 2025 19:22
Show Gist options
  • Save gcleaves/d837af20624c75c6d677dfb399d794d2 to your computer and use it in GitHub Desktop.
Save gcleaves/d837af20624c75c6d677dfb399d794d2 to your computer and use it in GitHub Desktop.
Automated timesheet entry script for BambooHR
#!/usr/bin/env python3
"""
timesheet.py - Automated timesheet entry script for BambooHR
This script automates the process of submitting timesheet entries to BambooHR.
It allows users to submit multiple workdays of time entries with customizable
time slots for weekdays and Fridays.
Getting an API key: https://documentation.bamboohr.com/docs/getting-started#authentication
Features:
- Automatically skips weekends
- Customizable time slots for weekdays and Fridays
- Default schedule:
- Weekdays (Mon-Thu): 8:30-13:15 and 14:15-17:30
- Fridays: 8:30-13:15 and 14:15-16:00
- Validates date format and number of days
- Provides a dry-run preview before submission
- Uses BambooHR API for submission
Requirements:
- Python 3.6+
- Valid BambooHR API credentials
Configuration:
- BAMBOOHR_APIKEY: Your BambooHR API key (environment variable)
- BAMBOOHR_EMPID: Your employee ID (environment variable)
"""
import os
import sys
import json
import base64
import argparse
from datetime import datetime, timedelta
from typing import List, Dict, Any, Optional, NamedTuple
from urllib import request, parse, error
from getpass import getpass
class TimeSlot(NamedTuple):
start: str
end: str
class TimeConfig:
def __init__(
self,
weekday_morning: TimeSlot = TimeSlot("08:30", "13:15"),
weekday_afternoon: TimeSlot = TimeSlot("14:15", "17:30"),
friday_morning: TimeSlot = TimeSlot("08:30", "13:15"),
friday_afternoon: TimeSlot = TimeSlot("14:15", "16:00")
):
self.weekday_morning = weekday_morning
self.weekday_afternoon = weekday_afternoon
self.friday_morning = friday_morning
self.friday_afternoon = friday_afternoon
@staticmethod
def validate_time(time_str: str) -> bool:
try:
hour, minute = map(int, time_str.split(':'))
return 0 <= hour <= 23 and 0 <= minute <= 59
except (ValueError, TypeError):
return False
@classmethod
def from_user_input(cls) -> 'TimeConfig':
print("\nConfigure time slots (press Enter to use defaults):")
def get_time_slot(prompt: str, default: TimeSlot) -> TimeSlot:
while True:
time_input = input(f"{prompt} [{default.start}-{default.end}]: ").strip()
if not time_input:
return default
try:
start, end = time_input.split('-')
start = start.strip()
end = end.strip()
if not (cls.validate_time(start) and cls.validate_time(end)):
print("Invalid time format. Please use HH:MM format (e.g., 08:30-13:15)")
continue
return TimeSlot(start, end)
except ValueError:
print("Invalid format. Please use START-END format (e.g., 08:30-13:15)")
weekday_morning = get_time_slot(
"Weekday morning slot (Mon-Thu)",
TimeSlot("08:30", "13:15")
)
weekday_afternoon = get_time_slot(
"Weekday afternoon slot (Mon-Thu)",
TimeSlot("14:15", "17:30")
)
friday_morning = get_time_slot(
"Friday morning slot",
TimeSlot("08:30", "13:15")
)
friday_afternoon = get_time_slot(
"Friday afternoon slot",
TimeSlot("14:15", "16:00")
)
return cls(
weekday_morning=weekday_morning,
weekday_afternoon=weekday_afternoon,
friday_morning=friday_morning,
friday_afternoon=friday_afternoon
)
class TimesheetEntry:
def __init__(self, employee_id: int, date: str, start: str, end: str):
self.employee_id = employee_id
self.date = date
self.start = start
self.end = end
def to_dict(self) -> Dict[str, Any]:
return {
"employeeId": self.employee_id,
"date": self.date,
"start": self.start,
"end": self.end
}
class BambooHRTimesheet:
def __init__(
self,
api_key: Optional[str] = None,
employee_id: Optional[str] = None,
time_config: Optional[TimeConfig] = None
):
self.api_key = api_key or os.getenv("BAMBOOHR_APIKEY")
self.employee_id = employee_id or os.getenv("BAMBOOHR_EMPID")
self.base_url = "https://api.bamboohr.com/api/gateway.php/redpoints/v1"
self.time_config = time_config or TimeConfig()
if not self.api_key:
self.api_key = getpass("Enter your BambooHR API key: ")
if not self.api_key:
raise ValueError("API key is required")
if not self.employee_id:
self.employee_id = input("Enter your BambooHR employee ID: ")
if not self.employee_id.isdigit():
raise ValueError("Employee ID must be a number")
self.employee_id = int(self.employee_id)
def get_entries(self, start_date: str, num_days: int) -> List[TimesheetEntry]:
entries: List[TimesheetEntry] = []
current_date = datetime.strptime(start_date, "%Y-%m-%d")
days_added = 0
while days_added < num_days:
# Skip weekends (5 = Saturday, 6 = Sunday)
if current_date.weekday() < 5:
is_friday = current_date.weekday() == 4
# Get appropriate time slots based on day
morning_slot = self.time_config.friday_morning if is_friday else self.time_config.weekday_morning
afternoon_slot = self.time_config.friday_afternoon if is_friday else self.time_config.weekday_afternoon
# Add morning entry
entries.append(TimesheetEntry(
self.employee_id,
current_date.strftime("%Y-%m-%d"),
morning_slot.start,
morning_slot.end
))
# Add afternoon entry
entries.append(TimesheetEntry(
self.employee_id,
current_date.strftime("%Y-%m-%d"),
afternoon_slot.start,
afternoon_slot.end
))
days_added += 1
current_date += timedelta(days=1)
return entries
def submit_entries(self, entries: List[TimesheetEntry]) -> None:
payload = {
"entries": [entry.to_dict() for entry in entries]
}
# Preview the payload
print("\n๐Ÿ“ Dry run preview:")
print("-" * 28)
print(json.dumps(payload, indent=2))
print("-" * 28)
confirm = input("Do you want to submit this data? [y/N]: ").lower()
if confirm != 'y':
print("โŒ Submission canceled.")
return
print("๐Ÿš€ Sending data...")
# Prepare the request
url = f"{self.base_url}/time_tracking/clock_entries/store"
data = json.dumps(payload).encode('utf-8')
# Create request with authentication
auth_string = base64.b64encode(f"{self.api_key}:x".encode()).decode()
headers = {
'Authorization': f'Basic {auth_string}',
'Content-Type': 'application/json',
'Accept': 'application/json'
}
req = request.Request(url, data=data, headers=headers, method='POST')
try:
with request.urlopen(req) as response:
if response.status in (200, 201):
print("\nโœ… Submission complete.")
else:
print(f"\nโŒ Error: HTTP {response.status}")
print(f"Response: {response.read().decode()}")
sys.exit(1)
except error.HTTPError as e:
if e.code == 401:
print("\nโŒ Authentication failed. Please check your API key.")
elif e.code == 403:
print("\nโŒ Access forbidden. Please check your permissions.")
elif e.code == 400:
print("\nโŒ Bad request. Error details:")
print(e.read().decode())
else:
print(f"\nโŒ Error: HTTP {e.code}")
print(f"Response: {e.read().decode()}")
sys.exit(1)
except error.URLError as e:
print(f"\nโŒ Network error: {str(e)}")
sys.exit(1)
def parse_args():
parser = argparse.ArgumentParser(
description="Automated timesheet entry script for BambooHR",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=__doc__
)
parser.add_argument(
"--start-date",
help="Start date in YYYY-MM-DD format (default: today)",
type=str
)
parser.add_argument(
"--days",
help="Number of workdays to submit (default: 5)",
type=int
)
parser.add_argument(
"--api-key",
help="BambooHR API key (default: from BAMBOOHR_APIKEY env)",
type=str
)
parser.add_argument(
"--employee-id",
help="Employee ID (default: from BAMBOOHR_EMPID env)",
type=str
)
return parser.parse_args()
def main():
try:
args = parse_args()
# Initialize the timesheet handler with custom time slots
time_config = TimeConfig.from_user_input()
timesheet = BambooHRTimesheet(
api_key=args.api_key,
employee_id=args.employee_id,
time_config=time_config
)
# Get start date (from args or prompt)
start_date = args.start_date
if not start_date:
start_date = input("Enter start date (YYYY-MM-DD) [default: today]: ").strip()
if not start_date:
start_date = datetime.now().strftime("%Y-%m-%d")
# Validate date format
try:
datetime.strptime(start_date, "%Y-%m-%d")
except ValueError:
print("Invalid date format. Please use YYYY-MM-DD.")
sys.exit(1)
# Get number of days (from args or prompt)
num_days = args.days
if num_days is None:
num_days_input = input("How many workdays to submit? [default: 5]: ").strip()
num_days = int(num_days_input) if num_days_input else 5
if num_days < 1:
print("Please enter a valid number of days (1 or more).")
sys.exit(1)
# Generate and submit entries
entries = timesheet.get_entries(start_date, num_days)
timesheet.submit_entries(entries)
except KeyboardInterrupt:
print("\n\nScript interrupted by user.")
sys.exit(1)
except Exception as e:
print(f"\nโŒ Error: {str(e)}")
sys.exit(1)
if __name__ == "__main__":
main()
@gcleaves
Copy link
Author

You must have python installed to use this.

Usage: python timesheet.py .

python timesheet.py --help for more info.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment