mirror of
https://github.com/Sudo-JHare/FHIRFLARE-IG-Toolkit.git
synced 2025-06-15 00:40:00 +00:00
https://hl7.org/fhir/extensions/StructureDefinition-structuredefinition-compliesWithProfile.html added logic to deal with Imposes and complies with
652 lines
31 KiB
Python
652 lines
31 KiB
Python
from flask import Flask, render_template, render_template_string, request, redirect, url_for, flash, jsonify, Response
|
|
from flask_sqlalchemy import SQLAlchemy
|
|
from flask_wtf import FlaskForm
|
|
from wtforms import StringField, SubmitField, SelectField
|
|
from wtforms.validators import DataRequired, Regexp
|
|
import os
|
|
import tarfile
|
|
import json
|
|
from datetime import datetime
|
|
import services
|
|
import logging
|
|
import requests
|
|
import re
|
|
|
|
# Set up logging
|
|
logging.basicConfig(level=logging.DEBUG)
|
|
logger = logging.getLogger(__name__)
|
|
|
|
app = Flask(__name__)
|
|
app.config['SECRET_KEY'] = 'your-secret-key-here'
|
|
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:////app/instance/fhir_ig.db'
|
|
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
|
|
app.config['FHIR_PACKAGES_DIR'] = os.path.join(app.instance_path, 'fhir_packages')
|
|
app.config['API_KEY'] = 'your-api-key-here'
|
|
app.config['VALIDATE_IMPOSED_PROFILES'] = True # Enable/disable imposed profile validation
|
|
app.config['DISPLAY_PROFILE_RELATIONSHIPS'] = True # Enable/disable UI display of relationships
|
|
|
|
# Ensure directories exist and are writable
|
|
instance_path = '/app/instance'
|
|
db_path = os.path.join(instance_path, 'fhir_ig.db')
|
|
packages_path = app.config['FHIR_PACKAGES_DIR']
|
|
|
|
logger.debug(f"Instance path: {instance_path}")
|
|
logger.debug(f"Database path: {db_path}")
|
|
logger.debug(f"Packages path: {packages_path}")
|
|
|
|
try:
|
|
os.makedirs(instance_path, exist_ok=True)
|
|
os.makedirs(packages_path, exist_ok=True)
|
|
os.chmod(instance_path, 0o777)
|
|
os.chmod(packages_path, 0o777)
|
|
logger.debug(f"Directories created: {os.listdir('/app')}")
|
|
logger.debug(f"Instance contents: {os.listdir(instance_path)}")
|
|
except Exception as e:
|
|
logger.error(f"Failed to create directories: {e}")
|
|
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:////tmp/fhir_ig.db'
|
|
logger.warning("Falling back to /tmp/fhir_ig.db")
|
|
|
|
db = SQLAlchemy(app)
|
|
|
|
class IgImportForm(FlaskForm):
|
|
package_name = StringField('Package Name (e.g., hl7.fhir.us.core)', validators=[
|
|
DataRequired(),
|
|
Regexp(r'^[a-zAZ0-9]+(\.[a-zA-Z0-9]+)+$', message='Invalid package name format.')
|
|
])
|
|
package_version = StringField('Package Version (e.g., 1.0.0 or current)', validators=[
|
|
DataRequired(),
|
|
Regexp(r'^[a-zA-Z0-9\.\-]+$', message='Invalid version format.')
|
|
])
|
|
dependency_mode = SelectField('Dependency Pulling Mode', choices=[
|
|
('recursive', 'Current Recursive'),
|
|
('patch-canonical', 'Patch Canonical Versions'),
|
|
('tree-shaking', 'Tree Shaking (Only Used Dependencies)')
|
|
], default='recursive')
|
|
submit = SubmitField('Fetch & Download IG')
|
|
|
|
class ProcessedIg(db.Model):
|
|
id = db.Column(db.Integer, primary_key=True)
|
|
package_name = db.Column(db.String(128), nullable=False)
|
|
version = db.Column(db.String(32), nullable=False)
|
|
processed_date = db.Column(db.DateTime, nullable=False)
|
|
resource_types_info = db.Column(db.JSON, nullable=False)
|
|
must_support_elements = db.Column(db.JSON, nullable=True)
|
|
examples = db.Column(db.JSON, nullable=True)
|
|
|
|
# Middleware to check API key
|
|
def check_api_key():
|
|
api_key = request.json.get('api_key') if request.is_json else None
|
|
if not api_key:
|
|
logger.error("API key missing in request")
|
|
return jsonify({"status": "error", "message": "API key missing"}), 401
|
|
if api_key != app.config['API_KEY']:
|
|
logger.error(f"Invalid API key provided: {api_key}")
|
|
return jsonify({"status": "error", "message": "Invalid API key"}), 401
|
|
logger.debug("API key validated successfully")
|
|
return None
|
|
|
|
@app.route('/')
|
|
def index():
|
|
return render_template('index.html', site_name='FHIRFLARE IG Toolkit', now=datetime.now())
|
|
|
|
@app.route('/import-ig', methods=['GET', 'POST'])
|
|
def import_ig():
|
|
form = IgImportForm()
|
|
if form.validate_on_submit():
|
|
name = form.package_name.data
|
|
version = form.package_version.data
|
|
dependency_mode = form.dependency_mode.data
|
|
try:
|
|
result = services.import_package_and_dependencies(name, version, dependency_mode=dependency_mode)
|
|
if result['errors'] and not result['downloaded']:
|
|
error_msg = result['errors'][0]
|
|
simplified_msg = error_msg.split(": ")[-1] if ": " in error_msg else error_msg
|
|
flash(f"Failed to import {name}#{version}: {simplified_msg}", "error - check the name and version!")
|
|
return redirect(url_for('import_ig'))
|
|
flash(f"Successfully downloaded {name}#{version} and dependencies! Mode: {dependency_mode}", "success")
|
|
return redirect(url_for('view_igs'))
|
|
except Exception as e:
|
|
flash(f"Error downloading IG: {str(e)}", "error")
|
|
return render_template('import_ig.html', form=form, site_name='FLARE FHIR IG Toolkit', now=datetime.now())
|
|
|
|
@app.route('/view-igs')
|
|
def view_igs():
|
|
igs = ProcessedIg.query.all()
|
|
processed_ids = {(ig.package_name, ig.version) for ig in igs}
|
|
|
|
packages = []
|
|
packages_dir = app.config['FHIR_PACKAGES_DIR']
|
|
logger.debug(f"Scanning packages directory: {packages_dir}")
|
|
if os.path.exists(packages_dir):
|
|
for filename in os.listdir(packages_dir):
|
|
if filename.endswith('.tgz'):
|
|
last_hyphen_index = filename.rfind('-')
|
|
if last_hyphen_index != -1 and filename.endswith('.tgz'):
|
|
name = filename[:last_hyphen_index]
|
|
version = filename[last_hyphen_index + 1:-4]
|
|
if version[0].isdigit() or version in ('preview', 'current', 'latest'):
|
|
name = name.replace('_', '.')
|
|
packages.append({'name': name, 'version': version, 'filename': filename})
|
|
else:
|
|
name = filename[:-4]
|
|
version = ''
|
|
logger.warning(f"Could not parse version from {filename}, treating as name only")
|
|
packages.append({'name': name, 'version': version, 'filename': filename})
|
|
else:
|
|
name = filename[:-4]
|
|
version = ''
|
|
logger.warning(f"Could not parse version from {filename}, treating as name only")
|
|
packages.append({'name': name, 'version': version, 'filename': filename})
|
|
logger.debug(f"Found packages: {packages}")
|
|
else:
|
|
logger.warning(f"Packages directory not found: {packages_dir}")
|
|
|
|
# Calculate duplicate_names
|
|
duplicate_names = {}
|
|
for pkg in packages:
|
|
name = pkg['name']
|
|
if name not in duplicate_names:
|
|
duplicate_names[name] = []
|
|
duplicate_names[name].append(pkg)
|
|
|
|
# Calculate duplicate_groups
|
|
duplicate_groups = {}
|
|
for name, pkgs in duplicate_names.items():
|
|
if len(pkgs) > 1:
|
|
duplicate_groups[name] = [pkg['version'] for pkg in pkgs]
|
|
|
|
# Precompute group colors
|
|
colors = ['bg-warning', 'bg-info', 'bg-success', 'bg-danger']
|
|
group_colors = {}
|
|
for i, name in enumerate(duplicate_groups.keys()):
|
|
group_colors[name] = colors[i % len(colors)]
|
|
|
|
return render_template('cp_downloaded_igs.html', packages=packages, processed_list=igs,
|
|
processed_ids=processed_ids, duplicate_names=duplicate_names,
|
|
duplicate_groups=duplicate_groups, group_colors=group_colors,
|
|
site_name='FLARE FHIR IG Toolkit', now=datetime.now(),
|
|
config=app.config)
|
|
|
|
@app.route('/push-igs', methods=['GET', 'POST'])
|
|
def push_igs():
|
|
igs = ProcessedIg.query.all()
|
|
processed_ids = {(ig.package_name, ig.version) for ig in igs}
|
|
|
|
packages = []
|
|
packages_dir = app.config['FHIR_PACKAGES_DIR']
|
|
logger.debug(f"Scanning packages directory: {packages_dir}")
|
|
if os.path.exists(packages_dir):
|
|
for filename in os.listdir(packages_dir):
|
|
if filename.endswith('.tgz'):
|
|
last_hyphen_index = filename.rfind('-')
|
|
if last_hyphen_index != -1 and filename.endswith('.tgz'):
|
|
name = filename[:last_hyphen_index]
|
|
version = filename[last_hyphen_index + 1:-4]
|
|
if version[0].isdigit() or version in ('preview', 'current', 'latest'):
|
|
name = name.replace('_', '.')
|
|
packages.append({'name': name, 'version': version, 'filename': filename})
|
|
else:
|
|
name = filename[:-4]
|
|
version = ''
|
|
logger.warning(f"Could not parse version from {filename}, treating as name only")
|
|
packages.append({'name': name, 'version': version, 'filename': filename})
|
|
else:
|
|
name = filename[:-4]
|
|
version = ''
|
|
logger.warning(f"Could not parse version from {filename}, treating as name only")
|
|
packages.append({'name': name, 'version': version, 'filename': filename})
|
|
logger.debug(f"Found packages: {packages}")
|
|
else:
|
|
logger.warning(f"Packages directory not found: {packages_dir}")
|
|
|
|
# Calculate duplicate_names
|
|
duplicate_names = {}
|
|
for pkg in packages:
|
|
name = pkg['name']
|
|
if name not in duplicate_names:
|
|
duplicate_names[name] = []
|
|
duplicate_names[name].append(pkg)
|
|
|
|
# Calculate duplicate_groups
|
|
duplicate_groups = {}
|
|
for name, pkgs in duplicate_names.items():
|
|
if len(pkgs) > 1:
|
|
duplicate_groups[name] = [pkg['version'] for pkg in pkgs]
|
|
|
|
# Precompute group colors
|
|
colors = ['bg-warning', 'bg-info', 'bg-success', 'bg-danger']
|
|
group_colors = {}
|
|
for i, name in enumerate(duplicate_groups.keys()):
|
|
group_colors[name] = colors[i % len(colors)]
|
|
|
|
return render_template('cp_push_igs.html', packages=packages, processed_list=igs,
|
|
processed_ids=processed_ids, duplicate_names=duplicate_names,
|
|
duplicate_groups=duplicate_groups, group_colors=group_colors,
|
|
site_name='FLARE FHIR IG Toolkit', now=datetime.now(),
|
|
api_key=app.config['API_KEY'],
|
|
config=app.config)
|
|
|
|
@app.route('/process-igs', methods=['POST'])
|
|
def process_ig():
|
|
filename = request.form.get('filename')
|
|
if not filename or not filename.endswith('.tgz'):
|
|
flash("Invalid package file.", "error")
|
|
return redirect(url_for('view_igs'))
|
|
|
|
tgz_path = os.path.join(app.config['FHIR_PACKAGES_DIR'], filename)
|
|
if not os.path.exists(tgz_path):
|
|
flash(f"Package file not found: {filename}", "error")
|
|
return redirect(url_for('view_igs'))
|
|
|
|
try:
|
|
last_hyphen_index = filename.rfind('-')
|
|
if last_hyphen_index != -1 and filename.endswith('.tgz'):
|
|
name = filename[:last_hyphen_index]
|
|
version = filename[last_hyphen_index + 1:-4]
|
|
name = name.replace('_', '.')
|
|
else:
|
|
name = filename[:-4]
|
|
version = ''
|
|
logger.warning(f"Could not parse version from {filename} during processing")
|
|
package_info = services.process_package_file(tgz_path)
|
|
processed_ig = ProcessedIg(
|
|
package_name=name,
|
|
version=version,
|
|
processed_date=datetime.now(),
|
|
resource_types_info=package_info['resource_types_info'],
|
|
must_support_elements=package_info.get('must_support_elements'),
|
|
examples=package_info.get('examples')
|
|
)
|
|
db.session.add(processed_ig)
|
|
db.session.commit()
|
|
flash(f"Successfully processed {name}#{version}!", "success")
|
|
except Exception as e:
|
|
flash(f"Error processing IG: {str(e)}", "error")
|
|
return redirect(url_for('view_igs'))
|
|
|
|
@app.route('/delete-ig', methods=['POST'])
|
|
def delete_ig():
|
|
filename = request.form.get('filename')
|
|
if not filename or not filename.endswith('.tgz'):
|
|
flash("Invalid package file.", "error")
|
|
return redirect(url_for('view_igs'))
|
|
|
|
tgz_path = os.path.join(app.config['FHIR_PACKAGES_DIR'], filename)
|
|
metadata_path = tgz_path.replace('.tgz', '.metadata.json')
|
|
if os.path.exists(tgz_path):
|
|
try:
|
|
os.remove(tgz_path)
|
|
if os.path.exists(metadata_path):
|
|
os.remove(metadata_path)
|
|
logger.debug(f"Deleted metadata file: {metadata_path}")
|
|
flash(f"Deleted {filename}", "success")
|
|
except Exception as e:
|
|
flash(f"Error deleting {filename}: {str(e)}", "error")
|
|
else:
|
|
flash(f"File not found: {filename}", "error")
|
|
return redirect(url_for('view_igs'))
|
|
|
|
@app.route('/unload-ig', methods=['POST'])
|
|
def unload_ig():
|
|
ig_id = request.form.get('ig_id')
|
|
if not ig_id:
|
|
flash("Invalid package ID.", "error")
|
|
return redirect(url_for('view_igs'))
|
|
|
|
processed_ig = db.session.get(ProcessedIg, ig_id)
|
|
if processed_ig:
|
|
try:
|
|
db.session.delete(processed_ig)
|
|
db.session.commit()
|
|
flash(f"Unloaded {processed_ig.package_name}#{processed_ig.version}", "success")
|
|
except Exception as e:
|
|
flash(f"Error unloading package: {str(e)}", "error")
|
|
else:
|
|
flash(f"Package not found with ID: {ig_id}", "error")
|
|
return redirect(url_for('view_igs'))
|
|
|
|
@app.route('/view-ig/<int:processed_ig_id>')
|
|
def view_ig(processed_ig_id):
|
|
processed_ig = ProcessedIg.query.get_or_404(processed_ig_id)
|
|
profile_list = [t for t in processed_ig.resource_types_info if t.get('is_profile')]
|
|
base_list = [t for t in processed_ig.resource_types_info if not t.get('is_profile')]
|
|
examples_by_type = processed_ig.examples or {}
|
|
|
|
# Load metadata to get profile relationships
|
|
package_name = processed_ig.package_name
|
|
version = processed_ig.version
|
|
metadata_filename = f"{services.sanitize_filename_part(package_name)}-{services.sanitize_filename_part(version)}.metadata.json"
|
|
metadata_path = os.path.join(app.config['FHIR_PACKAGES_DIR'], metadata_filename)
|
|
complies_with_profiles = []
|
|
imposed_profiles = []
|
|
if os.path.exists(metadata_path):
|
|
with open(metadata_path, 'r') as f:
|
|
metadata = json.load(f)
|
|
complies_with_profiles = metadata.get('complies_with_profiles', [])
|
|
imposed_profiles = metadata.get('imposed_profiles', [])
|
|
|
|
return render_template('cp_view_processed_ig.html', title=f"View {processed_ig.package_name}#{processed_ig.version}",
|
|
processed_ig=processed_ig, profile_list=profile_list, base_list=base_list,
|
|
examples_by_type=examples_by_type, site_name='FLARE FHIR IG Toolkit', now=datetime.now(),
|
|
complies_with_profiles=complies_with_profiles, imposed_profiles=imposed_profiles,
|
|
config=app.config)
|
|
|
|
@app.route('/get-structure')
|
|
def get_structure_definition():
|
|
package_name = request.args.get('package_name')
|
|
package_version = request.args.get('package_version')
|
|
resource_identifier = request.args.get('resource_type')
|
|
if not all([package_name, package_version, resource_identifier]):
|
|
return jsonify({"error": "Missing query parameters"}), 400
|
|
|
|
tgz_path = os.path.join(app.config['FHIR_PACKAGES_DIR'], services._construct_tgz_filename(package_name, package_version))
|
|
sd_data = None
|
|
fallback_used = False
|
|
|
|
if os.path.exists(tgz_path):
|
|
sd_data, _ = services.find_and_extract_sd(tgz_path, resource_identifier)
|
|
|
|
if sd_data is None:
|
|
logger.debug(f"Structure definition for '{resource_identifier}' not found in {package_name}#{package_version}, attempting fallback to hl7.fhir.r4.core#4.0.1")
|
|
core_package_name = "hl7.fhir.r4.core"
|
|
core_package_version = "4.0.1"
|
|
|
|
core_tgz_path = os.path.join(app.config['FHIR_PACKAGES_DIR'], services._construct_tgz_filename(core_package_name, core_package_version))
|
|
if not os.path.exists(core_tgz_path):
|
|
logger.debug(f"Core package {core_package_name}#{core_package_version} not found, attempting to download")
|
|
try:
|
|
result = services.import_package_and_dependencies(core_package_name, core_package_version)
|
|
if result['errors'] and not result['downloaded']:
|
|
logger.error(f"Failed to download core package: {result['errors'][0]}")
|
|
return jsonify({"error": f"SD for '{resource_identifier}' not found in {package_name}#{package_version}, and failed to download core package: {result['errors'][0]}"}), 404
|
|
except Exception as e:
|
|
logger.error(f"Error downloading core package: {str(e)}")
|
|
return jsonify({"error": f"SD for '{resource_identifier}' not found in {package_name}#{package_version}, and error downloading core package: {str(e)}"}), 500
|
|
|
|
if os.path.exists(core_tgz_path):
|
|
sd_data, _ = services.find_and_extract_sd(core_tgz_path, resource_identifier)
|
|
if sd_data is None:
|
|
return jsonify({"error": f"SD for '{resource_identifier}' not found in {package_name}#{package_version} or in core package {core_package_name}#{core_package_version}."}), 404
|
|
fallback_used = True
|
|
else:
|
|
return jsonify({"error": f"SD for '{resource_identifier}' not found in {package_name}#{package_version}, and core package {core_package_name}#{core_package_version} could not be located."}), 404
|
|
|
|
elements = sd_data.get('snapshot', {}).get('element', []) or sd_data.get('differential', {}).get('element', [])
|
|
processed_ig = ProcessedIg.query.filter_by(package_name=package_name, version=package_version).first()
|
|
must_support_paths = processed_ig.must_support_elements.get(resource_identifier, []) if processed_ig else []
|
|
|
|
response = {
|
|
"elements": elements,
|
|
"must_support_paths": must_support_paths,
|
|
"fallback_used": fallback_used,
|
|
"source_package": f"{core_package_name}#{core_package_version}" if fallback_used else f"{package_name}#{package_version}"
|
|
}
|
|
return jsonify(response)
|
|
|
|
@app.route('/get-example')
|
|
def get_example_content():
|
|
package_name = request.args.get('package_name')
|
|
package_version = request.args.get('package_version')
|
|
example_member_path = request.args.get('filename')
|
|
if not all([package_name, package_version, example_member_path]):
|
|
return jsonify({"error": "Missing query parameters"}), 400
|
|
tgz_path = os.path.join(app.config['FHIR_PACKAGES_DIR'], services._construct_tgz_filename(package_name, package_version))
|
|
if not os.path.exists(tgz_path):
|
|
return jsonify({"error": f"Package file not found: {tgz_path}"}), 404
|
|
if not example_member_path.startswith('package/') or '..' in example_member_path:
|
|
return jsonify({"error": "Invalid example file path."}), 400
|
|
try:
|
|
with tarfile.open(tgz_path, "r:gz") as tar:
|
|
try:
|
|
example_member = tar.getmember(example_member_path)
|
|
except KeyError:
|
|
return jsonify({"error": f"Example file '{example_member_path}' not found."}), 404
|
|
with tar.extractfile(example_member) as example_fileobj:
|
|
content_bytes = example_fileobj.read()
|
|
return content_bytes.decode('utf-8-sig')
|
|
except tarfile.TarError as e:
|
|
return jsonify({"error": f"Error reading {tgz_path}: {e}"}), 500
|
|
|
|
@app.route('/get-package-metadata')
|
|
def get_package_metadata():
|
|
package_name = request.args.get('package_name')
|
|
version = request.args.get('version')
|
|
if not package_name or not version:
|
|
return jsonify({'error': 'Missing package_name or version'}), 400
|
|
metadata = services.get_package_metadata(package_name, version)
|
|
if metadata:
|
|
return jsonify({'dependency_mode': metadata['dependency_mode']})
|
|
return jsonify({'error': 'Metadata not found'}), 404
|
|
|
|
# API Endpoint: Import IG Package
|
|
@app.route('/api/import-ig', methods=['POST'])
|
|
def api_import_ig():
|
|
auth_error = check_api_key()
|
|
if auth_error:
|
|
return auth_error
|
|
|
|
if not request.is_json:
|
|
return jsonify({"status": "error", "message": "Request must be JSON"}), 400
|
|
|
|
data = request.get_json()
|
|
package_name = data.get('package_name')
|
|
version = data.get('version')
|
|
dependency_mode = data.get('dependency_mode', 'recursive')
|
|
|
|
if not package_name or not version:
|
|
return jsonify({"status": "error", "message": "Missing package_name or version"}), 400
|
|
|
|
if not (isinstance(package_name, str) and isinstance(version, str) and
|
|
re.match(r'^[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)+$', package_name) and
|
|
re.match(r'^[a-zA-Z0-9\.\-]+$', version)):
|
|
return jsonify({"status": "error", "message": "Invalid package name or version format"}), 400
|
|
|
|
valid_modes = ['recursive', 'patch-canonical', 'tree-shaking']
|
|
if dependency_mode not in valid_modes:
|
|
return jsonify({"status": "error", "message": f"Invalid dependency mode: {dependency_mode}. Must be one of {valid_modes}"}), 400
|
|
|
|
try:
|
|
result = services.import_package_and_dependencies(package_name, version, dependency_mode=dependency_mode)
|
|
if result['errors'] and not result['downloaded']:
|
|
return jsonify({"status": "error", "message": f"Failed to import {package_name}#{version}: {result['errors'][0]}"}), 500
|
|
|
|
# Process the package to get compliesWithProfile and imposeProfile
|
|
package_filename = f"{services.sanitize_filename_part(package_name)}-{services.sanitize_filename_part(version)}.tgz"
|
|
package_path = os.path.join(app.config['FHIR_PACKAGES_DIR'], package_filename)
|
|
complies_with_profiles = []
|
|
imposed_profiles = []
|
|
if os.path.exists(package_path):
|
|
process_result = services.process_package_file(package_path)
|
|
complies_with_profiles = process_result.get('complies_with_profiles', [])
|
|
imposed_profiles = process_result.get('imposed_profiles', [])
|
|
else:
|
|
logger.warning(f"Package file not found after import: {package_path}")
|
|
|
|
# Check for duplicates
|
|
packages = []
|
|
packages_dir = app.config['FHIR_PACKAGES_DIR']
|
|
if os.path.exists(packages_dir):
|
|
for filename in os.listdir(packages_dir):
|
|
if filename.endswith('.tgz'):
|
|
last_hyphen_index = filename.rfind('-')
|
|
if last_hyphen_index != -1 and filename.endswith('.tgz'):
|
|
name = filename[:last_hyphen_index]
|
|
version = filename[last_hyphen_index + 1:-4]
|
|
name = name.replace('_', '.')
|
|
if version[0].isdigit() or version in ('preview', 'current', 'latest'):
|
|
packages.append({'name': name, 'version': version, 'filename': filename})
|
|
else:
|
|
name = filename[:-4]
|
|
version = ''
|
|
packages.append({'name': name, 'version': version, 'filename': filename})
|
|
else:
|
|
name = filename[:-4]
|
|
version = ''
|
|
packages.append({'name': name, 'version': version, 'filename': filename})
|
|
|
|
duplicate_names = {}
|
|
for pkg in packages:
|
|
name = pkg['name']
|
|
if name not in duplicate_names:
|
|
duplicate_names[name] = []
|
|
duplicate_names[name].append(pkg)
|
|
|
|
duplicates = []
|
|
for name, pkgs in duplicate_names.items():
|
|
if len(pkgs) > 1:
|
|
versions = [pkg['version'] for pkg in pkgs]
|
|
duplicates.append(f"{name} (exists as {', '.join(versions)})")
|
|
|
|
seen = set()
|
|
unique_dependencies = []
|
|
for dep in result.get('dependencies', []):
|
|
dep_str = f"{dep['name']}#{dep['version']}"
|
|
if dep_str not in seen:
|
|
seen.add(dep_str)
|
|
unique_dependencies.append(dep_str)
|
|
|
|
response = {
|
|
"status": "success",
|
|
"message": "Package imported successfully",
|
|
"package_name": package_name,
|
|
"version": version,
|
|
"dependency_mode": dependency_mode,
|
|
"dependencies": unique_dependencies,
|
|
"complies_with_profiles": complies_with_profiles,
|
|
"imposed_profiles": imposed_profiles,
|
|
"duplicates": duplicates
|
|
}
|
|
return jsonify(response), 200
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error in api_import_ig: {str(e)}")
|
|
return jsonify({"status": "error", "message": f"Error importing package: {str(e)}"}), 500
|
|
|
|
# API Endpoint: Push IG to FHIR Server with Streaming
|
|
@app.route('/api/push-ig', methods=['POST'])
|
|
def api_push_ig():
|
|
auth_error = check_api_key()
|
|
if auth_error:
|
|
return auth_error
|
|
|
|
if not request.is_json:
|
|
return jsonify({"status": "error", "message": "Request must be JSON"}), 400
|
|
|
|
data = request.get_json()
|
|
package_name = data.get('package_name')
|
|
version = data.get('version')
|
|
fhir_server_url = data.get('fhir_server_url')
|
|
include_dependencies = data.get('include_dependencies', True)
|
|
|
|
if not all([package_name, version, fhir_server_url]):
|
|
return jsonify({"status": "error", "message": "Missing package_name, version, or fhir_server_url"}), 400
|
|
|
|
if not (isinstance(package_name, str) and isinstance(version, str) and
|
|
re.match(r'^[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)+$', package_name) and
|
|
re.match(r'^[a-zA-Z0-9\.\-]+$', version)):
|
|
return jsonify({"status": "error", "message": "Invalid package name or version format"}), 400
|
|
|
|
tgz_filename = services._construct_tgz_filename(package_name, version)
|
|
tgz_path = os.path.join(app.config['FHIR_PACKAGES_DIR'], tgz_filename)
|
|
if not os.path.exists(tgz_path):
|
|
return jsonify({"status": "error", "message": f"Package not found: {package_name}#{version}"}), 404
|
|
|
|
def generate_stream():
|
|
try:
|
|
yield json.dumps({"type": "start", "message": f"Starting push for {package_name}#{version}..."}) + "\n"
|
|
|
|
resources = []
|
|
packages_to_push = [(package_name, version, tgz_path)]
|
|
if include_dependencies:
|
|
yield json.dumps({"type": "progress", "message": "Processing dependencies..."}) + "\n"
|
|
metadata = services.get_package_metadata(package_name, version)
|
|
dependencies = metadata.get('imported_dependencies', []) if metadata else []
|
|
for dep in dependencies:
|
|
dep_name = dep['name']
|
|
dep_version = dep['version']
|
|
dep_tgz_filename = services._construct_tgz_filename(dep_name, dep_version)
|
|
dep_tgz_path = os.path.join(app.config['FHIR_PACKAGES_DIR'], dep_tgz_filename)
|
|
if os.path.exists(dep_tgz_path):
|
|
packages_to_push.append((dep_name, dep_version, dep_tgz_path))
|
|
yield json.dumps({"type": "progress", "message": f"Added dependency {dep_name}#{dep_version}"}) + "\n"
|
|
else:
|
|
yield json.dumps({"type": "warning", "message": f"Dependency {dep_name}#{dep_version} not found, skipping"}) + "\n"
|
|
|
|
for pkg_name, pkg_version, pkg_path in packages_to_push:
|
|
with tarfile.open(pkg_path, "r:gz") as tar:
|
|
for member in tar.getmembers():
|
|
if member.name.startswith('package/') and member.name.endswith('.json') and not member.name.endswith('package.json'):
|
|
with tar.extractfile(member) as f:
|
|
resource_data = json.load(f)
|
|
if 'resourceType' in resource_data:
|
|
resources.append((resource_data, pkg_name, pkg_version))
|
|
|
|
server_response = []
|
|
success_count = 0
|
|
failure_count = 0
|
|
total_resources = len(resources)
|
|
yield json.dumps({"type": "progress", "message": f"Found {total_resources} resources to upload"}) + "\n"
|
|
|
|
pushed_packages = []
|
|
for i, (resource, pkg_name, pkg_version) in enumerate(resources, 1):
|
|
resource_type = resource.get('resourceType')
|
|
resource_id = resource.get('id')
|
|
if not resource_type or not resource_id:
|
|
yield json.dumps({"type": "warning", "message": f"Skipping invalid resource at index {i} from {pkg_name}#{pkg_version}"}) + "\n"
|
|
failure_count += 1
|
|
continue
|
|
|
|
# Validate against the profile and imposed profiles
|
|
validation_result = services.validate_resource_against_profile(resource, pkg_name, pkg_version, resource_type)
|
|
if not validation_result['valid']:
|
|
yield json.dumps({"type": "error", "message": f"Validation failed for {resource_type}/{resource_id} in {pkg_name}#{pkg_version}: {', '.join(validation_result['errors'])}"}) + "\n"
|
|
failure_count += 1
|
|
continue
|
|
|
|
resource_url = f"{fhir_server_url.rstrip('/')}/{resource_type}/{resource_id}"
|
|
yield json.dumps({"type": "progress", "message": f"Uploading {resource_type}/{resource_id} ({i}/{total_resources}) from {pkg_name}#{pkg_version}..."}) + "\n"
|
|
|
|
try:
|
|
response = requests.put(resource_url, json=resource, headers={'Content-Type': 'application/fhir+json'})
|
|
response.raise_for_status()
|
|
server_response.append(f"Uploaded {resource_type}/{resource_id} successfully")
|
|
yield json.dumps({"type": "success", "message": f"Uploaded {resource_type}/{resource_id} successfully"}) + "\n"
|
|
success_count += 1
|
|
if f"{pkg_name}#{pkg_version}" not in pushed_packages:
|
|
pushed_packages.append(f"{pkg_name}#{pkg_version}")
|
|
except requests.exceptions.RequestException as e:
|
|
error_msg = f"Failed to upload {resource_type}/{resource_id}: {str(e)}"
|
|
server_response.append(error_msg)
|
|
yield json.dumps({"type": "error", "message": error_msg}) + "\n"
|
|
failure_count += 1
|
|
|
|
summary = {
|
|
"status": "success" if failure_count == 0 else "partial",
|
|
"message": f"Push completed: {success_count} resources uploaded, {failure_count} failed",
|
|
"package_name": package_name,
|
|
"version": version,
|
|
"pushed_packages": pushed_packages,
|
|
"server_response": "; ".join(server_response) if server_response else "No resources uploaded",
|
|
"success_count": success_count,
|
|
"failure_count": failure_count
|
|
}
|
|
yield json.dumps({"type": "complete", "data": summary}) + "\n"
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error in api_push_ig: {str(e)}")
|
|
error_response = {
|
|
"status": "error",
|
|
"message": f"Error pushing package: {str(e)}"
|
|
}
|
|
yield json.dumps({"type": "error", "message": error_response["message"]}) + "\n"
|
|
yield json.dumps({"type": "complete", "data": error_response}) + "\n"
|
|
|
|
return Response(generate_stream(), mimetype='application/x-ndjson')
|
|
|
|
with app.app_context():
|
|
logger.debug(f"Creating database at: {app.config['SQLALCHEMY_DATABASE_URI']}")
|
|
db.create_all()
|
|
logger.debug("Database initialization complete")
|
|
|
|
if __name__ == '__main__':
|
|
app.run(debug=True) |