local-web-services-python-sdk 0.1.1__tar.gz → 0.1.3__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 (28) hide show
  1. {local_web_services_python_sdk-0.1.1 → local_web_services_python_sdk-0.1.3}/PKG-INFO +1 -1
  2. {local_web_services_python_sdk-0.1.1 → local_web_services_python_sdk-0.1.3}/pyproject.toml +1 -1
  3. {local_web_services_python_sdk-0.1.1 → local_web_services_python_sdk-0.1.3}/src/lws_testing/_discovery/hcl.py +19 -0
  4. {local_web_services_python_sdk-0.1.1 → local_web_services_python_sdk-0.1.3}/src/lws_testing/session.py +62 -0
  5. {local_web_services_python_sdk-0.1.1 → local_web_services_python_sdk-0.1.3}/tests/unit/test_session_lifecycle.py +44 -0
  6. {local_web_services_python_sdk-0.1.1 → local_web_services_python_sdk-0.1.3}/.github/workflows/ci.yml +0 -0
  7. {local_web_services_python_sdk-0.1.1 → local_web_services_python_sdk-0.1.3}/.github/workflows/publish.yml +0 -0
  8. {local_web_services_python_sdk-0.1.1 → local_web_services_python_sdk-0.1.3}/.gitignore +0 -0
  9. {local_web_services_python_sdk-0.1.1 → local_web_services_python_sdk-0.1.3}/Makefile +0 -0
  10. {local_web_services_python_sdk-0.1.1 → local_web_services_python_sdk-0.1.3}/README.md +0 -0
  11. {local_web_services_python_sdk-0.1.1 → local_web_services_python_sdk-0.1.3}/src/lws_testing/__init__.py +0 -0
  12. {local_web_services_python_sdk-0.1.1 → local_web_services_python_sdk-0.1.3}/src/lws_testing/_builders/__init__.py +0 -0
  13. {local_web_services_python_sdk-0.1.1 → local_web_services_python_sdk-0.1.3}/src/lws_testing/_builders/chaos.py +0 -0
  14. {local_web_services_python_sdk-0.1.1 → local_web_services_python_sdk-0.1.3}/src/lws_testing/_builders/iam.py +0 -0
  15. {local_web_services_python_sdk-0.1.1 → local_web_services_python_sdk-0.1.3}/src/lws_testing/_builders/mock.py +0 -0
  16. {local_web_services_python_sdk-0.1.1 → local_web_services_python_sdk-0.1.3}/src/lws_testing/_discovery/__init__.py +0 -0
  17. {local_web_services_python_sdk-0.1.1 → local_web_services_python_sdk-0.1.3}/src/lws_testing/_discovery/cdk.py +0 -0
  18. {local_web_services_python_sdk-0.1.1 → local_web_services_python_sdk-0.1.3}/src/lws_testing/_logs.py +0 -0
  19. {local_web_services_python_sdk-0.1.1 → local_web_services_python_sdk-0.1.3}/src/lws_testing/_resources/__init__.py +0 -0
  20. {local_web_services_python_sdk-0.1.1 → local_web_services_python_sdk-0.1.3}/src/lws_testing/_resources/dynamodb.py +0 -0
  21. {local_web_services_python_sdk-0.1.1 → local_web_services_python_sdk-0.1.3}/src/lws_testing/_resources/s3.py +0 -0
  22. {local_web_services_python_sdk-0.1.1 → local_web_services_python_sdk-0.1.3}/src/lws_testing/_resources/sqs.py +0 -0
  23. {local_web_services_python_sdk-0.1.1 → local_web_services_python_sdk-0.1.3}/src/lws_testing/_transport/__init__.py +0 -0
  24. {local_web_services_python_sdk-0.1.1 → local_web_services_python_sdk-0.1.3}/src/lws_testing/_transport/inprocess.py +0 -0
  25. {local_web_services_python_sdk-0.1.1 → local_web_services_python_sdk-0.1.3}/src/lws_testing/fixtures.py +0 -0
  26. {local_web_services_python_sdk-0.1.1 → local_web_services_python_sdk-0.1.3}/tests/__init__.py +0 -0
  27. {local_web_services_python_sdk-0.1.1 → local_web_services_python_sdk-0.1.3}/tests/unit/__init__.py +0 -0
  28. {local_web_services_python_sdk-0.1.1 → local_web_services_python_sdk-0.1.3}/tests/unit/test_session_imports.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: local-web-services-python-sdk
3
- Version: 0.1.1
3
+ Version: 0.1.3
4
4
  Summary: Python testing SDK for local-web-services — in-process pytest fixtures and boto3 helpers
5
5
  Project-URL: Homepage, https://local-web-services.github.io/www
6
6
  Project-URL: Repository, https://github.com/local-web-services/local-web-services-sdk-python
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "local-web-services-python-sdk"
3
- version = "0.1.1"
3
+ version = "0.1.3"
4
4
  description = "Python testing SDK for local-web-services — in-process pytest fixtures and boto3 helpers"
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -14,6 +14,8 @@ _RESOURCE_HEADER = re.compile(
14
14
  _ATTR_STR = re.compile(r'^\s*(?P<key>\w+)\s*=\s*"(?P<value>[^"]*)"')
15
15
  # Matches a simple key = value (unquoted booleans / numbers)
16
16
  _ATTR_BARE = re.compile(r"^\s*(?P<key>\w+)\s*=\s*(?P<value>\S+)")
17
+ # Matches a heredoc opener: key = <<MARKER
18
+ _ATTR_HEREDOC = re.compile(r"^\s*(?P<key>\w+)\s*=\s*<<(?P<marker>\w+)\s*$")
17
19
 
18
20
  # Maps Terraform resource types to (collection_key, builder_function)
19
21
  _DISPATCH: dict[str, tuple[str, Any]] = {
@@ -110,6 +112,23 @@ def _collect_block(lines: list[str], start: int) -> tuple[dict[str, str], int]:
110
112
  i = start
111
113
  while i < len(lines) and depth > 0:
112
114
  line = lines[i]
115
+
116
+ # Handle heredoc (key = <<MARKER ... MARKER) before adjusting depth,
117
+ # so the JSON content inside the heredoc does not affect brace counting.
118
+ if depth == 1:
119
+ m_heredoc = _ATTR_HEREDOC.match(line)
120
+ if m_heredoc:
121
+ key = m_heredoc.group("key")
122
+ marker = m_heredoc.group("marker")
123
+ i += 1
124
+ heredoc_lines: list[str] = []
125
+ while i < len(lines) and lines[i].rstrip() != marker:
126
+ heredoc_lines.append(lines[i])
127
+ i += 1
128
+ attrs[key] = "\n".join(heredoc_lines)
129
+ i += 1 # skip the closing marker line
130
+ continue
131
+
113
132
  depth += line.count("{") - line.count("}")
114
133
  if depth > 1:
115
134
  i += 1
@@ -3,6 +3,7 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import asyncio
6
+ import os
6
7
  import shutil
7
8
  import socket
8
9
  import tempfile
@@ -12,6 +13,26 @@ from contextlib import contextmanager
12
13
  from pathlib import Path
13
14
  from typing import Any
14
15
 
16
+ # Maps boto3 service name → AWS SDK endpoint URL env var.
17
+ # Setting these redirects *any* boto3 client created in the process to the
18
+ # local LWS service — no production-code changes required.
19
+ _SERVICE_ENV_VARS: dict[str, str] = {
20
+ "dynamodb": "AWS_ENDPOINT_URL_DYNAMODB",
21
+ "sqs": "AWS_ENDPOINT_URL_SQS",
22
+ "s3": "AWS_ENDPOINT_URL_S3",
23
+ "sns": "AWS_ENDPOINT_URL_SNS",
24
+ "stepfunctions": "AWS_ENDPOINT_URL_STEPFUNCTIONS",
25
+ "ssm": "AWS_ENDPOINT_URL_SSM",
26
+ "secretsmanager": "AWS_ENDPOINT_URL_SECRETSMANAGER",
27
+ }
28
+
29
+ # Credential / region overrides so boto3 never tries to contact IAM or STS.
30
+ _TEST_CREDENTIALS: dict[str, str] = {
31
+ "AWS_ACCESS_KEY_ID": "test",
32
+ "AWS_SECRET_ACCESS_KEY": "test",
33
+ "AWS_DEFAULT_REGION": "us-east-1",
34
+ }
35
+
15
36
 
16
37
  def _free_port() -> int:
17
38
  """Return a free ephemeral TCP port on localhost."""
@@ -70,6 +91,7 @@ class LwsSession:
70
91
  self._thread: threading.Thread | None = None
71
92
  self._data_dir: Path | None = None
72
93
  self._log_handler: Any = None
94
+ self._saved_env: dict[str, str | None] = {}
73
95
 
74
96
  # ── Constructors ──────────────────────────────────────────────────────────
75
97
 
@@ -139,6 +161,8 @@ class LwsSession:
139
161
  exc = error_holder[0]
140
162
  raise RuntimeError(f"LwsSession failed to start: {exc}") from exc
141
163
 
164
+ self._patch_env()
165
+
142
166
  async def _async_start(self, ready: threading.Event) -> None:
143
167
  """Create providers, build apps, start servers."""
144
168
  from lws_testing._transport.inprocess import start_services
@@ -150,6 +174,8 @@ class LwsSession:
150
174
 
151
175
  def _stop(self) -> None:
152
176
  """Stop all servers and clean up temporary state."""
177
+ self._restore_env()
178
+
153
179
  if self._loop and self._loop.is_running():
154
180
  future = asyncio.run_coroutine_threadsafe(self._async_stop(), self._loop)
155
181
  future.result(timeout=10)
@@ -167,6 +193,33 @@ class LwsSession:
167
193
 
168
194
  await stop_services(self._servers)
169
195
 
196
+ # ── Environment patching ──────────────────────────────────────────────────
197
+
198
+ def _patch_env(self) -> None:
199
+ """Set AWS SDK endpoint env vars so any boto3 client hits local LWS.
200
+
201
+ This is the drop-in mechanism: production code that creates boto3
202
+ clients the normal way (``boto3.client("dynamodb")``) is automatically
203
+ redirected to the local services without any code changes.
204
+ """
205
+ for service, env_var in _SERVICE_ENV_VARS.items():
206
+ port = self._ports.get(service)
207
+ if port:
208
+ self._saved_env[env_var] = os.environ.get(env_var)
209
+ os.environ[env_var] = f"http://127.0.0.1:{port}"
210
+ for key, val in _TEST_CREDENTIALS.items():
211
+ self._saved_env[key] = os.environ.get(key)
212
+ os.environ[key] = val
213
+
214
+ def _restore_env(self) -> None:
215
+ """Restore all env vars that were overridden by :meth:`_patch_env`."""
216
+ for key, saved in self._saved_env.items():
217
+ if saved is None:
218
+ os.environ.pop(key, None)
219
+ else:
220
+ os.environ[key] = saved
221
+ self._saved_env = {}
222
+
170
223
  # ── boto3 client factory ──────────────────────────────────────────────────
171
224
 
172
225
  def client(self, service: str) -> Any:
@@ -279,6 +332,15 @@ class LwsSession:
279
332
 
280
333
  return S3Helper(bucket_name, self.client("s3"))
281
334
 
335
+ def queue_url(self, queue_name: str) -> str:
336
+ """Return the local SQS URL for *queue_name*.
337
+
338
+ Use this to set the queue URL env var that production code reads::
339
+
340
+ os.environ["ORDER_QUEUE_URL"] = session.queue_url("OrderQueue")
341
+ """
342
+ return f"http://127.0.0.1:{self._ports['sqs']}/000000000000/{queue_name}"
343
+
282
344
  # ── Mock / chaos / IAM builders ───────────────────────────────────────────
283
345
 
284
346
  def mock(self, service: str) -> Any:
@@ -103,3 +103,47 @@ def test_all_services_available(session):
103
103
  for svc in expected_services:
104
104
  client = session.client(svc)
105
105
  assert client is not None, f"Expected service {svc!r} to be available"
106
+
107
+
108
+ def test_vanilla_boto3_client_is_redirected_to_lws(session):
109
+ # Arrange — env vars are set by the session; a plain boto3 client should
110
+ # hit LWS without any explicit endpoint_url
111
+ import boto3
112
+
113
+ # Act
114
+ vanilla_client = boto3.client("dynamodb", region_name="us-east-1")
115
+ response = vanilla_client.list_tables()
116
+
117
+ # Assert — LWS responded (real AWS would also return this key, but if the
118
+ # env var wasn't set the call would fail with NoCredentialsError or timeout)
119
+ assert "TableNames" in response
120
+
121
+
122
+ def test_env_vars_are_restored_after_session_closes():
123
+ # Arrange — capture state before any session starts
124
+ import os
125
+
126
+ expected_key = "AWS_ENDPOINT_URL_DYNAMODB"
127
+ value_before = os.environ.get(expected_key)
128
+
129
+ # Act
130
+ with LwsSession(tables=[{"name": "Tmp", "partition_key": "id"}]):
131
+ actual_during = os.environ.get(expected_key)
132
+
133
+ actual_after = os.environ.get(expected_key)
134
+
135
+ # Assert
136
+ assert actual_during is not None, "Expected env var to be set during session"
137
+ assert actual_after == value_before, "Expected env var to be restored after session"
138
+
139
+
140
+ def test_queue_url_returns_local_url(session):
141
+ # Arrange
142
+ expected_queue_name = "OrderQueue"
143
+
144
+ # Act
145
+ actual_url = session.queue_url(expected_queue_name)
146
+
147
+ # Assert
148
+ assert "127.0.0.1" in actual_url
149
+ assert expected_queue_name in actual_url