import contextvars import uuid from contextvars import ContextVar from dataclasses import asdict, dataclass, field from typing import Any, Dict, Iterable, List, Mapping, Optional from audience_common import constants from audience_common.utils.dictutil import exclude _correlation_id: ContextVar[Optional[str]] = ContextVar("correlation_id", default=None) def get_correlation_id() -> Optional[str]: """Get the current correlation id.""" return _correlation_id.get() def set_correlation_id( correlation_id: Optional[str], ) -> contextvars.Token: # type: ignore[type-arg] """Set the current correlation id.""" return _correlation_id.set(correlation_id) def reset_correlation_id(token: contextvars.Token) -> None: # type: ignore[type-arg] """Reset the current correlation id.""" _correlation_id.reset(token) @dataclass class RequestContext: """Request context.""" context_type: Optional[str] = constants.CONTEXT_TYPE_NONE requestor_service_name: Optional[str] = None identity_id: Optional[str] = None identity_uuid: Optional[str] = None profile_type: Optional[str] = None profile_id: Optional[int] = None profile_uuid: Optional[uuid.UUID] = None roles: List[str] = field(default_factory=list) orchard_user_id: Optional[str] = None authorization: Optional[str] = None brand: str = constants.DEFAULT_BRAND def dict( self, exclude_empty: bool = False, exclude_keys: Optional[Iterable[str]] = None ) -> Dict[str, Any]: d = asdict(self) if exclude_keys: d = exclude(d, keys=exclude_keys) if exclude_empty: return {k: v for k, v in d.items() if v} return d def request_context_from_headers( headers: Mapping[str, str], label_profile: bool = False ) -> RequestContext: """Create a request context from headers.""" # account context headers grass_account_type = headers.get(constants.HEADER_GRASS_ACCOUNT_TYPE) grass_account_id = headers.get(constants.HEADER_GRASS_ACCOUNT_ID) orchard_user_id = headers.get(constants.HEADER_ORCHARD_USER_ID) # profile context headers profile_type = headers.get(constants.HEADER_ORCHARD_PROFILE_TYPE) profile_id = headers.get(constants.HEADER_ORCHARD_PROFILE_ID) profile_uuid = headers.get(constants.HEADER_ORCHARD_PROFILE_UUID) # identity identity_id = headers.get(constants.HEADER_ORCHARD_IDENTITY_ID) identity_uuid = headers.get(constants.HEADER_ORCHARD_IDENTITY_UUID) # roles roles_str: Optional[str] = headers.get(constants.HEADER_ORCHARD_ROLES) if roles_str: roles = roles_str.split(",") else: roles = [] # add the requesting microservice name requestor_service_name = headers.get(constants.ORCHARD_REQUESTOR_SERVICE) context_type: Optional[str] = constants.CONTEXT_TYPE_ERROR # profile based context takes precedence if (profile_type and profile_id) or profile_uuid: context_type = constants.CONTEXT_TYPE_PROFILE # account based context elif (grass_account_type and grass_account_id) or orchard_user_id: if orchard_user_id and label_profile: if orchard_user_id.startswith("alw"): profile_id = orchard_user_id.lstrip("alw:") profile_type = constants.PROFILE_TYPE_LABEL context_type = constants.CONTEXT_TYPE_PROFILE if orchard_user_id.startswith("oa"): profile_id = orchard_user_id.lstrip("oa:") profile_type = constants.PROFILE_TYPE_ORCH_ADMIN context_type = constants.CONTEXT_TYPE_PROFILE else: context_type = constants.CONTEXT_TYPE_ACCOUNT profile_type = grass_account_type profile_id = grass_account_id # only identity id or identity_uuid elif (context_type != constants.CONTEXT_TYPE_ACCOUNT) and ( identity_id or identity_uuid ): context_type = constants.CONTEXT_TYPE_PROFILE # no context elif ( not grass_account_type and not grass_account_id and not orchard_user_id and not profile_type and not profile_id and not identity_id and not identity_uuid ): context_type = constants.CONTEXT_TYPE_NONE authorization: Optional[str] = headers.get("authorization") # TODO: get brand from jwt brand = constants.DEFAULT_BRAND return RequestContext( requestor_service_name=requestor_service_name, context_type=context_type, identity_id=identity_id, identity_uuid=identity_uuid, profile_type=profile_type, profile_id=_parse_profile_id(profile_id), profile_uuid=uuid.UUID(profile_uuid) if profile_uuid else None, roles=roles, orchard_user_id=orchard_user_id, authorization=authorization, brand=brand, ) def _parse_profile_id(profile_id: Any) -> Optional[int]: return int(profile_id) if profile_id and str.isnumeric(profile_id) else None _request_context: ContextVar[Optional[RequestContext]] = ContextVar( "request_context", default=None ) def get_request_context() -> Optional[RequestContext]: """Get the current request context.""" return _request_context.get() def set_request_context( request_context: Optional[RequestContext], ) -> contextvars.Token: # type: ignore[type-arg] """Set the current request context.""" return _request_context.set(request_context) def reset_request_context( token: contextvars.Token, # type: ignore[type-arg] ) -> None: """Reset the current request context.""" _request_context.reset(token)