diff --git a/main.py b/main.py index b4352f7..e3cad8f 100644 --- a/main.py +++ b/main.py @@ -63,6 +63,11 @@ def parse_args(): send_file_parser = subparsers.add_parser('send-file', help='Send a single DICOM file to destination PACS') send_file_parser.add_argument('--file-path', required=True, help='Path to DICOM file to send') + # Add to the parse_args() function after the 'process' command + process_study_parser = subparsers.add_parser('process-study', help='Process full workflow for a specific study by StudyInstanceUID') + process_study_parser.add_argument('--study-uid', required=True, help='StudyInstanceUID to process') + process_study_parser.add_argument('--log-dir', help='Directory to save JSON logs (default: JSON_OUTPUT_DIR)') + # Process command - full workflow: find, get, and send DICOM data for a date range process_parser = subparsers.add_parser('process', help='Process full workflow (find, get, send) for a date range') process_parser.add_argument('--start-date', required=True, help='Start date in YYYYMMDD format') @@ -481,7 +486,7 @@ def process_workflow(args): his_url = f"http://{settings.HIS_HOST}{settings.HIS_URL}" try: response = requests.post(his_url, json=study_log) - if response.status_code == 200 and response.json().get('status') == "OK": + if response.status_code == 200 and response.json().get('OK') == "1": his_log.append(study_log) logger.info(f"Successfully sent JSON {accession_number} to HIS API") else: @@ -506,6 +511,108 @@ def process_workflow(args): logger.error(f"Error during workflow processing: {str(e)}") sys.exit(1) +def process_workflow_by_study(args): + """ + Process the full workflow for a specific study: retrieve it, send to destination PACS, + and create detailed JSON logs. + + Args: + args: Command line arguments + """ + study_uid = args.study_uid + log_dir = args.log_dir or settings.JSON_OUTPUT_DIR + + logger.info(f"Starting full workflow process for study: {study_uid}") + + # Create services + finder = DicomFinder() + retriever = DicomRetriever() + sender = DicomSender() + + # Create directory for logs + create_directory_if_not_exists(log_dir) + + # Process the study + try: + # First, get the study metadata with a query + study_metadata = finder.find_study_by_uid(study_uid) + + if not study_metadata: + logger.error(f"Study not found: {study_uid}") + sys.exit(1) + + accession_number = getattr(study_metadata, 'AccessionNumber', '') + logger.info(f"Processing study: {accession_number} with Study_IUID ({study_uid})") + + # STEP 1: Retrieve the study + retrieve_result = retriever.retrieve_study(study_uid, accession_number=accession_number) + if not retrieve_result['success']: + logger.error(f"Failed to retrieve study {study_uid}: {retrieve_result['status']}") + sys.exit(1) + + logger.info(f"Retrieved {retrieve_result['successful_instances']} instances for study {study_uid}") + + # STEP 2: Send the study to destination PACS + send_result = sender.send_study(os.path.join(settings.DICOM_STORE_DIR, study_uid)) + if not send_result['success']: + logger.error(f"Failed to send study {study_uid}: {send_result.get('error', 'Unknown error')}") + + logger.info(f"Sent {send_result['successful_sends']} of {send_result['total_files']} files to destination PACS") + + # STEP 3: Find all series and create detailed logs + series_list = finder.find_series_for_study(study_uid, accession_number=accession_number) + + study_date = getattr(study_metadata, 'StudyDate', '') + study_time = getattr(study_metadata, 'StudyTime', '000000') # Default to midnight if StudyTime is not available + study_datetime = f"{study_date}{study_time}" + + study_log = { + 'Study_IUID': study_uid, + 'AccessionNumber': getattr(study_metadata, 'AccessionNumber', ''), + 'PatientID': getattr(study_metadata, 'PatientID', ''), # MedrecID + 'StudyDescription': getattr(study_metadata, 'StudyDescription', ''), + 'StudyDateTime': study_datetime, + 'CstoreSuccess': send_result['success'], + 'Series': [] + } + + for series in series_list: + series_uid = series.SeriesInstanceUID + + # Get first instance for the series + instance = finder.find_first_instance_for_series(study_uid, series_uid) + + series_info = { + 'Series_IUID': getattr(series, 'SeriesInstanceUID', ''), + 'SeriesNumber': getattr(series, 'SeriesNumber', ''), + 'SeriesDescription': getattr(series, 'SeriesDescription', ''), + 'NumberOfInstances': getattr(series, 'NumberOfSeriesRelatedInstances', ''), + 'SOP_IUID': getattr(instance, 'SOPInstanceUID', '') if instance else '' + } + + study_log['Series'].append(series_info) + + # STEP 4: Send study_log to HIS API + his_url = f"http://{settings.HIS_HOST}{settings.HIS_URL}" + try: + response = requests.post(his_url, json=study_log) + if response.status_code == 200 and response.json().get('OK') == "1": + logger.info(f"Successfully sent JSON {accession_number} to HIS API") + # Save successful log + save_json_data([study_log], f"sendtohis_study_{study_uid}.json", "logs") + else: + logger.error(f"Failed to send JSON for {accession_number} to HIS API. Study_IUID: {study_uid}") + # Save failed log + save_json_data([study_log], f"fail_sendtohis_study_{study_uid}.json", "logs") + except Exception as e: + logger.error(f"Error sending study for {accession_number} to HIS API: {str(e)}") + + logger.info(f"Completed processing study: {study_uid}") + + except Exception as e: + logger.error(f"Error during study processing: {str(e)}") + sys.exit(1) + def main(): """Main function.""" # Register cleanup handlers for graceful exit @@ -544,6 +651,8 @@ def main(): send_file(args) elif args.command == 'process': process_workflow(args) + elif args.command == 'process-study': + process_workflow_by_study(args) else: logger.error("No command specified. Use --help for options.") sys.exit(1) diff --git a/services/dicom_finder.py b/services/dicom_finder.py index f046f74..d0cfe52 100644 --- a/services/dicom_finder.py +++ b/services/dicom_finder.py @@ -269,4 +269,82 @@ class DicomFinder: logger.error(error_msg) raise DicomQueryError(error_msg) - return first_instance \ No newline at end of file + return first_instance + + @dicom_retry(exception_types=(DicomQueryError, ConnectionError)) + def find_study_by_uid(self, study_instance_uid): + """ + Find a specific study by its StudyInstanceUID. + + Args: + study_instance_uid (str): StudyInstanceUID to find + + Returns: + Dataset: Study dataset or None if not found + """ + logger.info(f"Finding study with UID: {study_instance_uid}") + + # Create query dataset + ds = Dataset() + ds.QueryRetrieveLevel = 'STUDY' + + # Set the StudyInstanceUID filter + ds.StudyInstanceUID = study_instance_uid + + # Required fields (minimal set) + ds.StudyDate = '' + + # Additional fields we want to retrieve + ds.AccessionNumber = '' + ds.PatientID = '' # MedrecID + ds.StudyDescription = '' + ds.StudyTime = '' + ds.NumberOfStudyRelatedSeries = '' + + # Create association + assoc = self.ae.associate( + self.pacs_config['host'], + self.pacs_config['port'], + ae_title=self.pacs_config['aet'] + ) + + study = None + + if assoc.is_established: + try: + logger.debug("Association established, sending C-FIND request") + + # Send C-FIND request + responses = assoc.send_c_find( + ds, + StudyRootQueryRetrieveInformationModelFind + ) + + # Process responses - just get the first one + for (status, dataset) in responses: + if status and status.Status == 0xFF00: # Pending + if dataset: + study = dataset + logger.debug(f"Found study: {dataset.StudyInstanceUID}") + break + elif status and status.Status != 0x0000: # Not success + logger.error(f"C-FIND error: {status}") + + if study: + logger.info(f"Found study {study_instance_uid}") + else: + logger.warning(f"No study found with UID {study_instance_uid}") + + except Exception as e: + logger.error(f"Error during C-FIND: {str(e)}") + raise DicomQueryError(f"C-FIND operation failed: {str(e)}") + finally: + # Release the association + assoc.release() + logger.debug("Association released") + else: + error_msg = f"Association rejected, aborted or never connected to {self.pacs_config['aet']}" + logger.error(error_msg) + raise DicomQueryError(error_msg) + + return study \ No newline at end of file