triplexanalytics 0.2.2__cp39-abi3-win_amd64.whl

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.
triplex/__init__.py ADDED
@@ -0,0 +1,170 @@
1
+ """Triplex Analytics in plain Python: schema contracts, anonymization and synthetic data.
2
+
3
+ The engine is the same native code the `triplex` command line runs. Everything happens in this
4
+ process, on this machine; nothing is sent anywhere.
5
+
6
+ A table goes in as a file path, a pandas or Polars DataFrame, or anything else Arrow can read.
7
+ A result table comes back as a `Table`, which converts to whichever of those you use.
8
+ """
9
+
10
+ import io
11
+ import json
12
+ import os
13
+ from dataclasses import dataclass
14
+
15
+ from . import _native
16
+ from ._native import TriplexError
17
+
18
+ __all__ = [
19
+ "Result",
20
+ "Table",
21
+ "TriplexError",
22
+ "anonymize",
23
+ "convert",
24
+ "draft_contract",
25
+ "infer_schema",
26
+ "synthesize",
27
+ "validate",
28
+ ]
29
+
30
+ # Rows read from a file to infer its schema, as in the command line. A table passed in memory
31
+ # is always read in full.
32
+ SAMPLE_ROWS = 1000
33
+
34
+
35
+ class Table:
36
+ """A result table, held as Arrow IPC stream bytes until you ask for it in a library's form."""
37
+
38
+ def __init__(self, ipc):
39
+ self.ipc = ipc
40
+
41
+ def to_arrow(self):
42
+ import pyarrow as pa
43
+
44
+ return pa.ipc.open_stream(self.ipc).read_all()
45
+
46
+ def to_pandas(self):
47
+ return self.to_arrow().to_pandas()
48
+
49
+ def to_polars(self):
50
+ import polars as pl
51
+
52
+ return pl.read_ipc_stream(io.BytesIO(self.ipc))
53
+
54
+ def __arrow_c_stream__(self, requested_schema=None):
55
+ return self.to_arrow().__arrow_c_stream__(requested_schema)
56
+
57
+
58
+ @dataclass(frozen=True)
59
+ class Result:
60
+ """What an anonymize or synthesize run produced: the table and the run's report."""
61
+
62
+ table: Table
63
+ report: dict
64
+
65
+
66
+ def _input(data):
67
+ """`(path, ipc)` for the engine: a path stays a path, a table becomes Arrow IPC bytes."""
68
+ if isinstance(data, (str, os.PathLike)):
69
+ return os.fspath(data), None
70
+ if isinstance(data, Table):
71
+ return None, data.ipc
72
+ if hasattr(data, "write_ipc_stream"): # Polars, which needs no pyarrow for this
73
+ buffer = io.BytesIO()
74
+ data.write_ipc_stream(buffer)
75
+ return None, buffer.getvalue()
76
+
77
+ import pyarrow as pa
78
+
79
+ if type(data).__module__.partition(".")[0] == "pandas":
80
+ # The row index is not a column of the dataset.
81
+ table = pa.Table.from_pandas(data, preserve_index=False)
82
+ else:
83
+ table = pa.table(data)
84
+ sink = pa.BufferOutputStream()
85
+ with pa.ipc.new_stream(sink, table.schema) as writer:
86
+ writer.write_table(table)
87
+ return None, sink.getvalue().to_pybytes()
88
+
89
+
90
+ def infer_schema(data, *, sample_rows=SAMPLE_ROWS, delimiter=None):
91
+ """Infer the schema of `data` and return it as a dict (what `triplex schema` prints).
92
+
93
+ `delimiter` is the field separator of a `.csv` path that does not use a comma.
94
+ """
95
+ path, ipc = _input(data)
96
+ return json.loads(_native.infer_schema(path, ipc, sample_rows, delimiter))
97
+
98
+
99
+ def draft_contract(data, *, output=None, sample_rows=SAMPLE_ROWS, delimiter=None):
100
+ """A starting contract for `data`: every column with the type found in it, all required.
101
+
102
+ Returns the contract as a dict, which `validate` accepts. `output` also writes it as a
103
+ `.triplex.yaml` file. A draft only describes the table it was made from: add the limits you
104
+ expect (null rate, value range, mean, pattern) before you rely on it.
105
+ """
106
+ path, ipc = _input(data)
107
+ contract, yaml = _native.draft_contract(path, ipc, sample_rows, delimiter)
108
+ if output is not None:
109
+ with open(output, "w", encoding="utf-8") as file:
110
+ file.write(yaml)
111
+ return json.loads(contract)
112
+
113
+
114
+ def validate(data, contract, *, sample_rows=SAMPLE_ROWS, delimiter=None):
115
+ """Check `data` against `contract`: a `.triplex.yaml` path, or a contract as a dict.
116
+
117
+ A dict is what `draft_contract` returns, or the same shape built or edited in Python.
118
+ Returns the validation report: `{"valid": bool, "errors": [...]}`.
119
+ """
120
+ if isinstance(contract, dict):
121
+ if any("inferred_type" in column for column in contract.get("columns", ())):
122
+ # The two dicts look alike, but an inferred schema describes one table as it is and
123
+ # sets no expectation to check against.
124
+ raise TypeError(
125
+ "this is an inferred schema, not a contract; "
126
+ "pass triplex.draft_contract(data) to check against the same columns and types"
127
+ )
128
+ contract_path, contract_text = None, json.dumps(contract)
129
+ else:
130
+ contract_path, contract_text = os.fspath(contract), None
131
+ path, ipc = _input(data)
132
+ return json.loads(
133
+ _native.validate(contract_path, contract_text, path, ipc, sample_rows, delimiter)
134
+ )
135
+
136
+
137
+ def convert(input, output, *, input_delimiter=None, output_delimiter=None):
138
+ """Convert the file at `input` to the format of `output`'s extension. Returns the report."""
139
+ return json.loads(_native.convert(input, output, input_delimiter, output_delimiter))
140
+
141
+
142
+ def anonymize(
143
+ data, recipe, *, mapping_out, output=None, mapping_passphrase_env=None, exclude_rows=()
144
+ ):
145
+ """Mask the columns the `.triplex.yaml` recipe at `recipe` declares.
146
+
147
+ Each salt is read from the environment variable its recipe column names. The mapping file,
148
+ which can reverse the result, is written to `mapping_out` and is not returned; name an
149
+ environment variable in `mapping_passphrase_env` to have it encrypted. `output` also writes
150
+ the masked table as CSV. `exclude_rows` are 0-based rows to leave out.
151
+
152
+ The result is pseudonymized, not anonymous: see `report["warning"]`.
153
+ """
154
+ path, ipc = _input(data)
155
+ table, report = _native.anonymize(
156
+ recipe, mapping_out, path, ipc, output, mapping_passphrase_env, set(exclude_rows)
157
+ )
158
+ return Result(Table(table), json.loads(report))
159
+
160
+
161
+ def synthesize(data, *, rows=None, seed=42, min_category_count=5, method="copula", output=None):
162
+ """Generate a synthetic table shaped like `data`.
163
+
164
+ `method` is `"copula"`, `"bayes-net"` or `"marginals"`. None of the three carries a formal
165
+ privacy guarantee: read `report["privacy_statement"]` and the leakage figures in the report.
166
+ `rows` defaults to as many as the input has. `output` also writes the table as CSV.
167
+ """
168
+ path, ipc = _input(data)
169
+ table, report = _native.synthesize(path, ipc, output, rows, seed, min_category_count, method)
170
+ return Result(Table(table), json.loads(report))
triplex/_native.pyd ADDED
Binary file
@@ -0,0 +1,135 @@
1
+ Metadata-Version: 2.4
2
+ Name: triplexanalytics
3
+ Version: 0.2.2
4
+ Classifier: Development Status :: 3 - Alpha
5
+ Classifier: Intended Audience :: Developers
6
+ Classifier: Intended Audience :: Science/Research
7
+ Classifier: Operating System :: OS Independent
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Programming Language :: Rust
10
+ Classifier: Topic :: Scientific/Engineering :: Information Analysis
11
+ Classifier: Topic :: Security
12
+ Requires-Dist: pytest ; extra == 'dev'
13
+ Requires-Dist: pyarrow ; extra == 'dev'
14
+ Requires-Dist: pandas ; extra == 'dev'
15
+ Requires-Dist: polars ; extra == 'dev'
16
+ Provides-Extra: dev
17
+ Summary: Triplex Analytics in plain Python: schema contracts, anonymization and synthetic data, run locally.
18
+ Keywords: privacy,anonymization,pseudonymization,synthetic-data,schema
19
+ License-Expression: Apache-2.0
20
+ Requires-Python: >=3.9
21
+ Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
22
+ Project-URL: Changelog, https://github.com/triplexanalytics/triplex/blob/main/CHANGELOG.md
23
+ Project-URL: Documentation, https://triplexanalytics.io/docs/python.html
24
+ Project-URL: Homepage, https://triplexanalytics.io
25
+ Project-URL: Source, https://github.com/triplexanalytics/triplex
26
+
27
+ # triplex-python
28
+
29
+ Triplex Analytics in plain Python: infer and check schema contracts, anonymize a table, generate
30
+ synthetic data. No notebook, no browser, no server. The engine is the same native code the
31
+ `triplex` command line runs.
32
+
33
+ Not published yet. Install from a source checkout (needs the Rust toolchain):
34
+
35
+ ```sh
36
+ cd packages/triplex-python
37
+ pip install maturin
38
+ maturin develop --release
39
+ ```
40
+
41
+ ## Use
42
+
43
+ ```python
44
+ import pandas as pd
45
+ import triplex
46
+
47
+ # 1. Check the raw export against the contract you keep in version control.
48
+ raw = pd.read_csv("customers.csv")
49
+ report = triplex.validate(raw, "customers.triplex.yaml")
50
+ if not report["valid"]:
51
+ raise SystemExit(report["errors"])
52
+
53
+ # 2. Your own pandas work: clean, filter, derive, drop.
54
+ df = raw.dropna(subset=["email"])
55
+ df = df[df["plan"] != "free"]
56
+ df["age_band"] = (df["age"] // 10) * 10
57
+ df = df.drop(columns=["signup_date"])
58
+
59
+ # 3. See what the table holds now: types, null rates, likely identifiers.
60
+ schema = triplex.infer_schema(df)
61
+
62
+ # 4. Generate a synthetic copy for tests or a demo.
63
+ run = triplex.synthesize(df, rows=1000, seed=1)
64
+ synthetic = run.table.to_pandas()
65
+ run.report["leakage"]
66
+ ```
67
+
68
+ | Function | What it does |
69
+ |---|---|
70
+ | `infer_schema(data)` | The inferred schema as a dict |
71
+ | `draft_contract(data, output=...)` | A starting contract: every column with its type, all required. Returns a dict; `output` also writes it as `.triplex.yaml` |
72
+ | `validate(data, contract)` | Checks `data` against a contract (a `.triplex.yaml` path or a dict); returns `{"valid": ..., "errors": [...]}` |
73
+ | `convert(input, output)` | Converts a file between `.csv`, `.tsv`, `.jsonl` and `.parquet`, by extension |
74
+ | `anonymize(data, recipe, mapping_out=...)` | Masks the columns a `.triplex.yaml` recipe declares |
75
+ | `synthesize(data, rows=..., seed=..., method=...)` | Generates a synthetic table; `method` is `"copula"` (default), `"bayes-net"` or `"marginals"` |
76
+
77
+ A contract is what you expect the data to be; an inferred schema is what one table holds right
78
+ now. `draft_contract` gives you a first contract to edit (add null-rate, range, mean or pattern
79
+ limits) and commit. Passing the dict from `infer_schema` to `validate` is an error.
80
+
81
+ `data` is a file path, a pandas or Polars DataFrame, a pyarrow Table, or anything else Arrow can
82
+ read. `anonymize` and `synthesize` read `.csv` and `.parquet` paths; `infer_schema` and
83
+ `validate` also read `.tsv` and `.jsonl`.
84
+
85
+ `anonymize` and `synthesize` return a result with two fields: `report` (a dict) and `table`.
86
+ Read the table with `table.to_pandas()`, `table.to_polars()` or `table.to_arrow()`, or pass
87
+ `output="result.csv"` to have it written as CSV as well.
88
+
89
+ An engine error is raised as `triplex.TriplexError`, with a stable `code` such as
90
+ `"COLUMN_NOT_FOUND"`.
91
+
92
+ pandas, Polars and pyarrow are optional: install the ones you use. pandas input and
93
+ `to_pandas()` need pyarrow; Polars works without it.
94
+
95
+ ## Anonymizing
96
+
97
+ ```python
98
+ import os
99
+ os.environ["CUSTOMER_SALT"] = "..." # better: set it outside the script
100
+
101
+ run = triplex.anonymize(df, "recipe.triplex.yaml", mapping_out="mapping.csv")
102
+ masked = run.table.to_pandas()
103
+ ```
104
+
105
+ - **The salt** is read from the environment variable each recipe column names (`salt_env`). It
106
+ is never a function argument.
107
+ - **The mapping file** reverses the pseudonyms. It is written to the path you give in
108
+ `mapping_out` and is not returned to Python. Pass `mapping_passphrase_env="SOME_VAR"` to have
109
+ it encrypted with the passphrase in that variable.
110
+ - The result is pseudonymized, not anonymous, as long as the mapping file exists. The report says
111
+ so in `report["warning"]`.
112
+
113
+ ## Synthetic data
114
+
115
+ None of the three methods here carries a formal privacy guarantee. Every report includes a
116
+ `privacy_statement` and measured leakage figures; read them before sharing the output.
117
+ Identifier-like columns and dates are left out of the synthetic table and listed in
118
+ `report["columns_dropped"]`.
119
+
120
+ The differentially private methods and the sketch method of the command line are not available
121
+ from Python yet.
122
+
123
+ ## Where your data goes
124
+
125
+ Nowhere. The engine runs inside your Python process and makes no network call. Files are read
126
+ from and written to the paths you pass.
127
+
128
+ ## Limits
129
+
130
+ - Column types of a result table are inferred by the engine from the values (integer, float,
131
+ boolean or text). They are not copied from the input, so a date column comes back as text.
132
+ - A table is copied when it is handed to the engine and again when it comes back. Large tables
133
+ are untested.
134
+ - Tested on Linux with CPython 3.13, pandas, Polars and pyarrow.
135
+
@@ -0,0 +1,6 @@
1
+ triplex/__init__.py,sha256=eXEr6t1gvvPmMWUgznPLyO3HrbvVHuUXkDvjyusQ1S0,6528
2
+ triplex/_native.pyd,sha256=5dYccwA7zTgosrZ7foFPymUoC-eXOtnEHL-6ECImZHA,10924032
3
+ triplexanalytics-0.2.2.dist-info/METADATA,sha256=LtL2WwakxvsxFSii-PG9BFmhl7zUSZOhMpo6qNz4LWY,5881
4
+ triplexanalytics-0.2.2.dist-info/WHEEL,sha256=xe4_tbg8wYdeh4O7nLbnWckzIQtCR4nODDgwke_TKy8,95
5
+ triplexanalytics-0.2.2.dist-info/sboms/triplex-python.cyclonedx.json,sha256=gUPBjv7UHBtoi4OS83pwOtVW6bUqmdkwLZ1M1Ai6BgY,139470
6
+ triplexanalytics-0.2.2.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: maturin (1.15.0)
3
+ Root-Is-Purelib: false
4
+ Tag: cp39-abi3-win_amd64