From ca82885fb776759c419f3e1463160417c896eead Mon Sep 17 00:00:00 2001 From: Valentijn Date: Wed, 18 Mar 2026 19:49:14 +0100 Subject: [PATCH] first commit --- .gitignore | 2 + API.py | 0 DB.py | 52 +++++++++++++ PMT.py | 127 ++++++++++++++++++++++++++++++++ __pycache__/DB.cpython-313.pyc | Bin 0 -> 2728 bytes __pycache__/PMT.cpython-313.pyc | Bin 0 -> 7095 bytes main.py | 24 ++++++ 7 files changed, 205 insertions(+) create mode 100644 .gitignore create mode 100644 API.py create mode 100644 DB.py create mode 100644 PMT.py create mode 100644 __pycache__/DB.cpython-313.pyc create mode 100644 __pycache__/PMT.cpython-313.pyc create mode 100644 main.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4b95af1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.env +shifts.db \ No newline at end of file diff --git a/API.py b/API.py new file mode 100644 index 0000000..e69de29 diff --git a/DB.py b/DB.py new file mode 100644 index 0000000..429328b --- /dev/null +++ b/DB.py @@ -0,0 +1,52 @@ +import sqlite3 + + +class Database: + def __init__(self): + self._con = sqlite3.connect("shifts.db") + self._setup_tables() + pass + + + def _setup_tables(self): + cur = self._con.cursor() + cur.execute(''' + CREATE TABLE IF NOT EXISTS shifts ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + shift_start DATETIME NOT NULL, + shift_end DATETIME NOT NULL, + department TEXT NOT NULL, + duration TEXT NOT NULL, + description TEXT, + fetched_at DATETIME DEFAULT CURRENT_TIMESTAMP, + + UNIQUE(shift_start, department) -- No duplicates + ); + ''') + self._con.commit() + + + def insert_shifts(self, shifts): + cur = self._con.cursor() + inserted = 0 + + for shift in shifts: + start = shift.get('start_datetime', 'N/A') + end = shift.get('end_datetime', 'N/A') + + department = shift.get('department_name', 'N/A') + duration = shift.get('duration', 'N/A') + description = shift.get('shift_remark', shift.get('description', 'N/A')) + + cur.execute(""" + INSERT OR IGNORE INTO shifts + (shift_start, shift_end, department, duration, description) + VALUES (?, ?, ?, ?, ?) + """, (start, end, department, duration, description)) + + if cur.rowcount > 0: + inserted += 1 + + self._con.commit() + print(f"āœ… Inserted {inserted}/{len(shifts)} new shifts") + diff --git a/PMT.py b/PMT.py new file mode 100644 index 0000000..4f748ad --- /dev/null +++ b/PMT.py @@ -0,0 +1,127 @@ +import os +import requests +import json +from datetime import datetime, timedelta +import sys + +class PMT: + def __init__(self): + self.session = None + self.context_token = None + self.user_token = None + + self._username = os.environ.get("USERNAME_PMT") + self._password = os.environ.get("PASSWORD_PMT") + self.base_url = os.environ.get("URL_PMT") + + def get_week_range(self, week_offset): + """Get Monday date for week_offset weeks from now""" + today = datetime.now() + days_to_monday = 0 if today.weekday() == 0 else 7 - today.weekday() + monday = today + timedelta(days=days_to_monday) + target_monday = monday + timedelta(weeks=week_offset) + return target_monday.strftime("%Y-%m-%d") + + def login(self): + self.session = requests.Session() + + login_url = f"{self.base_url}/login" + resp = self.session.get(login_url) + + # Step 2: SSO login POST + sso_data = { + "username": self._username, + "password": self._password, + "browserinfo": { + "osname": "Linux 148", + "browsername": "Firefox", + "browserversion": "148", + "screenresolution": "1080x1920" + } + } + + sso_resp = self.session.post(f"{self.base_url}/pmtLoginSso", + json=sso_data, + headers={'Content-Type': 'application/json'}) + + if sso_resp.status_code != 200 or not sso_resp.json().get('result', {}).get('authenticated'): + print("Login failed!") + print(sso_resp.text) + sys.exit(1) + + login_data = sso_resp.json()['result'] + self.context_token = login_data['context_token'] + self.user_token = login_data['user_token'] + + print(f"āœ… Logged in as {self._username} (store AH 8541 Rhoon)") + return self.session, self.context_token, self.user_token + + def get_shifts(self, days_ahead=14): + """Fetch shifts from now to days_ahead using the REAL endpoint""" + if not self.session or not self.context_token or not self.user_token: + raise ValueError("Must call login() first!") + + shifts = [] + + from_date = datetime.now().strftime("%Y-%m-%d") + to_date = (datetime.now() + timedelta(days=days_ahead)).strftime("%Y-%m-%d") + + print(f"Fetching shifts from {from_date} to {to_date}...") + + headers = { + 'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:148.0) Gecko/20100101 Firefox/148.0', + 'Accept': 'application/json, text/plain, */*', + 'Accept-Language': 'en-US,en;q=0.9', + 'Accept-Encoding': 'gzip, deflate, br, zstd', + 'Referer': 'https://ah8541.personeelstool.nl/my-overview/my-schedule/11-2026', + 'Content-Type': 'application/json', + 'x-api-user': self.user_token, + 'x-api-context': self.context_token, + 'Connection': 'keep-alive', + 'Sec-Fetch-Dest': 'empty', + 'Sec-Fetch-Mode': 'cors', + 'Sec-Fetch-Site': 'same-origin' + } + + shifts_url = f"{self.base_url}/api/v3/environments/21/stores/311/employee/98104/simpleShifts" + params = { + 'from_date': from_date, + 'to_date': to_date + } + + resp = self.session.get(shifts_url, headers=headers, params=params) + + if resp.status_code == 200: + data = resp.json() + week_shifts = data.get('result', {}).get('shift_instances', []) + shifts.extend(week_shifts) + print(f"āœ… Got {len(week_shifts)} shifts!") + else: + print(f"āŒ Failed: {resp.status_code}") + print(resp.text[:500]) + + return shifts + + @classmethod + def print_shifts(cls, shifts): + """Pretty print shifts (class method since it doesn't need instance)""" + if not shifts: + print("No upcoming shifts found.") + return + + print("\nšŸ“… UPCOMING SHIFTS:\n") + print("Date | Time | Dept | Duration | Description") + print("-" * 80) + + for shift in sorted(shifts, key=lambda x: x.get('start_datetime', '')): + start = shift.get('start_datetime', 'N/A') + end = shift.get('end_datetime', 'N/A') + dept = shift.get('department_name', 'N/A') + duration = shift.get('duration', 'N/A') + desc = shift.get('shift_remark', shift.get('description', 'N/A')) + + date_str = start[:10] if start != 'N/A' else 'N/A' + time_str = f"{start[11:16]}-{end[11:16]}" if start != 'N/A' and end != 'N/A' else 'N/A' + + print(f"{date_str} | {time_str:<12} | {dept:<10} | {duration:<8} | {desc[:40]}") + diff --git a/__pycache__/DB.cpython-313.pyc b/__pycache__/DB.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6d84096d06c6068b48395970c9d13b6e46b28744 GIT binary patch literal 2728 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