Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions doc/changes/unreleased.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ Besides there are many formal and syntactic changes

## Summary

* #147: Added `num_nodes` support to the handwritten database creation helpers.

## Refactorings

* #160: Updated PTB to version 7.0.0
39 changes: 39 additions & 0 deletions exasol/saas/client/api_access.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
create_database,
delete_database,
get_database,
get_database_settings,
list_databases,
)
from exasol.saas.client.openapi.api.security import (
Expand Down Expand Up @@ -310,6 +311,7 @@ def create_database(
cluster_size: str = "XS",
region: str = "eu-central-1",
idle_time: timedelta | None = None,
num_nodes: int | None = None,
) -> ExasolDatabase | None:
def minutes(x: timedelta) -> int:
return x.seconds // 60
Expand All @@ -331,6 +333,8 @@ def minutes(x: timedelta) -> int:
region=region,
stream_type="innovation-release",
)
if num_nodes is not None:
database_spec.num_nodes = num_nodes
resp = create_database.sync(
self._account_id,
client=self._client,
Expand Down Expand Up @@ -500,12 +504,14 @@ def database(
keep: bool = False,
ignore_delete_failure: bool = False,
idle_time: timedelta | None = None,
num_nodes: int | None = None,
):
db = None
try:
db = self.create_database(
name,
idle_time=idle_time,
num_nodes=num_nodes,
)
yield db
finally:
Expand Down Expand Up @@ -537,6 +543,39 @@ def get_database(
ExasolDatabase, resp, f"Failed to get database {database_id}"
)

def get_database_settings(
self,
database_id: str,
) -> openapi.models.DatabaseSettings | None:
def is_retry(resp: ApiError) -> bool:
return _is_not_found(resp)

@interval_retry(
interval=timedelta(seconds=5),
timeout=timedelta(minutes=2),
)
def retrieve_settings() -> openapi.models.DatabaseSettings:
resp = get_database_settings.sync(
self._account_id,
database_id,
client=self._client,
)
_log_api_output(
"get_database_settings.sync",
resp,
account_id=self._account_id,
database_id=database_id,
)
if isinstance(resp, ApiError) and is_retry(resp):
raise TryAgain
return ensure_type(
openapi.models.DatabaseSettings,
resp,
f"Failed to get settings of database {database_id}",
)

return retrieve_settings()

def wait_until_running(
self,
database_id: str,
Expand Down
9 changes: 9 additions & 0 deletions test/integration/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,15 @@ def database_name(project_short_tag) -> str:
return timestamp_name(project_short_tag)


@pytest.fixture
def local_name(project_short_tag: str | None) -> str:
"""
Other than global fixture database_name this fixture uses scope
"function" to generate an individual name for each test case.
"""
return timestamp_name(project_short_tag)


@pytest.fixture(scope="session")
def allow_connection(api_access) -> None:
"""
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,10 @@
import logging
from datetime import (
datetime,
timedelta,
)
from datetime import timedelta

import pytest
from tenacity import RetryError

from exasol.saas.client import PROMISING_STATES
from exasol.saas.client.api_access import (
timestamp_name,
)
from exasol.saas.client.openapi.models.exasol_database import ExasolDatabase

LOG = logging.getLogger(__name__)
Expand All @@ -20,15 +14,6 @@
)


@pytest.fixture
def local_name(project_short_tag: str | None) -> str:
"""
Other than global fixture database_name this fixture uses scope
"function" to generate an individual name for each test case in this file.
"""
return timestamp_name(project_short_tag)


def test_lifecycle(api_access, local_name):
"""
This integration test uses the database created and provided by pytest
Expand All @@ -52,20 +37,16 @@ def get_connection(db: ExasolDatabase):
return api_access.get_connection(db.id, clusters[0].id)

with api_access.database(local_name, ignore_delete_failure=True) as db:
start = datetime.now()
# verify state and clusters of created database
assert db.status in PROMISING_STATES and db.clusters.total == 1

with pytest.raises(RetryError):
wait_until_running_too_short(db)

# verify database is listed
assert db.id in api_access.list_database_ids()

con = get_connection(db)
assert con.db_username is not None and con.port == 8563

# delete database and verify database is not listed anymore
api_access.delete_database(db.id)
api_access.wait_until_deleted(db.id)
assert db.id not in api_access.list_database_ids()
14 changes: 14 additions & 0 deletions test/integration/test_database_num_nodes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
def test_create_database_with_two_nodes(api_access, local_name):
"""
This integration test verifies that the handwritten helper forwards
`num_nodes` and the created database reports that setting back.
"""
with api_access.database(
local_name,
ignore_delete_failure=True,
num_nodes=2,
) as db:
settings = api_access.get_database_settings(db.id)

assert settings is not None
assert settings.num_nodes == 2
92 changes: 92 additions & 0 deletions test/unit/test_api_access.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,22 @@ def delete_mock(monkeypatch, side_effect) -> Mock:
return mock


def create_database_mock(monkeypatch, side_effect) -> Mock:
from exasol.saas.client.api_access import create_database as api

mock = Mock(side_effect=side_effect)
monkeypatch.setattr(api, "sync", mock)
return mock


def get_database_settings_mock(monkeypatch, side_effect) -> Mock:
from exasol.saas.client.api_access import get_database_settings as api

mock = Mock(side_effect=side_effect)
monkeypatch.setattr(api, "sync", mock)
return mock


def list_allowed_ips_mock(monkeypatch, side_effect) -> Mock:
from exasol.saas.client.api_access import list_allowed_i_ps as api

Expand Down Expand Up @@ -207,6 +223,82 @@ def test_timestamp_name() -> None:
assert all(tag == "TEST" for tag in tags)


@pytest.mark.parametrize(
"num_nodes, expected_num_nodes",
[
pytest.param(None, UNSET, id="uses_backend_default"),
pytest.param(2, 2, id="forwards_explicit_value"),
],
)
def test_create_database_num_nodes(
api_mock, monkeypatch, num_nodes, expected_num_nodes
) -> None:
create = create_database_mock(
monkeypatch,
[database_response("db-with-nodes")],
)

result = api_mock.create_database("db-with-nodes", num_nodes=num_nodes)

assert result is not None
assert create.called
body = create.call_args.kwargs["body"]
assert body.num_nodes == expected_num_nodes


def test_database_context_forwards_num_nodes(api_mock, monkeypatch) -> None:
create = Mock(return_value=database_response("db-with-context"))
delete = Mock()
monkeypatch.setattr(api_mock, "create_database", create)
monkeypatch.setattr(api_mock, "delete_database", delete)

with api_mock.database("db-with-context", num_nodes=2) as db:
assert db is not None
assert db.name == "db-with-context"

assert create.call_args.args == ("db-with-context",)
assert create.call_args.kwargs == {"idle_time": None, "num_nodes": 2}
delete.assert_called_once_with("db-id", False)


def test_get_database_settings_retries_transient_not_found(
api_mock, monkeypatch
) -> None:
monkeypatch.setattr(
"exasol.saas.client.api_access.interval_retry",
immediate_retry,
)
get_settings = get_database_settings_mock(
monkeypatch,
[
api_error(404, "User/Database not found"),
database_settings_response(),
],
)

result = api_mock.get_database_settings("db-id")

assert result is not None
assert result.num_nodes == 2
assert get_settings.call_count == 2


def test_get_database_settings_raises_non_retryable_error(
api_mock, monkeypatch
) -> None:
monkeypatch.setattr(
"exasol.saas.client.api_access.interval_retry",
lambda *_args, **_kwargs: (lambda func: func),
)
get_database_settings_mock(
monkeypatch,
[api_error(500, "boom")],
)

with pytest.raises(OpenApiError, match="Failed to get settings of database db-id"):
api_mock.get_database_settings("db-id")


def test_log_api_output_serializes_payloads(caplog) -> None:
caplog.set_level(logging.DEBUG, logger="exasol.saas.client.api_access")

Expand Down
Loading