CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/pypi-linear-api

A comprehensive Python wrapper for the Linear API with rich Pydantic models, simplified workflows, and an object-oriented design.

Overview
Eval results
Files

data-models-types.mddocs/

Data Models and Types

Rich Pydantic models covering all Linear entities with complete field definitions, input/output types, enums, and connection types for GraphQL pagination.

Core Data Models

Issue Models

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 attachment
class 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]] = None

Usage 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 pagination

Label and Attachment Models

Models 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=True
class 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] = None
class 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]] = None

Usage 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})")

Project Models

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 Models

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=True

Usage 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 Models

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] = None

Usage 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}")

Enums and Constants

Priority and Status Enums

class LinearPriority(Enum):
    """Issue priority levels."""
    URGENT = 0
    HIGH = 1
    MEDIUM = 2
    LOW = 3
    NONE = 4
class 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"

Service Integration Enums

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}")

Base Classes and Utilities

Base Model Class

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."""

Connection Types

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]] = None

Specific 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 entities

Common Models

Shared 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: int

Relationship Models

Models 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] = None

Advanced Model Usage

Model Validation and Type Safety

from 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
)

Dynamic Property Access

# 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}")

Model Serialization

# 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'})

Custom Model Extensions

# 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

docs

cache-utilities.md

client-management.md

data-models-types.md

index.md

issue-operations.md

project-management.md

team-administration.md

user-management.md

tile.json