deidkit 0.1.0__tar.gz → 0.1.2__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.
Files changed (71) hide show
  1. {deidkit-0.1.0/src/deidkit.egg-info → deidkit-0.1.2}/PKG-INFO +123 -117
  2. {deidkit-0.1.0 → deidkit-0.1.2}/README.md +119 -113
  3. {deidkit-0.1.0 → deidkit-0.1.2}/pyproject.toml +4 -5
  4. {deidkit-0.1.0 → deidkit-0.1.2}/src/deidkit/api.py +21 -1
  5. {deidkit-0.1.0 → deidkit-0.1.2}/src/deidkit/detect/pipeline.py +13 -6
  6. {deidkit-0.1.0 → deidkit-0.1.2}/src/deidkit/io.py +14 -3
  7. {deidkit-0.1.0 → deidkit-0.1.2}/src/deidkit/version.py +1 -1
  8. {deidkit-0.1.0 → deidkit-0.1.2/src/deidkit.egg-info}/PKG-INFO +123 -117
  9. {deidkit-0.1.0 → deidkit-0.1.2}/src/deidkit.egg-info/SOURCES.txt +1 -0
  10. deidkit-0.1.2/tests/test_qa_hardening.py +89 -0
  11. {deidkit-0.1.0 → deidkit-0.1.2}/LICENSE +0 -0
  12. {deidkit-0.1.0 → deidkit-0.1.2}/MANIFEST.in +0 -0
  13. {deidkit-0.1.0 → deidkit-0.1.2}/examples/benchmark.py +0 -0
  14. {deidkit-0.1.0 → deidkit-0.1.2}/examples/make_sample.py +0 -0
  15. {deidkit-0.1.0 → deidkit-0.1.2}/examples/policy.example.yaml +0 -0
  16. {deidkit-0.1.0 → deidkit-0.1.2}/examples/policy.meridian.yaml +0 -0
  17. {deidkit-0.1.0 → deidkit-0.1.2}/examples/quickstart.md +0 -0
  18. {deidkit-0.1.0 → deidkit-0.1.2}/setup.cfg +0 -0
  19. {deidkit-0.1.0 → deidkit-0.1.2}/src/deidkit/__init__.py +0 -0
  20. {deidkit-0.1.0 → deidkit-0.1.2}/src/deidkit/__main__.py +0 -0
  21. {deidkit-0.1.0 → deidkit-0.1.2}/src/deidkit/cli.py +0 -0
  22. {deidkit-0.1.0 → deidkit-0.1.2}/src/deidkit/config.py +0 -0
  23. {deidkit-0.1.0 → deidkit-0.1.2}/src/deidkit/data/NOTICES.md +0 -0
  24. {deidkit-0.1.0 → deidkit-0.1.2}/src/deidkit/data/context_triggers_en.txt +0 -0
  25. {deidkit-0.1.0 → deidkit-0.1.2}/src/deidkit/data/context_triggers_es.txt +0 -0
  26. {deidkit-0.1.0 → deidkit-0.1.2}/src/deidkit/data/en_given_names.txt +0 -0
  27. {deidkit-0.1.0 → deidkit-0.1.2}/src/deidkit/data/en_surnames.txt +0 -0
  28. {deidkit-0.1.0 → deidkit-0.1.2}/src/deidkit/data/es_given_names.txt +0 -0
  29. {deidkit-0.1.0 → deidkit-0.1.2}/src/deidkit/data/es_surnames.txt +0 -0
  30. {deidkit-0.1.0 → deidkit-0.1.2}/src/deidkit/data/honorifics_en.txt +0 -0
  31. {deidkit-0.1.0 → deidkit-0.1.2}/src/deidkit/data/honorifics_es.txt +0 -0
  32. {deidkit-0.1.0 → deidkit-0.1.2}/src/deidkit/data/medical_stoplist_en.txt +0 -0
  33. {deidkit-0.1.0 → deidkit-0.1.2}/src/deidkit/data/medical_stoplist_es.txt +0 -0
  34. {deidkit-0.1.0 → deidkit-0.1.2}/src/deidkit/data/medical_vocab.txt +0 -0
  35. {deidkit-0.1.0 → deidkit-0.1.2}/src/deidkit/dates.py +0 -0
  36. {deidkit-0.1.0 → deidkit-0.1.2}/src/deidkit/detect/__init__.py +0 -0
  37. {deidkit-0.1.0 → deidkit-0.1.2}/src/deidkit/detect/checksums.py +0 -0
  38. {deidkit-0.1.0 → deidkit-0.1.2}/src/deidkit/detect/context.py +0 -0
  39. {deidkit-0.1.0 → deidkit-0.1.2}/src/deidkit/detect/gazetteer.py +0 -0
  40. {deidkit-0.1.0 → deidkit-0.1.2}/src/deidkit/detect/patterns.py +0 -0
  41. {deidkit-0.1.0 → deidkit-0.1.2}/src/deidkit/detect/spacy_ner.py +0 -0
  42. {deidkit-0.1.0 → deidkit-0.1.2}/src/deidkit/detect/types.py +0 -0
  43. {deidkit-0.1.0 → deidkit-0.1.2}/src/deidkit/engine.py +0 -0
  44. {deidkit-0.1.0 → deidkit-0.1.2}/src/deidkit/generators/__init__.py +0 -0
  45. {deidkit-0.1.0 → deidkit-0.1.2}/src/deidkit/generators/names.py +0 -0
  46. {deidkit-0.1.0 → deidkit-0.1.2}/src/deidkit/generators/surrogates.py +0 -0
  47. {deidkit-0.1.0 → deidkit-0.1.2}/src/deidkit/learn.py +0 -0
  48. {deidkit-0.1.0 → deidkit-0.1.2}/src/deidkit/mapping.py +0 -0
  49. {deidkit-0.1.0 → deidkit-0.1.2}/src/deidkit/report.py +0 -0
  50. {deidkit-0.1.0 → deidkit-0.1.2}/src/deidkit/resources.py +0 -0
  51. {deidkit-0.1.0 → deidkit-0.1.2}/src/deidkit/schema.py +0 -0
  52. {deidkit-0.1.0 → deidkit-0.1.2}/src/deidkit/secret.py +0 -0
  53. {deidkit-0.1.0 → deidkit-0.1.2}/src/deidkit/textnorm.py +0 -0
  54. {deidkit-0.1.0 → deidkit-0.1.2}/src/deidkit.egg-info/dependency_links.txt +0 -0
  55. {deidkit-0.1.0 → deidkit-0.1.2}/src/deidkit.egg-info/entry_points.txt +0 -0
  56. {deidkit-0.1.0 → deidkit-0.1.2}/src/deidkit.egg-info/requires.txt +0 -0
  57. {deidkit-0.1.0 → deidkit-0.1.2}/src/deidkit.egg-info/top_level.txt +0 -0
  58. {deidkit-0.1.0 → deidkit-0.1.2}/tests/test_api.py +0 -0
  59. {deidkit-0.1.0 → deidkit-0.1.2}/tests/test_checksums.py +0 -0
  60. {deidkit-0.1.0 → deidkit-0.1.2}/tests/test_dates.py +0 -0
  61. {deidkit-0.1.0 → deidkit-0.1.2}/tests/test_db.py +0 -0
  62. {deidkit-0.1.0 → deidkit-0.1.2}/tests/test_detect.py +0 -0
  63. {deidkit-0.1.0 → deidkit-0.1.2}/tests/test_engine.py +0 -0
  64. {deidkit-0.1.0 → deidkit-0.1.2}/tests/test_hardening.py +0 -0
  65. {deidkit-0.1.0 → deidkit-0.1.2}/tests/test_known_phi.py +0 -0
  66. {deidkit-0.1.0 → deidkit-0.1.2}/tests/test_learn.py +0 -0
  67. {deidkit-0.1.0 → deidkit-0.1.2}/tests/test_medical_vocab.py +0 -0
  68. {deidkit-0.1.0 → deidkit-0.1.2}/tests/test_names.py +0 -0
  69. {deidkit-0.1.0 → deidkit-0.1.2}/tests/test_qa_fixes.py +0 -0
  70. {deidkit-0.1.0 → deidkit-0.1.2}/tests/test_qa_freetext_leaks.py +0 -0
  71. {deidkit-0.1.0 → deidkit-0.1.2}/tests/test_schema_lang.py +0 -0
@@ -1,12 +1,12 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: deidkit
3
- Version: 0.1.0
3
+ Version: 0.1.2
4
4
  Summary: Schema-driven pseudonymization for clinical/tabular datasets: synthetic names, interval-preserving date shifting, multi-stage free-text PII detection, and before/after audit exports.
5
5
  Author: Meridian Data
6
6
  License: MIT
7
- Project-URL: Homepage, https://github.com/your-org/deidkit
8
- Project-URL: Documentation, https://github.com/your-org/deidkit#readme
9
- Project-URL: Issues, https://github.com/your-org/deidkit/issues
7
+ Project-URL: Homepage, https://github.com/Meridian-Int/meridian-deid-engine
8
+ Project-URL: Documentation, https://github.com/Meridian-Int/meridian-deid-engine#readme
9
+ Project-URL: Issues, https://github.com/Meridian-Int/meridian-deid-engine/issues
10
10
  Keywords: de-identification,pseudonymization,hipaa,phi,ner,clinical,healthcare,privacy
11
11
  Classifier: Programming Language :: Python :: 3
12
12
  Classifier: Programming Language :: Python :: 3.9
@@ -79,18 +79,6 @@ re-runnable.
79
79
  pip install deidkit
80
80
  ```
81
81
 
82
- One command, no square brackets, nothing to pick. The database drivers needed to
83
- read your Azure SQL / Fabric source (SQLAlchemy + pyodbc) are bundled in, so a
84
- plain `pip install deidkit` is ready to connect.
85
-
86
- > **Heads-up — not on PyPI yet.** Until the package is published (see
87
- > [Publishing](#publishing-to-pypi)), install it straight from the repository:
88
- > ```bash
89
- > pip install git+https://github.com/<your-org>/deidkit.git
90
- > # or, from a local clone: pip install .
91
- > ```
92
- > Both give you the identical `deidkit` command and library.
93
-
94
82
  One system-level thing is required **once**, on the machine that will connect:
95
83
  the Microsoft **ODBC Driver 18 for SQL Server** — the actual database driver that
96
84
  `pyodbc` talks to:
@@ -113,38 +101,46 @@ deidkit --version
113
101
 
114
102
  ## 2. Quick start
115
103
 
116
- **CLI one line:**
104
+ Your data is the **Azure SQL Database mirrored into Microsoft Fabric**, so the
105
+ run connects to that endpoint, de-identifies the six tables together, and writes
106
+ a clean copy out. That's the whole thing:
117
107
 
118
108
  ```bash
119
- deidkit run data/
109
+ deidkit init-secret --out secret.key # do this once; keep the file private
120
110
  ```
121
111
 
122
- Reads every table in `data/`, de-identifies it, and writes two folders:
123
- `data_deid/` (ship this) and `data_deid-PRIVATE/` (keep this — see §5).
124
-
125
- **Python — one call:**
126
-
127
112
  ```python
128
113
  import deidkit as dk
114
+ from deidkit.io import read_sql_table
129
115
 
130
- result = dk.deidentify("data/", out="deid_out", secret_file="secret.key")
131
- print(result.summary) # counts of what changed
132
- clean = result.tables["tbl_notes"] # or write to files via out=
133
- ```
134
-
135
- **The recommended run** (pins a secret, uses the schema-tuned policy):
116
+ # connection string built from the values in section 10 (nothing writes back)
117
+ CONN = (
118
+ "mssql+pyodbc://@<sql-endpoint>.datawarehouse.fabric.microsoft.com/<database>"
119
+ "?driver=ODBC+Driver+18+for+SQL+Server&authentication=ActiveDirectoryInteractive"
120
+ )
121
+ TABLES = ["tbl_patient", "tbl_encounter", "tbl_notes",
122
+ "tbl_lab", "tbl_imaging", "tbl_medication"]
136
123
 
137
- ```bash
138
- deidkit init-secret --out secret.key # do this once, keep the file private
139
- deidkit run data/ deid_out \
140
- --policy examples/policy.meridian.yaml \
141
- --secret-file secret.key
124
+ tables = {t: read_sql_table(CONN, t) for t in TABLES} # read the DB (read-only)
125
+ result = dk.deidentify(
126
+ tables, out="deid_out", # de-identify all six together
127
+ secret_file="secret.key",
128
+ lang="es",
129
+ entity_key="patient_id",
130
+ known_id_columns=["patient_id"],
131
+ )
132
+ print(result.summary)
133
+ # -> deid_out/ (ship this) + deid_out-PRIVATE/ (keep private — see section 5)
142
134
  ```
143
135
 
144
- > The `data/` above is a folder of table files, used here just to show the
145
- > shape of a run. **Your data is in the Fabric / Azure SQL database, not a
146
- > folder** for that path (what you connect to and how), jump to **§9** and
147
- > **§10**; it's a few lines that read straight from the SQL endpoint.
136
+ That reads the tables from Fabric, never writes back, and produces the two output
137
+ folders. Section 9 walks through the connection (plus a database-to-database
138
+ variant), and section 10 lists the exact connection values to put in `CONN`.
139
+
140
+ > **Handed a file export instead of database access?** If you receive the tables
141
+ > as files (CSV etc.) rather than a live endpoint, the same run is one CLI line —
142
+ > `deidkit run data/` — writing `data_deid/` and `data_deid-PRIVATE/`. See
143
+ > section 3.
148
144
 
149
145
  > **CLI or code — same engine.** Everything below is shown for **both**
150
146
  > interfaces. The rule of thumb: every CLI `--flag` is a field on the Python
@@ -163,10 +159,10 @@ mirrored into Microsoft Fabric. deidkit connects to the Fabric **SQL analytics
163
159
  endpoint** (read-only), reads the six tables directly, de-identifies them, and
164
160
  writes the clean copy out. It **never writes back** to your database.
165
161
 
166
- To connect, we need a few **connection details from you** (the data owner) the
167
- SQL endpoint, the database name, and how to authenticate. Exactly what to send,
168
- and how the connection works, is spelled out in **§9 (how it runs)** and **§10
169
- (the connection details you provide)**. Start there.
162
+ deidkit connects on its own it only needs the connection details for that
163
+ endpoint (the SQL analytics endpoint host, the database name, and how to
164
+ authenticate). Those go into one connection string; section 9 shows how a run
165
+ works and section 10 lists the exact values. Start there.
170
166
 
171
167
  ### Or: a data export as files
172
168
 
@@ -189,7 +185,7 @@ deidkit run data/ # a folder of table files
189
185
  - **Column names drive the plan.** deidkit recognises clinical column names in
190
186
  **English and Spanish** (e.g. `note_full`, `result_interpretation`,
191
187
  `report_text`, `clinical_justification_pbs`) and classifies each one
192
- automatically. You can override any decision (§12). An optional
188
+ automatically. You can override any decision (section 12). An optional
193
189
  **data-dictionary JSON** (`--schema dict.json`) makes the classification
194
190
  authoritative instead of heuristic.
195
191
 
@@ -212,7 +208,7 @@ force any of them with a rule.
212
208
  | `redact` | blank the value | |
213
209
  | `drop` | remove the column entirely | |
214
210
 
215
- Preview the full plan before running anything with `deidkit plan` (§6).
211
+ Preview the full plan before running anything with `deidkit plan` (section 6).
216
212
 
217
213
  ---
218
214
 
@@ -233,9 +229,9 @@ deid_out/ ← SHIP THIS — de-identified data only
233
229
  deid_out-PRIVATE/ ← NEVER SHARE — the re-identification key
234
230
  deidkit-secret.key ← the secret (only if it was auto-generated)
235
231
  mapping.private.json ← value → surrogate map (reverses/extends the run)
236
- deid_audit.PRIVATE.xlsx ← before/after audit workbook (see §6)
232
+ deid_audit.PRIVATE.xlsx ← before/after audit workbook (see section 6)
237
233
  deid_audit.PRIVATE_freetext_changes.csv
238
- review.csv ← uncertain detections for a human (see §7)
234
+ review.csv ← uncertain detections for a human (see section 7)
239
235
  ```
240
236
 
241
237
  - **`deid_out/`** — the pseudonymized tables, same structure as the input. This
@@ -245,7 +241,7 @@ deid_out-PRIVATE/ ← NEVER SHARE — the re-identification key
245
241
 
246
242
  **Output format** defaults to CSV; use `--format parquet|tsv|json` (CLI) or
247
243
  `fmt=` (Python) to change it. Reading from a DB and want the result written back
248
- to a DB? See `run-db` in §9.
244
+ to a DB? See `run-db` in section 9.
249
245
 
250
246
  ---
251
247
 
@@ -316,32 +312,70 @@ print(los.describe()) # identical to the source — only absolute dates moved
316
312
 
317
313
  ## 7. Training it — the review → learn loop
318
314
 
319
- deidkit deliberately **does not guess** on ambiguous names. Instead it puts them
320
- in `review.csv`, and you teach it. This gets better every pass, is fully
321
- auditable, and uses no black-box model.
315
+ deidkit **never guesses** on an ambiguous name. Anything it detects but isn't sure
316
+ about (for example a lone surname that is also a common word) is left untouched
317
+ and listed in **`review.csv`** for a person to rule on. Confirm or reject those,
318
+ and the next run does better — deterministically, with no model.
319
+
320
+ `review.csv` holds only that **uncertain tail**; the confident detections are
321
+ already scrubbed in the output and never appear here. It is written to the
322
+ **private** folder on every run (`deid_out-PRIVATE/review.csv`) and contains raw
323
+ candidate text, so treat it as sensitive — never ship it.
324
+
325
+ ### What the reviewer does
326
+
327
+ Open `deid_out-PRIVATE/review.csv` (Excel, Google Sheets, any editor). Each row is
328
+ one uncertain candidate, with a `context` snippet so you can judge it. Fill in the
329
+ **first column only** — `decision(y/n)` — and save:
330
+
331
+ | decision(y/n) | entity_type | candidate | context | table | column | row_id | confidence | detectors |
332
+ | --- | --- | --- | --- | --- | --- | --- | --- | --- |
333
+ | `y` | PERSON | `Bello` | `…paciente Bello refiere dolor…` | tbl_notes | note_full | 87 | 0.6 | context |
334
+ | `n` | PERSON | `Cruz` | `…la Cruz Roja atendió…` | tbl_notes | note_full | 42 | 0.55 | gazetteer |
335
+ | *(blank)* | PERSON | `Sur` | `…zona Sur del hospital…` | tbl_notes | note_full | 91 | 0.5 | gazetteer |
336
+
337
+ - **`y`** — yes, this **is** a real person's name (it should be redacted).
338
+ - **`n`** — no, it's a **false positive** — not a name (e.g. "Cruz" in "Cruz Roja").
339
+ - **blank** — undecided; that row is skipped and simply stays for a later pass.
340
+
341
+ That is the whole reviewer job: read the `context`, type `y` or `n`, save the
342
+ file. (Lenient spellings work too: `yes` / `1` / `true` = yes; `no` / `0` /
343
+ `false` / `fp` = no.)
344
+
345
+ ### The four steps
322
346
 
323
347
  ```bash
324
- # 1. Run with a learn directory. Uncertain detections go to review.csv.
325
- deidkit run data/ deid_out --learn-dir learned/
348
+ # 1. Run. Uncertain detections are written to deid_out-PRIVATE/review.csv.
349
+ deidkit run <input> deid_out
326
350
 
327
- # 2. A human opens deid_out-PRIVATE/review.csv and sets the first column:
328
- # y = real PII (learn it as a name → higher recall next time)
329
- # n = false positive (learn it as a stopword → higher precision next time)
351
+ # 2. A reviewer opens deid_out-PRIVATE/review.csv and marks each row y / n (above).
330
352
 
331
- # 3. Fold those decisions into the learned dictionaries.
353
+ # 3. Fold those decisions into a "learn" directory.
332
354
  deidkit learn --review deid_out-PRIVATE/review.csv --learn-dir learned/
333
355
 
334
- # 4. Re-run. Confirmed names are now caught; rejected ones are suppressed.
335
- deidkit run data/ deid_out --learn-dir learned/
356
+ # 4. Re-run pointing at what it learned: confirmed names are now caught
357
+ # automatically, and rejected candidates are suppressed.
358
+ deidkit run <input> deid_out --learn-dir learned/
336
359
  ```
337
360
 
338
- `learned/` accumulates `learned_names.txt` and `learned_stoplist.txt`. Point every
339
- future run at the same `--learn-dir` and it auto-loads them.
361
+ Step 3 appends to two plain-text files that `learned/` **accumulates** across
362
+ cycles (so it keeps improving):
363
+
364
+ - `learned_names.txt` — words from every `y` → added to the name list (**higher recall**),
365
+ - `learned_stoplist.txt` — words from every `n` → added to the stoplist (**higher precision**).
340
366
 
341
- *In code:* pass the learned files onto the policy
367
+ Both are one token per line and human-editable. *In code*, load them with
342
368
  `Policy(extra_name_files=["learned/learned_names.txt"],
343
369
  extra_stoplist_files=["learned/learned_stoplist.txt"])`.
344
370
 
371
+ **Good to know:**
372
+ - `review.csv` is produced on **every** run — `--learn-dir` is only needed to
373
+ *load* what was learned (steps 3–4), not to create the queue.
374
+ - Learning is **token-level**: marking "María Gómez" as `y` teaches both `maría`
375
+ and `gómez` (lowercased, accent-folded), so they are caught elsewhere too.
376
+ - The loop tunes the **borderline** cases. A name the detector missed entirely
377
+ will not appear in `review.csv` — for those, use the known-PHI columns below.
378
+
345
379
  ### Two stronger levers (deterministic, near-100% recall)
346
380
 
347
381
  - **Known-PHI columns** — if you already know a row's real name or ID (from a
@@ -397,27 +431,27 @@ dk.Policy(mode="balanced", spacy_model="es_core_news_lg",
397
431
 
398
432
  ## 9. Reading from a database (Fabric / Azure SQL)
399
433
 
400
- ### How we get access to your system
434
+ ### How the connection works
401
435
 
402
- We do **not** log into your Azure portal or touch your production database. The
403
- flow is the standard Fabric mirror share:
436
+ deidkit talks only to the Fabric **SQL analytics endpoint** it never logs into
437
+ the Azure portal or the production database. The setup is the standard Fabric
438
+ mirror:
404
439
 
405
- 1. **You (the data owner)** mirror the Azure SQL Database into a Microsoft Fabric
406
- workspace and **share the mirrored database with us as read-only** — grant
407
- *Read* + *Read all SQL analytics endpoint data*, nothing else. (This is the
408
- setup in the onboarding document; no write or admin access is ever needed.)
409
- 2. **You send us four connection details** (listed in §10) the SQL analytics
410
- endpoint, the database name, the authentication method, and the workspace URL.
411
- 3. **We connect to that read-only SQL endpoint** from our side using those
412
- details, read the six tables, de-identify them, and write out the clean copy.
413
- deidkit **never writes back** to your database — it only reads.
440
+ 1. The Azure SQL Database is mirrored into a Microsoft Fabric workspace, and the
441
+ mirrored database is shared **read-only** — *Read* + *Read all SQL analytics
442
+ endpoint data*, nothing else. No write or admin access is ever required.
443
+ 2. deidkit is pointed at that endpoint with a connection string built from four
444
+ values (see section 10): the SQL analytics endpoint, the database name, the
445
+ authentication method, and the workspace URL.
446
+ 3. It connects to the read-only endpoint, reads the six tables, de-identifies
447
+ them, and writes out the clean copy. It **never writes back** it only reads.
414
448
 
415
- So the only thing you provide is read-only access plus those four details; the
416
- de-identification runs entirely on our side against the shared endpoint.
449
+ All that's needed is read-only access to the shared endpoint plus those values.
417
450
 
418
451
  ### Then it runs — two patterns
419
452
 
420
- The database drivers ship with deidkit (§1). Pick based on where you want the output.
453
+ The database drivers ship with deidkit (section 1). Pick based on where you want
454
+ the output.
421
455
 
422
456
  ### Pattern 1 — Database → files (recommended, simplest)
423
457
 
@@ -490,9 +524,9 @@ dk.deidentify_database(
490
524
 
491
525
  ## 10. Database connection parameters — exactly what to fill in
492
526
 
493
- ### What you (the data owner) send us
527
+ ### The four connection values
494
528
 
495
- After sharing the mirrored database read-only, send us these **four items** (they
529
+ Once the mirrored database is shared read-only, gather these **four values** (they
496
530
  are exactly what the Fabric "share" step produces):
497
531
 
498
532
  | # | Item | Example | Used for |
@@ -502,10 +536,11 @@ are exactly what the Fabric "share" step produces):
502
536
  | 3 | **Authentication method** — Microsoft Entra ID *or* SQL Login | `Microsoft Entra ID` | connecting |
503
537
  | 4 | **Fabric Workspace URL** | `https://app.fabric.microsoft.com/groups/...` | locating & confirming the shared database |
504
538
 
505
- Items 1–3 go into the connection string; item 4 is how we find and verify the
506
- shared item in Fabric (it is not part of the connection string).
539
+ Items 1–3 go into the connection string; item 4 (the workspace URL) only locates
540
+ and confirms the shared database in Fabric it is not part of the connection
541
+ string.
507
542
 
508
- ### How we assemble the connection string
543
+ ### The connection string
509
544
 
510
545
  Connections are **SQLAlchemy URLs**. For the Fabric source, the shape is:
511
546
 
@@ -515,10 +550,10 @@ mssql+pyodbc://@<HOST>/<DATABASE>?driver=<DRIVER>&authentication=<AUTH>
515
550
 
516
551
  | Placeholder | Filled from | Example |
517
552
  | --- | --- | --- |
518
- | `<HOST>` | item 1 — the SQL analytics endpoint you send us | `abc123....datawarehouse.fabric.microsoft.com` |
519
- | `<DATABASE>` | item 2 — the database name you send us | `ProductionMirror` |
520
- | `<AUTH>` | item 3 — your authentication method | `ActiveDirectoryInteractive` |
521
- | `<DRIVER>` | **installed on our machine** (§1), not sent by you | `ODBC+Driver+18+for+SQL+Server` |
553
+ | `<HOST>` | item 1 — the SQL analytics endpoint | `abc123....datawarehouse.fabric.microsoft.com` |
554
+ | `<DATABASE>` | item 2 — the database name | `ProductionMirror` |
555
+ | `<AUTH>` | item 3 — the authentication method | `ActiveDirectoryInteractive` |
556
+ | `<DRIVER>` | installed locally (section 1), not a Fabric value | `ODBC+Driver+18+for+SQL+Server` |
522
557
 
523
558
  > Note the `@` before `<HOST>`: `...pyodbc://@abc123...`. With Entra ID auth there
524
559
  > is no username/password in the URL — the `@` with an empty user is intentional.
@@ -724,42 +759,13 @@ print(deid.summary())
724
759
  | Symptom | Fix |
725
760
  | --- | --- |
726
761
  | `ModuleNotFoundError: pyodbc` / SQLAlchemy | Reinstall: `pip install --force-reinstall deidkit` (the DB drivers ship with it) |
727
- | `Data source name not found` / driver error | Install **ODBC Driver 18 for SQL Server** and make the `driver=` value match exactly (§10) |
762
+ | `Data source name not found` / driver error | Install **ODBC Driver 18 for SQL Server** and make the `driver=` value match exactly (section 10) |
728
763
  | Browser never opens for `ActiveDirectoryInteractive` | Run from an interactive session; for servers use `ActiveDirectoryServicePrincipal` |
729
764
  | `cannot reach the output database` (run-db) | Create the empty output DB first and pass a reachable `--out-db` URL |
730
765
  | A column was changed that shouldn’t be | Add it to `--ignore`, or add a `FieldRule(..., "passthrough")`; confirm with `deidkit plan` |
731
- | A free-text name slipped through | Try `--mode balanced`, enable `--spacy-model`, or add the name via the review→learn loop (§7) |
766
+ | A free-text name slipped through | Try `--mode balanced`, enable `--spacy-model`, or add the name via the review→learn loop (section 7) |
732
767
  | Non-UTF-8 CSV export | Handled automatically (deidkit falls back to latin-1/cp1252) |
733
- | Different output each run | You’re not pinning the secret — pass the same `--secret-file` every time (§2) |
734
-
735
- ---
736
-
737
- ## Publishing to PyPI
738
-
739
- deidkit is **not on PyPI yet**, so `pip install deidkit` will only work once you
740
- publish it under your own PyPI account. The package already builds cleanly into a
741
- wheel (verified). To publish:
742
-
743
- ```bash
744
- # 0. one-time: make a PyPI account at https://pypi.org and create an API token.
745
- # Also edit the [project.urls] in pyproject.toml (they still say "your-org").
746
-
747
- pip install build twine # the standard build + upload tools
748
-
749
- python -m build # produces dist/deidkit-0.1.0-py3-none-any.whl (+ .tar.gz)
750
- python -m twine check dist/* # validates the package metadata
751
-
752
- # optional dry run on the test index first:
753
- python -m twine upload --repository testpypi dist/*
754
-
755
- # the real publish (asks for your PyPI token):
756
- python -m twine upload dist/*
757
- ```
758
-
759
- After that first upload, `pip install deidkit` works for everyone, and the name
760
- `deidkit` is yours (it is currently unclaimed on PyPI). Bump `version` in
761
- `pyproject.toml` for each subsequent release — PyPI refuses to overwrite an
762
- existing version.
768
+ | Different output each run | You’re not pinning the secret — pass the same `--secret-file` every time (section 2) |
763
769
 
764
770
  ---
765
771