Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
6e4e9d4
HTTP API settings
zz1874 Jan 18, 2024
eab5127
Create an argparse.Namespace
zz1874 Jan 19, 2024
72087ee
Add run function from cli_pydantic
zz1874 Jan 19, 2024
1734c80
Adjust enrich_args_via_cfg to http api
zz1874 Jan 19, 2024
e0e3a6f
Run adjusted enrich_args_via_cfg in http api
zz1874 Jan 19, 2024
67182dd
Re-organize cli_pydantic.py to run looper run via CLI and http-api
zz1874 Jan 19, 2024
6346654
Slight refactor of `create_argparse_namespace`
simeoncarstens Jan 19, 2024
e1f7308
Remove `run` from route
simeoncarstens Jan 19, 2024
dd978c8
Capture stderr / stdout and return in HTTP response
simeoncarstens Jan 19, 2024
e010f75
Rename `run_endpoint` -> `main_endpoint`
simeoncarstens Jan 19, 2024
8af2bb2
Add response model
simeoncarstens Jan 19, 2024
a89e7bc
Add a comment about the endpoint likely being blocking
simeoncarstens Jan 19, 2024
1880372
Apply formatter
zz1874 Jan 22, 2024
42119f0
Add logger def to be captured by API and also CLI
zz1874 Jan 22, 2024
f0c749d
Add README for the API
zz1874 Jan 22, 2024
6d146b5
Add endpoint "\status" to capture UUID
zz1874 Jan 23, 2024
59869fa
Create 2-step job submission and result workflow
zz1874 Jan 24, 2024
384a898
Allow `None` stderr / stdout in job model
simeoncarstens Jan 24, 2024
0161cc6
Run `run_looper()` in FastAPI background task
simeoncarstens Jan 24, 2024
67c5d34
Document / make self-documenting `Job` fields
simeoncarstens Jan 24, 2024
8869378
Make `/` route return a 202 (Accepted) HTTP status code
simeoncarstens Jan 24, 2024
e76c135
Replace job UUID with a shorter random string
simeoncarstens Jan 24, 2024
adc451a
Reorder imports
simeoncarstens Jan 24, 2024
a3b85de
Fix a typo
simeoncarstens Jan 24, 2024
ea81cb1
Remove `uuid` dependency
simeoncarstens Jan 27, 2024
279d24f
Change `yacman` dependency to a hacked, but threadable version
simeoncarstens Jan 27, 2024
4c88788
Add lower bound for FastAPI version
simeoncarstens Jan 27, 2024
a82a8f7
Add lower bound for uvicorn version
simeoncarstens Jan 27, 2024
f995b47
Make background task function non-`async`
simeoncarstens Jan 27, 2024
3c54546
[DELETE ME] hack to use local `yacman` copy
simeoncarstens Jan 29, 2024
9b3a1da
Don't call `logmuse.init_logger()` in `looper.__init__.py`
simeoncarstens Jan 29, 2024
16f0ab5
Explicitly initialize `logmuse` logger with `sys.stderr` as stream
simeoncarstens Jan 29, 2024
2370aa6
Selectively capture logs from separate jobs
zz1874 Jan 30, 2024
8ffaef7
Add source for `stdout_redirects.py`
simeoncarstens Jan 30, 2024
d8ae6ec
Add a comment about not calling `stdout_redirect.stop_redirect()`
simeoncarstens Jan 30, 2024
db9f8f5
Remove superfluous import
simeoncarstens Jan 30, 2024
ad621c6
Remove `progress` field from `Job` model
simeoncarstens Jan 30, 2024
95278b3
Capture subprocess output to `sys.stdout`/`sys.stderr`
simeoncarstens Jan 30, 2024
6870bcd
Make CLI for HTTP API server
simeoncarstens Feb 1, 2024
8ead693
Add entry point console script for HTTP API server
simeoncarstens Feb 1, 2024
8b1b2ca
Replace `stdout` / `stderr` job fields with `job_output` field
simeoncarstens Feb 1, 2024
0d6b016
Add return type to `/status` endpoint
simeoncarstens Feb 1, 2024
2b4a962
Add HTTP API Documentation (#3)
zz1874 Feb 1, 2024
6619440
Run formatter
simeoncarstens Feb 1, 2024
b3aa4aa
Make HTTP API code Python 3.8 compatible
simeoncarstens Feb 2, 2024
d75942c
Update README with more detailed usage instructions
simeoncarstens Feb 2, 2024
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
4 changes: 0 additions & 4 deletions looper/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,6 @@

"""

import logmuse

logmuse.init_logger("looper")

from .divvy import ComputingConfiguration, select_divvy_config
from .divvy import DEFAULT_COMPUTE_RESOURCES_NAME
from .divvy import NEW_COMPUTE_KEY as COMPUTE_KEY
Expand Down
34 changes: 34 additions & 0 deletions looper/api/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Looper HTTP API

## Overview

This API provides an HTTP interface for running the `looper` commands, allowing users to interact with Looper via HTTP requests.

## Usage
### Running the server
Run the app:
```bash
looper-serve [--host <host IP address>] [--port <port>]
```

> [!NOTE]
This assumes that all files specified in the arguments are available on the file system of the machine that is running the HTTP API server. Best make sure you use absolute file paths in all `looper` YAML configuration files.

### Sending requests
To test this, you can clone the [`hello_looper`](https://github.com/pepkit/hello_looper) repository and then run (for example) the following in a second terminal:
```bash
curl -X POST -H "Content-Type: application/json" -d '{"run": {"time_delay": 5}, "looper_config": "/path/to/hello_looper/.looper.yaml"}' "http://127.0.0.1:8000"
```
This will return a six-letter job ID, say `abc123`. Then get the result / output of the run with
```bash
curl -X GET -v localhost:8000/status/abc123
```
For better visualization / readability, you can post-process the output by piping it to `jq` (` | jq -r .console_output`).

## API Documentation
The API documentation is automatically generated and can be accessed in your web browser:

Swagger UI: http://127.0.0.1:8000/docs
ReDoc: http://127.0.0.1:8000/redoc

Explore the API documentation to understand available endpoints, request parameters, and response formats.
Empty file added looper/api/__init__.py
Empty file.
126 changes: 126 additions & 0 deletions looper/api/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
from argparse import ArgumentParser, Namespace
import secrets
from typing import Dict, TypeAlias

import fastapi
import pydantic
import uvicorn

from fastapi import FastAPI

from looper.cli_pydantic import run_looper
from looper.command_models.commands import SUPPORTED_COMMANDS, TopLevelParser

from looper.api import stdout_redirects

stdout_redirects.enable_proxy()


class Job(pydantic.BaseModel):
id: str = pydantic.Field(
default_factory=lambda: secrets.token_urlsafe(4),
description="The unique identifier of the job",
)
status: str = pydantic.Field(
default="in_progress",
description="The current status of the job. Can be either `in_progress` or `completed`.",
)
console_output: str | None = pydantic.Field(
default=None,
description="Console output produced by `looper` while performing the requested action",
)


app = FastAPI(validate_model=True)
jobs: Dict[str, Job] = {}


def background_async(top_level_model: TopLevelParser, job_id: str) -> None:
argparse_namespace = create_argparse_namespace(top_level_model)
output_stream = stdout_redirects.redirect()

run_looper(argparse_namespace, None, True)

# Here, we should call `stdout_redirects.stop_redirect()`, but that fails for reasons discussed
# in the following issue: https://github.com/python/cpython/issues/80374
# But this *seems* not to pose any problems.
jobs[job_id].status = "completed"
jobs[job_id].console_output = output_stream.getvalue()


def create_argparse_namespace(top_level_model: TopLevelParser) -> Namespace:
"""
Converts a TopLevelParser instance into an argparse.Namespace object.

This function takes a TopLevelParser instance, and converts it into an
argparse.Namespace object. It includes handling for supported commands
specified in SUPPORTED_COMMANDS.

:param TopLevelParser top_level_model: An instance of the TopLevelParser
model
:return argparse.Namespace: An argparse.Namespace object representing
the parsed command-line arguments.
"""
namespace = Namespace()

for argname, value in vars(top_level_model).items():
if argname not in [cmd.name for cmd in SUPPORTED_COMMANDS]:
setattr(namespace, argname, value)
else:
command_namespace = Namespace()
command_namespace_args = value
for command_argname, command_arg_value in vars(
command_namespace_args
).items():
setattr(
command_namespace,
command_argname,
command_arg_value,
)
setattr(namespace, argname, command_namespace)
return namespace


@app.post(
"/",
status_code=202,
summary="Run Looper",
description="Start a `looper` command with arguments specified in "
"`top_level_model` in the background and return a job identifier.",
)
async def main_endpoint(
top_level_model: TopLevelParser, background_tasks: fastapi.BackgroundTasks
) -> Dict:
job = Job()
jobs[job.id] = job
background_tasks.add_task(background_async, top_level_model, job.id)
return {"job_id": job.id}


@app.get(
"/status/{job_id}",
summary="Get job status",
description="Retrieve the status of a job based on its unique identifier.",
)
async def get_status(job_id: str) -> Job:
return jobs[job_id]


def main() -> None:
parser = ArgumentParser("looper-serve", description="Run looper HTTP API server")
parser.add_argument(
"--host",
type=str,
default="0.0.0.0",
help="Host IP address to use (127.0.0.1 for local access only)",
)
parser.add_argument(
"--port", type=int, default=8000, help="Port the server listens on"
)
args = parser.parse_args()

uvicorn.run(app, host=args.host, port=args.port)


if __name__ == "__main__":
main()
Loading