airbyte-internal-ops 0.4.2__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.2.dist-info → airbyte_internal_ops-0.5.0.dist-info}/METADATA +1 -1
  2. {airbyte_internal_ops-0.4.2.dist-info → airbyte_internal_ops-0.5.0.dist-info}/RECORD +13 -52
  3. airbyte_ops_mcp/cli/cloud.py +27 -0
  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 +5 -44
  8. airbyte_ops_mcp/regression_tests/ci_output.py +8 -4
  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.2.dist-info → airbyte_internal_ops-0.5.0.dist-info}/WHEEL +0 -0
  53. {airbyte_internal_ops-0.4.2.dist-info → airbyte_internal_ops-0.5.0.dist-info}/entry_points.txt +0 -0
@@ -1,751 +0,0 @@
1
- #
2
- # Copyright (c) 2025 Airbyte, Inc., all rights reserved.
3
- #
4
-
5
- from __future__ import annotations
6
-
7
- import logging
8
- import os
9
- import textwrap
10
- import time
11
- import webbrowser
12
- from collections.abc import AsyncIterable, Callable, Generator, Iterable
13
- from itertools import product
14
- from pathlib import Path
15
- from typing import TYPE_CHECKING, List, Optional
16
-
17
- import dagger
18
- import pytest
19
- from airbyte_protocol.models import ConfiguredAirbyteCatalog # type: ignore
20
- from connection_retriever.audit_logging import get_user_email # type: ignore
21
- from connection_retriever.retrieval import (
22
- ConnectionNotFoundError,
23
- get_current_docker_image_tag,
24
- ) # type: ignore
25
- from live_tests import stash_keys
26
- from live_tests.commons.connection_objects_retrieval import (
27
- ConnectionObject,
28
- InvalidConnectionError,
29
- get_connection_objects,
30
- )
31
- from live_tests.commons.connector_runner import ConnectorRunner, Proxy
32
- from live_tests.commons.evaluation_modes import TestEvaluationMode
33
- from live_tests.commons.models import (
34
- ActorType,
35
- Command,
36
- ConnectionObjects,
37
- ConnectionSubset,
38
- ConnectorUnderTest,
39
- ExecutionInputs,
40
- ExecutionResult,
41
- SecretDict,
42
- TargetOrControl,
43
- )
44
- from live_tests.commons.secret_access import get_airbyte_api_key
45
- from live_tests.commons.segment_tracking import track_usage
46
- from live_tests.commons.utils import clean_up_artifacts
47
- from live_tests.report import PrivateDetailsReport, ReportState, TestReport
48
- from rich.prompt import Confirm, Prompt
49
-
50
- if TYPE_CHECKING:
51
- from _pytest.config import Config
52
- from _pytest.config.argparsing import Parser
53
- from _pytest.fixtures import SubRequest
54
- from pytest_sugar import SugarTerminalReporter # type: ignore
55
-
56
- # CONSTS
57
- LOGGER = logging.getLogger("live-tests")
58
- MAIN_OUTPUT_DIRECTORY = Path("/tmp/live_tests_artifacts")
59
-
60
- # It's used by Dagger and its very verbose
61
- logging.getLogger("httpx").setLevel(logging.ERROR)
62
-
63
-
64
- # PYTEST HOOKS
65
- def pytest_addoption(parser: Parser) -> None:
66
- parser.addoption(
67
- "--connector-image",
68
- help="The connector image name on which the tests will run: e.g. airbyte/source-faker",
69
- )
70
- parser.addoption(
71
- "--control-version",
72
- help="The control version used for regression testing.",
73
- )
74
- parser.addoption(
75
- "--target-version",
76
- default="dev",
77
- help="The target version used for regression and validation testing. Defaults to dev.",
78
- )
79
- parser.addoption("--config-path")
80
- parser.addoption("--catalog-path")
81
- parser.addoption("--state-path")
82
- parser.addoption("--connection-id")
83
- parser.addoption("--pr-url", help="The URL of the PR you are testing")
84
- parser.addoption(
85
- "--stream",
86
- help="The stream to run the tests on. (Can be used multiple times)",
87
- action="append",
88
- )
89
- # Required when running in CI
90
- parser.addoption("--run-id", type=str)
91
- parser.addoption(
92
- "--should-read-with-state",
93
- type=bool,
94
- help="Whether to run the `read` command with state. \n"
95
- "We recommend reading with state to properly test incremental sync. \n"
96
- "But if the target version introduces a breaking change in the state, you might want to run without state. \n",
97
- )
98
- parser.addoption(
99
- "--test-evaluation-mode",
100
- choices=[e.value for e in TestEvaluationMode],
101
- default=TestEvaluationMode.STRICT.value,
102
- help='If "diagnostic" mode is selected, all tests will pass as long as there is no exception; warnings will be logged. In "strict" mode, tests may fail.',
103
- )
104
- parser.addoption(
105
- "--connection-subset",
106
- choices=[c.value for c in ConnectionSubset],
107
- default=ConnectionSubset.SANDBOXES.value,
108
- help="Whether to select from sandbox accounts only.",
109
- )
110
- parser.addoption(
111
- "--max-connections",
112
- default=None,
113
- help="The maximum number of connections to retrieve and use for testing.",
114
- )
115
- parser.addoption(
116
- "--disable-proxy",
117
- type=bool,
118
- default=False,
119
- help="If a connector uses provider-specific libraries (e.g., facebook-business), it is better to disable the proxy.",
120
- )
121
-
122
-
123
- def pytest_configure(config: Config) -> None:
124
- user_email = get_user_email()
125
- config.stash[stash_keys.RUN_IN_AIRBYTE_CI] = bool(
126
- os.getenv("RUN_IN_AIRBYTE_CI", False)
127
- )
128
- config.stash[stash_keys.IS_PRODUCTION_CI] = bool(os.getenv("CI", False))
129
-
130
- if not config.stash[stash_keys.RUN_IN_AIRBYTE_CI]:
131
- prompt_for_confirmation(user_email)
132
-
133
- track_usage(
134
- "production-ci"
135
- if config.stash[stash_keys.IS_PRODUCTION_CI]
136
- else "local-ci"
137
- if config.stash[stash_keys.RUN_IN_AIRBYTE_CI]
138
- else user_email,
139
- vars(config.option),
140
- )
141
- config.stash[stash_keys.AIRBYTE_API_KEY] = get_airbyte_api_key()
142
- config.stash[stash_keys.USER] = user_email
143
- config.stash[stash_keys.SESSION_RUN_ID] = config.getoption("--run-id") or str(
144
- int(time.time())
145
- )
146
- test_artifacts_directory = get_artifacts_directory(config)
147
- duckdb_path = test_artifacts_directory / "duckdb.db"
148
- config.stash[stash_keys.DUCKDB_PATH] = duckdb_path
149
- test_artifacts_directory.mkdir(parents=True, exist_ok=True)
150
- dagger_log_path = test_artifacts_directory / "dagger.log"
151
- config.stash[stash_keys.IS_PERMITTED_BOOL] = False
152
- report_path = test_artifacts_directory / "report.html"
153
- private_details_path = test_artifacts_directory / "private_details.html"
154
- config.stash[stash_keys.TEST_ARTIFACT_DIRECTORY] = test_artifacts_directory
155
- dagger_log_path.touch()
156
- LOGGER.info("Dagger log path: %s", dagger_log_path)
157
- config.stash[stash_keys.DAGGER_LOG_PATH] = dagger_log_path
158
- config.stash[stash_keys.PR_URL] = get_option_or_fail(config, "--pr-url")
159
- _connection_id = config.getoption("--connection-id")
160
- config.stash[stash_keys.AUTO_SELECT_CONNECTION] = _connection_id == "auto"
161
- config.stash[stash_keys.CONNECTOR_IMAGE] = get_option_or_fail(
162
- config, "--connector-image"
163
- )
164
- config.stash[stash_keys.TARGET_VERSION] = get_option_or_fail(
165
- config, "--target-version"
166
- )
167
- config.stash[stash_keys.CONTROL_VERSION] = get_control_version(config)
168
- config.stash[stash_keys.CONNECTION_SUBSET] = ConnectionSubset(
169
- get_option_or_fail(config, "--connection-subset")
170
- )
171
- custom_source_config_path = config.getoption("--config-path")
172
- custom_configured_catalog_path = config.getoption("--catalog-path")
173
- custom_state_path = config.getoption("--state-path")
174
- config.stash[stash_keys.SELECTED_STREAMS] = set(config.getoption("--stream") or [])
175
- config.stash[stash_keys.TEST_EVALUATION_MODE] = TestEvaluationMode(
176
- config.getoption("--test-evaluation-mode", "strict")
177
- )
178
- config.stash[stash_keys.MAX_CONNECTIONS] = config.getoption("--max-connections")
179
- config.stash[stash_keys.MAX_CONNECTIONS] = (
180
- int(config.stash[stash_keys.MAX_CONNECTIONS])
181
- if config.stash[stash_keys.MAX_CONNECTIONS]
182
- else None
183
- )
184
-
185
- config.stash[stash_keys.DISABLE_PROXY] = config.getoption("--disable-proxy")
186
-
187
- if config.stash[stash_keys.RUN_IN_AIRBYTE_CI]:
188
- config.stash[stash_keys.SHOULD_READ_WITH_STATE] = bool(
189
- config.getoption("--should-read-with-state")
190
- )
191
- elif _should_read_with_state := config.getoption("--should-read-with-state"):
192
- config.stash[stash_keys.SHOULD_READ_WITH_STATE] = _should_read_with_state
193
- else:
194
- config.stash[stash_keys.SHOULD_READ_WITH_STATE] = (
195
- prompt_for_read_with_or_without_state()
196
- )
197
-
198
- retrieval_reason = f"Running live tests on connection for connector {config.stash[stash_keys.CONNECTOR_IMAGE]} on target versions ({config.stash[stash_keys.TARGET_VERSION]})."
199
-
200
- try:
201
- config.stash[stash_keys.ALL_CONNECTION_OBJECTS] = get_connection_objects(
202
- {
203
- ConnectionObject.SOURCE_CONFIG,
204
- ConnectionObject.CATALOG,
205
- ConnectionObject.CONFIGURED_CATALOG,
206
- ConnectionObject.STATE,
207
- ConnectionObject.WORKSPACE_ID,
208
- ConnectionObject.SOURCE_DOCKER_IMAGE,
209
- ConnectionObject.SOURCE_ID,
210
- ConnectionObject.DESTINATION_ID,
211
- },
212
- None if _connection_id == "auto" else _connection_id,
213
- Path(custom_source_config_path) if custom_source_config_path else None,
214
- Path(custom_configured_catalog_path)
215
- if custom_configured_catalog_path
216
- else None,
217
- Path(custom_state_path) if custom_state_path else None,
218
- retrieval_reason,
219
- connector_image=config.stash[stash_keys.CONNECTOR_IMAGE],
220
- connector_version=config.stash[stash_keys.CONTROL_VERSION],
221
- auto_select_connections=config.stash[stash_keys.AUTO_SELECT_CONNECTION],
222
- selected_streams=config.stash[stash_keys.SELECTED_STREAMS],
223
- connection_subset=config.stash[stash_keys.CONNECTION_SUBSET],
224
- max_connections=config.stash[stash_keys.MAX_CONNECTIONS],
225
- )
226
- config.stash[stash_keys.IS_PERMITTED_BOOL] = True
227
- except (ConnectionNotFoundError, InvalidConnectionError) as exc:
228
- clean_up_artifacts(MAIN_OUTPUT_DIRECTORY, LOGGER)
229
- LOGGER.error(
230
- f"Failed to retrieve a valid a connection which is using the control version {config.stash[stash_keys.CONTROL_VERSION]}."
231
- )
232
- pytest.exit(str(exc))
233
-
234
- if (
235
- config.stash[stash_keys.CONTROL_VERSION]
236
- == config.stash[stash_keys.TARGET_VERSION]
237
- ):
238
- pytest.exit(
239
- f"Control and target versions are the same: {control_version}. Please provide different versions."
240
- )
241
-
242
- config.stash[stash_keys.PRIVATE_DETAILS_REPORT] = PrivateDetailsReport(
243
- private_details_path,
244
- config,
245
- )
246
-
247
- config.stash[stash_keys.TEST_REPORT] = TestReport(
248
- report_path,
249
- config,
250
- private_details_url=config.stash[stash_keys.PRIVATE_DETAILS_REPORT]
251
- .path.resolve()
252
- .as_uri(),
253
- )
254
-
255
- webbrowser.open_new_tab(
256
- config.stash[stash_keys.TEST_REPORT].path.resolve().as_uri()
257
- )
258
-
259
-
260
- def get_artifacts_directory(config: pytest.Config) -> Path:
261
- run_id = config.stash[stash_keys.SESSION_RUN_ID]
262
- return MAIN_OUTPUT_DIRECTORY / f"session_{run_id}"
263
-
264
-
265
- def pytest_collection_modifyitems(
266
- config: pytest.Config, items: list[pytest.Item]
267
- ) -> None:
268
- for item in items:
269
- if (
270
- config.stash[stash_keys.SHOULD_READ_WITH_STATE]
271
- and "without_state" in item.keywords
272
- ):
273
- item.add_marker(
274
- pytest.mark.skip(reason="Test is marked with without_state marker")
275
- )
276
- if (
277
- not config.stash[stash_keys.SHOULD_READ_WITH_STATE]
278
- and "with_state" in item.keywords
279
- ):
280
- item.add_marker(
281
- pytest.mark.skip(reason="Test is marked with with_state marker")
282
- )
283
-
284
-
285
- def pytest_terminal_summary(
286
- terminalreporter: SugarTerminalReporter, exitstatus: int, config: Config
287
- ) -> None:
288
- config.stash[stash_keys.TEST_REPORT].update(ReportState.FINISHED)
289
- config.stash[stash_keys.PRIVATE_DETAILS_REPORT].update(ReportState.FINISHED)
290
- if not config.stash.get(stash_keys.IS_PERMITTED_BOOL, False):
291
- # Don't display the prompt if the tests were not run due to inability to fetch config
292
- clean_up_artifacts(MAIN_OUTPUT_DIRECTORY, LOGGER)
293
- pytest.exit(str(NotPermittedError))
294
-
295
- terminalreporter.ensure_newline()
296
- terminalreporter.section("Test artifacts", sep="=", bold=True, blue=True)
297
- terminalreporter.line(
298
- f"All tests artifacts for this sessions should be available in {config.stash[stash_keys.TEST_ARTIFACT_DIRECTORY].resolve()}"
299
- )
300
-
301
- if not config.stash[stash_keys.RUN_IN_AIRBYTE_CI]:
302
- try:
303
- Prompt.ask(
304
- textwrap.dedent(
305
- """
306
- Test artifacts will be destroyed after this prompt.
307
- Press enter when you're done reading them.
308
- 🚨 Do not copy them elsewhere on your disk!!! 🚨
309
- """
310
- )
311
- )
312
- finally:
313
- clean_up_artifacts(MAIN_OUTPUT_DIRECTORY, LOGGER)
314
-
315
-
316
- def pytest_keyboard_interrupt(excinfo: Exception) -> None:
317
- LOGGER.error(
318
- "Test execution was interrupted by the user. Cleaning up test artifacts."
319
- )
320
- clean_up_artifacts(MAIN_OUTPUT_DIRECTORY, LOGGER)
321
-
322
-
323
- @pytest.hookimpl(tryfirst=True, hookwrapper=True)
324
- def pytest_runtest_makereport(item: pytest.Item, call: pytest.CallInfo) -> Generator:
325
- outcome = yield
326
- report = outcome.get_result()
327
-
328
- # Overwrite test failures with passes for tests being run in diagnostic mode
329
- if (
330
- item.config.stash.get(
331
- stash_keys.TEST_EVALUATION_MODE, TestEvaluationMode.STRICT
332
- )
333
- == TestEvaluationMode.DIAGNOSTIC
334
- and "allow_diagnostic_mode" in item.keywords
335
- ):
336
- if call.when == "call":
337
- if call.excinfo:
338
- if report.outcome == "failed":
339
- report.outcome = "passed"
340
-
341
- # This is to add skipped or failed tests due to upstream fixture failures on setup
342
- if report.outcome in ["failed", "skipped"] or report.when == "call":
343
- item.config.stash[stash_keys.TEST_REPORT].add_test_result(
344
- report,
345
- item.function.__doc__, # type: ignore
346
- )
347
-
348
-
349
- # HELPERS
350
-
351
-
352
- def get_option_or_fail(config: pytest.Config, option: str) -> str:
353
- if option_value := config.getoption(option):
354
- return option_value
355
- pytest.fail(f"Missing required option: {option}")
356
-
357
-
358
- def get_control_version(config: pytest.Config) -> str:
359
- if control_version := config.getoption("--control-version"):
360
- return control_version
361
- if connector_docker_repository := config.getoption("--connector-image"):
362
- return get_current_docker_image_tag(connector_docker_repository)
363
- raise ValueError(
364
- "The control version can't be determined, please pass a --control-version or a --connector-image"
365
- )
366
-
367
-
368
- def prompt_for_confirmation(user_email: str) -> None:
369
- message = textwrap.dedent(
370
- f"""
371
- 👮 This program is running on live Airbyte Cloud connection.
372
- It means that it might induce costs or rate limits on the source.
373
- This program is storing tests artifacts in {MAIN_OUTPUT_DIRECTORY.resolve()} that you can use for debugging. They will get destroyed after the program execution.
374
-
375
- By approving this prompt, you ({user_email}) confirm that:
376
- 1. You understand the implications of running this test suite.
377
- 2. You have selected the correct target and control versions.
378
- 3. You have selected the right tests according to your testing needs.
379
- 4. You will not copy the test artifacts content.
380
- 5. You want to run the program on the passed connection ID.
381
-
382
- Usage of this tool is tracked and logged.
383
-
384
- Do you want to continue?
385
- """
386
- )
387
- if not os.environ.get("CI") and not Confirm.ask(message):
388
- pytest.exit("Test execution was interrupted by the user.")
389
-
390
-
391
- def prompt_for_read_with_or_without_state() -> bool:
392
- message = textwrap.dedent(
393
- """
394
- 📖 Do you want to run the read command with or without state?
395
- 1. Run the read command with state
396
- 2. Run the read command without state
397
-
398
- We recommend reading with state to properly test incremental sync.
399
- But if the target version introduces a breaking change in the state, you might want to run without state.
400
- """
401
- )
402
- return Prompt.ask(message) == "1"
403
-
404
-
405
- # FIXTURES
406
-
407
-
408
- @pytest.fixture(scope="session")
409
- def anyio_backend() -> str:
410
- return "asyncio"
411
-
412
-
413
- @pytest.fixture(scope="session")
414
- def test_artifacts_directory(request: SubRequest) -> Path:
415
- return request.config.stash[stash_keys.TEST_ARTIFACT_DIRECTORY]
416
-
417
-
418
- @pytest.fixture(scope="session")
419
- def connector_image(request: SubRequest) -> str:
420
- return request.config.stash[stash_keys.CONNECTOR_IMAGE]
421
-
422
-
423
- @pytest.fixture(scope="session")
424
- def control_version(request: SubRequest) -> str:
425
- return request.config.stash[stash_keys.CONTROL_VERSION]
426
-
427
-
428
- @pytest.fixture(scope="session")
429
- def target_version(request: SubRequest) -> str:
430
- return request.config.stash[stash_keys.TARGET_VERSION]
431
-
432
-
433
- @pytest.fixture(scope="session")
434
- def all_connection_objects(request: SubRequest) -> List[ConnectionObjects]:
435
- return request.config.stash[stash_keys.ALL_CONNECTION_OBJECTS]
436
-
437
-
438
- def get_connector_config(
439
- connection_objects: ConnectionObjects, control_connector: ConnectorUnderTest
440
- ) -> Optional[SecretDict]:
441
- if control_connector.actor_type is ActorType.SOURCE:
442
- return connection_objects.source_config
443
- elif control_connector.actor_type is ActorType.DESTINATION:
444
- return connection_objects.destination_config
445
- else:
446
- raise ValueError(f"Actor type {control_connector.actor_type} is not supported")
447
-
448
-
449
- @pytest.fixture(scope="session")
450
- def actor_id(
451
- connection_objects: ConnectionObjects, control_connector: ConnectorUnderTest
452
- ) -> str | None:
453
- if control_connector.actor_type is ActorType.SOURCE:
454
- return connection_objects.source_id
455
- elif control_connector.actor_type is ActorType.DESTINATION:
456
- return connection_objects.destination_id
457
- else:
458
- raise ValueError(f"Actor type {control_connector.actor_type} is not supported")
459
-
460
-
461
- def get_actor_id(
462
- connection_objects: ConnectionObjects, control_connector: ConnectorUnderTest
463
- ) -> str | None:
464
- if control_connector.actor_type is ActorType.SOURCE:
465
- return connection_objects.source_id
466
- elif control_connector.actor_type is ActorType.DESTINATION:
467
- return connection_objects.destination_id
468
- else:
469
- raise ValueError(f"Actor type {control_connector.actor_type} is not supported")
470
-
471
-
472
- @pytest.fixture(scope="session")
473
- def configured_streams(
474
- configured_catalog: ConfiguredAirbyteCatalog,
475
- ) -> Iterable[str]:
476
- return {stream.stream.name for stream in configured_catalog.streams}
477
-
478
-
479
- @pytest.fixture(scope="session")
480
- def state(connection_objects: ConnectionObjects) -> Optional[dict]:
481
- return connection_objects.state
482
-
483
-
484
- @pytest.fixture(scope="session")
485
- def dagger_connection(request: SubRequest) -> dagger.Connection:
486
- return dagger.Connection(
487
- dagger.Config(
488
- log_output=request.config.stash[stash_keys.DAGGER_LOG_PATH].open("w")
489
- )
490
- )
491
-
492
-
493
- @pytest.fixture(scope="session", autouse=True)
494
- async def dagger_client(
495
- dagger_connection: dagger.Connection,
496
- ) -> AsyncIterable[dagger.Client]:
497
- async with dagger_connection as client:
498
- yield client
499
-
500
-
501
- @pytest.fixture(scope="session")
502
- async def control_connector(
503
- dagger_client: dagger.Client, connector_image: str, control_version: str
504
- ) -> ConnectorUnderTest:
505
- return await ConnectorUnderTest.from_image_name(
506
- dagger_client, f"{connector_image}:{control_version}", TargetOrControl.CONTROL
507
- )
508
-
509
-
510
- @pytest.fixture(scope="session")
511
- async def target_connector(
512
- dagger_client: dagger.Client, connector_image: str, target_version: str
513
- ) -> ConnectorUnderTest:
514
- return await ConnectorUnderTest.from_image_name(
515
- dagger_client, f"{connector_image}:{target_version}", TargetOrControl.TARGET
516
- )
517
-
518
-
519
- @pytest.fixture(scope="session")
520
- def duckdb_path(request: SubRequest) -> Path:
521
- return request.config.stash[stash_keys.DUCKDB_PATH]
522
-
523
-
524
- def get_execution_inputs_for_command(
525
- command: Command,
526
- connection_objects: ConnectionObjects,
527
- control_connector: ConnectorUnderTest,
528
- test_artifacts_directory: Path,
529
- duckdb_path: Path,
530
- ) -> ExecutionInputs:
531
- """Get the execution inputs for the given command and connection objects."""
532
- actor_id = get_actor_id(connection_objects, control_connector)
533
-
534
- inputs_arguments = {
535
- "hashed_connection_id": connection_objects.hashed_connection_id,
536
- "connector_under_test": control_connector,
537
- "actor_id": actor_id,
538
- "global_output_dir": test_artifacts_directory,
539
- "command": command,
540
- "duckdb_path": duckdb_path,
541
- }
542
-
543
- if command.needs_config:
544
- connector_config = get_connector_config(connection_objects, control_connector)
545
- if not connector_config:
546
- pytest.skip("Config is not provided. The config fixture can't be used.")
547
- inputs_arguments["config"] = connector_config
548
- if command.needs_catalog:
549
- configured_catalog = connection_objects.configured_catalog
550
- if not configured_catalog:
551
- pytest.skip("Catalog is not provided. The catalog fixture can't be used.")
552
- inputs_arguments["configured_catalog"] = connection_objects.configured_catalog
553
- if command.needs_state:
554
- state = connection_objects.state
555
- if not state:
556
- pytest.skip("State is not provided. The state fixture can't be used.")
557
- inputs_arguments["state"] = state
558
-
559
- return ExecutionInputs(**inputs_arguments)
560
-
561
-
562
- async def run_command(
563
- dagger_client: dagger.Client,
564
- command: Command,
565
- connection_objects: ConnectionObjects,
566
- connector: ConnectorUnderTest,
567
- test_artifacts_directory: Path,
568
- duckdb_path: Path,
569
- runs_in_ci,
570
- disable_proxy: bool = False,
571
- ) -> ExecutionResult:
572
- """Run the given command for the given connector and connection objects."""
573
- execution_inputs = get_execution_inputs_for_command(
574
- command, connection_objects, connector, test_artifacts_directory, duckdb_path
575
- )
576
- logging.info(
577
- f"Running {command} for {connector.target_or_control.value} connector {execution_inputs.connector_under_test.name}"
578
- )
579
- proxy = None
580
-
581
- if not disable_proxy:
582
- proxy_hostname = f"proxy_server_{command.value}_{execution_inputs.connector_under_test.version.replace('.', '_')}"
583
- proxy = Proxy(dagger_client, proxy_hostname, connection_objects.connection_id)
584
-
585
- runner = ConnectorRunner(
586
- dagger_client, execution_inputs, runs_in_ci, http_proxy=proxy
587
- )
588
- execution_result = await runner.run()
589
- return execution_result, proxy
590
-
591
-
592
- async def run_command_and_add_to_report(
593
- dagger_client: dagger.Client,
594
- command: Command,
595
- connection_objects: ConnectionObjects,
596
- connector: ConnectorUnderTest,
597
- test_artifacts_directory: Path,
598
- duckdb_path: Path,
599
- runs_in_ci,
600
- test_report: TestReport,
601
- private_details_report: PrivateDetailsReport,
602
- disable_proxy: bool = False,
603
- ) -> ExecutionResult:
604
- """Run the given command for the given connector and connection objects and add the results to the test report."""
605
- execution_result, proxy = await run_command(
606
- dagger_client,
607
- command,
608
- connection_objects,
609
- connector,
610
- test_artifacts_directory,
611
- duckdb_path,
612
- runs_in_ci,
613
- disable_proxy=disable_proxy,
614
- )
615
- if connector.target_or_control is TargetOrControl.CONTROL:
616
- test_report.add_control_execution_result(execution_result)
617
- private_details_report.add_control_execution_result(execution_result)
618
- if connector.target_or_control is TargetOrControl.TARGET:
619
- test_report.add_target_execution_result(execution_result)
620
- private_details_report.add_target_execution_result(execution_result)
621
- return execution_result, proxy
622
-
623
-
624
- def generate_execution_results_fixture(
625
- command: Command, control_or_target: str
626
- ) -> Callable:
627
- """Dynamically generate the fixture for the given command and control/target.
628
- This is mainly to avoid code duplication and to make the code more maintainable.
629
- Declaring this explicitly for each command and control/target combination would be cumbersome.
630
- """
631
-
632
- if control_or_target not in ["control", "target"]:
633
- raise ValueError("control_or_target should be either 'control' or 'target'")
634
- if command not in [
635
- Command.SPEC,
636
- Command.CHECK,
637
- Command.DISCOVER,
638
- Command.READ,
639
- Command.READ_WITH_STATE,
640
- ]:
641
- raise ValueError(
642
- "command should be either 'spec', 'check', 'discover', 'read' or 'read_with_state'"
643
- )
644
-
645
- if control_or_target == "control":
646
-
647
- @pytest.fixture(scope="session")
648
- async def generated_fixture(
649
- request: SubRequest,
650
- dagger_client: dagger.Client,
651
- control_connector: ConnectorUnderTest,
652
- test_artifacts_directory: Path,
653
- ) -> ExecutionResult:
654
- connection_objects = request.param
655
- disable_proxy = request.config.stash[stash_keys.DISABLE_PROXY]
656
-
657
- execution_results, proxy = await run_command_and_add_to_report(
658
- dagger_client,
659
- command,
660
- connection_objects,
661
- control_connector,
662
- test_artifacts_directory,
663
- request.config.stash[stash_keys.DUCKDB_PATH],
664
- request.config.stash[stash_keys.RUN_IN_AIRBYTE_CI],
665
- request.config.stash[stash_keys.TEST_REPORT],
666
- request.config.stash[stash_keys.PRIVATE_DETAILS_REPORT],
667
- disable_proxy=disable_proxy,
668
- )
669
-
670
- yield execution_results
671
-
672
- if not disable_proxy:
673
- await proxy.clear_cache_volume()
674
-
675
- else:
676
-
677
- @pytest.fixture(scope="session")
678
- async def generated_fixture(
679
- request: SubRequest,
680
- dagger_client: dagger.Client,
681
- target_connector: ConnectorUnderTest,
682
- test_artifacts_directory: Path,
683
- ) -> ExecutionResult:
684
- connection_objects = request.param
685
- disable_proxy = request.config.stash[stash_keys.DISABLE_PROXY]
686
-
687
- execution_results, proxy = await run_command_and_add_to_report(
688
- dagger_client,
689
- command,
690
- connection_objects,
691
- target_connector,
692
- test_artifacts_directory,
693
- request.config.stash[stash_keys.DUCKDB_PATH],
694
- request.config.stash[stash_keys.RUN_IN_AIRBYTE_CI],
695
- request.config.stash[stash_keys.TEST_REPORT],
696
- request.config.stash[stash_keys.PRIVATE_DETAILS_REPORT],
697
- disable_proxy=disable_proxy,
698
- )
699
-
700
- yield execution_results
701
-
702
- if not disable_proxy:
703
- await proxy.clear_cache_volume()
704
-
705
- return generated_fixture
706
-
707
-
708
- def inject_fixtures() -> set[str]:
709
- """Dynamically generate th execution result fixtures for all the combinations of commands and control/target.
710
- The fixtures will be named as <command>_<control/target>_execution_result
711
- Add the generated fixtures to the global namespace.
712
- """
713
- execution_result_fixture_names = []
714
- for command, control_or_target in product(
715
- [command for command in Command], ["control", "target"]
716
- ):
717
- fixture_name = f"{command.name.lower()}_{control_or_target}_execution_result"
718
- globals()[fixture_name] = generate_execution_results_fixture(
719
- command, control_or_target
720
- )
721
- execution_result_fixture_names.append(fixture_name)
722
- return set(execution_result_fixture_names)
723
-
724
-
725
- EXECUTION_RESULT_FIXTURES = inject_fixtures()
726
-
727
-
728
- def pytest_generate_tests(metafunc):
729
- """This function is called for each test function.
730
- It helps in parameterizing the test functions with the connection objects.
731
- It will provide the connection objects to the "*_execution_result" fixtures as parameters.
732
- This will make sure that the tests are run for all the connection objects available in the configuration.
733
- """
734
- all_connection_objects = metafunc.config.stash[stash_keys.ALL_CONNECTION_OBJECTS]
735
- requested_fixtures = [
736
- fixture_name
737
- for fixture_name in metafunc.fixturenames
738
- if fixture_name in EXECUTION_RESULT_FIXTURES
739
- ]
740
- assert isinstance(all_connection_objects, list), (
741
- "all_connection_objects should be a list"
742
- )
743
-
744
- if not requested_fixtures:
745
- return
746
- metafunc.parametrize(
747
- requested_fixtures,
748
- [[c] * len(requested_fixtures) for c in all_connection_objects],
749
- indirect=requested_fixtures,
750
- ids=[f"CONNECTION {c.connection_id[:8]}" for c in all_connection_objects],
751
- )