Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,15 +28,18 @@ The factory class must be instantiated only once, and a new session can be obtai
subtype: ServiceSubtype = ...
proj_id: UUID = ...
user_id: UUID = ...
name: str | None = ...
estimated_count: int = ...
async with accounting_session_factory.oneshot_session(
subtype=subtype,
proj_id=proj_id,
user_id=user_id,
name=name,
count=estimated_count,
) as acc_session:
# actual logic
acc_session.count = actual_count
acc_session.name = actual_name
```

In the example above:
Expand Down
21 changes: 20 additions & 1 deletion src/obp_accounting_sdk/_async/oneshot.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

import httpx

from obp_accounting_sdk.constants import ServiceSubtype, ServiceType
from obp_accounting_sdk.constants import MAX_JOB_NAME_LENGTH, ServiceSubtype, ServiceType
from obp_accounting_sdk.errors import (
AccountingCancellationError,
AccountingReservationError,
Expand All @@ -31,6 +31,7 @@ def __init__(
proj_id: UUID | str,
user_id: UUID | str,
count: int,
name: str | None = None,
) -> None:
"""Initialization."""
self._http_client = http_client
Expand All @@ -39,6 +40,7 @@ def __init__(
self._service_subtype: ServiceSubtype = ServiceSubtype(subtype)
self._proj_id: UUID = UUID(str(proj_id))
self._user_id: UUID = UUID(str(user_id))
self._name = name
self._job_id: UUID | None = None
self._count = self.count = count

Expand All @@ -57,6 +59,21 @@ def count(self, value: int) -> None:
L.info("Overriding previous count value %s with %s", self.count, value)
self._count = value

@property
def name(self) -> str | None:
"""Return the job name."""
return self._name

@name.setter
def name(self, value: str) -> None:
"""Set the job name."""
if not isinstance(value, str) or len(value) > MAX_JOB_NAME_LENGTH:
errmsg = f"Job name must be a string with max length {MAX_JOB_NAME_LENGTH}"
raise ValueError(errmsg)
if self.name is not None and self.name != value:
L.info("Overriding previous name value '%s' with '%s'", self.name, value)
self._name = value

async def _make_reservation(self) -> None:
"""Make a new reservation."""
if self._job_id is not None:
Expand All @@ -68,6 +85,7 @@ async def _make_reservation(self) -> None:
"subtype": self._service_subtype,
"proj_id": str(self._proj_id),
"user_id": str(self._user_id),
"name": self.name,
"count": str(self.count),
}
try:
Expand Down Expand Up @@ -120,6 +138,7 @@ async def _send_usage(self) -> None:
"type": self._service_type,
"subtype": self._service_subtype,
"proj_id": str(self._proj_id),
"name": self.name,
"count": str(self.count),
"job_id": str(self._job_id),
"timestamp": get_current_timestamp(),
Expand Down
21 changes: 20 additions & 1 deletion src/obp_accounting_sdk/_sync/oneshot.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

import httpx

from obp_accounting_sdk.constants import ServiceSubtype, ServiceType
from obp_accounting_sdk.constants import MAX_JOB_NAME_LENGTH, ServiceSubtype, ServiceType
from obp_accounting_sdk.errors import (
AccountingCancellationError,
AccountingReservationError,
Expand All @@ -31,6 +31,7 @@ def __init__(
proj_id: UUID | str,
user_id: UUID | str,
count: int,
name: str | None = None,
) -> None:
"""Initialization."""
self._http_client = http_client
Expand All @@ -39,6 +40,7 @@ def __init__(
self._service_subtype: ServiceSubtype = ServiceSubtype(subtype)
self._proj_id: UUID = UUID(str(proj_id))
self._user_id: UUID = UUID(str(user_id))
self._name = name
self._job_id: UUID | None = None
self._count = self.count = count

Expand All @@ -57,6 +59,21 @@ def count(self, value: int) -> None:
L.info("Overriding previous count value %s with %s", self.count, value)
self._count = value

@property
def name(self) -> str | None:
"""Return the job name."""
return self._name

@name.setter
def name(self, value: str) -> None:
"""Set the job name."""
if not isinstance(value, str) or len(value) > MAX_JOB_NAME_LENGTH:
errmsg = f"Job name must be a string with max length {MAX_JOB_NAME_LENGTH}"
raise ValueError(errmsg)
if self.name is not None and self.name != value:
L.info("Overriding previous name value '%s' with '%s'", self.name, value)
self._name = value

def _make_reservation(self) -> None:
"""Make a new reservation."""
if self._job_id is not None:
Expand All @@ -68,6 +85,7 @@ def _make_reservation(self) -> None:
"subtype": self._service_subtype,
"proj_id": str(self._proj_id),
"user_id": str(self._user_id),
"name": self.name,
"count": str(self.count),
}
try:
Expand Down Expand Up @@ -120,6 +138,7 @@ def _send_usage(self) -> None:
"type": self._service_type,
"subtype": self._service_subtype,
"proj_id": str(self._proj_id),
"name": self.name,
"count": str(self.count),
"job_id": str(self._job_id),
"timestamp": get_current_timestamp(),
Expand Down
2 changes: 2 additions & 0 deletions src/obp_accounting_sdk/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

from enum import StrEnum, auto

MAX_JOB_NAME_LENGTH = 255


class HyphenStrEnum(StrEnum):
"""Enum where members are also (and must be) strings.
Expand Down
13 changes: 13 additions & 0 deletions tests/_async/test_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,19 @@ async def test_factory_with_aclosing(monkeypatch):
assert isinstance(oneshot_session, AsyncOneshotSession)


async def test_factory_with_name(monkeypatch):
monkeypatch.setenv("ACCOUNTING_BASE_URL", BASE_URL)
async with aclosing(test_module.AsyncAccountingSessionFactory()) as session_factory:
oneshot_session = session_factory.oneshot_session(
subtype=ServiceSubtype.ML_LLM,
proj_id=PROJ_ID,
user_id=USER_ID,
name="test job",
count=10,
)
assert isinstance(oneshot_session, AsyncOneshotSession)


async def test_factory_without_env_var_accounting_base_url(monkeypatch):
monkeypatch.delenv("ACCOUNTING_BASE_URL", raising=False)
with pytest.raises(RuntimeError, match="ACCOUNTING_BASE_URL must be set"):
Expand Down
40 changes: 39 additions & 1 deletion tests/_async/test_oneshot.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import pytest

from obp_accounting_sdk._async import oneshot as test_module
from obp_accounting_sdk.constants import ServiceSubtype
from obp_accounting_sdk.constants import MAX_JOB_NAME_LENGTH, ServiceSubtype
from obp_accounting_sdk.errors import (
AccountingReservationError,
AccountingUsageError,
Expand Down Expand Up @@ -46,6 +46,44 @@ async def test_oneshot_session_success(httpx_mock):
session.count = 20
assert session.count == 20

assert session.name is None
name_value_error = f"Job name must be a string with max length {MAX_JOB_NAME_LENGTH}"
with pytest.raises(ValueError, match=name_value_error):
session.name = None
with pytest.raises(ValueError, match=name_value_error):
session.name = 123
session.name = "test job"
assert session.name == "test job"

# Overwrite existing name
session.name = "test job 2 updated"
assert session.name == "test job 2 updated"


async def test_oneshot_session_with_name(httpx_mock):
httpx_mock.add_response(
json={"message": "", "data": {"job_id": JOB_ID}},
method="POST",
url=f"{BASE_URL}/reservation/oneshot",
)
httpx_mock.add_response(
json={"message": "", "data": None},
method="POST",
url=f"{BASE_URL}/usage/oneshot",
)

async with httpx.AsyncClient() as http_client:
async with test_module.AsyncOneshotSession(
http_client=http_client,
base_url=BASE_URL,
subtype=ServiceSubtype.ML_LLM,
proj_id=PROJ_ID,
user_id=USER_ID,
name="test job",
count=10,
) as session:
assert session.name == "test job"


async def test_oneshot_session_with_insufficient_funds(httpx_mock):
httpx_mock.add_response(
Expand Down
13 changes: 13 additions & 0 deletions tests/_sync/test_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,19 @@ def test_factory_with_aclosing(monkeypatch):
assert isinstance(oneshot_session, OneshotSession)


def test_factory_with_name(monkeypatch):
monkeypatch.setenv("ACCOUNTING_BASE_URL", BASE_URL)
with closing(test_module.AccountingSessionFactory()) as session_factory:
oneshot_session = session_factory.oneshot_session(
subtype=ServiceSubtype.ML_LLM,
proj_id=PROJ_ID,
user_id=USER_ID,
name="test job",
count=10,
)
assert isinstance(oneshot_session, OneshotSession)


def test_factory_without_env_var_accounting_base_url(monkeypatch):
monkeypatch.delenv("ACCOUNTING_BASE_URL", raising=False)
with pytest.raises(RuntimeError, match="ACCOUNTING_BASE_URL must be set"):
Expand Down
40 changes: 39 additions & 1 deletion tests/_sync/test_oneshot.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import pytest

from obp_accounting_sdk._sync import oneshot as test_module
from obp_accounting_sdk.constants import ServiceSubtype
from obp_accounting_sdk.constants import MAX_JOB_NAME_LENGTH, ServiceSubtype
from obp_accounting_sdk.errors import (
AccountingReservationError,
AccountingUsageError,
Expand Down Expand Up @@ -46,6 +46,44 @@ def test_oneshot_session_success(httpx_mock):
session.count = 20
assert session.count == 20

assert session.name is None
name_value_error = f"Job name must be a string with max length {MAX_JOB_NAME_LENGTH}"
with pytest.raises(ValueError, match=name_value_error):
session.name = None
with pytest.raises(ValueError, match=name_value_error):
session.name = 123
session.name = "test job"
assert session.name == "test job"

# Overwrite existing name
session.name = "test job 2 updated"
assert session.name == "test job 2 updated"


def test_oneshot_session_with_name(httpx_mock):
httpx_mock.add_response(
json={"message": "", "data": {"job_id": JOB_ID}},
method="POST",
url=f"{BASE_URL}/reservation/oneshot",
)
httpx_mock.add_response(
json={"message": "", "data": None},
method="POST",
url=f"{BASE_URL}/usage/oneshot",
)

with httpx.Client() as http_client:
with test_module.OneshotSession(
http_client=http_client,
base_url=BASE_URL,
subtype=ServiceSubtype.ML_LLM,
proj_id=PROJ_ID,
user_id=USER_ID,
name="test job",
count=10,
) as session:
assert session.name == "test job"


def test_oneshot_session_with_insufficient_funds(httpx_mock):
httpx_mock.add_response(
Expand Down