\n",
+ "\n",
"```\n",
+ "\n",
+ "You will be prompted to enter your password.\n",
+ "\n",
+ "\n",
+ "\n",
+ "Your username and password will be saved locally in plain-text for future use!\n",
+ "\n",
+ "
\n",
+ "\n",
+ "\n",
+ "\n",
"When initializing a new **Connection** instance, **pyam** will automatically search for the configuration in a known location."
]
},
diff --git a/pyam/iiasa.py b/pyam/iiasa.py
index e4303a050..87387f1f3 100644
--- a/pyam/iiasa.py
+++ b/pyam/iiasa.py
@@ -23,6 +23,10 @@
is_list_like,
)
from pyam.logging import deprecation_warning
+import ixmp4
+from ixmp4.conf import settings
+from ixmp4.conf.auth import ManagerAuth
+
logger = logging.getLogger(__name__)
# set requests-logger to WARNING only
@@ -36,28 +40,22 @@
""".replace(
"\n", ""
)
+IXMP4_LOGIN = "Please run `ixmp4 login ` in a console"
# path to local configuration settings
DEFAULT_IIASA_CREDS = Path("~").expanduser() / ".local" / "pyam" / "iiasa.yaml"
-JWT_DECODE_ARGS = {"verify_signature": False, "verify_exp": True}
-
def set_config(user, password, file=None):
- """Save username and password for the IIASA API connection to a file"""
- file = Path(file) if file is not None else DEFAULT_IIASA_CREDS
- if not file.parent.exists():
- file.parent.mkdir(parents=True)
-
- with open(file, mode="w") as f:
- logger.info(f"Setting IIASA-connection configuration file: {file}")
- yaml.dump(dict(username=user, password=password), f, sort_keys=False)
+ raise DeprecationWarning(f"This method is deprecated. {IXMP4_LOGIN}.")
def _read_config(file):
"""Read username and password for IIASA API connection from file"""
with open(file, "r") as stream:
- return yaml.safe_load(stream)
+ creds = yaml.safe_load(stream)
+
+ return ManagerAuth(**creds, url=settings.manager_url)
def _check_response(r, msg="Error connecting to IIASA database", error=RuntimeError):
@@ -73,74 +71,58 @@ def __init__(self, creds: str = None, auth_url: str = _AUTH_URL):
----------
creds : pathlib.Path or str, optional
Path to a file with authentication credentials
- auth_url : str, optionl
+ auth_url : str, optional
Url of the authentication service
"""
self.client = httpx.Client(base_url=auth_url, timeout=10.0, http2=True)
- self.access_token, self.refresh_token = None, None
if creds is None:
if DEFAULT_IIASA_CREDS.exists():
- self.creds = _read_config(DEFAULT_IIASA_CREDS)
+ deprecation_warning(
+ f"{IXMP4_LOGIN} and manually delete the file '{DEFAULT_IIASA_CREDS}'.",
+ "Using a pyam-credentials file",
+ )
+ self.auth = _read_config(DEFAULT_IIASA_CREDS)
else:
- self.creds = None
+ self.auth = ixmp4.conf.settings.default_auth
elif isinstance(creds, Path) or is_str(creds):
- self.creds = _read_config(creds)
+ self.auth = _read_config(creds)
else:
raise DeprecationWarning(
"Passing credentials as clear-text is not allowed. "
- "Please use `pyam.iiasa.set_config(, )` instead!"
+ f"{IXMP4_LOGIN} instead."
)
- # if no creds, get anonymous token
- # TODO: explicit token for anonymous login will not be necessary for ixmp-server
- if self.creds is None:
- r = self.client.get("/legacy/anonym/")
- if r.status_code >= 400:
- raise ValueError("Unknown API error: " + r.text)
- self.user = None
- self.access_token = r.json()
+ # explicit token for anonymous login is not necessary for ixmp4 platforms
+ # but is required for legacy Scenario Explorer databases
+ if self.auth.user.username == "@anonymous":
+ self._get_anonymous_token()
- # else get user-token
else:
- self.user = self.creds["username"]
- self.obtain_jwt()
-
- def __call__(self):
- try:
- # raises jwt.ExpiredSignatureError if token is expired
- jwt.decode(self.access_token, options=JWT_DECODE_ARGS)
-
- except jwt.ExpiredSignatureError:
- self.refresh_jwt()
+ self.user = self.auth.user.username
+ self.access_token = self.auth.access_token
- return {"Authorization": "Bearer " + self.access_token}
-
- def obtain_jwt(self):
- r = self.client.post("/v1/token/obtain/", json=self.creds)
- if r.status_code == 401:
- raise ValueError(
- "Credentials not valid to connect to https://manager.ece.iiasa.ac.at."
- )
- elif r.status_code >= 400:
+ def _get_anonymous_token(self):
+ r = self.client.get("/legacy/anonym/")
+ if r.status_code >= 400:
raise ValueError("Unknown API error: " + r.text)
+ self.user, self.access_token = None, r.json()
- _json = r.json()
- self.access_token = _json["access"]
- self.refresh_token = _json["refresh"]
-
- def refresh_jwt(self):
+ def __call__(self):
try:
# raises jwt.ExpiredSignatureError if token is expired
- jwt.decode(self.refresh_token, options=JWT_DECODE_ARGS)
- r = self.client.post(
- "/v1/token/refresh/", json={"refresh": self.refresh_token}
+ jwt.decode(
+ self.access_token,
+ options={"verify_signature": False, "verify_exp": True},
)
- if r.status_code >= 400:
- raise ValueError("Unknown API error: " + r.text)
- self.access_token = r.json()["access"]
except jwt.ExpiredSignatureError:
- self.obtain_jwt()
+ if self.auth.user.username == "@anonymous":
+ self._get_anonymous_token()
+ else:
+ self.auth.refresh_or_reobtain_jwt()
+ self.access_token = self.auth.access_token
+
+ return {"Authorization": "Bearer " + self.access_token}
class Connection(object):
diff --git a/pyam/logging.py b/pyam/logging.py
index e34e00632..6587eff8f 100644
--- a/pyam/logging.py
+++ b/pyam/logging.py
@@ -20,6 +20,7 @@ def adjust_log_level(logger="pyam", level="ERROR"):
def deprecation_warning(msg, item="This method", stacklevel=3):
"""Write deprecation warning to log"""
+ warnings.simplefilter("always", DeprecationWarning)
message = f"{item} is deprecated and will be removed in future versions. {msg}"
warnings.warn(message, DeprecationWarning, stacklevel=stacklevel)
diff --git a/setup.cfg b/setup.cfg
index 21cc2d126..c6c125d10 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -19,7 +19,7 @@ classifiers =
[options]
packages = pyam
include_package_data = True
-python_requires = >=3.8, <3.12
+python_requires = >=3.10, <3.12
# NOTE TO DEVS
# If you change a minimum version below, please explicitly implement the change
@@ -27,17 +27,17 @@ python_requires = >=3.8, <3.12
# Please also add a section "Dependency changes" to the release notes
install_requires =
iam-units >= 2020.4.21
- numpy >= 1.19.0, < 1.24
+ ixmp4 >= 0.2.0
+ numpy >= 1.23.0, < 1.24
requests
pyjwt
httpx[http2]
openpyxl
- # remove exception in test_io.py when changing requirement to pandas ≥ 1.5
- pandas >= 1.2.0
+ pandas >= 2.0.0
scipy
pint >= 0.13
PyYAML
- matplotlib >= 3.5.0, < 3.7.1
+ matplotlib >= 3.6.0, < 3.7.1
seaborn
six
setuptools >= 41
diff --git a/tests/test_iiasa.py b/tests/test_iiasa.py
index a98417fac..dc07bd661 100644
--- a/tests/test_iiasa.py
+++ b/tests/test_iiasa.py
@@ -6,6 +6,7 @@
import numpy as np
import numpy.testing as npt
import yaml
+from ixmp4.core.exceptions import InvalidCredentials
from pyam import IamDataFrame, iiasa, lazy_read_iiasa, read_iiasa
from pyam.utils import META_IDX
@@ -18,15 +19,6 @@
pytest.skip("IIASA database API unavailable", allow_module_level=True)
-# TODO environment variables are currently not set up on GitHub Actions
-TEST_ENV_USER = "IIASA_CONN_TEST_USER"
-TEST_ENV_PW = "IIASA_CONN_TEST_PW"
-CONN_ENV_AVAILABLE = TEST_ENV_USER in os.environ and TEST_ENV_PW in os.environ
-CONN_ENV_REASON = "Requires env variables defined: {} and {}".format(
- TEST_ENV_USER, TEST_ENV_PW
-)
-
-
FILTER_ARGS = [{}, dict(model="model_a"), dict(model=["model_a"]), dict(model="m*_a")]
VERSION_COLS = ["version", "is_default"]
@@ -79,13 +71,6 @@ def test_anon_conn(conn):
assert conn.current_connection == TEST_API_NAME
-@pytest.mark.skipif(not CONN_ENV_AVAILABLE, reason=CONN_ENV_REASON)
-def test_conn_creds_config():
- iiasa.set_config(os.environ[TEST_ENV_USER], os.environ[TEST_ENV_PW])
- conn = iiasa.Connection(TEST_API)
- assert conn.current_connection == TEST_API_NAME
-
-
def test_conn_nonexisting_creds_file():
# pointing to non-existing creds file raises
with pytest.raises(FileNotFoundError):
@@ -93,17 +78,17 @@ def test_conn_nonexisting_creds_file():
@pytest.mark.parametrize(
- "creds, match",
+ "creds, error, match",
[
- (dict(username="user", password="password"), "Credentials not valid "),
- (dict(username="user"), "Unknown API error:*."),
+ (dict(username="foo", password="bar"), InvalidCredentials, " rejected "),
+ (dict(username="user"), TypeError, "missing 1 required .* 'password'"),
],
)
-def test_conn_invalid_creds_file(creds, match, tmpdir):
+def test_conn_invalid_creds_file(creds, error, match, tmpdir):
# invalid credentials raises the expected errors
with open(tmpdir / "creds.yaml", mode="w") as f:
yaml.dump(creds, f)
- with pytest.raises(ValueError, match=match):
+ with pytest.raises(error, match=match):
iiasa.Connection(TEST_API, creds=Path(tmpdir) / "creds.yaml")
diff --git a/tests/test_io.py b/tests/test_io.py
index 3c4f38a27..330df8fe4 100644
--- a/tests/test_io.py
+++ b/tests/test_io.py
@@ -66,11 +66,7 @@ def test_io_csv_none(test_df_year):
"model_a,scen_a,World,Primary Energy|Coal,EJ/yr,0.5,3.0\n"
"model_a,scen_b,World,Primary Energy,EJ/yr,2.0,7.0\n"
)
- try:
- assert test_df_year.to_csv(lineterminator="\n") == exp
- # special treatment for pandas < 1.5 (used in the legacy tests)
- except TypeError:
- assert test_df_year.to_csv(line_terminator="\n") == exp
+ assert test_df_year.to_csv(lineterminator="\n") == exp
@pytest.mark.parametrize(