Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add dev_server library #173

Merged
merged 12 commits into from
Oct 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ jobs:
strategy:
matrix:
python-version: ["3.9", "3.12"]
timeout-minutes: 5
steps:
- uses: "actions/checkout@v2"
- name: "Set up Python ${{ matrix.python-version }}"
Expand Down
5 changes: 5 additions & 0 deletions inngest/experimental/dev_server/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# ruff: noqa: D104

from .dev_server import server

__all__ = ["server"]
117 changes: 117 additions & 0 deletions inngest/experimental/dev_server/dev_server.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
# ruff: noqa: S603, S607, T201

import os
import subprocess
import threading
import time
import typing

import httpx


class _Server:
@property
def origin(self) -> str:
return f"http://0.0.0.0:{self.port}"

def __init__(self) -> None:
self._enabled = os.getenv("DEV_SERVER_ENABLED") != "0"
self._output_thread: typing.Optional[threading.Thread] = None

port: int
dev_server_port_env_var = os.getenv("DEV_SERVER_PORT")
if dev_server_port_env_var:
port = int(dev_server_port_env_var)
else:
port = 8288
self.port = port

self._process: typing.Optional[subprocess.Popen[str]] = None
self._ready_event = threading.Event()
self._stop_event = threading.Event()

def start(self) -> None:
if self._enabled is False:
return

print("Dev Server: starting")

if self._process:
raise Exception("Dev Server is already running")

self._process = subprocess.Popen(
[
"npx",
"--yes",
"inngest-cli@latest",
"dev",
"--no-discovery",
"--no-poll",
"--port",
f"{self.port}",
],
bufsize=1,
stderr=subprocess.STDOUT,
stdout=subprocess.PIPE,
text=True,
universal_newlines=True,
)
Comment on lines +42 to +58
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might be good to move away from npx for the Python SDK when this comes out of experimental - npm could be an extra tool folks have to install.

Not sure what we've move to, though, outside of managing a /tmp/ version where we install the binary directly.


self._ready_event.clear()
self._output_thread = threading.Thread(target=self._print_output)
self._output_thread.start()
self._wait_for_server()

def _print_output(self) -> None:
if self._process is None:
raise Exception("missing process")
if self._process.stdout is None:
raise Exception("missing stdout")

for line in self._process.stdout:
if self._stop_event.is_set():
break

if self._ready_event.is_set() is False:
print(line, end="")

def _wait_for_server(self) -> None:
print("Dev Server: waiting for start")

while not self._ready_event.is_set():
try:
httpx.get(f"http://127.0.0.1:{self.port}")
self._ready_event.set()
break
except Exception:
time.sleep(0.1)

print("Dev Server: started")

def stop(self) -> None:
if self._enabled is False:
return

print("Dev Server: stopping")

if self._output_thread is None:
raise Exception("missing output thread")
if self._process is None:
raise Exception("missing process")

self._process.terminate()

# Try to gracefully stop but kill it if that fails.
try:
self._process.wait(timeout=5)
except subprocess.TimeoutExpired:
self._process.kill()
self._process.wait(timeout=5)

self._stop_event.set()
self._output_thread.join()

print("Dev Server: stopped")


server = _Server()
83 changes: 42 additions & 41 deletions inngest/experimental/mocked/trigger_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,12 @@
import pytest

import inngest
from inngest.experimental import mocked

from .client import Inngest
from .consts import Status, Timeout
from .errors import UnstubbedStepError
from .trigger import trigger

client = Inngest(app_id="test")
client = inngest.Inngest(app_id="my-app")
client_mock = mocked.Inngest(app_id="test")


class TestTriggerAsync(unittest.TestCase):
Expand All @@ -31,8 +30,8 @@ async def fn(
)
)

res = trigger(fn, inngest.Event(name="test"), client)
assert res.status is Status.COMPLETED
res = mocked.trigger(fn, inngest.Event(name="test"), client_mock)
assert res.status is mocked.Status.COMPLETED
assert res.output == ("a", "b")


Expand All @@ -48,8 +47,8 @@ def fn(
) -> str:
return "hi"

res = trigger(fn, inngest.Event(name="test"), client)
assert res.status is Status.COMPLETED
res = mocked.trigger(fn, inngest.Event(name="test"), client_mock)
assert res.status is mocked.Status.COMPLETED
assert res.output == "hi"

def test_two_steps(self) -> None:
Expand All @@ -65,11 +64,17 @@ def fn(
step.run("b", lambda: None)
return "hi"

res = trigger(fn, inngest.Event(name="test"), client)
assert res.status is Status.COMPLETED
res = mocked.trigger(fn, inngest.Event(name="test"), client_mock)
assert res.status is mocked.Status.COMPLETED
assert res.output == "hi"

def test_client_send(self) -> None:
"""
TODO: Figure out how to support this use case. Since the client in the
Inngest function is real, it's trying to send the event to a real
Inngest server.
"""

@client.create_function(
fn_id="test",
trigger=inngest.TriggerEvent(event="test"),
Expand All @@ -85,12 +90,8 @@ def fn(
]
)

res = trigger(fn, inngest.Event(name="test"), client)
assert res.status is Status.COMPLETED
assert res.output == [
"00000000000000000000000000",
"00000000000000000000000000",
]
res = mocked.trigger(fn, inngest.Event(name="test"), client_mock)
assert res.status is mocked.Status.FAILED

def test_send_event(self) -> None:
@client.create_function(
Expand All @@ -109,8 +110,8 @@ def fn(
],
)

res = trigger(fn, inngest.Event(name="test"), client)
assert res.status is Status.COMPLETED
res = mocked.trigger(fn, inngest.Event(name="test"), client_mock)
assert res.status is mocked.Status.COMPLETED
assert res.output == [
"00000000000000000000000000",
"00000000000000000000000000",
Expand All @@ -131,13 +132,13 @@ def fn(
function_id="bar",
)

res = trigger(
res = mocked.trigger(
fn,
inngest.Event(name="test"),
client,
client_mock,
step_stubs={"a": "hi"},
)
assert res.status is Status.COMPLETED
assert res.status is mocked.Status.COMPLETED
assert res.output == "hi"

def test_parallel(self) -> None:
Expand All @@ -156,8 +157,8 @@ def fn(
)
)

res = trigger(fn, inngest.Event(name="test"), client)
assert res.status is Status.COMPLETED
res = mocked.trigger(fn, inngest.Event(name="test"), client_mock)
assert res.status is mocked.Status.COMPLETED
assert res.output == ("a", "b")

def test_sleep(self) -> None:
Expand All @@ -172,8 +173,8 @@ def fn(
step.sleep("a", datetime.timedelta(seconds=1))
return "hi"

res = trigger(fn, inngest.Event(name="test"), client)
assert res.status is Status.COMPLETED
res = mocked.trigger(fn, inngest.Event(name="test"), client_mock)
assert res.status is mocked.Status.COMPLETED
assert res.output == "hi"

def test_wait_for_event(self) -> None:
Expand All @@ -193,15 +194,15 @@ def fn(
assert event is not None
return event.data

res = trigger(
res = mocked.trigger(
fn,
inngest.Event(name="test"),
client,
client_mock,
step_stubs={
"a": inngest.Event(data={"foo": 1}, name="other-event")
},
)
assert res.status is Status.COMPLETED
assert res.status is mocked.Status.COMPLETED
assert res.output == {"foo": 1}

def test_wait_for_event_timeout(self) -> None:
Expand All @@ -220,13 +221,13 @@ def fn(
)
assert event is None

res = trigger(
res = mocked.trigger(
fn,
inngest.Event(name="test"),
client,
step_stubs={"a": Timeout},
client_mock,
step_stubs={"a": mocked.Timeout},
)
assert res.status is Status.COMPLETED
assert res.status is mocked.Status.COMPLETED

def test_wait_for_event_not_stubbed(self) -> None:
@client.create_function(
Expand All @@ -244,7 +245,7 @@ def fn(
)

with pytest.raises(UnstubbedStepError):
trigger(fn, inngest.Event(name="test"), client)
mocked.trigger(fn, inngest.Event(name="test"), client_mock)

def test_retry_step(self) -> None:
counter = 0
Expand All @@ -266,8 +267,8 @@ def a() -> str:

return step.run("a", a)

res = trigger(fn, inngest.Event(name="test"), client)
assert res.status is Status.COMPLETED
res = mocked.trigger(fn, inngest.Event(name="test"), client_mock)
assert res.status is mocked.Status.COMPLETED
assert res.output == "hi"

def test_fail_step(self) -> None:
Expand All @@ -285,8 +286,8 @@ def a() -> None:

step.run("a", a)

res = trigger(fn, inngest.Event(name="test"), client)
assert res.status is Status.FAILED
res = mocked.trigger(fn, inngest.Event(name="test"), client_mock)
assert res.status is mocked.Status.FAILED
assert res.output is None
assert isinstance(res.error, Exception)
assert str(res.error) == "oh no"
Expand All @@ -308,8 +309,8 @@ def fn(
raise Exception("oh no")
return "hi"

res = trigger(fn, inngest.Event(name="test"), client)
assert res.status is Status.COMPLETED
res = mocked.trigger(fn, inngest.Event(name="test"), client_mock)
assert res.status is mocked.Status.COMPLETED
assert res.output == "hi"

def test_fail_fn(self) -> None:
Expand All @@ -324,8 +325,8 @@ def fn(
) -> None:
raise Exception("oh no")

res = trigger(fn, inngest.Event(name="test"), client)
assert res.status is Status.FAILED
res = mocked.trigger(fn, inngest.Event(name="test"), client_mock)
assert res.status is mocked.Status.FAILED
assert res.output is None
assert isinstance(res.error, Exception)
assert str(res.error) == "oh no"
6 changes: 3 additions & 3 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import pytest

from . import dev_server
from inngest.experimental import dev_server

pytest.register_assert_rewrite("tests")


def pytest_configure(config: pytest.Config) -> None:
dev_server.singleton.start()
dev_server.server.start()


def pytest_unconfigure(config: pytest.Config) -> None:
dev_server.singleton.stop()
dev_server.server.stop()
Loading
Loading