From 51af61f73a83e1c104a2617eb3dfeff2f88e939e Mon Sep 17 00:00:00 2001 From: Sam Whyte Date: Tue, 14 Jul 2026 12:31:57 -0400 Subject: [PATCH 1/5] Release 0.20.0 Add remove_user_from_team endpoint (POST /users/{email}/teams/remove). Co-Authored-By: Claude Sonnet 4.6 --- gremlinapi/users.py | 18 ++++++++++++++++++ pyproject.toml | 2 +- tests/test_users.py | 9 +++++++++ 3 files changed, 28 insertions(+), 1 deletion(-) diff --git a/gremlinapi/users.py b/gremlinapi/users.py index 80e4480..5ce15e6 100644 --- a/gremlinapi/users.py +++ b/gremlinapi/users.py @@ -114,6 +114,24 @@ def deactivate_user( (resp, body) = https_client.api_call(method, endpoint, **payload) return body + @classmethod + @register_cli_action("remove_user_from_team", ("email", "body"), ("",)) + def remove_user_from_team( + cls, + https_client: Type[GremlinAPIHttpClient] = get_gremlin_httpclient(), + *args: tuple, + **kwargs: dict, + ) -> dict: + method: str = "POST" + email: str = cls._error_if_not_email(**kwargs) + team_ids: Union[list, dict] = cls._error_if_not_json_body(**kwargs) + if isinstance(team_ids, str): + team_ids = [team_ids] + endpoint: str = f"/users/{email}/teams/remove" + payload: dict = cls._payload(**{"headers": https_client.header(), "data": {"teamIds": team_ids}}) # type: ignore + (resp, body) = https_client.api_call(method, endpoint, **payload) + return body + @classmethod @register_cli_action("list_active_user", ("",), ("teamId", "pageSize")) def list_active_users( diff --git a/pyproject.toml b/pyproject.toml index 79cc569..1ac2ca0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "gremlinapi" -version = "0.19.3" +version = "0.20.0" description = "Gremlin library for Python" readme = "README.md" license = { text = "Apache 2.0" } diff --git a/tests/test_users.py b/tests/test_users.py index bb3aac8..3b270a8 100644 --- a/tests/test_users.py +++ b/tests/test_users.py @@ -46,6 +46,15 @@ def test_deactivate_user_with_decorator(self, mock_get) -> None: mock_get.return_value.json = mock_json self.assertEqual(GremlinAPIUsers.deactivate_user(**mock_users), mock_data) + @patch("requests.post") + def test_remove_user_from_team_with_decorator(self, mock_post) -> None: + mock_post.return_value = requests.Response() + mock_post.return_value.status_code = 200 + mock_post.return_value.json = mock_json + self.assertEqual( + GremlinAPIUsers.remove_user_from_team(**{**mock_users, **mock_body}), mock_data + ) + @patch("requests.get") def test_list_active_users_with_decorator(self, mock_get) -> None: mock_get.return_value = requests.Response() From e669473414abcfaf8ef72f59ef807715572e70c0 Mon Sep 17 00:00:00 2001 From: Sam Whyte Date: Mon, 3 Aug 2026 13:26:06 -0600 Subject: [PATCH 2/5] Update gremlinapi/users.py Co-authored-by: Phil Gebhardt --- gremlinapi/users.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gremlinapi/users.py b/gremlinapi/users.py index 5ce15e6..1044f97 100644 --- a/gremlinapi/users.py +++ b/gremlinapi/users.py @@ -124,7 +124,7 @@ def remove_user_from_team( ) -> dict: method: str = "POST" email: str = cls._error_if_not_email(**kwargs) - team_ids: Union[list, dict] = cls._error_if_not_json_body(**kwargs) + team_ids: Union[list, str] = cls._error_if_not_json_body(**kwargs) if isinstance(team_ids, str): team_ids = [team_ids] endpoint: str = f"/users/{email}/teams/remove" From cfaa79f2e3065feb05ddf685013383e70b3792e8 Mon Sep 17 00:00:00 2001 From: Sam Whyte Date: Mon, 3 Aug 2026 14:17:23 -0600 Subject: [PATCH 3/5] Fix remove_user_from_team to validate and send team_ids correctly Validate team_ids explicitly instead of a generic JSON body check, and send it via form-encoded data so requests sets Content-Type correctly. --- gremlinapi/gremlinapi.py | 11 +++++++++++ gremlinapi/users.py | 6 ++---- tests/test_users.py | 12 ++++++------ tests/util.py | 1 + 4 files changed, 20 insertions(+), 10 deletions(-) diff --git a/gremlinapi/gremlinapi.py b/gremlinapi/gremlinapi.py index 5167184..d2339a1 100644 --- a/gremlinapi/gremlinapi.py +++ b/gremlinapi/gremlinapi.py @@ -141,6 +141,17 @@ def _error_if_not_json_body(cls, **kwargs: dict) -> dict: raise GremlinParameterError(error_msg) return body + @classmethod + def _error_if_not_team_ids(cls, **kwargs: dict) -> list: + team_ids: Union[list, str] = cls._info_if_not_param("team_ids", **kwargs) + if not team_ids: + error_msg: str = f"team_ids not passed to users endpoint: {kwargs}" + log.error(error_msg) + raise GremlinParameterError(error_msg) + if isinstance(team_ids, str): + team_ids = [team_ids] + return team_ids + @classmethod def _error_if_not_email(cls, **kwargs: dict) -> str: email: str = cls._info_if_not_param("email", **kwargs) diff --git a/gremlinapi/users.py b/gremlinapi/users.py index 1044f97..b798370 100644 --- a/gremlinapi/users.py +++ b/gremlinapi/users.py @@ -115,7 +115,7 @@ def deactivate_user( return body @classmethod - @register_cli_action("remove_user_from_team", ("email", "body"), ("",)) + @register_cli_action("remove_user_from_team", ("email", "team_ids"), ("",)) def remove_user_from_team( cls, https_client: Type[GremlinAPIHttpClient] = get_gremlin_httpclient(), @@ -124,9 +124,7 @@ def remove_user_from_team( ) -> dict: method: str = "POST" email: str = cls._error_if_not_email(**kwargs) - team_ids: Union[list, str] = cls._error_if_not_json_body(**kwargs) - if isinstance(team_ids, str): - team_ids = [team_ids] + team_ids: Union[list, str] = cls._error_if_not_team_ids(**kwargs) endpoint: str = f"/users/{email}/teams/remove" payload: dict = cls._payload(**{"headers": https_client.header(), "data": {"teamIds": team_ids}}) # type: ignore (resp, body) = https_client.api_call(method, endpoint, **payload) diff --git a/tests/test_users.py b/tests/test_users.py index 3b270a8..b103322 100644 --- a/tests/test_users.py +++ b/tests/test_users.py @@ -8,7 +8,7 @@ GremlinAPIUsersAuthMFA, ) -from .util import mock_json, mock_data, mock_users, mock_body, mock_paged_json, mock_paged_data +from .util import mock_json, mock_data, mock_users, mock_body, mock_team_ids, mock_paged_json, mock_paged_data class TestUsers(unittest.TestCase): @@ -47,12 +47,12 @@ def test_deactivate_user_with_decorator(self, mock_get) -> None: self.assertEqual(GremlinAPIUsers.deactivate_user(**mock_users), mock_data) @patch("requests.post") - def test_remove_user_from_team_with_decorator(self, mock_post) -> None: - mock_post.return_value = requests.Response() - mock_post.return_value.status_code = 200 - mock_post.return_value.json = mock_json + def test_remove_user_from_team_with_decorator(self, mock_get) -> None: + mock_get.return_value = requests.Response() + mock_get.return_value.status_code = 200 + mock_get.return_value.json = mock_json self.assertEqual( - GremlinAPIUsers.remove_user_from_team(**{**mock_users, **mock_body}), mock_data + GremlinAPIUsers.remove_user_from_team(**{**mock_users, **mock_team_ids}), mock_data ) @patch("requests.get") diff --git a/tests/util.py b/tests/util.py index 1bd5ae1..e8646b7 100644 --- a/tests/util.py +++ b/tests/util.py @@ -43,6 +43,7 @@ def mock_paged_json_page2(): mock_org_id = "1234567890a" mock_team_id = "1234567890a" +mock_team_ids = {"team_ids": [mock_team_id]} mock_body = {"body": mock_data} mock_guid = {"guid": mock_data} mock_scenario_guid = { From 25390657644cd3b76b6a46606cef5852794b7819 Mon Sep 17 00:00:00 2001 From: Sam Whyte Date: Mon, 3 Aug 2026 14:19:25 -0600 Subject: [PATCH 4/5] Pin gh-action-pypi-publish to release/v1 instead of sunset master branch --- .github/workflows/pypi-prod.yaml | 2 +- .github/workflows/pypi-test.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pypi-prod.yaml b/.github/workflows/pypi-prod.yaml index 57a7e10..815e504 100644 --- a/.github/workflows/pypi-prod.yaml +++ b/.github/workflows/pypi-prod.yaml @@ -35,7 +35,7 @@ jobs: run: | python -m tox -e build-dists --parallel 0 - name: Publish 📦 to Prod PyPI - uses: pypa/gh-action-pypi-publish@master + uses: pypa/gh-action-pypi-publish@release/v1 with: username: __token__ password: ${{ secrets.Prod_PyPI_token }} \ No newline at end of file diff --git a/.github/workflows/pypi-test.yaml b/.github/workflows/pypi-test.yaml index 2844130..8bbd4e7 100644 --- a/.github/workflows/pypi-test.yaml +++ b/.github/workflows/pypi-test.yaml @@ -35,7 +35,7 @@ jobs: run: | python -m tox -e build-dists --parallel 0 - name: Publish 📦 to Test PyPI - uses: pypa/gh-action-pypi-publish@master + uses: pypa/gh-action-pypi-publish@release/v1 with: username: __token__ password: ${{ secrets.Test_PyPI_token }} From a6f9a1ff289eba5d5b388f5ac68a594d6b140c1d Mon Sep 17 00:00:00 2001 From: Sam Whyte Date: Mon, 3 Aug 2026 14:20:56 -0600 Subject: [PATCH 5/5] Bump version to 0.20.1 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 1ac2ca0..cbafc30 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "gremlinapi" -version = "0.20.0" +version = "0.20.1" description = "Gremlin library for Python" readme = "README.md" license = { text = "Apache 2.0" }