py2ls 0.1.10.12__py3-none-any.whl → 0.2.7.10__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of py2ls might be problematic. Click here for more details.
- py2ls/.DS_Store +0 -0
- py2ls/.git/.DS_Store +0 -0
- py2ls/.git/index +0 -0
- py2ls/.git/logs/refs/remotes/origin/HEAD +1 -0
- py2ls/.git/objects/.DS_Store +0 -0
- py2ls/.git/refs/.DS_Store +0 -0
- py2ls/ImageLoader.py +621 -0
- py2ls/__init__.py +7 -5
- py2ls/apptainer2ls.py +3940 -0
- py2ls/batman.py +164 -42
- py2ls/bio.py +2595 -0
- py2ls/cell_image_clf.py +1632 -0
- py2ls/container2ls.py +4635 -0
- py2ls/corr.py +475 -0
- py2ls/data/.DS_Store +0 -0
- py2ls/data/email/email_html_template.html +88 -0
- py2ls/data/hyper_param_autogluon_zeroshot2024.json +2383 -0
- py2ls/data/hyper_param_tabrepo_2024.py +1753 -0
- py2ls/data/mygenes_fields_241022.txt +355 -0
- py2ls/data/re_common_pattern.json +173 -0
- py2ls/data/sns_info.json +74 -0
- py2ls/data/styles/.DS_Store +0 -0
- py2ls/data/styles/example/.DS_Store +0 -0
- py2ls/data/styles/stylelib/.DS_Store +0 -0
- py2ls/data/styles/stylelib/grid.mplstyle +15 -0
- py2ls/data/styles/stylelib/high-contrast.mplstyle +6 -0
- py2ls/data/styles/stylelib/high-vis.mplstyle +4 -0
- py2ls/data/styles/stylelib/ieee.mplstyle +15 -0
- py2ls/data/styles/stylelib/light.mplstyl +6 -0
- py2ls/data/styles/stylelib/muted.mplstyle +6 -0
- py2ls/data/styles/stylelib/nature-reviews-latex.mplstyle +616 -0
- py2ls/data/styles/stylelib/nature-reviews.mplstyle +616 -0
- py2ls/data/styles/stylelib/nature.mplstyle +31 -0
- py2ls/data/styles/stylelib/no-latex.mplstyle +10 -0
- py2ls/data/styles/stylelib/notebook.mplstyle +36 -0
- py2ls/data/styles/stylelib/paper.mplstyle +290 -0
- py2ls/data/styles/stylelib/paper2.mplstyle +305 -0
- py2ls/data/styles/stylelib/retro.mplstyle +4 -0
- py2ls/data/styles/stylelib/sans.mplstyle +10 -0
- py2ls/data/styles/stylelib/scatter.mplstyle +7 -0
- py2ls/data/styles/stylelib/science.mplstyle +48 -0
- py2ls/data/styles/stylelib/std-colors.mplstyle +4 -0
- py2ls/data/styles/stylelib/vibrant.mplstyle +6 -0
- py2ls/data/tiles.csv +146 -0
- py2ls/data/usages_pd.json +1417 -0
- py2ls/data/usages_sns.json +31 -0
- py2ls/docker2ls.py +5446 -0
- py2ls/ec2ls.py +61 -0
- py2ls/fetch_update.py +145 -0
- py2ls/ich2ls.py +1955 -296
- py2ls/im2.py +8242 -0
- py2ls/image_ml2ls.py +2100 -0
- py2ls/ips.py +33909 -3418
- py2ls/ml2ls.py +7700 -0
- py2ls/mol.py +289 -0
- py2ls/mount2ls.py +1307 -0
- py2ls/netfinder.py +873 -351
- py2ls/nl2ls.py +283 -0
- py2ls/ocr.py +1581 -458
- py2ls/plot.py +10394 -314
- py2ls/rna2ls.py +311 -0
- py2ls/ssh2ls.md +456 -0
- py2ls/ssh2ls.py +5933 -0
- py2ls/ssh2ls_v01.py +2204 -0
- py2ls/stats.py +66 -172
- py2ls/temp20251124.py +509 -0
- py2ls/translator.py +2 -0
- py2ls/utils/decorators.py +3564 -0
- py2ls/utils_bio.py +3453 -0
- {py2ls-0.1.10.12.dist-info → py2ls-0.2.7.10.dist-info}/METADATA +113 -224
- {py2ls-0.1.10.12.dist-info → py2ls-0.2.7.10.dist-info}/RECORD +72 -16
- {py2ls-0.1.10.12.dist-info → py2ls-0.2.7.10.dist-info}/WHEEL +0 -0
py2ls/temp20251124.py
ADDED
|
@@ -0,0 +1,509 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Ultimate December Duty Package Generator
|
|
4
|
+
Generates:
|
|
5
|
+
- Excel workbook: Calendar + AML rotation + CAR-T + Volunteers + TOIL log + Policy & Messages
|
|
6
|
+
- Printable PDF schedule (calendar + assigned minimal duties + critical day highlights)
|
|
7
|
+
- Policy PDF
|
|
8
|
+
- Slack message text file
|
|
9
|
+
- Email-to-PI text file
|
|
10
|
+
- Fair rotation table CSV + PDF
|
|
11
|
+
|
|
12
|
+
Usage:
|
|
13
|
+
python generate_december_duty_package.py --year 2025 --month 12 --vacation_csv vacations.csv
|
|
14
|
+
|
|
15
|
+
If vacation_csv omitted, sample vacation data will be used.
|
|
16
|
+
|
|
17
|
+
Author: Jeff's assistant (ultimate script)
|
|
18
|
+
"""
|
|
19
|
+
import argparse
|
|
20
|
+
from datetime import datetime, timedelta
|
|
21
|
+
import calendar
|
|
22
|
+
import pandas as pd
|
|
23
|
+
import numpy as np
|
|
24
|
+
import os
|
|
25
|
+
from io import BytesIO
|
|
26
|
+
import matplotlib.pyplot as plt
|
|
27
|
+
from matplotlib.backends.backend_pdf import PdfPages
|
|
28
|
+
from reportlab.lib.pagesizes import A4, landscape
|
|
29
|
+
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle, PageBreak
|
|
30
|
+
from reportlab.lib.styles import getSampleStyleSheet
|
|
31
|
+
from reportlab.lib import colors
|
|
32
|
+
from tabulate import tabulate
|
|
33
|
+
|
|
34
|
+
# ---------- CONFIG / DEFAULTS ----------
|
|
35
|
+
AML_ROTATION = ["L", "S", "A", "T", "N"] # user-provided AML rotation order
|
|
36
|
+
CAR_T_MAIN = ["J"] # main CAR-T person(s)
|
|
37
|
+
CAR_T_BACKUP = ["TechnicianBackup"] # replace with real name(s)
|
|
38
|
+
|
|
39
|
+
OUTPUT_DIR = "december_duty_output"
|
|
40
|
+
EXCEL_FILENAME = "December_Duty_Ultimate_Template.xlsx"
|
|
41
|
+
PDF_SCHEDULE_FILENAME = "December_Duty_Schedule.pdf"
|
|
42
|
+
POLICY_PDF_FILENAME = "December_Duty_Policy.pdf"
|
|
43
|
+
SLACK_FILENAME = "Slack_message_volunteers.txt"
|
|
44
|
+
PI_EMAIL_FILENAME = "Email_to_PI.txt"
|
|
45
|
+
ROTATION_CSV = "Fair_Rotation_Table.csv"
|
|
46
|
+
ROTATION_PDF = "Fair_Rotation_Table.pdf"
|
|
47
|
+
|
|
48
|
+
# TOIL rules
|
|
49
|
+
TOIL_SHORT_SHIFT = 0.5 # half-day for short minimal duty
|
|
50
|
+
TOIL_FULL_SHIFT = 1.0 # full day for full minimal duty
|
|
51
|
+
TOIL_VALIDITY_MONTHS = 3 # take TOIL within this many months
|
|
52
|
+
|
|
53
|
+
# Critical threshold: day is critical if available AML-trained staff <= threshold
|
|
54
|
+
CRITICAL_THRESHOLD = 1
|
|
55
|
+
|
|
56
|
+
# ---------- HELPERS ----------
|
|
57
|
+
def ensure_output_dir():
|
|
58
|
+
os.makedirs(OUTPUT_DIR, exist_ok=True)
|
|
59
|
+
return OUTPUT_DIR
|
|
60
|
+
|
|
61
|
+
def daterange(start_date, end_date):
|
|
62
|
+
for n in range(int((end_date - start_date).days) + 1):
|
|
63
|
+
yield start_date + timedelta(n)
|
|
64
|
+
|
|
65
|
+
def read_vacations_csv(path):
|
|
66
|
+
"""
|
|
67
|
+
Expected CSV format:
|
|
68
|
+
name,start,end
|
|
69
|
+
L,2025-12-20,2025-12-31
|
|
70
|
+
S,2025-12-24,2025-12-26
|
|
71
|
+
"""
|
|
72
|
+
df = pd.read_csv(path, parse_dates=["start", "end"])
|
|
73
|
+
return df
|
|
74
|
+
|
|
75
|
+
# ---------- MAIN GENERATORS ----------
|
|
76
|
+
def build_calendar_df(year, month):
|
|
77
|
+
start = datetime(year, month, 1)
|
|
78
|
+
last_day = calendar.monthrange(year, month)[1]
|
|
79
|
+
end = datetime(year, month, last_day)
|
|
80
|
+
dates = pd.date_range(start=start, end=end)
|
|
81
|
+
df = pd.DataFrame({
|
|
82
|
+
"date": dates,
|
|
83
|
+
"weekday": dates.strftime("%a"),
|
|
84
|
+
"day": dates.day,
|
|
85
|
+
"is_weekend": dates.weekday >= 5
|
|
86
|
+
})
|
|
87
|
+
df["public_holiday"] = False # user can mark later
|
|
88
|
+
df["avail_aml_count"] = np.nan
|
|
89
|
+
df["critical_flag"] = False
|
|
90
|
+
return df
|
|
91
|
+
|
|
92
|
+
def apply_vacations_to_calendar(calendar_df, vacations_df, aml_people, car_t_main, car_t_backup):
|
|
93
|
+
"""
|
|
94
|
+
Mark who is available on each day from AML and CAR-T lists.
|
|
95
|
+
vacations_df columns: name,start,end
|
|
96
|
+
Return:
|
|
97
|
+
calendar_df with columns 'available_aml' (list) and 'available_car_t'
|
|
98
|
+
"""
|
|
99
|
+
cal = calendar_df.copy()
|
|
100
|
+
cal["available_aml"] = [[] for _ in range(len(cal))]
|
|
101
|
+
cal["available_car_t"] = [[] for _ in range(len(cal))]
|
|
102
|
+
|
|
103
|
+
vac_periods = {}
|
|
104
|
+
if vacations_df is not None:
|
|
105
|
+
for _, row in vacations_df.iterrows():
|
|
106
|
+
name = str(row["name"]).strip()
|
|
107
|
+
vac_periods.setdefault(name, []).append((row["start"].to_pydatetime().date(),
|
|
108
|
+
row["end"].to_pydatetime().date()))
|
|
109
|
+
|
|
110
|
+
for idx, row in cal.iterrows():
|
|
111
|
+
d = row["date"].date()
|
|
112
|
+
# AML people availability
|
|
113
|
+
for p in aml_people:
|
|
114
|
+
# if p has any vacation covering this date -> not available
|
|
115
|
+
off = False
|
|
116
|
+
for pr in vac_periods.get(p, []):
|
|
117
|
+
if pr[0] <= d <= pr[1]:
|
|
118
|
+
off = True
|
|
119
|
+
break
|
|
120
|
+
if not off:
|
|
121
|
+
cal.at[idx, "available_aml"].append(p)
|
|
122
|
+
# CAR-T main
|
|
123
|
+
for p in car_t_main + car_t_backup:
|
|
124
|
+
off = False
|
|
125
|
+
for pr in vac_periods.get(p, []):
|
|
126
|
+
if pr[0] <= d <= pr[1]:
|
|
127
|
+
off = True
|
|
128
|
+
break
|
|
129
|
+
if not off:
|
|
130
|
+
cal.at[idx, "available_car_t"].append(p)
|
|
131
|
+
|
|
132
|
+
cal.at[idx, "avail_aml_count"] = len(cal.at[idx, "available_aml"])
|
|
133
|
+
cal.at[idx, "critical_flag"] = cal.at[idx, "avail_aml_count"] <= CRITICAL_THRESHOLD
|
|
134
|
+
return cal
|
|
135
|
+
|
|
136
|
+
def generate_aml_rotation_sheet(calendar_df, aml_people):
|
|
137
|
+
"""
|
|
138
|
+
Create a suggested AML rotation "assigned order" repeating across the month.
|
|
139
|
+
Note: AML arrival detection is event-driven; this is a queue, not day assignment.
|
|
140
|
+
We generate a repeating sequence that can be used as "next responsible".
|
|
141
|
+
"""
|
|
142
|
+
cal = calendar_df.copy()
|
|
143
|
+
n = len(cal)
|
|
144
|
+
seq = (aml_people * ((n // len(aml_people)) + 2))[:n]
|
|
145
|
+
cal["assigned_next_in_queue"] = seq
|
|
146
|
+
cal["actual_processed_by"] = ""
|
|
147
|
+
cal["aml_notes"] = ""
|
|
148
|
+
return cal[["date", "weekday", "day", "assigned_next_in_queue", "actual_processed_by", "aml_notes"]]
|
|
149
|
+
|
|
150
|
+
def create_excel(calendar_df, aml_sheet_df, car_t_df, volunteers_df, toil_log_df, messages_dict, policy_text):
|
|
151
|
+
outdir = ensure_output_dir()
|
|
152
|
+
out_path = os.path.join(outdir, EXCEL_FILENAME)
|
|
153
|
+
writer = pd.ExcelWriter(out_path, engine="xlsxwriter", datetime_format="yyyy-mm-dd", date_format="yyyy-mm-dd")
|
|
154
|
+
workbook = writer.book
|
|
155
|
+
|
|
156
|
+
# Calendar sheet
|
|
157
|
+
cal_w = calendar_df.copy()
|
|
158
|
+
# Flatten available lists to strings for Excel view
|
|
159
|
+
cal_w["available_aml"] = cal_w["available_aml"].apply(lambda x: ", ".join(x) if isinstance(x, list) else "")
|
|
160
|
+
cal_w["available_car_t"] = cal_w["available_car_t"].apply(lambda x: ", ".join(x) if isinstance(x, list) else "")
|
|
161
|
+
cal_w.to_excel(writer, sheet_name="Calendar_Dec", index=False)
|
|
162
|
+
|
|
163
|
+
# AML rotation
|
|
164
|
+
aml_sheet_df.to_excel(writer, sheet_name="AML_Rotation", index=False)
|
|
165
|
+
|
|
166
|
+
# CAR-T sheet
|
|
167
|
+
car_t_df.to_excel(writer, sheet_name="CAR-T", index=False)
|
|
168
|
+
|
|
169
|
+
# Volunteers + TOIL
|
|
170
|
+
volunteers_df.to_excel(writer, sheet_name="Volunteers", index=False)
|
|
171
|
+
toil_log_df.to_excel(writer, sheet_name="TOIL_Log", index=False)
|
|
172
|
+
|
|
173
|
+
# Messages and policy sheet as text
|
|
174
|
+
# We write them into one sheet with wide column
|
|
175
|
+
messages_df = pd.DataFrame({
|
|
176
|
+
"type":["slack_message","email_to_pi","fair_rotation_rules","policy"],
|
|
177
|
+
"content":[messages_dict["slack"], messages_dict["email"], messages_dict["fair_rotation"], policy_text]
|
|
178
|
+
})
|
|
179
|
+
messages_df.to_excel(writer, sheet_name="Messages_and_Policy", index=False)
|
|
180
|
+
|
|
181
|
+
# Conditional formatting for critical days on calendar
|
|
182
|
+
worksheet = writer.sheets["Calendar_Dec"]
|
|
183
|
+
# find the row numbers for avail_aml_count and critical_flag (we wrote headers)
|
|
184
|
+
# Use column indices by label:
|
|
185
|
+
header = cal_w.columns.tolist()
|
|
186
|
+
try:
|
|
187
|
+
col_avail_idx = header.index("avail_aml_count")
|
|
188
|
+
col_crit_idx = header.index("critical_flag")
|
|
189
|
+
except ValueError:
|
|
190
|
+
col_avail_idx = None
|
|
191
|
+
col_crit_idx = None
|
|
192
|
+
|
|
193
|
+
# Apply conditional format: highlight rows where critical_flag == True
|
|
194
|
+
if col_crit_idx is not None:
|
|
195
|
+
# Excel uses A1 notation; compute range
|
|
196
|
+
row_count = len(cal_w)
|
|
197
|
+
# columns start at A (0) -> +1 for Excel column. We'll color the avail_aml_count column to highlight.
|
|
198
|
+
crit_col_letter = xlsx_colname(col_crit_idx + 1)
|
|
199
|
+
# highlight True cells
|
|
200
|
+
worksheet.conditional_format(f"{crit_col_letter}2:{crit_col_letter}{row_count+1}", {
|
|
201
|
+
'type': 'cell',
|
|
202
|
+
'criteria': 'equal to',
|
|
203
|
+
'value': True,
|
|
204
|
+
'format': workbook.add_format({'bg_color': '#FFC7CE'}) # light red
|
|
205
|
+
})
|
|
206
|
+
writer.close()
|
|
207
|
+
return out_path
|
|
208
|
+
|
|
209
|
+
def xlsx_colname(col):
|
|
210
|
+
"""1-indexed column number -> Excel column letter"""
|
|
211
|
+
string = ""
|
|
212
|
+
while col > 0:
|
|
213
|
+
col, remainder = divmod(col-1, 26)
|
|
214
|
+
string = chr(65 + remainder) + string
|
|
215
|
+
return string
|
|
216
|
+
|
|
217
|
+
def generate_car_t_sheet(calendar_df, car_t_main, car_t_backup):
|
|
218
|
+
cal = calendar_df.copy()
|
|
219
|
+
df = pd.DataFrame({
|
|
220
|
+
"date": cal["date"],
|
|
221
|
+
"weekday": cal["weekday"],
|
|
222
|
+
"CAR-T_main": [", ".join(car_t_main)] * len(cal),
|
|
223
|
+
"CAR-T_backup": [", ".join(car_t_backup)] * len(cal),
|
|
224
|
+
"available_car_t_list": cal["available_car_t"].apply(lambda x: ", ".join(x) if isinstance(x, list) else "")
|
|
225
|
+
})
|
|
226
|
+
df["note"] = ""
|
|
227
|
+
return df
|
|
228
|
+
|
|
229
|
+
def generate_volunteer_sheet(calendar_df):
|
|
230
|
+
df = pd.DataFrame({
|
|
231
|
+
"date": calendar_df["date"],
|
|
232
|
+
"weekday": calendar_df["weekday"],
|
|
233
|
+
"critical_day": calendar_df["critical_flag"],
|
|
234
|
+
"volunteer_name": "",
|
|
235
|
+
"volunteer_contact": "",
|
|
236
|
+
"toil_days_granted": 0.0,
|
|
237
|
+
"notes": ""
|
|
238
|
+
})
|
|
239
|
+
return df
|
|
240
|
+
|
|
241
|
+
def generate_toil_log():
|
|
242
|
+
df = pd.DataFrame(columns=["name", "date_covered", "toil_days", "taken_YN", "notes"])
|
|
243
|
+
return df
|
|
244
|
+
|
|
245
|
+
# ---------- PDF / textual exports ----------
|
|
246
|
+
def create_pdf_schedule(calendar_df, aml_sheet_df, volunteers_df, out_path):
|
|
247
|
+
"""
|
|
248
|
+
Create a simple multi-page PDF schedule showing:
|
|
249
|
+
- calendar with critical days highlighted
|
|
250
|
+
- AML rotation table (assigned queue)
|
|
251
|
+
- volunteer signups (if provided)
|
|
252
|
+
"""
|
|
253
|
+
outdir = ensure_output_dir()
|
|
254
|
+
pdf_path = os.path.join(outdir, out_path)
|
|
255
|
+
|
|
256
|
+
styles = getSampleStyleSheet()
|
|
257
|
+
doc = SimpleDocTemplate(pdf_path, pagesize=landscape(A4))
|
|
258
|
+
elements = []
|
|
259
|
+
|
|
260
|
+
title = Paragraph("<b>December Duty Schedule</b>", styles['Title'])
|
|
261
|
+
elements.append(title)
|
|
262
|
+
elements.append(Spacer(1, 12))
|
|
263
|
+
|
|
264
|
+
# Calendar table: date, weekday, avail_aml_count, available_aml, critical_flag
|
|
265
|
+
cal = calendar_df.copy()
|
|
266
|
+
cal['date_s'] = cal['date'].dt.strftime("%Y-%m-%d")
|
|
267
|
+
cal_table = cal[["date_s", "weekday", "avail_aml_count", "available_aml", "critical_flag"]]
|
|
268
|
+
cal_table = cal_table.fillna("")
|
|
269
|
+
table_data = [cal_table.columns.tolist()] + cal_table.values.tolist()
|
|
270
|
+
t = Table(table_data, repeatRows=1)
|
|
271
|
+
t.setStyle(TableStyle([
|
|
272
|
+
('BACKGROUND', (0,0), (-1,0), colors.grey),
|
|
273
|
+
('FONTNAME', (0,0), (-1,0), 'Helvetica-Bold'),
|
|
274
|
+
('INNERGRID', (0,0), (-1,-1), 0.25, colors.black),
|
|
275
|
+
('BOX', (0,0), (-1,-1), 0.5, colors.black),
|
|
276
|
+
]))
|
|
277
|
+
elements.append(Paragraph("<b>Calendar (availability & critical flags)</b>", styles['Heading2']))
|
|
278
|
+
elements.append(t)
|
|
279
|
+
elements.append(PageBreak())
|
|
280
|
+
|
|
281
|
+
# AML rotation (short)
|
|
282
|
+
aml = aml_sheet_df.copy()
|
|
283
|
+
aml['date_s'] = aml['date'].dt.strftime("%Y-%m-%d")
|
|
284
|
+
aml_table = aml[["date_s", "weekday", "assigned_next_in_queue", "actual_processed_by", "aml_notes"]]
|
|
285
|
+
aml_table = aml_table.fillna("")
|
|
286
|
+
t2 = Table([aml_table.columns.tolist()] + aml_table.values.tolist(), repeatRows=1)
|
|
287
|
+
t2.setStyle(TableStyle([
|
|
288
|
+
('BACKGROUND', (0,0), (-1,0), colors.grey),
|
|
289
|
+
('INNERGRID', (0,0), (-1,-1), 0.25, colors.black),
|
|
290
|
+
]))
|
|
291
|
+
elements.append(Paragraph("<b>AML Rotation (suggested queue)</b>", styles['Heading2']))
|
|
292
|
+
elements.append(t2)
|
|
293
|
+
elements.append(PageBreak())
|
|
294
|
+
|
|
295
|
+
# Volunteers
|
|
296
|
+
vol = volunteers_df.copy()
|
|
297
|
+
if "date" in vol.columns:
|
|
298
|
+
vol['date_s'] = vol['date'].dt.strftime("%Y-%m-%d")
|
|
299
|
+
vol_table = vol[["date_s", "weekday", "critical_day", "volunteer_name", "toil_days_granted", "notes"]].fillna("")
|
|
300
|
+
t3 = Table([vol_table.columns.tolist()] + vol_table.values.tolist(), repeatRows=1)
|
|
301
|
+
t3.setStyle(TableStyle([
|
|
302
|
+
('BACKGROUND', (0,0), (-1,0), colors.grey),
|
|
303
|
+
('INNERGRID', (0,0), (-1,-1), 0.25, colors.black),
|
|
304
|
+
]))
|
|
305
|
+
elements.append(Paragraph("<b>Volunteer Signups</b>", styles['Heading2']))
|
|
306
|
+
elements.append(t3)
|
|
307
|
+
|
|
308
|
+
doc.build(elements)
|
|
309
|
+
return pdf_path
|
|
310
|
+
|
|
311
|
+
def create_policy_pdf(policy_text, out_path):
|
|
312
|
+
outdir = ensure_output_dir()
|
|
313
|
+
pdf_path = os.path.join(outdir, out_path)
|
|
314
|
+
doc = SimpleDocTemplate(pdf_path, pagesize=A4)
|
|
315
|
+
styles = getSampleStyleSheet()
|
|
316
|
+
elements = []
|
|
317
|
+
elements.append(Paragraph("<b>Holiday Season Duty Coverage Policy</b>", styles['Title']))
|
|
318
|
+
elements.append(Spacer(1, 12))
|
|
319
|
+
# split long policy into paragraphs by blank lines
|
|
320
|
+
for chunk in policy_text.strip().split("\n\n"):
|
|
321
|
+
elements.append(Paragraph(chunk.replace("\n", "<br/>"), styles['BodyText']))
|
|
322
|
+
elements.append(Spacer(1, 8))
|
|
323
|
+
doc.build(elements)
|
|
324
|
+
return pdf_path
|
|
325
|
+
|
|
326
|
+
def save_plain_text_message(msg, filename):
|
|
327
|
+
outdir = ensure_output_dir()
|
|
328
|
+
p = os.path.join(outdir, filename)
|
|
329
|
+
with open(p, "w", encoding="utf-8") as f:
|
|
330
|
+
f.write(msg)
|
|
331
|
+
return p
|
|
332
|
+
|
|
333
|
+
def export_rotation_csv_pdf(rotation_df, csv_name, pdf_name):
|
|
334
|
+
outdir = ensure_output_dir()
|
|
335
|
+
csv_path = os.path.join(outdir, csv_name)
|
|
336
|
+
rotation_df.to_csv(csv_path, index=False)
|
|
337
|
+
|
|
338
|
+
# quick PDF using matplotlib table
|
|
339
|
+
pdf_path = os.path.join(outdir, pdf_name)
|
|
340
|
+
fig, ax = plt.subplots(figsize=(11.69, 8.27)) # A4 landscape in inches
|
|
341
|
+
ax.axis('off')
|
|
342
|
+
tbl = ax.table(cellText=rotation_df.values, colLabels=rotation_df.columns, loc='center', cellLoc='center')
|
|
343
|
+
tbl.auto_set_font_size(False)
|
|
344
|
+
tbl.set_fontsize(8)
|
|
345
|
+
tbl.scale(1, 1.2)
|
|
346
|
+
plt.savefig(pdf_path, bbox_inches='tight')
|
|
347
|
+
plt.close(fig)
|
|
348
|
+
return csv_path, pdf_path
|
|
349
|
+
|
|
350
|
+
# ---------- FAIR ROTATION SUMMARY builder ----------
|
|
351
|
+
def build_fair_rotation_summary(aml_people, vacation_df, processed_history=None):
|
|
352
|
+
"""
|
|
353
|
+
processed_history: optional dict name->count of AML duties already done in the year
|
|
354
|
+
"""
|
|
355
|
+
if processed_history is None:
|
|
356
|
+
processed_history = {p: 0 for p in aml_people}
|
|
357
|
+
rows = []
|
|
358
|
+
for p in aml_people:
|
|
359
|
+
vac_periods = []
|
|
360
|
+
if vacation_df is not None:
|
|
361
|
+
subset = vacation_df[vacation_df["name"] == p]
|
|
362
|
+
for _, r in subset.iterrows():
|
|
363
|
+
vac_periods.append(f"{r['start'].date()}->{r['end'].date()}")
|
|
364
|
+
rows.append({
|
|
365
|
+
"Person": p,
|
|
366
|
+
"AML_trained": True,
|
|
367
|
+
"CAR-T_trained": p in CAR_T_MAIN,
|
|
368
|
+
"Vacations_in_Dec": "; ".join(vac_periods),
|
|
369
|
+
"AML_duties_completed_YTD": processed_history.get(p, 0),
|
|
370
|
+
"Rotation_position_estimate": "" # could be computed
|
|
371
|
+
})
|
|
372
|
+
df = pd.DataFrame(rows)
|
|
373
|
+
return df
|
|
374
|
+
|
|
375
|
+
# ---------- SAMPLE / CLI ----------
|
|
376
|
+
SAMPLE_VACATIONS = pd.DataFrame([
|
|
377
|
+
{"name": "S", "start": pd.Timestamp("2025-12-20"), "end": pd.Timestamp("2025-12-31")},
|
|
378
|
+
{"name": "N", "start": pd.Timestamp("2025-12-24"), "end": pd.Timestamp("2025-12-26")},
|
|
379
|
+
{"name": "T", "start": pd.Timestamp("2025-12-10"), "end": pd.Timestamp("2025-12-12")},
|
|
380
|
+
# J (CAR-T) maybe off:
|
|
381
|
+
{"name": "J", "start": pd.Timestamp("2025-12-25"), "end": pd.Timestamp("2025-12-26")},
|
|
382
|
+
])
|
|
383
|
+
|
|
384
|
+
SLACK_MESSAGE_TEMPLATE = """Hi everyone,
|
|
385
|
+
as we approach the December holiday period, several days currently have limited staff available for AML sample duty and general lab coverage.
|
|
386
|
+
|
|
387
|
+
To ensure essential tasks are covered fairly, I kindly ask for volunteers to cover a few minimal-coverage shifts (mainly AML sample duty). These shifts apply ONLY if a sample actually arrives on that day, and volunteers will receive Freizeitausgleich (time-off in lieu) for their support.
|
|
388
|
+
|
|
389
|
+
If you can cover one of the critical staffing days, please add your name in the Volunteers sheet in the shared Excel file.
|
|
390
|
+
Please indicate your preferred TOIL arrangement (half-day or full-day).
|
|
391
|
+
If you already have approved vacation, you do not need to volunteer.
|
|
392
|
+
|
|
393
|
+
Thank you — your help keeps the lab functioning smoothly and fairly over the holidays.
|
|
394
|
+
— Jeff
|
|
395
|
+
"""
|
|
396
|
+
|
|
397
|
+
EMAIL_TO_PI_TEMPLATE = """Subject: December AML/CAR-T Duty Coverage – Request for Decision on Fully Staffed-Out Days
|
|
398
|
+
|
|
399
|
+
Dear Claudia,
|
|
400
|
+
|
|
401
|
+
I have prepared a draft December duty schedule for AML samples, CAR-T samples, and essential lab tasks. Several team members have approved vacation days, and for the following dates we currently have no AML-trained personnel available:
|
|
402
|
+
|
|
403
|
+
[see attached schedule]
|
|
404
|
+
|
|
405
|
+
Since colleagues on approved vacation cannot be required to perform sample duties, we need a PI-level decision for the affected dates. Preferred options:
|
|
406
|
+
1) Pause research AML sample processing on those dates.
|
|
407
|
+
2) Request voluntary backup support from clinical/diagnostic colleagues (with Freizeitausgleich).
|
|
408
|
+
3) Designate a PI-decided fallback person to be on-site for those dates (with compensation as applicable).
|
|
409
|
+
|
|
410
|
+
Please let me know your preferred option and I will finalize the schedule accordingly.
|
|
411
|
+
|
|
412
|
+
Kind regards,
|
|
413
|
+
Jeff
|
|
414
|
+
"""
|
|
415
|
+
|
|
416
|
+
FAIR_ROTATION_TEXT = """Fair Rotation Rules (Ultimate Version):
|
|
417
|
+
1. Equal Opportunities: Each AML-trained person should have approximately the same number of duty opportunities over a long-term window.
|
|
418
|
+
2. Vacation Exemption: Staff on approved vacation are excluded from rotation during those dates.
|
|
419
|
+
3. Worked Duty Counts: Rotation credit only counts when a person actually processes a sample.
|
|
420
|
+
4. Automatic Forward Rotation: If Person A processed the last sample, the next sample goes to Person B.
|
|
421
|
+
5. Volunteer Bonus: Volunteers on critical days receive TOIL and +1 rotation credit.
|
|
422
|
+
6. Emergency Handling: If no one is available, PI decides whether to pause processing or appoint a fallback person.
|
|
423
|
+
7. CAR-T Special Rule: CAR-T duties are restricted to qualified staff; both primary + backup unavailable -> escalate to PI.
|
|
424
|
+
"""
|
|
425
|
+
|
|
426
|
+
POLICY_TEXT = f"""
|
|
427
|
+
Holiday Season Duty Coverage Policy – AML, CAR-T, and Essential Lab Work
|
|
428
|
+
Version: 1.0
|
|
429
|
+
Applies to: Your Lab
|
|
430
|
+
Period: December 1 – January 7 each year
|
|
431
|
+
|
|
432
|
+
Purpose:
|
|
433
|
+
This document outlines how essential laboratory tasks and sample processing (AML, CAR-T, animals, general duties) are maintained during the holiday period in a fair, transparent, and legally compliant manner.
|
|
434
|
+
|
|
435
|
+
Key Principles:
|
|
436
|
+
- Vacation protection: Staff on approved vacation are exempt from duties during their approved dates.
|
|
437
|
+
- Fair rotation: AML duties assigned using a sequential rotation; duty counts only when actually performed.
|
|
438
|
+
- Volunteer system: Critical days covered by volunteers, compensated with Freizeitausgleich (time-off in lieu).
|
|
439
|
+
- No forced assignments: No one is required to cancel approved leave.
|
|
440
|
+
- PI decision for extreme cases: If no one is available, PI decides between pausing processing or appointing a fallback.
|
|
441
|
+
- Documentation: All volunteers and TOIL entries are recorded.
|
|
442
|
+
|
|
443
|
+
TOIL Guidelines:
|
|
444
|
+
- Short minimal duty (few hours) => 0.5 day TOIL.
|
|
445
|
+
- Full day minimal duty => 1.0 day TOIL.
|
|
446
|
+
- TOIL should be taken within {TOIL_VALIDITY_MONTHS} months unless otherwise approved.
|
|
447
|
+
"""
|
|
448
|
+
|
|
449
|
+
def main(year, month, vacations_csv=None, aml_people=None, car_t_main=None, car_t_backup=None):
|
|
450
|
+
if aml_people is None:
|
|
451
|
+
aml_people = AML_ROTATION.copy()
|
|
452
|
+
if car_t_main is None:
|
|
453
|
+
car_t_main = CAR_T_MAIN.copy()
|
|
454
|
+
if car_t_backup is None:
|
|
455
|
+
car_t_backup = CAR_T_BACKUP.copy()
|
|
456
|
+
|
|
457
|
+
if vacations_csv and os.path.exists(vacations_csv):
|
|
458
|
+
vacations_df = read_vacations_csv(vacations_csv)
|
|
459
|
+
else:
|
|
460
|
+
vacations_df = SAMPLE_VACATIONS
|
|
461
|
+
|
|
462
|
+
cal_df = build_calendar_df(year, month)
|
|
463
|
+
cal_df = apply_vacations_to_calendar(cal_df, vacations_df, aml_people, car_t_main, car_t_backup)
|
|
464
|
+
|
|
465
|
+
aml_sheet = generate_aml_rotation_sheet(cal_df, aml_people)
|
|
466
|
+
car_t_sheet = generate_car_t_sheet(cal_df, car_t_main, car_t_backup)
|
|
467
|
+
volunteers_sheet = generate_volunteer_sheet(cal_df)
|
|
468
|
+
toil_log = generate_toil_log()
|
|
469
|
+
|
|
470
|
+
messages = {
|
|
471
|
+
"slack": SLACK_MESSAGE_TEMPLATE,
|
|
472
|
+
"email": EMAIL_TO_PI_TEMPLATE,
|
|
473
|
+
"fair_rotation": FAIR_ROTATION_TEXT
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
# create Excel
|
|
477
|
+
excel_path = create_excel(cal_df, aml_sheet, car_t_sheet, volunteers_sheet, toil_log, messages, POLICY_TEXT)
|
|
478
|
+
print(f"Excel file written to: {excel_path}")
|
|
479
|
+
|
|
480
|
+
# create PDF schedule
|
|
481
|
+
pdf_path = create_pdf_schedule(cal_df, aml_sheet, volunteers_sheet, PDF_SCHEDULE_FILENAME)
|
|
482
|
+
print(f"Schedule PDF written to: {pdf_path}")
|
|
483
|
+
|
|
484
|
+
# create policy PDF
|
|
485
|
+
policy_pdf_path = create_policy_pdf(POLICY_TEXT, POLICY_PDF_FILENAME)
|
|
486
|
+
print(f"Policy PDF written to: {policy_pdf_path}")
|
|
487
|
+
|
|
488
|
+
# plain text messages
|
|
489
|
+
slack_txt = save_plain_text_message(SLACK_MESSAGE_TEMPLATE, SLACK_FILENAME)
|
|
490
|
+
email_txt = save_plain_text_message(EMAIL_TO_PI_TEMPLATE, PI_EMAIL_FILENAME)
|
|
491
|
+
print(f"Slack message text saved to: {slack_txt}")
|
|
492
|
+
print(f"Email-to-PI text saved to: {email_txt}")
|
|
493
|
+
|
|
494
|
+
# fair rotation table
|
|
495
|
+
rotation_summary = build_fair_rotation_summary(aml_people, vacations_df)
|
|
496
|
+
csv_path, pdf_rot_path = export_rotation_csv_pdf(rotation_summary, ROTATION_CSV, ROTATION_PDF)
|
|
497
|
+
print(f"Fair rotation CSV: {csv_path}")
|
|
498
|
+
print(f"Fair rotation PDF: {pdf_rot_path}")
|
|
499
|
+
|
|
500
|
+
print("All outputs generated in folder:", ensure_output_dir())
|
|
501
|
+
|
|
502
|
+
if __name__ == "__main__":
|
|
503
|
+
parser = argparse.ArgumentParser(description="Generate December duty package")
|
|
504
|
+
parser.add_argument("--year", type=int, default=2025, help="Year (default 2025)")
|
|
505
|
+
parser.add_argument("--month", type=int, default=12, help="Month (1-12) default 12")
|
|
506
|
+
parser.add_argument("--vacation_csv", type=str, default=None, help="Optional vacations CSV path")
|
|
507
|
+
parser.add_argument("--excel_out", type=str, default=None, help="Optional excel output name")
|
|
508
|
+
args = parser.parse_args()
|
|
509
|
+
main(args.year, args.month, vacations_csv=args.vacation_csv)
|
py2ls/translator.py
CHANGED
|
@@ -586,6 +586,8 @@ def replace_text(text, dict_replace=None, robust=True):
|
|
|
586
586
|
Returns:
|
|
587
587
|
str: The text after replacements have been made.
|
|
588
588
|
"""
|
|
589
|
+
if not all(text):
|
|
590
|
+
return ''
|
|
589
591
|
# Default replacements for newline and tab characters
|
|
590
592
|
default_replacements = {
|
|
591
593
|
"\a": "",
|