pinky-core 0.1.0.dev3__tar.gz → 0.1.0.dev5__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 (95) hide show
  1. {pinky_core-0.1.0.dev3 → pinky_core-0.1.0.dev5}/.github/workflows/ci.yml +4 -3
  2. {pinky_core-0.1.0.dev3 → pinky_core-0.1.0.dev5}/.gitignore +4 -1
  3. {pinky_core-0.1.0.dev3 → pinky_core-0.1.0.dev5}/.pre-commit-config.yaml +25 -4
  4. pinky_core-0.1.0.dev5/.python-version +1 -0
  5. pinky_core-0.1.0.dev5/.vscode/settings.json +8 -0
  6. pinky_core-0.1.0.dev5/CONTEXT.md +24 -0
  7. {pinky_core-0.1.0.dev3 → pinky_core-0.1.0.dev5}/PKG-INFO +16 -15
  8. {pinky_core-0.1.0.dev3 → pinky_core-0.1.0.dev5}/README.md +15 -1
  9. pinky_core-0.1.0.dev5/docs/adr/0001-zero-dependency-static-html.md +26 -0
  10. pinky_core-0.1.0.dev5/docs/explanation/2-features.md +44 -0
  11. {pinky_core-0.1.0.dev3 → pinky_core-0.1.0.dev5}/docs/reference/generated/coverage_badge.svg +1 -1
  12. {pinky_core-0.1.0.dev3 → pinky_core-0.1.0.dev5}/docs/reference/generated/hooks_badge.svg +3 -3
  13. {pinky_core-0.1.0.dev3 → pinky_core-0.1.0.dev5}/docs/reference/generated/interrogate_badge.svg +4 -4
  14. {pinky_core-0.1.0.dev3 → pinky_core-0.1.0.dev5}/pyproject.toml +36 -9
  15. pinky_core-0.1.0.dev5/scripts/hooks/check_weak_assertions.py +250 -0
  16. {pinky_core-0.1.0.dev3 → pinky_core-0.1.0.dev5}/scripts/hooks/commit-msg +16 -17
  17. pinky_core-0.1.0.dev5/scripts/hooks/generate-docs +90 -0
  18. {pinky_core-0.1.0.dev3 → pinky_core-0.1.0.dev5}/src/pinky_core/__init__.py +28 -4
  19. {pinky_core-0.1.0.dev3 → pinky_core-0.1.0.dev5}/src/pinky_core/finance/fec.py +9 -0
  20. {pinky_core-0.1.0.dev3 → pinky_core-0.1.0.dev5}/src/pinky_core/fmt.py +28 -1
  21. {pinky_core-0.1.0.dev3 → pinky_core-0.1.0.dev5}/src/pinky_core/html.py +9 -9
  22. {pinky_core-0.1.0.dev3 → pinky_core-0.1.0.dev5}/src/pinky_core/security.py +5 -4
  23. {pinky_core-0.1.0.dev3 → pinky_core-0.1.0.dev5}/src/pinky_core/sql.py +48 -2
  24. {pinky_core-0.1.0.dev3 → pinky_core-0.1.0.dev5}/src/pinky_core/xml.py +6 -6
  25. pinky_core-0.1.0.dev5/tests/fixtures/golden/dl_card_basic.html +29 -0
  26. pinky_core-0.1.0.dev5/tests/fixtures/golden/dl_card_expiry.html +29 -0
  27. pinky_core-0.1.0.dev5/tests/fixtures/golden/generate_html_basic.html +67 -0
  28. pinky_core-0.1.0.dev5/tests/fixtures/golden/task_table_error.html +20 -0
  29. pinky_core-0.1.0.dev5/tests/fixtures/golden/task_table_no_snowsight.html +20 -0
  30. pinky_core-0.1.0.dev5/tests/fixtures/golden/task_table_snowsight.html +20 -0
  31. {pinky_core-0.1.0.dev3 → pinky_core-0.1.0.dev5}/tests/test_fmt.py +18 -0
  32. pinky_core-0.1.0.dev5/tests/test_html.py +102 -0
  33. pinky_core-0.1.0.dev5/tests/test_public_api.py +69 -0
  34. pinky_core-0.1.0.dev5/tests/test_sql.py +170 -0
  35. pinky_core-0.1.0.dev5/uv.lock +1826 -0
  36. pinky_core-0.1.0.dev3/docs/adr/0001-zero-dependency-static-html.md +0 -26
  37. pinky_core-0.1.0.dev3/docs/explanation/2-features.md +0 -74
  38. pinky_core-0.1.0.dev3/scripts/hooks/generate-docs +0 -48
  39. pinky_core-0.1.0.dev3/tests/test_html.py +0 -63
  40. pinky_core-0.1.0.dev3/tests/test_sql.py +0 -82
  41. {pinky_core-0.1.0.dev3 → pinky_core-0.1.0.dev5}/.github/workflows/publish.yml +0 -0
  42. {pinky_core-0.1.0.dev3 → pinky_core-0.1.0.dev5}/.github/workflows/release.yml +0 -0
  43. {pinky_core-0.1.0.dev3 → pinky_core-0.1.0.dev5}/.gitleaks.toml +0 -0
  44. {pinky_core-0.1.0.dev3 → pinky_core-0.1.0.dev5}/.secrets.baseline +0 -0
  45. {pinky_core-0.1.0.dev3 → pinky_core-0.1.0.dev5}/LICENSE +0 -0
  46. {pinky_core-0.1.0.dev3 → pinky_core-0.1.0.dev5}/docs/.pages +0 -0
  47. {pinky_core-0.1.0.dev3 → pinky_core-0.1.0.dev5}/docs/adr/.pages +0 -0
  48. {pinky_core-0.1.0.dev3 → pinky_core-0.1.0.dev5}/docs/explanation/.pages +0 -0
  49. {pinky_core-0.1.0.dev3 → pinky_core-0.1.0.dev5}/docs/explanation/1-design.md +0 -0
  50. {pinky_core-0.1.0.dev3 → pinky_core-0.1.0.dev5}/docs/how-to/.pages +0 -0
  51. {pinky_core-0.1.0.dev3 → pinky_core-0.1.0.dev5}/docs/how-to/index.md +0 -0
  52. {pinky_core-0.1.0.dev3 → pinky_core-0.1.0.dev5}/docs/index.md +0 -0
  53. {pinky_core-0.1.0.dev3 → pinky_core-0.1.0.dev5}/docs/mkdocs.yml +0 -0
  54. {pinky_core-0.1.0.dev3 → pinky_core-0.1.0.dev5}/docs/reference/.pages +0 -0
  55. {pinky_core-0.1.0.dev3 → pinky_core-0.1.0.dev5}/docs/reference/api/.pages +0 -0
  56. {pinky_core-0.1.0.dev3 → pinky_core-0.1.0.dev5}/docs/reference/api/fec.md +0 -0
  57. {pinky_core-0.1.0.dev3 → pinky_core-0.1.0.dev5}/docs/reference/api/files.md +0 -0
  58. {pinky_core-0.1.0.dev3 → pinky_core-0.1.0.dev5}/docs/reference/api/fmt.md +0 -0
  59. {pinky_core-0.1.0.dev3 → pinky_core-0.1.0.dev5}/docs/reference/api/html.md +0 -0
  60. {pinky_core-0.1.0.dev3 → pinky_core-0.1.0.dev5}/docs/reference/api/lint.md +0 -0
  61. {pinky_core-0.1.0.dev3 → pinky_core-0.1.0.dev5}/docs/reference/api/schedule.md +0 -0
  62. {pinky_core-0.1.0.dev3 → pinky_core-0.1.0.dev5}/docs/reference/api/security.md +0 -0
  63. {pinky_core-0.1.0.dev3 → pinky_core-0.1.0.dev5}/docs/reference/api/sql.md +0 -0
  64. {pinky_core-0.1.0.dev3 → pinky_core-0.1.0.dev5}/docs/reference/api/validate.md +0 -0
  65. {pinky_core-0.1.0.dev3 → pinky_core-0.1.0.dev5}/docs/reference/api/warnings.md +0 -0
  66. {pinky_core-0.1.0.dev3 → pinky_core-0.1.0.dev5}/docs/reference/api/xml.md +0 -0
  67. {pinky_core-0.1.0.dev3 → pinky_core-0.1.0.dev5}/docs/reference/generated/noqa_badge.svg +0 -0
  68. {pinky_core-0.1.0.dev3 → pinky_core-0.1.0.dev5}/docs/tutorials/.pages +0 -0
  69. {pinky_core-0.1.0.dev3 → pinky_core-0.1.0.dev5}/docs/tutorials/index.md +0 -0
  70. {pinky_core-0.1.0.dev3 → pinky_core-0.1.0.dev5}/scripts/deploy_validate_udfs.py +0 -0
  71. {pinky_core-0.1.0.dev3 → pinky_core-0.1.0.dev5}/scripts/hooks/badges.toml +0 -0
  72. {pinky_core-0.1.0.dev3 → pinky_core-0.1.0.dev5}/scripts/hooks/check_forbidden_keywords.py +0 -0
  73. {pinky_core-0.1.0.dev3 → pinky_core-0.1.0.dev5}/scripts/hooks/check_snowpark_api.py +0 -0
  74. {pinky_core-0.1.0.dev3 → pinky_core-0.1.0.dev5}/scripts/hooks/check_typed_api.py +0 -0
  75. {pinky_core-0.1.0.dev3 → pinky_core-0.1.0.dev5}/scripts/hooks/check_zws.py +0 -0
  76. {pinky_core-0.1.0.dev3 → pinky_core-0.1.0.dev5}/scripts/hooks/docs_identity.json +0 -0
  77. {pinky_core-0.1.0.dev3 → pinky_core-0.1.0.dev5}/scripts/hooks/generate_ci_badges.py +0 -0
  78. {pinky_core-0.1.0.dev3 → pinky_core-0.1.0.dev5}/scripts/hooks/render_docs.py +0 -0
  79. {pinky_core-0.1.0.dev3 → pinky_core-0.1.0.dev5}/sql/deploy_udf_example.sql +0 -0
  80. {pinky_core-0.1.0.dev3 → pinky_core-0.1.0.dev5}/src/pinky_core/files.py +0 -0
  81. {pinky_core-0.1.0.dev3 → pinky_core-0.1.0.dev5}/src/pinky_core/finance/__init__.py +0 -0
  82. {pinky_core-0.1.0.dev3 → pinky_core-0.1.0.dev5}/src/pinky_core/lint.py +0 -0
  83. {pinky_core-0.1.0.dev3 → pinky_core-0.1.0.dev5}/src/pinky_core/schedule.py +0 -0
  84. {pinky_core-0.1.0.dev3 → pinky_core-0.1.0.dev5}/src/pinky_core/validate.py +0 -0
  85. {pinky_core-0.1.0.dev3 → pinky_core-0.1.0.dev5}/src/pinky_core/warnings.py +0 -0
  86. {pinky_core-0.1.0.dev3 → pinky_core-0.1.0.dev5}/tests/__init__.py +0 -0
  87. {pinky_core-0.1.0.dev3 → pinky_core-0.1.0.dev5}/tests/fixtures/cats_domination.xml +0 -0
  88. {pinky_core-0.1.0.dev3 → pinky_core-0.1.0.dev5}/tests/test_fec.py +0 -0
  89. {pinky_core-0.1.0.dev3 → pinky_core-0.1.0.dev5}/tests/test_files.py +0 -0
  90. {pinky_core-0.1.0.dev3 → pinky_core-0.1.0.dev5}/tests/test_lint.py +0 -0
  91. {pinky_core-0.1.0.dev3 → pinky_core-0.1.0.dev5}/tests/test_schedule.py +0 -0
  92. {pinky_core-0.1.0.dev3 → pinky_core-0.1.0.dev5}/tests/test_security.py +0 -0
  93. {pinky_core-0.1.0.dev3 → pinky_core-0.1.0.dev5}/tests/test_validate.py +0 -0
  94. {pinky_core-0.1.0.dev3 → pinky_core-0.1.0.dev5}/tests/test_warnings.py +0 -0
  95. {pinky_core-0.1.0.dev3 → pinky_core-0.1.0.dev5}/tests/test_xml_xpath.py +0 -0
@@ -3,10 +3,8 @@ name: CI
3
3
  on:
4
4
  push:
5
5
  branches: [main, develop]
6
- paths: ['src/**', 'tests/**', 'pyproject.toml', 'scripts/**', '.pre-commit-config.yaml', '.github/workflows/ci.yml']
7
6
  pull_request:
8
7
  branches: [main, develop]
9
- paths: ['src/**', 'tests/**', 'pyproject.toml', 'scripts/**', '.pre-commit-config.yaml', '.github/workflows/ci.yml']
10
8
 
11
9
  jobs:
12
10
  ci:
@@ -33,7 +31,10 @@ jobs:
33
31
  uses: j178/prek-action@v2
34
32
 
35
33
  - name: Install dependencies
36
- run: pip install -e ".[validate,address,dev]"
34
+ # --group dev, pas ".[dev]" : les deps de dev sont un dependency-group PEP 735 depuis
35
+ # 2026-07-22 (voir le commentaire dans pyproject.toml — `uv run` désinstallait les extras).
36
+ # Requiert pip >= 25.1 ; ensurepip en livre 26.1.2 avec Python 3.12, donc rien à épingler.
37
+ run: pip install -e ".[validate,address,schedule,xml,lint]" --group dev
37
38
 
38
39
  - name: Tests
39
40
  run: pytest
@@ -12,7 +12,10 @@ build/
12
12
 
13
13
  # ── Environments ─────────────────────────────────────────────────────────────
14
14
  .env
15
- .venv/
15
+ # No trailing slash: with `centralized-project-envs`, uv puts the env in ~/.cache/uv and drops a
16
+ # SYMLINK named .venv here. `.venv/` matches directories only, so the link stayed untracked-visible
17
+ # and committable — a machine-absolute path, useless on any other machine.
18
+ .venv
16
19
  venv/
17
20
  .conda/
18
21
 
@@ -15,6 +15,15 @@ default_install_hook_types: [pre-commit, commit-msg]
15
15
  # Pin explicite : sans ça, les hooks `language: python` (mdformat, mypy, interrogate, darglint…)
16
16
  # créent leur venv avec le python3 ambiant du PATH au moment de l'install, pas une version garantie
17
17
  # par le repo — non-déterministe en local (CI est protégée par actions/setup-python en amont).
18
+ #
19
+ # Règle (2026-07-22) : tout hook `repo: local` dont l'`entry` commence par `python` DOIT être
20
+ # `language: python`, jamais `language: system`. `system` exécute la commande telle quelle dans le
21
+ # shell, et `python` nu n'existe ni sur macOS hors conda (homebrew ne pose que `python3`) ni sur une
22
+ # machine Windows vanilla — ça ne marchait que par l'auto-activation de conda base. Avec
23
+ # `language: python`, prek provisionne l'interpréteur (et le télécharge s'il manque) et met le bin/
24
+ # de son venv en tête du PATH : `python` y est garanti sur les trois OS. Les scripts de
25
+ # scripts/hooks/ étant 100 % stdlib, ils partagent tous un seul venv (dédupliqué par langage +
26
+ # version + additional_dependencies) — coût quasi nul.
18
27
  default_language_version:
19
28
  python: python3.12
20
29
 
@@ -80,7 +89,7 @@ repos:
80
89
  - id: check-zws
81
90
  name: "[ZWS] Invisible characters (zero-width)"
82
91
  entry: python scripts/hooks/check_zws.py
83
- language: system
92
+ language: python
84
93
  pass_filenames: true
85
94
 
86
95
  # ── Employer reference guard ──────────────────────────────────────────────
@@ -89,7 +98,7 @@ repos:
89
98
  - id: check-forbidden-keywords
90
99
  name: "[FORBIDDEN] Employer references"
91
100
  entry: python scripts/hooks/check_forbidden_keywords.py
92
- language: system
101
+ language: python
93
102
  pass_filenames: true
94
103
 
95
104
  # ── Commit message format ─────────────────────────────────────────────────
@@ -171,10 +180,22 @@ repos:
171
180
  - id: check-typed-api
172
181
  name: "[TYPED-API] No raw dict/tuple/set in public annotations"
173
182
  entry: python scripts/hooks/check_typed_api.py
174
- language: system
183
+ language: python
175
184
  types: [python]
176
185
  files: ^src/
177
186
 
187
+ # Weak-assertion ratchet — first hook to target tests/, not src/ (tests.md § generator oracle).
188
+ # Blocks a fragment `assert "x" in <generator(...)>` that has no whole-output equality oracle in
189
+ # the same file. Narrow by design (name-matched generators only) so it stays at ~0 flags.
190
+ - repo: local
191
+ hooks:
192
+ - id: check-weak-assertions
193
+ name: "[WEAK-ASSERT] Generated-output assertion needs an equality oracle"
194
+ entry: python scripts/hooks/check_weak_assertions.py
195
+ language: python
196
+ types: [python]
197
+ files: ^tests/
198
+
178
199
  # ── EXTRA snowflake — Snowpark / Core API repos ───────────────────────────
179
200
  # Snowpark / Core API conventions (no session.sql(), canonical method names)
180
201
  - repo: local
@@ -182,6 +203,6 @@ repos:
182
203
  - id: check-snowpark-api
183
204
  name: "[SNOWPARK-API] No session.sql(), canonical method names"
184
205
  entry: python scripts/hooks/check_snowpark_api.py
185
- language: system
206
+ language: python
186
207
  types: [python]
187
208
  files: ^src/
@@ -0,0 +1 @@
1
+ 3.12
@@ -0,0 +1,8 @@
1
+ {
2
+ // uv keeps this repo's environment in its own cache, OUT of the synced iCloud drive (ADR-0023),
3
+ // and the `.venv` here is a SYMLINK to it. VSCode's environment auto-discovery does NOT follow that
4
+ // symlink, so the interpreter is declared explicitly rather than picked by hand each time.
5
+ // ${workspaceFolder} is THIS repo when the multi-root `pinky.code-workspace` is open.
6
+ // Env missing? `uv sync --all-extras` recreates it.
7
+ "python.defaultInterpreterPath": "${workspaceFolder}/.venv/bin/python"
8
+ }
@@ -0,0 +1,24 @@
1
+ # CONTEXT.md — pinky-core
2
+ *Update date : 2026-07-12 13:12*
3
+
4
+ Single source of truth for naming in this repo. Variables, functions, modules, and prose follow it.
5
+ **Agent rule: read this before naming anything.**
6
+
7
+ | Term (preferred) | Avoid | Definition |
8
+ |---|---|---|
9
+ | `validate_*` (`validate_siret`, `validate_identifier`, …) | check, verify, sanitize | Reject-only — returns the input unchanged if valid, raises/returns invalid otherwise. Never transforms. |
10
+ | `standardize_*` (`standardize_email`, `standardize_phone`) | normalize, format | Validates **and** normalizes to a canonical form (e.g. phone → E.164). |
11
+ | `safe_*` (`safe_filepath`) | sanitize, escape | Same contract as `validate_*` — reject-only. Named `safe_` (not `sanitize_`) because the caller's safety comes from the *rejection*, not from any escaping/transform; a docstring once claimed it escaped quotes, it never did (coco review, 2026-07-12) — don't reintroduce that confusion. |
12
+ | `ref` / `refs` (`extract_table_refs`, `resolve_query`'s `refs` param) | reference(s) | A table reference string — see `FQN`. |
13
+ | `FQN` | fully-qualified name, qualified identifier | `DATABASE.SCHEMA.TABLE` (or `SCHEMA.TABLE`) — pinky-provider's identifier convention. A bare, unqualified name is never a legitimate FQN in this codebase (see `extract_table_refs`). |
14
+ | `predicate` (schedule.py's `is_business_day`, `is_eve_of_holiday`, …) | check, rule, condition | A pure `bool`-returning function taking a `date`, callable as a DAG root-task SKIP gate (ADR-0013 in pinky-provider). |
15
+ | `*Result` (`EmailResult`, `FecParseResult`, …) | `*Response`, `*Output` | A model wrapping the outcome of one validate/parse call — success or failure both representable, never raised. |
16
+ | `*Error` (`FecRowError`, `FecBalanceError`) | `*Failure`, `*Issue` | A record of one specific failure collected *during* processing (e.g. one bad row) — distinct from `*Result`, which wraps the whole call's outcome. |
17
+ | `lazy import` (`from unidecode import unidecode` inside a function body) | optional import, deferred import | The pattern every optional-dependency function uses so importing the *module* never requires the extra — only calling that specific function does. |
18
+
19
+ ## Flagged ambiguities
20
+
21
+ - **`slugify` breaks the `to_*` prefix convention** (`to_upper_snake_case`, `to_camel_case`) — kept
22
+ bare because "slugify" is an established cross-language term (same name in Django, JS `slugify`
23
+ packages, etc.); renaming it `to_slug` would be more consistent internally but less recognizable.
24
+ Not revisited unless a second bare-verb name is added and the inconsistency compounds.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pinky-core
3
- Version: 0.1.0.dev3
3
+ Version: 0.1.0.dev5
4
4
  Summary: Shared Python utilities for Snowflake development
5
5
  Project-URL: Homepage, https://github.com/pinky-and-co/pinky-core
6
6
  Project-URL: Repository, https://github.com/pinky-and-co/pinky-core
@@ -40,19 +40,6 @@ Requires-Dist: sqlglot
40
40
  Requires-Dist: unidecode
41
41
  Provides-Extra: address
42
42
  Requires-Dist: usaddress; extra == 'address'
43
- Provides-Extra: dev
44
- Requires-Dist: bandit; extra == 'dev'
45
- Requires-Dist: darglint; extra == 'dev'
46
- Requires-Dist: genbadge[coverage]; extra == 'dev'
47
- Requires-Dist: interrogate; extra == 'dev'
48
- Requires-Dist: mkdocs-awesome-pages-plugin>=2.0; extra == 'dev'
49
- Requires-Dist: mkdocs-material>=9.0; extra == 'dev'
50
- Requires-Dist: mkdocstrings[python]>=0.25; extra == 'dev'
51
- Requires-Dist: mypy; extra == 'dev'
52
- Requires-Dist: pre-commit; extra == 'dev'
53
- Requires-Dist: pytest; extra == 'dev'
54
- Requires-Dist: pytest-cov; extra == 'dev'
55
- Requires-Dist: ruff; extra == 'dev'
56
43
  Provides-Extra: fec
57
44
  Requires-Dist: unidecode; extra == 'fec'
58
45
  Provides-Extra: lint
@@ -158,12 +145,15 @@ pinky-core/
158
145
  │ ├── ci.yml
159
146
  │ ├── publish.yml
160
147
  │ └── release.yml
148
+ ├── .vscode/
149
+ │ └── settings.json
161
150
  ├── scripts/
162
151
  │ ├── hooks/
163
152
  │ │ ├── badges.toml
164
153
  │ │ ├── check_forbidden_keywords.py
165
154
  │ │ ├── check_snowpark_api.py
166
155
  │ │ ├── check_typed_api.py
156
+ │ │ ├── check_weak_assertions.py
167
157
  │ │ ├── check_zws.py
168
158
  │ │ ├── commit-msg
169
159
  │ │ ├── docs_identity.json
@@ -191,6 +181,13 @@ pinky-core/
191
181
  │ └── xml.py
192
182
  ├── tests/
193
183
  │ ├── fixtures/
184
+ │ │ ├── golden/
185
+ │ │ │ ├── dl_card_basic.html
186
+ │ │ │ ├── dl_card_expiry.html
187
+ │ │ │ ├── generate_html_basic.html
188
+ │ │ │ ├── task_table_error.html
189
+ │ │ │ ├── task_table_no_snowsight.html
190
+ │ │ │ └── task_table_snowsight.html
194
191
  │ │ └── cats_domination.xml
195
192
  │ ├── __init__.py
196
193
  │ ├── test_fec.py
@@ -198,6 +195,7 @@ pinky-core/
198
195
  │ ├── test_fmt.py
199
196
  │ ├── test_html.py
200
197
  │ ├── test_lint.py
198
+ │ ├── test_public_api.py
201
199
  │ ├── test_schedule.py
202
200
  │ ├── test_security.py
203
201
  │ ├── test_sql.py
@@ -207,10 +205,13 @@ pinky-core/
207
205
  ├── .gitignore
208
206
  ├── .gitleaks.toml
209
207
  ├── .pre-commit-config.yaml
208
+ ├── .python-version
210
209
  ├── .secrets.baseline
210
+ ├── CONTEXT.md
211
211
  ├── LICENSE
212
212
  ├── pyproject.toml
213
- └── README.md
213
+ ├── README.md
214
+ └── uv.lock
214
215
  ```
215
216
 
216
217
  </details>
@@ -85,12 +85,15 @@ pinky-core/
85
85
  │ ├── ci.yml
86
86
  │ ├── publish.yml
87
87
  │ └── release.yml
88
+ ├── .vscode/
89
+ │ └── settings.json
88
90
  ├── scripts/
89
91
  │ ├── hooks/
90
92
  │ │ ├── badges.toml
91
93
  │ │ ├── check_forbidden_keywords.py
92
94
  │ │ ├── check_snowpark_api.py
93
95
  │ │ ├── check_typed_api.py
96
+ │ │ ├── check_weak_assertions.py
94
97
  │ │ ├── check_zws.py
95
98
  │ │ ├── commit-msg
96
99
  │ │ ├── docs_identity.json
@@ -118,6 +121,13 @@ pinky-core/
118
121
  │ └── xml.py
119
122
  ├── tests/
120
123
  │ ├── fixtures/
124
+ │ │ ├── golden/
125
+ │ │ │ ├── dl_card_basic.html
126
+ │ │ │ ├── dl_card_expiry.html
127
+ │ │ │ ├── generate_html_basic.html
128
+ │ │ │ ├── task_table_error.html
129
+ │ │ │ ├── task_table_no_snowsight.html
130
+ │ │ │ └── task_table_snowsight.html
121
131
  │ │ └── cats_domination.xml
122
132
  │ ├── __init__.py
123
133
  │ ├── test_fec.py
@@ -125,6 +135,7 @@ pinky-core/
125
135
  │ ├── test_fmt.py
126
136
  │ ├── test_html.py
127
137
  │ ├── test_lint.py
138
+ │ ├── test_public_api.py
128
139
  │ ├── test_schedule.py
129
140
  │ ├── test_security.py
130
141
  │ ├── test_sql.py
@@ -134,10 +145,13 @@ pinky-core/
134
145
  ├── .gitignore
135
146
  ├── .gitleaks.toml
136
147
  ├── .pre-commit-config.yaml
148
+ ├── .python-version
137
149
  ├── .secrets.baseline
150
+ ├── CONTEXT.md
138
151
  ├── LICENSE
139
152
  ├── pyproject.toml
140
- └── README.md
153
+ ├── README.md
154
+ └── uv.lock
141
155
  ```
142
156
 
143
157
  </details>
@@ -0,0 +1,26 @@
1
+ ---
2
+ title: "0001"
3
+ ---
4
+ # ADR-0001 — Zero Snowflake dependency at import time
5
+ Status: accepted
6
+ Date: 2026-05-31
7
+
8
+ ## Contexte
9
+
10
+ `pinky-core` doit être importable dans tous les contextes où Python s'exécute : pipelines CI,
11
+ scripts de développement local, gestionnaires UDF à l'intérieur de Snowflake, et Streamlit dans les applications Snowflake.
12
+ L'importation inconditionnelle de Snowpark lève une erreur ou ajoute un coût de démarrage dans les contextes
13
+ qui n'ont pas de session active.
14
+
15
+ ## Décision
16
+
17
+ Aucun module dans `pinky-core` n'importe Snowflake ou Snowpark au niveau du module.
18
+ Les dépendances optionnelles (`email-validator`, `phonenumbers`, `python-stdnum`, `croniter`) sont
19
+ importées paresseusement dans les fonctions. Les importations réservées au type vérification utilisent la garde `TYPE_CHECKING`.
20
+
21
+ ## Conséquences
22
+
23
+ - `pinky-core` est installable dans n'importe quel environnement Python 3.11+ sans client
24
+ Snowflake — `pip install pinky-core` fonctionne en CI pur.
25
+ - Tout module qui a besoin d'une session active appartient à `pinky-snowpark`, pas ici.
26
+ - Les erreurs d'exécution dues aux dépendances optionnelles manquantes apparaissent au moment de l'appel, non à l'importation.
@@ -0,0 +1,44 @@
1
+ ---
2
+ title: Features
3
+ ---
4
+ # Features — pinky-core
5
+ *Update date : 2026-07-25 09:25*
6
+
7
+ Feature list ordered by priority. `stable` = in production. `planned` = not yet built, order = priority.
8
+
9
+ ---
10
+
11
+ ## Stable
12
+
13
+ | Module | Description | Status |
14
+ |--------|-------------|--------|
15
+ | `fmt.py` | Formatters: dates/periods, numbers (compact/duration/fraction/trend/stars/boolean), string case (UPPER_SNAKE/camelCase/slug), text/city normalization, ISO3→ISO2, header dedup, FR/AFNOR address parsing | stable |
16
+ | `validate.py` | Input validation: SIRET/SIREN, IBAN/BIC, TVA EU, NIR, email, phone | stable |
17
+ | `security.py` | Filepath and SQL-identifier sanitisation — guards against injection in dynamically built DDL | stable |
18
+ | `sql.py` | SQL parsing helpers powered by sqlglot: statement kind, table-ref extraction, query resolution, statement splitting | stable |
19
+ | `html.py` | HTML templating engine — emails, reports, mini-sites, Outlook-safe | stable |
20
+ | `xml.py` | XPath helpers, namespace-aware parsing, safe attribute extraction | stable |
21
+ | `files.py` | S3/MFT filename pattern normalisation — `detect_file_pattern`, `FileDetection`; replaces dates, timestamps, UUIDs, ISO country codes, structured business identifiers (SIRET/SIREN/IBAN/VAT_FR), and numeric IDs with placeholders | stable |
22
+ | `schedule.py` | French public holidays, business-day helpers, cron human-readable formatting, Airflow-timetable predicates (month/week/fiscal-quarter, eve-of-holiday) | stable |
23
+ | `lint.py` | Optional lint/format delegation (sqlfluff/ruff/yamllint/detect-secrets) — consumed by pinky-provider for the secret-scan deploy gate and SQL pre-flight | stable |
24
+ | `warnings.py` | `PinkyDeprecationWarning` — deprecated pinky APIs and UDFs. No consumer wired yet (destined for pinky-provider's registry `status: deprecated`) | stable |
25
+ | `finance/fec.py` | FEC parser (Fichier des Écritures Comptables, arrêté A47 A-1) — `parse_fec`/`check_fec_balance`, positional column reading, dual format tolerance (delimiter/date/decimal/amount schema), mojibake repair | stable |
26
+
27
+ ---
28
+
29
+ ## Planned
30
+
31
+ Ordered by priority — closest to implementation first.
32
+
33
+ ### finance/ — extend beyond FEC
34
+
35
+ Same lineage as `finance/fec.py` (real FR accounting/payroll file formats) — no current consumer card, kept for the likely follow-on employer-migration need.
36
+
37
+ - `finance/dsn.py` — Déclaration Sociale Nominative parser (INSEE/Net-Entreprises)
38
+ - `finance/sepa.py` — SEPA pain.001 / pain.008 XML builders (credit transfers, direct debits)
39
+ - `finance/ofx.py` — bank statement parsers (OFX/QFX for reconciliation)
40
+
41
+ ### finance/ — FEC validator as CLI
42
+
43
+ `python -m pinky_core.finance.fec validate file.txt` — thin CLI wrapper around the already-built
44
+ `parse_fec`/`check_fec_balance`. Gated on `pinky-tools` CLI integration (chantier 0002), not speculative.
@@ -1 +1 @@
1
- <svg xmlns="http://www.w3.org/2000/svg" width="114" height="20" role="img" aria-label="coverage: 50.56%"><title>coverage: 50.56%</title><filter id="blur"><feGaussianBlur stdDeviation="16"/></filter><linearGradient id="s" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><clipPath id="r"><rect width="114" height="20" rx="3"/></clipPath><g clip-path="url(#r)"><rect width="61" height="20" fill="#555"/><rect x="61" width="53" height="20" fill="#ea7233"/><rect width="114" height="20" fill="url(#s)"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110"><g transform="scale(.1)"><g aria-hidden="true" fill="#010101"><text x="315" y="150" fill-opacity=".8" filter="url(#blur)" textLength="510">coverage</text><text x="315" y="150" fill-opacity=".3" textLength="510">coverage</text></g><text x="315" y="140" textLength="510">coverage</text></g><g transform="scale(.1)"><g aria-hidden="true" fill="#010101"><text x="865" y="150" fill-opacity=".8" filter="url(#blur)" textLength="430">50.56%</text><text x="865" y="150" fill-opacity=".3" textLength="430">50.56%</text></g><text x="865" y="140" textLength="430">50.56%</text></g></g></svg>
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="114" height="20" role="img" aria-label="coverage: 84.34%"><title>coverage: 84.34%</title><filter id="blur"><feGaussianBlur stdDeviation="16"/></filter><linearGradient id="s" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><clipPath id="r"><rect width="114" height="20" rx="3"/></clipPath><g clip-path="url(#r)"><rect width="61" height="20" fill="#555"/><rect x="61" width="53" height="20" fill="#67ac09"/><rect width="114" height="20" fill="url(#s)"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110"><g transform="scale(.1)"><g aria-hidden="true" fill="#010101"><text x="315" y="150" fill-opacity=".8" filter="url(#blur)" textLength="510">coverage</text><text x="315" y="150" fill-opacity=".3" textLength="510">coverage</text></g><text x="315" y="140" textLength="510">coverage</text></g><g transform="scale(.1)"><g aria-hidden="true" fill="#010101"><text x="865" y="150" fill-opacity=".8" filter="url(#blur)" textLength="430">84.34%</text><text x="865" y="150" fill-opacity=".3" textLength="430">84.34%</text></g><text x="865" y="140" textLength="430">84.34%</text></g></g></svg>
@@ -1,12 +1,12 @@
1
1
  <svg xmlns="http://www.w3.org/2000/svg" width="81" height="20">
2
- <title>generate-docs · mdformat · check-zws · check-forbidden-keywords · commit-msg-format · mypy · interrogate · darglint · check-typed-api · check-snowpark-api</title>
2
+ <title>generate-docs · mdformat · check-zws · check-forbidden-keywords · commit-msg-format · mypy · interrogate · darglint · check-typed-api · check-weak-assertions · check-snowpark-api</title>
3
3
  <rect width="81" height="20" rx="3" fill="#555"/>
4
4
  <rect x="50" width="31" height="20" rx="3" fill="#007ec6"/>
5
5
  <rect x="50" width="4" height="20" fill="#007ec6"/>
6
6
  <g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11">
7
7
  <text x="25" y="15" fill="#010101" fill-opacity=".3">hooks</text>
8
8
  <text x="25" y="14">hooks</text>
9
- <text x="65" y="15" fill="#010101" fill-opacity=".3">10</text>
10
- <text x="65" y="14">10</text>
9
+ <text x="65" y="15" fill="#010101" fill-opacity=".3">11</text>
10
+ <text x="65" y="14">11</text>
11
11
  </g>
12
12
  </svg>
@@ -1,10 +1,10 @@
1
1
  <svg width="140" height="20" viewBox="0 0 140 20" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
2
- <title>interrogate: 65.9%</title>
2
+ <title>interrogate: 100%</title>
3
3
  <g transform="matrix(1,0,0,1,22,0)">
4
4
  <g id="backgrounds" transform="matrix(1.32789,0,0,1,-22.3892,0)">
5
5
  <rect x="0" y="0" width="71" height="20" style="fill:rgb(85,85,85);"/>
6
6
  </g>
7
- <rect x="71" y="0" width="47" height="20" data-interrogate="color" style="fill:#dfb317"/>
7
+ <rect x="71" y="0" width="47" height="20" data-interrogate="color" style="fill:#4c1"/>
8
8
  <g transform="matrix(1.19746,0,0,1,-22.3744,-4.85723e-16)">
9
9
  <rect x="0" y="0" width="118" height="20" style="fill:url(#_Linear1);"/>
10
10
  </g>
@@ -12,8 +12,8 @@
12
12
  <g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="110">
13
13
  <text x="590" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="610">interrogate</text>
14
14
  <text x="590" y="140" transform="scale(.1)" textLength="610">interrogate</text>
15
- <text x="1160" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="370" data-interrogate="result">65.9%</text>
16
- <text x="1160" y="140" transform="scale(.1)" textLength="370" data-interrogate="result">65.9%</text>
15
+ <text x="1160" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="330" data-interrogate="result">100%</text>
16
+ <text x="1160" y="140" transform="scale(.1)" textLength="330" data-interrogate="result">100%</text>
17
17
  </g>
18
18
  <g id="logo-shadow" serif:id="logo shadow" transform="matrix(0.854876,0,0,0.854876,-6.73514,1.732)">
19
19
  <g transform="matrix(0.299012,0,0,0.299012,9.70229,-6.68582)">
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "pinky-core"
7
- version = "0.1.0.dev3"
7
+ version = "0.1.0.dev5"
8
8
  description = "Shared Python utilities for Snowflake development"
9
9
  readme = "README.md"
10
10
  license = { file = "LICENSE" }
@@ -32,7 +32,18 @@ xml = ["lxml", "lxml-stubs"]
32
32
  fec = ["unidecode"]
33
33
  # Delegated lint/format/scan backends for pinky_core.lint (SQL / YAML / Python / secrets).
34
34
  lint = ["sqlfluff>=3.0", "yamllint>=1.35", "ruff", "detect-secrets>=1.5"]
35
- dev = [
35
+
36
+ # Deps de dev en dependency-group (PEP 735), PAS en extra (2026-07-22, repo pilote).
37
+ # `uv run` resynchronise l'environnement vers l'état PAR DÉFAUT : les extras n'en font pas partie, il
38
+ # les DÉSINSTALLE donc silencieusement, puis retombe sur le premier pytest trouvé sur le PATH — c'est
39
+ # ce qui avait produit un « 1180 tests au vert » qui validait en réalité les envs conda. Le groupe
40
+ # `dev` de PEP 735, lui, est synchronisé par défaut : `uv run` redevient correct sans aucun flag à
41
+ # retenir. (`[tool.uv] default-extras` est accepté sans erreur mais reste sans effet — testé.)
42
+ # Côté CI, l'équivalent de `-e ".[dev]"` est `-e . --group dev` (pip >= 25.1 ; ensurepip livre 26.1.2
43
+ # avec Python 3.12, donc aucune contrainte à gérer). Un extra reste le bon outil pour une option
44
+ # destinée aux CONSOMMATEURS de la lib — validate/address/schedule/xml/fec/lint restent des extras.
45
+ [dependency-groups]
46
+ dev = [
36
47
  "mkdocs-material>=9.0",
37
48
  "mkdocs-awesome-pages-plugin>=2.0",
38
49
  "mkdocstrings[python]>=0.25",
@@ -52,6 +63,14 @@ Homepage = "https://github.com/pinky-and-co/pinky-core"
52
63
  Repository = "https://github.com/pinky-and-co/pinky-core"
53
64
  Issues = "https://github.com/pinky-and-co/pinky-core/issues"
54
65
 
66
+ # ── uv ───────────────────────────────────────────────────────────────────────
67
+ # The project environment lives in uv's cache, NOT in a `.venv` at the repo root: this workspace sits
68
+ # on a synced iCloud drive, and ADR-0023 recorded confirmed corruption when a tool environment lives
69
+ # inside one. Preview feature, accepted knowingly — `uv sync` prints the resolved path, so re-check it
70
+ # still lands under ~/.cache/uv after a uv upgrade.
71
+ [tool.uv]
72
+ preview-features = ["centralized-project-envs"]
73
+
55
74
  [tool.hatch.build.targets.wheel]
56
75
  packages = ["src/pinky_core"]
57
76
 
@@ -105,13 +124,21 @@ follow_imports = "skip"
105
124
 
106
125
  # ── Interrogate ───────────────────────────────────────────────────────────────
107
126
  [tool.interrogate]
108
- ignore-init-method = true
109
- ignore-magic = true
110
- ignore-module = true
111
- ignore-private = true
112
- fail-under = 60
113
- paths = ["src/"]
114
- generate-badge = "docs/reference/generated/"
127
+ ignore-init-method = true
128
+ ignore-magic = true
129
+ ignore-module = true
130
+ ignore-private = true
131
+ # --ignore-private only covers dunder-prefixed (__foo) names; this codebase's actual
132
+ # private-helper convention is single-underscore (_foo) — that's --ignore-semiprivate.
133
+ # Without it, every internal helper (xml.py's _parse_path, fmt.py's _parse_fr, …) counted
134
+ # against docstring coverage, deflating the real number from ~98% to ~66%.
135
+ ignore-semiprivate = true
136
+ # Real coverage is ~97.6% now that ignore-semiprivate is set (was capped at 60 to not
137
+ # block interrogate's initial rollout). Threshold left with headroom below current, not
138
+ # pinned to 97.6, so incidental drift doesn't fail CI on unrelated PRs.
139
+ fail-under = 95
140
+ paths = ["src/"]
141
+ generate-badge = "docs/reference/generated/"
115
142
 
116
143
  # ── Darglint ─────────────────────────────────────────────────────────────────
117
144
  [tool.darglint]