Created
March 6, 2025 08:59
-
-
Save jangxx/8ed599eb108b8653e1cf0a3dfdb7d5fe to your computer and use it in GitHub Desktop.
Sawayo automation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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