cdapython 2.0.8__tar.gz → 2.0.10__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.
@@ -1,3 +1,7 @@
1
+ 2025-10-27: 2.0.10: integrate column\_values() parameter validation with shared validation library; expand support for column\_values( data\_source ) to accept list values in addition to single strings
2
+
3
+ 2025-10-22: 2.0.9: parametrize data\_source check in column\_values()
4
+
1
5
  2025-10-20: 2.0.8: rename CDS to GC throughout
2
6
 
3
7
  2025-10-08: 2.0.7: fix malformed column metadata selector inside `collate=True` provenance management in get\_data()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cdapython
3
- Version: 2.0.8
3
+ Version: 2.0.10
4
4
  Summary: NCI CRDC CDA front-end user interface library
5
5
  License-File: LICENSE
6
6
  Requires-Python: >=3.9,<4.0
@@ -48,10 +48,11 @@ def cda_functions():
48
48
  'summarize_files',
49
49
  'get_subject_data',
50
50
  'get_file_data',
51
- 'intersect_subject_results',
52
- 'intersect_file_results',
53
51
  'expand_subject_results',
54
52
  'expand_file_results',
53
+ 'intersect_subject_results',
54
+ 'intersect_file_results',
55
+ 'release_metadata',
55
56
  'get_valid_log_levels',
56
57
  'get_log_level',
57
58
  'set_log_level',
@@ -737,7 +738,7 @@ def column_values(
737
738
  output_file='',
738
739
  sort_by='',
739
740
  filters=None,
740
- data_source='',
741
+ data_source=None,
741
742
  force=False
742
743
  ):
743
744
  """
@@ -789,7 +790,7 @@ def column_values(
789
790
  data_source ( string; optional ):
790
791
  Restrict returned values to the given upstream data source. Current
791
792
  valid values are 'GC', 'GDC', 'PDC', 'IDC' and 'ICDC'.
792
- Defaults to '' (no filter).
793
+ Defaults to None (no filter).
793
794
 
794
795
  force( boolean; optional ):
795
796
  Force execution of high-overhead queries on columns (like IDs)
@@ -806,57 +807,93 @@ def column_values(
806
807
  log = get_logger()
807
808
 
808
809
  #############################################################################################################################
809
- # Check for our one required parameter: column.
810
-
811
- if ( not isinstance( column, str ) ) or column == '':
812
- log.critical( 'column_values(): ERROR: parameter \'column\' cannot be omitted. Please specify a column from which to fetch a list of distinct values.')
813
- return
810
+ # Validate parameter inputs.
811
+ #############################################################################################################################
814
812
 
815
- # Let's not care about case, and if there's whitespace in our column name, remove it before it does any damage.
816
- column = re.sub( r'\s+', r'', column ).lower()
813
+ # Normalize user-supplied parameter data so we can assume from here on out that these are always lists of values:
814
+ # convert any of the following that come in as single values (instead of lists of values) into one-element lists,
815
+ # and leave the rest unmodified. Also convert everything to lowercase.
817
816
 
818
- # See if columns() agrees that the requested column exists. Note: cdapython.columns() and
819
- # the API's /columns endpoint give different sets of columns, by design. Here we want our output
820
- # to match the former, because we postprocess some of the columns offered by the API instead of
821
- # exposing them directly.
817
+ # If someone can devise a way to do this with a control loop, I'm all ears. I gave up after 20 minutes
818
+ # of fiddling with `locals()`.
822
819
 
823
- if len( columns( column=column, return_data_as='list' ) ) == 0:
824
- log.critical( f"column_values(): ERROR: parameter 'column' must be a searchable CDA column name. You supplied '{column}', which is not." )
820
+ try:
821
+ filters = normalize_to_list( 'filters', filters, str )
822
+ data_source = normalize_to_list( 'data_source', data_source, str )
823
+ except Exception as e:
824
+ log.error( e )
825
825
  return
826
826
 
827
- #############################################################################################################################
828
- # Check the data_source parameter.
827
+ # Cache CDA table and column metadata from the API for downstream reuse without further
828
+ # network disturbance. The data structure coming back from columns() is a DataFrame
829
+ # with columns [ 'table', 'column', 'data_type', 'nullable', 'description' ].
829
830
 
830
- if not isinstance( data_source, str ):
831
- log.error( f"The 'data_source' parameter must be a string (e.g. 'GDC'); you specified '{data_source}', which is not." )
831
+ cached_column_metadata = columns()
832
+
833
+ # Cache valid labels for upstream data sources. The data structure coming back from
834
+ # release_metadata() is a list of dicts, with each dict looking like
835
+ #
836
+ # {
837
+ # 'cda_table': 'file',
838
+ # 'cda_column': 'access',
839
+ # 'data_source': 'GDC',
840
+ # 'data_source_version': 'March 2025',
841
+ # 'data_source_extraction_date': '2025-03-21',
842
+ # 'data_source_row_count': 3025352,
843
+ # 'data_source_unique_value_count': 4,
844
+ # 'data_source_null_count': 407714
845
+ # }
846
+
847
+ try:
848
+ cached_release_metadata = release_metadata()
849
+ except Exception as e:
850
+ log.critical( e )
832
851
  return
833
852
 
834
- # Let's not care about case, and remove any whitespace before it can do any damage.
835
- data_source = re.sub( r'\s+', r'', data_source ).upper()
853
+ valid_data_sources = set()
836
854
 
837
- # TEMPORARY: enumerate valid `data_source` values and warn the user if they supplied something else.
838
- #
839
- # This should be replaced ASAP with a fetch from the /release_metadata endpoint.
840
-
841
- allowed_data_source_values = {
842
- 'GC',
843
- 'GDC',
844
- 'PDC',
845
- 'IDC',
846
- 'ICDC'
847
- }
855
+ for column_record in cached_release_metadata:
856
+ record_data_source = column_record['data_source']
857
+ # Let's not care about case.
858
+ if record_data_source.upper() != 'CDA':
859
+ valid_data_sources.add( record_data_source.upper() )
848
860
 
849
- if data_source != '' and data_source not in allowed_data_source_values:
850
- log.error( f"The 'data_source' parameter must be one of [ 'GC', 'GDC', 'PDC', 'IDC', 'ICDC' ]. You supplied '{data_source}', which is not." )
861
+ # Validate user-supplied parameter data.
862
+
863
+ try:
864
+ validate_parameter_values(
865
+ called_function='column_values',
866
+ cached_column_metadata=cached_column_metadata,
867
+ valid_data_sources=valid_data_sources,
868
+ table=None,
869
+ column=column,
870
+ match_from_file=None,
871
+ data_source=data_source,
872
+ add_columns=None,
873
+ exclude_columns=None,
874
+ collate_results=None,
875
+ include_external_refs=None,
876
+ return_data_as=return_data_as,
877
+ output_file=output_file,
878
+ sort_by=sort_by,
879
+ log=log
880
+ )
881
+ except Exception as e:
882
+ log.error( e )
851
883
  return
852
884
 
885
+ # Normalize sort_by case. Also, ':asc' is redundant: remove it (politely).
886
+ sort_by = re.sub( r':asc$', r'', sort_by ).lower()
887
+
853
888
  #############################################################################################################################
854
889
  # Check in advance for columns flagged as high-overhead.
855
890
 
891
+ # TO DO: Maybe store this somewhere better than here.
892
+
856
893
  expensive_columns = {
857
894
  'file_id',
858
- 'description',
859
895
  'drs_uri',
896
+ 'file_description',
860
897
  'file_name',
861
898
  'size',
862
899
  'case_id',
@@ -878,121 +915,20 @@ def column_values(
878
915
  'tumor_seq_allele1',
879
916
  'tumor_seq_allele2',
880
917
  'tumor_submitter_uuid',
918
+ 'upstream_id',
881
919
  'subject_id'
882
920
  }
883
921
 
884
- # Warn the user if an override hasn't been requested.
922
+ # Make sure the `force` parameter is a Boolean.
923
+ if not isinstance( force, bool ):
924
+ log.warning( f"The `force` parameter must be True or False; you supplied {force}, which is neither." )
925
+ return
885
926
 
927
+ # Warn the user if an override hasn't been requested.
886
928
  if not force and column in expensive_columns:
887
929
  log.warning( f"'{column}' has a very large number of values; retrieval is blocked by default. To perform this query, use column_values( ..., 'force=True' )." )
888
930
  return
889
931
 
890
- #############################################################################################################################
891
- # Listify `filters`, if it's a string, so we can process it in a uniform way later on.
892
-
893
- if filters is None:
894
- filters = list()
895
- elif isinstance( filters, str ):
896
- filters = [ filters ]
897
-
898
- #############################################################################################################################
899
- # Process return_data_as and output_file directives.
900
-
901
- allowed_return_types = {
902
- '',
903
- 'dataframe',
904
- 'tsv',
905
- 'list'
906
- }
907
-
908
- if not isinstance( return_data_as, str ):
909
- log.critical( f"column_values(): ERROR: unrecognized return type '{return_data_as}' requested. Please use one of 'dataframe', 'list' or 'tsv'." )
910
- return
911
-
912
- # Let's not be picky if someone wants to give us return_data_as='DataFrame' or return_data_as='TSV'
913
- return_data_as = return_data_as.lower()
914
-
915
- # We can't do much validation on filenames. If `output_file` isn't
916
- # a locally writeable path, it'll fail when we try to open it for
917
- # writing. Strip trailing whitespace from both ends and wrap the
918
- # file-access operation (later, below) in a try{} block.
919
-
920
- if not isinstance( output_file, str ):
921
- log.critical( f"column_values(): ERROR: the `output_file` parameter, if not omitted, should be a string containing a path to the desired output file. You supplied '{output_file}', which is not a string, let alone a valid path." )
922
- return
923
-
924
- output_file = output_file.strip()
925
-
926
- if return_data_as not in allowed_return_types:
927
-
928
- log.critical( f"column_values(): ERROR: unrecognized return type '{return_data_as}' requested. Please use one of 'dataframe', 'list' or 'tsv'." )
929
- return
930
-
931
- elif return_data_as == 'tsv' and output_file == '':
932
-
933
- log.critical( 'column_values(): ERROR: return type \'tsv\' requested, but \'output_file\' not specified. Please specify output_file=\'some/path/string/to/write/your/tsv/to\'.')
934
- return
935
-
936
- elif return_data_as != 'tsv' and output_file != '':
937
-
938
- # If the user put something in the `output_file` parameter but didn't specify `result_data_as='tsv'`,
939
- # they most likely want their data saved to a file (so ignoring the parameter misconfiguration
940
- # isn't safe), but ultimately we can't be sure what they meant (so taking an action isn't safe),
941
- # so we complain and ask them to clarify.
942
-
943
- log.error( f"'output_file' was specified, but this is only meaningful if 'return_data_as' is set to 'tsv'. You requested return_data_as='{return_data_as}'.\n(Note that if you don't specify any value for 'return_data_as', it defaults to 'dataframe'.)." )
944
- return
945
-
946
- #############################################################################################################################
947
- # Process sorting directives.
948
-
949
- # Enumerate all allowed values that a user can specify using the `sort_by` parameter. ( 'X:asc' will be aliased immediately to just 'X'. )
950
-
951
- allowed_sort_by_options = {
952
- 'list': {
953
- '',
954
- 'value',
955
- 'value:desc'
956
- },
957
- 'dataframe_or_tsv': {
958
- '',
959
- 'count',
960
- 'count:desc',
961
- 'value',
962
- 'value:desc'
963
- }
964
- }
965
-
966
- if not isinstance( sort_by, str ):
967
-
968
- # Complain if we receive any unexpected data types instead of string directives.
969
-
970
- log.critical( f"column_values(): ERROR: 'sort_by' must be a string; you used '{sort_by}', which is not." )
971
- return
972
-
973
- # Let's not care about case. Also, ':asc' is redundant: remove it (politely).
974
- sort_by = re.sub( r':asc$', r'', sort_by ).lower()
975
-
976
- if return_data_as == 'list':
977
-
978
- # Restrict sorting options for lists.
979
-
980
- if sort_by == '':
981
- sort_by = 'value'
982
- elif sort_by not in allowed_sort_by_options['list']:
983
- log.critical( f"column_values(): ERROR: return_data_as='list' can only be processed with sort_by='value' or sort_by='value:desc' (or omitting sort_by altogether). Please modify unsupported sort_by directive '{sort_by}' and try again." )
984
- return
985
-
986
- else:
987
-
988
- # For TSV output files and DataFrames, we support more user-configurable options (defaulting to sort_by='count:desc'):
989
-
990
- if sort_by == '':
991
- sort_by = 'count:desc'
992
- elif sort_by not in allowed_sort_by_options['dataframe_or_tsv']:
993
- log.critical( f"column_values(): ERROR: unrecognized sort_by '{sort_by}'. Please use one of 'count', 'value', 'count:desc', 'value:desc', 'count:asc' or 'value:asc' (or omit the sort_by parameter altogether)." )
994
- return
995
-
996
932
  #############################################################################################################################
997
933
  # Report details of the final parsed parameters.
998
934
 
@@ -1021,7 +957,8 @@ def column_values(
1021
957
  cda_client.api.column_values.column_values_endpoint_column_values_column_post.sync(
1022
958
  client=query_api_instance,
1023
959
  column=column,
1024
- data_source=data_source,
960
+ # This is how the API expects the data_source parameter to be encoded.
961
+ data_source=', '.join( data_source ),
1025
962
  limit=records_per_page,
1026
963
  offset=starting_offset
1027
964
  )
@@ -1033,7 +970,7 @@ def column_values(
1033
970
  log.error( f"{type(error)}: {error}" )
1034
971
  return
1035
972
 
1036
- log.debug( f"Sending query to API:\n{json.dumps( { 'columnname': column, 'system': data_source, 'count': True, 'total_count': True, 'limit': records_per_page, 'offset': starting_offset }, indent=4 )}\n" )
973
+ log.debug( f"Sending query to API:\n{json.dumps( { 'column': column, 'data_source': ', '.join( data_source ), 'limit': records_per_page, 'offset': starting_offset }, indent=4 )}\n" )
1037
974
 
1038
975
  # Report some metadata about the results we got back.
1039
976
 
@@ -1093,7 +1030,7 @@ def column_values(
1093
1030
  cda_client.api.column_values.column_values_endpoint_column_values_column_post.sync(
1094
1031
  client=query_api_instance,
1095
1032
  column=column,
1096
- data_source=data_source,
1033
+ data_source=', '.join( data_source ),
1097
1034
  limit=records_per_page,
1098
1035
  offset=incremented_offset
1099
1036
  )
@@ -1409,4 +1346,7 @@ def release_metadata():
1409
1346
  #
1410
1347
  #############################################################################################################################
1411
1348
 
1349
+ # This is down here to avoid a circular import problem.
1350
+ from cdapython.validation import normalize_to_list, validate_parameter_values
1351
+
1412
1352
 
@@ -457,6 +457,7 @@ def get_data(
457
457
  cached_column_metadata=cached_column_metadata,
458
458
  valid_data_sources=valid_data_sources,
459
459
  table=table,
460
+ column=None,
460
461
  match_from_file=match_from_file,
461
462
  data_source=data_source,
462
463
  add_columns=add_columns,
@@ -465,6 +466,7 @@ def get_data(
465
466
  include_external_refs=include_external_refs,
466
467
  return_data_as=return_data_as,
467
468
  output_file=output_file,
469
+ sort_by=None,
468
470
  log=log
469
471
  )
470
472
  except Exception as e:
@@ -587,14 +587,14 @@ def summarize_files(
587
587
  Users can require partial matches to string VALUEs by adding * to either or
588
588
  both ends. For example:
589
589
 
590
- primary_disease_type = *duct*
590
+ diagnosis = *duct*
591
591
  sex = F*
592
592
  size < 100
593
593
 
594
594
  String VALUEs need not be quoted inside of filter strings. For example, to include
595
595
  the filters specified just above in the `match_all` argument, we can write:
596
596
 
597
- summarize_files( match_all=[ 'primary_disease_type = *duct*', 'sex = F*', 'size < 100' ] )
597
+ summarize_files( match_all=[ 'diagnosis = *duct*', 'sex = F*', 'size < 100' ] )
598
598
 
599
599
  NULL is a special VALUE which can be used to match missing data. For
600
600
  example, to get a summary report for CDA files where the `access` field
@@ -728,14 +728,14 @@ def summarize_subjects(
728
728
  Users can require partial matches to string VALUEs by adding * to either or
729
729
  both ends. For example:
730
730
 
731
- primary_disease_type = *duct*
731
+ diagnosis = *duct*
732
732
  sex = F*
733
733
  size < 100
734
734
 
735
735
  String VALUEs need not be quoted inside of filter strings. For example, to include
736
736
  the filters specified just above in the `match_all` argument, we can write:
737
737
 
738
- summarize_subjects( match_all=[ 'primary_disease_type = *duct*', 'sex = F*' ] )
738
+ summarize_subjects( match_all=[ 'diagnosis = *duct*', 'sex = F*' ] )
739
739
 
740
740
  NULL is a special VALUE which can be used to match missing data. For
741
741
  example, to get a summary report for CDA subjects where the `year_of_birth` field
@@ -876,13 +876,13 @@ def summarize(
876
876
  Users can require partial matches to string VALUEs by adding * to either or
877
877
  both ends. For example:
878
878
 
879
- primary_disease_type = *duct*
879
+ diagnosis = *duct*
880
880
  sex = F*
881
881
 
882
882
  String VALUEs need not be quoted inside of filter strings. For example, to include
883
883
  the filters specified just above in the `match_all` argument, we can write:
884
884
 
885
- summarize( table='subject', match_all=[ 'primary_disease_type = *duct*', 'sex = F*' ] )
885
+ summarize( table='subject', match_all=[ 'diagnosis = *duct*', 'sex = F*' ] )
886
886
 
887
887
  NULL is a special VALUE which can be used to match missing data. For
888
888
  example, to get a count summary for rows where the `sex` field is missing data,
@@ -993,6 +993,7 @@ def summarize(
993
993
  cached_column_metadata=cached_column_metadata,
994
994
  valid_data_sources=valid_data_sources,
995
995
  table=table,
996
+ column=None,
996
997
  match_from_file=match_from_file,
997
998
  data_source=data_source,
998
999
  add_columns=add_columns,
@@ -1001,6 +1002,7 @@ def summarize(
1001
1002
  include_external_refs=None,
1002
1003
  return_data_as=return_data_as,
1003
1004
  output_file=output_file,
1005
+ sort_by=None,
1004
1006
  log=log
1005
1007
  )
1006
1008
  except Exception as e:
@@ -1,8 +1,6 @@
1
1
  import re
2
2
  import sys
3
3
 
4
- from cdapython.discover import tables
5
-
6
4
  #############################################################################################################################
7
5
  #
8
6
  # normalize_to_list( parameter_name, user_supplied_parameter_value, value_type ):
@@ -406,7 +404,8 @@ def validate_and_transform_match_from_file_values( cda_column_to_match, target_d
406
404
  # Fail if:
407
405
  #
408
406
  # * `called_function` is not in [ 'column_values', 'get_data', 'summarize' ]
409
- # * `table` is not a CDA table
407
+ # * `table` is not a CDA table (unless `called_function` is 'column_values', in which case `table` must be None)
408
+ # * `column` is not a CDA column (unless `called_function` is 'get_data' or 'summarize', in which case `column` must be None)
410
409
  # * `match_from_file` isn't a dict which (if non-null) specifies an accessible input file
411
410
  # containing a user-specified column, whose values are to be matched against a CDA column
412
411
  # that exists
@@ -417,6 +416,7 @@ def validate_and_transform_match_from_file_values( cda_column_to_match, target_d
417
416
  # * `include_external_refs` isn't a boolean value or None, depending on `called_function`
418
417
  # * `return_data_as` isn't one of the allowable types for `called_function`
419
418
  # * The value of `output_file` isn't consistent with the directive in `return_data_as` for `called_function`
419
+ # * `sort_by` isn't an allowable string value (or None, for callers get_data() and summarize())
420
420
  #
421
421
  #############################################################################################################################
422
422
 
@@ -425,6 +425,7 @@ def validate_parameter_values(
425
425
  cached_column_metadata,
426
426
  valid_data_sources,
427
427
  table,
428
+ column,
428
429
  match_from_file,
429
430
  data_source,
430
431
  add_columns,
@@ -433,39 +434,44 @@ def validate_parameter_values(
433
434
  include_external_refs,
434
435
  return_data_as,
435
436
  output_file,
437
+ sort_by,
436
438
  log
437
439
  ):
438
440
  # The data structure coming back from columns() is a DataFrame with columns [ 'table', 'column', 'data_type', 'nullable', 'description' ].
439
441
 
440
- # Make sure `table` exists.
441
-
442
- if table is None or not isinstance( table, str ) or table not in cached_column_metadata['table'].unique():
443
- raise RuntimeError( f"The required parameter 'table' must be a searchable CDA table; you supplied '{table}', which is not. Please run tables() for a list." )
442
+ # Make sure `table` exists, unless we're called by column_values().
443
+ if called_function in [ 'column_values' ]:
444
+ if table is not None:
445
+ raise RuntimeError( f"'table' cannot be non-null for column_values() caller: please alert the CDA devs to this event, something is misconfigured in our code." )
446
+ elif called_function in [ 'get_data', 'summarize' ]:
447
+ if table is None or not isinstance( table, str ) or table not in cached_column_metadata['table'].unique():
448
+ raise RuntimeError( f"The required parameter 'table' must be a searchable CDA table; you supplied '{table}', which is not. Please run tables() for a list." )
444
449
 
450
+ # Make sure `column` exists, unless we're called by get_data() or summarize().
445
451
  if called_function in [ 'get_data', 'summarize' ]:
446
-
447
- # Check `match_from_file` data for sanity.
452
+ if column is not None:
453
+ raise RuntimeError( f"'column' cannot be non-null for get_data() or summarize() caller: please alert the CDA devs to this event, something is misconfigured in our code." )
454
+ elif called_function in [ 'column_values' ]:
455
+ if column is None or not isinstance( column, str ) or column not in cached_column_metadata['column'].unique():
456
+ raise RuntimeError( f"The required parameter 'column' must be a searchable CDA column; you supplied '{column}', which is not. Please run columns() for a list." )
448
457
 
458
+ # Check `match_from_file` data for sanity.
459
+ if called_function in [ 'get_data', 'summarize' ]:
449
460
  # Is `match_from_file` a dict with the expected set of keys?
450
461
  if not isinstance( match_from_file, dict ) or set( match_from_file.keys() ) != { 'input_file', 'input_column', 'cda_column_to_match' }:
451
462
  raise RuntimeError( f"'match_from_file' must be a 3-element dictionary with keys ['input_file', 'input_column', 'cda_column_to_match']; you specified '{match_from_file}', which is not." )
452
-
453
463
  # Does `match_from_file`['cda_column_to_match'] exist?
454
464
  if match_from_file['cda_column_to_match'] != '' and match_from_file['cda_column_to_match'] not in cached_column_metadata['column'].unique():
455
465
  raise RuntimeError( f"'match_from_file['cda_column_to_match']' must be a valid CDA column; you supplied {match_from_file['cda_column_to_match']}, which is not." )
456
-
457
466
  # Does `match_from_file`['input_file'] exist and does it have a column named `match_from_file`['input_column']?
458
467
  if match_from_file['input_file'] != '':
459
-
460
468
  try:
461
469
  with open( match_from_file['input_file'] ) as IN:
462
470
  input_file_column_names = next( IN ).rstrip( '\n' ).split( '\t' )
463
471
  if match_from_file['input_column'] not in input_file_column_names:
464
472
  raise RuntimeError( f"'match_from_file['input_column']' must specify a column that exists in 'match_from_file['input_file']'. You specified '{match_from_file['input_column']}', which is not present in '{match_from_file['input_file']}'." )
465
-
466
473
  except Exception as error:
467
474
  raise RuntimeError( f"Couldn't read from match_from_file input file '{match_from_file['input_file']}': got error of type '{type(error)}', with error message '{error}'." )
468
-
469
475
  # Are the values given in `match_from_file` internally consistent? (Strange results might occur if not.)
470
476
  if match_from_file['cda_column_to_match'] == '':
471
477
  if match_from_file['input_file'] != '' or match_from_file['input_column'] != '':
@@ -476,19 +482,18 @@ def validate_parameter_values(
476
482
  elif match_from_file['input_column'] == '':
477
483
  if match_from_file['cda_column_to_match'] != '' or match_from_file['input_file'] != '':
478
484
  raise RuntimeError( f"If the 'match_from_file' parameter is used, it must be a 3-element dictionary with keys ['input_file', 'input_column', 'cda_column_to_match'] pointing to non-empty values. You specified '{match_from_file}', which is not that." )
479
-
480
- if match_from_file['input_file'] != '' and match_from_file['input_file'] == output_file:
481
- raise RuntimeError( f"You specified the same file ('{output_file}') as both a source of filter values (via 'match_from_file') and the target output file ( via 'output_file'). Please make sure these two files are different." )
482
-
483
- # Check that `data_source` is a single valid upstream data source label (for `called_function`=='column_values')
484
- # or a list of valid upstream data source labels (for `called_function` in [ 'get_data', 'summarize' ]).
485
+ if match_from_file['input_file'] != '' and match_from_file['input_file'] == output_file:
486
+ raise RuntimeError( f"You specified the same file ('{output_file}') as both a source of filter values (via 'match_from_file') and the target output file ( via 'output_file'). Please make sure these two files are different." )
487
+ elif called_function in [ 'column_values' ]:
488
+ if match_from_file is not None:
489
+ raise RuntimeError( f"'match_from_file' cannot be non-null for column_values() caller: please alert the CDA devs to this event, something is misconfigured in our code." )
490
+
491
+ # Check that `data_source` is a list of valid upstream data source labels (for
492
+ # `called_function` in [ 'column_values', 'get_data', 'summarize' ]).
485
493
  # `valid_data_sources` was normalized to uppercase when it was constructed by the caller.
486
494
 
487
495
  normalized_data_source = list()
488
496
 
489
- if called_function == 'column_values' and len( data_source ) > 1:
490
- raise RuntimeError( f"The 'data_source' parameter must be one valid data source name (e.g. 'GDC'); you specified '{data_source}', which is not." )
491
-
492
497
  for ds in data_source:
493
498
  if ds.upper() not in valid_data_sources:
494
499
  raise RuntimeError( f"The 'data_source' parameter must be a list containing one or more of [ {', '.join( sorted( valid_data_sources ) )} ]. You supplied '{data_source}', which is not that." )
@@ -496,31 +501,30 @@ def validate_parameter_values(
496
501
 
497
502
  data_source = normalized_data_source
498
503
 
499
- # Make sure CDA columns named in `add_columns` exist.
500
-
501
- for column_name in add_columns:
502
- match_result = re.search( r'^(.+)\.\*$', column_name )
503
-
504
- if match_result is not None:
505
- foreign_table = match_result.group(1)
506
- if foreign_table not in tables():
504
+ # Make sure CDA columns named in `add_columns` and `exclude_columns` exist, unless we're called by 'column_values'.
505
+
506
+ if called_function in [ 'column_values' ]:
507
+ if add_columns is not None or exclude_columns is not None:
508
+ raise RuntimeError( f"'add_columns' and 'exclude_columns' must be null for column_values() caller: please alert the CDA devs to this event, something is misconfigured in our code." )
509
+ elif called_function in [ 'get_data', 'summarize' ]:
510
+ for column_name in add_columns:
511
+ match_result = re.search( r'^(.+)\.\*$', column_name )
512
+ if match_result is not None:
513
+ foreign_table = match_result.group(1)
514
+ if foreign_table not in tables():
515
+ raise RuntimeError( f"'add_columns' can only contain valid CDA column names, or macros for whole tables like 'treatment.*'. You specified '{column_name}', which is neither." )
516
+ elif column_name not in cached_column_metadata['column'].unique():
507
517
  raise RuntimeError( f"'add_columns' can only contain valid CDA column names, or macros for whole tables like 'treatment.*'. You specified '{column_name}', which is neither." )
508
- elif column_name not in cached_column_metadata['column'].unique():
509
- raise RuntimeError( f"'add_columns' can only contain valid CDA column names, or macros for whole tables like 'treatment.*'. You specified '{column_name}', which is neither." )
510
-
511
- # Make sure CDA columns named in `exclude_columns` exist.
512
-
513
- for column_name in exclude_columns:
514
- match_result = re.search( r'^(.+)\.\*$', column_name )
518
+ for column_name in exclude_columns:
519
+ match_result = re.search( r'^(.+)\.\*$', column_name )
520
+ if match_result is not None:
521
+ foreign_table = match_result.group(1)
522
+ if foreign_table not in tables():
523
+ raise RuntimeError( f"'exclude_columns' can only contain valid CDA column names, or macros for whole tables like 'treatment.*'. You specified '{column_name}', which is neither." )
524
+ elif column_name not in cached_column_metadata['column'].unique():
525
+ raise RuntimeError( f"'exclude_columns' can only contain valid CDA column names. You specified '{column_name}', which is not that." )
515
526
 
516
- if match_result is not None:
517
- foreign_table = match_result.group(1)
518
- if foreign_table not in tables():
519
- raise RuntimeError( f"'exclude_columns' can only contain valid CDA column names, or macros for whole tables like 'treatment.*'. You specified '{column_name}', which is neither." )
520
- elif column_name not in cached_column_metadata['column'].unique():
521
- raise RuntimeError( f"'exclude_columns' can only contain valid CDA column names. You specified '{column_name}', which is not that." )
522
-
523
- # Check that `collate_results` is a boolean (for get_data) or None (for column_values, summarize) and that `called_function` has an expected value.
527
+ # Check that `collate_results` and `include_external_refs` are booleans (for get_data) or None (for column_values, summarize) and that `called_function` has an expected value.
524
528
 
525
529
  if called_function == 'get_data':
526
530
  if collate_results != True and collate_results != False:
@@ -529,9 +533,9 @@ def validate_parameter_values(
529
533
  raise RuntimeError( f"The `include_external_refs` parameter must be set to True or False; you specified '{include_external_refs}', which is neither." )
530
534
  elif called_function == 'summarize' or called_function == 'column_values':
531
535
  if collate_results is not None:
532
- raise RuntimeError( f"Something has gone horribly and unexpectedly wrong with respect to phantom collate_results values in summarize() calls; please notify the CDA devs of this event." )
536
+ raise RuntimeError( f"Something has gone horribly and unexpectedly wrong with respect to phantom collate_results values in summarize() or column_values() calls; please notify the CDA devs of this event." )
533
537
  if include_external_refs is not None:
534
- raise RuntimeError( f"Something has gone horribly and unexpectedly wrong with respect to phantom include_external_refs values in summarize() calls; please notify the CDA devs of this event." )
538
+ raise RuntimeError( f"Something has gone horribly and unexpectedly wrong with respect to phantom include_external_refs values in summarize() or column_values() calls; please notify the CDA devs of this event." )
535
539
  else:
536
540
  raise RuntimeError( f"`called_function` must be one of [ 'column_values', 'get_data', 'summarize' ] -- '{called_function}' is none of those. Please notify the CDA devs of this event." )
537
541
 
@@ -542,6 +546,12 @@ def validate_parameter_values(
542
546
  # writing.
543
547
 
544
548
  allowed_return_types = {
549
+ 'column_values': {
550
+ '',
551
+ 'dataframe',
552
+ 'tsv',
553
+ 'list'
554
+ },
545
555
  'get_data': {
546
556
  'dataframe',
547
557
  'tsv'
@@ -595,9 +605,66 @@ def validate_parameter_values(
595
605
 
596
606
  raise RuntimeError( f"'output_file' was specified, but this is only meaningful if 'return_data_as' is set to 'json'. You requested return_data_as='{return_data_as}'.\n(Note that if you don't specify any value for 'return_data_as', it defaults to printing tables to the standard output stream and not to an output file.)." )
597
607
 
608
+ elif called_function == 'column_values':
609
+
610
+ if return_data_as == 'tsv' and output_file == '':
611
+ # If the user asks for TSV, they also have to give us a path for the output file. If they didn't, complain.
612
+ raise RuntimeError( 'Return type \'tsv\' was requested, but \'output_file\' was not specified. Please specify output_file=\'some/path/string/to/write/your/tsv/to/your_tsv_output_file.tsv\'.' )
613
+
614
+ elif return_data_as != 'tsv' and output_file != '':
615
+
616
+ # If the user put something in the `output_file` parameter but didn't specify `result_data_as`='tsv',
617
+ # they most likely want their data saved to a file (so ignoring the parameter misconfiguration
618
+ # isn't safe), but ultimately we can't be sure what they meant (so taking an action isn't safe),
619
+ # so we complain and ask them to clarify.
620
+
621
+ raise RuntimeError( f"'output_file' was specified, but this is only meaningful if 'return_data_as' is set to 'tsv'. You requested return_data_as='{return_data_as}'.\n(Note that if you don't specify any value for 'return_data_as', it defaults to 'dataframe'.)." )
622
+
598
623
  else:
599
624
  raise RuntimeError( f"Got unpexpectedly non-null 'return_data_as' value '{return_data_as}' from function '{called_function}'. Please report this event to the CDA devs." )
600
625
 
626
+ if called_function in [ 'column_values' ]:
627
+
628
+ #############################################################################################################################
629
+ # Process sorting directives.
630
+
631
+ # Enumerate all allowed values that a user can specify using the `sort_by` parameter. ( 'X:asc' will be aliased immediately to just 'X'. )
632
+
633
+ allowed_sort_by_options = {
634
+ 'list': {
635
+ '',
636
+ 'value',
637
+ 'value:desc'
638
+ },
639
+ 'dataframe_or_tsv': {
640
+ '',
641
+ 'count',
642
+ 'count:desc',
643
+ 'value',
644
+ 'value:desc'
645
+ }
646
+ }
647
+
648
+ if not isinstance( sort_by, str ):
649
+ # Complain if we receive any unexpected data types instead of string directives.
650
+ raise RuntimeError( f"'sort_by' must be a string; you used '{sort_by}', which is not." )
651
+
652
+ if return_data_as == 'list':
653
+ # Restrict sorting options for lists.
654
+ if sort_by == '':
655
+ sort_by = 'value'
656
+ elif sort_by not in allowed_sort_by_options['list']:
657
+ raise RuntimeError( f"return_data_as='list' can only be processed with sort_by='value' or sort_by='value:desc' (or omitting sort_by altogether). Please modify unsupported sort_by directive '{sort_by}' and try again." )
658
+ else:
659
+ # For TSV output files and DataFrames, we support more user-configurable options (defaulting to sort_by='count:desc'):
660
+ if sort_by == '':
661
+ sort_by = 'count:desc'
662
+ elif sort_by not in allowed_sort_by_options['dataframe_or_tsv']:
663
+ raise RuntimeError( f"unrecognized sort_by '{sort_by}'. Please use one of 'count', 'value', 'count:desc', 'value:desc', 'count:asc' or 'value:asc' (or omit the sort_by parameter altogether)." )
664
+
665
+ elif sort_by is not None:
666
+ raise RuntimeError( f"'sort_by' cannot be non-null for get_data() or summarize() callers: please alert the CDA devs to this event, something is misconfigured in our code." )
667
+
601
668
  return
602
669
 
603
670
  #############################################################################################################################
@@ -606,4 +673,7 @@ def validate_parameter_values(
606
673
  #
607
674
  #############################################################################################################################
608
675
 
676
+ # This is down here to avoid a circular import problem.
677
+ from cdapython.discover import tables
678
+
609
679
 
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "cdapython"
3
- version = "2.0.8"
3
+ version = "2.0.10"
4
4
  description = "NCI CRDC CDA front-end user interface library"
5
5
  authors = []
6
6
  readme = "README.md"
File without changes
File without changes