This commit is contained in:
2025-12-22 15:23:44 -05:00
parent dd3f0dea56
commit cef92e9144
9 changed files with 670 additions and 103 deletions

61
hubstaff-cli/README.md Normal file
View File

@@ -0,0 +1,61 @@
# Hubstaff CLI
This project is a command-line interface (CLI) tool for fetching timesheet data from the Hubstaff API. It provides a simple way to interact with the API and retrieve timesheet information formatted according to specified requirements.
## Table of Contents
- [Installation](#installation)
- [Usage](#usage)
- [Output Format](#output-format)
- [Dependencies](#dependencies)
## Installation
1. Clone the repository:
```
git clone <repository-url>
cd hubstaff-cli
```
2. Install the required dependencies:
```
pip install -r requirements.txt
```
3. Set up your environment variables for the Hubstaff API:
- Create a `.env` file in the root directory and add your Hubstaff API key:
```
HUBSTAFF_API_KEY=your_api_key_here
```
## Usage
To run the timesheet data fetching script, execute the following command:
```
python src/timesheet.py
```
This will log in to the Hubstaff API and fetch the timesheet data for the specified user and date range.
## Output Format
The output will be displayed in a tabular format with the following columns:
- **Date**: The date of the time entry.
- **Started At**: The time the entry started.
- **Tracked (minutes)**: The total minutes tracked for the entry.
Example output:
```
| Date | Started At | Tracked (minutes) |
|------------|------------|--------------------|
| 2023-10-01 | 09:00 | 120 |
| 2023-10-01 | 13:00 | 60 |
```
## Dependencies
- `requests`: For making HTTP requests to the Hubstaff API.
- `python-dotenv`: For loading environment variables from a `.env` file.

View File

@@ -0,0 +1,2 @@
requests
python-dotenv

View File

@@ -0,0 +1,32 @@
class HubstaffAPI:
def __init__(self, refresh_token: str):
self.refresh_token = refresh_token
self.base_url = "https://api.hubstaff.com/v2"
self.bearer_token = None
async def fetch_bearer_token(self):
# Logic to fetch bearer token using the refresh token
pass
async def get_current_user(self):
# Logic to get current user information
pass
async def get_activities(self, organization_id: int, user_id: int, start: str, stop: str):
# Logic to fetch activities from Hubstaff API
pass
async def get_timesheets(self, organization_id: int, user_id: int, start: str, stop: str):
# Logic to fetch timesheet data from Hubstaff API
url = f"{self.base_url}/organizations/{organization_id}/users/{user_id}/timesheets"
params = {
"start": start,
"stop": stop
}
# Make an API request to fetch timesheets
response = await self._make_request(url, params)
return response
async def _make_request(self, url: str, params: dict):
# Logic to make an HTTP request to the Hubstaff API
pass

View File

@@ -0,0 +1,112 @@
from services.hubstaff_api import HubstaffAPI
import os
import shutil
import asyncio
from datetime import datetime, timedelta
async def main():
# Load the Hubstaff API key from environment variables
api_key = os.getenv("HUBSTAFF_API_KEY")
if not api_key:
print("Error: HUBSTAFF_API_KEY environment variable not set.")
return
# Load the Hubstaff organization ID from environment variables
org_id = os.getenv("HUBSTAFF_ORG_ID")
if not org_id:
print("Error: HUBSTAFF_ORG_ID environment variable not set.")
return
# Initialize the Hubstaff API client
hubstaff = HubstaffAPI(refresh_token=api_key)
try:
# Fetch user info
user_info = await hubstaff.get_current_user()
user_id = user_info["user"]["id"]
# Define the date range for the timesheet
end_date = datetime.now()
start_date = end_date - timedelta(days=7)
# Fetch timesheet data
timesheet_data = await hubstaff.get_timesheets(
user_id, start=start_date.isoformat(), stop=end_date.isoformat()
)
# Check if timesheet_data is valid and "times" is a list before proceeding
times = timesheet_data.get("times") if timesheet_data else None
if not times or not isinstance(times, list):
print("Error: No timesheet data found or invalid format received.")
return
# Prepare invoice configuration based on timesheet data
invoice_logo = os.getenv("INVOICE_LOGO", "/path/to/logo.png")
invoice_from = os.getenv("INVOICE_FROM", "Default Company")
invoice_to = os.getenv("INVOICE_TO", "Client Company")
invoice_tax = float(os.getenv("INVOICE_TAX", "0.0"))
invoice_rate = float(os.getenv("INVOICE_RATE", "25"))
config = {
"logo": invoice_logo,
"from": invoice_from,
"to": invoice_to,
"tax": invoice_tax,
"items": [],
"quantities": [],
"rates": [],
}
# For each timesheet entry, we'll use the note as item.
# If note is missing or "N/A", we'll use the date instead.
for entry in times:
entry_date = datetime.fromtimestamp(int(entry["date"])).strftime("%Y-%m-%d")
note = entry.get("note", "N/A")
item = note if note != "N/A" else f"Work on {entry_date}"
minutes = entry.get("minutes", 0)
config["items"].append(item)
config["quantities"].append(minutes)
config["rates"].append(invoice_rate)
# Generate YAML output
yaml_lines = [
f"logo: {config['logo']}",
f"from: {config['from']}",
f"to: {config['to']}",
f"tax: {config['tax']}",
"items:",
]
for item in config["items"]:
yaml_lines.append(f' - "{item}"')
yaml_lines.append("quantities:")
for qty in config["quantities"]:
yaml_lines.append(f" - {qty}")
yaml_lines.append("rates:")
for rate in config["rates"]:
yaml_lines.append(f" - {rate}")
yaml_content = "\n".join(yaml_lines)
config_filename = "invoice_config.yaml"
with open(config_filename, "w") as f:
f.write(yaml_content)
print(f"Invoice configuration written to {config_filename}")
# Call the invoice command if available locally
if shutil.which("invoice"):
os.system(
f"invoice generate --import {config_filename} --output timesheet-invoice.pdf"
)
else:
print(
"Invoice command not found locally. Please install it to generate invoices."
)
except Exception as e:
print(f"Error fetching timesheet data: {e}")
if __name__ == "__main__":
asyncio.run(main())