19
19
20
20
import csv
21
21
from datetime import datetime
22
+ from enum import Enum
22
23
import io
23
24
24
25
import json # Needed by mypy.
26
+ import logging
25
27
import os
26
28
27
29
import re # Needed by mypy.
48
50
from google .protobuf import field_mask_pb2
49
51
50
52
import kaggle
51
- from kagglesdk import KaggleClient , KaggleEnv # type: ignore[attr-defined]
53
+ from kagglesdk import get_access_token_from_env , KaggleClient , KaggleEnv # type: ignore[attr-defined]
52
54
from kagglesdk .admin .types .inbox_file_service import CreateInboxFileRequest
53
55
from kagglesdk .blobs .types .blob_api_service import ApiStartBlobUploadRequest , ApiStartBlobUploadResponse , ApiBlobType
54
56
from kagglesdk .competitions .types .competition_api_service import (
150
152
T = TypeVar ('T' )
151
153
152
154
155
+ class AuthMethod (Enum ):
156
+ LEGACY_API_KEY = 0
157
+ ACCESS_TOKEN = 1
158
+
159
+ def __str__ (self ):
160
+ return self .name
161
+
162
+
153
163
class DirectoryArchive (object ):
154
164
155
165
def __init__ (self , fullpath , fmt ):
@@ -365,7 +375,9 @@ class KaggleApi:
365
375
CONFIG_NAME_COMPETITION = 'competition'
366
376
CONFIG_NAME_PATH = 'path'
367
377
CONFIG_NAME_USER = 'username'
378
+ CONFIG_NAME_AUTH_METHOD = 'auth_method'
368
379
CONFIG_NAME_KEY = 'key'
380
+ CONFIG_NAME_TOKEN = 'token'
369
381
CONFIG_NAME_SSL_CA_CERT = 'ssl_ca_cert'
370
382
371
383
HEADER_API_VERSION = 'X-Kaggle-ApiVersion'
@@ -398,10 +410,12 @@ class KaggleApi:
398
410
399
411
args : List [str ] = []
400
412
if os .environ .get ('KAGGLE_API_ENVIRONMENT' ) == 'LOCALHOST' :
401
- # Make it verbose when running in the debugger.
402
- args = ['--verbose' , '--local' ]
403
- elif os .environ .get ('KAGGLE_API_ENDPOINT' ) == 'http://localhost' :
404
- args = ['--local' ]
413
+ args .append ('--local' )
414
+ verbose = (os .environ .get ('VERBOSE' ) or os .environ .get ('VERBOSE_OUTPUT' ) or "false" ).lower ()
415
+ if verbose in ('1' , 'true' , 'yes' ):
416
+ args .append ('--verbose' )
417
+ logging .basicConfig (level = logging .DEBUG )
418
+ logger = logging .getLogger (__name__ )
405
419
406
420
# Kernels valid types
407
421
valid_push_kernel_types = ['script' , 'notebook' ]
@@ -513,39 +527,76 @@ def retriable_func(*args):
513
527
## Authentication
514
528
515
529
def authenticate (self ) -> None :
516
- """Authenticate the user with the Kaggle API.
530
+ if self ._authenticate_with_access_token ():
531
+ return
532
+ if self ._authenticate_with_legacy_apikey ():
533
+ return
534
+ print (
535
+ 'Could not find {}. Make sure it\' s located in'
536
+ ' {}. Or use the environment method. See setup'
537
+ ' instructions at'
538
+ ' https://github.com/Kaggle/kaggle-api/' .format (self .config_file , self .config_dir )
539
+ )
540
+ exit (1 )
541
+
542
+ def _authenticate_with_legacy_apikey (self ) -> bool :
543
+ """Authenticate the user with the Kaggle API using legacy API key.
517
544
518
545
This method will generate a configuration, first checking the
519
546
environment for credential variables, and falling back to looking
520
547
for the .kaggle/kaggle.json configuration file.
521
548
"""
522
549
523
- config_data : Dict [str , str ] = {}
550
+ config_values : Dict [str , str ] = {}
524
551
# Ex: 'datasets list', 'competitions files', 'models instances get', etc.
525
552
api_command = ' ' .join (sys .argv [1 :])
526
553
527
554
# Step 1: try getting username/password from environment
528
- config_data = self .read_config_environment (config_data )
555
+ config_values = self .read_config_environment (config_values )
529
556
530
557
# Step 2: if credentials were not in env read in configuration file
531
- if self .CONFIG_NAME_USER not in config_data or self .CONFIG_NAME_KEY not in config_data :
558
+ if self .CONFIG_NAME_USER not in config_values or self .CONFIG_NAME_KEY not in config_values :
532
559
if os .path .exists (self .config ):
533
- config_data = self .read_config_file (config_data )
534
- elif self ._is_help_or_version_command (api_command ) or (
535
- len (sys .argv ) > 2 and api_command .startswith (self .command_prefixes_allowing_anonymous_access )
536
- ):
560
+ config_values = self .read_config_file (config_values )
561
+ elif self ._command_allows_logged_out (api_command ):
537
562
# Some API commands should be allowed without authentication.
538
- return
563
+ return True
539
564
else :
540
- raise IOError (
541
- 'Could not find {}. Make sure it\' s located in'
542
- ' {}. Or use the environment method. See setup'
543
- ' instructions at'
544
- ' https://github.com/Kaggle/kaggle-api/' .format (self .config_file , self .config_dir )
545
- )
565
+ return False
566
+
567
+ # Step 3: Validate and save
568
+ # Username and password are required.
569
+ for item in [self .CONFIG_NAME_USER , self .CONFIG_NAME_KEY ]:
570
+ if item not in config_values :
571
+ raise ValueError ('Error: Missing %s in configuration.' % item )
572
+ self .config_values = config_values
573
+ self .config_values [self .CONFIG_NAME_AUTH_METHOD ] = AuthMethod .LEGACY_API_KEY
574
+ self .logger .debug (f'Authenticated with legacy api key in: { self .config } ' )
575
+ return True
576
+
577
+ def _authenticate_with_access_token (self ) -> bool :
578
+ (access_token , source ) = get_access_token_from_env ()
579
+ if not access_token :
580
+ return False
581
+
582
+ username = self ._introspect_token (access_token )
583
+ if not username :
584
+ self .logger .debug (f'Ignoring invalid/expired access token in \" { source } \" .' )
585
+ return False
546
586
547
- # Step 3: load into configuration!
548
- self ._load_config (config_data )
587
+ self .config_values : Dict [str , str ] = {
588
+ self .CONFIG_NAME_TOKEN : access_token ,
589
+ self .CONFIG_NAME_USER : username ,
590
+ self .CONFIG_NAME_AUTH_METHOD : AuthMethod .ACCESS_TOKEN ,
591
+ }
592
+ self .logger .debug (f'Authenticated with access token in: { source } ' )
593
+ return True
594
+
595
+ def _command_allows_logged_out (self , api_command : str ) -> bool :
596
+ # Some API commands do not required authentication.
597
+ return self ._is_help_or_version_command (api_command ) or (
598
+ len (sys .argv ) > 2 and api_command .startswith (self .command_prefixes_allowing_anonymous_access )
599
+ )
549
600
550
601
def _is_help_or_version_command (self , api_command : str ) -> bool :
551
602
"""Determines if the string command passed in is for a help or version
@@ -582,23 +633,6 @@ def read_config_environment(self, config_data: Optional[Dict[str, str]] = None)
582
633
583
634
## Configuration
584
635
585
- def _load_config (self , config_data : Dict [str , str ]) -> None :
586
- """The final step of the authenticate steps, where we load the values from
587
- config_data into the Configuration object.
588
-
589
- Parameters
590
- ==========
591
- config_data: a dictionary with configuration values (keys) to read
592
- into self.config_values
593
- """
594
- # Username and password are required.
595
-
596
- for item in [self .CONFIG_NAME_USER , self .CONFIG_NAME_KEY ]:
597
- if item not in config_data :
598
- raise ValueError ('Error: Missing %s in configuration.' % item )
599
-
600
- self .config_values = config_data
601
-
602
636
def read_config_file (self , config_data : Optional [Dict [str , str ]] = None , quiet : bool = False ) -> Dict [str , str ]:
603
637
"""read_config_file is the first effort to get a username and key to
604
638
authenticate to the Kaggle API. Since we can get the username and password
@@ -715,9 +749,7 @@ def get_config_value(self, name: str) -> Optional[str]:
715
749
==========
716
750
name: the config value key to get
717
751
"""
718
- if name in self .config_values :
719
- return self .config_values [name ]
720
- return None
752
+ return self .config_values .get (name )
721
753
722
754
def get_default_download_dir (self , * subdirs : str ) -> str :
723
755
"""Get the download path for a file. If not defined, return default from
@@ -749,7 +781,7 @@ def print_config_value(self, name, prefix='- ', separator=': '):
749
781
value_out = 'None'
750
782
if name in self .config_values and self .config_values [name ] is not None :
751
783
value_out = self .config_values [name ]
752
- print (prefix + name + separator + value_out )
784
+ print (f" { prefix } { name } { separator } { value_out } " )
753
785
754
786
def print_config_values (self , prefix = '- ' ):
755
787
"""Print all configuration values.
@@ -762,6 +794,7 @@ def print_config_values(self, prefix='- '):
762
794
return
763
795
print ('Configuration values from ' + self .config_dir )
764
796
self .print_config_value (self .CONFIG_NAME_USER , prefix = prefix )
797
+ self .print_config_value (self .CONFIG_NAME_AUTH_METHOD , prefix = prefix )
765
798
self .print_config_value (self .CONFIG_NAME_PATH , prefix = prefix )
766
799
self .print_config_value (self .CONFIG_NAME_PROXY , prefix = prefix )
767
800
self .print_config_value (self .CONFIG_NAME_COMPETITION , prefix = prefix )
@@ -777,9 +810,12 @@ def build_kaggle_client(self) -> kagglesdk.kaggle_client.KaggleClient:
777
810
)
778
811
)
779
812
verbose = '--verbose' in self .args or '-v' in self .args
780
- # config = self.api_client.configuration
781
813
return KaggleClient (
782
- env = env , verbose = verbose , username = self .config_values ['username' ], password = self .config_values ['key' ]
814
+ env = env ,
815
+ verbose = verbose ,
816
+ username = self .config_values .get (self .CONFIG_NAME_USER ),
817
+ password = self .config_values .get (self .CONFIG_NAME_KEY ),
818
+ api_token = self .config_values .get (self .CONFIG_NAME_TOKEN ),
783
819
)
784
820
785
821
def camel_to_snake (self , name : str ) -> str :
0 commit comments