Skip to content

Commit 6729cef

Browse files
✨ Capture osparc envs automatically using a pydantic model (#170)
* cleanup * overrides Config with new defaults * testing config * upgrades pre-commit * mv pytest.ini * monkey patch * update workflow before publishing python package fix dependency issue and bump version point to website in project description fix broken dependency improve doc add github token to download artifacts ensure only read-access @wvangeit yet another attempt at downloading artifacts make sure to use repo that ran the trigger wf another attempt at fixing change owner allow publishing to testpypi also when pr minor change revert minor (but breaking) change minor fix add debug messages another debug message hopefully the final version final fix minor fix move master and tag to individual jobs add debug messages add python script for determining semantic version minor changes minor changes improve error handling and add version file to artifacts check if release minor fix ensure to enter venv also when tagging source venv in publishin workflow ensure only master add script for testing 'pure' semver adapt workflows to new python script minor change attempt to evaluate expressions correctly several fixes to fix tests ensure repo is checked out in publish workflow several small fixes cleanup debug minor cleanup mionr changes add debug message minor change minor change yet another try minor change minor change minor change mionr change minor changes correct workflow run id cosmetic change avoid using gh change to a single job for publishing minor cleanup swap loops in clean up jobs correction get correct versions of github workflow files update a couple of other files update a few more tests update yet another file yet another file * wrap api_client * add type * remove _configuration.py * avoid bad import * fix import * fix e2e tests * add unit test * require api_client in wrapped apis * fix unit tests --------- Co-authored-by: Pedro Crespo-Valero <[email protected]>
1 parent 6c7a019 commit 6729cef

File tree

13 files changed

+197
-40
lines changed

13 files changed

+197
-40
lines changed

clients/python/client/osparc/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44

55
import nest_asyncio
66
from osparc_client import ( # APIs; API client; models
7-
ApiClient,
87
ApiException,
98
ApiKeyError,
109
ApiTypeError,
@@ -38,6 +37,7 @@
3837
)
3938
from packaging.version import Version
4039

40+
from ._api_client import ApiClient
4141
from ._exceptions import RequestError, VisibleDeprecationWarning
4242
from ._files_api import FilesApi
4343
from ._info import openapi
@@ -114,4 +114,4 @@
114114
"UsersApi",
115115
"UsersGroup",
116116
"ValidationError",
117-
)
117+
) # type: ignore
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
from typing import Optional
2+
3+
from osparc_client import ApiClient as _ApiClient
4+
from osparc_client import Configuration
5+
from pydantic import ValidationError
6+
7+
from ._models import ConfigurationModel
8+
9+
10+
class ApiClient(_ApiClient):
11+
def __init__(
12+
self,
13+
configuration: Optional[Configuration] = None,
14+
header_name=None,
15+
header_value=None,
16+
cookie=None,
17+
pool_threads=1,
18+
):
19+
if configuration is None:
20+
try:
21+
env_vars = ConfigurationModel()
22+
configuration = Configuration(
23+
host=f"{env_vars.OSPARC_API_HOST}".rstrip(
24+
"/"
25+
), # https://github.com/pydantic/pydantic/issues/7186
26+
username=env_vars.OSPARC_API_KEY,
27+
password=env_vars.OSPARC_API_SECRET,
28+
)
29+
except ValidationError as exc:
30+
raise RuntimeError(
31+
"Could not initialize configuration from environment. "
32+
"If your osparc host, key and secret are not exposed as "
33+
"environment variables you must construct the "
34+
"Configuration object explicitly"
35+
) from exc
36+
37+
super().__init__(configuration, header_name, header_value, cookie, pool_threads)

clients/python/client/osparc/_files_api.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@
2121
from tqdm.asyncio import tqdm
2222
from tqdm.contrib.logging import logging_redirect_tqdm
2323

24-
from . import ApiClient, File
24+
from . import File
25+
from ._api_client import ApiClient
2526
from ._http_client import AsyncHttpClient
2627
from ._utils import (
2728
DEFAULT_TIMEOUT_SECONDS,
@@ -36,7 +37,7 @@
3637
class FilesApi(_FilesApi):
3738
"""Class for interacting with files"""
3839

39-
def __init__(self, api_client: Optional[ApiClient] = None):
40+
def __init__(self, api_client: ApiClient):
4041
"""Construct object
4142
4243
Args:

clients/python/client/osparc/_models.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from typing import Optional
22
from uuid import UUID
33

4-
from pydantic import Field, field_validator
4+
from pydantic import AnyHttpUrl, Field, field_validator
55
from pydantic_settings import BaseSettings
66

77

@@ -18,7 +18,19 @@ class ParentProjectInfo(BaseSettings):
1818

1919
@field_validator("x_simcore_parent_project_uuid", "x_simcore_parent_node_id")
2020
@classmethod
21-
def _validate_uuids(cls, v: Optional[str]) -> str:
21+
def _validate_uuids(cls, v: Optional[str]) -> Optional[str]:
2222
if v is not None:
2323
_ = UUID(v)
2424
return v
25+
26+
27+
class ConfigurationModel(BaseSettings):
28+
"""Model for capturing env vars which should go into the Configuration"""
29+
30+
OSPARC_API_HOST: AnyHttpUrl = Field(
31+
default=...,
32+
description="OSPARC api url",
33+
examples=["https://api.osparc-master.speag.com/"],
34+
)
35+
OSPARC_API_KEY: str = Field(default=..., description="OSPARC api key")
36+
OSPARC_API_SECRET: str = Field(default=..., description="OSPARC api secret")

clients/python/client/osparc/_solvers_api.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from osparc_client import JobInputs, OnePageSolverPort, SolverPort
55
from osparc_client import SolversApi as _SolversApi
66

7-
from . import ApiClient
7+
from ._api_client import ApiClient
88
from ._models import ParentProjectInfo
99
from ._utils import (
1010
_DEFAULT_PAGINATION_LIMIT,
@@ -27,7 +27,7 @@ def __getattr__(self, name: str) -> Any:
2727
raise NotImplementedError(f"SolversApi.{name} is still under development")
2828
return super().__getattribute__(name)
2929

30-
def __init__(self, api_client: Optional[ApiClient] = None):
30+
def __init__(self, api_client: ApiClient):
3131
"""Construct object
3232
3333
Args:

clients/python/client/osparc/_studies_api.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,11 @@
55
from typing import Any, Optional
66

77
import httpx
8-
from osparc_client import ApiClient, JobInputs, JobLogsMap, PageStudy
8+
from osparc_client import JobInputs, JobLogsMap, PageStudy
99
from osparc_client import StudiesApi as _StudiesApi
1010
from tqdm.asyncio import tqdm_asyncio
1111

12+
from ._api_client import ApiClient
1213
from ._http_client import AsyncHttpClient
1314
from ._models import ParentProjectInfo
1415
from ._utils import (
@@ -39,7 +40,7 @@ class StudiesApi(_StudiesApi):
3940
"stop_study_job",
4041
]
4142

42-
def __init__(self, api_client: Optional[ApiClient] = None):
43+
def __init__(self, api_client: ApiClient):
4344
"""Construct object
4445
4546
Args:

clients/python/test/e2e/conftest.py

Lines changed: 74 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
# pylint: disable=unused-argument
55
# pylint: disable=unused-variable
66

7+
import datetime
78
import logging
89
import os
910
from pathlib import Path
@@ -14,37 +15,93 @@
1415
import pytest
1516
from httpx import AsyncClient, BasicAuth
1617
from numpy import random
18+
from osparc._models import ConfigurationModel
1719
from pydantic import ByteSize
1820

1921
_KB: ByteSize = ByteSize(1024) # in bytes
2022
_MB: ByteSize = ByteSize(_KB * 1024) # in bytes
2123
_GB: ByteSize = ByteSize(_MB * 1024) # in bytes
2224

2325

24-
@pytest.fixture
25-
def configuration() -> osparc.Configuration:
26-
assert (host := os.environ.get("OSPARC_API_HOST"))
27-
assert (username := os.environ.get("OSPARC_API_KEY"))
28-
assert (password := os.environ.get("OSPARC_API_SECRET"))
29-
return osparc.Configuration(
30-
host=host,
31-
username=username,
32-
password=password,
33-
)
26+
# Dictionary to store start times of tests
27+
_test_start_times = {}
28+
29+
30+
def _utc_now():
31+
return datetime.datetime.now(tz=datetime.timezone.utc)
32+
33+
34+
def _construct_graylog_url(api_host, start_time, end_time):
35+
"""
36+
Construct a Graylog URL for the given time interval.
37+
"""
38+
base_url = api_host.replace("api.", "monitoring.", 1).rstrip("/")
39+
url = f"{base_url}/graylog/search"
40+
start_time_str = start_time.strftime("%Y-%m-%dT%H:%M:%S.%fZ")
41+
end_time_str = end_time.strftime("%Y-%m-%dT%H:%M:%S.%fZ")
42+
query = f"from={start_time_str}&to={end_time_str}"
43+
return f"{url}?{query}"
44+
45+
46+
def pytest_runtest_setup(item):
47+
"""
48+
Hook to capture the start time of each test.
49+
"""
50+
_test_start_times[item.name] = _utc_now()
51+
52+
53+
def pytest_runtest_makereport(item, call):
54+
"""
55+
Hook to add extra information when a test fails.
56+
"""
57+
if call.when == "call":
58+
# Check if the test failed
59+
if call.excinfo is not None:
60+
test_name = item.name
61+
test_location = item.location
62+
api_host = os.environ.get("OSPARC_API_HOST", "")
63+
64+
diagnostics = {
65+
"test_name": test_name,
66+
"test_location": test_location,
67+
"api_host": api_host,
68+
}
69+
70+
# Get the start and end times of the test
71+
start_time = _test_start_times.get(test_name)
72+
end_time = _utc_now()
73+
74+
if start_time:
75+
diagnostics["graylog_url"] = _construct_graylog_url(
76+
api_host, start_time, end_time
77+
)
78+
79+
# Print the diagnostics
80+
print(f"\nDiagnostics for {test_name}:")
81+
for key, value in diagnostics.items():
82+
print(" ", key, ":", value)
83+
84+
85+
@pytest.hookimpl(tryfirst=True)
86+
def pytest_configure(config):
87+
config.pluginmanager.register(pytest_runtest_setup, "osparc_test_times_plugin")
88+
config.pluginmanager.register(pytest_runtest_makereport, "osparc_makereport_plugin")
3489

3590

3691
@pytest.fixture
37-
def api_client(configuration: osparc.Configuration) -> Iterable[osparc.ApiClient]:
38-
with osparc.ApiClient(configuration=configuration) as _api_client:
39-
yield _api_client
92+
def api_client() -> Iterable[osparc.ApiClient]:
93+
with osparc.ApiClient() as api_client:
94+
yield api_client
4095

4196

4297
@pytest.fixture
43-
def async_client(configuration: osparc.Configuration) -> AsyncClient:
44-
return AsyncClient(
45-
base_url=configuration.host,
98+
def async_client() -> Iterable[AsyncClient]:
99+
configuration = ConfigurationModel()
100+
yield AsyncClient(
101+
base_url=f"{configuration.OSPARC_API_HOST}".rstrip("/"),
46102
auth=BasicAuth(
47-
username=configuration.username, password=configuration.password
103+
username=configuration.OSPARC_API_KEY,
104+
password=configuration.OSPARC_API_SECRET,
48105
),
49106
) # type: ignore
50107

clients/python/test/test_osparc/conftest.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import pytest
33
from faker import Faker
44

5+
56
@pytest.fixture
67
def cfg(faker: Faker) -> osparc.Configuration:
78
return osparc.Configuration(
@@ -12,5 +13,10 @@ def cfg(faker: Faker) -> osparc.Configuration:
1213

1314

1415
@pytest.fixture
15-
def dev_mode_enabled(monkeypatch:pytest.MonkeyPatch):
16+
def api_client(cfg: osparc.Configuration) -> osparc.ApiClient:
17+
return osparc.ApiClient(configuration=cfg)
18+
19+
20+
@pytest.fixture
21+
def dev_mode_enabled(monkeypatch: pytest.MonkeyPatch) -> None:
1622
monkeypatch.setenv("OSPARC_DEV_FEATURES_ENABLED", "1")

clients/python/test/test_osparc/test_apis.py

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,11 @@
44
# pylint: disable=too-many-arguments
55

66
import os
7-
from typing import Callable
7+
from typing import Callable, Optional
88

99
import pytest
1010
from faker import Faker
11-
from osparc import SolversApi, StudiesApi
11+
from osparc import ApiClient, SolversApi, StudiesApi
1212
from pytest_mock import MockerFixture
1313

1414

@@ -31,6 +31,7 @@ def test_create_jobs_parent_headers(
3131
create_parent_env: Callable,
3232
dev_mode_enabled: None,
3333
parent_env: bool,
34+
api_client: ApiClient,
3435
):
3536
create_parent_env(parent_env)
3637

@@ -58,9 +59,43 @@ def check_headers(**kwargs):
5859
side_effect=lambda study_id, **kwargs: check_headers(**kwargs),
5960
)
6061

61-
solvers_api = SolversApi()
62+
solvers_api = SolversApi(api_client=api_client)
6263
solvers_api.create_job(solver_key="mysolver", version="1.2.3", job_inputs={})
6364

64-
studies_api = StudiesApi()
65+
studies_api = StudiesApi(api_client=api_client)
6566
studies_api.create_study_job(study_id=faker.uuid4(), job_inputs={})
6667
studies_api.clone_study(study_id=faker.uuid4())
68+
69+
70+
@pytest.mark.parametrize(
71+
"OSPARC_API_HOST", ["https://api.foo.com", "https://api.bar.com/", None]
72+
)
73+
@pytest.mark.parametrize("OSPARC_API_KEY", ["key", None])
74+
@pytest.mark.parametrize("OSPARC_API_SECRET", ["secret", None])
75+
def test_api_client_constructor(
76+
monkeypatch: pytest.MonkeyPatch,
77+
OSPARC_API_HOST: Optional[str],
78+
OSPARC_API_KEY: Optional[str],
79+
OSPARC_API_SECRET: Optional[str],
80+
):
81+
with monkeypatch.context() as patch:
82+
patch.delenv("OSPARC_API_HOST", raising=False)
83+
patch.delenv("OSPARC_API_KEY", raising=False)
84+
patch.delenv("OSPARC_API_SECRET", raising=False)
85+
86+
if OSPARC_API_HOST is not None:
87+
patch.setenv("OSPARC_API_HOST", OSPARC_API_HOST)
88+
if OSPARC_API_KEY is not None:
89+
patch.setenv("OSPARC_API_KEY", OSPARC_API_KEY)
90+
if OSPARC_API_SECRET is not None:
91+
patch.setenv("OSPARC_API_SECRET", OSPARC_API_SECRET)
92+
93+
if OSPARC_API_HOST and OSPARC_API_KEY and OSPARC_API_SECRET:
94+
api = ApiClient()
95+
assert api.configuration.host == OSPARC_API_HOST.rstrip("/")
96+
assert api.configuration.username == OSPARC_API_KEY
97+
assert api.configuration.password == OSPARC_API_SECRET
98+
99+
else:
100+
with pytest.raises(RuntimeError):
101+
ApiClient()

clients/python/test/test_osparc/test_osparc_client/test_files_api.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,16 @@
1414

1515
import unittest
1616

17-
from osparc import FilesApi # noqa: E501
17+
from osparc import ApiClient, Configuration, FilesApi # noqa: E501
1818

1919

2020
class TestFilesApi(unittest.TestCase):
2121
"""FilesApi unit test stubs"""
2222

2323
def setUp(self):
24-
self.api = FilesApi() # noqa: E501
24+
self.api = FilesApi(
25+
api_client=ApiClient(configuration=Configuration())
26+
) # noqa: E501
2527

2628
def tearDown(self):
2729
pass

0 commit comments

Comments
 (0)