A comprehensive Python wrapper for the Linear API with rich Pydantic models, simplified workflows, and an object-oriented design.
Rich Pydantic models covering all Linear entities with complete field definitions, input/output types, enums, and connection types for GraphQL pagination.
Comprehensive models for issue management with full field coverage and dynamic properties.
class LinearIssue(LinearModel):
# Required fields
id: str
title: str
url: str
state: LinearState
priority: LinearPriority
team: LinearTeam
createdAt: datetime
updatedAt: datetime
number: int
customerTicketCount: int
# Optional fields (50+ additional fields)
description: Optional[str] = None
assignee: Optional[LinearUser] = None
project: Optional[LinearProject] = None
labels: Optional[List[LinearLabel]] = None
dueDate: Optional[datetime] = None
parentId: Optional[str] = None
estimate: Optional[int] = None
attachments: Optional[List[LinearAttachment]] = None
# ... many more optional fields
# Dynamic properties (accessed through client reference)
@property
def parent(self) -> Optional['LinearIssue']: ...
@property
def children(self) -> Dict[str, 'LinearIssue']: ...
@property
def comments(self) -> List['Comment']: ...
@property
def history(self) -> List[Dict[str, Any]]: ...
@property
def relations(self) -> List['IssueRelation']: ...
@property
def metadata(self) -> Dict[str, Any]: ...class LinearIssueInput(LinearModel):
"""Input model for creating issues with metadata auto-conversion."""
title: str # Required
teamName: str # Required
description: Optional[str] = None
priority: Optional[LinearPriority] = None
assigneeId: Optional[str] = None
projectId: Optional[str] = None
stateId: Optional[str] = None
parentId: Optional[str] = None
labelIds: Optional[List[str]] = None
estimate: Optional[int] = None
dueDate: Optional[datetime] = None
metadata: Optional[Dict[str, Any]] = None # Auto-converted to attachmentclass LinearIssueUpdateInput(LinearModel):
"""Input model for updating issues with partial field updates."""
title: Optional[str] = None
description: Optional[str] = None
priority: Optional[LinearPriority] = None
assigneeId: Optional[str] = None
projectId: Optional[str] = None
stateId: Optional[str] = None
teamId: Optional[str] = None
labelIds: Optional[List[str]] = None
estimate: Optional[int] = None
dueDate: Optional[datetime] = None
metadata: Optional[Dict[str, Any]] = NoneUsage examples:
from linear_api import LinearIssue, LinearIssueInput, LinearPriority
# Create issue input
issue_input = LinearIssueInput(
title="New feature request",
teamName="Engineering",
description="Detailed feature description",
priority=LinearPriority.HIGH,
metadata={"category": "feature", "source": "customer-feedback"}
)
# Access issue properties
issue = client.issues.get("issue-id")
print(f"Issue: {issue.title}")
print(f"State: {issue.state.name}")
print(f"Priority: {issue.priority}")
print(f"Assignee: {issue.assignee.name if issue.assignee else 'Unassigned'}")
# Access dynamic properties
children = issue.children # Automatically fetched from API
comments = issue.comments # Automatically fetched with paginationModels for issue categorization and file attachments.
class LinearLabel(LinearModel):
id: str
name: str
color: str
archivedAt: Optional[datetime] = None
createdAt: Optional[datetime] = None
updatedAt: Optional[datetime] = None
description: Optional[str] = None
isGroup: Optional[bool] = None
inheritedFrom: Optional[Dict[str, Any]] = None
parent: Optional[Dict[str, Any]] = None
creator: Optional[LinearUser] = None
issue_ids: Optional[List[str]] = None # When include_issue_ids=Trueclass LinearAttachment(LinearModel):
id: str
url: str
title: Optional[str] = None
subtitle: Optional[str] = None
metadata: Optional[Dict[str, Any]] = None
issueId: str
createdAt: datetime
updatedAt: datetime
creator: Optional[LinearUser] = Noneclass LinearAttachmentInput(LinearModel):
"""Input for creating attachments with metadata support."""
url: str # Required
issueId: str # Required
title: Optional[str] = None
subtitle: Optional[str] = None
metadata: Optional[Dict[str, Any]] = NoneUsage examples:
# Create attachment
attachment_input = LinearAttachmentInput(
url="https://docs.company.com/spec.pdf",
issueId="issue-id",
title="Technical Specification",
subtitle="Version 2.0",
metadata={"version": "2.0", "type": "specification"}
)
# Access label information
for label in issue.labels or []:
print(f"Label: {label.name} ({label.color})")Comprehensive project management models with lifecycle tracking.
class LinearProject(LinearModel):
# Required fields
id: str
name: str
createdAt: datetime
updatedAt: datetime
slugId: str
url: str
color: str
priority: int
status: ProjectStatus
progress: float
scope: int
# Optional fields (20+ additional fields)
description: Optional[str] = None
startDate: Optional[TimelessDate] = None
targetDate: Optional[TimelessDate] = None
completedAt: Optional[datetime] = None
canceledAt: Optional[datetime] = None
health: Optional[str] = None
# ... many more optional fields
# Dynamic properties
@property
def members(self) -> List['LinearUser']: ...
@property
def issues(self) -> List['LinearIssue']: ...
@property
def projectUpdates(self) -> List['ProjectUpdate']: ...
@property
def relations(self) -> List['ProjectRelation']: ...class ProjectStatus(LinearModel):
type: ProjectStatusType # Enum: PLANNED, BACKLOG, STARTED, etc.class ProjectMilestone(LinearModel):
id: str
name: str
# Additional milestone fields...Usage examples:
from linear_api import ProjectStatusType
# Access project information
project = client.projects.get("project-id")
print(f"Project: {project.name}")
print(f"Status: {project.status.type}")
print(f"Progress: {project.progress}%")
print(f"Priority: {project.priority}")
# Check project dates
if project.startDate:
print(f"Start: {project.startDate.year}-{project.startDate.month}-{project.startDate.day}")
if project.targetDate:
print(f"Target: {project.targetDate.year}-{project.targetDate.month}-{project.targetDate.day}")Team configuration and workflow models with extensive settings.
class LinearTeam(LinearModel):
# Required fields
id: str
name: str
key: str
# Optional configuration fields (50+ fields)
description: Optional[str] = None
color: Optional[str] = None
icon: Optional[str] = None
createdAt: Optional[datetime] = None
updatedAt: Optional[datetime] = None
parentId: Optional[str] = None
# Automation settings
autoArchivePeriod: Optional[int] = None
autoCloseChildIssues: Optional[bool] = None
autoCloseParentIssues: Optional[bool] = None
# Cycle configuration
cycleDuration: Optional[int] = None
cycleStartDay: Optional[int] = None
cyclesEnabled: Optional[bool] = None
# Estimation settings
defaultIssueEstimate: Optional[int] = None
issueEstimationType: Optional[str] = None
# Template settings
defaultIssueState: Optional[Dict[str, Any]] = None
defaultProjectTemplate: Optional[Dict[str, Any]] = None
# SCIM integration
scimGroupName: Optional[str] = None
scimManaged: Optional[bool] = None
# Dynamic properties
@property
def members(self) -> List['LinearUser']: ...
@property
def states(self) -> List['LinearState']: ...
@property
def labels(self) -> List['LinearLabel']: ...
@property
def activeCycle(self) -> Optional[Dict[str, Any]]: ...
@property
def projects(self) -> Dict[str, Any]: ...class LinearState(LinearModel):
id: str
name: str
type: str
color: str
archivedAt: Optional[datetime] = None
createdAt: Optional[datetime] = None
updatedAt: Optional[datetime] = None
description: Optional[str] = None
position: Optional[int] = None
inheritedFrom: Optional[Dict[str, Any]] = None
issue_ids: Optional[List[str]] = None # When include_issue_ids=TrueUsage examples:
# Access team configuration
team = client.teams.get("team-id")
print(f"Team: {team.name} ({team.key})")
print(f"Cycles enabled: {team.cyclesEnabled}")
print(f"Cycle duration: {team.cycleDuration}")
print(f"Auto-archive period: {team.autoArchivePeriod}")
# Access team states
states = team.states
for state in states:
print(f"State: {state.name} ({state.type}) - {state.color}")User profile and organizational relationship models.
class LinearUser(LinearModel):
# Required fields
id: str
name: str
displayName: str
email: str
createdAt: datetime
updatedAt: datetime
# Boolean status fields
active: bool
admin: bool
app: bool
guest: bool
isMe: bool
# Optional profile fields
avatarUrl: Optional[str] = None
archivedAt: Optional[datetime] = None
avatarBackgroundColor: Optional[str] = None
calendarHash: Optional[str] = None
createdIssueCount: Optional[int] = None
description: Optional[str] = None
disableReason: Optional[str] = None
initials: Optional[str] = None
inviteHash: Optional[str] = None
lastSeen: Optional[datetime] = None
statusEmoji: Optional[str] = None
statusLabel: Optional[str] = None
statusUntilAt: Optional[datetime] = None
timezone: Optional[str] = None
url: Optional[str] = None
# Organizational context
organization: Optional[Organization] = None
# Dynamic properties
@property
def assignedIssues(self) -> Dict[str, 'LinearIssue']: ...
@property
def createdIssues(self) -> List[Dict[str, Any]]: ...
@property
def teams(self) -> List['LinearTeam']: ...
@property
def teamMemberships(self) -> List[Dict[str, Any]]: ...class LinearUserReference(LinearModel):
"""Simplified user reference for nested objects."""
id: str
name: str
displayName: str
email: Optional[str] = None
createdAt: Optional[datetime] = None
updatedAt: Optional[datetime] = None
avatarUrl: Optional[str] = NoneUsage examples:
# Access user information
user = client.users.get_me()
print(f"User: {user.displayName} ({user.email})")
print(f"Status: {'Active' if user.active else 'Inactive'}")
print(f"Admin: {'Yes' if user.admin else 'No'}")
# Access profile details
if user.statusEmoji:
print(f"Status: {user.statusEmoji} {user.statusLabel}")
if user.timezone:
print(f"Timezone: {user.timezone}")class LinearPriority(Enum):
"""Issue priority levels."""
URGENT = 0
HIGH = 1
MEDIUM = 2
LOW = 3
NONE = 4class ProjectStatusType(StrEnum):
"""Project lifecycle status types."""
PLANNED = "planned"
BACKLOG = "backlog"
STARTED = "started"
PAUSED = "paused"
COMPLETED = "completed"
CANCELED = "canceled"class ProjectUpdateHealthType(StrEnum):
"""Project health indicators."""
ON_TRACK = "onTrack"
AT_RISK = "atRisk"
OFF_TRACK = "offTrack"class IntegrationService(StrEnum):
"""Supported integration services."""
ASANA = "asana"
FIGMA = "figma"
GITHUB = "github"
GITLAB = "gitlab"
INTERCOM = "intercom"
JIRA = "jira"
NOTION = "notion"
SLACK = "slack"
ZENDESK = "zendesk"class SLADayCountType(StrEnum):
"""SLA day counting methods."""
ALL = "all"
ONLY_BUSINESS_DAYS = "onlyBusinessDays"Usage examples:
from linear_api import LinearPriority, ProjectStatusType, IntegrationService, SLADayCountType
# Use priority enum
issue_input = LinearIssueInput(
title="High priority issue",
teamName="Engineering",
priority=LinearPriority.HIGH
)
# Use project status enum
client.projects.update("project-id", status=ProjectStatusType.STARTED)
# Use SLA day counting
print("SLA counting methods:")
for method in SLADayCountType:
print(f" - {method.value}")
# Check integration services
print("Supported integrations:")
for service in IntegrationService:
print(f" - {service.value}")class LinearModel(BaseModel):
"""Base class for all Linear domain models with Pydantic integration."""
linear_class_name: ClassVar[str] # Maps to GraphQL type names
known_missing_fields: ClassVar[List[str]] = [] # Tracks missing API fields
known_extra_fields: ClassVar[List[str]] = [] # Tracks additional fields
_client: Any = PrivateAttr() # Private client reference
def with_client(self, client) -> 'LinearModel':
"""Sets client reference for dynamic property access."""
def model_dump(self, **kwargs) -> Dict[str, Any]:
"""Enhanced serialization excluding class variables."""
@classmethod
def get_linear_class_name(cls) -> str:
"""Returns GraphQL type name for this model."""Generic connection models for GraphQL pagination.
class Connection(LinearModel, Generic[T]):
"""Generic connection model for GraphQL pagination."""
nodes: List[T]
pageInfo: Optional[Dict[str, Any]] = NoneSpecific connection types for different entities:
class CommentConnection(LinearModel):
nodes: List[Comment]
pageInfo: Optional[Dict[str, Any]] = None
class UserConnection(LinearModel):
nodes: List[LinearUserReference]
pageInfo: Optional[Dict[str, Any]] = None
class TeamConnection(LinearModel):
nodes: List[LinearTeam]
pageInfo: Optional[Dict[str, Any]] = None
# ... many more connection types for different entitiesShared models used across different domain areas.
class Organization(LinearModel):
id: str
name: str
class Comment(LinearModel):
id: str
body: str
createdAt: datetime
updatedAt: datetime
creator: Optional[LinearUserReference] = None
class Document(LinearModel):
id: str
title: Optional[str] = None
icon: Optional[str] = None
createdAt: datetime
updatedAt: datetime
class EntityExternalLink(LinearModel):
id: str
url: str
label: Optional[str] = None
createdAt: datetime
class Reaction(LinearModel):
id: str
emoji: str
createdAt: datetime
user: Optional[LinearUserReference] = None
class TimelessDate(LinearModel):
"""Date without time information."""
year: int
month: int
day: intModels for managing relationships between entities.
class IssueRelation(LinearModel):
id: str
type: str
relatedIssue: Optional[Dict[str, Any]] = None
createdAt: datetime
class ProjectRelation(LinearModel):
id: str
createdAt: datetime
type: str
project: Optional[Dict[str, Any]] = None
relatedProject: Optional[Dict[str, Any]] = None
class TeamMembership(LinearModel):
id: str
createdAt: Optional[datetime] = None
updatedAt: Optional[datetime] = None
archivedAt: Optional[datetime] = None
owner: Optional[bool] = None
sortOrder: Optional[int] = None
user: Optional[LinearUser] = None
team: Optional[LinearTeam] = Nonefrom pydantic import ValidationError
try:
# Pydantic validation ensures type safety
issue_input = LinearIssueInput(
title="Valid issue",
teamName="Engineering",
priority="INVALID_PRIORITY" # This will raise ValidationError
)
except ValidationError as e:
print(f"Validation error: {e}")
# Proper usage with enum
issue_input = LinearIssueInput(
title="Valid issue",
teamName="Engineering",
priority=LinearPriority.HIGH # Type-safe enum usage
)# Models with client references can access related data dynamically
issue = client.issues.get("issue-id")
# These properties trigger API calls automatically
parent_issue = issue.parent # Fetches parent issue if exists
child_issues = issue.children # Fetches all child issues
comments = issue.comments # Fetches all comments with pagination
history = issue.history # Fetches change history
# Properties return appropriate model types
for comment in comments:
print(f"Comment by {comment.creator.name}: {comment.body}")# Models can be serialized to dictionaries
issue_dict = issue.model_dump()
print(f"Issue as dict: {issue_dict}")
# Or to JSON
issue_json = issue.model_dump_json()
print(f"Issue as JSON: {issue_json}")
# Exclude private fields
clean_dict = issue.model_dump(exclude={'_client'})# Models can be extended with custom properties
class ExtendedLinearIssue(LinearIssue):
@property
def is_overdue(self) -> bool:
"""Check if issue is overdue."""
if not self.dueDate:
return False
return datetime.now() > self.dueDate
@property
def priority_name(self) -> str:
"""Get human-readable priority name."""
priority_names = {
LinearPriority.URGENT: "🔴 Urgent",
LinearPriority.HIGH: "🟠 High",
LinearPriority.MEDIUM: "🟡 Medium",
LinearPriority.LOW: "🔵 Low",
LinearPriority.NONE: "⚪ None"
}
return priority_names.get(self.priority, "Unknown")
# Use extended model
issue = client.issues.get("issue-id")
extended_issue = ExtendedLinearIssue(**issue.model_dump())
print(f"Priority: {extended_issue.priority_name}")
print(f"Overdue: {extended_issue.is_overdue}")Install with Tessl CLI
npx tessl i tessl/pypi-linear-api