diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..788c9aed --- /dev/null +++ b/.dockerignore @@ -0,0 +1,16 @@ +.git +.idea +.pytest_cache +.ruff_cache +__pycache__ +*.pyc +*.pyo +*.pyd +.env +.env.* +tests/ +docs/ +notebooks/ +output/ +*.md +!README.md diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..87172247 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,48 @@ +# HealthChain application Dockerfile +# +# Usage: +# Build: docker build -t my-healthcare-app . +# Run: docker run -p 8000:8000 --env-file .env my-healthcare-app +# +# Required environment variables (set in .env or pass via -e): +# APP_MODULE Python module path to your app, e.g. "myapp:app" (default: "app:app") +# +# FHIR source credentials (if connecting to Epic/Cerner): +# FHIR_BASE_URL, CLIENT_ID, CLIENT_SECRET or CLIENT_SECRET_PATH +# +# See docs: https://dotimplement.github.io/HealthChain/reference/gateway/gateway/ + +FROM python:3.11-slim + +# Keeps Python from buffering stdout/stderr +ENV PYTHONUNBUFFERED=1 \ + PYTHONDONTWRITEBYTECODE=1 \ + APP_MODULE=app:app \ + PORT=8000 + +WORKDIR /app + +# Install system dependencies needed by lxml and spaCy +RUN apt-get update && apt-get install -y --no-install-recommends \ + gcc \ + g++ \ + && rm -rf /var/lib/apt/lists/* + +# Install healthchain from PyPI +RUN pip install --no-cache-dir healthchain + +# Install any additional dependencies your application needs +# (copy requirements first to leverage Docker layer caching) +COPY requirements.txt* ./ +RUN if [ -f requirements.txt ]; then pip install --no-cache-dir -r requirements.txt; fi + +# Copy application code +COPY . . + +# Run as non-root user +RUN useradd -m appuser && chown -R appuser /app +USER appuser + +EXPOSE $PORT + +CMD uvicorn $APP_MODULE --host 0.0.0.0 --port $PORT diff --git a/README.md b/README.md index a004dda6..877a755b 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@
-HealthChain is an open-source developer framework to build healthcare AI applications with native protocol understanding. Skip months of custom integration with **built-in FHIR support**, **real-time EHR connectivity**, and **production-ready deployment** - all in Python. +HealthChain is an open-source SDK for production-ready healthcare AI. Skip months of custom integration work with **built-in FHIR support**, **real-time EHR connectivity**, and **deployment tooling for healthcare AI/ML systems** — all in Python.
@@ -34,8 +34,17 @@ HealthChain is an open-source developer framework to build healthcare AI applica ```bash pip install healthchain + +# Scaffold a new project +healthchain new my-app +cd my-app + +# Run locally +healthchain serve ``` +See the [CLI reference](https://dotimplement.github.io/HealthChain/cli/) for all commands. + ## Core Features HealthChain is the **quickest way for AI/ML engineers to integrate their models with real healthcare systems**. @@ -232,7 +241,7 @@ client.save_results("./output/") - [ ] 🔍 Data provenance and observability - [ ] 🔒 Production security and compliance (Authentication, audit logging, HIPAA) - [ ] 🔄 HL7v2 parsing, FHIR profile conversion and OMOP mapping support -- [ ] 🚀 Enhanced deployment support (Docker, Kubernetes, telemetry) +- [ ] 🚀 Kubernetes and telemetry support - [ ] 📊 Model performance monitoring with MLFlow integration - [ ] 🤖 MCP server integration diff --git a/docs/cli.md b/docs/cli.md new file mode 100644 index 00000000..de36fa8f --- /dev/null +++ b/docs/cli.md @@ -0,0 +1,94 @@ +# CLI Reference + +HealthChain ships with a CLI to help you scaffold, run, and customize projects. + +```bash +healthchain --help +``` + +--- + +## `healthchain new` + +Scaffold a new project directory with everything you need to get started. + +```bash +healthchain new my-app +``` + +Creates: + +``` +my-app/ +├── app.py # your application entry point +├── .env.example # FHIR credential template — copy to .env and fill in +├── requirements.txt # add extra dependencies here +├── Dockerfile +└── .dockerignore +``` + +--- + +## `healthchain serve` + +Start your app locally with uvicorn. + +```bash +healthchain serve # defaults to app:app on port 8000 +healthchain serve app:app +healthchain serve app:app --port 8080 +healthchain serve app:app --host 127.0.0.1 --port 8080 +``` + +The `app_module` argument is the Python import path to your FastAPI app instance — `:`. If your app is defined as `app = HealthChainAPI()` in `app.py`, the default `app:app` works as-is. + +To run in Docker instead: + +```bash +docker build -t my-app . +docker run -p 8000:8000 --env-file .env my-app +``` + +--- + +## `healthchain eject-templates` + +Copy the built-in interop templates into your project so you can customize them. + +```bash +healthchain eject-templates ./my_configs +``` + +Only needed if you're using the [InteropEngine](reference/interop/interop.md) and want to customize FHIR↔CDA conversion beyond the defaults. After ejecting: + +```python +from healthchain.interop import create_interop + +engine = create_interop(config_dir="./my_configs") +``` + +See [Interoperability](reference/interop/interop.md) for details. + +--- + +## Typical workflow + +```bash +# 1. Scaffold a new project +healthchain new my-cds-service +cd my-cds-service + +# 2. Build your app in app.py +# See https://dotimplement.github.io/HealthChain/cookbook/ for examples + +# 3. Set credentials +cp .env.example .env +# edit .env with your FHIR_BASE_URL, CLIENT_ID, CLIENT_SECRET + +# 4. Run locally +healthchain serve + +# 5. Ship it +docker build -t my-cds-service . +docker run -p 8000:8000 --env-file .env my-cds-service +``` diff --git a/docs/index.md b/docs/index.md index da431181..65b5505d 100644 --- a/docs/index.md +++ b/docs/index.md @@ -53,7 +53,7 @@ HealthChain is an open-source Python toolkit that streamlines productionizing he ## Getting Started with Healthcare AI -HealthChain provides the missing middleware layer between healthcare systems and modern AI/ML development. Whether you're building clinical decision support tools, processing medical documents, or creating multi-system integrations, these docs will guide you through: +HealthChain is production-ready healthcare AI infrastructure: built-in FHIR support, real-time EHR connectivity, and deployment tooling for healthcare AI/ML systems. Skip months of custom integration work. Whether you're building clinical decision support tools, processing medical documents, or creating multi-system integrations, these docs will guide you through: - **🔧 Core concepts** - Understand FHIR resources, pipelines, and gateway patterns - **📚 Real examples** - Step-by-step tutorials for common healthcare AI use cases diff --git a/docs/reference/interop/configuration.md b/docs/reference/interop/configuration.md index 5eb66304..e4548203 100644 --- a/docs/reference/interop/configuration.md +++ b/docs/reference/interop/configuration.md @@ -27,8 +27,8 @@ engine = create_interop(config_dir="/path/to/custom/configs") To create editable configuration templates: ```bash -# Create customizable config templates -healthchain init-configs ./my_configs +# Eject built-in templates for customization +healthchain eject-templates ./my_configs # Then use them in your code engine = create_interop(config_dir="./my_configs") diff --git a/docs/reference/interop/engine.md b/docs/reference/interop/engine.md index 903dabe5..67d79509 100644 --- a/docs/reference/interop/engine.md +++ b/docs/reference/interop/engine.md @@ -43,10 +43,10 @@ engine = create_interop(validation_level="warn", environment="production") > **💡 Tip:** -> To create editable configuration templates, run: +> To eject the built-in templates for customization, run: > > ```bash -> healthchain init-configs ./my_configs +> healthchain eject-templates ./my_configs > ``` > This will create a `my_configs` directory with editable default configuration templates. diff --git a/docs/reference/interop/experimental.md b/docs/reference/interop/experimental.md index ba8793db..cc757e69 100644 --- a/docs/reference/interop/experimental.md +++ b/docs/reference/interop/experimental.md @@ -28,7 +28,7 @@ This page tracks templates that are under development or have known issues. Use 1. Copy experimental files to your custom config: ```bash - # After running: healthchain init-configs my_configs + # After running: healthchain eject-templates my_configs cp dev-templates/allergies/allergies.yaml my_configs/interop/cda/sections/ cp dev-templates/allergies/allergy_*.liquid my_configs/templates/cda_fhir/ cp dev-templates/allergies/allergy_*.liquid my_configs/templates/fhir_cda/ diff --git a/docs/reference/interop/interop.md b/docs/reference/interop/interop.md index 1c073e38..29918f8f 100644 --- a/docs/reference/interop/interop.md +++ b/docs/reference/interop/interop.md @@ -65,11 +65,11 @@ cda_document = engine.from_fhir(fhir_resources, dest_format="cda") ``` ### Custom Configs -The default templates that come with the package are limited to problems, medications, and notes and are meant for basic testing and prototyping. Use the `healthchain init-configs` command to create editable configuration templates: +The default templates that come with the package are limited to problems, medications, and notes and are meant for basic testing and prototyping. Use the `healthchain eject-templates` command to create editable configuration templates: ```bash -# Create editable configuration templates -healthchain init-configs ./my_configs +# Eject built-in templates for customization +healthchain eject-templates ./my_configs ``` Then use the `config_dir` parameter to specify the path to your custom configs: diff --git a/healthchain/cli.py b/healthchain/cli.py index bf4577c6..f720367e 100644 --- a/healthchain/cli.py +++ b/healthchain/cli.py @@ -1,64 +1,219 @@ import argparse import subprocess +import sys +from pathlib import Path +_DOCKERFILE = """\ +# HealthChain application Dockerfile +# +# Usage: +# Build: docker build -t my-healthcare-app . +# Run: docker run -p 8000:8000 --env-file .env my-healthcare-app +# +# Required environment variables (set in .env or pass via -e): +# APP_MODULE Python module path to your app, e.g. "myapp:app" (default: "app:app") +# +# FHIR source credentials (if connecting to Epic/Cerner): +# FHIR_BASE_URL, CLIENT_ID, CLIENT_SECRET or CLIENT_SECRET_PATH +# +# See docs: https://dotimplement.github.io/HealthChain/reference/gateway/gateway/ -def run_file(filename): - try: - result = subprocess.run(["uv", "run", "python", filename], check=True) - print(result.stdout) - except subprocess.CalledProcessError as e: - print(f"An error occurred while trying to run the file: {e}") +FROM python:3.11-slim + +# Keeps Python from buffering stdout/stderr +ENV PYTHONUNBUFFERED=1 \\ + PYTHONDONTWRITEBYTECODE=1 \\ + APP_MODULE=app:app \\ + PORT=8000 + +WORKDIR /app + +# Install system dependencies needed by lxml and spaCy +RUN apt-get update && apt-get install -y --no-install-recommends \\ + gcc \\ + g++ \\ + && rm -rf /var/lib/apt/lists/* + +# Install healthchain from PyPI +RUN pip install --no-cache-dir healthchain + +# Install any additional dependencies your application needs +# (copy requirements first to leverage Docker layer caching) +COPY requirements.txt* ./ +RUN if [ -f requirements.txt ]; then pip install --no-cache-dir -r requirements.txt; fi + +# Copy application code +COPY . . + +# Run as non-root user +RUN useradd -m appuser && chown -R appuser /app +USER appuser + +EXPOSE $PORT + +CMD uvicorn $APP_MODULE --host 0.0.0.0 --port $PORT +""" + +_DOCKERIGNORE = """\ +.git +.idea +.pytest_cache +.ruff_cache +__pycache__ +*.pyc +*.pyo +*.pyd +.env +.env.* +tests/ +docs/ +notebooks/ +output/ +*.md +!README.md +""" + +_ENV_EXAMPLE = """\ +# FHIR source credentials +FHIR_BASE_URL= +CLIENT_ID= +CLIENT_SECRET= + +# For JWT assertion flow (e.g. Epic SMART on FHIR) +# CLIENT_SECRET_PATH=/path/to/private_key.pem +""" + +_REQUIREMENTS = "healthchain\n" + +_APP_PY = """\ +# Your HealthChain application goes here. +# See https://dotimplement.github.io/HealthChain/ for examples. +""" -def init_configs(target_dir: str): - """Initialize configuration templates for customization.""" +def new_project(name: str): + """Scaffold a new HealthChain project.""" + project_dir = Path(name) + + if project_dir.exists(): + print(f"❌ Directory '{name}' already exists.") + return + + project_dir.mkdir() + (project_dir / "app.py").write_text(_APP_PY) + (project_dir / ".env.example").write_text(_ENV_EXAMPLE) + (project_dir / "requirements.txt").write_text(_REQUIREMENTS) + (project_dir / "Dockerfile").write_text(_DOCKERFILE) + (project_dir / ".dockerignore").write_text(_DOCKERIGNORE) + + print(f"\nCreated project '{name}/'") + print(f" {name}/app.py") + print(f" {name}/.env.example") + print(f" {name}/requirements.txt") + print(f" {name}/Dockerfile") + print("\nNext steps:") + print(f" 1. Build your app in {name}/app.py") + print(" 2. Copy .env.example to .env and fill in your credentials") + print(f" 3. healthchain serve app:app (from inside {name}/)") + print(f" 4. docker build -t {name} {name}/") + print(f" 5. docker run -p 8000:8000 --env-file {name}/.env {name}") + print("\nUsing format conversion? Run: healthchain eject-templates ./configs") + print("See https://dotimplement.github.io/HealthChain/ for examples.") + + +def eject_templates(target_dir: str): + """Eject built-in interop templates for customization.""" try: from healthchain.interop import init_config_templates target_path = init_config_templates(target_dir) - print(f"\n🎉 Success! Configuration templates created at: {target_path}") - print("\n📖 Next steps:") - print(" 1. Customize the configuration files in the created directory") + print(f"\n✅ Templates ejected to: {target_path}") + print("\nNext steps:") + print(" 1. Customize the templates in the created directory") print(" 2. Use them in your code:") print(" from healthchain.interop import create_interop") print(f" engine = create_interop(config_dir='{target_dir}')") - print("\n📚 See documentation for configuration options") + print( + "\nSee https://dotimplement.github.io/HealthChain/reference/interop/ for details." + ) except FileExistsError as e: print(f"❌ Error: {str(e)}") - print("💡 Tip: Choose a different directory name or remove the existing one") + print("💡 Choose a different directory name or remove the existing one.") except Exception as e: - print(f"❌ Error initializing configs: {str(e)}") - print("💡 Tip: Make sure HealthChain is properly installed") + print(f"❌ Error ejecting templates: {str(e)}") + print("💡 Make sure HealthChain is properly installed.") + + +def serve(app_module: str, host: str, port: int): + """Start a HealthChain app with uvicorn.""" + try: + subprocess.run( + [ + sys.executable, + "-m", + "uvicorn", + app_module, + "--host", + host, + "--port", + str(port), + ], + check=True, + ) + except subprocess.CalledProcessError as e: + print(f"❌ Server error: {e}") + except KeyboardInterrupt: + pass def main(): parser = argparse.ArgumentParser(description="HealthChain command-line interface") subparsers = parser.add_subparsers(dest="command", required=True) - # Subparser for the 'run' command - run_parser = subparsers.add_parser("run", help="Run a specified file") - run_parser.add_argument("filename", type=str, help="The filename to run") + # Subparser for the 'new' command + new_parser = subparsers.add_parser("new", help="Scaffold a new HealthChain project") + new_parser.add_argument("name", type=str, help="Project name (creates a directory)") + + # Subparser for the 'serve' command + serve_parser = subparsers.add_parser( + "serve", help="Start a HealthChain app with uvicorn" + ) + serve_parser.add_argument( + "app_module", + type=str, + nargs="?", + default="app:app", + help="Module path to your app (default: app:app)", + ) + serve_parser.add_argument( + "--host", type=str, default="0.0.0.0", help="Host (default: 0.0.0.0)" + ) + serve_parser.add_argument( + "--port", type=int, default=8000, help="Port (default: 8000)" + ) - # Subparser for the 'init-configs' command - init_parser = subparsers.add_parser( - "init-configs", - help="Initialize configuration templates for interop customization", + # Subparser for the 'eject-templates' command + eject_parser = subparsers.add_parser( + "eject-templates", + help="Eject built-in interop templates for customization", ) - init_parser.add_argument( + eject_parser.add_argument( "target_dir", type=str, nargs="?", default="./healthchain_configs", - help="Directory to create configuration templates (default: ./healthchain_configs)", + help="Directory to eject templates into (default: ./healthchain_configs)", ) args = parser.parse_args() - if args.command == "run": - run_file(args.filename) - elif args.command == "init-configs": - init_configs(args.target_dir) + if args.command == "new": + new_project(args.name) + elif args.command == "serve": + serve(args.app_module, args.host, args.port) + elif args.command == "eject-templates": + eject_templates(args.target_dir) if __name__ == "__main__": diff --git a/mkdocs.yml b/mkdocs.yml index e84e4350..77d7c3ae 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -11,6 +11,7 @@ nav: - Getting Started: - Installation: installation.md - Quickstart: quickstart.md + - CLI: cli.md - Licence: distribution.md - Tutorials: - tutorials/index.md diff --git a/tests/test_cli.py b/tests/test_cli.py index 9d513865..d1448e47 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -4,7 +4,7 @@ import subprocess from unittest.mock import patch -from healthchain.cli import init_configs, run_file, main +from healthchain.cli import eject_templates, serve, main @pytest.mark.parametrize( @@ -14,27 +14,27 @@ FileExistsError("Directory already exists"), [ "❌ Error: Directory already exists", - "💡 Tip: Choose a different directory name or remove the existing one", + "💡 Choose a different directory name or remove the existing one", ], ), ( Exception("Something went wrong"), [ - "❌ Error initializing configs: Something went wrong", - "💡 Tip: Make sure HealthChain is properly installed", + "❌ Error ejecting templates: Something went wrong", + "💡 Make sure HealthChain is properly installed", ], ), ], ) @patch("healthchain.interop.init_config_templates") -def test_init_configs_error_handling_provides_helpful_guidance( +def test_eject_templates_error_handling_provides_helpful_guidance( mock_init_templates, error, expected_messages ): - """init_configs provides helpful error messages and guidance when template creation fails.""" + """eject_templates provides helpful error messages and guidance when template creation fails.""" mock_init_templates.side_effect = error with patch("builtins.print") as mock_print: - init_configs("./test_configs") + eject_templates("./test_configs") # Verify helpful error messages are displayed for expected_msg in expected_messages: @@ -42,50 +42,59 @@ def test_init_configs_error_handling_provides_helpful_guidance( @patch("healthchain.interop.init_config_templates") -def test_init_configs_success_provides_usage_instructions(mock_init_templates): - """init_configs provides clear usage instructions when successful.""" +def test_eject_templates_success_provides_usage_instructions(mock_init_templates): + """eject_templates provides clear usage instructions when successful.""" target_dir = "./test_configs" mock_init_templates.return_value = target_dir with patch("builtins.print") as mock_print: - init_configs(target_dir) + eject_templates(target_dir) # Verify success message and usage instructions are provided print_output = " ".join(str(call) for call in mock_print.call_args_list) - assert "🎉 Success!" in print_output + assert "✅ Templates ejected to:" in print_output assert "create_interop(config_dir=" in print_output - assert "📖 Next steps:" in print_output + assert "Next steps:" in print_output @patch("subprocess.run") -def test_run_file_handles_execution_errors_gracefully(mock_run): - """run_file provides clear error message when script execution fails.""" - mock_run.side_effect = subprocess.CalledProcessError(1, "uv") +def test_serve_handles_execution_errors_gracefully(mock_run): + """serve provides clear error message when server fails to start.""" + mock_run.side_effect = subprocess.CalledProcessError(1, "uvicorn") with patch("builtins.print") as mock_print: - run_file("failing_script.py") + serve("app:app", "0.0.0.0", 8000) # Verify error message is informative error_message = mock_print.call_args[0][0] - assert "An error occurred while trying to run the file:" in error_message + assert "❌ Server error:" in error_message @pytest.mark.parametrize( "args,expected_call", [ - (["healthchain", "run", "test.py"], ("run_file", "test.py")), - (["healthchain", "init-configs", "my_configs"], ("init_configs", "my_configs")), - (["healthchain", "init-configs"], ("init_configs", "./healthchain_configs")), + ( + ["healthchain", "serve", "test.py:app"], + ("serve", ("test.py:app", "0.0.0.0", 8000)), + ), + ( + ["healthchain", "eject-templates", "my_configs"], + ("eject_templates", ("my_configs",)), + ), + ( + ["healthchain", "eject-templates"], + ("eject_templates", ("./healthchain_configs",)), + ), ], ) def test_main_routes_commands_correctly(args, expected_call): """Main function correctly routes CLI commands to appropriate handlers.""" - function_name, expected_arg = expected_call + function_name, expected_args = expected_call with patch(f"healthchain.cli.{function_name}") as mock_function: with patch("sys.argv", args): main() - mock_function.assert_called_once_with(expected_arg) + mock_function.assert_called_once_with(*expected_args) def test_main_requires_command_argument():