diff --git a/.github/workflows/pyatlan-pr.yaml b/.github/workflows/pyatlan-pr.yaml index b77cf1207..9d916e62f 100644 --- a/.github/workflows/pyatlan-pr.yaml +++ b/.github/workflows/pyatlan-pr.yaml @@ -56,7 +56,7 @@ jobs: - name: Install dependencies run: | - python -m pip install --no-cache-dir --upgrade pip + python -m pip install --no-cache-dir --upgrade pip setuptools if [ -f requirements.txt ]; then pip install --no-cache-dir -r requirements.txt; fi if [ -f requirements-dev.txt ]; then pip install --no-cache-dir -r requirements-dev.txt; fi @@ -102,7 +102,7 @@ jobs: - name: Install dependencies run: | - python -m pip install --no-cache-dir --upgrade pip + python -m pip install --no-cache-dir --upgrade pip setuptools if [ -f requirements.txt ]; then pip install --no-cache-dir -r requirements.txt; fi if [ -f requirements-dev.txt ]; then pip install --no-cache-dir -r requirements-dev.txt; fi diff --git a/pyatlan/errors.py b/pyatlan/errors.py index edb45efec..8dadf1d9d 100644 --- a/pyatlan/errors.py +++ b/pyatlan/errors.py @@ -51,6 +51,17 @@ def __str__(self): ) +class DependencyNotFoundError(Exception): + """ + Raised when a required external dependency is not installed. + + This exception is typically used to indicate that an optional library + or plugin needed for a specific feature is missing. + """ + + pass + + class ApiConnectionError(AtlanError): """Error that occurs when there is an intermittent issue with the API, such as a network outage or an inability to connect due to an incorrect URL.""" diff --git a/pyatlan/test_utils/__init__.py b/pyatlan/test_utils/__init__.py index e2a1ec59d..b82aec9f9 100644 --- a/pyatlan/test_utils/__init__.py +++ b/pyatlan/test_utils/__init__.py @@ -1,5 +1,5 @@ # SPDX-License-Identifier: Apache-2.0 -# Copyright 2024 Atlan Pte. Ltd. +# Copyright 2025 Atlan Pte. Ltd. import logging import random from os import path diff --git a/pyatlan/test_utils/base_vcr.py b/pyatlan/test_utils/base_vcr.py new file mode 100644 index 000000000..1930b346b --- /dev/null +++ b/pyatlan/test_utils/base_vcr.py @@ -0,0 +1,294 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright 2025 Atlan Pte. Ltd. + +import pkg_resources # type: ignore[import-untyped] + +from pyatlan.errors import DependencyNotFoundError + +# Check if pytest-vcr plugin is installed +try: + pkg_resources.get_distribution("pytest-vcr") +except pkg_resources.DistributionNotFound: + raise DependencyNotFoundError( + "pytest-vcr plugin is not installed. Please install pytest-vcr." + ) + +# Check if vcrpy is installed and ensure the version is 6.0.x +try: + vcr_version = pkg_resources.get_distribution("vcrpy").version + if not vcr_version.startswith("6.0"): + raise DependencyNotFoundError( + f"vcrpy version 6.0.x is required, but found {vcr_version}. Please install the correct version." + ) +except pkg_resources.DistributionNotFound: + raise DependencyNotFoundError( + "vcrpy version 6.0.x is not installed. Please install vcrpy version 6.0.x." + ) + +import json +import os +from typing import Any, Dict, Union + +import pytest +import yaml # type: ignore[import-untyped] + + +class LiteralBlockScalar(str): + """Formats the string as a literal block scalar, preserving whitespace and + without interpreting escape characters""" + + +def literal_block_scalar_presenter(dumper, data): + """Represents a scalar string as a literal block, via '|' syntax""" + return dumper.represent_scalar("tag:yaml.org,2002:str", data, style="|") + + +yaml.add_representer(LiteralBlockScalar, literal_block_scalar_presenter) + + +def process_string_value(string_value): + """Pretty-prints JSON or returns long strings as a LiteralBlockScalar""" + try: + json_data = json.loads(string_value) + return LiteralBlockScalar(json.dumps(json_data, indent=2)) + except (ValueError, TypeError): + if len(string_value) > 80: + return LiteralBlockScalar(string_value) + return string_value + + +def convert_body_to_literal(data): + """Searches the data for body strings, attempting to pretty-print JSON""" + if isinstance(data, dict): + for key, value in data.items(): + # Handle response body case (e.g: response.body.string) + if key == "body" and isinstance(value, dict) and "string" in value: + value["string"] = process_string_value(value["string"]) + + # Handle request body case (e.g: request.body) + elif key == "body" and isinstance(value, str): + data[key] = process_string_value(value) + + else: + convert_body_to_literal(value) + + elif isinstance(data, list): + for idx, choice in enumerate(data): + data[idx] = convert_body_to_literal(choice) + + return data + + +class VCRPrettyPrintYamlJSONBody: + """This makes request and response YAML JSON body recordings more readable.""" + + @staticmethod + def serialize(cassette_dict): + cassette_dict = convert_body_to_literal(cassette_dict) + return yaml.dump(cassette_dict, default_flow_style=False, allow_unicode=True) + + @staticmethod + def deserialize(cassette_string): + return yaml.safe_load(cassette_string) + + +class VCRPrettyPrintJSONBody: + """Makes request and response JSON body recordings more readable.""" + + @staticmethod + def _parse_json_body( + body: Union[str, bytes, None], + ) -> Union[Dict[str, Any], str, None, bytes]: + """Parse JSON body if possible, otherwise return the original body.""" + if body is None: + return None + + # Convert bytes to string if needed + if isinstance(body, bytes): + try: + body = body.decode("utf-8") + except UnicodeDecodeError: + return body # Return original if can't decode + + # If it's a string, try to parse as JSON + if isinstance(body, str): + try: + return json.loads(body) + except json.JSONDecodeError: + return body # Return original if not valid JSON + + return body # Return original for other types + + @staticmethod + def serialize(cassette_dict: dict) -> str: + """ + Converts body strings to parsed JSON objects for better readability when possible. + """ + # Safety check for cassette_dict + if not cassette_dict or not isinstance(cassette_dict, dict): + cassette_dict = {} + + interactions = cassette_dict.get("interactions", []) or [] + + for interaction in interactions: + if not interaction: + continue + + # Handle response body + response = interaction.get("response") or {} + body_container = response.get("body") + if isinstance(body_container, dict) and "string" in body_container: + parsed_body = VCRPrettyPrintJSONBody._parse_json_body( + body_container["string"] + ) + if isinstance(parsed_body, dict): + # Replace string field with parsed_json field + response["body"] = {"parsed_json": parsed_body} + + # Handle request body + request = interaction.get("request") or {} + body_container = request.get("body") + if isinstance(body_container, dict) and "string" in body_container: + parsed_body = VCRPrettyPrintJSONBody._parse_json_body( + body_container["string"] + ) + if isinstance(parsed_body, dict): + # Replace string field with parsed_json field + request["body"] = {"parsed_json": parsed_body} + + # Serialize the final dictionary into a JSON string with pretty formatting + try: + return json.dumps(cassette_dict, indent=2, ensure_ascii=False) + "\n" + except TypeError as exc: + raise TypeError( + "Does this HTTP interaction contain binary data? " + "If so, use a different serializer (like the YAML serializer)." + ) from exc + + @staticmethod + def deserialize(cassette_string: str) -> dict: + """ + Deserializes a JSON string into a dictionary and converts + parsed_json fields back to string fields. + """ + # Safety check for cassette_string + if not cassette_string: + return {} + + try: + cassette_dict = json.loads(cassette_string) + except json.JSONDecodeError: + return {} + + # Convert parsed_json back to string format + interactions = cassette_dict.get("interactions", []) or [] + + for interaction in interactions: + if not interaction: + continue + + # Handle response body + response = interaction.get("response") or {} + body_container = response.get("body") + if isinstance(body_container, dict) and "parsed_json" in body_container: + json_body = body_container["parsed_json"] + response["body"] = {"string": json.dumps(json_body)} + + # Handle request body + request = interaction.get("request") or {} + body_container = request.get("body") + if isinstance(body_container, dict) and "parsed_json" in body_container: + json_body = body_container["parsed_json"] + request["body"] = {"string": json.dumps(json_body)} + + return cassette_dict + + +class BaseVCR: + """ + A base class for configuring VCR (Virtual Cassette Recorder) + for HTTP request/response recording and replaying. + + This class provides pytest fixtures to set up the VCR configuration + and custom serializers for JSON and YAML formats. + It also handles cassette directory configuration. + """ + + class VCRRemoveAllHeaders: + """ + A class responsible for removing all headers from requests and responses. + This can be useful for scenarios where headers are not needed for matching or comparison + in VCR (Virtual Cassette Recorder) interactions, such as when recording or replaying HTTP requests. + """ + + @staticmethod + def remove_all_request_headers(request): + # Save only what's necessary for matching + request.headers = {} + return request + + @staticmethod + def remove_all_response_headers(response): + # Save only what's necessary for matching + response["headers"] = {} + return response + + _CASSETTES_DIR = None + _BASE_CONFIG = { + # More config options can be found at: + # https://vcrpy.readthedocs.io/en/latest/configuration.html#configuration + "record_mode": "once", # (default: "once", "always", "none", "new_episodes") + "serializer": "pretty-yaml", # (default: "yaml") + "decode_compressed_response": True, # Decode compressed responses + # (optional) Replace the Authorization request header with "**REDACTED**" in cassettes + # "filter_headers": [("authorization", "**REDACTED**")], + "before_record_request": VCRRemoveAllHeaders.remove_all_request_headers, + "before_record_response": VCRRemoveAllHeaders.remove_all_response_headers, + } + + @pytest.fixture(scope="module") + def vcr(self, vcr): + """ + Registers custom serializers for VCR and returns the VCR instance. + + The method registers two custom serializers: + - "pretty-json" for pretty-printing JSON responses. + - "pretty-yaml" for pretty-printing YAML responses. + + :param vcr: The VCR instance provided by the pytest-vcr plugin + :returns: modified VCR instance with custom serializers registered + """ + vcr.register_serializer("pretty-json", VCRPrettyPrintJSONBody) + vcr.register_serializer("pretty-yaml", VCRPrettyPrintYamlJSONBody) + return vcr + + @pytest.fixture(scope="module") + def vcr_config(self): + """ + Provides the VCR configuration dictionary. + + The configuration includes default options for the recording mode, + serializer, response decoding, and filtering headers. + This configuration is used to set up VCR behavior during tests. + + :returns: a dictionary with VCR configuration options + """ + return self._BASE_CONFIG + + @pytest.fixture(scope="module") + def vcr_cassette_dir(self, request): + """ + Provides the directory path for storing VCR cassettes. + + If a custom cassette directory is set in the class, it is used; + otherwise, the default directory structure is created under "tests/cassettes". + The directory path will be based on the module name. + + :param request: request object which provides metadata about the test + + :returns: directory path for storing cassettes + """ + # Set self._CASSETTES_DIR or use the default directory path based on the test module name + return self._CASSETTES_DIR or os.path.join( + "tests/vcr_cassettes", request.module.__name__ + ) diff --git a/requirements-dev.txt b/requirements-dev.txt index dec693508..3d25f0c2c 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,7 +1,14 @@ mypy~=1.9.0 ruff~=0.9.9 -types-requests~=2.32.0.20241016 +# [PINNED] for Python 3.8 compatibility +# higher versions require urllib3>=2.0 +types-requests~=2.31.0.6 +types-setuptools~=75.8.0.20250110 pytest~=8.3.4 +pytest-vcr~=1.0.2 +# [PINNED] to v6.x since vcrpy>=7.0 requires urllib3>=2.0 +# which breaks compatibility with Python 3.8 +vcrpy~=6.0.2 pytest-order~=1.3.0 pytest-timer[termcolor]~=1.0.0 pytest-sugar~=1.0.0 diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py index a4a71b48e..ee8142e0e 100644 --- a/tests/unit/conftest.py +++ b/tests/unit/conftest.py @@ -31,3 +31,19 @@ def mock_custom_metadata_cache(): def mock_tag_cache(): with patch("pyatlan.cache.atlan_tag_cache.AtlanTagCache") as cache: yield cache + + +@pytest.fixture(autouse=True) +def patch_vcr_http_response_version_string(): + """ + Patch the VCRHTTPResponse class to add a version_string attribute if it doesn't exist. + + This patch is necessary to avoid bumping vcrpy to 7.0.0, + which drops support for Python 3.8. + """ + from vcr.stubs import VCRHTTPResponse # type: ignore[import-untyped] + + if not hasattr(VCRHTTPResponse, "version_string"): + VCRHTTPResponse.version_string = None + + yield diff --git a/tests/unit/test_base_vcr_json.py b/tests/unit/test_base_vcr_json.py new file mode 100644 index 000000000..32f79a673 --- /dev/null +++ b/tests/unit/test_base_vcr_json.py @@ -0,0 +1,65 @@ +import pytest +import requests + +from pyatlan.test_utils.base_vcr import BaseVCR + + +class TestBaseVCRJSON(BaseVCR): + """ + Integration tests to demonstrate VCR.py capabilities + by recording and replaying HTTP interactions using + HTTPBin (https://httpbin.org) for GET, POST, PUT, and DELETE requests. + """ + + BASE_URL = "https://httpbin.org" + + @pytest.fixture(scope="session") + def vcr_config(self): + """ + Override the VCR configuration to use JSON serialization across the module. + """ + config = self._BASE_CONFIG.copy() + config.update({"serializer": "pretty-json"}) + return config + + @pytest.mark.vcr() + def test_httpbin_get(self): + """ + Test a simple GET request to httpbin. + """ + url = f"{self.BASE_URL}/get" + response = requests.get(url, params={"test": "value"}) + assert response.status_code == 200 + assert response.json()["args"]["test"] == "value" + + @pytest.mark.vcr() + def test_httpbin_post(self): + """ + Test a simple POST request to httpbin. + """ + url = f"{self.BASE_URL}/post" + payload = {"name": "atlan", "type": "integration-test"} + response = requests.post(url, json=payload) + assert response.status_code == 200 + assert response.json()["json"] == payload + + @pytest.mark.vcr() + def test_httpbin_put(self): + """ + Test a simple PUT request to httpbin. + """ + url = f"{self.BASE_URL}/put" + payload = {"update": "value"} + response = requests.put(url, json=payload) + assert response.status_code == 200 + assert response.json()["json"] == payload + + @pytest.mark.vcr() + def test_httpbin_delete(self): + """ + Test a simple DELETE request to httpbin. + """ + url = f"{self.BASE_URL}/delete" + response = requests.delete(url) + assert response.status_code == 200 + assert response.json()["args"] == {} diff --git a/tests/unit/test_base_vcr_yaml.py b/tests/unit/test_base_vcr_yaml.py new file mode 100644 index 000000000..107f9f351 --- /dev/null +++ b/tests/unit/test_base_vcr_yaml.py @@ -0,0 +1,61 @@ +import pytest +import requests + +from pyatlan.test_utils.base_vcr import BaseVCR + + +class TestBaseVCRYAML(BaseVCR): + """ + Integration tests to demonstrate VCR.py capabilities + by recording and replaying HTTP interactions using + HTTPBin (https://httpbin.org) for GET, POST, PUT, and DELETE requests. + """ + + BASE_URL = "https://httpbin.org" + + @pytest.mark.vcr() + def test_httpbin_get(self): + """ + Test a simple GET request to httpbin. + """ + url = f"{self.BASE_URL}/get" + response = requests.get(url, params={"test": "value"}) + + assert response.status_code == 200 + assert response.json()["args"]["test"] == "value" + + @pytest.mark.vcr() + def test_httpbin_post(self): + """ + Test a simple POST request to httpbin. + """ + url = f"{self.BASE_URL}/post" + payload = {"name": "atlan", "type": "integration-test"} + response = requests.post(url, json=payload) + + assert response.status_code == 200 + assert response.json()["json"] == payload + + @pytest.mark.vcr() + def test_httpbin_put(self): + """ + Test a simple PUT request to httpbin. + """ + url = f"{self.BASE_URL}/put" + payload = {"update": "value"} + response = requests.put(url, json=payload) + + assert response.status_code == 200 + assert response.json()["json"] == payload + + @pytest.mark.vcr() + def test_httpbin_delete(self): + """ + Test a simple DELETE request to httpbin. + """ + url = f"{self.BASE_URL}/delete" + response = requests.delete(url) + + assert response.status_code == 200 + # HTTPBin returns an empty JSON object for DELETE + assert response.json()["args"] == {} diff --git a/tests/vcr_cassettes/tests.unit.test_base_vcr_json/TestBaseVCRJSON.test_httpbin_delete.yaml b/tests/vcr_cassettes/tests.unit.test_base_vcr_json/TestBaseVCRJSON.test_httpbin_delete.yaml new file mode 100644 index 000000000..bd2cb4d96 --- /dev/null +++ b/tests/vcr_cassettes/tests.unit.test_base_vcr_json/TestBaseVCRJSON.test_httpbin_delete.yaml @@ -0,0 +1,39 @@ +{ + "version": 1, + "interactions": [ + { + "request": { + "method": "DELETE", + "uri": "https://httpbin.org/delete", + "body": null, + "headers": {} + }, + "response": { + "status": { + "code": 200, + "message": "OK" + }, + "headers": {}, + "body": { + "parsed_json": { + "args": {}, + "data": "", + "files": {}, + "form": {}, + "headers": { + "Accept": "*/*", + "Accept-Encoding": "gzip, deflate", + "Content-Length": "0", + "Host": "httpbin.org", + "User-Agent": "python-requests/2.32.3", + "X-Amzn-Trace-Id": "Root=1-680f7263-439ec4f97dc37ffe07c63697" + }, + "json": null, + "origin": "x.x.x.x", + "url": "https://httpbin.org/delete" + } + } + } + } + ] +} diff --git a/tests/vcr_cassettes/tests.unit.test_base_vcr_json/TestBaseVCRJSON.test_httpbin_get.yaml b/tests/vcr_cassettes/tests.unit.test_base_vcr_json/TestBaseVCRJSON.test_httpbin_get.yaml new file mode 100644 index 000000000..ed2c14dcf --- /dev/null +++ b/tests/vcr_cassettes/tests.unit.test_base_vcr_json/TestBaseVCRJSON.test_httpbin_get.yaml @@ -0,0 +1,36 @@ +{ + "version": 1, + "interactions": [ + { + "request": { + "method": "GET", + "uri": "https://httpbin.org/get?test=value", + "body": null, + "headers": {} + }, + "response": { + "status": { + "code": 200, + "message": "OK" + }, + "headers": {}, + "body": { + "parsed_json": { + "args": { + "test": "value" + }, + "headers": { + "Accept": "*/*", + "Accept-Encoding": "gzip, deflate", + "Host": "httpbin.org", + "User-Agent": "python-requests/2.32.3", + "X-Amzn-Trace-Id": "Root=1-680f7259-4e5c927e25a0aa78202e04e1" + }, + "origin": "x.x.x.x", + "url": "https://httpbin.org/get?test=value" + } + } + } + } + ] +} diff --git a/tests/vcr_cassettes/tests.unit.test_base_vcr_json/TestBaseVCRJSON.test_httpbin_post.yaml b/tests/vcr_cassettes/tests.unit.test_base_vcr_json/TestBaseVCRJSON.test_httpbin_post.yaml new file mode 100644 index 000000000..2d245a440 --- /dev/null +++ b/tests/vcr_cassettes/tests.unit.test_base_vcr_json/TestBaseVCRJSON.test_httpbin_post.yaml @@ -0,0 +1,43 @@ +{ + "version": 1, + "interactions": [ + { + "request": { + "method": "POST", + "uri": "https://httpbin.org/post", + "body": "{\"name\": \"atlan\", \"type\": \"integration-test\"}", + "headers": {} + }, + "response": { + "status": { + "code": 200, + "message": "OK" + }, + "headers": {}, + "body": { + "parsed_json": { + "args": {}, + "data": "{\"name\": \"atlan\", \"type\": \"integration-test\"}", + "files": {}, + "form": {}, + "headers": { + "Accept": "*/*", + "Accept-Encoding": "gzip, deflate", + "Content-Length": "45", + "Content-Type": "application/json", + "Host": "httpbin.org", + "User-Agent": "python-requests/2.32.3", + "X-Amzn-Trace-Id": "Root=1-680f725c-53119e7d4121a80069c14836" + }, + "json": { + "name": "atlan", + "type": "integration-test" + }, + "origin": "x.x.x.x", + "url": "https://httpbin.org/post" + } + } + } + } + ] +} diff --git a/tests/vcr_cassettes/tests.unit.test_base_vcr_json/TestBaseVCRJSON.test_httpbin_put.yaml b/tests/vcr_cassettes/tests.unit.test_base_vcr_json/TestBaseVCRJSON.test_httpbin_put.yaml new file mode 100644 index 000000000..a5872a1b6 --- /dev/null +++ b/tests/vcr_cassettes/tests.unit.test_base_vcr_json/TestBaseVCRJSON.test_httpbin_put.yaml @@ -0,0 +1,42 @@ +{ + "version": 1, + "interactions": [ + { + "request": { + "method": "PUT", + "uri": "https://httpbin.org/put", + "body": "{\"update\": \"value\"}", + "headers": {} + }, + "response": { + "status": { + "code": 200, + "message": "OK" + }, + "headers": {}, + "body": { + "parsed_json": { + "args": {}, + "data": "{\"update\": \"value\"}", + "files": {}, + "form": {}, + "headers": { + "Accept": "*/*", + "Accept-Encoding": "gzip, deflate", + "Content-Length": "19", + "Content-Type": "application/json", + "Host": "httpbin.org", + "User-Agent": "python-requests/2.32.3", + "X-Amzn-Trace-Id": "Root=1-680f7261-631f6aae6a8ae85365354c87" + }, + "json": { + "update": "value" + }, + "origin": "x.x.x.x", + "url": "https://httpbin.org/put" + } + } + } + } + ] +} diff --git a/tests/vcr_cassettes/tests.unit.test_base_vcr_yaml/TestBaseVCRYAML.test_httpbin_delete.yaml b/tests/vcr_cassettes/tests.unit.test_base_vcr_yaml/TestBaseVCRYAML.test_httpbin_delete.yaml new file mode 100644 index 000000000..7fccbf28b --- /dev/null +++ b/tests/vcr_cassettes/tests.unit.test_base_vcr_yaml/TestBaseVCRYAML.test_httpbin_delete.yaml @@ -0,0 +1,31 @@ +interactions: +- request: + body: null + headers: {} + method: DELETE + uri: https://httpbin.org/delete + response: + body: + string: |- + { + "args": {}, + "data": "", + "files": {}, + "form": {}, + "headers": { + "Accept": "*/*", + "Accept-Encoding": "gzip, deflate", + "Content-Length": "0", + "Host": "httpbin.org", + "User-Agent": "python-requests/2.32.3", + "X-Amzn-Trace-Id": "Root=1-680f7294-550da7df2956737310f29c0c" + }, + "json": null, + "origin": "x.x.x.x", + "url": "https://httpbin.org/delete" + } + headers: {} + status: + code: 200 + message: OK +version: 1 diff --git a/tests/vcr_cassettes/tests.unit.test_base_vcr_yaml/TestBaseVCRYAML.test_httpbin_get.yaml b/tests/vcr_cassettes/tests.unit.test_base_vcr_yaml/TestBaseVCRYAML.test_httpbin_get.yaml new file mode 100644 index 000000000..e4e5b86f1 --- /dev/null +++ b/tests/vcr_cassettes/tests.unit.test_base_vcr_yaml/TestBaseVCRYAML.test_httpbin_get.yaml @@ -0,0 +1,28 @@ +interactions: +- request: + body: null + headers: {} + method: GET + uri: https://httpbin.org/get?test=value + response: + body: + string: |- + { + "args": { + "test": "value" + }, + "headers": { + "Accept": "*/*", + "Accept-Encoding": "gzip, deflate", + "Host": "httpbin.org", + "User-Agent": "python-requests/2.32.3", + "X-Amzn-Trace-Id": "Root=1-680f728f-7e7ba4866bd0c4c847de2cc2" + }, + "origin": "x.x.x.x", + "url": "https://httpbin.org/get?test=value" + } + headers: {} + status: + code: 200 + message: OK +version: 1 diff --git a/tests/vcr_cassettes/tests.unit.test_base_vcr_yaml/TestBaseVCRYAML.test_httpbin_post.yaml b/tests/vcr_cassettes/tests.unit.test_base_vcr_yaml/TestBaseVCRYAML.test_httpbin_post.yaml new file mode 100644 index 000000000..3ab6ee8b7 --- /dev/null +++ b/tests/vcr_cassettes/tests.unit.test_base_vcr_yaml/TestBaseVCRYAML.test_httpbin_post.yaml @@ -0,0 +1,39 @@ +interactions: +- request: + body: |- + { + "name": "atlan", + "type": "integration-test" + } + headers: {} + method: POST + uri: https://httpbin.org/post + response: + body: + string: |- + { + "args": {}, + "data": "{\"name\": \"atlan\", \"type\": \"integration-test\"}", + "files": {}, + "form": {}, + "headers": { + "Accept": "*/*", + "Accept-Encoding": "gzip, deflate", + "Content-Length": "45", + "Content-Type": "application/json", + "Host": "httpbin.org", + "User-Agent": "python-requests/2.32.3", + "X-Amzn-Trace-Id": "Root=1-680f7290-276efa7f015f83d24d9fdfc4" + }, + "json": { + "name": "atlan", + "type": "integration-test" + }, + "origin": "x.x.x.x", + "url": "https://httpbin.org/post" + } + headers: {} + status: + code: 200 + message: OK +version: 1 diff --git a/tests/vcr_cassettes/tests.unit.test_base_vcr_yaml/TestBaseVCRYAML.test_httpbin_put.yaml b/tests/vcr_cassettes/tests.unit.test_base_vcr_yaml/TestBaseVCRYAML.test_httpbin_put.yaml new file mode 100644 index 000000000..ec1bdd256 --- /dev/null +++ b/tests/vcr_cassettes/tests.unit.test_base_vcr_yaml/TestBaseVCRYAML.test_httpbin_put.yaml @@ -0,0 +1,37 @@ +interactions: +- request: + body: |- + { + "update": "value" + } + headers: {} + method: PUT + uri: https://httpbin.org/put + response: + body: + string: |- + { + "args": {}, + "data": "{\"update\": \"value\"}", + "files": {}, + "form": {}, + "headers": { + "Accept": "*/*", + "Accept-Encoding": "gzip, deflate", + "Content-Length": "19", + "Content-Type": "application/json", + "Host": "httpbin.org", + "User-Agent": "python-requests/2.32.3", + "X-Amzn-Trace-Id": "Root=1-680f7292-14a3d32d1869399c2db8f571" + }, + "json": { + "update": "value" + }, + "origin": "x.x.x.x", + "url": "https://httpbin.org/put" + } + headers: {} + status: + code: 200 + message: OK +version: 1