Skip to content

Commit 14aadb3

Browse files
authored
New localstack_host format (#45)
* Use new LOCALSTACK_HOST format This format includes the port number, so the service endpoints have been re-written as service ports, since we can combine that with the LOCALSTACK_HOST variable.
1 parent 65109d6 commit 14aadb3

File tree

5 files changed

+163
-125
lines changed

5 files changed

+163
-125
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# LocalStack Python Client Change Log
22

3+
* v2.0: Change `LOCALSTACK_HOSTNAME` from `<hostname>` to `<hostname>:<port>`; remove `EDGE_PORT` environment variable
34
* v1.39: Add endpoint for Amazon MQ
45
* v1.38: Add `enable_local_endpoints()` util function; slight project refactoring, migrate from `nose` to `pytests`
56
* v1.37: Add endpoint for Amazon Transcribe

README.md

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -66,14 +66,12 @@ sqs = boto3.client('sqs')
6666
assert sqs.list_queues() is not None # list SQS in localstack
6767
```
6868

69-
## Configurations
69+
## Configuration
7070

7171
You can use the following environment variables for configuration:
7272

73-
* `LOCALSTACK_HOST`: Set the hostname for the LocalStack instance. Useful when you have
74-
LocalStack bound to a different host (e.g., within docker-compose).
75-
* `EDGE_PORT`: Port number to use when connecting to LocalStack services. Defaults to `4566`.
76-
* `USE_SSL`: Whether to use `https` endpoint URLs. Defaults to `false`.
73+
* `LOCALSTACK_HOST`: A `<hostname>:<port>` variable defining where to find LocalStack (default: `localhost:4566`).
74+
* `USE_SSL`: Whether to use SSL when connecting to LocalStack (default: `False`).
7775

7876
### Enabling Transparent Local Endpoints
7977

localstack_client/config.py

Lines changed: 138 additions & 119 deletions
Original file line numberDiff line numberDiff line change
@@ -1,130 +1,131 @@
1-
import json
21
import os
3-
from typing import Dict
2+
from typing import Dict, Optional, Tuple
43
from urllib.parse import urlparse
54

65
# note: leave this import here for now, as some upstream code is depending on it (TODO needs to be updated)
76
from localstack_client.patch import patch_expand_host_prefix # noqa
87

98
# central entrypoint port for all LocalStack API endpoints
10-
EDGE_PORT = int(os.environ.get("EDGE_PORT") or 4566)
9+
DEFAULT_EDGE_PORT = 4566
1110

12-
# NOTE: The endpoints below will soon become deprecated/removed, as the default in the
11+
# NOTE: The ports listed below will soon become deprecated/removed, as the default in the
1312
# latest version is to access all services via a single "edge service" (port 4566 by default)
14-
_service_endpoints_template = {
15-
"edge": "{proto}://{host}:4566",
16-
"apigateway": "{proto}://{host}:4567",
17-
"apigatewayv2": "{proto}://{host}:4567",
18-
"kinesis": "{proto}://{host}:4568",
19-
"dynamodb": "{proto}://{host}:4569",
20-
"dynamodbstreams": "{proto}://{host}:4570",
21-
"elasticsearch": "{proto}://{host}:4571",
22-
"s3": "{proto}://{host}:4572",
23-
"firehose": "{proto}://{host}:4573",
24-
"lambda": "{proto}://{host}:4574",
25-
"sns": "{proto}://{host}:4575",
26-
"sqs": "{proto}://{host}:4576",
27-
"redshift": "{proto}://{host}:4577",
28-
"redshift-data": "{proto}://{host}:4577",
29-
"es": "{proto}://{host}:4578",
30-
"opensearch": "{proto}://{host}:4578",
31-
"ses": "{proto}://{host}:4579",
32-
"sesv2": "{proto}://{host}:4579",
33-
"route53": "{proto}://{host}:4580",
34-
"route53resolver": "{proto}://{host}:4580",
35-
"cloudformation": "{proto}://{host}:4581",
36-
"cloudwatch": "{proto}://{host}:4582",
37-
"ssm": "{proto}://{host}:4583",
38-
"secretsmanager": "{proto}://{host}:4584",
39-
"stepfunctions": "{proto}://{host}:4585",
40-
"logs": "{proto}://{host}:4586",
41-
"events": "{proto}://{host}:4587",
42-
"elb": "{proto}://{host}:4588",
43-
"iot": "{proto}://{host}:4589",
44-
"iotanalytics": "{proto}://{host}:4589",
45-
"iotevents": "{proto}://{host}:4589",
46-
"iotevents-data": "{proto}://{host}:4589",
47-
"iotwireless": "{proto}://{host}:4589",
48-
"iot-data": "{proto}://{host}:4589",
49-
"iot-jobs-data": "{proto}://{host}:4589",
50-
"cognito-idp": "{proto}://{host}:4590",
51-
"cognito-identity": "{proto}://{host}:4591",
52-
"sts": "{proto}://{host}:4592",
53-
"iam": "{proto}://{host}:4593",
54-
"rds": "{proto}://{host}:4594",
55-
"rds-data": "{proto}://{host}:4594",
56-
"cloudsearch": "{proto}://{host}:4595",
57-
"swf": "{proto}://{host}:4596",
58-
"ec2": "{proto}://{host}:4597",
59-
"elasticache": "{proto}://{host}:4598",
60-
"kms": "{proto}://{host}:4599",
61-
"emr": "{proto}://{host}:4600",
62-
"ecs": "{proto}://{host}:4601",
63-
"eks": "{proto}://{host}:4602",
64-
"xray": "{proto}://{host}:4603",
65-
"elasticbeanstalk": "{proto}://{host}:4604",
66-
"appsync": "{proto}://{host}:4605",
67-
"cloudfront": "{proto}://{host}:4606",
68-
"athena": "{proto}://{host}:4607",
69-
"glue": "{proto}://{host}:4608",
70-
"sagemaker": "{proto}://{host}:4609",
71-
"sagemaker-runtime": "{proto}://{host}:4609",
72-
"ecr": "{proto}://{host}:4610",
73-
"qldb": "{proto}://{host}:4611",
74-
"qldb-session": "{proto}://{host}:4611",
75-
"cloudtrail": "{proto}://{host}:4612",
76-
"glacier": "{proto}://{host}:4613",
77-
"batch": "{proto}://{host}:4614",
78-
"organizations": "{proto}://{host}:4615",
79-
"autoscaling": "{proto}://{host}:4616",
80-
"mediastore": "{proto}://{host}:4617",
81-
"mediastore-data": "{proto}://{host}:4617",
82-
"transfer": "{proto}://{host}:4618",
83-
"acm": "{proto}://{host}:4619",
84-
"codecommit": "{proto}://{host}:4620",
85-
"kinesisanalytics": "{proto}://{host}:4621",
86-
"kinesisanalyticsv2": "{proto}://{host}:4621",
87-
"amplify": "{proto}://{host}:4622",
88-
"application-autoscaling": "{proto}://{host}:4623",
89-
"kafka": "{proto}://{host}:4624",
90-
"apigatewaymanagementapi": "{proto}://{host}:4625",
91-
"timestream": "{proto}://{host}:4626",
92-
"timestream-query": "{proto}://{host}:4626",
93-
"timestream-write": "{proto}://{host}:4626",
94-
"s3control": "{proto}://{host}:4627",
95-
"elbv2": "{proto}://{host}:4628",
96-
"support": "{proto}://{host}:4629",
97-
"neptune": "{proto}://{host}:4594",
98-
"docdb": "{proto}://{host}:4594",
99-
"servicediscovery": "{proto}://{host}:4630",
100-
"serverlessrepo": "{proto}://{host}:4631",
101-
"appconfig": "{proto}://{host}:4632",
102-
"ce": "{proto}://{host}:4633",
103-
"mediaconvert": "{proto}://{host}:4634",
104-
"resourcegroupstaggingapi": "{proto}://{host}:4635",
105-
"resource-groups": "{proto}://{host}:4636",
106-
"efs": "{proto}://{host}:4637",
107-
"backup": "{proto}://{host}:4638",
108-
"lakeformation": "{proto}://{host}:4639",
109-
"waf": "{proto}://{host}:4640",
110-
"wafv2": "{proto}://{host}:4640",
111-
"config": "{proto}://{host}:4641",
112-
"configservice": "{proto}://{host}:4641",
113-
"mwaa": "{proto}://{host}:4642",
114-
"fis": "{proto}://{host}:4643",
115-
"meteringmarketplace": "{proto}://{host}:4644",
116-
"transcribe": "{proto}://{host}:4566",
117-
"mq": "{proto}://{host}:4566"
13+
_service_ports: Dict[str, int] = {
14+
"edge": 4566,
15+
"apigateway": 4567,
16+
"apigatewayv2": 4567,
17+
"kinesis": 4568,
18+
"dynamodb": 4569,
19+
"dynamodbstreams": 4570,
20+
"elasticsearch": 4571,
21+
"s3": 4572,
22+
"firehose": 4573,
23+
"lambda": 4574,
24+
"sns": 4575,
25+
"sqs": 4576,
26+
"redshift": 4577,
27+
"redshift-data": 4577,
28+
"es": 4578,
29+
"opensearch": 4578,
30+
"ses": 4579,
31+
"sesv2": 4579,
32+
"route53": 4580,
33+
"route53resolver": 4580,
34+
"cloudformation": 4581,
35+
"cloudwatch": 4582,
36+
"ssm": 4583,
37+
"secretsmanager": 4584,
38+
"stepfunctions": 4585,
39+
"logs": 4586,
40+
"events": 4587,
41+
"elb": 4588,
42+
"iot": 4589,
43+
"iotanalytics": 4589,
44+
"iotevents": 4589,
45+
"iotevents-data": 4589,
46+
"iotwireless": 4589,
47+
"iot-data": 4589,
48+
"iot-jobs-data": 4589,
49+
"cognito-idp": 4590,
50+
"cognito-identity": 4591,
51+
"sts": 4592,
52+
"iam": 4593,
53+
"rds": 4594,
54+
"rds-data": 4594,
55+
"cloudsearch": 4595,
56+
"swf": 4596,
57+
"ec2": 4597,
58+
"elasticache": 4598,
59+
"kms": 4599,
60+
"emr": 4600,
61+
"ecs": 4601,
62+
"eks": 4602,
63+
"xray": 4603,
64+
"elasticbeanstalk": 4604,
65+
"appsync": 4605,
66+
"cloudfront": 4606,
67+
"athena": 4607,
68+
"glue": 4608,
69+
"sagemaker": 4609,
70+
"sagemaker-runtime": 4609,
71+
"ecr": 4610,
72+
"qldb": 4611,
73+
"qldb-session": 4611,
74+
"cloudtrail": 4612,
75+
"glacier": 4613,
76+
"batch": 4614,
77+
"organizations": 4615,
78+
"autoscaling": 4616,
79+
"mediastore": 4617,
80+
"mediastore-data": 4617,
81+
"transfer": 4618,
82+
"acm": 4619,
83+
"codecommit": 4620,
84+
"kinesisanalytics": 4621,
85+
"kinesisanalyticsv2": 4621,
86+
"amplify": 4622,
87+
"application-autoscaling": 4623,
88+
"kafka": 4624,
89+
"apigatewaymanagementapi": 4625,
90+
"timestream": 4626,
91+
"timestream-query": 4626,
92+
"timestream-write": 4626,
93+
"s3control": 4627,
94+
"elbv2": 4628,
95+
"support": 4629,
96+
"neptune": 4594,
97+
"docdb": 4594,
98+
"servicediscovery": 4630,
99+
"serverlessrepo": 4631,
100+
"appconfig": 4632,
101+
"ce": 4633,
102+
"mediaconvert": 4634,
103+
"resourcegroupstaggingapi": 4635,
104+
"resource-groups": 4636,
105+
"efs": 4637,
106+
"backup": 4638,
107+
"lakeformation": 4639,
108+
"waf": 4640,
109+
"wafv2": 4640,
110+
"config": 4641,
111+
"configservice": 4641,
112+
"mwaa": 4642,
113+
"fis": 4643,
114+
"meteringmarketplace": 4644,
115+
"transcribe": 4566,
116+
"mq": 4566,
118117
}
119118

120119
# TODO remove service port mapping above entirely
121120
if os.environ.get("USE_LEGACY_PORTS") not in ["1", "true"]:
122-
for key, value in _service_endpoints_template.items():
121+
for key, value in _service_ports.items():
123122
if key not in ["dashboard", "elasticsearch"]:
124-
_service_endpoints_template[key] = f"{value.rpartition(':')[0]}:{EDGE_PORT}"
123+
_service_ports[key] = DEFAULT_EDGE_PORT
125124

126125

127-
def get_service_endpoint(service: str, localstack_host: str = None) -> str:
126+
def get_service_endpoint(
127+
service: str, localstack_host: Optional[str] = None
128+
) -> Optional[str]:
128129
"""
129130
Return the local endpoint URL for the given boto3 service (e.g., "s3").
130131
If $AWS_ENDPOINT_URL is configured in the environment, it is returned directly.
@@ -137,19 +138,37 @@ def get_service_endpoint(service: str, localstack_host: str = None) -> str:
137138
return endpoints.get(service)
138139

139140

140-
def get_service_endpoints(localstack_host: str = None) -> Dict[str, str]:
141+
def parse_localstack_host(given: str) -> Tuple[str, int]:
142+
parts = given.split(":", 1)
143+
if len(parts) == 1:
144+
# just hostname
145+
return parts[0].strip() or "localhost", DEFAULT_EDGE_PORT
146+
elif len(parts) == 2:
147+
hostname = parts[0].strip() or "localhost"
148+
port_s = parts[1]
149+
try:
150+
port = int(port_s)
151+
return (hostname, port)
152+
except Exception:
153+
raise RuntimeError(f"could not parse {given} into <hostname>:<port>")
154+
else:
155+
raise RuntimeError(f"could not parse {given} into <hostname>:<port>")
156+
157+
158+
def get_service_endpoints(localstack_host: Optional[str] = None) -> Dict[str, str]:
141159
if localstack_host is None:
142-
localstack_host = os.environ.get("LOCALSTACK_HOST", "localhost")
160+
localstack_host = os.environ.get(
161+
"LOCALSTACK_HOST", f"localhost:{DEFAULT_EDGE_PORT}"
162+
)
163+
164+
hostname, port = parse_localstack_host(localstack_host)
165+
143166
protocol = "https" if os.environ.get("USE_SSL") in ("1", "true") else "http"
144167

145-
return json.loads(
146-
json.dumps(_service_endpoints_template)
147-
.replace("{proto}", protocol)
148-
.replace("{host}", localstack_host)
149-
)
168+
return {key: f"{protocol}://{hostname}:{port}" for key in _service_ports.keys()}
150169

151170

152-
def get_service_port(service: str) -> int:
171+
def get_service_port(service: str) -> Optional[int]:
153172
ports = get_service_ports()
154173
return ports.get(service)
155174

setup.cfg

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[metadata]
22
name = localstack-client
3-
version = 1.39
3+
version = 2.0
44
url = https://github.com/localstack/localstack-python-client
55
author = LocalStack Team
66
author_email = info@localstack.cloud

tests/test_config.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
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"

0 commit comments

Comments
 (0)