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.
- {cdapython-2.0.8 → cdapython-2.0.10}/CHANGELOG.md +4 -0
- {cdapython-2.0.8 → cdapython-2.0.10}/PKG-INFO +1 -1
- {cdapython-2.0.8 → cdapython-2.0.10}/cdapython/discover.py +87 -147
- {cdapython-2.0.8 → cdapython-2.0.10}/cdapython/get_data.py +2 -0
- {cdapython-2.0.8 → cdapython-2.0.10}/cdapython/summarize.py +8 -6
- {cdapython-2.0.8 → cdapython-2.0.10}/cdapython/validation.py +118 -48
- {cdapython-2.0.8 → cdapython-2.0.10}/pyproject.toml +1 -1
- {cdapython-2.0.8 → cdapython-2.0.10}/LICENSE +0 -0
- {cdapython-2.0.8 → cdapython-2.0.10}/README.md +0 -0
- {cdapython-2.0.8 → cdapython-2.0.10}/cdapython/__init__.py +0 -0
- {cdapython-2.0.8 → cdapython-2.0.10}/cdapython/application_utilities.py +0 -0
- {cdapython-2.0.8 → cdapython-2.0.10}/cdapython/config/__init__.py +0 -0
- {cdapython-2.0.8 → cdapython-2.0.10}/cdapython/config/logger_default_config.yml +0 -0
- {cdapython-2.0.8 → cdapython-2.0.10}/cdapython/logging_wrappers.py +0 -0
|
@@ -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()
|
|
@@ -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
|
|
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
|
-
#
|
|
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
|
-
#
|
|
816
|
-
|
|
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
|
-
#
|
|
819
|
-
#
|
|
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
|
-
|
|
824
|
-
|
|
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
|
-
#
|
|
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
|
-
|
|
831
|
-
|
|
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
|
-
|
|
835
|
-
data_source = re.sub( r'\s+', r'', data_source ).upper()
|
|
853
|
+
valid_data_sources = set()
|
|
836
854
|
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
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
|
-
|
|
850
|
-
|
|
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
|
-
#
|
|
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
|
|
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( { '
|
|
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
|
-
|
|
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=[ '
|
|
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
|
-
|
|
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=[ '
|
|
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
|
-
|
|
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=[ '
|
|
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
|
-
|
|
443
|
-
|
|
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
|
-
|
|
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
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
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
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
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
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
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
|
-
|
|
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
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|