# -*- coding: utf-8 -*-
from setuptools import setup

packages = \
['lobotomy',
 'lobotomy._cli',
 'lobotomy._clients',
 'lobotomy._services',
 'lobotomy._services._augmentations']

package_data = \
{'': ['*']}

install_requires = \
['PyYAML>=5.3.0', 'boto3>=1.16.0', 'python-dateutil>=2.8.0', 'toml>=0.10.0']

entry_points = \
{'console_scripts': ['lobotomy = lobotomy:run_cli']}

setup_kwargs = {
    'name': 'lobotomy',
    'version': '0.3.12',
    'description': 'Boto3 low-level client mocking library.',
    'long_description': '# Lobotomy\n\n[![PyPI version](https://badge.fury.io/py/lobotomy.svg)](https://pypi.org/project/lobotomy/)\n[![build status](https://gitlab.com/rocket-boosters/lobotomy/badges/main/pipeline.svg)](https://gitlab.com/rocket-boosters/lobotomy/commits/main)\n[![coverage report](https://gitlab.com/rocket-boosters/lobotomy/badges/main/coverage.svg)](https://gitlab.com/rocket-boosters/lobotomy/commits/main)\n[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)\n[![Code style: flake8](https://img.shields.io/badge/code%20style-flake8-white)](https://gitlab.com/pycqa/flake8)\n[![Code style: mypy](https://img.shields.io/badge/code%20style-mypy-white)](http://mypy-lang.org/)\n[![Code style: pydocstyle](https://img.shields.io/badge/code%20style-pydocstyle-white)](http://www.pydocstyle.org/en/stable/)\n[![Code style: radon](https://img.shields.io/badge/code%20style-radon-white)](https://radon.readthedocs.io/en/latest/)\n[![PyPI - License](https://img.shields.io/pypi/l/lobotomy)](https://pypi.org/project/lobotomy/)\n\n- [Installation](#installation)\n- [Tl;dr Usage](#tldr-usage)\n- [Usage](#usage)\n    - [Configuration Files](#configuration-files)\n    - [Test Patching](#test-patching)\n    - [YAML IO Modifiers](#yaml-io-modifiers)\n      - [!lobotomy.to_json](#to_json)\n      - [!lobotomy.inject_string](#inject_string)\n    - [Special Cases](#special-cases)\n      - [s3.download_fileobj](#s3download_fileobj)\n    - [Command Line Interface](#command-line-interface)\n- [Advanced Usage](#advanced-usage)\n    - [Key Prefixes](#key-prefixes)\n    - [Patching Targets](#patching-targets)\n    - [Session Configuration](#session-configuration)\n    - [Client Overrides](#client-overrides)\n    - [Error Handling](#error-handling)\n    - [Callable Responses](#callable-responses)\n\nThe *lo&#8226;**boto**&#8226;my* library allows one to mock the low-level boto3\nclient libraries efficiently, especially in more complex scenario testing \nsituations, using configuration-based response definitions. The benefit is a\nseparation of the configuration from the test execution, which cleans up the\ntest invocation process.\n\n## Installation\n\nlobotomy is available on pypi and installable via pip:\n\n```shell script\n$ pip install lobotomy\n```\n\nor via poetry as a development dependency in a project:\n\n```shell script\n$ poetry add lobotomy -D\n```\n\n## Tl;dr Usage\n\nCreate a configuration file in YAML, TOML, or JSON format with a root *clients*\nkey. Beneath that key add the desired client calls mocked responses by service\nas shown here for a `session.client(\'s3\').get_object()` mocked response:\n\n```yaml\nclients:\n  s3:\n    get_object:\n      Body: \'The contents of my S3 file.\'\n      LastModified: \'2020-12-01T01:02:03Z\'\n```\n\nThen in the test notice that the lobotomy patching process handles the\nrest, including casting the configuration values specified in the call\nabove into the more complex data types returned for the specific call.\n\n```python\nimport lobotomy\nimport pathlib\nimport boto3\nimport datetime\n\nmy_directory = pathlib.Path(__file__).parent\n\n@lobotomy.patch(my_directory.joinpath("test_lobotomy.yaml"))\ndef test_lobotomy(lobotomized: lobotomy.Lobotomy):\n    """\n    Should return the mocked get_object response generated from the\n    configuration data specified in the lobotomy patch above. By default\n    the patch(...) applies to the boto3.Session object, so calling \n    boto3.Session() will create a lobotomy.Session instead of a normal\n    boto3.Session. From there the low-level client interface is designed\n    to match normal usage.\n    """\n    s3_client = boto3.Session().client("s3")\n    \n    # Lobotomy will validate that you have specified the required keys\n    # in your request, so Bucket and Key have to be supplied here even though\n    # they are not meaningful values in this particular test scenario.\n    response = s3_client.get_object(Bucket="foo", Key="bar")\n\n    expected = b"The contents of my S3 file."\n    assert response["Body"].read() == expected, """\n        Expected the mocked response body data to be returned as a\n        StreamingBody object with blob/bytes contents. The lobotomy\n        library introspects boto to properly convert the string body\n        value in the configuration file into the expected return format\n        for the particular call.\n        """\n    \n    expected = datetime.datetime(2020, 12, 1, 1, 2, 3, 0, datetime.timezone.utc)\n    assert response["LastModified"] == expected, """\n        Expected the mocked response last modified value to be a timezone-aware\n        datetime value generated from the string timestamp value in the\n        configuration file to match how it would be returned by boto in\n        an actual response.\n        """\n\n    call = lobotomized.get_service_call("s3", "get_object")\n    assert call.request["Bucket"] == "foo", """\n        Expected the s3.get_object method call arguments to have specified the bucket\n        as "foo".\n        """\n    assert call.request["Key"] == "bar", """\n        Expected the s3.get_object method call argumets to have specified the key as\n        "bar".\n        """\n```\n\n# Usage\n\n## Configuration Files\n\nTest scenarios can be written in YAML, TOML, or JSON formats. YAML or TOML are\nrecommended unless copying output responses from JSON calls is easier for a\ngiven use-case. In the following example, we\'ll define the calls using YAML:\n\n```yaml\nclients:\n  sts:\n    get_caller_identity:\n      Account: \'987654321\'\n  s3:\n    get_object:\n      Body: \'The contents of my S3 file.\'\n      LastModified: \'2020-11-01T12:23:34Z\'\n```\n\nAll call responses are stored within the root *clients* attribute with service\nnames as sub-attributes and then the service method call responses defined \nbeneath the service name attributes. Multiple services and multiple methods\nper service are specified in this way by the hierarchical key lists.\n\nIf the contents of a method response definition are not a list, the same\nresponse will be returned for each call to that client method during the\ntest. Specifying a list of responses will alter the behavior such that each\nsuccessive call will iterate through that list of configured responses.\n\n```yaml\nclients:\n  s3:\n    get_object:\n    - Body: \'The contents of my S3 file.\'\n      LastModified: \'2020-11-01T12:23:34Z\'\n    - Body: \'Another S3 file.\'\n      LastModified: \'2020-11-03T12:23:34Z\'\n```\n\nThe lobotomy library dynamically inspects boto for its response structure\nand data types, such that they match the types for the normal boto response\nfor each client method call. In the case of the\n`session.client(\'s3\').get_object()` calls above, the `Body` would return\na `StreamingBody` object with the string converted to bytes to match the\nnormal output. Similarly, the `LastModified` would be converted to a\ntimezone-aware datetime object by lobotomy as well. This makes it easy\nto specify primitive data types in the configuration file that are transformed\ninto their more complex counterparts when returned within the execution of\nthe actual code.\n\n## Test Patching\n\nOnce the configuration file has been specified, it is used within a test\nvia a `lobotomy.patch` as shown below.\n\n```python\nimport lobotomy\nimport pathlib\nimport boto3\nimport datetime\n\nmy_directory = pathlib.Path(__file__).parent\n\n@lobotomy.patch(my_directory.joinpath("test_lobotomy.yaml"))\ndef test_lobotomy(lobotomized: lobotomy.Lobotomy):\n    """\n    Should return the mocked get_object response generated from the\n    configuration data specified in the lobotomy patch above. By default\n    the patch(...) applies to the boto3.Session object, so calling \n    boto3.Session() will create a lobotomy.Session instead of a normal\n    boto3.Session. From there the low-level client interface is designed\n    to match normal usage.\n    """\n    s3_client = boto3.Session().client("s3")\n    \n    # Lobotomy will validate that you have specified the required keys\n    # in your request, so Bucket and Key have to be supplied here even though\n    # they are not meaningful values in this particular test scenario.\n    response = s3_client.get_object(Bucket="foo", Key="bar")\n\n    expected = b"The contents of my S3 file."\n    assert response[\'Body\'].read() == expected, """\n        Expect the mocked response body data to be returned as a\n        StreamingBody object with blob/bytes contents. The lobotomy\n        library introspects boto to properly convert the string body\n        value in the configuration file into the expected return format\n        for the particular call.\n        """\n    \n    expected = datetime.datetime(2020, 12, 1, 1, 2, 3, 0, datetime.timezone.utc)\n    assert response[\'LastModified\'] == expected, """\n        Expect the mocked response last modified value to be a timezeon-aware\n        datetime value generated from the string timestamp value in the\n        configuration file to match how it would be returned by boto in\n        an actual response.\n        """\n```\n\nThe patching process replaces the `boto3.Session` class with a\n`lobotomy.Lobotomy` object that contains the loaded configuration data.\nWhen patched in this fashion, `boto3.Session()` calls will actually be\n`lobotomy.Lobotomy()` calls that return `lobotomy.Session` objects. These\nsessions have the interface of the `boto3.Session` object, but behave in\na way such that client responses are returned from the configuration data\ninstead of through interactivity with AWS.\n\nFor simple cases with little configuration, it is also possible to patch\ndata stored directly within the Python code. The above test could be rewritten\nin this way as:\n\n```python\nimport lobotomy\n\nconfiguration = {\n    "clients": {\n        "s3": {\n            "get_object": [\n                { \n                    "Body": "The contents of my S3 file.",\n                    "LastModified": "2020-11-01T12:23:34Z",\n                },\n                {\n                    "Body": "Another S3 file.",\n                    "LastModified": "2020-11-03T12:23:34Z",\n                },\n            ],\n        },\n    },\n}\n\n\n@lobotomy.patch(data=configuration)\ndef test_lobotomy(lobotomized: lobotomy.Lobotomy):\n    """..."""\n```\n\nAlthough one of the benefits of lobotomy is the ability to streamline the\ntests files by reducing the response configuration, which can be a bit\nverbose inside Python files and that is the highly recommended approach.\n\nA third option for simpler cases is to use the `Lobotomy.add_call()` method\nto register calls and responses.\n\n```python\nimport lobotomy\n\n\n@lobotomy.patch()\ndef test_lobotomy(lobotomized: lobotomy.Lobotomy):\n    """..."""\n    lobotomized.add_call(\n        service_name="s3", \n        method_name="get_object", \n        response={ \n            "Body": "The contents of my S3 file.",\n            "LastModified": "2020-11-01T12:23:34Z",\n        },\n    )\n    lobotomized.add_call(\n        service_name="s3", \n        method_name="get_object", \n        response={\n            "Body": "Another S3 file.",\n            "LastModified": "2020-11-03T12:23:34Z",\n        },\n    )\n```\n\nIn the case above no data or path was supplied to the `lobotomy.patch()` and instead\ncall responses were registered within the test function itself. The response argument\nin the `Lobotomy.add_call()` method is optional. If omitted, a default one will be\ncreated instead in the same fashion as one would be created via the CLI (see below).\n\nNote that it is also possible to mix loading data and adding calls within the test\nfunction.\n\n## YAML IO Modifiers\n\nWhen using YAML files, lobotomy comes with custom YAML classes that can provide\neven more flexibility and ease in defining data. The following are the available\nmodifiers and how to use them:\n\n### to_json\n\nThis modifier will convert YAML data into a JSON string in the creation of the\nresponse, which makes it easier to represent complex JSON data in the scenario\ndata.\n\n```yaml\nclients:\n  secretsmanager:\n    get_secret_value:\n      SecretString: !lobotomy.to_json\n        first: first_value\n        second: second_value\n```\n\nIn this example the `!lobotomy.to_json` YAML modifier instructs the lobotomy to\nconverts the object data beneath the `SecretString` attribute into a JSON string\nas part of the response object. In this case then in the associated Python test:\n\n```python\nimport boto3\nimport lobotomy\nimport json\n\n\n@lobotomy.patch(path="scenario.yaml")\ndef test_showing_to_json(lobotomized: lobotomy.Lobotomy):\n    """Should expect a JSON string for the \'SecretString\' value."""\n    client = boto3.Session().client("secretsmanager")\n\n    response = client.get_secret_value(SecretId="fake")\n    \n    expected = {"first": "first_value", "second": "second_value"}\n    assert expected == json.loads(response["SecretValue"])\n```\n\nthe value is returned as the expected JSON string.\n\n### inject_string\n\nThis modifier is used to inject the string contents of another file into the value\nof the associated attribute.\n\n```yaml\nclients:\n  s3:\n    get_object:\n      Body: !lobotomy.inject_string \'./body.txt\'\n```\n\nHere the `!lobotomy.inject_string` YAML modifier instructs lobotomy to load the\ncontents of the external file `./body.txt` into the `Body` attribute value where\nthe external file path is defined relative to the defining YAML file.\n\nSo in this case, if there\'s a `body.txt` file with the contents `Hello lobotomy!`,\nthe Python test would find this in reading the body:\n\n```python\nimport boto3\nimport lobotomy\n\n\n@lobotomy.patch(path="scenario.yaml")\ndef test_showing_inject_string(lobotomized: lobotomy.Lobotomy):\n    """Should expect the body.txt to be injected into the Body response attribute."""\n    client = boto3.Session().client("s3")\n    response = client.get_object(Bucket="fake", Key="fake")\n    assert response["Body"].read() == b"Hello lobotomy!"\n```\n\n## Special Cases\n\nThe following are special case operations that are handled slightly differently than\nthe regular lobotomy behaviors.\n\n### s3.download_fileobj\n\nThis is a special action that is not actually included in the botocore API. It\'s a\nconvenience wrapper that has the special behavior of written data to a byte-buffer\nobject instead of returning a value. To handle that, the `"Fileobj"` response should\nbe populated with either a string or a bytes object. Lobotomy will internally handle\nwriting that to the specified buffer like this:\n\n```python\nimport boto3\nimport lobotomy\nimport io\n\n\n@lobotomy.Patch()\ndef test_s3_download_fileobj(lobotomized: lobotomy.Lobotomy):\n    """Should write s3.download_fileobj outputs to the Fileobj argument buffer."""\n    lobotomized.add_call("s3", "download_fileobj", {"Fileobj": "Hello World!"})\n    client = boto3.Session().client("s3")\n\n    data = io.BytesIO()\n    client.download_fileobj(Bucket="bar", Key="baz", Fileobj=data)\n\n    call = lobotomized.get_service_calls("s3", "download_fileobj")[0]\n    assert call.request["Fileobj"] == data\n    assert call.request["Bucket"] == "bar"\n    assert call.request["Key"] == "baz"\n    assert data.getvalue().decode() == "Hello World!"\n```\n\n## Command Line Interface\n\nThe lobotomy library also has a command line interface to help streamline\nthe process of creating configuration files. The CLI has an `add` command\nthat can be used to auto-generate method call response configurations to\na new or existing configuration file. The values are meant to be replaced\nand unused keys to be removed to streamline for testing, but it helps a lot\nto get the full structure of the response in place and work from there instead\nof having to look it up yourself.\n\nFor example, creating the file:\n\n```yaml\nclients:\n  sts:\n    get_caller_identity:\n      Account: \'987654321\'\n  s3:\n    get_object:\n    - Body: \'The contents of my S3 file.\'\n      LastModified: \'2020-11-01T12:23:34Z\'\n    - Body: \'Another S3 file.\'\n      LastModified: \'2020-11-03T12:23:34Z\'\n```\n\ncould be done first through the CLI commands:\n\n```shell script\n$ lobotomy add sts.get_caller_identity example.yaml\n``` \n\nAfter that command is executed, the `example.yaml` file will be\ncreated and populated initially with:\n\n```yaml\nclients:\n  sts:\n    get_caller_identity:\n      Account: \'...\'\n      Arn: \'...\'\n      UserId: \'...\'\n```\n\nNotice the values are placeholders. We can adjust the values to what we want\nand remove the unnecessary keys for our particular case such that the file\ncontents are then:\n\n```yaml\nclients:\n  sts:\n    get_caller_identity:\n      Account: \'987654321\'\n```\n\nNext add the first `s3.get_object` call:\n\n```shell script\n$ lobotomy add s3.get_object example.yaml\n```\n\nThe configuration file now looks like:\n\n```yaml\nclients:\n  s3:\n    get_object:\n      AcceptRanges: \'...\'\n      Body: \'...\'\n      CacheControl: \'...\'\n      ContentDisposition: \'...\'\n      ContentEncoding: \'...\'\n      ContentLanguage: \'...\'\n      ContentLength: 1\n      ContentRange: \'...\'\n      ContentType: \'...\'\n      DeleteMarker: null\n      ETag: \'...\'\n      Expiration: \'...\'\n      Expires: \'2020-11-04T14:37:18.042821Z\'\n      LastModified: \'2020-11-04T14:37:18.042821Z\'\n      Metadata: null\n      MissingMeta: 1\n      ObjectLockLegalHoldStatus: \'...\'\n      ObjectLockMode: \'...\'\n      ObjectLockRetainUntilDate: \'2020-11-04T14:37:18.042821Z\'\n      PartsCount: 1\n      ReplicationStatus: \'...\'\n      RequestCharged: \'...\'\n      Restore: \'...\'\n      SSECustomerAlgorithm: \'...\'\n      SSECustomerKeyMD5: \'...\'\n      SSEKMSKeyId: \'...\'\n      ServerSideEncryption: \'...\'\n      StorageClass: \'...\'\n      TagCount: 1\n      VersionId: \'...\'\n      WebsiteRedirectLocation: \'...\'\n  sts:\n    get_caller_identity:\n      Account: \'987654321\'\n```\n\nOf course, this is a simple case because we don\'t need much of the response\nstructure in our simplified use-case, but hopefully you can see the value\nof being able to add the response structure so easily for more complex cases.\nOnce again, the new call is adjusted to fit our particular needs:\n\n```yaml\nclients:\n  s3:\n    get_object:\n      Body: \'The contents of my S3 file.\'\n      LastModified: \'2020-11-01T12:23:34Z\'\n  sts:\n    get_caller_identity:\n      Account: \'987654321\'\n```\n\nAdding the second `s3.get_object` call is identical:\n\n```shell script\n$ lobotomy add s3.get_object example.yaml\n```\n\nHowever, lobotomy notices the existing call there and so converts the\n`get_object` response configuration to a list of responses for you:\n\n```yaml\nclients:\n  s3:\n    Body: \'The contents of my S3 file.\'\n    LastModified: \'2020-11-01T12:23:34Z\'\n    get_object:\n      AcceptRanges: \'...\'\n      Body: \'...\'\n      CacheControl: \'...\'\n      ContentDisposition: \'...\'\n      ContentEncoding: \'...\'\n      ContentLanguage: \'...\'\n      ContentLength: 1\n      ContentRange: \'...\'\n      ContentType: \'...\'\n      DeleteMarker: null\n      ETag: \'...\'\n      Expiration: \'...\'\n      Expires: \'2020-11-04T14:42:51.077364Z\'\n      LastModified: \'2020-11-04T14:42:51.077364Z\'\n      Metadata: null\n      MissingMeta: 1\n      ObjectLockLegalHoldStatus: \'...\'\n      ObjectLockMode: \'...\'\n      ObjectLockRetainUntilDate: \'2020-11-04T14:42:51.077364Z\'\n      PartsCount: 1\n      ReplicationStatus: \'...\'\n      RequestCharged: \'...\'\n      Restore: \'...\'\n      SSECustomerAlgorithm: \'...\'\n      SSECustomerKeyMD5: \'...\'\n      SSEKMSKeyId: \'...\'\n      ServerSideEncryption: \'...\'\n      StorageClass: \'...\'\n      TagCount: 1\n      VersionId: \'...\'\n      WebsiteRedirectLocation: \'...\'\n  sts:\n    get_caller_identity:\n      Account: \'987654321\'\n```\n\nFinally, edit the new call response configuration and we end up with the\nconfiguration we were looking for:\n\n```yaml\nclients:\n  sts:\n    get_caller_identity:\n      Account: \'987654321\'\n  s3:\n    get_object:\n    - Body: \'The contents of my S3 file.\'\n      LastModified: \'2020-11-01T12:23:34Z\'\n    - Body: \'Another S3 file.\'\n      LastModified: \'2020-11-03T12:23:34Z\'\n```\n\n# Advanced Usage\n\n## Key Prefixes\n\nBy default configuration files are rooted at the `clients` key within the\nfile. However, it is possible to specify a different root key prefix, which\nis useful when co-locating lobotomy test configuration with other test\nconfiguration data in the same file, or when co-locating multiple lobotomy\ntest configurations within the same file. To achieve that a prefix must be\nspecified during patching.\n\nAs an example, consider the configuration file:\n\n```yaml\nlobotomy:\n  test_a:\n    clients:\n      sts:\n        get_caller_identity:\n          Account: \'987654321\'\n  test_b:\n    clients:\n      sts:\n        get_caller_identity:\n          Account: \'123456678\'\n          UserId: \'AIFASDJWISJAVHXME\'\n```\n\nIn this case the prefixes are `lobotomy.test_a` and `lobotomy.test_b`.\nTo use these in a test the *prefix* must be specified in the patch:\n\n```python\nimport pathlib\nimport lobotomy\n\nconfig_path = pathlib.Path(__file__).parent.joinpath("validation.yaml")\n\n\n@lobotomy.patch(config_path, prefix="lobotomy.test_a")\ndef test_a(lobotomized: lobotomy.Lobotomy):\n    """..."""\n\n\n@lobotomy.patch(config_path, prefix="lobotomy.test_b")\ndef test_b(lobotomized: lobotomy.Lobotomy):\n    """..."""\n```\n\nThe prefix can be specified as a `.` delimited string, or as a list/tuple.\nThe list/tuple is needed if the keys themselves contains `.`.\n\n## Patching Targets\n\nBy default, lobotomy will patch `boto3.Session`. There are scenarios where\na different patch would be desired either to limit the scope of the patch or\nto patch another library wrapping the `boto3.Session` call. In those cases,\nspecify the patch path argument:\n\n```python\nimport pathlib\nimport lobotomy\n\nconfig_path = pathlib.Path(__file__).parent.joinpath("test.yaml")\n\n\n@lobotomy.patch(config_path, patch_path="something.else.Session")\ndef test_another_patch_path(lobotomized: lobotomy.Lobotomy):\n    """..."""\n```\n\n## Session Configuration\n\nIn addition to the clients configurations described above, it is also possible\nto configure the session values as well with the `session:` attribute. The\navailable configuration settings for the session are:\n\n```yaml\nsession:\n  profile_name: some-profile\n  region_name: us-west-2\n  available_profiles:\n    - some-profile\n    - some-other-profile\n  credentials:\n    access_key: A123KEY\n    secret_key: somesecretkeyvalue\n    token: theaccesstokenifset\n    method: how-the-credentials-were-loaded\n\nclients:\n  ...\n```\n\nAll of these values are optional and in cases where they are omitted, but the session\nrequires having them, they will be defaulted.\n\n\n## Client Overrides\n\nThere are cases where it is desirable to replace one or more service clients with\nnon-lobotomy objects. Lobotomy supports that by adding client overrides to the patched\nlobotomy object:\n\n```python\nfrom unittest.mock import MagicMock\n\nimport lobotomy\n\n\n@lobotomy.patch()\ndef test_example(lobotomized: lobotomy.Lobotomy):\n    """Should do something..."""\n    mock_dynamo_db_client = MagicMock()\n    lobotomized.add_client_override("dynamodb", mock_dynamo_db_client)\n    # continue testing...\n```\n\n\n## Error Handling\n\nThe lobotomy library mimics client error handling with a lobotomized version of the\nsame interface used by the live clients. As such, handling and capturing errors works\ntransparently. For example,\n\n```python\nimport boto3\nimport pytest\n\nimport lobotomy\n\n\n@lobotomy.patch()\ndef test_client_errors(lobotomized: lobotomy.Lobotomy):\n    """Should raise the specified error."""\n    lobotomized.add_error_call(\n        service_name="s3",\n        method_name="list_objects",\n        error_code="NoSuchBucket", \n        error_message="Hello...",\n    )\n\n    session = boto3.Session()\n    client = session.client("s3")\n\n    with pytest.raises(client.exceptions.NoSuchBucket):\n        client.list_objects(Bucket="foo")\n```\n\nOr the generic client error handling with response codes can be used as well:\n\n\n```python\nimport boto3\nimport pytest\n\nimport lobotomy\n\n\n@lobotomy.patch()\ndef test_client_errors(lobotomized: lobotomy.Lobotomy):\n    """Should raise the specified error."""\n    lobotomized.add_call(\n        service_name="s3",\n        method_name="list_objects",\n        error_code="NoSuchBucket", \n        error_message="Hello...",\n    )\n\n    session = boto3.Session()\n    client = session.client("s3")\n\n    with pytest.raises(lobotomy.ClientError) as exception_info:\n        client.list_objects(Bucket="foo")\n\n    assert exception_info.value.response["Error"]["Code"] == "NoSuchBucket"\n```\n\nThese errors can also be specified in YAML files like this:\n\n```yaml\nclients:\n  s3:\n    list_objects: !lobotomy.error\n      code: NoSuchBucket\n      message: Hello...\n```\n\nThe `!lobotomy.error` will load this as an error response with the same behavior as the\nexamples shown above.\n\n## Callable Responses\n\nFor more advanced cases it is also possible to use any callable as the source for\ngenerating the response. For example, if I want the response to be different based\non the arguments specified when making the call, I can create a function to return\na result that reflects the inputs.\n\n```python\nimport typing\n\nimport boto3\n\nimport lobotomy\n\n\ndef _respond(*args, **kwargs) -> typing.Dict[str, typing.Any]:\n    return {\n        "Body": "{}.{}".format(kwargs["Bucket"], kwargs["Key"]).encode()\n    }\n\n\n@lobotomy.patch()\ndef test_callable_responses(lobotomized: "lobotomy.Lobotomy"):\n    """Should return the expected body value from the callable response."""\n    lobotomized.add_call("s3", "get_object", _respond)\n\n    session = boto3.Session()\n    client = session.client("s3")\n\n    response = client.get_object(Bucket="foo", Key="bar")\n    assert response["Body"].read() == b"foo.bar"\n```\n\nAny callable is supported as long as it accepts the `*args, **kwargs` appropriate to\nthe calling client service request.\n',
    'author': 'Scott Ernst',
    'author_email': 'swernst@gmail.com',
    'maintainer': None,
    'maintainer_email': None,
    'url': 'https://gitlab.com/rocket-boosters/lobotomy',
    'packages': packages,
    'package_data': package_data,
    'install_requires': install_requires,
    'entry_points': entry_points,
    'python_requires': '>=3.8.0,<4.0.0',
}


setup(**setup_kwargs)
