pinky-core 0.1.0.dev1__py3-none-any.whl
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.
- pinky_core/__init__.py +149 -0
- pinky_core/files.py +178 -0
- pinky_core/fmt.py +729 -0
- pinky_core/schedule.py +230 -0
- pinky_core/security.py +52 -0
- pinky_core/sql.py +83 -0
- pinky_core/sql_custom.py +78 -0
- pinky_core/validate.py +575 -0
- pinky_core/xml.py +451 -0
- pinky_core-0.1.0.dev1.dist-info/METADATA +87 -0
- pinky_core-0.1.0.dev1.dist-info/RECORD +13 -0
- pinky_core-0.1.0.dev1.dist-info/WHEEL +4 -0
- pinky_core-0.1.0.dev1.dist-info/licenses/LICENSE +21 -0
pinky_core/__init__.py
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
"""
|
|
2
|
+
pinky_core
|
|
3
|
+
=============
|
|
4
|
+
Core Python utilities for Snowflake development.
|
|
5
|
+
|
|
6
|
+
Modules
|
|
7
|
+
-------
|
|
8
|
+
fmt Pure formatting helpers (text, dates, numbers, FR addresses).
|
|
9
|
+
files File pattern normalization for S3 and MFT path analysis.
|
|
10
|
+
schedule Business-day calendar (FR) and cron utilities for SPs and DAGs.
|
|
11
|
+
security Path and identifier sanitisation for safe dynamic SQL.
|
|
12
|
+
sql SQL parsing helpers powered by sqlglot (Snowflake dialect).
|
|
13
|
+
validate French/international data validation (email, phone, SIRET, NIR, IBAN/BIC).
|
|
14
|
+
xml XML utilities for Snowflake VARIANT — UDF handlers and helpers.
|
|
15
|
+
|
|
16
|
+
No Snowflake connection required — importable in any context (CI, local, UDF).
|
|
17
|
+
Optional deps for validate: email-validator, phonenumbers, python-stdnum.
|
|
18
|
+
Optional deps for address: usaddress.
|
|
19
|
+
Optional deps for schedule: croniter (next_schedule, is_in_schedule).
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
# files — pure file pattern normalization, always available
|
|
25
|
+
from pinky_core.files import (
|
|
26
|
+
FileDetection,
|
|
27
|
+
detect_file_pattern,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
# fmt — pure formatting, always available
|
|
31
|
+
from pinky_core.fmt import (
|
|
32
|
+
AddressComponents,
|
|
33
|
+
AfnorAddress,
|
|
34
|
+
deduplicate_headers,
|
|
35
|
+
format_boolean,
|
|
36
|
+
format_date,
|
|
37
|
+
format_datetime,
|
|
38
|
+
format_duration,
|
|
39
|
+
format_fraction,
|
|
40
|
+
format_number,
|
|
41
|
+
format_period,
|
|
42
|
+
format_stars,
|
|
43
|
+
format_trend,
|
|
44
|
+
iso3_to_iso2,
|
|
45
|
+
normalize_city_fr,
|
|
46
|
+
normalize_text,
|
|
47
|
+
parse_address,
|
|
48
|
+
period_to_date,
|
|
49
|
+
to_camel_case,
|
|
50
|
+
to_upper_snake_case,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
# schedule — pure calendar + cron, always available (croniter optional)
|
|
54
|
+
from pinky_core.schedule import (
|
|
55
|
+
business_days_between,
|
|
56
|
+
cron_to_human,
|
|
57
|
+
is_business_day,
|
|
58
|
+
is_in_schedule,
|
|
59
|
+
next_business_day,
|
|
60
|
+
next_schedule,
|
|
61
|
+
public_holidays_fr,
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
# security — pure sanitisation, always available
|
|
65
|
+
from pinky_core.security import (
|
|
66
|
+
safe_filepath,
|
|
67
|
+
validate_identifier,
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
# sql — pure SQL parsing, always available
|
|
71
|
+
from pinky_core.sql import (
|
|
72
|
+
is_select,
|
|
73
|
+
split_sql_statements,
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
# validate — lazy optional deps (email-validator, phonenumbers, python-stdnum)
|
|
77
|
+
from pinky_core.validate import (
|
|
78
|
+
EmailResult,
|
|
79
|
+
FrIdResult,
|
|
80
|
+
IbanBicResult,
|
|
81
|
+
NirResult,
|
|
82
|
+
PhoneResult,
|
|
83
|
+
VatResult,
|
|
84
|
+
standardize_email,
|
|
85
|
+
standardize_phone,
|
|
86
|
+
validate_iban_bic,
|
|
87
|
+
validate_nir,
|
|
88
|
+
validate_siret,
|
|
89
|
+
validate_vat_eu,
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
# xml — XML/VARIANT navigation UDF handlers, always available (lxml optional for xml_path_str)
|
|
93
|
+
from pinky_core.xml import xml_path, xml_path_str
|
|
94
|
+
|
|
95
|
+
__all__ = [
|
|
96
|
+
# files
|
|
97
|
+
"FileDetection",
|
|
98
|
+
"detect_file_pattern",
|
|
99
|
+
# fmt
|
|
100
|
+
"AddressComponents",
|
|
101
|
+
"AfnorAddress",
|
|
102
|
+
"deduplicate_headers",
|
|
103
|
+
"iso3_to_iso2",
|
|
104
|
+
"normalize_city_fr",
|
|
105
|
+
"normalize_text",
|
|
106
|
+
"format_boolean",
|
|
107
|
+
"format_date",
|
|
108
|
+
"format_datetime",
|
|
109
|
+
"format_duration",
|
|
110
|
+
"format_fraction",
|
|
111
|
+
"format_number",
|
|
112
|
+
"format_period",
|
|
113
|
+
"format_stars",
|
|
114
|
+
"format_trend",
|
|
115
|
+
"parse_address",
|
|
116
|
+
"period_to_date",
|
|
117
|
+
"to_camel_case",
|
|
118
|
+
"to_upper_snake_case",
|
|
119
|
+
# schedule
|
|
120
|
+
"public_holidays_fr",
|
|
121
|
+
"is_business_day",
|
|
122
|
+
"next_business_day",
|
|
123
|
+
"business_days_between",
|
|
124
|
+
"cron_to_human",
|
|
125
|
+
"next_schedule",
|
|
126
|
+
"is_in_schedule",
|
|
127
|
+
# security
|
|
128
|
+
"safe_filepath",
|
|
129
|
+
"validate_identifier",
|
|
130
|
+
# sql
|
|
131
|
+
"is_select",
|
|
132
|
+
"split_sql_statements",
|
|
133
|
+
# xml
|
|
134
|
+
"xml_path",
|
|
135
|
+
"xml_path_str",
|
|
136
|
+
# validate
|
|
137
|
+
"EmailResult",
|
|
138
|
+
"FrIdResult",
|
|
139
|
+
"IbanBicResult",
|
|
140
|
+
"NirResult",
|
|
141
|
+
"PhoneResult",
|
|
142
|
+
"standardize_email",
|
|
143
|
+
"standardize_phone",
|
|
144
|
+
"VatResult",
|
|
145
|
+
"validate_iban_bic",
|
|
146
|
+
"validate_nir",
|
|
147
|
+
"validate_siret",
|
|
148
|
+
"validate_vat_eu",
|
|
149
|
+
]
|
pinky_core/files.py
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
"""File pattern normalization utilities for S3 and MFT path analysis."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import re
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
|
|
9
|
+
# ISO 3166-1 alpha-2 codes (pipe-separated, used in regex alternation)
|
|
10
|
+
_ISO2 = (
|
|
11
|
+
"AD|AE|AF|AG|AI|AL|AM|AO|AQ|AR|AS|AT|AU|AW|AX|AZ|BA|BB|BD|BE|BF|BG|BH|BI|BJ|BL|"
|
|
12
|
+
"BM|BN|BO|BQ|BR|BS|BT|BV|BW|BY|BZ|CA|CC|CD|CF|CG|CH|CI|CK|CL|CM|CN|CO|CR|CU|CV|"
|
|
13
|
+
"CW|CX|CY|CZ|DE|DJ|DK|DM|DO|DZ|EC|EE|EG|EH|ER|ES|ET|FI|FJ|FK|FM|FO|FR|GA|GB|GD|"
|
|
14
|
+
"GE|GF|GG|GH|GI|GL|GM|GN|GP|GQ|GR|GS|GT|GU|GW|GY|HK|HM|HN|HR|HT|HU|ID|IE|IL|IM|"
|
|
15
|
+
"IN|IO|IQ|IR|IS|IT|JE|JM|JO|JP|KE|KG|KH|KI|KM|KN|KP|KR|KW|KY|KZ|LA|LB|LC|LI|LK|"
|
|
16
|
+
"LR|LS|LT|LU|LV|LY|MA|MC|MD|ME|MF|MG|MH|MK|ML|MM|MN|MO|MP|MQ|MR|MS|MT|MU|MV|MW|"
|
|
17
|
+
"MX|MY|MZ|NA|NC|NE|NF|NG|NI|NL|NO|NP|NR|NU|NZ|OM|PA|PE|PF|PG|PH|PK|PL|PM|PN|PR|"
|
|
18
|
+
"PS|PT|PW|PY|QA|RE|RO|RS|RU|RW|SA|SB|SC|SD|SE|SG|SH|SI|SJ|SK|SL|SM|SN|SO|SR|SS|"
|
|
19
|
+
"ST|SV|SX|SY|SZ|TC|TD|TF|TG|TH|TJ|TK|TL|TM|TN|TO|TR|TT|TV|TW|TZ|UA|UG|UM|US|UY|"
|
|
20
|
+
"UZ|VA|VC|VE|VG|VI|VN|VU|WF|WS|YE|YT|ZA|ZM|ZW|UK"
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
# ISO 3166-1 alpha-3 codes (pipe-separated, used in regex alternation)
|
|
24
|
+
_ISO3 = (
|
|
25
|
+
"ABW|AFG|AGO|AIA|ALA|ALB|AND|ARE|ARG|ARM|ASM|ATA|ATF|ATG|AUS|AUT|AZE|BDI|BEL|BEN|"
|
|
26
|
+
"BES|BFA|BGD|BGR|BHR|BHS|BIH|BLM|BLR|BLZ|BMU|BOL|BRA|BRB|BRN|BTN|BVT|BWA|CAF|CAN|"
|
|
27
|
+
"CCK|CHE|CHL|CHN|CIV|CMR|COD|COG|COK|COL|COM|CPV|CRI|CUB|CUW|CXR|CYM|CYP|CZE|DEU|"
|
|
28
|
+
"DJI|DMA|DNK|DOM|DZA|ECU|EGY|ERI|ESH|ESP|EST|ETH|FIN|FJI|FLK|FRA|FRO|FSM|GAB|GBR|"
|
|
29
|
+
"GEO|GGY|GHA|GIB|GIN|GLP|GMB|GNB|GNQ|GRC|GRD|GRL|GTM|GUF|GUM|GUY|HKG|HMD|HND|HRV|"
|
|
30
|
+
"HTI|HUN|IDN|IMN|IND|IOT|IRL|IRN|IRQ|ISL|ISR|ITA|JAM|JEY|JOR|JPN|KAZ|KEN|KGZ|KHM|"
|
|
31
|
+
"KIR|KNA|KOR|KWT|LAO|LBN|LBR|LBY|LCA|LIE|LKA|LSO|LTU|LUX|LVA|MAC|MAF|MAR|MCO|MDA|"
|
|
32
|
+
"MDG|MDV|MEX|MHL|MKD|MLI|MLT|MMR|MNE|MNG|MNP|MOZ|MRT|MSR|MTQ|MUS|MWI|MYS|MYT|NAM|"
|
|
33
|
+
"NCL|NER|NFK|NGA|NIC|NIU|NLD|NOR|NPL|NRU|NZL|OMN|PAK|PAN|PCN|PER|PHL|PLW|PNG|POL|"
|
|
34
|
+
"PRI|PRK|PRT|PRY|PSE|PYF|QAT|REU|ROU|RUS|RWA|SAU|SDN|SEN|SGP|SGS|SHN|SJM|SLB|SLE|"
|
|
35
|
+
"SLV|SMR|SOM|SPM|SRB|SSD|STP|SUR|SVK|SVN|SWE|SWZ|SXM|SYC|SYR|TCA|TCD|TGO|THA|TJK|"
|
|
36
|
+
"TKL|TKM|TLS|TON|TTO|TUN|TUR|TUV|TWN|TZA|UGA|UKR|UMI|URY|USA|UZB|VAT|VCT|VEN|VGB|"
|
|
37
|
+
"VIR|VNM|VUT|WLF|WSM|YEM|ZAF|ZMB|ZWE"
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass
|
|
42
|
+
class FileDetection:
|
|
43
|
+
"""A custom detection rule for :func:`detect_file_pattern`.
|
|
44
|
+
|
|
45
|
+
Attributes:
|
|
46
|
+
pattern: Regex applied to the working string (after prior substitutions).
|
|
47
|
+
replacement: Replacement string, e.g. ``"{COST_CENTER}"`` or ``"<SAP>"``.
|
|
48
|
+
substring: If given, the rule is skipped when this literal is absent from
|
|
49
|
+
the *original* basename — cheap fast-path for context-specific rules.
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
pattern: str
|
|
53
|
+
replacement: str
|
|
54
|
+
substring: str | None = None
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def detect_file_pattern(
|
|
58
|
+
filepath: str,
|
|
59
|
+
custom_detections: list[FileDetection] | None = None,
|
|
60
|
+
) -> str:
|
|
61
|
+
"""Normalize a filename to a canonical pattern by replacing variable segments.
|
|
62
|
+
|
|
63
|
+
Replaces dates, timestamps, UUIDs, ISO country codes, structured business
|
|
64
|
+
identifiers (SIRET, SIREN, IBAN, BIC, VAT), and generic numeric IDs with
|
|
65
|
+
named placeholders — enabling grouping of structurally similar filenames
|
|
66
|
+
regardless of the specific value they carry.
|
|
67
|
+
|
|
68
|
+
Detection is purely structural: no business-context hint is required.
|
|
69
|
+
Substitutions run most-specific first to avoid double-replacement.
|
|
70
|
+
|
|
71
|
+
Placeholder conventions:
|
|
72
|
+
|
|
73
|
+
- ``{...}`` — temporal or generic numeric variable (date, timestamp, UUID, ID)
|
|
74
|
+
- ``<...>`` — typed structured identifier (country code, SIRET, IBAN…)
|
|
75
|
+
|
|
76
|
+
Substitution order:
|
|
77
|
+
|
|
78
|
+
1. Timestamps (most digits, most specific)
|
|
79
|
+
2. UUIDs
|
|
80
|
+
3. Dates (delimited, then bare YYYYMMDD)
|
|
81
|
+
4. Year-month combinations
|
|
82
|
+
5. ISO country codes (alpha-3 before alpha-2)
|
|
83
|
+
6. Structured business identifiers (IBAN, SIRET, SIREN, VAT_FR)
|
|
84
|
+
7. Generic numeric segments (least specific)
|
|
85
|
+
|
|
86
|
+
Note: BIC/SWIFT codes (e.g. ``BNPAFRPP``) are structurally indistinguishable
|
|
87
|
+
from 8-letter uppercase words and cannot be detected reliably without a bank
|
|
88
|
+
directory. BIC detection is intentionally excluded.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
filepath: Full file path or bare filename to normalize. The basename
|
|
92
|
+
(last ``/``-separated segment) is extracted automatically before
|
|
93
|
+
substitution, so both ``"EMPLOYEES_20240115.csv"`` and
|
|
94
|
+
``"s3://bucket/feed/EMPLOYEES_20240115.csv"`` produce the same output.
|
|
95
|
+
custom_detections: Optional list of :class:`FileDetection` rules applied
|
|
96
|
+
*before* built-in substitutions. Each rule's ``substring`` filter is
|
|
97
|
+
tested against the **full** ``filepath`` (not just the basename) so
|
|
98
|
+
that directory-level context (e.g. ``"workday"`` in the S3 prefix) can
|
|
99
|
+
gate project-specific patterns.
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
Pattern string for the basename, with variable segments replaced by
|
|
103
|
+
placeholders.
|
|
104
|
+
|
|
105
|
+
Examples:
|
|
106
|
+
>>> detect_file_pattern("EMPLOYEES_20240115.csv")
|
|
107
|
+
'EMPLOYEES_{DATE}.csv'
|
|
108
|
+
>>> detect_file_pattern("s3://bucket/feed/EMPLOYEES_20240115.csv")
|
|
109
|
+
'EMPLOYEES_{DATE}.csv'
|
|
110
|
+
>>> detect_file_pattern("PAYROLL_55208131700027.csv")
|
|
111
|
+
'PAYROLL_{SIRET}.csv'
|
|
112
|
+
>>> detect_file_pattern("TRANSFER_FR7630004000031234567890143.csv")
|
|
113
|
+
'TRANSFER_{IBAN}.csv'
|
|
114
|
+
"""
|
|
115
|
+
basename = os.path.basename(filepath) if "/" in filepath else filepath
|
|
116
|
+
r = basename
|
|
117
|
+
|
|
118
|
+
# 0. Caller-supplied rules — run before all built-ins
|
|
119
|
+
# substring is tested against the original filepath so directory context is available
|
|
120
|
+
if custom_detections:
|
|
121
|
+
for rule in custom_detections:
|
|
122
|
+
if rule.substring is None or rule.substring in filepath:
|
|
123
|
+
r = re.sub(rule.pattern, rule.replacement, r)
|
|
124
|
+
|
|
125
|
+
# 1. Timestamps — anchored to avoid matching inside longer digit sequences
|
|
126
|
+
r = re.sub(r"(?<![0-9])[0-9]{8}[-._/:][0-9]{6}(?![0-9])", "{TIMESTAMP}", r)
|
|
127
|
+
r = re.sub(r"(?<![0-9])(19|20)[0-9]{12}(?![0-9])", "{TIMESTAMP_YYYYMMDDHHMMSS}", r)
|
|
128
|
+
r = re.sub(r"(?<![0-9])(19|20)[0-9]{10}(?![0-9])", "{TIMESTAMP_YYYYMMDDHHMM}", r)
|
|
129
|
+
|
|
130
|
+
# 2. UUIDs (self-anchoring via hyphens)
|
|
131
|
+
r = re.sub(
|
|
132
|
+
r"[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}",
|
|
133
|
+
"{UUID}",
|
|
134
|
+
r,
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
# 3. Dates — delimited formats before bare YYYYMMDD, all anchored
|
|
138
|
+
r = re.sub(r"(?<![0-9])[0-9]{4}[-._/][0-9]{2}[-._/][0-9]{2}(?![0-9])", "{DATE}", r)
|
|
139
|
+
r = re.sub(r"(?<![0-9])[0-9]{2}[-._/][0-9]{2}[-._/][0-9]{4}(?![0-9])", "{DATE}", r)
|
|
140
|
+
r = re.sub(r"(?<![0-9])(19|20)[0-9]{6}(?![0-9])", "{DATE}", r)
|
|
141
|
+
|
|
142
|
+
# 4. Year-month — delimited before bare YYYYMM, all anchored
|
|
143
|
+
r = re.sub(
|
|
144
|
+
r"(?<![0-9])(19|20)[0-9]{2}[-._/](0[1-9]|1[0-2])(?![0-9])", "{YEAR_MONTH}", r
|
|
145
|
+
)
|
|
146
|
+
r = re.sub(
|
|
147
|
+
r"(?<![0-9])(0[1-9]|1[0-2])[-._/](19|20)[0-9]{2}(?![0-9])", "{YEAR_MONTH}", r
|
|
148
|
+
)
|
|
149
|
+
r = re.sub(r"(?<![0-9])(19|20)[0-9]{2}(0[1-9]|1[0-2])(?![0-9])", "{YEAR_MONTH}", r)
|
|
150
|
+
r = re.sub(r"(?<![0-9])(0[1-9]|1[0-2])(19|20)[0-9]{2}(?![0-9])", "{YEAR_MONTH}", r)
|
|
151
|
+
|
|
152
|
+
# 5. ISO country codes — alpha-3 before alpha-2 (more specific first)
|
|
153
|
+
r = re.sub(f"_({_ISO3})_", "_<ISO3_COUNTRY>_", r)
|
|
154
|
+
r = re.sub(f"^({_ISO3})_", "<ISO3_COUNTRY>_", r)
|
|
155
|
+
r = re.sub(f"_({_ISO2})_", "_<ISO2_COUNTRY>_", r)
|
|
156
|
+
r = re.sub(f"^({_ISO2})_", "<ISO2_COUNTRY>_", r)
|
|
157
|
+
|
|
158
|
+
# 6. Structured business identifiers — context-free, anchored by non-alphanumeric boundaries
|
|
159
|
+
# IBAN: 2-letter country + 2-digit check + 11-30 alphanum (run before VAT_FR)
|
|
160
|
+
r = re.sub(r"(?<![A-Z0-9])[A-Z]{2}[0-9]{2}[A-Z0-9]{11,30}(?![A-Z0-9])", "{IBAN}", r)
|
|
161
|
+
# FR VAT number: FR + 2 alphanum + 9 digits (run after IBAN to avoid partial match)
|
|
162
|
+
r = re.sub(r"(?<![A-Z0-9])FR[A-Z0-9]{2}[0-9]{9}(?![A-Z0-9])", "{VAT_FR}", r)
|
|
163
|
+
# SIRET: exactly 14 digits — timestamps starting with 19/20 already consumed above
|
|
164
|
+
r = re.sub(r"(?<![0-9])[0-9]{14}(?![0-9])", "{SIRET}", r)
|
|
165
|
+
# SIREN: exactly 9 digits
|
|
166
|
+
r = re.sub(r"(?<![0-9])[0-9]{9}(?![0-9])", "{SIREN}", r)
|
|
167
|
+
|
|
168
|
+
# 7. Generic numeric segments (least specific, run last)
|
|
169
|
+
r = re.sub(r"_([0-9]{6,})_", "_{ID}_", r)
|
|
170
|
+
r = re.sub(r"_([0-9]+)_", "_{NUM}_", r)
|
|
171
|
+
r = re.sub(r"_([0-9]+)\.", "_{NUM}.", r)
|
|
172
|
+
r = re.sub(r"^([0-9]+)_", "{NUM}_", r)
|
|
173
|
+
# Numbers immediately after a closing placeholder brace
|
|
174
|
+
r = re.sub(r"\}([0-9]+)", "}{NUM}", r)
|
|
175
|
+
# Trailing number before extension (not preceded by > or })
|
|
176
|
+
r = re.sub(r"([^}>])([0-9]+)\.([a-z]+)$", r"\1{NUM}.\3", r)
|
|
177
|
+
|
|
178
|
+
return r
|