This PR adds a simplified Linear toolkit focused on core functionality: ## What's Included **Tools:** - `get_issue` - Get detailed information about Linear issues - `get_teams` - Get team information and details **Models:** - `DateRange` enum with comprehensive date range support (TODAY, YESTERDAY, THIS_WEEK, LAST_WEEK, THIS_MONTH, LAST_MONTH, THIS_YEAR, LAST_YEAR, LAST_7_DAYS, LAST_30_DAYS) - Timezone-aware datetime handling following Google toolkit patterns ## What's Simplified This toolkit has been streamlined by removing: - Cycles management tools - Projects management tools - Users management tools - Workflows management tools - Corresponding tests and evaluations for removed features ## Quality Assurance - All linting and formatting checks pass - Comprehensive test coverage for included functionality - Follows established patterns from Google toolkit --------- Co-authored-by: Eric Gustin <34000337+EricGustin@users.noreply.github.com>
194 lines
7.2 KiB
Python
194 lines
7.2 KiB
Python
"""Linear toolkit models and enums"""
|
|
|
|
from datetime import datetime, timedelta, timezone
|
|
from enum import Enum
|
|
from typing import Optional
|
|
|
|
|
|
class DateRange(Enum):
|
|
"""Date range enum for Linear datetime queries
|
|
|
|
Provides consistent datetime range handling.
|
|
All ranges are calculated in UTC and return timezone-aware datetime objects.
|
|
"""
|
|
|
|
TODAY = "today"
|
|
YESTERDAY = "yesterday"
|
|
THIS_WEEK = "this_week"
|
|
LAST_WEEK = "last_week"
|
|
THIS_MONTH = "this_month"
|
|
LAST_MONTH = "last_month"
|
|
THIS_YEAR = "this_year"
|
|
LAST_YEAR = "last_year"
|
|
LAST_7_DAYS = "last_7_days"
|
|
LAST_30_DAYS = "last_30_days"
|
|
|
|
def to_datetime_range(
|
|
self, reference_time: datetime | None = None
|
|
) -> tuple[datetime, datetime]:
|
|
"""Convert DateRange to start and end datetime objects
|
|
|
|
Args:
|
|
reference_time: Optional reference time for calculations. Defaults to now in UTC.
|
|
|
|
Returns:
|
|
Tuple of (start_datetime, end_datetime) - both timezone-aware in UTC
|
|
"""
|
|
now = reference_time or datetime.now(timezone.utc)
|
|
|
|
# Map enum values to their corresponding helper methods
|
|
range_methods = {
|
|
DateRange.TODAY: self._get_today_range,
|
|
DateRange.YESTERDAY: self._get_yesterday_range,
|
|
DateRange.THIS_WEEK: self._get_this_week_range,
|
|
DateRange.LAST_WEEK: self._get_last_week_range,
|
|
DateRange.THIS_MONTH: self._get_this_month_range,
|
|
DateRange.LAST_MONTH: self._get_last_month_range,
|
|
DateRange.THIS_YEAR: self._get_this_year_range,
|
|
DateRange.LAST_YEAR: self._get_last_year_range,
|
|
DateRange.LAST_7_DAYS: lambda n: self._get_last_n_days_range(n, 7),
|
|
DateRange.LAST_30_DAYS: lambda n: self._get_last_n_days_range(n, 30),
|
|
}
|
|
|
|
if self in range_methods:
|
|
return range_methods[self](now)
|
|
else:
|
|
raise ValueError("Invalid DateRange enum value")
|
|
|
|
def _get_today_range(self, now: datetime) -> tuple[datetime, datetime]:
|
|
"""Get today's start and end datetime."""
|
|
start = now.replace(hour=0, minute=0, second=0, microsecond=0)
|
|
end = now.replace(hour=23, minute=59, second=59, microsecond=999999)
|
|
return start, end
|
|
|
|
def _get_yesterday_range(self, now: datetime) -> tuple[datetime, datetime]:
|
|
"""Get yesterday's start and end datetime."""
|
|
yesterday = now - timedelta(days=1)
|
|
start = yesterday.replace(hour=0, minute=0, second=0, microsecond=0)
|
|
end = yesterday.replace(hour=23, minute=59, second=59, microsecond=999999)
|
|
return start, end
|
|
|
|
def _get_this_week_range(self, now: datetime) -> tuple[datetime, datetime]:
|
|
"""Get this week's start and end datetime."""
|
|
# Start of current week (Monday)
|
|
start = now - timedelta(days=now.weekday())
|
|
start = start.replace(hour=0, minute=0, second=0, microsecond=0)
|
|
end = start + timedelta(days=6, hours=23, minutes=59, seconds=59, microseconds=999999)
|
|
return start, end
|
|
|
|
def _get_last_week_range(self, now: datetime) -> tuple[datetime, datetime]:
|
|
"""Get last week's start and end datetime."""
|
|
# Start of last week (Monday)
|
|
start = now - timedelta(days=now.weekday() + 7)
|
|
start = start.replace(hour=0, minute=0, second=0, microsecond=0)
|
|
end = start + timedelta(days=6, hours=23, minutes=59, seconds=59, microseconds=999999)
|
|
return start, end
|
|
|
|
def _get_this_month_range(self, now: datetime) -> tuple[datetime, datetime]:
|
|
"""Get this month's start and end datetime."""
|
|
start = now.replace(day=1, hour=0, minute=0, second=0, microsecond=0)
|
|
end = now
|
|
return start, end
|
|
|
|
def _get_last_month_range(self, now: datetime) -> tuple[datetime, datetime]:
|
|
"""Get last month's start and end datetime."""
|
|
# First day of current month
|
|
first_of_current_month = now.replace(day=1, hour=0, minute=0, second=0, microsecond=0)
|
|
# Last day of previous month
|
|
end = first_of_current_month - timedelta(microseconds=1)
|
|
# First day of previous month
|
|
start = end.replace(day=1, hour=0, minute=0, second=0, microsecond=0)
|
|
return start, end
|
|
|
|
def _get_this_year_range(self, now: datetime) -> tuple[datetime, datetime]:
|
|
"""Get this year's start and end datetime."""
|
|
start = now.replace(month=1, day=1, hour=0, minute=0, second=0, microsecond=0)
|
|
end = now
|
|
return start, end
|
|
|
|
def _get_last_year_range(self, now: datetime) -> tuple[datetime, datetime]:
|
|
"""Get last year's start and end datetime."""
|
|
last_year = now.year - 1
|
|
start = now.replace(
|
|
year=last_year, month=1, day=1, hour=0, minute=0, second=0, microsecond=0
|
|
)
|
|
end = now.replace(
|
|
year=last_year,
|
|
month=12,
|
|
day=31,
|
|
hour=23,
|
|
minute=59,
|
|
second=59,
|
|
microsecond=999999,
|
|
)
|
|
return start, end
|
|
|
|
def _get_last_n_days_range(self, now: datetime, days: int) -> tuple[datetime, datetime]:
|
|
"""Get range for last N days."""
|
|
start = now - timedelta(days=days)
|
|
end = now
|
|
return start, end
|
|
|
|
def to_start_datetime(self, reference_time: datetime | None = None) -> datetime:
|
|
"""Get just the start datetime for this range
|
|
|
|
Args:
|
|
reference_time: Optional reference time for calculations. Defaults to now in UTC.
|
|
|
|
Returns:
|
|
Start datetime for this range
|
|
"""
|
|
start, _ = self.to_datetime_range(reference_time)
|
|
return start
|
|
|
|
def to_end_datetime(self, reference_time: datetime | None = None) -> datetime:
|
|
"""Get just the end datetime for this range
|
|
|
|
Args:
|
|
reference_time: Optional reference time for calculations. Defaults to now in UTC.
|
|
|
|
Returns:
|
|
End datetime for this range
|
|
"""
|
|
_, end = self.to_datetime_range(reference_time)
|
|
return end
|
|
|
|
def to_iso_strings(self, reference_time: datetime | None = None) -> tuple[str, str]:
|
|
"""Get start and end as ISO format strings
|
|
|
|
Args:
|
|
reference_time: Optional reference time for calculations. Defaults to now in UTC.
|
|
|
|
Returns:
|
|
Tuple of (start_iso, end_iso) strings
|
|
"""
|
|
start, end = self.to_datetime_range(reference_time)
|
|
return start.isoformat(), end.isoformat()
|
|
|
|
@classmethod
|
|
def from_string(cls, date_str: str) -> Optional["DateRange"]:
|
|
"""Create DateRange from string if it matches a known value
|
|
|
|
Args:
|
|
date_str: String representation of date range
|
|
|
|
Returns:
|
|
DateRange enum or None if no match found
|
|
"""
|
|
normalized = date_str.lower().strip()
|
|
|
|
# Direct mapping
|
|
value_map = {
|
|
"today": cls.TODAY,
|
|
"yesterday": cls.YESTERDAY,
|
|
"this week": cls.THIS_WEEK,
|
|
"last week": cls.LAST_WEEK,
|
|
"this month": cls.THIS_MONTH,
|
|
"last month": cls.LAST_MONTH,
|
|
"this year": cls.THIS_YEAR,
|
|
"last year": cls.LAST_YEAR,
|
|
"last 7 days": cls.LAST_7_DAYS,
|
|
"last 30 days": cls.LAST_30_DAYS,
|
|
}
|
|
|
|
return value_map.get(normalized)
|