PySpark-Column-Selectors 0.1.3__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.
- PySparkSelectors/__init__.py +240 -0
- PySparkSelectors/__main__.py +4 -0
- PySparkSelectors/cli.py +18 -0
- PySparkSelectors/models.py +1564 -0
- PySparkSelectors/py.typed +1 -0
- PySparkSelectors/spark_overrides.py +677 -0
- PySparkSelectors/user_functions.py +745 -0
- PySparkSelectors/utils.py +298 -0
- pyspark_column_selectors-0.1.3.dist-info/METADATA +674 -0
- pyspark_column_selectors-0.1.3.dist-info/RECORD +13 -0
- pyspark_column_selectors-0.1.3.dist-info/WHEEL +4 -0
- pyspark_column_selectors-0.1.3.dist-info/entry_points.txt +2 -0
- pyspark_column_selectors-0.1.3.dist-info/licenses/LICENSE +279 -0
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
"""non-column selector imports"""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Callable, Sequence
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from pyspark.sql import DataFrame
|
|
7
|
+
from pyspark.sql import functions as F
|
|
8
|
+
from pyspark.sql.column import Column
|
|
9
|
+
|
|
10
|
+
""" enable support for many different types of pyspark environments"""
|
|
11
|
+
# if your environment isn't supported then you can add it to this section.
|
|
12
|
+
# Spark supports multiple DataFrame implementations. Keep every available runtime
|
|
13
|
+
# class in the patch set: public/classic DataFrame and Spark Connect DataFrame.
|
|
14
|
+
# using try except as to allow passthrough if environment doesn't support these.
|
|
15
|
+
try:
|
|
16
|
+
from pyspark.sql.classic.dataframe import DataFrame as _ClassicDataFrame
|
|
17
|
+
except ImportError:
|
|
18
|
+
_ClassicDataFrame = None
|
|
19
|
+
|
|
20
|
+
try:
|
|
21
|
+
from pyspark.sql.connect.dataframe import DataFrame as _ConnectDataFrame
|
|
22
|
+
except ImportError:
|
|
23
|
+
_ConnectDataFrame = None
|
|
24
|
+
|
|
25
|
+
_DATAFRAME_CLASSES = tuple(
|
|
26
|
+
dict.fromkeys(cls for cls in (DataFrame, _ClassicDataFrame, _ConnectDataFrame) if cls is not None)
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
try:
|
|
30
|
+
from pyspark.sql.connect.column import Column as _ConnectColumn
|
|
31
|
+
except ImportError:
|
|
32
|
+
_ConnectColumn = None
|
|
33
|
+
|
|
34
|
+
_COLUMN_CLASSES = tuple(dict.fromkeys(cls for cls in (Column, _ConnectColumn) if cls is not None))
|
|
35
|
+
|
|
36
|
+
# --------------------------------------------------
|
|
37
|
+
# return selector names
|
|
38
|
+
# create column names for functions that require columns to have them.
|
|
39
|
+
# --------------------------------------------------
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _resolve_selector_names(df: DataFrame, args: Sequence[Any]) -> list[Any]:
|
|
43
|
+
"""Turn any `BaseSelector` in `args` into the plain column names it matches.
|
|
44
|
+
|
|
45
|
+
Shared resolver #1 -- used by the fx overrides that only need bare names
|
|
46
|
+
(`groupBy`, `drop`), the same way `select_patched` originally resolved
|
|
47
|
+
selectors for `select`.
|
|
48
|
+
|
|
49
|
+
Parameters
|
|
50
|
+
----------
|
|
51
|
+
df : pyspark.sql.DataFrame
|
|
52
|
+
The dataframe to resolve any selector's matched column names against.
|
|
53
|
+
args : sequence of Any
|
|
54
|
+
The positional arguments originally passed to the overridden pyspark
|
|
55
|
+
method (e.g. `DataFrame.groupBy`'s `*cols`). Any `BaseSelector` is
|
|
56
|
+
expanded to the names it matches; anything else passes through
|
|
57
|
+
untouched.
|
|
58
|
+
|
|
59
|
+
Returns
|
|
60
|
+
-------
|
|
61
|
+
list of Any
|
|
62
|
+
`args`, with every `BaseSelector` expanded into its matched column
|
|
63
|
+
name strings, in order.
|
|
64
|
+
|
|
65
|
+
Examples
|
|
66
|
+
--------
|
|
67
|
+
>>> _resolve_selector_names(df, (by_dtype([T.StringType]),))
|
|
68
|
+
['string_col', 'data_column_6']
|
|
69
|
+
"""
|
|
70
|
+
from .models import BaseSelector
|
|
71
|
+
|
|
72
|
+
resolved = []
|
|
73
|
+
|
|
74
|
+
for a in args:
|
|
75
|
+
if isinstance(a, BaseSelector):
|
|
76
|
+
# a resolved name may legally contain a literal dot (see
|
|
77
|
+
# `_quote_identifier`) -- these names get handed straight to a
|
|
78
|
+
# real pyspark method expecting bare name strings (`groupBy`,
|
|
79
|
+
# `drop`), which -- same as calling that method directly with a
|
|
80
|
+
# dotted name -- would otherwise misparse an un-escaped dot as
|
|
81
|
+
# struct-path access. quote defensively here since these names
|
|
82
|
+
# came from our own selector resolution, not the caller's
|
|
83
|
+
# original input.
|
|
84
|
+
resolved.extend(_quote_identifier(name) for name in a.resolve(df))
|
|
85
|
+
else:
|
|
86
|
+
resolved.append(a)
|
|
87
|
+
|
|
88
|
+
return resolved
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
# --------------------------------------------------
|
|
92
|
+
# return selector expressions
|
|
93
|
+
# create column expressions for functions that require columns to have them.
|
|
94
|
+
# --------------------------------------------------
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _resolve_selector_exprs(df: DataFrame, args: Sequence[Any]) -> list[tuple[str | None, Any]]:
|
|
98
|
+
"""Turn any `BaseSelector` in `args` into `(name, Column expr)` pairs.
|
|
99
|
+
|
|
100
|
+
Shared resolver #2 -- applies whatever transforms are chained onto the
|
|
101
|
+
selector (``.sum()``, ``.cast(...)``, ``+1``, ``.desc()``, etc, all
|
|
102
|
+
already available via `SelectorcolumnOperations`). A selector with no
|
|
103
|
+
transforms resolves to plain `F.col(name)` expressions.
|
|
104
|
+
|
|
105
|
+
Parameters
|
|
106
|
+
----------
|
|
107
|
+
df : pyspark.sql.DataFrame
|
|
108
|
+
The dataframe to resolve any selector's matched columns against.
|
|
109
|
+
args : sequence of Any
|
|
110
|
+
The positional arguments originally passed to the overridden pyspark
|
|
111
|
+
method. Any `BaseSelector` is expanded to `(name, expr)` pairs for
|
|
112
|
+
every column it matches; anything else passes through as
|
|
113
|
+
``(None, value)``.
|
|
114
|
+
|
|
115
|
+
Returns
|
|
116
|
+
-------
|
|
117
|
+
list of tuple of (str or None, Any)
|
|
118
|
+
One `(name, expr)` pair per resolved column/argument, in order. When
|
|
119
|
+
a rename (`.prefix()`/`.suffix()`/`.map_alias()`) has been queued on
|
|
120
|
+
the selector (`self._name_transform`), `name` is the *new* name
|
|
121
|
+
computed by applying it to the originally-matched column name,
|
|
122
|
+
instead of the originally-matched name itself -- harmless when a
|
|
123
|
+
caller re-aliases to it (it's already the correct name), and
|
|
124
|
+
required by callers like `withColumns_patched` that use `name` as
|
|
125
|
+
the output column's real key.
|
|
126
|
+
|
|
127
|
+
Examples
|
|
128
|
+
--------
|
|
129
|
+
>>> _resolve_selector_exprs(df, (by_dtype([T.IntegerType]) + 1,))
|
|
130
|
+
[('integer_col', Column<'(integer_col + 1)'>)]
|
|
131
|
+
>>> _resolve_selector_exprs(df, (by_dtype([T.IntegerType]).prefix('int_'),))
|
|
132
|
+
[('int_integer_col', Column<'integer_col'>)]
|
|
133
|
+
"""
|
|
134
|
+
from .models import BaseSelector
|
|
135
|
+
|
|
136
|
+
pairs = []
|
|
137
|
+
|
|
138
|
+
for a in args:
|
|
139
|
+
if isinstance(a, BaseSelector):
|
|
140
|
+
names = a.resolve(df)
|
|
141
|
+
exprs = a.resolve_columns(df) if a.transforms else [_quoted_col(n) for n in names]
|
|
142
|
+
|
|
143
|
+
# a queued rename (.prefix()/.suffix()/.map_alias()) is pure name
|
|
144
|
+
# metadata -- apply it here to each originally-matched name to get
|
|
145
|
+
# the real output name, without ever having touched the Column
|
|
146
|
+
# expression itself (so it can't be corrupted by whatever value
|
|
147
|
+
# transform text, e.g. .cast(...)/arithmetic, is baked into `expr`).
|
|
148
|
+
if a._name_transform is not None:
|
|
149
|
+
pairs.extend((a._name_transform(name), expr) for name, expr in zip(names, exprs, strict=True))
|
|
150
|
+
else:
|
|
151
|
+
pairs.extend(zip(names, exprs, strict=True))
|
|
152
|
+
else:
|
|
153
|
+
pairs.append((None, a))
|
|
154
|
+
|
|
155
|
+
return pairs
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
# --------------------------------------------------------------------------------
|
|
159
|
+
# support for edge case column selection, including support for <.>, <`> in column names
|
|
160
|
+
# --------------------------------------------------------------------------------
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _quote_identifier(name: str) -> str:
|
|
164
|
+
"""Backtick-quote `name` if needed so pyspark treats it as one flat identifier.
|
|
165
|
+
|
|
166
|
+
Parameters
|
|
167
|
+
----------
|
|
168
|
+
name : str
|
|
169
|
+
A real, flat column name (as returned by `BaseSelector.resolve`),
|
|
170
|
+
which may legally contain a literal dot or backtick.
|
|
171
|
+
|
|
172
|
+
Returns
|
|
173
|
+
-------
|
|
174
|
+
str
|
|
175
|
+
`name` unchanged if it has no dot/backtick, otherwise the
|
|
176
|
+
backtick-quoted form (with any embedded backtick doubled, Spark
|
|
177
|
+
SQL's own backtick-escaping convention).
|
|
178
|
+
|
|
179
|
+
Examples
|
|
180
|
+
--------
|
|
181
|
+
>>> _quote_identifier('integer_col')
|
|
182
|
+
'integer_col'
|
|
183
|
+
>>> _quote_identifier('meta.source')
|
|
184
|
+
'`meta.source`'
|
|
185
|
+
"""
|
|
186
|
+
|
|
187
|
+
if "." in name or "`" in name:
|
|
188
|
+
escaped = name.replace("`", "``")
|
|
189
|
+
return f"`{escaped}`"
|
|
190
|
+
|
|
191
|
+
return name
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def _quoted_col(name: str) -> Column:
|
|
195
|
+
"""Build `F.col(name)`, backtick-quoting `name` if needed to keep it flat.
|
|
196
|
+
|
|
197
|
+
Parameters
|
|
198
|
+
----------
|
|
199
|
+
name : str
|
|
200
|
+
A real, flat column name (as returned by `BaseSelector.resolve`),
|
|
201
|
+
which may legally contain a literal dot or backtick.
|
|
202
|
+
|
|
203
|
+
Returns
|
|
204
|
+
-------
|
|
205
|
+
pyspark.sql.Column
|
|
206
|
+
`F.col` built from `_quote_identifier(name)`, so pyspark always
|
|
207
|
+
resolves it as one flat column name instead of a struct-path
|
|
208
|
+
reference.
|
|
209
|
+
|
|
210
|
+
Examples
|
|
211
|
+
--------
|
|
212
|
+
>>> _quoted_col('integer_col')
|
|
213
|
+
Column<'integer_col'>
|
|
214
|
+
>>> _quoted_col('meta.source')
|
|
215
|
+
Column<'meta.source'>
|
|
216
|
+
"""
|
|
217
|
+
|
|
218
|
+
return F.col(_quote_identifier(name))
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
# --------------------------------------------------
|
|
222
|
+
# allow for checking for columns across different types of pyspark dataframes
|
|
223
|
+
# spark can have multiple different types this handles the changes.
|
|
224
|
+
# --------------------------------------------------
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def _get_column_method(method_name: str) -> Callable[..., Any]:
|
|
228
|
+
"""Look up a `Column` instance method, checking classic then Spark Connect.
|
|
229
|
+
|
|
230
|
+
Used wherever a method is looked up purely for introspection (its
|
|
231
|
+
`__doc__`, or an existence/callability check) rather than to be bound and
|
|
232
|
+
called directly -- e.g. by `_make_column_only_wrapper`/
|
|
233
|
+
`_make_dispatching_column_wrapper` when building a chained selector
|
|
234
|
+
method. Falls back to Spark Connect's `Column` when the classic
|
|
235
|
+
`pyspark.sql.Column` doesn't define `method_name` at all, so a
|
|
236
|
+
Connect-only method still gets picked up instead of silently never
|
|
237
|
+
getting a selector-chainable wrapper.
|
|
238
|
+
|
|
239
|
+
Parameters
|
|
240
|
+
----------
|
|
241
|
+
method_name : str
|
|
242
|
+
Name of the `Column` instance method to look up.
|
|
243
|
+
|
|
244
|
+
Returns
|
|
245
|
+
-------
|
|
246
|
+
callable
|
|
247
|
+
The method, from whichever of classic/Connect `Column` actually
|
|
248
|
+
defines it (classic checked first).
|
|
249
|
+
|
|
250
|
+
Raises
|
|
251
|
+
------
|
|
252
|
+
AttributeError
|
|
253
|
+
If neither classic nor (when available) Spark Connect's `Column`
|
|
254
|
+
defines `method_name`.
|
|
255
|
+
"""
|
|
256
|
+
if hasattr(Column, method_name):
|
|
257
|
+
return getattr(Column, method_name)
|
|
258
|
+
return getattr(_ConnectColumn, method_name)
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
# --------------------------------------------------
|
|
262
|
+
# protect against re-overriding the same functions on packages initialization.
|
|
263
|
+
# --------------------------------------------------
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def _true_original(cls: type, method_name: str) -> Callable[..., Any]:
|
|
267
|
+
"""Re-run-safe replacement for ``getattr(cls, method_name)`` when capturing an original method.
|
|
268
|
+
|
|
269
|
+
Every override in this notebook does ``_original_x = <the real pyspark
|
|
270
|
+
method>`` once, then reassigns ``SomeClass.method = patched_version``.
|
|
271
|
+
That's fine the first time a cell runs -- but if the SAME cell gets
|
|
272
|
+
re-run (very normal during interactive experimentation),
|
|
273
|
+
``getattr(cls, method_name)`` on the second run fetches the
|
|
274
|
+
*already-patched* version instead of the real pyspark one, and the newly
|
|
275
|
+
defined patched function ends up calling itself forever the moment it's
|
|
276
|
+
invoked -- causing infinite recursion.
|
|
277
|
+
|
|
278
|
+
Parameters
|
|
279
|
+
----------
|
|
280
|
+
cls : type
|
|
281
|
+
The class the method lives on (e.g. `pyspark.sql.DataFrame`,
|
|
282
|
+
`pyspark.sql.group.GroupedData`).
|
|
283
|
+
method_name : str
|
|
284
|
+
The name of the method to fetch the true original implementation of.
|
|
285
|
+
|
|
286
|
+
Returns
|
|
287
|
+
-------
|
|
288
|
+
callable
|
|
289
|
+
The true, never-patched method. Every patched function this returns
|
|
290
|
+
the original for gets tagged with ``._cs_original`` pointing at the
|
|
291
|
+
true, never-patched method -- so on a re-run, this peels back
|
|
292
|
+
through any already-applied patch layer(s) and always hands back the
|
|
293
|
+
one real pyspark implementation, no matter how many times the cell
|
|
294
|
+
is executed.
|
|
295
|
+
"""
|
|
296
|
+
current = getattr(cls, method_name)
|
|
297
|
+
|
|
298
|
+
return getattr(current, "_cs_original", current)
|