summaryrefslogtreecommitdiff
path: root/overlays/worktime
diff options
context:
space:
mode:
Diffstat (limited to 'overlays/worktime')
-rw-r--r--overlays/worktime/.envrc4
-rw-r--r--overlays/worktime/.gitignore2
-rw-r--r--overlays/worktime/default.nix26
-rw-r--r--overlays/worktime/poetry.lock248
-rw-r--r--overlays/worktime/pyproject.toml41
-rw-r--r--overlays/worktime/uv.lock248
-rwxr-xr-xoverlays/worktime/worktime/__main__.py523
7 files changed, 631 insertions, 461 deletions
diff --git a/overlays/worktime/.envrc b/overlays/worktime/.envrc
new file mode 100644
index 00000000..2c909235
--- /dev/null
+++ b/overlays/worktime/.envrc
@@ -0,0 +1,4 @@
1use flake
2
3[[ -d ".venv" ]] || ( uv venv && uv sync )
4. .venv/bin/activate
diff --git a/overlays/worktime/.gitignore b/overlays/worktime/.gitignore
new file mode 100644
index 00000000..4ccfae70
--- /dev/null
+++ b/overlays/worktime/.gitignore
@@ -0,0 +1,2 @@
1.venv
2**/__pycache__
diff --git a/overlays/worktime/default.nix b/overlays/worktime/default.nix
index 5ecdf149..579cf7ad 100644
--- a/overlays/worktime/default.nix
+++ b/overlays/worktime/default.nix
@@ -1,13 +1,19 @@
1{ prev, ... }: 1{ prev, final, flake, flakeInputs, ... }:
2 2
3with prev.poetry2nix; 3let
4 4 workspace = flakeInputs.uv2nix.lib.workspace.loadWorkspace { workspaceRoot = ./.; };
5{ 5 pythonSet = flake.lib.pythonSet {
6 worktime = mkPoetryApplication { 6 pkgs = final;
7 python = prev.python310; 7 python = final.python312;
8 8 overlay = workspace.mkPyprojectOverlay {
9 projectDir = cleanPythonSources { src = ./.; }; 9 sourcePreference = "wheel";
10 10 };
11 meta.mainProgram = "worktime";
12 }; 11 };
12 virtualEnv = pythonSet.mkVirtualEnv "worktime" workspace.deps.default;
13in {
14 worktime = virtualEnv.overrideAttrs (oldAttrs: {
15 meta = (oldAttrs.meta or {}) // {
16 mainProgram = "worktime";
17 };
18 });
13} 19}
diff --git a/overlays/worktime/poetry.lock b/overlays/worktime/poetry.lock
deleted file mode 100644
index 54182b09..00000000
--- a/overlays/worktime/poetry.lock
+++ /dev/null
@@ -1,248 +0,0 @@
1# This file is automatically @generated by Poetry 1.6.1 and should not be changed by hand.
2
3[[package]]
4name = "backoff"
5version = "2.2.1"
6description = "Function decoration for backoff and retry"
7optional = false
8python-versions = ">=3.7,<4.0"
9files = [
10 {file = "backoff-2.2.1-py3-none-any.whl", hash = "sha256:63579f9a0628e06278f7e47b7d7d5b6ce20dc65c5e96a6f3ca99a6adca0396e8"},
11 {file = "backoff-2.2.1.tar.gz", hash = "sha256:03f829f5bb1923180821643f8753b0502c3b682293992485b0eef2807afa5cba"},
12]
13
14[[package]]
15name = "certifi"
16version = "2022.12.7"
17description = "Python package for providing Mozilla's CA Bundle."
18optional = false
19python-versions = ">=3.6"
20files = [
21 {file = "certifi-2022.12.7-py3-none-any.whl", hash = "sha256:4ad3232f5e926d6718ec31cfc1fcadfde020920e278684144551c91769c7bc18"},
22 {file = "certifi-2022.12.7.tar.gz", hash = "sha256:35824b4c3a97115964b408844d64aa14db1cc518f6562e8d7261699d1350a9e3"},
23]
24
25[[package]]
26name = "charset-normalizer"
27version = "3.1.0"
28description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
29optional = false
30python-versions = ">=3.7.0"
31files = [
32 {file = "charset-normalizer-3.1.0.tar.gz", hash = "sha256:34e0a2f9c370eb95597aae63bf85eb5e96826d81e3dcf88b8886012906f509b5"},
33 {file = "charset_normalizer-3.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e0ac8959c929593fee38da1c2b64ee9778733cdf03c482c9ff1d508b6b593b2b"},
34 {file = "charset_normalizer-3.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d7fc3fca01da18fbabe4625d64bb612b533533ed10045a2ac3dd194bfa656b60"},
35 {file = "charset_normalizer-3.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:04eefcee095f58eaabe6dc3cc2262f3bcd776d2c67005880894f447b3f2cb9c1"},
36 {file = "charset_normalizer-3.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:20064ead0717cf9a73a6d1e779b23d149b53daf971169289ed2ed43a71e8d3b0"},
37 {file = "charset_normalizer-3.1.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1435ae15108b1cb6fffbcea2af3d468683b7afed0169ad718451f8db5d1aff6f"},
38 {file = "charset_normalizer-3.1.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c84132a54c750fda57729d1e2599bb598f5fa0344085dbde5003ba429a4798c0"},
39 {file = "charset_normalizer-3.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75f2568b4189dda1c567339b48cba4ac7384accb9c2a7ed655cd86b04055c795"},
40 {file = "charset_normalizer-3.1.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:11d3bcb7be35e7b1bba2c23beedac81ee893ac9871d0ba79effc7fc01167db6c"},
41 {file = "charset_normalizer-3.1.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:891cf9b48776b5c61c700b55a598621fdb7b1e301a550365571e9624f270c203"},
42 {file = "charset_normalizer-3.1.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:5f008525e02908b20e04707a4f704cd286d94718f48bb33edddc7d7b584dddc1"},
43 {file = "charset_normalizer-3.1.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:b06f0d3bf045158d2fb8837c5785fe9ff9b8c93358be64461a1089f5da983137"},
44 {file = "charset_normalizer-3.1.0-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:49919f8400b5e49e961f320c735388ee686a62327e773fa5b3ce6721f7e785ce"},
45 {file = "charset_normalizer-3.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:22908891a380d50738e1f978667536f6c6b526a2064156203d418f4856d6e86a"},
46 {file = "charset_normalizer-3.1.0-cp310-cp310-win32.whl", hash = "sha256:12d1a39aa6b8c6f6248bb54550efcc1c38ce0d8096a146638fd4738e42284448"},
47 {file = "charset_normalizer-3.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:65ed923f84a6844de5fd29726b888e58c62820e0769b76565480e1fdc3d062f8"},
48 {file = "charset_normalizer-3.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9a3267620866c9d17b959a84dd0bd2d45719b817245e49371ead79ed4f710d19"},
49 {file = "charset_normalizer-3.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6734e606355834f13445b6adc38b53c0fd45f1a56a9ba06c2058f86893ae8017"},
50 {file = "charset_normalizer-3.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f8303414c7b03f794347ad062c0516cee0e15f7a612abd0ce1e25caf6ceb47df"},
51 {file = "charset_normalizer-3.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aaf53a6cebad0eae578f062c7d462155eada9c172bd8c4d250b8c1d8eb7f916a"},
52 {file = "charset_normalizer-3.1.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3dc5b6a8ecfdc5748a7e429782598e4f17ef378e3e272eeb1340ea57c9109f41"},
53 {file = "charset_normalizer-3.1.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e1b25e3ad6c909f398df8921780d6a3d120d8c09466720226fc621605b6f92b1"},
54 {file = "charset_normalizer-3.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0ca564606d2caafb0abe6d1b5311c2649e8071eb241b2d64e75a0d0065107e62"},
55 {file = "charset_normalizer-3.1.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b82fab78e0b1329e183a65260581de4375f619167478dddab510c6c6fb04d9b6"},
56 {file = "charset_normalizer-3.1.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:bd7163182133c0c7701b25e604cf1611c0d87712e56e88e7ee5d72deab3e76b5"},
57 {file = "charset_normalizer-3.1.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:11d117e6c63e8f495412d37e7dc2e2fff09c34b2d09dbe2bee3c6229577818be"},
58 {file = "charset_normalizer-3.1.0-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:cf6511efa4801b9b38dc5546d7547d5b5c6ef4b081c60b23e4d941d0eba9cbeb"},
59 {file = "charset_normalizer-3.1.0-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:abc1185d79f47c0a7aaf7e2412a0eb2c03b724581139193d2d82b3ad8cbb00ac"},
60 {file = "charset_normalizer-3.1.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:cb7b2ab0188829593b9de646545175547a70d9a6e2b63bf2cd87a0a391599324"},
61 {file = "charset_normalizer-3.1.0-cp311-cp311-win32.whl", hash = "sha256:c36bcbc0d5174a80d6cccf43a0ecaca44e81d25be4b7f90f0ed7bcfbb5a00909"},
62 {file = "charset_normalizer-3.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:cca4def576f47a09a943666b8f829606bcb17e2bc2d5911a46c8f8da45f56755"},
63 {file = "charset_normalizer-3.1.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0c95f12b74681e9ae127728f7e5409cbbef9cd914d5896ef238cc779b8152373"},
64 {file = "charset_normalizer-3.1.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fca62a8301b605b954ad2e9c3666f9d97f63872aa4efcae5492baca2056b74ab"},
65 {file = "charset_normalizer-3.1.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ac0aa6cd53ab9a31d397f8303f92c42f534693528fafbdb997c82bae6e477ad9"},
66 {file = "charset_normalizer-3.1.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c3af8e0f07399d3176b179f2e2634c3ce9c1301379a6b8c9c9aeecd481da494f"},
67 {file = "charset_normalizer-3.1.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a5fc78f9e3f501a1614a98f7c54d3969f3ad9bba8ba3d9b438c3bc5d047dd28"},
68 {file = "charset_normalizer-3.1.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:628c985afb2c7d27a4800bfb609e03985aaecb42f955049957814e0491d4006d"},
69 {file = "charset_normalizer-3.1.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:74db0052d985cf37fa111828d0dd230776ac99c740e1a758ad99094be4f1803d"},
70 {file = "charset_normalizer-3.1.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:1e8fcdd8f672a1c4fc8d0bd3a2b576b152d2a349782d1eb0f6b8e52e9954731d"},
71 {file = "charset_normalizer-3.1.0-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:04afa6387e2b282cf78ff3dbce20f0cc071c12dc8f685bd40960cc68644cfea6"},
72 {file = "charset_normalizer-3.1.0-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:dd5653e67b149503c68c4018bf07e42eeed6b4e956b24c00ccdf93ac79cdff84"},
73 {file = "charset_normalizer-3.1.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:d2686f91611f9e17f4548dbf050e75b079bbc2a82be565832bc8ea9047b61c8c"},
74 {file = "charset_normalizer-3.1.0-cp37-cp37m-win32.whl", hash = "sha256:4155b51ae05ed47199dc5b2a4e62abccb274cee6b01da5b895099b61b1982974"},
75 {file = "charset_normalizer-3.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:322102cdf1ab682ecc7d9b1c5eed4ec59657a65e1c146a0da342b78f4112db23"},
76 {file = "charset_normalizer-3.1.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e633940f28c1e913615fd624fcdd72fdba807bf53ea6925d6a588e84e1151531"},
77 {file = "charset_normalizer-3.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:3a06f32c9634a8705f4ca9946d667609f52cf130d5548881401f1eb2c39b1e2c"},
78 {file = "charset_normalizer-3.1.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7381c66e0561c5757ffe616af869b916c8b4e42b367ab29fedc98481d1e74e14"},
79 {file = "charset_normalizer-3.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3573d376454d956553c356df45bb824262c397c6e26ce43e8203c4c540ee0acb"},
80 {file = "charset_normalizer-3.1.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e89df2958e5159b811af9ff0f92614dabf4ff617c03a4c1c6ff53bf1c399e0e1"},
81 {file = "charset_normalizer-3.1.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:78cacd03e79d009d95635e7d6ff12c21eb89b894c354bd2b2ed0b4763373693b"},
82 {file = "charset_normalizer-3.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:de5695a6f1d8340b12a5d6d4484290ee74d61e467c39ff03b39e30df62cf83a0"},
83 {file = "charset_normalizer-3.1.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1c60b9c202d00052183c9be85e5eaf18a4ada0a47d188a83c8f5c5b23252f649"},
84 {file = "charset_normalizer-3.1.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:f645caaf0008bacf349875a974220f1f1da349c5dbe7c4ec93048cdc785a3326"},
85 {file = "charset_normalizer-3.1.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:ea9f9c6034ea2d93d9147818f17c2a0860d41b71c38b9ce4d55f21b6f9165a11"},
86 {file = "charset_normalizer-3.1.0-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:80d1543d58bd3d6c271b66abf454d437a438dff01c3e62fdbcd68f2a11310d4b"},
87 {file = "charset_normalizer-3.1.0-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:73dc03a6a7e30b7edc5b01b601e53e7fc924b04e1835e8e407c12c037e81adbd"},
88 {file = "charset_normalizer-3.1.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6f5c2e7bc8a4bf7c426599765b1bd33217ec84023033672c1e9a8b35eaeaaaf8"},
89 {file = "charset_normalizer-3.1.0-cp38-cp38-win32.whl", hash = "sha256:12a2b561af122e3d94cdb97fe6fb2bb2b82cef0cdca131646fdb940a1eda04f0"},
90 {file = "charset_normalizer-3.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:3160a0fd9754aab7d47f95a6b63ab355388d890163eb03b2d2b87ab0a30cfa59"},
91 {file = "charset_normalizer-3.1.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:38e812a197bf8e71a59fe55b757a84c1f946d0ac114acafaafaf21667a7e169e"},
92 {file = "charset_normalizer-3.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6baf0baf0d5d265fa7944feb9f7451cc316bfe30e8df1a61b1bb08577c554f31"},
93 {file = "charset_normalizer-3.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8f25e17ab3039b05f762b0a55ae0b3632b2e073d9c8fc88e89aca31a6198e88f"},
94 {file = "charset_normalizer-3.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3747443b6a904001473370d7810aa19c3a180ccd52a7157aacc264a5ac79265e"},
95 {file = "charset_normalizer-3.1.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b116502087ce8a6b7a5f1814568ccbd0e9f6cfd99948aa59b0e241dc57cf739f"},
96 {file = "charset_normalizer-3.1.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d16fd5252f883eb074ca55cb622bc0bee49b979ae4e8639fff6ca3ff44f9f854"},
97 {file = "charset_normalizer-3.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21fa558996782fc226b529fdd2ed7866c2c6ec91cee82735c98a197fae39f706"},
98 {file = "charset_normalizer-3.1.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6f6c7a8a57e9405cad7485f4c9d3172ae486cfef1344b5ddd8e5239582d7355e"},
99 {file = "charset_normalizer-3.1.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ac3775e3311661d4adace3697a52ac0bab17edd166087d493b52d4f4f553f9f0"},
100 {file = "charset_normalizer-3.1.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:10c93628d7497c81686e8e5e557aafa78f230cd9e77dd0c40032ef90c18f2230"},
101 {file = "charset_normalizer-3.1.0-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:6f4f4668e1831850ebcc2fd0b1cd11721947b6dc7c00bf1c6bd3c929ae14f2c7"},
102 {file = "charset_normalizer-3.1.0-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:0be65ccf618c1e7ac9b849c315cc2e8a8751d9cfdaa43027d4f6624bd587ab7e"},
103 {file = "charset_normalizer-3.1.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:53d0a3fa5f8af98a1e261de6a3943ca631c526635eb5817a87a59d9a57ebf48f"},
104 {file = "charset_normalizer-3.1.0-cp39-cp39-win32.whl", hash = "sha256:a04f86f41a8916fe45ac5024ec477f41f886b3c435da2d4e3d2709b22ab02af1"},
105 {file = "charset_normalizer-3.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:830d2948a5ec37c386d3170c483063798d7879037492540f10a475e3fd6f244b"},
106 {file = "charset_normalizer-3.1.0-py3-none-any.whl", hash = "sha256:3d9098b479e78c85080c98e1e35ff40b4a31d8953102bb0fd7d1b6f8a2111a3d"},
107]
108
109[[package]]
110name = "idna"
111version = "3.4"
112description = "Internationalized Domain Names in Applications (IDNA)"
113optional = false
114python-versions = ">=3.5"
115files = [
116 {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"},
117 {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"},
118]
119
120[[package]]
121name = "jsonpickle"
122version = "3.0.2"
123description = "Python library for serializing any arbitrary object graph into JSON"
124optional = false
125python-versions = ">=3.7"
126files = [
127 {file = "jsonpickle-3.0.2-py3-none-any.whl", hash = "sha256:4a8442d97ca3f77978afa58068768dba7bff2dbabe79a9647bc3cdafd4ef019f"},
128 {file = "jsonpickle-3.0.2.tar.gz", hash = "sha256:e37abba4bfb3ca4a4647d28bb9f4706436f7b46c8a8333b4a718abafa8e46b37"},
129]
130
131[package.extras]
132docs = ["jaraco.packaging (>=3.2)", "rst.linker (>=1.9)", "sphinx"]
133testing = ["ecdsa", "feedparser", "gmpy2", "numpy", "pandas", "pymongo", "pytest (>=3.5,!=3.7.3)", "pytest-black-multipy", "pytest-checkdocs (>=1.2.3)", "pytest-cov", "pytest-flake8 (>=1.1.1)", "scikit-learn", "sqlalchemy"]
134testing-libs = ["simplejson", "ujson"]
135
136[[package]]
137name = "python-dateutil"
138version = "2.8.2"
139description = "Extensions to the standard Python datetime module"
140optional = false
141python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7"
142files = [
143 {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"},
144 {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"},
145]
146
147[package.dependencies]
148six = ">=1.5"
149
150[[package]]
151name = "pyxdg"
152version = "0.28"
153description = "PyXDG contains implementations of freedesktop.org standards in python."
154optional = false
155python-versions = "*"
156files = [
157 {file = "pyxdg-0.28-py2.py3-none-any.whl", hash = "sha256:bdaf595999a0178ecea4052b7f4195569c1ff4d344567bccdc12dfdf02d545ab"},
158 {file = "pyxdg-0.28.tar.gz", hash = "sha256:3267bb3074e934df202af2ee0868575484108581e6f3cb006af1da35395e88b4"},
159]
160
161[[package]]
162name = "requests"
163version = "2.28.2"
164description = "Python HTTP for Humans."
165optional = false
166python-versions = ">=3.7, <4"
167files = [
168 {file = "requests-2.28.2-py3-none-any.whl", hash = "sha256:64299f4909223da747622c030b781c0d7811e359c37124b4bd368fb8c6518baa"},
169 {file = "requests-2.28.2.tar.gz", hash = "sha256:98b1b2782e3c6c4904938b84c0eb932721069dfdb9134313beff7c83c2df24bf"},
170]
171
172[package.dependencies]
173certifi = ">=2017.4.17"
174charset-normalizer = ">=2,<4"
175idna = ">=2.5,<4"
176urllib3 = ">=1.21.1,<1.27"
177
178[package.extras]
179socks = ["PySocks (>=1.5.6,!=1.5.7)"]
180use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"]
181
182[[package]]
183name = "six"
184version = "1.16.0"
185description = "Python 2 and 3 compatibility utilities"
186optional = false
187python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
188files = [
189 {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"},
190 {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"},
191]
192
193[[package]]
194name = "tabulate"
195version = "0.9.0"
196description = "Pretty-print tabular data"
197optional = false
198python-versions = ">=3.7"
199files = [
200 {file = "tabulate-0.9.0-py3-none-any.whl", hash = "sha256:024ca478df22e9340661486f85298cff5f6dcdba14f3813e8830015b9ed1948f"},
201 {file = "tabulate-0.9.0.tar.gz", hash = "sha256:0095b12bf5966de529c0feb1fa08671671b3368eec77d7ef7ab114be2c068b3c"},
202]
203
204[package.extras]
205widechars = ["wcwidth"]
206
207[[package]]
208name = "toml"
209version = "0.10.2"
210description = "Python Library for Tom's Obvious, Minimal Language"
211optional = false
212python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
213files = [
214 {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"},
215 {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"},
216]
217
218[[package]]
219name = "uritools"
220version = "4.0.1"
221description = "URI parsing, classification and composition"
222optional = false
223python-versions = "~=3.7"
224files = [
225 {file = "uritools-4.0.1-py3-none-any.whl", hash = "sha256:d122d394ed6e6e15ac0fddba6a5b19e9fa204e7797507815cbfb0e1455ac0475"},
226 {file = "uritools-4.0.1.tar.gz", hash = "sha256:efc5c3a6de05404850685a8d3f34da8476b56aa3516fbf8eff5c8704c7a2826f"},
227]
228
229[[package]]
230name = "urllib3"
231version = "1.26.15"
232description = "HTTP library with thread-safe connection pooling, file post, and more."
233optional = false
234python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*"
235files = [
236 {file = "urllib3-1.26.15-py2.py3-none-any.whl", hash = "sha256:aa751d169e23c7479ce47a0cb0da579e3ede798f994f5816a74e4f4500dcea42"},
237 {file = "urllib3-1.26.15.tar.gz", hash = "sha256:8a388717b9476f934a21484e8c8e61875ab60644d29b9b39e11e4b9dc1c6b305"},
238]
239
240[package.extras]
241brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)", "brotlipy (>=0.6.0)"]
242secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "ipaddress", "pyOpenSSL (>=0.14)", "urllib3-secure-extra"]
243socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"]
244
245[metadata]
246lock-version = "2.0"
247python-versions = "^3.10"
248content-hash = "d9137b4f8e37bba934abf732e4a2aeeb9924c4b6576830d8ae08bdb43b4e147f"
diff --git a/overlays/worktime/pyproject.toml b/overlays/worktime/pyproject.toml
index 08002d4d..42da51f5 100644
--- a/overlays/worktime/pyproject.toml
+++ b/overlays/worktime/pyproject.toml
@@ -1,23 +1,28 @@
1[tool.poetry] 1[project]
2name = "worktime" 2name = "worktime"
3version = "0.1.0" 3version = "1.0.0"
4description = "" 4requires-python = "~=3.12"
5authors = ["Gregor Kleen <gkleen@yggdrasil.li>"] 5dependencies = [
6 "pyxdg>=0.28,<0.29",
7 "python-dateutil>=2.9.0.post0,<3",
8 "uritools>=4.0.3,<5",
9 "requests>=2.32.3,<3",
10 "tabulate>=0.9.0,<0.10",
11 "toml>=0.10.2,<0.11",
12 "jsonpickle>=4.0.5,<5",
13 "frozendict>=2.4.6",
14 "atomicwriter>=0.2.5",
15 "desktop-notify>=1.3.3",
16]
6 17
7[tool.poetry.dependencies] 18[project.scripts]
8python = "^3.10"
9pyxdg = "^0.28"
10python-dateutil = "^2.8.2"
11uritools = "^4.0.1"
12requests = "^2.28.2"
13tabulate = "^0.9.0"
14backoff = "^2.2.1"
15toml = "^0.10.2"
16jsonpickle = "^3.0.2"
17
18[tool.poetry.scripts]
19worktime = "worktime.__main__:main" 19worktime = "worktime.__main__:main"
20worktime-ui = "worktime.__main__:ui"
21worktime-stop = "worktime.__main__:stop"
20 22
21[build-system] 23[build-system]
22requires = ["poetry-core"] 24requires = ["hatchling"]
23build-backend = "poetry.core.masonry.api" \ No newline at end of file 25build-backend = "hatchling.build"
26
27[dependency-groups]
28dev = []
diff --git a/overlays/worktime/uv.lock b/overlays/worktime/uv.lock
new file mode 100644
index 00000000..39de4ccf
--- /dev/null
+++ b/overlays/worktime/uv.lock
@@ -0,0 +1,248 @@
1version = 1
2revision = 2
3requires-python = ">=3.12, <4"
4
5[[package]]
6name = "atomicwriter"
7version = "0.2.5"
8source = { registry = "https://pypi.org/simple" }
9sdist = { url = "https://files.pythonhosted.org/packages/50/b4/dd04e186eb244d1ed84b1d0ebfba19ddc7f8886b98e345aaca4208b031d2/atomicwriter-0.2.5.tar.gz", hash = "sha256:5ced6afb0579377a13e191b17a16115e14c30ec00e6c38b60403f58235a867af", size = 64990, upload-time = "2025-05-24T20:35:42.538Z" }
10wheels = [
11 { url = "https://files.pythonhosted.org/packages/99/7c/672a0de09b0b355a2ffa521ef25cf106f1984823379dee37f7305fdc1774/atomicwriter-0.2.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5c1fab874e62ebe96f1af0e965dc1e92c4c1ef2e2e9612a444371b8fc751ec43", size = 234141, upload-time = "2025-05-24T20:34:32.74Z" },
12 { url = "https://files.pythonhosted.org/packages/b9/0c/e1c5bad033284c212c0a77121b48dd4147f80e9a7cd82a9d2ce0a2160901/atomicwriter-0.2.5-cp312-cp312-macosx_11_0_x86_64.whl", hash = "sha256:8dbb67cc730be7d6bdfd5e991271bc17052be8fb2e4fa27854b47d8a76d36349", size = 245788, upload-time = "2025-05-24T20:34:33.897Z" },
13 { url = "https://files.pythonhosted.org/packages/f4/d3/7036e203cc5fc4c49bf916b4ba158e0d2779de127afad5963edd7e3b9400/atomicwriter-0.2.5-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:a4e7f81932839c738425dc96ad98e4a7511b740cd3d75f480bfabbcf8e6f7eae", size = 260428, upload-time = "2025-05-24T20:34:35.533Z" },
14 { url = "https://files.pythonhosted.org/packages/e5/b9/9a4d235a8d67fb442302dc0f3ea2394b7bd994bfc99b1dc0f744c7852418/atomicwriter-0.2.5-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:de37a3a5d1b57b719cfb0b81a11cab2114acfdc2c36051bf0af72d05eb644411", size = 263648, upload-time = "2025-05-24T20:34:36.72Z" },
15 { url = "https://files.pythonhosted.org/packages/71/7c/32d4ddad53375de42f3e972bb0633ec76f2c31772f2e508479d4788651d9/atomicwriter-0.2.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0b925e55750092fd482565b6068b8c8366fd79de526681af9e58eb209f0deeca", size = 323775, upload-time = "2025-05-24T20:34:37.968Z" },
16 { url = "https://files.pythonhosted.org/packages/06/fe/6a226368a3f7ea30001fbd165f6a97f28c8f1a884896357b3d694983f5d2/atomicwriter-0.2.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:538f78f25e01584535782397211c66b8b3c9de90c2d1fc01a668ddce73dd0cb2", size = 340819, upload-time = "2025-05-24T20:34:39.63Z" },
17 { url = "https://files.pythonhosted.org/packages/92/95/b035b2296c483fde5392c629e0b6e3844eba6e54ea965c4b8827379b0893/atomicwriter-0.2.5-cp312-cp312-win_amd64.whl", hash = "sha256:1d2d49a1b94ea7b289be9f7134d756bfb0bbf53eb0e58411334ed1b9958abe5e", size = 152789, upload-time = "2025-05-24T20:34:40.905Z" },
18 { url = "https://files.pythonhosted.org/packages/da/25/caa0959ae8ce24763e24e1f45be6cb897414545d224a155f929d496d6812/atomicwriter-0.2.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3f5490fd5bec378509521f7c2a19a64031a0de07d368d76733c3f76a0b9f026b", size = 233830, upload-time = "2025-05-24T20:34:42.532Z" },
19 { url = "https://files.pythonhosted.org/packages/d2/76/3c41bfd4fd74bc63bec29f05a806a767258eea7cf151496b4ab015cb5323/atomicwriter-0.2.5-cp313-cp313-macosx_11_0_x86_64.whl", hash = "sha256:a4dada83ff1255c7e640363cc2a4399ab9a822d4dbc9c18f55bbf0c8b12ce056", size = 245461, upload-time = "2025-05-24T20:34:44.454Z" },
20 { url = "https://files.pythonhosted.org/packages/c3/1e/5512dbdfdc3f4ab12f5923c50ae4765cc2fc65a9f112bb9dccbcbe60b395/atomicwriter-0.2.5-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:ef2cf15e67513f05ad37d4cec48e403982c6b3c07f491472effd76d2157de7e2", size = 259892, upload-time = "2025-05-24T20:34:45.688Z" },
21 { url = "https://files.pythonhosted.org/packages/e5/1d/2382b6cacb119115828eb519697a555900bcfdb062efeb0f82603295402d/atomicwriter-0.2.5-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:73618f74c3c5f5401d3da0a3cd3043f23de5b6bb4a3d85bc580940a441355d25", size = 263125, upload-time = "2025-05-24T20:34:47.205Z" },
22 { url = "https://files.pythonhosted.org/packages/07/d7/c4d68386161870db4a8d0452f0655a19902fa435b749c12e6ef800e89b19/atomicwriter-0.2.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bbd5eda80710ddac7aefb421c79cef6b905852a827e764f0f12fcbaa88919f7a", size = 323503, upload-time = "2025-05-24T20:34:48.417Z" },
23 { url = "https://files.pythonhosted.org/packages/b7/08/0fc03c0736ab8466e1b47a3ee17a528da18019cff93b7c4c2b33df82c19e/atomicwriter-0.2.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b4776aaca40bc3040c3716c2adad74625c42285083ff31e8bf24a95315225c7b", size = 340156, upload-time = "2025-05-24T20:34:50.389Z" },
24 { url = "https://files.pythonhosted.org/packages/fa/09/7ba888cf4d90bcabd9e82db3bdb9de50e4ef072e0ea0d375cd1931b79349/atomicwriter-0.2.5-cp313-cp313-win_amd64.whl", hash = "sha256:225ed1fbfa1996d9b0b2252f8a5d81263e51cbc797086d830f488c35b1d2ab42", size = 152274, upload-time = "2025-05-24T20:34:51.785Z" },
25 { url = "https://files.pythonhosted.org/packages/2a/70/07d2ba2e0a126cfecfbfed46baf599c9e2155f4c8338fed4d3ae0041b133/atomicwriter-0.2.5-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:63b55982cfa47232f179689933bf003eefb2bd33464235883ed3ce7322cf38f3", size = 232879, upload-time = "2025-05-24T20:34:53.195Z" },
26 { url = "https://files.pythonhosted.org/packages/f6/4d/397eb5435917135df93b339d849884bb1125896b1e15163c5244aa590336/atomicwriter-0.2.5-cp313-cp313t-macosx_11_0_x86_64.whl", hash = "sha256:e33f40b2a27f8831beeabb485923acb6dd067cc70bba1a63096749b3dc4747ff", size = 244386, upload-time = "2025-05-24T20:34:54.852Z" },
27 { url = "https://files.pythonhosted.org/packages/8b/01/73f0b683fa55e61dd29d30e48e9a75ddb049e6dad0ac4ae1a29dbc05f21e/atomicwriter-0.2.5-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:c646e115e88147d71f845a005fc53910f22c4dc65bd634768cb90b7f34259359", size = 258255, upload-time = "2025-05-24T20:34:56.046Z" },
28 { url = "https://files.pythonhosted.org/packages/4b/19/692387c1fb1b8714a9b2fab99a58850fd4136bed988814c8ff74d0c8de02/atomicwriter-0.2.5-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:47f974e986ff6514351c3ea75041009a514be0c34c225c062b0ad8a28ec9c0a3", size = 261768, upload-time = "2025-05-24T20:34:57.795Z" },
29 { url = "https://files.pythonhosted.org/packages/3e/f2/4d466f52ee635cc54011713272f302584c6d1ce612c331d9989fa6fa672f/atomicwriter-0.2.5-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:e1db8b9004cd3f628166e83b25eb814b82345f9d6bc15e99b6d201c355455b45", size = 321975, upload-time = "2025-05-24T20:34:59.45Z" },
30 { url = "https://files.pythonhosted.org/packages/84/ad/0189ad9783ca6609df47e06cc0cd22866a8073d46478f59c6ab3ec13e0fb/atomicwriter-0.2.5-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:a7da4a114121ab865663578b801a0520b2b518d4591af0bd294f6aac0dad243b", size = 338946, upload-time = "2025-05-24T20:35:01.501Z" },
31 { url = "https://files.pythonhosted.org/packages/94/79/2c4d8f75eeb09192cf572957f031271998f3c985fabd79d513fff66ac715/atomicwriter-0.2.5-cp313-cp313t-win_amd64.whl", hash = "sha256:7aab4b3956cc17219e7e4da76e8a1bceb3d3aeaf03234f89b90e234a2adcf27b", size = 151571, upload-time = "2025-05-24T20:35:02.747Z" },
32 { url = "https://files.pythonhosted.org/packages/32/19/d6a686d189c3577e7f08b33df398b959c24bf74b3cec34359104db1a24ff/atomicwriter-0.2.5-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:8d0fccac2dfe5d884d97edbda28be9c16d55faee9bdf66f53a99384ac387cc43", size = 239320, upload-time = "2025-05-24T20:35:04.028Z" },
33 { url = "https://files.pythonhosted.org/packages/8e/35/35571a4eed57816c3b5fdbefcb15f38563fbe4f3a4a7d1588c8ef899afaf/atomicwriter-0.2.5-cp39-abi3-macosx_11_0_x86_64.whl", hash = "sha256:6583c24333508839db2156d895cbbb5cd3ff20d4f9c698e341435e5b35990eaa", size = 250818, upload-time = "2025-05-24T20:35:05.21Z" },
34 { url = "https://files.pythonhosted.org/packages/81/d9/145093630bc25f115a49d32d9ef66745f5cdef787492d77fd27e74d20389/atomicwriter-0.2.5-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:136a9902ae3f1c0cb262a07dd3ac85069d71f8b11347cd740030567e67d611aa", size = 265796, upload-time = "2025-05-24T20:35:06.388Z" },
35 { url = "https://files.pythonhosted.org/packages/58/32/d1881adade2ebc70aa9dbb61cadabc2c00cfa99a7a5d6ba48f44e279056f/atomicwriter-0.2.5-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:0b6830434b6a49c19473c3f3975dfa0a87dec95bee81297f7393e378f9a0b82f", size = 269378, upload-time = "2025-05-24T20:35:07.578Z" },
36 { url = "https://files.pythonhosted.org/packages/93/f5/2661ea763784a4991c4c7be5c932a468937bd1d4618b833a63ec638a3b76/atomicwriter-0.2.5-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:53095a01891a2901aa04c10c8de52c0ba41e0d8a4a1893318cf34ccbdbde00b7", size = 328167, upload-time = "2025-05-24T20:35:08.764Z" },
37 { url = "https://files.pythonhosted.org/packages/ec/bc/e3aa521671a589bee9662d3e2108e4835a5d80e6da76e4d05d98d1c78005/atomicwriter-0.2.5-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:ecf4dc3983bb1f28b21cb09c2d96b6936d8864c559dcf151b57813cb1eae998b", size = 347153, upload-time = "2025-05-24T20:35:10.507Z" },
38 { url = "https://files.pythonhosted.org/packages/59/b7/e190383e7240b1f247c6df9bc6667db8df10190cd0bb2dba8ea6bd704ea4/atomicwriter-0.2.5-cp39-abi3-win_amd64.whl", hash = "sha256:92cff264a20364301ab341b332fd0112866870b8cb35caf99a3f3fee0e6c19e8", size = 156374, upload-time = "2025-05-24T20:35:11.716Z" },
39]
40
41[[package]]
42name = "certifi"
43version = "2025.1.31"
44source = { registry = "https://pypi.org/simple" }
45sdist = { url = "https://files.pythonhosted.org/packages/1c/ab/c9f1e32b7b1bf505bf26f0ef697775960db7932abeb7b516de930ba2705f/certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651", size = 167577, upload-time = "2025-01-31T02:16:47.166Z" }
46wheels = [
47 { url = "https://files.pythonhosted.org/packages/38/fc/bce832fd4fd99766c04d1ee0eead6b0ec6486fb100ae5e74c1d91292b982/certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe", size = 166393, upload-time = "2025-01-31T02:16:45.015Z" },
48]
49
50[[package]]
51name = "charset-normalizer"
52version = "3.4.1"
53source = { registry = "https://pypi.org/simple" }
54sdist = { url = "https://files.pythonhosted.org/packages/16/b0/572805e227f01586461c80e0fd25d65a2115599cc9dad142fee4b747c357/charset_normalizer-3.4.1.tar.gz", hash = "sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3", size = 123188, upload-time = "2024-12-24T18:12:35.43Z" }
55wheels = [
56 { url = "https://files.pythonhosted.org/packages/0a/9a/dd1e1cdceb841925b7798369a09279bd1cf183cef0f9ddf15a3a6502ee45/charset_normalizer-3.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:73d94b58ec7fecbc7366247d3b0b10a21681004153238750bb67bd9012414545", size = 196105, upload-time = "2024-12-24T18:10:38.83Z" },
57 { url = "https://files.pythonhosted.org/packages/d3/8c/90bfabf8c4809ecb648f39794cf2a84ff2e7d2a6cf159fe68d9a26160467/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dad3e487649f498dd991eeb901125411559b22e8d7ab25d3aeb1af367df5efd7", size = 140404, upload-time = "2024-12-24T18:10:44.272Z" },
58 { url = "https://files.pythonhosted.org/packages/ad/8f/e410d57c721945ea3b4f1a04b74f70ce8fa800d393d72899f0a40526401f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c30197aa96e8eed02200a83fba2657b4c3acd0f0aa4bdc9f6c1af8e8962e0757", size = 150423, upload-time = "2024-12-24T18:10:45.492Z" },
59 { url = "https://files.pythonhosted.org/packages/f0/b8/e6825e25deb691ff98cf5c9072ee0605dc2acfca98af70c2d1b1bc75190d/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2369eea1ee4a7610a860d88f268eb39b95cb588acd7235e02fd5a5601773d4fa", size = 143184, upload-time = "2024-12-24T18:10:47.898Z" },
60 { url = "https://files.pythonhosted.org/packages/3e/a2/513f6cbe752421f16d969e32f3583762bfd583848b763913ddab8d9bfd4f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc2722592d8998c870fa4e290c2eec2c1569b87fe58618e67d38b4665dfa680d", size = 145268, upload-time = "2024-12-24T18:10:50.589Z" },
61 { url = "https://files.pythonhosted.org/packages/74/94/8a5277664f27c3c438546f3eb53b33f5b19568eb7424736bdc440a88a31f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffc9202a29ab3920fa812879e95a9e78b2465fd10be7fcbd042899695d75e616", size = 147601, upload-time = "2024-12-24T18:10:52.541Z" },
62 { url = "https://files.pythonhosted.org/packages/7c/5f/6d352c51ee763623a98e31194823518e09bfa48be2a7e8383cf691bbb3d0/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:804a4d582ba6e5b747c625bf1255e6b1507465494a40a2130978bda7b932c90b", size = 141098, upload-time = "2024-12-24T18:10:53.789Z" },
63 { url = "https://files.pythonhosted.org/packages/78/d4/f5704cb629ba5ab16d1d3d741396aec6dc3ca2b67757c45b0599bb010478/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f55e69f030f7163dffe9fd0752b32f070566451afe180f99dbeeb81f511ad8d", size = 149520, upload-time = "2024-12-24T18:10:55.048Z" },
64 { url = "https://files.pythonhosted.org/packages/c5/96/64120b1d02b81785f222b976c0fb79a35875457fa9bb40827678e54d1bc8/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c4c3e6da02df6fa1410a7680bd3f63d4f710232d3139089536310d027950696a", size = 152852, upload-time = "2024-12-24T18:10:57.647Z" },
65 { url = "https://files.pythonhosted.org/packages/84/c9/98e3732278a99f47d487fd3468bc60b882920cef29d1fa6ca460a1fdf4e6/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:5df196eb874dae23dcfb968c83d4f8fdccb333330fe1fc278ac5ceeb101003a9", size = 150488, upload-time = "2024-12-24T18:10:59.43Z" },
66 { url = "https://files.pythonhosted.org/packages/13/0e/9c8d4cb99c98c1007cc11eda969ebfe837bbbd0acdb4736d228ccaabcd22/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e358e64305fe12299a08e08978f51fc21fac060dcfcddd95453eabe5b93ed0e1", size = 146192, upload-time = "2024-12-24T18:11:00.676Z" },
67 { url = "https://files.pythonhosted.org/packages/b2/21/2b6b5b860781a0b49427309cb8670785aa543fb2178de875b87b9cc97746/charset_normalizer-3.4.1-cp312-cp312-win32.whl", hash = "sha256:9b23ca7ef998bc739bf6ffc077c2116917eabcc901f88da1b9856b210ef63f35", size = 95550, upload-time = "2024-12-24T18:11:01.952Z" },
68 { url = "https://files.pythonhosted.org/packages/21/5b/1b390b03b1d16c7e382b561c5329f83cc06623916aab983e8ab9239c7d5c/charset_normalizer-3.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:6ff8a4a60c227ad87030d76e99cd1698345d4491638dfa6673027c48b3cd395f", size = 102785, upload-time = "2024-12-24T18:11:03.142Z" },
69 { url = "https://files.pythonhosted.org/packages/38/94/ce8e6f63d18049672c76d07d119304e1e2d7c6098f0841b51c666e9f44a0/charset_normalizer-3.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:aabfa34badd18f1da5ec1bc2715cadc8dca465868a4e73a0173466b688f29dda", size = 195698, upload-time = "2024-12-24T18:11:05.834Z" },
70 { url = "https://files.pythonhosted.org/packages/24/2e/dfdd9770664aae179a96561cc6952ff08f9a8cd09a908f259a9dfa063568/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22e14b5d70560b8dd51ec22863f370d1e595ac3d024cb8ad7d308b4cd95f8313", size = 140162, upload-time = "2024-12-24T18:11:07.064Z" },
71 { url = "https://files.pythonhosted.org/packages/24/4e/f646b9093cff8fc86f2d60af2de4dc17c759de9d554f130b140ea4738ca6/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8436c508b408b82d87dc5f62496973a1805cd46727c34440b0d29d8a2f50a6c9", size = 150263, upload-time = "2024-12-24T18:11:08.374Z" },
72 { url = "https://files.pythonhosted.org/packages/5e/67/2937f8d548c3ef6e2f9aab0f6e21001056f692d43282b165e7c56023e6dd/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d074908e1aecee37a7635990b2c6d504cd4766c7bc9fc86d63f9c09af3fa11b", size = 142966, upload-time = "2024-12-24T18:11:09.831Z" },
73 { url = "https://files.pythonhosted.org/packages/52/ed/b7f4f07de100bdb95c1756d3a4d17b90c1a3c53715c1a476f8738058e0fa/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:955f8851919303c92343d2f66165294848d57e9bba6cf6e3625485a70a038d11", size = 144992, upload-time = "2024-12-24T18:11:12.03Z" },
74 { url = "https://files.pythonhosted.org/packages/96/2c/d49710a6dbcd3776265f4c923bb73ebe83933dfbaa841c5da850fe0fd20b/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:44ecbf16649486d4aebafeaa7ec4c9fed8b88101f4dd612dcaf65d5e815f837f", size = 147162, upload-time = "2024-12-24T18:11:13.372Z" },
75 { url = "https://files.pythonhosted.org/packages/b4/41/35ff1f9a6bd380303dea55e44c4933b4cc3c4850988927d4082ada230273/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0924e81d3d5e70f8126529951dac65c1010cdf117bb75eb02dd12339b57749dd", size = 140972, upload-time = "2024-12-24T18:11:14.628Z" },
76 { url = "https://files.pythonhosted.org/packages/fb/43/c6a0b685fe6910d08ba971f62cd9c3e862a85770395ba5d9cad4fede33ab/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2967f74ad52c3b98de4c3b32e1a44e32975e008a9cd2a8cc8966d6a5218c5cb2", size = 149095, upload-time = "2024-12-24T18:11:17.672Z" },
77 { url = "https://files.pythonhosted.org/packages/4c/ff/a9a504662452e2d2878512115638966e75633519ec11f25fca3d2049a94a/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c75cb2a3e389853835e84a2d8fb2b81a10645b503eca9bcb98df6b5a43eb8886", size = 152668, upload-time = "2024-12-24T18:11:18.989Z" },
78 { url = "https://files.pythonhosted.org/packages/6c/71/189996b6d9a4b932564701628af5cee6716733e9165af1d5e1b285c530ed/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:09b26ae6b1abf0d27570633b2b078a2a20419c99d66fb2823173d73f188ce601", size = 150073, upload-time = "2024-12-24T18:11:21.507Z" },
79 { url = "https://files.pythonhosted.org/packages/e4/93/946a86ce20790e11312c87c75ba68d5f6ad2208cfb52b2d6a2c32840d922/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa88b843d6e211393a37219e6a1c1df99d35e8fd90446f1118f4216e307e48cd", size = 145732, upload-time = "2024-12-24T18:11:22.774Z" },
80 { url = "https://files.pythonhosted.org/packages/cd/e5/131d2fb1b0dddafc37be4f3a2fa79aa4c037368be9423061dccadfd90091/charset_normalizer-3.4.1-cp313-cp313-win32.whl", hash = "sha256:eb8178fe3dba6450a3e024e95ac49ed3400e506fd4e9e5c32d30adda88cbd407", size = 95391, upload-time = "2024-12-24T18:11:24.139Z" },
81 { url = "https://files.pythonhosted.org/packages/27/f2/4f9a69cc7712b9b5ad8fdb87039fd89abba997ad5cbe690d1835d40405b0/charset_normalizer-3.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:b1ac5992a838106edb89654e0aebfc24f5848ae2547d22c2c3f66454daa11971", size = 102702, upload-time = "2024-12-24T18:11:26.535Z" },
82 { url = "https://files.pythonhosted.org/packages/0e/f6/65ecc6878a89bb1c23a086ea335ad4bf21a588990c3f535a227b9eea9108/charset_normalizer-3.4.1-py3-none-any.whl", hash = "sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85", size = 49767, upload-time = "2024-12-24T18:12:32.852Z" },
83]
84
85[[package]]
86name = "dbus-next"
87version = "0.2.3"
88source = { registry = "https://pypi.org/simple" }
89sdist = { url = "https://files.pythonhosted.org/packages/ce/45/6a40fbe886d60a8c26f480e7d12535502b5ba123814b3b9a0b002ebca198/dbus_next-0.2.3.tar.gz", hash = "sha256:f4eae26909332ada528c0a3549dda8d4f088f9b365153952a408e28023a626a5", size = 71112, upload-time = "2021-07-25T22:11:28.398Z" }
90wheels = [
91 { url = "https://files.pythonhosted.org/packages/d2/fc/c0a3f4c4eaa5a22fbef91713474666e13d0ea2a69c84532579490a9f2cc8/dbus_next-0.2.3-py3-none-any.whl", hash = "sha256:58948f9aff9db08316734c0be2a120f6dc502124d9642f55e90ac82ffb16a18b", size = 57885, upload-time = "2021-07-25T22:11:25.466Z" },
92]
93
94[[package]]
95name = "desktop-notify"
96version = "1.3.3"
97source = { registry = "https://pypi.org/simple" }
98dependencies = [
99 { name = "dbus-next" },
100]
101sdist = { url = "https://files.pythonhosted.org/packages/7a/d8/7ae5779257f5f1aa0a2d50c02d70b29522bd414692f3d3bd18ef119fe82d/desktop-notify-1.3.3.tar.gz", hash = "sha256:62934ad1f72f292f9a3af5ffe45af32814af18c396c00369385540c72bf08077", size = 7828, upload-time = "2021-01-03T16:46:36.483Z" }
102wheels = [
103 { url = "https://files.pythonhosted.org/packages/0a/cd/a7e3bd0262f3e8a9272fd24d0193e24dad7cb4e4edd27da48e74b5523e59/desktop_notify-1.3.3-py3-none-any.whl", hash = "sha256:8ad7ecc3a9a603dd5fa3cdc11cc6265cfbc7f6df9d8ed240f4663f43ef0de37a", size = 9937, upload-time = "2021-01-03T16:46:35.157Z" },
104]
105
106[[package]]
107name = "frozendict"
108version = "2.4.6"
109source = { registry = "https://pypi.org/simple" }
110sdist = { url = "https://files.pythonhosted.org/packages/bb/59/19eb300ba28e7547538bdf603f1c6c34793240a90e1a7b61b65d8517e35e/frozendict-2.4.6.tar.gz", hash = "sha256:df7cd16470fbd26fc4969a208efadc46319334eb97def1ddf48919b351192b8e", size = 316416, upload-time = "2024-10-13T12:15:32.449Z" }
111wheels = [
112 { url = "https://files.pythonhosted.org/packages/04/13/d9839089b900fa7b479cce495d62110cddc4bd5630a04d8469916c0e79c5/frozendict-2.4.6-py311-none-any.whl", hash = "sha256:d065db6a44db2e2375c23eac816f1a022feb2fa98cbb50df44a9e83700accbea", size = 16148, upload-time = "2024-10-13T12:15:26.839Z" },
113 { url = "https://files.pythonhosted.org/packages/ba/d0/d482c39cee2ab2978a892558cf130681d4574ea208e162da8958b31e9250/frozendict-2.4.6-py312-none-any.whl", hash = "sha256:49344abe90fb75f0f9fdefe6d4ef6d4894e640fadab71f11009d52ad97f370b9", size = 16146, upload-time = "2024-10-13T12:15:28.16Z" },
114 { url = "https://files.pythonhosted.org/packages/a5/8e/b6bf6a0de482d7d7d7a2aaac8fdc4a4d0bb24a809f5ddd422aa7060eb3d2/frozendict-2.4.6-py313-none-any.whl", hash = "sha256:7134a2bb95d4a16556bb5f2b9736dceb6ea848fa5b6f3f6c2d6dba93b44b4757", size = 16146, upload-time = "2024-10-13T12:15:29.495Z" },
115]
116
117[[package]]
118name = "idna"
119version = "3.10"
120source = { registry = "https://pypi.org/simple" }
121sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload-time = "2024-09-15T18:07:39.745Z" }
122wheels = [
123 { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" },
124]
125
126[[package]]
127name = "jsonpickle"
128version = "4.0.5"
129source = { registry = "https://pypi.org/simple" }
130sdist = { url = "https://files.pythonhosted.org/packages/d6/33/4bda317ab294722fcdfff8f63aab74af9fda3675a4652d984a101aa7587e/jsonpickle-4.0.5.tar.gz", hash = "sha256:f299818b39367c361b3f26bdba827d4249ab5d383cd93144d0f94b5417aacb35", size = 315661, upload-time = "2025-03-29T19:22:56.92Z" }
131wheels = [
132 { url = "https://files.pythonhosted.org/packages/dc/1b/0e79cf115e0f54f1e8f56effb6ffd2ef8f92e9c324d692ede660067f1bfe/jsonpickle-4.0.5-py3-none-any.whl", hash = "sha256:b4ac7d0a75ddcdfd93445737f1d36ff28768690d43e54bf5d0ddb1d915e580df", size = 46382, upload-time = "2025-03-29T19:22:54.252Z" },
133]
134
135[[package]]
136name = "python-dateutil"
137version = "2.9.0.post0"
138source = { registry = "https://pypi.org/simple" }
139dependencies = [
140 { name = "six" },
141]
142sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" }
143wheels = [
144 { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" },
145]
146
147[[package]]
148name = "pyxdg"
149version = "0.28"
150source = { registry = "https://pypi.org/simple" }
151sdist = { url = "https://files.pythonhosted.org/packages/b0/25/7998cd2dec731acbd438fbf91bc619603fc5188de0a9a17699a781840452/pyxdg-0.28.tar.gz", hash = "sha256:3267bb3074e934df202af2ee0868575484108581e6f3cb006af1da35395e88b4", size = 77776, upload-time = "2022-06-05T11:35:01Z" }
152wheels = [
153 { url = "https://files.pythonhosted.org/packages/e5/8d/cf41b66a8110670e3ad03dab9b759704eeed07fa96e90fdc0357b2ba70e2/pyxdg-0.28-py2.py3-none-any.whl", hash = "sha256:bdaf595999a0178ecea4052b7f4195569c1ff4d344567bccdc12dfdf02d545ab", size = 49520, upload-time = "2022-06-05T11:34:58.832Z" },
154]
155
156[[package]]
157name = "requests"
158version = "2.32.3"
159source = { registry = "https://pypi.org/simple" }
160dependencies = [
161 { name = "certifi" },
162 { name = "charset-normalizer" },
163 { name = "idna" },
164 { name = "urllib3" },
165]
166sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218, upload-time = "2024-05-29T15:37:49.536Z" }
167wheels = [
168 { url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928, upload-time = "2024-05-29T15:37:47.027Z" },
169]
170
171[[package]]
172name = "six"
173version = "1.17.0"
174source = { registry = "https://pypi.org/simple" }
175sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" }
176wheels = [
177 { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" },
178]
179
180[[package]]
181name = "tabulate"
182version = "0.9.0"
183source = { registry = "https://pypi.org/simple" }
184sdist = { url = "https://files.pythonhosted.org/packages/ec/fe/802052aecb21e3797b8f7902564ab6ea0d60ff8ca23952079064155d1ae1/tabulate-0.9.0.tar.gz", hash = "sha256:0095b12bf5966de529c0feb1fa08671671b3368eec77d7ef7ab114be2c068b3c", size = 81090, upload-time = "2022-10-06T17:21:48.54Z" }
185wheels = [
186 { url = "https://files.pythonhosted.org/packages/40/44/4a5f08c96eb108af5cb50b41f76142f0afa346dfa99d5296fe7202a11854/tabulate-0.9.0-py3-none-any.whl", hash = "sha256:024ca478df22e9340661486f85298cff5f6dcdba14f3813e8830015b9ed1948f", size = 35252, upload-time = "2022-10-06T17:21:44.262Z" },
187]
188
189[[package]]
190name = "toml"
191version = "0.10.2"
192source = { registry = "https://pypi.org/simple" }
193sdist = { url = "https://files.pythonhosted.org/packages/be/ba/1f744cdc819428fc6b5084ec34d9b30660f6f9daaf70eead706e3203ec3c/toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f", size = 22253, upload-time = "2020-11-01T01:40:22.204Z" }
194wheels = [
195 { url = "https://files.pythonhosted.org/packages/44/6f/7120676b6d73228c96e17f1f794d8ab046fc910d781c8d151120c3f1569e/toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", size = 16588, upload-time = "2020-11-01T01:40:20.672Z" },
196]
197
198[[package]]
199name = "uritools"
200version = "4.0.3"
201source = { registry = "https://pypi.org/simple" }
202sdist = { url = "https://files.pythonhosted.org/packages/d3/43/4182fb2a03145e6d38698e38b49114ce59bc8c79063452eb585a58f8ce78/uritools-4.0.3.tar.gz", hash = "sha256:ee06a182a9c849464ce9d5fa917539aacc8edd2a4924d1b7aabeeecabcae3bc2", size = 24184, upload-time = "2024-05-28T18:07:45.194Z" }
203wheels = [
204 { url = "https://files.pythonhosted.org/packages/e6/17/5a4510d9ca9cc8be217ce359eb54e693dca81cf4d442308b282d5131b17d/uritools-4.0.3-py3-none-any.whl", hash = "sha256:bae297d090e69a0451130ffba6f2f1c9477244aa0a5543d66aed2d9f77d0dd9c", size = 10304, upload-time = "2024-05-28T18:07:42.731Z" },
205]
206
207[[package]]
208name = "urllib3"
209version = "2.3.0"
210source = { registry = "https://pypi.org/simple" }
211sdist = { url = "https://files.pythonhosted.org/packages/aa/63/e53da845320b757bf29ef6a9062f5c669fe997973f966045cb019c3f4b66/urllib3-2.3.0.tar.gz", hash = "sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d", size = 307268, upload-time = "2024-12-22T07:47:30.032Z" }
212wheels = [
213 { url = "https://files.pythonhosted.org/packages/c8/19/4ec628951a74043532ca2cf5d97b7b14863931476d117c471e8e2b1eb39f/urllib3-2.3.0-py3-none-any.whl", hash = "sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df", size = 128369, upload-time = "2024-12-22T07:47:28.074Z" },
214]
215
216[[package]]
217name = "worktime"
218version = "1.0.0"
219source = { editable = "." }
220dependencies = [
221 { name = "atomicwriter" },
222 { name = "desktop-notify" },
223 { name = "frozendict" },
224 { name = "jsonpickle" },
225 { name = "python-dateutil" },
226 { name = "pyxdg" },
227 { name = "requests" },
228 { name = "tabulate" },
229 { name = "toml" },
230 { name = "uritools" },
231]
232
233[package.metadata]
234requires-dist = [
235 { name = "atomicwriter", specifier = ">=0.2.5" },
236 { name = "desktop-notify", specifier = ">=1.3.3" },
237 { name = "frozendict", specifier = ">=2.4.6" },
238 { name = "jsonpickle", specifier = ">=4.0.5,<5" },
239 { name = "python-dateutil", specifier = ">=2.9.0.post0,<3" },
240 { name = "pyxdg", specifier = ">=0.28,<0.29" },
241 { name = "requests", specifier = ">=2.32.3,<3" },
242 { name = "tabulate", specifier = ">=0.9.0,<0.10" },
243 { name = "toml", specifier = ">=0.10.2,<0.11" },
244 { name = "uritools", specifier = ">=4.0.3,<5" },
245]
246
247[package.metadata.requires-dev]
248dev = []
diff --git a/overlays/worktime/worktime/__main__.py b/overlays/worktime/worktime/__main__.py
index 9335afdc..bf24bbec 100755
--- a/overlays/worktime/worktime/__main__.py
+++ b/overlays/worktime/worktime/__main__.py
@@ -1,10 +1,12 @@
1import requests 1import requests
2from requests.exceptions import HTTPError 2from requests.exceptions import HTTPError
3from requests.auth import HTTPBasicAuth 3from requests.auth import HTTPBasicAuth
4from requests.adapters import HTTPAdapter, Retry
4from datetime import * 5from datetime import *
5from xdg import BaseDirectory 6from xdg import BaseDirectory
6import toml 7import toml
7from uritools import (uricompose) 8from uritools import uricompose
9from urllib.parse import urljoin
8 10
9from inspect import signature 11from inspect import signature
10 12
@@ -27,77 +29,76 @@ from sys import stderr, stdout
27 29
28from tabulate import tabulate 30from tabulate import tabulate
29 31
30from itertools import groupby, count 32from itertools import groupby, count, islice
31from functools import cache, partial 33from functools import cache, partial
32 34
33import backoff
34
35from pathlib import Path 35from pathlib import Path
36 36
37from collections import defaultdict 37from collections import defaultdict
38from collections.abc import Iterable, Generator
39from typing import Any
38 40
39import jsonpickle 41import jsonpickle
40from hashlib import blake2s 42from hashlib import blake2s
41import json 43import json
42 44
43class TogglAPISection(Enum): 45import asyncio
44 TOGGL = '/api/v9' 46
45 REPORTS = '/reports/api/v2' 47from frozendict import frozendict
46 48from contextlib import closing
47class TogglAPIError(Exception): 49import os
48 def __init__(self, response, *, http_error=None): 50from time import clock_gettime_ns, CLOCK_MONOTONIC
49 self.http_error = http_error 51from atomicwriter import AtomicWriter
50 self.response = response 52import desktop_notify.aio as notify
51 53
52 def __str__(self): 54class BearerAuth(requests.auth.AuthBase):
53 if not self.http_error is None: 55 def __init__(self, token):
54 return str(self.http_error) 56 self.token = token
55 else: 57 def __call__(self, r):
56 return self.response.text 58 r.headers["authorization"] = "Bearer " + self.token
57 59 return r
58class TogglAPI(object): 60
59 def __init__(self, api_token, workspace_id, client_ids): 61class KimaiSession(requests.Session):
60 self._api_token = api_token 62 def __init__(self, base_url: str, api_token: str):
61 self._workspace_id = workspace_id 63 super().__init__()
62 self._client_ids = set(map(int, client_ids.split(','))) if client_ids else None 64 self.base_url = base_url
63 65 self.auth = BearerAuth(api_token)
64 def _make_url(self, api=TogglAPISection.TOGGL, section=['me', 'time_entries', 'current'], params={}): 66 retries = Retry(total=5, backoff_factor=0.1, status_forcelist=[500, 502, 503, 504])
65 if api is TogglAPISection.REPORTS: 67 super().mount(base_url, HTTPAdapter(max_retries=retries))
66 params.update({'user_agent': 'worktime', 'workspace_id': self._workspace_id}) 68
67 69 def request(self, method, url, *args, **kwargs):
68 api_path = api.value 70 joined_url = urljoin(self.base_url, url)
69 section_path = '/'.join(section) 71 return super().request(method, joined_url, *args, headers = {'Accept': 'application/json'} | (kwargs['headers'] if 'headers' in kwargs else {}), **{k: v for k, v in kwargs.items() if k not in ['headers']})
70 uri = uricompose(scheme='https', host='api.track.toggl.com', path=f"{api_path}/{section_path}", query=params) 72
71 73class KimaiAPI(object):
72 return uri 74 def __init__(self, base_url: str, api_token: str, clients: Iterable[str]):
73 75 self._session = KimaiSession(base_url, api_token)
74 def _query(self, url, method): 76 self._kimai_clients = self._session.get('/api/customers').json()
75 response = self._raw_query(url, method) 77 self._client_ids = self.resolve_clients(clients)
76 response.raise_for_status() 78 kimai_user = self._session.get('/api/users/me').json()
77 return response 79 self._tz = gettz(kimai_user['timezone'])
78 80
79 @backoff.on_predicate( 81 def resolve_clients(self, clients: Iterable[str]) -> frozenset[int]:
80 backoff.expo, 82 return frozenset({ client['id'] for client in self._kimai_clients if client['name'] in clients })
81 factor=0.1, max_value=2, 83
82 predicate=lambda r: r.status_code == 429, 84 def render_datetime(self, datetime: datetime) -> str:
83 max_time=10, 85 return datetime.astimezone(self._tz).strftime('%Y-%m-%dT%H:%M:%S')
84 ) 86
85 def _raw_query(self, url, method): 87 def get_timesheets(self, params: dict[str, Any] = {}) -> Generator[Any]:
86 headers = {'content-type': 'application/json', 'accept': 'application/json'} 88 for page in count(start=1):
87 response = None 89 resp = self._session.get('/api/timesheets', params=params | {'size': 100, 'page': page})
88 90 if resp.status_code == 404:
89 if method == 'GET': 91 break
90 response = requests.get(url, headers=headers, auth=HTTPBasicAuth(self._api_token, 'api_token')) 92 yield from resp.json()
91 elif method == 'POST':
92 response = requests.post(url, headers=headers, auth=HTTPBasicAuth(self._api_token, 'api_token'))
93 else:
94 raise ValueError(f"Undefined HTTP method “{method}”")
95
96 return response
97 93
98 def entry_durations(self, start_date, *, end_date, rounding=False, client_ids): 94 def entry_durations(self, start_date: datetime, *, end_date: datetime, clients: Iterable[str] | None = None) -> Generator[timedelta]:
99 if client_ids is not None and not client_ids: 95 client_ids = None
96 if clients is not None and not clients:
100 return 97 return
98 elif clients is None:
99 client_ids = self._client_ids
100 else:
101 client_ids = self.resolve_clients(clients)
101 102
102 cache_dir = Path(BaseDirectory.save_cache_path('worktime')) / 'entry_durations' 103 cache_dir = Path(BaseDirectory.save_cache_path('worktime')) / 'entry_durations'
103 step = timedelta(days = 120) 104 step = timedelta(days = 120)
@@ -116,11 +117,8 @@ class TogglAPI(object):
116 cache_key = blake2s(jsonpickle.encode({ 117 cache_key = blake2s(jsonpickle.encode({
117 'start': req_start, 118 'start': req_start,
118 'end': req_end, 119 'end': req_end,
119 'rounding': rounding, 120 'client_ids': client_ids,
120 'clients': client_ids, 121 }).encode('utf-8'), key = self._session.auth.token.encode('utf-8')).hexdigest()
121 'workspace': self._workspace_id,
122 'workspace_clients': self._client_ids
123 }).encode('utf-8'), key = self._api_token.encode('utf-8')).hexdigest()
124 cache_path = cache_dir / cache_key[:2] / cache_key[2:4] / f'{cache_key[4:]}.json' 122 cache_path = cache_dir / cache_key[:2] / cache_key[2:4] / f'{cache_key[4:]}.json'
125 try: 123 try:
126 with cache_path.open('r', encoding='utf-8') as ch: 124 with cache_path.open('r', encoding='utf-8') as ch:
@@ -130,85 +128,83 @@ class TogglAPI(object):
130 pass 128 pass
131 129
132 entries = list() 130 entries = list()
133 params = { 'since': (req_start - timedelta(days=1)).date().isoformat(), 131 params = {
134 'until': (req_end + timedelta(days=1)).date().isoformat(), 132 'begin': self.render_datetime(req_start),
135 'rounding': 'yes' if rounding else 'no', 133 'end': self.render_datetime(req_end),
136 'billable': 'yes' 134 'customers[]': list(client_ids),
137 } 135 'billable': 1,
138 if client_ids is not None: 136 }
139 params |= { 'client_ids': ','.join(map(str, client_ids)) } 137
140 for page in count(start = 1): 138 for entry in self.get_timesheets(params):
141 url = self._make_url(api = TogglAPISection.REPORTS, section = ['details'], params = params | { 'page': page }) 139 if entry['end'] is None:
142 r = self._query(url = url, method='GET') 140 continue
143 if not r or not r.json(): 141
144 raise TogglAPIError(r) 142 start = isoparse(entry['begin'])
145 report = r.json() 143 end = isoparse(entry['end'])
146 for entry in report['data']:
147 start = isoparse(entry['start'])
148 end = isoparse(entry['end'])
149
150 if start > req_end or end < req_start:
151 continue
152 144
153 x = min(end, req_end) - max(start, req_start) 145 if start > req_end or end < req_start:
154 if cache_key: 146 continue
155 entries.append(x) 147
156 yield x 148 x = min(end, req_end) - max(start, req_start)
157 if not report['data']: 149 if cache_key:
158 break 150 entries.append(x)
151 yield x
159 152
160 if cache_path: 153 if cache_path:
161 cache_path.parent.mkdir(parents=True, exist_ok=True) 154 cache_path.parent.mkdir(parents=True, exist_ok=True)
162 with cache_path.open('w', encoding='utf-8') as ch: 155 with cache_path.open('w', encoding='utf-8') as ch:
163 ch.write(jsonpickle.encode(entries)) 156 ch.write(jsonpickle.encode(entries))
164 # res = timedelta(milliseconds=report['total_billable']) if report['total_billable'] else timedelta(milliseconds=0)
165 # return res
166
167 def get_billable_hours(self, start_date, end_date=datetime.now(timezone.utc), rounding=False):
168 billable_acc = timedelta(milliseconds = 0)
169 if 0 in self._client_ids:
170 url = self._make_url(api = TogglAPISection.TOGGL, section = ['workspaces', self._workspace_id, 'clients'])
171 r = self._query(url = url, method = 'GET')
172 if not r or not r.json():
173 raise TogglAPIError(r)
174 157
175 billable_acc += sum(self.entry_durations(start_date, end_date=end_date, rounding=rounding, client_ids=None), start=timedelta(milliseconds=0)) - sum(self.entry_durations(start_date, end_date=end_date, rounding=rounding, client_ids=frozenset(map(lambda c: c['id'], r.json()))), start=timedelta(milliseconds=0)) 158 def get_billable_hours(self, start_date: datetime, end_date: datetime = datetime.now(timezone.utc)) -> timedelta:
159 return sum(self.entry_durations(start_date, end_date=end_date), start=timedelta(milliseconds=0))
176 160
177 billable_acc += sum(self.entry_durations(start_date, end_date=end_date, rounding=rounding, client_ids=frozenset(*(self._client_ids - {0}))), start=timedelta(milliseconds=0)) 161 def get_running_entry(self) -> Any | None:
178 162 kimai_entries = self._session.get('/api/timesheets/active').json()
179 return billable_acc 163 if not kimai_entries:
164 return None
165 entry = kimai_entries[0]
180 166
181 def get_running_clock(self, now=datetime.now(timezone.utc)): 167 if entry['project']['customer']['id'] not in self._client_ids:
182 url = self._make_url(api = TogglAPISection.TOGGL, section = ['me', 'time_entries', 'current']) 168 return None
183 r = self._query(url = url, method='GET')
184 169
185 if not r or (not r.json() and r.json() is not None): 170 return entry
186 raise TogglAPIError(r)
187 171
188 if not r.json() or not r.json()['billable']: 172 def get_running_clock(self, now: datetime = datetime.now(timezone.utc)) -> timedelta | None:
173 entry = self.get_running_entry()
174 if not entry:
189 return None 175 return None
176 start = isoparse(entry['begin'])
177 return now - start if start <= now else None
190 178
191 if self._client_ids is not None: 179 def get_recent_entries(self) -> Generator[Any]:
192 if 'pid' in r.json() and r.json()['pid']: 180 step = timedelta(days = 7)
193 url = self._make_url(api = TogglAPISection.TOGGL, section = ['projects', str(r.json()['pid'])]) 181 now = datetime.now().astimezone(timezone.utc)
194 pr = self._query(url = url, method = 'GET') 182 ids = set()
195 if not pr or not pr.json(): 183 for req_end in (now - step * i for i in count()):
196 raise TogglAPIError(pr) 184 params = {
197 185 'begin': self.render_datetime(req_end - step),
198 if not pr.json(): 186 'end': self.render_datetime(req_end),
199 return None 187 'full': 'true',
188 }
189 for entry in self.get_timesheets(params):
190 if entry['id'] in ids:
191 continue
192 ids.add(entry['id'])
193 yield entry
200 194
201 if 'cid' in pr.json() and pr.json()['cid']: 195 def start_clock(self, project_id: int, activity_id: int, description: str | None = None, tags: Iterable[str] | None = None, billable: bool = True):
202 if pr.json()['cid'] not in self._client_ids: 196 self._session.post('/api/timesheets', json={
203 return None 197 'begin': self.render_datetime(datetime.now()),
204 elif 0 not in self._client_ids: 198 'project': project_id,
205 return None 199 'activity': activity_id,
206 elif 0 not in self._client_ids: 200 'description': description if description else '',
207 return None 201 'tags': (','.join(tags)) if tags else '',
202 'billable': billable,
203 }).raise_for_status()
208 204
209 start = isoparse(r.json()['start']) 205 def stop_clock(self, running_id: int):
206 self._session.patch(f'/api/timesheets/{running_id}/stop').raise_for_status()
210 207
211 return now - start if start <= now else None
212 208
213class Worktime(object): 209class Worktime(object):
214 time_worked = timedelta() 210 time_worked = timedelta()
@@ -224,6 +220,7 @@ class Worktime(object):
224 leave_budget = dict() 220 leave_budget = dict()
225 time_per_day = None 221 time_per_day = None
226 workdays = None 222 workdays = None
223 pull_forward = dict()
227 224
228 @staticmethod 225 @staticmethod
229 @cache 226 @cache
@@ -280,10 +277,10 @@ class Worktime(object):
280 277
281 config = Worktime.config() 278 config = Worktime.config()
282 config_dir = BaseDirectory.load_first_config('worktime') 279 config_dir = BaseDirectory.load_first_config('worktime')
283 api = TogglAPI( 280 api = KimaiAPI(
284 api_token=config.get("TOGGL", {}).get("ApiToken", None), 281 base_url=config.get("KIMAI", {}).get("BaseUrl", None),
285 workspace_id=config.get("TOGGL", {}).get("Workspace", None), 282 api_token=config.get("KIMAI", {}).get("ApiToken", None),
286 client_ids=config.get("TOGGL", {}).get("ClientIds", None) 283 clients=config.get("KIMAI", {}).get("Clients", None)
287 ) 284 )
288 date_format = config.get("WORKTIME", {}).get("DateFormat", '%Y-%m-%d') 285 date_format = config.get("WORKTIME", {}).get("DateFormat", '%Y-%m-%d')
289 286
@@ -391,7 +388,7 @@ class Worktime(object):
391 if e.errno != 2: 388 if e.errno != 2:
392 raise e 389 raise e
393 390
394 pull_forward = dict() 391 self.time_per_day = lambda day: timedelta(hours = hours_per_week(day)) / len(self.workdays) - (holidays[day] if day in holidays else timedelta())
395 392
396 start_day = self.start_date.date() 393 start_day = self.start_date.date()
397 end_day = self.end_date.date() 394 end_day = self.end_date.date()
@@ -419,21 +416,19 @@ class Worktime(object):
419 if not d == datetime.strptime(c, date_format).replace(tzinfo=tzlocal()).date(): break 416 if not d == datetime.strptime(c, date_format).replace(tzinfo=tzlocal()).date(): break
420 else: 417 else:
421 if d >= self.end_date.date(): 418 if d >= self.end_date.date():
422 pull_forward[d] = min(timedelta(hours = float(hours)), self.time_per_day(d) - (holidays[d] if d in holidays else timedelta())) 419 self.pull_forward[d] = min(timedelta(hours = float(hours)), self.time_per_day(d) - (holidays[d] if d in holidays else timedelta()))
423 except IOError as e: 420 except IOError as e:
424 if e.errno != 2: 421 if e.errno != 2:
425 raise e 422 raise e
426 423
427 self.days_to_work = dict() 424 self.days_to_work = dict()
428 425
429 if pull_forward: 426 if self.pull_forward:
430 end_day = max(end_day, max(list(pull_forward))) 427 end_day = max(end_day, max(list(self.pull_forward)))
431 428
432 for day in [start_day + timedelta(days = x) for x in range(0, (end_day - start_day).days + 1)]: 429 for day in [start_day + timedelta(days = x) for x in range(0, (end_day - start_day).days + 1)]:
433 if day.isoweekday() in self.workdays: 430 if day.isoweekday() in self.workdays:
434 time_to_work = self.time_per_day(day) 431 time_to_work = self.time_per_day(day)
435 if day in holidays.keys():
436 time_to_work -= holidays[day]
437 if time_to_work > timedelta(): 432 if time_to_work > timedelta():
438 self.days_to_work[day] = time_to_work 433 self.days_to_work[day] = time_to_work
439 434
@@ -471,17 +466,17 @@ class Worktime(object):
471 self.extra_days_to_work[self.now.date()] = timedelta() 466 self.extra_days_to_work[self.now.date()] = timedelta()
472 467
473 self.time_to_work = sum([self.days_to_work[day] for day in self.days_to_work.keys() if day <= self.end_date.date()], timedelta()) 468 self.time_to_work = sum([self.days_to_work[day] for day in self.days_to_work.keys() if day <= self.end_date.date()], timedelta())
474 for day in [d for d in list(pull_forward) if d > self.end_date.date()]: 469 for day in [d for d in list(self.pull_forward) if d > self.end_date.date()]:
475 days_forward = set([d for d in self.days_to_work.keys() if d >= self.end_date.date() and d < day and (not d in pull_forward or d == self.end_date.date())]) 470 days_forward = set([d for d in self.days_to_work.keys() if d >= self.end_date.date() and d < day and (not d in self.pull_forward or d == self.end_date.date())])
476 extra_days_forward = set([d for d in self.extra_days_to_work.keys() if d >= self.end_date.date() and d < day and (not d in pull_forward or d == self.end_date.date())]) 471 extra_days_forward = set([d for d in self.extra_days_to_work.keys() if d >= self.end_date.date() and d < day and (not d in self.pull_forward or d == self.end_date.date())])
477 days_forward = days_forward.union(extra_days_forward) 472 days_forward = days_forward.union(extra_days_forward)
478 473
479 extra_day_time_left = timedelta() 474 extra_day_time_left = timedelta()
480 for extra_day in extra_days_forward: 475 for extra_day in extra_days_forward:
481 day_time = max(timedelta(), self.time_per_day(extra_day) - self.extra_days_to_work[extra_day]) 476 day_time = max(timedelta(), self.time_per_day(extra_day) - self.extra_days_to_work[extra_day])
482 extra_day_time_left += day_time 477 extra_day_time_left += day_time
483 extra_day_time = min(extra_day_time_left, pull_forward[day]) 478 extra_day_time = min(extra_day_time_left, self.pull_forward[day])
484 time_forward = pull_forward[day] - extra_day_time 479 time_forward = self.pull_forward[day] - extra_day_time
485 if extra_day_time_left > timedelta(): 480 if extra_day_time_left > timedelta():
486 for extra_day in extra_days_forward: 481 for extra_day in extra_days_forward:
487 day_time = max(timedelta(), self.time_per_day(extra_day) - self.extra_days_to_work[extra_day]) 482 day_time = max(timedelta(), self.time_per_day(extra_day) - self.extra_days_to_work[extra_day])
@@ -497,7 +492,7 @@ class Worktime(object):
497 492
498 self.time_to_work += self.time_pulled_forward 493 self.time_to_work += self.time_pulled_forward
499 494
500 self.time_worked += api.get_billable_hours(self.start_date, self.now, rounding = config.get("WORKTIME", {}).get("rounding", True)) 495 self.time_worked += api.get_billable_hours(self.start_date, self.now)
501 496
502def format_days(worktime, days, date_format=None): 497def format_days(worktime, days, date_format=None):
503 if not date_format: 498 if not date_format:
@@ -565,27 +560,29 @@ def worktime(pull_forward_cutoff, waybar, **args):
565 return f"{indicator}{difference_string}" 560 return f"{indicator}{difference_string}"
566 else: 561 else:
567 difference_string = difference_string(total_minutes_difference * timedelta(minutes = 1)) 562 difference_string = difference_string(total_minutes_difference * timedelta(minutes = 1))
568 if worktime.now_is_workday: 563 return difference_string
569 return difference_string
570 else:
571 return f"({difference_string})"
572 564
573 out_class = "running" if worktime.running_entry else "stopped" 565 out_class = "running" if worktime.running_entry else "stopped"
574 tooltip = tooltip_timedelta(worktime.time_to_work - worktime.time_worked) 566 difference = worktime.time_to_work - worktime.time_worked
575 if worktime.time_pulled_forward >= min(pull_forward_cutoff, timedelta(seconds = 1)): 567 if worktime.running_entry and -min(timedelta(milliseconds=0), difference) > sum(worktime.pull_forward.values(), start=timedelta(milliseconds=0)) or not worktime.running_entry and max(timedelta(milliseconds=0), difference) > worktime.time_per_day(worktime.now.date()) and worktime.now_is_workday:
568 out_class = "over"
569 pull_forward_sum = sum(worktime.pull_forward.values(), start=timedelta(milliseconds=0))
570 if pull_forward_sum >= min(pull_forward_cutoff, timedelta(seconds = 1)):
576 worktime_no_pulled_forward = deepcopy(worktime) 571 worktime_no_pulled_forward = deepcopy(worktime)
577 worktime_no_pulled_forward.time_to_work -= worktime_no_pulled_forward.time_pulled_forward 572 worktime_no_pulled_forward.time_to_work -= worktime_no_pulled_forward.time_pulled_forward
578 worktime_no_pulled_forward.time_pulled_forward = timedelta() 573 worktime_no_pulled_forward.time_pulled_forward = timedelta()
574 worktime_no_pulled_forward.pull_forward = dict()
575 worktime.time_to_work += pull_forward_sum
579 576
580 difference_string = format_worktime(worktime)
581 difference_string_no_pulled_forward = format_worktime(worktime_no_pulled_forward) 577 difference_string_no_pulled_forward = format_worktime(worktime_no_pulled_forward)
582 578
583 tooltip = tooltip_timedelta(worktime_no_pulled_forward.time_to_work - worktime_no_pulled_forward.time_worked) + "…" + tooltip 579 tooltip = tooltip_timedelta(worktime_no_pulled_forward.time_to_work - worktime_no_pulled_forward.time_worked) + "…" + tooltip_timedelta(difference + pull_forward_sum)
584 if worktime.time_pulled_forward >= pull_forward_cutoff: 580 if pull_forward_sum >= pull_forward_cutoff:
585 out_text = f"{difference_string_no_pulled_forward}…{difference_string}" 581 out_text = f"{difference_string_no_pulled_forward}…{format_worktime(worktime)}"
586 else: 582 else:
587 out_text = format_worktime(worktime) 583 out_text = format_worktime(worktime)
588 else: 584 else:
585 tooltip = tooltip_timedelta(difference)
589 out_text = format_worktime(worktime) 586 out_text = format_worktime(worktime)
590 587
591 if waybar: 588 if waybar:
@@ -593,6 +590,10 @@ def worktime(pull_forward_cutoff, waybar, **args):
593 else: 590 else:
594 print(out_text) 591 print(out_text)
595 592
593def pull_forward(**args):
594 worktime = Worktime(**args)
595 print(tooltip_timedelta(sum(worktime.pull_forward.values(), start=timedelta(milliseconds=0))))
596
596def time_worked(now, waybar, **args): 597def time_worked(now, waybar, **args):
597 then = now.replace(hour = 0, minute = 0, second = 0, microsecond = 0) 598 then = now.replace(hour = 0, minute = 0, second = 0, microsecond = 0)
598 if now.time() == time(): 599 if now.time() == time():
@@ -604,40 +605,56 @@ def time_worked(now, waybar, **args):
604 worked = now.time_worked - then.time_worked 605 worked = now.time_worked - then.time_worked
605 606
606 out_text = None 607 out_text = None
607 out_class = "stopped" 608 out_class = "running" if now.running_entry else "stopped"
608 tooltip = tooltip_timedelta(worked) 609 tooltip = tooltip_timedelta(worked)
610 target_time = max(then.time_per_day(then.now.date()), now.time_per_day(now.now.date())) if then.time_per_day(then.now.date()) and now.time_per_day(now.now.date()) else (then.time_per_day(then.now.date()) if then.time_per_day(then.now.date()) else now.time_per_day(now.now.date()));
611 difference = target_time - worked
612 difference_pull_forward = difference + now.time_pulled_forward
613 if now.running_entry and difference_pull_forward < timedelta(seconds=0):
614 out_class = "over"
609 if args['do_round']: 615 if args['do_round']:
610 total_minutes_difference = 5 * ceil(worked / timedelta(minutes = 5)) 616 total_minutes_difference = 5 * ceil(worked / timedelta(minutes = 5))
611 (hours_difference, minutes_difference) = divmod(abs(total_minutes_difference), 60) 617 (hours_difference, minutes_difference) = divmod(abs(total_minutes_difference), 60)
612 sign = '' if total_minutes_difference >= 0 else '-' 618 sign = '' if total_minutes_difference >= 0 else '-'
613
614 difference_string = f"{sign}"
615 if hours_difference != 0:
616 difference_string += f"{hours_difference}h"
617 if hours_difference == 0 or minutes_difference != 0:
618 difference_string += f"{minutes_difference}m"
619
620 clockout_time = None
621 clockout_difference = None
622 if then.now_is_workday or now.now_is_workday:
623 target_time = max(then.time_per_day(then.now.date()), now.time_per_day(now.now.date())) if then.time_per_day(then.now.date()) and now.time_per_day(now.now.date()) else (then.time_per_day(then.now.date()) if then.time_per_day(then.now.date()) else now.time_per_day(now.now.date()));
624 difference = target_time - worked
625 clockout_difference = 5 * ceil(difference / timedelta(minutes = 5))
626 clockout_time = now.now + difference
627 exact_clockout_time = clockout_time
628 clockout_time += (5 - clockout_time.minute % 5) * timedelta(minutes = 1)
629 clockout_time = clockout_time.replace(second = 0, microsecond = 0)
630 619
631 if now.running_entry and clockout_time and clockout_difference >= 0: 620 difference_string = f"{sign}"
632 out_class = "running" 621 if hours_difference != 0:
633 out_text = f"{difference_string}/{clockout_time:%H:%M}" 622 difference_string += f"{hours_difference}h"
634 tooltip = f"{tooltip_timedelta(worked)}/{exact_clockout_time:%H:%M}" 623 if hours_difference == 0 or minutes_difference != 0:
635 else: 624 difference_string += f"{minutes_difference}m"
636 if now.running_entry: 625
637 out_class = "over" 626 def round_clockout_time(difference):
638 out_text = difference_string 627 clockout_time = None
628 clockout_difference = None
629 exact_clockout_time = None
630 if then.now_is_workday or now.now_is_workday:
631 clockout_difference = 5 * ceil(difference / timedelta(minutes = 5))
632 clockout_time = now.now + difference
633 exact_clockout_time = clockout_time
634 clockout_time += (5 - clockout_time.minute % 5) * timedelta(minutes = 1)
635 clockout_time = clockout_time.replace(second = 0, microsecond = 0)
636
637 return clockout_time, exact_clockout_time, clockout_difference
638
639 clockout_time, exact_clockout_time, clockout_difference = round_clockout_time(difference)
640 clockout_time_pull_forward, exact_clockout_time_pull_forward, clockout_difference_pull_forward = round_clockout_time(difference_pull_forward)
641 clockout_pull_forward_sum, exact_clockout_pull_forward_sum, _ = round_clockout_time(now.time_to_work - now.time_worked + sum(now.pull_forward.values(), start=timedelta(milliseconds=0)))
642
643 if now.running_entry and clockout_time and (clockout_difference >= 0 or clockout_difference_pull_forward >= 0):
644 out_text = f"{difference_string}/{clockout_time:%H:%M}"
645 tooltip = f"{tooltip_timedelta(worked)}/{exact_clockout_time:%H:%M:%S}"
646
647 if clockout_pull_forward_sum >= clockout_time_pull_forward and clockout_time_pull_forward != clockout_time:
648 out_text += f"…{clockout_time_pull_forward:%H:%M}"
649 if exact_clockout_pull_forward_sum >= exact_clockout_time_pull_forward and exact_clockout_time_pull_forward != exact_clockout_time:
650 tooltip += f"…{exact_clockout_time_pull_forward:%H:%M:%S}"
651 else:
652 out_text = difference_string
639 else: 653 else:
640 out_text = str(worked) 654 out_text = str(worked)
655
656 if not now.now_is_workday:
657 out_text = f'({out_text})'
641 658
642 if waybar: 659 if waybar:
643 json.dump({"text": out_text, "class": out_class, "tooltip": tooltip}, stdout) 660 json.dump({"text": out_text, "class": out_class, "tooltip": tooltip}, stdout)
@@ -865,7 +882,7 @@ def main():
865 882
866 config = Worktime.config() 883 config = Worktime.config()
867 884
868 parser = argparse.ArgumentParser(prog = "worktime", description = 'Track worktime using toggl API') 885 parser = argparse.ArgumentParser(prog = "worktime", description = 'Track worktime using Kimai API')
869 parser.add_argument('--time', dest = 'now', metavar = 'TIME', type = isotime, help = 'Time to calculate status for (default: current time)', default = datetime.now(tzlocal())) 886 parser.add_argument('--time', dest = 'now', metavar = 'TIME', type = isotime, help = 'Time to calculate status for (default: current time)', default = datetime.now(tzlocal()))
870 parser.add_argument('--start', dest = 'start_datetime', metavar = 'TIME', type = isotime, help = 'Time to calculate status from (default: None)', default = None) 887 parser.add_argument('--start', dest = 'start_datetime', metavar = 'TIME', type = isotime, help = 'Time to calculate status from (default: None)', default = None)
871 parser.add_argument('--no-running', dest = 'include_running', action = 'store_false') 888 parser.add_argument('--no-running', dest = 'include_running', action = 'store_false')
@@ -895,10 +912,146 @@ def main():
895 classification_parser.add_argument('--table', action = 'store_true') 912 classification_parser.add_argument('--table', action = 'store_true')
896 classification_parser.add_argument('--table-format', dest='table_format', type=str, default='fancy_grid') 913 classification_parser.add_argument('--table-format', dest='table_format', type=str, default='fancy_grid')
897 classification_parser.set_defaults(cmd = partial(classification, classification_name=classification_name)) 914 classification_parser.set_defaults(cmd = partial(classification, classification_name=classification_name))
915 pull_forward_parser = subparsers.add_parser('pull-forward')
916 pull_forward_parser.set_defaults(cmd = pull_forward)
898 parser.set_default_subparser('time_worked') 917 parser.set_default_subparser('time_worked')
899 args = parser.parse_args() 918 args = parser.parse_args()
900 919
901 args.cmd(**vars(args)) 920 args.cmd(**vars(args))
902 921
922async def ui_update_options(api, cache_path):
923 options = set()
924 sort_order = dict()
925 entry_iter = enumerate(api.get_recent_entries())
926 loop = asyncio.get_event_loop()
927 start = clock_gettime_ns(CLOCK_MONOTONIC)
928 while item := await loop.run_in_executor(None, next, entry_iter):
929 ix, entry = item
930 if len(options) >= 20 or ix >= 1000:
931 break
932 elif len(options) >= 3:
933 now = clock_gettime_ns(CLOCK_MONOTONIC)
934 if now - start >= 4000000000:
935 break
936
937 option = frozendict({
938 'tags': frozenset(entry['tags']),
939 'activity': frozendict({'id': entry['activity']['id'], 'name': entry['activity']['name']}),
940 'project': frozendict({'id': entry['project']['id'], 'customer': entry['project']['customer']['name'], 'name': entry['project']['name']}),
941 'description': entry['description'] if entry['description'] else None,
942 'billable': entry['billable'],
943 })
944 sort_value = isoparse(entry['begin'])
945 if option in sort_order:
946 sort_value = max(sort_value, sort_order[option])
947 sort_order[option] = sort_value
948 options.add(option)
949
950 options = list(sorted(options, key = lambda o: sort_order[o], reverse = True))
951
952 with AtomicWriter(cache_path, overwrite=True) as ch:
953 ch.write_text(jsonpickle.encode(options))
954
955 return options
956
957def ui_render_option(option):
958 res = ''
959 if option['description']:
960 res += '„{}“, '.format(option['description'])
961 res += option['activity']['name'] + ', '
962 res += option['project']['name']
963 if option['project']['customer'] not in option['project']['name']:
964 res += ' ({})'.format(option['project']['customer'])
965 if option['tags']:
966 res += ', {}'.format(' '.join(map(lambda t: '#{}'.format(t), option['tags'])))
967 if not option['billable']:
968 res += ', not billable'
969 return res
970
971async def ui_main():
972 cache_path = Path(BaseDirectory.save_cache_path('worktime-ui')) / 'options.json'
973 options = None
974 try:
975 with cache_path.open('r', encoding='utf-8') as ch:
976 options = jsonpickle.decode(ch.read())
977 except FileNotFoundError:
978 pass
979
980 config = Worktime.config()
981 api = KimaiAPI(
982 base_url=config.get("KIMAI", {}).get("BaseUrl", None),
983 api_token=config.get("KIMAI", {}).get("ApiToken", None),
984 clients=config.get("KIMAI", {}).get("Clients", None)
985 )
986 running_entry = api.get_running_entry()
987
988 async with asyncio.TaskGroup() as tg:
989 update_options = tg.create_task(ui_update_options(api, cache_path))
990 if not options:
991 options = await update_options
992
993 read_fd, write_fd = os.pipe()
994 w_pipe = open(write_fd, 'wb', 0)
995 loop = asyncio.get_event_loop()
996 w_transport, _ = await loop.connect_write_pipe(
997 asyncio.Protocol,
998 w_pipe,
999 )
1000 r_pipe = open(read_fd, 'rb', 0)
1001
1002 proc = await asyncio.create_subprocess_exec(
1003 "fuzzel", "--dmenu", "--index", "--width=60",
1004 stdout = asyncio.subprocess.PIPE,
1005 stdin = r_pipe,
1006 )
1007
1008 with closing(w_transport) as t:
1009 if running_entry:
1010 t.write(b'Stop running timesheet\n')
1011 for option in options:
1012 t.write(ui_render_option(option).encode('utf-8') + b'\n')
1013
1014 stdout, _ = await proc.communicate()
1015 if proc.returncode != 0:
1016 return
1017 fuzzel_out = int(stdout.decode('utf-8'))
1018 if fuzzel_out < 0 or fuzzel_out >= len(options):
1019 return
1020 elif running_entry and fuzzel_out == 0:
1021 api.stop_clock(running_entry['id'])
1022 await notify.Server('worktime').Notify("Stopped running timesheet").set_timeout(65000).show()
1023 else:
1024 if running_entry:
1025 fuzzel_out -= 1
1026 option = options[fuzzel_out]
1027 api.start_clock(
1028 project_id = option['project']['id'],
1029 activity_id = option['activity']['id'],
1030 description = option['description'],
1031 tags = option['tags'],
1032 billable = option['billable'],
1033 )
1034 await notify.Server('worktime').Notify("Timesheet started…").set_timeout(65000).show()
1035
1036
1037def ui():
1038 asyncio.run(ui_main())
1039
1040async def stop_main():
1041 config = Worktime.config()
1042 api = KimaiAPI(
1043 base_url=config.get("KIMAI", {}).get("BaseUrl", None),
1044 api_token=config.get("KIMAI", {}).get("ApiToken", None),
1045 clients=config.get("KIMAI", {}).get("Clients", None)
1046 )
1047 if running_entry := api.get_running_entry():
1048 api.stop_clock(running_entry['id'])
1049 await notify.Server('worktime').Notify("Stopped running timesheet").set_timeout(65000).show()
1050 else:
1051 await notify.Server('worktime').Notify("No timesheet currently running").set_timeout(65000).show()
1052
1053def stop():
1054 asyncio.run(stop_main())
1055
903if __name__ == "__main__": 1056if __name__ == "__main__":
904 sys.exit(main()) 1057 sys.exit(main())