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 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
- # TO DO: RE-ENABLE enforce_column_uniqueness AFTER FIGURING OUT NUMERIC HANDLING AND COMPATIBILITY BETWEEN GET_DATA AND SUMMARIZE
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
- # TO DO: RE-ENABLE enforce_column_uniqueness AFTER FIGURING OUT NUMERIC HANDLING AND COMPATIBILITY BETWEEN GET_DATA AND SUMMARIZE
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
- # Validate 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)>
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
- if not isinstance( filter_expression, str ) or len( filter_expression ) == 0:
218
- raise RuntimeError( f"Match parameters must be nonempty filter strings: you specified '{filter_expression}' (from match list {match_statement_list}), which is not." )
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
- filter_column_name = re.sub( r'^([\S]+)\s.*', r'\1', filter_expression ).lower()
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 not in column_names_exempt_from_uniqueness_check and filter_column_name in seen_filter_column_names:
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
- # See what the operator is.
251
- filter_operator = re.sub( r'^\S+\s+(\S+)\s.*', r'\1', filter_expression )
233
+ normalized_filter_expression = filter_expression
252
234
 
253
- # Be kind to computer scientists.
254
- if filter_operator == '==':
255
- filter_operator = '='
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
- # Make sure the operator specified is allowed for the data type of the column being filtered.
258
- if filter_operator not in operators_by_data_type[target_data_type]:
259
- raise RuntimeError( f"Operator '{filter_operator}' is not usable for values of type '{target_data_type}'." )
240
+ # Be kind to computer scientists.
241
+ if filter_operator == '==':
242
+ filter_operator = '='
260
243
 
261
- # Extract the filter value/pattern.
262
- filter_value = re.sub( r'^\S+\s+\S+\s+(\S.*)$', r'\1', filter_expression )
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
- # We said quotes weren't required for string values. Doesn't technically mean they can't be used. Remove them.
265
- filter_value = re.sub( r'''^['"]*(.*)['"]*$''', r'\1', filter_value)
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
- # Validate VALUE types and process wildcards.
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
- # Ignore leading and trailing whitespace unless we're dealing with strings.
254
+ #############################################################################################################################
255
+ # Validate VALUE types and process wildcards.
271
256
 
272
- if target_data_type != 'text':
273
- filter_value = filter_value.strip()
257
+ # Ignore leading and trailing whitespace unless we're dealing with strings.
274
258
 
275
- if filter_value.lower() != 'null':
276
-
277
- if target_data_type == 'boolean':
259
+ if target_data_type != 'text':
260
+ filter_value = filter_value.strip()
261
+
262
+ if filter_value.lower() != 'null':
278
263
 
279
- # 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.
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
- filter_value = filter_value.lower()
268
+ filter_value = filter_value.lower()
282
269
 
283
- if filter_value not in boolean_alias:
284
- 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." )
285
- else:
286
- filter_value = boolean_alias[filter_value]
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
- elif target_data_type in [ 'bigint', 'integer', 'numeric' ]:
289
-
290
- # If we're supposed to be in a numeric column, make sure we've got a number.
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
- if re.search( r'^[-+]?\d+(\.\d+)?$', filter_value ) is None:
293
- 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." )
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
- elif target_data_type == 'text':
296
-
297
- # Check for wildcards: if found, adjust operator and
298
- # wildcard syntax to match API expectations on incoming queries.
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
- original_filter_value = filter_value
287
+ original_filter_value = filter_value
301
288
 
302
- if re.search( r'^\*', filter_value ) is not None or re.search( r'\*$', filter_value ) is not None:
303
-
304
- # API expects lowercase operators.
305
- if filter_operator == '!=':
306
- filter_operator = 'not like'
307
- else:
308
- filter_operator = 'like'
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
- if re.search(r'.\*.', filter_value) is not None:
311
- 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." )
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
- # API expects percent signs.
314
- filter_value = re.sub( r'\*', r'%', filter_value )
300
+ # API expects percent signs.
301
+ filter_value = re.sub( r'\*', r'%', filter_value )
315
302
 
316
- # API needs strings quoted. Minimal replication case: match a string that contains only numbers. Need to distinguish from integer input.
317
- filter_value = f"'{filter_value}'"
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
- else:
306
+ else:
320
307
 
321
- # Just to be safe. Types change.
322
- 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." )
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
- raise RuntimeError( f"Unexpected operator encountered for NULL: '{filter_operator}' (from '{filter_expression}') -- please use = or != instead." )
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
- filter_value = 'null'
320
+ filter_value = 'null'
334
321
 
335
- normalized_filter_expression = filter_column_name + ' ' + filter_operator + ' ' + filter_value
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.3
1
+ Metadata-Version: 2.4
2
2
  Name: cdapython
3
- Version: 2.0.2
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,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: poetry-core 2.1.3
2
+ Generator: poetry-core 2.2.0
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
@@ -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,,