airbyte-internal-ops 0.4.1__py3-none-any.whl → 0.5.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {airbyte_internal_ops-0.4.1.dist-info → airbyte_internal_ops-0.5.0.dist-info}/METADATA +1 -1
- {airbyte_internal_ops-0.4.1.dist-info → airbyte_internal_ops-0.5.0.dist-info}/RECORD +13 -52
- airbyte_ops_mcp/cli/cloud.py +42 -3
- airbyte_ops_mcp/cloud_admin/api_client.py +473 -0
- airbyte_ops_mcp/cloud_admin/models.py +56 -0
- airbyte_ops_mcp/mcp/cloud_connector_versions.py +460 -0
- airbyte_ops_mcp/mcp/prerelease.py +6 -46
- airbyte_ops_mcp/regression_tests/ci_output.py +151 -71
- airbyte_ops_mcp/regression_tests/http_metrics.py +21 -2
- airbyte_ops_mcp/regression_tests/models.py +6 -0
- airbyte_ops_mcp/telemetry.py +162 -0
- airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/.gitignore +0 -1
- airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/README.md +0 -420
- airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/__init__.py +0 -2
- airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/commons/__init__.py +0 -1
- airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/commons/backends/__init__.py +0 -8
- airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/commons/backends/base_backend.py +0 -16
- airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/commons/backends/duckdb_backend.py +0 -87
- airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/commons/backends/file_backend.py +0 -165
- airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/commons/connection_objects_retrieval.py +0 -377
- airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/commons/connector_runner.py +0 -247
- airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/commons/errors.py +0 -7
- airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/commons/evaluation_modes.py +0 -25
- airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/commons/hacks.py +0 -23
- airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/commons/json_schema_helper.py +0 -384
- airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/commons/mitm_addons.py +0 -37
- airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/commons/models.py +0 -595
- airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/commons/proxy.py +0 -207
- airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/commons/secret_access.py +0 -47
- airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/commons/segment_tracking.py +0 -45
- airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/commons/utils.py +0 -214
- airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/conftest.py.disabled +0 -751
- airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/consts.py +0 -4
- airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/poetry.lock +0 -4480
- airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/pytest.ini +0 -9
- airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/regression_tests/__init__.py +0 -1
- airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/regression_tests/test_check.py +0 -61
- airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/regression_tests/test_discover.py +0 -117
- airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/regression_tests/test_read.py +0 -627
- airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/regression_tests/test_spec.py +0 -43
- airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/report.py +0 -542
- airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/stash_keys.py +0 -38
- airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/templates/__init__.py +0 -0
- airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/templates/private_details.html.j2 +0 -305
- airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/templates/report.html.j2 +0 -515
- airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/utils.py +0 -187
- airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/validation_tests/__init__.py +0 -0
- airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/validation_tests/test_check.py +0 -61
- airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/validation_tests/test_discover.py +0 -217
- airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/validation_tests/test_read.py +0 -177
- airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/validation_tests/test_spec.py +0 -631
- {airbyte_internal_ops-0.4.1.dist-info → airbyte_internal_ops-0.5.0.dist-info}/WHEEL +0 -0
- {airbyte_internal_ops-0.4.1.dist-info → airbyte_internal_ops-0.5.0.dist-info}/entry_points.txt +0 -0
|
@@ -15,6 +15,9 @@ from airbyte import constants
|
|
|
15
15
|
from airbyte.exceptions import PyAirbyteInputError
|
|
16
16
|
|
|
17
17
|
from airbyte_ops_mcp import constants as ops_constants
|
|
18
|
+
from airbyte_ops_mcp.mcp.prod_db_queries import (
|
|
19
|
+
_resolve_canonical_name_to_definition_id,
|
|
20
|
+
)
|
|
18
21
|
|
|
19
22
|
# Internal enums for scoped configuration API "magic strings"
|
|
20
23
|
# These values caused issues during development and are now centralized here
|
|
@@ -959,3 +962,473 @@ def set_connector_version_override(
|
|
|
959
962
|
)
|
|
960
963
|
|
|
961
964
|
return True
|
|
965
|
+
|
|
966
|
+
|
|
967
|
+
def set_workspace_connector_version_override(
|
|
968
|
+
workspace_id: str,
|
|
969
|
+
connector_name: str,
|
|
970
|
+
connector_type: Literal["source", "destination"],
|
|
971
|
+
api_root: str,
|
|
972
|
+
client_id: str | None = None,
|
|
973
|
+
client_secret: str | None = None,
|
|
974
|
+
version: str | None = None,
|
|
975
|
+
unset: bool = False,
|
|
976
|
+
override_reason: str | None = None,
|
|
977
|
+
override_reason_reference_url: str | None = None,
|
|
978
|
+
user_email: str | None = None,
|
|
979
|
+
bearer_token: str | None = None,
|
|
980
|
+
) -> bool:
|
|
981
|
+
"""Set or clear a workspace-level version override for a connector type.
|
|
982
|
+
|
|
983
|
+
This pins ALL instances of a connector type within a workspace to a specific version.
|
|
984
|
+
For example, pinning 'source-github' at workspace level means all GitHub sources
|
|
985
|
+
in that workspace will use the pinned version.
|
|
986
|
+
|
|
987
|
+
Args:
|
|
988
|
+
workspace_id: The workspace ID
|
|
989
|
+
connector_name: The connector name (e.g., 'source-github')
|
|
990
|
+
connector_type: Either "source" or "destination"
|
|
991
|
+
api_root: The API root URL
|
|
992
|
+
client_id: The Airbyte Cloud client ID (required if no bearer_token)
|
|
993
|
+
client_secret: The Airbyte Cloud client secret (required if no bearer_token)
|
|
994
|
+
version: The version to pin to (e.g., "0.1.0"), or None to unset
|
|
995
|
+
unset: If True, removes any existing override
|
|
996
|
+
override_reason: Required when setting. Explanation for the override
|
|
997
|
+
override_reason_reference_url: Optional URL with more context
|
|
998
|
+
user_email: Email of user creating the override
|
|
999
|
+
bearer_token: Pre-existing bearer token (takes precedence over client credentials)
|
|
1000
|
+
|
|
1001
|
+
Returns:
|
|
1002
|
+
True if operation succeeded, False if no override existed (unset only)
|
|
1003
|
+
|
|
1004
|
+
Raises:
|
|
1005
|
+
PyAirbyteInputError: If the API request fails or parameters are invalid
|
|
1006
|
+
"""
|
|
1007
|
+
# Input validation
|
|
1008
|
+
if (version is None) == (not unset):
|
|
1009
|
+
raise PyAirbyteInputError(
|
|
1010
|
+
message="Must specify EXACTLY ONE of version (to set) OR unset=True (to clear), but not both",
|
|
1011
|
+
)
|
|
1012
|
+
|
|
1013
|
+
if not unset and (not override_reason or len(override_reason.strip()) < 10):
|
|
1014
|
+
raise PyAirbyteInputError(
|
|
1015
|
+
message="override_reason is required when setting a version and must be at least 10 characters",
|
|
1016
|
+
)
|
|
1017
|
+
|
|
1018
|
+
access_token = _get_access_token(client_id, client_secret, bearer_token)
|
|
1019
|
+
|
|
1020
|
+
# Resolve connector name to actor_definition_id using the shared registry lookup
|
|
1021
|
+
actor_definition_id = _resolve_canonical_name_to_definition_id(connector_name)
|
|
1022
|
+
|
|
1023
|
+
if unset:
|
|
1024
|
+
# Get the existing workspace-level configuration
|
|
1025
|
+
active_config = _get_scoped_configuration_context(
|
|
1026
|
+
actor_definition_id=actor_definition_id,
|
|
1027
|
+
scope_type=_ScopeType.WORKSPACE,
|
|
1028
|
+
scope_id=workspace_id,
|
|
1029
|
+
api_root=api_root,
|
|
1030
|
+
access_token=access_token,
|
|
1031
|
+
)
|
|
1032
|
+
|
|
1033
|
+
if not active_config:
|
|
1034
|
+
return False
|
|
1035
|
+
|
|
1036
|
+
# Verify this is actually a workspace-scoped config (not inherited from org)
|
|
1037
|
+
api_scope_type = active_config.get("scope_type", "").lower()
|
|
1038
|
+
api_scope_id = active_config.get("scope_id", "")
|
|
1039
|
+
if api_scope_type != _ScopeType.WORKSPACE.value or api_scope_id != workspace_id:
|
|
1040
|
+
raise PyAirbyteInputError(
|
|
1041
|
+
message=f"Cannot delete: the active config is not workspace-scoped. "
|
|
1042
|
+
f"Expected scope_type='{_ScopeType.WORKSPACE.value}' and scope_id='{workspace_id}', "
|
|
1043
|
+
f"but got scope_type='{api_scope_type}' and scope_id='{api_scope_id}'. "
|
|
1044
|
+
f"This may be an inherited config from organization level.",
|
|
1045
|
+
context={
|
|
1046
|
+
"workspace_id": workspace_id,
|
|
1047
|
+
"expected_scope_type": _ScopeType.WORKSPACE.value,
|
|
1048
|
+
"actual_scope_type": api_scope_type,
|
|
1049
|
+
"actual_scope_id": api_scope_id,
|
|
1050
|
+
"full_config": active_config,
|
|
1051
|
+
},
|
|
1052
|
+
)
|
|
1053
|
+
|
|
1054
|
+
# Delete the configuration
|
|
1055
|
+
delete_endpoint = f"{api_root}/scoped_configuration/delete"
|
|
1056
|
+
delete_payload = {"scopedConfigurationId": active_config["id"]}
|
|
1057
|
+
|
|
1058
|
+
response = requests.post(
|
|
1059
|
+
delete_endpoint,
|
|
1060
|
+
json=delete_payload,
|
|
1061
|
+
headers={
|
|
1062
|
+
"Authorization": f"Bearer {access_token}",
|
|
1063
|
+
"User-Agent": ops_constants.USER_AGENT,
|
|
1064
|
+
"Content-Type": "application/json",
|
|
1065
|
+
},
|
|
1066
|
+
timeout=30,
|
|
1067
|
+
)
|
|
1068
|
+
|
|
1069
|
+
if response.status_code not in (200, 204):
|
|
1070
|
+
raise PyAirbyteInputError(
|
|
1071
|
+
message=f"Failed to delete workspace version override: {response.status_code} {response.text}",
|
|
1072
|
+
context={
|
|
1073
|
+
"delete_endpoint": delete_endpoint,
|
|
1074
|
+
"config_id": active_config["id"],
|
|
1075
|
+
"status_code": response.status_code,
|
|
1076
|
+
"response": response.text,
|
|
1077
|
+
},
|
|
1078
|
+
)
|
|
1079
|
+
|
|
1080
|
+
return True
|
|
1081
|
+
|
|
1082
|
+
# Set a new workspace-level override
|
|
1083
|
+
# Resolve version string to version ID
|
|
1084
|
+
version_id = resolve_connector_version_id(
|
|
1085
|
+
actor_definition_id=actor_definition_id,
|
|
1086
|
+
connector_type=connector_type,
|
|
1087
|
+
version=version,
|
|
1088
|
+
api_root=api_root,
|
|
1089
|
+
bearer_token=access_token,
|
|
1090
|
+
)
|
|
1091
|
+
|
|
1092
|
+
# Check for existing workspace-level configuration
|
|
1093
|
+
existing_config = _get_scoped_configuration_context(
|
|
1094
|
+
actor_definition_id=actor_definition_id,
|
|
1095
|
+
scope_type=_ScopeType.WORKSPACE,
|
|
1096
|
+
scope_id=workspace_id,
|
|
1097
|
+
api_root=api_root,
|
|
1098
|
+
access_token=access_token,
|
|
1099
|
+
)
|
|
1100
|
+
|
|
1101
|
+
if existing_config:
|
|
1102
|
+
existing_version_id = existing_config.get("value")
|
|
1103
|
+
existing_version_name = existing_config.get("value_name", "unknown")
|
|
1104
|
+
|
|
1105
|
+
# If already pinned to the same version, no action needed
|
|
1106
|
+
if existing_version_id == version_id:
|
|
1107
|
+
raise PyAirbyteInputError(
|
|
1108
|
+
message=f"Workspace is already pinned to version {existing_version_name} for {connector_name}. "
|
|
1109
|
+
f"Use unset=True first if you want to re-pin to a different version.",
|
|
1110
|
+
context={
|
|
1111
|
+
"workspace_id": workspace_id,
|
|
1112
|
+
"connector_name": connector_name,
|
|
1113
|
+
"existing_version": existing_version_name,
|
|
1114
|
+
"requested_version": version,
|
|
1115
|
+
},
|
|
1116
|
+
)
|
|
1117
|
+
|
|
1118
|
+
# Verify this is a workspace-scoped config before deleting
|
|
1119
|
+
api_scope_type = existing_config.get("scope_type", "").lower()
|
|
1120
|
+
api_scope_id = existing_config.get("scope_id", "")
|
|
1121
|
+
if (
|
|
1122
|
+
api_scope_type == _ScopeType.WORKSPACE.value
|
|
1123
|
+
and api_scope_id == workspace_id
|
|
1124
|
+
):
|
|
1125
|
+
# Delete existing workspace-level config before creating new one
|
|
1126
|
+
delete_endpoint = f"{api_root}/scoped_configuration/delete"
|
|
1127
|
+
delete_payload = {"scopedConfigurationId": existing_config["id"]}
|
|
1128
|
+
|
|
1129
|
+
delete_response = requests.post(
|
|
1130
|
+
delete_endpoint,
|
|
1131
|
+
json=delete_payload,
|
|
1132
|
+
headers={
|
|
1133
|
+
"Authorization": f"Bearer {access_token}",
|
|
1134
|
+
"User-Agent": ops_constants.USER_AGENT,
|
|
1135
|
+
"Content-Type": "application/json",
|
|
1136
|
+
},
|
|
1137
|
+
timeout=30,
|
|
1138
|
+
)
|
|
1139
|
+
|
|
1140
|
+
if delete_response.status_code not in (200, 204):
|
|
1141
|
+
raise PyAirbyteInputError(
|
|
1142
|
+
message=f"Failed to delete existing workspace version override: "
|
|
1143
|
+
f"{delete_response.status_code} {delete_response.text}",
|
|
1144
|
+
)
|
|
1145
|
+
|
|
1146
|
+
# Get user ID from email
|
|
1147
|
+
if not user_email:
|
|
1148
|
+
raise PyAirbyteInputError(
|
|
1149
|
+
message="user_email is required to set a version override",
|
|
1150
|
+
)
|
|
1151
|
+
origin = get_user_id_by_email(
|
|
1152
|
+
email=user_email,
|
|
1153
|
+
api_root=api_root,
|
|
1154
|
+
bearer_token=access_token,
|
|
1155
|
+
)
|
|
1156
|
+
|
|
1157
|
+
# Create the override
|
|
1158
|
+
endpoint = f"{api_root}/scoped_configuration/create"
|
|
1159
|
+
payload: dict[str, Any] = {
|
|
1160
|
+
"config_key": _ScopedConfigKey.CONNECTOR_VERSION.value,
|
|
1161
|
+
"resource_type": _ResourceType.ACTOR_DEFINITION.value,
|
|
1162
|
+
"resource_id": actor_definition_id,
|
|
1163
|
+
"scope_type": _ScopeType.WORKSPACE.value,
|
|
1164
|
+
"scope_id": workspace_id,
|
|
1165
|
+
"value": version_id,
|
|
1166
|
+
"description": override_reason,
|
|
1167
|
+
"origin_type": _OriginType.USER.value,
|
|
1168
|
+
"origin": origin,
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
if override_reason_reference_url:
|
|
1172
|
+
payload["reference_url"] = override_reason_reference_url
|
|
1173
|
+
|
|
1174
|
+
response = requests.post(
|
|
1175
|
+
endpoint,
|
|
1176
|
+
json=payload,
|
|
1177
|
+
headers={
|
|
1178
|
+
"Authorization": f"Bearer {access_token}",
|
|
1179
|
+
"User-Agent": ops_constants.USER_AGENT,
|
|
1180
|
+
"Content-Type": "application/json",
|
|
1181
|
+
},
|
|
1182
|
+
timeout=30,
|
|
1183
|
+
)
|
|
1184
|
+
|
|
1185
|
+
if response.status_code not in (200, 201):
|
|
1186
|
+
raise PyAirbyteInputError(
|
|
1187
|
+
message=f"Failed to set workspace version override: {response.status_code} {response.text}",
|
|
1188
|
+
context={
|
|
1189
|
+
"workspace_id": workspace_id,
|
|
1190
|
+
"connector_name": connector_name,
|
|
1191
|
+
"version": version,
|
|
1192
|
+
"endpoint": endpoint,
|
|
1193
|
+
"status_code": response.status_code,
|
|
1194
|
+
"response": response.text,
|
|
1195
|
+
},
|
|
1196
|
+
)
|
|
1197
|
+
|
|
1198
|
+
return True
|
|
1199
|
+
|
|
1200
|
+
|
|
1201
|
+
def set_organization_connector_version_override(
|
|
1202
|
+
organization_id: str,
|
|
1203
|
+
connector_name: str,
|
|
1204
|
+
connector_type: Literal["source", "destination"],
|
|
1205
|
+
api_root: str,
|
|
1206
|
+
client_id: str | None = None,
|
|
1207
|
+
client_secret: str | None = None,
|
|
1208
|
+
version: str | None = None,
|
|
1209
|
+
unset: bool = False,
|
|
1210
|
+
override_reason: str | None = None,
|
|
1211
|
+
override_reason_reference_url: str | None = None,
|
|
1212
|
+
user_email: str | None = None,
|
|
1213
|
+
bearer_token: str | None = None,
|
|
1214
|
+
) -> bool:
|
|
1215
|
+
"""Set or clear an organization-level version override for a connector type.
|
|
1216
|
+
|
|
1217
|
+
This pins ALL instances of a connector type across an entire organization to a
|
|
1218
|
+
specific version. For example, pinning 'source-github' at organization level means
|
|
1219
|
+
all GitHub sources in all workspaces within that organization will use the pinned version.
|
|
1220
|
+
|
|
1221
|
+
Args:
|
|
1222
|
+
organization_id: The organization ID
|
|
1223
|
+
connector_name: The connector name (e.g., 'source-github')
|
|
1224
|
+
connector_type: Either "source" or "destination"
|
|
1225
|
+
api_root: The API root URL
|
|
1226
|
+
client_id: The Airbyte Cloud client ID (required if no bearer_token)
|
|
1227
|
+
client_secret: The Airbyte Cloud client secret (required if no bearer_token)
|
|
1228
|
+
version: The version to pin to (e.g., "0.1.0"), or None to unset
|
|
1229
|
+
unset: If True, removes any existing override
|
|
1230
|
+
override_reason: Required when setting. Explanation for the override
|
|
1231
|
+
override_reason_reference_url: Optional URL with more context
|
|
1232
|
+
user_email: Email of user creating the override
|
|
1233
|
+
bearer_token: Pre-existing bearer token (takes precedence over client credentials)
|
|
1234
|
+
|
|
1235
|
+
Returns:
|
|
1236
|
+
True if operation succeeded, False if no override existed (unset only)
|
|
1237
|
+
|
|
1238
|
+
Raises:
|
|
1239
|
+
PyAirbyteInputError: If the API request fails or parameters are invalid
|
|
1240
|
+
"""
|
|
1241
|
+
# Input validation
|
|
1242
|
+
if (version is None) == (not unset):
|
|
1243
|
+
raise PyAirbyteInputError(
|
|
1244
|
+
message="Must specify EXACTLY ONE of version (to set) OR unset=True (to clear), but not both",
|
|
1245
|
+
)
|
|
1246
|
+
|
|
1247
|
+
if not unset and (not override_reason or len(override_reason.strip()) < 10):
|
|
1248
|
+
raise PyAirbyteInputError(
|
|
1249
|
+
message="override_reason is required when setting a version and must be at least 10 characters",
|
|
1250
|
+
)
|
|
1251
|
+
|
|
1252
|
+
access_token = _get_access_token(client_id, client_secret, bearer_token)
|
|
1253
|
+
|
|
1254
|
+
# Resolve connector name to actor_definition_id using the shared registry lookup
|
|
1255
|
+
actor_definition_id = _resolve_canonical_name_to_definition_id(connector_name)
|
|
1256
|
+
|
|
1257
|
+
if unset:
|
|
1258
|
+
# Get the existing organization-level configuration
|
|
1259
|
+
active_config = _get_scoped_configuration_context(
|
|
1260
|
+
actor_definition_id=actor_definition_id,
|
|
1261
|
+
scope_type=_ScopeType.ORGANIZATION,
|
|
1262
|
+
scope_id=organization_id,
|
|
1263
|
+
api_root=api_root,
|
|
1264
|
+
access_token=access_token,
|
|
1265
|
+
)
|
|
1266
|
+
|
|
1267
|
+
if not active_config:
|
|
1268
|
+
return False
|
|
1269
|
+
|
|
1270
|
+
# Verify this is actually an organization-scoped config
|
|
1271
|
+
api_scope_type = active_config.get("scope_type", "").lower()
|
|
1272
|
+
api_scope_id = active_config.get("scope_id", "")
|
|
1273
|
+
if (
|
|
1274
|
+
api_scope_type != _ScopeType.ORGANIZATION.value
|
|
1275
|
+
or api_scope_id != organization_id
|
|
1276
|
+
):
|
|
1277
|
+
raise PyAirbyteInputError(
|
|
1278
|
+
message=f"Cannot delete: the active config is not organization-scoped. "
|
|
1279
|
+
f"Expected scope_type='{_ScopeType.ORGANIZATION.value}' and scope_id='{organization_id}', "
|
|
1280
|
+
f"but got scope_type='{api_scope_type}' and scope_id='{api_scope_id}'.",
|
|
1281
|
+
context={
|
|
1282
|
+
"organization_id": organization_id,
|
|
1283
|
+
"expected_scope_type": _ScopeType.ORGANIZATION.value,
|
|
1284
|
+
"actual_scope_type": api_scope_type,
|
|
1285
|
+
"actual_scope_id": api_scope_id,
|
|
1286
|
+
"full_config": active_config,
|
|
1287
|
+
},
|
|
1288
|
+
)
|
|
1289
|
+
|
|
1290
|
+
# Delete the configuration
|
|
1291
|
+
delete_endpoint = f"{api_root}/scoped_configuration/delete"
|
|
1292
|
+
delete_payload = {"scopedConfigurationId": active_config["id"]}
|
|
1293
|
+
|
|
1294
|
+
response = requests.post(
|
|
1295
|
+
delete_endpoint,
|
|
1296
|
+
json=delete_payload,
|
|
1297
|
+
headers={
|
|
1298
|
+
"Authorization": f"Bearer {access_token}",
|
|
1299
|
+
"User-Agent": ops_constants.USER_AGENT,
|
|
1300
|
+
"Content-Type": "application/json",
|
|
1301
|
+
},
|
|
1302
|
+
timeout=30,
|
|
1303
|
+
)
|
|
1304
|
+
|
|
1305
|
+
if response.status_code not in (200, 204):
|
|
1306
|
+
raise PyAirbyteInputError(
|
|
1307
|
+
message=f"Failed to delete organization version override: {response.status_code} {response.text}",
|
|
1308
|
+
context={
|
|
1309
|
+
"delete_endpoint": delete_endpoint,
|
|
1310
|
+
"config_id": active_config["id"],
|
|
1311
|
+
"status_code": response.status_code,
|
|
1312
|
+
"response": response.text,
|
|
1313
|
+
},
|
|
1314
|
+
)
|
|
1315
|
+
|
|
1316
|
+
return True
|
|
1317
|
+
|
|
1318
|
+
# Set a new organization-level override
|
|
1319
|
+
# Resolve version string to version ID
|
|
1320
|
+
version_id = resolve_connector_version_id(
|
|
1321
|
+
actor_definition_id=actor_definition_id,
|
|
1322
|
+
connector_type=connector_type,
|
|
1323
|
+
version=version,
|
|
1324
|
+
api_root=api_root,
|
|
1325
|
+
bearer_token=access_token,
|
|
1326
|
+
)
|
|
1327
|
+
|
|
1328
|
+
# Check for existing organization-level configuration
|
|
1329
|
+
existing_config = _get_scoped_configuration_context(
|
|
1330
|
+
actor_definition_id=actor_definition_id,
|
|
1331
|
+
scope_type=_ScopeType.ORGANIZATION,
|
|
1332
|
+
scope_id=organization_id,
|
|
1333
|
+
api_root=api_root,
|
|
1334
|
+
access_token=access_token,
|
|
1335
|
+
)
|
|
1336
|
+
|
|
1337
|
+
if existing_config:
|
|
1338
|
+
existing_version_id = existing_config.get("value")
|
|
1339
|
+
existing_version_name = existing_config.get("value_name", "unknown")
|
|
1340
|
+
|
|
1341
|
+
# If already pinned to the same version, no action needed
|
|
1342
|
+
if existing_version_id == version_id:
|
|
1343
|
+
raise PyAirbyteInputError(
|
|
1344
|
+
message=f"Organization is already pinned to version {existing_version_name} for {connector_name}. "
|
|
1345
|
+
f"Use unset=True first if you want to re-pin to a different version.",
|
|
1346
|
+
context={
|
|
1347
|
+
"organization_id": organization_id,
|
|
1348
|
+
"connector_name": connector_name,
|
|
1349
|
+
"existing_version": existing_version_name,
|
|
1350
|
+
"requested_version": version,
|
|
1351
|
+
},
|
|
1352
|
+
)
|
|
1353
|
+
|
|
1354
|
+
# Verify this is an organization-scoped config before deleting
|
|
1355
|
+
api_scope_type = existing_config.get("scope_type", "").lower()
|
|
1356
|
+
api_scope_id = existing_config.get("scope_id", "")
|
|
1357
|
+
if (
|
|
1358
|
+
api_scope_type == _ScopeType.ORGANIZATION.value
|
|
1359
|
+
and api_scope_id == organization_id
|
|
1360
|
+
):
|
|
1361
|
+
# Delete existing organization-level config before creating new one
|
|
1362
|
+
delete_endpoint = f"{api_root}/scoped_configuration/delete"
|
|
1363
|
+
delete_payload = {"scopedConfigurationId": existing_config["id"]}
|
|
1364
|
+
|
|
1365
|
+
delete_response = requests.post(
|
|
1366
|
+
delete_endpoint,
|
|
1367
|
+
json=delete_payload,
|
|
1368
|
+
headers={
|
|
1369
|
+
"Authorization": f"Bearer {access_token}",
|
|
1370
|
+
"User-Agent": ops_constants.USER_AGENT,
|
|
1371
|
+
"Content-Type": "application/json",
|
|
1372
|
+
},
|
|
1373
|
+
timeout=30,
|
|
1374
|
+
)
|
|
1375
|
+
|
|
1376
|
+
if delete_response.status_code not in (200, 204):
|
|
1377
|
+
raise PyAirbyteInputError(
|
|
1378
|
+
message=f"Failed to delete existing organization version override: "
|
|
1379
|
+
f"{delete_response.status_code} {delete_response.text}",
|
|
1380
|
+
)
|
|
1381
|
+
|
|
1382
|
+
# Get user ID from email
|
|
1383
|
+
if not user_email:
|
|
1384
|
+
raise PyAirbyteInputError(
|
|
1385
|
+
message="user_email is required to set a version override",
|
|
1386
|
+
)
|
|
1387
|
+
origin = get_user_id_by_email(
|
|
1388
|
+
email=user_email,
|
|
1389
|
+
api_root=api_root,
|
|
1390
|
+
bearer_token=access_token,
|
|
1391
|
+
)
|
|
1392
|
+
|
|
1393
|
+
# Create the override
|
|
1394
|
+
endpoint = f"{api_root}/scoped_configuration/create"
|
|
1395
|
+
payload: dict[str, Any] = {
|
|
1396
|
+
"config_key": _ScopedConfigKey.CONNECTOR_VERSION.value,
|
|
1397
|
+
"resource_type": _ResourceType.ACTOR_DEFINITION.value,
|
|
1398
|
+
"resource_id": actor_definition_id,
|
|
1399
|
+
"scope_type": _ScopeType.ORGANIZATION.value,
|
|
1400
|
+
"scope_id": organization_id,
|
|
1401
|
+
"value": version_id,
|
|
1402
|
+
"description": override_reason,
|
|
1403
|
+
"origin_type": _OriginType.USER.value,
|
|
1404
|
+
"origin": origin,
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1407
|
+
if override_reason_reference_url:
|
|
1408
|
+
payload["reference_url"] = override_reason_reference_url
|
|
1409
|
+
|
|
1410
|
+
response = requests.post(
|
|
1411
|
+
endpoint,
|
|
1412
|
+
json=payload,
|
|
1413
|
+
headers={
|
|
1414
|
+
"Authorization": f"Bearer {access_token}",
|
|
1415
|
+
"User-Agent": ops_constants.USER_AGENT,
|
|
1416
|
+
"Content-Type": "application/json",
|
|
1417
|
+
},
|
|
1418
|
+
timeout=30,
|
|
1419
|
+
)
|
|
1420
|
+
|
|
1421
|
+
if response.status_code not in (200, 201):
|
|
1422
|
+
raise PyAirbyteInputError(
|
|
1423
|
+
message=f"Failed to set organization version override: {response.status_code} {response.text}",
|
|
1424
|
+
context={
|
|
1425
|
+
"organization_id": organization_id,
|
|
1426
|
+
"connector_name": connector_name,
|
|
1427
|
+
"version": version,
|
|
1428
|
+
"endpoint": endpoint,
|
|
1429
|
+
"status_code": response.status_code,
|
|
1430
|
+
"response": response.text,
|
|
1431
|
+
},
|
|
1432
|
+
)
|
|
1433
|
+
|
|
1434
|
+
return True
|
|
@@ -71,3 +71,59 @@ class VersionOverrideOperationResult(BaseModel):
|
|
|
71
71
|
if self.success:
|
|
72
72
|
return f"✓ {self.message}"
|
|
73
73
|
return f"✗ {self.message}"
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class WorkspaceVersionOverrideResult(BaseModel):
|
|
77
|
+
"""Result of a workspace-level version override operation.
|
|
78
|
+
|
|
79
|
+
This model provides detailed information about the outcome of a workspace-level
|
|
80
|
+
version pinning or unpinning operation.
|
|
81
|
+
"""
|
|
82
|
+
|
|
83
|
+
success: bool = Field(description="Whether the operation succeeded")
|
|
84
|
+
message: str = Field(description="Human-readable message describing the result")
|
|
85
|
+
workspace_id: str = Field(description="The workspace ID")
|
|
86
|
+
connector_name: str = Field(
|
|
87
|
+
description="The connector name (e.g., 'source-github')"
|
|
88
|
+
)
|
|
89
|
+
connector_type: Literal["source", "destination"] = Field(
|
|
90
|
+
description="The type of connector (source or destination)"
|
|
91
|
+
)
|
|
92
|
+
version: str | None = Field(
|
|
93
|
+
default=None,
|
|
94
|
+
description="The version that was pinned (None if cleared or failed)",
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
def __str__(self) -> str:
|
|
98
|
+
"""Return a string representation of the operation result."""
|
|
99
|
+
if self.success:
|
|
100
|
+
return f"✓ {self.message}"
|
|
101
|
+
return f"✗ {self.message}"
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class OrganizationVersionOverrideResult(BaseModel):
|
|
105
|
+
"""Result of an organization-level version override operation.
|
|
106
|
+
|
|
107
|
+
This model provides detailed information about the outcome of an organization-level
|
|
108
|
+
version pinning or unpinning operation.
|
|
109
|
+
"""
|
|
110
|
+
|
|
111
|
+
success: bool = Field(description="Whether the operation succeeded")
|
|
112
|
+
message: str = Field(description="Human-readable message describing the result")
|
|
113
|
+
organization_id: str = Field(description="The organization ID")
|
|
114
|
+
connector_name: str = Field(
|
|
115
|
+
description="The connector name (e.g., 'source-github')"
|
|
116
|
+
)
|
|
117
|
+
connector_type: Literal["source", "destination"] = Field(
|
|
118
|
+
description="The type of connector (source or destination)"
|
|
119
|
+
)
|
|
120
|
+
version: str | None = Field(
|
|
121
|
+
default=None,
|
|
122
|
+
description="The version that was pinned (None if cleared or failed)",
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
def __str__(self) -> str:
|
|
126
|
+
"""Return a string representation of the operation result."""
|
|
127
|
+
if self.success:
|
|
128
|
+
return f"✓ {self.message}"
|
|
129
|
+
return f"✗ {self.message}"
|