Input & Output Ports
This is the piece the data product exists for: the ports. inputPorts describe what the product consumes,
outputPorts describe what it produces. Both come from the library's product package — product.input and
product.output — and both are discoverable, so every entry can carry tags, customProperties, and
authoritativeDefinitions of its own.
This is where the modular approach earns its keep. Each port is written once as a named, typed value; the root then lists those ports by name. A port written this way is easy to read, easy to reuse, and validated the moment you build it.
Input ports
An InputPort requires name, version, and contractId — the last one referencing the
ODCS data contract that governs the data being consumed. The
upstream example lists two ports, payments and onlinetransactions, each at two versions, so each name+version
pair becomes its own named value. This is
input/port.k:
import enkinex_odps.product.input
Payments1_0 = input.InputPort {
name = "payments"
version = "1.0.0"
contractId = "dbb7b1eb-7628-436e-8914-2a00638ba6db"
}
Payments2_0 = input.InputPort {
name = "payments"
version = "2.0.0"
contractId = "dbb7b1eb-7628-436e-8914-2a00638ba6da"
}
OnlineTransactions1_0 = input.InputPort {
name = "onlinetransactions"
version = "1.0.0"
contractId = "ec2a112d-5cfe-49f3-8760-f9cfb4597544"
}
OnlineTransactions1_1 = input.InputPort {
name = "onlinetransactions"
version = "1.1.0"
contractId = "ec2a112d-5cfe-49f3-8760-f9cfb4597547"
tags = ["transactions"]
customProperties = [
{property = "transactionsVersion", value = "1.1.0"}
]
authoritativeDefinitions = [
{$type = "data_dictionary", url = "https://mydata/dictionary"}
]
}
Only the last port carries discovery metadata. The first three are three lines each — the schema requires nothing more,
and the type checker tells you so immediately if a contractId goes missing.
Output ports
OutputPort is the most composite entity in the library. Alongside name, version, and contractId, it can carry a
list of Sbom entries — the Software Bill of Materials for what the port ships — and a list of InputContract
dependencies recording which contracts, at which versions, this output was built from. The example's second
rawtransactions version uses both. This is
output/port.k:
import enkinex_odps.product.output
RawTransactions1_0 = output.OutputPort {
name = "rawtransactions"
description = "Raw Transactions"
$type = "tables"
version = "1.0.0"
contractId = "c2798941-1b7e-4b03-9e0d-955b1a872b32"
}
RawTransactions2_0 = output.OutputPort {
name = "rawtransactions"
description = "Raw Transactions"
$type = "tables"
version = "2.0.0"
contractId = "c2798941-1b7e-4b03-9e0d-955b1a872b33"
tags = ["transactions"]
customProperties = [
{property = "transactionsVersion", value = "2.0.0"}
]
authoritativeDefinitions = [
{$type = "data_dictionary", url = "https://mydata/dictionary"}
]
sbom = [
{$type = "external", url = "https://mysbomserver/mysbom"}
]
inputContracts = [
{id = "dbb7b1eb-7628-436e-8914-2a00638ba6db", version = "2.0.0"}
{id = "ec2a112d-5cfe-49f3-8760-f9cfb4597544", version = "1.0.0"}
]
}
ConsolidatedTransactions1_0 = output.OutputPort {
name = "consolidatedtransactions"
description = "Consolidated transactions"
$type = "tables"
version = "1.0.0"
contractId = "a44978be-1fe0-4226-b840-1b715bc25c63"
}
FullTransactionsWithReturns0_3 = output.OutputPort {
name = "fulltransactionswithreturns"
description = "Full transactions with returns"
$type = "tables"
version = "0.3.0"
contractId = "ef769969-0cbe-4188-876f-bb00abadaee4"
}
The sbom and inputContracts entries use the same bare dict literals we saw on the management port, and the same
inference applies: Sbom and InputContract are resolved from OutputPort's attribute types. It is worth knowing why
the library keeps Sbom in its own product package rather than beside the output port — both product.input and
product.output depend on it, and a shared package is what lets them do that without a circular import.
The complete product.k
Two final local imports join the root, the inputPorts and outputPorts fields fall into place, and the data product
is complete. This is product.k in full:
import enkinex_odps.odps
import .metadata.description as product_description
import .input.port as input_port
import .output.port as output_port
import .management.port as management_port
import .support.channels as support_channels
import .team.team as product_team
odps.DataProduct {
name = "Customer Data Product"
id = "fbe8d147-28db-4f1d-bedf-a3fe9f458427"
domain = "seller"
status = "draft"
tenant = "RetailCorp"
tags = ["customer"]
description = product_description.ProductDescription
inputPorts = [
input_port.Payments1_0
input_port.Payments2_0
input_port.OnlineTransactions1_0
input_port.OnlineTransactions1_1
]
outputPorts = [
output_port.RawTransactions1_0
output_port.RawTransactions2_0
output_port.ConsolidatedTransactions1_0
output_port.FullTransactionsWithReturns0_3
]
managementPorts = [
management_port.DictionaryUpdatesPort
]
support = [
support_channels.DataTeamSlack
support_channels.EmailSupport
]
team = product_team.DataTeam
productCreatedTs = "2023-01-15T10:30:00Z"
}
Six local imports, thirteen named references, and not a single value defined twice. All that is left is to run it.