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

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

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

install_requires = \
['PyYAML>=5.3.1,<6.0.0',
 'boto3>=1.16.0,<2.0.0',
 'python-dateutil>=2.8.1,<3.0.0',
 'toml>=0.10.1,<0.11.0']

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

setup_kwargs = {
    'name': 'lobotomy',
    'version': '0.1.2',
    'description': 'Boto3 low-level client mocking library.',
    'long_description': '# Lobotomy\n\n[![PyPI version](https://badge.fury.io/py/lobotomy.svg)](https://badge.fury.io/py/lobotomy)\n[![build status](https://gitlab.com/rocket-boosters/lobotomy/badges/master/build.svg)](https://gitlab.com/rocket-boosters/lobotomy/commits/main)\n[![coverage report](https://gitlab.com/rocket-boosters/lobotomy/badges/master/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\n- [Installation](#installation)\n- [Tl;dr Usage](#tldr-usage)\n- [Usage](#usage)\n    - [Configuration Files](#configuration-files)\n    - [Test Patching](#test-patching)\n    - [Command Line Interface](#command-line-interface)\n- [Advanced Usage](#advanced-usage)\n    - [Key Prefixes](#key-prefixes)\n    - [Patching Targets](#patching-targets)\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        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\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\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',
    '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.1,<4.0.0',
}


setup(**setup_kwargs)
