From 4b96ee148514c6da13206b241de2212cbbac2fe7 Mon Sep 17 00:00:00 2001 From: Valentin Pesendorfer Date: Wed, 2 Jan 2019 17:15:19 +0100 Subject: [PATCH 1/7] add add pload, pdump and Credentials class --- wsmtk/utils.py | 116 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 116 insertions(+) diff --git a/wsmtk/utils.py b/wsmtk/utils.py index c4a99be1..54504a60 100644 --- a/wsmtk/utils.py +++ b/wsmtk/utils.py @@ -7,6 +7,9 @@ import multiprocessing.pool import array from wsmtk.whittaker import ws2d, ws2d_vc, ws2d_vc_asy +import pickle +import os +from cryptography.fernet import Fernet # assign xrange to range if py2 try: @@ -180,6 +183,119 @@ def getDV(self,nd): def getDIX(self): return([self.daily.index(x) for x in self.target]) + +class Credentials: + '''Credentials helper class''' + + def __init__(self, username = None, password = None): + '''Create Credentials instance + + Args: + username (str): Earthdata username + password (str): Earthdata passsword + ''' + + + self.username = username + self.password = password + + self.complete = not (not self.username or not self.password) + + def retrieve(self): + '''Retrieve credentials from disk''' + + try: + u, p = pload('wsmtk.cred.pkl') + + k = pload('wsmtk.key.pkl') + + cipher_suite = Fernet(k) + + self.username = cipher_suite.decrypt(u).decode() + self.password = cipher_suite.decrypt(p).decode() + + except: + + self.destroy() + raise + + def store(self): + '''Store credentials on disk''' + + try: + + k = Fernet.generate_key() + + cipher_suite = Fernet(k) + + u = cipher_suite.encrypt(self.username.encode()) + + p = cipher_suite.encrypt(self.password.encode()) + + pdump((u,p),'wsmtk.cred.pkl') + + pdump(k,'wsmtk.key.pkl') + + except: + + self.destroy() + + print('Storing Earthdata credentials failed!') + + + + def destroy(self): + '''Remove all credential files on disk''' + + try: + os.remove('wsmtk.cred.pkl') + except FileNotFoundError: + pass + + try: + os.remove('wsmtk.key.pkl') + except FileNotFoundError: + pass + + + +def pdump(obj,filename): + '''Pickle dump wrapper + + Agrs: + obj: Python object to be pickled + filename: name of target pickle file + + Returns: + None + ''' + + try: + with open(filename,'wb') as pkl: + pickle.dump(obj,pkl) + + except FileNotFoundError: + raise + +def pload(filename): + '''Pickle load wrapper + + Agrs: + filename: name of target pickle file + + Returns: + Pickled object + ''' + + + try: + with open(filename,'rb') as pkl: + return(pickle.load(pkl)) + + except FileNotFoundError: + raise + + def dtype_GDNP(dt): '''GDAL/NP DataType helper. From 14fec1d5a9ceddf7d878b0c1de47f9515f4c309c Mon Sep 17 00:00:00 2001 From: Valentin Pesendorfer Date: Wed, 2 Jan 2019 17:15:42 +0100 Subject: [PATCH 2/7] implement Credentials class --- wsmtk/downloadMODIS.py | 29 ++++++++++++++++++++++++----- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/wsmtk/downloadMODIS.py b/wsmtk/downloadMODIS.py index 48c4913a..6d82de76 100644 --- a/wsmtk/downloadMODIS.py +++ b/wsmtk/downloadMODIS.py @@ -8,6 +8,8 @@ import pickle import re import ogr +from wsmtk.utils import Credentials, pload + try: range = xrange @@ -34,6 +36,7 @@ def main(): parser.add_argument("--username", help='Earthdata username (required for download)',metavar='') parser.add_argument("--password", help='Earthdata password (required for download)',metavar='') parser.add_argument("-d","--targetdir", help='Destination directory',default=os.getcwd(),metavar='') + parser.add_argument("--store-credentials", help='Store Earthdata credentials on disk to be used for future downloads (unsecure!)',action='store_true') #parser.add_argument("-v","--verbose", help='Verbosity',action='store_true') parser.add_argument("--download", help='Download data',action='store_true') parser.add_argument("--aria2", help='Use ARIA2 for downloading',action='store_true') @@ -45,16 +48,32 @@ def main(): args = parser.parse_args() + credentials = CredHelper(args.username, args.password) + # Check for credentials if download is True - if args.download & (not args.username or not args.password): - raise SystemExit('Downloading requires username and password!') + if args.download & (not credentials.complete): + + try: + + credentials.retrieve() + + except: + + raise SystemExit('\nError: Earthdata credentials not found!\n') + + elif args.store_credentials: + + credentials.store() + + else: + + pass args.product = [x.upper() for x in args.product] # Load product table this_dir, this_filename = os.path.split(__file__) - with open(os.path.join(this_dir, "data", "MODIS_V6_PT.pkl"),'rb') as table_raw: - product_table = pickle.load(table_raw) + product_table = pload(os.path.join(this_dir, "data", "MODIS_V6_PT.pkl")) for p in args.product: @@ -193,7 +212,7 @@ def main(): # If download is True and at least one result, download data if args.download and res.results > 0: - res.setCredentials(args.username,args.password) + res.setCredentials(credentials.username,credentials.password) res.download() From 3001c64ff101ba7b6d861f6ac572bf5249e573da Mon Sep 17 00:00:00 2001 From: Valentin Pesendorfer Date: Wed, 2 Jan 2019 17:22:51 +0100 Subject: [PATCH 3/7] add cryptography as requirement --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 2347b0c5..00021bff 100644 --- a/setup.py +++ b/setup.py @@ -60,6 +60,6 @@ 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', ], - install_requires=['numpy','gdal>=2','h5py','beautifulsoup4','requests','progress','pandas'], + install_requires=['numpy','gdal>=2','h5py','beautifulsoup4','requests','progress','pandas', "cryptography"], python_requires='>=2.7.11, <4', ) From cc719ab13033e8fe3a791ddbc372db9b59e864f8 Mon Sep 17 00:00:00 2001 From: Valentin Pesendorfer Date: Wed, 2 Jan 2019 17:24:08 +0100 Subject: [PATCH 4/7] forgot to update class name in downloadMODIS --- wsmtk/downloadMODIS.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wsmtk/downloadMODIS.py b/wsmtk/downloadMODIS.py index 6d82de76..c05c7a21 100644 --- a/wsmtk/downloadMODIS.py +++ b/wsmtk/downloadMODIS.py @@ -48,7 +48,7 @@ def main(): args = parser.parse_args() - credentials = CredHelper(args.username, args.password) + credentials = Credentials(args.username, args.password) # Check for credentials if download is True if args.download & (not credentials.complete): From a238c493f894ccd0802a6e701a7931711e30bc07 Mon Sep 17 00:00:00 2001 From: Valentin Pesendorfer Date: Wed, 2 Jan 2019 17:24:45 +0100 Subject: [PATCH 5/7] update .gitignore --- .gitignore | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.gitignore b/.gitignore index 7bbc71c0..f66dc9a8 100644 --- a/.gitignore +++ b/.gitignore @@ -99,3 +99,7 @@ ENV/ # mypy .mypy_cache/ + +# cred files +wsmtk.cred.pkl +wsmtk.key.pkl From bd6e9c462a95645a4e9450a76d517ccf93799b98 Mon Sep 17 00:00:00 2001 From: Valentin Pesendorfer Date: Thu, 3 Jan 2019 10:09:09 +0100 Subject: [PATCH 6/7] fix minimum numpy version to get rid of compatibility warnings --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 00021bff..7640b4d1 100644 --- a/setup.py +++ b/setup.py @@ -60,6 +60,6 @@ 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', ], - install_requires=['numpy','gdal>=2','h5py','beautifulsoup4','requests','progress','pandas', "cryptography"], + install_requires=['numpy>=1.15.1','gdal>=2','h5py','beautifulsoup4','requests','progress','pandas', "cryptography"], python_requires='>=2.7.11, <4', ) From 1a9bd539d0c3b3aad7c4fb6f3d392d40ed216edc Mon Sep 17 00:00:00 2001 From: Valentin Pesendorfer Date: Thu, 3 Jan 2019 10:09:28 +0100 Subject: [PATCH 7/7] update pkl file for py2x compatibility --- wsmtk/data/ModlandTiles_bbx.pkl | Bin 32144 -> 37079 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/wsmtk/data/ModlandTiles_bbx.pkl b/wsmtk/data/ModlandTiles_bbx.pkl index 5eb91a70e6991fa1135b474373ca8bfa9c7934e5..e3096b093a59b6a9ce5edda8467d0949b87ba99e 100644 GIT binary patch literal 37079 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 literal 32144 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 V(`ue}&hhwV{x)+4aAv@j@Ne6$YmNW_