-
-
Notifications
You must be signed in to change notification settings - Fork 219
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
CLI: Load from file or module. Add software tests and documentation.
- Loading branch information
Showing
9 changed files
with
272 additions
and
19 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,82 @@ | ||
Responder CLI | ||
============= | ||
|
||
Responder installs a command line program ``responder``. Use it to launch | ||
a Responder application from a file or module. | ||
|
||
Launch application from file | ||
---------------------------- | ||
|
||
Acquire minimal example application, `helloworld.py`_, | ||
implementing a basic echo handler, and launch the HTTP service. | ||
|
||
.. code-block:: shell | ||
wget https://github.com/kennethreitz/responder/raw/refs/heads/main/examples/helloworld.py | ||
responder run helloworld.py | ||
In another terminal, invoke a HTTP request, for example using `HTTPie`_. | ||
|
||
.. code-block:: shell | ||
http http://127.0.0.1:5042/hello | ||
The response is no surprise. | ||
|
||
:: | ||
|
||
HTTP/1.1 200 OK | ||
content-length: 13 | ||
content-type: text/plain | ||
date: Sat, 26 Oct 2024 13:16:55 GMT | ||
encoding: utf-8 | ||
server: uvicorn | ||
|
||
hello, world! | ||
|
||
|
||
Launch application from module | ||
------------------------------ | ||
|
||
If your Responder application has been implemented as a Python module, | ||
launch it like this: | ||
|
||
.. code-block:: shell | ||
responder run acme.app | ||
That assumes a Python package ``acme`` including an ``app`` module | ||
``acme/app.py`` that includes an attribute ``api`` that refers | ||
to a ``responder.API`` instance, reflecting the typical layout of | ||
a standard Responder application. | ||
|
||
.. rubric:: Non-standard instance name | ||
|
||
When your attribute that references the ``responder.API`` instance | ||
is called differently than ``api``, append it to the launch target | ||
address like this: | ||
|
||
.. code-block:: shell | ||
responder run acme.app:service | ||
Within your ``app.py``, the instance would have been defined like this: | ||
|
||
.. code-block:: python | ||
service = responder.API() | ||
Build JavaScript application | ||
---------------------------- | ||
|
||
The ``build`` subcommand invokes ``npm run build``, optionally accepting | ||
a target directory. | ||
|
||
.. code-block:: shell | ||
responder build [<target>] | ||
.. _helloworld.py: https://github.com/kennethreitz/responder/blob/main/examples/helloworld.py | ||
.. _HTTPie: https://httpie.io/docs/cli |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -96,6 +96,7 @@ User Guides | |
deployment | ||
testing | ||
api | ||
cli | ||
|
||
|
||
Installing Responder | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
# ruff: noqa: S603, S607 | ||
import shutil | ||
import subprocess | ||
import threading | ||
|
||
RESPONDER_BIN = shutil.which("responder") | ||
|
||
|
||
class ResponderServer(threading.Thread): | ||
""" | ||
A little wrapper around `responder run`. Mostly used for testing. | ||
""" | ||
|
||
def __init__(self, target: str, port: int = 5042, limit_max_requests: int = None): | ||
super().__init__() | ||
self.target = target | ||
self.port = port | ||
self.limit_max_requests = limit_max_requests | ||
|
||
def run(self): | ||
command = [ | ||
RESPONDER_BIN, | ||
"run", | ||
self.target, | ||
] | ||
if self.limit_max_requests is not None: | ||
command += [f"--limit-max-requests={self.limit_max_requests}"] | ||
env = {} | ||
if self.port is not None: | ||
env = {"PORT": str(self.port)} | ||
subprocess.check_call(command, env=env) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
import importlib | ||
import importlib.util | ||
import sys | ||
from pathlib import Path | ||
|
||
|
||
def load_target(target: str, default_property: str = "api", method: str = "run"): | ||
""" | ||
Load Python code from file or module. | ||
""" | ||
|
||
# Decode launch target location address. | ||
# Module: acme.app:foo | ||
# Path: /path/to/acme/app.py:foo | ||
target_fragments = target.split(":") | ||
if len(target_fragments) > 1: | ||
target = target_fragments[0] | ||
prop = target_fragments[1] | ||
else: | ||
prop = default_property | ||
|
||
# Import launch target. Treat input location either as a filesystem path | ||
# (/path/to/acme/app.py), or as a module address specification (acme.app). | ||
if Path(target).exists(): | ||
app = load_file_module(target) | ||
else: | ||
app = importlib.import_module(target) | ||
|
||
# Invoke launch target. | ||
msg_prefix = f"Failed to import target '{target}'" | ||
try: | ||
api = getattr(app, prop, None) | ||
if api is None: | ||
raise AttributeError( | ||
f"{msg_prefix}: Module has no API instance attribute '{prop}'" | ||
) | ||
if not hasattr(api, method): | ||
raise AttributeError( | ||
f"{msg_prefix}: API instance '{prop}' has no method 'run'" | ||
) | ||
return api | ||
except ImportError as ex: | ||
raise ImportError(f"{msg_prefix}: {ex}") from ex | ||
|
||
|
||
def load_file_module(module: str): | ||
""" | ||
Load Python file as Python module. | ||
""" | ||
spec = importlib.util.spec_from_file_location("__app__", module) | ||
app = importlib.util.module_from_spec(spec) | ||
sys.modules["__app__"] = app | ||
spec.loader.exec_module(app) | ||
return app |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,18 +1,85 @@ | ||
# ruff: noqa: S603, S607 | ||
import json | ||
import subprocess | ||
import time | ||
|
||
import pytest | ||
import requests | ||
|
||
from responder.__version__ import __version__ | ||
from responder.util.cmd import RESPONDER_BIN, ResponderServer | ||
|
||
pytest.importorskip("docopt", reason="docopt-ng package not installed") | ||
|
||
|
||
def test_cli_version(capfd): | ||
# S603, S607 are suppressed as we're using fixed arguments, not user input | ||
""" | ||
Verify that `responder --version` works as expected. | ||
""" | ||
try: | ||
subprocess.check_call(["responder", "--version"]) # noqa: S603, S607 | ||
subprocess.check_call(["responder", "--version"]) | ||
except subprocess.CalledProcessError as ex: | ||
pytest.fail(f"CLI command failed with exit code {ex.returncode}") | ||
|
||
stdout = capfd.readouterr().out.strip() | ||
assert stdout == __version__ | ||
|
||
|
||
def test_cli_build(capfd, tmp_path): | ||
""" | ||
Verify that `responder build` works as expected. | ||
""" | ||
|
||
# Temporary surrogate `package.json` file. | ||
package_json = {"scripts": {"build": "echo foobar"}} | ||
package_json_file = tmp_path / "package.json" | ||
package_json_file.write_text(json.dumps(package_json)) | ||
|
||
# Invoke `responder build`. | ||
command = [ | ||
RESPONDER_BIN, | ||
"build", | ||
str(tmp_path), | ||
] | ||
subprocess.check_call(command) | ||
|
||
output = capfd.readouterr() | ||
|
||
stdout = output.out.strip() | ||
assert "foobar" in stdout | ||
|
||
|
||
def test_cli_run(capfd): | ||
""" | ||
Verify that `responder run` works as expected. | ||
""" | ||
|
||
# Invoke `responder run`. | ||
# Start a Responder service instance in the background, using its CLI. | ||
# Make it terminate itself after serving one HTTP request. | ||
server = ResponderServer( | ||
target="examples/helloworld.py", port=9876, limit_max_requests=1 | ||
) | ||
server.start() | ||
time.sleep(0.5) | ||
|
||
response = requests.get(f"http://127.0.0.1:{server.port}/hello", timeout=1) | ||
assert "hello, world!" == response.text | ||
server.join() | ||
|
||
output = capfd.readouterr() | ||
|
||
stdout = output.out.strip() | ||
assert '"GET /hello HTTP/1.1" 200 OK' in stdout | ||
|
||
stderr = output.err.strip() | ||
|
||
assert "Started server process" in stderr | ||
assert "Waiting for application startup" in stderr | ||
assert "Application startup complete" in stderr | ||
assert "Uvicorn running" in stderr | ||
|
||
assert "Shutting down" in stderr | ||
assert "Waiting for application shutdown" in stderr | ||
assert "Application shutdown complete" in stderr | ||
assert "Finished server process" in stderr |