pinky-core 0.1.0.dev5__tar.gz → 0.1.0.dev6__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 (92) hide show
  1. {pinky_core-0.1.0.dev5 → pinky_core-0.1.0.dev6}/.github/workflows/ci.yml +8 -0
  2. {pinky_core-0.1.0.dev5 → pinky_core-0.1.0.dev6}/.pre-commit-config.yaml +22 -0
  3. {pinky_core-0.1.0.dev5 → pinky_core-0.1.0.dev6}/PKG-INFO +3 -2
  4. {pinky_core-0.1.0.dev5 → pinky_core-0.1.0.dev6}/README.md +2 -1
  5. {pinky_core-0.1.0.dev5 → pinky_core-0.1.0.dev6}/docs/reference/generated/coverage_badge.svg +1 -1
  6. {pinky_core-0.1.0.dev5 → pinky_core-0.1.0.dev6}/docs/reference/generated/hooks_badge.svg +3 -3
  7. {pinky_core-0.1.0.dev5 → pinky_core-0.1.0.dev6}/pyproject.toml +1 -1
  8. {pinky_core-0.1.0.dev5 → pinky_core-0.1.0.dev6}/scripts/hooks/check_weak_assertions.py +5 -4
  9. {pinky_core-0.1.0.dev5 → pinky_core-0.1.0.dev6}/scripts/hooks/generate-docs +22 -4
  10. pinky_core-0.1.0.dev6/scripts/hooks/run-tests +67 -0
  11. pinky_core-0.1.0.dev6/src/pinky_core/__init__.py +132 -0
  12. {pinky_core-0.1.0.dev5 → pinky_core-0.1.0.dev6}/src/pinky_core/files.py +1 -1
  13. {pinky_core-0.1.0.dev5 → pinky_core-0.1.0.dev6}/uv.lock +1 -1
  14. pinky_core-0.1.0.dev5/src/pinky_core/__init__.py +0 -221
  15. {pinky_core-0.1.0.dev5 → pinky_core-0.1.0.dev6}/.github/workflows/publish.yml +0 -0
  16. {pinky_core-0.1.0.dev5 → pinky_core-0.1.0.dev6}/.github/workflows/release.yml +0 -0
  17. {pinky_core-0.1.0.dev5 → pinky_core-0.1.0.dev6}/.gitignore +0 -0
  18. {pinky_core-0.1.0.dev5 → pinky_core-0.1.0.dev6}/.gitleaks.toml +0 -0
  19. {pinky_core-0.1.0.dev5 → pinky_core-0.1.0.dev6}/.python-version +0 -0
  20. {pinky_core-0.1.0.dev5 → pinky_core-0.1.0.dev6}/.secrets.baseline +0 -0
  21. {pinky_core-0.1.0.dev5 → pinky_core-0.1.0.dev6}/.vscode/settings.json +0 -0
  22. {pinky_core-0.1.0.dev5 → pinky_core-0.1.0.dev6}/CONTEXT.md +0 -0
  23. {pinky_core-0.1.0.dev5 → pinky_core-0.1.0.dev6}/LICENSE +0 -0
  24. {pinky_core-0.1.0.dev5 → pinky_core-0.1.0.dev6}/docs/.pages +0 -0
  25. {pinky_core-0.1.0.dev5 → pinky_core-0.1.0.dev6}/docs/adr/.pages +0 -0
  26. {pinky_core-0.1.0.dev5 → pinky_core-0.1.0.dev6}/docs/adr/0001-zero-dependency-static-html.md +0 -0
  27. {pinky_core-0.1.0.dev5 → pinky_core-0.1.0.dev6}/docs/explanation/.pages +0 -0
  28. {pinky_core-0.1.0.dev5 → pinky_core-0.1.0.dev6}/docs/explanation/1-design.md +0 -0
  29. {pinky_core-0.1.0.dev5 → pinky_core-0.1.0.dev6}/docs/explanation/2-features.md +0 -0
  30. {pinky_core-0.1.0.dev5 → pinky_core-0.1.0.dev6}/docs/how-to/.pages +0 -0
  31. {pinky_core-0.1.0.dev5 → pinky_core-0.1.0.dev6}/docs/how-to/index.md +0 -0
  32. {pinky_core-0.1.0.dev5 → pinky_core-0.1.0.dev6}/docs/index.md +0 -0
  33. {pinky_core-0.1.0.dev5 → pinky_core-0.1.0.dev6}/docs/mkdocs.yml +0 -0
  34. {pinky_core-0.1.0.dev5 → pinky_core-0.1.0.dev6}/docs/reference/.pages +0 -0
  35. {pinky_core-0.1.0.dev5 → pinky_core-0.1.0.dev6}/docs/reference/api/.pages +0 -0
  36. {pinky_core-0.1.0.dev5 → pinky_core-0.1.0.dev6}/docs/reference/api/fec.md +0 -0
  37. {pinky_core-0.1.0.dev5 → pinky_core-0.1.0.dev6}/docs/reference/api/files.md +0 -0
  38. {pinky_core-0.1.0.dev5 → pinky_core-0.1.0.dev6}/docs/reference/api/fmt.md +0 -0
  39. {pinky_core-0.1.0.dev5 → pinky_core-0.1.0.dev6}/docs/reference/api/html.md +0 -0
  40. {pinky_core-0.1.0.dev5 → pinky_core-0.1.0.dev6}/docs/reference/api/lint.md +0 -0
  41. {pinky_core-0.1.0.dev5 → pinky_core-0.1.0.dev6}/docs/reference/api/schedule.md +0 -0
  42. {pinky_core-0.1.0.dev5 → pinky_core-0.1.0.dev6}/docs/reference/api/security.md +0 -0
  43. {pinky_core-0.1.0.dev5 → pinky_core-0.1.0.dev6}/docs/reference/api/sql.md +0 -0
  44. {pinky_core-0.1.0.dev5 → pinky_core-0.1.0.dev6}/docs/reference/api/validate.md +0 -0
  45. {pinky_core-0.1.0.dev5 → pinky_core-0.1.0.dev6}/docs/reference/api/warnings.md +0 -0
  46. {pinky_core-0.1.0.dev5 → pinky_core-0.1.0.dev6}/docs/reference/api/xml.md +0 -0
  47. {pinky_core-0.1.0.dev5 → pinky_core-0.1.0.dev6}/docs/reference/generated/interrogate_badge.svg +0 -0
  48. {pinky_core-0.1.0.dev5 → pinky_core-0.1.0.dev6}/docs/reference/generated/noqa_badge.svg +0 -0
  49. {pinky_core-0.1.0.dev5 → pinky_core-0.1.0.dev6}/docs/tutorials/.pages +0 -0
  50. {pinky_core-0.1.0.dev5 → pinky_core-0.1.0.dev6}/docs/tutorials/index.md +0 -0
  51. {pinky_core-0.1.0.dev5 → pinky_core-0.1.0.dev6}/scripts/deploy_validate_udfs.py +0 -0
  52. {pinky_core-0.1.0.dev5 → pinky_core-0.1.0.dev6}/scripts/hooks/badges.toml +0 -0
  53. {pinky_core-0.1.0.dev5 → pinky_core-0.1.0.dev6}/scripts/hooks/check_forbidden_keywords.py +0 -0
  54. {pinky_core-0.1.0.dev5 → pinky_core-0.1.0.dev6}/scripts/hooks/check_snowpark_api.py +0 -0
  55. {pinky_core-0.1.0.dev5 → pinky_core-0.1.0.dev6}/scripts/hooks/check_typed_api.py +0 -0
  56. {pinky_core-0.1.0.dev5 → pinky_core-0.1.0.dev6}/scripts/hooks/check_zws.py +0 -0
  57. {pinky_core-0.1.0.dev5 → pinky_core-0.1.0.dev6}/scripts/hooks/commit-msg +0 -0
  58. {pinky_core-0.1.0.dev5 → pinky_core-0.1.0.dev6}/scripts/hooks/docs_identity.json +0 -0
  59. {pinky_core-0.1.0.dev5 → pinky_core-0.1.0.dev6}/scripts/hooks/generate_ci_badges.py +0 -0
  60. {pinky_core-0.1.0.dev5 → pinky_core-0.1.0.dev6}/scripts/hooks/render_docs.py +0 -0
  61. {pinky_core-0.1.0.dev5 → pinky_core-0.1.0.dev6}/sql/deploy_udf_example.sql +0 -0
  62. {pinky_core-0.1.0.dev5 → pinky_core-0.1.0.dev6}/src/pinky_core/finance/__init__.py +0 -0
  63. {pinky_core-0.1.0.dev5 → pinky_core-0.1.0.dev6}/src/pinky_core/finance/fec.py +0 -0
  64. {pinky_core-0.1.0.dev5 → pinky_core-0.1.0.dev6}/src/pinky_core/fmt.py +0 -0
  65. {pinky_core-0.1.0.dev5 → pinky_core-0.1.0.dev6}/src/pinky_core/html.py +0 -0
  66. {pinky_core-0.1.0.dev5 → pinky_core-0.1.0.dev6}/src/pinky_core/lint.py +0 -0
  67. {pinky_core-0.1.0.dev5 → pinky_core-0.1.0.dev6}/src/pinky_core/schedule.py +0 -0
  68. {pinky_core-0.1.0.dev5 → pinky_core-0.1.0.dev6}/src/pinky_core/security.py +0 -0
  69. {pinky_core-0.1.0.dev5 → pinky_core-0.1.0.dev6}/src/pinky_core/sql.py +0 -0
  70. {pinky_core-0.1.0.dev5 → pinky_core-0.1.0.dev6}/src/pinky_core/validate.py +0 -0
  71. {pinky_core-0.1.0.dev5 → pinky_core-0.1.0.dev6}/src/pinky_core/warnings.py +0 -0
  72. {pinky_core-0.1.0.dev5 → pinky_core-0.1.0.dev6}/src/pinky_core/xml.py +0 -0
  73. {pinky_core-0.1.0.dev5 → pinky_core-0.1.0.dev6}/tests/__init__.py +0 -0
  74. {pinky_core-0.1.0.dev5 → pinky_core-0.1.0.dev6}/tests/fixtures/cats_domination.xml +0 -0
  75. {pinky_core-0.1.0.dev5 → pinky_core-0.1.0.dev6}/tests/fixtures/golden/dl_card_basic.html +0 -0
  76. {pinky_core-0.1.0.dev5 → pinky_core-0.1.0.dev6}/tests/fixtures/golden/dl_card_expiry.html +0 -0
  77. {pinky_core-0.1.0.dev5 → pinky_core-0.1.0.dev6}/tests/fixtures/golden/generate_html_basic.html +0 -0
  78. {pinky_core-0.1.0.dev5 → pinky_core-0.1.0.dev6}/tests/fixtures/golden/task_table_error.html +0 -0
  79. {pinky_core-0.1.0.dev5 → pinky_core-0.1.0.dev6}/tests/fixtures/golden/task_table_no_snowsight.html +0 -0
  80. {pinky_core-0.1.0.dev5 → pinky_core-0.1.0.dev6}/tests/fixtures/golden/task_table_snowsight.html +0 -0
  81. {pinky_core-0.1.0.dev5 → pinky_core-0.1.0.dev6}/tests/test_fec.py +0 -0
  82. {pinky_core-0.1.0.dev5 → pinky_core-0.1.0.dev6}/tests/test_files.py +0 -0
  83. {pinky_core-0.1.0.dev5 → pinky_core-0.1.0.dev6}/tests/test_fmt.py +0 -0
  84. {pinky_core-0.1.0.dev5 → pinky_core-0.1.0.dev6}/tests/test_html.py +0 -0
  85. {pinky_core-0.1.0.dev5 → pinky_core-0.1.0.dev6}/tests/test_lint.py +0 -0
  86. {pinky_core-0.1.0.dev5 → pinky_core-0.1.0.dev6}/tests/test_public_api.py +0 -0
  87. {pinky_core-0.1.0.dev5 → pinky_core-0.1.0.dev6}/tests/test_schedule.py +0 -0
  88. {pinky_core-0.1.0.dev5 → pinky_core-0.1.0.dev6}/tests/test_security.py +0 -0
  89. {pinky_core-0.1.0.dev5 → pinky_core-0.1.0.dev6}/tests/test_sql.py +0 -0
  90. {pinky_core-0.1.0.dev5 → pinky_core-0.1.0.dev6}/tests/test_validate.py +0 -0
  91. {pinky_core-0.1.0.dev5 → pinky_core-0.1.0.dev6}/tests/test_warnings.py +0 -0
  92. {pinky_core-0.1.0.dev5 → pinky_core-0.1.0.dev6}/tests/test_xml_xpath.py +0 -0
@@ -11,6 +11,12 @@ jobs:
11
11
  runs-on: ubuntu-latest
12
12
  env:
13
13
  FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
14
+ # Explicite, alors que le défaut d'org est déjà `read` : ce défaut est un réglage
15
+ # d'organisation, invisible du fichier et modifiable sans toucher aucun repo. Le déclarer ici
16
+ # rend le workflow immunisé si ce défaut repasse un jour en `write` (règle CodeQL
17
+ # actions/missing-workflow-permissions, 14 alertes sur l'org au scan 2026-07-25).
18
+ permissions:
19
+ contents: read
14
20
 
15
21
  steps:
16
22
  - uses: actions/checkout@v4
@@ -43,6 +49,8 @@ jobs:
43
49
  # ADR-0023 pilot gate: hermetic pre-commit hooks must run on the brother's
44
50
  # machine (Windows, prek only, no toolchain) — this is the real gate, not the Mac.
45
51
  runs-on: windows-latest
52
+ permissions:
53
+ contents: read
46
54
  strategy:
47
55
  matrix:
48
56
  shell: [cmd, pwsh]
@@ -52,6 +52,28 @@ repos:
52
52
  stages: [pre-commit]
53
53
  pass_filenames: false
54
54
 
55
+ # ── Test suite: the blocking quality gate ─────────────────────────────────
56
+ # Universal, NOT the `python` extra (moved 2026-07-25, same day it was introduced). The first
57
+ # placement rested on an unverified assumption — "the repos outside the python extra have no test
58
+ # suite". Measured: pinky-workspace, the engine itself, carries 25 test files / 264 tests, and
59
+ # pinky-studio carries tests with NO CI at all. Only assets/infra have no tests/ directory, and
60
+ # the hook's own `[ -d tests ]` guard makes those a real no-op.
61
+ #
62
+ # Before this hook, the only test execution at commit time was the one buried inside generate-docs
63
+ # feeding the coverage badge, whose failure printed a warning and returned 0 — a broken suite
64
+ # passed pre-commit. generate-docs keeps its own run for the badge data and stays non-blocking;
65
+ # this hook owns the verdict. See core/hooks/run-tests for why the two runs stay independent, and
66
+ # why an absent pytest warns instead of blocking.
67
+ - repo: local
68
+ hooks:
69
+ - id: run-tests
70
+ name: "[TESTS] Suite must pass"
71
+ entry: scripts/hooks/run-tests
72
+ language: script
73
+ files: ^(src|tests)/
74
+ pass_filenames: false
75
+ stages: [pre-commit]
76
+
55
77
  # ── Formatting: Markdown ──────────────────────────────────────────────────
56
78
  - repo: local
57
79
  hooks:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pinky-core
3
- Version: 0.1.0.dev5
3
+ Version: 0.1.0.dev6
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
@@ -159,7 +159,8 @@ pinky-core/
159
159
  │ │ ├── docs_identity.json
160
160
  │ │ ├── generate-docs
161
161
  │ │ ├── generate_ci_badges.py
162
- │ │ └── render_docs.py
162
+ │ │ ├── render_docs.py
163
+ │ │ └── run-tests
163
164
  │ └── deploy_validate_udfs.py
164
165
  ├── sql/
165
166
  │ └── deploy_udf_example.sql
@@ -99,7 +99,8 @@ pinky-core/
99
99
  │ │ ├── docs_identity.json
100
100
  │ │ ├── generate-docs
101
101
  │ │ ├── generate_ci_badges.py
102
- │ │ └── render_docs.py
102
+ │ │ ├── render_docs.py
103
+ │ │ └── run-tests
103
104
  │ └── deploy_validate_udfs.py
104
105
  ├── sql/
105
106
  │ └── deploy_udf_example.sql
@@ -1 +1 @@
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
+ <svg xmlns="http://www.w3.org/2000/svg" width="114" height="20" role="img" aria-label="coverage: 84.32%"><title>coverage: 84.32%</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.32%</text><text x="865" y="150" fill-opacity=".3" textLength="430">84.32%</text></g><text x="865" y="140" textLength="430">84.32%</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-weak-assertions · check-snowpark-api</title>
2
+ <title>generate-docs · run-tests · 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">11</text>
10
- <text x="65" y="14">11</text>
9
+ <text x="65" y="15" fill="#010101" fill-opacity=".3">12</text>
10
+ <text x="65" y="14">12</text>
11
11
  </g>
12
12
  </svg>
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "pinky-core"
7
- version = "0.1.0.dev5"
7
+ version = "0.1.0.dev6"
8
8
  description = "Shared Python utilities for Snowflake development"
9
9
  readme = "README.md"
10
10
  license = { file = "LICENSE" }
@@ -29,10 +29,11 @@ CHANGED files, so main() builds the covered set by scanning every `.py` under th
29
29
  of the passed paths, not just argv. check_file() stays per-file (the testable unit of the AST
30
30
  scan); main() is the layer that applies the repo-wide covered set.
31
31
 
32
- Generators are matched BY NAME only (assumed hole, decided 2026-07-24): a badly-named generator
33
- escapes the ratchet — it is not blocked, merely unguarded. Widening to "returns markup" was
34
- rejected: it reopens the false positives the audit eliminated. (`html.py`'s `dl_card`/`task_table`
35
- were the audit's found instancesrenamed `render_dl_card`/`render_task_table` to close the hole.)
32
+ Generators are matched BY NAME only (assumed hole, decided 2026-07-24): a function that builds
33
+ output but is named without a convention prefix/suffix escapes the ratchet — not blocked, merely
34
+ unguarded. The intended fix is to NAME it as a generator (tests.md § generator oracle requires it),
35
+ not to widen the matcherwidening to "returns markup" was rejected: it reopens the false positives
36
+ the audit eliminated.
36
37
 
37
38
  Usage (pre-commit local hook):
38
39
  entry: python scripts/hooks/check_weak_assertions.py
@@ -23,6 +23,10 @@
23
23
  # `coverage run -m pytest --no-cov` (not pytest-cov's own instrumentation) — editable-install
24
24
  # path setups (multiple repos' src/ on sys.path at once) can make pytest-cov silently measure
25
25
  # nothing ("module-not-measured") with no visible error; wrapping externally sidesteps that.
26
+ #
27
+ # Coverage badge reliability fix #2 (2026-07-25): that 2026-07-12 pass fixed the *data* (stale
28
+ # .coverage) but left the *tool* resolved off the bare PATH while $PY was resolved out of .venv —
29
+ # same class of bug, one line lower. See the comment at the genbadge block below.
26
30
  # =============================================================================
27
31
 
28
32
  # 0. Interpréteur — résolu explicitement, jamais `python` nu (2026-07-22). `python` n'existe ni dans
@@ -73,14 +77,28 @@ if [ "$SRC_STAGED" -gt 0 ]; then
73
77
  COV_LOG=$(mktemp)
74
78
  if $PY -m coverage run -m pytest -q --no-cov > "$COV_LOG" 2>&1; then
75
79
  $PY -m coverage xml -o .cache/coverage/coverage.xml > /dev/null 2>&1
76
- if command -v genbadge > /dev/null 2>&1; then
77
- genbadge coverage -i .cache/coverage/coverage.xml -o docs/reference/generated/coverage_badge.svg > /dev/null 2>&1
80
+ # genbadge est un console-script : le chercher dans le venv du projet AVANT le PATH, même
81
+ # ladder que $PY plus haut (2026-07-25). Le lookup PATH nu a gelé le badge de pinky-connect
82
+ # six semaines à 77.03% alors que genbadge était bien installé dans .venv — c'est le PATH
83
+ # qui ne le portait pas. Il a fini par « marcher » par accident, l'env uv d'un repo sans
84
+ # rapport (pinky-airflow) s'étant retrouvé en tête de PATH : le vrai chiffre était 18.28%.
85
+ if [ -x .venv/bin/genbadge ]; then GB=".venv/bin/genbadge" # POSIX
86
+ elif [ -x .venv/Scripts/genbadge.exe ]; then GB=".venv/Scripts/genbadge.exe" # Windows
87
+ elif command -v genbadge > /dev/null 2>&1; then GB="genbadge"
88
+ else GB=""
89
+ fi
90
+ if [ -n "$GB" ]; then
91
+ "$GB" coverage -i .cache/coverage/coverage.xml -o docs/reference/generated/coverage_badge.svg > /dev/null 2>&1
78
92
  git add docs/reference/generated/coverage_badge.svg 2>/dev/null
79
93
  else
80
- echo "⚠ coverage_badge.svg not regenerated: 'genbadge' not installedpip install -e '.[dev]'" >&2
94
+ echo "⚠ coverage_badge.svg not regenerated: 'genbadge' not found (.venv nor PATH) uv sync --group dev" >&2
81
95
  fi
82
96
  else
83
- echo "⚠ coverage_badge.svg not regenerated: test run failed, see $COV_LOG" >&2
97
+ # Depuis 2026-07-25 le verdict pass/fail appartient au hook run-tests (extra python), qui
98
+ # bloque le commit en amont — arriver ici veut dire que la suite passe en bare pytest mais
99
+ # casse sous instrumentation coverage, ou que run-tests n'est pas actif sur ce repo. On
100
+ # reste non bloquant (ce hook fait des docs, pas de la garde qualité) mais on le dit.
101
+ echo "⚠ coverage_badge.svg not regenerated: coverage run failed, see $COV_LOG" >&2
84
102
  fi
85
103
 
86
104
  $PY scripts/hooks/generate_ci_badges.py --noqa > /dev/null 2>&1
@@ -0,0 +1,67 @@
1
+ #!/bin/bash
2
+ # =============================================================================
3
+ # PRE-COMMIT HOOK — run-tests (2026-07-25)
4
+ # =============================================================================
5
+ # Distributed from pinky-workspace/core/hooks/ — do not edit in place. Declared in
6
+ # core/pre-commit/base.yaml (universal, `files: ^(src|tests)/`), NOT in the `python` extra.
7
+ #
8
+ # An earlier version of this header claimed the opposite ("extra `python` only … NOT in base.yaml —
9
+ # that one … would declare a pytest hook on studio/assets/infra/workspace, four repos with no test
10
+ # suite at all"). Both halves were false and the second one bit: **10 of the 13 repos carry a
11
+ # tests/ dir**, studio included — so the `[ -d tests ] || exit 0` guard below never fired for it,
12
+ # and every studio commit touching src/ or tests/ was blocked (2026-07-25). Keep this header in
13
+ # sync with base.yaml: a stale comment here is what justified the wrong placement in the first
14
+ # place (see core/rules/base/verify-diagnosis.md — a written cause is a hypothesis).
15
+ #
16
+ # Why this hook exists separately from generate-docs: until now the ONLY execution of the test suite
17
+ # at commit time was the one buried inside generate-docs to produce the coverage badge's data, and
18
+ # its failure printed a warning on stderr then returned 0 — a commit touching src/ with a broken
19
+ # suite passed pre-commit, the warning lost among ~20 other hooks' output. Separation of concerns:
20
+ # generate-docs does docs (non-blocking, cosmetic output), run-tests guards quality (blocking).
21
+ #
22
+ # The two runs stay INDEPENDENT rather than sharing coverage data through a file. Measured
23
+ # 2026-07-25: each suite takes 0-3s, so the double run costs ~3s at worst — far cheaper than the
24
+ # risk of an inter-hook artifact going silently stale. That exact coupling (trusting a pre-existing
25
+ # .coverage) is what froze pinky-connect's badge for six weeks; see the generate-docs header.
26
+ # =============================================================================
27
+
28
+ # Interpreter — resolved explicitly, never a bare `python`. Same ladder as generate-docs: the
29
+ # project venv first (the test suite needs the repo's own dependencies, which a system python3
30
+ # does not have), then the system fallbacks.
31
+ if [ -x .venv/bin/python ]; then PY=".venv/bin/python" # POSIX
32
+ elif [ -x .venv/Scripts/python.exe ]; then PY=".venv/Scripts/python.exe" # Windows
33
+ elif command -v python3 > /dev/null 2>&1; then PY="python3"
34
+ elif command -v py > /dev/null 2>&1; then PY="py -3"
35
+ else
36
+ echo "⚠ run-tests: aucun interpréteur Python (.venv / python3 / py) — tests non exécutés" >&2
37
+ exit 0
38
+ fi
39
+
40
+ # Pas de suite de tests → rien à garder. Cible « template light » du chantier 0001 : un repo cloné
41
+ # sans tests ne doit pas voir ses commits bloqués par un hook qui n'a rien à exécuter.
42
+ [ -d tests ] || exit 0
43
+
44
+ # pytest indisponible → on ne bloque pas. Ce garde devient indispensable depuis que le hook est
45
+ # déclaré dans base.yaml (2026-07-25) et non plus dans le seul extra `python` : un repo qui a un
46
+ # dossier tests/ mais pas les extras dev verrait `$PY -m pytest` sortir non-zéro (« No module named
47
+ # pytest ») et se retrouverait avec TOUS ses commits bloqués. Même posture permissive que le
48
+ # fallback genbadge de generate-docs — un outil de dev absent est une machine mal équipée, pas un
49
+ # test qui échoue.
50
+ if ! $PY -c "import pytest" > /dev/null 2>&1; then
51
+ echo "⚠ run-tests: pytest non installé — tests non exécutés" >&2
52
+ exit 0
53
+ fi
54
+
55
+ # --no-cov : l'instrumentation coverage est le travail de generate-docs, pas le nôtre — ici on veut
56
+ # le verdict pass/fail le plus rapide possible.
57
+ # Pas de `-p no:cacheprovider` : les repos configurent déjà `cache_dir = ".cache/pytest"` en
58
+ # pyproject, donc rien ne salit l'arbre — et désactiver le plugin rend ce réglage inconnu, ce qui
59
+ # fait cracher un PytestConfigWarning à chaque commit (mesuré sur pinky-tools, 2026-07-25).
60
+ if ! $PY -m pytest -q --no-cov; then
61
+ echo "" >&2
62
+ echo "✖ run-tests: la suite de tests échoue — commit bloqué." >&2
63
+ echo " Corrige les tests, ou --no-verify si le WIP est délibéré." >&2
64
+ exit 1
65
+ fi
66
+
67
+ exit 0
@@ -0,0 +1,132 @@
1
+ """
2
+ pinky_core
3
+ =============
4
+ Core Python utilities for Snowflake development.
5
+
6
+ Modules
7
+ -------
8
+ files File pattern normalization for S3 and MFT path analysis.
9
+ finance French accounting file formats (FEC parser).
10
+ fmt Pure formatting helpers (text, dates, numbers, FR addresses).
11
+ html HTML templating engine (emails, reports, mini-sites) — Outlook-safe.
12
+ lint Delegated tooling for pinky-provider's SQL/secret pre-flight — accessed
13
+ via ``pinky_core.lint`` directly, never top-level.
14
+ schedule Business-day calendar (FR) and cron utilities for SPs and DAGs.
15
+ security Path and identifier sanitisation for safe dynamic SQL.
16
+ sql SQL parsing helpers powered by sqlglot (Snowflake dialect).
17
+ validate French/international data validation (email, phone, SIRET, NIR, IBAN/BIC).
18
+ warnings PinkyDeprecationWarning — deprecated pinky APIs and UDFs.
19
+ xml XML utilities for Snowflake VARIANT — UDF handlers and helpers.
20
+
21
+ No Snowflake connection required — importable in any context (CI, local, UDF).
22
+ Optional deps for validate: email-validator, phonenumbers, python-stdnum.
23
+ Optional deps for address: usaddress.
24
+ Optional deps for schedule: croniter (next_schedule, is_in_schedule).
25
+ """
26
+
27
+ from __future__ import annotations
28
+
29
+ from typing import Any
30
+
31
+ # name → module. Les imports sont RÉSOLUS À LA DEMANDE (PEP 562) : un `from pinky_core.security
32
+ # import validate_identifier` exécutait tout ce fichier, donc `pinky_core.sql` → **sqlglot**, et
33
+ # une SP Snowflake qui ne déclare pas ce paquet tombait à l'exécution. Le coût était payé par
34
+ # TOUS les consommateurs — un handler qui veut une fonction de sanitisation n'a pas à embarquer
35
+ # un parseur SQL. Garder ce fichier paresseux.
36
+ _EXPORTS: dict[str, str] = {
37
+ "AddressComponents": "pinky_core.fmt",
38
+ "AfnorAddress": "pinky_core.fmt",
39
+ "EmailLang": "pinky_core.html",
40
+ "EmailResult": "pinky_core.validate",
41
+ "EmailTheme": "pinky_core.html",
42
+ "FecAmountSchema": "pinky_core.finance.fec",
43
+ "FecBalanceError": "pinky_core.finance.fec",
44
+ "FecEntry": "pinky_core.finance.fec",
45
+ "FecParseResult": "pinky_core.finance.fec",
46
+ "FecRowError": "pinky_core.finance.fec",
47
+ "FecSens": "pinky_core.finance.fec",
48
+ "FileDetection": "pinky_core.files",
49
+ "FrIdResult": "pinky_core.validate",
50
+ "IbanBicResult": "pinky_core.validate",
51
+ "NirResult": "pinky_core.validate",
52
+ "NumType": "pinky_core.fmt",
53
+ "PhoneResult": "pinky_core.validate",
54
+ "PinkyDeprecationWarning": "pinky_core.warnings",
55
+ "TaskRow": "pinky_core.html",
56
+ "VatResult": "pinky_core.validate",
57
+ "business_days_between": "pinky_core.schedule",
58
+ "check_fec_balance": "pinky_core.finance.fec",
59
+ "cron_to_human": "pinky_core.schedule",
60
+ "days_until_end_of_month": "pinky_core.schedule",
61
+ "deduplicate_headers": "pinky_core.fmt",
62
+ "detect_file_pattern": "pinky_core.files",
63
+ "extract_table_refs": "pinky_core.sql",
64
+ "first_business_day_of_month": "pinky_core.schedule",
65
+ "fiscal_quarter": "pinky_core.schedule",
66
+ "format_boolean": "pinky_core.fmt",
67
+ "format_date": "pinky_core.fmt",
68
+ "format_datetime": "pinky_core.fmt",
69
+ "format_duration": "pinky_core.fmt",
70
+ "format_fraction": "pinky_core.fmt",
71
+ "format_number": "pinky_core.fmt",
72
+ "format_period": "pinky_core.fmt",
73
+ "format_stars": "pinky_core.fmt",
74
+ "format_trend": "pinky_core.fmt",
75
+ "generate_html": "pinky_core.html",
76
+ "is_business_day": "pinky_core.schedule",
77
+ "is_eve_of_holiday": "pinky_core.schedule",
78
+ "is_even_week": "pinky_core.schedule",
79
+ "is_in_schedule": "pinky_core.schedule",
80
+ "is_odd_week": "pinky_core.schedule",
81
+ "iso3_to_iso2": "pinky_core.fmt",
82
+ "last_business_day_of_month": "pinky_core.schedule",
83
+ "next_business_day": "pinky_core.schedule",
84
+ "next_schedule": "pinky_core.schedule",
85
+ "normalize_city_fr": "pinky_core.fmt",
86
+ "normalize_text": "pinky_core.fmt",
87
+ "nth_business_day_of_month": "pinky_core.schedule",
88
+ "parse_address": "pinky_core.fmt",
89
+ "parse_fec": "pinky_core.finance.fec",
90
+ "period_to_date": "pinky_core.fmt",
91
+ "public_holidays_fr": "pinky_core.schedule",
92
+ "render_dl_card": "pinky_core.html",
93
+ "render_task_table": "pinky_core.html",
94
+ "resolve_query": "pinky_core.sql",
95
+ "safe_filepath": "pinky_core.security",
96
+ "slugify": "pinky_core.fmt",
97
+ "split_sql_statements": "pinky_core.sql",
98
+ "standardize_email": "pinky_core.validate",
99
+ "standardize_phone": "pinky_core.validate",
100
+ "statement_kind": "pinky_core.sql",
101
+ "to_camel_case": "pinky_core.fmt",
102
+ "to_upper_snake_case": "pinky_core.fmt",
103
+ "validate_iban_bic": "pinky_core.validate",
104
+ "validate_identifier": "pinky_core.security",
105
+ "validate_nir": "pinky_core.validate",
106
+ "validate_siret": "pinky_core.validate",
107
+ "validate_vat_eu": "pinky_core.validate",
108
+ "xml_path": "pinky_core.xml",
109
+ "xml_path_str": "pinky_core.xml",
110
+ }
111
+
112
+ __all__ = sorted(_EXPORTS)
113
+
114
+
115
+ def __getattr__(name: str) -> Any:
116
+ """Import an export on first access (PEP 562).
117
+
118
+ Args:
119
+ name: The attribute being looked up.
120
+
121
+ Returns:
122
+ The requested object.
123
+
124
+ Raises:
125
+ AttributeError: If the name is not an export of this package.
126
+ """
127
+ module_path = _EXPORTS.get(name)
128
+ if module_path is None:
129
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
130
+ from importlib import import_module
131
+
132
+ return getattr(import_module(module_path), name)
@@ -61,7 +61,7 @@ def detect_file_pattern(
61
61
  """Normalize a filename to a canonical pattern by replacing variable segments.
62
62
 
63
63
  Replaces dates, timestamps, UUIDs, ISO country codes, structured business
64
- identifiers (SIRET, SIREN, IBAN, BIC, VAT), and generic numeric IDs with
64
+ identifiers (SIRET, SIREN, IBAN, VAT), and generic numeric IDs with
65
65
  named placeholders — enabling grouping of structurally similar filenames
66
66
  regardless of the specific value they carry.
67
67
 
@@ -1072,7 +1072,7 @@ wheels = [
1072
1072
 
1073
1073
  [[package]]
1074
1074
  name = "pinky-core"
1075
- version = "0.1.0.dev5"
1075
+ version = "0.1.0.dev6"
1076
1076
  source = { editable = "." }
1077
1077
  dependencies = [
1078
1078
  { name = "pydantic" },
@@ -1,221 +0,0 @@
1
- """
2
- pinky_core
3
- =============
4
- Core Python utilities for Snowflake development.
5
-
6
- Modules
7
- -------
8
- files File pattern normalization for S3 and MFT path analysis.
9
- finance French accounting file formats (FEC parser).
10
- fmt Pure formatting helpers (text, dates, numbers, FR addresses).
11
- html HTML templating engine (emails, reports, mini-sites) — Outlook-safe.
12
- lint Delegated tooling for pinky-provider's SQL/secret pre-flight — accessed
13
- via ``pinky_core.lint`` directly, never top-level.
14
- schedule Business-day calendar (FR) and cron utilities for SPs and DAGs.
15
- security Path and identifier sanitisation for safe dynamic SQL.
16
- sql SQL parsing helpers powered by sqlglot (Snowflake dialect).
17
- validate French/international data validation (email, phone, SIRET, NIR, IBAN/BIC).
18
- warnings PinkyDeprecationWarning — deprecated pinky APIs and UDFs.
19
- xml XML utilities for Snowflake VARIANT — UDF handlers and helpers.
20
-
21
- No Snowflake connection required — importable in any context (CI, local, UDF).
22
- Optional deps for validate: email-validator, phonenumbers, python-stdnum.
23
- Optional deps for address: usaddress.
24
- Optional deps for schedule: croniter (next_schedule, is_in_schedule).
25
- """
26
-
27
- from __future__ import annotations
28
-
29
- # files — pure file pattern normalization, always available
30
- from pinky_core.files import (
31
- FileDetection,
32
- detect_file_pattern,
33
- )
34
-
35
- # finance — pure FEC parsing, always available
36
- from pinky_core.finance.fec import (
37
- FecAmountSchema,
38
- FecBalanceError,
39
- FecEntry,
40
- FecParseResult,
41
- FecRowError,
42
- FecSens,
43
- check_fec_balance,
44
- parse_fec,
45
- )
46
-
47
- # fmt — pure formatting, always available
48
- from pinky_core.fmt import (
49
- AddressComponents,
50
- AfnorAddress,
51
- NumType,
52
- deduplicate_headers,
53
- format_boolean,
54
- format_date,
55
- format_datetime,
56
- format_duration,
57
- format_fraction,
58
- format_number,
59
- format_period,
60
- format_stars,
61
- format_trend,
62
- iso3_to_iso2,
63
- normalize_city_fr,
64
- normalize_text,
65
- parse_address,
66
- period_to_date,
67
- slugify,
68
- to_camel_case,
69
- to_upper_snake_case,
70
- )
71
-
72
- # html — pure HTML templating, always available
73
- from pinky_core.html import (
74
- EmailLang,
75
- EmailTheme,
76
- TaskRow,
77
- generate_html,
78
- render_dl_card,
79
- render_task_table,
80
- )
81
-
82
- # schedule — pure calendar + cron, always available (croniter optional)
83
- from pinky_core.schedule import (
84
- business_days_between,
85
- cron_to_human,
86
- days_until_end_of_month,
87
- first_business_day_of_month,
88
- fiscal_quarter,
89
- is_business_day,
90
- is_eve_of_holiday,
91
- is_even_week,
92
- is_in_schedule,
93
- is_odd_week,
94
- last_business_day_of_month,
95
- next_business_day,
96
- next_schedule,
97
- nth_business_day_of_month,
98
- public_holidays_fr,
99
- )
100
-
101
- # security — pure sanitisation, always available
102
- from pinky_core.security import (
103
- safe_filepath,
104
- validate_identifier,
105
- )
106
-
107
- # sql — pure SQL parsing, always available
108
- from pinky_core.sql import (
109
- extract_table_refs,
110
- resolve_query,
111
- split_sql_statements,
112
- statement_kind,
113
- )
114
-
115
- # validate — lazy optional deps (email-validator, phonenumbers, python-stdnum)
116
- from pinky_core.validate import (
117
- EmailResult,
118
- FrIdResult,
119
- IbanBicResult,
120
- NirResult,
121
- PhoneResult,
122
- VatResult,
123
- standardize_email,
124
- standardize_phone,
125
- validate_iban_bic,
126
- validate_nir,
127
- validate_siret,
128
- validate_vat_eu,
129
- )
130
-
131
- # warnings — pure deprecation warning, always available
132
- from pinky_core.warnings import PinkyDeprecationWarning
133
-
134
- # xml — XML/VARIANT navigation UDF handlers, always available (lxml optional for xml_path_str)
135
- from pinky_core.xml import xml_path, xml_path_str
136
-
137
- __all__ = [
138
- # finance
139
- "FecAmountSchema",
140
- "FecBalanceError",
141
- "FecEntry",
142
- "FecParseResult",
143
- "FecRowError",
144
- "FecSens",
145
- "check_fec_balance",
146
- "parse_fec",
147
- # files
148
- "FileDetection",
149
- "detect_file_pattern",
150
- # fmt
151
- "AddressComponents",
152
- "AfnorAddress",
153
- "NumType",
154
- "deduplicate_headers",
155
- "iso3_to_iso2",
156
- "normalize_city_fr",
157
- "normalize_text",
158
- "format_boolean",
159
- "format_date",
160
- "format_datetime",
161
- "format_duration",
162
- "format_fraction",
163
- "format_number",
164
- "format_period",
165
- "format_stars",
166
- "format_trend",
167
- "parse_address",
168
- "period_to_date",
169
- "slugify",
170
- "to_camel_case",
171
- "to_upper_snake_case",
172
- # html
173
- "EmailLang",
174
- "EmailTheme",
175
- "TaskRow",
176
- "generate_html",
177
- "render_dl_card",
178
- "render_task_table",
179
- # schedule
180
- "public_holidays_fr",
181
- "is_business_day",
182
- "next_business_day",
183
- "business_days_between",
184
- "is_eve_of_holiday",
185
- "last_business_day_of_month",
186
- "first_business_day_of_month",
187
- "nth_business_day_of_month",
188
- "is_even_week",
189
- "is_odd_week",
190
- "fiscal_quarter",
191
- "days_until_end_of_month",
192
- "cron_to_human",
193
- "next_schedule",
194
- "is_in_schedule",
195
- # security
196
- "safe_filepath",
197
- "validate_identifier",
198
- # sql
199
- "extract_table_refs",
200
- "resolve_query",
201
- "split_sql_statements",
202
- "statement_kind",
203
- # xml
204
- "xml_path",
205
- "xml_path_str",
206
- # validate
207
- "EmailResult",
208
- "FrIdResult",
209
- "IbanBicResult",
210
- "NirResult",
211
- "PhoneResult",
212
- "standardize_email",
213
- "standardize_phone",
214
- "VatResult",
215
- "validate_iban_bic",
216
- "validate_nir",
217
- "validate_siret",
218
- "validate_vat_eu",
219
- # warnings
220
- "PinkyDeprecationWarning",
221
- ]
File without changes