diff options
Diffstat (limited to 'jb/managers')
| -rw-r--r-- | jb/managers/__init__.py | 2 | ||||
| -rw-r--r-- | jb/managers/amt.py | 2 | ||||
| -rw-r--r-- | jb/managers/gr_api.py | 112 | ||||
| -rw-r--r-- | jb/managers/thl.py | 12 |
4 files changed, 120 insertions, 8 deletions
diff --git a/jb/managers/__init__.py b/jb/managers/__init__.py index e99569a..ec64f9f 100644 --- a/jb/managers/__init__.py +++ b/jb/managers/__init__.py @@ -1,7 +1,7 @@ from enum import IntEnum from typing import Collection -from generalresearchutils.pg_helper import PostgresConfig +from generalresearch.pg_helper import PostgresConfig class Permission(IntEnum): diff --git a/jb/managers/amt.py b/jb/managers/amt.py index 88ae07d..2cb0cbd 100644 --- a/jb/managers/amt.py +++ b/jb/managers/amt.py @@ -3,7 +3,7 @@ from datetime import timezone, datetime from typing import Tuple, Optional, List, Dict, Any import botocore.exceptions -from generalresearchutils.currency import USDCent +from generalresearch.currency import USDCent from mypy_boto3_mturk import MTurkClient from mypy_boto3_mturk.type_defs import ( AssignmentTypeDef, diff --git a/jb/managers/gr_api.py b/jb/managers/gr_api.py new file mode 100644 index 0000000..8ffeaf0 --- /dev/null +++ b/jb/managers/gr_api.py @@ -0,0 +1,112 @@ +"""Client for General Research's product-user API.""" + +from typing import Any + +import requests + +from jb.models.auth import User + + +class GRApiError(RuntimeError): + """The General Research API could not satisfy a request.""" + + +class GRApiManager: + def __init__( + self, + base_url: str, + token: str, + product_id: str, + timeout: float = 5.0, + session: requests.Session | None = None, + ) -> None: + if not token: + raise ValueError("General Research API token must not be empty") + + self.base_url = base_url.rstrip("/") + self.product_id = product_id + self.timeout = timeout + self.session = session or requests.Session() + self.session.headers.update( + { + "Authorization": token, + "Accept": "application/json", + "Content-Type": "application/json", + } + ) + + def _request(self, method: str, url: str, **kwargs: Any) -> requests.Response: + try: + response = self.session.request(method, url, timeout=self.timeout, **kwargs) + response.raise_for_status() + return response + except requests.RequestException as exc: + raise GRApiError( + f"General Research API request failed: {method} {url}" + ) from exc + + def _parse_user_response(self, res: dict) -> User: + return User.model_validate( + { + "product_user_id": res["product_user_id"], + "email": res["metadata"].get("email_address"), + "display_name": res["metadata"].get("display_name"), + } + ) + + def ensure_user_exists(self, user: User) -> User: + """Idempotently create the product user if it does not already exist.""" + url = f"{self.base_url}/{self.product_id}/user/{user.product_user_id}/" + res = self._request("PUT", url).json() + if res["metadata"].get("email_address") is None: + self.set_user_email(user) + return self.get_user(user.product_user_id) + user_thl = self._parse_user_response(res) + if user_thl.email != user.email: + raise ValueError( + f"user {user.product_user_id} already exists with email {user_thl.email}" + ) + return user_thl + + def get_user(self, product_user_id: str) -> User: + """Retrieve the user's email address and display name.""" + url = f"{self.base_url}/{self.product_id}/user/{product_user_id}/" + res = self._request("GET", url).json() + return self._parse_user_response(res) + + def get_user_by_email(self, email: str) -> User: + user = User.model_validate({"email": email}) + return self.get_user(user.product_user_id) + + def set_user_email(self, user: User) -> None: + """This should only be called once per user upon account creation. + A user cannot change their email address.""" + url = f"{self.base_url}/{self.product_id}/user/{user.product_user_id}/metadata/" + res = self._request( + "PATCH", + url, + json={"email_address": str(user.email)}, + ).json() + + def set_user_display_name(self, user: User) -> User: + """Can be called as many times as needed. Multiple + users can have the same display name.""" + if user.display_name is None: + return user + url = f"{self.base_url}/{self.product_id}/user/{user.product_user_id}/metadata/" + res = self._request( + "PATCH", + url, + json={"display_name": user.display_name}, + ).json() + return self.get_user(user.product_user_id) + + def transition_product_user_id(self, user: User, amt_worker_id: str) -> User: + """This should only be called once upon transition from an + AMT account to a General Research account.""" + url = f"{self.base_url}/{self.product_id}/user/{amt_worker_id}/" + res = self._request( + "PATCH", url, json={"product_user_id": user.product_user_id} + ) + self.set_user_email(user) + return self.get_user(user.product_user_id) diff --git a/jb/managers/thl.py b/jb/managers/thl.py index 6e8effc..f0534db 100644 --- a/jb/managers/thl.py +++ b/jb/managers/thl.py @@ -1,16 +1,16 @@ -from generalresearchutils.models.thl.payout import UserPayoutEvent -from generalresearchutils.models.thl.task_status import TaskStatusResponse -from generalresearchutils.models.thl.wallet.cashout_method import ( +from generalresearch.models.thl.payout import UserPayoutEvent +from generalresearch.models.thl.task_status import TaskStatusResponse +from generalresearch.models.thl.wallet.cashout_method import ( CashoutRequestResponse, CashoutRequestInfo, ) -from generalresearchutils.models.thl.user_profile import UserProfile -from generalresearchutils.currency import USDCent +from generalresearch.models.thl.user_profile import UserProfile +from generalresearch.currency import USDCent from jb.config import settings -from generalresearchutils.models.thl.definitions import PayoutStatus +from generalresearch.models.thl.definitions import PayoutStatus from typing import Optional |
