diff --git a/src/clockify_unofficial_cli/commands/entry.py b/src/clockify_unofficial_cli/commands/entry.py index b0f3150..48e4d6d 100644 --- a/src/clockify_unofficial_cli/commands/entry.py +++ b/src/clockify_unofficial_cli/commands/entry.py @@ -1,5 +1,5 @@ import dataclasses -import datetime +import datetime as dt from dataclasses import dataclass from typing import Annotated from typing import Final @@ -49,8 +49,8 @@ def _optional_tag_ids(app_context: AppContext, terms: tuple[str, ...] | None) -> @dataclass(frozen=True, slots=True) class _ListArgs: description: str | None - start: datetime.datetime | None - end: datetime.datetime | None + start: dt.datetime | None + end: dt.datetime | None project_id: ProjectId | None task_id: TaskId | None tag_ids: list[TagId] | None @@ -66,10 +66,10 @@ class _ListOptions: typer.Option("--description", help="Substring filter on the entry description."), ] = None from_: Annotated[ - datetime.datetime | None, + dt.datetime | None, typer.Option("--from", help="Lower bound on start, e.g. 2026-09-17T09:00 or 'yesterday 09:00'."), ] = None - to: Annotated[datetime.datetime | None, typer.Option("--to", help="Upper bound on start.")] = None + to: Annotated[dt.datetime | None, typer.Option("--to", help="Upper bound on start.")] = None project: Annotated[ str | None, typer.Option("--project", "-P", help="Filter by project ID or exact name."), @@ -154,8 +154,8 @@ class _CreateArgs: task_id: TaskId | None tag_ids: list[TagId] | None billable: bool - start: datetime.datetime | None - end: datetime.datetime | None + start: dt.datetime | None + end: dt.datetime | None @dataclass(frozen=True, slots=True) @@ -175,10 +175,10 @@ class _CreateOptions: typer.Option("--billable/--no-billable", help="Mark the entry as billable."), ] = False from_: Annotated[ - datetime.datetime | None, + dt.datetime | None, typer.Option("--from", help="Start instant; e.g. 2026-09-17T09:00 or 'yesterday 09:00'."), ] = None - to: Annotated[datetime.datetime | None, typer.Option("--to", help="End instant.")] = None + to: Annotated[dt.datetime | None, typer.Option("--to", help="End instant.")] = None @APP.command(help="Create a finished time entry (use `start` for a running entry).") @@ -211,8 +211,8 @@ class _UpdateArgs: task_id: TaskId | None tag_ids: list[TagId] | None billable: bool | None - start: datetime.datetime - end: datetime.datetime | None + start: dt.datetime + end: dt.datetime | None @dataclass(frozen=True, slots=True) @@ -230,10 +230,10 @@ class _UpdateOptions: ] = None billable: Annotated[bool | None, typer.Option("--billable/--no-billable", help="Toggle billable.")] = None from_: Annotated[ - datetime.datetime | None, + dt.datetime | None, typer.Option("--from", help="New start instant; required because Clockify PUT replaces the entry."), ] = None - to: Annotated[datetime.datetime | None, typer.Option("--to", help="New end instant.")] = None + to: Annotated[dt.datetime | None, typer.Option("--to", help="New end instant.")] = None @APP.command(help="Update an existing time entry (start is required by the SDK).") diff --git a/src/clockify_unofficial_cli/commands/time_off_request.py b/src/clockify_unofficial_cli/commands/time_off_request.py index 40e00d1..8db7483 100644 --- a/src/clockify_unofficial_cli/commands/time_off_request.py +++ b/src/clockify_unofficial_cli/commands/time_off_request.py @@ -1,4 +1,4 @@ -import datetime +import datetime as dt import functools from dataclasses import dataclass from typing import Annotated @@ -91,9 +91,7 @@ class _CreateOptions: user: Annotated[str | None, typer.Option("--user", help="Request on behalf of this user.")] = None -def _period( - start: datetime.date, end: datetime.date | None, half_day: TimeOffHalfDayPeriod | None -) -> TimeOffRequestPeriodRequest: +def _period(start: dt.date, end: dt.date | None, half_day: TimeOffHalfDayPeriod | None) -> TimeOffRequestPeriodRequest: period = TimeOffPeriodRequest(start=start, end=end or start) if half_day is None: return TimeOffRequestPeriodRequest(period=period) diff --git a/src/clockify_unofficial_cli/services/expenses.py b/src/clockify_unofficial_cli/services/expenses.py index facea06..a4494db 100644 --- a/src/clockify_unofficial_cli/services/expenses.py +++ b/src/clockify_unofficial_cli/services/expenses.py @@ -1,6 +1,6 @@ from __future__ import annotations -import datetime +import datetime as dt import mimetypes from dataclasses import dataclass from typing import TYPE_CHECKING @@ -91,7 +91,7 @@ def build_update(current: Expense, changes: ExpenseChanges) -> ExpenseUpdate: return ExpenseUpdate( user_id=current.user_id, # type: ignore[arg-type] category_id=changes.category_id or current.category_id, # type: ignore[arg-type] - date=parse_instant(when) if when else datetime.datetime.now(datetime.UTC), + date=parse_instant(when) if when else dt.datetime.now(dt.UTC), amount=changes.amount if changes.amount is not None else current.total or 0.0, change_fields=changed, **{key: value for key, value in optional.items() if value is not None}, # type: ignore[arg-type] diff --git a/src/clockify_unofficial_cli/services/invoices.py b/src/clockify_unofficial_cli/services/invoices.py index c0b3f18..e9e05e9 100644 --- a/src/clockify_unofficial_cli/services/invoices.py +++ b/src/clockify_unofficial_cli/services/invoices.py @@ -17,7 +17,7 @@ from clockify_unofficial_cli.runtime.exit_codes import ExitCode if TYPE_CHECKING: - import datetime + import datetime as dt from collections.abc import Sequence from clockify import InvoiceDetails @@ -41,8 +41,8 @@ def _required[T](value: T | None, option: str) -> T: class InvoiceChanges: number: str | None = None currency: str | None = None - issued_date: datetime.datetime | None = None - due_date: datetime.datetime | None = None + issued_date: dt.datetime | None = None + due_date: dt.datetime | None = None discount: float | None = None tax: float | None = None tax2: float | None = None diff --git a/src/clockify_unofficial_cli/services/parsing.py b/src/clockify_unofficial_cli/services/parsing.py index 2971ade..3f7b54f 100644 --- a/src/clockify_unofficial_cli/services/parsing.py +++ b/src/clockify_unofficial_cli/services/parsing.py @@ -1,6 +1,6 @@ from __future__ import annotations -import datetime +import datetime as dt import re from typing import Final @@ -19,7 +19,7 @@ _DAY_KEYWORDS: Final[frozenset[str]] = frozenset({"today", "tomorrow", "yesterday"}) -def parse_duration(value: str) -> datetime.timedelta: +def parse_duration(value: str) -> dt.timedelta: raw = value.strip() if not raw: message = f"Invalid duration '{value}'." @@ -32,28 +32,28 @@ def parse_duration(value: str) -> datetime.timedelta: raise CliError(message, exit_code=ExitCode.USAGE) -def _parse_compact_duration(value: str) -> datetime.timedelta: +def _parse_compact_duration(value: str) -> dt.timedelta: match = _DURATION_RE.match(value) if match is None: message = f"Invalid duration '{value}'." raise CliError(message, exit_code=ExitCode.USAGE) - return datetime.timedelta( + return dt.timedelta( hours=int(match.group(1) or 0), minutes=int(match.group(2) or 0), seconds=int(match.group(3) or 0), ) -def _parse_clock_duration(value: str) -> datetime.timedelta: +def _parse_clock_duration(value: str) -> dt.timedelta: match = _CLOCK_RE.match(value) if match is None: message = f"Invalid duration '{value}'." raise CliError(message, exit_code=ExitCode.USAGE) hours, minutes, seconds = match.groups() - return datetime.timedelta(hours=int(hours), minutes=int(minutes), seconds=int(seconds or 0)) + return dt.timedelta(hours=int(hours), minutes=int(minutes), seconds=int(seconds or 0)) -def parse_instant(value: str) -> datetime.datetime: +def parse_instant(value: str) -> dt.datetime: raw = value.strip() if not raw: message = f"Invalid instant '{value}'." @@ -69,22 +69,22 @@ def parse_instant(value: str) -> datetime.datetime: return _resolve_absolute(raw) -def _today_at(time_str: str) -> datetime.datetime: - today = datetime.datetime.now(datetime.UTC).date() +def _today_at(time_str: str) -> dt.datetime: + today = dt.datetime.now(dt.UTC).date() hour, minute = _parse_clock(time_str) - return datetime.datetime(today.year, today.month, today.day, hour, minute, tzinfo=datetime.UTC) + return dt.datetime(today.year, today.month, today.day, hour, minute, tzinfo=dt.UTC) -def _day_keyword_at(keyword: str, time_str: str) -> datetime.datetime: - today = datetime.datetime.now(datetime.UTC).date() +def _day_keyword_at(keyword: str, time_str: str) -> dt.datetime: + today = dt.datetime.now(dt.UTC).date() if keyword == "today": target = today elif keyword == "tomorrow": - target = today + datetime.timedelta(days=1) + target = today + dt.timedelta(days=1) else: - target = today - datetime.timedelta(days=1) + target = today - dt.timedelta(days=1) hour, minute = _parse_clock(time_str) - return datetime.datetime(target.year, target.month, target.day, hour, minute, tzinfo=datetime.UTC) + return dt.datetime(target.year, target.month, target.day, hour, minute, tzinfo=dt.UTC) def _parse_clock(value: str) -> tuple[int, int]: @@ -96,26 +96,26 @@ def _parse_clock(value: str) -> tuple[int, int]: return int(hours), int(minutes) -def _resolve_relative(value: str) -> datetime.datetime: +def _resolve_relative(value: str) -> dt.datetime: sign, amount, unit = _RELATIVE_RE.match(value).groups() # type: ignore[union-attr] - delta = datetime.timedelta(hours=int(amount)) if unit == "h" else datetime.timedelta(minutes=int(amount)) + delta = dt.timedelta(hours=int(amount)) if unit == "h" else dt.timedelta(minutes=int(amount)) if sign == "-": delta = -delta - return datetime.datetime.now(datetime.UTC) + delta + return dt.datetime.now(dt.UTC) + delta -def _resolve_absolute(value: str) -> datetime.datetime: +def _resolve_absolute(value: str) -> dt.datetime: try: - parsed = datetime.datetime.fromisoformat(value) + parsed = dt.datetime.fromisoformat(value) except ValueError as exc: message = f"Invalid instant '{value}'." raise CliError(message, exit_code=ExitCode.USAGE) from exc if parsed.tzinfo is None: - return parsed.replace(tzinfo=datetime.UTC) - return parsed.astimezone(datetime.UTC) + return parsed.replace(tzinfo=dt.UTC) + return parsed.astimezone(dt.UTC) -def parse_date(value: str) -> datetime.date: +def parse_date(value: str) -> dt.date: return parse_instant(value).date() diff --git a/src/clockify_unofficial_cli/services/reports.py b/src/clockify_unofficial_cli/services/reports.py index f62d500..6a9564c 100644 --- a/src/clockify_unofficial_cli/services/reports.py +++ b/src/clockify_unofficial_cli/services/reports.py @@ -1,6 +1,6 @@ from __future__ import annotations -import datetime +import datetime as dt from dataclasses import dataclass from enum import StrEnum from typing import TYPE_CHECKING @@ -42,8 +42,8 @@ class Period(StrEnum): @dataclass(frozen=True, slots=True) class DateRange: - start: datetime.datetime - end: datetime.datetime + start: dt.datetime + end: dt.datetime @dataclass(frozen=True, slots=True) @@ -83,40 +83,40 @@ def fields(self) -> dict[str, Any]: } -_ONE_SECOND: Final = datetime.timedelta(seconds=1) +_ONE_SECOND: Final = dt.timedelta(seconds=1) -def _start_of_day(moment: datetime.datetime) -> datetime.datetime: +def _start_of_day(moment: dt.datetime) -> dt.datetime: return moment.replace(hour=0, minute=0, second=0, microsecond=0) -def _start_of_month(moment: datetime.datetime) -> datetime.datetime: +def _start_of_month(moment: dt.datetime) -> dt.datetime: return _start_of_day(moment).replace(day=1) -def _next_month(first_of_month: datetime.datetime) -> datetime.datetime: - return (first_of_month + datetime.timedelta(days=32)).replace(day=1) +def _next_month(first_of_month: dt.datetime) -> dt.datetime: + return (first_of_month + dt.timedelta(days=32)).replace(day=1) # The upper bound is the last second of the period, because Clockify treats dateRangeEnd as inclusive. -def _preset(period: Period, now: datetime.datetime) -> DateRange: +def _preset(period: Period, now: dt.datetime) -> DateRange: today = _start_of_day(now) - monday = today - datetime.timedelta(days=today.weekday()) + monday = today - dt.timedelta(days=today.weekday()) month = _start_of_month(now) - bounds: dict[Period, tuple[datetime.datetime, datetime.datetime]] = { - Period.TODAY: (today, today + datetime.timedelta(days=1)), - Period.YESTERDAY: (today - datetime.timedelta(days=1), today), - Period.THIS_WEEK: (monday, monday + datetime.timedelta(days=7)), - Period.LAST_WEEK: (monday - datetime.timedelta(days=7), monday), + bounds: dict[Period, tuple[dt.datetime, dt.datetime]] = { + Period.TODAY: (today, today + dt.timedelta(days=1)), + Period.YESTERDAY: (today - dt.timedelta(days=1), today), + Period.THIS_WEEK: (monday, monday + dt.timedelta(days=7)), + Period.LAST_WEEK: (monday - dt.timedelta(days=7), monday), Period.THIS_MONTH: (month, _next_month(month)), - Period.LAST_MONTH: (_start_of_month(month - datetime.timedelta(days=1)), month), + Period.LAST_MONTH: (_start_of_month(month - dt.timedelta(days=1)), month), } start, end = bounds[period] - return DateRange(start=start.astimezone(datetime.UTC), end=(end - _ONE_SECOND).astimezone(datetime.UTC)) + return DateRange(start=start.astimezone(dt.UTC), end=(end - _ONE_SECOND).astimezone(dt.UTC)) -def resolve_range(selection: ReportSelection, *, now: datetime.datetime | None = None) -> DateRange: - local_now = now or datetime.datetime.now().astimezone() +def resolve_range(selection: ReportSelection, *, now: dt.datetime | None = None) -> DateRange: + local_now = now or dt.datetime.now().astimezone() explicit = selection.start is not None or selection.end is not None if selection.period is not None and explicit: message = "--period cannot be combined with --from/--to." @@ -127,7 +127,7 @@ def resolve_range(selection: ReportSelection, *, now: datetime.datetime | None = message = "--to needs --from." raise CliError(message, exit_code=ExitCode.USAGE) start = parse_instant(selection.start) - end = parse_instant(selection.end) if selection.end is not None else local_now.astimezone(datetime.UTC) + end = parse_instant(selection.end) if selection.end is not None else local_now.astimezone(dt.UTC) if end <= start: message = "--to must be after --from." raise CliError(message, exit_code=ExitCode.USAGE) @@ -143,9 +143,7 @@ def _entity_filter( return ReportEntityFilter(ids=ids, contains=ReportFilterContains.CONTAINS) -def build_query( - app_context: AppContext, selection: ReportSelection, *, now: datetime.datetime | None = None -) -> ReportQuery: +def build_query(app_context: AppContext, selection: ReportSelection, *, now: dt.datetime | None = None) -> ReportQuery: date_range = resolve_range(selection, now=now) candidates = { "projects": _entity_filter(app_context, selection.projects, resolve_project), diff --git a/src/clockify_unofficial_cli/services/timer.py b/src/clockify_unofficial_cli/services/timer.py index 7b732b3..844e7df 100644 --- a/src/clockify_unofficial_cli/services/timer.py +++ b/src/clockify_unofficial_cli/services/timer.py @@ -1,4 +1,4 @@ -import datetime +import datetime as dt from dataclasses import dataclass from typing import TYPE_CHECKING @@ -27,13 +27,13 @@ class TimerRequest: task_id: TaskId | None tag_ids: tuple[TagId, ...] billable: bool - start: datetime.datetime | None - end: datetime.datetime | None - duration: datetime.timedelta | None + start: dt.datetime | None + end: dt.datetime | None + duration: dt.timedelta | None -def _now() -> datetime.datetime: - return datetime.datetime.now(datetime.UTC) +def _now() -> dt.datetime: + return dt.datetime.now(dt.UTC) def start_timer(app_context: AppContext, request: TimerRequest) -> object: @@ -50,7 +50,7 @@ def start_timer(app_context: AppContext, request: TimerRequest) -> object: return app_context.workspace().time_entries.start(app_context.user_id(), payload) -def stop_timer(app_context: AppContext, *, end: datetime.datetime | None) -> object: +def stop_timer(app_context: AppContext, *, end: dt.datetime | None) -> object: return app_context.workspace().time_entries.stop(app_context.user_id(), end=end) @@ -71,9 +71,9 @@ class LogRequest: task_id: TaskId | None tag_ids: tuple[TagId, ...] billable: bool - start: datetime.datetime | None - end: datetime.datetime | None - duration: datetime.timedelta | None + start: dt.datetime | None + end: dt.datetime | None + duration: dt.timedelta | None def log_entry( @@ -103,13 +103,13 @@ def log_entry( return app_context.workspace().time_entries.create(payload) -def parse_optional_duration(value: str | None) -> datetime.timedelta | None: +def parse_optional_duration(value: str | None) -> dt.timedelta | None: if value is None: return None return parse_duration(value) -def parse_optional_instant(value: str | None) -> datetime.datetime | None: +def parse_optional_instant(value: str | None) -> dt.datetime | None: if value is None: return None return parse_instant(value) diff --git a/tests/unit/services/test_parsing.py b/tests/unit/services/test_parsing.py index c66cb3f..1144271 100644 --- a/tests/unit/services/test_parsing.py +++ b/tests/unit/services/test_parsing.py @@ -1,6 +1,6 @@ from __future__ import annotations -import datetime +import datetime as dt import pytest @@ -17,7 +17,7 @@ def test_parse_duration_with_hours_and_minutes() -> None: # Act result = parse_duration(value) # Assert - assert result == datetime.timedelta(hours=1, minutes=30) + assert result == dt.timedelta(hours=1, minutes=30) def test_parse_duration_with_only_minutes() -> None: @@ -26,7 +26,7 @@ def test_parse_duration_with_only_minutes() -> None: # Act result = parse_duration(value) # Assert - assert result == datetime.timedelta(minutes=90) + assert result == dt.timedelta(minutes=90) def test_parse_duration_with_clock_format() -> None: @@ -35,7 +35,7 @@ def test_parse_duration_with_clock_format() -> None: # Act result = parse_duration(value) # Assert - assert result == datetime.timedelta(hours=1, minutes=30) + assert result == dt.timedelta(hours=1, minutes=30) def test_parse_duration_with_clock_format_with_seconds() -> None: @@ -44,7 +44,7 @@ def test_parse_duration_with_clock_format_with_seconds() -> None: # Act result = parse_duration(value) # Assert - assert result == datetime.timedelta(hours=1, minutes=30, seconds=45) + assert result == dt.timedelta(hours=1, minutes=30, seconds=45) def test_parse_duration_rejects_invalid_input() -> None: @@ -63,7 +63,7 @@ def test_parse_instant_with_iso_format() -> None: # Act result = parse_instant(value) # Assert - assert result == datetime.datetime(2026, 9, 17, 9, 0, tzinfo=datetime.UTC) + assert result == dt.datetime(2026, 9, 17, 9, 0, tzinfo=dt.UTC) def test_parse_instant_with_clock_only() -> None: @@ -72,7 +72,7 @@ def test_parse_instant_with_clock_only() -> None: # Act result = parse_instant(value) # Assert - assert result.tzinfo == datetime.UTC + assert result.tzinfo == dt.UTC assert result.hour == 9 assert result.minute == 0 @@ -81,12 +81,12 @@ def test_parse_instant_with_relative_offset() -> None: # Arrange value = "-2h" # Act - before = datetime.datetime.now(datetime.UTC) + before = dt.datetime.now(dt.UTC) result = parse_instant(value) - after = datetime.datetime.now(datetime.UTC) + after = dt.datetime.now(dt.UTC) # Assert - expected_low = before - datetime.timedelta(hours=2, seconds=1) - expected_high = after - datetime.timedelta(hours=2) + datetime.timedelta(seconds=1) + expected_low = before - dt.timedelta(hours=2, seconds=1) + expected_high = after - dt.timedelta(hours=2) + dt.timedelta(seconds=1) assert expected_low <= result <= expected_high @@ -96,8 +96,8 @@ def test_parse_instant_with_yesterday_keyword() -> None: # Act result = parse_instant(value) # Assert - today = datetime.datetime.now(datetime.UTC).date() - assert result.date() == today - datetime.timedelta(days=1) + today = dt.datetime.now(dt.UTC).date() + assert result.date() == today - dt.timedelta(days=1) assert result.hour == 9 assert result.minute == 0 @@ -108,7 +108,7 @@ def test_parse_instant_with_today_keyword() -> None: # Act result = parse_instant(value) # Assert - today = datetime.datetime.now(datetime.UTC).date() + today = dt.datetime.now(dt.UTC).date() assert result.date() == today @@ -128,4 +128,4 @@ def test_parse_date_keeps_the_calendar_day() -> None: # Act result = parse_date(value) # Assert - assert result == datetime.date(2026, 9, 14) + assert result == dt.date(2026, 9, 14) diff --git a/tests/unit/services/test_reports.py b/tests/unit/services/test_reports.py index de6073c..41c3acc 100644 --- a/tests/unit/services/test_reports.py +++ b/tests/unit/services/test_reports.py @@ -1,6 +1,6 @@ from __future__ import annotations -import datetime +import datetime as dt from typing import Final import pytest @@ -17,11 +17,11 @@ from clockify_unofficial_cli.services.reports import resolve_range # A Wednesday, so "this-week" starts two days earlier and "last-week" a week before that. -NOW: Final = datetime.datetime(2026, 9, 16, 15, 30, tzinfo=datetime.UTC) +NOW: Final = dt.datetime(2026, 9, 16, 15, 30, tzinfo=dt.UTC) -def _utc(*parts: int) -> datetime.datetime: - return datetime.datetime(*parts, tzinfo=datetime.UTC) +def _utc(*parts: int) -> dt.datetime: + return dt.datetime(*parts, tzinfo=dt.UTC) @pytest.mark.parametrize( @@ -55,7 +55,7 @@ def test_resolve_range_defaults_to_this_week() -> None: def test_resolve_range_last_month_wraps_the_year() -> None: # Arrange - january = datetime.datetime(2027, 1, 10, tzinfo=datetime.UTC) + january = dt.datetime(2027, 1, 10, tzinfo=dt.UTC) # Act result = resolve_range(ReportSelection(period=Period.LAST_MONTH), now=january) # Assert diff --git a/tests/unit/services/test_timer.py b/tests/unit/services/test_timer.py index e99d2ac..ac97028 100644 --- a/tests/unit/services/test_timer.py +++ b/tests/unit/services/test_timer.py @@ -1,6 +1,6 @@ from __future__ import annotations -import datetime +import datetime as dt from unittest.mock import MagicMock import pytest @@ -31,7 +31,7 @@ def start(self, user_id: object, payload: object) -> object: self.sent.append((user_id, {"start": payload})) return MagicMock() - def stop(self, user_id: object, end: datetime.datetime | None = None) -> object: + def stop(self, user_id: object, end: dt.datetime | None = None) -> object: self.sent.append((user_id, {"stop": end})) return MagicMock() @@ -56,7 +56,7 @@ def _app_context(entries: list[object]) -> AppContext: def test_start_timer_sends_payload() -> None: # Arrange context = _app_context([]) - start = datetime.datetime(2026, 9, 17, 9, 0, tzinfo=datetime.UTC) + start = dt.datetime(2026, 9, 17, 9, 0, tzinfo=dt.UTC) request = TimerRequest( description="spike", project_id=None, @@ -110,13 +110,13 @@ def test_start_timer_defaults_start_to_now() -> None: expected_payload = sent[0][1]["start"] assert sent == [("user-1", {"start": expected_payload})] assert expected_payload.description == "now" - assert expected_payload.start.tzinfo == datetime.UTC + assert expected_payload.start.tzinfo == dt.UTC def test_stop_timer_passes_end() -> None: # Arrange context = _app_context([]) - end = datetime.datetime(2026, 9, 17, 10, 0, tzinfo=datetime.UTC) + end = dt.datetime(2026, 9, 17, 10, 0, tzinfo=dt.UTC) # Act stop_timer(context, end=end) # Assert @@ -148,8 +148,8 @@ def test_running_entry_returns_none_when_empty() -> None: def test_log_entry_with_start_and_end() -> None: # Arrange context = _app_context([]) - start = datetime.datetime(2026, 9, 17, 9, 0, tzinfo=datetime.UTC) - end = datetime.datetime(2026, 9, 17, 10, 0, tzinfo=datetime.UTC) + start = dt.datetime(2026, 9, 17, 9, 0, tzinfo=dt.UTC) + end = dt.datetime(2026, 9, 17, 10, 0, tzinfo=dt.UTC) request = LogRequest( description="log", project_id=None, diff --git a/uv.lock b/uv.lock index bd326f8..1f7bacd 100644 --- a/uv.lock +++ b/uv.lock @@ -869,27 +869,27 @@ wheels = [ [[package]] name = "ruff" -version = "0.16.10" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c4/49/23802c45f093eb14bde54b141d2b2f058edfa63a06db7beed047308cc08f/ruff-0.16.10.tar.gz", hash = "sha256:eff4728c4eaae93f0955cd264d24b2ab348e74bf59986ccf282ba6dc16b3b017", size = 4958724, upload-time = "2026-10-01T18:03:21.697Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2f/21/ebce22e1d90cdb2cd691026b9c6e9bec6499f0b396089481755b5d49efff/ruff-0.16.10-py3-none-linux_armv6l.whl", hash = "sha256:488b0fe3f3574210e5cf80d9f59b9e3ab17a127a8155de3f307b392589cfb511", size = 10095557, upload-time = "2026-10-01T18:02:36.072Z" }, - { url = "https://files.pythonhosted.org/packages/cb/98/a54de85876a8b2612bfa0d84c7b9abfb39c6a3354aee7800b09c1649b9e3/ruff-0.16.10-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:e748ff95c934c4e978783b8e687bc174e7bd84e8ad24e3243e1ecfcda5e0282d", size = 10340577, upload-time = "2026-10-01T18:02:39.258Z" }, - { url = "https://files.pythonhosted.org/packages/9f/16/1a5a4a2657effe4806110f2b907313802f1367fcdbb29e8122766e407fab/ruff-0.16.10-py3-none-macosx_11_0_arm64.whl", hash = "sha256:3031a4a2e8e7b8a46f70be45f198c35a11ece509a94b80334d8d397a33c67550", size = 9774282, upload-time = "2026-10-01T18:02:41.79Z" }, - { url = "https://files.pythonhosted.org/packages/6e/fb/470085af734da396e68cd80fb0a3e7c109a459716ae59588c0f6fab8a17d/ruff-0.16.10-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:494401c86df4c4c25f69b9419605d944467ee98c42fb6ad405ef4fa40b8fb67d", size = 9920895, upload-time = "2026-10-01T18:02:44.458Z" }, - { url = "https://files.pythonhosted.org/packages/57/de/f10cffe4f88a37ea6615ec460f01bf76f0bc1477737a35d9ee61a630a78b/ruff-0.16.10-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d203abc0ff2b773ee33d00ab8df0bb67046fbc7c07b119332f08b9b345cf8221", size = 9902707, upload-time = "2026-10-01T18:02:47.041Z" }, - { url = "https://files.pythonhosted.org/packages/ff/44/3fdcedf83ae60ef837dd239170e480dce606a9142cfa2db911afdf855fd9/ruff-0.16.10-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bd83d1235a5258d318477bc5b576303974cbdef5df0c01a1bff14efcc23bd12a", size = 10619287, upload-time = "2026-10-01T18:02:49.485Z" }, - { url = "https://files.pythonhosted.org/packages/c1/62/02e76a5574002153618eafbb70e159468addc72a5d2c488e65cbb4639d2d/ruff-0.16.10-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bc2610fb269fa56dd8a68669ae470fa6272902668c0fc2ebc3aa112b2633d5b8", size = 11339942, upload-time = "2026-10-01T18:02:52.008Z" }, - { url = "https://files.pythonhosted.org/packages/6b/c4/cde27d47ad8d4126c587e608c67c46e7feac11763f606d261a1bc8a489e6/ruff-0.16.10-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3e70175e29cc94c26ea296c80e470180b744b7419026898e58f520c6ab32578e", size = 10934316, upload-time = "2026-10-01T18:02:54.811Z" }, - { url = "https://files.pythonhosted.org/packages/e4/03/17234145f302a645a123e8c3bb2411ecf4669fbc350de3b0d803230a1729/ruff-0.16.10-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f33f43a864a8483eebd160e713336c8bab02c934feaff0a33cf5ccb41546d09a", size = 10387968, upload-time = "2026-10-01T18:02:57.497Z" }, - { url = "https://files.pythonhosted.org/packages/71/29/2493af60240ee7644b38a4b821f5f9c3b5a4fa3178770fe1d0c685217395/ruff-0.16.10-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:1dfc6f0088149fb6a362c1c446bcbb3fd2157b3852fe2fa68409276eab9ad9b3", size = 10537906, upload-time = "2026-10-01T18:03:00.006Z" }, - { url = "https://files.pythonhosted.org/packages/2e/9a/f56b28f3b143bb9e518c34fb89ab191b626bca8b70dbf8af0d2e2d473572/ruff-0.16.10-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:6553498afc35f580f036030795810b9e6bcea31604b0fd9e8d352795473042e3", size = 10012938, upload-time = "2026-10-01T18:03:03.006Z" }, - { url = "https://files.pythonhosted.org/packages/c2/c4/fca37362848ea4d4d80712df13632e645e7c7cfb1bdedf140699ac7a090b/ruff-0.16.10-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:a3b8471dea115d37f123882be852bed13403746d3a76c11de4a19ec5f9ff5a03", size = 9897945, upload-time = "2026-10-01T18:03:05.818Z" }, - { url = "https://files.pythonhosted.org/packages/7d/c7/e0bc57664d6e0c61fd22f620af260fe9662d7e1ddb320ea7f160e76ef165/ruff-0.16.10-py3-none-musllinux_1_2_i686.whl", hash = "sha256:92e59a70bcbd9d3a5483656da906ec28edfdacfce00afd99edb8b4e9d15644be", size = 10333134, upload-time = "2026-10-01T18:03:08.25Z" }, - { url = "https://files.pythonhosted.org/packages/21/aa/5c9f3b68737c0e4a33a1db5dfab44f7b786d96ca91a7233112ddab1e6dc2/ruff-0.16.10-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:7ae7375f803b5520dc9f546bed7e3a0acb70b91812e9bb4b19927de22f25b77d", size = 10741702, upload-time = "2026-10-01T18:03:10.775Z" }, - { url = "https://files.pythonhosted.org/packages/78/fa/0f9c2020dc316c53d983be011720b8157cc052b97e3f4dfa7db6d0f880a6/ruff-0.16.10-py3-none-win32.whl", hash = "sha256:2a12e01cb9156c10c466f63b46eaae5ecea28dfbd21b5836353ae498e7d1349a", size = 10139176, upload-time = "2026-10-01T18:03:13.267Z" }, - { url = "https://files.pythonhosted.org/packages/99/29/cfb0df9448d4d4ad48c2de029ada9ebd71baa6da983a6c77ee6c6cd0fe82/ruff-0.16.10-py3-none-win_amd64.whl", hash = "sha256:97f2015c92aa97105b0eab19eb5d224884399281cfc5da86a92db4ab5e7fb2ca", size = 10584734, upload-time = "2026-10-01T18:03:16.006Z" }, - { url = "https://files.pythonhosted.org/packages/fc/05/c16957eb287c3fc062e032619a25868d93d408a844b725bcf514f7a378ff/ruff-0.16.10-py3-none-win_arm64.whl", hash = "sha256:25a65fe998c4e6861ec079ada5826a2fc605e6cbccbe9dcd7fac1f54e791621b", size = 10366440, upload-time = "2026-10-01T18:03:19.04Z" }, +version = "0.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e9/a7/70debb024dfacda67b8e560cc7511f52b34b9348a8cc8c1ec23036dcd51d/ruff-0.17.0.tar.gz", hash = "sha256:5cd03240d8208a557c2a9655a5cb07ebe36aa6bb35065f97d48c1f6adef5a322", size = 5320554, upload-time = "2026-10-09T19:47:29.248Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bc/f8/ee5ab9da6089eae2a33e6008b01deb1eda19992c1c8e10661e98cee1640f/ruff-0.17.0-py3-none-linux_armv6l.whl", hash = "sha256:0e271826af9a20d18c6cfae8c51e82959167c24859686ddd3eb9a7f0842ce81e", size = 10148907, upload-time = "2026-10-09T19:46:38.695Z" }, + { url = "https://files.pythonhosted.org/packages/9f/d9/2f81fb5a9d580afbb11b1c8ff915233a11f2a1b27405d7991f183c5e1976/ruff-0.17.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:5f0ca4a40f81403689c04f12966e22f44e329ae362072d8f1587b7bda87f603b", size = 10224208, upload-time = "2026-10-09T19:46:41.711Z" }, + { url = "https://files.pythonhosted.org/packages/a7/20/643f3c8f75594f937b2bf74801241c56a2e2b8e139d24dff8b66b28cdd7f/ruff-0.17.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:cbf7149e0927dc3295d5d64679a4765576eef71b00782b2ae969ef82274d6bb9", size = 9273324, upload-time = "2026-10-09T19:46:44.323Z" }, + { url = "https://files.pythonhosted.org/packages/ec/91/627700b233d367736cb274f1bd0b47d1f2b12f68878192812bd875adadc3/ruff-0.17.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:13ee90156522998c3037059d8f66885c8adeeaf7643bdce2caceee196ecd23e0", size = 9953190, upload-time = "2026-10-09T19:46:47.155Z" }, + { url = "https://files.pythonhosted.org/packages/cd/92/91f7b5ed39490f89d6cbf56e1f543c383667a725efa8e2c2dee0f01f5591/ruff-0.17.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3d8e4a002a94cd9d0dc48b51dc69d807a172b5b9bf2b668e656424dc5b55ead1", size = 9961746, upload-time = "2026-10-09T19:46:50.098Z" }, + { url = "https://files.pythonhosted.org/packages/87/c5/7310f9fc63ce11ff6394edbd5e85433dfb0e14c9fbf6ccc97f1538491bc7/ruff-0.17.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c0b8a60c06a218c337e1161638d34757f83449243e2db161483ddf948e53ad14", size = 10625483, upload-time = "2026-10-09T19:46:53.379Z" }, + { url = "https://files.pythonhosted.org/packages/a5/8d/97443f0dca4a03a0bc7629fd396fd494a1cb6666121e38c5075acb217d8f/ruff-0.17.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a330178bdffc4205dbf3bda11d93e059e388fd6546f8cdd304501a9160363c0d", size = 11350129, upload-time = "2026-10-09T19:46:56.486Z" }, + { url = "https://files.pythonhosted.org/packages/9c/0a/c525efd9777be4b6b012e6969a3012648468e7e6c4b3e5b46af69f46e8eb/ruff-0.17.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7bb08489e234876fa2da67ae3ea938e9a2156da80293e0e4365abd6973d98329", size = 10906223, upload-time = "2026-10-09T19:47:00.263Z" }, + { url = "https://files.pythonhosted.org/packages/2c/3c/4a01195d93420cad1175bedad13a515dc8a56f95a6e39789b92e582682f5/ruff-0.17.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc73e7c133e82d55b5f15897b2a442d72c0cb4a0c886c46801ce3c247150b60c", size = 10386314, upload-time = "2026-10-09T19:47:03.057Z" }, + { url = "https://files.pythonhosted.org/packages/c7/72/1a3951665485a921f6375f91e754a1854d5a645d41acc3642668064ff64d/ruff-0.17.0-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:db4f74c533403ab70fe4007873f6ae0c9f94a8b03158cf48d78788e47cdbe399", size = 10565464, upload-time = "2026-10-09T19:47:05.831Z" }, + { url = "https://files.pythonhosted.org/packages/90/8c/539b4d8c082f57e18db8ae2be85a460d77861c79dcd5798e32b536a6a06f/ruff-0.17.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:3d8cc360e666d1914e47b0777c6906d70cf18891a55532bd0a16844195d70859", size = 10079468, upload-time = "2026-10-09T19:47:08.617Z" }, + { url = "https://files.pythonhosted.org/packages/e0/b8/84286966db79434e8c26b585b0a0f6897cb3ab1c51a4aa4df10c28488b62/ruff-0.17.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:d66de796b726c4801e05fa99a2a8d7a780e107be222486c304ab61765561e866", size = 9958556, upload-time = "2026-10-09T19:47:11.324Z" }, + { url = "https://files.pythonhosted.org/packages/69/50/27b6eed27b83fcdd5bfa0d52b83231e29094754374698da404d094487ae3/ruff-0.17.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:c3f268baf004aea944f040623327119527ea231af15f7fb7890e82cea0679589", size = 10352173, upload-time = "2026-10-09T19:47:14.188Z" }, + { url = "https://files.pythonhosted.org/packages/2a/fa/955399fd13044cd827862044117d784a59e3196f6cce7424908ac9a7f914/ruff-0.17.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:864b6c1acb6b0bccf94b5a3938a1531fd09aaca5e5659a2e7bf0f3cf2a685540", size = 10791761, upload-time = "2026-10-09T19:47:16.931Z" }, + { url = "https://files.pythonhosted.org/packages/ae/bf/024e01e1f6aec87768696725e648ed5b438941341eea8f8100beb681961f/ruff-0.17.0-py3-none-win32.whl", hash = "sha256:5e50aa5b84decd9fe5b0bb0e6f71c3b592f1767ed09faa4b7207d933961e35cd", size = 9904112, upload-time = "2026-10-09T19:47:19.75Z" }, + { url = "https://files.pythonhosted.org/packages/cc/77/1ee73df41dcc8d1cdb686ee4bc46ea29704ea175feb6b95c78420f631ab8/ruff-0.17.0-py3-none-win_amd64.whl", hash = "sha256:8ab76bcda86dfd28e13776cb5de3c7bcdcf1ae3d37ed761113d1a5a415dc134c", size = 10115585, upload-time = "2026-10-09T19:47:22.698Z" }, + { url = "https://files.pythonhosted.org/packages/fd/71/eb4f0ccc844aece56963e8578df9c95d4c00f580547d52329d4035d3af18/ruff-0.17.0-py3-none-win_arm64.whl", hash = "sha256:c154c73ff43f9854395e24cac507af13078962e53d2b511605058d22af1fdb88", size = 9872035, upload-time = "2026-10-09T19:47:26.306Z" }, ] [[package]]