pylantir 0.0.9__py3-none-any.whl → 0.1.0__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/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.")
@@ -102,6 +105,7 @@ def sync_redcap_to_db(
102
105
  site_id: str,
103
106
  protocol: dict,
104
107
  redcap2wl: dict,
108
+ interval: float = 60.0,
105
109
  ) -> None:
106
110
  """Sync REDCap patient data with the worklist database."""
107
111
 
@@ -121,7 +125,7 @@ def sync_redcap_to_db(
121
125
  if i not in redcap_fields:
122
126
  redcap_fields.append(i)
123
127
 
124
- redcap_entries = fetch_redcap_entries(redcap_fields)
128
+ redcap_entries = fetch_redcap_entries(redcap_fields, interval)
125
129
 
126
130
  for record in redcap_entries:
127
131
  study_id = record.get("study_id")
@@ -217,38 +221,102 @@ def sync_redcap_to_db_repeatedly(
217
221
  site_id=None,
218
222
  protocol=None,
219
223
  redcap2wl=None,
220
- interval=60
224
+ interval=60,
225
+ operation_interval={"start_time": [00,00], "end_time": [23,59]},
221
226
  ):
222
227
  """
223
- Keep syncing with REDCap in a loop every `interval` seconds.
228
+ Keep syncing with REDCap in a loop every `interval` seconds,
229
+ but only between operation_interval[start_time] and operation_interval[end_time].
224
230
  Exit cleanly when STOP_EVENT is set.
225
231
  """
232
+ if operation_interval is None:
233
+ operation_interval = {"start_time": [0, 0], "end_time": [23, 59]}
234
+
235
+ start_h, start_m = operation_interval.get("start_time", [0, 0])
236
+ end_h, end_m = operation_interval.get("end_time", [23, 59])
237
+ start_time = time(start_h, start_m)
238
+ end_time = time(end_h, end_m)
239
+
240
+ # last_sync_date = datetime.now().date()
241
+ last_sync_date = datetime.now().date() - timedelta(days=1)
242
+ interval_sync = interval + 600 # add 10 minutes to the interval to overlap with the previous sync and avoid missing data
243
+
226
244
  while not STOP_EVENT.is_set():
227
- try:
228
- sync_redcap_to_db(
229
- site_id=site_id,
230
- protocol=protocol,
231
- redcap2wl=redcap2wl,
245
+ # === 1) BASELINE: set defaults for flags and wait-time each iteration ===
246
+ is_first_run = False
247
+ extended_interval = interval
248
+
249
+ # === 2) FIGURE OUT "NOW" in hours/minutes (zero out seconds) ===
250
+ now_dt = datetime.now().replace(second=0, microsecond=0)
251
+ now_time = now_dt.time()
252
+ today_date = now_dt.date()
253
+
254
+ # === 3) ONLY SYNC IF WE'RE WITHIN [start_time, end_time] ===
255
+ if start_time <= now_time <= end_time:
256
+ # Check if we haven't synced today yet
257
+ is_first_run = (last_sync_date != today_date)
258
+
259
+ # If it really *is* the first sync of this new day (and it's not the very first run ever)
260
+ if is_first_run and (last_sync_date is not None):
261
+ logging.info(f"First sync of the day for site {site_id} at {now_time}.")
262
+ # Calculate how many seconds from "end_time of yesterday" until "start_time of today"
263
+ yesterday = last_sync_date
264
+ dt_end_yesterday = datetime.combine(yesterday, end_time)
265
+ dt_start_today = datetime.combine(today_date, start_time)
266
+ delta = dt_start_today - dt_end_yesterday
267
+ # guaranteed to be positive if yesterday < today
268
+ extended_interval = delta.total_seconds()
269
+ logging.info(f"Using extended interval: {extended_interval}, {interval} seconds until next sync.")
270
+ else:
271
+ # Either not first run, or last_sync_date is None (this is first-ever run)
272
+ logging.info("Using default interval {interval} seconds.")
273
+
274
+ # --- CALL THE SYNC FUNCTION INSIDE A TRY/EXCEPT ---
275
+ logging.debug(f"Syncing REDCap to DB for site {site_id} at {now_time}.")
276
+ logging.debug(f"First run {is_first_run}")
277
+ try:
278
+ logging.debug(f"last_sync_date was: {last_sync_date}")
279
+ if is_first_run and (last_sync_date is not None):
280
+ sync_redcap_to_db(
281
+ site_id=site_id,
282
+ protocol=protocol,
283
+ redcap2wl=redcap2wl,
284
+ interval=extended_interval,
285
+ )
286
+ else:
287
+ sync_redcap_to_db(
288
+ site_id=site_id,
289
+ protocol=protocol,
290
+ redcap2wl=redcap2wl,
291
+ interval=interval_sync,
292
+ )
293
+ last_sync_date = today_date
294
+ logging.debug(f"REDCap sync completed at {now_time}. Next sync atempt in {interval} seconds.")
295
+ except Exception as exc:
296
+ logging.error(f"Error in REDCap sync: {exc}")
297
+ else:
298
+ # We're outside of operation hours. Just log once and sleep a bit.
299
+ logging.debug(
300
+ f"Current time {now_time} is outside operation window "
301
+ f"({start_time}–{end_time}). Sleeping for {interval} seconds."
232
302
  )
233
- except Exception as exc:
234
- logging.error(f"Error in REDCap sync: {exc}")
235
303
 
236
- # Wait up to `interval` seconds, or break early if STOP_EVENT is set
304
+ # === 4) WAIT before the next iteration. We already set extended_interval above. ===
305
+ logging.debug(f"Sleeping for {interval} seconds before next check...")
237
306
  STOP_EVENT.wait(interval)
238
307
 
239
308
  logging.info("Exiting sync_redcap_to_db_repeatedly because STOP_EVENT was set.")
240
309
 
241
310
 
242
311
  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
312
  try:
246
313
  sync_redcap_to_db_repeatedly(
247
314
  site_id=None,
248
315
  protocol=None,
249
316
  redcap2wl=None,
250
- interval=60
317
+ interval=60,
318
+ operation_interval={"start_time": [0, 0], "end_time": [23, 59]},
251
319
  )
252
320
  except KeyboardInterrupt:
253
321
  logging.info("KeyboardInterrupt received. Stopping background sync...")
254
- STOP_EVENT.set()
322
+ 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.0
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=kl2Et644PvUIvziU4BTxiTD1W4_g7E0xBYCHgPE6RZc,363
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=bHOKTgiGZYrke8KK3MUkR1W50w7YxS8szXVwBz5eW_U,13349
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.0.dist-info/entry_points.txt,sha256=vxaxvfGppLqRt9_4sqNDdP6b2jlgpcHIwP7UQfrM1T0,50
11
+ pylantir-0.1.0.dist-info/licenses/LICENSE,sha256=ws_MuBL-SCEBqPBFl9_FqZkaaydIJmxHrJG2parhU4M,1141
12
+ pylantir-0.1.0.dist-info/WHEEL,sha256=G2gURzTEtmeR8nrdXUJfNiB3VYVxigPQ-bEQujpNiNs,82
13
+ pylantir-0.1.0.dist-info/METADATA,sha256=j9vRexpSBt9a-rytlEmW0r8Oh9EsrpyUcCBpidrVoVs,7585
14
+ pylantir-0.1.0.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,,