dto-strict 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.
Files changed (32) hide show
  1. dto_strict-0.1.0/.gitignore +44 -0
  2. dto_strict-0.1.0/LICENSE +139 -0
  3. dto_strict-0.1.0/PKG-INFO +401 -0
  4. dto_strict-0.1.0/README.md +373 -0
  5. dto_strict-0.1.0/VERIFICATION.txt +96 -0
  6. dto_strict-0.1.0/example/dto-strict-workflow.yml +20 -0
  7. dto_strict-0.1.0/pyproject.toml +71 -0
  8. dto_strict-0.1.0/src/dto_strict/__init__.py +8 -0
  9. dto_strict-0.1.0/src/dto_strict/checkers.py +318 -0
  10. dto_strict-0.1.0/src/dto_strict/cli.py +50 -0
  11. dto_strict-0.1.0/src/dto_strict/config.py +54 -0
  12. dto_strict-0.1.0/src/dto_strict/linter.py +112 -0
  13. dto_strict-0.1.0/src/dto_strict/rules.py +159 -0
  14. dto_strict-0.1.0/tests/fixtures/bad/r001_bad_param.py +13 -0
  15. dto_strict-0.1.0/tests/fixtures/bad/r001_bad_return.py +13 -0
  16. dto_strict-0.1.0/tests/fixtures/bad/r002_bad_inline.py +17 -0
  17. dto_strict-0.1.0/tests/fixtures/bad/r003_bad_dataclass.py +26 -0
  18. dto_strict-0.1.0/tests/fixtures/bad/r004_bad_facade.py +16 -0
  19. dto_strict-0.1.0/tests/fixtures/bad/r005_bad_validator.py +18 -0
  20. dto_strict-0.1.0/tests/fixtures/good/r001_good_basic.py +30 -0
  21. dto_strict-0.1.0/tests/fixtures/good/r002_good_dict.py +27 -0
  22. dto_strict-0.1.0/tests/fixtures/good/r003_good_dataclass.py +20 -0
  23. dto_strict-0.1.0/tests/fixtures/good/r004_good_facade.py +19 -0
  24. dto_strict-0.1.0/tests/fixtures/good/r005_good_validator.py +36 -0
  25. dto_strict-0.1.0/tests/test_cli.py +96 -0
  26. dto_strict-0.1.0/tests/test_config_loading.py +47 -0
  27. dto_strict-0.1.0/tests/test_github_format.py +69 -0
  28. dto_strict-0.1.0/tests/test_r001_dict_str_any.py +46 -0
  29. dto_strict-0.1.0/tests/test_r002_inline_dict.py +37 -0
  30. dto_strict-0.1.0/tests/test_r003_dataclass_trio.py +37 -0
  31. dto_strict-0.1.0/tests/test_r004_module_facade.py +38 -0
  32. dto_strict-0.1.0/tests/test_r005_validator_pattern.py +37 -0
@@ -0,0 +1,44 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *$py.class
4
+ *.so
5
+ .Python
6
+ build/
7
+ develop-eggs/
8
+ dist/
9
+ downloads/
10
+ eggs/
11
+ .eggs/
12
+ lib/
13
+ lib64/
14
+ parts/
15
+ sdist/
16
+ var/
17
+ wheels/
18
+ pip-wheel-metadata/
19
+ share/python-wheels/
20
+ *.egg-info/
21
+ .installed.cfg
22
+ *.egg
23
+ MANIFEST
24
+ .pytest_cache/
25
+ .coverage
26
+ .coverage.*
27
+ .cache
28
+ nosetests.xml
29
+ coverage.xml
30
+ *.cover
31
+ *.py,cover
32
+ .hypothesis/
33
+ .venv
34
+ env/
35
+ venv/
36
+ ENV/
37
+ env.bak/
38
+ venv.bak/
39
+ .idea/
40
+ .vscode/
41
+ *.swp
42
+ *.swo
43
+ *~
44
+ .DS_Store
@@ -0,0 +1,139 @@
1
+ Apache License
2
+ Version 2.0, January 2004
3
+
4
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
5
+
6
+ 1. Definitions.
7
+
8
+ "License" shall mean the terms and conditions for use, reproduction,
9
+ and distribution as defined in Sections 1 through 9 of this document.
10
+
11
+ "Licensor" shall mean the copyright owner or entity authorized by
12
+ the copyright owner that is granting the License.
13
+
14
+ "Legal Entity" shall mean the union of the acting entity and all
15
+ other entities that control, are controlled by, or are under common
16
+ control with that entity. For the purposes of this definition,
17
+ "control" means (i) the power, direct or indirect, to cause the
18
+ direction or management of such entity, whether by contract or
19
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
20
+ outstanding shares, or (iii) beneficial ownership of such entity.
21
+
22
+ "You" (or "Your") shall mean an individual or Legal Entity exercising
23
+ permissions granted by this License.
24
+
25
+ "Source" form shall mean the preferred form for making modifications,
26
+ including but not limited to software source code, documentation
27
+ source, and configuration files.
28
+
29
+ "Object" form shall mean any form resulting from mechanical
30
+ transformation or translation of a Source form, including but
31
+ not limited to compiled object code, generated documentation,
32
+ and conversions to other media types.
33
+
34
+ "Work" shall mean the work of authorship, whether in Source or Object
35
+ form, made available under the License, as indicated by a copyright
36
+ notice that is included in or attached to the work (an example is
37
+ provided in the Appendix below).
38
+
39
+ "Derivative Works" shall mean any work, whether in Object or Source
40
+ form, that is based on (or derived from) the Work and for which the
41
+ editorial revisions, annotations, elaborations, or other modifications
42
+ represent, as a whole, an original work of authorship. For the purposes
43
+ of this License, Derivative Works shall not include works that remain
44
+ separable from, or merely link (or bind by name) to the interfaces of,
45
+ the Work and Derivative Works thereof.
46
+
47
+ "Contribution" shall mean any work of authorship, including
48
+ the original Work and any Derivative Works thereof, submitted to,
49
+ or received by, Licensor and intentionally submitted for inclusion
50
+ in, by or on behalf of the original author or any individual or Legal
51
+ Entity on behalf of whom a Contribution has been received by Licensor
52
+ and subsequently incorporated within the Work.
53
+
54
+ "Contributor" shall mean Licensor and any Legal Entity on behalf
55
+ of whom a Contribution has been received by Licensor and incorporated
56
+ within the Work.
57
+
58
+ 2. Grant of Copyright License. Subject to the terms and conditions of
59
+ this License, each Contributor hereby grants to You a perpetual,
60
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
61
+ copyright license to reproduce, prepare Derivative Works of,
62
+ publicly display, publicly perform, sublicense, and distribute the
63
+ Work and such Derivative Works in Source or Object form.
64
+
65
+ 3. Grant of Patent License. Subject to the terms and conditions of
66
+ this License, each Contributor hereby grants to You a perpetual,
67
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
68
+ (except as stated in this section) patent license to make, have made,
69
+ use, offer to sell, sell, import, and otherwise transfer the Work,
70
+ where such license applies only to those patent claims licensable
71
+ by such Contributor that are necessarily infringed by their
72
+ Contribution(s) alone or by combination of their Contribution(s)
73
+ with the Work to which such Contribution(s) was submitted.
74
+
75
+ 4. Redistribution. You may reproduce and distribute copies of the
76
+ Work or Derivative Works thereof in any medium, with or without
77
+ modifications, and in Source or Object form, provided that You
78
+ meet the following conditions:
79
+
80
+ (a) You must give any other recipients of the Work or
81
+ Derivative Works a copy of this License; and
82
+
83
+ (b) You must cause any modified files to carry prominent notices
84
+ stating that You changed the files; and
85
+
86
+ (c) You must retain, in the Source form of any Derivative Works
87
+ that You distribute, all copyright, patent, trademark, and
88
+ attribution notices from the Source form of the Work,
89
+ excluding those notices that do not pertain to any part of
90
+ the Derivative Works; and
91
+
92
+ (d) If the Work includes a "NOTICE" text file, then any
93
+ Derivative Works that You distribute must include a readable
94
+ copy of the attribution notices contained
95
+ within such NOTICE file, excluding those notices that do not
96
+ pertain to any part of the Derivative Works, in at least one
97
+ of the following places: within a NOTICE text file distributed
98
+ as part of the Derivative Works; within the Source form or
99
+ documentation, if provided along with the Derivative Works; or,
100
+ within a display generated by the Derivative Works, if and
101
+ wherever such third-party notices normally appear. The contents
102
+ of the NOTICE file are for informational purposes only and
103
+ do not modify the License.
104
+
105
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
106
+ any Contribution intentionally submitted for inclusion in the Work
107
+ by You to Licensor shall be under the terms and conditions of
108
+ this License, without limitation of any additional terms or conditions.
109
+
110
+ 6. Trademarks. This License does not grant permission to use the trade
111
+ names, trademarks, service marks, or product names of the Licensor,
112
+ except as required for reasonable and customary use in describing the
113
+ origin of the Work and reproducing the content of the NOTICE file.
114
+
115
+ 7. Disclaimer of Warranty. Unless required by applicable law or
116
+ agreed to in writing, Licensor provides the Work (and each
117
+ Contributor provides its Contributions) on an "AS IS" BASIS,
118
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
119
+ or implied, including, without limitation, any warranties or
120
+ conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS
121
+ FOR A PARTICULAR PURPOSE.
122
+
123
+ 8. Limitation of Liability. In no event and under no legal theory,
124
+ whether in tort (including negligence), contract, or otherwise,
125
+ unless required by applicable law (such as deliberate and grossly
126
+ negligent acts) or agreed to in writing, shall any Contributor be
127
+ liable to You for damages, including any direct, indirect, special,
128
+ incidental, or consequential damages of any character arising as a
129
+ result of this License or out of the inability to use the Work
130
+ (including but not limited to damages for loss of goodwill, work
131
+ stoppage, computer failure or malfunction, or any and all other
132
+ commercial damages or losses), even if such Contributor has been
133
+ advised of the possibility of such damages.
134
+
135
+ 9. Accepting Warranty or Additional Liability. While redistributing
136
+ the Work or Derivative Works thereof, You may choose to offer,
137
+ and charge a fee for, acceptance of support, warranty, indemnity,
138
+ or other liability obligations and/or rights consistent with this
139
+ License.
@@ -0,0 +1,401 @@
1
+ Metadata-Version: 2.4
2
+ Name: dto-strict
3
+ Version: 0.1.0
4
+ Summary: AST-based linter for Python DTO discipline and facade-ban enforcement — framework-agnostic.
5
+ Project-URL: Homepage, https://github.com/jekhator/dto-strict
6
+ Project-URL: Repository, https://github.com/jekhator/dto-strict.git
7
+ Project-URL: Issues, https://github.com/jekhator/dto-strict/issues
8
+ Author: James Ekhator
9
+ License: Apache-2.0
10
+ License-File: LICENSE
11
+ Keywords: ast,code-quality,dataclass,dto,linter,static-analysis
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: Apache Software License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Topic :: Software Development :: Libraries
20
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
21
+ Classifier: Topic :: Software Development :: Quality Assurance
22
+ Classifier: Typing :: Typed
23
+ Requires-Python: >=3.10
24
+ Provides-Extra: dev
25
+ Requires-Dist: pytest-cov; extra == 'dev'
26
+ Requires-Dist: pytest>=8; extra == 'dev'
27
+ Description-Content-Type: text/markdown
28
+
29
+ # dto-strict
30
+
31
+ AST-based linter for Python DTO discipline and facade-ban enforcement — pluggable, framework-agnostic.
32
+
33
+ ## Why dto-strict?
34
+
35
+ Data Transfer Objects (DTOs) provide a critical boundary between services and prevent the fragmentation of business-logic definitions across codebases. However, when function signatures leak `Dict[str, Any]` or when services build dict literals inline instead of using structured DTOs, code becomes:
36
+
37
+ - **Loosely typed**: Shape mismatches only surface at runtime.
38
+ - **Duplicated**: The same business object gets redefined wherever it's used.
39
+ - **Hard to evolve**: Changing a field requires updating dicts in 10+ places.
40
+
41
+ Facade functions (module-level helpers that wrap framework machinery) similarly tend to proliferate and obscure intent when unmarked. The "facade—celery schedule" pattern makes intent explicit.
42
+
43
+ **dto-strict** enforces DTO and facade discipline via static AST analysis, with 5 focused rules:
44
+
45
+ 1. **R001 (HIGH)**: Detect `Dict[str, Any]` in service-layer function signatures.
46
+ 2. **R002 (MEDIUM)**: Flag inline dict literals with 3+ string keys.
47
+ 3. **R003 (MEDIUM)**: Require `@dataclass(frozen=True, slots=True, repr=False)` trio in DTOs.
48
+ 4. **R004 (HIGH)**: Demand exception tags on module-level functions (e.g., `# facade — celery schedule`).
49
+ 5. **R005 (LOW)**: Encourage validators to use `DTO.from_dict()` pattern.
50
+
51
+ All rules are configurable; violations can be disabled, severity overridden, or paths scoped.
52
+
53
+ ## Install
54
+
55
+ ```bash
56
+ pip install dto-strict
57
+ ```
58
+
59
+ ## Quick Start
60
+
61
+ ### Basic CLI Usage
62
+
63
+ ```bash
64
+ # Lint a single file
65
+ dto-strict apps/compliance/services.py
66
+
67
+ # Lint a directory
68
+ dto-strict apps/
69
+
70
+ # Output as GitHub Actions annotations
71
+ dto-strict apps/ --format github
72
+
73
+ # Output as JSON
74
+ dto-strict apps/ --format json
75
+ ```
76
+
77
+ ### Configuration (pyproject.toml)
78
+
79
+ ```toml
80
+ [tool.dto-strict]
81
+ service_paths = [
82
+ "apps/*/services/*.py",
83
+ "**/services/*.py",
84
+ ]
85
+ dto_paths = [
86
+ "**/dtos.py",
87
+ "**/dtos/*.py",
88
+ ]
89
+ exception_tags = [
90
+ "facade — celery schedule",
91
+ "FRAMEWORK",
92
+ ]
93
+ disabled_rules = ["R005"] # Disable low-priority rules if desired
94
+ severity_overrides = { "R002" = "low" } # Downgrade specific rules
95
+ ```
96
+
97
+ ### GitHub Actions
98
+
99
+ Create `.github/workflows/dto-strict.yml`:
100
+
101
+ ```yaml
102
+ name: dto-strict
103
+ on:
104
+ pull_request:
105
+ paths: ['apps/**.py']
106
+
107
+ jobs:
108
+ lint:
109
+ runs-on: ubuntu-latest
110
+ steps:
111
+ - uses: actions/checkout@v4
112
+ - uses: actions/setup-python@v5
113
+ with:
114
+ python-version: '3.12'
115
+ - run: pip install dto-strict
116
+ - run: dto-strict apps/ --format github
117
+ ```
118
+
119
+ ### Pre-commit Hook
120
+
121
+ Add to `.pre-commit-config.yaml`:
122
+
123
+ ```yaml
124
+ - repo: local
125
+ hooks:
126
+ - id: dto-strict
127
+ name: dto-strict
128
+ entry: dto-strict
129
+ language: python
130
+ types: [python]
131
+ additional_dependencies: ['dto-strict']
132
+ stages: [commit]
133
+ ```
134
+
135
+ ## Rules
136
+
137
+ ### R001: Dict[str, Any] in Service Signatures (HIGH)
138
+
139
+ Service-layer functions should not accept or return `Dict[str, Any]`. Use a DTO instead.
140
+
141
+ **Fail:**
142
+ ```python
143
+ def process_user(config: Dict[str, Any]) -> Dict[str, Any]:
144
+ return {"status": "ok"}
145
+ ```
146
+
147
+ **Pass:**
148
+ ```python
149
+ @dataclass(frozen=True, slots=True, repr=False)
150
+ class UserConfigDTO:
151
+ timeout: int
152
+ retries: int
153
+
154
+ def process_user(config: UserConfigDTO) -> dict:
155
+ return {"status": "ok"}
156
+ ```
157
+
158
+ **Rationale:** Typed parameters enable IDE completion and catch shape mismatches early.
159
+
160
+ ---
161
+
162
+ ### R002: Inline Dict Literals (MEDIUM)
163
+
164
+ Service files with inline dict literals containing 3+ string keys should define a DTO instead.
165
+
166
+ **Fail:**
167
+ ```python
168
+ def build_response(user_id: int) -> dict:
169
+ return {
170
+ "user_id": user_id,
171
+ "status": "active",
172
+ "timestamp": "2025-01-01",
173
+ }
174
+ ```
175
+
176
+ **Pass:**
177
+ ```python
178
+ @dataclass(frozen=True, slots=True, repr=False)
179
+ class ResponseDTO:
180
+ user_id: int
181
+ status: str
182
+ timestamp: str
183
+
184
+ def build_response(user_id: int) -> ResponseDTO:
185
+ return ResponseDTO(user_id, "active", "2025-01-01")
186
+ ```
187
+
188
+ **Rationale:** Shared shapes should live in DTOs. Inline dicts make duplication invisible.
189
+
190
+ ---
191
+
192
+ ### R003: Dataclass Decorator Trio (MEDIUM)
193
+
194
+ All `@dataclass` definitions in DTO files must include `frozen=True`, `slots=True`, and `repr=False`.
195
+
196
+ **Fail:**
197
+ ```python
198
+ @dataclass
199
+ class UserDTO:
200
+ user_id: int
201
+
202
+ @dataclass(frozen=True)
203
+ class ConfigDTO:
204
+ timeout: int
205
+ ```
206
+
207
+ **Pass:**
208
+ ```python
209
+ @dataclass(frozen=True, slots=True, repr=False)
210
+ class UserDTO:
211
+ user_id: int
212
+
213
+ @dataclass(frozen=True, slots=True, repr=False)
214
+ class ConfigDTO:
215
+ timeout: int
216
+ ```
217
+
218
+ **Rationale:**
219
+ - `frozen=True`: Immutability enforces single-source-of-truth.
220
+ - `slots=True`: Memory efficiency and prevents attribute typos.
221
+ - `repr=False`: Prevents accidental logging of sensitive fields in tracebacks.
222
+
223
+ ---
224
+
225
+ ### R004: Module-Level Functions (HIGH)
226
+
227
+ Bare module-level functions (facades, framework hooks) must carry an exception tag in a comment or docstring.
228
+
229
+ **Fail:**
230
+ ```python
231
+ def process_user(user_id: int):
232
+ pass
233
+
234
+ def send_notification(message: str):
235
+ pass
236
+ ```
237
+
238
+ **Pass:**
239
+ ```python
240
+ def process_user(user_id: int): # facade — celery schedule
241
+ pass
242
+
243
+ def send_notification(message: str): # FRAMEWORK
244
+ """Send via SNS."""
245
+ pass
246
+
247
+ class UserService:
248
+ def process(self, user_id: int):
249
+ # Class methods don't need tags
250
+ pass
251
+ ```
252
+
253
+ **Exception Tags:** Configurable via `pyproject.toml` `exception_tags` list.
254
+
255
+ **Rationale:** Facades blur intent. Tags make intent explicit and signal "this is framework-specific, not business logic."
256
+
257
+ ---
258
+
259
+ ### R005: Validator Pattern (LOW)
260
+
261
+ `validate_*()` functions should use `DTO.from_dict()` or raise `ValidationError` to enforce payload shape.
262
+
263
+ **Fail:**
264
+ ```python
265
+ def validate_user_payload(payload: dict) -> bool:
266
+ return "user_id" in payload and "email" in payload
267
+ ```
268
+
269
+ **Pass:**
270
+ ```python
271
+ def validate_user_payload(payload: dict) -> UserDTO:
272
+ try:
273
+ user = UserDTO(
274
+ user_id=payload["user_id"],
275
+ email=payload["email"],
276
+ )
277
+ return user
278
+ except (KeyError, TypeError) as e:
279
+ raise ValidationError(f"Invalid shape: {e}")
280
+ ```
281
+
282
+ **Rationale:** Validators should enforce structure, not just presence.
283
+
284
+ ---
285
+
286
+ ## Output Formats
287
+
288
+ ### Text (default)
289
+
290
+ ```
291
+ app.py:10: R001 Dict[str, Any] in signature: process_user
292
+ service.py:20: R002 Inline dict literal with 4 keys
293
+ ```
294
+
295
+ ### GitHub Actions
296
+
297
+ ```
298
+ ::error file=app.py,line=10,col=5::R001 Dict[str, Any] in signature: process_user
299
+ ::warning file=service.py,line=20,col=0::R002 Inline dict literal with 4 keys
300
+ ```
301
+
302
+ ### JSON
303
+
304
+ ```json
305
+ [
306
+ {
307
+ "rule_id": "R001",
308
+ "severity": "HIGH",
309
+ "file": "app.py",
310
+ "line": 10,
311
+ "col": 5,
312
+ "message": "Dict[str, Any] in signature: process_user"
313
+ }
314
+ ]
315
+ ```
316
+
317
+ ## Exit Codes
318
+
319
+ | Code | Meaning |
320
+ |------|---------|
321
+ | 0 | No violations |
322
+ | 1 | HIGH severity violations present |
323
+ | 2 | MEDIUM severity violations only |
324
+ | 3 | LOW severity violations only |
325
+
326
+ ## Configuration Reference
327
+
328
+ ```toml
329
+ [tool.dto-strict]
330
+
331
+ # Paths to check for service-layer violations (R001, R002, R004)
332
+ # Default: ["apps/*/services/*.py", "**/services/*.py"]
333
+ service_paths = [
334
+ "apps/*/services/*.py",
335
+ "**/services/*.py",
336
+ ]
337
+
338
+ # Paths to check for DTO definitions (R003)
339
+ # Default: ["**/dtos.py", "**/dtos/*.py"]
340
+ dto_paths = [
341
+ "**/dtos.py",
342
+ "**/dtos/*.py",
343
+ ]
344
+
345
+ # Allowed exception tags for R004 (module-level facades)
346
+ # Default: ["facade — celery schedule", "FRAMEWORK"]
347
+ exception_tags = [
348
+ "facade — celery schedule",
349
+ "FRAMEWORK",
350
+ "CUSTOM_TAG",
351
+ ]
352
+
353
+ # Disable specific rules entirely
354
+ # Default: []
355
+ disabled_rules = ["R005"]
356
+
357
+ # Override severity for specific rules
358
+ # Valid values: "HIGH", "MEDIUM", "LOW"
359
+ # Default: {}
360
+ severity_overrides = {
361
+ "R002" = "low",
362
+ }
363
+ ```
364
+
365
+ ## Design Philosophy
366
+
367
+ **Pluggable, not opinionated.** Every rule is:
368
+
369
+ - **Configurable**: Path patterns, exception tags, severity levels.
370
+ - **Disable-able**: Set `disabled_rules = ["R001"]` to skip it entirely.
371
+ - **Framework-agnostic**: No Django/FastAPI/Flask assumptions; adapters for each framework are opt-in extras.
372
+
373
+ **Defaults bundled, not imposed.** Out-of-the-box rules target Django + DRF + Celery patterns, but you can customize for your stack.
374
+
375
+ ## Development
376
+
377
+ ```bash
378
+ git clone https://github.com/jekhator/dto-strict.git
379
+ cd dto-strict
380
+ python3 -m venv .venv && source .venv/bin/activate
381
+ pip install -e .[dev]
382
+
383
+ # Run tests
384
+ pytest tests/ -v
385
+
386
+ # Run linter on itself
387
+ dto-strict src/ --format github
388
+ ```
389
+
390
+ ## License
391
+
392
+ Apache License 2.0. See LICENSE.
393
+
394
+ ## Contributing
395
+
396
+ Issues and PRs welcome. Please include fixtures (good + bad examples) for new rules.
397
+
398
+ ## See Also
399
+
400
+ - [pii-aware-mixin](https://github.com/jekhator/pii-aware-mixin) — Auto-hide PII in dataclass repr/logging.
401
+ - [logging-mixin](https://github.com/jekhator/logging-mixin) — Class-bound structured logging with correlation IDs.