aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorstuppie2026-08-28 15:58:42 -0600
committerstuppie2026-08-28 15:58:42 -0600
commitf7b187e553bea8f8a393d5c67c607fa667de1925 (patch)
tree65295b8ce496f5d1f48e0a22f2a539a503a1c4af
parent7a2d647577fd62db042a1ec72a8a0696e7552ad4 (diff)
downloadgeneralresearch-f7b187e553bea8f8a393d5c67c607fa667de1925.tar.gz
generalresearch-f7b187e553bea8f8a393d5c67c607fa667de1925.zip
add user manager change_product_user_id
-rw-r--r--generalresearch/managers/thl/user_manager/mysql_user_manager.py36
-rw-r--r--generalresearch/managers/thl/user_manager/redis_user_manager.py1
-rw-r--r--generalresearch/managers/thl/user_manager/user_manager.py84
3 files changed, 91 insertions, 30 deletions
diff --git a/generalresearch/managers/thl/user_manager/mysql_user_manager.py b/generalresearch/managers/thl/user_manager/mysql_user_manager.py
index 22abaa8..109b601 100644
--- a/generalresearch/managers/thl/user_manager/mysql_user_manager.py
+++ b/generalresearch/managers/thl/user_manager/mysql_user_manager.py
@@ -40,6 +40,42 @@ class MysqlUserManager:
params=[now, user.user_id],
)
+ def _change_product_user_id(
+ self, *, user: User, new_product_user_id: str
+ ) -> User:
+ """Change a user's supplier-provided ID in the primary database."""
+ assert not self.is_read_replica
+ assert user.user_id is not None
+ assert user.product_id is not None
+ assert user.product_user_id is not None
+
+ with self.pg_config.make_connection() as conn:
+ with conn.cursor() as c:
+ c.execute(
+ query="""
+ UPDATE thl_user
+ SET product_user_id = %(new_product_user_id)s
+ WHERE id = %(user_id)s
+ AND product_id = %(product_id)s
+ AND product_user_id = %(old_product_user_id)s
+ RETURNING id AS user_id, product_id, product_user_id,
+ uuid, blocked, created, last_seen
+ """,
+ params={
+ "new_product_user_id": new_product_user_id,
+ "user_id": user.user_id,
+ "product_id": user.product_id,
+ "old_product_user_id": user.product_user_id,
+ },
+ )
+ row = c.fetchone()
+
+ if row is None:
+ raise RuntimeError(
+ "User was not updated; it may have been changed concurrently"
+ )
+ return User.from_db(row)
+
def get_user_from_mysql(
self,
*,
diff --git a/generalresearch/managers/thl/user_manager/redis_user_manager.py b/generalresearch/managers/thl/user_manager/redis_user_manager.py
index 2190e6e..730cb9e 100644
--- a/generalresearch/managers/thl/user_manager/redis_user_manager.py
+++ b/generalresearch/managers/thl/user_manager/redis_user_manager.py
@@ -76,7 +76,6 @@ class RedisUserManager:
p.execute()
def clear_user(self, user: User) -> None:
- # this should only be used by tests
with self.client.pipeline(transaction=False) as p:
p.delete(f"{self.cache_prefix}:uuid:{user.uuid}")
p.delete(f"{self.cache_prefix}:user_id:{user.user_id}")
diff --git a/generalresearch/managers/thl/user_manager/user_manager.py b/generalresearch/managers/thl/user_manager/user_manager.py
index 5da3370..7f24029 100644
--- a/generalresearch/managers/thl/user_manager/user_manager.py
+++ b/generalresearch/managers/thl/user_manager/user_manager.py
@@ -35,9 +35,9 @@ auditlog = logging.getLogger("auditlog")
class UserManager:
def __init__(
self,
- redis: RedisDsn | None = None,
- pg_config: PostgresConfig | None = None,
- pg_config_rr: PostgresConfig | None = None,
+ redis: RedisDsn,
+ pg_config: PostgresConfig,
+ pg_config_rr: PostgresConfig,
sql_permissions: Collection[Permission] | None = None,
cache_prefix: str | None = None,
redis_timeout: float | None = None,
@@ -47,9 +47,9 @@ class UserManager:
sql_permissions = []
if pg_config is not None:
- assert (
- pg_config_rr is not None
- ), "you should pass RR credentials also for fast lookups"
+ assert pg_config_rr is not None, (
+ "you should pass RR credentials also for fast lookups"
+ )
assert Permission.DELETE not in sql_permissions, "delete not allowed"
if Permission.UPDATE in sql_permissions or Permission.CREATE in sql_permissions:
@@ -87,6 +87,33 @@ class UserManager:
assert Permission.UPDATE in self.sql_permissions, "permission error"
return self.mysql_user_manager._set_last_seen(user)
+ def change_product_user_id(self, *, user: User, new_product_user_id: str) -> User:
+ """Change a user's supplier-provided ID and refresh user lookup caches.
+
+ This does not rewrite historical or derived data that copied the old ID,
+ such as leaderboards and activity counters.
+ """
+ assert Permission.UPDATE in self.sql_permissions, "permission error"
+ assert self.mysql_user_manager is not None
+ assert user.product_id is not None
+ assert user.product_user_id is not None
+
+ if new_product_user_id == user.product_user_id:
+ return user
+ if not User.is_valid_ubp(
+ product_id=user.product_id, product_user_id=new_product_user_id
+ ):
+ raise ValueError("invalid product_id/product_user_id")
+
+ updated_user = self.mysql_user_manager._change_product_user_id(
+ user=user, new_product_user_id=new_product_user_id
+ )
+
+ self.cache_clear()
+ if self.redis_user_manager:
+ self.redis_user_manager.clear_user(user)
+ return updated_user
+
def audit_log(
self,
user: User,
@@ -99,6 +126,7 @@ class UserManager:
from generalresearch.models.thl.userhealth import AuditLogLevel
alm = AuditLogManager(pg_config=self.mysql_user_manager.pg_config)
+ assert user.user_id is not None
alm.create(
user_id=user.user_id,
level=AuditLogLevel(level),
@@ -107,8 +135,6 @@ class UserManager:
event_value=event_value,
)
- return None
-
def cache_clear(self) -> None:
# Generally this is used in testing. This clears get_user's TTL cache.
# It does not clear any redis caches; that has to be done separately.
@@ -134,16 +160,16 @@ class UserManager:
Raises UserDoesntExistError if user is not found.
(the * makes all arguments keyword-only arguments)
"""
- assert (
- (product_id and product_user_id) or user_id or user_uuid
- ), "Must pass either (product_id, product_user_id), or user_id, or uuid"
+ assert (product_id and product_user_id) or user_id or user_uuid, (
+ "Must pass either (product_id, product_user_id), or user_id, or uuid"
+ )
if product_id or product_user_id:
- assert (
- product_id and product_user_id
- ), "Must pass both product_id and product_user_id"
- assert (
- sum(map(bool, [product_id or product_id, user_id, user_uuid])) == 1
- ), "Must pass only 1 of (product_id, product_user_id), or user_id, or uuid"
+ assert product_id and product_user_id, (
+ "Must pass both product_id and product_user_id"
+ )
+ assert sum(map(bool, [product_id or product_id, user_id, user_uuid])) == 1, (
+ "Must pass only 1 of (product_id, product_user_id), or user_id, or uuid"
+ )
user = self.get_user_inmemory_cache(
product_id=product_id,
product_user_id=product_user_id,
@@ -245,13 +271,13 @@ class UserManager:
"""
assert Permission.CREATE in self.sql_permissions
assert self.mysql_user_manager is not None
- assert (
- self.redis_user_manager is not None
- ), "need at least redis to synchronize user creation"
+ assert self.redis_user_manager is not None, (
+ "need at least redis to synchronize user creation"
+ )
- assert (
- self.user_manager_limiter is not None
- ), "Need user_manager_limiter to get_or_create_user"
+ assert self.user_manager_limiter is not None, (
+ "Need user_manager_limiter to get_or_create_user"
+ )
# Attempt to create common_struct solely for validation purposes
if not User.is_valid_ubp(
product_id=product_id, product_user_id=product_user_id
@@ -275,9 +301,9 @@ class UserManager:
created: datetime | None = None,
) -> User:
- assert (
- self.user_manager_limiter is not None
- ), "Need user_manager_limiter to create_user"
+ assert self.user_manager_limiter is not None, (
+ "Need user_manager_limiter to create_user"
+ )
assert product_id or product, "Needs a product_id or a Product instance"
if product is None:
@@ -354,9 +380,9 @@ class UserManager:
user_ids: Collection[int] | None = None,
user_uuids: Collection[str] | None = None,
) -> list[User]:
- assert (user_ids or user_uuids) and not (
- user_ids and user_uuids
- ), "Must pass ONE of user_ids, user_uuids"
+ assert (user_ids or user_uuids) and not (user_ids and user_uuids), (
+ "Must pass ONE of user_ids, user_uuids"
+ )
return self.mysql_user_manager_rr.fetch(
user_ids=user_ids, user_uuids=user_uuids
)