Skip to content
Merged
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
65 changes: 65 additions & 0 deletions tests/integration/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,21 @@
import pytest

from hotdata import ApiClient, Configuration
from hotdata.api.connection_types_api import ConnectionTypesApi
from hotdata.api.connections_api import ConnectionsApi
from hotdata.api.database_context_api import DatabaseContextApi
from hotdata.api.databases_api import DatabasesApi
from hotdata.api.datasets_api import DatasetsApi
from hotdata.api.embedding_providers_api import EmbeddingProvidersApi
from hotdata.api.indexes_api import IndexesApi
from hotdata.api.information_schema_api import InformationSchemaApi
from hotdata.api.jobs_api import JobsApi
from hotdata.api.refresh_api import RefreshApi
from hotdata.api.saved_queries_api import SavedQueriesApi
from hotdata.api.secrets_api import SecretsApi
from hotdata.api.uploads_api import UploadsApi
from hotdata.api.workspaces_api import WorkspacesApi
from hotdata.exceptions import ApiException
from hotdata.models.create_database_request import CreateDatabaseRequest


Expand Down Expand Up @@ -152,3 +160,60 @@ def saved_queries_api(api_client: ApiClient) -> SavedQueriesApi:
@pytest.fixture
def secrets_api(api_client: ApiClient) -> SecretsApi:
return SecretsApi(api_client)


@pytest.fixture
def database_context_api(api_client: ApiClient) -> DatabaseContextApi:
return DatabaseContextApi(api_client)


@pytest.fixture
def embedding_providers_api(api_client: ApiClient) -> EmbeddingProvidersApi:
return EmbeddingProvidersApi(api_client)


@pytest.fixture
def connection_types_api(api_client: ApiClient) -> ConnectionTypesApi:
return ConnectionTypesApi(api_client)


@pytest.fixture
def jobs_api(api_client: ApiClient) -> JobsApi:
return JobsApi(api_client)


@pytest.fixture
def information_schema_api(api_client: ApiClient) -> InformationSchemaApi:
return InformationSchemaApi(api_client)


@pytest.fixture
def uploads_api(api_client: ApiClient) -> UploadsApi:
return UploadsApi(api_client)


@pytest.fixture
def refresh_api(api_client: ApiClient) -> RefreshApi:
return RefreshApi(api_client)


@pytest.fixture
def scratch_database(databases_api: DatabasesApi, sdkci_name) -> Iterator[str]:
"""Yields the id of a fresh, isolated database, deleting it on teardown.

Unlike the session-scoped `database_id` (the shared `sdkci-shared` db reused
across runs), scenarios that declare schemas/tables/contexts or attach
catalogs need their own throwaway database so they never touch seeded data
or collide with a parallel run. `expires_at` is a safety net: if teardown is
interrupted, the server reclaims the database rather than leaking it.
"""
created = databases_api.create_database(
CreateDatabaseRequest(name=sdkci_name("scratch"), expires_at="2h")
)
try:
yield created.id
finally:
try:
databases_api.delete_database(created.id)
except ApiException:
pass
27 changes: 27 additions & 0 deletions tests/integration/test_connection_types_read.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
"""Scenario: connection_types_read.

Read-only: list_connection_types returns the available connector catalog, and
get_connection_type fetches one by name with its config schema. Mutates nothing.
"""

from __future__ import annotations

from hotdata.api.connection_types_api import ConnectionTypesApi


def test_connection_types_read(connection_types_api: ConnectionTypesApi) -> None:
listing = connection_types_api.list_connection_types()
assert listing.connection_types, "connector catalog is unexpectedly empty"
for ct in listing.connection_types:
assert ct.name
assert ct.label

# Fetch one by name; prefer postgres if present, else the first entry.
names = [ct.name for ct in listing.connection_types]
target = "postgres" if "postgres" in names else names[0]

detail = connection_types_api.get_connection_type(target)
assert detail.name == target
assert detail.label
# Each connector advertises a config schema clients use to build a request.
assert detail.config_schema is not None
42 changes: 42 additions & 0 deletions tests/integration/test_database_catalogs_attach.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
"""Scenario: database_catalogs_attach.

Attach the seeded connection to a fresh scratch database as a catalog (under an
alias), confirm it's reachable via get_database, then detach it. Reversible and
idempotent — it never mutates the connection itself, only the scratch
database's attachment list (which is torn down with the database).
"""

from __future__ import annotations

from hotdata.api.databases_api import DatabasesApi
from hotdata.models.attach_database_catalog_request import (
AttachDatabaseCatalogRequest,
)


def test_database_catalogs_attach(
databases_api: DatabasesApi, scratch_database: str, connection_id: str
) -> None:
db_id = scratch_database
alias = "sdkci_cat"

databases_api.attach_database_catalog(
db_id,
AttachDatabaseCatalogRequest(connection_id=connection_id, alias=alias),
)

detail = databases_api.get_database(db_id)
attached = [a for a in detail.attachments if a.connection_id == connection_id]
assert attached, (
f"connection {connection_id} not in attachments after attach"
)
assert any(a.alias == alias for a in attached), (
f"alias {alias!r} not reflected in attachment list"
)

databases_api.detach_database_catalog(db_id, connection_id)

detail_after = databases_api.get_database(db_id)
assert all(a.connection_id != connection_id for a in detail_after.attachments), (
f"connection {connection_id} still attached after detach"
)
55 changes: 55 additions & 0 deletions tests/integration/test_database_contexts_crud.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
"""Scenario: database_contexts_crud.

Against a fresh scratch database: upsert a named context document, read it back,
confirm it appears in list_database_contexts, upsert the same name again to
verify replace-on-write, delete the context, and confirm it's gone. The
`scratch_database` fixture creates and tears down the owning database.
"""

from __future__ import annotations

import pytest

from hotdata.api.database_context_api import DatabaseContextApi
from hotdata.exceptions import ApiException
from hotdata.models.upsert_database_context_request import (
UpsertDatabaseContextRequest,
)


def test_database_contexts_crud(
database_context_api: DatabaseContextApi, scratch_database: str, sdkci_name
) -> None:
db_id = scratch_database
# Context keys follow dataset table-name rules (letter/underscore first).
name = "sdkci_" + sdkci_name("ctx").replace("-", "_")
initial = "First revision of the context document."
replaced = "Second revision — replace-on-write."

upserted = database_context_api.upsert_database_context(
db_id, UpsertDatabaseContextRequest(name=name, content=initial)
)
assert upserted.context.name == name
assert upserted.context.content == initial

got = database_context_api.get_database_context(db_id, name)
assert got.context.name == name
assert got.context.content == initial

listing = database_context_api.list_database_contexts(db_id)
assert any(c.name == name for c in listing.contexts), (
f"context {name} not in list_database_contexts"
)

# Upsert reuses the same name: content is replaced, not appended.
database_context_api.upsert_database_context(
db_id, UpsertDatabaseContextRequest(name=name, content=replaced)
)
got2 = database_context_api.get_database_context(db_id, name)
assert got2.context.content == replaced

database_context_api.delete_database_context(db_id, name)

with pytest.raises(ApiException) as excinfo:
database_context_api.get_database_context(db_id, name)
assert excinfo.value.status == 404
72 changes: 72 additions & 0 deletions tests/integration/test_databases_lifecycle.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
"""Scenario: databases_lifecycle.

Create a database (a metadata-only grouping that auto-provisions a default
catalog), read it back, confirm it appears in list_databases, declare a schema
and a table on its default catalog, then delete it and verify it's gone.

Self-cleaning: the database is created with a short `expires_at` and deleted in
a finally block, so a failed assertion never leaks a database into prod.
"""

from __future__ import annotations

import pytest

from hotdata.api.databases_api import DatabasesApi
from hotdata.exceptions import ApiException
from hotdata.models.add_managed_schema_request import AddManagedSchemaRequest
from hotdata.models.add_managed_table_request import AddManagedTableRequest
from hotdata.models.create_database_request import CreateDatabaseRequest


def test_databases_lifecycle(databases_api: DatabasesApi, sdkci_name) -> None:
name = sdkci_name("databases-lifecycle")
# Schema/table identifiers must be SQL identifiers (no dashes).
schema_name = "sdkci_schema"
table_name = "sdkci_table"
db_id: str | None = None

try:
created = databases_api.create_database(
CreateDatabaseRequest(name=name, expires_at="2h")
)
db_id = created.id
assert created.id
assert created.name == name
assert created.default_connection_id, (
"create_database must expose the auto-provisioned default catalog connection"
)

detail = databases_api.get_database(db_id)
assert detail.id == db_id
assert detail.name == name
assert detail.default_connection_id == created.default_connection_id

listing = databases_api.list_databases()
assert any(d.id == db_id for d in listing.databases), (
f"created database {db_id} not in list_databases"
)

schema_resp = databases_api.add_database_schema(
db_id, AddManagedSchemaRequest(name=schema_name)
)
assert schema_resp.var_schema == schema_name

table_resp = databases_api.add_database_table(
db_id, schema_name, AddManagedTableRequest(name=table_name)
)
assert table_resp.var_schema == schema_name
assert table_resp.table == table_name

databases_api.delete_database(db_id)
db_id = None

with pytest.raises(ApiException) as excinfo:
databases_api.get_database(created.id)
assert excinfo.value.status == 404
finally:
if db_id is not None:
try:
databases_api.delete_database(db_id)
except ApiException:
pass
79 changes: 79 additions & 0 deletions tests/integration/test_embedding_providers_crud.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
"""Scenario: embedding_providers_crud.
Register an embedding provider, read it, confirm it appears in
list_embedding_providers, update it, then delete it.
The scenario calls for a `service` provider with **no** api_key/secret_name
(not `local` — the runtime currently rejects `local` as "not yet supported").
A service provider's key is only consulted when embeddings are actually
generated (indexing) — never at create/get/update — so this exercises the full
CRUD surface without any real external credential and without auto-creating a
secret that would need cleanup.
"""

from __future__ import annotations

import pytest

from hotdata.api.embedding_providers_api import EmbeddingProvidersApi
from hotdata.exceptions import ApiException
from hotdata.models.create_embedding_provider_request import (
CreateEmbeddingProviderRequest,
)
from hotdata.models.update_embedding_provider_request import (
UpdateEmbeddingProviderRequest,
)


def test_embedding_providers_crud(
embedding_providers_api: EmbeddingProvidersApi, sdkci_name
) -> None:
name = sdkci_name("embprov")
updated_name = sdkci_name("embprov-updated")
provider_id: str | None = None

try:
created = embedding_providers_api.create_embedding_provider(
CreateEmbeddingProviderRequest(
name=name,
provider_type="service",
config={"model": "text-embedding-3-small"},
)
)
provider_id = created.id
assert created.id
assert created.name == name
assert created.provider_type == "service"

got = embedding_providers_api.get_embedding_provider(provider_id)
assert got.id == provider_id
assert got.name == name

listing = embedding_providers_api.list_embedding_providers()
assert any(p.id == provider_id for p in listing.embedding_providers), (
f"created provider {provider_id} not in list_embedding_providers"
)

updated = embedding_providers_api.update_embedding_provider(
provider_id, UpdateEmbeddingProviderRequest(name=updated_name)
)
assert updated.id == provider_id
assert updated.name == updated_name

# Read-after-update reflects the rename.
assert embedding_providers_api.get_embedding_provider(provider_id).name == (
updated_name
)

embedding_providers_api.delete_embedding_provider(provider_id)
provider_id = None

with pytest.raises(ApiException) as excinfo:
embedding_providers_api.get_embedding_provider(created.id)
assert excinfo.value.status == 404
finally:
if provider_id is not None:
try:
embedding_providers_api.delete_embedding_provider(provider_id)
except ApiException:
pass
31 changes: 31 additions & 0 deletions tests/integration/test_information_schema_read.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
"""Scenario: information_schema_read.

Read-only: information_schema returns the catalog/schema/table metadata visible
to the workspace. Verify the seeded connection's tables surface when filtered by
connection id, including column definitions.
"""

from __future__ import annotations

from hotdata.api.information_schema_api import InformationSchemaApi


def test_information_schema_read(
information_schema_api: InformationSchemaApi, connection_id: str
) -> None:
resp = information_schema_api.information_schema(
connection_id=connection_id, include_columns=True
)
# Response is well-formed: count matches the page, has_more drives paging.
assert isinstance(resp.tables, list)
assert resp.count == len(resp.tables)
assert isinstance(resp.has_more, bool)

assert resp.tables, (
f"seeded connection {connection_id} exposed no tables in information_schema"
)
for table in resp.tables:
assert table.table
assert table.var_schema
# include_columns=True must populate column definitions.
assert table.columns is not None
Loading
Loading