-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #226 from lsst-sqre/tickets/DM-38339
DM-38339: Add support for running a business once
- Loading branch information
Showing
9 changed files
with
252 additions
and
4 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
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
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,47 @@ | ||
"""Models for running a single instance of a business by itself.""" | ||
|
||
from __future__ import annotations | ||
|
||
from typing import Optional | ||
|
||
from pydantic import BaseModel, Field | ||
|
||
from .business.empty import EmptyLoopConfig | ||
from .business.jupyterpythonloop import JupyterPythonLoopConfig | ||
from .business.notebookrunner import NotebookRunnerConfig | ||
from .business.tapqueryrunner import TAPQueryRunnerConfig | ||
from .user import User | ||
|
||
|
||
class SolitaryConfig(BaseModel): | ||
"""Configuration for a solitary monkey. | ||
This is similar to `~mobu.models.flock.FlockConfig`, but less complex | ||
since it can only wrap a single monkey business. | ||
""" | ||
|
||
user: User = Field(..., title="User to run as") | ||
|
||
scopes: list[str] = Field( | ||
..., | ||
title="Token scopes", | ||
description="Must include all scopes required to run the business", | ||
example=["exec:notebook", "read:tap"], | ||
) | ||
|
||
business: ( | ||
TAPQueryRunnerConfig | ||
| NotebookRunnerConfig | ||
| JupyterPythonLoopConfig | ||
| EmptyLoopConfig | ||
) = Field(..., title="Business to run") | ||
|
||
|
||
class SolitaryResult(BaseModel): | ||
"""Results from executing a solitary monkey.""" | ||
|
||
success: bool = Field(..., title="Whether the business succeeded") | ||
|
||
error: Optional[str] = Field(None, title="Error if the business failed") | ||
|
||
log: str = Field(..., title="Log of the business execution") |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
"""Manager for a solitary monkey.""" | ||
|
||
from __future__ import annotations | ||
|
||
from pathlib import Path | ||
|
||
from aiohttp import ClientSession | ||
from structlog.stdlib import BoundLogger | ||
|
||
from ..models.solitary import SolitaryConfig, SolitaryResult | ||
from ..models.user import AuthenticatedUser | ||
from .monkey import Monkey | ||
|
||
__all__ = ["Solitary"] | ||
|
||
|
||
class Solitary: | ||
"""Runs a single monkey to completion and reports its results. | ||
Parameters | ||
---------- | ||
solitary_config | ||
Configuration for the monkey. | ||
session | ||
HTTP client session. | ||
logger | ||
Global logger. | ||
""" | ||
|
||
def __init__( | ||
self, | ||
solitary_config: SolitaryConfig, | ||
session: ClientSession, | ||
logger: BoundLogger, | ||
) -> None: | ||
self._config = solitary_config | ||
self._session = session | ||
self._logger = logger | ||
|
||
async def run(self) -> SolitaryResult: | ||
"""Run the monkey and return its results. | ||
Returns | ||
------- | ||
SolitaryResult | ||
Result of monkey run. | ||
""" | ||
user = await AuthenticatedUser.create( | ||
self._config.user, self._config.scopes, self._session | ||
) | ||
monkey = Monkey( | ||
name=f"solitary-{user.username}", | ||
business_config=self._config.business, | ||
user=user, | ||
session=self._session, | ||
logger=self._logger, | ||
) | ||
error = await monkey.run_once() | ||
return SolitaryResult( | ||
success=error is None, | ||
error=error, | ||
log=Path(monkey.logfile()).read_text(), | ||
) |
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,63 @@ | ||
"""Tests for running a solitary monkey.""" | ||
|
||
from __future__ import annotations | ||
|
||
from unittest.mock import ANY | ||
|
||
import pytest | ||
from aioresponses import aioresponses | ||
from httpx import AsyncClient | ||
from safir.testing.slack import MockSlackWebhook | ||
|
||
from ..support.gafaelfawr import mock_gafaelfawr | ||
|
||
|
||
@pytest.mark.asyncio | ||
async def test_run( | ||
client: AsyncClient, mock_aioresponses: aioresponses | ||
) -> None: | ||
mock_gafaelfawr(mock_aioresponses) | ||
|
||
r = await client.post( | ||
"/mobu/run", | ||
json={ | ||
"user": {"username": "solitary"}, | ||
"scopes": ["exec:notebook"], | ||
"business": {"type": "EmptyLoop"}, | ||
}, | ||
) | ||
assert r.status_code == 200 | ||
result = r.json() | ||
assert result == {"success": True, "log": ANY} | ||
assert "Starting up..." in result["log"] | ||
assert "Shutting down..." in result["log"] | ||
|
||
|
||
@pytest.mark.asyncio | ||
async def test_error( | ||
client: AsyncClient, | ||
slack: MockSlackWebhook, | ||
mock_aioresponses: aioresponses, | ||
) -> None: | ||
mock_gafaelfawr(mock_aioresponses) | ||
|
||
r = await client.post( | ||
"/mobu/run", | ||
json={ | ||
"user": {"username": "solitary"}, | ||
"scopes": ["exec:notebook"], | ||
"business": { | ||
"type": "JupyterPythonLoop", | ||
"options": { | ||
"code": 'raise Exception("some error")', | ||
"spawn_settle_time": 0, | ||
}, | ||
}, | ||
}, | ||
) | ||
assert r.status_code == 200 | ||
result = r.json() | ||
assert result == {"success": False, "error": ANY, "log": ANY} | ||
assert "solitary: running code 'raise Exception" in result["error"] | ||
assert "Exception: some error\n" in result["error"] | ||
assert "Exception: some error" in result["log"] |