canpii 0.1.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.
- canpii-0.1.0/.gitignore +10 -0
- canpii-0.1.0/CHANGELOG.md +30 -0
- canpii-0.1.0/CONTRIBUTING.md +107 -0
- canpii-0.1.0/LICENSE +202 -0
- canpii-0.1.0/NOTICE +13 -0
- canpii-0.1.0/PKG-INFO +274 -0
- canpii-0.1.0/README.md +236 -0
- canpii-0.1.0/benchmarks/RESULTS.md +31 -0
- canpii-0.1.0/benchmarks/run_benchmark.py +126 -0
- canpii-0.1.0/docs/entities.md +98 -0
- canpii-0.1.0/docs/issues/01-manitoba-phin.md +35 -0
- canpii-0.1.0/docs/issues/02-saskatchewan-hsn.md +21 -0
- canpii-0.1.0/docs/issues/03-nova-scotia-health-card.md +18 -0
- canpii-0.1.0/docs/issues/04-alberta-driver-licence.md +25 -0
- canpii-0.1.0/docs/issues/05-manitoba-driver-licence.md +23 -0
- canpii-0.1.0/docs/issues/06-ircc-uci.md +22 -0
- canpii-0.1.0/docs/issues/07-french-context-review.md +23 -0
- canpii-0.1.0/docs/issues/08-benchmark-realistic-corpus.md +20 -0
- canpii-0.1.0/docs/presets.md +70 -0
- canpii-0.1.0/docs/roadmap.md +55 -0
- canpii-0.1.0/pyproject.toml +122 -0
- canpii-0.1.0/src/canpii/__init__.py +49 -0
- canpii-0.1.0/src/canpii/__main__.py +5 -0
- canpii-0.1.0/src/canpii/catalog.py +123 -0
- canpii-0.1.0/src/canpii/cli.py +93 -0
- canpii-0.1.0/src/canpii/conf/recognizers.yaml +116 -0
- canpii-0.1.0/src/canpii/exporters.py +390 -0
- canpii-0.1.0/src/canpii/presets.py +142 -0
- canpii-0.1.0/src/canpii/py.typed +0 -0
- canpii-0.1.0/src/canpii/recognizers/__init__.py +50 -0
- canpii-0.1.0/src/canpii/recognizers/_base.py +166 -0
- canpii-0.1.0/src/canpii/recognizers/_context.py +33 -0
- canpii-0.1.0/src/canpii/recognizers/ca_ab_phn_recognizer.py +43 -0
- canpii-0.1.0/src/canpii/recognizers/ca_bank_account_recognizer.py +43 -0
- canpii-0.1.0/src/canpii/recognizers/ca_bc_driver_license_recognizer.py +35 -0
- canpii-0.1.0/src/canpii/recognizers/ca_bc_phn_recognizer.py +38 -0
- canpii-0.1.0/src/canpii/recognizers/ca_business_number_recognizer.py +71 -0
- canpii-0.1.0/src/canpii/recognizers/ca_on_driver_license_recognizer.py +45 -0
- canpii-0.1.0/src/canpii/recognizers/ca_on_ohip_recognizer.py +59 -0
- canpii-0.1.0/src/canpii/recognizers/ca_passport_recognizer.py +37 -0
- canpii-0.1.0/src/canpii/recognizers/ca_qc_driver_license_recognizer.py +40 -0
- canpii-0.1.0/src/canpii/recognizers/ca_qc_ramq_recognizer.py +59 -0
- canpii-0.1.0/src/canpii/registry.py +189 -0
- canpii-0.1.0/src/canpii/synthetic.py +513 -0
- canpii-0.1.0/src/canpii/validators.py +307 -0
- canpii-0.1.0/tests/conftest.py +62 -0
- canpii-0.1.0/tests/test_docs.py +56 -0
- canpii-0.1.0/tests/test_e2e.py +111 -0
- canpii-0.1.0/tests/test_exporters.py +114 -0
- canpii-0.1.0/tests/test_false_positives.py +60 -0
- canpii-0.1.0/tests/test_recognizers.py +236 -0
- canpii-0.1.0/tests/test_registry_presets_cli.py +86 -0
- canpii-0.1.0/tests/test_synthetic.py +54 -0
- canpii-0.1.0/tests/test_validators.py +161 -0
canpii-0.1.0/.gitignore
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented here. The format follows
|
|
4
|
+
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the project uses
|
|
5
|
+
[Semantic Versioning](https://semver.org/) (0.x: minor versions may break APIs).
|
|
6
|
+
|
|
7
|
+
## [Unreleased]
|
|
8
|
+
|
|
9
|
+
## [0.1.0] - 2026-11-05
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- Recognizers (English and French instances): `CaOnOhipRecognizer`,
|
|
14
|
+
`CaQcRamqRecognizer`, `CaBcPhnRecognizer`, `CaAbPhnRecognizer`
|
|
15
|
+
(`CA_HEALTH_NUMBER`); `CaPassportRecognizer` (`CA_PASSPORT`);
|
|
16
|
+
`CaBusinessNumberRecognizer` (`CA_BN`); `CaBankAccountRecognizer`
|
|
17
|
+
(`CA_BANK_ACCOUNT`); `CaOnDriverLicenseRecognizer`, `CaQcDriverLicenseRecognizer`,
|
|
18
|
+
`CaBcDriverLicenseRecognizer` (`CA_DRIVER_LICENSE`).
|
|
19
|
+
- Two-tier validation: official rules reject, secondary/community rules only add score
|
|
20
|
+
(`strict=True` to reject on them too); negative context for weak matches.
|
|
21
|
+
- Pure-Python `canpii.validators` module (no Presidio dependency).
|
|
22
|
+
- `register_all()`, `get_recognizers()`, `create_analyzer()` and a bundled
|
|
23
|
+
`RecognizerRegistryProvider` YAML; enables upstream `CA_SIN` / `CA_POSTAL_CODE`.
|
|
24
|
+
- Policy presets `pipeda`, `phipa`, `quebec_law25`.
|
|
25
|
+
- CLI `canpii export` for LiteLLM (`presidio_ad_hoc_recognizers` JSON and
|
|
26
|
+
guardrail config), ContextForge `custom_patterns`, generic regex JSON and Presidio YAML.
|
|
27
|
+
- Synthetic data generator, labeled bilingual benchmark, docs with sourced format table.
|
|
28
|
+
|
|
29
|
+
[Unreleased]: https://github.com/ArthurJZL/canpii/compare/v0.1.0...HEAD
|
|
30
|
+
[0.1.0]: https://github.com/ArthurJZL/canpii/releases/tag/v0.1.0
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# Contributing to canpii
|
|
2
|
+
|
|
3
|
+
Thanks for helping! This project is small on purpose: every recognizer must be
|
|
4
|
+
**sourced, tested and documented**. The rules below exist so that a reviewer can
|
|
5
|
+
check a contribution without having to trust it.
|
|
6
|
+
|
|
7
|
+
## Ground rules
|
|
8
|
+
|
|
9
|
+
1. **No real identifiers, ever.** Not in code, tests, issues, PRs, commit messages,
|
|
10
|
+
screenshots or logs. Use `canpii generate <kind>` or obviously fake values
|
|
11
|
+
(`1234-567-897`, `ZZZX 8812 2512`). PRs containing anything that looks like a real
|
|
12
|
+
identifier will be closed and the history rewritten.
|
|
13
|
+
2. **Every format rule needs a source.** Official (government / regulator / payments
|
|
14
|
+
network) sources are preferred. Secondary sources (vendor DLP docs, Wikipedia) and
|
|
15
|
+
community sources (open-source validators, EMR forums) are acceptable only if marked
|
|
16
|
+
as such in `docs/entities.md` — and then the rule may *raise* a score but must not
|
|
17
|
+
*reject* a match (see `CanadianPatternRecognizer.unverified_check`).
|
|
18
|
+
3. **Follow Presidio's conventions** so recognizers can be upstreamed:
|
|
19
|
+
one class per file, `COUNTRY_CODE = "ca"`, `PATTERNS` / `CONTEXT` class attributes,
|
|
20
|
+
the standard `__init__(patterns, context, supported_language, supported_entity, name)`
|
|
21
|
+
signature, a docstring with the format and a reference URL.
|
|
22
|
+
4. **Portable regexes.** No look-arounds and no back-references, so that exports work
|
|
23
|
+
in Rust-based gateways (`check_contextforge_pattern` enforces this in tests).
|
|
24
|
+
|
|
25
|
+
## Development setup
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
git clone https://github.com/ArthurJZL/canpii
|
|
29
|
+
cd canpii
|
|
30
|
+
uv sync
|
|
31
|
+
uv pip install en_core_web_sm@https://github.com/explosion/spacy-models/releases/download/en_core_web_sm-3.8.0/en_core_web_sm-3.8.0-py3-none-any.whl \
|
|
32
|
+
fr_core_news_sm@https://github.com/explosion/spacy-models/releases/download/fr_core_news_sm-3.8.0/fr_core_news_sm-3.8.0-py3-none-any.whl
|
|
33
|
+
uv run ruff check . && uv run ruff format --check .
|
|
34
|
+
uv run pytest
|
|
35
|
+
uv run python benchmarks/run_benchmark.py
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Tests that need spaCy models are marked `nlp` and are skipped automatically when the
|
|
39
|
+
models are not installed; pattern-level tests always run.
|
|
40
|
+
|
|
41
|
+
## Recipe: add a province or identifier
|
|
42
|
+
|
|
43
|
+
Example: Manitoba PHIN (`CA_HEALTH_NUMBER`, province `MB`).
|
|
44
|
+
|
|
45
|
+
1. **Source.** Find the format in an official document. Save the URL, the page/section,
|
|
46
|
+
and the exact wording. Decide the confidence level for each rule (official /
|
|
47
|
+
secondary / community).
|
|
48
|
+
2. **Validator.** Add pure functions to `src/canpii/validators.py` — standard
|
|
49
|
+
library only, no Presidio import — with the provenance in the docstring, e.g.
|
|
50
|
+
`"""9 digits. [official]"""`.
|
|
51
|
+
3. **Recognizer.** Create `src/canpii/recognizers/ca_mb_phin_recognizer.py`:
|
|
52
|
+
* subclass `CanadianPatternRecognizer`; set `ENTITY`, `PROVINCE`, `LABEL`,
|
|
53
|
+
`PATTERNS`, `CONTEXT` (English **and** French, single tokens, see `_context.py`);
|
|
54
|
+
* implement `verified_check` for official rules and `unverified_check` for the rest;
|
|
55
|
+
* choose base scores: ≥ 0.4 only for formats that are distinctive *and* validated,
|
|
56
|
+
0.05–0.3 for plausible formats, ≤ 0.01 for "any N digits" shapes (context-only).
|
|
57
|
+
4. **Register.** Add the class to `ALL_RECOGNIZERS` in `recognizers/__init__.py` and a
|
|
58
|
+
`CatalogEntry` in `catalog.py`; run `uv run python scripts/generate_docs.py`.
|
|
59
|
+
5. **Synthetic data.** Add a generator (and, if there is a checksum, a near-miss
|
|
60
|
+
generator) to `synthetic.py`, plus English and French templates.
|
|
61
|
+
6. **Tests.** At minimum: valid / invalid / checksum cases at pattern level, English and
|
|
62
|
+
French sentences end-to-end, and collisions with phone numbers, dates, SIN and other
|
|
63
|
+
identifiers of the same length.
|
|
64
|
+
7. **Docs.** Add a row to `docs/entities.md` (rule, confidence, source URL), update the
|
|
65
|
+
matrix in `docs/roadmap.md` and the table in `README.md`, and add a line to the
|
|
66
|
+
`Unreleased` section of `CHANGELOG.md`.
|
|
67
|
+
8. **Benchmark.** Run `benchmarks/run_benchmark.py` and paste the table for your entity
|
|
68
|
+
into the PR description.
|
|
69
|
+
|
|
70
|
+
## Pull request checklist
|
|
71
|
+
|
|
72
|
+
- [ ] No real identifiers anywhere in the diff (including test data and docs).
|
|
73
|
+
- [ ] Every new rule has a row in `docs/entities.md` with a source URL and confidence.
|
|
74
|
+
- [ ] Unofficial rules only add score; they do not reject (unless `strict=True`).
|
|
75
|
+
- [ ] English and French context words, and tests in both languages.
|
|
76
|
+
- [ ] `uv run ruff check . && uv run ruff format --check .` passes.
|
|
77
|
+
- [ ] `uv run pytest` passes (with spaCy models installed).
|
|
78
|
+
- [ ] `CHANGELOG.md` updated under `Unreleased`.
|
|
79
|
+
- [ ] Commits are signed off (`git commit -s`).
|
|
80
|
+
|
|
81
|
+
## Developer Certificate of Origin (DCO)
|
|
82
|
+
|
|
83
|
+
All commits must carry a `Signed-off-by:` line certifying the
|
|
84
|
+
[Developer Certificate of Origin 1.1](https://developercertificate.org/): that you wrote
|
|
85
|
+
the change or otherwise have the right to submit it under the project's license.
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
git commit -s -m "Add Manitoba PHIN recognizer"
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
The CI `dco` job checks every commit in a pull request. To fix a missing sign-off:
|
|
92
|
+
`git commit --amend -s` (last commit) or `git rebase --signoff main` (several).
|
|
93
|
+
|
|
94
|
+
**Do not copy code from other projects with incompatible licenses**, and do not copy
|
|
95
|
+
code from open upstream Presidio pull requests: write your own implementation from the
|
|
96
|
+
public specification and link the upstream PR in the description instead.
|
|
97
|
+
|
|
98
|
+
## Upstreaming
|
|
99
|
+
|
|
100
|
+
If your recognizer is backed by official sources and has been released here, consider
|
|
101
|
+
proposing it to Presidio (`presidio-analyzer/presidio_analyzer/predefined_recognizers/country_specific/canada/`),
|
|
102
|
+
following Presidio's `CONTRIBUTING.md`. Mention it in our issue tracker so we can
|
|
103
|
+
deprecate the duplicate once it is merged.
|
|
104
|
+
|
|
105
|
+
## Code of Conduct
|
|
106
|
+
|
|
107
|
+
This project follows the [Contributor Covenant](CODE_OF_CONDUCT.md).
|
canpii-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
|
|
2
|
+
Apache License
|
|
3
|
+
Version 2.0, January 2004
|
|
4
|
+
http://www.apache.org/licenses/
|
|
5
|
+
|
|
6
|
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
7
|
+
|
|
8
|
+
1. Definitions.
|
|
9
|
+
|
|
10
|
+
"License" shall mean the terms and conditions for use, reproduction,
|
|
11
|
+
and distribution as defined by Sections 1 through 9 of this document.
|
|
12
|
+
|
|
13
|
+
"Licensor" shall mean the copyright owner or entity authorized by
|
|
14
|
+
the copyright owner that is granting the License.
|
|
15
|
+
|
|
16
|
+
"Legal Entity" shall mean the union of the acting entity and all
|
|
17
|
+
other entities that control, are controlled by, or are under common
|
|
18
|
+
control with that entity. For the purposes of this definition,
|
|
19
|
+
"control" means (i) the power, direct or indirect, to cause the
|
|
20
|
+
direction or management of such entity, whether by contract or
|
|
21
|
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
22
|
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
23
|
+
|
|
24
|
+
"You" (or "Your") shall mean an individual or Legal Entity
|
|
25
|
+
exercising permissions granted by this License.
|
|
26
|
+
|
|
27
|
+
"Source" form shall mean the preferred form for making modifications,
|
|
28
|
+
including but not limited to software source code, documentation
|
|
29
|
+
source, and configuration files.
|
|
30
|
+
|
|
31
|
+
"Object" form shall mean any form resulting from mechanical
|
|
32
|
+
transformation or translation of a Source form, including but
|
|
33
|
+
not limited to compiled object code, generated documentation,
|
|
34
|
+
and conversions to other media types.
|
|
35
|
+
|
|
36
|
+
"Work" shall mean the work of authorship, whether in Source or
|
|
37
|
+
Object form, made available under the License, as indicated by a
|
|
38
|
+
copyright notice that is included in or attached to the work
|
|
39
|
+
(an example is provided in the Appendix below).
|
|
40
|
+
|
|
41
|
+
"Derivative Works" shall mean any work, whether in Source or Object
|
|
42
|
+
form, that is based on (or derived from) the Work and for which the
|
|
43
|
+
editorial revisions, annotations, elaborations, or other modifications
|
|
44
|
+
represent, as a whole, an original work of authorship. For the purposes
|
|
45
|
+
of this License, Derivative Works shall not include works that remain
|
|
46
|
+
separable from, or merely link (or bind by name) to the interfaces of,
|
|
47
|
+
the Work and Derivative Works thereof.
|
|
48
|
+
|
|
49
|
+
"Contribution" shall mean any work of authorship, including
|
|
50
|
+
the original version of the Work and any modifications or additions
|
|
51
|
+
to that Work or Derivative Works thereof, that is intentionally
|
|
52
|
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
|
53
|
+
or by an individual or Legal Entity authorized to submit on behalf of
|
|
54
|
+
the copyright owner. For the purposes of this definition, "submitted"
|
|
55
|
+
means any form of electronic, verbal, or written communication sent
|
|
56
|
+
to the Licensor or its representatives, including but not limited to
|
|
57
|
+
communication on electronic mailing lists, source code control systems,
|
|
58
|
+
and issue tracking systems that are managed by, or on behalf of, the
|
|
59
|
+
Licensor for the purpose of discussing and improving the Work, but
|
|
60
|
+
excluding communication that is conspicuously marked or otherwise
|
|
61
|
+
designated in writing by the copyright owner as "Not a Contribution."
|
|
62
|
+
|
|
63
|
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
64
|
+
on behalf of whom a Contribution has been received by Licensor and
|
|
65
|
+
subsequently incorporated within the Work.
|
|
66
|
+
|
|
67
|
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
68
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
69
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
70
|
+
copyright license to reproduce, prepare Derivative Works of,
|
|
71
|
+
publicly display, publicly perform, sublicense, and distribute the
|
|
72
|
+
Work and such Derivative Works in Source or Object form.
|
|
73
|
+
|
|
74
|
+
3. Grant of Patent License. Subject to the terms and conditions of
|
|
75
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
76
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
77
|
+
(except as stated in this section) patent license to make, have made,
|
|
78
|
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
79
|
+
where such license applies only to those patent claims licensable
|
|
80
|
+
by such Contributor that are necessarily infringed by their
|
|
81
|
+
Contribution(s) alone or by combination of their Contribution(s)
|
|
82
|
+
with the Work to which such Contribution(s) was submitted. If You
|
|
83
|
+
institute patent litigation against any entity (including a
|
|
84
|
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
|
85
|
+
or a Contribution incorporated within the Work constitutes direct
|
|
86
|
+
or contributory patent infringement, then any patent licenses
|
|
87
|
+
granted to You under this License for that Work shall terminate
|
|
88
|
+
as of the date such litigation is filed.
|
|
89
|
+
|
|
90
|
+
4. Redistribution. You may reproduce and distribute copies of the
|
|
91
|
+
Work or Derivative Works thereof in any medium, with or without
|
|
92
|
+
modifications, and in Source or Object form, provided that You
|
|
93
|
+
meet the following conditions:
|
|
94
|
+
|
|
95
|
+
(a) You must give any other recipients of the Work or
|
|
96
|
+
Derivative Works a copy of this License; and
|
|
97
|
+
|
|
98
|
+
(b) You must cause any modified files to carry prominent notices
|
|
99
|
+
stating that You changed the files; and
|
|
100
|
+
|
|
101
|
+
(c) You must retain, in the Source form of any Derivative Works
|
|
102
|
+
that You distribute, all copyright, patent, trademark, and
|
|
103
|
+
attribution notices from the Source form of the Work,
|
|
104
|
+
excluding those notices that do not pertain to any part of
|
|
105
|
+
the Derivative Works; and
|
|
106
|
+
|
|
107
|
+
(d) If the Work includes a "NOTICE" text file as part of its
|
|
108
|
+
distribution, then any Derivative Works that You distribute must
|
|
109
|
+
include a readable copy of the attribution notices contained
|
|
110
|
+
within such NOTICE file, excluding those notices that do not
|
|
111
|
+
pertain to any part of the Derivative Works, in at least one
|
|
112
|
+
of the following places: within a NOTICE text file distributed
|
|
113
|
+
as part of the Derivative Works; within the Source form or
|
|
114
|
+
documentation, if provided along with the Derivative Works; or,
|
|
115
|
+
within a display generated by the Derivative Works, if and
|
|
116
|
+
wherever such third-party notices normally appear. The contents
|
|
117
|
+
of the NOTICE file are for informational purposes only and
|
|
118
|
+
do not modify the License. You may add Your own attribution
|
|
119
|
+
notices within Derivative Works that You distribute, alongside
|
|
120
|
+
or as an addendum to the NOTICE text from the Work, provided
|
|
121
|
+
that such additional attribution notices cannot be construed
|
|
122
|
+
as modifying the License.
|
|
123
|
+
|
|
124
|
+
You may add Your own copyright statement to Your modifications and
|
|
125
|
+
may provide additional or different license terms and conditions
|
|
126
|
+
for use, reproduction, or distribution of Your modifications, or
|
|
127
|
+
for any such Derivative Works as a whole, provided Your use,
|
|
128
|
+
reproduction, and distribution of the Work otherwise complies with
|
|
129
|
+
the conditions stated in this License.
|
|
130
|
+
|
|
131
|
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
132
|
+
any Contribution intentionally submitted for inclusion in the Work
|
|
133
|
+
by You to the Licensor shall be under the terms and conditions of
|
|
134
|
+
this License, without any additional terms or conditions.
|
|
135
|
+
Notwithstanding the above, nothing herein shall supersede or modify
|
|
136
|
+
the terms of any separate license agreement you may have executed
|
|
137
|
+
with Licensor regarding such Contributions.
|
|
138
|
+
|
|
139
|
+
6. Trademarks. This License does not grant permission to use the trade
|
|
140
|
+
names, trademarks, service marks, or product names of the Licensor,
|
|
141
|
+
except as required for reasonable and customary use in describing the
|
|
142
|
+
origin of the Work and reproducing the content of the NOTICE file.
|
|
143
|
+
|
|
144
|
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
145
|
+
agreed to in writing, Licensor provides the Work (and each
|
|
146
|
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
|
147
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
148
|
+
implied, including, without limitation, any warranties or conditions
|
|
149
|
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
150
|
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
151
|
+
appropriateness of using or redistributing the Work and assume any
|
|
152
|
+
risks associated with Your exercise of permissions under this License.
|
|
153
|
+
|
|
154
|
+
8. Limitation of Liability. In no event and under no legal theory,
|
|
155
|
+
whether in tort (including negligence), contract, or otherwise,
|
|
156
|
+
unless required by applicable law (such as deliberate and grossly
|
|
157
|
+
negligent acts) or agreed to in writing, shall any Contributor be
|
|
158
|
+
liable to You for damages, including any direct, indirect, special,
|
|
159
|
+
incidental, or consequential damages of any character arising as a
|
|
160
|
+
result of this License or out of the use or inability to use the
|
|
161
|
+
Work (including but not limited to damages for loss of goodwill,
|
|
162
|
+
work stoppage, computer failure or malfunction, or any and all
|
|
163
|
+
other commercial damages or losses), even if such Contributor
|
|
164
|
+
has been advised of the possibility of such damages.
|
|
165
|
+
|
|
166
|
+
9. Accepting Warranty or Additional Liability. While redistributing
|
|
167
|
+
the Work or Derivative Works thereof, You may choose to offer,
|
|
168
|
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
|
169
|
+
or other liability obligations and/or rights consistent with this
|
|
170
|
+
License. However, in accepting such obligations, You may act only
|
|
171
|
+
on Your own behalf and on Your sole responsibility, not on behalf
|
|
172
|
+
of any other Contributor, and only if You agree to indemnify,
|
|
173
|
+
defend, and hold each Contributor harmless for any liability
|
|
174
|
+
incurred by, or claims asserted against, such Contributor by reason
|
|
175
|
+
of your accepting any such warranty or additional liability.
|
|
176
|
+
|
|
177
|
+
END OF TERMS AND CONDITIONS
|
|
178
|
+
|
|
179
|
+
APPENDIX: How to apply the Apache License to your work.
|
|
180
|
+
|
|
181
|
+
To apply the Apache License to your work, attach the following
|
|
182
|
+
boilerplate notice, with the fields enclosed by brackets "[]"
|
|
183
|
+
replaced with your own identifying information. (Don't include
|
|
184
|
+
the brackets!) The text should be enclosed in the appropriate
|
|
185
|
+
comment syntax for the file format. We also recommend that a
|
|
186
|
+
file or class name and description of purpose be included on the
|
|
187
|
+
same "printed page" as the copyright notice for easier
|
|
188
|
+
identification within third-party archives.
|
|
189
|
+
|
|
190
|
+
Copyright [yyyy] [name of copyright owner]
|
|
191
|
+
|
|
192
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
193
|
+
you may not use this file except in compliance with the License.
|
|
194
|
+
You may obtain a copy of the License at
|
|
195
|
+
|
|
196
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
197
|
+
|
|
198
|
+
Unless required by applicable law or agreed to in writing, software
|
|
199
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
200
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
201
|
+
See the License for the specific language governing permissions and
|
|
202
|
+
limitations under the License.
|
canpii-0.1.0/NOTICE
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
canpii
|
|
2
|
+
Copyright 2026 Arthur Jin and canpii contributors
|
|
3
|
+
|
|
4
|
+
This product is licensed under the Apache License, Version 2.0 (see LICENSE).
|
|
5
|
+
|
|
6
|
+
canpii extends Microsoft Presidio (https://github.com/data-privacy-stack/presidio),
|
|
7
|
+
which is licensed under the MIT License. canpii does not copy Presidio source
|
|
8
|
+
code; it subclasses Presidio's public APIs and, where noted, enables Presidio's own
|
|
9
|
+
CaSinRecognizer and CaPostalCodeRecognizer at runtime.
|
|
10
|
+
|
|
11
|
+
canpii is an independent community project. It is not affiliated with or
|
|
12
|
+
endorsed by Microsoft, the Presidio maintainers, the Government of Canada, or any
|
|
13
|
+
provincial government or agency named in its documentation.
|
canpii-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: canpii
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Canadian PII recognizers (health numbers, passport, BN, bank, driver's licences) for Microsoft Presidio, with English/French context, LiteLLM/MCP-gateway exports and PIPEDA/PHIPA/Law 25 presets.
|
|
5
|
+
Project-URL: Homepage, https://github.com/ArthurJZL/canpii
|
|
6
|
+
Project-URL: Documentation, https://github.com/ArthurJZL/canpii#readme
|
|
7
|
+
Project-URL: Repository, https://github.com/ArthurJZL/canpii
|
|
8
|
+
Project-URL: Issues, https://github.com/ArthurJZL/canpii/issues
|
|
9
|
+
Project-URL: Changelog, https://github.com/ArthurJZL/canpii/blob/main/CHANGELOG.md
|
|
10
|
+
Author-email: Arthur Jin <arthurjin1998@gmail.com>
|
|
11
|
+
Maintainer-email: Arthur Jin <arthurjin1998@gmail.com>
|
|
12
|
+
License-Expression: Apache-2.0
|
|
13
|
+
License-File: LICENSE
|
|
14
|
+
License-File: NOTICE
|
|
15
|
+
Keywords: anonymization,canada,de-identification,guardrails,law 25,litellm,loi 25,mcp,ohip,phipa,pii,pipeda,presidio,privacy,ramq,redaction
|
|
16
|
+
Classifier: Development Status :: 3 - Alpha
|
|
17
|
+
Classifier: Intended Audience :: Developers
|
|
18
|
+
Classifier: Intended Audience :: Healthcare Industry
|
|
19
|
+
Classifier: Natural Language :: English
|
|
20
|
+
Classifier: Natural Language :: French
|
|
21
|
+
Classifier: Operating System :: OS Independent
|
|
22
|
+
Classifier: Programming Language :: Python :: 3
|
|
23
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
24
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
25
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
26
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
27
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
28
|
+
Classifier: Topic :: Scientific/Engineering :: Information Analysis
|
|
29
|
+
Classifier: Topic :: Security
|
|
30
|
+
Classifier: Topic :: Text Processing
|
|
31
|
+
Classifier: Typing :: Typed
|
|
32
|
+
Requires-Python: >=3.10
|
|
33
|
+
Requires-Dist: presidio-analyzer>=2.2.365
|
|
34
|
+
Requires-Dist: pyyaml>=6.0
|
|
35
|
+
Provides-Extra: anonymizer
|
|
36
|
+
Requires-Dist: presidio-anonymizer>=2.2.365; extra == 'anonymizer'
|
|
37
|
+
Description-Content-Type: text/markdown
|
|
38
|
+
|
|
39
|
+
# canpii
|
|
40
|
+
|
|
41
|
+
**Canadian PII recognizers for [Microsoft Presidio](https://github.com/data-privacy-stack/presidio):
|
|
42
|
+
provincial health numbers, passport, CRA Business Number, bank routing and driver's
|
|
43
|
+
licences, with English and French context, LiteLLM / MCP-gateway exports and
|
|
44
|
+
PIPEDA / PHIPA / Quebec Law 25 presets.**
|
|
45
|
+
|
|
46
|
+
[](https://github.com/ArthurJZL/canpii/actions/workflows/ci.yml)
|
|
47
|
+

|
|
48
|
+

|
|
49
|
+

|
|
50
|
+
|
|
51
|
+
## Why this exists
|
|
52
|
+
|
|
53
|
+
Presidio is the de-facto open-source PII detector (the `presidio-analyzer` package has
|
|
54
|
+
millions of monthly downloads), but for Canada it ships only two recognizers —
|
|
55
|
+
`CA_SIN` and `CA_POSTAL_CODE` — and both are disabled by default. Canadian
|
|
56
|
+
health numbers, driver's licences, passports, Business Numbers and bank details are
|
|
57
|
+
not covered, while closed products (Microsoft Purview's PIPEDA/PHIPA templates, AWS
|
|
58
|
+
Bedrock Guardrails' `CA_HEALTH_NUMBER`) do cover them.
|
|
59
|
+
|
|
60
|
+
New country recognizers are merged into Presidio slowly, and that is reasonable for a
|
|
61
|
+
project of its size. This package lets you **use Canadian recognizers today**, written
|
|
62
|
+
in Presidio's own style, and we **upstream what Presidio accepts**.
|
|
63
|
+
|
|
64
|
+
## Quickstart
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
pip install canpii
|
|
68
|
+
python -m spacy download en_core_web_sm
|
|
69
|
+
python -m spacy download fr_core_news_sm
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
from canpii import create_analyzer, get_preset
|
|
74
|
+
|
|
75
|
+
analyzer = create_analyzer(languages=["en", "fr"]) # spaCy *_sm models, threshold 0.4
|
|
76
|
+
text = "Carte RAMQ ZZZX 8812 2512, numéro d'entreprise 123456782 RT 0001."
|
|
77
|
+
for r in analyzer.analyze(text, language="fr", entities=get_preset("quebec_law25").entity_list()):
|
|
78
|
+
print(r.entity_type, text[r.start : r.end], round(r.score, 2))
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
```text
|
|
82
|
+
CA_BN 123456782 RT 0001 1.0
|
|
83
|
+
CA_HEALTH_NUMBER ZZZX 8812 2512 0.85
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Already have an `AnalyzerEngine`? Add the recognizers to its registry:
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
from presidio_analyzer import AnalyzerEngine
|
|
90
|
+
from presidio_anonymizer import AnonymizerEngine
|
|
91
|
+
from canpii import register_all
|
|
92
|
+
|
|
93
|
+
analyzer = AnalyzerEngine() # your existing engine (Presidio's default uses en_core_web_lg)
|
|
94
|
+
register_all(analyzer.registry, languages=("en",))
|
|
95
|
+
text = "Patient OHIP 1234-567-897 AB, BC PHN 9000 000 018."
|
|
96
|
+
results = analyzer.analyze(text, language="en", score_threshold=0.4)
|
|
97
|
+
print(AnonymizerEngine().anonymize(text=text, analyzer_results=results).text)
|
|
98
|
+
# Patient OHIP <CA_HEALTH_NUMBER>, BC PHN <CA_HEALTH_NUMBER>.
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
`register_all()` also enables Presidio's own `CaSinRecognizer` and
|
|
102
|
+
`CaPostalCodeRecognizer`. Use a **score threshold of about 0.4**: weak,
|
|
103
|
+
context-dependent patterns are scored well below it and only cross it when a
|
|
104
|
+
context word ("OHIP", "carte santé", "permis de conduire", …) is nearby.
|
|
105
|
+
|
|
106
|
+
No-code / YAML setup with Presidio's `RecognizerRegistryProvider`:
|
|
107
|
+
|
|
108
|
+
```python
|
|
109
|
+
import canpii # registers the classes so `type: predefined` entries resolve
|
|
110
|
+
from presidio_analyzer.recognizer_registry import RecognizerRegistryProvider
|
|
111
|
+
|
|
112
|
+
registry = RecognizerRegistryProvider(
|
|
113
|
+
conf_file=canpii.recognizer_registry_yaml()
|
|
114
|
+
).create_recognizer_registry()
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## Supported entities
|
|
118
|
+
|
|
119
|
+
| Entity | Province | Format | Validation | Format source | Languages |
|
|
120
|
+
|---|---|---|---|---|---|
|
|
121
|
+
| `CA_HEALTH_NUMBER` | ON (OHIP) | 10 digits + optional 1–2 letter version code | structure; Luhn as bonus only (community-sourced) | official | en, fr |
|
|
122
|
+
| `CA_HEALTH_NUMBER` | QC (RAMQ / NAM) | 4 letters + 8 digits with embedded birth date | structure + birth-date check | official | en, fr |
|
|
123
|
+
| `CA_HEALTH_NUMBER` | BC (PHN) | 10 digits starting with 9 | MOD-11 checksum | official | en, fr |
|
|
124
|
+
| `CA_HEALTH_NUMBER` | AB (AHCIP) | 9 digits | context-only | official | en, fr |
|
|
125
|
+
| `CA_PASSPORT` | federal | 2 letters + 6 digits | context-only | secondary | en, fr |
|
|
126
|
+
| `CA_BN` | federal | 9 digits, or BN + RT/RP/RC/… + 4 digits | program id check; Luhn as bonus only (community-sourced) | official | en, fr |
|
|
127
|
+
| `CA_BANK_ACCOUNT` | federal | `TTTTT-III` transit, `0IIITTTTT` routing, 7–12 digit account | context-only | official / secondary | en, fr |
|
|
128
|
+
| `CA_DRIVER_LICENSE` | ON | 1 letter + 14 digits (`A1234-56789-01234`) | structure; birth date as bonus only | secondary | en, fr |
|
|
129
|
+
| `CA_DRIVER_LICENSE` | QC | 1 letter + 12 digits | context-only | secondary | en, fr |
|
|
130
|
+
| `CA_DRIVER_LICENSE` | BC | 7 digits | context-only (very weak) | secondary | en, fr |
|
|
131
|
+
| `CA_SIN`, `CA_POSTAL_CODE` | federal | — | upstream Presidio recognizers, enabled by `register_all()` | — | en, fr |
|
|
132
|
+
|
|
133
|
+
Every rule, with its source URL and confidence level, is in
|
|
134
|
+
[`docs/entities.md`](docs/entities.md). Rules that are only **secondary** or
|
|
135
|
+
**community-sourced** never reject a match on their own; they raise the score when
|
|
136
|
+
they pass (pass `strict=True` to also reject on failure). Rules from **official**
|
|
137
|
+
sources reject matches that fail them.
|
|
138
|
+
|
|
139
|
+
Other provinces and territories are tracked in [`docs/roadmap.md`](docs/roadmap.md).
|
|
140
|
+
|
|
141
|
+
## Gateways: LiteLLM, ContextForge and others
|
|
142
|
+
|
|
143
|
+
```bash
|
|
144
|
+
# LiteLLM presidio_ad_hoc_recognizers JSON (one copy per language)
|
|
145
|
+
canpii export --format litellm --preset pipeda -l en,fr -o canpii_ad_hoc.json
|
|
146
|
+
# LiteLLM config.yaml guardrail snippet (pii_entities_config for the preset, MASK)
|
|
147
|
+
canpii export --format litellm-config --preset pipeda
|
|
148
|
+
# IBM ContextForge PIIFilterPlugin custom_patterns (distinctive formats only)
|
|
149
|
+
canpii export --format contextforge --preset phipa
|
|
150
|
+
# Gateway-neutral regex + entity + score + context JSON
|
|
151
|
+
canpii export --format generic
|
|
152
|
+
# Presidio RecognizerRegistryProvider YAML (keeps full validation)
|
|
153
|
+
canpii export --format presidio-yaml --preset quebec_law25
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
**Regex-only exports lose validation.** LiteLLM's ad-hoc recognizers and gateway
|
|
157
|
+
custom patterns are plain regular expressions: BC's MOD-11 checksum, the RAMQ
|
|
158
|
+
birth-date check, the Luhn bonuses and the negative-context filter do not travel with
|
|
159
|
+
them. Expect more false positives than with the Python package. The ContextForge
|
|
160
|
+
export keeps only patterns with a base score ≥ 0.3, because gateways without context
|
|
161
|
+
support would otherwise redact every 7- to 12-digit number. All exported regexes avoid
|
|
162
|
+
look-arounds and back-references so they also compile in Rust's `regex` crate.
|
|
163
|
+
|
|
164
|
+
For LiteLLM, the Presidio analyzer server must have a French spaCy model loaded to use
|
|
165
|
+
`presidio_language: fr`. If you can run your own analyzer image, `pip install
|
|
166
|
+
canpii` into it and load the `presidio-yaml` export instead: you keep every
|
|
167
|
+
check.
|
|
168
|
+
|
|
169
|
+
## Policy presets
|
|
170
|
+
|
|
171
|
+
| Preset | Law | Highlights |
|
|
172
|
+
|---|---|---|
|
|
173
|
+
| `pipeda` | PIPEDA (federal private sector) | identity, government and financial numbers, contact details |
|
|
174
|
+
| `phipa` | Ontario PHIPA (health) | health numbers, names, contact details, dates and locations |
|
|
175
|
+
| `quebec_law25` | Quebec P-39.1 as amended by Law 25 | as `pipeda` + dates; French first |
|
|
176
|
+
|
|
177
|
+
```python
|
|
178
|
+
from canpii import get_preset
|
|
179
|
+
|
|
180
|
+
get_preset("phipa").entity_list()
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
Presets are **a starting point, not legal advice** — see [`docs/presets.md`](docs/presets.md).
|
|
184
|
+
|
|
185
|
+
## Benchmark
|
|
186
|
+
|
|
187
|
+
`benchmarks/run_benchmark.py` generates a labeled bilingual synthetic corpus (1,740
|
|
188
|
+
sentences: 740 positives with and without context words, 1,000 negatives including
|
|
189
|
+
phone numbers, dates, order IDs, amounts, near-miss identifiers and "hard negatives"
|
|
190
|
+
that put a context word next to an unrelated number) and reports precision/recall at
|
|
191
|
+
threshold 0.4.
|
|
192
|
+
|
|
193
|
+
Positives **with context words** (seed 2026, `presidio-analyzer` 2.2.366, spaCy `*_sm`):
|
|
194
|
+
|
|
195
|
+
| Entity | Gold | P (substring) | R (substring) | P (whole-word) | R (whole-word) |
|
|
196
|
+
|---|---:|---:|---:|---:|---:|
|
|
197
|
+
| CA_HEALTH_NUMBER | 160 | 0.870 | 1.000 | 0.870 | 1.000 |
|
|
198
|
+
| CA_PASSPORT | 40 | 1.000 | 1.000 | 1.000 | 1.000 |
|
|
199
|
+
| CA_BN | 40 | 1.000 | 1.000 | 1.000 | 1.000 |
|
|
200
|
+
| CA_BANK_ACCOUNT | 100 | 0.714 | 1.000 | 0.714 | 1.000 |
|
|
201
|
+
| CA_DRIVER_LICENSE | 120 | 0.909 | 1.000 | 0.909 | 1.000 |
|
|
202
|
+
| CA_SIN (upstream) | 20 | 0.526 | 1.000 | 1.000 | 1.000 |
|
|
203
|
+
|
|
204
|
+
All positives, including sentences **without** context words:
|
|
205
|
+
|
|
206
|
+
| Entity | Gold | P (substring) | R (substring) | P (whole-word) | R (whole-word) |
|
|
207
|
+
|---|---:|---:|---:|---:|---:|
|
|
208
|
+
| CA_HEALTH_NUMBER | 240 | 0.896 | 0.863 | 0.896 | 0.863 |
|
|
209
|
+
| CA_PASSPORT | 60 | 1.000 | 0.667 | 1.000 | 0.667 |
|
|
210
|
+
| CA_BN | 60 | 1.000 | 0.933 | 1.000 | 0.933 |
|
|
211
|
+
| CA_BANK_ACCOUNT | 160 | 0.714 | 0.625 | 0.714 | 0.625 |
|
|
212
|
+
| CA_DRIVER_LICENSE | 180 | 0.921 | 0.778 | 0.921 | 0.778 |
|
|
213
|
+
| CA_SIN (upstream) | 40 | 0.679 | 0.950 | 1.000 | 0.950 |
|
|
214
|
+
|
|
215
|
+
How to read this:
|
|
216
|
+
|
|
217
|
+
* **This is a regression benchmark on template sentences we wrote, not a real-world
|
|
218
|
+
accuracy estimate.** Scores and negative-context words were tuned on part of the
|
|
219
|
+
negatives; the remaining false positives come from an "untuned" set ("Patient chart
|
|
220
|
+
1234567890", "Bank statement page …", "Driver ID …") where a context word sits next
|
|
221
|
+
to an unrelated number — something no pattern recognizer can resolve.
|
|
222
|
+
* Context-only identifiers (Alberta PHN, BC licence, bank account, passport) are
|
|
223
|
+
intentionally missed without context words; that is what lowers recall in the second
|
|
224
|
+
table.
|
|
225
|
+
* "substring" vs "whole-word" is Presidio's context matching mode.
|
|
226
|
+
`create_analyzer()` defaults to whole-word: in substring mode, upstream `CA_SIN`'s
|
|
227
|
+
context word "sin" matches inside "**busin**ess", so every Business Number next to the
|
|
228
|
+
word "business" is also reported as a SIN.
|
|
229
|
+
|
|
230
|
+
Reproduce with `uv run python benchmarks/run_benchmark.py --show-errors`.
|
|
231
|
+
|
|
232
|
+
## Relationship to upstream Presidio
|
|
233
|
+
|
|
234
|
+
* This is an independent community package, **not affiliated with Microsoft or the
|
|
235
|
+
Presidio maintainers**. It builds on Presidio's public APIs (`PatternRecognizer`,
|
|
236
|
+
`RecognizerRegistry`, `RecognizerRegistryProvider`) and follows its contribution
|
|
237
|
+
conventions: one recognizer class per file, `COUNTRY_CODE = "ca"`, per-language
|
|
238
|
+
instances, `invalidate_result` for checks, source URLs in docstrings.
|
|
239
|
+
* Credit to the Presidio maintainers and to the upstream Canadian contributors:
|
|
240
|
+
`CA_SIN`, and `CA_POSTAL_CODE` (presidio#2204, by GitHub user watsos). We use those recognizers as
|
|
241
|
+
they are rather than reimplementing them.
|
|
242
|
+
* **Business Number:** upstream issue presidio#2206 proposes a `CA_BN` recognizer and
|
|
243
|
+
PR presidio#2207 by GitHub user Joey239716 implements it (open since 2026-07-29). Our
|
|
244
|
+
`CA_BN` recognizer is an independent implementation written from the CRA
|
|
245
|
+
documentation and deliberately uses the same entity name, so switching is seamless.
|
|
246
|
+
**Once #2207 (or an equivalent) is merged, we will defer to upstream**: deprecate ours
|
|
247
|
+
for one minor release, then delegate.
|
|
248
|
+
* Upstreaming plan: recognizers whose formats are backed by official sources (BC PHN,
|
|
249
|
+
RAMQ first) will be proposed to Presidio as individual PRs, in Presidio's code style,
|
|
250
|
+
once they have had real-world use here. See [`docs/roadmap.md`](docs/roadmap.md).
|
|
251
|
+
|
|
252
|
+
## Disclaimer
|
|
253
|
+
|
|
254
|
+
* **Not legal advice.** Presets and documentation describe engineering starting points.
|
|
255
|
+
Compliance with PIPEDA, PHIPA, Law 25 or any other law is your responsibility.
|
|
256
|
+
* **Detection is probabilistic.** Pattern recognizers miss identifiers and flag
|
|
257
|
+
unrelated numbers. Do not rely on this package alone to protect sensitive data.
|
|
258
|
+
* **Synthetic data only.** All identifiers in this repository, its tests and its
|
|
259
|
+
documentation are generated or obviously fake. Randomly generated values can still
|
|
260
|
+
coincide with issued identifiers; never use them as real identifiers. **Never commit,
|
|
261
|
+
paste or upload real identifiers** — not in issues, PRs, fixtures, screenshots or
|
|
262
|
+
logs. See [`SECURITY.md`](SECURITY.md).
|
|
263
|
+
* Government programs and agencies are named only to describe the identifiers they
|
|
264
|
+
issue; no endorsement is implied.
|
|
265
|
+
|
|
266
|
+
## Contributing
|
|
267
|
+
|
|
268
|
+
Adding a province or identifier is a well-defined task — see
|
|
269
|
+
[`CONTRIBUTING.md`](CONTRIBUTING.md) and the drafted good-first-issues in
|
|
270
|
+
[`docs/issues/`](docs/issues/). Commits must be signed off (DCO).
|
|
271
|
+
|
|
272
|
+
## License
|
|
273
|
+
|
|
274
|
+
[Apache-2.0](LICENSE). Presidio is MIT-licensed; see [`NOTICE`](NOTICE).
|