openlinktoken-pyspark 2.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- openlinktoken_pyspark-2.0.0/.flake8 +3 -0
- openlinktoken_pyspark-2.0.0/MANIFEST.in +13 -0
- openlinktoken_pyspark-2.0.0/PKG-INFO +220 -0
- openlinktoken_pyspark-2.0.0/README.md +448 -0
- openlinktoken_pyspark-2.0.0/dev-requirements.txt +3 -0
- openlinktoken_pyspark-2.0.0/examples/simple_example.py +168 -0
- openlinktoken_pyspark-2.0.0/notebooks/Custom_Token_Definition_Guide.ipynb +512 -0
- openlinktoken_pyspark-2.0.0/notebooks/Dataset_Overlap_Analysis_Guide.ipynb +417 -0
- openlinktoken_pyspark-2.0.0/notebooks/OpenLinkToken_PySpark_Example.ipynb +361 -0
- openlinktoken_pyspark-2.0.0/pyproject.toml +58 -0
- openlinktoken_pyspark-2.0.0/requirements.txt +13 -0
- openlinktoken_pyspark-2.0.0/setup.cfg +4 -0
- openlinktoken_pyspark-2.0.0/setup.py +83 -0
- openlinktoken_pyspark-2.0.0/src/main/openlinktoken_pyspark/__init__.py +28 -0
- openlinktoken_pyspark-2.0.0/src/main/openlinktoken_pyspark/notebook_helpers.py +356 -0
- openlinktoken_pyspark-2.0.0/src/main/openlinktoken_pyspark/overlap_analyzer.py +392 -0
- openlinktoken_pyspark-2.0.0/src/main/openlinktoken_pyspark/token_processor.py +522 -0
- openlinktoken_pyspark-2.0.0/src/main/openlinktoken_pyspark.egg-info/PKG-INFO +220 -0
- openlinktoken_pyspark-2.0.0/src/main/openlinktoken_pyspark.egg-info/SOURCES.txt +22 -0
- openlinktoken_pyspark-2.0.0/src/main/openlinktoken_pyspark.egg-info/dependency_links.txt +1 -0
- openlinktoken_pyspark-2.0.0/src/main/openlinktoken_pyspark.egg-info/requires.txt +57 -0
- openlinktoken_pyspark-2.0.0/src/main/openlinktoken_pyspark.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
include README.md
|
|
2
|
+
include INSTALL.md
|
|
3
|
+
include requirements.txt
|
|
4
|
+
include dev-requirements.txt
|
|
5
|
+
include pyproject.toml
|
|
6
|
+
include .flake8
|
|
7
|
+
# Include repository root docs used for long_description
|
|
8
|
+
include ../../../README.md
|
|
9
|
+
include ../../../LICENSE
|
|
10
|
+
recursive-include notebooks *.ipynb
|
|
11
|
+
recursive-include examples *.py
|
|
12
|
+
recursive-exclude * __pycache__
|
|
13
|
+
recursive-exclude * *.py[co]
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: openlinktoken-pyspark
|
|
3
|
+
Version: 2.0.0
|
|
4
|
+
Summary: Open Link Token PySpark bridge for distributed token generation
|
|
5
|
+
Home-page: https://github.com/TruvetaPublic/OpenLinkToken
|
|
6
|
+
Author: Open Link Token Contributors
|
|
7
|
+
Project-URL: Source, https://github.com/TruvetaPublic/OpenLinkToken
|
|
8
|
+
Project-URL: Documentation, https://github.com/TruvetaPublic/OpenLinkToken/blob/main/README.md
|
|
9
|
+
Requires-Python: >=3.10
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
Requires-Dist: openlinktoken==2.0.0
|
|
12
|
+
Requires-Dist: pycryptodome==3.23.0
|
|
13
|
+
Requires-Dist: jwcrypto==1.5.7
|
|
14
|
+
Provides-Extra: spark41
|
|
15
|
+
Requires-Dist: pyspark==4.1.0; extra == "spark41"
|
|
16
|
+
Requires-Dist: pyarrow==24.0.0; extra == "spark41"
|
|
17
|
+
Requires-Dist: pandas==2.2.3; python_version < "3.11" and extra == "spark41"
|
|
18
|
+
Requires-Dist: pandas==3.0.3; python_version >= "3.11" and extra == "spark41"
|
|
19
|
+
Provides-Extra: spark40
|
|
20
|
+
Requires-Dist: pyspark==4.0.1; extra == "spark40"
|
|
21
|
+
Requires-Dist: pyarrow==24.0.0; extra == "spark40"
|
|
22
|
+
Requires-Dist: pandas==2.2.3; python_version < "3.11" and extra == "spark40"
|
|
23
|
+
Requires-Dist: pandas==3.0.3; python_version >= "3.11" and extra == "spark40"
|
|
24
|
+
Provides-Extra: spark35
|
|
25
|
+
Requires-Dist: pyspark==3.5.5; extra == "spark35"
|
|
26
|
+
Requires-Dist: pyarrow==19.0.0; extra == "spark35"
|
|
27
|
+
Requires-Dist: pandas==2.2.3; python_version < "3.11" and extra == "spark35"
|
|
28
|
+
Requires-Dist: pandas==3.0.3; python_version >= "3.11" and extra == "spark35"
|
|
29
|
+
Requires-Dist: setuptools==82.0.1; python_version >= "3.12" and extra == "spark35"
|
|
30
|
+
Provides-Extra: spark34
|
|
31
|
+
Requires-Dist: pyspark==3.4.4; extra == "spark34"
|
|
32
|
+
Requires-Dist: pyarrow==15.0.0; extra == "spark34"
|
|
33
|
+
Requires-Dist: pandas==2.1.4; python_version < "3.11" and extra == "spark34"
|
|
34
|
+
Requires-Dist: pandas==3.0.3; python_version >= "3.11" and extra == "spark34"
|
|
35
|
+
Requires-Dist: setuptools==82.0.1; python_version >= "3.12" and extra == "spark34"
|
|
36
|
+
Provides-Extra: dev
|
|
37
|
+
Requires-Dist: pytest==9.0.3; extra == "dev"
|
|
38
|
+
Requires-Dist: pytest-cov==7.1.0; extra == "dev"
|
|
39
|
+
Requires-Dist: flake8==7.3.0; extra == "dev"
|
|
40
|
+
Requires-Dist: jupyter==1.1.1; extra == "dev"
|
|
41
|
+
Requires-Dist: notebook==7.5.7; extra == "dev"
|
|
42
|
+
Requires-Dist: ipykernel==7.3.0; extra == "dev"
|
|
43
|
+
Dynamic: author
|
|
44
|
+
Dynamic: description
|
|
45
|
+
Dynamic: description-content-type
|
|
46
|
+
Dynamic: home-page
|
|
47
|
+
Dynamic: project-url
|
|
48
|
+
Dynamic: provides-extra
|
|
49
|
+
Dynamic: requires-dist
|
|
50
|
+
Dynamic: requires-python
|
|
51
|
+
Dynamic: summary
|
|
52
|
+
|
|
53
|
+
# Open Link Token
|
|
54
|
+
|
|
55
|
+
Privacy-preserving tokenization and matching library for secure PII-based person linkage. Open Link Token generates deterministic, cryptographically secure tokens from person attributes (name, birthdate, SSN, etc.) so datasets can be matched without exposing raw identifiers.
|
|
56
|
+
|
|
57
|
+
## Introduction
|
|
58
|
+
|
|
59
|
+
Our approach to record linkage relies on building a set of matching tokens (or token signatures) per person which are derived from deterministic person data but preserve privacy by using cryptographically secure hashing algorithms.
|
|
60
|
+
|
|
61
|
+
- [Open Link Token](#open-link-token)
|
|
62
|
+
- [Introduction](#introduction)
|
|
63
|
+
- [Highlights](#highlights)
|
|
64
|
+
- [Demo](#demo)
|
|
65
|
+
- [Overview](#overview)
|
|
66
|
+
- [Why Open Link Token](#why-open-link-token)
|
|
67
|
+
- [Quickstart](#quickstart)
|
|
68
|
+
- [Key Matching Ideas](#key-matching-ideas)
|
|
69
|
+
- [Running Open Link Token](#running-open-link-token)
|
|
70
|
+
- [Security Notes](#security-notes)
|
|
71
|
+
- [Contributing \& Community](#contributing--community)
|
|
72
|
+
- [Documentation](#documentation)
|
|
73
|
+
|
|
74
|
+
## Highlights
|
|
75
|
+
|
|
76
|
+
- Multi-language Support
|
|
77
|
+
- Cryptographically Secure encryption that prevents re-identification
|
|
78
|
+
- Enables straightforward person-matching by comparing 5 deterministic and unique hash values (after decryption), providing a high degree of confidence in matches
|
|
79
|
+
|
|
80
|
+
## Demo
|
|
81
|
+
|
|
82
|
+
New to Open Link Token? Start with the **[PPRL Superhero Demo](demos/pprl-superhero-example/)** — a beginner-friendly, end-to-end walkthrough showing how two parties (hospital and pharmacy) can privately find matching records without exposing raw identifiers.
|
|
83
|
+
|
|
84
|
+
The demo includes:
|
|
85
|
+
|
|
86
|
+
- **Interactive Jupyter notebook** with step-by-step explanations
|
|
87
|
+
- **One-command runner** (`run_end_to_end.sh`) for quick execution
|
|
88
|
+
- Synthetic superhero dataset generation
|
|
89
|
+
- Token generation and overlap analysis examples
|
|
90
|
+
|
|
91
|
+
Perfect for understanding privacy-preserving record linkage concepts before diving into production use.
|
|
92
|
+
|
|
93
|
+
## Overview
|
|
94
|
+
|
|
95
|
+
- **Multi-language parity**: Java and Python implementations produce byte-identical hash outputs (decrypted values)
|
|
96
|
+
- **Deterministic matching values**: Same input always produces the same cryptographically secure hash for matching
|
|
97
|
+
- **Privacy-preserving**: Encrypted tokens cannot be reversed to recover original person data
|
|
98
|
+
|
|
99
|
+
## Why Open Link Token
|
|
100
|
+
|
|
101
|
+
- Practical validation and normalization for common PII-derived attributes (names, birthdates, SSN, postal codes, sex)
|
|
102
|
+
- Secure pipeline: SHA-256 → HMAC-SHA256 → AES-256 (or hash-only mode)
|
|
103
|
+
- Multiple token rules (T1–T5) to increase match confidence across varied data quality
|
|
104
|
+
|
|
105
|
+
## Quickstart
|
|
106
|
+
|
|
107
|
+
**Self-contained executable (easiest):**
|
|
108
|
+
|
|
109
|
+
Download the binary for your platform from the [latest release](https://github.com/TruvetaPublic/OpenLinkToken/releases):
|
|
110
|
+
|
|
111
|
+
| Platform | Asset |
|
|
112
|
+
| ----------------------------- | ------------------------------- |
|
|
113
|
+
| Linux | `olt-vX.Y.Z-linux-x86_64` |
|
|
114
|
+
| macOS (Intel + Apple Silicon) | `olt-vX.Y.Z-macos-universal` |
|
|
115
|
+
| Windows | `olt-vX.Y.Z-windows-x86_64.exe` |
|
|
116
|
+
|
|
117
|
+
Each asset has a matching `.sha256` file you can use to verify the download.
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
# Linux
|
|
121
|
+
chmod +x olt-v*-linux-x86_64
|
|
122
|
+
mv olt-v*-linux-x86_64 olt
|
|
123
|
+
|
|
124
|
+
# macOS — make executable, clear Gatekeeper quarantine, and rename
|
|
125
|
+
chmod +x olt-v*-macos-universal
|
|
126
|
+
xattr -d com.apple.quarantine olt-v*-macos-universal
|
|
127
|
+
mv olt-v*-macos-universal olt
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Then run:
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
# Linux/macOS
|
|
134
|
+
# Simulate receiving the recipient's public key (in practice, your partner provides this)
|
|
135
|
+
./olt generate-key-pair --name recipient
|
|
136
|
+
# Create the exchange config using the recipient's public key
|
|
137
|
+
./olt initiate-exchange --public-key "$HOME/.openlinktoken/recipient.public.pem"
|
|
138
|
+
./olt package -i ./resources/sample.csv
|
|
139
|
+
|
|
140
|
+
# Windows
|
|
141
|
+
# Simulate receiving the recipient's public key (in practice, your partner provides this)
|
|
142
|
+
.\olt.exe generate-key-pair --name recipient
|
|
143
|
+
# Create the exchange config using the recipient's public key
|
|
144
|
+
.\olt.exe initiate-exchange --public-key "$HOME/.openlinktoken/recipient.public.pem"
|
|
145
|
+
.\olt.exe package -i .\resources\sample.csv
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
**Docker convenience scripts (Linux/macOS and Windows):**
|
|
149
|
+
|
|
150
|
+
```bash
|
|
151
|
+
# Linux/macOS
|
|
152
|
+
./run-olt.sh generate-key-pair --name recipient
|
|
153
|
+
./run-olt.sh initiate-exchange --public-key "$HOME/.openlinktoken/recipient.public.pem"
|
|
154
|
+
./run-olt.sh package \
|
|
155
|
+
-i ./resources/sample.csv -o ./resources/output.zip \
|
|
156
|
+
--exchange-config ./resources/openlinktoken.exchange.json \
|
|
157
|
+
--private-key "$HOME/.openlinktoken/recipient.private.pem"
|
|
158
|
+
|
|
159
|
+
# Windows
|
|
160
|
+
.\run-olt.ps1 generate-key-pair --name recipient
|
|
161
|
+
.\run-olt.ps1 initiate-exchange --public-key "$HOME\.openlinktoken\recipient.public.pem"
|
|
162
|
+
.\run-olt.ps1 package `
|
|
163
|
+
-i .\resources\sample.csv -o .\resources\output.zip `
|
|
164
|
+
--exchange-config .\resources\openlinktoken.exchange.json `
|
|
165
|
+
--private-key "$HOME\.openlinktoken\recipient.private.pem"
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
**Available Commands:**
|
|
169
|
+
|
|
170
|
+
- `olt package` - Generate and encrypt tokens in one step using the exchange config
|
|
171
|
+
- `olt tokenize` - Generate internal hashed tokens using the exchange config, or use `--mode hash-only` for deterministic SHA-256 output without an exchange config
|
|
172
|
+
- `olt encrypt` - Encrypt existing hashed tokens using the exchange config
|
|
173
|
+
- `olt decrypt` - Decrypt encrypted tokens using the exchange config
|
|
174
|
+
- `olt initiate-exchange` - Create the exchange config consumed by later commands
|
|
175
|
+
- `olt help [command]` - Show help for a specific command
|
|
176
|
+
|
|
177
|
+
See <a href="https://truvetapublic.github.io/OpenLinkToken/quickstarts/" target="_blank" rel="noopener noreferrer">Quickstarts</a> for Python CLI and detailed setup instructions.
|
|
178
|
+
|
|
179
|
+
## Key Matching Ideas
|
|
180
|
+
|
|
181
|
+
- **Token rules**: Five rules (T1–T5) combine attributes in different ways — see <a href="https://truvetapublic.github.io/OpenLinkToken/concepts/token-rules.html" target="_blank" rel="noopener noreferrer">Token Rules</a>
|
|
182
|
+
- **Normalization**: Names, dates, postal codes normalized before tokenization — see <a href="https://truvetapublic.github.io/OpenLinkToken/concepts/normalization-and-validation.html" target="_blank" rel="noopener noreferrer">Normalization and Validation</a>
|
|
183
|
+
- **Metadata**: Processing statistics and audit trail — see <a href="https://truvetapublic.github.io/OpenLinkToken/reference/metadata-format.html" target="_blank" rel="noopener noreferrer">Metadata Format</a>
|
|
184
|
+
|
|
185
|
+
## Running Open Link Token
|
|
186
|
+
|
|
187
|
+
- **CLI subcommands** (`olt` binary or Docker convenience scripts):
|
|
188
|
+
- `tokenize` - Internal hashed token generation (`--mode hash-only` is available for deterministic SHA-256 output)
|
|
189
|
+
- `encrypt` - Encrypt existing hashed tokens
|
|
190
|
+
- `decrypt` - Decrypt encrypted tokens
|
|
191
|
+
- `package` - Tokenize + encrypt in one step (recommended)
|
|
192
|
+
- `initiate-exchange` - Create an exchange config from a partner's public key
|
|
193
|
+
- `generate-key-pair` - Generate an ECDH key pair
|
|
194
|
+
- See <a href="https://truvetapublic.github.io/OpenLinkToken/running-openlinktoken/" target="_blank" rel="noopener noreferrer">Running Open Link Token</a>
|
|
195
|
+
- **Docker convenience scripts**: `run-olt.sh` (Linux/macOS) and `run-olt.ps1` (Windows) wrap Docker automatically — see <a href="https://truvetapublic.github.io/OpenLinkToken/quickstarts/" target="_blank" rel="noopener noreferrer">Quickstarts</a>
|
|
196
|
+
- **PySpark**: Distributed processing for large datasets — see <a href="https://truvetapublic.github.io/OpenLinkToken/operations/spark-or-databricks.html" target="_blank" rel="noopener noreferrer">Spark or Databricks</a>
|
|
197
|
+
|
|
198
|
+
## Security Notes
|
|
199
|
+
|
|
200
|
+
- **Crypto pipeline**: Token signature → SHA-256 → HMAC-SHA256 → AES-256 (or hash-only) — see <a href="https://truvetapublic.github.io/OpenLinkToken/security.html" target="_blank" rel="noopener noreferrer">Security</a>
|
|
201
|
+
- **`tokenize --mode hash-only`**: Deterministic SHA-256 output with no exchange config or secret. Useful for local exploration, but **not** for production or cross-organisation exchange
|
|
202
|
+
- **Secret management**: Handle hashing/encryption secrets securely; avoid committing secrets; prefer env/secret stores
|
|
203
|
+
- **Validation**: Reject placeholders and malformed attributes before tokenization
|
|
204
|
+
|
|
205
|
+
## Contributing & Community
|
|
206
|
+
|
|
207
|
+
- <a href="https://truvetapublic.github.io/OpenLinkToken/community/contributing.html" target="_blank" rel="noopener noreferrer">Contributing Guide</a> — Branching, PR expectations, coding standards
|
|
208
|
+
- <a href="https://truvetapublic.github.io/OpenLinkToken/community/code-of-conduct.html" target="_blank" rel="noopener noreferrer">Code of Conduct</a>
|
|
209
|
+
|
|
210
|
+
## Documentation
|
|
211
|
+
|
|
212
|
+
- <a href="https://truvetapublic.github.io/OpenLinkToken/" target="_blank" rel="noopener noreferrer">Documentation Index</a>
|
|
213
|
+
- <a href="https://truvetapublic.github.io/OpenLinkToken/quickstarts/" target="_blank" rel="noopener noreferrer">Quickstarts</a>
|
|
214
|
+
- <a href="https://truvetapublic.github.io/OpenLinkToken/specification.html" target="_blank" rel="noopener noreferrer">Specification</a>
|
|
215
|
+
- <a href="https://truvetapublic.github.io/OpenLinkToken/reference/cli.html" target="_blank" rel="noopener noreferrer">CLI Reference</a>
|
|
216
|
+
- <a href="https://truvetapublic.github.io/OpenLinkToken/reference/metadata-format.html" target="_blank" rel="noopener noreferrer">Metadata Format</a>
|
|
217
|
+
|
|
218
|
+
For issues or support, file an issue in this repository.
|
|
219
|
+
|
|
220
|
+
<!-- Re-run CI checks -->
|
|
@@ -0,0 +1,448 @@
|
|
|
1
|
+
# Open Link Token PySpark Bridge
|
|
2
|
+
|
|
3
|
+
A PySpark integration for the Open Link Token library, enabling distributed privacy-preserving token generation for large-scale record linkage workflows.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
The Open Link Token PySpark Bridge provides a seamless interface between PySpark DataFrames and the Open Link Token library, allowing you to generate cryptographically secure tokens for record linkage in a distributed computing environment.
|
|
8
|
+
|
|
9
|
+
## Features
|
|
10
|
+
|
|
11
|
+
- **Distributed Processing**: Leverage PySpark's distributed computing capabilities for large datasets
|
|
12
|
+
- **Simple API**: Easy-to-use interface that accepts PySpark DataFrames
|
|
13
|
+
- **Exchange-Config Ready**: Recommended initiate-exchange flow with direct-secret support when you manage raw secrets yourself
|
|
14
|
+
- **Flexible Column Names**: Supports multiple column name variants (e.g., FirstName/GivenName)
|
|
15
|
+
- **Jupyter Ready**: Includes example notebooks for interactive exploration
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
### Prerequisites
|
|
20
|
+
|
|
21
|
+
- Python 3.10 or higher
|
|
22
|
+
- Open Link Token core library (automatically installed as dependency)
|
|
23
|
+
- Apache Spark or PySpark environment
|
|
24
|
+
|
|
25
|
+
### Version Compatibility
|
|
26
|
+
|
|
27
|
+
Open Link Token PySpark supports multiple Spark versions to accommodate different Java environments and cluster configurations:
|
|
28
|
+
|
|
29
|
+
| Spark Version | PySpark Version | PyArrow Version | Pandas Version | Java Version | Installation Extra |
|
|
30
|
+
| ------------- | --------------- | --------------- | -------------- | ------------ | ----------------------------- |
|
|
31
|
+
| **4.0.x** | >=4.0.1, <5.0 | >=17.0.0, <18.0 | >=2.0.0, <2.4 | **21** | `[spark40]` **(Recommended)** |
|
|
32
|
+
| 4.1.x | >=4.1.0, <5.0 | >=17.0.0, <18.0 | >=2.0.0, <2.4 | **21** | `[spark41]` |
|
|
33
|
+
| 3.5.x | >=3.5.0, <3.6 | >=15.0.0, <20 | >=1.5, <2.3 | 8-17 | `[spark35]` |
|
|
34
|
+
| 3.4.x | >=3.4.0, <3.5 | >=10.0.0, <15 | >=1.5, <2.2 | 8-17 | `[spark34]` |
|
|
35
|
+
|
|
36
|
+
**Important:**
|
|
37
|
+
|
|
38
|
+
- PySpark 3.5.x and earlier are **NOT compatible** with Java 21
|
|
39
|
+
- If you're using Java 21, you **must** use PySpark 4.0.x+ or 4.1.x+ (Spark 4.0.x recommended)
|
|
40
|
+
- For managed clusters (Databricks, EMR, Azure Synapse), PySpark is typically pre-installed
|
|
41
|
+
|
|
42
|
+
### Installation Options
|
|
43
|
+
|
|
44
|
+
#### Option 1: Spark 4.0+ (Recommended - Java 21)
|
|
45
|
+
|
|
46
|
+
For local development or clusters with Java 21:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
uv pip install openlinktoken-pyspark[spark40]
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
#### Option 2: Spark 3.5.x (Java 8-17)
|
|
53
|
+
|
|
54
|
+
For clusters still using Java 8-17 and Spark 3.5.x:
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
uv pip install openlinktoken-pyspark[spark35]
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
#### Option 3: Spark 3.4.x (Legacy)
|
|
61
|
+
|
|
62
|
+
For older Spark 3.4.x clusters:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
uv pip install openlinktoken-pyspark[spark34]
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
#### Option 4: Managed Cluster Environments
|
|
69
|
+
|
|
70
|
+
For environments where PySpark is pre-installed (Databricks, EMR, etc.):
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
uv pip install openlinktoken-pyspark
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
This installs only the core dependencies without PySpark. You'll use the cluster's PySpark installation.
|
|
77
|
+
|
|
78
|
+
### Development Setup
|
|
79
|
+
|
|
80
|
+
For local development with editable install:
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
# Install Open Link Token core library first
|
|
84
|
+
cd lib/python/openlinktoken
|
|
85
|
+
uv pip install -e .
|
|
86
|
+
|
|
87
|
+
# Install PySpark bridge with Spark 4.0 dependencies
|
|
88
|
+
cd ../openlinktoken-pyspark
|
|
89
|
+
uv pip install -e .[spark40,dev]
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
The `dev` extra includes testing and notebook dependencies (pytest, jupyter, etc.).
|
|
93
|
+
|
|
94
|
+
## Quick Start
|
|
95
|
+
|
|
96
|
+
### Basic Usage (All Spark Versions)
|
|
97
|
+
|
|
98
|
+
The recommended production flow resolves hashing and transport secrets from an initiate-exchange config on the driver:
|
|
99
|
+
|
|
100
|
+
```python
|
|
101
|
+
import sys
|
|
102
|
+
import os
|
|
103
|
+
from pyspark.sql import SparkSession
|
|
104
|
+
from openlinktoken_pyspark import OpenLinkTokenProcessor
|
|
105
|
+
|
|
106
|
+
# Create Spark session (PySpark 4.0.1+ handles Java 21 natively)
|
|
107
|
+
spark = SparkSession.builder \
|
|
108
|
+
.appName("OpenLinkTokenExample") \
|
|
109
|
+
.master("local[*]") \
|
|
110
|
+
.config("spark.executorEnv.PYTHONPATH", os.pathsep.join(sys.path)) \
|
|
111
|
+
.getOrCreate()
|
|
112
|
+
|
|
113
|
+
# Load your data
|
|
114
|
+
df = spark.read.csv("data.csv", header=True)
|
|
115
|
+
|
|
116
|
+
# Initialize the processor from an initiate-exchange config
|
|
117
|
+
processor = OpenLinkTokenProcessor.from_exchange_config(
|
|
118
|
+
exchange_config_path="initiate-exchange-config.json",
|
|
119
|
+
private_key_path="participant-private-key.pem",
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
# Generate tokens
|
|
123
|
+
tokens_df = processor.process_dataframe(df)
|
|
124
|
+
|
|
125
|
+
# View results
|
|
126
|
+
tokens_df.show()
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
`from_exchange_config(...)` resolves the exchange config and private key on the driver, then Spark workers receive only the derived byte payloads needed for token generation.
|
|
130
|
+
|
|
131
|
+
### Azure Key Vault Example
|
|
132
|
+
|
|
133
|
+
When your Spark job runs on Azure, keep the vault lookup on the driver and pass the resolved inputs into
|
|
134
|
+
`from_exchange_config(...)`. In the PySpark bridge, the direct inputs are still the initiate-exchange config JSON plus the
|
|
135
|
+
participant private key. The sender and recipient public keys are already embedded in the generated exchange config payload,
|
|
136
|
+
so you normally keep them in Key Vault for exchange creation, rotation, or validation rather than passing them directly to
|
|
137
|
+
`OpenLinkTokenProcessor`.
|
|
138
|
+
|
|
139
|
+
```python
|
|
140
|
+
import os
|
|
141
|
+
|
|
142
|
+
from azure.identity import DefaultAzureCredential
|
|
143
|
+
from azure.keyvault.secrets import SecretClient
|
|
144
|
+
from openlinktoken_pyspark import OpenLinkTokenProcessor
|
|
145
|
+
|
|
146
|
+
vault = SecretClient(
|
|
147
|
+
vault_url=os.environ["AZURE_KEY_VAULT_URL"],
|
|
148
|
+
credential=DefaultAzureCredential(),
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
exchange_config_json = vault.get_secret("openlinktoken-initiate-exchange-config").value
|
|
152
|
+
participant_private_key_pem = vault.get_secret("openlinktoken-participant-private-key-pem").value
|
|
153
|
+
|
|
154
|
+
# Optional: keep the public keys in Key Vault for exchange creation/rotation or validation.
|
|
155
|
+
sender_public_key_pem = vault.get_secret("openlinktoken-sender-public-key-pem").value
|
|
156
|
+
recipient_public_key_pem = vault.get_secret("openlinktoken-recipient-public-key-pem").value
|
|
157
|
+
|
|
158
|
+
processor = OpenLinkTokenProcessor.from_exchange_config(
|
|
159
|
+
exchange_config_value=exchange_config_json,
|
|
160
|
+
private_key_value=participant_private_key_pem,
|
|
161
|
+
)
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
If Azure Key Vault is your system of record for the public keys, use those values when you create or rotate the
|
|
165
|
+
initiate-exchange config upstream. The PySpark bridge does not take public-key arguments directly.
|
|
166
|
+
|
|
167
|
+
### Java 8-17 Setup (PySpark 3.5.x)
|
|
168
|
+
|
|
169
|
+
If you're using Java 8-17 and cannot upgrade to Java 21:
|
|
170
|
+
|
|
171
|
+
```python
|
|
172
|
+
import sys
|
|
173
|
+
import os
|
|
174
|
+
from pyspark.sql import SparkSession
|
|
175
|
+
from openlinktoken_pyspark import OpenLinkTokenProcessor
|
|
176
|
+
|
|
177
|
+
# Create Spark session (PySpark 3.5.x with PyArrow <20)
|
|
178
|
+
spark = SparkSession.builder \
|
|
179
|
+
.appName("OpenLinkTokenExample") \
|
|
180
|
+
.master("local[*]") \
|
|
181
|
+
.config("spark.executorEnv.PYTHONPATH", os.pathsep.join(sys.path)) \
|
|
182
|
+
.getOrCreate()
|
|
183
|
+
|
|
184
|
+
# Load your data
|
|
185
|
+
df = spark.read.csv("data.csv", header=True)
|
|
186
|
+
|
|
187
|
+
# Initialize the processor from an initiate-exchange config
|
|
188
|
+
processor = OpenLinkTokenProcessor.from_exchange_config(
|
|
189
|
+
exchange_config_path="initiate-exchange-config.json",
|
|
190
|
+
private_key_path="participant-private-key.pem",
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
# Generate tokens
|
|
194
|
+
tokens_df = processor.process_dataframe(df)
|
|
195
|
+
|
|
196
|
+
# View results
|
|
197
|
+
tokens_df.show()
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
**Note:** Ensure you have PyArrow <20 installed: `uv pip install 'pyarrow>=15.0.0,<20'`
|
|
201
|
+
|
|
202
|
+
### Databricks Example
|
|
203
|
+
|
|
204
|
+
```python
|
|
205
|
+
from openlinktoken_pyspark import OpenLinkTokenProcessor
|
|
206
|
+
|
|
207
|
+
# Load data from Delta table or CSV
|
|
208
|
+
df = spark.read.table("my_database.person_records")
|
|
209
|
+
|
|
210
|
+
# Resolve private key material on the driver before processing
|
|
211
|
+
participant_private_key_pem = dbutils.secrets.get(
|
|
212
|
+
"openlinktoken",
|
|
213
|
+
"participant_private_key_pem",
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
processor = OpenLinkTokenProcessor.from_exchange_config(
|
|
217
|
+
exchange_config_path="/dbfs/FileStore/openlinktoken/initiate-exchange-config.json",
|
|
218
|
+
private_key_value=participant_private_key_pem,
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
# Generate tokens
|
|
222
|
+
tokens_df = processor.process_dataframe(df)
|
|
223
|
+
|
|
224
|
+
# Save results
|
|
225
|
+
tokens_df.write.mode("overwrite").saveAsTable("my_database.person_tokens")
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
### Direct Secret Usage
|
|
229
|
+
|
|
230
|
+
If you already inject raw secrets into your Spark application, the direct constructor remains supported:
|
|
231
|
+
|
|
232
|
+
```python
|
|
233
|
+
from openlinktoken_pyspark import OpenLinkTokenProcessor
|
|
234
|
+
|
|
235
|
+
processor = OpenLinkTokenProcessor(
|
|
236
|
+
hashing_secret="your-hashing-secret",
|
|
237
|
+
encryption_key="0123456789abcdef0123456789abcdef",
|
|
238
|
+
)
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
Use this path only when you intentionally manage raw secrets yourself. New integrations should prefer `from_exchange_config(...)`.
|
|
242
|
+
|
|
243
|
+
## Input DataFrame Requirements
|
|
244
|
+
|
|
245
|
+
Your input DataFrame must contain the following columns (alternative names are supported):
|
|
246
|
+
|
|
247
|
+
| Standard Name | Alternative Names | Description |
|
|
248
|
+
| -------------------- | ---------------------------- | ------------------------------------------------------------- |
|
|
249
|
+
| RecordId | Id | Unique identifier (optional - auto-generated if not provided) |
|
|
250
|
+
| FirstName | GivenName | Person's first name |
|
|
251
|
+
| LastName | Surname | Person's last name |
|
|
252
|
+
| BirthDate | DateOfBirth | Date of birth in YYYY-MM-DD format |
|
|
253
|
+
| Sex | Gender | Sex/Gender (Male, Female, M, F) |
|
|
254
|
+
| PostalCode | ZipCode | US ZIP code or Canadian postal code |
|
|
255
|
+
| SocialSecurityNumber | NationalIdentificationNumber | SSN or national ID number |
|
|
256
|
+
|
|
257
|
+
## Output Format
|
|
258
|
+
|
|
259
|
+
The output DataFrame contains:
|
|
260
|
+
|
|
261
|
+
- **RecordId**: The original record identifier
|
|
262
|
+
- **RuleId**: Token rule identifier (T1, T2, T3, T4, T5)
|
|
263
|
+
- **Token**: Encrypted match token in `olt.V1.<JWE>` format (or base64-encoded HMAC in hash-only flows)
|
|
264
|
+
|
|
265
|
+
Each input record produces multiple output rows (one per token rule).
|
|
266
|
+
|
|
267
|
+
## Using Custom Token Definitions
|
|
268
|
+
|
|
269
|
+
You can define custom tokens using the `openlinktoken_pyspark.notebook_helpers` module and pass them to the processor:
|
|
270
|
+
|
|
271
|
+
```python
|
|
272
|
+
from openlinktoken_pyspark import (
|
|
273
|
+
CustomTokenDefinition,
|
|
274
|
+
OpenLinkTokenProcessor,
|
|
275
|
+
TokenBuilder,
|
|
276
|
+
quick_token_from_exchange_config,
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
# Method 1: Using TokenBuilder
|
|
280
|
+
custom_token = TokenBuilder("T6") \
|
|
281
|
+
.add("last_name", "T|U") \
|
|
282
|
+
.add("first_name", "T|U") \
|
|
283
|
+
.add("birth_date", "T|D") \
|
|
284
|
+
.add("postal_code", "T|S(0,3)") \
|
|
285
|
+
.add("sex", "T|U") \
|
|
286
|
+
.build()
|
|
287
|
+
|
|
288
|
+
custom_definition = CustomTokenDefinition().add_token(custom_token)
|
|
289
|
+
|
|
290
|
+
# Create a processor with the custom definition
|
|
291
|
+
processor = OpenLinkTokenProcessor.from_exchange_config(
|
|
292
|
+
exchange_config_path="initiate-exchange-config.json",
|
|
293
|
+
private_key_path="participant-private-key.pem",
|
|
294
|
+
token_definition=custom_definition,
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
# Process DataFrame - will use T6 instead of default T1-T5
|
|
298
|
+
tokens_df = processor.process_dataframe(df)
|
|
299
|
+
|
|
300
|
+
# For one-off notebook exploration, the package root also exports
|
|
301
|
+
# create_token_generator_from_exchange_config(...) and quick_token_from_exchange_config(...)
|
|
302
|
+
generator = quick_token_from_exchange_config(
|
|
303
|
+
"T7",
|
|
304
|
+
[("last_name", "T|U"), ("first_name", "T|U"), ("birth_date", "T|D")],
|
|
305
|
+
exchange_config_path="initiate-exchange-config.json",
|
|
306
|
+
private_key_path="participant-private-key.pem",
|
|
307
|
+
)
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
For more examples and interactive experimentation with custom tokens, see the [Custom Token Definition Guide](notebooks/Custom_Token_Definition_Guide.ipynb).
|
|
311
|
+
|
|
312
|
+
## Example Notebooks
|
|
313
|
+
|
|
314
|
+
See the included Jupyter notebooks for complete examples:
|
|
315
|
+
|
|
316
|
+
**Basic Usage:**
|
|
317
|
+
|
|
318
|
+
```bash
|
|
319
|
+
cd notebooks
|
|
320
|
+
jupyter notebook OpenLinkToken_PySpark_Example.ipynb
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
**Custom Token Definitions:**
|
|
324
|
+
|
|
325
|
+
```bash
|
|
326
|
+
cd notebooks
|
|
327
|
+
jupyter notebook Custom_Token_Definition_Guide.ipynb
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
**Dataset Overlap Analysis:**
|
|
331
|
+
|
|
332
|
+
```bash
|
|
333
|
+
cd notebooks
|
|
334
|
+
jupyter notebook Dataset_Overlap_Analysis_Guide.ipynb
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
## Dataset Overlap Analysis
|
|
338
|
+
|
|
339
|
+
The `OpenLinkTokenOverlapAnalyzer` class helps identify matching records between two tokenized datasets by decrypting tokens to deterministic values before comparison.
|
|
340
|
+
|
|
341
|
+
### Basic Usage
|
|
342
|
+
|
|
343
|
+
```python
|
|
344
|
+
from openlinktoken_pyspark import OpenLinkTokenOverlapAnalyzer
|
|
345
|
+
|
|
346
|
+
# Initialize from the same initiate-exchange config used for token generation
|
|
347
|
+
analyzer = OpenLinkTokenOverlapAnalyzer.from_exchange_config(
|
|
348
|
+
exchange_config_path="initiate-exchange-config.json",
|
|
349
|
+
private_key_path="participant-private-key.pem",
|
|
350
|
+
)
|
|
351
|
+
|
|
352
|
+
# Analyze overlap between two tokenized datasets
|
|
353
|
+
# Match on tokens T1 and T2 (both must match)
|
|
354
|
+
results = analyzer.analyze_overlap(
|
|
355
|
+
tokens_df1,
|
|
356
|
+
tokens_df2,
|
|
357
|
+
matching_rules=["T1", "T2"],
|
|
358
|
+
dataset1_name="Hospital_A",
|
|
359
|
+
dataset2_name="Hospital_B"
|
|
360
|
+
)
|
|
361
|
+
|
|
362
|
+
# Print summary
|
|
363
|
+
analyzer.print_summary(results)
|
|
364
|
+
|
|
365
|
+
# Access detailed results
|
|
366
|
+
print(f"Total records in dataset 1: {results['total_records_dataset1']}")
|
|
367
|
+
print(f"Matching records: {results['matching_records_dataset1']}")
|
|
368
|
+
print(f"Overlap percentage: {results['overlap_percentage']:.2f}%")
|
|
369
|
+
|
|
370
|
+
# Get DataFrame of matched record pairs
|
|
371
|
+
matches_df = results['matches']
|
|
372
|
+
matches_df.show()
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
### Compare with Multiple Rule Sets
|
|
376
|
+
|
|
377
|
+
```python
|
|
378
|
+
# Compare overlap using different matching criteria
|
|
379
|
+
rule_sets = [
|
|
380
|
+
["T1"], # Match on T1 only
|
|
381
|
+
["T1", "T2"], # Match on T1 AND T2
|
|
382
|
+
["T1", "T2", "T3"] # Match on T1 AND T2 AND T3
|
|
383
|
+
]
|
|
384
|
+
|
|
385
|
+
results = analyzer.compare_with_multiple_rules(
|
|
386
|
+
tokens_df1, tokens_df2, rule_sets
|
|
387
|
+
)
|
|
388
|
+
|
|
389
|
+
# See how overlap changes with stricter rules
|
|
390
|
+
for result in results:
|
|
391
|
+
print(f"Rules {result['matching_rules']}: "
|
|
392
|
+
f"{result['overlap_percentage']:.2f}% overlap")
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
### Use Cases
|
|
396
|
+
|
|
397
|
+
- **Data Quality Assessment**: Identify duplicate records across datasets
|
|
398
|
+
- **Patient Matching**: Find matching patients between healthcare systems
|
|
399
|
+
- **Research Cohort Overlap**: Analyze overlap between research study populations
|
|
400
|
+
- **Data Sharing Analysis**: Assess data overlap before establishing data sharing agreements
|
|
401
|
+
|
|
402
|
+
### How It Works
|
|
403
|
+
|
|
404
|
+
1. Both datasets must contain tokenized records (RecordId, RuleId, Token columns)
|
|
405
|
+
2. Matching rules specify which token types must match (e.g., ["T1", "T2"])
|
|
406
|
+
3. Records are considered matching only if ALL specified token types match
|
|
407
|
+
4. The analyzer provides statistics and a DataFrame of matched record pairs
|
|
408
|
+
5. Uses the exchange-derived transport key that was used to generate the tokens, supporting both `olt.V1` JWE tokens and legacy encrypted token format
|
|
409
|
+
|
|
410
|
+
## Testing
|
|
411
|
+
|
|
412
|
+
Run the test suite:
|
|
413
|
+
|
|
414
|
+
```bash
|
|
415
|
+
# From the openlinktoken-pyspark directory
|
|
416
|
+
pytest
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
## Performance Considerations
|
|
420
|
+
|
|
421
|
+
- **Partitioning**: PySpark processes data in parallel across partitions. Adjust `spark.sql.shuffle.partitions` for your cluster size.
|
|
422
|
+
- **Memory**: Token generation is memory-efficient but ensure adequate executor memory for your data volume.
|
|
423
|
+
- **Secret Resolution**: Exchange configs and private keys are resolved on the driver; workers receive only the derived hashing/encryption bytes.
|
|
424
|
+
- **Secrets on Workers**: Derived secret bytes are still serialized to worker nodes, so ensure secure cluster configuration.
|
|
425
|
+
|
|
426
|
+
## Architecture
|
|
427
|
+
|
|
428
|
+
The PySpark bridge uses Pandas UDFs (User Defined Functions) to efficiently process batches of records:
|
|
429
|
+
|
|
430
|
+
1. Data is partitioned across the Spark cluster
|
|
431
|
+
2. Each partition is processed by a Pandas UDF
|
|
432
|
+
3. Within each batch, the Open Link Token library generates tokens
|
|
433
|
+
4. Results are collected back into a PySpark DataFrame
|
|
434
|
+
|
|
435
|
+
This architecture balances the benefits of distributed computing with the cryptographic requirements of token generation.
|
|
436
|
+
|
|
437
|
+
## Security Notes
|
|
438
|
+
|
|
439
|
+
- **Secrets Management**: Prefer initiate-exchange configs plus secure private-key delivery (for example AWS Secrets Manager or Azure Key Vault)
|
|
440
|
+
- **Network Security**: Ensure secure communication between Spark nodes
|
|
441
|
+
- **Data Privacy**: Generated tokens are cryptographically secure and cannot be reversed to original values
|
|
442
|
+
|
|
443
|
+
## Related Documentation
|
|
444
|
+
|
|
445
|
+
- [Open Link Token Core Library](../openlinktoken/) - Python core implementation
|
|
446
|
+
- [Open Link Token CLI](../openlinktoken-cli/) - Python CLI with I/O support (CSV, Parquet)
|
|
447
|
+
- [Main Open Link Token Documentation](../../../README.md) - Project overview and setup
|
|
448
|
+
- [Development Guide](../../../docs/dev-guide-development.md) - Contributor documentation
|