From ec4f06cabe86220fb61492b491d0c068db568335 Mon Sep 17 00:00:00 2001 From: mibe Date: Tue, 10 Feb 2026 16:23:09 +0000 Subject: [PATCH 1/3] #55 Added database idle time --- doc/changes/unreleased.md | 4 ++++ exasol/saas/client/api_access.py | 16 ++++++++++++++-- test/unit/test_api_access.py | 14 ++++++++++++++ 3 files changed, 32 insertions(+), 2 deletions(-) diff --git a/doc/changes/unreleased.md b/doc/changes/unreleased.md index 8e31d73..97416b2 100644 --- a/doc/changes/unreleased.md +++ b/doc/changes/unreleased.md @@ -7,3 +7,7 @@ tbd. ## Refactorings * #128: Explicitly configured validation of SaaS SSL certificates in pyexasol connection + +## Bug fixing + +* #135: Made the database name semi-unique. \ No newline at end of file diff --git a/exasol/saas/client/api_access.py b/exasol/saas/client/api_access.py index e8eaafb..3f11b21 100644 --- a/exasol/saas/client/api_access.py +++ b/exasol/saas/client/api_access.py @@ -60,9 +60,21 @@ def interval_retry(interval: timedelta, timeout: timedelta): def timestamp_name(project_short_tag: str | None = None) -> str: """ - project_short_tag: Abbreviation of your project + Generates a semi-unique name for a database with the following format: + - 0-4: number of minutes since the start of the year in hex, + - 0-5: a semi-random number, + - provided tag, + - -username. + + Args: + project_short_tag: Abbreviation of your project """ - timestamp = f"{datetime.now(timezone.utc).timestamp():.0f}" + now = datetime.now() + year_start = datetime(now.year, 1, 1) + minutes_elapsed = int((now - year_start).total_seconds() // 60) + random_suffix = time.time_ns() % 1048576 + timestamp = f"{minutes_elapsed:05x}{random_suffix:05x}" + owner = getpass.getuser() candidate = f"{timestamp}{project_short_tag or ''}-{owner}" return candidate[: Limits.MAX_DATABASE_NAME_LENGTH] diff --git a/test/unit/test_api_access.py b/test/unit/test_api_access.py index f936705..3cbfc3b 100644 --- a/test/unit/test_api_access.py +++ b/test/unit/test_api_access.py @@ -7,6 +7,7 @@ from exasol.saas.client.api_access import ( OpenApiAccess, indicates_retry, + timestamp_name ) from exasol.saas.client.openapi.errors import UnexpectedStatus @@ -121,3 +122,16 @@ def test_delete_success( ) assert api_runner.mock.called assert expected_log_message in caplog.text + + +def test_timestamp_name() -> None: + names = [timestamp_name('TEST') for _ in range(3)] + minutes = [int(name[:5], 16) for name in names] + suffixes = [int(name[5:10], 16) for name in names] + tags = [name[10:14] for name in names] + # minutes from the start of the year should be the same + assert minutes[0] == minutes[1] or minutes[1] == minutes[2] + # suffixes should all be different + assert len(set(suffixes)) == 3 + # the provided tag should follow the hacky timestamp. + assert all(tag == 'TEST' for tag in tags) From af5980069f971183738c644538b5ade91859ed85 Mon Sep 17 00:00:00 2001 From: mibe Date: Wed, 11 Feb 2026 08:13:45 +0000 Subject: [PATCH 2/3] #135 re-generated the API --- .../client/openapi/models/create_cluster.py | 9 + .../models/create_database_initial_cluster.py | 9 + .../client/openapi/models/scale_cluster.py | 10 + openapi.json | 213 +++++++++--------- test/unit/test_api_access.py | 6 +- 5 files changed, 142 insertions(+), 105 deletions(-) diff --git a/exasol/saas/client/openapi/models/create_cluster.py b/exasol/saas/client/openapi/models/create_cluster.py index c09c60e..bf7b9ab 100644 --- a/exasol/saas/client/openapi/models/create_cluster.py +++ b/exasol/saas/client/openapi/models/create_cluster.py @@ -35,12 +35,14 @@ class CreateCluster: Attributes: name (str): size (str): + family (Union[Unset, str]): auto_stop (Union[Unset, AutoStop]): settings (Union[Unset, ClusterSettingsUpdate]): """ name: str size: str + family: Union[Unset, str] = UNSET auto_stop: Union[Unset, "AutoStop"] = UNSET settings: Union[Unset, "ClusterSettingsUpdate"] = UNSET @@ -52,6 +54,8 @@ def to_dict(self) -> dict[str, Any]: size = self.size + family = self.family + auto_stop: Union[Unset, dict[str, Any]] = UNSET if not isinstance(self.auto_stop, Unset): auto_stop = self.auto_stop.to_dict() @@ -68,6 +72,8 @@ def to_dict(self) -> dict[str, Any]: "size": size, } ) + if family is not UNSET: + field_dict["family"] = family if auto_stop is not UNSET: field_dict["autoStop"] = auto_stop if settings is not UNSET: @@ -85,6 +91,8 @@ def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: size = d.pop("size") + family = d.pop("family", UNSET) + _auto_stop = d.pop("autoStop", UNSET) auto_stop: Union[Unset, AutoStop] if isinstance(_auto_stop, Unset): @@ -102,6 +110,7 @@ def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: create_cluster = cls( name=name, size=size, + family=family, auto_stop=auto_stop, settings=settings, ) diff --git a/exasol/saas/client/openapi/models/create_database_initial_cluster.py b/exasol/saas/client/openapi/models/create_database_initial_cluster.py index bb2cd90..4405a68 100644 --- a/exasol/saas/client/openapi/models/create_database_initial_cluster.py +++ b/exasol/saas/client/openapi/models/create_database_initial_cluster.py @@ -35,12 +35,14 @@ class CreateDatabaseInitialCluster: Attributes: name (str): size (str): + family (Union[Unset, str]): auto_stop (Union[Unset, AutoStop]): settings (Union[Unset, ClusterSettingsUpdate]): """ name: str size: str + family: Union[Unset, str] = UNSET auto_stop: Union[Unset, "AutoStop"] = UNSET settings: Union[Unset, "ClusterSettingsUpdate"] = UNSET @@ -52,6 +54,8 @@ def to_dict(self) -> dict[str, Any]: size = self.size + family = self.family + auto_stop: Union[Unset, dict[str, Any]] = UNSET if not isinstance(self.auto_stop, Unset): auto_stop = self.auto_stop.to_dict() @@ -68,6 +72,8 @@ def to_dict(self) -> dict[str, Any]: "size": size, } ) + if family is not UNSET: + field_dict["family"] = family if auto_stop is not UNSET: field_dict["autoStop"] = auto_stop if settings is not UNSET: @@ -85,6 +91,8 @@ def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: size = d.pop("size") + family = d.pop("family", UNSET) + _auto_stop = d.pop("autoStop", UNSET) auto_stop: Union[Unset, AutoStop] if isinstance(_auto_stop, Unset): @@ -102,6 +110,7 @@ def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: create_database_initial_cluster = cls( name=name, size=size, + family=family, auto_stop=auto_stop, settings=settings, ) diff --git a/exasol/saas/client/openapi/models/scale_cluster.py b/exasol/saas/client/openapi/models/scale_cluster.py index 609e714..85ee826 100644 --- a/exasol/saas/client/openapi/models/scale_cluster.py +++ b/exasol/saas/client/openapi/models/scale_cluster.py @@ -9,6 +9,7 @@ Optional, TextIO, TypeVar, + Union, ) from attrs import define as _attrs_define @@ -27,13 +28,17 @@ class ScaleCluster: """ Attributes: size (str): + family (Union[Unset, str]): """ size: str + family: Union[Unset, str] = UNSET def to_dict(self) -> dict[str, Any]: size = self.size + family = self.family + field_dict: dict[str, Any] = {} field_dict.update( @@ -41,6 +46,8 @@ def to_dict(self) -> dict[str, Any]: "size": size, } ) + if family is not UNSET: + field_dict["family"] = family return field_dict @@ -49,8 +56,11 @@ def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: d = dict(src_dict) size = d.pop("size") + family = d.pop("family", UNSET) + scale_cluster = cls( size=size, + family=family, ) return scale_cluster diff --git a/openapi.json b/openapi.json index 19f263b..bfe6109 100644 --- a/openapi.json +++ b/openapi.json @@ -6,7 +6,7 @@ "version": "1.0", "download": { "source": "https://cloud.exasol.com/openapi.json", - "timestamp": "2025-11-27T09:52:30.043415+00:00" + "timestamp": "2026-02-11T08:12:54.599117+00:00" } }, "servers": [ @@ -628,6 +628,107 @@ ] } }, + "/api/v1/accounts/{accountId}/databases": { + "post": { + "operationId": "CreateDatabase", + "responses": { + "200": { + "description": "A database", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Database" + } + } + } + }, + "default": { + "description": "Default api error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + }, + "tags": [ + "Databases" + ], + "parameters": [ + { + "in": "path", + "required": true, + "name": "accountId", + "schema": { + "type": "string" + } + } + ], + "security": [ + { + "authorizer": [] + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateDatabase" + } + } + }, + "required": true + } + }, + "get": { + "operationId": "ListDatabases", + "responses": { + "200": { + "description": "List of databases the user has access to", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Database" + } + } + } + } + }, + "default": { + "description": "Default api error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + }, + "tags": [ + "Databases" + ], + "parameters": [ + { + "in": "path", + "required": true, + "name": "accountId", + "schema": { + "type": "string" + } + } + ], + "security": [ + { + "authorizer": [] + } + ] + } + }, "/api/v1/accounts/{accountId}/databases/{databaseId}": { "delete": { "operationId": "DeleteDatabase", @@ -1481,107 +1582,6 @@ ] } }, - "/api/v1/accounts/{accountId}/databases": { - "post": { - "operationId": "CreateDatabase", - "responses": { - "200": { - "description": "A database", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Database" - } - } - } - }, - "default": { - "description": "Default api error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ApiError" - } - } - } - } - }, - "tags": [ - "Databases" - ], - "parameters": [ - { - "in": "path", - "required": true, - "name": "accountId", - "schema": { - "type": "string" - } - } - ], - "security": [ - { - "authorizer": [] - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CreateDatabase" - } - } - }, - "required": true - } - }, - "get": { - "operationId": "ListDatabases", - "responses": { - "200": { - "description": "List of databases the user has access to", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Database" - } - } - } - } - }, - "default": { - "description": "Default api error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ApiError" - } - } - } - } - }, - "tags": [ - "Databases" - ], - "parameters": [ - { - "in": "path", - "required": true, - "name": "accountId", - "schema": { - "type": "string" - } - } - ], - "security": [ - { - "authorizer": [] - } - ] - } - }, "/api/v1/accounts/{accountId}/databases/{databaseId}/start": { "put": { "operationId": "StartDatabase", @@ -3327,6 +3327,9 @@ "size": { "type": "string" }, + "family": { + "type": "string" + }, "autoStop": { "$ref": "#/components/schemas/AutoStop" }, @@ -3360,6 +3363,9 @@ "size": { "type": "string" }, + "family": { + "type": "string" + }, "autoStop": { "$ref": "#/components/schemas/AutoStop" }, @@ -3716,6 +3722,9 @@ "properties": { "size": { "type": "string" + }, + "family": { + "type": "string" } }, "additionalProperties": false, diff --git a/test/unit/test_api_access.py b/test/unit/test_api_access.py index 3cbfc3b..b4a0216 100644 --- a/test/unit/test_api_access.py +++ b/test/unit/test_api_access.py @@ -7,7 +7,7 @@ from exasol.saas.client.api_access import ( OpenApiAccess, indicates_retry, - timestamp_name + timestamp_name, ) from exasol.saas.client.openapi.errors import UnexpectedStatus @@ -125,7 +125,7 @@ def test_delete_success( def test_timestamp_name() -> None: - names = [timestamp_name('TEST') for _ in range(3)] + names = [timestamp_name("TEST") for _ in range(3)] minutes = [int(name[:5], 16) for name in names] suffixes = [int(name[5:10], 16) for name in names] tags = [name[10:14] for name in names] @@ -134,4 +134,4 @@ def test_timestamp_name() -> None: # suffixes should all be different assert len(set(suffixes)) == 3 # the provided tag should follow the hacky timestamp. - assert all(tag == 'TEST' for tag in tags) + assert all(tag == "TEST" for tag in tags) From 543e2564d3047d56a8088dd98a1b1836f2d75b6e Mon Sep 17 00:00:00 2001 From: Mikhail Beck Date: Wed, 11 Feb 2026 12:59:14 +0000 Subject: [PATCH 3/3] Update doc/changes/unreleased.md Co-authored-by: Christoph Kuhnke --- doc/changes/unreleased.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/changes/unreleased.md b/doc/changes/unreleased.md index 97416b2..8661472 100644 --- a/doc/changes/unreleased.md +++ b/doc/changes/unreleased.md @@ -8,6 +8,6 @@ tbd. * #128: Explicitly configured validation of SaaS SSL certificates in pyexasol connection -## Bug fixing +## Bugfixes * #135: Made the database name semi-unique. \ No newline at end of file