Connector Framework - Design Specification¶
| Document ID | AH-DS-04 |
| Package | mineproductivity.connectors |
| Status | Draft - Design Complete, Pending Implementation |
| Version | 1.0.0 |
| Conforms to | Master Architecture Handbook v1.0; Reference Implementation Blueprint v1.0; Developer & Cookbook Guide Parts I-III; Learning & Benchmark Suite v1.0 |
| Builds on | Repository Skeleton v0.1.0 (LOCKED); Core Foundation Library v0.2.0 (LOCKED) |
| Author | Chief Software Architect, MineProductivity |
| Classification | Public - Open Source Design Documentation |
Document Control¶
Design specification only - no implementation. Concrete OEM adapters (MineStar, DISPATCH, Wenco, Modular Mining, Hexagon) are specified here as connector shapes and mapping responsibilities, not as working vendor integrations - no vendor SDK code exists in this repository or is implied by this document. Per Cookbook Part I, Ch. 7, real OEM connectors ship as separate, independently-versioned plugin packages (e.g. a hypothetical mineproductivity-minestar), never inside mineproductivity.connectors itself.
1. Purpose¶
The Connector Framework is how MineProductivity meets the real world. It defines the single, small contract every data source - a fleet-management system, a CSV export, a REST API, a Kafka topic - must satisfy to feed the platform, and it is the only place in the codebase permitted to know that a specific vendor or file format exists. This is the concrete implementation of the root README's forbidden-imports rule and the Cookbook's Vendor-Neutrality principle: "Connectors are the only place in the codebase allowed to know about a specific OEM."
2. Scope¶
In scope:
- The
FMSConnectorabstract base contract. - Reference connector shapes for CSV, Excel, REST, GraphQL, Kafka, MQTT sources.
- The OEM adapter shape for MineStar, DISPATCH, Wenco, Modular Mining, and Hexagon - as a mapping/translation responsibility, not vendor SDK bindings.
- Streaming, batch, and incremental ingestion modes.
- Validation and normalization at the connector boundary (the anti-corruption layer).
- Field/code mapping (vendor dialect → canonical ontology/event model).
- Authentication, retry, and backoff contracts for network-based connectors.
- Connector monitoring/health surface.
- Connector registration and discovery (specialization of the Registry Framework).
- Connector contract testing strategy.
Out of scope (see §4).
3. Responsibilities¶
- Define one abstract contract (
FMSConnector) that every source - file, database, stream, REST API, vendor SDK - implements identically. - Quarantine vendor dialects. A connector's entire job is "read a source, yield canonical events" (Cookbook Part I, Ch. 7); nothing downstream ever sees a vendor's field names, status codes, or reason codes.
- Perform semantic, not just structural, translation - recomputing from underlying time buckets when a vendor's percentage-based metric uses a different denominator than the platform's canonical time model, rather than copying a number that looks similar but means something different (Developer & Cookbook Guide Part III, "Compatibility with external systems").
- Own authentication, retry, and backoff for network-connected sources, so this concern is solved once, consistently, rather than once per connector.
- Ship connectors as discoverable plugins (Registry Framework specialization), never as core-code special cases.
4. Out of Scope¶
- Event definitions (
CycleEvent,DelayEvent, ...) - owned byevents; connectors produce instances of these types, never redefine their shape. - Ontology entity definitions - owned by
ontology; connectors resolve against entities (§15 of the Ontology Framework spec's reference/embedding pattern), never define them. - The
EventStore/EventBusimplementations connectors write into - owned byevents/io. - KPI computation - connectors never import
kpis(§7). - Actual vendor SDK code or credentials. This repository never bundles a proprietary OEM SDK; a real MineStar/DISPATCH/Wenco/Hexagon connector is an independent plugin package with its own dependency on the vendor's SDK, isolated entirely from
mineproductivity.connectors' own dependency graph.
5. Architecture¶
core → ontology → events → connectors → kpis → ...
↑ │
└───────────┘
connectors also depends on io, config
connectors sits after events and ontology in the dependency stack - it produces events and resolves ontology references, but is itself a plumbing package (Cookbook Part I, Ch. 3's three-band model: "domain heart," "plumbing," "intelligence/interfaces" - connectors is plumbing).
flowchart LR
subgraph Vendor Systems
MS[MineStar]
DP[DISPATCH]
WC[Wenco]
HX[Hexagon]
CSVF[CSV/Excel export]
RESTAPI[REST API]
KFK[Kafka/MQTT stream]
end
subgraph connectors["connectors package - THIS SPEC"]
FMS["FMSConnector (ABC)"]
NORM[Normalizer / Mapper]
AUTH[AuthProvider]
RETRY[RetryPolicy]
MON[ConnectorHealth]
end
subgraph ontology["ontology (LOCKED SHAPE, spec 02)"]
ONT[Entity resolution]
end
subgraph events["events (spec 01)"]
VAL[EventValidator]
ES[(EventStore)]
end
MS & DP & WC & HX --> FMS
CSVF & RESTAPI & KFK --> FMS
FMS --> NORM
NORM --> ONT
NORM -->|canonical BaseEvent instances| VAL
VAL --> ES
FMS --> AUTH
FMS --> RETRY
FMS --> MON
6. Package Structure¶
src/mineproductivity/connectors/
├── __init__.py # public API: get_connector, CONNECTORS, register
├── base.py # FMSConnector (ABC)
├── normalization.py # Normalizer, FieldMapper, ReasonCodeMap
├── auth.py # AuthProvider (ABC), token/credential shapes
├── retry.py # RetryPolicy, BackoffStrategy
├── health.py # ConnectorHealth, HealthCheck
├── file/ # File-based reference connectors
│ ├── __init__.py
│ ├── csv_connector.py # CSVConnector
│ └── excel_connector.py # ExcelConnector
├── network/ # Network-based reference connectors
│ ├── __init__.py
│ ├── rest_connector.py # RestConnector
│ └── graphql_connector.py # GraphQLConnector
├── streaming/ # Streaming reference connectors
│ ├── __init__.py
│ ├── kafka_connector.py # KafkaConnector
│ └── mqtt_connector.py # MqttConnector
├── oem/ # OEM adapter SHAPES (no vendor SDK code)
│ ├── __init__.py
│ ├── minestar_shape.py # MineStarConnector shape/mapping contract
│ ├── dispatch_shape.py # DispatchConnector shape/mapping contract
│ ├── wenco_shape.py # WencoConnector shape/mapping contract
│ ├── modular_shape.py # ModularConnector shape/mapping contract
│ └── hexagon_shape.py # HexagonConnector shape/mapping contract
├── contract_tests.py # shared FMSConnector contract test suite
├── exceptions.py
└── README.md
The oem/ modules contain only the mapping contract and reason-code-map shape each vendor plugin must fulfill (e.g. MineStarReasonCodeMap's expected keys) - never a working MineStarClient. A real OEM plugin lives entirely outside this repository, as its own installable package, per Cookbook Part I Ch. 7's mineproductivity-minestar example.
7. Dependency Direction¶
connectorsdepends on:core,ontology,events, and the cross-cuttingio,config,registrypackages.connectorsis depended on by: nothing in the domain-heart or intelligence layers directly - the platform depends onconnectorsonly through theeventsit produces, never through a direct import.digital_twinandkpisnever importconnectors.- Forbidden (the single most load-bearing rule in this document):
connectorsMUST NOT importkpis,analytics,optimization,simulation,decision,digital_twin, oragents. This is what the Cookbook calls out explicitly: "connectors/ may not import kpis/ - the dependency-direction check forbids it - so [vendor-specific] parsing belongs in a connector, which normalises vendor data into canonical events before any KPI sees it."
8. Public API¶
from mineproductivity.connectors import (
FMSConnector, get_connector, register_connector, CONNECTORS,
Normalizer, FieldMapper, ReasonCodeMap,
AuthProvider, Credentials, RetryPolicy, BackoffStrategy,
ConnectorHealth, HealthStatus,
CSVConnector, ExcelConnector, RestConnector, GraphQLConnector,
KafkaConnector, MqttConnector,
IngestionMode,
ConnectorError, MappingError, AuthenticationError,
SourceUnavailableError, ContractViolationError,
)
9. Internal API¶
connectors.oem._reason_code_maps- internal registry of vendor-name →ReasonCodeMapshape, used only by contract tests validating a plugin's mapping completeness.connectors.contract_tests.run_fms_contract_suite(connector_factory)- the shared, parameterizable pytest suite every connector (built-in or plugin) is expected to run against (§29).
10. Object Model¶
10.1 FMSConnector - the one contract¶
class FMSConnector(ABC):
"""Adapter from a fleet-management (or any other) source to canonical
events. This is the entire contract every source-specific adapter
implements -- deliberately small (Cookbook Part I, Ch. 7: 'Every
connector implements one small abstract base class')."""
name: ClassVar[str] = "abstract"
@abstractmethod
def get_cycle_data(self, since: datetime, until: datetime) -> Iterable[CycleEvent]:
"""Yield CycleEvents in [since, until). MUST be a generator or
otherwise lazy Iterable -- never a materialized list (Cookbook
Part I, Ch. 7's Python Insight: 'the same connector handles a
200-row test file or a 50-million-row shift export')."""
@abstractmethod
def get_delay_data(self, since: datetime, until: datetime) -> Iterable[DelayEvent]:
"""Yield DelayEvents in [since, until)."""
def get_maintenance_data(self, since: datetime, until: datetime) -> Iterable[MaintenanceEvent]:
"""Optional: default implementation yields nothing. Override for
sources that carry maintenance telemetry."""
return iter(())
def get_production_data(self, since: datetime, until: datetime) -> Iterable[ProductionEvent]:
return iter(())
def get_consumption_data(self, since: datetime, until: datetime) -> Iterable[ConsumptionEvent]:
return iter(())
def get_safety_data(self, since: datetime, until: datetime) -> Iterable[SafetyEvent]:
return iter(())
def health_check(self) -> "ConnectorHealth":
"""Default: assumes healthy. Override for sources with a real
liveness/auth check (§28)."""
return ConnectorHealth(status=HealthStatus.UNKNOWN)
Only get_cycle_data and get_delay_data are abstract, matching the Cookbook's minimal contract exactly; the remaining four get_*_data methods have sane no-op defaults so a source that only produces cycles (like the reference CSV connector) is not forced to implement methods it has nothing to yield for.
10.2 Normalizer, FieldMapper, ReasonCodeMap¶
class Normalizer(ABC):
"""Applies FieldMapper + ReasonCodeMap to translate one raw source
record into a canonical BaseEvent. Separated from FMSConnector itself
so mapping logic is independently unit-testable without a live/fixture
source connection."""
@abstractmethod
def normalize_cycle(self, raw: Mapping[str, Any]) -> CycleEvent: ...
@abstractmethod
def normalize_delay(self, raw: Mapping[str, Any]) -> DelayEvent: ...
@dataclass(frozen=True, slots=True)
class FieldMapper(BaseValueObject):
"""Declarative field-name mapping: vendor field name -> canonical field name."""
mapping: Mapping[str, str]
def apply(self, raw: Mapping[str, Any]) -> dict[str, Any]:
return {self.mapping.get(k, k): v for k, v in raw.items()}
@dataclass(frozen=True, slots=True)
class ReasonCodeMap(BaseValueObject):
"""Vendor delay-reason code -> canonical DelayCategory + reason string.
Cookbook Part I, Ch. 7: 'The hardest part of a real connector is the
reason-code map... Get this mapping right and every downstream delay
analysis becomes comparable.'
"""
vendor_name: str
mapping: Mapping[str, tuple["DelayCategory", str]] # DelayCategory from ontology
def resolve(self, vendor_code: str) -> Maybe[tuple["DelayCategory", str]]:
return Maybe.some(self.mapping[vendor_code]) if vendor_code in self.mapping else Maybe.nothing()
Semantic-translation rule (normative, from Developer & Cookbook Guide Part III): when a vendor's own percentage-based availability/utilisation figure uses a denominator that differs from MineProductivity's canonical time model (calendar → scheduled → available → operating, per the KPI Engine spec §19), the connector MUST recompute from the vendor's underlying time buckets, and MUST NOT copy the vendor's pre-computed percentage. "A percentage on the wrong denominator is not convertible after the fact."
10.3 AuthProvider, RetryPolicy, BackoffStrategy¶
class AuthProvider(ABC):
"""Isolates credential acquisition/refresh from connector I/O logic."""
@abstractmethod
def credentials(self) -> "Credentials": ...
@abstractmethod
def refresh(self) -> Result["Credentials"]: ...
@dataclass(frozen=True, slots=True)
class Credentials(BaseValueObject):
token: str
expires_at_utc: datetime | None = field(default=None, kw_only=True)
class BackoffStrategy(Enum):
FIXED = "fixed"
EXPONENTIAL = "exponential"
EXPONENTIAL_JITTER = "exponential_jitter"
@dataclass(frozen=True, slots=True)
class RetryPolicy(BaseConfiguration):
max_attempts: int = field(default=3, kw_only=True)
backoff: BackoffStrategy = field(default=BackoffStrategy.EXPONENTIAL_JITTER, kw_only=True)
base_delay_s: float = field(default=1.0, kw_only=True)
retryable_exceptions: tuple[type[Exception], ...] = field(
default=(SourceUnavailableError,), kw_only=True
)
10.4 ConnectorHealth¶
class HealthStatus(Enum):
HEALTHY = "healthy"
DEGRADED = "degraded"
UNHEALTHY = "unhealthy"
UNKNOWN = "unknown"
@dataclass(frozen=True, slots=True)
class ConnectorHealth(BaseValueObject):
status: HealthStatus
last_successful_pull_utc: datetime | None = field(default=None, kw_only=True)
detail: str = field(default="", kw_only=True)
10.5 IngestionMode¶
class IngestionMode(Enum):
BATCH = "batch" # one-shot pull over a fixed [since, until) window
INCREMENTAL = "incremental" # repeated pulls, each resuming from the prior high-watermark
STREAMING = "streaming" # push-based, continuous (Kafka/MQTT)
Every FMSConnector declares which mode(s) it supports via name-adjacent class metadata; the mine.ingest(connector=..., mode=...) call site (Cookbook Part I, Ch. 4/Ch. 7) selects among them.
10.6 Reference Connector Shapes¶
class CSVConnector(FMSConnector):
"""Reads a haul-cycle CSV export. The reference implementation of the
minimal contract -- see Cookbook Part I, Ch. 7 for the full worked
version this specification's shape is drawn from."""
name: ClassVar[str] = "csv"
def __init__(self, path: str, shift_id: str) -> None: ...
def get_cycle_data(self, since: datetime, until: datetime) -> Iterable[CycleEvent]: ...
def get_delay_data(self, since: datetime, until: datetime) -> Iterable[DelayEvent]: ...
class RestConnector(FMSConnector):
"""Pages through an HTTP API, yielding one event per record per page."""
name: ClassVar[str] = "rest"
def __init__(self, base_url: str, auth: AuthProvider, retry: RetryPolicy) -> None: ...
class KafkaConnector(FMSConnector):
"""Subscribes to a topic; yields one event per message. Naturally
STREAMING-mode; get_cycle_data/get_delay_data block/iterate over the
live subscription rather than a bounded [since, until) pull."""
name: ClassVar[str] = "kafka"
ExcelConnector, GraphQLConnector, and MqttConnector follow the identical shape (constructor takes source-specific connection parameters; get_cycle_data/get_delay_data yield canonical events).
10.7 OEM Adapter Shapes¶
class MineStarConnector(FMSConnector):
"""Shape only -- a real implementation lives in an independent plugin
package with its own MineStar SDK dependency, per Cookbook Part I,
Ch. 7's 'conceptual outline.' This class exists in THIS package only
as documentation of the expected shape and constructor signature a
conformant MineStar plugin should mirror; it is not registered by
default and has no working method bodies."""
name: ClassVar[str] = "minestar"
def __init__(self, host: str, auth: AuthProvider) -> None: ...
DispatchConnector (Modular Mining's DISPATCH), WencoConnector, ModularConnector, and HexagonConnector follow the identical documentation-only shape. Each ships, in this specification, with:
| Vendor | Reason-code map source | Notes |
|---|---|---|
| MineStar (Caterpillar) | MineStarReasonCodeMap shape |
Cookbook's worked "conceptual" example (Ch. 7). |
| DISPATCH (Modular Mining) | DispatchReasonCodeMap shape |
|
| Wenco | WencoReasonCodeMap shape |
Named as the Cookbook's entry-point worked example (wenco = "mineproductivity_wenco.connector:WencoConnector"). |
| Modular Mining | ModularReasonCodeMap shape |
|
| Hexagon | HexagonReasonCodeMap shape |
11. Lifecycle¶
stateDiagram-v2
[*] --> Instantiated: FMSConnector subclass constructed with source config
Instantiated --> Authenticated: AuthProvider.credentials() succeeds (network sources only)
Instantiated --> Ready: file/local sources skip auth
Authenticated --> Ready
Ready --> Pulling: get_cycle_data()/get_delay_data()/... called
Pulling --> Ready: pull completes (batch/incremental) or is paused (streaming)
Pulling --> Retrying: transient failure, RetryPolicy engaged
Retrying --> Pulling: retry attempt
Retrying --> Failed: max_attempts exceeded
Ready --> Closed: connector explicitly closed/disposed
Failed --> [*]
Closed --> [*]
12. State Machine¶
ConnectorHealth.status (§10.4) is the externally-observable health state machine, updated after every pull attempt:
stateDiagram-v2
[*] --> Unknown
Unknown --> Healthy: first successful pull
Healthy --> Degraded: a retryable failure occurred but eventually succeeded
Healthy --> Unhealthy: pull failed after exhausting RetryPolicy
Degraded --> Healthy: subsequent pull succeeds cleanly
Unhealthy --> Healthy: subsequent pull succeeds
Unhealthy --> Unhealthy: repeated failure
13. Sequence Diagrams¶
13.1 Batch CSV ingestion (Cookbook Part I, Ch. 5 & Ch. 7, generalized)¶
sequenceDiagram
participant App as Application (mine.ingest(...))
participant Reg as ConnectorRegistry
participant Conn as CSVConnector
participant Norm as Normalizer
participant Ont as ontology (resolve)
participant Evt as events (validate + append)
App->>Reg: get_connector("csv")
Reg-->>App: CSVConnector (type)
App->>Conn: CSVConnector(path=..., shift_id=...)
App->>Conn: get_cycle_data(since, until)
loop each CSV row
Conn->>Norm: normalize_cycle(raw_row)
Norm->>Ont: resolve(equipment_id) [optional confidence check]
Norm-->>Conn: CycleEvent
Conn-->>App: yield CycleEvent
end
App->>Evt: EventValidator.validate() + EventStore.append() per event
13.2 REST connector with retry/backoff and auth refresh¶
sequenceDiagram
participant App
participant Conn as RestConnector
participant Auth as AuthProvider
participant Retry as RetryPolicy
participant API as Vendor REST API
App->>Conn: get_cycle_data(since, until)
Conn->>Auth: credentials()
Auth-->>Conn: Credentials(token, expires_at)
Conn->>API: GET /cycles?since=...&page=0
API-->>Conn: 401 Unauthorized (token expired)
Conn->>Auth: refresh()
Auth-->>Conn: Result.ok(new Credentials)
Conn->>API: GET /cycles?since=...&page=0 (retry, per RetryPolicy)
API-->>Conn: 200 OK, page of records
Conn-->>App: yield CycleEvent per record
Conn->>API: GET /cycles?since=...&page=1
API--xConn: connection timeout
Conn->>Retry: should_retry(attempt=1)?
Retry-->>Conn: yes, wait base_delay_s * 2^1 (+jitter)
Conn->>API: GET /cycles?since=...&page=1 (retry)
API-->>Conn: 200 OK
13.3 Vendor semantic mapping (availability denominator mismatch)¶
sequenceDiagram
participant Vendor as Vendor system
participant Conn as OEM Connector
participant Norm as Normalizer
Vendor->>Conn: raw "availability = 91.2%" (vendor's own denominator)
Conn->>Conn: does vendor denominator == canonical scheduled-time basis?
alt denominators match
Conn->>Norm: pass through as a documented, source-attributed figure
else denominators differ
Conn->>Vendor: pull underlying time-bucket data (available_h, scheduled_h, ...)
Conn->>Norm: recompute UTIL.PA from canonical time model, never copy vendor %
Note over Conn,Norm: "a percentage on the wrong denominator is not convertible after the fact"
end
14. Class Diagrams¶
classDiagram
class FMSConnector {
<<abstract>>
+name: ClassVar~str~
+get_cycle_data(since, until) Iterable~CycleEvent~
+get_delay_data(since, until) Iterable~DelayEvent~
+get_maintenance_data(since, until) Iterable~MaintenanceEvent~
+health_check() ConnectorHealth
}
FMSConnector <|-- CSVConnector
FMSConnector <|-- ExcelConnector
FMSConnector <|-- RestConnector
FMSConnector <|-- GraphQLConnector
FMSConnector <|-- KafkaConnector
FMSConnector <|-- MqttConnector
FMSConnector <|-- MineStarConnector : shape only
FMSConnector <|-- DispatchConnector : shape only
FMSConnector <|-- WencoConnector : shape only
class Normalizer {
<<abstract>>
+normalize_cycle(raw) CycleEvent
+normalize_delay(raw) DelayEvent
}
class FieldMapper
class ReasonCodeMap
Normalizer --> FieldMapper
Normalizer --> ReasonCodeMap
FMSConnector --> Normalizer : uses
class AuthProvider {
<<abstract>>
+credentials() Credentials
+refresh() Result
}
class RetryPolicy
class BackoffStrategy
class ConnectorHealth
class HealthStatus
RestConnector --> AuthProvider
RestConnector --> RetryPolicy
RetryPolicy --> BackoffStrategy
FMSConnector --> ConnectorHealth
ConnectorHealth --> HealthStatus
class Registry~TKey,TItem~
Registry <.. FMSConnector : registered as
15. Data Flow¶
Source (file / vendor SDK / REST / Kafka / MQTT)
│
▼
FMSConnector.get_cycle_data() / get_delay_data() / ... (streamed, one record at a time)
│
▼
Normalizer.normalize_cycle()/.normalize_delay()
│ FieldMapper: vendor field names -> canonical field names
│ ReasonCodeMap: vendor reason codes -> canonical DelayCategory (ontology)
│ semantic recomputation when denominators differ (§13.3)
▼
canonical BaseEvent instance (events package's types)
│
▼
EventValidator.validate() -> EventStore.append() (events package, per spec 01 §15)
Everything left of the second arrow may know about a vendor's dialect. Nothing right of it ever does - this single boundary is the entire value of the Connector Framework.
16. Extension Points¶
- New connectors for new sources. Implement
FMSConnector; ship as a plugin (§17) or, for genuinely generic reference sources, add underfile/,network/, orstreaming/in this package. - New OEM adapters. Follow the
oem/shape (§10.7); a real implementation lives in an independent plugin package, never inmineproductivity.connectorsitself. - New
Normalizer/ReasonCodeMapper vendor. Composable independently of theFMSConnectorthat uses them - a vendor's mapping logic can be unit-tested without any live connection (§10.2). - New
AuthProviderimplementations (OAuth2, API key, mutual TLS, ...) for network connectors, without touchingFMSConnectoritself.
17. Plugin Strategy¶
Identical mechanism to every other extension point (Registry Framework spec §17), specialized for connectors:
from mineproductivity.connectors import FMSConnector, register_connector
@register_connector
class WencoConnector(FMSConnector):
name = "wenco"
...
[project.entry-points."mineproductivity.connectors"]
wenco = "mineproductivity_wenco.connector:WencoConnector"
After pip install mineproductivity-wenco, get_connector("wenco") and mine.ingest(connector="wenco") work with zero core change (Cookbook Part I, Ch. 7).
18. Metadata¶
Every connector declares, alongside name:
| Field | Purpose |
|---|---|
name |
Registry key, e.g. "csv", "minestar". |
supported_modes |
Which IngestionModes it implements (§10.5). |
provided_event_types |
Which of get_cycle_data/get_delay_data/etc. it actually overrides (non-default), so discovery/documentation tooling can report what a connector is good for without instantiating it. |
vendor_name (OEM connectors only) |
For ReasonCodeMap lookup and documentation cross-referencing. |
19. Validation¶
Two boundaries, both mandatory:
- Structural validation happens inside
events(§19 of the Event Framework spec) once aBaseEventis constructed -connectorsdoes not duplicate this logic. - Contract validation happens via the shared
run_fms_contract_suite(§9, §29): any connector, built-in or plugin, is expected to pass a fixed suite of structural assertions (yields well-formed events, respects[since, until), is a lazy generator) before it is trusted with a live source, mirroring Cookbook Part I Ch. 7's "Every connector is run through a shared contract test suite against recorded fixtures."
20. Versioning¶
- Connector plugin versioning follows the identical SemVer discipline as KPI plugins (Registry Framework spec §20): a MAJOR bump for a breaking change to what a connector's
nameproduces or requires; MINOR for a new optional capability (e.g. addingget_maintenance_datasupport); PATCH for a mapping-table correction that does not change the connector's contract. ReasonCodeMapversioning is independent of the connector's own version - a vendor's code table can change (a new delay code introduced by the vendor) without the connector's Python interface changing;ReasonCodeMapinstances carry their ownEventVersion-style revision so a mapping update is auditable.
21. Serialization¶
Connectors do not define new serialization formats; they consume whatever format the source provides (CSV rows, JSON API responses, Avro/Protobuf stream messages) and produce BaseEvent instances that flow into events' serialization contracts (Event Framework spec §21). RetryPolicy, Credentials, and ConnectorHealth are core.BaseValueObject/BaseConfiguration types and serialize via the standard core.serialization surface for logging/diagnostics purposes only - never as the primary data path.
22. Performance Considerations¶
- Lazy generators, always (§10.1) - the single most important performance property of this package; a connector's memory footprint is independent of source size.
- Paging for REST/database sources (§10.6's
RestConnectorsketch) keeps any one request bounded regardless of total record count. - Streaming connectors (Kafka/MQTT) are inherently backpressure-sensitive; a conformant
KafkaConnector/MqttConnectorimplementation MUST NOT buffer unbounded messages if the consumer (validation/append path) falls behind - this is an implementation-checklist-level requirement, not merely a nice-to-have. - Batch windows should be chosen to bound per-call memory, not to bound wall-clock time; a caller ingesting a year of history should chunk
[since, until)into multiple calls rather than expect one connector call to hold a year of events in flight.
23. Memory Considerations¶
- No
FMSConnectorimplementation may hold more than a small, bounded working set (a page of REST results, a chunk of CSV rows) in memory at once - enforced by the "always a generator" rule (§10.1, §22). ReasonCodeMapandFieldMapperinstances are small, static, frozen value objects loaded once per connector instantiation.
24. Thread Safety¶
FMSConnectorinstances are not guaranteed thread-safe by this specification - a connector wrapping a stateful vendor SDK client may not support concurrentget_cycle_data()calls. Implementations MUST document their own thread-safety guarantee explicitly (mirroring theEventStoreobligation in the Event Framework spec §24); callers requiring concurrent ingestion from the same source should construct one connector instance per worker unless a specific connector documents shared-instance safety.AuthProvider.refresh()MUST be safe to call concurrently without acquiring duplicate tokens or corrupting the cachedCredentials- this one guarantee is mandatory (not merely "must be documented") because concurrent 401-triggered refreshes are a realistic, common failure mode for any multi-threaded ingestion pipeline.
25. Concurrency¶
- Multiple connectors ingesting concurrently (e.g.
csvfor historical backfill whileminestarstreams live) is a supported, expected pattern - each connector instance is independent and writes into the sameEventStore, whose own concurrency guarantees (Event Framework spec §25) govern the merge. - Streaming connectors are expected to run on their own thread/task for the life of the subscription, yielding events as they arrive rather than being polled - callers integrate them via
EventBus-style push (Event Framework spec §10.9) rather than the pull-basedquery()path used for batch sources.
26. Error Handling¶
class ConnectorError(MineProductivityError):
"""Root of connector-specific errors."""
class MappingError(ConnectorError):
"""A raw record could not be normalized -- e.g. an unrecognized
vendor reason code with no ReasonCodeMap entry (§10.2)."""
class AuthenticationError(ConnectorError):
"""AuthProvider could not obtain or refresh valid credentials."""
class SourceUnavailableError(ConnectorError):
"""The source is unreachable (network, file-not-found, ...) --
the default retryable exception type (§10.3)."""
class ContractViolationError(ConnectorError):
"""A connector implementation failed the shared contract test suite
(§9, §29) -- e.g. it returned a list instead of a lazy Iterable, or
yielded an event outside the requested [since, until) window."""
Rule: a MappingError for one record MUST NOT abort an entire get_cycle_data() generator - the connector logs the unmapped record (§27) and continues yielding subsequent records, exactly mirroring the events package's "reject one, don't crash the batch" philosophy (§26 of the Event Framework spec).
27. Logging¶
- Every unmapped vendor reason code (
ReasonCodeMap.resolve()returningMaybe.nothing()) logs atWARNINGwith the vendor name and raw code - this is the primary signal an operator uses to notice a vendor introduced a new code the platform's mapping table has not caught up with yet. - Every retry attempt (
RetryPolicyengaging) logs atINFOwith attempt number and computed backoff delay; exhaustingmax_attemptslogs atERROR. ConnectorHealthtransitions (§12) log atWARNINGon any transition intoDegraded/Unhealthy, and atINFOon recovery toHealthy.
28. Configuration¶
Per-connector configuration (source path/URL, credentials reference, polling interval, RetryPolicy overrides) is expressed as core.BaseConfiguration subclasses, sourced by the future config package's layered defaults → enterprise → region → site model (Cookbook Part I, Ch. 3). connectors defines the configuration shapes (e.g. CSVConnectorConfig, RestConnectorConfig); it never reads environment variables or files directly, consistent with the same boundary already established for events (§28 of that spec) and BaseConfiguration itself (core/README.md).
29. Testing Strategy¶
- Contract tests (
connectors.contract_tests.run_fms_contract_suite) - every connector, built-in or plugin, is run against fixture data and asserted to: yield well-formed, schema-valid events; respect the[since, until)window; return a lazyIterable; and degrade per §26 on a malformed record rather than crashing. - Unit tests -
Normalizer/FieldMapper/ReasonCodeMaplogic tested independently of any live or fixture connection (§10.2), including the semantic-recomputation rule (§13.3). - Recorded-fixture tests - a small, anonymised fixture recorded from each reference source type, committed to
tests/fixtures/connectors/, protecting against a vendor silently renaming a field (Cookbook Part I, Ch. 7's Best Practice). - Retry/backoff tests - simulate transient failures and assert
RetryPolicytiming andmax_attemptsbehavior deterministically (no real sleeping in unit tests - inject a fake clock/backoff function).
30. Certification Requirements¶
| Category | Requirement for connectors |
|---|---|
| A - Golden datasets | The Learning & Benchmark Suite's cycle_events.csv/delay_events.csv fixtures, run through CSVConnector, reproduce the golden CycleEvent/DelayEvent instances exactly. |
| B - Integration | CSV → CSVConnector → EventValidator → EventStore → query reproduces golden outputs with no direct function call bypassing a stage (mirrors Event Framework spec §30's Category B, since it is the same pipeline observed from the connector side). |
| C - Edge cases | An empty CSV (zero rows), a [since, until) window with no matching records, and a record with every optional field absent are all handled without error. |
| D - Corrupted data | A malformed CSV row (non-numeric payload, missing required column) produces a MappingError for that row only - the rest of the file still ingests (§26). |
| F - Timezone | A CSV with local (non-UTC) timestamps is correctly normalized to event_time_utc (Learning & Benchmark Suite's temporal philosophy, applied at the connector boundary since that is where raw timestamps first enter the platform). |
31. Example Usage¶
from mineproductivity.connectors import get_connector, CONNECTORS
from datetime import datetime, timezone
print("csv" in CONNECTORS) # True
Conn = get_connector("csv")
conn = Conn(path="bingham_west_shift_A.csv", shift_id="A-2026-06-25")
since = datetime(2026, 6, 25, tzinfo=timezone.utc)
until = datetime(2026, 6, 26, tzinfo=timezone.utc)
events = list(conn.get_cycle_data(since, until))
print(len(events), "events |", "first payload:", events[0].payload_t)
health = conn.health_check()
print(health.status) # HealthStatus.HEALTHY
32. Anti-Patterns¶
- ❌ Any code outside
connectorsimporting a vendor SDK. Ifkpisoreventsever needs toimport minestar_sdk, the architecture has already failed - the whole point of this package is that this never happens. - ❌ A connector returning a
listinstead of yielding. Fails the contract suite (§9, §29) immediately and defeats the "200-row test file or 50-million-row export, same code" guarantee. - ❌ Copying a vendor's pre-computed availability/utilisation percentage instead of recomputing from time buckets when denominators differ (§13.3). This single mistake is called out as normative-severity in the Developer & Cookbook Guide Part III.
- ❌ Letting one malformed record abort an entire ingestion run. Log and skip (§26); a whole shift's data must not be lost because one row has a typo.
- ❌ Hard-coding a vendor's reason codes as
if/elifchains inside connector logic instead of an externally-maintainableReasonCodeMap. The map must be inspectable, testable, and updatable independent of the connector's control flow. - ❌ A connector plugin skipping the shared contract test suite "because our vendor is different." Every connector, however exotic its source, satisfies the same small contract; that is the guarantee the whole framework exists to make.
33. Future Extensions¶
- Database/historian connectors (Cookbook Part I, Ch. 7 mentions these conceptually: "Query a historian or SQL table in pages") - same
FMSConnectorshape, a newnetwork/ordatabase/module. - Bidirectional connectors for future dispatch/optimization write-back (hinted at by
optimization's future dispatch solvers) - out of scope for v1.0's read-only ingestion focus, flagged here as a likely v2.0 contract extension, not a v1.0 change. - Automatic vendor reason-code discovery/suggestion (an AI-assisted mapping-table bootstrapper) - consistent with the Cookbook's "AI Contributor Note" that a connector is "the ideal task to delegate to an AI agent."
- A formal connector certification badge program, once the Learning & Benchmark Suite's certification thresholds (currently unpublished, per that document's own disclaimer) are finalized.
34. Known Constraints¶
- This specification defines the contract and reference shapes for OEM connectors; it explicitly does not and cannot validate against real vendor systems, since no vendor SDK is bundled with or accessible to this repository. Real-world conformance of any OEM plugin is the responsibility of that plugin's own test suite run against
run_fms_contract_suite(§9). - Streaming connector backpressure behavior (§22) is specified as a requirement, not a fully worked algorithm - the exact buffering/flow-control strategy is an implementation-checklist-level decision, likely to differ between a Kafka and an MQTT adapter.
connectorstargets the same Python 3.12+ baseline as the rest of the platform; a vendor SDK requiring an older or newer Python is a plugin-level constraint outside this specification's control.
35. Architecture Decisions¶
| ID | Decision | Rationale |
|---|---|---|
| AD-CN-01 | FMSConnector has only two abstract methods (get_cycle_data, get_delay_data); the other four get_*_data methods have no-op defaults. |
Matches the Cookbook's minimal contract exactly and keeps the barrier to writing a first connector as low as possible - a CSV-only source should not be forced to stub out methods it has nothing to yield for. |
| AD-CN-02 | Normalizer/FieldMapper/ReasonCodeMap are separate, independently testable objects, not private methods inside each FMSConnector subclass. |
Composition over inheritance; a vendor's mapping table can be reviewed, versioned, and unit-tested by a domain expert who never has to read or run the connector's I/O code. |
| AD-CN-03 | OEM adapter classes exist in this package only as documentation-only shapes, never as working implementations. | Keeps mineproductivity.connectors' own dependency graph free of any proprietary vendor SDK, preserving pip-installability and open-source distributability of the core package - exactly the boundary the Developer Documentation's "OEM connectors are separate plugins, so the core stays vendor-neutral and lightweight" describes. |
| AD-CN-04 | The semantic-recomputation rule (§13.3) is normative, not a suggestion. | Directly enforces the KPI Standard Library's canonical-semantics ruling at the one boundary where a violation could otherwise creep in silently - the connector is the last point before a vendor's number either becomes trustworthy or corrupts every downstream KPI. |
| AD-CN-05 | AuthProvider/RetryPolicy/BackoffStrategy are shared, generic types used by every network connector, not reimplemented per connector. |
One retry/backoff/auth-refresh implementation to test and trust, rather than five OEM-specific ones with subtly different bugs. |
36. Definition of Done¶
-
FMSConnectorand all reference connector shapes (§10.6) implemented exactly per this specification. -
Normalizer/FieldMapper/ReasonCodeMap/AuthProvider/RetryPolicyimplemented per §10.2-§10.3. -
tests/unit/connectors/mirrorssrc/mineproductivity/connectors/1:1, ≥95% coverage. - The shared contract test suite (§9, §29) exists and the reference
CSVConnectorpasses it. -
mypy --strictandruffclean. -
examples/connectors/demonstrates CSV ingestion end-to-end, plus a REST connector example with mocked retry/auth-refresh. - No import from
connectorsreacheskpis,analytics,optimization,simulation,decision,digital_twin, oragents- mechanically verified.
37. Package Acceptance Criteria¶
- Contract conformance proof: the reference
CSVConnectorand at least one network-based reference connector (RestConnector, exercised against a mocked HTTP server) both passrun_fms_contract_suitewithout modification to the suite itself. - Isolation proof: a fixture CSV with one malformed row and nine well-formed rows yields exactly nine valid events plus one logged
MappingError, never a crash and never ten events with a fabricated value. - Semantic-recomputation proof: a fixture OEM mapping scenario with a deliberately mismatched availability denominator demonstrates recomputation from time buckets, not pass-through, per §13.3.
- No architectural drift:
connectorsappears in the dependency graph exactly per §7; the forbidden-imports check (nokpis, noanalytics, etc.) passes mechanically. - Cross-reference audit: the OEM vendor list (MineStar, DISPATCH, Wenco, Modular Mining, Hexagon), the reason-code-map pattern, and the semantic-recomputation rule are each traceable to a specific passage in the Developer & Cookbook Guide cited in this document.
End of Connector Framework Design Specification. See docs/design/04_Connector_Implementation_Checklist.md for the actionable implementation contract.