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
@@ -1,207 +0,0 @@
1
- # Copyright (c) 2024 Airbyte, Inc., all rights reserved.
2
- from __future__ import annotations
3
-
4
- import logging
5
- import uuid
6
-
7
- import dagger
8
-
9
- from . import mitm_addons
10
-
11
-
12
- class Proxy:
13
- """
14
- This class is a wrapper around a mitmproxy container. It allows to declare a mitmproxy container,
15
- bind it as a service to a different container and retrieve the mitmproxy stream file.
16
- """
17
-
18
- MITMPROXY_IMAGE = "mitmproxy/mitmproxy:10.2.4"
19
- MITM_STREAM_FILE = "stream.mitm"
20
- PROXY_PORT = 8080
21
- MITM_ADDONS_PATH = mitm_addons.__file__
22
-
23
- def __init__(
24
- self,
25
- dagger_client: dagger.Client,
26
- hostname: str,
27
- session_id: str,
28
- stream_for_server_replay: dagger.File | None = None,
29
- ) -> None:
30
- self.dagger_client = dagger_client
31
- self.hostname = hostname
32
- self.session_id = session_id
33
- self.stream_for_server_replay = stream_for_server_replay
34
-
35
- @property
36
- def dump_cache_volume(self) -> dagger.CacheVolume:
37
- # We namespace the cache by:
38
- # - Mitmproxy image name to make sure we're not re-using a cached artifact on a different and potentially incompatible mitmproxy version
39
- # - Hostname to avoid sharing the same https dump between different tests
40
- # - Session id to avoid sharing the same https dump between different runs of the same tests
41
- # The session id is set to the Airbyte Connection ID to ensure that no cache is shared between connections
42
- return self.dagger_client.cache_volume(
43
- f"{self.MITMPROXY_IMAGE}{self.hostname}{self.session_id}"
44
- )
45
-
46
- @property
47
- def mitmproxy_dir_cache(self) -> dagger.CacheVolume:
48
- return self.dagger_client.cache_volume(self.MITMPROXY_IMAGE)
49
-
50
- def generate_mitmconfig(self):
51
- return (
52
- self.dagger_client.container()
53
- .from_(self.MITMPROXY_IMAGE)
54
- # Mitmproxy generates its self signed certs at first run, we need to run it once to generate the certs
55
- # They are stored in /root/.mitmproxy
56
- .with_exec(["timeout", "--preserve-status", "1", "mitmdump"])
57
- .directory("/root/.mitmproxy")
58
- )
59
-
60
- async def get_container(self, mitm_config: dagger.Directory) -> dagger.Container:
61
- """Get a container for the mitmproxy service.
62
- If a stream for server replay is provided, it will be used to replay requests to the same URL.
63
-
64
- Returns:
65
- dagger.Container: The container for the mitmproxy service.
66
- mitm_config (dagger.Directory): The directory containing the mitmproxy configuration.
67
- """
68
- container_addons_path = "/addons.py"
69
- proxy_container = (
70
- self.dagger_client.container()
71
- .from_(self.MITMPROXY_IMAGE)
72
- .with_exec(["mkdir", "/dumps"])
73
- # This is caching the mitmproxy stream files, which can contain sensitive information
74
- # We want to nuke this cache after test suite execution.
75
- .with_mounted_cache("/dumps", self.dump_cache_volume)
76
- # This is caching the mitmproxy self-signed certificate, no sensitive information is stored in it
77
- .with_file(
78
- container_addons_path,
79
- self.dagger_client.host().file(self.MITM_ADDONS_PATH),
80
- )
81
- .with_directory("/root/.mitmproxy", mitm_config)
82
- )
83
-
84
- # If the proxy was instantiated with a stream for server replay from a previous run, we want to use it.
85
- # Requests to the same URL will be replayed from the stream instead of being sent to the server.
86
- # This is useful to avoid rate limiting issues and limits responses drifts due to time based logics.
87
- if (
88
- self.stream_for_server_replay is not None
89
- and await self.stream_for_server_replay.size() > 0
90
- ):
91
- proxy_container = proxy_container.with_file(
92
- "/cache.mitm", self.stream_for_server_replay
93
- )
94
- command = [
95
- "mitmdump",
96
- "-s",
97
- container_addons_path,
98
- "--flow-detail",
99
- "2",
100
- "--server-replay",
101
- "/cache.mitm",
102
- "--save-stream-file",
103
- f"/dumps/{self.MITM_STREAM_FILE}",
104
- ]
105
- else:
106
- command = [
107
- "mitmdump",
108
- "-s",
109
- container_addons_path,
110
- "--flow-detail",
111
- "2",
112
- "--save-stream-file",
113
- f"/dumps/{self.MITM_STREAM_FILE}",
114
- ]
115
-
116
- return proxy_container.with_exec(command)
117
-
118
- async def get_service(self, mitm_config: dagger.Directory) -> dagger.Service:
119
- return (
120
- (await self.get_container(mitm_config))
121
- .with_exposed_port(self.PROXY_PORT)
122
- .as_service()
123
- )
124
-
125
- async def bind_container(self, container: dagger.Container) -> dagger.Container:
126
- """Bind a container to the proxy service and set environment variables to use the proxy for HTTP(S) traffic.
127
-
128
- Args:
129
- container (dagger.Container): The container to bind to the proxy service.
130
-
131
- Returns:
132
- dagger.Container: The container with the proxy service bound and environment variables set.
133
- """
134
- mitmconfig_dir = self.generate_mitmconfig()
135
- pem = mitmconfig_dir.file("mitmproxy-ca.pem")
136
-
137
- # Find the python version in the container to get the correct path for the requests cert file
138
- # We will overwrite this file with the mitmproxy self-signed certificate
139
- # I could not find a less brutal way to make Requests trust the mitmproxy self-signed certificate
140
- # I tried running update-ca-certificates + setting REQUESTS_CA_BUNDLE in the container but it did not work
141
- python_version_output = (
142
- await container.with_exec(["python", "--version"]).stdout()
143
- ).strip()
144
- python_version = python_version_output.split(" ")[-1]
145
- python_version_minor_only = ".".join(python_version.split(".")[:-1])
146
- requests_cert_path = f"/usr/local/lib/python{python_version_minor_only}/site-packages/certifi/cacert.pem"
147
- current_user = (await container.with_exec(["whoami"]).stdout()).strip()
148
- try:
149
- return await (
150
- container.with_user("root")
151
- # Overwrite the requests cert file with the mitmproxy self-signed certificate
152
- .with_file(requests_cert_path, pem, owner=current_user)
153
- .with_env_variable("http_proxy", f"{self.hostname}:{self.PROXY_PORT}")
154
- .with_env_variable("https_proxy", f"{self.hostname}:{self.PROXY_PORT}")
155
- .with_user(current_user)
156
- .with_service_binding(
157
- self.hostname, await self.get_service(mitmconfig_dir)
158
- )
159
- )
160
- except dagger.DaggerError as e:
161
- # This is likely hapenning on Java connector images whose certificates location is different
162
- # TODO handle this case
163
- logging.warn(f"Failed to bind container to proxy: {e}")
164
- return container
165
-
166
- async def retrieve_http_dump(self) -> dagger.File | None:
167
- """We mount the cache volume, where the mitmproxy container saves the stream file, to a fresh container.
168
- We then copy the stream file to a new directory and return it as a dagger.File.
169
- The copy operation to /to_export is required as Dagger does not support direct access to files in cache volumes.
170
-
171
-
172
- Returns:
173
- dagger.File | None: The mitmproxy stream file if it exists, None otherwise.
174
- """
175
- container = (
176
- self.dagger_client.container()
177
- .from_("alpine:latest")
178
- .with_env_variable("CACHEBUSTER", str(uuid.uuid4()))
179
- .with_mounted_cache("/dumps", self.dump_cache_volume)
180
- )
181
- dump_files = (await container.with_exec(["ls", "/dumps"]).stdout()).splitlines()
182
- if self.MITM_STREAM_FILE not in dump_files:
183
- return None
184
- return await (
185
- container.with_exec(["mkdir", "/to_export"], use_entrypoint=True)
186
- .with_exec(
187
- [
188
- "cp",
189
- "-r",
190
- f"/dumps/{self.MITM_STREAM_FILE}",
191
- f"/to_export/{self.MITM_STREAM_FILE}",
192
- ],
193
- use_entrypoint=True,
194
- )
195
- .file(f"/to_export/{self.MITM_STREAM_FILE}")
196
- )
197
-
198
- async def clear_cache_volume(self) -> None:
199
- """Delete all files in the cache volume. This is useful to avoid caching sensitive information between tests."""
200
- await (
201
- self.dagger_client.container()
202
- .from_("alpine:latest")
203
- .with_mounted_cache("/to_clear", self.dump_cache_volume)
204
- .with_exec(["rm", "-rf", "/to_clear/*"], use_entrypoint=True)
205
- .sync()
206
- )
207
- logging.info(f"Cache volume {self.dump_cache_volume} cleared")
@@ -1,47 +0,0 @@
1
- # Copyright (c) 2024 Airbyte, Inc., all rights reserved.
2
- from __future__ import annotations
3
-
4
- import logging
5
-
6
- from google.api_core.exceptions import PermissionDenied
7
- from google.cloud import secretmanager
8
-
9
- LIVE_TESTS_AIRBYTE_API_KEY_SECRET_ID = (
10
- "projects/587336813068/secrets/live_tests_airbyte_api_key"
11
- )
12
-
13
-
14
- def get_secret_value(
15
- secret_manager_client: secretmanager.SecretManagerServiceClient, secret_id: str
16
- ) -> str:
17
- """Get the value of the enabled version of a secret
18
-
19
- Args:
20
- secret_manager_client (secretmanager.SecretManagerServiceClient): The secret manager client
21
- secret_id (str): The id of the secret
22
-
23
- Returns:
24
- str: The value of the enabled version of the secret
25
- """
26
- try:
27
- response = secret_manager_client.list_secret_versions(
28
- request={"parent": secret_id, "filter": "state:ENABLED"}
29
- )
30
- if len(response.versions) == 0:
31
- raise ValueError(f"No enabled version of secret {secret_id} found")
32
- enabled_version = response.versions[0]
33
- response = secret_manager_client.access_secret_version(
34
- name=enabled_version.name
35
- )
36
- return response.payload.data.decode("UTF-8")
37
- except PermissionDenied as e:
38
- logging.exception(
39
- f"Permission denied while trying to access secret {secret_id}. Please write to #dev-tooling in Airbyte Slack for help.",
40
- exc_info=e,
41
- )
42
- raise e
43
-
44
-
45
- def get_airbyte_api_key() -> str:
46
- secret_manager_client = secretmanager.SecretManagerServiceClient()
47
- return get_secret_value(secret_manager_client, LIVE_TESTS_AIRBYTE_API_KEY_SECRET_ID)
@@ -1,45 +0,0 @@
1
- # Copyright (c) 2024 Airbyte, Inc., all rights reserved.
2
- from __future__ import annotations
3
-
4
- import logging
5
- import os
6
- from importlib.metadata import version
7
- from typing import Any, Optional
8
-
9
- from segment import analytics # type: ignore
10
-
11
- ENABLE_TRACKING = os.getenv("REGRESSION_TEST_DISABLE_TRACKING") is None
12
- DEBUG_SEGMENT = os.getenv("DEBUG_SEGMENT") is not None
13
- EVENT_NAME = "regression_test_start"
14
- CURRENT_VERSION = version(__name__.split(".")[0])
15
-
16
-
17
- def on_error(error: Exception, items: Any) -> None:
18
- logging.warning("An error occurred in Segment Tracking", exc_info=error)
19
-
20
-
21
- # This is not a secret key, it is a public key that is used to identify the Segment project
22
- analytics.write_key = "hnWfMdEtXNKBjvmJ258F72wShsLmcsZ8"
23
- analytics.send = ENABLE_TRACKING
24
- analytics.debug = DEBUG_SEGMENT
25
- analytics.on_error = on_error
26
-
27
-
28
- def track_usage(
29
- user_id: Optional[str],
30
- pytest_options: dict[str, Any],
31
- ) -> None:
32
- if user_id:
33
- analytics.identify(user_id)
34
- else:
35
- user_id = "airbyte-ci"
36
-
37
- # It contains default pytest option and the custom one passed by the user
38
- analytics.track(
39
- user_id,
40
- EVENT_NAME,
41
- {
42
- "pytest_options": pytest_options,
43
- "package_version": CURRENT_VERSION,
44
- },
45
- )
@@ -1,214 +0,0 @@
1
- # Copyright (c) 2023 Airbyte, Inc., all rights reserved.
2
- from __future__ import annotations
3
-
4
- import logging
5
- import os
6
- import re
7
- import shutil
8
- from pathlib import Path
9
- from typing import Optional
10
-
11
- import dagger
12
- import docker # type: ignore
13
- import pytest
14
- from mitmproxy import http, io # type: ignore
15
- from mitmproxy.addons.savehar import SaveHar # type: ignore
16
- from slugify import slugify
17
-
18
-
19
- async def get_container_from_id(
20
- dagger_client: dagger.Client, container_id: str
21
- ) -> dagger.Container:
22
- """Get a dagger container from its id.
23
- Please remind that container id are not persistent and can change between Dagger sessions.
24
-
25
- Args:
26
- dagger_client (dagger.Client): The dagger client to use to import the connector image
27
- """
28
- try:
29
- return await dagger_client.load_container_from_id(
30
- dagger.ContainerID(container_id)
31
- )
32
- except dagger.DaggerError as e:
33
- pytest.exit(f"Failed to load connector container: {e}")
34
-
35
-
36
- async def get_container_from_tarball_path(
37
- dagger_client: dagger.Client, tarball_path: Path
38
- ) -> dagger.Container:
39
- if not tarball_path.exists():
40
- pytest.exit(f"Connector image tarball {tarball_path} does not exist")
41
- container_under_test_tar_file = (
42
- dagger_client.host()
43
- .directory(str(tarball_path.parent), include=[tarball_path.name])
44
- .file(tarball_path.name)
45
- )
46
- try:
47
- return await dagger_client.container().import_(container_under_test_tar_file)
48
- except dagger.DaggerError as e:
49
- pytest.exit(f"Failed to import connector image from tarball: {e}")
50
-
51
-
52
- async def get_container_from_local_image(
53
- dagger_client: dagger.Client, local_image_name: str
54
- ) -> Optional[dagger.Container]:
55
- """Get a dagger container from a local image.
56
- It will use Docker python client to export the image to a tarball and then import it into dagger.
57
-
58
- Args:
59
- dagger_client (dagger.Client): The dagger client to use to import the connector image
60
- local_image_name (str): The name of the local image to import
61
-
62
- Returns:
63
- Optional[dagger.Container]: The dagger container for the local image or None if the image does not exist
64
- """
65
- docker_client = docker.from_env()
66
-
67
- try:
68
- image = docker_client.images.get(local_image_name)
69
- except docker.errors.ImageNotFound:
70
- return None
71
-
72
- image_digest = image.id.replace("sha256:", "")
73
- tarball_path = Path(f"/tmp/{image_digest}.tar")
74
- if not tarball_path.exists():
75
- logging.info(
76
- f"Exporting local connector image {local_image_name} to tarball {tarball_path}"
77
- )
78
- with open(tarball_path, "wb") as f:
79
- for chunk in image.save(named=True):
80
- f.write(chunk)
81
- return await get_container_from_tarball_path(dagger_client, tarball_path)
82
-
83
-
84
- async def get_container_from_dockerhub_image(
85
- dagger_client: dagger.Client, dockerhub_image_name: str
86
- ) -> dagger.Container:
87
- """Get a dagger container from a dockerhub image.
88
-
89
- Args:
90
- dagger_client (dagger.Client): The dagger client to use to import the connector image
91
- dockerhub_image_name (str): The name of the dockerhub image to import
92
-
93
- Returns:
94
- dagger.Container: The dagger container for the dockerhub image
95
- """
96
- try:
97
- return await dagger_client.container().from_(dockerhub_image_name)
98
- except dagger.DaggerError as e:
99
- pytest.exit(f"Failed to import connector image from DockerHub: {e}")
100
-
101
-
102
- async def get_connector_container(
103
- dagger_client: dagger.Client, image_name_with_tag: str
104
- ) -> dagger.Container:
105
- """Get a dagger container for the connector image to test.
106
-
107
- Args:
108
- dagger_client (dagger.Client): The dagger client to use to import the connector image
109
- image_name_with_tag (str): The docker image name and tag of the connector image to test
110
-
111
- Returns:
112
- dagger.Container: The dagger container for the connector image to test
113
- """
114
- # If a container_id.txt file is available, we'll use it to load the connector container
115
- # We use a txt file as container ids can be too long to be passed as env vars
116
- # It's used for dagger-in-dagger use case with airbyte-ci, when the connector container is built via an upstream dagger operation
117
- container_id_path = Path(f"/tmp/{slugify(image_name_with_tag)}_container_id.txt")
118
- if container_id_path.exists():
119
- return await get_container_from_id(dagger_client, container_id_path.read_text())
120
-
121
- # If the CONNECTOR_UNDER_TEST_IMAGE_TAR_PATH env var is set, we'll use it to import the connector image from the tarball
122
- if connector_image_tarball_path := os.environ.get(
123
- "CONNECTOR_UNDER_TEST_IMAGE_TAR_PATH"
124
- ):
125
- tarball_path = Path(connector_image_tarball_path)
126
- return await get_container_from_tarball_path(dagger_client, tarball_path)
127
-
128
- # Let's try to load the connector container from a local image
129
- if connector_container := await get_container_from_local_image(
130
- dagger_client, image_name_with_tag
131
- ):
132
- return connector_container
133
-
134
- # If we get here, we'll try to pull the connector image from DockerHub
135
- return await get_container_from_dockerhub_image(dagger_client, image_name_with_tag)
136
-
137
-
138
- def sh_dash_c(lines: list[str]) -> list[str]:
139
- """Wrap sequence of commands in shell for safe usage of dagger Container's with_exec method."""
140
- return ["sh", "-c", " && ".join(["set -o xtrace"] + lines)]
141
-
142
-
143
- def clean_up_artifacts(directory: Path, logger: logging.Logger) -> None:
144
- if directory.exists():
145
- shutil.rmtree(directory)
146
- logger.info(f"🧹 Test artifacts cleaned up from {directory}")
147
-
148
-
149
- def get_http_flows_from_mitm_dump(mitm_dump_path: Path) -> list[http.HTTPFlow]:
150
- """Get http flows from a mitmproxy dump file.
151
-
152
- Args:
153
- mitm_dump_path (Path): Path to the mitmproxy dump file.
154
-
155
- Returns:
156
- List[http.HTTPFlow]: List of http flows.
157
- """
158
- with open(mitm_dump_path, "rb") as dump_file:
159
- return [
160
- f for f in io.FlowReader(dump_file).stream() if isinstance(f, http.HTTPFlow)
161
- ]
162
-
163
-
164
- def mitm_http_stream_to_har(mitm_http_stream_path: Path, har_file_path: Path) -> Path:
165
- """Converts a mitmproxy http stream file to a har file.
166
-
167
- Args:
168
- mitm_http_stream_path (Path): Path to the mitmproxy http stream file.
169
- har_file_path (Path): Path where the har file will be saved.
170
-
171
- Returns:
172
- Path: Path to the har file.
173
- """
174
- flows = get_http_flows_from_mitm_dump(mitm_http_stream_path)
175
- SaveHar().export_har(flows, str(har_file_path))
176
- return har_file_path
177
-
178
-
179
- def extract_connection_id_from_url(url: str) -> str:
180
- pattern = r"/connections/([a-f0-9\-]+)"
181
- match = re.search(pattern, url)
182
- if match:
183
- return match.group(1)
184
- else:
185
- raise ValueError(f"Could not extract connection id from url {url}")
186
-
187
-
188
- def extract_workspace_id_from_url(url: str) -> str:
189
- pattern = r"/workspaces/([a-f0-9\-]+)"
190
- match = re.search(pattern, url)
191
- if match:
192
- return match.group(1)
193
- else:
194
- raise ValueError(f"Could not extract workspace id from url {url}")
195
-
196
-
197
- def build_connection_url(workspace_id: str | None, connection_id: str | None) -> str:
198
- if not workspace_id or not connection_id:
199
- raise ValueError("Both workspace_id and connection_id must be provided")
200
- return f"https://cloud.airbyte.com/workspaces/{workspace_id}/connections/{connection_id}"
201
-
202
-
203
- def sort_dict_keys(d: dict) -> dict:
204
- if isinstance(d, dict):
205
- sorted_dict = {}
206
- for key in sorted(d.keys()):
207
- sorted_dict[key] = sort_dict_keys(d[key])
208
- return sorted_dict
209
- else:
210
- return d
211
-
212
-
213
- def sanitize_stream_name(stream_name: str) -> str:
214
- return stream_name.replace("/", "_").replace(" ", "_").lower()