cdapython 2.0.2__py3-none-any.whl → 2.0.4__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.
- cdapython/get_data.py +1 -2
- cdapython/summarize.py +1 -2
- cdapython/validation.py +91 -104
- {cdapython-2.0.2.dist-info → cdapython-2.0.4.dist-info}/METADATA +4 -2
- cdapython-2.0.4.dist-info/RECORD +13 -0
- {cdapython-2.0.2.dist-info → cdapython-2.0.4.dist-info}/WHEEL +1 -1
- cdapython-2.0.2.dist-info/RECORD +0 -13
- {cdapython-2.0.2.dist-info → cdapython-2.0.4.dist-info/licenses}/LICENSE +0 -0
cdapython/get_data.py
CHANGED
|
@@ -510,8 +510,7 @@ def get_data(
|
|
|
510
510
|
# Validate and normalize match_all filter strings; save results as a list of statement strings.
|
|
511
511
|
|
|
512
512
|
try:
|
|
513
|
-
|
|
514
|
-
queries_for_match_all = validate_and_transform_match_filter_list( cached_column_metadata, match_all, enforce_column_uniqueness=False )
|
|
513
|
+
queries_for_match_all = validate_and_transform_match_filter_list( cached_column_metadata, match_all, enforce_column_uniqueness=True )
|
|
515
514
|
except Exception as e:
|
|
516
515
|
log.error( e )
|
|
517
516
|
return
|
cdapython/summarize.py
CHANGED
|
@@ -1056,8 +1056,7 @@ def summarize(
|
|
|
1056
1056
|
# ...and save parse results for each filter expression as a separate Query object (to be combined later).
|
|
1057
1057
|
|
|
1058
1058
|
try:
|
|
1059
|
-
|
|
1060
|
-
queries_for_match_all = validate_and_transform_match_filter_list( cached_column_metadata, match_all, enforce_column_uniqueness=False )
|
|
1059
|
+
queries_for_match_all = validate_and_transform_match_filter_list( cached_column_metadata, match_all, enforce_column_uniqueness=True )
|
|
1061
1060
|
except Exception as e:
|
|
1062
1061
|
log.error( e )
|
|
1063
1062
|
return
|
cdapython/validation.py
CHANGED
|
@@ -152,43 +152,13 @@ def validate_and_transform_match_filter_list( cached_column_metadata, match_stat
|
|
|
152
152
|
'f': 'false'
|
|
153
153
|
}
|
|
154
154
|
|
|
155
|
-
# Intercept filter expressions of the form <numeric literal> <comparison operator> <column name> <comparison operator> <numeric literal>
|
|
156
|
-
# and split each into two two-term/one-operator comparisons, marking the field as exempt from potential column uniqueness
|
|
157
|
-
# constraints (unless there are more top-level filters on this column in addition to the first 'X <= column < Y' expression encountered)
|
|
158
|
-
column_names_exempt_from_uniqueness_check = set()
|
|
159
|
-
new_match_statement_list = list()
|
|
160
|
-
|
|
161
|
-
for filter_expression in match_statement_list:
|
|
162
|
-
match_result = re.search( r'^(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S.*)$', filter_expression ) if isinstance( filter_expression, str ) else None
|
|
163
|
-
if match_result is not None:
|
|
164
|
-
left_numeric = match_result.group(1)
|
|
165
|
-
left_operator = match_result.group(2)
|
|
166
|
-
column_name = match_result.group(3)
|
|
167
|
-
right_operator = match_result.group(4)
|
|
168
|
-
right_numeric = match_result.group(5)
|
|
169
|
-
if re.search( r'^[-+]?\d+(\.\d+)?$', left_numeric ) is None or re.search( r'^[-+]?\d+(\.\d+)?$', right_numeric ) is None or \
|
|
170
|
-
left_operator not in comparison_operators or right_operator not in comparison_operators:
|
|
171
|
-
# This is not a numeric comparison of the type we seek. Pass it along to downstream validation.
|
|
172
|
-
new_match_statement_list.append( filter_expression )
|
|
173
|
-
else:
|
|
174
|
-
# If we're enforcing column uniqueness, then at this level, whatever happens to a ternary filter downstream, we
|
|
175
|
-
# still only want to see each column filtered via at most one filter expression.
|
|
176
|
-
if enforce_column_uniqueness and column_name in column_names_exempt_from_uniqueness_check:
|
|
177
|
-
raise RuntimeError( f"Requested column '{filter_column_name}' cannot be used twice in a 'match_all' list." )
|
|
178
|
-
else:
|
|
179
|
-
column_names_exempt_from_uniqueness_check.add( column_name )
|
|
180
|
-
new_match_statement_list.append( f"{column_name} {flip_comparison_operator[left_operator]} {left_numeric}" )
|
|
181
|
-
new_match_statement_list.append( f"{column_name} {right_operator} {right_numeric}" )
|
|
182
|
-
else:
|
|
183
|
-
new_match_statement_list.append( filter_expression )
|
|
184
|
-
|
|
185
|
-
match_statement_list = new_match_statement_list
|
|
186
|
-
|
|
187
|
-
# Track which columns have been seen in case we need to catch disallowed sets of multiple queries on the same column.
|
|
188
155
|
seen_filter_column_names = set()
|
|
189
156
|
|
|
190
157
|
for filter_expression in match_statement_list:
|
|
191
158
|
|
|
159
|
+
if not isinstance( filter_expression, str ) or len( filter_expression ) == 0:
|
|
160
|
+
raise RuntimeError( f"Match parameters must be nonempty filter strings: you specified '{filter_expression}' (from match list {match_statement_list}), which is not." )
|
|
161
|
+
|
|
192
162
|
#############################################################################################################################
|
|
193
163
|
# Enforce the simplified cdapython query syntax as described in the docs, but quietly allow synonyms if received. Map them
|
|
194
164
|
# first back to the canonical operator in the simplified query syntax, then validate, then normalize for API request syntax
|
|
@@ -196,7 +166,6 @@ def validate_and_transform_match_filter_list( cached_column_metadata, match_stat
|
|
|
196
166
|
|
|
197
167
|
# Take care of two-word operators first: downstream, unmodified, they break simplifying assumptions
|
|
198
168
|
# about filter string tokenization that should safe to make given the spec of the cdapython query syntax.
|
|
199
|
-
|
|
200
169
|
if re.search( r'^(\S+)\s+IS\s+NOT\s+(\S.*)$', filter_expression, flags=re.IGNORECASE ) is not None:
|
|
201
170
|
filter_expression = re.sub( r'^(\S+)\s+IS\s+NOT\s+(\S.*)$', r'\1 != \2', filter_expression, flags=re.IGNORECASE )
|
|
202
171
|
|
|
@@ -204,31 +173,45 @@ def validate_and_transform_match_filter_list( cached_column_metadata, match_stat
|
|
|
204
173
|
filter_expression = re.sub( r'^(\S+)\s+NOT\s+LIKE\s+(\S.*)$', r'\1 != \2', filter_expression, flags=re.IGNORECASE )
|
|
205
174
|
|
|
206
175
|
# Normalize the rest of the known operators (used for null and fuzzy matches) to conform to cdapython query syntax.
|
|
207
|
-
|
|
208
176
|
elif re.search( r'^(\S+)\s+IS\s+(\S.*)$', filter_expression, flags=re.IGNORECASE ) is not None:
|
|
209
177
|
filter_expression = re.sub( r'^(\S+)\s+IS\s+(\S.*)$', r'\1 = \2', filter_expression, flags=re.IGNORECASE )
|
|
210
178
|
|
|
211
179
|
elif re.search( r'^(\S+)\s+LIKE\s+(\S.*)$', filter_expression, flags=re.IGNORECASE ) is not None:
|
|
212
180
|
filter_expression = re.sub( r'^(\S+)\s+LIKE\s+(\S.*)$', r'\1 = \2', filter_expression, flags=re.IGNORECASE )
|
|
213
181
|
|
|
214
|
-
|
|
215
|
-
|
|
182
|
+
# Handle ternary comparisons and mark matching filters as such.
|
|
183
|
+
filter_is_ternary = False
|
|
184
|
+
filter_column_name = ''
|
|
185
|
+
match_result = re.search( r'^(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S.*)$', filter_expression )
|
|
186
|
+
if match_result is not None:
|
|
187
|
+
left_numeric = match_result.group(1)
|
|
188
|
+
left_operator = match_result.group(2)
|
|
189
|
+
column_name = match_result.group(3)
|
|
190
|
+
right_operator = match_result.group(4)
|
|
191
|
+
right_numeric = match_result.group(5)
|
|
192
|
+
if re.search( r'^[-+]?\d+(\.\d+)?$', left_numeric ) is not None and re.search( r'^[-+]?\d+(\.\d+)?$', right_numeric ) is not None and \
|
|
193
|
+
left_operator in comparison_operators and right_operator in comparison_operators:
|
|
194
|
+
# This is a numeric comparison of the type we seek.
|
|
195
|
+
is_ternary = True
|
|
196
|
+
filter_column_name = column_name.lower()
|
|
216
197
|
|
|
217
|
-
|
|
218
|
-
|
|
198
|
+
#############################################################################################################################
|
|
199
|
+
# Validate (non-ternary) minimal filter string format: <non-whitespace string (column name)><whitespace><non-whitespace string (operator)><whitespace><non-whitespace string (beginning of value to match)><any mix of whitespace and non-whitespace characters (end of value to match)>
|
|
219
200
|
|
|
220
|
-
if re.search( r'^\S+\s+\S+\s+\S.*$', filter_expression ) is None:
|
|
201
|
+
if not filter_is_ternary and re.search( r'^\S+\s+\S+\s+\S.*$', filter_expression ) is None:
|
|
221
202
|
raise RuntimeError( f"Filter string '{filter_expression}' does not conform to 'COLUMN_NAME OP VALUE' format. See the help text for details." )
|
|
222
203
|
|
|
223
204
|
#############################################################################################################################
|
|
224
205
|
# Now parse the filter expression and validate COLUMN and OP tokens.
|
|
225
206
|
|
|
226
|
-
# Try to extract a column name from this filter expression. Don't be case-sensitive.
|
|
227
|
-
|
|
207
|
+
# Try to extract a column name from this filter expression. Don't be case-sensitive. For
|
|
208
|
+
# ternary filters, we've already extracted `filter_column_name`, above.
|
|
209
|
+
if not filter_is_ternary:
|
|
210
|
+
filter_column_name = re.sub( r'^([\S]+)\s.*', r'\1', filter_expression ).lower()
|
|
228
211
|
|
|
229
212
|
# Have we seen this before (and do we care)?
|
|
230
213
|
if enforce_column_uniqueness:
|
|
231
|
-
if filter_column_name
|
|
214
|
+
if filter_column_name in seen_filter_column_names:
|
|
232
215
|
raise RuntimeError( f"Requested column '{filter_column_name}' cannot be used twice in a 'match_all' list." )
|
|
233
216
|
else:
|
|
234
217
|
seen_filter_column_names.add( filter_column_name )
|
|
@@ -247,92 +230,96 @@ def validate_and_transform_match_filter_list( cached_column_metadata, match_stat
|
|
|
247
230
|
if target_data_type not in operators_by_data_type:
|
|
248
231
|
raise RuntimeError( f"Requested column '{filter_column_name}' is of unknown data_type '{target_data_type}': cannot continue, please contact the CDA devs with a description of this event." )
|
|
249
232
|
|
|
250
|
-
|
|
251
|
-
filter_operator = re.sub( r'^\S+\s+(\S+)\s.*', r'\1', filter_expression )
|
|
233
|
+
normalized_filter_expression = filter_expression
|
|
252
234
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
235
|
+
if not filter_is_ternary:
|
|
236
|
+
|
|
237
|
+
# See what the operator is.
|
|
238
|
+
filter_operator = re.sub( r'^\S+\s+(\S+)\s.*', r'\1', filter_expression )
|
|
256
239
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
240
|
+
# Be kind to computer scientists.
|
|
241
|
+
if filter_operator == '==':
|
|
242
|
+
filter_operator = '='
|
|
260
243
|
|
|
261
|
-
|
|
262
|
-
|
|
244
|
+
# Make sure the operator specified is allowed for the data type of the column being filtered.
|
|
245
|
+
if filter_operator not in operators_by_data_type[target_data_type]:
|
|
246
|
+
raise RuntimeError( f"Operator '{filter_operator}' is not usable for values of type '{target_data_type}'." )
|
|
263
247
|
|
|
264
|
-
|
|
265
|
-
|
|
248
|
+
# Extract the filter value/pattern.
|
|
249
|
+
filter_value = re.sub( r'^\S+\s+\S+\s+(\S.*)$', r'\1', filter_expression )
|
|
266
250
|
|
|
267
|
-
|
|
268
|
-
|
|
251
|
+
# We said quotes weren't required for string values. Doesn't technically mean they can't be used. Remove them.
|
|
252
|
+
filter_value = re.sub( r'''^['"]*(.*)['"]*$''', r'\1', filter_value)
|
|
269
253
|
|
|
270
|
-
|
|
254
|
+
#############################################################################################################################
|
|
255
|
+
# Validate VALUE types and process wildcards.
|
|
271
256
|
|
|
272
|
-
|
|
273
|
-
filter_value = filter_value.strip()
|
|
257
|
+
# Ignore leading and trailing whitespace unless we're dealing with strings.
|
|
274
258
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
259
|
+
if target_data_type != 'text':
|
|
260
|
+
filter_value = filter_value.strip()
|
|
261
|
+
|
|
262
|
+
if filter_value.lower() != 'null':
|
|
278
263
|
|
|
279
|
-
|
|
264
|
+
if target_data_type == 'boolean':
|
|
265
|
+
|
|
266
|
+
# If we're supposed to be in a boolean column, make sure we've got a true/false value. Normalize recognized synonyms for valid values.
|
|
280
267
|
|
|
281
|
-
|
|
268
|
+
filter_value = filter_value.lower()
|
|
282
269
|
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
270
|
+
if filter_value not in boolean_alias:
|
|
271
|
+
raise RuntimeError( f"Requested column {filter_column_name} has data type 'boolean', requiring a true/false value; you specified '{filter_value}', which is not valid." )
|
|
272
|
+
else:
|
|
273
|
+
filter_value = boolean_alias[filter_value]
|
|
287
274
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
275
|
+
elif target_data_type in [ 'bigint', 'integer', 'numeric' ]:
|
|
276
|
+
|
|
277
|
+
# If we're supposed to be in a numeric column, make sure we've got a number.
|
|
291
278
|
|
|
292
|
-
|
|
293
|
-
|
|
279
|
+
if re.search( r'^[-+]?\d+(\.\d+)?$', filter_value ) is None:
|
|
280
|
+
raise RuntimeError( f"Requested column {filter_column_name} has data type '{target_data_type}', requiring a number value; you specified '{filter_value}', which is not." )
|
|
294
281
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
282
|
+
elif target_data_type == 'text':
|
|
283
|
+
|
|
284
|
+
# Check for wildcards: if found, adjust operator and
|
|
285
|
+
# wildcard syntax to match API expectations on incoming queries.
|
|
299
286
|
|
|
300
|
-
|
|
287
|
+
original_filter_value = filter_value
|
|
301
288
|
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
289
|
+
if re.search( r'^\*', filter_value ) is not None or re.search( r'\*$', filter_value ) is not None:
|
|
290
|
+
|
|
291
|
+
# API expects lowercase operators.
|
|
292
|
+
if filter_operator == '!=':
|
|
293
|
+
filter_operator = 'not like'
|
|
294
|
+
else:
|
|
295
|
+
filter_operator = 'like'
|
|
309
296
|
|
|
310
|
-
|
|
311
|
-
|
|
297
|
+
if re.search(r'.\*.', filter_value) is not None:
|
|
298
|
+
raise RuntimeError( f"Wildcards (*) are only allowed at the ends of string values; string '{original_filter_value}' is noncompliant (it has one in the middle). Please fix." )
|
|
312
299
|
|
|
313
|
-
|
|
314
|
-
|
|
300
|
+
# API expects percent signs.
|
|
301
|
+
filter_value = re.sub( r'\*', r'%', filter_value )
|
|
315
302
|
|
|
316
|
-
|
|
317
|
-
|
|
303
|
+
# API needs strings quoted. Minimal replication case: match a string that contains only numbers. Need to distinguish from integer input.
|
|
304
|
+
filter_value = f"'{filter_value}'"
|
|
318
305
|
|
|
319
|
-
|
|
306
|
+
else:
|
|
320
307
|
|
|
321
|
-
|
|
322
|
-
|
|
308
|
+
# Just to be safe. Types change.
|
|
309
|
+
raise RuntimeError( f"Unanticipated data_type '{target_data_type}' encountered for filter-target column '{filter_column_name}'; cannot continue. Please report this event to CDA developers." )
|
|
323
310
|
|
|
324
|
-
else:
|
|
325
|
-
|
|
326
|
-
if filter_operator == '=':
|
|
327
|
-
filter_operator = 'is'
|
|
328
|
-
elif filter_operator == '!=':
|
|
329
|
-
filter_operator = 'is not'
|
|
330
311
|
else:
|
|
331
|
-
|
|
312
|
+
|
|
313
|
+
if filter_operator == '=':
|
|
314
|
+
filter_operator = 'is'
|
|
315
|
+
elif filter_operator == '!=':
|
|
316
|
+
filter_operator = 'is not'
|
|
317
|
+
else:
|
|
318
|
+
raise RuntimeError( f"Unexpected operator encountered for NULL: '{filter_operator}' (from '{filter_expression}') -- please use = or != instead." )
|
|
332
319
|
|
|
333
|
-
|
|
320
|
+
filter_value = 'null'
|
|
334
321
|
|
|
335
|
-
|
|
322
|
+
normalized_filter_expression = filter_column_name + ' ' + filter_operator + ' ' + filter_value
|
|
336
323
|
|
|
337
324
|
normalized_match_statement_list.append( normalized_filter_expression )
|
|
338
325
|
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: cdapython
|
|
3
|
-
Version: 2.0.
|
|
3
|
+
Version: 2.0.4
|
|
4
4
|
Summary: NCI CRDC CDA front-end user interface library
|
|
5
|
+
License-File: LICENSE
|
|
5
6
|
Requires-Python: >=3.9,<4.0
|
|
6
7
|
Classifier: Programming Language :: Python :: 3
|
|
7
8
|
Classifier: Programming Language :: Python :: 3.9
|
|
@@ -9,6 +10,7 @@ Classifier: Programming Language :: Python :: 3.10
|
|
|
9
10
|
Classifier: Programming Language :: Python :: 3.11
|
|
10
11
|
Classifier: Programming Language :: Python :: 3.12
|
|
11
12
|
Classifier: Programming Language :: Python :: 3.13
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
12
14
|
Requires-Dist: cda-client (>=2.0.0)
|
|
13
15
|
Requires-Dist: pandas (>=2.2.3)
|
|
14
16
|
Requires-Dist: pyyaml (>=6.0.0)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
cdapython/__init__.py,sha256=5XapTJOqQ9IfFcO4UrM09teNP_xYkTR_bQTRww6e3Wk,575
|
|
2
|
+
cdapython/application_utilities.py,sha256=oD8LA9N2cM8gnTx353IdEgjtcHiI1LSlKQgUtKXtANM,1615
|
|
3
|
+
cdapython/config/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
|
+
cdapython/config/logger_default_config.yml,sha256=j4kaJk0w4-xiOyCl5Ml0DhWnK6dAWy33UecLsl1vI_w,434
|
|
5
|
+
cdapython/discover.py,sha256=o15pbyPE9x0u3jf2phT-y0V2ZyppZUJWkuUb4qLpaiQ,60343
|
|
6
|
+
cdapython/get_data.py,sha256=kDNxe5HhH7wou_nPngZQeDOOsU-cSywMPGNYLli1KVA,57826
|
|
7
|
+
cdapython/logging_wrappers.py,sha256=SsgKrLh9y6RR-iAiMAm0Is9fGjLKQZ8BckA1MBCCLig,6885
|
|
8
|
+
cdapython/summarize.py,sha256=ACwf5uuuKBjtWKGwfczR6DPIYxAw_L6xM1YKDW_DXY4,90566
|
|
9
|
+
cdapython/validation.py,sha256=rVKdfEiqk0iiIx0HK7YY2vqK92jnCybrv_i3t_PUCnI,34043
|
|
10
|
+
cdapython-2.0.4.dist-info/METADATA,sha256=FEEMFnYI20iw00ys3RsUYj1fcyoKs4gkt-0oL80SiYc,764
|
|
11
|
+
cdapython-2.0.4.dist-info/WHEEL,sha256=M5asmiAlL6HEcOq52Yi5mmk9KmTVjY2RDPtO4p9DMrc,88
|
|
12
|
+
cdapython-2.0.4.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
|
13
|
+
cdapython-2.0.4.dist-info/RECORD,,
|
cdapython-2.0.2.dist-info/RECORD
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
cdapython/__init__.py,sha256=5XapTJOqQ9IfFcO4UrM09teNP_xYkTR_bQTRww6e3Wk,575
|
|
2
|
-
cdapython/application_utilities.py,sha256=oD8LA9N2cM8gnTx353IdEgjtcHiI1LSlKQgUtKXtANM,1615
|
|
3
|
-
cdapython/config/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
|
-
cdapython/config/logger_default_config.yml,sha256=j4kaJk0w4-xiOyCl5Ml0DhWnK6dAWy33UecLsl1vI_w,434
|
|
5
|
-
cdapython/discover.py,sha256=o15pbyPE9x0u3jf2phT-y0V2ZyppZUJWkuUb4qLpaiQ,60343
|
|
6
|
-
cdapython/get_data.py,sha256=vuiqpY9BQf7gSSk52ls9-qsOo5alkm1NTj55qXr8mx4,57965
|
|
7
|
-
cdapython/logging_wrappers.py,sha256=SsgKrLh9y6RR-iAiMAm0Is9fGjLKQZ8BckA1MBCCLig,6885
|
|
8
|
-
cdapython/summarize.py,sha256=Zpq96HpZr4wn4NVMaYfKMTWi8BQh59fpsp5rzVjW_TQ,90705
|
|
9
|
-
cdapython/validation.py,sha256=Y9n440pmyFt5rc4lxKNgSRIhbgF6IQD6Xq8eB8LCjbA,35143
|
|
10
|
-
cdapython-2.0.2.dist-info/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
|
11
|
-
cdapython-2.0.2.dist-info/METADATA,sha256=Ejc_tcIjBeBYnVbznY4sZ1hzYgoYr9QnS4oVZutVeK0,691
|
|
12
|
-
cdapython-2.0.2.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
|
|
13
|
-
cdapython-2.0.2.dist-info/RECORD,,
|
|
File without changes
|