diff options
-rw-r--r-- | nix/default.nix | 3 | ||||
-rw-r--r-- | nix/notmuch-links.nix | 24 | ||||
-rw-r--r-- | nix/notmuch-tcp.nix | 22 | ||||
-rw-r--r-- | notmuch-links | 30 | ||||
-rwxr-xr-x | notmuch-tcp | 2 | ||||
-rw-r--r-- | notmuch-tcp-client | 61 | ||||
-rw-r--r-- | notmuch-tcp-server | 142 | ||||
-rwxr-xr-x | worktime.py | 53 |
8 files changed, 306 insertions, 31 deletions
diff --git a/nix/default.nix b/nix/default.nix index 097d092..5665a9d 100644 --- a/nix/default.nix +++ b/nix/default.nix | |||
@@ -1,6 +1,6 @@ | |||
1 | self: super: | 1 | self: super: |
2 | 2 | ||
3 | { | 3 | rec { |
4 | cliparg = self.callPackage ./cliparg.nix {}; | 4 | cliparg = self.callPackage ./cliparg.nix {}; |
5 | rebuild-system = self.callPackage ./rebuild-system.nix {}; | 5 | rebuild-system = self.callPackage ./rebuild-system.nix {}; |
6 | pulseaudio-ctl = self.callPackage ./pulseaudio-ctl.nix {}; | 6 | pulseaudio-ctl = self.callPackage ./pulseaudio-ctl.nix {}; |
@@ -8,6 +8,7 @@ self: super: | |||
8 | rolling-directory = self.callPackage ./rolling-directory.nix {}; | 8 | rolling-directory = self.callPackage ./rolling-directory.nix {}; |
9 | recv = self.callPackage ./recv.nix {}; | 9 | recv = self.callPackage ./recv.nix {}; |
10 | notmuch-tcp = self.callPackage ./notmuch-tcp.nix {}; | 10 | notmuch-tcp = self.callPackage ./notmuch-tcp.nix {}; |
11 | notmuch-links = self.callPackage ./notmuch-links.nix { inherit notmuch-tcp; }; | ||
11 | persistent-nix-shell = self.callPackage ./persistent-nix-shell.nix {}; | 12 | persistent-nix-shell = self.callPackage ./persistent-nix-shell.nix {}; |
12 | worktime = self.callPackage ./worktime.nix {}; | 13 | worktime = self.callPackage ./worktime.nix {}; |
13 | } | 14 | } |
diff --git a/nix/notmuch-links.nix b/nix/notmuch-links.nix new file mode 100644 index 0000000..67ebe3b --- /dev/null +++ b/nix/notmuch-links.nix | |||
@@ -0,0 +1,24 @@ | |||
1 | { stdenv | ||
2 | , notmuch-tcp | ||
3 | , zsh, coreutils, gawk, gnused | ||
4 | }: | ||
5 | |||
6 | stdenv.mkDerivation rec { | ||
7 | pname = "notmuch-links"; | ||
8 | version = "0.1"; | ||
9 | src = ../notmuch-links; | ||
10 | |||
11 | phases = [ "buildPhase" "installPhase" ]; | ||
12 | |||
13 | inherit zsh coreutils gawk gnused; | ||
14 | notmuchtcp = notmuch-tcp; | ||
15 | |||
16 | buildPhase = '' | ||
17 | substituteAll $src notmuch-links | ||
18 | ''; | ||
19 | |||
20 | installPhase = '' | ||
21 | install -m 0755 -D -t $out/bin \ | ||
22 | notmuch-links | ||
23 | ''; | ||
24 | } | ||
diff --git a/nix/notmuch-tcp.nix b/nix/notmuch-tcp.nix index 59d2e39..1a1947b 100644 --- a/nix/notmuch-tcp.nix +++ b/nix/notmuch-tcp.nix | |||
@@ -1,22 +1,30 @@ | |||
1 | { stdenv | 1 | { stdenv |
2 | , zsh, socat, nettools, coreutils | 2 | , python38 |
3 | }: | 3 | }: |
4 | 4 | ||
5 | stdenv.mkDerivation rec { | 5 | stdenv.mkDerivation rec { |
6 | pname = "notmuch-tcp"; | 6 | pname = "notmuch-tcp"; |
7 | version = "0.1"; | 7 | version = "1.0"; |
8 | src = ../notmuch-tcp; | 8 | src = [../notmuch-tcp-client ../notmuch-tcp-server]; |
9 | 9 | ||
10 | phases = [ "buildPhase" "installPhase" ]; | 10 | phases = [ "unpackPhase" "buildPhase" "installPhase" ]; |
11 | 11 | ||
12 | inherit zsh socat nettools coreutils; | 12 | python = python38; |
13 | |||
14 | unpackPhase = '' | ||
15 | for srcFile in $src; do | ||
16 | cp $srcFile $(stripHash $srcFile) | ||
17 | done | ||
18 | ''; | ||
13 | 19 | ||
14 | buildPhase = '' | 20 | buildPhase = '' |
15 | substituteAll $src notmuch-tcp | 21 | substituteAllInPlace notmuch-tcp-client |
22 | substituteAllInPlace notmuch-tcp-server | ||
16 | ''; | 23 | ''; |
17 | 24 | ||
18 | installPhase = '' | 25 | installPhase = '' |
19 | install -m 0755 -D -t $out/bin \ | 26 | install -m 0755 -D -t $out/bin \ |
20 | notmuch-tcp | 27 | notmuch-tcp-client notmuch-tcp-server |
28 | ln -s notmuch-tcp-client $out/bin/notmuch-tcp | ||
21 | ''; | 29 | ''; |
22 | } | 30 | } |
diff --git a/notmuch-links b/notmuch-links new file mode 100644 index 0000000..16cc01c --- /dev/null +++ b/notmuch-links | |||
@@ -0,0 +1,30 @@ | |||
1 | #!@zsh@/bin/zsh | ||
2 | |||
3 | set -xe | ||
4 | |||
5 | function notmuch { | ||
6 | NOTMUCH_TCP=${NOTMUCH_TCP:-2011} @notmuchtcp@/bin/notmuch-tcp ${@} | ||
7 | } | ||
8 | |||
9 | function browser { | ||
10 | ${BROWSER:-firefox} ${@} | ||
11 | } | ||
12 | |||
13 | maxCount=0 | ||
14 | if [[ -n "$1" && "$1" == <-> ]]; then | ||
15 | maxCount="$1" | ||
16 | fi | ||
17 | |||
18 | count=0 | ||
19 | for thread (${(z)$(notmuch search --sort=oldest-first 'tag:inbox AND is:link AND is:unread AND NOT (is:killed or tag:later)' | @coreutils@/bin/tee >(cat >&2) | @gawk@/bin/awk '{ print $1; }')}); do | ||
20 | url=$(notmuch show --format=mbox $thread | @gnused@/bin/sed -r '/^X-RSS-URL: /!d; s/^X-RSS-URL: (.*)$/\1/') | ||
21 | count=$((count + 1)) | ||
22 | |||
23 | if [[ -n "${url}" ]]; then | ||
24 | browser ${url} && notmuch tag -unread -inbox -- $thread | ||
25 | fi | ||
26 | |||
27 | if [[ ${maxCount} -gt 0 && ${count} -ge ${maxCount} ]]; then | ||
28 | exit 0 | ||
29 | fi | ||
30 | done | ||
diff --git a/notmuch-tcp b/notmuch-tcp index 57da3f5..d2f7a57 100755 --- a/notmuch-tcp +++ b/notmuch-tcp | |||
@@ -6,4 +6,4 @@ NOTMUCH_HOST=${NOTMUCH_HOST:-odin.asgard.yggdrasil} | |||
6 | 6 | ||
7 | cd ~/.notmuch-tcp | 7 | cd ~/.notmuch-tcp |
8 | 8 | ||
9 | @socat@/bin/socat STDIO,ignoreeof openssl:${NOTMUCH_HOST}:${NOTMUCH_TCP},verify=1,cafile=ca.pem,certificate=${HOST}.pem,key=${HOST}.key <<(print "${(@q)@}"; inp=$(cat); printf "%d\n" $(@coreutils@/bin/wc -c <<<"${inp}"); @coreutils@/bin/cat <<<"${inp}") | 9 | @socat@/bin/socat STDIO,ignoreeof openssl:${NOTMUCH_HOST}:${NOTMUCH_TCP},verify=1,cafile=ca.pem,certificate=${HOST}.pem,key=${HOST}.key <<(print "${(@q)@}"; inp=$(cat 2>&- || echo ""); printf "%d\n" $(@coreutils@/bin/wc -c <<<"${inp}"); @coreutils@/bin/cat <<<"${inp}") |
diff --git a/notmuch-tcp-client b/notmuch-tcp-client new file mode 100644 index 0000000..b29d6b2 --- /dev/null +++ b/notmuch-tcp-client | |||
@@ -0,0 +1,61 @@ | |||
1 | #!@python@/bin/python | ||
2 | |||
3 | from os import environ | ||
4 | from os.path import expanduser | ||
5 | import socket | ||
6 | import ssl | ||
7 | from shlex import quote | ||
8 | from sys import argv, stdin, stdout | ||
9 | from multiprocessing import Process | ||
10 | from select import select | ||
11 | |||
12 | |||
13 | port = environ.get('NOTMUCH_TCP') | ||
14 | host = environ.get('NOTMUCH_HOST') | ||
15 | hostname = socket.gethostname() | ||
16 | |||
17 | if port is None: | ||
18 | port = 2010 | ||
19 | if host is None: | ||
20 | host = "odin.asgard.yggdrasil" | ||
21 | |||
22 | |||
23 | sslcontext = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) | ||
24 | sslcontext.load_verify_locations(cafile = expanduser('~/.notmuch-tcp/ca.pem')) | ||
25 | sslcontext.load_cert_chain(certfile = expanduser(f"~/.notmuch-tcp/{hostname}.pem"), keyfile = expanduser(f"~/.notmuch-tcp/{hostname}.key")) | ||
26 | |||
27 | with socket.create_connection((host, port)) as sock: | ||
28 | with sslcontext.wrap_socket(sock, server_hostname = host) as ssock: | ||
29 | def send_args(): | ||
30 | escaped_args = ' '.join(map(quote, argv[1:])) | ||
31 | ssock.sendall(f"{escaped_args}\n".encode()) | ||
32 | |||
33 | def send_stdin(): | ||
34 | with open(0, 'rb') as stdin_bin: | ||
35 | while True: | ||
36 | to_send = stdin_bin.read(256) | ||
37 | |||
38 | if to_send: | ||
39 | ssock.sendall(to_send) | ||
40 | else: | ||
41 | break | ||
42 | |||
43 | def recv_stdout(): | ||
44 | with open(1, 'wb') as stdout_bin: | ||
45 | while True: | ||
46 | ready = select([ssock], [], [], 5) | ||
47 | if ready[0]: | ||
48 | received = ssock.recv(256) | ||
49 | if len(received) <= 0: | ||
50 | break | ||
51 | stdout_bin.write(received) | ||
52 | else: | ||
53 | break | ||
54 | |||
55 | send_args() | ||
56 | send = Process(target = send_stdin) | ||
57 | recv = Process(target = recv_stdout) | ||
58 | send.start() | ||
59 | recv.start() | ||
60 | recv.join() | ||
61 | send.terminate() | ||
diff --git a/notmuch-tcp-server b/notmuch-tcp-server new file mode 100644 index 0000000..328b72c --- /dev/null +++ b/notmuch-tcp-server | |||
@@ -0,0 +1,142 @@ | |||
1 | #!@python@/bin/python | ||
2 | |||
3 | from os import environ | ||
4 | from os.path import expanduser | ||
5 | import socket | ||
6 | import ssl | ||
7 | import shlex | ||
8 | from sys import argv, stdin, stdout, exit | ||
9 | from multiprocessing import Process | ||
10 | from select import select | ||
11 | from io import BytesIO | ||
12 | import subprocess | ||
13 | from time import sleep | ||
14 | |||
15 | import logging | ||
16 | |||
17 | |||
18 | logger = logging.getLogger('notmuch-tcp-server') | ||
19 | logger.setLevel(logging.DEBUG) | ||
20 | lh = logging.StreamHandler() | ||
21 | lh.setLevel(logging.DEBUG) | ||
22 | lh.setFormatter(logging.Formatter( fmt = '{levelname} - {message}', style = '{' )) | ||
23 | logger.addHandler(lh) | ||
24 | |||
25 | |||
26 | port = environ.get('NOTMUCH_TCP') | ||
27 | host = environ.get('NOTMUCH_HOST') | ||
28 | hostname = socket.gethostname() | ||
29 | |||
30 | if port is None: | ||
31 | port = 2010 | ||
32 | if host is None: | ||
33 | host = "odin.asgard.yggdrasil" | ||
34 | |||
35 | sslcontext = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) | ||
36 | sslcontext.load_verify_locations(cafile = expanduser('~/.notmuch-tcp/ca.pem')) | ||
37 | sslcontext.load_cert_chain(certfile = expanduser(f"~/.notmuch-tcp/{hostname}.pem"), keyfile = expanduser(f"~/.notmuch-tcp/{hostname}.key")) | ||
38 | |||
39 | s = None | ||
40 | for res in socket.getaddrinfo(host, int(port), socket.AF_UNSPEC, socket.SOCK_STREAM, 0, socket.AI_PASSIVE): | ||
41 | af, socktype, proto, canonname, sa = res | ||
42 | try: | ||
43 | s = socket.socket(af, socktype, proto) | ||
44 | except OSError as msg: | ||
45 | logger.error(f"Could not create socket: {msg}") | ||
46 | s = None | ||
47 | continue | ||
48 | try: | ||
49 | s.bind(sa) | ||
50 | s.listen() | ||
51 | except OSError as msg: | ||
52 | logger.error(f"Could not bind socket: {msg}") | ||
53 | s.close() | ||
54 | s = None | ||
55 | continue | ||
56 | break | ||
57 | if s is None: | ||
58 | logger.error('Could not open any sockets') | ||
59 | exit(1) | ||
60 | |||
61 | with s as sock: | ||
62 | with sslcontext.wrap_socket(sock, server_side = True) as ssock: | ||
63 | while True: | ||
64 | logger.debug('waiting...') | ||
65 | conn = None | ||
66 | try: | ||
67 | conn, client_addr = ssock.accept() | ||
68 | client_host, client_port = client_addr | ||
69 | logger.info(f"Connected: {client_host}:{client_port}") | ||
70 | |||
71 | with BytesIO() as buffer: | ||
72 | while True: | ||
73 | resp = conn.recv(256) | ||
74 | buffer.write(resp) | ||
75 | buffer.seek(0) | ||
76 | start_index = 0 | ||
77 | for line in buffer: | ||
78 | start_index += len(line) | ||
79 | escaped_args = line[:-1].decode() | ||
80 | break | ||
81 | if start_index: | ||
82 | buffer.seek(start_index) | ||
83 | remaining = buffer.read() | ||
84 | buffer.truncate(0) | ||
85 | buffer.seek(0) | ||
86 | buffer.write(remaining) | ||
87 | break | ||
88 | else: | ||
89 | buffer.seek(0, 2) | ||
90 | |||
91 | logger.info(f"Arguments: {escaped_args}") | ||
92 | |||
93 | sproc = subprocess.Popen(["notmuch"] + shlex.split(escaped_args), shell = False, stdin = subprocess.PIPE, stdout = subprocess.PIPE) | ||
94 | |||
95 | def send_stdout(): | ||
96 | while True: | ||
97 | to_send = sproc.stdout.read(256) | ||
98 | logger.debug(f"Sending: {to_send}") | ||
99 | |||
100 | if to_send: | ||
101 | conn.sendall(to_send) | ||
102 | else: | ||
103 | logger.debug(f"Done sending") | ||
104 | break | ||
105 | |||
106 | def recv_stdin(): | ||
107 | remaining = buffer.read() | ||
108 | logger.debug(f"Received (buffered): {remaining}") | ||
109 | sproc.stdin.write(remaining) | ||
110 | |||
111 | while True: | ||
112 | logger.debug("Waiting on input...") | ||
113 | ready = select([conn], [], [], 5) | ||
114 | if ready[0]: | ||
115 | resp = conn.recv(256) | ||
116 | logger.debug(f"Received: {resp}") | ||
117 | |||
118 | if len(resp) <= 0: | ||
119 | break | ||
120 | |||
121 | sproc.stdin.write(resp) | ||
122 | else: | ||
123 | break | ||
124 | logger.debug(f"Done receiving") | ||
125 | sproc.stdin.close() | ||
126 | |||
127 | sleep(5) | ||
128 | |||
129 | sproc.kill() | ||
130 | |||
131 | send = Process(target = send_stdout) | ||
132 | recv = Process(target = recv_stdin) | ||
133 | send.start() | ||
134 | recv.start() | ||
135 | sproc_res = sproc.wait() | ||
136 | logger.info(f"Subprocess result: {sproc_res}") | ||
137 | send.join() | ||
138 | recv.terminate() | ||
139 | logger.debug(f"Handled I/O") | ||
140 | finally: | ||
141 | if conn is not None: | ||
142 | conn.close() | ||
diff --git a/worktime.py b/worktime.py index 9c72d30..19e2186 100755 --- a/worktime.py +++ b/worktime.py | |||
@@ -60,7 +60,6 @@ class TogglAPI(object): | |||
60 | return now - start if start <= now else None | 60 | return now - start if start <= now else None |
61 | 61 | ||
62 | class Worktime(object): | 62 | class Worktime(object): |
63 | time_to_work = timedelta() | ||
64 | time_worked = timedelta() | 63 | time_worked = timedelta() |
65 | running_entry = None | 64 | running_entry = None |
66 | now = datetime.now(tzlocal()) | 65 | now = datetime.now(tzlocal()) |
@@ -82,34 +81,40 @@ class Worktime(object): | |||
82 | 81 | ||
83 | hours_per_week = float(config.get('WORKTIME', 'HoursPerWeek', fallback=40)) | 82 | hours_per_week = float(config.get('WORKTIME', 'HoursPerWeek', fallback=40)) |
84 | workdays = set([int(d.strip()) for d in config.get('WORKTIME', 'Workdays', fallback='1,2,3,4,5').split(',')]) | 83 | workdays = set([int(d.strip()) for d in config.get('WORKTIME', 'Workdays', fallback='1,2,3,4,5').split(',')]) |
85 | hours_per_day = hours_per_week / len(workdays) | 84 | time_per_day = timedelta(hours = hours_per_week) / len(workdays) |
86 | 85 | ||
87 | holidays = set() | 86 | holidays = dict() |
88 | 87 | ||
89 | for year in range(start_date.year, end_date.year + 1): | 88 | for year in range(start_date.year, end_date.year + 1): |
90 | y_easter = datetime.combine(easter(year), time(), tzinfo=tzlocal()) | 89 | y_easter = datetime.combine(easter(year), time(), tzinfo=tzlocal()) |
91 | 90 | ||
92 | # Legal holidays in munich, bavaria | 91 | # Legal holidays in munich, bavaria |
93 | holidays.add(datetime(year, 1, 1, tzinfo=tzlocal()).date()) | 92 | holidays[datetime(year, 1, 1, tzinfo=tzlocal()).date()] = time_per_day |
94 | holidays.add(datetime(year, 1, 6, tzinfo=tzlocal()).date()) | 93 | holidays[datetime(year, 1, 6, tzinfo=tzlocal()).date()] = time_per_day |
95 | holidays.add((y_easter+timedelta(days=-2)).date()) | 94 | holidays[(y_easter+timedelta(days=-2)).date()] = time_per_day |
96 | holidays.add((y_easter+timedelta(days=+1)).date()) | 95 | holidays[(y_easter+timedelta(days=+1)).date()] = time_per_day |
97 | holidays.add(datetime(year, 5, 1, tzinfo=tzlocal()).date()) | 96 | holidays[datetime(year, 5, 1, tzinfo=tzlocal()).date()] = time_per_day |
98 | holidays.add((y_easter+timedelta(days=+39)).date()) | 97 | holidays[(y_easter+timedelta(days=+39)).date()] = time_per_day |
99 | holidays.add((y_easter+timedelta(days=+50)).date()) | 98 | holidays[(y_easter+timedelta(days=+50)).date()] = time_per_day |
100 | holidays.add((y_easter+timedelta(days=+60)).date()) | 99 | holidays[(y_easter+timedelta(days=+60)).date()] = time_per_day |
101 | holidays.add(datetime(year, 8, 15, tzinfo=tzlocal()).date()) | 100 | holidays[datetime(year, 8, 15, tzinfo=tzlocal()).date()] = time_per_day |
102 | holidays.add(datetime(year, 10, 3, tzinfo=tzlocal()).date()) | 101 | holidays[datetime(year, 10, 3, tzinfo=tzlocal()).date()] = time_per_day |
103 | holidays.add(datetime(year, 11, 1, tzinfo=tzlocal()).date()) | 102 | holidays[datetime(year, 11, 1, tzinfo=tzlocal()).date()] = time_per_day |
104 | holidays.add(datetime(year, 12, 25, tzinfo=tzlocal()).date()) | 103 | holidays[datetime(year, 12, 25, tzinfo=tzlocal()).date()] = time_per_day |
105 | holidays.add(datetime(year, 12, 26, tzinfo=tzlocal()).date()) | 104 | holidays[datetime(year, 12, 26, tzinfo=tzlocal()).date()] = time_per_day |
106 | 105 | ||
107 | try: | 106 | try: |
108 | with open(f"{config_dir}/excused", 'r') as excused: | 107 | with open(f"{config_dir}/excused", 'r') as excused: |
109 | for line in excused: | 108 | for line in excused: |
110 | stripped_line = line.strip() | 109 | stripped_line = line.strip() |
111 | if stripped_line: | 110 | if stripped_line: |
112 | holidays.add(datetime.strptime(stripped_line, date_format).replace(tzinfo=tzlocal()).date()) | 111 | splitLine = stripped_line.split(' ') |
112 | if len(splitLine) == 2: | ||
113 | [hours, date] = splitLine | ||
114 | day = datetime.strptime(date, date_format).replace(tzinfo=tzlocal()).date() | ||
115 | holidays[day] = timedelta(hours = float(hours)) | ||
116 | else: | ||
117 | holidays[datetime.strptime(stripped_line, date_format).replace(tzinfo=tzlocal()).date()] = time_per_day | ||
113 | except IOError as e: | 118 | except IOError as e: |
114 | if e.errno != 2: | 119 | if e.errno != 2: |
115 | raise e | 120 | raise e |
@@ -130,7 +135,7 @@ class Worktime(object): | |||
130 | raise e | 135 | raise e |
131 | 136 | ||
132 | 137 | ||
133 | days_to_work = set() | 138 | days_to_work = dict() |
134 | 139 | ||
135 | start_day = start_date.date() | 140 | start_day = start_date.date() |
136 | end_day = end_date.date() | 141 | end_day = end_date.date() |
@@ -138,14 +143,18 @@ class Worktime(object): | |||
138 | end_day = max(end_day, max(list(pull_forward))) | 143 | end_day = max(end_day, max(list(pull_forward))) |
139 | 144 | ||
140 | for day in [start_day + timedelta(days = x) for x in range(0, (end_day - start_day).days + 1)]: | 145 | for day in [start_day + timedelta(days = x) for x in range(0, (end_day - start_day).days + 1)]: |
141 | if day.isoweekday() in workdays and not day in holidays: | 146 | if day.isoweekday() in workdays: |
142 | days_to_work.add(day) | 147 | time_to_work = time_per_day |
148 | if day in holidays.keys(): | ||
149 | time_to_work -= holidays[day] | ||
150 | if time_to_work > timedelta(): | ||
151 | days_to_work[day] = time_to_work | ||
143 | 152 | ||
144 | self.is_workday = self.now.date() in days_to_work | 153 | self.is_workday = self.now.date() in days_to_work |
145 | 154 | ||
146 | self.time_to_work = timedelta(hours = len([day for day in days_to_work if day <= end_date.date()]) * hours_per_day) | 155 | self.time_to_work = sum([days_to_work[day] for day in days_to_work.keys() if day <= end_date.date()], timedelta()) |
147 | for day in list(pull_forward): | 156 | for day in list(pull_forward): |
148 | days_forward = set([d for d in days_to_work if d >= end_date.date() and d < day and not d in pull_forward]) | 157 | days_forward = set([d for d in days_to_work.keys() if d >= end_date.date() and d < day and not d in pull_forward]) |
149 | hours_per_day_forward = pull_forward[day] / len(days_forward) if len(days_forward) > 0 else timedelta() | 158 | hours_per_day_forward = pull_forward[day] / len(days_forward) if len(days_forward) > 0 else timedelta() |
150 | days_forward.discard(end_date.date()) | 159 | days_forward.discard(end_date.date()) |
151 | self.time_pulled_forward += pull_forward[day] - hours_per_day_forward * len(days_forward) | 160 | self.time_pulled_forward += pull_forward[day] - hours_per_day_forward * len(days_forward) |