-
Notifications
You must be signed in to change notification settings - Fork 7
Issue #906: Phase 1a: Stand up LDE microservice #920
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
7908edc
5487ab6
3b9214d
e3600be
803fb52
5d30490
a9bdeb6
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| from lif.learner_data_export_api import core | ||
|
|
||
| __all__ = ["core"] | ||
| Original file line number | Diff line number | Diff line change | ||
|---|---|---|---|---|
| @@ -0,0 +1,41 @@ | ||||
| from fastapi import FastAPI | ||||
| from fastapi.middleware.cors import CORSMiddleware | ||||
| from lif.learner_data_export_api import learner_data_export_endpoints | ||||
| from lif.logging import get_logger | ||||
| from lif.mdr_auth.core import AuthMiddleware | ||||
| from lif.mdr_utils.config import get_settings | ||||
|
|
||||
| logger = get_logger(__name__) | ||||
| settings = get_settings() | ||||
| app = FastAPI(title="LIF Learner Data Export API", description="API for the LIF Learner Data Export", version="1.0.0") | ||||
|
|
||||
| app.add_middleware(AuthMiddleware) | ||||
| # Configure CORS middleware | ||||
| cors_origins = [origin.strip() for origin in settings.cors_allow_origins.split(",") if origin.strip()] | ||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. These reads — Either (a) add the fields to
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Those settings exist -
I plan to hold off adjusting them until we have time to generalize the auth flows. |
||||
| cors_methods = [method.strip() for method in settings.cors_allow_methods.split(",") if method.strip()] | ||||
| cors_headers = ( | ||||
| [header.strip() for header in settings.cors_allow_headers.split(",") if header.strip()] | ||||
| if settings.cors_allow_headers != "*" | ||||
| else ["*"] | ||||
| ) | ||||
| app.add_middleware( | ||||
| CORSMiddleware, | ||||
| allow_origins=cors_origins, | ||||
| allow_credentials=settings.cors_allow_credentials, | ||||
| allow_methods=cors_methods, | ||||
| allow_headers=cors_headers, | ||||
| ) | ||||
|
|
||||
|
|
||||
| # --- API Endpoints --- | ||||
|
|
||||
|
|
||||
| @app.get("/health") | ||||
| async def health_check(): | ||||
| """ | ||||
| Health check endpoint to verify the API is running | ||||
| """ | ||||
| return {"status": "ok"} | ||||
|
|
||||
|
|
||||
| app.include_router(learner_data_export_endpoints.router, prefix="") | ||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,50 @@ | ||
| from typing import Any, Dict | ||
|
|
||
| from fastapi import APIRouter, Request | ||
| from lif.datatypes.core import TargetTransformationDataModelDTO, TargetTransformationDataModelsDTO | ||
| from lif.mdr_utils.logger_config import get_logger | ||
|
|
||
| router = APIRouter() | ||
| logger = get_logger(__name__) | ||
|
|
||
|
|
||
| @router.get("/exports", response_model=Dict[str, Any]) | ||
| async def get_data(request: Request): | ||
| """Endpoint to export learner data in a specified format. | ||
|
|
||
| The response model is intentionally generic since it will depend | ||
| on the requested data format and transformation. | ||
| """ | ||
|
|
||
| # TODO: Build this out. | ||
| logger.info("Received request for learner data export as %s", request.state.principal) | ||
|
|
||
| return {"total": "data"} | ||
|
|
||
|
|
||
| @router.get("/available-data-formats", response_model=TargetTransformationDataModelsDTO) | ||
| async def get_available_data_formats(request: Request): | ||
| # TODO: Build this out. | ||
| logger.info("Received request for available data formats as %s", request.state.principal) | ||
|
|
||
| data_formats = TargetTransformationDataModelsDTO( | ||
| metadata={"total": 3}, | ||
| DataFormats=[ | ||
| TargetTransformationDataModelDTO( | ||
| name="OpenBadges 3.0", | ||
| version="1.0.3", | ||
| contributorOrganization="OB", | ||
| TransformationVersions=["1.0.0", "1.1.0"], | ||
| ), | ||
| TargetTransformationDataModelDTO( | ||
| name="CEDS", version="2.0.0", contributorOrganization="CEDS Org", TransformationVersions=["2.0.0"] | ||
| ), | ||
| TargetTransformationDataModelDTO( | ||
| name="ExampleDataSource", | ||
| version="1.0.1", | ||
| contributorOrganization="Community", | ||
| TransformationVersions=["1.3.0"], | ||
| ), | ||
| ], | ||
| ) | ||
| return data_formats |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| .venv |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| dist | ||
| requirements.txt | ||
| uv.lock |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| FROM python:3.13-slim | ||
|
|
||
| ARG wheel=lif_learner_data_export_api-0.1.0-py3-none-any.whl | ||
|
|
||
| ARG deps=requirements.txt | ||
|
|
||
| RUN python -m pip install --upgrade pip | ||
|
|
||
| WORKDIR /code | ||
|
|
||
| COPY ./dist/$wheel /code/$wheel | ||
| COPY ./$deps /code/$deps | ||
|
|
||
| # This is adjusted to install third-party dependencies separately (the --no-deps) and install from requirements | ||
| RUN pip install --no-cache-dir --upgrade --no-deps /code/$wheel | ||
| RUN pip install -r /code/$deps | ||
|
|
||
| CMD ["uvicorn", "lif.learner_data_export_api.core:app", "--host", "0.0.0.0", "--port", "8013"] |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,49 @@ | ||
| # Stage 1: Builder stage | ||
| FROM python:3.13-slim AS builder | ||
|
|
||
| # Build arguments for project configuration | ||
| ARG PROJECT_NAME=lif_learner_data_export_api | ||
|
|
||
| # Install uv | ||
| RUN pip install --no-cache-dir uv | ||
|
|
||
| # Set UV environment variables for optimal caching and behavior | ||
| ENV UV_COMPILE_BYTECODE=1 \ | ||
| UV_LINK_MODE=copy | ||
|
|
||
| # Set working directory in the container | ||
| WORKDIR /code | ||
|
|
||
| # Copy the entire monorepo structure needed for polylith | ||
| COPY projects/${PROJECT_NAME}/ /code/projects/${PROJECT_NAME}/ | ||
| COPY bases/ /code/bases/ | ||
| COPY components/ /code/components/ | ||
|
|
||
| # Change to the specific project directory | ||
| WORKDIR /code/projects/${PROJECT_NAME} | ||
|
|
||
| # Sync dependencies and build the project wheel | ||
| RUN --mount=type=cache,target=/root/.cache/uv \ | ||
| uv sync --no-dev && \ | ||
| uv build --wheel | ||
|
cbeach47 marked this conversation as resolved.
|
||
|
|
||
| # Stage 2: Final runtime image | ||
| FROM python:3.13-slim AS runtime | ||
|
|
||
| # Inherit build arguments in runtime stage | ||
| ARG PROJECT_NAME=lif_learner_data_export_api | ||
|
|
||
| # Install uv in runtime for faster package installation | ||
| RUN pip install --no-cache-dir uv | ||
|
|
||
| # Copy the built wheel from the builder stage | ||
| COPY --from=builder /code/projects/${PROJECT_NAME}/dist/*.whl /tmp/ | ||
|
|
||
| # Install the wheel in the final image | ||
| RUN uv pip install --system /tmp/*.whl && rm /tmp/*.whl | ||
|
|
||
| # Set working directory | ||
| WORKDIR /code | ||
|
|
||
| # Use uvicorn to run the FastAPI application | ||
| CMD ["uvicorn", "lif.learner_data_export_api.core:app", "--host", "0.0.0.0", "--port", "8013"] | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,75 @@ | ||
| # Learner Data Export API | ||
|
|
||
| The **Learner Data Export API** exports learner data in formats other than the Org LIF data model. | ||
|
|
||
| Offers two abilities: | ||
|
|
||
| - An endpoint to enumerate the available "Org LIF to other data models" that have a transformation group setup in **MDR**. | ||
| - An endpoint to consume a learner ID, dataModel, and transformation version. The endpoint will return data, for that learner, in that data model format | ||
| - Leverages **LIF Query Planner**, **MDR API**, and the **Translator**. | ||
|
|
||
|
|
||
| # Example usage | ||
|
|
||
| ## Build the project | ||
| Navigate to this folder (where the `pyproject.toml` file is) | ||
|
|
||
| 1. Export the dependencies (when using uv workspaces and having no project-specific lock-file): | ||
| ``` shell | ||
| uv export --no-emit-project --output-file requirements.txt | ||
| ``` | ||
|
|
||
| 2. Build a wheel: | ||
| ``` shell | ||
| uv build --out-dir ./dist | ||
| ``` | ||
|
|
||
| ## Build a docker image from root | ||
|
|
||
| ``` shell | ||
| ./build-docker.sh | ||
| ``` | ||
|
|
||
|
|
||
| ## Run the image | ||
|
|
||
| ``` shell | ||
| docker run --rm --name learner_data_export_api -p 8013:8013 \ | ||
| lif-learner-data-export-api | ||
| ``` | ||
|
|
||
| The OpenAPI specification of this FastAPI app can now be accessed at http://localhost:8013/docs# | ||
|
|
||
|
|
||
| ## API Calls for Manual Testing | ||
|
|
||
| You can manually test the API endpoints using `curl` and an environment variable for your access token. | ||
|
|
||
| *Note:* For now, the access token is a static value from the configuration. | ||
|
|
||
| ### Health Check | ||
| ```shell | ||
| curl -X GET http://localhost:8013/health-check \ | ||
| -H "Content-Type: application/json" | ||
| ``` | ||
|
|
||
| ### Test Auth | ||
| ```shell | ||
| curl -X GET http://localhost:8013/test/auth-info \ | ||
| -H "Content-Type: application/json" \ | ||
| -H "X-API-Key: changeme6" | ||
| ``` | ||
|
|
||
| ### Get Available Data Formats | ||
| ```shell | ||
| curl -X GET http://localhost:8013/available-data-formats \ | ||
| -H "Content-Type: application/json" \ | ||
| -H "X-API-Key: changeme6" | ||
| ``` | ||
|
|
||
| ### Export Learner Data | ||
| ```shell | ||
| curl -X GET http://localhost:8013/export \ | ||
| -H "Content-Type: application/json" \ | ||
| -H "X-API-Key: changeme6" | ||
| ``` |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| #!/bin/bash | ||
|
|
||
| set -e | ||
|
|
||
| # Configuration | ||
| IMAGE_TAG=${1:-lif-learner-data-export-api} | ||
| PROJ_DIR=${2:-lif_learner_data_export_api} | ||
|
|
||
| # Change to repo root | ||
| cd "$(dirname "$0")/../.." | ||
|
|
||
| echo "Building ${IMAGE_TAG} from repo root..." | ||
| docker build -f projects/${PROJ_DIR}/Dockerfile2 -t ${IMAGE_TAG} . | ||
|
|
||
| echo "Build complete!" |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| #!/bin/bash | ||
| uv export --no-emit-project --output-file requirements.txt | ||
| uv build --out-dir ./dist |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| [build-system] | ||
| requires = ["hatchling", "hatch-polylith-bricks"] | ||
| build-backend = "hatchling.build" | ||
|
|
||
| [project] | ||
| name = "lif_learner_data_export_api" | ||
| version = "0.1.0" | ||
|
|
||
| requires-python = ">=3.13,<3.14" | ||
|
|
||
| dependencies = [ | ||
| "fastapi~=0.115", | ||
| "pyjwt~=2.10", | ||
| "pydantic-settings~=2.1", | ||
| "uvicorn~=0.34", | ||
| ] | ||
|
|
||
| # This section is needed for the Hatchling build backend, to activate the polylith build hook. | ||
| [tool.hatch.build.hooks.polylith-bricks] | ||
|
|
||
| # This section is needed for building | ||
| [tool.hatch.build.targets.wheel] | ||
| packages = ["lif"] | ||
|
|
||
| [tool.polylith.bricks] | ||
| "../../bases/lif/learner_data_export_api" = "lif/learner_data_export_api" | ||
| "../../components/lif/logging" = "lif/logging" | ||
| "../../components/lif/datatypes" = "lif/datatypes" | ||
| "../../components/lif/mdr_utils" = "lif/mdr_utils" | ||
| "../../components/lif/mdr_auth" = "lif/mdr_auth" | ||
| "../../components/lif/tenant_routing" = "lif/tenant_routing" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Other bases (e.g.
bases/lif/translator_restapi/__init__.py) are empty. The eagerfrom … import corehere triggers FastAPI app instantiation as a side effect of importing the package — can cause subtle test-order issues if anything imports the package without wanting the app initialized. Recommend dropping the body.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
All bases sub-folders follow this pattern of:
Do you feel all the `base/lif/.../init.py files should be empty?