localstack-sdk-python 0.0.2__tar.gz → 0.0.4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. {localstack_sdk_python-0.0.2 → localstack_sdk_python-0.0.4}/PKG-INFO +7 -5
  2. {localstack_sdk_python-0.0.2 → localstack_sdk_python-0.0.4}/README.md +6 -4
  3. localstack_sdk_python-0.0.4/localstack-sdk-python/localstack/sdk/aws/__init__.py +3 -0
  4. localstack_sdk_python-0.0.4/localstack-sdk-python/localstack/sdk/aws/client.py +186 -0
  5. localstack_sdk_python-0.0.4/localstack-sdk-python/localstack/sdk/chaos/client.py +69 -0
  6. {localstack_sdk_python-0.0.2 → localstack_sdk_python-0.0.4}/localstack-sdk-python/localstack/sdk/chaos/managers.py +5 -0
  7. localstack_sdk_python-0.0.4/localstack-sdk-python/localstack/sdk/clients.py +32 -0
  8. localstack_sdk_python-0.0.4/localstack-sdk-python/localstack/sdk/pods/client.py +115 -0
  9. localstack_sdk_python-0.0.4/localstack-sdk-python/localstack/sdk/pods/exceptions.py +18 -0
  10. localstack_sdk_python-0.0.4/localstack-sdk-python/localstack/sdk/state/__init__.py +3 -0
  11. localstack_sdk_python-0.0.4/localstack-sdk-python/localstack/sdk/state/client.py +19 -0
  12. localstack_sdk_python-0.0.4/localstack-sdk-python/localstack/sdk/testing/__init__.py +3 -0
  13. localstack_sdk_python-0.0.4/localstack-sdk-python/localstack/sdk/testing/decorators.py +31 -0
  14. localstack_sdk_python-0.0.4/localstack-sdk-python/localstack/sdk/testing/pytest/__init__.py +0 -0
  15. localstack_sdk_python-0.0.4/localstack-sdk-python/localstack/sdk/testing/pytest/plugins.py +11 -0
  16. {localstack_sdk_python-0.0.2 → localstack_sdk_python-0.0.4}/localstack-sdk-python/localstack_sdk_python.egg-info/PKG-INFO +7 -5
  17. {localstack_sdk_python-0.0.2 → localstack_sdk_python-0.0.4}/localstack-sdk-python/localstack_sdk_python.egg-info/SOURCES.txt +11 -1
  18. localstack_sdk_python-0.0.4/localstack-sdk-python/localstack_sdk_python.egg-info/entry_points.txt +2 -0
  19. {localstack_sdk_python-0.0.2 → localstack_sdk_python-0.0.4}/pyproject.toml +8 -3
  20. localstack_sdk_python-0.0.2/localstack-sdk-python/localstack/clients.py +0 -18
  21. localstack_sdk_python-0.0.2/localstack-sdk-python/localstack/sdk/chaos/client.py +0 -39
  22. localstack_sdk_python-0.0.2/localstack-sdk-python/localstack/sdk/pods/client.py +0 -77
  23. {localstack_sdk_python-0.0.2 → localstack_sdk_python-0.0.4}/LICENSE.txt +0 -0
  24. {localstack_sdk_python-0.0.2 → localstack_sdk_python-0.0.4}/localstack-sdk-python/localstack/sdk/chaos/__init__.py +0 -0
  25. {localstack_sdk_python-0.0.2 → localstack_sdk_python-0.0.4}/localstack-sdk-python/localstack/sdk/pods/__init__.py +0 -0
  26. {localstack_sdk_python-0.0.2 → localstack_sdk_python-0.0.4}/localstack-sdk-python/localstack_sdk_python.egg-info/dependency_links.txt +0 -0
  27. {localstack_sdk_python-0.0.2 → localstack_sdk_python-0.0.4}/localstack-sdk-python/localstack_sdk_python.egg-info/requires.txt +0 -0
  28. {localstack_sdk_python-0.0.2 → localstack_sdk_python-0.0.4}/localstack-sdk-python/localstack_sdk_python.egg-info/top_level.txt +0 -0
  29. {localstack_sdk_python-0.0.2 → localstack_sdk_python-0.0.4}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: localstack-sdk-python
3
- Version: 0.0.2
3
+ Version: 0.0.4
4
4
  Summary: Python SDK for LocalStack
5
5
  Author-email: LocalStack Team <info@localstack.cloud>
6
6
  Project-URL: Homepage, https://localstack.cloud
@@ -12,23 +12,25 @@ License-File: LICENSE.txt
12
12
  Requires-Dist: localstack-sdk-generated
13
13
 
14
14
  # LocalStack Python SDK
15
+ [![PyPI version](https://img.shields.io/pypi/v/localstack-sdk-python)](https://pypi.org/project/localstack-sdk-python/)
15
16
 
16
17
  This is the Python SDK for LocalStack.
17
18
  LocalStack offers a number of developer endpoints (see [docs](https://docs.localstack.cloud/references/internal-endpoints/)).
18
19
  This SDK provides a programmatic and easy way to interact with them.
19
20
 
20
21
  > [!WARNING]
21
- > This project is still in a preview phase, and will be subject to fast and breaking changes.
22
+ > This project is still in a preview phase and will be subject to fast and breaking changes.
22
23
 
23
24
  ### Project Structure
24
25
 
25
26
  This project follows the following structure:
26
27
 
27
- - `packages/localstack-sdk-generated` is a python project generated from the OpenAPI specs with [openapi-generator](https://github.com/OpenAPITools/openapi-generator).
28
- - `localstack-sdk-python` is the main project that has `localstack-sdk-generated` has the main dependency.
28
+ - `packages/localstack-sdk-generated` is a Python project generated from the OpenAPI specs with [openapi-generator](https://github.com/OpenAPITools/openapi-generator).
29
+ LocalStack's OpenAPI specs are available in the [openapi repository](https://github.com/localstack/openapi).
30
+ - `localstack-sdk-python` is the main project that has `localstack-sdk-generated` as the main dependency.
29
31
 
30
32
  Developers are not supposed to modify at all `localstack-sdk-generated`.
31
- The code needs to be every time re-generated from specs using the `generate.sh` script in the `bin` folder.
33
+ The code needs to be re-generated from specs every time using the `generate.sh` script in the `bin` folder.
32
34
 
33
35
  This project uses [uv](https://github.com/astral-sh/uv) as package/project manager.
34
36
 
@@ -1,21 +1,23 @@
1
1
  # LocalStack Python SDK
2
+ [![PyPI version](https://img.shields.io/pypi/v/localstack-sdk-python)](https://pypi.org/project/localstack-sdk-python/)
2
3
 
3
4
  This is the Python SDK for LocalStack.
4
5
  LocalStack offers a number of developer endpoints (see [docs](https://docs.localstack.cloud/references/internal-endpoints/)).
5
6
  This SDK provides a programmatic and easy way to interact with them.
6
7
 
7
8
  > [!WARNING]
8
- > This project is still in a preview phase, and will be subject to fast and breaking changes.
9
+ > This project is still in a preview phase and will be subject to fast and breaking changes.
9
10
 
10
11
  ### Project Structure
11
12
 
12
13
  This project follows the following structure:
13
14
 
14
- - `packages/localstack-sdk-generated` is a python project generated from the OpenAPI specs with [openapi-generator](https://github.com/OpenAPITools/openapi-generator).
15
- - `localstack-sdk-python` is the main project that has `localstack-sdk-generated` has the main dependency.
15
+ - `packages/localstack-sdk-generated` is a Python project generated from the OpenAPI specs with [openapi-generator](https://github.com/OpenAPITools/openapi-generator).
16
+ LocalStack's OpenAPI specs are available in the [openapi repository](https://github.com/localstack/openapi).
17
+ - `localstack-sdk-python` is the main project that has `localstack-sdk-generated` as the main dependency.
16
18
 
17
19
  Developers are not supposed to modify at all `localstack-sdk-generated`.
18
- The code needs to be every time re-generated from specs using the `generate.sh` script in the `bin` folder.
20
+ The code needs to be re-generated from specs every time using the `generate.sh` script in the `bin` folder.
19
21
 
20
22
  This project uses [uv](https://github.com/astral-sh/uv) as package/project manager.
21
23
 
@@ -0,0 +1,3 @@
1
+ from localstack.sdk.aws.client import AWSClient
2
+
3
+ __all__ = ["AWSClient"]
@@ -0,0 +1,186 @@
1
+ import json
2
+
3
+ from localstack.sdk.api.aws_api import AwsApi
4
+ from localstack.sdk.clients import BaseClient
5
+ from localstack.sdk.models import (
6
+ Message,
7
+ SesSentEmail,
8
+ SNSPlatformEndpointResponse,
9
+ SNSSMSMessagesResponse,
10
+ )
11
+
12
+
13
+ def _from_sqs_query_to_json(xml_dict: dict) -> list[Message]:
14
+ """
15
+ todo: developer endpoint implements sqs-query protocol. Remove this workaround one we move them to json.
16
+ """
17
+ raw_messages = (
18
+ xml_dict.get("ReceiveMessageResponse", {}).get("ReceiveMessageResult", {}) or {}
19
+ ).get("Message", [])
20
+ if isinstance(raw_messages, dict):
21
+ raw_messages = [raw_messages]
22
+ messages = []
23
+ for msg in raw_messages:
24
+ _attributes = msg.get("Attribute", [])
25
+ attributes = {i["Name"]: i["Value"] for i in _attributes}
26
+ _m = {
27
+ "MessageId": msg.get("MessageId"),
28
+ "ReceiptHandle": msg.get("ReceiptHandle"),
29
+ "MD5OfBody": msg.get("MD5OfBody"),
30
+ "Body": msg.get("Body"),
31
+ "Attributes": attributes,
32
+ }
33
+ m = Message.from_dict(_m)
34
+ messages.append(m)
35
+ return messages
36
+
37
+
38
+ class AWSClient(BaseClient):
39
+ """
40
+ The client to interact with all the LocalStack's AWS endpoints.
41
+ These endpoints offer specific features in addition to the ones offered by the AWS services. For instance,
42
+ access all the messages withing a SQS without the side effect of deleting them.
43
+ """
44
+
45
+ def __init__(self, **kwargs) -> None:
46
+ super().__init__(**kwargs)
47
+ self._client = AwsApi(self._api_client)
48
+
49
+ ########
50
+ # SQS
51
+ ########
52
+
53
+ def list_sqs_messages(self, account_id: str, region: str, queue_name: str) -> list[Message]:
54
+ """
55
+ Lists all the SQS messages in a given queue in a specific account id and region, without any side effect.
56
+
57
+ :param account_id: the account id of the queue
58
+ :param region: the region of the queue
59
+ :param queue_name: the name of the queue
60
+ :return: the list of messages in the queue
61
+ """
62
+ response = self._client.list_sqs_messages_with_http_info(
63
+ account_id=account_id, region=region, queue_name=queue_name
64
+ )
65
+ return _from_sqs_query_to_json(json.loads(response.raw_data))
66
+
67
+ def list_sqs_messages_from_queue_url(self, queue_url: str) -> list[Message]:
68
+ """
69
+ Lists all the SQS messages in a given queue, without any side effect.
70
+
71
+ :param queue_url: the URL of the queue
72
+ :return: the list of messages in the queue
73
+ """
74
+ response = self._client.list_all_sqs_messages_with_http_info(queue_url=queue_url)
75
+ return _from_sqs_query_to_json(json.loads(response.raw_data))
76
+
77
+ ########
78
+ # SES
79
+ ########
80
+
81
+ def get_ses_messages(
82
+ self, id_filter: str | None = None, email_filter: str | None = None
83
+ ) -> list[SesSentEmail]:
84
+ """
85
+ Returns all the in-memory saved SES messages. They can be filtered by message ID and/or message source.
86
+
87
+ :param id_filter: the message id used as filter for the SES messages
88
+ :param email_filter: the message source filter
89
+ :return: a list of email sent with SES
90
+ """
91
+ response = self._client.get_ses_messages(id=id_filter, email=email_filter)
92
+ return response.messages
93
+
94
+ def discard_ses_messages(self, id_filter: str | None = None) -> None:
95
+ """
96
+ Clears all SES messages. An ID filter can be provided to delete only a specific message.
97
+
98
+ :param id_filter: the id filter
99
+ :return: None
100
+ """
101
+ return self._client.discard_ses_messages(id=id_filter)
102
+
103
+ ########
104
+ # SNS
105
+ ########
106
+
107
+ def get_sns_sms_messages(
108
+ self,
109
+ phone_number: str | None = None,
110
+ account_id: str = "000000000000",
111
+ region: str = "us-east-1",
112
+ ) -> SNSSMSMessagesResponse:
113
+ """
114
+ Returns all SMS messages published to a phone number.
115
+
116
+ :param phone_number: the phone number to which the messages have been published. If not specified, all messages
117
+ are returned.
118
+ :param account_id: the AWS Account ID from which the messages have been published. '000000000000' by default
119
+ :param region: the AWS region from which the messages have been published. us-east-1 by default
120
+ :return:
121
+ """
122
+ return self._client.get_sns_sms_messages(
123
+ phone_number=phone_number, account_id=account_id, region=region
124
+ )
125
+
126
+ def discard_sns_sms_messages(
127
+ self,
128
+ phone_number: str | None = None,
129
+ account_id: str = "000000000000",
130
+ region: str = "us-east-1",
131
+ ) -> None:
132
+ """
133
+ Discards all SMS messages published to a phone number.
134
+
135
+ :param phone_number: the phone number to which the messages have been published. If not specified, all messages
136
+ are deleted.
137
+ :param account_id: the AWS Account ID from which the messages have been published. '000000000000' by default
138
+ :param region: the AWS region from which the messages have been published. us-east-1 by default
139
+ :return: None
140
+ """
141
+ return self._client.discard_sns_sms_messages(
142
+ phone_number=phone_number, account_id=account_id, region=region
143
+ )
144
+
145
+ def get_sns_endpoint_messages(
146
+ self,
147
+ endpoint_arn: str | None = None,
148
+ account_id: str = "000000000000",
149
+ region: str = "us-east-1",
150
+ ) -> SNSPlatformEndpointResponse:
151
+ """
152
+ Returns all the messages published to a platform endpoint.
153
+
154
+ :param endpoint_arn: the ARN to which the messages have been published. If not specified, will return all the
155
+ messages.
156
+ :param account_id: the AWS Account ID from which the messages have been published. 000000000000 if not specified
157
+ :param region: the AWS region from which the messages have been published. us-east-1 by default
158
+ :return: a response with the list of messages and the queried region
159
+ """
160
+ return self._client.get_sns_endpoint_messages(
161
+ endpoint_arn=endpoint_arn, account_id=account_id, region=region
162
+ )
163
+
164
+ def discard_sns_endpoint_messages(
165
+ self,
166
+ endpoint_arn: str | None = None,
167
+ account_id: str = "000000000000",
168
+ region: str = "us-east-1",
169
+ ) -> None:
170
+ """
171
+ Discards all the messaged published to a platform endpoint.
172
+
173
+ :param endpoint_arn: the ARN to which the messages have been published. If not specified, will discard all the
174
+ messages.
175
+ :param account_id: the AWS Account ID from which the messages have been published. 000000000000 if not specified
176
+ :param region: the AWS region from which the messages have been published. us-east-1 by default
177
+ :return: None
178
+ """
179
+ return self._client.discard_sns_endpoint_messages(
180
+ endpoint_arn=endpoint_arn, account_id=account_id, region=region
181
+ )
182
+
183
+
184
+ def get_default(**args) -> AwsApi:
185
+ """Return a client with a default configuration"""
186
+ return AwsApi(args)
@@ -0,0 +1,69 @@
1
+ from localstack.sdk.api.chaos_api import ChaosApi
2
+ from localstack.sdk.clients import BaseClient
3
+ from localstack.sdk.models import FaultRule, NetworkEffectsConfig
4
+
5
+
6
+ class ChaosClient(BaseClient):
7
+ """
8
+ The client to interact with the LocalStack's Chaos API.
9
+ """
10
+
11
+ def __init__(self, **kwargs) -> None:
12
+ super().__init__(**kwargs)
13
+ self._client = ChaosApi(self._api_client)
14
+
15
+ def set_fault_rules(self, fault_rules: list[FaultRule]) -> list[FaultRule]:
16
+ """
17
+ Creates a new sets of fault rules. It overwrites the previous ones.
18
+ :param fault_rules: the list of FaultRule we want to set
19
+ :return: the list of FaultRule currently in place
20
+ """
21
+ return self._client.set_fault_rules(fault_rule=fault_rules)
22
+
23
+ def add_fault_rules(self, fault_rules: list[FaultRule]) -> list[FaultRule]:
24
+ """
25
+ Adds a new set of rules to the current fault configuration.
26
+ :param fault_rules: the FaultRule rules to add
27
+ :return: the list of FaultRule currently in place
28
+ """
29
+ return self._client.add_fault_rules(fault_rule=fault_rules)
30
+
31
+ def delete_fault_rules(self, fault_rules: list[FaultRule]) -> list[FaultRule]:
32
+ """
33
+ Deletes a set of rules from the fault configuration.
34
+ :param fault_rules: the FaultRule to delete
35
+ :return: the list of FaultRule currently in place
36
+ """
37
+ return self._client.delete_fault_rules(fault_rule=fault_rules)
38
+
39
+ def get_fault_rules(self) -> list[FaultRule]:
40
+ """
41
+ Gets the current fault configuration.
42
+ :return: the list of FaultRule of the current configuration
43
+ """
44
+ return self._client.get_fault_rules()
45
+
46
+ def get_network_effects(self) -> NetworkEffectsConfig:
47
+ """
48
+ Gets the current network effect configuration.
49
+ :return: the current NetworkEffectsConfig
50
+ """
51
+ return self._client.get_network_effects()
52
+
53
+ def set_network_effects(
54
+ self, network_effects_config: NetworkEffectsConfig
55
+ ) -> NetworkEffectsConfig:
56
+ """
57
+ Configure a new network effect, e.g, latency.
58
+ :param network_effects_config: the network config to be set
59
+ :return: the current configuration of network effects
60
+ """
61
+ return self._client.set_network_effects(network_effects_config=network_effects_config)
62
+
63
+
64
+ def get_default(**args) -> ChaosClient:
65
+ """
66
+ Return a chaos client with a default configuration. You can pass a host argument to overwrite the fault one
67
+ (http://localhost.localstack.cloud:4566).
68
+ """
69
+ return ChaosClient(**args)
@@ -6,6 +6,11 @@ from localstack.sdk.models import FaultRule
6
6
 
7
7
  @contextmanager
8
8
  def fault_configuration(fault_rules: list[FaultRule]):
9
+ """
10
+ This is a context manager that temporarily applies a given set of fault rules.
11
+ :param fault_rules: a list of FaultRule to be applied.
12
+ :return: None
13
+ """
9
14
  client = get_default()
10
15
  try:
11
16
  client.set_fault_rules(fault_rules=fault_rules)
@@ -0,0 +1,32 @@
1
+ from localstack.sdk.api_client import ApiClient
2
+ from localstack.sdk.configuration import Configuration
3
+
4
+
5
+ class BaseClient:
6
+ """
7
+ A BaseClient creates a configuration and instantiate a ApiClient, which is a generic OpenAPI client automatically
8
+ generated by the openapitools/openapi-generator tool.
9
+ """
10
+
11
+ configuration: Configuration
12
+ """The configuration for the base client"""
13
+ _api_client: ApiClient
14
+ """The OpenAPI client"""
15
+ host: str
16
+ """The LocalStack host (http://localhost.localstack.cloud:4566 by default)"""
17
+ auth_token: str | None
18
+ """A client can be injected with a LocalStack auth token. If not provided, the one used to start up the
19
+ LocalStack instance will be used (for the features needing it, e.g., Cloud Pods)."""
20
+
21
+ def __init__(self, host: str | None = None, auth_token: str | None = None, **kwargs) -> None:
22
+ """
23
+ Initializes a base client to interact with LocalStack developer endpoint.
24
+ :param host: the host, http://localhost.localstack.cloud:4566 by default.
25
+ :param auth_token: if provided, this token would be used for authentication against platform. It not, the
26
+ LocalStack runtime will use the one used to start the container. The token used determines the Cloud
27
+ Pods identity, i.e., which pods are available.
28
+ """
29
+ self.host = host or "http://localhost.localstack.cloud:4566"
30
+ self.auth_token = auth_token
31
+ self.configuration = Configuration(host=self.host)
32
+ self._api_client = ApiClient(configuration=self.configuration)
@@ -0,0 +1,115 @@
1
+ import base64
2
+ import json
3
+
4
+ from localstack.sdk.api import PodsApi
5
+ from localstack.sdk.clients import BaseClient
6
+ from localstack.sdk.models import PodList, PodSaveRequest, RemoteConfig
7
+ from localstack.sdk.pods.exceptions import PodLoadException, PodSaveException
8
+
9
+
10
+ def _empty_remote_config() -> RemoteConfig:
11
+ return RemoteConfig(oneof_schema_1_validator={}, actual_instance={})
12
+
13
+
14
+ def _read_ndjson(raw_content: bytes) -> list[dict]:
15
+ """
16
+ Reads the byte content of a ndjson response into a list of dictionaries.
17
+ :param raw_content: the byte content of a ndjson response.
18
+ :return: a list of dicts.
19
+ """
20
+ ndjson_str = raw_content.decode("utf-8")
21
+ return [json.loads(line) for line in ndjson_str.splitlines()]
22
+
23
+
24
+ def _get_completion_event(streamed_response: list[dict]) -> dict | None:
25
+ """
26
+ Parses the chucked response returned form the Cloud Pod save and load endpoints and return the completion event,
27
+ i.e., the one summarizing the output (success or error) of the operation.
28
+ :param streamed_response: a list of dictionaries for the chunked response.
29
+ :return: the dictionary of the completion event, if found. None otherwise.
30
+ """
31
+ completion_events = [line for line in streamed_response if line.get("event") == "completion"]
32
+ return completion_events[0] if completion_events else None
33
+
34
+
35
+ class PodsClient(BaseClient):
36
+ """
37
+ The client to interact with the Cloud Pod feature.
38
+ """
39
+
40
+ def __init__(self, **args) -> None:
41
+ """
42
+ Initializes a Cloud Pods client.
43
+ :param auth_token: if provided, this token will be used for platform authentication. If not, it will be
44
+ fetched from within the container itself.
45
+ """
46
+ super().__init__(**args)
47
+ self._client = PodsApi(self._api_client)
48
+ if self.auth_token:
49
+ # If an auth token is provided, it will be used to authenticate platform calls for Cloud Pods.
50
+ # Only the pods tied to this token will be visible. If not provided, the token will be fetched from the
51
+ # container. This allows to separate container identity for caller identity, if needed.
52
+ auth_header = get_platform_auth_header(self.auth_token)
53
+ self._api_client.set_default_header("Authorization", auth_header["Authorization"])
54
+
55
+ def save_pod(self, pod_name: str) -> None:
56
+ """
57
+ Saves the state in the LocalStack container into a Cloud Pod and uploads it to the LocalStack's platform.
58
+ If a Cloud Pod with the given name already exists, a new version is created.
59
+ :param pod_name: the name of the Cloud Pod to be saved.
60
+ :return: None
61
+ :raises PodSaveException: if the save operation returns an error
62
+ """
63
+ response = self._client.save_pod_with_http_info(
64
+ name=pod_name, pod_save_request=PodSaveRequest()
65
+ )
66
+ if response.status_code != 200:
67
+ raise PodSaveException(pod_name=pod_name)
68
+ streamed_response = _read_ndjson(response.raw_data)
69
+ completion_event = _get_completion_event(streamed_response)
70
+ if completion_event["status"] == "error":
71
+ raise PodSaveException(pod_name=pod_name, error=completion_event.get("message"))
72
+
73
+ def load_pod(self, pod_name: str) -> None:
74
+ """
75
+ Loads a Cloud Pod into the LocalStack container.
76
+ :param pod_name: the name of the Cloud Pod to load
77
+ :return: None
78
+ :raises PodLoadException: if the load operation returns an error
79
+ """
80
+ response = self._client.load_pod_with_http_info(
81
+ name=pod_name, remote_config=_empty_remote_config()
82
+ )
83
+ if response.status_code != 200:
84
+ raise PodLoadException(pod_name=pod_name)
85
+ streamed_response = _read_ndjson(response.raw_data)
86
+ completion_event = _get_completion_event(streamed_response)
87
+ if completion_event["status"] == "error":
88
+ raise PodLoadException(pod_name=pod_name, error=completion_event.get("message"))
89
+
90
+ def delete_pod(self, pod_name: str) -> None:
91
+ """
92
+ Deletes a Cloud Pod.
93
+ :param pod_name: the name of the Cloud Pod to be deleted.
94
+ :return: None
95
+ """
96
+ return self._client.delete_pod(name=pod_name, remote_config=_empty_remote_config())
97
+
98
+ def list_pods(self) -> PodList:
99
+ """
100
+ Returns the list of the Cloud Pods visible in the organization.
101
+ :return: a PodList object
102
+ """
103
+ pods = self._client.list_pods(remote_config=_empty_remote_config())
104
+ return pods
105
+
106
+
107
+ def get_platform_auth_header(token: str) -> dict[str, str]:
108
+ """
109
+ Given the auth token, crafts the authorization header to authenticate platform calls.
110
+ :param token: the localstack auth token
111
+ :return: a dictionary for the authorization header
112
+ """
113
+ _token = f":{token}"
114
+ auth_encoded = base64.b64encode(_token.encode("utf-8")).decode("utf-8")
115
+ return {"Authorization": f"Basic {auth_encoded}"}
@@ -0,0 +1,18 @@
1
+ class PodSaveException(Exception):
2
+ message = "An error occurred while saving the Cloud Pod"
3
+
4
+ def __init__(self, pod_name: str, error: str | None = None) -> None:
5
+ _message = f"{self.message} '{pod_name}'"
6
+ if error:
7
+ _message += f": {error}"
8
+ super().__init__(_message)
9
+
10
+
11
+ class PodLoadException(Exception):
12
+ message = "An error occurred while loading the Cloud Pod"
13
+
14
+ def __init__(self, pod_name: str, error: str | None = None) -> None:
15
+ _message = f"{self.message} '{pod_name}'"
16
+ if error:
17
+ _message += f": {error}"
18
+ super().__init__(_message)
@@ -0,0 +1,3 @@
1
+ from localstack.sdk.state.client import StateClient
2
+
3
+ __all__ = ["StateClient"]
@@ -0,0 +1,19 @@
1
+ from localstack.sdk.api import StateApi
2
+ from localstack.sdk.clients import BaseClient
3
+
4
+
5
+ class StateClient(BaseClient):
6
+ """
7
+ Initializes a client to handle LocalStack's state.
8
+ """
9
+
10
+ def __init__(self, **args) -> None:
11
+ super().__init__(**args)
12
+ self._client = StateApi(self._api_client)
13
+
14
+ def reset_state(self) -> None:
15
+ """
16
+ Resets the state of LocalStack for all running services.
17
+ :return: None
18
+ """
19
+ self._client.localstack_state_reset_post()
@@ -0,0 +1,3 @@
1
+ from localstack.sdk.testing.decorators import cloudpods
2
+
3
+ __all__ = ["cloudpods"]
@@ -0,0 +1,31 @@
1
+ import logging
2
+ from functools import wraps
3
+
4
+ from localstack.sdk.pods import PodsClient
5
+ from localstack.sdk.state import StateClient
6
+
7
+ LOG = logging.getLogger(__name__)
8
+
9
+
10
+ def cloudpods(*args, **kwargs):
11
+ """This is a decorator that loads a cloud pod before a test and resets the state afterward."""
12
+
13
+ def decorator(func):
14
+ @wraps(func)
15
+ def wrapper(*test_args, **test_kwargs):
16
+ if not (pod_name := kwargs.get("name")):
17
+ raise Exception("Specify a Cloud Pod name in the `name` arg")
18
+ pods_client = PodsClient()
19
+ LOG.debug("Loading %s", pod_name)
20
+ pods_client.load_pod(pod_name=pod_name)
21
+ try:
22
+ result = func(*test_args, **test_kwargs)
23
+ finally:
24
+ LOG.debug("Reset state of the container")
25
+ state_client = StateClient()
26
+ state_client.reset_state()
27
+ return result
28
+
29
+ return wrapper
30
+
31
+ return decorator
@@ -0,0 +1,11 @@
1
+ import pytest
2
+
3
+ from localstack.sdk.state import StateClient
4
+
5
+
6
+ @pytest.fixture
7
+ def reset_state():
8
+ """This fixture is used to completely reset the state of LocalStack after a test runs."""
9
+ yield
10
+ state_client = StateClient()
11
+ state_client.reset_state()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: localstack-sdk-python
3
- Version: 0.0.2
3
+ Version: 0.0.4
4
4
  Summary: Python SDK for LocalStack
5
5
  Author-email: LocalStack Team <info@localstack.cloud>
6
6
  Project-URL: Homepage, https://localstack.cloud
@@ -12,23 +12,25 @@ License-File: LICENSE.txt
12
12
  Requires-Dist: localstack-sdk-generated
13
13
 
14
14
  # LocalStack Python SDK
15
+ [![PyPI version](https://img.shields.io/pypi/v/localstack-sdk-python)](https://pypi.org/project/localstack-sdk-python/)
15
16
 
16
17
  This is the Python SDK for LocalStack.
17
18
  LocalStack offers a number of developer endpoints (see [docs](https://docs.localstack.cloud/references/internal-endpoints/)).
18
19
  This SDK provides a programmatic and easy way to interact with them.
19
20
 
20
21
  > [!WARNING]
21
- > This project is still in a preview phase, and will be subject to fast and breaking changes.
22
+ > This project is still in a preview phase and will be subject to fast and breaking changes.
22
23
 
23
24
  ### Project Structure
24
25
 
25
26
  This project follows the following structure:
26
27
 
27
- - `packages/localstack-sdk-generated` is a python project generated from the OpenAPI specs with [openapi-generator](https://github.com/OpenAPITools/openapi-generator).
28
- - `localstack-sdk-python` is the main project that has `localstack-sdk-generated` has the main dependency.
28
+ - `packages/localstack-sdk-generated` is a Python project generated from the OpenAPI specs with [openapi-generator](https://github.com/OpenAPITools/openapi-generator).
29
+ LocalStack's OpenAPI specs are available in the [openapi repository](https://github.com/localstack/openapi).
30
+ - `localstack-sdk-python` is the main project that has `localstack-sdk-generated` as the main dependency.
29
31
 
30
32
  Developers are not supposed to modify at all `localstack-sdk-generated`.
31
- The code needs to be every time re-generated from specs using the `generate.sh` script in the `bin` folder.
33
+ The code needs to be re-generated from specs every time using the `generate.sh` script in the `bin` folder.
32
34
 
33
35
  This project uses [uv](https://github.com/astral-sh/uv) as package/project manager.
34
36
 
@@ -1,14 +1,24 @@
1
1
  LICENSE.txt
2
2
  README.md
3
3
  pyproject.toml
4
- localstack-sdk-python/localstack/clients.py
4
+ localstack-sdk-python/localstack/sdk/clients.py
5
+ localstack-sdk-python/localstack/sdk/aws/__init__.py
6
+ localstack-sdk-python/localstack/sdk/aws/client.py
5
7
  localstack-sdk-python/localstack/sdk/chaos/__init__.py
6
8
  localstack-sdk-python/localstack/sdk/chaos/client.py
7
9
  localstack-sdk-python/localstack/sdk/chaos/managers.py
8
10
  localstack-sdk-python/localstack/sdk/pods/__init__.py
9
11
  localstack-sdk-python/localstack/sdk/pods/client.py
12
+ localstack-sdk-python/localstack/sdk/pods/exceptions.py
13
+ localstack-sdk-python/localstack/sdk/state/__init__.py
14
+ localstack-sdk-python/localstack/sdk/state/client.py
15
+ localstack-sdk-python/localstack/sdk/testing/__init__.py
16
+ localstack-sdk-python/localstack/sdk/testing/decorators.py
17
+ localstack-sdk-python/localstack/sdk/testing/pytest/__init__.py
18
+ localstack-sdk-python/localstack/sdk/testing/pytest/plugins.py
10
19
  localstack-sdk-python/localstack_sdk_python.egg-info/PKG-INFO
11
20
  localstack-sdk-python/localstack_sdk_python.egg-info/SOURCES.txt
12
21
  localstack-sdk-python/localstack_sdk_python.egg-info/dependency_links.txt
22
+ localstack-sdk-python/localstack_sdk_python.egg-info/entry_points.txt
13
23
  localstack-sdk-python/localstack_sdk_python.egg-info/requires.txt
14
24
  localstack-sdk-python/localstack_sdk_python.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ [pytest11]
2
+ localstack = localstack.sdk.testing.pytest.plugins
@@ -5,7 +5,7 @@ description = "Python SDK for LocalStack"
5
5
  authors = [
6
6
  { name = "LocalStack Team", email = "info@localstack.cloud"}
7
7
  ]
8
- version = "0.0.2"
8
+ version = "0.0.4"
9
9
  dependencies = [
10
10
  "localstack-sdk-generated"
11
11
  ]
@@ -25,8 +25,10 @@ readme = { file = ["README.md"], content-type = "text/markdown"}
25
25
 
26
26
  [tool.uv]
27
27
  dev-dependencies=[
28
- "pytest",
29
- "ruff"
28
+ "pytest>=8.3.3",
29
+ "ruff>=0.6.9",
30
+ "boto3>=1.35.40",
31
+ "sphinx>=8.1.3"
30
32
  ]
31
33
 
32
34
  [tool.uv.sources]
@@ -45,6 +47,9 @@ where = ["localstack-sdk-python/"]
45
47
  include = ["localstack*"]
46
48
  exclude = ["tests*"]
47
49
 
50
+ [project.entry-points.pytest11]
51
+ localstack = "localstack.sdk.testing.pytest.plugins"
52
+
48
53
  [tool.ruff]
49
54
  # Always generate Python 3.8-compatible code.
50
55
  target-version = "py38"
@@ -1,18 +0,0 @@
1
- import os
2
-
3
- from localstack.sdk.api_client import ApiClient
4
- from localstack.sdk.configuration import Configuration
5
-
6
-
7
- class BaseClient:
8
- """A BaseClient creates a configuration and instantiate a ApiClient"""
9
-
10
- configuration: Configuration
11
- _api_client: ApiClient
12
- auth_token: str | None
13
-
14
- def __init__(self, host: str | None = None, auth_token: str | None = None, **kwargs) -> None:
15
- _host = host or "http://localhost.localstack.cloud:4566"
16
- self.auth_token = auth_token or os.getenv("LOCALSTACK_AUTH_TOKEN", "").strip("'\" ")
17
- self.configuration = Configuration(host=_host)
18
- self._api_client = ApiClient(configuration=self.configuration)
@@ -1,39 +0,0 @@
1
- from localstack.clients import BaseClient
2
- from localstack.sdk.api.chaos_api import ChaosApi
3
- from localstack.sdk.models import FaultRule, NetworkEffectsConfig
4
-
5
-
6
- class ChaosClient(BaseClient):
7
- """
8
- The client for the ChaosAPI.
9
- This is mostly a wrapper of the ChaosApi class, which is automatically generated from the OpenAPI specs.
10
- """
11
-
12
- def __init__(self, **kwargs) -> None:
13
- super().__init__(**kwargs)
14
- self._client = ChaosApi(self._api_client)
15
-
16
- def set_fault_rules(self, fault_rules: list[FaultRule]) -> list[FaultRule]:
17
- return self._client.set_fault_rules_0(fault_rule=fault_rules)
18
-
19
- def add_fault_rules(self, fault_rules: list[FaultRule]) -> list[FaultRule]:
20
- return self._client.add_fault_rules_0(fault_rule=fault_rules)
21
-
22
- def delete_fault_rules(self, fault_rules: list[FaultRule]) -> list[FaultRule]:
23
- return self._client.delete_fault_rules_0(fault_rule=fault_rules)
24
-
25
- def get_fault_rules(self) -> list[FaultRule]:
26
- return self._client.get_fault_rules_0()
27
-
28
- def get_network_effects(self) -> NetworkEffectsConfig:
29
- return self._client.get_network_effects_0()
30
-
31
- def set_network_effects(
32
- self, network_effects_config: NetworkEffectsConfig
33
- ) -> NetworkEffectsConfig:
34
- return self._client.set_network_effects_0(network_effects_config=network_effects_config)
35
-
36
-
37
- def get_default(**args) -> ChaosClient:
38
- """Return a default chaos client with a default configuration"""
39
- return ChaosClient(**args)
@@ -1,77 +0,0 @@
1
- import base64
2
- import json
3
-
4
- from localstack.clients import BaseClient
5
- from localstack.sdk.api import PodsApi
6
- from localstack.sdk.models import PodSaveRequest, RemoteConfig
7
-
8
-
9
- def _empty_remote_config() -> RemoteConfig:
10
- return RemoteConfig(oneof_schema_1_validator={}, actual_instance={})
11
-
12
-
13
- def _read_ndjson(raw_content: bytes) -> list[dict]:
14
- ndjson_str = raw_content.decode("utf-8")
15
- return [json.loads(line) for line in ndjson_str.splitlines()]
16
-
17
-
18
- def _get_completion_event(streamed_response: list[dict]) -> dict | None:
19
- completion_events = [line for line in streamed_response if line.get("event") == "completion"]
20
- return completion_events[0] if completion_events else None
21
-
22
-
23
- class PodsClient(BaseClient):
24
- def __init__(self, **args) -> None:
25
- super().__init__(**args)
26
- self._client = PodsApi(self._api_client)
27
- # https://github.com/localstack/localstack-ext/pull/3469 could be avoided after this
28
- assert self.auth_token
29
- auth_header = get_platform_auth_header(self.auth_token)
30
- self._api_client.set_default_header("Authorization", auth_header["Authorization"])
31
-
32
- def save_pod(self, pod_name: str) -> None:
33
- """
34
- :raise exception if the save does not succeed
35
- """
36
- try:
37
- response = self._client.save_pod_0_with_http_info(
38
- name=pod_name, pod_save_request=PodSaveRequest()
39
- )
40
- except Exception as e:
41
- raise (e)
42
- if response.status_code != 200:
43
- pass
44
- streamed_response = _read_ndjson(response.raw_data)
45
- completion_event = _get_completion_event(streamed_response)
46
- if completion_event["status"] == "error":
47
- # todo: define exception
48
- raise Exception(completion_event.get("message"))
49
-
50
- def load_pod(self, pod_name: str) -> None:
51
- """
52
- :raise exception if the load does not succeed
53
- """
54
- response = self._client.load_pod_0_with_http_info(
55
- name=pod_name, remote_config=_empty_remote_config()
56
- )
57
- if response.status_code != 200:
58
- pass
59
- streamed_response = _read_ndjson(response.raw_data)
60
- completion_event = _get_completion_event(streamed_response)
61
- if completion_event["status"] == "error":
62
- # todo: define exception
63
- raise Exception(completion_event.get("message"))
64
-
65
- def delete_pod(self, pod_name: str) -> None:
66
- return self._client.delete_pod_0(name=pod_name, remote_config=_empty_remote_config())
67
-
68
- def list_pods(self):
69
- # todo: fix CloudPodInner model;
70
- pods = self._client.list_pods_0(remote_config=_empty_remote_config())
71
- return pods
72
-
73
-
74
- def get_platform_auth_header(token: str) -> dict[str, str]:
75
- _token = f":{token}"
76
- auth_encoded = base64.b64encode(_token.encode("utf-8")).decode("utf-8")
77
- return {"Authorization": f"Basic {auth_encoded}"}