Skip to content

Instantly share code, notes, and snippets.

@jangxx
Created March 6, 2025 08:59
Show Gist options
  • Save jangxx/8ed599eb108b8653e1cf0a3dfdb7d5fe to your computer and use it in GitHub Desktop.
Save jangxx/8ed599eb108b8653e1cf0a3dfdb7d5fe to your computer and use it in GitHub Desktop.
Sawayo automation
from datetime import datetime, timedelta, timezone
import argparse
import re
import random
import sys
import requests
def time_type(s: str):
m = re.match("^(\d{2}):(\d{2})$", s)
m2 = re.match("^(\d{4})-(\d{2})-(\d{2})\+(\d{2}):(\d{2})$", s)
if m is None and m2 is None:
raise argparse.ArgumentTypeError("Invalid time format")
if m is not None:
return datetime.now().replace(hour=int(m.group(1)), minute=int(m.group(2)), second=0, microsecond=0).astimezone(timezone.utc)
else:
return datetime.now().replace(
year=int(m2.group(1)),
month=int(m2.group(2)),
day=int(m2.group(3)),
hour=int(m2.group(4)),
minute=int(m2.group(5)),
second=0,
microsecond=0,
).astimezone(timezone.utc)
def date_type(s: str):
return datetime.strptime(s, "%Y-%m-%d").astimezone(timezone.utc)
# entry_type is either "office" or "break"
def AddTimeEntry(startTime: datetime, endTime: datetime, entry_type: str):
return {
"operationName": "AddTimeEntry",
"variables": {
"input": {
"startDateTime": startTime.strftime("%Y-%m-%dT%H:%M:%SZ"),
"endDateTime": endTime.strftime("%Y-%m-%dT%H:%M:%SZ"),
"entryType": entry_type,
"notes":"",
"projectTagIds":[]
}
},
"query": "mutation AddTimeEntry($input: AddTimeEntryInput!) {\n addTimeEntry(input: $input) {\n data {\n _id\n __typename\n }\n error {\n closedTime\n unauthorized\n trackModeDisabled\n timePeriodDisabled\n overlappingAbsence\n futureEntryDisabled\n __typename\n }\n __typename\n }\n}",
}
def track_time(sess: requests.Session, start: datetime, total_minutes: int, disable_fuzzing: bool = False):
entries = []
break_length = 0
if not disable_fuzzing:
start = start + timedelta(minutes=random.randint(-5, 5))
total_minutes += random.randint(0, 10)
if total_minutes <= 6*60:
break_length = 0
elif 6*60 < total_minutes <= 9*60: # insert 30 mins break in between
break_length = 30
elif total_minutes > 9*60: # 45 min break
break_length = 45
if break_length == 0:
entries.append( AddTimeEntry(start, start + timedelta(minutes=total_minutes), "office") )
else:
if not disable_fuzzing:
break_length += random.randint(0, 5)
first_leg = random.randint(2*60, 6*60)
entries.append( AddTimeEntry(start, start + timedelta(minutes=first_leg), "office") )
entries.append( AddTimeEntry(start + timedelta(minutes=first_leg), start + timedelta(minutes=first_leg+break_length), "break") )
entries.append( AddTimeEntry(start + timedelta(minutes=first_leg+break_length), start + timedelta(minutes=break_length+total_minutes), "office") )
for entry in entries:
create_resp = sess.post(
url="https://work2.sawayo.de/graphql2/",
json=entry,
headers={
"X-Sawayo-Client-Id": "employee-web-app",
}
)
if create_resp.status_code != 200:
print(f"Error while creating entry: {create_resp.text}")
return
resp_data = create_resp.json()
if resp_data["data"]["addTimeEntry"]["error"] is not None:
print(f"Error while creating entry: {resp_data['data']['addTimeEntry']['error']}")
return
print(f"Inserted {len(entries)} entries starting at {start} with a total of {total_minutes + break_length} minutes")
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Inserts time tracking data into sawayo")
parser.add_argument("-U", "--username", dest="username", help="Your login username (i.e. your email)", required=True)
parser.add_argument("-P", "--password", dest="password", help="Your login password", required=True)
parser.add_argument("-S", "--start-time", dest="start", help="Start Time in HH:mm (or YYYY-MM-DD+HH:mm) format [optional]", type=time_type, default=datetime.now(timezone.utc))
parser.add_argument("-H", "--hours", dest="hours", help="Number of hours to track", type=int, required=True)
parser.add_argument("--no-fuzz", dest="disable_fuzzing", help="Disable random offsets", action="store_true", default=False)
parser.add_argument("--add-until-date", dest="add_until_date", help="Add entries every day from start time until this date (YYYY-MM-DD)", type=date_type)
args = parser.parse_args()
sess = requests.Session()
auth_resp = sess.post("https://auth.sawayo.de/graphql/", json={
"operationName": "SignInViaEmail",
"variables": {
"input":{
"email":args.username,
"password":args.password,
"signInMode":"web",
"tenant":"sawayo"
}
},
"query": "mutation SignInViaEmail($input: SignInViaEmailInput!) {\n signInViaEmail(input: $input) {\n data {\n redirectTo\n type\n __typename\n }\n error {\n badRequest\n forbidden\n invalidCredentials\n invalidOtp\n invalidRecoveryCode\n invalidSignInMethod\n invalidTenant\n notFound\n passwordNotSet\n unauthorized\n userNotActive\n tooManyRequests\n __typename\n }\n __typename\n }\n}",
}, headers={
"X-Sawayo-Client-Id": "auth-app",
})
if auth_resp.status_code != 200 or auth_resp.json()["data"]["signInViaEmail"]["error"] is not None:
print(f"Error while authenticating: {auth_resp.text}")
sys.exit(1)
if args.add_until_date is None:
track_time(sess, args.start, args.hours * 60, args.disable_fuzzing)
else:
start_time = args.start
end_time = args.add_until_date + timedelta(days=1)
while start_time < end_time:
track_time(sess, start_time, args.hours * 60, args.disable_fuzzing)
start_time += timedelta(days=1)
certifi==2025.1.31
charset-normalizer==3.4.1
idna==3.10
requests==2.32.3
urllib3==2.3.0
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment