Skip to content

Commit e0a1b1d

Browse files
authored
Merge pull request #126 from stealthrocket/test-server
Mock server
2 parents 1eb3458 + f91a121 commit e0a1b1d

File tree

8 files changed

+233
-30
lines changed

8 files changed

+233
-30
lines changed

README.md

Lines changed: 42 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,9 @@ Python package to develop applications with the Dispatch platform.
2222
- [Usage](#usage)
2323
- [Configuration](#configuration)
2424
- [Integration with FastAPI](#integration-with-fastapi)
25-
- [Local testing with ngrok](#local-testing-with-ngrok)
26-
- [Distributed coroutines for Python](#distributed-coroutines-for-python)
25+
- [Local Testing](#local-testing)
26+
- [Distributed Coroutines for Python](#distributed-coroutines-for-python)
27+
- [Serialization](#serialization)
2728
- [Examples](#examples)
2829
- [Contributing](#contributing)
2930

@@ -123,10 +124,46 @@ program, driven by the Dispatch SDK.
123124
The instantiation of the `Dispatch` object on the `FastAPI` application
124125
automatically installs the HTTP route needed for Dispatch to invoke functions.
125126

126-
### Local testing with ngrok
127+
### Local Testing
127128

128-
To enable local testing, a common approach consists of using [ngrok][ngrok] to
129-
setup a public endpoint that forwards to the server running on localhost.
129+
#### Mock Dispatch
130+
131+
The SDK ships with a mock Dispatch server. It can be used to quickly test your
132+
local functions, without requiring internet access.
133+
134+
Note that the mock Dispatch server has very limited scheduling capabilities.
135+
136+
```console
137+
python -m dispatch.test $DISPATCH_ENDPOINT_URL
138+
```
139+
140+
The command will start a mock Dispatch server and print the configuration
141+
for the SDK.
142+
143+
For example, if your functions were exposed through a local endpoint
144+
listening on `http://127.0.0.1:8000`, you could run:
145+
146+
```console
147+
$ python -m dispatch.test http://127.0.0.1:8000
148+
Spawned a mock Dispatch server on 127.0.0.1:4450
149+
150+
Dispatching function calls to the endpoint at http://127.0.0.1:8000
151+
152+
The Dispatch SDK can be configured with:
153+
154+
export DISPATCH_API_URL="http://127.0.0.1:4450"
155+
export DISPATCH_API_KEY="test"
156+
export DISPATCH_ENDPOINT_URL="http://127.0.0.1:8000"
157+
export DISPATCH_VERIFICATION_KEY="Z+nTe2VRcw8t8Ihx++D+nXtbO28nwjWIOTLRgzrelYs="
158+
```
159+
160+
#### Real Dispatch
161+
162+
To test local functions with the production instance of Dispatch, it needs
163+
to be able to access your local endpoint.
164+
165+
A common approach consists of using [ngrok][ngrok] to setup a public endpoint
166+
that forwards to the server running on localhost.
130167

131168
For example, assuming the server is running on port 8000 (which is the default
132169
with FastAPI), the command to create a ngrok tunnel is:

pyproject.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@ dev = [
3030
"coverage >= 7.4.1",
3131
"requests >= 2.31.0",
3232
"types-requests >= 2.31.0.20240125",
33+
"docopt >= 0.6.2",
34+
"types-docopt >= 0.6.11.4",
35+
"uvicorn >= 0.28.0"
3336
]
3437

3538
docs = [

src/dispatch/scheduler.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -329,8 +329,8 @@ def _run(self, input: Input) -> Output:
329329
coroutine_id=coroutine.id, value=e.value
330330
)
331331
except Exception as e:
332-
logger.exception(
333-
f"@dispatch.function: '{coroutine}' raised an exception"
332+
logger.debug(
333+
f"@dispatch.function: '{coroutine}' raised an exception", exc_info=e
334334
)
335335
coroutine_result = CoroutineResult(coroutine_id=coroutine.id, error=e)
336336

src/dispatch/status.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,12 @@ class Status(int, enum.Enum):
3030

3131
_proto: status_pb.Status
3232

33+
def __repr__(self):
34+
return self.name
35+
36+
def __str__(self):
37+
return self.name
38+
3339

3440
# Maybe we should find a better way to define that enum. It's that way to please
3541
# Mypy and provide documentation for the enum values.

src/dispatch/test/__main__.py

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
"""Mock Dispatch server for use in test environments.
2+
3+
Usage:
4+
dispatch.test <endpoint> [--api-key=<key>] [--hostname=<name>] [--port=<port>] [-v | --verbose]
5+
dispatch.test -h | --help
6+
7+
Options:
8+
--api-key=<key> API key to require when clients connect to the server [default: test].
9+
10+
--hostname=<name> Hostname to listen on [default: 127.0.0.1].
11+
--port=<port> Port to listen on [default: 4450].
12+
13+
-v --verbose Show verbose details in the log.
14+
-h --help Show this help information.
15+
"""
16+
17+
import base64
18+
import logging
19+
import os
20+
import sys
21+
22+
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
23+
from docopt import docopt
24+
25+
from dispatch.test import DispatchServer, DispatchService, EndpointClient
26+
27+
28+
def main():
29+
args = docopt(__doc__)
30+
31+
if args["--help"]:
32+
print(__doc__)
33+
exit(0)
34+
35+
endpoint = args["<endpoint>"]
36+
api_key = args["--api-key"]
37+
hostname = args["--hostname"]
38+
port_str = args["--port"]
39+
40+
try:
41+
port = int(port_str)
42+
except ValueError:
43+
print(f"error: invalid port: {port_str}", file=sys.stderr)
44+
exit(1)
45+
46+
if not os.getenv("NO_COLOR"):
47+
logging.addLevelName(logging.WARNING, f"\033[1;33mWARN\033[1;0m")
48+
logging.addLevelName(logging.ERROR, "\033[1;31mERROR\033[1;0m")
49+
50+
logger = logging.getLogger()
51+
if args["--verbose"]:
52+
logger.setLevel(logging.DEBUG)
53+
fmt = "%(asctime)s [%(levelname)s] %(name)s - %(message)s"
54+
else:
55+
logger.setLevel(logging.INFO)
56+
fmt = "%(asctime)s [%(levelname)s] %(message)s"
57+
logging.getLogger("httpx").disabled = True
58+
59+
log_formatter = logging.Formatter(fmt=fmt, datefmt="%Y-%m-%d %H:%M:%S")
60+
log_handler = logging.StreamHandler(sys.stderr)
61+
log_handler.setFormatter(log_formatter)
62+
logger.addHandler(log_handler)
63+
64+
# This private key was generated randomly.
65+
signing_key = Ed25519PrivateKey.from_private_bytes(
66+
b"\x0e\xca\xfb\xc9\xa9Gc'fR\xe4\x97y\xf0\xae\x90\x01\xe8\xd9\x94\xa6\xd4@\xf6\xa7!\x90b\\!z!"
67+
)
68+
verification_key = base64.b64encode(
69+
signing_key.public_key().public_bytes_raw()
70+
).decode()
71+
72+
endpoint_client = EndpointClient.from_url(endpoint, signing_key=signing_key)
73+
74+
with DispatchService(endpoint_client, api_key=api_key) as service:
75+
with DispatchServer(service, hostname=hostname, port=port) as server:
76+
print(f"Spawned a mock Dispatch server on {hostname}:{port}")
77+
print()
78+
print(f"Dispatching function calls to the endpoint at {endpoint}")
79+
print()
80+
print("The Dispatch SDK can be configured with:")
81+
print()
82+
print(f' export DISPATCH_API_URL="http://{hostname}:{port}"')
83+
print(f' export DISPATCH_API_KEY="{api_key}"')
84+
print(f' export DISPATCH_ENDPOINT_URL="{endpoint}"')
85+
print(f' export DISPATCH_VERIFICATION_KEY="{verification_key}"')
86+
print()
87+
88+
server.wait()
89+
90+
91+
if __name__ == "__main__":
92+
main()

src/dispatch/test/server.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,10 @@ def start(self):
3939
"""Start the server."""
4040
self._server.start()
4141

42+
def wait(self):
43+
"""Block until the server terminates."""
44+
self._server.wait_for_termination()
45+
4246
def stop(self):
4347
"""Stop the server."""
4448
self._server.stop(0)

0 commit comments

Comments
 (0)