Skip to content

Commit a21e460

Browse files
authored
minor fixes and polishing for TypeDB extension (#114)
1 parent d401213 commit a21e460

10 files changed

Lines changed: 97 additions & 33 deletions

File tree

.github/workflows/typedb.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ jobs:
2626
pip install localstack
2727
2828
make install
29+
make lint
2930
make dist
3031
localstack extensions -v install file://$(ls ./dist/localstack_extension_typedb-*.tar.gz)
3132

CODEOWNERS

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,4 +18,5 @@
1818
/http-bin/ @thrau @dominikschubert
1919
/mailhog/ @lukqw @thrau
2020
/miniflare/ @whummer @HarshCasper
21-
/stripe/ @lukqw @thrau
21+
/stripe/ @lukqw @thrau
22+
/typedb/ @whummer @purcell

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ You can install the respective extension by calling `localstack extensions insta
7575
| [Miniflare](https://github.com/localstack/localstack-extensions/tree/main/miniflare) | localstack-extension-miniflare | 0.1.0 | Experimental |
7676
| [Stripe](https://github.com/localstack/localstack-extensions/tree/main/stripe) | localstack-extension-stripe | 0.2.0 | Stable |
7777
| [Terraform Init](https://github.com/localstack/localstack-extensions/tree/main/terraform-init) | localstack-extension-terraform-init | 0.2.0 | Experimental |
78-
| [TypeDB](https://github.com/localstack/localstack-extensions/tree/main/typedb) | localstack-extension-typedb | 0.1.0 | Experimental |
78+
| [TypeDB](https://github.com/localstack/localstack-extensions/tree/main/typedb) | localstack-extension-typedb | 0.1.2 | Experimental |
7979

8080

8181
## Developing Extensions

typedb/Makefile

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ VENV_BIN = python3 -m venv
22
VENV_DIR ?= .venv
33
VENV_ACTIVATE = $(VENV_DIR)/bin/activate
44
VENV_RUN = . $(VENV_ACTIVATE)
5+
TEST_PATH ?= tests
56

67
usage: ## Shows usage for this Makefile
78
@cat Makefile | grep -E '^[a-zA-Z_-]+:.*?## .*$$' | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-15s\033[0m %s\n", $$1, $$2}'
@@ -33,10 +34,13 @@ entrypoints: venv ## Generate plugin entrypoints for Python package
3334
$(VENV_RUN); python -m plux entrypoints
3435

3536
format: ## Run ruff to format the codebase
36-
$(VENV_RUN); python -m ruff format .; python -m ruff check --output-format=full --fix .
37+
$(VENV_RUN); python -m ruff format .; make lint
38+
39+
lint: ## Run ruff to lint the codebase
40+
$(VENV_RUN); python -m ruff check --output-format=full .
3741

3842
test: ## Run integration tests (requires LocalStack running with the Extension installed)
39-
$(VENV_RUN); pytest tests $(PYTEST_ARGS)
43+
$(VENV_RUN); pytest $(PYTEST_ARGS) $(TEST_PATH)
4044

4145
clean-dist: clean
4246
rm -rf dist/

typedb/README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,11 @@ localstack extensions install "git+https://github.com/localstack/localstack-exte
3333

3434
Please refer to the docs [here](https://github.com/localstack/localstack-extensions?tab=readme-ov-file#start-localstack-with-the-extension) for instructions on how to start the extension in developer mode.
3535

36+
## Change Log
37+
38+
* `0.1.1`: Minor fixes in CI setup, exception handling
39+
* `0.1.0`: Initial version of the extension
40+
3641
## License
3742

3843
The code in this repo is available under the Apache 2.0 license.

typedb/localstack_typedb/extension.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
import shlex
33

44
from localstack.config import is_env_not_false
5-
from localstack.utils.docker_utils import DOCKER_CLIENT
65
from localstack_typedb.utils.docker import ProxiedDockerContainerExtension
76
from rolo import Request
87
from werkzeug.datastructures import Headers

typedb/localstack_typedb/utils/docker.py

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
from localstack.config import is_env_true
1010
from localstack_typedb.utils.h2_proxy import (
1111
apply_http2_patches_for_grpc_support,
12-
ProxyRequestMatcher,
1312
)
1413
from localstack.utils.docker_utils import DOCKER_CLIENT
1514
from localstack.extensions.api import Extension, http
@@ -71,13 +70,11 @@ def __init__(
7170
):
7271
self.image_name = image_name
7372
if not container_ports:
74-
raise ArgumentError("container_ports is required")
73+
raise ValueError("container_ports is required")
7574
self.container_ports = container_ports
7675
self.host = host
7776
self.path = path
78-
self.container_name = re.sub(
79-
r"\W", "-", f"ls-ext-{self.name}"
80-
)
77+
self.container_name = re.sub(r"\W", "-", f"ls-ext-{self.name}")
8178
self.command = command
8279
self.request_to_port_router = request_to_port_router
8380
self.http2_ports = http2_ports

typedb/localstack_typedb/utils/h2_proxy.py

Lines changed: 48 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
ProxyRequestMatcher = Callable[[Headers], bool]
1818

19+
1920
class TcpForwarder:
2021
"""Simple helper class for bidirectional forwarding of TCP traffic."""
2122

@@ -47,6 +48,7 @@ def close(self):
4748

4849
patched_connection = False
4950

51+
5052
def apply_http2_patches_for_grpc_support(
5153
target_host: str, target_port: int, should_proxy_request: ProxyRequestMatcher
5254
):
@@ -56,7 +58,9 @@ def apply_http2_patches_for_grpc_support(
5658
"""
5759
LOG.debug(f"Enabling proxying to backend {target_host}:{target_port}")
5860
global patched_connection
59-
assert not patched_connection, "It is not safe to patch H2Connection twice with this function"
61+
assert not patched_connection, (
62+
"It is not safe to patch H2Connection twice with this function"
63+
)
6064
patched_connection = True
6165

6266
class ForwardingBuffer:
@@ -65,35 +69,57 @@ class ForwardingBuffer:
6569
data until the ProxyRequestMatcher tells us whether to send it
6670
to the backend, or leave it to the default handler.
6771
"""
72+
73+
backend: TcpForwarder
74+
buffer: list
75+
proxying: bool | None
76+
6877
def __init__(self, http_response_stream):
6978
self.http_response_stream = http_response_stream
70-
LOG.debug(f"Starting TCP forwarder to port {target_port} for new HTTP2 connection")
79+
LOG.debug(
80+
f"Starting TCP forwarder to port {target_port} for new HTTP2 connection"
81+
)
7182
self.backend = TcpForwarder(target_port, host=target_host)
7283
self.buffer = []
73-
self.proxying = False
74-
reactor.getThreadPool().callInThread(self.backend.receive_loop, self.received_from_backend)
84+
self.proxying = None
85+
reactor.getThreadPool().callInThread(
86+
self.backend.receive_loop, self.received_from_backend
87+
)
7588

7689
def received_from_backend(self, data):
7790
LOG.debug(f"Received {len(data)} bytes from backend")
7891
self.http_response_stream.write(data)
7992

80-
def received_from_http2_client(self, data, default_handler):
93+
def received_from_http2_client(self, data, default_handler: Callable):
94+
if self.proxying is False:
95+
# Note: Return here only if `proxying` is `False` (a value of `None` indicates
96+
# that the headers have not fully been received yet)
97+
return default_handler(data)
98+
8199
if self.proxying:
82100
assert not self.buffer
83101
# Keep sending data to the backend for the lifetime of this connection
84102
self.backend.send(data)
85-
else:
86-
self.buffer.append(data)
87-
if headers := get_headers_from_data_stream(self.buffer):
88-
self.proxying = should_proxy_request(headers)
89-
# Now we know what to do with the buffer
90-
buffered_data = b"".join(self.buffer)
91-
self.buffer = []
92-
if self.proxying:
93-
LOG.debug(f"Forwarding {len(buffered_data)} bytes to backend")
94-
self.backend.send(buffered_data)
95-
else:
96-
return default_handler(buffered_data)
103+
return
104+
105+
self.buffer.append(data)
106+
107+
if not (headers := get_headers_from_data_stream(self.buffer)):
108+
# If no headers received yet, then return (method will be called again for next chunk of data)
109+
return
110+
111+
self.proxying = should_proxy_request(headers)
112+
113+
buffered_data = b"".join(self.buffer)
114+
self.buffer = []
115+
116+
if not self.proxying:
117+
# if this is not a target request, then call the default handler
118+
default_handler(buffered_data)
119+
return
120+
121+
LOG.debug(f"Forwarding {len(buffered_data)} bytes to backend")
122+
self.backend.send(buffered_data)
97123

98124
def close(self):
99125
self.backend.close()
@@ -104,7 +130,9 @@ def _connectionMade(fn, self, *args, **kwargs):
104130

105131
@patch(H2Connection.dataReceived)
106132
def _dataReceived(fn, self, data, *args, **kwargs):
107-
self._ls_forwarding_buffer.received_from_http2_client(data, lambda d: fn(d, *args, **kwargs))
133+
self._ls_forwarding_buffer.received_from_http2_client(
134+
data, lambda d: fn(self, d, *args, **kwargs)
135+
)
108136

109137
@patch(H2Connection.connectionLost)
110138
def connectionLost(fn, self, *args, **kwargs):
@@ -132,12 +160,11 @@ def get_headers_from_frames(frames: Iterable[Frame]) -> Headers:
132160

133161

134162
def get_frames_from_http2_stream(data: bytes) -> Iterable[Frame]:
135-
"""Parse the data from an HTTP2 stream into a list of frames"""
136-
frames = []
163+
"""Parse the data from an HTTP2 stream into an iterable of frames"""
137164
buffer = FrameBuffer(server=True)
138165
buffer.max_frame_size = 16384
139-
buffer.add_data(data)
140166
try:
167+
buffer.add_data(data)
141168
for frame in buffer:
142169
yield frame
143170
except Exception:

typedb/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "localstack-extension-typedb"
7-
version = "0.1.0"
7+
version = "0.1.2"
88
description = "LocalStack Extension: TypeDB on LocalStack"
99
readme = {file = "README.md", content-type = "text/markdown; charset=UTF-8"}
1010
requires-python = ">=3.9"

typedb/tests/test_extension.py

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
import requests
2+
import httpx
23
from localstack.utils.strings import short_uid
3-
from localstack_typedb.utils.h2_proxy import get_frames_from_http2_stream, get_headers_from_frames
4+
from localstack_typedb.utils.h2_proxy import (
5+
get_frames_from_http2_stream,
6+
get_headers_from_frames,
7+
)
48
from typedb.driver import TypeDB, Credentials, DriverOptions, TransactionType
59

610

@@ -66,8 +70,34 @@ def test_connect_to_db_via_grpc_endpoint():
6670
results = tx.query(
6771
'match $p isa person; fetch {"name": $p.name};'
6872
).resolve()
73+
results = list(results)
6974
for json in results:
7075
print(json)
76+
assert len(results) == 2
77+
78+
79+
def test_connect_to_h2_endpoint_non_typedb():
80+
url = "https://s3.localhost.localstack.cloud:4566/"
81+
82+
# make an HTTP/2 request to the LocalStack health endpoint
83+
with httpx.Client(http2=True, verify=False, trust_env=False) as client:
84+
health_url = f"{url}/_localstack/health"
85+
response = client.get(health_url)
86+
87+
assert response.status_code == 200
88+
assert response.http_version == "HTTP/2"
89+
assert '"services":' in response.text
90+
91+
# make an HTTP/2 request to a LocalStack endpoint outside the extension (S3 list buckets)
92+
headers = {
93+
"Authorization": "AWS4-HMAC-SHA256 Credential=000000000000/20250101/us-east-1/s3/aws4_request, ..."
94+
}
95+
with httpx.Client(http2=True, verify=False, trust_env=False) as client:
96+
response = client.get(url, headers=headers)
97+
98+
assert response.status_code == 200
99+
assert response.http_version == "HTTP/2"
100+
assert "<ListAllMyBucketsResult" in response.text
71101

72102

73103
def test_get_frames_from_http2_stream():

0 commit comments

Comments
 (0)