@@ -49,25 +49,27 @@ def __init__(
4949 @with_db_transaction
5050 @Authz .authz_change (AuthzOperation .insert_many , ResourceType .user_namespace )
5151 @dispatch_message (events .InsertUserNamespace )
52- async def generate_user_namespaces (
53- self , * , session : AsyncSession | None = None
54- ) -> list [user_models .UserWithNamespace ]:
52+ async def generate_user_namespaces (self , * , session : AsyncSession | None = None ) -> list [user_models .UserInfo ]:
5553 """Generate user namespaces if the user table has data and the namespaces table is empty."""
5654 if not session :
5755 raise errors .ProgrammingError (message = "A database session is required" )
5856 # NOTE: lock to make sure another instance of the data service cannot insert/update but can read
59- output : list [user_models .UserWithNamespace ] = []
57+ output : list [user_models .UserInfo ] = []
6058 await session .execute (text ("LOCK TABLE common.namespaces IN EXCLUSIVE MODE" ))
6159 at_least_one_namespace = (await session .execute (select (schemas .NamespaceORM ).limit (1 ))).one_or_none ()
6260 if at_least_one_namespace :
6361 logger .info ("Found at least one user namespace, skipping creation" )
64- return output
62+ return []
6563 logger .info ("Found zero user namespaces, will try to create them from users table" )
6664 res = await session .scalars (select (user_schemas .UserORM ))
6765 for user in res :
68- ns = await self ._insert_user_namespace (session , user , retry_enumerate = 10 , retry_random = True )
66+ slug = base_models .Slug .from_user (user .email , user .first_name , user .last_name , user .keycloak_id )
67+ ns = await self ._insert_user_namespace (
68+ session , user .keycloak_id , slug .value , retry_enumerate = 10 , retry_random = True
69+ )
6970 logger .info (f"Creating user namespace { ns } " )
70- output .append (user_models .UserWithNamespace (user .dump (), ns ))
71+ user .namespace = ns
72+ output .append (user .dump ())
7173 logger .info (f"Created { len (output )} user namespaces" )
7274 return output
7375
@@ -329,7 +331,7 @@ async def get_namespaces(
329331 output .append (ns_orm .dump ())
330332 return output , group_count
331333
332- async def _get_user_namespaces (self ) -> AsyncGenerator [user_models .UserWithNamespace , None ]:
334+ async def _get_user_namespaces (self ) -> AsyncGenerator [user_models .UserInfo , None ]:
333335 """Lists all user namespaces without regard for authorization or permissions, used for migrations."""
334336 async with self .session_maker () as session , session .begin ():
335337 namespaces = await session .stream_scalars (
@@ -379,39 +381,37 @@ async def get_user_namespace(self, user_id: str) -> models.Namespace | None:
379381 raise errors .ProgrammingError (message = "Found a namespace that has no user associated with it." )
380382 return ns .dump ()
381383
384+ async def _create_user_namespace_slug (
385+ self , session : AsyncSession , user_slug : str , retry_enumerate : int = 0 , retry_random : bool = False
386+ ) -> str :
387+ """Create a valid namespace slug for a user."""
388+ nss = await session .scalars (
389+ select (schemas .NamespaceORM .slug ).where (schemas .NamespaceORM .slug .startswith (user_slug ))
390+ )
391+ nslist = nss .all ()
392+ if user_slug not in nslist :
393+ return user_slug
394+ if retry_enumerate :
395+ for inc in range (1 , retry_enumerate + 1 ):
396+ slug = f"{ user_slug } -{ inc } "
397+ if slug not in nslist :
398+ return slug
399+ if retry_random :
400+ suffix = "" .join ([random .choice (string .ascii_lowercase + string .digits ) for _ in range (8 )]) # nosec B311
401+ slug = f"{ user_slug } -{ suffix } "
402+ if slug not in nslist :
403+ return slug
404+
405+ raise errors .ValidationError (message = f"Cannot create generate a unique namespace slug for the user { user_slug } " )
406+
382407 async def _insert_user_namespace (
383- self , session : AsyncSession , user : schemas . UserORM , retry_enumerate : int = 0 , retry_random : bool = False
384- ) -> models . Namespace :
408+ self , session : AsyncSession , user_id : str , user_slug : str , retry_enumerate : int = 0 , retry_random : bool = False
409+ ) -> schemas . NamespaceORM :
385410 """Insert a new namespace for the user and optionally retry different variations to avoid collisions."""
386- original_slug = user .to_slug ()
387- for inc in range (0 , retry_enumerate + 1 ):
388- # NOTE: on iteration 0 we try with the optimal slug value derived from the user data without any suffix.
389- suffix = ""
390- if inc > 0 :
391- suffix = f"-{ inc } "
392- slug = base_models .Slug .from_name (original_slug .value .lower () + suffix )
393- ns = schemas .NamespaceORM (slug .value , user_id = user .keycloak_id )
394- try :
395- async with session .begin_nested ():
396- session .add (ns )
397- await session .flush ()
398- except IntegrityError :
399- if retry_enumerate == 0 :
400- raise errors .ValidationError (message = f"The user namespace slug { slug .value } already exists" )
401- continue
402- else :
403- await session .refresh (ns )
404- return ns .dump ()
405- if not retry_random :
406- raise errors .ValidationError (
407- message = f"Cannot create generate a unique namespace slug for the user with ID { user .keycloak_id } "
408- )
409- # NOTE: At this point the attempts to generate unique ID have ended and the only option is
410- # to add a small random suffix to avoid uniqueness constraints problems
411- suffix = "-" + "" .join ([random .choice (string .ascii_lowercase + string .digits ) for _ in range (8 )]) # nosec: B311
412- slug = base_models .Slug .from_name (original_slug .value .lower () + suffix )
413- ns = schemas .NamespaceORM (slug .value , user_id = user .keycloak_id )
411+ namespace = await self ._create_user_namespace_slug (session , user_slug , retry_enumerate , retry_random )
412+ slug = base_models .Slug .from_name (namespace )
413+ ns = schemas .NamespaceORM (slug .value , user_id = user_id )
414414 session .add (ns )
415415 await session .flush ()
416416 await session .refresh (ns )
417- return ns . dump ()
417+ return ns
0 commit comments