A comprehensive Python wrapper for the Linear API with rich Pydantic models, simplified workflows, and an object-oriented design.
User operations including profile access, team memberships, issue assignments, and organizational data management.
Basic user management operations for accessing user profiles and information.
def get(self, user_id: str) -> LinearUser:
"""
Fetch a Linear user by ID with comprehensive user details and organization information.
Args:
user_id: The ID of the user to fetch
Returns:
LinearUser with complete details
Raises:
ValueError: If user not found
"""
def get_all(self) -> Dict[str, LinearUser]:
"""
Get all users in the organization.
Returns:
Dictionary mapping user IDs to LinearUser objects
"""
def get_me(self) -> LinearUser:
"""
Get the current user based on the API key.
Returns:
LinearUser object for the authenticated user
Raises:
ValueError: If authentication fails
"""Usage examples:
from linear_api import LinearClient
client = LinearClient()
# Get current user
me = client.users.get_me()
print(f"Current user: {me.name} ({me.email})")
print(f"Admin: {me.admin}, Active: {me.active}")
# Get specific user
user = client.users.get("user-id")
print(f"User: {user.displayName} - {user.email}")
# Get all organization users
all_users = client.users.get_all()
print(f"Organization has {len(all_users)} users")Find users by various identifiers and attributes.
def get_email_map(self) -> Dict[str, str]:
"""
Get a mapping of user IDs to their email addresses.
Returns:
Dictionary mapping user IDs to email addresses
"""
def get_id_by_email(self, email: str) -> str:
"""
Get a user ID by their email address.
Args:
email: The email address to search for
Returns:
User ID string
Raises:
ValueError: If user not found
"""
def get_id_by_name(self, name: str) -> str:
"""
Get a user ID by their name (fuzzy match with exact, case-insensitive,
and partial matching).
Args:
name: The name to search for
Returns:
User ID string
Raises:
ValueError: If user not found
"""Usage examples:
# Get email mapping for all users
email_map = client.users.get_email_map()
print(f"Found {len(email_map)} users with emails")
# Find user by email
user_id = client.users.get_id_by_email("john.doe@company.com")
user = client.users.get(user_id)
# Find user by name (fuzzy matching)
user_id = client.users.get_id_by_name("John Doe")
# Also works with partial matches:
# user_id = client.users.get_id_by_name("john")
# user_id = client.users.get_id_by_name("John")Access issues assigned to or created by users.
def get_assigned_issues(self, user_id: str) -> Dict[str, LinearIssue]:
"""
Get issues assigned to a user.
Args:
user_id: The ID of the user
Returns:
Dictionary mapping issue IDs to LinearIssue objects
"""
def get_created_issues(self, user_id: str) -> List[Dict[str, Any]]:
"""
Get issues created by a user with basic issue information.
Args:
user_id: The ID of the user
Returns:
List of issue dictionaries
"""Usage examples:
# Get user's assigned issues
assigned_issues = client.users.get_assigned_issues("user-id")
print(f"User has {len(assigned_issues)} assigned issues")
# Group by status
from collections import defaultdict
by_status = defaultdict(list)
for issue in assigned_issues.values():
by_status[issue.state.name].append(issue)
print("Issues by status:")
for status, issues in by_status.items():
print(f" {status}: {len(issues)} issues")
# Get issues created by user
created_issues = client.users.get_created_issues("user-id")
print(f"User has created {len(created_issues)} issues")Access user team memberships and organizational relationships.
def get_team_memberships(self, user_id: str) -> List[Dict[str, Any]]:
"""
Get team memberships for a user with team information included.
Args:
user_id: The ID of the user
Returns:
List of team membership dictionaries
"""
def get_teams(self, user_id: str) -> List[LinearTeam]:
"""
Get teams that a user is a member of.
Args:
user_id: The ID of the user
Returns:
List of LinearTeam objects
"""Usage examples:
# Get user's team memberships
memberships = client.users.get_team_memberships("user-id")
print(f"User is a member of {len(memberships)} teams")
for membership in memberships:
team_info = membership.get('team', {})
is_owner = membership.get('owner', False)
print(f" - {team_info.get('name')} {'(Owner)' if is_owner else ''}")
# Get teams directly
teams = client.users.get_teams("user-id")
for team in teams:
print(f"Team: {team.name} ({team.key})")Access document and issue drafts created by users.
def get_drafts(self, user_id: str) -> List[Draft]:
"""
Get document drafts created by a user.
Args:
user_id: The ID of the user
Returns:
List of Draft objects
"""
def get_issue_drafts(self, user_id: str) -> List[IssueDraft]:
"""
Get issue drafts created by a user.
Args:
user_id: The ID of the user
Returns:
List of IssueDraft objects
"""Usage examples:
# Get user's document drafts
drafts = client.users.get_drafts("user-id")
print(f"User has {len(drafts)} document drafts")
# Get user's issue drafts
issue_drafts = client.users.get_issue_drafts("user-id")
print(f"User has {len(issue_drafts)} issue drafts")Control caching for user-related data.
def invalidate_cache(self) -> None:
"""
Invalidate all user-related caches.
"""Usage examples:
# Clear user caches
client.users.invalidate_cache()The LinearUser model includes comprehensive user information:
user = client.users.get_me()
# Required fields
print(f"ID: {user.id}")
print(f"Name: {user.name}")
print(f"Display Name: {user.displayName}")
print(f"Email: {user.email}")
print(f"Created: {user.createdAt}")
print(f"Updated: {user.updatedAt}")
# Boolean status fields
print(f"Active: {user.active}")
print(f"Admin: {user.admin}")
print(f"App User: {user.app}")
print(f"Guest: {user.guest}")
print(f"Is Me: {user.isMe}")# Optional profile information
if user.avatarUrl:
print(f"Avatar: {user.avatarUrl}")
if user.description:
print(f"Description: {user.description}")
if user.timezone:
print(f"Timezone: {user.timezone}")
if user.statusEmoji:
print(f"Status: {user.statusEmoji} {user.statusLabel}")
# Usage statistics
if hasattr(user, 'createdIssueCount'):
print(f"Created issues: {user.createdIssueCount}")
# Calendar integration
if user.calendarHash:
print(f"Calendar hash: {user.calendarHash}")def analyze_user_activity(user_id: str):
user = client.users.get(user_id)
assigned_issues = client.users.get_assigned_issues(user_id)
created_issues = client.users.get_created_issues(user_id)
teams = client.users.get_teams(user_id)
print(f"User Activity Report: {user.displayName}")
print(f"Email: {user.email}")
print(f"Status: {'Active' if user.active else 'Inactive'}")
print(f"Admin: {'Yes' if user.admin else 'No'}")
print(f"Teams: {len(teams)}")
print(f"Assigned Issues: {len(assigned_issues)}")
print(f"Created Issues: {len(created_issues)}")
# Analyze assigned issue states
if assigned_issues:
state_counts = {}
for issue in assigned_issues.values():
state = issue.state.name
state_counts[state] = state_counts.get(state, 0) + 1
print("\nAssigned Issues by State:")
for state, count in state_counts.items():
print(f" {state}: {count}")
# List teams
if teams:
print("\nTeam Memberships:")
for team in teams:
print(f" - {team.name} ({team.key})")
return {
"user": user,
"team_count": len(teams),
"assigned_issues": len(assigned_issues),
"created_issues": len(created_issues)
}
# Analyze user activity
activity = analyze_user_activity("user-id")def create_team_directory():
all_users = client.users.get_all()
# Filter active users
active_users = {uid: user for uid, user in all_users.items() if user.active}
# Group by team membership
team_members = {}
for user_id, user in active_users.items():
teams = client.users.get_teams(user_id)
for team in teams:
if team.id not in team_members:
team_members[team.id] = {
'team': team,
'members': []
}
team_members[team.id]['members'].append(user)
print("Team Directory:")
print("=" * 50)
for team_id, team_data in team_members.items():
team = team_data['team']
members = team_data['members']
print(f"\n{team.name} ({team.key})")
print("-" * 30)
# Sort members by name
members.sort(key=lambda u: u.displayName.lower())
for member in members:
admin_badge = " [ADMIN]" if member.admin else ""
print(f" • {member.displayName} ({member.email}){admin_badge}")
print(f" Total: {len(members)} members")
# Create directory
create_team_directory()def analyze_workloads():
all_users = client.users.get_all()
workloads = []
for user_id, user in all_users.items():
if not user.active:
continue
assigned_issues = client.users.get_assigned_issues(user_id)
# Calculate workload metrics
total_issues = len(assigned_issues)
in_progress = sum(1 for issue in assigned_issues.values()
if issue.state.type == 'started')
workloads.append({
'user': user,
'total_issues': total_issues,
'in_progress': in_progress,
'workload_score': in_progress + (total_issues * 0.3)
})
# Sort by workload
workloads.sort(key=lambda w: w['workload_score'], reverse=True)
print("User Workload Analysis:")
print("=" * 60)
print(f"{'Name':<25} {'Total':<8} {'Active':<8} {'Workload':<10}")
print("-" * 60)
for workload in workloads[:20]: # Top 20
user = workload['user']
print(f"{user.displayName:<25} {workload['total_issues']:<8} "
f"{workload['in_progress']:<8} {workload['workload_score']:<10.1f}")
# Analyze workloads
analyze_workloads()def search_users(query: str, filters: dict = None):
"""
Search for users with flexible criteria.
Args:
query: Search term for name or email
filters: Additional filters (active, admin, teams, etc.)
"""
all_users = client.users.get_all()
results = []
query_lower = query.lower()
for user_id, user in all_users.items():
# Text search
matches_query = (
query_lower in user.name.lower() or
query_lower in user.displayName.lower() or
query_lower in user.email.lower()
)
if not matches_query:
continue
# Apply filters
if filters:
if 'active' in filters and user.active != filters['active']:
continue
if 'admin' in filters and user.admin != filters['admin']:
continue
results.append(user)
# Sort by relevance (exact matches first, then by name)
def sort_key(user):
exact_name = user.name.lower() == query_lower
exact_display = user.displayName.lower() == query_lower
exact_email = user.email.lower() == query_lower
return (not (exact_name or exact_display or exact_email), user.displayName.lower())
results.sort(key=sort_key)
print(f"Search results for '{query}':")
for user in results[:10]: # Top 10 results
status = "Active" if user.active else "Inactive"
admin = " [ADMIN]" if user.admin else ""
print(f" • {user.displayName} ({user.email}) - {status}{admin}")
return results
# Search examples
search_users("john") # Find users named John
search_users("@company.com") # Find users by email domain
search_users("", {'active': True, 'admin': True}) # Find active adminsdef get_organization_metrics():
all_users = client.users.get_all()
# Calculate metrics
total_users = len(all_users)
active_users = sum(1 for user in all_users.values() if user.active)
admin_users = sum(1 for user in all_users.values() if user.admin)
guest_users = sum(1 for user in all_users.values() if user.guest)
print("Organization User Metrics:")
print("=" * 40)
print(f"Total Users: {total_users}")
print(f"Active Users: {active_users} ({active_users/total_users:.1%})")
print(f"Admin Users: {admin_users} ({admin_users/total_users:.1%})")
print(f"Guest Users: {guest_users} ({guest_users/total_users:.1%})")
print(f"Inactive Users: {total_users - active_users}")
# Get all teams for team membership analysis
all_teams = client.teams.get_all()
team_memberships = 0
for user in all_users.values():
if user.active:
teams = client.users.get_teams(user.id)
team_memberships += len(teams)
avg_teams = team_memberships / active_users if active_users > 0 else 0
print(f"Average teams per active user: {avg_teams:.1f}")
return {
'total': total_users,
'active': active_users,
'admin': admin_users,
'guest': guest_users,
'avg_teams_per_user': avg_teams
}
# Get organization metrics
metrics = get_organization_metrics()Install with Tessl CLI
npx tessl i tessl/pypi-linear-api