From 591be2e701be613ee32ff6b607ab4afa83aeb49c Mon Sep 17 00:00:00 2001 From: gcbor Date: Sun, 14 Apr 2019 02:41:55 +0200 Subject: [PATCH] Add support for multi user in credentials file The credentials file can now also hold a list of credentials. Providing the username when loging is trying to map it to the possible multiple supplied creditials. Signed-off-by: gcbor --- README.rst | 12 ++++++ pycaching/geocaching.py | 27 +++++++++++-- test/test_geocaching.py | 85 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 121 insertions(+), 3 deletions(-) diff --git a/README.rst b/README.rst index f16e135..f393bec 100644 --- a/README.rst +++ b/README.rst @@ -85,6 +85,18 @@ and ``password`` from that file as login credentials. { "username": "myusername", "password": "mypassword" } +You can also provide multiple username and password tuples in a file as login credentials. +The tuple to be used can be chosen by providing its username when calling pycaching.login(), +e.g. pycaching.login("myusername2"). The first username and password tuple specified will be +used as default if pycaching.login() is called without providing a username. + +.. code-block:: json + + # sample .gc_credentials JSON file with mutiple users + [ { "username": "myusername1", "password": "mypassword1" }, + { "username": "myusername2", "password": "mypassword2" } ] + + .. code-block:: python import pycaching diff --git a/pycaching/geocaching.py b/pycaching/geocaching.py index 5b52ac3..1af63e3 100644 --- a/pycaching/geocaching.py +++ b/pycaching/geocaching.py @@ -85,7 +85,7 @@ def login(self, username=None, password=None): if not username or not password: try: - username, password = self._load_credentials() + username, password = self._load_credentials(username=username) except FileNotFoundError as e: raise LoginFailedException("Credentials file not found and " "no username and password is given.") from e @@ -136,7 +136,7 @@ def login(self, username=None, password=None): raise LoginFailedException("Cannot login to the site " "(probably wrong username or password).") - def _load_credentials(self): + def _load_credentials(self, username=None): """Load credentials from file. Find credentials file in either current directory or user's home directory. If exists, load @@ -164,7 +164,28 @@ def _load_credentials(self): # load contents with open(credentials_file, "r") as f: - credentials = json.load(f) + cred = json.load(f) + if isinstance(cred, dict): + if username is None: + credentials = cred + else: + if "username" in cred and cred["username"] == username: + credentials = cred + else: + raise KeyError("User {} requested but not found in credential.".format(username)) + elif isinstance(cred, list): + if username is None and len(cred) > 0: + credentials = cred[0] + else: + for c in cred: + if "username" in c and c["username"] == username: + credentials = c + break + else: + raise KeyError("User {} requested but not found in credentials.".format(username)) + else: + raise KeyError("Credential data type is unexpected {}".format(type(cred))) + if "password" in credentials and "password_cmd" in credentials: raise KeyError("Ambiguous keys. Choose either \"password\" or \"password_cmd\".") elif "password" in credentials: diff --git a/test/test_geocaching.py b/test/test_geocaching.py index de36c95..4ee5108 100644 --- a/test/test_geocaching.py +++ b/test/test_geocaching.py @@ -187,6 +187,8 @@ def test_logout(self): def test_load_credentials(self): filename_backup = self.gc._credentials_file credentials = {"username": _username, "password": _password} + multi_credentials = [{"username": _username+"1", "password": _password+"1"}, + {"username": _username+"2", "password": _password+"2"}] empty_valid_json = {} nonsense_str = b"ss{}ef" password_cmd = "echo {}".format(_password) @@ -213,6 +215,59 @@ def test_load_credentials(self): finally: os.remove(valid.name) + with self.subTest("Try to load valid credentials with username from current directory"): + try: + with NamedTemporaryFile(dir=".", delete=False) as valid: + valid.write(json.dumps(credentials).encode()) + self.gc._credentials_file = os.path.basename(valid.name) + username, password = self.gc._load_credentials(_username) + self.assertEqual(_username, username) + self.assertEqual(_password, password) + finally: + os.remove(valid.name) + + with self.subTest("Try to load valid credentials with not existing username from current directory"): + try: + with NamedTemporaryFile(dir=".", delete=False) as valid: + valid.write(json.dumps(credentials).encode()) + self.gc._credentials_file = os.path.basename(valid.name) + with self.assertRaises(KeyError): + username, password = self.gc._load_credentials(_username+"3") + finally: + os.remove(valid.name) + + with self.subTest("Try to load valid multi credentials with default user from current directory"): + try: + with NamedTemporaryFile(dir=".", delete=False) as valid: + valid.write(json.dumps(multi_credentials).encode()) + self.gc._credentials_file = os.path.basename(valid.name) + username, password = self.gc._load_credentials() + self.assertEqual(_username+"1", username) + self.assertEqual(_password+"1", password) + finally: + os.remove(valid.name) + + with self.subTest("Try to load valid multi credentials with user from current directory"): + try: + with NamedTemporaryFile(dir=".", delete=False) as valid: + valid.write(json.dumps(multi_credentials).encode()) + self.gc._credentials_file = os.path.basename(valid.name) + username, password = self.gc._load_credentials(_username+"2") + self.assertEqual(_username+"2", username) + self.assertEqual(_password+"2", password) + finally: + os.remove(valid.name) + + with self.subTest("Try to load valid multi credentials with not existing user from current directory"): + try: + with NamedTemporaryFile(dir=".", delete=False) as valid: + valid.write(json.dumps(multi_credentials).encode()) + self.gc._credentials_file = os.path.basename(valid.name) + with self.assertRaises(KeyError): + username, password = self.gc._load_credentials(_username+"3") + finally: + os.remove(valid.name) + with self.subTest("Try to load empty file from current directory"): try: with NamedTemporaryFile(dir=".", delete=False) as empty: @@ -223,6 +278,36 @@ def test_load_credentials(self): finally: os.remove(empty.name) + with self.subTest("Try to load very empty multi credential file from current directory"): + try: + with NamedTemporaryFile(dir=".", delete=False) as valid: + valid.write(json.dumps([]).encode()) + self.gc._credentials_file = os.path.basename(valid.name) + with self.assertRaises(KeyError): + username, password = self.gc._load_credentials() + finally: + os.remove(valid.name) + + with self.subTest("Try to load empty multi credential file from current directory"): + try: + with NamedTemporaryFile(dir=".", delete=False) as valid: + valid.write(json.dumps([empty_valid_json]).encode()) + self.gc._credentials_file = os.path.basename(valid.name) + with self.assertRaises(KeyError): + username, password = self.gc._load_credentials() + finally: + os.remove(valid.name) + + with self.subTest("Try to load multi credential file with invalid format from current directory"): + try: + with NamedTemporaryFile(dir=".", delete=False) as valid: + valid.write(json.dumps("username").encode()) + self.gc._credentials_file = os.path.basename(valid.name) + with self.assertRaises(KeyError): + username, password = self.gc._load_credentials() + finally: + os.remove(valid.name) + with self.subTest("Try to load nonsense file from current directory"): try: with NamedTemporaryFile(dir=".", delete=False) as nonsense: