add http test | fix ctrl+c shutdown
This commit is contained in:
@@ -1,172 +1,101 @@
|
|||||||
# DICOM Migration REST API
|
# DICOM Migration REST API
|
||||||
|
|
||||||
This REST API provides an endpoint to trigger DICOM study migration for a specific Accession Number.
|
A simple REST API that triggers DICOM study migration for a specific Accession Number.
|
||||||
|
|
||||||
## Features
|
## How to Run
|
||||||
|
|
||||||
- Migrate DICOM studies by Accession Number
|
|
||||||
- Token-based authentication
|
|
||||||
- Detailed JSON responses with operation status
|
|
||||||
- Proper logging and error handling
|
|
||||||
- Automatic cleanup of DICOM files after each request
|
|
||||||
|
|
||||||
## Requirements
|
|
||||||
|
|
||||||
- Python 3.9 or higher
|
|
||||||
- FastAPI
|
|
||||||
- Uvicorn
|
|
||||||
- Dependencies from the main DICOM migration project
|
|
||||||
|
|
||||||
## Installation
|
|
||||||
|
|
||||||
1. Install the required dependencies:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd api-app
|
|
||||||
pip install -r requirements.txt
|
|
||||||
```
|
|
||||||
|
|
||||||
## Configuration
|
|
||||||
|
|
||||||
The API uses the same configuration settings as the main DICOM migration project.
|
|
||||||
|
|
||||||
### Authentication
|
|
||||||
|
|
||||||
The API uses token-based authentication. Set your token using the environment variable:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
export API_TOKEN=your-secure-token
|
|
||||||
```
|
|
||||||
|
|
||||||
If no token is provided, a default token will be used (not recommended for production).
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
### Starting the API Server
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Start the server on default port 8000
|
# Start the server on default port 8000
|
||||||
python run.py
|
python run.py
|
||||||
|
|
||||||
# Specify a custom port
|
# Custom port
|
||||||
python run.py --port 9000
|
python run.py --port 9000
|
||||||
|
|
||||||
# Specify a custom token
|
# Custom token
|
||||||
python run.py --token your-secure-token
|
python run.py --token your-secure-token
|
||||||
|
|
||||||
# Enable auto-reload for development
|
# Development mode with auto-reload
|
||||||
python run.py --reload
|
python run.py --reload
|
||||||
```
|
```
|
||||||
|
|
||||||
### API Endpoints
|
## How to Use
|
||||||
|
|
||||||
#### 1. Migrate Study by Accession Number
|
### Make a Request
|
||||||
|
|
||||||
```
|
|
||||||
POST /migrate/{accession_number}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Authentication:**
|
|
||||||
- Bearer token in Authorization header
|
|
||||||
|
|
||||||
**Response:**
|
|
||||||
|
|
||||||
Successful migration:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"success": true,
|
|
||||||
"message": "Successfully migrated study for accession number: ABC12345",
|
|
||||||
"details": {
|
|
||||||
"study_uid": "1.2.826.0.1.3680043.9.7307.1.123456",
|
|
||||||
"his_integration_success": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Failed migration:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"success": false,
|
|
||||||
"message": "Failed to process study: Study not found",
|
|
||||||
"details": null
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Example API Calls
|
|
||||||
|
|
||||||
### Using curl
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Migrate a study by accession number
|
# Using curl
|
||||||
curl -X POST "http://localhost:8000/migrate/ABC12345" \
|
curl -X POST "http://localhost:8000/migrate/R.20240401.0138" \
|
||||||
-H "Authorization: Bearer your-token" \
|
-H "Authorization: Bearer token-his2-untuk-hit-api-migrasi-clarity" \
|
||||||
-H "Content-Type: application/json"
|
-H "Content-Type: application/json"
|
||||||
```
|
```
|
||||||
|
|
||||||
### Using Python requests
|
### Response Example
|
||||||
|
|
||||||
```python
|
```json
|
||||||
import requests
|
{
|
||||||
|
"success":true,
|
||||||
url = "http://localhost:8000/migrate/ABC12345"
|
"message":"Berhasil migrasi file DICOM Accession Number: R.20240401.0138",
|
||||||
headers = {
|
"details":
|
||||||
"Authorization": "Bearer your-token",
|
{
|
||||||
"Content-Type": "application/json"
|
"accession_number":"R.20240401.0138",
|
||||||
|
"process_start_time":"2025-07-11 15:27:26",
|
||||||
|
"steps_completed":[
|
||||||
|
"study_found","study_retrieved","study_sent","log_created","his_api_success","cleanup_success"
|
||||||
|
],
|
||||||
|
"study_uid":"1.2.826.1.3680043.9.5282.150415.544835.202404010138",
|
||||||
|
"patient_id":"00544835",
|
||||||
|
"study_description":"A103 - Thorax PA",
|
||||||
|
"instances_retrieved":4,
|
||||||
|
"files_sent":2,
|
||||||
|
"total_files":2,
|
||||||
|
"c_store_success":true,
|
||||||
|
"series_count":4,
|
||||||
|
"his_api_status_code":200,
|
||||||
|
"his_integration_success":true,
|
||||||
|
"cleanup_success":true,
|
||||||
|
"process_end_time":"2025-07-11 15:27:46"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
response = requests.post(url, headers=headers)
|
|
||||||
print(response.json())
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Logs
|
## Logs
|
||||||
|
|
||||||
The API logs are stored in the `logs/api.log` file. The log includes information about:
|
- Main API logs: `logs/api.log`
|
||||||
|
- Process details: `logs/api_process.log`
|
||||||
|
|
||||||
- API startup and shutdown
|
## Simple Workflow
|
||||||
- Migration requests
|
|
||||||
- Errors during migration process
|
|
||||||
- Integration with HIS results
|
|
||||||
|
|
||||||
## Troubleshooting
|
1. API receives request with accession number
|
||||||
|
2. Finds study UID using C-FIND
|
||||||
|
3. Downloads DICOM files using C-GET
|
||||||
|
4. Uploads files to destination PACS using C-STORE
|
||||||
|
5. Updates HIS system via API call
|
||||||
|
6. Cleans up all temporary DICOM files
|
||||||
|
7. Returns success/failure response
|
||||||
|
|
||||||
1. **Authentication Error**:
|
## Troubleshooting Tips
|
||||||
- Make sure you're providing the correct token in the Authorization header
|
|
||||||
- Check that the token format is `Bearer your-token`
|
|
||||||
|
|
||||||
2. **Study Not Found**:
|
- Check PACS connectivity in `config/settings.py`
|
||||||
- Verify that the accession number is correct
|
- Verify the accession number exists in source PACS
|
||||||
- Check if the PACS server is reachable
|
- Ensure permissions for log directories
|
||||||
- Verify that the study exists in the source PACS
|
- Check both log files for detailed error messages
|
||||||
|
- Verify HIS API connectivity for end-to-end success
|
||||||
|
|
||||||
3. **Connection Error**:
|
## Quick Test
|
||||||
- Check PACS connectivity
|
|
||||||
- Verify network settings in `config/settings.py`
|
|
||||||
|
|
||||||
## Integrating with External Systems
|
1. Start the server:
|
||||||
|
```bash
|
||||||
|
python run.py
|
||||||
|
```
|
||||||
|
|
||||||
You can call this API from other systems to trigger DICOM migrations for specific studies:
|
2. In another terminal, test the root endpoint:
|
||||||
|
```bash
|
||||||
|
curl http://localhost:8000/
|
||||||
|
```
|
||||||
|
|
||||||
1. Obtain the API token
|
3. Test migration with a valid accession number:
|
||||||
2. Make a POST request to `/migrate/{accession_number}`
|
```bash
|
||||||
3. Handle the JSON response based on the `success` field
|
curl -X POST "http://localhost:8000/migrate/R.20240401.0138" \
|
||||||
|
-H "Authorization: Bearer token-his2-untuk-hit-api-migrasi-clarity"
|
||||||
|
```
|
||||||
|
|
||||||
## Technical Details
|
|
||||||
|
|
||||||
### DICOM File Cleanup
|
|
||||||
|
|
||||||
The API implements a robust cleanup mechanism to ensure that DICOM files are always removed after processing:
|
|
||||||
|
|
||||||
1. **Exit Handlers**: The application registers cleanup handlers via Python's `atexit` module and signal handlers for graceful cleanup during server shutdown.
|
|
||||||
|
|
||||||
2. **Per-Request Cleanup**: After each migration request completes (whether successful or not), the API explicitly calls the cleanup function to remove all DICOM files associated with the study.
|
|
||||||
|
|
||||||
3. **Directory Registration**: All temporary directories created during the migration process are registered for cleanup, ensuring no files are left behind.
|
|
||||||
|
|
||||||
This dual approach (exit handlers + explicit cleanup) ensures that:
|
|
||||||
- DICOM files are immediately removed after each API request completes
|
|
||||||
- No files are left behind even if the server crashes or is terminated abnormally
|
|
||||||
- Disk space is efficiently managed during high-volume operations
|
|
||||||
|
|
||||||
### Authentication
|
|
||||||
|
|
||||||
The API uses Bearer token authentication. The token is specified through:
|
|
||||||
|
|||||||
25
api-app/api_cleanup.py
Normal file
25
api-app/api_cleanup.py
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
"""
|
||||||
|
Custom cleanup utilities for the API application.
|
||||||
|
This module provides customized cleanup functions that are more compatible with FastAPI.
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import shutil
|
||||||
|
import atexit
|
||||||
|
from config import settings
|
||||||
|
from utils.logger import setup_logger
|
||||||
|
from utils.cleanup import _cleanup_dirs, cleanup_dicom_files
|
||||||
|
|
||||||
|
# Create API cleanup logger
|
||||||
|
cleanup_logger = setup_logger("api_cleanup", "logs/api_cleanup.log")
|
||||||
|
|
||||||
|
def register_api_cleanup():
|
||||||
|
"""
|
||||||
|
Register cleanup handlers for the API application.
|
||||||
|
This avoids registering signal handlers which can conflict with FastAPI.
|
||||||
|
"""
|
||||||
|
# Only register the atexit handler, not signal handlers
|
||||||
|
atexit.register(cleanup_dicom_files)
|
||||||
|
cleanup_logger.info("Registered API cleanup handler for exit")
|
||||||
|
|
||||||
|
return True
|
||||||
@@ -17,7 +17,7 @@ sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|||||||
from process import process_study_by_accession
|
from process import process_study_by_accession
|
||||||
from utils.logger import setup_logger
|
from utils.logger import setup_logger
|
||||||
from utils.dicom_utils import create_directory_if_not_exists
|
from utils.dicom_utils import create_directory_if_not_exists
|
||||||
from utils.cleanup import register_exit_handlers
|
from api_cleanup import register_api_cleanup
|
||||||
|
|
||||||
# Create API logger
|
# Create API logger
|
||||||
api_logger = setup_logger("api", "logs/api.log")
|
api_logger = setup_logger("api", "logs/api.log")
|
||||||
@@ -71,10 +71,23 @@ def verify_token(credentials: HTTPAuthorizationCredentials = Depends(security)):
|
|||||||
@app.on_event("startup")
|
@app.on_event("startup")
|
||||||
async def startup_event():
|
async def startup_event():
|
||||||
"""Initialize resources when the API starts."""
|
"""Initialize resources when the API starts."""
|
||||||
register_exit_handlers()
|
# Create necessary directories
|
||||||
|
create_directory_if_not_exists("logs")
|
||||||
|
|
||||||
|
# Use our custom API cleanup registration that avoids signal handlers
|
||||||
|
register_api_cleanup()
|
||||||
|
|
||||||
api_logger.info("API started successfully")
|
api_logger.info("API started successfully")
|
||||||
|
|
||||||
|
@app.on_event("shutdown")
|
||||||
|
async def shutdown_event():
|
||||||
|
"""Clean up resources when the API shuts down."""
|
||||||
|
api_logger.info("API shutting down, cleaning up resources...")
|
||||||
|
# Perform cleanup operations here
|
||||||
|
from utils.cleanup import cleanup_dicom_files
|
||||||
|
cleanup_dicom_files()
|
||||||
|
api_logger.info("Cleanup complete, API shutdown successful")
|
||||||
|
|
||||||
@app.get("/", response_model=MigrationResponse)
|
@app.get("/", response_model=MigrationResponse)
|
||||||
async def root():
|
async def root():
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ Script to run the FastAPI server for DICOM migration service.
|
|||||||
"""
|
"""
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
import signal
|
||||||
import uvicorn
|
import uvicorn
|
||||||
import argparse
|
import argparse
|
||||||
|
|
||||||
@@ -16,6 +17,12 @@ def parse_args():
|
|||||||
parser.add_argument('--reload', action='store_true', help='Enable auto-reload for development')
|
parser.add_argument('--reload', action='store_true', help='Enable auto-reload for development')
|
||||||
return parser.parse_args()
|
return parser.parse_args()
|
||||||
|
|
||||||
|
def handle_exit(signum, frame):
|
||||||
|
"""Handle exit signals gracefully."""
|
||||||
|
print(f"Received signal {signum}, shutting down gracefully...")
|
||||||
|
# Let uvicorn handle the exit
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
"""Main function to run the API server."""
|
"""Main function to run the API server."""
|
||||||
args = parse_args()
|
args = parse_args()
|
||||||
@@ -29,6 +36,10 @@ def main():
|
|||||||
print("WARNING: API_TOKEN not set. Using default token 'token-his2-untuk-hit-api-migrasi-clarity'.")
|
print("WARNING: API_TOKEN not set. Using default token 'token-his2-untuk-hit-api-migrasi-clarity'.")
|
||||||
print("Set environment variable API_TOKEN or use --token argument for better security.")
|
print("Set environment variable API_TOKEN or use --token argument for better security.")
|
||||||
|
|
||||||
|
# Set up signal handlers for graceful shutdown
|
||||||
|
signal.signal(signal.SIGINT, handle_exit)
|
||||||
|
signal.signal(signal.SIGTERM, handle_exit)
|
||||||
|
|
||||||
print(f"Starting DICOM Migration API server on {args.host}:{args.port}")
|
print(f"Starting DICOM Migration API server on {args.host}:{args.port}")
|
||||||
uvicorn.run(
|
uvicorn.run(
|
||||||
"main:app",
|
"main:app",
|
||||||
|
|||||||
12
api-app/test.http
Normal file
12
api-app/test.http
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
@host = http://localhost:8000
|
||||||
|
@token = token-his2-untuk-hit-api-migrasi-clarity
|
||||||
|
|
||||||
|
|
||||||
|
### Healtcheck
|
||||||
|
GET {{host}}/
|
||||||
|
|
||||||
|
|
||||||
|
###
|
||||||
|
POST {{host}}/migrate/R.20240401.0138
|
||||||
|
Authorization: Bearer {{token}}
|
||||||
|
Content-Type: application/json
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
DICOM file cleanup utility to ensure temporary DICOM files are removed even on script termination.
|
DICOM file cleanup utility to ensure temporary DICOM files are removed even on script termination.
|
||||||
"""
|
"""
|
||||||
import os
|
import os
|
||||||
|
import sys
|
||||||
import shutil
|
import shutil
|
||||||
import atexit
|
import atexit
|
||||||
import signal
|
import signal
|
||||||
@@ -100,4 +101,12 @@ def signal_handler(sig, frame):
|
|||||||
|
|
||||||
logger.info(f"Received {signal_name}, cleaning up...")
|
logger.info(f"Received {signal_name}, cleaning up...")
|
||||||
cleanup_dicom_files()
|
cleanup_dicom_files()
|
||||||
exit(0)
|
# Don't call exit() directly in a web application context
|
||||||
|
# Let the calling application handle the exit process
|
||||||
|
# This fix makes it compatible with FastAPI and other web frameworks
|
||||||
|
# Raise SystemExit instead of calling exit() directly
|
||||||
|
if hasattr(frame, '_is_web_app') and frame._is_web_app:
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
# For command-line applications, we can exit directly
|
||||||
|
sys.exit(0)
|
||||||
Reference in New Issue
Block a user