Skip to content

🐛 ✨ Check #credits >= 0.0 before returning solver job outputs #5249

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

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
68e0bbc
add custom error classes and handler
bisgaard-itis Jan 15, 2024
7487dfc
add check that #credits > 0
bisgaard-itis Jan 15, 2024
b62d147
improve error message
bisgaard-itis Jan 15, 2024
382f783
further improvements to exception message
bisgaard-itis Jan 15, 2024
51bf6e9
allow to enable capture. This used to be possible, but seems the func…
bisgaard-itis Jan 16, 2024
586f400
add initial basic unittest
bisgaard-itis Jan 16, 2024
4180247
add check that payment is enabled
bisgaard-itis Jan 16, 2024
0e79a59
merge master into 5236-check-positive-credits-before-returneing-credits
bisgaard-itis Jan 16, 2024
0be6b5b
minor change
bisgaard-itis Jan 17, 2024
490d6b1
change validation of capture mechanism setting
bisgaard-itis Jan 18, 2024
709be7c
change validation of capture mechanism setting
bisgaard-itis Jan 18, 2024
afa6994
fix applications settigns
bisgaard-itis Jan 18, 2024
19b6c32
improve applications settings check
bisgaard-itis Jan 18, 2024
6f1b386
get product price
bisgaard-itis Jan 19, 2024
7f7a739
merge master into 5236-check-positive-credits-before-returning-credits
bisgaard-itis Jan 19, 2024
76ebf2f
fix swagger in webserver
bisgaard-itis Jan 19, 2024
ec067d5
test almost there
bisgaard-itis Jan 19, 2024
920378b
Fix tests and custom errors
bisgaard-itis Jan 19, 2024
de28fa5
make pylint happy
bisgaard-itis Jan 19, 2024
83d45a6
@sanderegg fix relative import
bisgaard-itis Jan 22, 2024
13f7875
@pcrespov correct precheck
bisgaard-itis Jan 22, 2024
e6fe493
merge master into 5236-check-positive-credits-before-returning-credits
bisgaard-itis Jan 22, 2024
c773e35
Merge branch 'master' into 5236-check-positive-credits-before-returni…
bisgaard-itis Jan 23, 2024
0b4926f
Add OEC @pcrespov
bisgaard-itis Jan 24, 2024
92f135d
merge master into 5236-check-positive-credits-before-returning-credits
bisgaard-itis Jan 24, 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
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import logging
from urllib.request import Request

from fastapi import status
from servicelib.error_codes import create_error_code
from starlette.responses import JSONResponse

_logger = logging.getLogger(__name__)


class CustomBaseError(Exception):
pass


class InsufficientCredits(CustomBaseError):
pass


class MissingWallet(CustomBaseError):
pass


async def custom_error_handler(_: Request, exc: CustomBaseError):
if isinstance(exc, InsufficientCredits):
return JSONResponse(
status_code=status.HTTP_402_PAYMENT_REQUIRED, content=f"{exc}"
)
if isinstance(exc, MissingWallet):
return JSONResponse(
status_code=status.HTTP_424_FAILED_DEPENDENCY, content=f"{exc}"
)
error_code = create_error_code(exc)
_logger.error("Unexpected error", extra={"error_cose": error_code})
return JSONResponse(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
content=f"Unexpected server error [OEC: {error_code}]",
)
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
from ..dependencies.rabbitmq import get_log_distributor, get_max_log_check_seconds
from ..dependencies.services import get_api_client
from ..dependencies.webserver import AuthSession, get_webserver_session
from ..errors.custom_errors import InsufficientCredits, MissingWallet
from ..errors.http_error import create_error_json_response
from ._common import API_SERVER_DEV_FEATURES_ENABLED, job_output_logfile_responses
from .solvers_jobs import (
Expand Down Expand Up @@ -178,6 +179,19 @@ async def get_job_outputs(
node_ids = list(project.workbench.keys())
assert len(node_ids) == 1 # nosec

product_price = await webserver_api.get_product_price()
if product_price is not None:
wallet = await webserver_api.get_project_wallet(project_id=project.uuid)
if wallet is None:
raise MissingWallet(
f"Job {project.uuid} does not have an associated wallet."
)
wallet_with_credits = await webserver_api.get_wallet(wallet_id=wallet.wallet_id)
if wallet_with_credits.available_credits < 0.0:
raise InsufficientCredits(
f"Wallet '{wallet_with_credits.name}' does not have any credits. Please add some before requesting solver ouputs"
)

outputs: dict[str, ResultsTypes] = await get_solver_output_results(
user_id=user_id,
project_uuid=job_id,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
from starlette.exceptions import HTTPException

from .._meta import API_VERSION, API_VTAG
from ..api.errors.custom_errors import CustomBaseError, custom_error_handler
from ..api.errors.http_error import (
http_error_handler,
make_http_error_handler_for_exception,
Expand Down Expand Up @@ -108,6 +109,7 @@ def init_app(settings: ApplicationSettings | None = None) -> FastAPI:
app.add_exception_handler(RequestValidationError, http422_error_handler)
app.add_exception_handler(HTTPStatusError, httpx_client_error_handler)
app.add_exception_handler(LogDistributionBaseException, log_handling_error_handler)
app.add_exception_handler(CustomBaseError, custom_error_handler)

# SEE https://docs.python.org/3/library/exceptions.html#exception-hierarchy
app.add_exception_handler(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from functools import cached_property
from pathlib import Path
from typing import Any, Mapping

from models_library.basic_types import BootModeEnum, LogLevel
from pydantic import Field, NonNegativeInt, SecretStr, parse_obj_as
Expand Down Expand Up @@ -152,16 +153,23 @@ def debug(self) -> bool:
"""If True, debug tracebacks should be returned on errors."""
return self.SC_BOOT_MODE is not None and self.SC_BOOT_MODE.is_devel_mode()

@validator("API_SERVER_DEV_HTTP_CALLS_LOGS_PATH")
@validator("API_SERVER_DEV_HTTP_CALLS_LOGS_PATH", pre=True)
@classmethod
def _enable_only_in_devel_mode(cls, v, values):
if v and not (
values
and (boot_mode := values.get("SC_BOOT_MODE"))
and boot_mode.is_devel_mode()
):
msg = "API_SERVER_DEV_HTTP_CALLS_LOGS_PATH only allowed in devel mode"
raise ValueError(msg)
def _enable_only_in_devel_mode(cls, v: Any, values: Mapping[str, Any]):
if isinstance(v, str):
path_str = v.strip()
if not path_str:
return None

if (
values
and (boot_mode := values.get("SC_BOOT_MODE"))
and boot_mode.is_devel_mode()
):
raise ValueError(
"API_SERVER_DEV_HTTP_CALLS_LOGS_PATH only allowed in devel mode"
)

return v


Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
# pylint: disable=R0904

import json
import logging
import urllib.parse
Expand All @@ -11,6 +13,7 @@
from fastapi import FastAPI, HTTPException
from httpx import Response
from models_library.api_schemas_webserver.computations import ComputationStart
from models_library.api_schemas_webserver.product import GetCreditPrice
from models_library.api_schemas_webserver.projects import ProjectCreateNew, ProjectGet
from models_library.api_schemas_webserver.projects_metadata import (
ProjectMetadataGet,
Expand All @@ -24,6 +27,7 @@
WalletGet,
WalletGetWithAvailableCredits,
)
from models_library.basic_types import NonNegativeDecimal
from models_library.clusters import ClusterID
from models_library.generics import Envelope
from models_library.projects import ProjectID
Expand Down Expand Up @@ -468,6 +472,19 @@ async def get_project_wallet(self, project_id: ProjectID) -> WalletGet | None:
data = Envelope[WalletGet].parse_raw(response.text).data
return data

# PRODUCTS -------------------------------------------------

async def get_product_price(self) -> NonNegativeDecimal | None:
with _handle_webserver_api_errors():
response = await self.client.get(
"/credits-price",
cookies=self.session_cookies,
)
response.raise_for_status()
data = Envelope[GetCreditPrice].parse_raw(response.text).data
assert data is not None
return data.usd_per_credit

# SERVICES -------------------------------------------------

async def get_service_pricing_plan(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,10 @@ class AsyncClientForDevelopmentOnly(httpx.AsyncClient):

def __init__(self, capture_file: Path, **async_clint_kwargs):
super().__init__(**async_clint_kwargs)
assert capture_file.name.endswith( # nosec
".json"
), "The capture file should be a json file"
if capture_file.is_file():
assert capture_file.name.endswith( # nosec
".json"
), "The capture file should be a json file"
self._capture_file: Path = capture_file

async def request(self, method: str, url: URLTypes, **kwargs):
Expand Down
Loading