Skip to content

Commit

Permalink
Add timer schedule helper objects
Browse files Browse the repository at this point in the history
  • Loading branch information
sirosen committed Oct 26, 2023
1 parent 10dc7af commit 73bbc3f
Show file tree
Hide file tree
Showing 5 changed files with 114 additions and 36 deletions.
30 changes: 23 additions & 7 deletions docs/services/timer.rst
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,29 @@ Globus Timer
Helper Objects
--------------

The :class:`TimerJob` class is used to set up request data to send to Timer for
creating a recurring job. Currently only recurring transfers are supported.
Thus, a :class:`TimerJob` should not be initialized directly; use the
:meth:`TimerJob.from_transfer_data` method to construct one to start a Timer job to run a
transfer. This will require having a :class:`TransferClient`
nstantiated first -- see the Transfer service docs
for details and examples.
The main helper users should use is the one for constructing Transfer Timers:

.. autoclass:: TransferTimer
:members:
:show-inheritance:

In order to schedule a timer, pass a ``schedule`` with relevant parameters.
This can be done using the two schedule helper classes

.. autoclass:: OnceTimerSchedule
:members:
:show-inheritance:

.. autoclass:: RecurringTimerSchedule
:members:
:show-inheritance:

TimerJob (legacy)
~~~~~~~~~~~~~~~~~

The ``TimerJob`` class is still supported for creating timer, but it is
not recommended.
New users should prefer the ``TransferTimer`` class.

.. autoclass:: TimerJob
:members:
Expand Down
6 changes: 6 additions & 0 deletions src/globus_sdk/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,8 @@ def _force_eager_imports() -> None:
"TimerClient",
"TransferTimer",
"TimerJob",
"OnceTimerSchedule",
"RecurringTimerSchedule",
},
"services.transfer": {
"ActivationRequirementsResponse",
Expand Down Expand Up @@ -211,6 +213,8 @@ def _force_eager_imports() -> None:
from .services.timer import TimerClient
from .services.timer import TransferTimer
from .services.timer import TimerJob
from .services.timer import OnceTimerSchedule
from .services.timer import RecurringTimerSchedule
from .services.transfer import ActivationRequirementsResponse
from .services.transfer import DeleteData
from .services.transfer import IterableTransferResponse
Expand Down Expand Up @@ -313,11 +317,13 @@ def __getattr__(name: str) -> t.Any:
"NullAuthorizer",
"OAuthDependentTokenResponse",
"OAuthTokenResponse",
"OnceTimerSchedule",
"OneDriveStoragePolicies",
"POSIXCollectionPolicies",
"POSIXStagingCollectionPolicies",
"POSIXStagingStoragePolicies",
"POSIXStoragePolicies",
"RecurringTimerSchedule",
"RefreshTokenAuthorizer",
"RemovedInV4Warning",
"S3StoragePolicies",
Expand Down
12 changes: 11 additions & 1 deletion src/globus_sdk/_generate_init.py
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,17 @@ def __getattr__(name: str) -> t.Any:
"SearchScrollQuery",
),
),
("services.timer", ("TimerAPIError", "TimerClient", "TransferTimer", "TimerJob")),
(
"services.timer",
(
"TimerAPIError",
"TimerClient",
"TransferTimer",
"TimerJob",
"OnceTimerSchedule",
"RecurringTimerSchedule",
),
),
(
"services.transfer",
(
Expand Down
4 changes: 3 additions & 1 deletion src/globus_sdk/services/timer/__init__.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
from .client import TimerClient
from .data import TimerJob, TransferTimer
from .data import OnceTimerSchedule, RecurringTimerSchedule, TimerJob, TransferTimer
from .errors import TimerAPIError

__all__ = (
"TimerAPIError",
"TimerClient",
"OnceTimerSchedule",
"RecurringTimerSchedule",
"TimerJob",
"TransferTimer",
)
98 changes: 71 additions & 27 deletions src/globus_sdk/services/timer/data.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
from __future__ import annotations

import datetime
# the name "datetime" is used in this module, so use an alternative name
# in order to avoid name shadowing
import datetime as dt
import logging
import typing as t

from globus_sdk.config import get_service_url
from globus_sdk.exc import warn_deprecated
from globus_sdk.services.transfer import TransferData
from globus_sdk.utils import PayloadWrapper, slash_join
from globus_sdk.utils import MISSING, MissingType, PayloadWrapper, slash_join

log = logging.getLogger(__name__)

Expand Down Expand Up @@ -38,8 +40,9 @@ class TransferTimer(PayloadWrapper):
The Schedule field encodes data which determines when the Timer will run.
Timers may be "run once" or "recurring", and "recurring" timers may specify an end
date or a number of executions after which the timer will stop.
date or a number of executions after which the timer will stop. A ``schedule`` is
specified as a dict, but the SDK provides two useful helpers for constructing these
data.
Example schedules:
.. tab-set::
Expand All @@ -48,39 +51,37 @@ class TransferTimer(PayloadWrapper):
.. code-block:: python
schedule = {"type": "once"}
schedule = OnceTimerSchedule()
.. tab-item:: Run Once, At a Specific Time
.. code-block:: python
schedule = {"type": "once", "datetime": "2023-09-22T00:00:00Z"}
schedule = OnceTimerSchedule(datetime="2023-09-22T00:00:00Z")
.. tab-item:: Run Every 5 Minutes, Until a Specific Time
.. code-block:: python
schedule = {
"type": "recurring",
"interval_seconds": 300,
"end": {"condition": "time", "datetime": "2023-10-01T00:00:00Z"},
}
schedule = RecurringTimerSchedule(
interval_seconds=300,
end={"condition": "time", "datetime": "2023-10-01T00:00:00Z"},
)
.. tab-item:: Run Every 30 Minutes, 10 Times
.. code-block:: python
schedule = {
"type": "recurring",
"interval_seconds": 1800,
"end": {"condition": "iterations", "iterations": 10},
}
schedule = RecurringTimerSchedule(
interval_seconds=1800,
end={"condition": "iterations", "iterations": 10},
)
.. tab-item:: Run Every 10 Minutes, Indefinitely
.. code-block:: python
schedule = {"type": "recurring", "interval_seconds": 600}
schedule = RecurringTimerSchedule(interval_seconds=600)
Using these schedules, you can create a timer from a ``TransferData`` object:
Expand All @@ -102,8 +103,8 @@ class TransferTimer(PayloadWrapper):
def __init__(
self,
*,
name: str | None = None,
schedule: dict[str, t.Any],
name: str | MissingType = MISSING,
schedule: dict[str, t.Any] | RecurringTimerSchedule | OnceTimerSchedule,
body: dict[str, t.Any] | TransferData,
) -> None:
super().__init__()
Expand All @@ -124,6 +125,42 @@ def _preprocess_body(
return new_body


class RecurringTimerSchedule(PayloadWrapper):
"""
A helper used as part of a *timer* to define the "schedule" for the *timer*.
A ``RecurringTimerSchedule`` is used to describe a *timer* which runs repeatedly
until some end condition is reached.
"""

def __init__(
self,
interval_seconds: int,
start: str | dt.datetime | MissingType = MISSING,
end: dict[str, t.Any] | MissingType = MISSING,
) -> None:
self["type"] = "recurring"
self["interval_seconds"] = interval_seconds
self["end"] = end
self["start"] = _format_date(start)


class OnceTimerSchedule(PayloadWrapper):
"""
A helper used as part of a *timer* to define the "schedule" for the *timer*.
A ``OnceTimerSchedule`` is used to describe a *timer* which runs exactly once.
It may be scheduled for a time in the future.
"""

def __init__(
self,
datetime: str | dt.datetime | MissingType = MISSING,
) -> None:
self["type"] = "once"
self["datetime"] = _format_date(datetime)


class TimerJob(PayloadWrapper):
r"""
.. warning::
Expand Down Expand Up @@ -167,22 +204,22 @@ def __init__(
self,
callback_url: str,
callback_body: dict[str, t.Any],
start: datetime.datetime | str,
interval: datetime.timedelta | int | None,
start: dt.datetime | str,
interval: dt.timedelta | int | None,
*,
name: str | None = None,
stop_after: datetime.datetime | None = None,
stop_after: dt.datetime | None = None,
stop_after_n: int | None = None,
scope: str | None = None,
) -> None:
super().__init__()
self["callback_url"] = callback_url
self["callback_body"] = callback_body
if isinstance(start, datetime.datetime):
if isinstance(start, dt.datetime):
self["start"] = start.isoformat()
else:
self["start"] = start
if isinstance(interval, datetime.timedelta):
if isinstance(interval, dt.timedelta):
self["interval"] = int(interval.total_seconds())
else:
self["interval"] = interval
Expand All @@ -199,11 +236,11 @@ def __init__(
def from_transfer_data(
cls,
transfer_data: TransferData | dict[str, t.Any],
start: datetime.datetime | str,
interval: datetime.timedelta | int | None,
start: dt.datetime | str,
interval: dt.timedelta | int | None,
*,
name: str | None = None,
stop_after: datetime.datetime | None = None,
stop_after: dt.datetime | None = None,
stop_after_n: int | None = None,
scope: str | None = None,
environment: str | None = None,
Expand Down Expand Up @@ -265,3 +302,10 @@ def from_transfer_data(
stop_after_n=stop_after_n,
scope=scope,
)


def _format_date(date: str | dt.datetime | MISSING) -> str | MISSING:
if isinstance(date, dt.datetime):
return date.astimezone(dt.timezone.utc).isoformat()
else:
return date

0 comments on commit 73bbc3f

Please sign in to comment.