"""Format Neo4j identity query results into flat CSV format.""" from collections import Counter from datetime import datetime, timezone from typing import Any def format_identity_result(auth0_user_id: str, query_result: dict[str, Any] | None, last_login: str | None = None) -> dict[str, Any]: """ Transform a Neo4j identity query result into a flat dictionary for CSV export. Args: auth0_user_id: The Auth0 user ID that was queried query_result: The Neo4j query result, or None if identity not found Returns: Dictionary with flattened identity data for CSV export :param query_result: :param auth0_user_id: :param last_login: """ # Calculate days since last login days_since_login: int | str = '' if last_login: try: last_login_dt = datetime.fromisoformat(last_login.replace('Z', '+00:00')) now = datetime.now(timezone.utc) days_since_login = (now - last_login_dt).days except Exception: days_since_login = 'Error' # Handle not found case if not query_result: return { 'auth0_user_id': auth0_user_id, 'found': False, 'identity_id': '', 'active': '', 'email': '', 'name': '', 'last_login': last_login or '', 'days_since_login': days_since_login, 'profile_count': 0, 'active_access_count': 0, 'deleted_access_count': 0, 'tenant_ids': '', 'tenant_access_summary': 'Not found', } # Extract identity fields identity = query_result.get('identity', {}) identity_id = identity.get('id', '') active = identity.get('active', 'N') email = identity.get('email', '') name = identity.get('name', '') # Process profiles profiles = identity.get('profiles', []) profile_count = len(profiles) # Track access types access_relationship_types = { 'HAS_ACCESS_TO', 'HAS_ADMIN_ACCESS_TO', 'DELETED_HAS_ACCESS_TO', 'DELETED_HAS_ADMIN_ACCESS_TO', } # Relevant tenant labels (exclude Orchard, Label) relevant_tenant_types = {'Vendor', 'Subaccount', 'LabelParticipant', 'Collaborator'} active_access_count = 0 deleted_access_count = 0 tenant_type_counter: Counter[str] = Counter() tenant_ids_by_type: dict[str, set[str]] = {} # Process all relationships across all profiles for profile in profiles: relationships = profile.get('relationships', []) for rel in relationships: rel_type = rel.get('relationshipType', '') # Only count access-related relationships if rel_type not in access_relationship_types: continue # Count active vs deleted if rel_type.startswith('DELETED_'): deleted_access_count += 1 else: active_access_count += 1 # Collect tenant IDs by type tenant_id = rel.get('tenantId') tenant_labels = rel.get('tenantLabels', []) if tenant_id: # Find the relevant tenant type for label in tenant_labels: if label in relevant_tenant_types: if label not in tenant_ids_by_type: tenant_ids_by_type[label] = set() tenant_ids_by_type[label].add(str(tenant_id)) tenant_type_counter[label] += 1 break # Only use first matching relevant type # Build access summary total_access = active_access_count + deleted_access_count if total_access == 0: access_summary = 'No access' else: summary_parts = [f'{label}: {count}' for label, count in tenant_type_counter.most_common()] access_summary = ', '.join(summary_parts) if summary_parts else 'No tenant types' # Format tenant IDs by type: "Vendor: 123,456 - LabelParticipant: 789" tenant_id_parts = [] for tenant_type in ['Vendor', 'Subaccount', 'LabelParticipant', 'Collaborator']: if tenant_type in tenant_ids_by_type: ids = ','.join(sorted(tenant_ids_by_type[tenant_type])) tenant_id_parts.append(f'{tenant_type}: {ids}') tenant_ids_str = ' - '.join(tenant_id_parts) if tenant_id_parts else '' return { 'auth0_user_id': auth0_user_id, 'found': True, 'identity_id': identity_id, 'active': active, 'email': email, 'name': name, 'last_login': last_login or '', 'days_since_login': days_since_login, 'profile_count': profile_count, 'active_access_count': active_access_count, 'deleted_access_count': deleted_access_count, 'tenant_ids': tenant_ids_str, 'tenant_access_summary': access_summary, }