Skip to content

Commit

Permalink
Merge pull request #41 from valory-xyz/refactor/api
Browse files Browse the repository at this point in the history
Refactors API
  • Loading branch information
angrybayblade authored Mar 27, 2024
2 parents 2242198 + ff3e26e commit bde824e
Show file tree
Hide file tree
Showing 12 changed files with 1,738 additions and 726 deletions.
440 changes: 405 additions & 35 deletions api.md

Large diffs are not rendered by default.

255 changes: 216 additions & 39 deletions operate/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,58 +19,67 @@

"""Operate app CLI module."""

import logging
import os
import traceback
import typing as t
from pathlib import Path

from aea.helpers.logging import setup_logger
from aea_ledger_ethereum.ethereum import EthereumCrypto
from clea import group, params, run
from starlette.applications import Starlette
from starlette.middleware import Middleware
from starlette.middleware.cors import CORSMiddleware
from starlette.routing import Route
from fastapi import FastAPI, Request
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse
from typing_extensions import Annotated
from uvicorn.main import run as uvicorn

from operate import services
from operate.constants import KEY, KEYS, OPERATE, SERVICES
from operate.http import Resource
from operate.keys import Keys
from operate.services.manage import Services


DEFAULT_HARDHAT_KEY = (
"0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"
).encode()
DEFAULT_MAX_RETRIES = 3


class App(Resource):
"""App resource."""
class OperateApp:
"""Operate app."""

def __init__(self, home: t.Optional[Path] = None) -> None:
def __init__(
self,
home: t.Optional[Path] = None,
logger: t.Optional[logging.Logger] = None,
) -> None:
"""Initialize object."""
super().__init__()
self._path = (home or (Path.home() / OPERATE)).resolve()
self._path = (home or (Path.cwd() / OPERATE)).resolve()
self._services = self._path / SERVICES
self._keys = self._path / KEYS
self._key = self._path / KEY
self._master_key = self._path / KEY
self.setup()

self.make()

self.keys = Keys(path=self._keys)
self.services = Services(
self.logger = logger or setup_logger(name="operate")
self.keys_manager = services.manage.KeysManager(
path=self._keys,
logger=self.logger,
)
self.service_manager = services.manage.ServiceManager(
path=self._services,
keys=self.keys,
key=self._key,
keys_manager=self.keys_manager,
master_key_path=self._master_key,
logger=self.logger,
)

def make(self) -> None:
def setup(self) -> None:
"""Make the root directory."""
self._path.mkdir(exist_ok=True)
self._services.mkdir(exist_ok=True)
self._keys.mkdir(exist_ok=True)
if not self._key.exists():
if not self._master_key.exists():
# TODO: Add support for multiple master keys
self._key.write_bytes(
self._master_key.write_bytes(
DEFAULT_HARDHAT_KEY
if os.environ.get("DEV", "false") == "true"
else EthereumCrypto().private_key.encode()
Expand All @@ -83,12 +92,196 @@ def json(self) -> dict:
"name": "Operate HTTP server",
"version": "0.1.0.rc0",
"account": {
"key": EthereumCrypto(self._key).address,
"key": EthereumCrypto(self._master_key).address,
},
"home": str(self._path),
}


def create_app( # pylint: disable=too-many-locals, unused-argument
home: t.Optional[Path] = None,
) -> FastAPI:
"""Create FastAPI object."""

logger = setup_logger(name="operate")
operate = OperateApp(home=home, logger=logger)
app = FastAPI()

app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_methods=["GET", "POST", "PUT", "DELETE"],
)

def with_retries(f: t.Callable) -> t.Callable:
"""Retries decorator."""

async def _call(request: Request) -> JSONResponse:
"""Call the endpoint."""
logger.info(f"Calling `{f.__name__}` with retries enabled")
retries = 0
errors = []
while retries < DEFAULT_MAX_RETRIES:
try:
return await f(request)
except Exception as e: # pylint: disable=broad-except
errors.append(
{"error": str(e), "traceback": traceback.format_exc()}
)
logger.error(f"Error {e}\n{traceback.format_exc()}")
retries += 1
return {"errors": errors}

return _call

@app.get("/api")
@with_retries
async def _get_api(request: Request) -> JSONResponse:
"""Get API info."""
return JSONResponse(content=operate.json)

@app.get("/api/services")
@with_retries
async def _get_services(request: Request) -> JSONResponse:
"""Get available services."""
return JSONResponse(content=operate.service_manager.json)

@app.post("/api/services")
@with_retries
async def _create_services(request: Request) -> JSONResponse:
"""Create a service."""
template = await request.json()
service = operate.service_manager.create_or_load(
hash=template["hash"],
rpc=template["configuration"]["rpc"],
on_chain_user_params=services.manage.OnChainUserParams.from_json(
template["configuration"]
),
)
if template.get("deploy", False):
operate.service_manager.deploy_service_onchain(hash=service.hash)
operate.service_manager.stake_service_on_chain(hash=service.hash)
service.deployment.build()
service.deployment.start()
return JSONResponse(
content=operate.service_manager.create_or_load(hash=service.hash).json
)

@app.put("/api/services")
@with_retries
async def _update_services(request: Request) -> JSONResponse:
"""Create a service."""
template = await request.json()
service = operate.service_manager.update_service(
old_hash=template["old_service_hash"],
new_hash=template["new_service_hash"],
)
if template.get("deploy", False):
operate.service_manager.deploy_service_onchain(hash=service.hash)
operate.service_manager.stake_service_on_chain(hash=service.hash)
service.deployment.build()
service.deployment.start()
return JSONResponse(content=service.json)

@app.get("/api/services/{service}")
@with_retries
async def _get_service(request: Request) -> JSONResponse:
"""Create a service."""
return JSONResponse(
content=operate.service_manager.create_or_load(
hash=request.path_params["service"],
).json
)

@app.post("/api/services/{service}/onchain/deploy")
@with_retries
async def _deploy_service_onchain(request: Request) -> JSONResponse:
"""Create a service."""
operate.service_manager.deploy_service_onchain(
hash=request.path_params["service"]
)
operate.service_manager.stake_service_on_chain(
hash=request.path_params["service"]
)
return JSONResponse(
content=operate.service_manager.create_or_load(
hash=request.path_params["service"]
).json
)

@app.post("/api/services/{service}/onchain/stop")
@with_retries
async def _stop_service_onchain(request: Request) -> JSONResponse:
"""Create a service."""
operate.service_manager.terminate_service_on_chain(
hash=request.path_params["service"]
)
operate.service_manager.unbond_service_on_chain(
hash=request.path_params["service"]
)
operate.service_manager.unstake_service_on_chain(
hash=request.path_params["service"]
)
return JSONResponse(
content=operate.service_manager.create_or_load(
hash=request.path_params["service"]
).json
)

@app.get("/api/services/{service}/deployment")
@with_retries
async def _get_service_deployment(request: Request) -> JSONResponse:
"""Create a service."""
return JSONResponse(
content=operate.service_manager.create_or_load(
request.path_params["service"],
).deployment.json
)

@app.post("/api/services/{service}/deployment/build")
@with_retries
async def _build_service_locally(request: Request) -> JSONResponse:
"""Create a service."""
deployment = operate.service_manager.create_or_load(
request.path_params["service"],
).deployment
deployment.build()
return JSONResponse(content=deployment.json)

@app.post("/api/services/{service}/deployment/start")
@with_retries
async def _start_service_locally(request: Request) -> JSONResponse:
"""Create a service."""
deployment = operate.service_manager.create_or_load(
request.path_params["service"],
).deployment
deployment.build()
deployment.start()
return JSONResponse(content=deployment.json)

@app.post("/api/services/{service}/deployment/stop")
@with_retries
async def _stop_service_locally(request: Request) -> JSONResponse:
"""Create a service."""
deployment = operate.service_manager.create_or_load(
request.path_params["service"],
).deployment
deployment.stop()
return JSONResponse(content=deployment.json)

@app.post("/api/services/{service}/deployment/delete")
@with_retries
async def _delete_service_locally(request: Request) -> JSONResponse:
"""Create a service."""
deployment = operate.service_manager.create_or_load(
request.path_params["service"],
).deployment
deployment.delete()
return JSONResponse(content=deployment.json)

return app


@group(name="operate")
def _operate() -> None:
"""Operate - deploy autonomous services."""
Expand All @@ -103,24 +296,8 @@ def _daemon(
] = None,
) -> None:
"""Launch operate daemon."""
app = App(home=home)
uvicorn(
app=Starlette(
debug=True,
routes=[
Route("/api", app),
Route("/api/services", app.services),
Route("/api/services/{service}", app.services),
Route("/api/services/{service}/{action}", app.services),
],
middleware=[
Middleware(
CORSMiddleware,
allow_origins=["*"],
allow_methods=("GET", "POST", "PUT", "DELETE"),
)
],
),
app=create_app(home=home),
host=host,
port=port,
)
Expand Down
Loading

0 comments on commit bde824e

Please sign in to comment.