Skip to content

Commit 48e92f2

Browse files
authored
Fixes for ixmp4 v0.15.x (#970)
* adjust test_platform fixture to use the new ixmp4 Transport class * pin ixmp4>=0.15.1 * instantiate ixmp4 settings instead of using old global settings * adjust imports for renamed facade classes * Add Max Wolschlager to AUTHORS.rst * pin ixmp4>=0.15.2 * Update RELEASE_NOTES.md * fix ixmp4 auth construction * pin ixmp4>=0.15.3 * dont pass None filters to ixmp4 * pin ixmp4>=0.15.4 * fix ManagerPlatforms init and user getter * fix expected exception class * fix manager platforms list call * fix lazy_read_iiasa still using the old settings object * add tests to cover all of ixmp4.py * adjust error message regex for InvalidCredentials exception * fix SceSeAuth class to use scse-toolkit directly and fix the username assertaining logic * adjust test_platforms.py to assert simpler strings so table rendering can be changed
1 parent 0ef5694 commit 48e92f2

9 files changed

Lines changed: 778 additions & 155 deletions

File tree

AUTHORS.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ The following persons contributed to the development of the |pyam| package:
2424
- Edward Byers `@byersiiasa <https://github.com/byersiiasa>`_
2525
- Fridolin Glatter `@glatterf42 <https://github.com/glatterf42>`_
2626
- Linh Ho `@linhho <https://github.com/LinhHo>`
27+
- Max Wolschlager `@meksor <https://github.com/meksor>`
2728

2829
| The core maintenance of the |pyam| package is done by
2930
the *Scenario Services & Scientific Software* research theme

RELEASE_NOTES.md

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# Next Release
22

3+
## Individual updates
4+
5+
- [#970](https://github.com/IAMconsortium/pyam/pull/970) Fixes for ixmp4 v0.15.x
6+
37
# Release v3.3.0
48

59
## Highlights
@@ -169,7 +173,7 @@ Credentials to access the IIASA scenario database infrastructure should now be m
169173
using the **ixmp4** package
170174
(see [here](https://pyam-iamc.readthedocs.io/en/stable/api/iiasa.html)).
171175

172-
The column *exclude* of the `meta` indicators was moved to a new attribute `exclude`.
176+
The column _exclude_ of the `meta` indicators was moved to a new attribute `exclude`.
173177
All validation methods are refactored such that the argument `exclude_on_fail` changes
174178
this new attribute (see PR [#759](https://github.com/IAMconsortium/pyam/pull/759)).
175179

@@ -266,7 +270,7 @@ Bump minimum version of **pandas** to v1.2.0 to support automatic engine selecti
266270

267271
## Highlights
268272

269-
- Improved performance for writing data to *xlsx*
273+
- Improved performance for writing data to _xlsx_
270274
- Support for filtering by model-scenario pairs with an `index` argument
271275
- Better integration with the IIASA Scenario Explorer database API
272276

@@ -367,7 +371,7 @@ pandas [v1.4.0](https://pandas.pydata.org/docs/whatsnew/v1.4.0.html).
367371

368372
## Highlights
369373

370-
- Update the source code of the manuscript in *Open Research Europe* to reflect changes
374+
- Update the source code of the manuscript in _Open Research Europe_ to reflect changes
371375
based on reviewer comments
372376
- Increase the performance of the IamDataFrame initialization
373377
- Add an experimental "profiler" module for performance benchmarking

poetry.lock

Lines changed: 647 additions & 95 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyam/iiasa.py

Lines changed: 27 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,15 @@
66
from pathlib import Path
77

88
import httpx
9-
import ixmp4
109
import jwt
1110
import numpy as np
1211
import pandas as pd
1312
import requests
1413
import yaml
1514
from ixmp4.cli.platforms import tabulate_manager_platforms
16-
from ixmp4.conf import settings
17-
from ixmp4.conf.auth import ManagerAuth
15+
from ixmp4.conf.settings import Settings
1816
from requests.auth import AuthBase
17+
from toolkit.client.auth import ManagerAuth, SelfSignedAuth
1918

2019
from pyam.core import IamDataFrame
2120
from pyam.exceptions import deprecation_warning
@@ -49,16 +48,18 @@ def platforms() -> None:
4948
5049
See Also
5150
--------
52-
ixmp4.conf.settings.manager.list_platforms
51+
ixmp4.conf.platforms.ManagerPlatforms.list_platforms
5352
"""
54-
tabulate_manager_platforms(ixmp4.conf.settings.manager.list_platforms())
53+
settings = Settings()
54+
manager_platforms = settings.get_manager_platforms()
55+
tabulate_manager_platforms(manager_platforms.list_platforms())
5556

5657

5758
def _read_config(file):
5859
"""Read username and password for IIASA API connection from file"""
5960
with open(file) as stream:
6061
creds = yaml.safe_load(stream)
61-
62+
settings = Settings()
6263
return ManagerAuth(**creds, url=str(settings.manager_url))
6364

6465

@@ -68,6 +69,8 @@ def _check_response(r, msg="Error connecting to IIASA database", error=RuntimeEr
6869

6970

7071
class SceSeAuth(AuthBase):
72+
auth: ManagerAuth | SelfSignedAuth | None = None
73+
7174
def __init__(self, creds: str = None, auth_url: str = _AUTH_URL):
7275
"""Connection to the Scenario Services manager service for authentication.
7376
@@ -89,7 +92,9 @@ def __init__(self, creds: str = None, auth_url: str = _AUTH_URL):
8992
)
9093
self.auth = _read_config(DEFAULT_IIASA_CREDS)
9194
else:
92-
self.auth = ixmp4.conf.settings.default_auth
95+
settings = Settings()
96+
cred_dict = settings.get_credentials().get("default")
97+
self.auth = settings.get_client_auth(cred_dict)
9398
elif isinstance(creds, Path) or is_str(creds):
9499
deprecation_warning(f"{IXMP4_LOGIN}.", "Using a pyam-credentials file")
95100
self.auth = _read_config(creds)
@@ -98,19 +103,20 @@ def __init__(self, creds: str = None, auth_url: str = _AUTH_URL):
98103
"Passing credentials as clear-text is not allowed. "
99104
f"{IXMP4_LOGIN} instead."
100105
)
101-
102-
# self.auth is None if connection to manager service cannot be established
103-
if self.auth is None:
104-
raise httpx.ConnectError("No connection to IIASA manager service.")
105-
106106
# explicit token for anonymous login is not necessary for ixmp4 platforms
107107
# but is required for legacy Scenario Explorer databases
108-
if self.auth.user.username == "@anonymous":
108+
if self.auth is None:
109109
self._get_anonymous_token()
110110

111111
else:
112-
self.user = self.auth.user.username
113-
self.access_token = self.auth.access_token
112+
token_username = getattr(
113+
self.auth.access_token.user, "username", "@unknown"
114+
)
115+
if token_username == "@anonymous":
116+
self._get_anonymous_token()
117+
else:
118+
self.user = token_username
119+
self.access_token = self.auth.access_token
114120

115121
def _get_anonymous_token(self):
116122
r = self.client.get("/legacy/anonym/")
@@ -611,7 +617,9 @@ def read_iiasa(name, default_only=True, meta=True, creds=None, **kwargs):
611617
Credentials (username & password) are not required to access any public |ixmp4|
612618
or Scenario Explorer database (i.e., with Guest login).
613619
"""
614-
if name in [i.name for i in ixmp4.conf.settings.manager.list_platforms()]:
620+
settings = Settings()
621+
manager_platforms = settings.get_manager_platforms()
622+
if name in [i.name for i in manager_platforms.list_platforms()]:
615623
if meta is not True:
616624
raise NotImplementedError(
617625
"Reading from ixmp4 platforms requires `meta=True`"
@@ -660,9 +668,9 @@ def lazy_read_iiasa(file, name, default_only=True, meta=True, creds=None, **kwar
660668
Credentials (username & password) are not required to access any public |ixmp4|
661669
or Scenario Explorer database (i.e., with Guest login).
662670
"""
663-
if name in [
664-
platform.name for platform in ixmp4.conf.settings.manager.list_platforms()
665-
]:
671+
settings = Settings()
672+
manager_platforms = settings.get_manager_platforms()
673+
if name in [platform.name for platform in manager_platforms.list_platforms()]:
666674
raise NotImplementedError(
667675
"The function `lazy_read_iiasa()` does not support ixmp4 platforms."
668676
)

pyam/ixmp4.py

Lines changed: 28 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,12 @@
22

33
import ixmp4
44
import pandas as pd
5-
from ixmp4.core.region import RegionModel
6-
from ixmp4.core.unit import UnitModel
7-
from ixmp4.data.abstract import DataPoint
5+
from ixmp4.core.iamc import DataPoint
6+
from ixmp4.core.region import Region
7+
from ixmp4.core.unit import Unit
8+
from ixmp4.data.iamc.datapoint.filter import FacadeDataPointFilter
9+
from ixmp4.data.meta.filter import FacadeRunMetaEntryFilter
10+
from ixmp4.data.run.filter import FacadeRunFilter
811

912
logger = logging.getLogger(__name__)
1013

@@ -38,11 +41,15 @@ def read_ixmp4(
3841
if not isinstance(platform, ixmp4.Platform):
3942
platform = ixmp4.Platform(platform)
4043

44+
run_filters = FacadeRunFilter(default_only=default_only)
45+
if model is not None:
46+
run_filters["model"] = model
47+
if scenario is not None:
48+
run_filters["scenario"] = scenario
49+
4150
# TODO This may have to be revised, see https://github.com/iiasa/ixmp4/issues/72
42-
meta_filters = dict(
43-
run=dict(default_only=default_only, model=model, scenario=scenario)
44-
)
45-
iamc_filters = dict(run=dict(default_only=default_only))
51+
meta_filters = FacadeRunMetaEntryFilter(run=run_filters)
52+
iamc_filters = FacadeDataPointFilter(run=dict(default_only=default_only))
4653
for key, value in (
4754
("model", model),
4855
("scenario", scenario),
@@ -97,21 +104,24 @@ def read_run(
97104
"""
98105
from pyam import IamDataFrame
99106

100-
if year is not None:
101-
raise NotImplementedError("Filter by 'year' not implemented in ixmp4.")
107+
iamc_filters = FacadeDataPointFilter()
108+
for key, value in (
109+
("region", region),
110+
("variable", variable),
111+
("unit", unit),
112+
("year", year),
113+
):
114+
if value is not None:
115+
iamc_filters[key] = value
102116

103-
meta = pd.DataFrame.from_dict(run.meta, orient="index").T
117+
meta = pd.DataFrame.from_dict(dict(run.meta), orient="index").T
104118
meta.index = pd.MultiIndex.from_tuples(
105-
[(run.model.name, run.scenario.name)], name=["model", "scenario"]
119+
[(run.model.name, run.scenario.name)], names=["model", "scenario"]
106120
)
107121
meta["version"] = run.version
108122

109123
return IamDataFrame(
110-
data=run.iamc.tabulate(
111-
region=region,
112-
variable=variable,
113-
unit=unit,
114-
),
124+
data=run.iamc.tabulate(**iamc_filters),
115125
meta=meta,
116126
model=run.model.name,
117127
scenario=run.scenario.name,
@@ -179,8 +189,8 @@ def write_to_ixmp4(platform: ixmp4.Platform | str, df, checkpoint_message: str):
179189
def _validate_dimensions(platform, df):
180190
"""Ensure that all regions and units in the DataFrame exist in the platform"""
181191
for dimension, values, model in [
182-
("regions", df.region, RegionModel),
183-
("units", df.unit, UnitModel),
192+
("regions", df.region, Region),
193+
("units", df.unit, Unit),
184194
]:
185195
platform_values = getattr(platform, dimension).tabulate().name.values
186196
if missing := set(values).difference(platform_values):

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ classifiers = [
4040
# Please also add a section "Dependency changes" to the release notes
4141
dependencies = [
4242
"iam-units>=2020.4.21",
43-
"ixmp4>=0.13.0",
43+
"ixmp4>=0.15.4",
4444
"matplotlib>=3.6.0",
4545
"numpy>=1.26.0",
4646
"openpyxl>=3.1.2",

tests/conftest.py

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,9 @@
1111
import pandas as pd
1212
import pytest
1313
from httpx import ConnectError
14-
from ixmp4.conf.base import PlatformInfo
1514
from ixmp4.core import Platform
16-
from ixmp4.data.backend import SqliteTestBackend
15+
from ixmp4.db.models import get_metadata
16+
from ixmp4.transport import DirectTransport
1717

1818
from pyam import IamDataFrame, iiasa
1919
from pyam.utils import IAMC_IDX, META_IDX
@@ -262,19 +262,18 @@ def plot_stackplot_df():
262262

263263
@pytest.fixture(scope="function")
264264
def test_platform():
265-
sqlite = SqliteTestBackend(
266-
PlatformInfo(name="sqlite-test", dsn="sqlite:///:memory:")
267-
)
268-
sqlite.setup()
265+
transport = DirectTransport.from_dsn("sqlite:///:memory:")
266+
bind = transport.session.bind
267+
meta = get_metadata()
268+
meta.create_all(bind=bind, tables=meta.sorted_tables, checkfirst=True)
269269

270-
platform = Platform(_backend=sqlite)
270+
platform = Platform(transport)
271271
platform.regions.create(name="World", hierarchy="common")
272272
platform.units.create(name="EJ/yr")
273273

274274
yield platform
275275

276-
sqlite.close()
277-
sqlite.teardown()
276+
meta.drop_all(bind=bind, checkfirst=True)
278277

279278

280279
@pytest.fixture(scope="session")

tests/test_iiasa.py

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
import pandas.testing as pdt
88
import pytest
99
import yaml
10-
from ixmp4.core.exceptions import InvalidCredentials
10+
from toolkit.exceptions import InvalidCredentials
1111

1212
from pyam import IamDataFrame, iiasa, lazy_read_iiasa, read_iiasa
1313
from pyam.testing import assert_iamframe_equal
@@ -58,10 +58,9 @@
5858
def test_platforms(capsys):
5959
# test that the function does not raise an error
6060
iiasa.platforms()
61-
assert (
62-
"public-test public This is a public ixmp4 test instance"
63-
in capsys.readouterr().out
64-
)
61+
output = capsys.readouterr().out
62+
assert "Public Testing Instance" in output
63+
assert "This is a public ixmp4 test instance" in output
6564

6665

6766
def test_unknown_conn():
@@ -89,7 +88,7 @@ def test_conn_nonexisting_creds_file():
8988
@pytest.mark.parametrize(
9089
"creds, error, match",
9190
[
92-
(dict(username="foo", password="bar"), InvalidCredentials, " rejected "),
91+
(dict(username="foo", password="bar"), InvalidCredentials, "rejected"),
9392
(dict(username="user"), TypeError, "missing 1 required .* 'password'"),
9493
],
9594
)

tests/test_ixmp4.py

Lines changed: 54 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
1+
from unittest.mock import patch
2+
3+
import ixmp4 as ixmp4_module
14
import pytest
2-
from ixmp4.core.region import RegionModel
3-
from ixmp4.core.unit import UnitModel
5+
from ixmp4.core.region import Region
6+
from ixmp4.core.unit import Unit
47

58
import pyam
69
from pyam import read_ixmp4
@@ -11,14 +14,14 @@
1114
def test_to_ixmp4_missing_region_raises(test_platform, test_df_year):
1215
"""Writing to platform raises if region not defined"""
1316
test_df_year.rename(region={"World": "foo"}, inplace=True)
14-
with pytest.raises(RegionModel.NotFound, match="foo. Use `Platform.regions."):
17+
with pytest.raises(Region.NotFound, match="foo. Use `Platform.regions."):
1518
test_df_year.to_ixmp4(platform=test_platform)
1619

1720

1821
def test_to_ixmp4_missing_unit_raises(test_platform, test_df_year):
1922
"""Writing to platform raises if unit not defined"""
2023
test_df_year.rename(unit={"EJ/yr": "foo"}, inplace=True)
21-
with pytest.raises(UnitModel.NotFound, match="foo. Use `Platform.units."):
24+
with pytest.raises(Unit.NotFound, match="foo. Use `Platform.units."):
2225
test_df_year.to_ixmp4(platform=test_platform)
2326

2427

@@ -131,3 +134,50 @@ def test_ixmp4_read_run(test_platform, test_df):
131134
exp = test_df.filter(scenario="scen_a")
132135
exp.set_meta(1, "version") # the IamDataFrame read from ixmp4 includes the version
133136
pyam.assert_iamframe_equal(exp, obs)
137+
138+
139+
def test_ixmp4_read_run_with_filters(test_platform, test_df_year):
140+
"""Read a run with filters"""
141+
142+
test_df_year.to_ixmp4(platform=test_platform)
143+
144+
run = test_platform.runs.get("model_a", "scen_a")
145+
obs = read_run(run, region="World", variable="Primary Energy", year=2010)
146+
147+
exp = test_df_year.filter(
148+
scenario="scen_a", region="World", variable="Primary Energy", year=2010
149+
)
150+
exp.set_meta(1, "version")
151+
pyam.assert_iamframe_equal(exp, obs)
152+
153+
154+
def test_read_ixmp4_string_platform(test_platform, test_df_year):
155+
"""read_ixmp4 converts a string platform name to a Platform instance"""
156+
157+
test_df_year.to_ixmp4(platform=test_platform)
158+
159+
class FakePlatform:
160+
def __new__(cls, name):
161+
return test_platform
162+
163+
with patch.object(ixmp4_module, "Platform", FakePlatform):
164+
obs = read_ixmp4(platform="test-platform-name")
165+
166+
test_df_year.set_meta(1, "version")
167+
pyam.assert_iamframe_equal(test_df_year, obs)
168+
169+
170+
def test_write_to_ixmp4_string_platform(test_platform, test_df_year):
171+
"""write_to_ixmp4 converts a string platform name to a Platform instance"""
172+
173+
class FakePlatform:
174+
def __new__(cls, name):
175+
return test_platform
176+
177+
with patch.object(ixmp4_module, "Platform", FakePlatform):
178+
test_df_year.to_ixmp4(platform="test-platform-name")
179+
180+
# verify data was written correctly
181+
obs = read_ixmp4(platform=test_platform)
182+
test_df_year.set_meta(1, "version")
183+
pyam.assert_iamframe_equal(test_df_year, obs)

0 commit comments

Comments
 (0)