Compare commits

..

13 Commits

Author SHA1 Message Date
Maximilian Zettler fa3e79a1b3 update typing 4 years ago
Maximilian Zettler d81471969e fix way to long timeout for connect 4 years ago
Maximilian Zettler 98f6a88fe6 cleanup gui code 4 years ago
Maximilian Zettler 89a8b43a39 refresh main window once per second 4 years ago
Maximilian Zettler 061e72a8f6 add password dialog 4 years ago
Maximilian Zettler 5578dd9af7 add error popup 4 years ago
Maximilian Zettler 0885bf300a enhance get_hours_present 4 years ago
Maximilian Zettler 9037a0f91c add doc in timebot class and fix enum usage 4 years ago
Maximilian Zettler 2ab0d6d920 wip: add get_hours_present method 4 years ago
Maximilian Zettler e32192f8dd update button text 4 years ago
Maximilian Zettler 836e7d9b0e add auto refresh of status and run status_update in thread 4 years ago
Maximilian Zettler c537ae87b7 update readme 4 years ago
Maximilian Zettler 9c905cfaa8 wip: add qt5 ui 4 years ago
  1. 14
      Makefile
  2. 2
      README.md
  3. 82
      poetry.lock
  4. 1
      pyproject.toml
  5. 43
      timebot/app.py
  6. 37
      timebot/constants.py
  7. 172
      timebot/gui.py
  8. 145
      timebot/timebot.py

@ -0,0 +1,14 @@
.PHONY: nuitka clean env
#export CCFLAGS += -O1
#export CXXFLAGS += -O1
nuitka:
poetry run python -m nuitka --standalone --onefile --enable-plugin=pyqt5 --output-dir=build/nuitka timebot/app.py
env:
env
clean:
rm -rf dist
rm -rf build

@ -20,7 +20,7 @@ This is a poetry managed project. For details see:
### App Image
``poetry run python -m nuitka --standalone --onefile --output-dir=build/nuitka timebot/app.py``
``poetry run python -m nuitka --standalone --onefile --enable-plugin=pyqt5 --output-dir=build/nuitka timebot/app.py``
# ERRORS

82
poetry.lock generated

@ -30,7 +30,7 @@ python-versions = "*"
[[package]]
name = "charset-normalizer"
version = "2.0.11"
version = "2.0.12"
description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
category = "main"
optional = false
@ -82,7 +82,7 @@ python-versions = ">=3.5"
[[package]]
name = "nuitka"
version = "0.6.19.5"
version = "0.6.19.7"
description = "Python compiler with full language support and CPython compatibility"
category = "dev"
optional = false
@ -132,6 +132,34 @@ python-versions = ">=3.6"
[package.extras]
diagrams = ["jinja2", "railroad-diagrams"]
[[package]]
name = "pyqt5"
version = "5.15.6"
description = "Python bindings for the Qt cross platform application toolkit"
category = "main"
optional = false
python-versions = ">=3.6"
[package.dependencies]
PyQt5-Qt5 = ">=5.15.2"
PyQt5-sip = ">=12.8,<13"
[[package]]
name = "pyqt5-qt5"
version = "5.15.2"
description = "The subset of a Qt installation needed by PyQt5."
category = "main"
optional = false
python-versions = "*"
[[package]]
name = "pyqt5-sip"
version = "12.9.1"
description = "The sip module support for PyQt5"
category = "main"
optional = false
python-versions = ">=3.5"
[[package]]
name = "pytest"
version = "5.4.3"
@ -175,7 +203,7 @@ use_chardet_on_py3 = ["chardet (>=3.0.2,<5)"]
[[package]]
name = "typing-extensions"
version = "4.0.1"
version = "4.1.1"
description = "Backported and Experimental Type Hints for Python 3.6+"
category = "dev"
optional = false
@ -217,7 +245,7 @@ testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytes
[metadata]
lock-version = "1.1"
python-versions = "^3.6"
content-hash = "69c9457fa471c11ea8c160fbc1fd743e319c7cf8fd88730a60f318d4059e7448"
content-hash = "55bdfbbf52fa812d5d62f1d8117311eff037ce586754aa3541e5917903497643"
[metadata.files]
atomicwrites = [
@ -233,8 +261,8 @@ certifi = [
{file = "certifi-2021.10.8.tar.gz", hash = "sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872"},
]
charset-normalizer = [
{file = "charset-normalizer-2.0.11.tar.gz", hash = "sha256:98398a9d69ee80548c762ba991a4728bfc3836768ed226b3945908d1a688371c"},
{file = "charset_normalizer-2.0.11-py3-none-any.whl", hash = "sha256:2842d8f5e82a1f6aa437380934d5e1cd4fcf2003b06fed6940769c164a480a45"},
{file = "charset-normalizer-2.0.12.tar.gz", hash = "sha256:2857e29ff0d34db842cd7ca3230549d1a697f96ee6d3fb071cfa6c7393832597"},
{file = "charset_normalizer-2.0.12-py3-none-any.whl", hash = "sha256:6881edbebdb17b39b4eaaa821b438bf6eddffb4468cf344f09f89def34a8b1df"},
]
colorama = [
{file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"},
@ -253,7 +281,7 @@ more-itertools = [
{file = "more_itertools-8.12.0-py3-none-any.whl", hash = "sha256:43e6dd9942dffd72661a2c4ef383ad7da1e6a3e968a927ad7a6083ab410a688b"},
]
nuitka = [
{file = "Nuitka-0.6.19.5.tar.gz", hash = "sha256:b100789ea71aff8814cf52958e20f82c4c5a01aaa8a8eca38cd6a7e1cc5cae25"},
{file = "Nuitka-0.6.19.7.tar.gz", hash = "sha256:15f9618f3536b9933fe0e621ecfc8cb6ec0893cf23ef28be95392ac192a464be"},
]
packaging = [
{file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"},
@ -271,6 +299,42 @@ pyparsing = [
{file = "pyparsing-3.0.7-py3-none-any.whl", hash = "sha256:a6c06a88f252e6c322f65faf8f418b16213b51bdfaece0524c1c1bc30c63c484"},
{file = "pyparsing-3.0.7.tar.gz", hash = "sha256:18ee9022775d270c55187733956460083db60b37d0d0fb357445f3094eed3eea"},
]
pyqt5 = [
{file = "PyQt5-5.15.6-cp36-abi3-macosx_10_13_x86_64.whl", hash = "sha256:33ced1c876f6a26e7899615a5a4efef2167c263488837c7beed023a64cebd051"},
{file = "PyQt5-5.15.6-cp36-abi3-manylinux1_x86_64.whl", hash = "sha256:9d6efad0377aa78bf081a20ac752ce86096ded18f04c592d98f5b92dc879ad0a"},
{file = "PyQt5-5.15.6-cp36-abi3-win32.whl", hash = "sha256:9d2dcdaf82263ae56023410a7af15d1fd746c8e361733a7d0d1bd1f004ec2793"},
{file = "PyQt5-5.15.6-cp36-abi3-win_amd64.whl", hash = "sha256:f411ecda52e488e1d3c5cce7563e1b2ca9cf0b7531e3c25b03d9a7e56e07e7fc"},
{file = "PyQt5-5.15.6.tar.gz", hash = "sha256:80343bcab95ffba619f2ed2467fd828ffeb0a251ad7225be5fc06dcc333af452"},
]
pyqt5-qt5 = [
{file = "PyQt5_Qt5-5.15.2-py3-none-macosx_10_13_intel.whl", hash = "sha256:76980cd3d7ae87e3c7a33bfebfaee84448fd650bad6840471d6cae199b56e154"},
{file = "PyQt5_Qt5-5.15.2-py3-none-manylinux2014_x86_64.whl", hash = "sha256:1988f364ec8caf87a6ee5d5a3a5210d57539988bf8e84714c7d60972692e2f4a"},
{file = "PyQt5_Qt5-5.15.2-py3-none-win32.whl", hash = "sha256:9cc7a768b1921f4b982ebc00a318ccb38578e44e45316c7a4a850e953e1dd327"},
{file = "PyQt5_Qt5-5.15.2-py3-none-win_amd64.whl", hash = "sha256:750b78e4dba6bdf1607febedc08738e318ea09e9b10aea9ff0d73073f11f6962"},
]
pyqt5-sip = [
{file = "PyQt5_sip-12.9.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:6b2e553e21b7ff124007a6b9168f8bb8c171fdf230d31ca0588df180f10bacbe"},
{file = "PyQt5_sip-12.9.1-cp310-cp310-manylinux1_x86_64.whl", hash = "sha256:5740a1770d6b92a5dca8bb0bda4620baf0d7a726beb864f69c667ddac91d6f64"},
{file = "PyQt5_sip-12.9.1-cp310-cp310-win32.whl", hash = "sha256:9699286fcdf4f75a4b91c7e4832c0f926af18d648c62a4ed72dd294c1a93705a"},
{file = "PyQt5_sip-12.9.1-cp310-cp310-win_amd64.whl", hash = "sha256:e2792af660da7479799f53028da88190ae8b4a0ad5acc2acbfd6c7bbfe110d58"},
{file = "PyQt5_sip-12.9.1-cp36-cp36m-macosx_10_6_intel.whl", hash = "sha256:7ee08ad0ebf85b935f5d8d38306f8665fff9a6026c14fc0a7d780649e889c096"},
{file = "PyQt5_sip-12.9.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:2d21420b0739df2607864e2c80ca01994bc40cb116da6ad024ea8d9f407b0356"},
{file = "PyQt5_sip-12.9.1-cp36-cp36m-win32.whl", hash = "sha256:ffd25051962c593d1c3c30188b9fbd8589ba17acd23a0202dc987bd3552fa611"},
{file = "PyQt5_sip-12.9.1-cp36-cp36m-win_amd64.whl", hash = "sha256:78ef8f1f41819661aa8e3117d6c1cd76fa14aef265e5bfd515dbfc64d412416b"},
{file = "PyQt5_sip-12.9.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:5e641182bfee0501267c55e687832e4efe05becdae9e555d3695d706009b6598"},
{file = "PyQt5_sip-12.9.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:c9a977d2835a5fbf250b00d61267dc228bdec9e20c7420d4e8d54d6f20410f87"},
{file = "PyQt5_sip-12.9.1-cp37-cp37m-win32.whl", hash = "sha256:cec6ebf0b1163b18f09bc523160c467a9528b6dca129753827ac0bc432b332ae"},
{file = "PyQt5_sip-12.9.1-cp37-cp37m-win_amd64.whl", hash = "sha256:82c1b3080db7634fa318fddbb3cfaa30e63a67bca1001a76958c31f30b774a9d"},
{file = "PyQt5_sip-12.9.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:cfaad4a773c18b963092589b1a98153d36624601de8597a4dc287e5a295d5625"},
{file = "PyQt5_sip-12.9.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:ce7a8b3af9db378c46b345d9809d481a74c4bfcd3129486c054fbdbc6a3503f9"},
{file = "PyQt5_sip-12.9.1-cp38-cp38-win32.whl", hash = "sha256:8fe5b3e4bbb8b472d05631cad21028d073f9f8eda770041449514cb3824a867f"},
{file = "PyQt5_sip-12.9.1-cp38-cp38-win_amd64.whl", hash = "sha256:5d59c4a5e856a35c41b47f5a23e1635b38cd1672f4f0122a68ebcb6889523ff2"},
{file = "PyQt5_sip-12.9.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b56aedf7b0a496e4a8bd6087566888cea448aa01c76126cdb8b140e3ff3f5d93"},
{file = "PyQt5_sip-12.9.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:53e23dcc0fc3857204abd47660e383b930941bd1aeaf3c78ed59c5c12dd48010"},
{file = "PyQt5_sip-12.9.1-cp39-cp39-win32.whl", hash = "sha256:ee188eac5fd94dfe8d9e04a9e7fbda65c3535d5709278d8b7367ebd54f00e27f"},
{file = "PyQt5_sip-12.9.1-cp39-cp39-win_amd64.whl", hash = "sha256:989d51c41456cc496cb96f0b341464932b957040d26561f0bb4cf5a0914d6b36"},
{file = "PyQt5_sip-12.9.1.tar.gz", hash = "sha256:2f24f299b44c511c23796aafbbb581bfdebf78d0905657b7cee2141b4982030e"},
]
pytest = [
{file = "pytest-5.4.3-py3-none-any.whl", hash = "sha256:5c0db86b698e8f170ba4582a492248919255fcd4c79b1ee64ace34301fb589a1"},
{file = "pytest-5.4.3.tar.gz", hash = "sha256:7979331bfcba207414f5e1263b5a0f8f521d0f457318836a7355531ed1a4c7d8"},
@ -280,8 +344,8 @@ requests = [
{file = "requests-2.27.1.tar.gz", hash = "sha256:68d7c56fd5a8999887728ef304a6d12edc7be74f1cfa47714fc8b414525c9a61"},
]
typing-extensions = [
{file = "typing_extensions-4.0.1-py3-none-any.whl", hash = "sha256:7f001e5ac290a0c0401508864c7ec868be4e701886d5b573a9528ed3973d9d3b"},
{file = "typing_extensions-4.0.1.tar.gz", hash = "sha256:4ca091dea149f945ec56afb48dae714f21e8692ef22a395223bcd328961b6a0e"},
{file = "typing_extensions-4.1.1-py3-none-any.whl", hash = "sha256:21c85e0fe4b9a155d0799430b0ad741cdce7e359660ccbd8b530613e8df88ce2"},
{file = "typing_extensions-4.1.1.tar.gz", hash = "sha256:1a9462dcc3347a79b1f1c0271fbe79e844580bb598bafa1ed208b94da3cdcd42"},
]
urllib3 = [
{file = "urllib3-1.26.8-py2.py3-none-any.whl", hash = "sha256:000ca7f471a233c2251c6c7023ee85305721bfdf18621ebff4fd17a8653427ed"},

@ -11,6 +11,7 @@ timebot = 'timebot.app:run'
[tool.poetry.dependencies]
python = "^3.6"
requests = "^2.27.1"
PyQt5 = "^5.15.6"
[tool.poetry.dev-dependencies]
pytest = "^5.2"

@ -3,9 +3,10 @@ import configparser
import datetime
import logging
import os
import signal
import sys
from timebot.constants import SIMPLE_DATETIME_FORMAT_HUMAN, PUNCH_COMMANDS, SIMPLE_DATE_FORMAT, SIMPLE_TIME_FORMAT
from timebot.constants import PUNCH_COMMANDS, DateFormats
from timebot.timebot import MobatimeApi, TimeBot, parse_user_time_input
logger = logging.getLogger()
@ -24,15 +25,18 @@ def run():
subparsers = parser.add_subparsers(help='sub-command help', dest='subparser_name')
# subparser command: `status`
parser_status = subparsers.add_parser("status", help="show your current tracking status")
subparsers.add_parser("status", help="show your current tracking status")
# subparser command: `gui`
subparsers.add_parser("gui", help="start qt5 gui")
# subparser command: `list-entries`
parser_list_entries = subparsers.add_parser("list-entries", help="use this command to list your time entries")
parser_list_entries.add_argument("--start-date",
help=f"start date filter in format `{SIMPLE_DATETIME_FORMAT_HUMAN}` "
help=f"start date filter in format `{DateFormats.SIMPLE_DATETIME.evalue}` "
f"(default: now - 10days; unset for default)")
parser_list_entries.add_argument("--end-date",
help=f"end date filter in format `{SIMPLE_DATETIME_FORMAT_HUMAN}` "
help=f"end date filter in format `{DateFormats.SIMPLE_DATETIME.evalue}` "
f"(default: now; unset for default)")
parser_list_entries.add_argument("--items", help="max items to request per page", default=20, type=int)
@ -43,8 +47,9 @@ def run():
help=f"type of time entry; this can be {', '.join(PUNCH_COMMANDS)}",
default="punch_in",
choices=PUNCH_COMMANDS)
parser_punch.add_argument("-s", help=f"timestamp in format `{SIMPLE_DATETIME_FORMAT_HUMAN}` or `now`",
default="now")
parser_punch.add_argument("-s",
help=f"timestamp in format `{DateFormats.SIMPLE_DATETIME.evalue}`, "
f"`{DateFormats.SIMPLE_TIME.evalue}` or `now`", default="now")
# subparser command: `smart-punch`
parser_smart_punch = subparsers.add_parser("smart-punch",
@ -52,8 +57,8 @@ def run():
"entries; this command tries to detect your last action and will "
"create entries in to following order: "
"punch_in -> break_start -> break_end -> punch_out")
parser_smart_punch.add_argument("-s", help=f"timestamp in format `{SIMPLE_DATETIME_FORMAT_HUMAN}` or `now`",
default="now")
parser_smart_punch.add_argument("-s", help=f"timestamp in format `{DateFormats.SIMPLE_DATETIME.evalue}`,"
f" `{DateFormats.SIMPLE_TIME.evalue}` or `now`", default="now")
args = parser.parse_args()
@ -77,8 +82,8 @@ def run():
punch_datetime = parse_user_time_input(args.s)
logger.info("running `{}` with date `{}` and time `{}`".format(
args.t,
punch_datetime.strftime(SIMPLE_DATE_FORMAT),
punch_datetime.strftime(SIMPLE_TIME_FORMAT),
punch_datetime.strftime(DateFormats.SIMPLE_DATE.value),
punch_datetime.strftime(DateFormats.SIMPLE_TIME.value),
))
getattr(tb, args.t)(punch_datetime)
elif args.subparser_name == "smart-punch":
@ -99,15 +104,15 @@ def run():
for i in data:
print("Entry: {} - DateTime: {} - Note: {}".format(i["entryName"], i["dateTime"], i["note"]))
elif args.subparser_name == "status":
tracking_data = tb.mobatime_api.get_tracking_data()
account_info = tb.mobatime_api.get_account_information()
print("Tracking Info:")
print(f" Current State: {tracking_data['actualState']}")
print(f" Last Booking: {tracking_data['lastBooking']}")
print(f" Last Booking Date: {tracking_data['lastBookingDate']}")
print("Account Infos:")
for i in account_info:
print(f" {i['accountName']}: {i['value']}")
print(tb.status())
elif args.subparser_name == "gui":
from PyQt5.QtWidgets import QApplication
from timebot.gui import TimebotMainWindow
signal.signal(signal.SIGINT, lambda *_args: QApplication.quit())
app = QApplication(sys.argv)
main_window = TimebotMainWindow(timebot=tb)
main_window.show()
sys.exit(app.exec_())
else:
logger.error("Noting done... dunno what you want!")
sys.exit(1)

@ -1,13 +1,32 @@
COMING_ENTRY_CODE_ID = 16
LEAVING_ENTRY_CODE_ID = 32
BREAK_START_ENTRY_CODE_ID = 48
BREAK_END_ENTRY_CODE_ID = 64
from enum import Enum
from types import DynamicClassAttribute
PUNCH_COMMANDS = ("punch_in", "punch_out", "break_start", "break_end")
SIMPLE_DATETIME_FORMAT = "%Y-%m-%dT%H:%M"
SIMPLE_DATETIME_FORMAT_HUMAN = "%%Y-%%m-%%dT%%H:%%M"
SIMPLE_DATE_FORMAT = "%Y-%m-%d"
SIMPLE_TIME_FORMAT = "%H:%M"
class PunchCodes(Enum):
COMING = 16
LEAVING = 32
BREAK_START = 48
BREAK_END = 64
class DateFormats(Enum):
SIMPLE_DATETIME = "%Y-%m-%dT%H:%M"
FULL_DATETIME = "%Y-%m-%dT%H:%M:%S"
SIMPLE_DATE = "%Y-%m-%d"
SIMPLE_TIME = "%H:%M"
@DynamicClassAttribute
def evalue(self):
"""
Return escaped version of value if value is string.
"%" will be escaped -> "%%"
"""
if isinstance(self._value_, (str,)):
return self._value_.replace("%", "%%")
return self._value_
PUNCH_COMMANDS = ("punch_in", "punch_out", "break_start", "break_end")
USER_AGENT = "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:94.0) Gecko/20100101 Firefox/94.0"

@ -0,0 +1,172 @@
import datetime
import logging
from PyQt5 import QtGui
from PyQt5.QtCore import QTimer, QObject, pyqtSignal, QThread, Qt
from PyQt5.QtWidgets import QWidget, QVBoxLayout, QPushButton, QLabel, QMessageBox, QInputDialog, QLineEdit, QHBoxLayout
from timebot.timebot import TimeBot, TimebotObtainPasswordError
package_logger = logging.getLogger(__name__)
class HoursPresentLabelArea(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.text_box_hours_present_label = QLabel()
self.text_box_hours_present_label.setText("Hours present:")
self.text_box_hours_present = QLabel()
self.text_box_hours_present.setStyleSheet("background : #E0E0E0")
self.text_box_hours_present.setAlignment(Qt.AlignCenter)
layout = QHBoxLayout()
layout.addWidget(self.text_box_hours_present_label)
layout.addWidget(self.text_box_hours_present)
self.setLayout(layout)
class TimebotMainWindow(QWidget):
def __init__(self, timebot: TimeBot, parent=None):
super().__init__(parent)
self.timebot = timebot
self.timebot.mobatime_api.ask_for_password = False
self.setWindowTitle("Timebot")
self.setWindowIcon(QtGui.QIcon.fromTheme('face-devilish'))
self.resize(300, 270)
self.hours_present = datetime.timedelta(microseconds=0)
self.text_box_status = QLabel()
self.text_box_hours_present_area = HoursPresentLabelArea(parent=self)
self.btn_update_status = QPushButton("Refresh Status")
layout = QVBoxLayout()
layout.addWidget(self.text_box_status)
layout.addWidget(self.text_box_hours_present_area)
layout.addWidget(self.btn_update_status)
self.setLayout(layout)
self.btn_update_status.clicked.connect(self.update_status)
self.status_timer_time = 60000 # msec -> 60s
self.status_timer = QTimer()
self.status_timer.timeout.connect(self.update_status)
self.status_timer.start(self.status_timer_time)
self.main_window_timer_time = 1000 # msec -> 1s
self.main_window_timer = QTimer()
self.main_window_timer.timeout.connect(self.update_main_window)
self.main_window_timer.start(self.main_window_timer_time)
self.error_msg = QMessageBox()
self.error_msg.setWindowTitle("Timebot ERROR")
self.error_msg.setIcon(QMessageBox.Critical)
self.error_msg.finished.connect(self.error_msg_finished)
self.password_dialog = QInputDialog()
self.update_status_running = False
self.update_status()
def update_main_window(self):
self.main_window_info_thread = QThread()
self.main_window_info_worker = MainWindowInfoWorker(self)
self.main_window_info_worker.moveToThread(self.main_window_info_thread)
self.main_window_info_thread.started.connect(self.main_window_info_worker.run)
self.main_window_info_worker.finished.connect(self.main_window_info_thread.quit)
self.main_window_info_worker.finished.connect(self.main_window_info_worker.deleteLater)
self.main_window_info_thread.finished.connect(self.main_window_info_thread.deleteLater)
self.main_window_info_thread.start()
def update_status(self):
if self.update_status_running:
return
self.status_thread = QThread()
self.status_worker = TimebotApiWorker(self, self.timebot)
self.status_worker.moveToThread(self.status_thread)
self.status_thread.started.connect(self.status_worker.run)
self.status_worker.finished.connect(self.status_thread.quit)
self.status_worker.finished.connect(self.status_worker.deleteLater)
self.status_thread.finished.connect(self.status_thread.deleteLater)
self.status_thread.finished.connect(self.update_status_finished)
self.status_worker.error.connect(lambda exp, *args: self.error_msg_show(str(exp)))
self.status_worker.obtain_password.connect(lambda *args: self.get_password(self.update_status))
self.status_thread.start()
self.update_status_started()
def get_password(self, callback: callable = None):
self.status_timer.stop()
password, ok = self.password_dialog.getText(self, "Timebot Password", 'Enter your password:',
QLineEdit.Password)
if ok:
self.timebot.mobatime_api.password = password
try:
_ = self.timebot.mobatime_api.session
except Exception as e:
self.error_msg_show(str(e))
self.timebot.mobatime_api.password = None
if callback is not None:
callback()
self.status_timer.start(self.status_timer_time)
def error_msg_show(self, error_text: str):
self.error_msg.setText(error_text)
self.status_timer.stop()
self.error_msg.exec_()
def error_msg_finished(self):
self.status_timer.start(self.status_timer_time)
def update_status_started(self):
self.btn_update_status.setEnabled(False)
self.btn_update_status.setText("Refreshing...")
self.update_status_running = True
def update_status_finished(self):
self.btn_update_status.setEnabled(True)
self.btn_update_status.setText("Refresh Status")
self.update_status_running = False
def update_hours_present(self, override: datetime.timedelta = None):
if override is not None:
self.hours_present: datetime.timedelta = override
elif self.hours_present > datetime.timedelta(seconds=self.main_window_timer_time / 100):
self.hours_present = self.hours_present + datetime.timedelta(seconds=1)
hp = self.hours_present - datetime.timedelta(microseconds=self.hours_present.microseconds)
self.text_box_hours_present_area.text_box_hours_present.setText(str(hp))
class TimebotApiWorker(QObject):
error = pyqtSignal(object)
obtain_password = pyqtSignal()
finished = pyqtSignal()
def __init__(self, tmw: TimebotMainWindow, timebot: TimeBot):
super().__init__()
self.tmw = tmw
self.timebot = timebot
def run(self):
try:
self.tmw.update_hours_present(override=self.timebot.get_hours_present())
self.tmw.text_box_status.setText(self.timebot.status())
except TimebotObtainPasswordError:
self.obtain_password.emit()
except Exception as e:
self.error.emit(e)
self.finished.emit()
class MainWindowInfoWorker(QObject):
finished = pyqtSignal()
def __init__(self, tmw: TimebotMainWindow):
super().__init__()
self.tmw = tmw
def run(self):
self.tmw.update_hours_present()
self.finished.emit()

@ -3,17 +3,19 @@ import getpass
import logging
import pickle
import sys
from typing import Union
import requests as requests
from timebot.constants import COMING_ENTRY_CODE_ID, LEAVING_ENTRY_CODE_ID, BREAK_START_ENTRY_CODE_ID, \
BREAK_END_ENTRY_CODE_ID, SIMPLE_DATE_FORMAT, SIMPLE_TIME_FORMAT, SIMPLE_DATETIME_FORMAT, \
USER_AGENT
from timebot.constants import PunchCodes, DateFormats, USER_AGENT
package_logger = logging.getLogger(__name__)
class TimebotObtainPasswordError(Exception):
pass
class TimeParseError(Exception):
pass
@ -21,20 +23,21 @@ class TimeParseError(Exception):
def parse_user_time_input(stamp: str) -> datetime.datetime:
errors = []
try:
package_logger.debug(f"trying to parse date with format \"{SIMPLE_DATETIME_FORMAT}\"")
return datetime.datetime.strptime(stamp, SIMPLE_DATETIME_FORMAT)
package_logger.debug(f"trying to parse date with format \"{DateFormats.SIMPLE_DATETIME.value}\"")
return datetime.datetime.strptime(stamp, DateFormats.SIMPLE_DATETIME.value)
except ValueError as e:
errors.append(e)
try:
package_logger.debug(f"trying to parse date with format \"{SIMPLE_TIME_FORMAT}\"")
t = datetime.datetime.strptime(stamp, SIMPLE_TIME_FORMAT)
package_logger.debug(f"trying to parse date with format \"{DateFormats.SIMPLE_TIME}\"")
t = datetime.datetime.strptime(stamp, DateFormats.SIMPLE_TIME.value)
dt = datetime.datetime.now()
dt = dt.replace(hour=0, minute=0, second=0, microsecond=0)
dt += datetime.timedelta(hours=t.hour, minutes=t.minute)
return dt
except ValueError as e:
errors.append(e)
package_logger.error(f"could not parse date with format \"{SIMPLE_DATETIME_FORMAT}\" or \"{SIMPLE_TIME_FORMAT}\"")
package_logger.error(f"could not parse date with format \"{DateFormats.SIMPLE_DATETIME.value}\" or"
f" \"{DateFormats.SIMPLE_TIME.value}\"")
raise TimeParseError(errors)
@ -45,7 +48,7 @@ class MobatimeApi:
self.baseurl = self._sanitize_baseurl(baseurl)
self.user = user
self.password = password
self._ask_for_password = ask_for_password
self.ask_for_password = ask_for_password
self._save_session = save_session
self._session = None
self._cookie_store = cookie_store
@ -74,7 +77,7 @@ class MobatimeApi:
self.logger.warning(e) # file does not exist... ignored
except (EOFError, pickle.UnpicklingError) as e:
self.logger.warning(e) # file seems to be corrupt... ignoring
request = self._session.get(self.baseurl + "Employee/GetEmployeeList")
request = self._session.get(self.baseurl + "Employee/GetEmployeeList", timeout=2)
if 400 <= request.status_code < 500:
self.logger.debug(f"got error {request.status_code}... trying to log in")
self._login(self._session)
@ -89,17 +92,21 @@ class MobatimeApi:
:raises: on status code != 2xx
"""
if self.password is None and self._ask_for_password:
if self.password is None and self.ask_for_password:
self.password = self._get_password()
elif self.password and not self.ask_for_password:
pass
else:
raise Exception("could not obtain password")
raise TimebotObtainPasswordError("Unauthorized: could not obtain password")
login_data = {
"username": self.user,
"password": self.password,
}
session.cookies.clear_session_cookies()
request = session.post(self.baseurl + "Account/LogOn", data=login_data)
request.raise_for_status()
session.post(self.baseurl + "Account/LogOn",
data=login_data,
timeout=2).raise_for_status() # This always gives 200 ... even with wrong password
session.get(self.baseurl + "Employee/GetEmployeeList", timeout=2).raise_for_status()
@staticmethod
def _get_password():
@ -156,7 +163,7 @@ class MobatimeApi:
List all employees which are obviously in the same team as you.
:return: list of employees
"""
request = self.session.get(self.baseurl + "Employee/GetEmployees")
request = self.session.get(self.baseurl + "Employee/GetEmployees", timeout=2)
request.raise_for_status()
return request.json()
@ -166,7 +173,7 @@ class MobatimeApi:
:return: all user information as dict
"""
request = self.session.get(self.baseurl + "Employee/GetUserProfileForCurrentUser")
request = self.session.get(self.baseurl + "Employee/GetUserProfileForCurrentUser", timeout=2)
request.raise_for_status()
return request.json()
@ -179,8 +186,8 @@ class MobatimeApi:
:param str note: free text note added to the Mobatime entry
:return: requests response object
"""
punch_date = punch_datetime.strftime(SIMPLE_DATE_FORMAT)
punch_time = punch_datetime.strftime(SIMPLE_TIME_FORMAT)
punch_date = punch_datetime.strftime(DateFormats.SIMPLE_DATE.value)
punch_time = punch_datetime.strftime(DateFormats.SIMPLE_TIME.value)
entry_data = {
"periode0Date": punch_date,
"periode0Time": punch_time,
@ -190,10 +197,10 @@ class MobatimeApi:
}
if note:
entry_data["note"] = note
request = self.session.post(self.baseurl + "Entry/SaveEntry", data=entry_data)
request = self.session.post(self.baseurl + "Entry/SaveEntry", data=entry_data, timeout=2)
return request
def get_entries(self, entries: int = 10,
def get_entries(self, entries: Union[str, None] = 10,
start_date: datetime.datetime = None,
end_date: datetime.datetime = None) -> list:
"""
@ -217,14 +224,14 @@ class MobatimeApi:
filters["filter"]["filters"].append({
"field": "startDate",
"operator": "gte",
"value": start_date.strftime(SIMPLE_DATETIME_FORMAT),
"value": start_date.strftime(DateFormats.SIMPLE_DATETIME.value),
})
filters["filter"]["filters"].append({
"field": "endDate",
"operator": "lte",
"value": end_date.strftime(SIMPLE_DATETIME_FORMAT),
"value": end_date.strftime(DateFormats.SIMPLE_DATETIME.value),
})
request = self.session.post(self.baseurl + "Entry/GetEntries", json=filters)
request = self.session.post(self.baseurl + "Entry/GetEntries", json=filters, timeout=2)
request.raise_for_status()
return request.json()["data"]
@ -241,7 +248,7 @@ class MobatimeApi:
:return: dict of the info mentioned above
"""
request = self.session.get(self.baseurl + "Tracking/GetTrackingData")
request = self.session.get(self.baseurl + "Tracking/GetTrackingData", timeout=2)
request.raise_for_status()
return request.json()
@ -254,7 +261,7 @@ class MobatimeApi:
:return: list with mentioned infos
"""
request = self.session.get(self.baseurl + "Employee/GetAccountInformation")
request = self.session.get(self.baseurl + "Employee/GetAccountInformation", timeout=2)
request.raise_for_status()
return request.json()
@ -269,30 +276,38 @@ class TimeBot:
:param datetime.datetime punch_datetime: datetime object
:raises: on status code != 2xx
"""
self.mobatime_api.save_entry(punch_datetime, COMING_ENTRY_CODE_ID, note="da").raise_for_status()
self.mobatime_api.save_entry(punch_datetime, PunchCodes.COMING.value, note="da").raise_for_status()
def punch_out(self, punch_datetime: datetime.datetime):
"""
:param datetime.datetime punch_datetime: datetime object
:raises: on status code != 2xx
"""
self.mobatime_api.save_entry(punch_datetime, LEAVING_ENTRY_CODE_ID, note="weg").raise_for_status()
self.mobatime_api.save_entry(punch_datetime, PunchCodes.LEAVING.value, note="weg").raise_for_status()
def break_start(self, punch_datetime: datetime.datetime):
"""
:param datetime.datetime punch_datetime: datetime object
:raises: on status code != 2xx
"""
self.mobatime_api.save_entry(punch_datetime, BREAK_START_ENTRY_CODE_ID, note="pause").raise_for_status()
self.mobatime_api.save_entry(punch_datetime, PunchCodes.BREAK_START.value, note="pause").raise_for_status()
def break_end(self, punch_datetime: datetime.datetime):
"""
:param datetime.datetime punch_datetime: datetime object
:raises: on status code != 2xx
"""
self.mobatime_api.save_entry(punch_datetime, BREAK_END_ENTRY_CODE_ID, note="pause ende").raise_for_status()
self.mobatime_api.save_entry(punch_datetime, PunchCodes.BREAK_END.value, note="pause ende").raise_for_status()
def smart_punch(self, punch_datetime):
def smart_punch(self, punch_datetime: datetime.datetime) -> None:
"""
Perform smart punch for today.
This tries to detect your last action and will create entries in to following order:
punch_in -> break_start -> break_end -> punch_out
:param datetime.datetime punch_datetime: a datetime object to identify the current day aka. today
"""
last_punch = self.mobatime_api.get_entries(
1,
start_date=punch_datetime.replace(hour=0, minute=0, second=0, microsecond=0),
@ -302,16 +317,16 @@ class TimeBot:
if not last_punch or last_punch[0]["entryNumber"] is None:
self.logger.debug("could not detect any time entry for today... punching in")
method = "punch_in"
elif last_punch[0]["entryNumber"] == COMING_ENTRY_CODE_ID:
elif last_punch[0]["entryNumber"] == PunchCodes.COMING.value:
self.logger.debug("your last entry was `punch_in`... starting break")
method = "break_start"
elif last_punch[0]["entryNumber"] == BREAK_START_ENTRY_CODE_ID:
elif last_punch[0]["entryNumber"] == PunchCodes.BREAK_START.value:
self.logger.debug("your last entry was `break_start`... ending break")
method = "break_end"
elif last_punch[0]["entryNumber"] == BREAK_END_ENTRY_CODE_ID:
elif last_punch[0]["entryNumber"] == PunchCodes.BREAK_END.value:
self.logger.debug("your last entry was `break_end`... punching out")
method = "punch_out"
elif last_punch[0]["entryNumber"] == LEAVING_ENTRY_CODE_ID:
elif last_punch[0]["entryNumber"] == PunchCodes.LEAVING.value:
self.logger.error("your last entry was `punch_out`... punching in again with this command is not supported")
sys.exit(1)
if method is None:
@ -320,7 +335,63 @@ class TimeBot:
exit(1)
self.logger.info("running `{}` with date `{}` and time `{}`".format(
method,
punch_datetime.strftime(SIMPLE_DATE_FORMAT),
punch_datetime.strftime(SIMPLE_TIME_FORMAT),
punch_datetime.strftime(DateFormats.SIMPLE_DATE.value),
punch_datetime.strftime(DateFormats.SIMPLE_TIME.value),
))
getattr(self, method)(punch_datetime)
def status(self) -> str:
"""
Get and format Mobatime tracking data and and account information.
The output ist formatted to print it to the console.
:return: preformatted status text
"""
tracking_data = self.mobatime_api.get_tracking_data()
account_info = self.mobatime_api.get_account_information()
ret = "Tracking Info:\n"
ret += f" Current State: {tracking_data['actualState']}\n"
ret += f" Last Booking: {tracking_data['lastBooking']}\n"
ret += f" Last Booking Date: {tracking_data['lastBookingDate']}\n"
ret += "Account Infos:\n"
for i in account_info:
ret += f" {i['accountName']}: {i['value']}\n"
return ret
def get_hours_present(self) -> datetime.timedelta:
"""
Calculate today's hours present.
:return: timedelta object with today's hours present
"""
now = datetime.datetime.now()
punches = self.mobatime_api.get_entries(
None,
start_date=now.replace(hour=0, minute=0, second=0, microsecond=0),
end_date=now.replace(hour=0, minute=0, second=0, microsecond=0),
)
punch_out_time = now
if punches and punches[0]["entryNumber"] == PunchCodes.LEAVING.value:
punch_out_time = datetime.datetime.strptime(punches.pop(0)["dateTime"], DateFormats.FULL_DATETIME.value)
punch_in_time = None
if punches and punches[-1]["entryNumber"] == PunchCodes.COMING.value:
punch_in_time = datetime.datetime.strptime(punches.pop(-1)["dateTime"], DateFormats.FULL_DATETIME.value)
if punch_in_time is None:
return datetime.timedelta(seconds=0)
breaks = []
end = now
for i in punches:
if i["entryNumber"] == PunchCodes.BREAK_END.value:
end = datetime.datetime.strptime(i["dateTime"], DateFormats.FULL_DATETIME.value)
if i["entryNumber"] == PunchCodes.BREAK_START.value:
start = datetime.datetime.strptime(i["dateTime"], DateFormats.FULL_DATETIME.value)
breaks.append(end - start)
end = now
time_delta = punch_out_time - punch_in_time
for i in breaks:
time_delta = time_delta - i
return time_delta

Loading…
Cancel
Save