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/') 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)