Skip to content

Commit fc85617

Browse files
authored
Merge pull request #170 from dispatchrun/http-tests
test: decouple test components from httpx
2 parents fd42f89 + b197b74 commit fc85617

File tree

12 files changed

+116
-47
lines changed

12 files changed

+116
-47
lines changed

examples/auto_retry/test_app.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,10 @@
66
import unittest
77
from unittest import mock
88

9-
from fastapi.testclient import TestClient
10-
119
from dispatch import Client
1210
from dispatch.sdk.v1 import status_pb2 as status_pb
1311
from dispatch.test import DispatchServer, DispatchService, EndpointClient
12+
from dispatch.test.fastapi import http_client
1413

1514

1615
class TestAutoRetry(unittest.TestCase):
@@ -25,14 +24,14 @@ def test_app(self):
2524
from .app import app, dispatch
2625

2726
# Setup a fake Dispatch server.
28-
endpoint_client = EndpointClient(TestClient(app))
27+
app_client = http_client(app)
28+
endpoint_client = EndpointClient(app_client)
2929
dispatch_service = DispatchService(endpoint_client, collect_roundtrips=True)
3030
with DispatchServer(dispatch_service) as dispatch_server:
3131
# Use it when dispatching function calls.
3232
dispatch.set_client(Client(api_url=dispatch_server.url))
3333

34-
http_client = TestClient(app)
35-
response = http_client.get("/")
34+
response = app_client.get("/")
3635
self.assertEqual(response.status_code, 200)
3736

3837
dispatch_service.dispatch_calls()

examples/getting_started/test_app.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,9 @@
66
import unittest
77
from unittest import mock
88

9-
from fastapi.testclient import TestClient
10-
119
from dispatch import Client
1210
from dispatch.test import DispatchServer, DispatchService, EndpointClient
11+
from dispatch.test.fastapi import http_client
1312

1413

1514
class TestGettingStarted(unittest.TestCase):
@@ -24,14 +23,14 @@ def test_app(self):
2423
from .app import app, dispatch
2524

2625
# Setup a fake Dispatch server.
27-
endpoint_client = EndpointClient(TestClient(app))
26+
app_client = http_client(app)
27+
endpoint_client = EndpointClient(app_client)
2828
dispatch_service = DispatchService(endpoint_client, collect_roundtrips=True)
2929
with DispatchServer(dispatch_service) as dispatch_server:
3030
# Use it when dispatching function calls.
3131
dispatch.set_client(Client(api_url=dispatch_server.url))
3232

33-
http_client = TestClient(app)
34-
response = http_client.get("/")
33+
response = app_client.get("/")
3534
self.assertEqual(response.status_code, 200)
3635

3736
dispatch_service.dispatch_calls()

examples/github_stats/test_app.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,9 @@
66
import unittest
77
from unittest import mock
88

9-
from fastapi.testclient import TestClient
10-
119
from dispatch.function import Client
1210
from dispatch.test import DispatchServer, DispatchService, EndpointClient
11+
from dispatch.test.fastapi import http_client
1312

1413

1514
class TestGithubStats(unittest.TestCase):
@@ -24,14 +23,14 @@ def test_app(self):
2423
from .app import app, dispatch
2524

2625
# Setup a fake Dispatch server.
27-
endpoint_client = EndpointClient(TestClient(app))
26+
app_client = http_client(app)
27+
endpoint_client = EndpointClient(app_client)
2828
dispatch_service = DispatchService(endpoint_client, collect_roundtrips=True)
2929
with DispatchServer(dispatch_service) as dispatch_server:
3030
# Use it when dispatching function calls.
3131
dispatch.set_client(Client(api_url=dispatch_server.url))
3232

33-
http_client = TestClient(app)
34-
response = http_client.get("/")
33+
response = app_client.get("/")
3534
self.assertEqual(response.status_code, 200)
3635

3736
while dispatch_service.queue:

pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,16 @@ dependencies = [
1515
"grpc-stubs >= 1.53.0.5",
1616
"http-message-signatures >= 0.4.4",
1717
"tblib >= 3.0.0",
18-
"httpx >= 0.27.0",
1918
"typing_extensions >= 4.10"
2019
]
2120

2221
[project.optional-dependencies]
2322
fastapi = ["fastapi", "httpx"]
23+
flask = ["flask"]
2424
lambda = ["awslambdaric"]
2525

2626
dev = [
27+
"httpx >= 0.27.0",
2728
"black >= 24.1.0",
2829
"isort >= 5.13.2",
2930
"mypy >= 1.10.0",

src/dispatch/test/client.py

Lines changed: 11 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
from datetime import datetime
2-
from typing import Optional
2+
from typing import Mapping, Optional, Protocol, Union
33

44
import grpc
5-
import httpx
65

76
from dispatch.sdk.v1 import function_pb2 as function_pb
87
from dispatch.sdk.v1 import function_pb2_grpc as function_grpc
@@ -12,6 +11,7 @@
1211
Request,
1312
sign_request,
1413
)
14+
from dispatch.test.http import HttpClient
1515

1616

1717
class EndpointClient:
@@ -24,15 +24,15 @@ class EndpointClient:
2424
"""
2525

2626
def __init__(
27-
self, http_client: httpx.Client, signing_key: Optional[Ed25519PrivateKey] = None
27+
self, http_client: HttpClient, signing_key: Optional[Ed25519PrivateKey] = None
2828
):
2929
"""Initialize the client.
3030
3131
Args:
3232
http_client: Client to use to make HTTP requests.
3333
signing_key: Optional Ed25519 private key to use to sign requests.
3434
"""
35-
channel = _HttpxGrpcChannel(http_client, signing_key=signing_key)
35+
channel = _HttpGrpcChannel(http_client, signing_key=signing_key)
3636
self._stub = function_grpc.FunctionServiceStub(channel)
3737

3838
def run(self, request: function_pb.RunRequest) -> function_pb.RunResponse:
@@ -46,16 +46,10 @@ def run(self, request: function_pb.RunRequest) -> function_pb.RunResponse:
4646
"""
4747
return self._stub.Run(request)
4848

49-
@classmethod
50-
def from_url(cls, url: str, signing_key: Optional[Ed25519PrivateKey] = None):
51-
"""Returns an EndpointClient for a Dispatch endpoint URL."""
52-
http_client = httpx.Client(base_url=url)
53-
return EndpointClient(http_client, signing_key)
5449

55-
56-
class _HttpxGrpcChannel(grpc.Channel):
50+
class _HttpGrpcChannel(grpc.Channel):
5751
def __init__(
58-
self, http_client: httpx.Client, signing_key: Optional[Ed25519PrivateKey] = None
52+
self, http_client: HttpClient, signing_key: Optional[Ed25519PrivateKey] = None
5953
):
6054
self.http_client = http_client
6155
self.signing_key = signing_key
@@ -120,9 +114,11 @@ def __call__(
120114
wait_for_ready=None,
121115
compression=None,
122116
):
117+
url = self.client.url_for(self.method) # note: method==path in gRPC parlance
118+
123119
request = Request(
124120
method="POST",
125-
url=str(httpx.URL(self.client.base_url).join(self.method)),
121+
url=url,
126122
body=self.request_serializer(request),
127123
headers=CaseInsensitiveDict({"Content-Type": "application/grpc+proto"}),
128124
)
@@ -131,10 +127,10 @@ def __call__(
131127
sign_request(request, self.signing_key, datetime.now())
132128

133129
response = self.client.post(
134-
request.url, content=request.body, headers=request.headers
130+
request.url, body=request.body, headers=request.headers
135131
)
136132
response.raise_for_status()
137-
return self.response_deserializer(response.content)
133+
return self.response_deserializer(response.body)
138134

139135
def with_call(
140136
self,

src/dispatch/test/fastapi.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
from fastapi import FastAPI
2+
from fastapi.testclient import TestClient
3+
4+
import dispatch.test.httpx
5+
from dispatch.test.client import HttpClient
6+
7+
8+
def http_client(app: FastAPI) -> HttpClient:
9+
"""Build a client for a FastAPI app."""
10+
return dispatch.test.httpx.Client(TestClient(app))

src/dispatch/test/http.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
from dataclasses import dataclass
2+
from typing import Mapping, Protocol
3+
4+
5+
@dataclass
6+
class HttpResponse(Protocol):
7+
status_code: int
8+
body: bytes
9+
10+
def raise_for_status(self):
11+
"""Raise an exception on non-2xx responses."""
12+
...
13+
14+
15+
class HttpClient(Protocol):
16+
"""Protocol for HTTP clients."""
17+
18+
def get(self, url: str, headers: Mapping[str, str] = {}) -> HttpResponse:
19+
"""Make a GET request."""
20+
...
21+
22+
def post(
23+
self, url: str, body: bytes, headers: Mapping[str, str] = {}
24+
) -> HttpResponse:
25+
"""Make a POST request."""
26+
...
27+
28+
def url_for(self, path: str) -> str:
29+
"""Get the fully-qualified URL for a path."""
30+
...

src/dispatch/test/httpx.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
from typing import Mapping
2+
3+
import httpx
4+
5+
from dispatch.test.http import HttpClient, HttpResponse
6+
7+
8+
class Client(HttpClient):
9+
def __init__(self, client: httpx.Client):
10+
self.client = client
11+
12+
def get(self, url: str, headers: Mapping[str, str] = {}) -> HttpResponse:
13+
response = self.client.get(url, headers=headers)
14+
return Response(response)
15+
16+
def post(
17+
self, url: str, body: bytes, headers: Mapping[str, str] = {}
18+
) -> HttpResponse:
19+
response = self.client.post(url, content=body, headers=headers)
20+
return Response(response)
21+
22+
def url_for(self, path: str) -> str:
23+
return str(httpx.URL(self.client.base_url).join(path))
24+
25+
26+
class Response(HttpResponse):
27+
def __init__(self, response: httpx.Response):
28+
self.response = response
29+
30+
@property
31+
def status_code(self):
32+
return self.response.status_code
33+
34+
@property
35+
def body(self):
36+
return self.response.content
37+
38+
def raise_for_status(self):
39+
self.response.raise_for_status()

src/dispatch/test/service.py

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
from typing import Dict, List, Optional, Set, Tuple
99

1010
import grpc
11-
import httpx
1211
from typing_extensions import TypeAlias
1312

1413
import dispatch.sdk.v1.call_pb2 as call_pb
@@ -325,17 +324,6 @@ def _dispatch_continuously(self):
325324

326325
try:
327326
self.dispatch_calls()
328-
except httpx.HTTPStatusError as e:
329-
if e.response.status_code == 403:
330-
logger.error(
331-
"error dispatching function call to endpoint (403). Is the endpoint's DISPATCH_VERIFICATION_KEY correct?"
332-
)
333-
else:
334-
logger.exception(e)
335-
except httpx.ConnectError as e:
336-
logger.error(
337-
"error connecting to the endpoint. Is it running and accessible from DISPATCH_ENDPOINT_URL?"
338-
)
339327
except Exception as e:
340328
logger.exception(e)
341329

tests/test_client.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,20 @@
22
import unittest
33
from unittest import mock
44

5+
import httpx
6+
7+
import dispatch.test.httpx
58
from dispatch import Call, Client
69
from dispatch.proto import _any_unpickle as any_unpickle
710
from dispatch.test import DispatchServer, DispatchService, EndpointClient
811

912

1013
class TestClient(unittest.TestCase):
1114
def setUp(self):
12-
endpoint_client = EndpointClient.from_url("http://function-service")
15+
http_client = dispatch.test.httpx.Client(
16+
httpx.Client(base_url="http://function-service")
17+
)
18+
endpoint_client = EndpointClient(http_client)
1319

1420
api_key = "0000000000000000"
1521
self.dispatch_service = DispatchService(endpoint_client, api_key)

0 commit comments

Comments
 (0)