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

Use FastAPI to provide rest endpoints #135

Merged
merged 27 commits into from
May 11, 2023
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
5e62fb3
simple fastapi setup
Apr 20, 2023
2c56053
squashing commits; rebased onto master and modified some vscode setti…
May 5, 2023
2e539f5
made application compatible with config changes
May 5, 2023
db7e3cb
fixed linting error
May 5, 2023
16db22a
moved starting the app into blueapi/service/main instead of cli. Adde…
May 5, 2023
c2670dd
moved starting the app into blueapi/service/main instead of cli. Adde…
May 5, 2023
156d418
responded to comments
May 5, 2023
c407043
added some cli tests. Intend to add more...
May 5, 2023
f65425e
added mock as dependency to pass tests
May 5, 2023
236a08f
added more tests for cli and modified existing rest tests
May 9, 2023
a390a09
changed run to serve instead
May 9, 2023
0f8f45a
made teardown handler do nothing if handler not set
May 9, 2023
68b7330
removed redundant comment
May 9, 2023
1a26213
removed redundant string concatenation in test
May 9, 2023
e9cccbe
moved assert statement
May 9, 2023
a702f5f
made minor changes in response to comments; using functools wraps and…
May 9, 2023
5450bd1
fixed minor issues; param ingestion for cli.py::run_plan and made fix…
May 9, 2023
c060e93
moved common parts of tests to conftest.py, made session scope fixtures
May 9, 2023
6d1119a
fixed linting
May 9, 2023
a2731f3
made minor changes in response to comments; changed dependencies
May 9, 2023
486cc6c
modified pyproject toml to include all fastapi dependencies
May 9, 2023
6c0cce4
removed redundant fake_cli file which I just used for testing. Change…
May 11, 2023
5cbba67
fixing tests
May 11, 2023
75c6cdd
added extra test for the handler
May 11, 2023
8a633c2
made suggested changes
May 11, 2023
a724fab
Update src/blueapi/service/main.py
rosesyrett May 11, 2023
3863f94
modified docs
May 11, 2023
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
2 changes: 1 addition & 1 deletion .devcontainer/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,4 @@ ENV PATH=/venv/bin:$PATH

# change this entrypoint if it is not the same as the repo
ENTRYPOINT ["blueapi"]
CMD ["worker"]
CMD ["run"]
rosesyrett marked this conversation as resolved.
Show resolved Hide resolved
14 changes: 13 additions & 1 deletion .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,18 @@
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Python: FastAPI",
rosesyrett marked this conversation as resolved.
Show resolved Hide resolved
"type": "python",
"request": "launch",
"module": "uvicorn",
"args": [
"src.blueapi.rest.main:app",
"--reload"
],
"jinja": true,
"justMyCode": true
},
{
"name": "Debug Unit Test",
"type": "python",
Expand Down Expand Up @@ -68,4 +80,4 @@
]
}
]
}
}
4 changes: 2 additions & 2 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,5 @@
"source.organizeImports": true
},
"esbonio.server.enabled": true,
"esbonio.sphinx.confDir": ""
}
"esbonio.sphinx.confDir": "",
}
5 changes: 5 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ dependencies = [
"scanspec",
"PyYAML",
"click",
"fastapi",
"uvicorn",
"httpx",
rosesyrett marked this conversation as resolved.
Show resolved Hide resolved
rosesyrett marked this conversation as resolved.
Show resolved Hide resolved
]
dynamic = ["version"]
license.file = "LICENSE"
Expand All @@ -44,6 +47,8 @@ dev = [
"tox-direct",
"types-mock",
"types-PyYAML",
"types-requests",
"types-urllib3",
]

[project.scripts]
Expand Down
1 change: 1 addition & 0 deletions src/blueapi/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@
__version__ = version("blueapi")
del version


__all__ = ["__version__"]
91 changes: 56 additions & 35 deletions src/blueapi/cli/cli.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
import json
import logging
from pathlib import Path
from pprint import pprint
from typing import Optional

import click
import requests
from requests.exceptions import ConnectionError

from blueapi import __version__
from blueapi.config import ApplicationConfig, ConfigLoader
from blueapi.messaging import StompMessagingTemplate

from .amq import AmqClient
from .updates import CliEventRenderer
from blueapi.service.handler import setup_handler
from blueapi.service.main import app


@click.group(invoke_without_command=True)
Expand All @@ -19,24 +20,30 @@
@click.pass_context
def main(ctx, config: Optional[Path]) -> None:
# if no command is supplied, run with the options passed

config_loader = ConfigLoader(ApplicationConfig)
if config is not None:
config_loader.use_values_from_yaml(config)

ctx.ensure_object(dict)
ctx.obj["config"] = config_loader.load()
ctx.obj["config_loader"] = config_loader
rosesyrett marked this conversation as resolved.
Show resolved Hide resolved

if ctx.invoked_subcommand is None:
print("Please invoke subcommand!")


@main.command(name="worker")
@click.version_option(version=__version__)
@main.command(name="run")
rosesyrett marked this conversation as resolved.
Show resolved Hide resolved
@click.pass_obj
def start_worker(obj: dict) -> None:
from blueapi.service import start
def start_application(obj: dict):
import uvicorn

config_loader: ConfigLoader[ApplicationConfig] = obj["config_loader"]
config = config_loader.load()

setup_handler(config_loader)

config: ApplicationConfig = obj["config"]
start(config)
uvicorn.run(app, host=config.api.host, port=config.api.port)


@main.group()
Expand All @@ -47,41 +54,55 @@ def controller(ctx) -> None:
return

ctx.ensure_object(dict)
config: ApplicationConfig = ctx.obj["config"]
config_loader: ConfigLoader[ApplicationConfig] = ctx.obj["config_loader"]
config: ApplicationConfig = config_loader.load()
logging.basicConfig(level=config.logging.level)
client = AmqClient(StompMessagingTemplate.autoconfigured(config.stomp))
ctx.obj["client"] = client
client.app.connect()


def check_connection(func):
def wrapper(*args, **kwargs):
try:
func(*args, **kwargs)
except ConnectionError:
print("Failed to establish connection to FastAPI server.")
rosesyrett marked this conversation as resolved.
Show resolved Hide resolved

return wrapper


@controller.command(name="plans")
@click.pass_context
def get_plans(ctx) -> None:
client: AmqClient = ctx.obj["client"]
plans = client.get_plans()
print("PLANS")
for plan in plans.plans:
print("\t" + plan.name)
@check_connection
@click.pass_obj
def get_plans(obj: dict) -> None:
config_loader: ConfigLoader[ApplicationConfig] = obj["config_loader"]
config: ApplicationConfig = config_loader.load()

resp = requests.get(f"http://{config.api.host}:{config.api.port}/plans")
print(f"Response returned with {resp.status_code}: ")
pprint(resp.json())
rosesyrett marked this conversation as resolved.
Show resolved Hide resolved


@controller.command(name="devices")
@click.pass_context
def get_devices(ctx) -> None:
client: AmqClient = ctx.obj["client"]
print(client.get_devices().devices)
@check_connection
@click.pass_obj
def get_devices(obj: dict) -> None:
config_loader: ConfigLoader[ApplicationConfig] = obj["config_loader"]
config: ApplicationConfig = config_loader.load()
resp = requests.get(f"http://{config.api.host}:{config.api.port}/devices")
print(f"Response returned with {resp.status_code}: ")
pprint(resp.json())


@controller.command(name="run")
@click.argument("name", type=str)
@click.option("-p", "--parameters", type=str, help="Parameters as valid JSON")
@click.pass_context
def run_plan(ctx, name: str, parameters: str) -> None:
client: AmqClient = ctx.obj["client"]
renderer = CliEventRenderer()
client.run_plan(
name,
json.loads(parameters),
renderer.on_worker_event,
renderer.on_progress_event,
timeout=120.0,
@check_connection
@click.pass_obj
def run_plan(obj: dict, name: str, parameters: str) -> None:
config_loader: ConfigLoader[ApplicationConfig] = obj["config_loader"]
config: ApplicationConfig = config_loader.load()
resp = requests.put(
f"http://{config.api.host}:{config.api.port}/task/{name}",
json=json.loads(parameters),
rosesyrett marked this conversation as resolved.
Show resolved Hide resolved
)
print(f"Response returned with {resp.status_code}: ")
pprint(resp.json())
rosesyrett marked this conversation as resolved.
Show resolved Hide resolved
7 changes: 7 additions & 0 deletions src/blueapi/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,11 @@ class LoggingConfig(BlueapiBaseModel):
level: LogLevel = "INFO"


class FastApiConfig(BlueapiBaseModel):
rosesyrett marked this conversation as resolved.
Show resolved Hide resolved
host: str = "localhost"
port: int = 8000


class ApplicationConfig(BlueapiBaseModel):
"""
Config for the worker application as a whole. Root of
Expand All @@ -44,13 +49,15 @@ class ApplicationConfig(BlueapiBaseModel):
stomp: StompConfig = Field(default_factory=StompConfig)
env: EnvironmentConfig = Field(default_factory=EnvironmentConfig)
logging: LoggingConfig = Field(default_factory=LoggingConfig)
api: FastApiConfig = Field(default_factory=FastApiConfig)

def __eq__(self, other: object) -> bool:
if isinstance(other, ApplicationConfig):
return (
(self.stomp == other.stomp)
& (self.env == other.env)
& (self.logging == other.logging)
& (self.api == other.api)
)
return False

Expand Down
3 changes: 1 addition & 2 deletions src/blueapi/service/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
from .app import start
from .model import DeviceModel, PlanModel

__all__ = ["start", "PlanModel", "DeviceModel"]
__all__ = ["PlanModel", "DeviceModel"]
99 changes: 0 additions & 99 deletions src/blueapi/service/app.py

This file was deleted.

Loading