privaci 0.1.0b4__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 (141) hide show
  1. privaci-0.1.0b4/LICENSE +99 -0
  2. privaci-0.1.0b4/PKG-INFO +108 -0
  3. privaci-0.1.0b4/README.md +65 -0
  4. privaci-0.1.0b4/pyproject.toml +152 -0
  5. privaci-0.1.0b4/setup.cfg +4 -0
  6. privaci-0.1.0b4/src/privaci/__init__.py +5 -0
  7. privaci-0.1.0b4/src/privaci/__main__.py +8 -0
  8. privaci-0.1.0b4/src/privaci/autodetect/__init__.py +27 -0
  9. privaci-0.1.0b4/src/privaci/autodetect/actions.py +70 -0
  10. privaci-0.1.0b4/src/privaci/autodetect/freeform.py +68 -0
  11. privaci-0.1.0b4/src/privaci/autodetect/matcher.py +59 -0
  12. privaci-0.1.0b4/src/privaci/autodetect/models.py +63 -0
  13. privaci-0.1.0b4/src/privaci/autodetect/patterns.py +119 -0
  14. privaci-0.1.0b4/src/privaci/autodetect/report.py +71 -0
  15. privaci-0.1.0b4/src/privaci/autodetect/resolve.py +73 -0
  16. privaci-0.1.0b4/src/privaci/autodetect/scanner.py +153 -0
  17. privaci-0.1.0b4/src/privaci/autodetect/table_context.py +53 -0
  18. privaci-0.1.0b4/src/privaci/catalog/__init__.py +39 -0
  19. privaci-0.1.0b4/src/privaci/catalog/audit_skipped.py +40 -0
  20. privaci-0.1.0b4/src/privaci/catalog/detectors.py +217 -0
  21. privaci-0.1.0b4/src/privaci/catalog/graph.py +203 -0
  22. privaci-0.1.0b4/src/privaci/catalog/identifiers.py +76 -0
  23. privaci-0.1.0b4/src/privaci/catalog/introspect.py +371 -0
  24. privaci-0.1.0b4/src/privaci/catalog/models.py +242 -0
  25. privaci-0.1.0b4/src/privaci/catalog/partitions.py +129 -0
  26. privaci-0.1.0b4/src/privaci/catalog/queries.py +251 -0
  27. privaci-0.1.0b4/src/privaci/catalog/skipped.py +63 -0
  28. privaci-0.1.0b4/src/privaci/catalog/snapshot.py +292 -0
  29. privaci-0.1.0b4/src/privaci/cli/__init__.py +3 -0
  30. privaci-0.1.0b4/src/privaci/cli/_catalog.py +94 -0
  31. privaci-0.1.0b4/src/privaci/cli/_errors.py +93 -0
  32. privaci-0.1.0b4/src/privaci/cli/_resume.py +89 -0
  33. privaci-0.1.0b4/src/privaci/cli/_run.py +187 -0
  34. privaci-0.1.0b4/src/privaci/cli/app.py +306 -0
  35. privaci-0.1.0b4/src/privaci/cli/context.py +112 -0
  36. privaci-0.1.0b4/src/privaci/cli/generate_ci.py +152 -0
  37. privaci-0.1.0b4/src/privaci/cli/logging_setup.py +16 -0
  38. privaci-0.1.0b4/src/privaci/cli/options.py +66 -0
  39. privaci-0.1.0b4/src/privaci/config/__init__.py +50 -0
  40. privaci-0.1.0b4/src/privaci/config/actions.py +170 -0
  41. privaci-0.1.0b4/src/privaci/config/loader.py +253 -0
  42. privaci-0.1.0b4/src/privaci/config/models.py +133 -0
  43. privaci-0.1.0b4/src/privaci/contracts/__init__.py +50 -0
  44. privaci-0.1.0b4/src/privaci/contracts/base.py +125 -0
  45. privaci-0.1.0b4/src/privaci/contracts/fallbacks.py +94 -0
  46. privaci-0.1.0b4/src/privaci/contracts/plugins.py +86 -0
  47. privaci-0.1.0b4/src/privaci/errors.py +131 -0
  48. privaci-0.1.0b4/src/privaci/mask/__init__.py +17 -0
  49. privaci-0.1.0b4/src/privaci/mask/column_masker.py +201 -0
  50. privaci-0.1.0b4/src/privaci/mask/engine.py +95 -0
  51. privaci-0.1.0b4/src/privaci/mask/faker/__init__.py +31 -0
  52. privaci-0.1.0b4/src/privaci/mask/faker/base.py +32 -0
  53. privaci-0.1.0b4/src/privaci/mask/faker/context.py +34 -0
  54. privaci-0.1.0b4/src/privaci/mask/faker/engine.py +41 -0
  55. privaci-0.1.0b4/src/privaci/mask/faker/hash.py +47 -0
  56. privaci-0.1.0b4/src/privaci/mask/faker/libraries.py +192 -0
  57. privaci-0.1.0b4/src/privaci/mask/faker/providers/__init__.py +7 -0
  58. privaci-0.1.0b4/src/privaci/mask/faker/providers/builtin.py +240 -0
  59. privaci-0.1.0b4/src/privaci/mask/faker/registry.py +84 -0
  60. privaci-0.1.0b4/src/privaci/mask/faker/uniqueness.py +74 -0
  61. privaci-0.1.0b4/src/privaci/mask/ner.py +114 -0
  62. privaci-0.1.0b4/src/privaci/mask/regex_safe.py +98 -0
  63. privaci-0.1.0b4/src/privaci/mask/safe_log.py +25 -0
  64. privaci-0.1.0b4/src/privaci/observability/__init__.py +30 -0
  65. privaci-0.1.0b4/src/privaci/observability/events.py +93 -0
  66. privaci-0.1.0b4/src/privaci/observability/jsonlog.py +123 -0
  67. privaci-0.1.0b4/src/privaci/observability/metrics.py +92 -0
  68. privaci-0.1.0b4/src/privaci/observability/progress.py +86 -0
  69. privaci-0.1.0b4/src/privaci/observability/redact.py +104 -0
  70. privaci-0.1.0b4/src/privaci/packs/__init__.py +7 -0
  71. privaci-0.1.0b4/src/privaci/packs/install.py +173 -0
  72. privaci-0.1.0b4/src/privaci/packs/keys.py +40 -0
  73. privaci-0.1.0b4/src/privaci/packs/verify.py +64 -0
  74. privaci-0.1.0b4/src/privaci/pipeline/__init__.py +7 -0
  75. privaci-0.1.0b4/src/privaci/pipeline/lifecycle.py +169 -0
  76. privaci-0.1.0b4/src/privaci/pipeline/runner.py +277 -0
  77. privaci-0.1.0b4/src/privaci/pipeline/streaming.py +238 -0
  78. privaci-0.1.0b4/src/privaci/pipeline/table_plan.py +84 -0
  79. privaci-0.1.0b4/src/privaci/preflight/__init__.py +8 -0
  80. privaci-0.1.0b4/src/privaci/preflight/checks.py +214 -0
  81. privaci-0.1.0b4/src/privaci/preflight/runner.py +116 -0
  82. privaci-0.1.0b4/src/privaci/preflight/salt.py +67 -0
  83. privaci-0.1.0b4/src/privaci/preflight/target.py +101 -0
  84. privaci-0.1.0b4/src/privaci/py.typed +0 -0
  85. privaci-0.1.0b4/src/privaci/runtime/__init__.py +19 -0
  86. privaci-0.1.0b4/src/privaci/runtime/signals.py +55 -0
  87. privaci-0.1.0b4/src/privaci/schema/__init__.py +7 -0
  88. privaci-0.1.0b4/src/privaci/schema/ddl.py +101 -0
  89. privaci-0.1.0b4/src/privaci/schema/extensions.py +27 -0
  90. privaci-0.1.0b4/src/privaci/schema/replicate.py +185 -0
  91. privaci-0.1.0b4/src/privaci/schema/sequences.py +93 -0
  92. privaci-0.1.0b4/src/privaci/schema/strategies.py +55 -0
  93. privaci-0.1.0b4/src/privaci/secrets/__init__.py +26 -0
  94. privaci-0.1.0b4/src/privaci/secrets/backends/__init__.py +3 -0
  95. privaci-0.1.0b4/src/privaci/secrets/backends/aws_sm.py +105 -0
  96. privaci-0.1.0b4/src/privaci/secrets/backends/azure_kv.py +80 -0
  97. privaci-0.1.0b4/src/privaci/secrets/backends/constants.py +7 -0
  98. privaci-0.1.0b4/src/privaci/secrets/backends/env.py +30 -0
  99. privaci-0.1.0b4/src/privaci/secrets/backends/file.py +100 -0
  100. privaci-0.1.0b4/src/privaci/secrets/backends/hashicorp.py +95 -0
  101. privaci-0.1.0b4/src/privaci/secrets/parser.py +207 -0
  102. privaci-0.1.0b4/src/privaci/secrets/resolver.py +222 -0
  103. privaci-0.1.0b4/src/privaci/secrets/types.py +52 -0
  104. privaci-0.1.0b4/src/privaci/spikes/__init__.py +19 -0
  105. privaci-0.1.0b4/src/privaci/spikes/_env.py +11 -0
  106. privaci-0.1.0b4/src/privaci/spikes/copy_binary.py +106 -0
  107. privaci-0.1.0b4/src/privaci/spikes/cyclic_fk.py +62 -0
  108. privaci-0.1.0b4/src/privaci/spikes/spacy_throughput.py +105 -0
  109. privaci-0.1.0b4/src/privaci/state/__init__.py +65 -0
  110. privaci-0.1.0b4/src/privaci/state/audit.py +117 -0
  111. privaci-0.1.0b4/src/privaci/state/checkpoints.py +103 -0
  112. privaci-0.1.0b4/src/privaci/state/ddl.py +89 -0
  113. privaci-0.1.0b4/src/privaci/state/fingerprints.py +93 -0
  114. privaci-0.1.0b4/src/privaci/state/models.py +78 -0
  115. privaci-0.1.0b4/src/privaci/state/resume.py +251 -0
  116. privaci-0.1.0b4/src/privaci/state/runs.py +108 -0
  117. privaci-0.1.0b4/src/privaci/state/schema.py +88 -0
  118. privaci-0.1.0b4/src/privaci/stream/__init__.py +7 -0
  119. privaci-0.1.0b4/src/privaci/stream/batch.py +47 -0
  120. privaci-0.1.0b4/src/privaci/stream/batch_write.py +91 -0
  121. privaci-0.1.0b4/src/privaci/stream/coerce.py +124 -0
  122. privaci-0.1.0b4/src/privaci/stream/copy_binary.py +133 -0
  123. privaci-0.1.0b4/src/privaci/stream/fetch.py +113 -0
  124. privaci-0.1.0b4/src/privaci/stream/models.py +70 -0
  125. privaci-0.1.0b4/src/privaci/stream/retry.py +57 -0
  126. privaci-0.1.0b4/src/privaci/stream/table.py +221 -0
  127. privaci-0.1.0b4/src/privaci/verify/__init__.py +13 -0
  128. privaci-0.1.0b4/src/privaci/verify/compare.py +143 -0
  129. privaci-0.1.0b4/src/privaci/verify/models.py +74 -0
  130. privaci-0.1.0b4/src/privaci/verify/runner.py +147 -0
  131. privaci-0.1.0b4/src/privaci/verify/structural.py +141 -0
  132. privaci-0.1.0b4/src/privaci.egg-info/PKG-INFO +108 -0
  133. privaci-0.1.0b4/src/privaci.egg-info/SOURCES.txt +139 -0
  134. privaci-0.1.0b4/src/privaci.egg-info/dependency_links.txt +1 -0
  135. privaci-0.1.0b4/src/privaci.egg-info/entry_points.txt +2 -0
  136. privaci-0.1.0b4/src/privaci.egg-info/requires.txt +27 -0
  137. privaci-0.1.0b4/src/privaci.egg-info/top_level.txt +1 -0
  138. privaci-0.1.0b4/tests/test_cli.py +259 -0
  139. privaci-0.1.0b4/tests/test_errors.py +113 -0
  140. privaci-0.1.0b4/tests/test_secrets.py +56 -0
  141. privaci-0.1.0b4/tests/test_version.py +13 -0
@@ -0,0 +1,99 @@
1
+ # Copyright 2026 BoundaryLogic. All rights reserved.
2
+ #
3
+ # The PrivaCI source code is licensed under the Elastic License 2.0.
4
+ # See https://www.elastic.co/licensing/elastic-license
5
+
6
+
7
+ Elastic License 2.0
8
+
9
+ URL: https://www.elastic.co/licensing/elastic-license
10
+
11
+ ## Acceptance
12
+
13
+ By using the software, you agree to all of the terms and conditions below.
14
+
15
+ ## Copyright License
16
+
17
+ The licensor grants you a non-exclusive, royalty-free, worldwide,
18
+ non-sublicensable, non-transferable license to use, copy, distribute, make
19
+ available, and prepare derivative works of the software, in each case subject to
20
+ the limitations and conditions below.
21
+
22
+ ## Limitations
23
+
24
+ You may not provide the software to third parties as a hosted or managed
25
+ service, where the service provides users with access to any substantial set of
26
+ the features or functionality of the software.
27
+
28
+ You may not move, change, disable, or circumvent the license key functionality
29
+ in the software, and you may not remove or obscure any functionality in the
30
+ software that is protected by the license key.
31
+
32
+ You may not alter, remove, or obscure any licensing, copyright, or other notices
33
+ of the licensor in the software. Any use of the licensor’s trademarks is subject
34
+ to applicable law.
35
+
36
+ ## Patents
37
+
38
+ The licensor grants you a license, under any patent claims the licensor can
39
+ license, or becomes able to license, to make, have made, use, sell, offer for
40
+ sale, import and have imported the software, in each case subject to the
41
+ limitations and conditions in this license. This license does not cover any
42
+ patent claims that you cause to be infringed by modifications or additions to
43
+ the software. If you or your company make any written claim that the software
44
+ infringes or contributes to infringement of any patent, your patent license for
45
+ the software granted under these terms ends immediately. If your company makes
46
+ such a claim, your patent license ends immediately for work on behalf of your
47
+ company.
48
+
49
+ ## Notices
50
+
51
+ You must ensure that anyone who gets a copy of any part of the software from you
52
+ also gets a copy of these terms.
53
+
54
+ If you modify the software, you must include in any modified copies of the
55
+ software prominent notices stating that you have modified the software.
56
+
57
+ ## No Other Rights
58
+
59
+ These terms do not imply any licenses other than those expressly granted in
60
+ these terms.
61
+
62
+ ## Termination
63
+
64
+ If you use the software in violation of these terms, such use is not licensed,
65
+ and your licenses will automatically terminate. If the licensor provides you
66
+ with a notice of your violation, and you cease all violation of this license no
67
+ later than 30 days after you receive that notice, your licenses will be
68
+ reinstated retroactively. However, if you violate these terms after such
69
+ reinstatement, any additional violation of these terms will cause your licenses
70
+ to terminate automatically and permanently.
71
+
72
+ ## No Liability
73
+
74
+ *As far as the law allows, the software comes as is, without any warranty or
75
+ condition, and the licensor will not be liable to you for any damages arising
76
+ out of these terms or the use or nature of the software, under any kind of
77
+ legal claim.*
78
+
79
+ ## Definitions
80
+
81
+ The **licensor** is the entity offering these terms, and the **software** is the
82
+ software the licensor makes available under these terms, including any portion
83
+ of it.
84
+
85
+ **you** refers to the individual or entity agreeing to these terms.
86
+
87
+ **your company** is any legal entity, sole proprietorship, or other kind of
88
+ organization that you work for, plus all organizations that have control over,
89
+ are under the control of, or are under common control with that
90
+ organization. **control** means ownership of substantially all the assets of an
91
+ entity, or the power to direct its management and policies by vote, contract, or
92
+ otherwise. Control can be direct or indirect.
93
+
94
+ **your licenses** are all the licenses granted to you for the software under
95
+ these terms.
96
+
97
+ **use** means anything you do with the software requiring one of your licenses.
98
+
99
+ **trademark** means trademarks, service marks, and similar rights.
@@ -0,0 +1,108 @@
1
+ Metadata-Version: 2.4
2
+ Name: privaci
3
+ Version: 0.1.0b4
4
+ Summary: In-VPC PostgreSQL masking and anonymization engine
5
+ Author-email: BoundaryLogic <hello@boundarylogic.io>
6
+ License: Elastic License 2.0
7
+ Classifier: Development Status :: 3 - Alpha
8
+ Classifier: Intended Audience :: Developers
9
+ Classifier: License :: Other/Proprietary License
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3.12
12
+ Classifier: Topic :: Database
13
+ Classifier: Topic :: Security
14
+ Requires-Python: >=3.12
15
+ Description-Content-Type: text/markdown
16
+ License-File: LICENSE
17
+ Requires-Dist: asyncpg==0.30.0
18
+ Requires-Dist: click<8.5,>=8.1.7
19
+ Requires-Dist: cryptography==48.0.1
20
+ Requires-Dist: pydantic==2.10.6
21
+ Requires-Dist: pyyaml==6.0.2
22
+ Requires-Dist: typer==0.16.0
23
+ Provides-Extra: nlp
24
+ Requires-Dist: spacy==3.8.7; extra == "nlp"
25
+ Provides-Extra: dev
26
+ Requires-Dist: black==26.3.1; extra == "dev"
27
+ Requires-Dist: cosmic-ray==8.4.6; extra == "dev"
28
+ Requires-Dist: hypothesis==6.127.6; extra == "dev"
29
+ Requires-Dist: isort==6.0.1; extra == "dev"
30
+ Requires-Dist: mkdocs-material==9.6.11; extra == "dev"
31
+ Requires-Dist: mkdocs==1.6.1; extra == "dev"
32
+ Requires-Dist: mypy==1.15.0; extra == "dev"
33
+ Requires-Dist: pip-audit==2.7.3; extra == "dev"
34
+ Requires-Dist: pip-tools==7.4.1; extra == "dev"
35
+ Requires-Dist: pre-commit==4.1.0; extra == "dev"
36
+ Requires-Dist: pytest-asyncio==1.4.0; extra == "dev"
37
+ Requires-Dist: pytest-cov==6.0.0; extra == "dev"
38
+ Requires-Dist: pytest-mock==3.14.0; extra == "dev"
39
+ Requires-Dist: pytest==9.0.3; extra == "dev"
40
+ Requires-Dist: ruff==0.9.6; extra == "dev"
41
+ Requires-Dist: types-PyYAML==6.0.12.20241230; extra == "dev"
42
+ Dynamic: license-file
43
+
44
+ # PrivaCI
45
+
46
+ **One command. Sanitized staging data. No data leaves your VPC.**
47
+
48
+ PrivaCI is a stateless batch engine that reads from a PostgreSQL source
49
+ (typically a production replica), masks PII with a three-tier pipeline, and
50
+ writes realistic synthetic data to a staging database with referential
51
+ integrity preserved.
52
+
53
+ ## Prerequisites
54
+
55
+ - Python **3.12+**, or the official container image
56
+ (`ghcr.io/boundarylogic/privaci`)
57
+ - A PostgreSQL **source** (typically a production replica) and an empty
58
+ **target** database
59
+
60
+ ## Quickstart
61
+
62
+ **Fastest path** — self-contained evaluation stack (~60 s):
63
+
64
+ ```bash
65
+ export ANONYMIZATION_SALT="$(openssl rand -hex 32)"
66
+ make eval-up
67
+ ```
68
+
69
+ See [`docs/quickstart.md`](docs/quickstart.md) for the full walkthrough.
70
+
71
+ **Your own databases:**
72
+
73
+ ```bash
74
+ pip install -e .
75
+ privaci gen-salt > .privaci-salt && chmod 600 .privaci-salt
76
+ export ANONYMIZATION_SALT=$(cat .privaci-salt)
77
+ export SOURCE_DB_URL=postgresql://user:pass@source-host:5432/app
78
+ export TARGET_DB_URL=postgresql://user:pass@target-host:5432/staging
79
+
80
+ privaci validate && privaci dry-run && privaci run && privaci verify
81
+ ```
82
+
83
+ Browse the docs site locally: `pip install -e ".[dev]" && make docs-serve`
84
+
85
+ ## Documentation
86
+
87
+ Start with the [documentation index](docs/README.md) or [quickstart](docs/quickstart.md).
88
+ Key pages:
89
+
90
+ **Using PrivaCI**
91
+
92
+ - [CLI reference](docs/cli-reference.md) — every command, its options, and examples
93
+ - [Configuration reference](docs/configuration.md) — the `mask-rules.yaml` format
94
+ - [Error codes](docs/error-codes.md) — exit codes and message format
95
+ - [State & audit schema](docs/state-schema.md) — what runs write to `_privaci`
96
+ - [Extending PrivaCI](docs/extending-privaci.md) — the plugin contract model
97
+
98
+ **Developing PrivaCI**
99
+
100
+ - [Local development & testing](docs/local-development.md)
101
+ - [Test fixtures — MedicalHelpDesk Corp](docs/test-fixtures.md)
102
+ - [Architecture decision records](docs/adr/README.md)
103
+
104
+ ## License
105
+
106
+ The engine is licensed under the [Elastic License 2.0](LICENSE). Optional
107
+ paid features ship as a separate plugin layer via the
108
+ [plugin contracts](docs/extending-privaci.md).
@@ -0,0 +1,65 @@
1
+ # PrivaCI
2
+
3
+ **One command. Sanitized staging data. No data leaves your VPC.**
4
+
5
+ PrivaCI is a stateless batch engine that reads from a PostgreSQL source
6
+ (typically a production replica), masks PII with a three-tier pipeline, and
7
+ writes realistic synthetic data to a staging database with referential
8
+ integrity preserved.
9
+
10
+ ## Prerequisites
11
+
12
+ - Python **3.12+**, or the official container image
13
+ (`ghcr.io/boundarylogic/privaci`)
14
+ - A PostgreSQL **source** (typically a production replica) and an empty
15
+ **target** database
16
+
17
+ ## Quickstart
18
+
19
+ **Fastest path** — self-contained evaluation stack (~60 s):
20
+
21
+ ```bash
22
+ export ANONYMIZATION_SALT="$(openssl rand -hex 32)"
23
+ make eval-up
24
+ ```
25
+
26
+ See [`docs/quickstart.md`](docs/quickstart.md) for the full walkthrough.
27
+
28
+ **Your own databases:**
29
+
30
+ ```bash
31
+ pip install -e .
32
+ privaci gen-salt > .privaci-salt && chmod 600 .privaci-salt
33
+ export ANONYMIZATION_SALT=$(cat .privaci-salt)
34
+ export SOURCE_DB_URL=postgresql://user:pass@source-host:5432/app
35
+ export TARGET_DB_URL=postgresql://user:pass@target-host:5432/staging
36
+
37
+ privaci validate && privaci dry-run && privaci run && privaci verify
38
+ ```
39
+
40
+ Browse the docs site locally: `pip install -e ".[dev]" && make docs-serve`
41
+
42
+ ## Documentation
43
+
44
+ Start with the [documentation index](docs/README.md) or [quickstart](docs/quickstart.md).
45
+ Key pages:
46
+
47
+ **Using PrivaCI**
48
+
49
+ - [CLI reference](docs/cli-reference.md) — every command, its options, and examples
50
+ - [Configuration reference](docs/configuration.md) — the `mask-rules.yaml` format
51
+ - [Error codes](docs/error-codes.md) — exit codes and message format
52
+ - [State & audit schema](docs/state-schema.md) — what runs write to `_privaci`
53
+ - [Extending PrivaCI](docs/extending-privaci.md) — the plugin contract model
54
+
55
+ **Developing PrivaCI**
56
+
57
+ - [Local development & testing](docs/local-development.md)
58
+ - [Test fixtures — MedicalHelpDesk Corp](docs/test-fixtures.md)
59
+ - [Architecture decision records](docs/adr/README.md)
60
+
61
+ ## License
62
+
63
+ The engine is licensed under the [Elastic License 2.0](LICENSE). Optional
64
+ paid features ship as a separate plugin layer via the
65
+ [plugin contracts](docs/extending-privaci.md).
@@ -0,0 +1,152 @@
1
+ [build-system]
2
+ requires = ["setuptools>=69", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "privaci"
7
+ version = "0.1.0-beta.4"
8
+ description = "In-VPC PostgreSQL masking and anonymization engine"
9
+ readme = "README.md"
10
+ license = { text = "Elastic License 2.0" }
11
+ requires-python = ">=3.12"
12
+ authors = [{ name = "BoundaryLogic", email = "hello@boundarylogic.io" }]
13
+ classifiers = [
14
+ "Development Status :: 3 - Alpha",
15
+ "Intended Audience :: Developers",
16
+ "License :: Other/Proprietary License",
17
+ "Programming Language :: Python :: 3",
18
+ "Programming Language :: Python :: 3.12",
19
+ "Topic :: Database",
20
+ "Topic :: Security",
21
+ ]
22
+ dependencies = [
23
+ "asyncpg==0.30.0",
24
+ # Pinned alongside typer: Click 8.4 broke Typer 0.15 (make_metavar(ctx)).
25
+ # Keep this range until a Typer release is validated against newer Click.
26
+ "click>=8.1.7,<8.5",
27
+ "cryptography==48.0.1",
28
+ "pydantic==2.10.6",
29
+ "pyyaml==6.0.2",
30
+ "typer==0.16.0",
31
+ ]
32
+
33
+ [project.optional-dependencies]
34
+ nlp = [
35
+ "spacy==3.8.7",
36
+ ]
37
+ dev = [
38
+ "black==26.3.1",
39
+ "cosmic-ray==8.4.6",
40
+ "hypothesis==6.127.6",
41
+ "isort==6.0.1",
42
+ "mkdocs-material==9.6.11",
43
+ "mkdocs==1.6.1",
44
+ "mypy==1.15.0",
45
+ "pip-audit==2.7.3",
46
+ "pip-tools==7.4.1",
47
+ "pre-commit==4.1.0",
48
+ "pytest-asyncio==1.4.0",
49
+ "pytest-cov==6.0.0",
50
+ "pytest-mock==3.14.0",
51
+ "pytest==9.0.3",
52
+ "ruff==0.9.6",
53
+ "types-PyYAML==6.0.12.20241230",
54
+ ]
55
+
56
+ [project.scripts]
57
+ privaci = "privaci.cli.app:main"
58
+
59
+ [project.entry-points."privaci.plugins"]
60
+ # Commercial layer registers implementations here.
61
+
62
+ [tool.setuptools.packages.find]
63
+ where = ["src"]
64
+
65
+ [tool.setuptools.package-data]
66
+ # Ship the PEP 561 marker so downstream packages (e.g. privaci-commercial) get
67
+ # the engine's types when they build against privaci.contracts.
68
+ privaci = ["py.typed"]
69
+
70
+ [tool.pytest.ini_options]
71
+ asyncio_mode = "auto"
72
+ asyncio_default_fixture_loop_scope = "function"
73
+ testpaths = ["tests"]
74
+ markers = [
75
+ "integration: tests that require a real PostgreSQL instance",
76
+ "slow: long-running tests",
77
+ "spike: Week-1 architecture validation (see docs/spikes/)",
78
+ ]
79
+ addopts = "-m 'not integration'"
80
+
81
+ [tool.coverage.run]
82
+ source = ["src/privaci"]
83
+ branch = true
84
+ # Paths must match measured files under ``pytest --cov=src`` (``src/privaci/...``).
85
+ omit = [
86
+ "*/__main__.py",
87
+ "*/mask/__init__.py",
88
+ "*/stream/__init__.py",
89
+ "*/schema/__init__.py",
90
+ "*/pipeline/__init__.py",
91
+ "*/autodetect/__init__.py",
92
+ "*/observability/__init__.py",
93
+ "*/preflight/__init__.py",
94
+ "*/verify/__init__.py",
95
+ "*/verify/runner.py",
96
+ "*/verify/structural.py",
97
+ "*/spikes/*",
98
+ ]
99
+
100
+ [tool.coverage.report]
101
+ fail_under = 85
102
+ show_missing = true
103
+
104
+ [tool.black]
105
+ line-length = 88
106
+ target-version = ["py312"]
107
+
108
+ [tool.isort]
109
+ profile = "black"
110
+ line_length = 88
111
+ src_paths = ["src", "tests"]
112
+
113
+ [tool.mypy]
114
+ python_version = "3.12"
115
+ strict = true
116
+ warn_unreachable = true
117
+ packages = ["privaci", "tests"]
118
+ mypy_path = "src"
119
+
120
+ [[tool.mypy.overrides]]
121
+ # Optional/untyped third-party libs (cloud SDKs are lazy-imported per scheme).
122
+ module = [
123
+ "spacy",
124
+ "spacy.*",
125
+ "asyncpg",
126
+ "asyncpg.*",
127
+ "boto3",
128
+ "botocore",
129
+ "botocore.*",
130
+ "azure.*",
131
+ "hvac",
132
+ "hvac.*",
133
+ "prometheus_client",
134
+ "prometheus_client.*",
135
+ ]
136
+ ignore_missing_imports = true
137
+
138
+ [tool.ruff]
139
+ line-length = 88
140
+ target-version = "py312"
141
+ src = ["src", "tests"]
142
+
143
+ [tool.ruff.lint]
144
+ select = ["E", "F", "W", "I", "N", "UP", "S", "B", "A"]
145
+
146
+ [tool.ruff.lint.per-file-ignores]
147
+ "tests/conftest.py" = ["E402"]
148
+ "tests/**" = ["S101", "S105", "S108"]
149
+ "src/privaci/secrets/**" = ["A005"]
150
+
151
+ [tool.ruff.lint.isort]
152
+ known-first-party = ["privaci", "tests"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,5 @@
1
+ """PrivaCI — in-VPC PostgreSQL masking and anonymization engine."""
2
+
3
+ from __future__ import annotations
4
+
5
+ __version__ = "0.1.0"
@@ -0,0 +1,8 @@
1
+ """Allow `python -m privaci` to invoke the CLI."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from privaci.cli.app import main
6
+
7
+ if __name__ == "__main__":
8
+ raise SystemExit(main())
@@ -0,0 +1,27 @@
1
+ """PII column auto-detection."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from privaci.autodetect.models import (
6
+ DetectionConfidence,
7
+ DetectionFinding,
8
+ DetectionResult,
9
+ )
10
+ from privaci.autodetect.report import write_detection_report
11
+ from privaci.autodetect.resolve import (
12
+ build_detection,
13
+ resolve_effective_table_config,
14
+ uncovered_strict_columns,
15
+ )
16
+ from privaci.autodetect.scanner import scan_catalog
17
+
18
+ __all__ = [
19
+ "DetectionConfidence",
20
+ "DetectionFinding",
21
+ "DetectionResult",
22
+ "build_detection",
23
+ "resolve_effective_table_config",
24
+ "scan_catalog",
25
+ "uncovered_strict_columns",
26
+ "write_detection_report",
27
+ ]
@@ -0,0 +1,70 @@
1
+ """Map pattern rules to config column actions, respecting column types."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from privaci.autodetect.patterns import PatternRule
6
+ from privaci.catalog.models import ColumnInfo
7
+ from privaci.config.actions import (
8
+ ColumnAction,
9
+ FakeAction,
10
+ HashAction,
11
+ NerMaskAction,
12
+ StaticAction,
13
+ )
14
+
15
+ _TEXTLIKE = frozenset(
16
+ {"text", "character varying", "character", "varchar", "char", "citext", "name"}
17
+ )
18
+
19
+ # Column base types each fake provider can produce a valid value for. Providers
20
+ # absent from this map are treated as text-only.
21
+ _PROVIDER_TYPES: dict[str, frozenset[str]] = {
22
+ "dob": frozenset({"date"}) | _TEXTLIKE,
23
+ "ip_address": frozenset({"inet", "cidr"}) | _TEXTLIKE,
24
+ "uuid": frozenset({"uuid"}) | _TEXTLIKE,
25
+ }
26
+
27
+
28
+ def _base_type(data_type: str) -> str:
29
+ head, _, _ = data_type.strip().lower().partition("(")
30
+ return head.strip()
31
+
32
+
33
+ def action_for_column(rule: PatternRule, column: ColumnInfo) -> ColumnAction | None:
34
+ """Build a type-compatible action for ``column``, or ``None`` to skip.
35
+
36
+ Returns ``None`` when the matched rule's action cannot produce a value
37
+ valid for the column's type (e.g. ``hash`` on a ``uuid`` column), so the
38
+ scanner can leave the column as passthrough rather than emit a broken mask.
39
+ """
40
+ base = _base_type(column.data_type)
41
+ if rule.action == "fake":
42
+ return _fake_action(rule, base)
43
+ if rule.action == "hash":
44
+ return _hash_action(base)
45
+ if rule.action == "static":
46
+ if base not in _TEXTLIKE:
47
+ return None
48
+ value = rule.static_value or "privaci-test-pw"
49
+ return StaticAction(action="static", value=value)
50
+ if base not in _TEXTLIKE:
51
+ return None
52
+ return NerMaskAction(action="ner_mask")
53
+
54
+
55
+ def _fake_action(rule: PatternRule, base: str) -> ColumnAction | None:
56
+ if rule.provider is None:
57
+ msg = f"fake rule {rule.rule_id} missing provider"
58
+ raise ValueError(msg)
59
+ allowed = _PROVIDER_TYPES.get(rule.provider, _TEXTLIKE)
60
+ if base not in allowed:
61
+ return None
62
+ return FakeAction(action="fake", provider=rule.provider)
63
+
64
+
65
+ def _hash_action(base: str) -> ColumnAction | None:
66
+ if base == "uuid":
67
+ return FakeAction(action="fake", provider="uuid")
68
+ if base in _TEXTLIKE:
69
+ return HashAction(action="hash")
70
+ return None
@@ -0,0 +1,68 @@
1
+ """Freeform-text shape and confidence scoring."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+
7
+ from privaci.autodetect.models import DetectionConfidence
8
+ from privaci.autodetect.table_context import table_sensitivity
9
+ from privaci.catalog.models import ColumnInfo
10
+
11
+ _FREEFORM_MIN_AVG_WIDTH = 200
12
+ _FREEFORM_UNCERTAIN_MIN_AVG_WIDTH = 100
13
+ _TEXT_TYPES = frozenset({"text"})
14
+ _VARCHAR_RE = re.compile(r"^character varying\((\d+)\)$", re.IGNORECASE)
15
+
16
+
17
+ def is_freeform_eligible_type(column: ColumnInfo) -> bool:
18
+ """Return whether the column type can hold narrative freeform text."""
19
+ base = _base_type(column.data_type)
20
+ if base in _TEXT_TYPES:
21
+ return True
22
+ match = _VARCHAR_RE.match(column.data_type.strip())
23
+ if match is None:
24
+ return False
25
+ return int(match.group(1)) >= 500
26
+
27
+
28
+ def score_freeform_confidence(
29
+ *,
30
+ table_name: str,
31
+ column: ColumnInfo,
32
+ ) -> tuple[DetectionConfidence, tuple[str, ...]]:
33
+ """Score a freeform name-match using type, stats, and table context."""
34
+ reasons: list[str] = []
35
+ if not is_freeform_eligible_type(column):
36
+ reasons.append(f"type {column.data_type!r} is not freeform-eligible")
37
+ return "low", tuple(reasons)
38
+
39
+ reasons.append("column type is freeform-eligible")
40
+ sensitivity = table_sensitivity(table_name)
41
+ avg_width = column.avg_width
42
+
43
+ if avg_width is None:
44
+ reasons.append("pg_stats.avg_width unavailable")
45
+ if sensitivity == "sensitive":
46
+ reasons.append("sensitive table context")
47
+ return "medium", tuple(reasons)
48
+ return "low", tuple(reasons)
49
+
50
+ reasons.append(f"pg_stats.avg_width={avg_width:.0f}")
51
+ if avg_width >= _FREEFORM_MIN_AVG_WIDTH:
52
+ if sensitivity == "low":
53
+ reasons.append("low-sensitivity table context downgrades confidence")
54
+ return "medium", tuple(reasons)
55
+ if sensitivity == "sensitive":
56
+ reasons.append("sensitive table context")
57
+ return "high", tuple(reasons)
58
+
59
+ if avg_width >= _FREEFORM_UNCERTAIN_MIN_AVG_WIDTH and sensitivity == "sensitive":
60
+ reasons.append("borderline avg_width on sensitive table")
61
+ return "medium", tuple(reasons)
62
+
63
+ reasons.append("avg_width below freeform threshold")
64
+ return "low", tuple(reasons)
65
+
66
+
67
+ def _base_type(data_type: str) -> str:
68
+ return re.sub(r"\(.*\)$", "", data_type.strip().lower())
@@ -0,0 +1,59 @@
1
+ """Column-name pattern matching for auto-detect."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from typing import get_args
7
+
8
+ from privaci.autodetect.patterns import (
9
+ BUILTIN_PATTERNS,
10
+ PatternKind,
11
+ PatternRule,
12
+ compile_patterns,
13
+ )
14
+
15
+
16
+ def match_column_name(column_name: str) -> PatternRule | None:
17
+ """Return the first pattern rule matching ``column_name``, if any.
18
+
19
+ Rules are evaluated in library order; earlier rules win.
20
+ """
21
+ lowered = column_name.lower()
22
+ compiled = compile_patterns()
23
+ for rule in BUILTIN_PATTERNS:
24
+ if _rule_matches(lowered, rule, compiled):
25
+ return rule
26
+ return None
27
+
28
+
29
+ def _delimiter_bounded_substring(lowered: str, needle: str) -> bool:
30
+ """Match ``needle`` only at underscore boundaries or as the whole name."""
31
+ if lowered == needle:
32
+ return True
33
+ padded = f"_{lowered}_"
34
+ return f"_{needle}_" in padded
35
+
36
+
37
+ def _rule_matches(
38
+ lowered: str,
39
+ rule: PatternRule,
40
+ compiled: dict[str, re.Pattern[str]],
41
+ ) -> bool:
42
+ kind = rule.kind
43
+ if kind == "substring":
44
+ return rule.pattern in lowered
45
+ elif kind == "bounded_substring":
46
+ return _delimiter_bounded_substring(lowered, rule.pattern)
47
+ elif kind == "suffix":
48
+ return lowered.endswith(rule.pattern)
49
+ elif kind in ("prefix", "wildcard_prefix"):
50
+ return lowered.startswith(rule.pattern)
51
+ elif kind == "regex":
52
+ pattern = compiled.get(rule.rule_id)
53
+ return pattern is not None and pattern.fullmatch(lowered) is not None
54
+ # CodeQL's mixed-return rule wants an explicit non-returning terminal rather
55
+ # than an implicit fall-through after the exhaustive ``kind`` chain above.
56
+ # Derive the valid kinds from PatternKind so the message can never drift.
57
+ expected = ", ".join(get_args(PatternKind))
58
+ msg = f"Unknown auto-detect pattern kind: {kind!r}. Expected one of: {expected}."
59
+ raise ValueError(msg)