├── .github └── workflows │ └── ci.yml ├── .gitignore ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── Makefile ├── README.md ├── localstack_client ├── __init__.py ├── config.py ├── patch.py └── session.py ├── setup.cfg ├── setup.py └── tests ├── client ├── __init__.py ├── conftest.py ├── test_patches.py └── test_python_client.py └── test_config.py /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: LocalStack Python Client CI 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | workflow_dispatch: 9 | 10 | jobs: 11 | test: 12 | runs-on: ubuntu-latest 13 | strategy: 14 | fail-fast: false 15 | matrix: 16 | python-version: 17 | - "3.13" 18 | - "3.12" 19 | - "3.11" 20 | - "3.10" 21 | - "3.9" 22 | - "3.8" 23 | 24 | steps: 25 | - uses: actions/checkout@v2 26 | 27 | - name: Set up Python ${{ matrix.python-version }} 28 | uses: actions/setup-python@v2 29 | with: 30 | python-version: ${{ matrix.python-version }} 31 | 32 | - name: Upgrade pip version 33 | run: python3 -m pip install --upgrade pip 34 | 35 | - name: Install Dependencies 36 | run: make install 37 | 38 | - name: Lint 39 | run: make lint 40 | 41 | - name: Test 42 | run: make test 43 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.log 3 | /nosetests.xml 4 | /.venv/ 5 | .settings/ 6 | .project 7 | .classpath 8 | /.coverage 9 | .DS_Store 10 | /build/ 11 | dist/ 12 | *.egg-info/ 13 | .eggs/ 14 | *.sw* 15 | ~* 16 | *~ 17 | 18 | .idea/ 19 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # LocalStack Python Client Change Log 2 | 3 | * v2.10: Remove endpoints for 'bedrock-runtime' and 'textract' because overriding them is not supported by the AWS Terraform provider 4 | * v2.9: Add endpoints for Account Management, Private Certificate Authority, Bedrock, CloudControl, CodeBuild, CodeCommit, CodeConnections, CodeDeploy, CodePipeline, ElasticTranscoder, MemoryDB, Shield, Textract and Verified Permissions 5 | * v2.8: Removes support for python `3.6` and `3.7` and adds `3.12` and `3.13` for parity with boto3 6 | * v2.7: Add endpoint config for EventBridge Pipes 7 | * v2.6: Add endpoint config for Pinpoint 8 | * v2.5: Add endpoint config for AppConfig Data 9 | * v2.4: Add endpoint config for Resource Access Manager 10 | * v2.3: Add endpoint config for Amazon EventBridge Scheduler 11 | * v2.2: Add endpoint configs for `emr-serverless` and a few other services 12 | * v2.1: Consider `AWS_ENDPOINT_URL` configuration when resolving service endpoints 13 | * v2.0: Change `LOCALSTACK_HOSTNAME` from `` to `:`; remove `EDGE_PORT` environment variable 14 | * v1.39: Add endpoint for Amazon MQ 15 | * v1.38: Add `enable_local_endpoints()` util function; slight project refactoring, migrate from `nose` to `pytests` 16 | * v1.37: Add endpoint for Amazon Transcribe 17 | * v1.36: Add endpoints for Fault Injection Service (FIS) and Marketplace Metering 18 | * v1.35: Add endpoint for Amazon Managed Workflows for Apache Airflow (MWAA) 19 | * v1.33: Patch botocore to skip adding `data-` host prefixes to endpoint URLs; remove six dependency 20 | * v1.32: Add endpoint for KinesisAnalyticsV2 21 | * v1.31: Revert mapping for OpenSearch (drop support for `OPENSEARCH_ENDPOINT_STRATEGY=off`) 22 | * v1.30: Allow legacy port handling for OpenSearch (to support `OPENSEARCH_ENDPOINT_STRATEGY=off`) 23 | * v1.29: Add endpoint for OpenSearch 24 | * v1.28: Add endpoint for Route53Resolver 25 | * v1.27: Add endpoint for SESv2 26 | * v1.25: Remove mapping for deprecated/disabled Web UI on port 8080 27 | * v1.24: Add endpoints for Config Service 28 | * v1.23: Add endpoints for QLDB Session 29 | * v1.22: Add endpoints for LakeFormation and WAF/WAFv2 30 | * v1.21: Add endpoint for AWS Backup API 31 | * v1.20: Add endpoint for Resource Groups API 32 | * v1.19: Add endpoints for Resource Groups Tagging API 33 | * v1.18: Add endpoints for AppConfig, CostExplorer, MediaConvert 34 | * v1.17: Add endpoint for ServerlessApplicationRepository 35 | * v1.16: Add endpoints for AWS Support and ServiceDiscovery (CloudMap) 36 | * v1.14: Add endpoint for IoT Wireless 37 | * v1.13: Add endpoints for NeptuneDB and DocumentDB 38 | * v1.10: Add endpoint for ELBv2 39 | * v1.7: Add endpoints for AWS API GW Management, Timestream, S3 Control, and others 40 | * v1.5: Add endpoint for AWS Application Autoscaling, Kafka (MSK) 41 | * v1.4: Configure USE_LEGACY_PORTS=0 by default to accommodate upstream changes 42 | * v1.2: Add endpoint for AWS Amplify 43 | * v1.1: Add USE_LEGACY_PORTS config to disable using legacy ports 44 | * v1.0: Switch to using edge port for all service endpoints by default 45 | * v0.25: Add endpoint for AWS Kinesis Analytics; prepare for replacing service ports with edge port 46 | * v0.24: Add endpoints for AWS Transfer, ACM, and CodeCommit 47 | * v0.23: Add endpoints for AWS Autoscaling and MediaStore 48 | * v0.22: Import boto3 under different name to simplify mocking 49 | * v0.20: Add endpoints for AWS CloudTrail, Glacier, Batch, Organizations 50 | * v0.19: Add endpoints for AWS ECR and QLDB 51 | * v0.18: Add endpoint for AWS API Gateway V2 52 | * v0.16: Add endpoint for AWS SageMaker 53 | * v0.15: Add endpoint for AWS Glue 54 | * v0.14: Add endpoint for AWS Athena 55 | * v0.13: Add endpoint for AWS CloudFront 56 | * v0.8: Add more service endpoint mappings that will be implemented in the near future 57 | * v0.7: Add endpoint for AWS Step Functions 58 | * v0.6: Add endpoint for AWS Secrets Manager 59 | * v0.5: Fix passing of credentials to client session 60 | * v0.4: Add functions to retrieve service port mappings 61 | * v0.3: Add new service endpoints 62 | * v0.2: Add missing service endpoints; enable SSL connections; put default endpoints into `config.py` 63 | * v0.1: Initial version 64 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | We welcome feedback, bug reports, and pull requests! 4 | 5 | For pull requests, please stick to the following guidelines: 6 | 7 | - Add tests for any new features and bug fixes. 8 | - Follow the existing code style. Run `make lint` before checking in your code. 9 | - Put a reasonable amount of comments into the code. 10 | - Fork `localstack-python-client` on your GitHub user account, do your changes there and then create a PR against main `localstack-python-client` repository. 11 | - Separate unrelated changes into multiple pull requests. 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | VENV_DIR ?= .venv 2 | VENV_RUN = . $(VENV_DIR)/bin/activate 3 | PIP_CMD ?= pip 4 | BUILD_DIR ?= dist 5 | 6 | usage: ## Show this help 7 | @fgrep -h "##" $(MAKEFILE_LIST) | fgrep -v fgrep | sed -e 's/\\$$//' | sed -e 's/##//' 8 | 9 | install: ## Install dependencies in local virtualenv folder 10 | (test `which virtualenv` || $(PIP_CMD) install --user virtualenv) && \ 11 | (test -e $(VENV_DIR) || virtualenv $(VENV_OPTS) $(VENV_DIR)) && \ 12 | ($(VENV_RUN) && $(PIP_CMD) install --upgrade pip) && \ 13 | (test ! -e setup.cfg || ($(VENV_RUN); $(PIP_CMD) install .[test])) 14 | 15 | publish: ## Publish the library to the central PyPi repository 16 | # build and upload archive 17 | $(VENV_RUN); ./setup.py sdist && twine upload $(BUILD_DIR)/*.tar.gz 18 | 19 | test: ## Run automated tests 20 | ($(VENV_RUN); test `which localstack` || pip install .[test]) && \ 21 | $(VENV_RUN); DEBUG=$(DEBUG) PYTHONPATH=. pytest -sv $(PYTEST_ARGS) tests 22 | 23 | lint: ## Run code linter to check code style 24 | $(VENV_RUN); flake8 --ignore=E501 localstack_client tests 25 | 26 | format: ## Run code formatter (black) 27 | $(VENV_RUN); black localstack_client tests; isort localstack_client tests 28 | 29 | clean: ## Clean up virtualenv 30 | rm -rf $(VENV_DIR) 31 | 32 | .PHONY: usage install clean publish test lint format 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # LocalStack Python Client 2 | 3 |

4 | PyPI version 5 | license Apache 2.0 6 | GitHub-Actions-Build 7 | PyPi downloads 8 |

9 | 10 | This is an easy-to-use Python client for [LocalStack](https://github.com/localstack/localstack). 11 | The client library provides a thin wrapper around [boto3](https://github.com/boto/boto3) which 12 | automatically configures the target endpoints to use LocalStack for your local cloud 13 | application development. 14 | 15 | ## Prerequisites 16 | 17 | To make use of this library, you need to have [LocalStack](https://github.com/localstack/localstack) installed on your local machine. In particular, the `localstack` command needs to be available. 18 | 19 | ## Installation 20 | 21 | The easiest way to install *LocalStack* is via `pip`: 22 | 23 | ``` 24 | pip install localstack-client 25 | ``` 26 | 27 | ## Usage 28 | 29 | This library provides an API that is identical to `boto3`'s. A minimal way to try it out is to replace `import boto3` with `import localstack_client.session as boto3`. This will allow your boto3 calls to work as normal. 30 | 31 | For example, to list all s3 buckets in localstack: 32 | 33 | ```python 34 | import localstack_client.session as boto3 35 | client = boto3.client('s3') 36 | response = client.list_buckets() 37 | ``` 38 | 39 | Another example below shows using `localstack_client` directly. To list the SQS queues 40 | in your local (LocalStack) environment, use the following code: 41 | 42 | ```python 43 | import localstack_client.session 44 | 45 | session = localstack_client.session.Session() 46 | sqs = session.client('sqs') 47 | assert sqs.list_queues() is not None 48 | ``` 49 | 50 | If you use `boto3.client` directly in your code, you can mock it. 51 | 52 | ```python 53 | import localstack_client.session 54 | import pytest 55 | 56 | 57 | @pytest.fixture(autouse=True) 58 | def boto3_localstack_patch(monkeypatch): 59 | session_ls = localstack_client.session.Session() 60 | monkeypatch.setattr(boto3, "client", session_ls.client) 61 | monkeypatch.setattr(boto3, "resource", session_ls.resource) 62 | ``` 63 | 64 | ```python 65 | sqs = boto3.client('sqs') 66 | assert sqs.list_queues() is not None # list SQS in localstack 67 | ``` 68 | 69 | ## Configuration 70 | 71 | You can use the following environment variables for configuration: 72 | 73 | * `AWS_ENDPOINT_URL`: The endpoint URL to connect to (takes precedence over `USE_SSL`/`LOCALSTACK_HOST` below) 74 | * `LOCALSTACK_HOST` (deprecated): A `:` variable defining where to find LocalStack (default: `localhost:4566`). 75 | * `USE_SSL` (deprecated): Whether to use SSL when connecting to LocalStack (default: `False`). 76 | 77 | ### Enabling Transparent Local Endpoints 78 | 79 | The library contains a small `enable_local_endpoints()` util function that can be used to transparently run all `boto3` requests against the local endpoints. 80 | 81 | The following sample illustrates how it can be used - after calling `enable_local_endpoints()`, the S3 `ListBuckets` call will be run against LocalStack, even though we're using the default boto3 module. 82 | ``` 83 | import boto3 84 | from localstack_client.patch import enable_local_endpoints() 85 | enable_local_endpoints() 86 | # the call below will automatically target the LocalStack endpoints 87 | buckets = boto3.client("s3").list_buckets() 88 | ``` 89 | 90 | The patch can also be unapplied by calling `disable_local_endpoints()`: 91 | ``` 92 | from localstack_client.patch import disable_local_endpoints() 93 | disable_local_endpoints() 94 | # the call below will target the real AWS cloud again 95 | buckets = boto3.client("s3").list_buckets() 96 | ``` 97 | 98 | ## Contributing 99 | 100 | If you are interested in contributing to LocalStack Python Client, start by reading our [`CONTRIBUTING.md`](CONTRIBUTING.md) guide. You can further navigate our codebase and [open issues](https://github.com/localstack/localstack-python-client/issues). We are thankful for all the contributions and feedback we receive. 101 | 102 | ## Changelog 103 | 104 | Please refer to [`CHANGELOG.md`](CHANGELOG.md) to see the complete list of changes for each release. 105 | 106 | ## License 107 | 108 | The LocalStack Python Client is released under the Apache License, Version 2.0 (see `LICENSE`). 109 | -------------------------------------------------------------------------------- /localstack_client/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/localstack/localstack-python-client/041fdfb02d62f5a5fbabe313f80b6d94ec65c811/localstack_client/__init__.py -------------------------------------------------------------------------------- /localstack_client/config.py: -------------------------------------------------------------------------------- 1 | import os 2 | from typing import Dict, Optional, Tuple 3 | from urllib.parse import urlparse 4 | 5 | # note: leave this import here for now, as some upstream code is depending on it (TODO needs to be updated) 6 | from localstack_client.patch import patch_expand_host_prefix # noqa 7 | 8 | # central entrypoint port for all LocalStack API endpoints 9 | DEFAULT_EDGE_PORT = 4566 10 | 11 | # TODO: deprecated, remove! 12 | # NOTE: The ports listed below will soon become deprecated/removed, as the default in the 13 | # latest version is to access all services via a single "edge service" (port 4566 by default) 14 | _service_ports: Dict[str, int] = { 15 | "edge": 4566, 16 | # Botocore services 17 | # When adding new services below, assign them port 4566 18 | "account": 4566, 19 | "acm": 4619, 20 | "acm-pca": 4566, 21 | "amplify": 4622, 22 | "apigateway": 4567, 23 | "apigatewaymanagementapi": 4625, 24 | "apigatewayv2": 4567, 25 | "appconfig": 4632, 26 | "appconfigdata": 4632, 27 | "appflow": 4566, 28 | "application-autoscaling": 4623, 29 | "appsync": 4605, 30 | "athena": 4607, 31 | "autoscaling": 4616, 32 | "backup": 4638, 33 | "batch": 4614, 34 | "bedrock": 4566, 35 | "ce": 4633, 36 | "cloudcontrol": 4566, 37 | "cloudformation": 4581, 38 | "cloudfront": 4606, 39 | "cloudsearch": 4595, 40 | "cloudtrail": 4612, 41 | "cloudwatch": 4582, 42 | "codebuild": 4566, 43 | "codecommit": 4620, 44 | "codeconnections": 4566, 45 | "codedeploy": 4566, 46 | "codepipeline": 4566, 47 | "cognito-identity": 4591, 48 | "cognito-idp": 4590, 49 | "config": 4641, 50 | "configservice": 4641, 51 | "docdb": 4594, 52 | "dynamodb": 4569, 53 | "dynamodbstreams": 4570, 54 | "ec2": 4597, 55 | "ecr": 4610, 56 | "ecs": 4601, 57 | "efs": 4637, 58 | "eks": 4602, 59 | "elasticache": 4598, 60 | "elasticbeanstalk": 4604, 61 | "elasticsearch": 4571, 62 | "elastictranscoder": 4566, 63 | "elb": 4588, 64 | "elbv2": 4628, 65 | "emr": 4600, 66 | "emr-serverless": 4566, 67 | "es": 4578, 68 | "events": 4587, 69 | "firehose": 4573, 70 | "fis": 4643, 71 | "glacier": 4613, 72 | "glue": 4608, 73 | "iam": 4593, 74 | "iot": 4589, 75 | "iotanalytics": 4589, 76 | "iot-data": 4589, 77 | "iotevents": 4589, 78 | "iotevents-data": 4589, 79 | "iot-jobs-data": 4589, 80 | "iotwireless": 4589, 81 | "kafka": 4624, 82 | "keyspaces": 4566, 83 | "kinesis": 4568, 84 | "kinesisanalytics": 4621, 85 | "kinesisanalyticsv2": 4621, 86 | "kms": 4599, 87 | "lakeformation": 4639, 88 | "lambda": 4574, 89 | "logs": 4586, 90 | "mediaconvert": 4634, 91 | "mediastore": 4617, 92 | "mediastore-data": 4617, 93 | "meteringmarketplace": 4644, 94 | "memorydb": 4566, 95 | "mq": 4566, 96 | "mwaa": 4642, 97 | "neptune": 4594, 98 | "opensearch": 4578, 99 | "organizations": 4615, 100 | "pinpoint": 4566, 101 | "pipes": 4566, 102 | "qldb": 4611, 103 | "qldb-session": 4611, 104 | "ram": 4566, 105 | "rds": 4594, 106 | "rds-data": 4594, 107 | "redshift": 4577, 108 | "redshift-data": 4577, 109 | "resource-groups": 4636, 110 | "resourcegroupstaggingapi": 4635, 111 | "route53": 4580, 112 | "route53domains": 4566, 113 | "route53resolver": 4580, 114 | "s3": 4572, 115 | "s3control": 4627, 116 | "sagemaker": 4609, 117 | "sagemaker-runtime": 4609, 118 | "scheduler": 4566, 119 | "secretsmanager": 4584, 120 | "serverlessrepo": 4631, 121 | "servicediscovery": 4630, 122 | "ses": 4579, 123 | "sesv2": 4579, 124 | "shield": 4566, 125 | "sns": 4575, 126 | "sqs": 4576, 127 | "ssm": 4583, 128 | "stepfunctions": 4585, 129 | "sts": 4592, 130 | "support": 4629, 131 | "swf": 4596, 132 | "timestream": 4626, 133 | "timestream-query": 4626, 134 | "timestream-write": 4626, 135 | "transcribe": 4566, 136 | "transfer": 4618, 137 | "verifiedpermissions": 4566, 138 | "waf": 4640, 139 | "wafv2": 4640, 140 | "xray": 4603, 141 | } 142 | 143 | # TODO remove service port mapping above entirely 144 | if os.environ.get("USE_LEGACY_PORTS") not in ["1", "true"]: 145 | for key, value in _service_ports.items(): 146 | if key not in ["dashboard", "elasticsearch"]: 147 | _service_ports[key] = DEFAULT_EDGE_PORT 148 | 149 | 150 | def parse_localstack_host(given: str) -> Tuple[str, int]: 151 | parts = given.split(":", 1) 152 | if len(parts) == 1: 153 | # just hostname 154 | return parts[0].strip() or "localhost", DEFAULT_EDGE_PORT 155 | elif len(parts) == 2: 156 | hostname = parts[0].strip() or "localhost" 157 | port_s = parts[1] 158 | try: 159 | port = int(port_s) 160 | return (hostname, port) 161 | except Exception: 162 | raise RuntimeError(f"could not parse {given} into :") 163 | else: 164 | raise RuntimeError(f"could not parse {given} into :") 165 | 166 | 167 | def get_service_endpoints(localstack_host: Optional[str] = None) -> Dict[str, str]: 168 | """ 169 | Return the local endpoint URLs for the list of supported boto3 services (e.g., "s3", "lambda", etc). 170 | If $AWS_ENDPOINT_URL is configured in the environment, it is returned directly. Otherwise, 171 | the service endpoint is constructed from the dict of service ports (usually http://localhost:4566). 172 | """ 173 | env_endpoint_url = os.environ.get("AWS_ENDPOINT_URL", "").strip() 174 | if env_endpoint_url: 175 | return {key: env_endpoint_url for key in _service_ports.keys()} 176 | 177 | if localstack_host is None: 178 | localstack_host = os.environ.get( 179 | "LOCALSTACK_HOST", f"localhost:{DEFAULT_EDGE_PORT}" 180 | ) 181 | 182 | hostname, port = parse_localstack_host(localstack_host) 183 | 184 | protocol = "https" if os.environ.get("USE_SSL") in ("1", "true") else "http" 185 | 186 | return {key: f"{protocol}://{hostname}:{port}" for key in _service_ports.keys()} 187 | 188 | 189 | def get_service_endpoint( 190 | service: str, localstack_host: Optional[str] = None 191 | ) -> Optional[str]: 192 | endpoints = get_service_endpoints(localstack_host=localstack_host) 193 | return endpoints.get(service) 194 | 195 | 196 | def get_service_port(service: str) -> Optional[int]: 197 | ports = get_service_ports() 198 | return ports.get(service) 199 | 200 | 201 | def get_service_ports() -> Dict[str, int]: 202 | endpoints = get_service_endpoints() 203 | result = {} 204 | for service, url in endpoints.items(): 205 | result[service] = urlparse(url).port 206 | return result 207 | -------------------------------------------------------------------------------- /localstack_client/patch.py: -------------------------------------------------------------------------------- 1 | import types 2 | 3 | import boto3 4 | from boto3.session import Session 5 | from botocore.serialize import Serializer 6 | 7 | _state = {} 8 | 9 | DEFAULT_ACCESS_KEY_ID = "test" 10 | DEFAULT_SECRET_ACCESS_KEY = "test" 11 | 12 | 13 | def enable_local_endpoints(): 14 | """Patch the boto3 library to transparently use the LocalStack endpoints by default.""" 15 | from localstack_client.config import get_service_endpoint 16 | 17 | def _add_custom_kwargs( 18 | kwargs, 19 | service_name, 20 | endpoint_url=None, 21 | aws_access_key_id=None, 22 | aws_secret_access_key=None, 23 | ): 24 | kwargs["endpoint_url"] = endpoint_url or get_service_endpoint(service_name) 25 | kwargs["aws_access_key_id"] = aws_access_key_id or DEFAULT_ACCESS_KEY_ID 26 | kwargs["aws_secret_access_key"] = ( 27 | aws_secret_access_key or DEFAULT_SECRET_ACCESS_KEY 28 | ) 29 | 30 | def _client( 31 | self, 32 | service_name, 33 | region_name=None, 34 | api_version=None, 35 | use_ssl=True, 36 | verify=None, 37 | endpoint_url=None, 38 | aws_access_key_id=None, 39 | aws_secret_access_key=None, 40 | **kwargs, 41 | ): 42 | _add_custom_kwargs( 43 | kwargs, 44 | service_name, 45 | endpoint_url=endpoint_url, 46 | aws_access_key_id=aws_access_key_id, 47 | aws_secret_access_key=aws_secret_access_key, 48 | ) 49 | return _client_orig( 50 | self, 51 | service_name, 52 | region_name=region_name, 53 | api_version=api_version, 54 | use_ssl=use_ssl, 55 | verify=verify, 56 | **kwargs, 57 | ) 58 | 59 | def _resource( 60 | self, 61 | service_name, 62 | region_name=None, 63 | api_version=None, 64 | use_ssl=True, 65 | verify=None, 66 | endpoint_url=None, 67 | aws_access_key_id=None, 68 | aws_secret_access_key=None, 69 | **kwargs, 70 | ): 71 | _add_custom_kwargs( 72 | kwargs, 73 | service_name, 74 | endpoint_url=endpoint_url, 75 | aws_access_key_id=aws_access_key_id, 76 | aws_secret_access_key=aws_secret_access_key, 77 | ) 78 | return _resource_orig( 79 | self, 80 | service_name, 81 | region_name=region_name, 82 | api_version=api_version, 83 | use_ssl=use_ssl, 84 | verify=verify, 85 | **kwargs, 86 | ) 87 | 88 | if _state.get("_client_orig"): 89 | # patch already applied -> return 90 | return 91 | 92 | # patch boto3 default session (if available) 93 | try: 94 | session = boto3._get_default_session() 95 | _state["_default_client_orig"] = session.client 96 | session.client = types.MethodType(_client, session) 97 | _state["_default_resource_orig"] = session.resource 98 | session.resource = types.MethodType(_resource, session) 99 | except Exception: 100 | # swallowing for now - looks like the default session is not available (yet) 101 | pass 102 | 103 | # patch session.client(..) 104 | _client_orig = Session.client 105 | _state["_client_orig"] = _client_orig 106 | Session.client = _client 107 | 108 | # patch session.resource(..) 109 | _resource_orig = Session.resource 110 | _state["_resource_orig"] = _resource_orig 111 | Session.resource = _resource 112 | 113 | 114 | def disable_local_endpoints(): 115 | """Disable the boto3 patches and revert to using the default endpoints against real AWS.""" 116 | 117 | _client = _state.pop("_client_orig", None) 118 | if _client: 119 | Session.client = _client 120 | _resource = _state.pop("_resource_orig", None) 121 | if _resource: 122 | Session.resource = _resource 123 | 124 | # undo patches for boto3 default session 125 | try: 126 | session = boto3._get_default_session() 127 | if _state.get("_default_client_orig"): 128 | session.client = _state["_default_client_orig"] 129 | if _state.get("_default_resource_orig"): 130 | session.resource = _state["_default_resource_orig"] 131 | except Exception: 132 | pass 133 | 134 | 135 | def patch_expand_host_prefix(): 136 | """Apply a patch to botocore, to skip adding host prefixes to endpoint URLs""" 137 | 138 | def _expand_host_prefix(self, parameters, operation_model, *args, **kwargs): 139 | result = _expand_host_prefix_orig( 140 | self, parameters, operation_model, *args, **kwargs 141 | ) 142 | # skip adding host prefixes, to avoid making requests to, e.g., http://data-localhost:4566 143 | is_sd = operation_model.service_model.service_name == "servicediscovery" 144 | if is_sd and result == "data-": 145 | return None 146 | if operation_model.service_model.service_name == "mwaa" and result == "api.": 147 | return None 148 | return result 149 | 150 | _expand_host_prefix_orig = Serializer._expand_host_prefix 151 | Serializer._expand_host_prefix = _expand_host_prefix 152 | -------------------------------------------------------------------------------- /localstack_client/session.py: -------------------------------------------------------------------------------- 1 | from boto3 import client as boto3_client 2 | from boto3 import resource as boto3_resource 3 | from botocore.credentials import Credentials 4 | 5 | from localstack_client import config 6 | 7 | DEFAULT_SESSION = None 8 | 9 | 10 | class Session(object): 11 | """ 12 | This is a custom LocalStack session used to 13 | emulate the boto3.session object. 14 | """ 15 | 16 | def __init__( 17 | self, 18 | aws_access_key_id="accesskey", 19 | aws_secret_access_key="secretkey", 20 | aws_session_token="token", 21 | region_name="us-east-1", 22 | botocore_session=None, 23 | profile_name=None, 24 | localstack_host=None, 25 | ): 26 | self.env = "local" 27 | self.aws_access_key_id = aws_access_key_id 28 | self.aws_secret_access_key = aws_secret_access_key 29 | self.aws_session_token = aws_session_token 30 | self.region_name = region_name 31 | self._service_endpoint_mapping = config.get_service_endpoints(localstack_host) 32 | 33 | self.common_protected_kwargs = { 34 | "aws_access_key_id": self.aws_access_key_id, 35 | "aws_secret_access_key": self.aws_secret_access_key, 36 | "region_name": self.region_name, 37 | "verify": False, 38 | } 39 | 40 | def get_credentials(self): 41 | """ 42 | Returns botocore.credential.Credential object. 43 | """ 44 | return Credentials( 45 | access_key=self.aws_access_key_id, 46 | secret_key=self.aws_secret_access_key, 47 | token=self.aws_session_token, 48 | ) 49 | 50 | def client(self, service_name, **kwargs): 51 | """ 52 | Mock boto3 client 53 | If **kwargs are provided they will passed through to boto3.client unless already contained 54 | within protected_kwargs which are set with priority 55 | Returns boto3.resources.factory.s3.ServiceClient object 56 | """ 57 | if service_name not in self._service_endpoint_mapping: 58 | raise Exception( 59 | "%s is not supported by this mock session." % (service_name) 60 | ) 61 | 62 | protected_kwargs = { 63 | **self.common_protected_kwargs, 64 | "service_name": service_name, 65 | "endpoint_url": self._service_endpoint_mapping[service_name], 66 | } 67 | 68 | return boto3_client(**{**kwargs, **protected_kwargs}) 69 | 70 | def resource(self, service_name, **kwargs): 71 | """ 72 | Mock boto3 resource 73 | If **kwargs are provided they will passed through to boto3.client unless already contained 74 | within overwrite_kwargs which are set with priority 75 | Returns boto3.resources.factory.s3.ServiceResource object 76 | """ 77 | if service_name not in self._service_endpoint_mapping: 78 | raise Exception( 79 | "%s is not supported by this mock session." % (service_name) 80 | ) 81 | 82 | protected_kwargs = { 83 | **self.common_protected_kwargs, 84 | "service_name": service_name, 85 | "endpoint_url": self._service_endpoint_mapping[service_name], 86 | } 87 | 88 | return boto3_resource(**{**kwargs, **protected_kwargs}) 89 | 90 | 91 | def _get_default_session(): 92 | global DEFAULT_SESSION 93 | 94 | if DEFAULT_SESSION is None: 95 | DEFAULT_SESSION = Session() 96 | 97 | return DEFAULT_SESSION 98 | 99 | 100 | def client(*args, **kwargs): 101 | if kwargs: 102 | return Session(**kwargs).client(*args, **kwargs) 103 | return _get_default_session().client(*args, **kwargs) 104 | 105 | 106 | def resource(*args, **kwargs): 107 | if kwargs: 108 | return Session(**kwargs).resource(*args, **kwargs) 109 | return _get_default_session().resource(*args, **kwargs) 110 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | name = localstack-client 3 | version = 2.10 4 | url = https://github.com/localstack/localstack-python-client 5 | author = LocalStack Team 6 | author_email = info@localstack.cloud 7 | description = A lightweight Python client for LocalStack. 8 | license = Apache License 2.0 9 | classifiers = 10 | Programming Language :: Python :: 3 11 | Programming Language :: Python :: 3.8 12 | Programming Language :: Python :: 3.9 13 | Programming Language :: Python :: 3.10 14 | Programming Language :: Python :: 3.11 15 | Programming Language :: Python :: 3.12 16 | Programming Language :: Python :: 3.13 17 | License :: OSI Approved :: Apache Software License 18 | Topic :: Software Development :: Testing 19 | 20 | [options] 21 | packages = 22 | localstack_client 23 | 24 | install_requires = 25 | boto3 26 | 27 | [options.extras_require] 28 | # Dependencies to run the tests 29 | test = 30 | black 31 | coverage 32 | flake8 33 | isort 34 | localstack 35 | pytest 36 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from setuptools import setup 4 | setup() 5 | -------------------------------------------------------------------------------- /tests/client/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/localstack/localstack-python-client/041fdfb02d62f5a5fbabe313f80b6d94ec65c811/tests/client/__init__.py -------------------------------------------------------------------------------- /tests/client/conftest.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | 3 | import pytest 4 | 5 | 6 | @pytest.fixture(scope="session", autouse=True) 7 | def startup_localstack(): 8 | subprocess.check_output(["localstack", "start", "-d"]) 9 | subprocess.check_output(["localstack", "wait"]) 10 | 11 | yield 12 | 13 | subprocess.check_output(["localstack", "stop"]) 14 | -------------------------------------------------------------------------------- /tests/client/test_patches.py: -------------------------------------------------------------------------------- 1 | import uuid 2 | 3 | import boto3 4 | import pytest 5 | 6 | from localstack_client.patch import (disable_local_endpoints, 7 | enable_local_endpoints) 8 | 9 | 10 | def test_enable_local_endpoints(monkeypatch): 11 | monkeypatch.setenv("AWS_DEFAULT_REGION", "us-east-1") 12 | 13 | # create default client, requests should fail 14 | with pytest.raises(Exception): 15 | boto3.client("s3").list_buckets() 16 | with pytest.raises(Exception): 17 | resource = boto3.resource("s3") 18 | bucket_name = str(uuid.uuid4()) 19 | resource.Bucket(bucket_name).create() 20 | 21 | # enable local endpoints, request should pass 22 | enable_local_endpoints() 23 | assert "Buckets" in boto3.client("s3").list_buckets() 24 | resource = boto3.resource("s3") 25 | bucket_name = str(uuid.uuid4()) 26 | resource.Bucket(bucket_name).create() 27 | resource.Bucket(bucket_name).delete() 28 | 29 | # disable local endpoints again, request should fail 30 | disable_local_endpoints() 31 | with pytest.raises(Exception): 32 | boto3.client("s3").list_buckets() 33 | with pytest.raises(Exception): 34 | resource = boto3.resource("s3") 35 | bucket_name = str(uuid.uuid4()) 36 | resource.Bucket(bucket_name).create() 37 | -------------------------------------------------------------------------------- /tests/client/test_python_client.py: -------------------------------------------------------------------------------- 1 | from botocore.client import Config 2 | 3 | import localstack_client.session 4 | 5 | 6 | def test_session(): 7 | session = localstack_client.session.Session() 8 | sqs = session.client("sqs") 9 | assert sqs.list_queues() is not None 10 | 11 | 12 | def test_client_kwargs_passed(): 13 | """Test kwargs passed through to boto3.client creation""" 14 | session = localstack_client.session.Session() 15 | kwargs = {"config": Config(signature_version="s3v4")} 16 | sqs = session.client("sqs", **kwargs) 17 | assert sqs.meta.config.signature_version == "s3v4" 18 | 19 | 20 | def test_protected_client_kwargs_not_passed(): 21 | """Test protected kwargs not overwritten in boto3.client creation""" 22 | session = localstack_client.session.Session() 23 | kwargs = {"region_name": "another_region"} 24 | sqs = session.client("sqs", **kwargs) 25 | assert not sqs.meta.region_name == "another_region" 26 | 27 | 28 | def test_resource_kwargs_passed(): 29 | """Test kwargs passed through to boto3.resource creation""" 30 | session = localstack_client.session.Session() 31 | kwargs = {"config": Config(signature_version="s3v4")} 32 | sqs = session.resource("sqs", **kwargs) 33 | assert sqs.meta.client.meta.config.signature_version == "s3v4" 34 | 35 | 36 | def test_protected_resource_kwargs_not_passed(): 37 | """Test protected kwargs not overwritten in boto3.resource creation""" 38 | session = localstack_client.session.Session() 39 | kwargs = {"region_name": "another_region"} 40 | sqs = session.resource("sqs", **kwargs) 41 | assert not sqs.meta.client.meta.region_name == "another_region" 42 | -------------------------------------------------------------------------------- /tests/test_config.py: -------------------------------------------------------------------------------- 1 | from localstack_client import config 2 | 3 | 4 | def test_default_endpoint(): 5 | assert config.get_service_endpoint("sqs") == "http://localhost:4566" 6 | 7 | 8 | def test_with_localstack_host(monkeypatch): 9 | monkeypatch.setenv("LOCALSTACK_HOST", "foobar:9999") 10 | assert config.get_service_endpoint("sqs") == "http://foobar:9999" 11 | 12 | 13 | def test_without_port(monkeypatch): 14 | monkeypatch.setenv("LOCALSTACK_HOST", "foobar") 15 | assert config.get_service_endpoint("sqs") == "http://foobar:4566" 16 | 17 | 18 | def test_without_host(monkeypatch): 19 | monkeypatch.setenv("LOCALSTACK_HOST", ":4566") 20 | assert config.get_service_endpoint("sqs") == "http://localhost:4566" 21 | --------------------------------------------------------------------------------