Architecture and Python use

ACMAD Upload Helper combines a staged ETL pipeline with a ports-and-adapters architecture. Data moves from extraction, through transformation, to loading; dependencies point towards domain concepts and abstract ports rather than towards Excel, HTTP, or a particular user interface.

        classDiagram
    direction LR

    class UserInterfaces {
        <<component>>
        CLI
        Desktop GUI
    }
    class Application {
        <<component>>
        EtlPipeline
        Loaders
        Package orchestration
    }
    class Sources {
        <<component>>
        Filesystem discovery
        Excel extraction
        Source models
    }
    class Transformers {
        <<component>>
        Schema validation
        Canonical mapping
    }
    class Domain {
        <<component>>
        Canonical records
        Provenance
        Structured issues
    }
    class Ports {
        <<interfaces>>
        Extractor
        Transformer
        Loader
        Repositories
    }
    class RestAdapters {
        <<component>>
        ACMAD repositories
        OIDC and JSON API client
    }
    class ExternalSystems {
        <<external>>
        Package files
        ACMAD REST API
        Identity provider
    }

    UserInterfaces --> Application : invokes
    UserInterfaces --> Sources : assembles
    Application --> Ports : depends on
    Application --> Domain : returns results and issues
    Sources ..|> Ports : implements Extractor
    Sources --> Domain : records provenance
    Sources --> ExternalSystems : reads package files
    Transformers ..|> Ports : implements Transformer
    Transformers --> Domain : creates canonical records
    RestAdapters ..|> Ports : implements repositories
    RestAdapters --> Domain : consumes canonical records
    RestAdapters --> ExternalSystems : HTTP and OIDC
    

Components, interfaces, and dependency direction

ETL stages

Extraction

The acmad_uploader.sources package is the boundary between package files and the rest of the application. It discovers relevant workbooks and motion-capture artefacts, reads vertical Excel forms, and produces source models that retain values together with file, worksheet, cell, and field locations. Extraction describes what was present; it does not decide how an API record should look or make network requests.

Each source class satisfies the generic Extractor protocol. Consequently, the pipeline needs only an extract(source) operation and does not need to know whether values came from Excel, a different file format, or a test double.

Transformation

The acmad_uploader.transformers package converts extracted source models into canonical records from acmad_uploader.domain. This is where template-version checks, required-field validation, controlled-vocabulary mappings, numeric coercion, cross-workbook joins, and site consistency checks belong. Problems are returned as structured issues with the provenance captured during extraction.

Transformers satisfy the Transformer protocol. They depend on source and domain values, but not on HTTP clients or destination-specific response shapes. A package can therefore be fully validated without authentication or access to the ACMAD service.

Loading

Loaders in acmad_uploader.application apply canonical records to a destination. They select the appropriate repository operation, aggregate created, updated, unchanged, or skipped outcomes, and translate expected repository failures into pipeline issues. The package-wide orchestration also runs record types in dependency order—for example, patient before gait assessment and motion-capture data before biomechanics artefacts.

Loaders depend on repository protocols from acmad_uploader.ports, not on the concrete REST implementation. The classes in acmad_uploader.adapters.api implement those protocols. They own OIDC authentication, HTTP requests, JSON response validation, natural-key lookup, destination payloads, and upsert behaviour. These details remain outside the domain and application layers.

Cross-cutting components

Domain

The acmad_uploader.domain package contains the canonical record types, controlled vocabularies, source provenance, site overrides, and structured issues shared by the stages. Domain objects describe ACMAD concepts rather than spreadsheets or REST resources. They do not import Excel or HTTP libraries, which keeps the central data model usable by every interface and adapter.

Application orchestration

EtlPipeline coordinates the extract, transform, and optional load stages. It stops after a failed stage, keeps validation as the safe default, and returns each stage’s result for inspection. Package orchestration composes the record-specific pipelines and applies their dependency order. The command-line and desktop interfaces are thin composition roots: they choose concrete sources, transformers, loaders, and adapters, then present progress and issues to the user.

Ports and adapters

The protocols in acmad_uploader.ports define the capabilities that the application requires. Extractor, transformer, and loader protocols describe the generic stage boundaries; record-specific repository protocols describe destination operations. The REST repositories are outbound adapters, while the command-line and desktop applications are inbound adapters that invoke the use cases.

Loose coupling

The architecture is loosely coupled because components share small, typed contracts instead of concrete infrastructure:

  • A source extractor can be replaced without changing transformation or loading, provided it returns the expected source model.

  • A repository can be replaced with an in-memory implementation, another API, or a test double without changing its loader.

  • Canonical domain records remain independent of workbook layouts and REST payloads, so change at either boundary is localised.

  • Validation and upload use the same extraction and transformation path, preventing a second set of validation rules from drifting out of sync.

  • Record types are vertical slices. Adding one normally means adding its own source, domain model, transformer, port, loader, and adapter rather than modifying the internals of unrelated slices.

The result is not zero coupling: adjacent stages deliberately agree on typed values and protocols. It is controlled coupling, with infrastructure details kept at the edges and business meaning kept in the domain. This makes each stage independently testable and limits how far a workbook, API, or interface change can propagate.

Running an ETL pipeline

Components can be composed without the command-line interface. This example validates patient records locally; no repository is required because loader is omitted:

from pathlib import Path

from acmad_uploader.application.pipeline import EtlPipeline
from acmad_uploader.sources.patient import PatientWorkbookExtractor
from acmad_uploader.transformers.patient import PatientTransformer

pipeline = EtlPipeline(
    extractor=PatientWorkbookExtractor(),
    transformer=PatientTransformer(),
)
result = pipeline.run(Path("/path/to/package"))

if result.succeeded:
    assert result.transformation is not None
    for patient in result.transformation.value or ():
        print(patient.site_patient_id)
else:
    for issue in result.issues:
        print(issue.display())

To upload, supply the corresponding loader configured with an implementation of its repository protocol. The REST API classes in acmad_uploader.adapters.api are the built-in implementations.

Results and issues

Each stage returns StageResult rather than using exceptions for expected data-quality failures. A PipelineResult retains extraction, transformation, and loading outcomes, and combines their structured PipelineIssue values. Source locations carry file, worksheet, cell, and field context where available.