pylantir 0.0.9__py3-none-any.whl → 0.1.1__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.
pylantir/__init__.py CHANGED
@@ -5,4 +5,4 @@
5
5
  """Python Package Template"""
6
6
  from __future__ import annotations
7
7
 
8
- __version__ = "0.0.2"
8
+ __version__ = "0.0.2-rc76-post1"
pylantir/cli/run.py CHANGED
@@ -209,6 +209,9 @@ def main() -> None:
209
209
  # Extract the database update interval (default to 60 seconds if missing)
210
210
  db_update_interval = config.get("db_update_interval", 60)
211
211
 
212
+ # Extract the operation interval (default from 00:00 to 23:59 hours if missing)
213
+ operation_interval = config.get("operation_interval", {"start_time": [0,0], "end_time": [23,59]})
214
+
212
215
  # Extract allowed AE Titles (default to empty list if missing)
213
216
  allowed_aet = config.get("allowed_aet", [])
214
217
 
@@ -219,7 +222,6 @@ def main() -> None:
219
222
  redcap2wl = config.get("redcap2wl", {})
220
223
 
221
224
  # EXtract protocol mapping
222
-
223
225
  protocol = config.get("protocol", {})
224
226
 
225
227
  # Create and update the MWL database
@@ -230,6 +232,7 @@ def main() -> None:
230
232
  protocol=protocol,
231
233
  redcap2wl=redcap2wl,
232
234
  interval=db_update_interval,
235
+ operation_interval=operation_interval,
233
236
  )
234
237
 
235
238
  # sync_redcap_to_db(
@@ -3,6 +3,16 @@
3
3
  "db_echo": "0",
4
4
  "db_update_interval": 60,
5
5
  "allowed_aet": [],
6
+ "operation_interval": {
7
+ "start_time": [
8
+ 0,
9
+ 0
10
+ ],
11
+ "end_time": [
12
+ 23,
13
+ 59
14
+ ]
15
+ },
6
16
  "mri_visit_session_mapping": {
7
17
  "t1_arm_1": "1",
8
18
  "t2_arm_1": "2",
@@ -10,7 +20,7 @@
10
20
  },
11
21
  "site": "792",
12
22
  "redcap2wl": {
13
- "study_id" : "study_id",
23
+ "study_id": "study_id",
14
24
  "family_id": "family_id",
15
25
  "youth_dob_y": "youth_dob_y",
16
26
  "t1_date": "t1_date",
@@ -22,7 +32,7 @@
22
32
  "performing_physician": "performing_physician",
23
33
  "station_name": "station_name",
24
34
  "status": "performed_procedure_step_status"
25
- },
35
+ },
26
36
  "protocol": {
27
37
  "792": "BRAIN_MRI_3T"
28
38
  }
pylantir/redcap_to_db.py CHANGED
@@ -8,6 +8,7 @@ from .db_setup import engine
8
8
  from .models import WorklistItem
9
9
  import time
10
10
  import threading
11
+ from datetime import datetime, time, date, timedelta
11
12
 
12
13
  lgr = logging.getLogger(__name__)
13
14
 
@@ -22,7 +23,7 @@ Session = sessionmaker(bind=engine)
22
23
 
23
24
 
24
25
 
25
- def fetch_redcap_entries(redcap_fields: list) -> list:
26
+ def fetch_redcap_entries(redcap_fields: list, interval: float) -> list:
26
27
  """Fetch REDCap entries using PyCap and return a list of filtered dicts."""
27
28
  project = Project(REDCAP_API_URL, REDCAP_API_TOKEN)
28
29
 
@@ -41,7 +42,9 @@ def fetch_redcap_entries(redcap_fields: list) -> list:
41
42
  lgr.info(f"Fetching REDCap data for fields: {redcap_fields}")
42
43
 
43
44
  # Export data
44
- records = project.export_records(fields=redcap_fields, format_type="df")
45
+ datetime_now = datetime.now()
46
+ datetime_interval = datetime_now - timedelta(seconds=interval)
47
+ records = project.export_records(fields=redcap_fields, date_begin=datetime_interval, date_end=datetime_now, format_type="df")
45
48
 
46
49
  if records.empty:
47
50
  lgr.warning("No records retrieved from REDCap.")
@@ -60,7 +63,9 @@ def fetch_redcap_entries(redcap_fields: list) -> list:
60
63
  mri_rows = group[
61
64
  (group["redcap_repeat_instrument"] == "mri") &
62
65
  (group.get("mri_instance").notna()) &
63
- (group.get("mri_instance") != "")
66
+ (group.get("mri_instance") != "" ) &
67
+ (group.get("mri_date").notna()) &
68
+ (group.get("mri_time").notna())
64
69
  ]
65
70
 
66
71
  for _, mri_row in mri_rows.iterrows():
@@ -102,6 +107,7 @@ def sync_redcap_to_db(
102
107
  site_id: str,
103
108
  protocol: dict,
104
109
  redcap2wl: dict,
110
+ interval: float = 60.0,
105
111
  ) -> None:
106
112
  """Sync REDCap patient data with the worklist database."""
107
113
 
@@ -121,7 +127,7 @@ def sync_redcap_to_db(
121
127
  if i not in redcap_fields:
122
128
  redcap_fields.append(i)
123
129
 
124
- redcap_entries = fetch_redcap_entries(redcap_fields)
130
+ redcap_entries = fetch_redcap_entries(redcap_fields, interval)
125
131
 
126
132
  for record in redcap_entries:
127
133
  study_id = record.get("study_id")
@@ -217,38 +223,102 @@ def sync_redcap_to_db_repeatedly(
217
223
  site_id=None,
218
224
  protocol=None,
219
225
  redcap2wl=None,
220
- interval=60
226
+ interval=60,
227
+ operation_interval={"start_time": [00,00], "end_time": [23,59]},
221
228
  ):
222
229
  """
223
- Keep syncing with REDCap in a loop every `interval` seconds.
230
+ Keep syncing with REDCap in a loop every `interval` seconds,
231
+ but only between operation_interval[start_time] and operation_interval[end_time].
224
232
  Exit cleanly when STOP_EVENT is set.
225
233
  """
234
+ if operation_interval is None:
235
+ operation_interval = {"start_time": [0, 0], "end_time": [23, 59]}
236
+
237
+ start_h, start_m = operation_interval.get("start_time", [0, 0])
238
+ end_h, end_m = operation_interval.get("end_time", [23, 59])
239
+ start_time = time(start_h, start_m)
240
+ end_time = time(end_h, end_m)
241
+
242
+ # last_sync_date = datetime.now().date()
243
+ last_sync_date = datetime.now().date() - timedelta(days=1)
244
+ interval_sync = interval + 600 # add 10 minutes to the interval to overlap with the previous sync and avoid missing data
245
+
226
246
  while not STOP_EVENT.is_set():
227
- try:
228
- sync_redcap_to_db(
229
- site_id=site_id,
230
- protocol=protocol,
231
- redcap2wl=redcap2wl,
247
+ # === 1) BASELINE: set defaults for flags and wait-time each iteration ===
248
+ is_first_run = False
249
+ extended_interval = interval
250
+
251
+ # === 2) FIGURE OUT "NOW" in hours/minutes (zero out seconds) ===
252
+ now_dt = datetime.now().replace(second=0, microsecond=0)
253
+ now_time = now_dt.time()
254
+ today_date = now_dt.date()
255
+
256
+ # === 3) ONLY SYNC IF WE'RE WITHIN [start_time, end_time] ===
257
+ if start_time <= now_time <= end_time:
258
+ # Check if we haven't synced today yet
259
+ is_first_run = (last_sync_date != today_date)
260
+
261
+ # If it really *is* the first sync of this new day (and it's not the very first run ever)
262
+ if is_first_run and (last_sync_date is not None):
263
+ logging.info(f"First sync of the day for site {site_id} at {now_time}.")
264
+ # Calculate how many seconds from "end_time of yesterday" until "start_time of today"
265
+ yesterday = last_sync_date
266
+ dt_end_yesterday = datetime.combine(yesterday, end_time)
267
+ dt_start_today = datetime.combine(today_date, start_time)
268
+ delta = dt_start_today - dt_end_yesterday
269
+ # guaranteed to be positive if yesterday < today
270
+ extended_interval = delta.total_seconds()
271
+ logging.info(f"Using extended interval: {extended_interval}, {interval} seconds until next sync.")
272
+ else:
273
+ # Either not first run, or last_sync_date is None (this is first-ever run)
274
+ logging.info("Using default interval {interval} seconds.")
275
+
276
+ # --- CALL THE SYNC FUNCTION INSIDE A TRY/EXCEPT ---
277
+ logging.debug(f"Syncing REDCap to DB for site {site_id} at {now_time}.")
278
+ logging.debug(f"First run {is_first_run}")
279
+ try:
280
+ logging.debug(f"last_sync_date was: {last_sync_date}")
281
+ if is_first_run and (last_sync_date is not None):
282
+ sync_redcap_to_db(
283
+ site_id=site_id,
284
+ protocol=protocol,
285
+ redcap2wl=redcap2wl,
286
+ interval=extended_interval,
287
+ )
288
+ else:
289
+ sync_redcap_to_db(
290
+ site_id=site_id,
291
+ protocol=protocol,
292
+ redcap2wl=redcap2wl,
293
+ interval=interval_sync,
294
+ )
295
+ last_sync_date = today_date
296
+ logging.debug(f"REDCap sync completed at {now_time}. Next sync atempt in {interval} seconds.")
297
+ except Exception as exc:
298
+ logging.error(f"Error in REDCap sync: {exc}")
299
+ else:
300
+ # We're outside of operation hours. Just log once and sleep a bit.
301
+ logging.debug(
302
+ f"Current time {now_time} is outside operation window "
303
+ f"({start_time}–{end_time}). Sleeping for {interval} seconds."
232
304
  )
233
- except Exception as exc:
234
- logging.error(f"Error in REDCap sync: {exc}")
235
305
 
236
- # Wait up to `interval` seconds, or break early if STOP_EVENT is set
306
+ # === 4) WAIT before the next iteration. We already set extended_interval above. ===
307
+ logging.debug(f"Sleeping for {interval} seconds before next check...")
237
308
  STOP_EVENT.wait(interval)
238
309
 
239
310
  logging.info("Exiting sync_redcap_to_db_repeatedly because STOP_EVENT was set.")
240
311
 
241
312
 
242
313
  if __name__ == "__main__":
243
- # This block is just a demo usage. In practice, you might set STOP_EVENT
244
- # from a signal handler or from another part of your code.
245
314
  try:
246
315
  sync_redcap_to_db_repeatedly(
247
316
  site_id=None,
248
317
  protocol=None,
249
318
  redcap2wl=None,
250
- interval=60
319
+ interval=60,
320
+ operation_interval={"start_time": [0, 0], "end_time": [23, 59]},
251
321
  )
252
322
  except KeyboardInterrupt:
253
323
  logging.info("KeyboardInterrupt received. Stopping background sync...")
254
- STOP_EVENT.set()
324
+ STOP_EVENT.set()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pylantir
3
- Version: 0.0.9
3
+ Version: 0.1.1
4
4
  Summary: Python - DICOM Modality WorkList
5
5
  Author-email: Milton Camacho <miltoncamachoicc@gmail.com>
6
6
  Requires-Python: >=3.11.1
@@ -82,12 +82,19 @@ pylantir start --ip 127.0.0.1 --port 4242 --AEtitle MWL_SERVER --pylantir_config
82
82
 
83
83
  ## Tests
84
84
 
85
- Once your modality worklist server is running you can test it running the following:
85
+ If you want to run the tests make sure to clone the repository and run them from there.
86
+
87
+ Git clone the repository:
88
+
89
+ ```bash
90
+ git clone https://github.com/miltoncamacho/pylantir
91
+ cd pylantir/tests
92
+ ```
86
93
 
87
94
  Query the worklist database to check that you have some entries using:
88
95
 
89
96
  ```bash
90
- pylantir query-db
97
+ python query-db.py
91
98
  ```
92
99
 
93
100
  Then, you can get a StudyUID from one of the entries to test the MPPS workflow. For example: 1.2.840.10008.3.1.2.3.4.55635351412689303463019139483773956632
@@ -95,19 +102,19 @@ Then, you can get a StudyUID from one of the entries to test the MPPS workflow.
95
102
  Take this and run a create action to mark the worklist Procedure Step Status as IN_PROGRESS
96
103
 
97
104
  ```bash
98
- pylantir test-mpps --AEtitle MWL_SERVER --mpps_action create --callingAEtitle MWL_TESTER --ip 127.0.0.1 --port 4242 --study_uid 1.2.840.10008.3.1.2.3.4.55635351412689303463019139483773956632
105
+ python test-mpps.py --AEtitle MWL_SERVER --mpps_action create --callingAEtitle MWL_TESTER --ip 127.0.0.1 --port 4242 --study_uid 1.2.840.10008.3.1.2.3.4.55635351412689303463019139483773956632
99
106
  ```
100
107
 
101
108
  You can verify that this in fact modified your database re-running:
102
109
 
103
110
  ```bash
104
- pylantir query-db
111
+ python query-db.py
105
112
  ```
106
113
 
107
114
  Finally, you can also simulate the pocedure completion efectively updating the Procedure Step Status to COMPLETED or DISCONTINUED:
108
115
 
109
116
  ```bash
110
- pylantir test-mpps --AEtitle MWL_SERVER --mpps_action set --mpps_status COMPLETED --callingAEtitle MWL_TESTER --ip 127.0.0.1 --port 4242 --study_uid 1.2.840.10008.3.1.2.3.4.55635351412689303463019139483773956632 --sop_uid 1.2.840.10008.3.1.2.3.4.187176383255263644225774937658729238426
117
+ python test-mpps.py --AEtitle MWL_SERVER --mpps_action set --mpps_status COMPLETED --callingAEtitle MWL_TESTER --ip 127.0.0.1 --port 4242 --study_uid 1.2.840.10008.3.1.2.3.4.55635351412689303463019139483773956632 --sop_uid 1.2.840.10008.3.1.2.3.4.187176383255263644225774937658729238426
111
118
  ```
112
119
 
113
120
  ## Usage
@@ -137,6 +144,8 @@ usage: pylantir [-h] [--AEtitle AETITLE] [--ip IP] [--port PORT] [--pylantir_con
137
144
  - **site**: Site ID:string
138
145
  - **protocol**: `{"site": "protocol_name", "mapping": "HIS/RIS mapping"}`
139
146
  - **redcap2wl**: Dictionary of REDCap fields to worklist fields mapping e.g., `{"redcap_field": "worklist_field"}`
147
+ - **db_update_interval**: How often to reload the database e
148
+ - **operation_interval**: What is the time range in a day in which the database will be updated e.g., `{"start_time":[hours,minutes],"end_time":[hours,minutes]}`
140
149
  - **--mpps_action {create,set}**: Action to perform for MPPS either create or set
141
150
  - **--mpps_status {COMPLETED,DISCONTINUED}**: Status to set for MPPS either COMPLETED or DISCONTINUED
142
151
  - **--callingAEtitle CALLINGAETITLE**: Calling AE Title for MPPS, it helps when the MWL is limited to only accept certain AE titles
@@ -152,6 +161,7 @@ As a default pylantir will try to read a JSON structured file with the following
152
161
  "db_path": "/path/to/worklist.db",
153
162
  "db_echo": "False",
154
163
  "db_update_interval": 60,
164
+ "operation_interval": {"start_time": [0,0],"end_time": [23,59]},
155
165
  "allowed_aet": [],
156
166
  "site": "792",
157
167
  "redcap2wl": {
@@ -0,0 +1,14 @@
1
+ pylantir/__init__.py,sha256=eed6MscqD3GeVPx1JwDo0xYYOwQaOjVpFwvlb7NJTSA,374
2
+ pylantir/db_setup.py,sha256=KTILsRrH7V5EaPqbCfOYYECM9mUB-AvOdjqjMM2H1n0,1333
3
+ pylantir/models.py,sha256=bKgI0EN1VSYanPTOvEhEY2Zzqa0gDYLpVnE_KNQ6PEc,1780
4
+ pylantir/mwl_server.py,sha256=GMJDcK0u_KM3oa6UqQ87NxMVye2pvG2cdkcI9k_iExg,10338
5
+ pylantir/populate_db.py,sha256=KIbkVA-EAuTlDArXMFOHkjMmVfjlsTApj7S1wpUu1bM,2207
6
+ pylantir/redcap_to_db.py,sha256=uGjKAbvZr7DcWTQyBVWxwiIkW3RTB-u54q17EWAh9ck,13442
7
+ pylantir/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
+ pylantir/cli/run.py,sha256=ZE-CIBTn3vp4APqs0U7wKW70RFM4ph-AuKkOrGbruu8,10321
9
+ pylantir/config/mwl_config.json,sha256=v14HXu1ft1mwFyjsowHe3H1LXZGD6sAoYuGb9_4w2kA,1008
10
+ pylantir-0.1.1.dist-info/entry_points.txt,sha256=vxaxvfGppLqRt9_4sqNDdP6b2jlgpcHIwP7UQfrM1T0,50
11
+ pylantir-0.1.1.dist-info/licenses/LICENSE,sha256=ws_MuBL-SCEBqPBFl9_FqZkaaydIJmxHrJG2parhU4M,1141
12
+ pylantir-0.1.1.dist-info/WHEEL,sha256=G2gURzTEtmeR8nrdXUJfNiB3VYVxigPQ-bEQujpNiNs,82
13
+ pylantir-0.1.1.dist-info/METADATA,sha256=FLD6OFnJBDnSRSo2PJVB6Z5A_Laj5tCsmfw1at6LHb0,7585
14
+ pylantir-0.1.1.dist-info/RECORD,,
pylantir/.env DELETED
@@ -1,2 +0,0 @@
1
- DB_ECHO='0'
2
- DB_PATH='/Users/milton/Desktop/worklist.db'
@@ -1,15 +0,0 @@
1
- pylantir/.env,sha256=qU4xxA3iOy2DQGT78CQG05ljTsFwKzgF2wXdnBpg8xQ,56
2
- pylantir/__init__.py,sha256=kl2Et644PvUIvziU4BTxiTD1W4_g7E0xBYCHgPE6RZc,363
3
- pylantir/db_setup.py,sha256=KTILsRrH7V5EaPqbCfOYYECM9mUB-AvOdjqjMM2H1n0,1333
4
- pylantir/models.py,sha256=bKgI0EN1VSYanPTOvEhEY2Zzqa0gDYLpVnE_KNQ6PEc,1780
5
- pylantir/mwl_server.py,sha256=GMJDcK0u_KM3oa6UqQ87NxMVye2pvG2cdkcI9k_iExg,10338
6
- pylantir/populate_db.py,sha256=KIbkVA-EAuTlDArXMFOHkjMmVfjlsTApj7S1wpUu1bM,2207
7
- pylantir/redcap_to_db.py,sha256=cZam71uUOhkiO8z20pMEE9ExEOZEdisytYbVVlQhIZY,9589
8
- pylantir/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
- pylantir/cli/run.py,sha256=vqb7kbKsf39tI8-xjDceS4j5V-YJSaC_k0Lu6vlajmo,10072
10
- pylantir/config/mwl_config.json,sha256=1Ma2guYAEAXQh1z7959aZadAn3ORjBqnDDibSLcwv_g,851
11
- pylantir-0.0.9.dist-info/entry_points.txt,sha256=vxaxvfGppLqRt9_4sqNDdP6b2jlgpcHIwP7UQfrM1T0,50
12
- pylantir-0.0.9.dist-info/licenses/LICENSE,sha256=ws_MuBL-SCEBqPBFl9_FqZkaaydIJmxHrJG2parhU4M,1141
13
- pylantir-0.0.9.dist-info/WHEEL,sha256=G2gURzTEtmeR8nrdXUJfNiB3VYVxigPQ-bEQujpNiNs,82
14
- pylantir-0.0.9.dist-info/METADATA,sha256=70OdtHOBFhTtJNiA7rqQiDIud4BCfzccSNlIyqVLa8c,7174
15
- pylantir-0.0.9.dist-info/RECORD,,