Skip to content

Commit

Permalink
CLI: Load from file or module. Add software tests and documentation.
Browse files Browse the repository at this point in the history
Also, refactor to `responder.ext.cli`.
  • Loading branch information
amotl committed Oct 27, 2024
1 parent a6d048e commit cd3f238
Show file tree
Hide file tree
Showing 16 changed files with 896 additions and 52 deletions.
3 changes: 3 additions & 0 deletions .github/workflows/test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,9 @@ jobs:
- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- uses: actions/setup-node@v4
with:
node-version: 22
- uses: yezz123/setup-uv@v4

- name: Install package and run software tests (Python 3.6)
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
.pytest_cache
.DS_Store
coverage.xml
.coverage*

__pycache__
tests/__pycache__
Expand Down
7 changes: 5 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,14 @@ Install the most recent stable release:

pip install --upgrade 'responder'

Install package including CLI interface and GraphQL extension:
Install package with CLI and GraphQL support:

pip install --upgrade 'responder[cli,graphql]'

Or, install directly from the repository:
The CLI provides commands for running and building applications, while GraphQL
adds support for GraphQL query endpoints.

Alternatively, install directly from the repository:

pip install 'responder @ git+https://github.com/kennethreitz/responder.git'

Expand Down
90 changes: 90 additions & 0 deletions docs/source/cli.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
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. By default, it uses the current working directory,
where it expects a regular NPM ``package.json`` file.

.. code-block:: shell
responder build
When specifying a target directory, responder will change to that
directory beforehand.

.. code-block:: shell
responder build /path/to/project
.. _helloworld.py: https://github.com/kennethreitz/responder/blob/main/examples/helloworld.py
.. _HTTPie: https://httpie.io/docs/cli
1 change: 1 addition & 0 deletions docs/source/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ User Guides
deployment
testing
api
cli


Installing Responder
Expand Down
2 changes: 2 additions & 0 deletions examples/helloworld.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
# Example HTTP service definition, using Responder.
# https://pypi.org/project/responder/
import responder

api = responder.API()
Expand Down
15 changes: 15 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,21 @@ markers = [
]
xfail_strict = true

[tool.coverage.run]
branch = false
omit = [
"*.html",
"tests/*",
]

[tool.coverage.report]
fail_under = 0
show_missing = true
exclude_lines = [
"# pragma: no cover",
"raise NotImplemented",
]

[tool.poe.tasks]

check = [
Expand Down
7 changes: 7 additions & 0 deletions responder/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
"""
Responder - a familiar HTTP Service Framework.
This module exports the core functionality of the Responder framework,
including the API, Request, Response classes and CLI interface.
"""

from . import ext
from .core import API, Request, Response

Expand Down
46 changes: 0 additions & 46 deletions responder/cli.py

This file was deleted.

117 changes: 117 additions & 0 deletions responder/ext/cli.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
"""
Responder CLI.
A web framework for Python.
Commands:
run Start the application server
build Build frontend assets using npm
Usage:
responder
responder run [--debug] [--limit-max-requests=] <target>
responder build [<target>]
responder --version
Options:
-h --help Show this screen.
-v --version Show version.
--debug Enable debug mode with verbose logging.
--limit-max-requests=<n> Maximum number of requests to handle before shutting down.
Arguments:
<target> For run: Python module specifier (e.g., "app:api" loads api from app.py)
Format: "module.submodule:variable_name" where variable_name is your API instance
For build: Directory containing package.json (default: current directory)
Examples:
responder run app:api # Run the 'api' instance from app.py
responder run myapp/core.py:application # Run the 'application' instance from myapp/core.py
responder build # Build frontend assets
""" # noqa: E501

import logging
import platform
import subprocess
import sys
import typing as t
from pathlib import Path

import docopt

from responder.__version__ import __version__
from responder.util.python import load_target

logger = logging.getLogger(__name__)


def cli() -> None:
"""
Main entry point for the Responder CLI.
Parses command line arguments and executes the appropriate command.
Supports running the application, building assets, and displaying version info.
"""
args = docopt.docopt(__doc__, argv=None, version=__version__, options_first=False)
setup_logging(args["--debug"])

target: t.Optional[str] = args["<target>"]
build: bool = args["build"]
debug: bool = args["--debug"]
run: bool = args["run"]

if build:
target_path = Path(target).resolve() if target else Path.cwd()
if not target_path.is_dir() or not (target_path / "package.json").exists():
logger.error(
f"Invalid target directory or missing package.json: {target_path}"
)
sys.exit(1)
npm_cmd = "npm.cmd" if platform.system() == "Windows" else "npm"
try:
# # S603, S607 are addressed by validating the target directory.
subprocess.check_call( # noqa: S603, S607
[npm_cmd, "run", "build"],
cwd=target_path,
timeout=300,
)
except FileNotFoundError:
logger.error("npm not found. Please install Node.js and npm.")
sys.exit(1)
except subprocess.CalledProcessError as e:
logger.error(f"Build failed with exit code {e.returncode}")
sys.exit(1)

if run:
if not target:
logger.error("Target argument is required for run command")
sys.exit(1)

# Maximum request limit. Terminating afterward. Suitable for software testing.
limit_max_requests = args["--limit-max-requests"]
if limit_max_requests is not None:
try:
limit_max_requests = int(limit_max_requests)
if limit_max_requests <= 0:
logger.error("limit-max-requests must be a positive integer")
sys.exit(1)
except ValueError:
logger.error("limit-max-requests must be a valid integer")
sys.exit(1)

# Launch Responder API server (uvicorn).
api = load_target(target=target)
api.run(debug=debug, limit_max_requests=limit_max_requests)


def setup_logging(debug: bool) -> None:
"""
Configure logging based on debug mode.
Args:
debug: When True, sets logging level to DEBUG; otherwise, sets to INFO
"""
log_level = logging.DEBUG if debug else logging.INFO
logging.basicConfig(
level=log_level, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
)
Empty file added responder/util/__init__.py
Empty file.
Loading

0 comments on commit cd3f238

Please sign in to comment.