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.
Files changed (53) hide show
  1. {airbyte_internal_ops-0.4.1.dist-info → airbyte_internal_ops-0.5.0.dist-info}/METADATA +1 -1
  2. {airbyte_internal_ops-0.4.1.dist-info → airbyte_internal_ops-0.5.0.dist-info}/RECORD +13 -52
  3. airbyte_ops_mcp/cli/cloud.py +42 -3
  4. airbyte_ops_mcp/cloud_admin/api_client.py +473 -0
  5. airbyte_ops_mcp/cloud_admin/models.py +56 -0
  6. airbyte_ops_mcp/mcp/cloud_connector_versions.py +460 -0
  7. airbyte_ops_mcp/mcp/prerelease.py +6 -46
  8. airbyte_ops_mcp/regression_tests/ci_output.py +151 -71
  9. airbyte_ops_mcp/regression_tests/http_metrics.py +21 -2
  10. airbyte_ops_mcp/regression_tests/models.py +6 -0
  11. airbyte_ops_mcp/telemetry.py +162 -0
  12. airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/.gitignore +0 -1
  13. airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/README.md +0 -420
  14. airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/__init__.py +0 -2
  15. airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/commons/__init__.py +0 -1
  16. airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/commons/backends/__init__.py +0 -8
  17. airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/commons/backends/base_backend.py +0 -16
  18. airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/commons/backends/duckdb_backend.py +0 -87
  19. airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/commons/backends/file_backend.py +0 -165
  20. airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/commons/connection_objects_retrieval.py +0 -377
  21. airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/commons/connector_runner.py +0 -247
  22. airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/commons/errors.py +0 -7
  23. airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/commons/evaluation_modes.py +0 -25
  24. airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/commons/hacks.py +0 -23
  25. airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/commons/json_schema_helper.py +0 -384
  26. airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/commons/mitm_addons.py +0 -37
  27. airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/commons/models.py +0 -595
  28. airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/commons/proxy.py +0 -207
  29. airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/commons/secret_access.py +0 -47
  30. airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/commons/segment_tracking.py +0 -45
  31. airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/commons/utils.py +0 -214
  32. airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/conftest.py.disabled +0 -751
  33. airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/consts.py +0 -4
  34. airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/poetry.lock +0 -4480
  35. airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/pytest.ini +0 -9
  36. airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/regression_tests/__init__.py +0 -1
  37. airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/regression_tests/test_check.py +0 -61
  38. airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/regression_tests/test_discover.py +0 -117
  39. airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/regression_tests/test_read.py +0 -627
  40. airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/regression_tests/test_spec.py +0 -43
  41. airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/report.py +0 -542
  42. airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/stash_keys.py +0 -38
  43. airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/templates/__init__.py +0 -0
  44. airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/templates/private_details.html.j2 +0 -305
  45. airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/templates/report.html.j2 +0 -515
  46. airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/utils.py +0 -187
  47. airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/validation_tests/__init__.py +0 -0
  48. airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/validation_tests/test_check.py +0 -61
  49. airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/validation_tests/test_discover.py +0 -217
  50. airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/validation_tests/test_read.py +0 -177
  51. airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/validation_tests/test_spec.py +0 -631
  52. {airbyte_internal_ops-0.4.1.dist-info → airbyte_internal_ops-0.5.0.dist-info}/WHEEL +0 -0
  53. {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}"