Configurable TLV/LTV codec for payment protocol sub-fields (ISO 8583, acquirer-style)
Project description
pytlv-codec
Configurable TLV/LTV codec for payment protocol sub-fields (ISO 8583, acquirer-style).
Why
Most existing TLV libraries in Python target byte-oriented BER/DER (X.690, EMV chip cards). But many real-world payment protocols — especially ISO 8583 sub-fields and Latin American acquirer integrations — use string-based TLV/LTV encoding with configurable conventions:
- Order: TLV vs LTV
- Tag/length encoding: ASCII, BCD, HEX, binary
- LTV variants where length includes the tag
pytlv-codec provides a flexible, configurable codec for these formats.
Status
🚧 Early development (v0.3.1). API may change before 1.0.
Install
pip install pytlv-codec
Quick start (simple ASCII TLV)
from pytlv_codec import Codec, CodecConfig
# Default: TLV order, ASCII everywhere, 2-char tags, 4-char lengths
codec = Codec()
encoded = codec.encode({"05": "12345", "62": "teste"})
# encoded == "05000512345620005teste"
decoded = codec.decode(encoded)
# decoded == {"05": "12345", "62": "teste"}
Real-world example (LTV, BCD, length includes tag)
A real Brazilian acquirer-style sub-field encoding: LTV order, BCD-encoded tag and length, length includes the tag bytes, and value is opaque binary data represented as a hex string.
from pytlv_codec import Codec, CodecConfig, Encoding, Order, ValueType
config = CodecConfig(
order=Order.LTV,
tag_size=2,
tag_encoding=Encoding.BCD,
length_size=4,
length_encoding=Encoding.BCD,
value_type=ValueType.BINARY,
length_includes_tag=True,
)
codec = Codec(config)
# 74 hex chars = 37 bytes of opaque binary data
opaque_payload = (
"0544970000010009650840000001000966"
"00000100096655534431323334350212345678"
"06"
)
encoded = codec.encode({"33": opaque_payload})
# encoded == "0038" + "33" + opaque_payload (80 chars total = 40 bytes when packed)
# Length is 38 bytes = 1 (tag, BCD) + 37 (value, binary)
decoded = codec.decode(encoded)
# decoded == {"33": opaque_payload}
Schema-driven payload (structured TLV values)
Many real-world TLV values are not opaque blobs — they are concatenations of multiple named subfields, each with its own type (BCD, ASCII, BINARY) and either fixed size or variable length with a length prefix. SubfieldSchema lets you describe the structure once and pack/unpack with named values.
from pytlv_codec import (
Codec, CodecConfig, Encoding, Order, ValueType,
SubfieldSchema, Subfield, SubfieldType,
LengthPrefix, LengthPrefixEncoding,
)
# 1. Describe the structured payload
schema = SubfieldSchema([
Subfield("acquirer_code", SubfieldType.BCD, size_bytes=3), # 3 bytes BCD
Subfield("merchant_id", SubfieldType.BCD, size_bytes=6),
Subfield("currency_code", SubfieldType.ASCII, size_bytes=3), # "USD"
# variable-length: length prefix is 1 byte BCD (max 99 bytes)
Subfield(
"merchant_name",
SubfieldType.ASCII,
length_prefix=LengthPrefix(LengthPrefixEncoding.BCD, size_bytes=1),
),
])
# 2. Pack natural values into a hex payload string
payload_hex = schema.pack({
"acquirer_code": "054497", # BCD digits
"merchant_id": "000001000965",
"currency_code": "USD", # ASCII text
"merchant_name": "STORE 01", # any length up to 99 bytes
})
# 3. Wrap in a TLV envelope (acquirer-style LTV / BCD)
codec = Codec(CodecConfig(
order=Order.LTV,
tag_size=2, tag_encoding=Encoding.BCD,
length_size=4, length_encoding=Encoding.BCD,
value_type=ValueType.BINARY,
length_includes_tag=True,
))
encoded = codec.encode({"33": payload_hex})
# 4. Round trip: decode the envelope, then unpack the schema
decoded_envelope = codec.decode(encoded)
fields = schema.unpack(decoded_envelope["33"])
# fields == {"acquirer_code": "054497", "merchant_id": "...", "currency_code": "USD", "merchant_name": "STORE 01"}
Configuration reference
from pytlv_codec import CodecConfig, Encoding, Order, ValueType
CodecConfig(
order=Order.TLV, # Order.TLV | Order.LTV
tag_size=2, # logical units (chars/digits)
tag_encoding=Encoding.ASCII, # ASCII | BCD | HEX | BINARY
length_size=4,
length_encoding=Encoding.ASCII,
value_type=ValueType.ASCII, # ASCII | BCD | HEX | BINARY
length_includes_tag=False, # LTV: length covers tag + value
pad_char="0",
allow_empty_value=True,
allow_duplicate_tags=False,
big_endian=True,
)
Length is always counted as bytes-on-wire (after binary serialization downstream).
How encodings affect bytes-on-wire
| Encoding | Bytes per logical unit |
|---|---|
ASCII |
1 char = 1 byte |
BCD |
2 digits packed in 1 byte |
HEX |
2 hex chars in 1 byte |
BINARY |
2 hex chars in 1 byte |
For BCD/HEX/BINARY, the codec works with the hex string representation of the underlying bytes. Downstream conversion to actual binary is the responsibility of the caller (or a dedicated library like pyiso8583).
Custom exceptions
from pytlv_codec import (
PytlvError, # base
EncodingError, # invalid input on encode
InvalidTLVError, # malformed stream on decode
UnsupportedConfigError, # config combination not implemented
)
All custom exceptions also inherit from ValueError / NotImplementedError so existing generic handlers continue to work.
Development
git clone https://github.com/jacobdarrossi/pytlv-codec.git
cd pytlv-codec
pip install -e ".[dev]"
pytest
License
MIT
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file pytlv_codec-0.3.1.tar.gz.
File metadata
- Download URL: pytlv_codec-0.3.1.tar.gz
- Upload date:
- Size: 16.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
069a0ec0c3a85bf7902a0f75ef3a61d653c11820bf7d8ac45af6f34a574f58a8
|
|
| MD5 |
66e8d2e01f390491c6d95f837e0f904e
|
|
| BLAKE2b-256 |
747ac2c6b3538e0b29217ea672e36ebc73b885b9bd24a998afe5c7e0a687aabd
|
Provenance
The following attestation bundles were made for pytlv_codec-0.3.1.tar.gz:
Publisher:
publish.yml on jacobdarrossi/pytlv-codec
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pytlv_codec-0.3.1.tar.gz -
Subject digest:
069a0ec0c3a85bf7902a0f75ef3a61d653c11820bf7d8ac45af6f34a574f58a8 - Sigstore transparency entry: 1453449810
- Sigstore integration time:
-
Permalink:
jacobdarrossi/pytlv-codec@2f2273da7c531c65f80f8612406a011cb5d9bf21 -
Branch / Tag:
refs/tags/v0.3.1 - Owner: https://github.com/jacobdarrossi
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@2f2273da7c531c65f80f8612406a011cb5d9bf21 -
Trigger Event:
push
-
Statement type:
File details
Details for the file pytlv_codec-0.3.1-py3-none-any.whl.
File metadata
- Download URL: pytlv_codec-0.3.1-py3-none-any.whl
- Upload date:
- Size: 11.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a066eccdd74af9d4b9b282d7ceeb01506480577a1f3e772750bb809090716aec
|
|
| MD5 |
b33a41d000d03bb79afe08fae45eef6f
|
|
| BLAKE2b-256 |
a3a4925f91e6cfdeae3d953c9b24832006af707e2f0d05143672652e7237b657
|
Provenance
The following attestation bundles were made for pytlv_codec-0.3.1-py3-none-any.whl:
Publisher:
publish.yml on jacobdarrossi/pytlv-codec
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pytlv_codec-0.3.1-py3-none-any.whl -
Subject digest:
a066eccdd74af9d4b9b282d7ceeb01506480577a1f3e772750bb809090716aec - Sigstore transparency entry: 1453450093
- Sigstore integration time:
-
Permalink:
jacobdarrossi/pytlv-codec@2f2273da7c531c65f80f8612406a011cb5d9bf21 -
Branch / Tag:
refs/tags/v0.3.1 - Owner: https://github.com/jacobdarrossi
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@2f2273da7c531c65f80f8612406a011cb5d9bf21 -
Trigger Event:
push
-
Statement type: