diff --git a/.github/workflows/publish_to_pypi.yaml b/.github/workflows/publish_to_pypi.yaml index 3d92a0663..e96e53a3c 100644 --- a/.github/workflows/publish_to_pypi.yaml +++ b/.github/workflows/publish_to_pypi.yaml @@ -13,7 +13,7 @@ jobs: steps: - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 + - uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 with: python-version: "3.11" @@ -21,4 +21,4 @@ jobs: - run: python -m build . - name: Publish to PyPI - uses: pypa/gh-action-pypi-publish@76f52bc884231f62b9a034ebfe128415bbaabdfc # v1.12.4 + uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0 diff --git a/.github/workflows/publish_to_test_pypi.yaml b/.github/workflows/publish_to_test_pypi.yaml index cc627b5ae..f4664f660 100644 --- a/.github/workflows/publish_to_test_pypi.yaml +++ b/.github/workflows/publish_to_test_pypi.yaml @@ -13,7 +13,7 @@ jobs: steps: - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 + - uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 with: python-version: "3.11" @@ -21,6 +21,6 @@ jobs: - run: python -m build . - name: Publish to TestPyPI - uses: pypa/gh-action-pypi-publish@76f52bc884231f62b9a034ebfe128415bbaabdfc # v1.12.4 + uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0 with: repository-url: https://test.pypi.org/legacy/ diff --git a/.github/workflows/update_pr_references.yaml b/.github/workflows/update_pr_references.yaml index c568df9ce..bf026b725 100644 --- a/.github/workflows/update_pr_references.yaml +++ b/.github/workflows/update_pr_references.yaml @@ -10,9 +10,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - with: - fetch-depth: 0 - - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 + - uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 with: python-version: '3.x' - name: update any PR numbers in change fragments diff --git a/changelog.d/20250929_092604_kurtmckee_update_auth_get_my_groups.rst b/changelog.d/20250929_092604_kurtmckee_update_auth_get_my_groups.rst deleted file mode 100644 index c4269977d..000000000 --- a/changelog.d/20250929_092604_kurtmckee_update_auth_get_my_groups.rst +++ /dev/null @@ -1,4 +0,0 @@ -Added ------ - -- Add the ``statuses`` parameter to ``GroupsClient.get_my_groups()``. (:pr:`1317`) diff --git a/changelog.rst b/changelog.rst index 05818e224..bdbff3a35 100644 --- a/changelog.rst +++ b/changelog.rst @@ -367,6 +367,24 @@ Breaking Changes - Updated MappedCollectionDoc and GuestCollectionDoc with MissingType. (:pr:`1189`) +.. _changelog-3.65.0: + +v3.65.0 (2025-10-02) +==================== + +Added +----- + +- Add a ``FlowTimer`` payload class to aid in creating timers that run flows. +- Add the ``statuses`` parameter to ``GroupsClient.get_my_groups()``. (:pr:`1317`) +- Add ``.change_role()`` to the Globus Groups ``BatchMembershipActions`` helper class. (:pr:`1318`) +- Add ``.change_roles()`` to the Globus Groups ``GroupsManager`` class. (:pr:`1318`) + +Development +----------- + +- Fix a Poetry deprecation warning in the test suite. (:pr:`1320`) + .. _changelog-3.64.0: v3.64.0 (2025-09-24) diff --git a/docs/services/timers.rst b/docs/services/timers.rst index d5d9f082a..4cd8633c4 100644 --- a/docs/services/timers.rst +++ b/docs/services/timers.rst @@ -12,7 +12,11 @@ Globus Timers Helper Objects -------------- -A helper is provided for constructing Transfer Timers: +A helper is provided for constructing Transfer and Flows timers: + +.. autoclass:: FlowTimer + :members: + :show-inheritance: .. autoclass:: TransferTimer :members: diff --git a/src/globus_sdk/__init__.pyi b/src/globus_sdk/__init__.pyi index e4302bf78..883e1b17a 100644 --- a/src/globus_sdk/__init__.pyi +++ b/src/globus_sdk/__init__.pyi @@ -107,6 +107,7 @@ from .services.search import ( SearchScrollQuery, ) from .services.timers import ( + FlowTimer, OnceTimerSchedule, RecurringTimerSchedule, TimerJob, @@ -228,6 +229,7 @@ __all__ = ( "TimerJob", "TimersAPIError", "TimersClient", + "FlowTimer", "TransferTimer", "DeleteData", "IterableTransferResponse", diff --git a/src/globus_sdk/services/groups/data.py b/src/globus_sdk/services/groups/data.py index b1bf6f289..315976743 100644 --- a/src/globus_sdk/services/groups/data.py +++ b/src/globus_sdk/services/groups/data.py @@ -151,6 +151,22 @@ def approve_pending( ) return self + def change_roles( + self, + role: _GROUP_ROLE_T, + identity_ids: t.Iterable[uuid.UUID | str], + ) -> BatchMembershipActions: + """ + Assign a new role to a list of identities. + + :param role: The new role to assign. + :param identity_ids: The identities to assign to the new role. + """ + self.setdefault("change_role", []).extend( + {"role": role, "identity_id": identity_id} for identity_id in identity_ids + ) + return self + def decline_invites( self, identity_ids: t.Iterable[uuid.UUID | str] ) -> BatchMembershipActions: diff --git a/src/globus_sdk/services/groups/manager.py b/src/globus_sdk/services/groups/manager.py index da9204f92..08ada58d1 100644 --- a/src/globus_sdk/services/groups/manager.py +++ b/src/globus_sdk/services/groups/manager.py @@ -125,6 +125,22 @@ def approve_pending( actions = BatchMembershipActions().approve_pending([identity_id]) return self.client.batch_membership_action(group_id, actions) + def change_role( + self, + group_id: uuid.UUID | str, + identity_id: uuid.UUID | str, + role: _GROUP_ROLE_T, + ) -> response.GlobusHTTPResponse: + """ + Change the role of the given identity in the given group. + + :param group_id: The ID of the group + :param identity_id: The identity to assign the *role* to + :param role: The role that will be assigned to the *identity_id* + """ + actions = BatchMembershipActions().change_roles(role, [identity_id]) + return self.client.batch_membership_action(group_id, actions) + def decline_invite( self, group_id: uuid.UUID | str, identity_id: uuid.UUID | str ) -> response.GlobusHTTPResponse: diff --git a/src/globus_sdk/services/timers/__init__.py b/src/globus_sdk/services/timers/__init__.py index 303857aab..d83497f67 100644 --- a/src/globus_sdk/services/timers/__init__.py +++ b/src/globus_sdk/services/timers/__init__.py @@ -1,8 +1,15 @@ from .client import TimersClient -from .data import OnceTimerSchedule, RecurringTimerSchedule, TimerJob, TransferTimer +from .data import ( + FlowTimer, + OnceTimerSchedule, + RecurringTimerSchedule, + TimerJob, + TransferTimer, +) from .errors import TimersAPIError __all__ = ( + "FlowTimer", "TimersAPIError", "TimersClient", "OnceTimerSchedule", diff --git a/src/globus_sdk/services/timers/client.py b/src/globus_sdk/services/timers/client.py index 8f942febb..a0a0eddf1 100644 --- a/src/globus_sdk/services/timers/client.py +++ b/src/globus_sdk/services/timers/client.py @@ -13,7 +13,7 @@ TransferScopes, ) -from .data import TimerJob, TransferTimer +from .data import FlowTimer, TimerJob, TransferTimer from .errors import TimersAPIError log = logging.getLogger(__name__) @@ -136,7 +136,7 @@ def get_job( return self.get(f"/jobs/{job_id}", query_params=query_params) def create_timer( - self, timer: dict[str, t.Any] | TransferTimer + self, timer: dict[str, t.Any] | TransferTimer | FlowTimer ) -> response.GlobusHTTPResponse: """ :param timer: a document defining the new timer diff --git a/src/globus_sdk/services/timers/data.py b/src/globus_sdk/services/timers/data.py index a051f5f03..1fd9d048a 100644 --- a/src/globus_sdk/services/timers/data.py +++ b/src/globus_sdk/services/timers/data.py @@ -5,6 +5,7 @@ import datetime as dt import logging import typing as t +import uuid from globus_sdk._missing import MISSING, MissingType from globus_sdk._payload import GlobusPayload @@ -122,6 +123,114 @@ def _preprocess_body( return new_body +class FlowTimer(GlobusPayload): + """ + A helper for defining a payload for Flow Timer creation. + Use this along with :meth:`create_timer ` to + create a timer. + + .. note:: + + ``TimersClient`` has two methods for creating timers: + ``create_timer`` and ``create_job``. + + This helper class only works with the ``create_timer`` method. + + :param flow_id: The flow ID to run when the timer runs. + :param name: A name to identify this timer. + :param schedule: The schedule on which the timer runs + :param body: A transfer payload for the timer to use. + + The ``schedule`` field determines when the timer will run. + Timers may be "run once" or "recurring", and "recurring" timers may specify an end + date or the number of executions after which the timer will stop. + A ``schedule`` is specified as a dict, but the SDK provides two helpers + for constructing these data. + + **Example Schedules** + + .. tab-set:: + + .. tab-item:: Run Once, Right Now + + .. code-block:: python + + schedule = OnceTimerSchedule() + + .. tab-item:: Run Once, At a Specific Time + + .. code-block:: python + + schedule = OnceTimerSchedule(datetime="2023-09-22T00:00:00Z") + + .. tab-item:: Run Every 5 Minutes, Until a Specific Time + + .. code-block:: python + + 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 = RecurringTimerSchedule( + interval_seconds=1800, + end={"condition": "iterations", "iterations": 10}, + ) + + .. tab-item:: Run Every 10 Minutes, Indefinitely + + .. code-block:: python + + schedule = RecurringTimerSchedule(interval_seconds=600) + + Using these schedules, you can create a timer: + + .. code-block:: pycon + + >>> from globus_sdk import FlowTimer + >>> schedule = ... + >>> timer = FlowTimer( + ... name="my timer", + ... flow_id="00000000-19a9-44e6-9c1a-867da59d84ab", + ... schedule=schedule, + ... body={ + ... "body": { + ... "input_key": "input_value", + ... }, + ... "run_managers": [ + ... "urn:globus:auth:identity:11111111-be6a-473a-a027-4cfe4ceeafe3" + ... ], + ... }, + ... ) + + Submit the timer to the Timers service with + :meth:`create_timer `. + """ + + def __init__( + self, + *, + flow_id: uuid.UUID | str, + name: str | MissingType = MISSING, + schedule: dict[str, t.Any] | RecurringTimerSchedule | OnceTimerSchedule, + body: dict[str, t.Any], + ) -> None: + super().__init__() + self["timer_type"] = "flow" + self["flow_id"] = flow_id + self["name"] = name + self["schedule"] = schedule + self["body"] = self._preprocess_body(body) + + def _preprocess_body(self, body: dict[str, t.Any]) -> dict[str, t.Any]: + # Additional processing may be added in the future. + return body.copy() + + class RecurringTimerSchedule(GlobusPayload): """ A helper used as part of a *timer* to define when the *timer* will run. diff --git a/src/globus_sdk/testing/data/timer/_common.py b/src/globus_sdk/testing/data/timer/_common.py index 1853e3aed..8146af071 100644 --- a/src/globus_sdk/testing/data/timer/_common.py +++ b/src/globus_sdk/testing/data/timer/_common.py @@ -44,6 +44,41 @@ }, "status": "new", "submitted_at": "2023-10-26T20:31:09+00:00", + "timer_type": "transfer", +} + + +FLOW_ID = str(uuid.uuid4()) +V2_FLOW_TIMER = { + "callback_body": { + "body": {"input_key": "input_value"}, + "run_managers": [f"urn:globus:auth:identity:{uuid.uuid4()}"], + }, + "callback_url": f"https://flows.automate.globus.org/flows/{FLOW_ID}/run", + "inactive_reason": None, + "interval": None, + "job_id": str(uuid.uuid4()), + "last_ran_at": None, + "n_errors": 0, + "n_runs": 0, + "name": "Very Cool Timer", + "next_run": "2025-10-27T05:00:00+00:00", + "results": [], + "schedule": { + "datetime": "2025-10-27T05:00:00+00:00", + "type": "once", + }, + "scope": ( + f"https://auth.globus.org/scopes/{FLOW_ID}" + f"/flow_{FLOW_ID.replace('-', '_')}_user" + ), + "status": "new", + "stop_after": { + "date": None, + "n_runs": 1, + }, + "submitted_at": "2025-08-01T20:31:09+00:00", + "timer_type": "flow", } diff --git a/src/globus_sdk/testing/data/timer/create_timer.py b/src/globus_sdk/testing/data/timer/create_timer.py index 9cdc86e2d..2c9f2b598 100644 --- a/src/globus_sdk/testing/data/timer/create_timer.py +++ b/src/globus_sdk/testing/data/timer/create_timer.py @@ -1,6 +1,13 @@ from globus_sdk.testing.models import RegisteredResponse, ResponseSet -from ._common import DEST_EP_ID, SOURCE_EP_ID, TIMER_ID, V2_TRANSFER_TIMER +from ._common import ( + DEST_EP_ID, + FLOW_ID, + SOURCE_EP_ID, + TIMER_ID, + V2_FLOW_TIMER, + V2_TRANSFER_TIMER, +) RESPONSES = ResponseSet( default=RegisteredResponse( @@ -17,4 +24,19 @@ "destination_endpoint": DEST_EP_ID, }, ), + flow_timer_success=RegisteredResponse( + service="timer", + path="/v2/timer", + method="POST", + json={ + "timer": V2_FLOW_TIMER, + }, + status=201, + metadata={ + "timer_id": V2_FLOW_TIMER["job_id"], + "flow_id": FLOW_ID, + "callback_body": V2_FLOW_TIMER["callback_body"], + "schedule": V2_FLOW_TIMER["schedule"], + }, + ), ) diff --git a/tests/functional/services/groups/test_group_memberships.py b/tests/functional/services/groups/test_group_memberships.py index 769eeaeac..18adf173f 100644 --- a/tests/functional/services/groups/test_group_memberships.py +++ b/tests/functional/services/groups/test_group_memberships.py @@ -75,6 +75,7 @@ def test_batch_action_payload(groups_client, role): [uuid.uuid1(), uuid.uuid1()], role=role, ) + .change_roles("admin", [uuid.uuid1(), uuid.uuid1()]) .invite_members([uuid.uuid1(), uuid.uuid1()]) .join([uuid.uuid1(), uuid.uuid1()]) ) @@ -86,6 +87,11 @@ def test_batch_action_payload(groups_client, role): assert "accept" in batch_action assert len(batch_action["accept"]) == 1 + assert "change_role" in batch_action + assert len(batch_action["change_role"]) == 2 + for change_role in batch_action["change_role"]: + assert change_role["role"] == "admin" + assert "invite" in batch_action assert len(batch_action["invite"]) == 2 diff --git a/tests/functional/services/timers/test_create_timer.py b/tests/functional/services/timers/test_create_timer.py index 4167f704f..13faf1887 100644 --- a/tests/functional/services/timers/test_create_timer.py +++ b/tests/functional/services/timers/test_create_timer.py @@ -49,3 +49,24 @@ def test_transfer_timer_creation(client): for k, v in filter_missing(body).items() if k != "skip_activation_check" } + + +def test_flow_timer_creation(client): + # Setup + meta = load_response(client.create_timer, case="flow_timer_success").metadata + + # Act + client.create_timer( + timer=globus_sdk.FlowTimer( + flow_id=meta["flow_id"], + body=meta["callback_body"], + schedule=meta["schedule"], + ) + ) + + # Verify + req = get_last_request() + sent = json.loads(req.body) + assert sent["timer"]["flow_id"] == meta["flow_id"] + assert sent["timer"]["body"] == meta["callback_body"] + assert sent["timer"]["schedule"] == meta["schedule"] diff --git a/tests/non-pytest/poetry-lock-test/pyproject.toml b/tests/non-pytest/poetry-lock-test/pyproject.toml index 2c391b130..c1b722f5c 100644 --- a/tests/non-pytest/poetry-lock-test/pyproject.toml +++ b/tests/non-pytest/poetry-lock-test/pyproject.toml @@ -8,8 +8,6 @@ authors = ["Stephen Rosen "] python = "^3.8" globus-sdk = { path = "../../.." } -[tool.poetry.dev-dependencies] - [build-system] requires = ["poetry-core>=1.0.0"] build-backend = "poetry.core.masonry.api"