From 55653ac066f41e27d9eced5120c298dd7be95a72 Mon Sep 17 00:00:00 2001 From: Aryamanz29 Date: Fri, 22 May 2026 13:51:13 +0530 Subject: [PATCH 1/3] fix(connection): reject connector_type values that violate platform slug pattern (BLDX-1294) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Connection.creator and Connection.creator_async now validate connector_type.value against the same ^[a-z0-9-]+$ pattern the Atlan platform's server-side asset-import (RAB) path enforces. Mirrors the Java SDK constraint. Without this, users could create Connections via pyatlan with slugs containing underscores (e.g. 'dev_cmdr'), dots, whitespace, or other characters that the platform rejects later at import time — leaving phantom Connection rows in Atlas. Reported in APP-2263 where a customer created 'dev_cmdr' via pyatlan and then watched RAB reject every subsequent asset-import that referenced it. Built-in AtlanConnectorType members are all already kebab-case and pass the check. The validator only bites custom types created via CREATE_CUSTOM(value=...) — exactly the surface the customer hit. Error message points users at the simple fix: Invalid connector_type value 'dev_cmdr': must match pattern '^[a-z0-9-]+$' (lower-case alphanumerics and hyphens only). ... Replace any underscores with hyphens (e.g. 'dev_cmdr' -> 'dev-cmdr'). Validation lives in pyatlan/model/assets/connection.py as a module-level helper (_validate_connector_type_value) so pyatlan_v9 can import the same source-of-truth. Updated both the generated v9 connection.py and the _overlays/connection.py template so the next regeneration preserves the fix. Tests: - pyatlan: 6 bad-value parametrized cases (underscore / dot / whitespace / slash / special-char / trailing underscore), 6 good-value regression pins (hyphen / built-in-like / multi-hyphen / single-char / alphanum / digit-prefix), built-in-connector-type regression, async-creator rejection. - pyatlan_v9: parametrized bad-values + valid-value regression. --- pyatlan/model/assets/connection.py | 39 ++++++ .../model/assets/_overlays/connection.py | 3 + pyatlan_v9/model/assets/connection.py | 3 + tests/unit/model/connection_test.py | 117 ++++++++++++++++++ tests_v9/unit/model/connection_test.py | 63 ++++++++++ 5 files changed, 225 insertions(+) diff --git a/pyatlan/model/assets/connection.py b/pyatlan/model/assets/connection.py index c4969a01d..bf0f845b1 100644 --- a/pyatlan/model/assets/connection.py +++ b/pyatlan/model/assets/connection.py @@ -4,6 +4,7 @@ from __future__ import annotations +import re from datetime import datetime from typing import TYPE_CHECKING, ClassVar, Dict, List, Optional, Set from warnings import warn @@ -31,6 +32,42 @@ from pyatlan.client.atlan import AtlanClient +#: Allowed characters for the connector-type slug embedded in +#: ``connectionQualifiedName`` (the ``{slug}`` in +#: ``default/{slug}/{epoch}``). Lower-case alphanumerics and hyphens +#: only — mirrors the Java SDK constraint and the Atlan platform's +#: server-side ``RAB`` (Reading Asset Bulk) / asset-import validation. +#: Tightening pyatlan to this pattern at creation time (BLDX-1294) +#: closes a gap where users could create connections with underscores +#: (or other characters) via pyatlan only to discover them rejected +#: by RAB at import time — leaving phantom Connection rows in Atlas. +_CONNECTOR_TYPE_VALUE_PATTERN: re.Pattern = re.compile(r"^[a-z0-9-]+$") + + +def _validate_connector_type_value(connector_type: AtlanConnectorType) -> None: + """Reject ``connector_type`` values that wouldn't survive RAB / asset-import + validation server-side. See ``_CONNECTOR_TYPE_VALUE_PATTERN``. + + Built-in :class:`AtlanConnectorType` members are all kebab-case and + therefore always pass; this guard exists for custom types created + via :meth:`AtlanConnectorType.CREATE_CUSTOM` where the caller- + supplied ``value`` could otherwise contain underscores, dots, + uppercase letters, or other characters that the platform rejects + later in the pipeline. + """ + value = connector_type.value + if not _CONNECTOR_TYPE_VALUE_PATTERN.match(value): + raise ValueError( + f"Invalid connector_type value {value!r}: must match pattern " + f"'^[a-z0-9-]+$' (lower-case alphanumerics and hyphens only). " + f"Underscores, dots, uppercase letters, and other characters " + f"are not permitted because the Atlan platform's asset-import " + f"path rejects them at ingestion time, leaving phantom " + f"Connection rows in Atlas. Replace any underscores with " + f"hyphens (e.g. 'dev_cmdr' -> 'dev-cmdr')." + ) + + class Connection(Asset, type_name="Connection"): """Description""" @@ -51,6 +88,7 @@ def creator( validate_required_fields( ["client", "name", "connector_type"], [client, name, connector_type] ) + _validate_connector_type_value(connector_type) if not admin_users and not admin_groups and not admin_roles: raise ValueError( "One of admin_user, admin_groups or admin_roles is required" @@ -102,6 +140,7 @@ async def creator_async( validate_required_fields( ["client", "name", "connector_type"], [client, name, connector_type] ) + _validate_connector_type_value(connector_type) if not admin_users and not admin_groups and not admin_roles: raise ValueError( "One of admin_user, admin_groups or admin_roles is required" diff --git a/pyatlan_v9/model/assets/_overlays/connection.py b/pyatlan_v9/model/assets/_overlays/connection.py index 95f815543..bbf47d956 100644 --- a/pyatlan_v9/model/assets/_overlays/connection.py +++ b/pyatlan_v9/model/assets/_overlays/connection.py @@ -1,5 +1,6 @@ # STDLIB_IMPORT: from typing import TYPE_CHECKING, List, Optional # IMPORT: from pyatlan.model.enums import AtlanConnectorType +# INTERNAL_IMPORT: from pyatlan.model.assets.connection import _validate_connector_type_value # INTERNAL_IMPORT: from pyatlan.utils import init_guid, validate_required_fields @classmethod @@ -38,6 +39,7 @@ def creator( validate_required_fields( ["client", "name", "connector_type"], [client, name, connector_type] ) + _validate_connector_type_value(connector_type) if not admin_users and not admin_groups and not admin_roles: raise ValueError( "One of admin_user, admin_groups or admin_roles is required" @@ -92,6 +94,7 @@ async def creator_async( validate_required_fields( ["client", "name", "connector_type"], [client, name, connector_type] ) + _validate_connector_type_value(connector_type) if not admin_users and not admin_groups and not admin_roles: raise ValueError( "One of admin_user, admin_groups or admin_roles is required" diff --git a/pyatlan_v9/model/assets/connection.py b/pyatlan_v9/model/assets/connection.py index 4976accd2..07b312028 100644 --- a/pyatlan_v9/model/assets/connection.py +++ b/pyatlan_v9/model/assets/connection.py @@ -20,6 +20,7 @@ import msgspec from msgspec import UNSET, UnsetType +from pyatlan.model.assets.connection import _validate_connector_type_value from pyatlan.model.enums import AtlanConnectorType from pyatlan_v9.model.conversion_utils import ( categorize_relationships, @@ -439,6 +440,7 @@ def creator( validate_required_fields( ["client", "name", "connector_type"], [client, name, connector_type] ) + _validate_connector_type_value(connector_type) if not admin_users and not admin_groups and not admin_roles: raise ValueError( "One of admin_user, admin_groups or admin_roles is required" @@ -493,6 +495,7 @@ async def creator_async( validate_required_fields( ["client", "name", "connector_type"], [client, name, connector_type] ) + _validate_connector_type_value(connector_type) if not admin_users and not admin_groups and not admin_roles: raise ValueError( "One of admin_user, admin_groups or admin_roles is required" diff --git a/tests/unit/model/connection_test.py b/tests/unit/model/connection_test.py index 495c460e9..c0062cb93 100644 --- a/tests/unit/model/connection_test.py +++ b/tests/unit/model/connection_test.py @@ -321,3 +321,120 @@ def test_validation_of_admin_not_done_when_constructed_from_json( mock_role_cache.validate_idstrs.assert_not_called() mock_group_cache.validate_aliases.assert_not_called() mock_user_cache.validate_names.assert_not_called() + + +# --------------------------------------------------------------------------- +# BLDX-1294 — connector-type value regex validation in Connection.creator +# --------------------------------------------------------------------------- + + +@pytest.mark.parametrize( + "bad_value", + [ + "dev_cmdr", # underscore — the original reported case + "dev.cmdr", # dot + "dev cmdr", # whitespace + "dev/cmdr", # slash + "dev@cmdr", # special char + "dev_", # trailing underscore + ], +) +def test_creator_rejects_invalid_connector_type_value( + client: AtlanClient, + bad_value: str, + mock_role_cache, + mock_user_cache, + mock_group_cache, +): + """BLDX-1294: Connection.creator() must reject custom connector_type values + whose slug doesn't match the platform's [a-z0-9-]+ pattern. Without this, + callers create Connections via pyatlan that the server-side asset-import + path later rejects, leaving phantom Connection rows in Atlas.""" + custom = AtlanConnectorType.CREATE_CUSTOM( + name=bad_value.upper().replace("-", "_") or "EMPTY", + value=bad_value, + category=AtlanConnectionCategory.CUSTOM, + ) + + with pytest.raises(ValueError, match="Invalid connector_type value"): + Connection.creator( + client=client, + name=CONNECTION_NAME, + connector_type=custom, + admin_users=["ernest"], + ) + + +@pytest.mark.parametrize( + "good_value", + [ + "dev-cmdr", # hyphen — the recommended replacement + "snowflake", # built-in-like slug + "amazon-msk", # hyphenated multi-word + "a", # single-char + "abc123", # alphanumerics + "123-abc-456", # mixed digit + alpha + hyphen + ], +) +def test_creator_accepts_valid_connector_type_value( + client: AtlanClient, + good_value: str, + mock_role_cache, + mock_user_cache, + mock_group_cache, +): + """Valid lowercase-alphanumeric-and-hyphen slugs continue to work.""" + custom = AtlanConnectorType.CREATE_CUSTOM( + name="CUSTOM", + value=good_value, + category=AtlanConnectionCategory.CUSTOM, + ) + + sut = Connection.creator( + client=client, + name=CONNECTION_NAME, + connector_type=custom, + admin_users=["ernest"], + ) + assert sut.name == CONNECTION_NAME + assert sut.qualified_name.startswith(f"default/{good_value}/") + + +def test_creator_accepts_builtin_connector_types( + client: AtlanClient, + mock_role_cache, + mock_user_cache, + mock_group_cache, +): + """Built-in AtlanConnectorType members have always-valid slugs. + Regression pin — the new BLDX-1294 validator must not break them.""" + sut = Connection.creator( + client=client, + name=CONNECTION_NAME, + connector_type=AtlanConnectorType.SNOWFLAKE, + admin_users=["ernest"], + ) + assert sut.qualified_name.startswith("default/snowflake/") + + +@pytest.mark.asyncio +async def test_creator_async_rejects_invalid_connector_type_value( + client: AtlanClient, + mock_role_cache, + mock_user_cache, + mock_group_cache, +): + """Same validation must apply to the async creator path.""" + bad = AtlanConnectorType.CREATE_CUSTOM( + name="DEV_CMDR", + value="dev_cmdr", + category=AtlanConnectionCategory.CUSTOM, + ) + + with pytest.raises(ValueError, match="Invalid connector_type value"): + await Connection.creator_async( + client=client, + name=CONNECTION_NAME, + connector_type=bad, + admin_users=["ernest"], + ) diff --git a/tests_v9/unit/model/connection_test.py b/tests_v9/unit/model/connection_test.py index ff7639051..150d1ddc8 100644 --- a/tests_v9/unit/model/connection_test.py +++ b/tests_v9/unit/model/connection_test.py @@ -390,3 +390,66 @@ def test_type_name_defaults(): """Test that type_name defaults to 'Connection'.""" conn = Connection(name=CONNECTION_NAME, qualified_name=CONNECTION_QUALIFIED_NAME) assert conn.type_name == "Connection" + + +# --------------------------------------------------------------------------- +# BLDX-1294 — connector-type value regex validation (mirrors pyatlan) +# --------------------------------------------------------------------------- + + +@pytest.mark.parametrize( + "bad_value", + [ + "dev_cmdr", # underscore — the original reported case + "dev.cmdr", # dot + "dev cmdr", # whitespace + "dev/cmdr", # slash + "dev@cmdr", # special char + "dev_", # trailing underscore + ], +) +def test_creator_rejects_invalid_connector_type_value_v9( + client: AtlanClient, + bad_value: str, + mock_role_cache, + mock_user_cache, + mock_group_cache, +): + """BLDX-1294: pyatlan_v9 Connection.creator() must enforce the same + [a-z0-9-]+ slug rule as pyatlan to keep parity with the platform's + asset-import / RAB validation.""" + custom = AtlanConnectorType.CREATE_CUSTOM( + name=bad_value.upper().replace("-", "_") or "BADNAME", + value=bad_value, + category=AtlanConnectionCategory.CUSTOM, + ) + + with pytest.raises(ValueError, match="Invalid connector_type value"): + Connection.creator( + client=client, + name=CONNECTION_NAME, + connector_type=custom, + admin_users=["ernest"], + ) + + +def test_creator_accepts_valid_connector_type_value_v9( + client: AtlanClient, + mock_role_cache, + mock_user_cache, + mock_group_cache, +): + """Valid slugs (hyphen-only) continue to work in v9.""" + custom = AtlanConnectorType.CREATE_CUSTOM( + name="DEV_CMDR", + value="dev-cmdr", + category=AtlanConnectionCategory.CUSTOM, + ) + + sut = Connection.creator( + client=client, + name=CONNECTION_NAME, + connector_type=custom, + admin_users=["ernest"], + ) + assert sut.qualified_name.startswith("default/dev-cmdr/") From 45c21aed444ad0d369758825f94d6a9665ef3622 Mon Sep 17 00:00:00 2001 From: Aryamanz29 Date: Fri, 22 May 2026 13:59:54 +0530 Subject: [PATCH 2/3] fix(errors): use typed InvalidRequestError + ATLAN-PYTHON-400-079 for invalid connection QN (BLDX-1294) Aligns with the Java SDK convention from atlanhq/atlan-java#2504 which added ErrorCode.INVALID_CONNECTION_QN (ATLAN-JAVA-400-055) for the same class of failure. Cross-SDK error reporting now matches: typed exception class + stable error code that customer log-aggregation can group on. Replaces the bare ValueError raised by _validate_connector_type_value with InvalidRequestError carrying ErrorCode.INVALID_CONNECTION_QN (ATLAN-PYTHON-400-079). Message + user-action mirror the Java side. Tests updated to assert on: - isinstance(exc, InvalidRequestError) - "ATLAN-PYTHON-400-079" in str(exc) - bad slug surfaced in exc message --- pyatlan/errors.py | 7 +++++++ pyatlan/model/assets/connection.py | 18 +++++++++--------- tests/unit/model/connection_test.py | 12 ++++++++++-- tests_v9/unit/model/connection_test.py | 5 ++++- 4 files changed, 30 insertions(+), 12 deletions(-) diff --git a/pyatlan/errors.py b/pyatlan/errors.py index efc42d3a3..2a51e279d 100644 --- a/pyatlan/errors.py +++ b/pyatlan/errors.py @@ -689,6 +689,13 @@ class ErrorCode(Enum): "Ensure the file path does not point to a blocked location (system files, credential directories, or paths defined in PYATLAN_UPLOAD_FILE_BLOCKED_PATHS).", InvalidRequestError, ) + INVALID_CONNECTION_QN = ( + 400, + "ATLAN-PYTHON-400-079", + "Invalid connectorType slug '{0}' for connection qualifiedName: must match pattern '^[a-z0-9-]+$' (lower-case alphanumerics and hyphens only).", + "Replace any underscores with hyphens (e.g. 'dev_cmdr' -> 'dev-cmdr'). Underscores, dots, uppercase letters, whitespace, and other characters are not permitted because the Atlan platform's asset-import path rejects them at ingestion time, leaving phantom Connection rows in Atlas. Mirrors the Java SDK constraint (atlan-java ErrorCode.INVALID_CONNECTION_QN).", + InvalidRequestError, + ) AUTHENTICATION_PASSTHROUGH = ( 401, "ATLAN-PYTHON-401-000", diff --git a/pyatlan/model/assets/connection.py b/pyatlan/model/assets/connection.py index bf0f845b1..334b1e33e 100644 --- a/pyatlan/model/assets/connection.py +++ b/pyatlan/model/assets/connection.py @@ -11,6 +11,7 @@ from pydantic.v1 import Field, validator +from pyatlan.errors import ErrorCode from pyatlan.model.enums import ( AtlanConnectorType, ConnectionDQEnvironmentSetupStatus, @@ -54,18 +55,17 @@ def _validate_connector_type_value(connector_type: AtlanConnectorType) -> None: supplied ``value`` could otherwise contain underscores, dots, uppercase letters, or other characters that the platform rejects later in the pipeline. + + Raises: + InvalidRequestError: With error code + ``ATLAN-PYTHON-400-079`` (``INVALID_CONNECTION_QN``) when the + slug fails the pattern check. Mirrors the Java SDK's + ``ErrorCode.INVALID_CONNECTION_QN`` so cross-SDK error + reporting stays consistent. """ value = connector_type.value if not _CONNECTOR_TYPE_VALUE_PATTERN.match(value): - raise ValueError( - f"Invalid connector_type value {value!r}: must match pattern " - f"'^[a-z0-9-]+$' (lower-case alphanumerics and hyphens only). " - f"Underscores, dots, uppercase letters, and other characters " - f"are not permitted because the Atlan platform's asset-import " - f"path rejects them at ingestion time, leaving phantom " - f"Connection rows in Atlas. Replace any underscores with " - f"hyphens (e.g. 'dev_cmdr' -> 'dev-cmdr')." - ) + raise ErrorCode.INVALID_CONNECTION_QN.exception_with_parameters(value) class Connection(Asset, type_name="Connection"): diff --git a/tests/unit/model/connection_test.py b/tests/unit/model/connection_test.py index c0062cb93..ea19bbac9 100644 --- a/tests/unit/model/connection_test.py +++ b/tests/unit/model/connection_test.py @@ -5,6 +5,7 @@ from pyatlan.client.atlan import AtlanClient from pyatlan.client.token import TokenClient +from pyatlan.errors import InvalidRequestError from pyatlan.model.assets import Connection from pyatlan.model.enums import AtlanConnectionCategory, AtlanConnectorType from tests.unit.model.constants import CONNECTION_NAME, CONNECTION_QUALIFIED_NAME @@ -356,13 +357,18 @@ def test_creator_rejects_invalid_connector_type_value( category=AtlanConnectionCategory.CUSTOM, ) - with pytest.raises(ValueError, match="Invalid connector_type value"): + with pytest.raises(InvalidRequestError) as exc_info: Connection.creator( client=client, name=CONNECTION_NAME, connector_type=custom, admin_users=["ernest"], ) + # Match the Java SDK convention — typed error with code + # ATLAN-PYTHON-400-079 (INVALID_CONNECTION_QN) and the bad slug + # surfaced in the message. + assert "ATLAN-PYTHON-400-079" in str(exc_info.value) + assert bad_value in str(exc_info.value) @pytest.mark.parametrize( @@ -431,10 +437,12 @@ async def test_creator_async_rejects_invalid_connector_type_value( category=AtlanConnectionCategory.CUSTOM, ) - with pytest.raises(ValueError, match="Invalid connector_type value"): + with pytest.raises(InvalidRequestError) as exc_info: await Connection.creator_async( client=client, name=CONNECTION_NAME, connector_type=bad, admin_users=["ernest"], ) + assert "ATLAN-PYTHON-400-079" in str(exc_info.value) + assert "dev_cmdr" in str(exc_info.value) diff --git a/tests_v9/unit/model/connection_test.py b/tests_v9/unit/model/connection_test.py index 150d1ddc8..aa208e9c2 100644 --- a/tests_v9/unit/model/connection_test.py +++ b/tests_v9/unit/model/connection_test.py @@ -9,6 +9,7 @@ import pytest from msgspec import UNSET +from pyatlan.errors import InvalidRequestError from pyatlan_v9.client.atlan import AtlanClient from pyatlan_v9.model import Connection from pyatlan_v9.model.enums import AtlanConnectionCategory, AtlanConnectorType @@ -424,13 +425,15 @@ def test_creator_rejects_invalid_connector_type_value_v9( category=AtlanConnectionCategory.CUSTOM, ) - with pytest.raises(ValueError, match="Invalid connector_type value"): + with pytest.raises(InvalidRequestError) as exc_info: Connection.creator( client=client, name=CONNECTION_NAME, connector_type=custom, admin_users=["ernest"], ) + assert "ATLAN-PYTHON-400-079" in str(exc_info.value) + assert bad_value in str(exc_info.value) def test_creator_accepts_valid_connector_type_value_v9( From f74b8819ab10cec784e18c120e9445cf4c2c72d5 Mon Sep 17 00:00:00 2001 From: Aryamanz29 Date: Fri, 22 May 2026 14:27:17 +0530 Subject: [PATCH 3/3] fix(tests): make TestId.make_unique slug-safe so fixtures pass new connection QN validation (BLDX-1294) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `TestId.make_unique` was emitting ids like ``psdk__`` — mixed-case, underscore-separated, and uppercase-allowed session alphabet. After the new ``^[a-z0-9-]+$`` validation in ``Connection.creator``/``creator_async`` (ATLAN-PYTHON-400-079), every integration fixture that feeds ``MODULE_NAME`` into ``AtlanConnectorType.CREATE_CUSTOM(value=...)`` started failing setup because the slug contained underscores and/or uppercase letters. Changes: - ``TestId.make_unique`` (both ``pyatlan/test_utils`` and the ``tests/integration`` / ``tests_v9/integration`` mirrors) now lowercases the input, swaps underscores for hyphens, uses a lowercase-only nanoid alphabet, and joins with hyphens: ``psdk--`` (and ``psdkv9-...`` for v9). - Custom-connection fixture slugs switched from ``f"{MODULE_NAME}_type"`` → ``f"{MODULE_NAME}-type"`` (and the matching assertions) in: - ``tests/integration/connection_test.py`` - ``tests/integration/aio/test_connection.py`` - ``tests_v9/integration/connection_test.py`` - ``tests_v9/integration/aio/test_connection.py`` - Callers that previously matched on the literal ``psdk_*`` / ``psdkv9_*`` display-name prefix (token cleanup, search-pagination exclusion wildcards) now also match the new hyphenated form so they keep working across pre/post-BLDX-1294 runs. Co-Authored-By: Claude Sonnet 4.6 (1M context) --- pyatlan/test_utils/__init__.py | 35 +++++++++++++++++-- tests/integration/aio/test_connection.py | 6 ++-- tests/integration/aio/test_index_search.py | 4 +++ tests/integration/aio/utils.py | 8 ++++- tests/integration/client.py | 11 ++++-- tests/integration/connection_test.py | 8 ++--- tests/integration/requests_test.py | 11 +++++- tests/integration/test_index_search.py | 4 +++ tests_v9/integration/aio/test_connection.py | 6 ++-- tests_v9/integration/aio/test_index_search.py | 1 + tests_v9/integration/aio/utils.py | 9 ++++- tests_v9/integration/client.py | 10 ++++-- tests_v9/integration/connection_test.py | 8 ++--- tests_v9/integration/requests_test.py | 9 ++++- tests_v9/integration/test_index_search.py | 1 + 15 files changed, 106 insertions(+), 25 deletions(-) diff --git a/pyatlan/test_utils/__init__.py b/pyatlan/test_utils/__init__.py index 01f485090..bc69c7814 100644 --- a/pyatlan/test_utils/__init__.py +++ b/pyatlan/test_utils/__init__.py @@ -32,14 +32,36 @@ class TestId: + # Slug-safe alphabet (lower-case alphanumerics only). Together with + # the hyphen separators in ``make_unique``, this guarantees every + # generated id matches ``^[a-z0-9-]+$`` — the same pattern the + # platform's asset-import (RAB) validator enforces for the + # connectorType segment of ``connectionQualifiedName``. Tests can + # therefore feed ``TestId.make_unique(...)`` directly into + # ``AtlanConnectorType.CREATE_CUSTOM(value=...)`` without tripping + # ``ErrorCode.INVALID_CONNECTION_QN`` (ATLAN-PYTHON-400-079, see + # BLDX-1294). The previous mixed-case + underscore-separated form + # caused every integration test that built a custom connector slug + # from ``MODULE_NAME`` to fail at fixture setup. session_id = generate_nanoid( - alphabet="1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ", + alphabet="1234567890abcdefghijklmnopqrstuvwxyz", size=5, ) @classmethod def make_unique(cls, input: str): - return f"psdk_{input}_{cls.session_id}" + """Return a slug-safe, run-unique id of the form + ``psdk--`` where every segment matches + ``^[a-z0-9-]+$``. + + Input is lower-cased and any underscores are converted to + hyphens, so callers don't need to remember to slug-format the + input themselves. Backward-compat note for searches keyed on + the old ``psdk_`` prefix: switch to ``psdk-`` or strip the + separator entirely. + """ + slug = input.lower().replace("_", "-") + return f"psdk-{slug}-{cls.session_id}" def get_random_connector(): @@ -57,7 +79,14 @@ def delete_token(client: AtlanClient, token: Optional[ApiToken] = None): delete_tokens = [ token for token in tokens - if token.display_name and "psdk_Requests" in token.display_name + # Match both pre-BLDX-1294 (``psdk_Requests``) and current + # (``psdk-requests``) display-name shapes so cleanup works + # across runs. + if token.display_name + and ( + "psdk_Requests" in token.display_name + or "psdk-requests" in token.display_name + ) ] for token in delete_tokens: assert token and token.guid # noqa: S101 diff --git a/tests/integration/aio/test_connection.py b/tests/integration/aio/test_connection.py index 2cf409e3b..ab661a025 100644 --- a/tests/integration/aio/test_connection.py +++ b/tests/integration/aio/test_connection.py @@ -39,7 +39,7 @@ async def custom_connection( ) -> AsyncGenerator[Connection, None]: CUSTOM_CONNECTOR_TYPE = AtlanConnectorType.CREATE_CUSTOM( name=f"{MODULE_NAME}_NAME", - value=f"{MODULE_NAME}_type", + value=f"{MODULE_NAME}-type", category=AtlanConnectionCategory.API, ) result = await create_connection_async( @@ -52,7 +52,7 @@ async def custom_connection( async def test_custom_connection(custom_connection: Connection): assert custom_connection.name == MODULE_NAME - assert custom_connection.connector_name == f"{MODULE_NAME.lower()}_type" + assert custom_connection.connector_name == f"{MODULE_NAME.lower()}-type" assert custom_connection.qualified_name assert custom_connection.category == AtlanConnectionCategory.API @@ -68,4 +68,4 @@ async def test_custom_connection_qualified_name( ) assert found assert found.name == MODULE_NAME - assert found.connector_name == f"{MODULE_NAME.lower()}_type" + assert found.connector_name == f"{MODULE_NAME.lower()}-type" diff --git a/tests/integration/aio/test_index_search.py b/tests/integration/aio/test_index_search.py index 9cb26e964..d7fd19115 100644 --- a/tests/integration/aio/test_index_search.py +++ b/tests/integration/aio/test_index_search.py @@ -404,8 +404,12 @@ async def _assert_search_results( @patch.object(LOGGER, "debug") async def test_search_pagination(mock_logger, client: AsyncAtlanClient): # Avoid testing on integration tests objects + # Match both legacy ``psdk_*`` (pre-BLDX-1294, underscore separator) + # and current ``psdk-*`` (slug-safe hyphen separator) test-asset + # names so older runs' artifacts aren't accidentally pulled in. exclude_sdk_terms = [ Asset.NAME.wildcard("psdk_*"), + Asset.NAME.wildcard("psdk-*"), Asset.NAME.wildcard("jsdk_*"), Asset.NAME.wildcard("gsdk_*"), ] diff --git a/tests/integration/aio/utils.py b/tests/integration/aio/utils.py index 095ed1639..dbabd3998 100644 --- a/tests/integration/aio/utils.py +++ b/tests/integration/aio/utils.py @@ -40,7 +40,13 @@ async def delete_token_async( delete_tokens = [ token for token in tokens - if token.display_name and "psdk_async" in token.display_name + # Match both pre-BLDX-1294 (``psdk_async``) and current + # (``psdk-async``) display-name shapes so cleanup works + # across runs. + if token.display_name + and ( + "psdk_async" in token.display_name or "psdk-async" in token.display_name + ) ] for token in delete_tokens: assert token and token.guid diff --git a/tests/integration/client.py b/tests/integration/client.py index 4762863b7..6623a576e 100644 --- a/tests/integration/client.py +++ b/tests/integration/client.py @@ -13,16 +13,23 @@ class TestId: + # Mirrors :class:`pyatlan.test_utils.TestId` — kept in sync so a + # value passed to ``AtlanConnectorType.CREATE_CUSTOM`` matches the + # platform's ``^[a-z0-9-]+$`` connectorType slug rule (BLDX-1294 / + # ATLAN-PYTHON-400-079). Lowercase alphanumeric session_id + hyphen + # separators + lowercased+hyphenated input. from nanoid import generate as generate_nanoid # type: ignore session_id = generate_nanoid( - alphabet="1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ", + alphabet="1234567890abcdefghijklmnopqrstuvwxyz", size=5, ) @classmethod def make_unique(cls, input: str): - return f"psdk_{input}_{cls.session_id}" + """See :meth:`pyatlan.test_utils.TestId.make_unique`.""" + slug = input.lower().replace("_", "-") + return f"psdk-{slug}-{cls.session_id}" @pytest.fixture(scope="module") diff --git a/tests/integration/connection_test.py b/tests/integration/connection_test.py index 8fc2329dd..92aab7939 100644 --- a/tests/integration/connection_test.py +++ b/tests/integration/connection_test.py @@ -33,7 +33,7 @@ def create_connection( def custom_connection(client: AtlanClient) -> Generator[Connection, None, None]: CUSTOM_CONNECTOR_TYPE = AtlanConnectorType.CREATE_CUSTOM( name=f"{MODULE_NAME}_NAME", - value=f"{MODULE_NAME}_type", + value=f"{MODULE_NAME}-type", category=AtlanConnectionCategory.API, ) result = create_connection( @@ -46,11 +46,11 @@ def custom_connection(client: AtlanClient) -> Generator[Connection, None, None]: def test_custom_connection(custom_connection: Connection): assert custom_connection.name == MODULE_NAME - assert custom_connection.connector_name == f"{MODULE_NAME.lower()}_type" + assert custom_connection.connector_name == f"{MODULE_NAME.lower()}-type" assert custom_connection.qualified_name - assert f"default/{MODULE_NAME.lower()}_type" in custom_connection.qualified_name + assert f"default/{MODULE_NAME.lower()}-type" in custom_connection.qualified_name assert ( - AtlanConnectorType[f"{MODULE_NAME}_NAME"].value == f"{MODULE_NAME.lower()}_type" + AtlanConnectorType[f"{MODULE_NAME}_NAME"].value == f"{MODULE_NAME.lower()}-type" ) diff --git a/tests/integration/requests_test.py b/tests/integration/requests_test.py index f4a059b10..ea573fd44 100644 --- a/tests/integration/requests_test.py +++ b/tests/integration/requests_test.py @@ -26,7 +26,16 @@ def delete_token(token_client: AtlanClient, token: Optional[ApiToken] = None): delete_tokens = [ token for token in tokens - if token.display_name and "psdk_Requests" in token.display_name + # Match both pre-BLDX-1294 (``psdk_Requests``, mixed-case + # underscore-separated) and current (``psdk-requests``, + # lower-case hyphen-separated) display-name shapes — so a + # cleanup that catches partial failures from older sessions + # still works. + if token.display_name + and ( + "psdk_Requests" in token.display_name + or "psdk-requests" in token.display_name + ) ] for token in delete_tokens: assert token and token.guid diff --git a/tests/integration/test_index_search.py b/tests/integration/test_index_search.py index 0b9c02ccd..59cda5a97 100644 --- a/tests/integration/test_index_search.py +++ b/tests/integration/test_index_search.py @@ -385,8 +385,12 @@ def _assert_search_results(results, expected_sorts, size, TOTAL_ASSETS, bulk=Fal @patch.object(LOGGER, "debug") def test_search_pagination(mock_logger, client: AtlanClient): # Avoid testing on integration tests objects + # Match both legacy ``psdk_*`` (pre-BLDX-1294, underscore separator) + # and current ``psdk-*`` (slug-safe hyphen separator) test-asset + # names so older runs' artifacts aren't accidentally pulled in. exclude_sdk_terms = [ Asset.NAME.wildcard("psdk_*"), + Asset.NAME.wildcard("psdk-*"), Asset.NAME.wildcard("jsdk_*"), Asset.NAME.wildcard("gsdk_*"), ] diff --git a/tests_v9/integration/aio/test_connection.py b/tests_v9/integration/aio/test_connection.py index c36271b5e..131aa8197 100644 --- a/tests_v9/integration/aio/test_connection.py +++ b/tests_v9/integration/aio/test_connection.py @@ -39,7 +39,7 @@ async def custom_connection( ) -> AsyncGenerator[Connection, None]: CUSTOM_CONNECTOR_TYPE = AtlanConnectorType.CREATE_CUSTOM( name=f"{MODULE_NAME}_NAME", - value=f"{MODULE_NAME}_type", + value=f"{MODULE_NAME}-type", category=AtlanConnectionCategory.API, ) result = await create_connection_async( @@ -52,7 +52,7 @@ async def custom_connection( async def test_custom_connection(custom_connection: Connection): assert custom_connection.name == MODULE_NAME - assert custom_connection.connector_name == f"{MODULE_NAME.lower()}_type" + assert custom_connection.connector_name == f"{MODULE_NAME.lower()}-type" assert custom_connection.qualified_name assert custom_connection.category == AtlanConnectionCategory.API @@ -68,4 +68,4 @@ async def test_custom_connection_qualified_name( ) assert found assert found.name == MODULE_NAME - assert found.connector_name == f"{MODULE_NAME.lower()}_type" + assert found.connector_name == f"{MODULE_NAME.lower()}-type" diff --git a/tests_v9/integration/aio/test_index_search.py b/tests_v9/integration/aio/test_index_search.py index 2cf094ea2..cb9e52919 100644 --- a/tests_v9/integration/aio/test_index_search.py +++ b/tests_v9/integration/aio/test_index_search.py @@ -405,6 +405,7 @@ async def test_search_pagination(mock_logger, client: AsyncAtlanClient): # Avoid testing on integration tests objects exclude_sdk_terms = [ Asset.NAME.wildcard("psdkv9_*"), + Asset.NAME.wildcard("psdkv9-*"), Asset.NAME.wildcard("jsdk_*"), Asset.NAME.wildcard("gsdk_*"), ] diff --git a/tests_v9/integration/aio/utils.py b/tests_v9/integration/aio/utils.py index cf4231264..d78edab98 100644 --- a/tests_v9/integration/aio/utils.py +++ b/tests_v9/integration/aio/utils.py @@ -44,7 +44,14 @@ async def delete_token_async( delete_tokens = [ token for token in tokens - if token.display_name and "psdkv9_Async" in token.display_name + # Match both pre-BLDX-1294 (``psdkv9_Async``) and current + # (``psdkv9-async``) display-name shapes so cleanup works + # across runs. + if token.display_name + and ( + "psdkv9_Async" in token.display_name + or "psdkv9-async" in token.display_name + ) ] for token in delete_tokens: assert token and token.guid diff --git a/tests_v9/integration/client.py b/tests_v9/integration/client.py index e8c4864cb..198e05d16 100644 --- a/tests_v9/integration/client.py +++ b/tests_v9/integration/client.py @@ -15,16 +15,22 @@ class TestId: + # Mirrors :class:`tests.integration.client.TestId` — kept in sync so + # a value passed to ``AtlanConnectorType.CREATE_CUSTOM`` matches the + # platform's ``^[a-z0-9-]+$`` connectorType slug rule (BLDX-1294 / + # ATLAN-PYTHON-400-079). Lowercase alphanumeric session_id + hyphen + # separators + lowercased+hyphenated input. from nanoid import generate as generate_nanoid # type: ignore session_id = generate_nanoid( - alphabet="1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ", + alphabet="1234567890abcdefghijklmnopqrstuvwxyz", size=5, ) @classmethod def make_unique(cls, input: str): - return f"psdkv9_{input}_{cls.session_id}" + slug = input.lower().replace("_", "-") + return f"psdkv9-{slug}-{cls.session_id}" @pytest.fixture(scope="module") diff --git a/tests_v9/integration/connection_test.py b/tests_v9/integration/connection_test.py index e80d706d3..2a22a9a06 100644 --- a/tests_v9/integration/connection_test.py +++ b/tests_v9/integration/connection_test.py @@ -33,7 +33,7 @@ def create_connection( def custom_connection(client: AtlanClient) -> Generator[Connection, None, None]: CUSTOM_CONNECTOR_TYPE = AtlanConnectorType.CREATE_CUSTOM( name=f"{MODULE_NAME}_NAME", - value=f"{MODULE_NAME}_type", + value=f"{MODULE_NAME}-type", category=AtlanConnectionCategory.API, ) result = create_connection( @@ -46,11 +46,11 @@ def custom_connection(client: AtlanClient) -> Generator[Connection, None, None]: def test_custom_connection(custom_connection: Connection): assert custom_connection.name == MODULE_NAME - assert custom_connection.connector_name == f"{MODULE_NAME.lower()}_type" + assert custom_connection.connector_name == f"{MODULE_NAME.lower()}-type" assert custom_connection.qualified_name - assert f"default/{MODULE_NAME.lower()}_type" in custom_connection.qualified_name + assert f"default/{MODULE_NAME.lower()}-type" in custom_connection.qualified_name assert ( - AtlanConnectorType[f"{MODULE_NAME}_NAME"].value == f"{MODULE_NAME.lower()}_type" + AtlanConnectorType[f"{MODULE_NAME}_NAME"].value == f"{MODULE_NAME.lower()}-type" ) diff --git a/tests_v9/integration/requests_test.py b/tests_v9/integration/requests_test.py index 1dc77468b..19389643e 100644 --- a/tests_v9/integration/requests_test.py +++ b/tests_v9/integration/requests_test.py @@ -26,7 +26,14 @@ def delete_token(token_client: AtlanClient, token: Optional[ApiToken] = None): delete_tokens = [ token for token in tokens - if token.display_name and "psdkv9_Requests" in token.display_name + # Match both pre-BLDX-1294 (``psdkv9_Requests``) and current + # (``psdkv9-requests``) display-name shapes so cleanup works + # across runs. + if token.display_name + and ( + "psdkv9_Requests" in token.display_name + or "psdkv9-requests" in token.display_name + ) ] for token in delete_tokens: assert token and token.guid diff --git a/tests_v9/integration/test_index_search.py b/tests_v9/integration/test_index_search.py index 9f4be2eea..8ee870f57 100644 --- a/tests_v9/integration/test_index_search.py +++ b/tests_v9/integration/test_index_search.py @@ -386,6 +386,7 @@ def test_search_pagination(mock_logger, client: AtlanClient): # Avoid testing on integration tests objects exclude_sdk_terms = [ Asset.NAME.wildcard("psdkv9_*"), + Asset.NAME.wildcard("psdkv9-*"), Asset.NAME.wildcard("jsdk_*"), Asset.NAME.wildcard("gsdk_*"), ]