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.

Files changed (72) hide show
  1. py2ls/.DS_Store +0 -0
  2. py2ls/.git/.DS_Store +0 -0
  3. py2ls/.git/index +0 -0
  4. py2ls/.git/logs/refs/remotes/origin/HEAD +1 -0
  5. py2ls/.git/objects/.DS_Store +0 -0
  6. py2ls/.git/refs/.DS_Store +0 -0
  7. py2ls/ImageLoader.py +621 -0
  8. py2ls/__init__.py +7 -5
  9. py2ls/apptainer2ls.py +3940 -0
  10. py2ls/batman.py +164 -42
  11. py2ls/bio.py +2595 -0
  12. py2ls/cell_image_clf.py +1632 -0
  13. py2ls/container2ls.py +4635 -0
  14. py2ls/corr.py +475 -0
  15. py2ls/data/.DS_Store +0 -0
  16. py2ls/data/email/email_html_template.html +88 -0
  17. py2ls/data/hyper_param_autogluon_zeroshot2024.json +2383 -0
  18. py2ls/data/hyper_param_tabrepo_2024.py +1753 -0
  19. py2ls/data/mygenes_fields_241022.txt +355 -0
  20. py2ls/data/re_common_pattern.json +173 -0
  21. py2ls/data/sns_info.json +74 -0
  22. py2ls/data/styles/.DS_Store +0 -0
  23. py2ls/data/styles/example/.DS_Store +0 -0
  24. py2ls/data/styles/stylelib/.DS_Store +0 -0
  25. py2ls/data/styles/stylelib/grid.mplstyle +15 -0
  26. py2ls/data/styles/stylelib/high-contrast.mplstyle +6 -0
  27. py2ls/data/styles/stylelib/high-vis.mplstyle +4 -0
  28. py2ls/data/styles/stylelib/ieee.mplstyle +15 -0
  29. py2ls/data/styles/stylelib/light.mplstyl +6 -0
  30. py2ls/data/styles/stylelib/muted.mplstyle +6 -0
  31. py2ls/data/styles/stylelib/nature-reviews-latex.mplstyle +616 -0
  32. py2ls/data/styles/stylelib/nature-reviews.mplstyle +616 -0
  33. py2ls/data/styles/stylelib/nature.mplstyle +31 -0
  34. py2ls/data/styles/stylelib/no-latex.mplstyle +10 -0
  35. py2ls/data/styles/stylelib/notebook.mplstyle +36 -0
  36. py2ls/data/styles/stylelib/paper.mplstyle +290 -0
  37. py2ls/data/styles/stylelib/paper2.mplstyle +305 -0
  38. py2ls/data/styles/stylelib/retro.mplstyle +4 -0
  39. py2ls/data/styles/stylelib/sans.mplstyle +10 -0
  40. py2ls/data/styles/stylelib/scatter.mplstyle +7 -0
  41. py2ls/data/styles/stylelib/science.mplstyle +48 -0
  42. py2ls/data/styles/stylelib/std-colors.mplstyle +4 -0
  43. py2ls/data/styles/stylelib/vibrant.mplstyle +6 -0
  44. py2ls/data/tiles.csv +146 -0
  45. py2ls/data/usages_pd.json +1417 -0
  46. py2ls/data/usages_sns.json +31 -0
  47. py2ls/docker2ls.py +5446 -0
  48. py2ls/ec2ls.py +61 -0
  49. py2ls/fetch_update.py +145 -0
  50. py2ls/ich2ls.py +1955 -296
  51. py2ls/im2.py +8242 -0
  52. py2ls/image_ml2ls.py +2100 -0
  53. py2ls/ips.py +33909 -3418
  54. py2ls/ml2ls.py +7700 -0
  55. py2ls/mol.py +289 -0
  56. py2ls/mount2ls.py +1307 -0
  57. py2ls/netfinder.py +873 -351
  58. py2ls/nl2ls.py +283 -0
  59. py2ls/ocr.py +1581 -458
  60. py2ls/plot.py +10394 -314
  61. py2ls/rna2ls.py +311 -0
  62. py2ls/ssh2ls.md +456 -0
  63. py2ls/ssh2ls.py +5933 -0
  64. py2ls/ssh2ls_v01.py +2204 -0
  65. py2ls/stats.py +66 -172
  66. py2ls/temp20251124.py +509 -0
  67. py2ls/translator.py +2 -0
  68. py2ls/utils/decorators.py +3564 -0
  69. py2ls/utils_bio.py +3453 -0
  70. {py2ls-0.1.10.12.dist-info → py2ls-0.2.7.10.dist-info}/METADATA +113 -224
  71. {py2ls-0.1.10.12.dist-info → py2ls-0.2.7.10.dist-info}/RECORD +72 -16
  72. {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": "",