slack-objects 0.2.1__tar.gz → 0.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. {slack_objects-0.2.1 → slack_objects-0.3.0}/PKG-INFO +1 -1
  2. {slack_objects-0.2.1 → slack_objects-0.3.0}/src/slack_objects/_version.py +3 -3
  3. {slack_objects-0.2.1 → slack_objects-0.3.0}/src/slack_objects/idp_groups.py +113 -5
  4. {slack_objects-0.2.1 → slack_objects-0.3.0}/src/slack_objects.egg-info/PKG-INFO +1 -1
  5. slack_objects-0.3.0/src/slack_objects.egg-info/scm_version.json +8 -0
  6. {slack_objects-0.2.1 → slack_objects-0.3.0}/tests/UnitTests/idp_groups_unit_test.py +128 -0
  7. slack_objects-0.2.1/src/slack_objects.egg-info/scm_version.json +0 -8
  8. {slack_objects-0.2.1 → slack_objects-0.3.0}/.github/copilot-instructions.md +0 -0
  9. {slack_objects-0.2.1 → slack_objects-0.3.0}/.github/workflows/ci.yml +0 -0
  10. {slack_objects-0.2.1 → slack_objects-0.3.0}/.github/workflows/publish-pypi.yml +0 -0
  11. {slack_objects-0.2.1 → slack_objects-0.3.0}/.github/workflows/publish-testpypi.yml +0 -0
  12. {slack_objects-0.2.1 → slack_objects-0.3.0}/.gitignore +0 -0
  13. {slack_objects-0.2.1 → slack_objects-0.3.0}/LICENSE +0 -0
  14. {slack_objects-0.2.1 → slack_objects-0.3.0}/README.md +0 -0
  15. {slack_objects-0.2.1 → slack_objects-0.3.0}/pyproject.toml +0 -0
  16. {slack_objects-0.2.1 → slack_objects-0.3.0}/pytest.ini +0 -0
  17. {slack_objects-0.2.1 → slack_objects-0.3.0}/setup.cfg +0 -0
  18. {slack_objects-0.2.1 → slack_objects-0.3.0}/src/slack_objects/__init__.py +0 -0
  19. {slack_objects-0.2.1 → slack_objects-0.3.0}/src/slack_objects/api_caller.py +0 -0
  20. {slack_objects-0.2.1 → slack_objects-0.3.0}/src/slack_objects/base.py +0 -0
  21. {slack_objects-0.2.1 → slack_objects-0.3.0}/src/slack_objects/client.py +0 -0
  22. {slack_objects-0.2.1 → slack_objects-0.3.0}/src/slack_objects/config.py +0 -0
  23. {slack_objects-0.2.1 → slack_objects-0.3.0}/src/slack_objects/conversations.py +0 -0
  24. {slack_objects-0.2.1 → slack_objects-0.3.0}/src/slack_objects/files.py +0 -0
  25. {slack_objects-0.2.1 → slack_objects-0.3.0}/src/slack_objects/messages.py +0 -0
  26. {slack_objects-0.2.1 → slack_objects-0.3.0}/src/slack_objects/rate_limits.py +0 -0
  27. {slack_objects-0.2.1 → slack_objects-0.3.0}/src/slack_objects/scim_base.py +0 -0
  28. {slack_objects-0.2.1 → slack_objects-0.3.0}/src/slack_objects/usergroups.py +0 -0
  29. {slack_objects-0.2.1 → slack_objects-0.3.0}/src/slack_objects/users.py +0 -0
  30. {slack_objects-0.2.1 → slack_objects-0.3.0}/src/slack_objects/workspaces.py +0 -0
  31. {slack_objects-0.2.1 → slack_objects-0.3.0}/src/slack_objects.egg-info/SOURCES.txt +0 -0
  32. {slack_objects-0.2.1 → slack_objects-0.3.0}/src/slack_objects.egg-info/dependency_links.txt +0 -0
  33. {slack_objects-0.2.1 → slack_objects-0.3.0}/src/slack_objects.egg-info/requires.txt +0 -0
  34. {slack_objects-0.2.1 → slack_objects-0.3.0}/src/slack_objects.egg-info/scm_file_list.json +0 -0
  35. {slack_objects-0.2.1 → slack_objects-0.3.0}/src/slack_objects.egg-info/top_level.txt +0 -0
  36. {slack_objects-0.2.1 → slack_objects-0.3.0}/tests/Examples/examples_config.example.json +0 -0
  37. {slack_objects-0.2.1 → slack_objects-0.3.0}/tests/Examples/users_search_by_email_example.py +0 -0
  38. {slack_objects-0.2.1 → slack_objects-0.3.0}/tests/Examples/users_test_AzureKeyVault.py +0 -0
  39. {slack_objects-0.2.1 → slack_objects-0.3.0}/tests/SCIM/conftest_live.py +0 -0
  40. {slack_objects-0.2.1 → slack_objects-0.3.0}/tests/SCIM/run_all_scim_users_live_tests.py +0 -0
  41. {slack_objects-0.2.1 → slack_objects-0.3.0}/tests/SCIM/test_scim_idp_groups_live.py +0 -0
  42. {slack_objects-0.2.1 → slack_objects-0.3.0}/tests/SCIM/test_scim_users_create_live.py +0 -0
  43. {slack_objects-0.2.1 → slack_objects-0.3.0}/tests/SCIM/test_scim_users_deactivate_live.py +0 -0
  44. {slack_objects-0.2.1 → slack_objects-0.3.0}/tests/SCIM/test_scim_users_input_validation_live.py +0 -0
  45. {slack_objects-0.2.1 → slack_objects-0.3.0}/tests/SCIM/test_scim_users_make_guest_live.py +0 -0
  46. {slack_objects-0.2.1 → slack_objects-0.3.0}/tests/SCIM/test_scim_users_reactivate_live.py +0 -0
  47. {slack_objects-0.2.1 → slack_objects-0.3.0}/tests/SCIM/test_scim_users_update_attribute_live.py +0 -0
  48. {slack_objects-0.2.1 → slack_objects-0.3.0}/tests/SCIM/test_scim_users_update_email_live.py +0 -0
  49. {slack_objects-0.2.1 → slack_objects-0.3.0}/tests/Smoke/_smoke_harness.py +0 -0
  50. {slack_objects-0.2.1 → slack_objects-0.3.0}/tests/Smoke/api_caller_smoke_test.py +0 -0
  51. {slack_objects-0.2.1 → slack_objects-0.3.0}/tests/Smoke/conversations_smoke_test.py +0 -0
  52. {slack_objects-0.2.1 → slack_objects-0.3.0}/tests/Smoke/files_smoke_test.py +0 -0
  53. {slack_objects-0.2.1 → slack_objects-0.3.0}/tests/Smoke/idp_groups_smoke_test.py +0 -0
  54. {slack_objects-0.2.1 → slack_objects-0.3.0}/tests/Smoke/messages_smoke_test.py +0 -0
  55. {slack_objects-0.2.1 → slack_objects-0.3.0}/tests/Smoke/run_all_smoke.py +0 -0
  56. {slack_objects-0.2.1 → slack_objects-0.3.0}/tests/Smoke/security_smoke_test.py +0 -0
  57. {slack_objects-0.2.1 → slack_objects-0.3.0}/tests/Smoke/usergroups_smoke_test.py +0 -0
  58. {slack_objects-0.2.1 → slack_objects-0.3.0}/tests/Smoke/users_smoke_test.py +0 -0
  59. {slack_objects-0.2.1 → slack_objects-0.3.0}/tests/Smoke/workspaces_smoke_test.py +0 -0
  60. {slack_objects-0.2.1 → slack_objects-0.3.0}/tests/UnitTests/config_unit_test.py +0 -0
  61. {slack_objects-0.2.1 → slack_objects-0.3.0}/tests/UnitTests/conversations_unit_test.py +0 -0
  62. {slack_objects-0.2.1 → slack_objects-0.3.0}/tests/UnitTests/files_unit_test.py +0 -0
  63. {slack_objects-0.2.1 → slack_objects-0.3.0}/tests/UnitTests/messages_unit_test.py +0 -0
  64. {slack_objects-0.2.1 → slack_objects-0.3.0}/tests/UnitTests/rate_limits_unit_test.py +0 -0
  65. {slack_objects-0.2.1 → slack_objects-0.3.0}/tests/UnitTests/usergroups_test.py +0 -0
  66. {slack_objects-0.2.1 → slack_objects-0.3.0}/tests/UnitTests/users_unit_test.py +0 -0
  67. {slack_objects-0.2.1 → slack_objects-0.3.0}/tests/UnitTests/workspaces_unit_test.py +0 -0
  68. {slack_objects-0.2.1 → slack_objects-0.3.0}/tests/Users/conftest_live.py +0 -0
  69. {slack_objects-0.2.1 → slack_objects-0.3.0}/tests/Users/run_all_users_live_tests.py +0 -0
  70. {slack_objects-0.2.1 → slack_objects-0.3.0}/tests/Users/test_users_admin_live.py +0 -0
  71. {slack_objects-0.2.1 → slack_objects-0.3.0}/tests/Users/test_users_classification_live.py +0 -0
  72. {slack_objects-0.2.1 → slack_objects-0.3.0}/tests/Users/test_users_input_validation_live.py +0 -0
  73. {slack_objects-0.2.1 → slack_objects-0.3.0}/tests/Users/test_users_read_live.py +0 -0
  74. {slack_objects-0.2.1 → slack_objects-0.3.0}/tests/conftest.py +0 -0
  75. {slack_objects-0.2.1 → slack_objects-0.3.0}/tests/live_test_config.example.json +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: slack-objects
3
- Version: 0.2.1
3
+ Version: 0.3.0
4
4
  Summary: Opinionated, testable Python wrappers for Slack’s Web, Admin, and SCIM APIs, organized by object domain (users, conversations, messages, files, workspaces, and IdP groups). Designed for automation and administration workflows.
5
5
  Author-email: "Marcos E. Mercado" <marcos_elias@hotmail.com>
6
6
  Keywords: slack,objects,classes,slack objects,utilities,slack utilities,slack object types,slack types,types
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
18
18
  commit_id: str | None
19
19
  __commit_id__: str | None
20
20
 
21
- __version__ = version = '0.2.1'
22
- __version_tuple__ = version_tuple = (0, 2, 1)
21
+ __version__ = version = '0.3.0'
22
+ __version_tuple__ = version_tuple = (0, 3, 0)
23
23
 
24
- __commit_id__ = commit_id = 'g19585aaad'
24
+ __commit_id__ = commit_id = 'g88ac46367'
@@ -12,6 +12,7 @@ Manage Identity Provider (IdP) groups synced into Slack via SCIM.
12
12
  This module implements the following functionality:
13
13
 
14
14
  - list groups (paginated)
15
+ - get a single group's full SCIM record (name, members, metadata)
15
16
  - get members of a given group
16
17
  - check whether a user is a member of a group
17
18
 
@@ -20,6 +21,11 @@ Design decisions
20
21
  - SCIM REST calls are centralized in ScimMixin._scim_request(); all public methods call endpoint wrappers.
21
22
  - Uses an injectable `requests.Session` (`scim_session`) so tests can pass a fake session.
22
23
  - Keeps legacy output shapes: lists of dicts for groups and members.
24
+ - ``get_group()`` is the single read path for GET Groups/{id}; ``get_members()``,
25
+ ``is_member()`` and the ``display_name``/``members`` properties all compose on it
26
+ so the endpoint is only ever called from one place.
27
+ - Attributes are loaded lazily (like ``Users``): binding a group_id does no network I/O,
28
+ the fetch happens on first property access or on ``refresh()``.
23
29
  - This module is SCIM-only. For Slack-native usergroups, see ``usergroups.py``.
24
30
  """
25
31
 
@@ -42,9 +48,18 @@ class IDP_groups(ScimMixin, SlackObjectBase):
42
48
  idp = slack.idp_groups() # unbound
43
49
  bound = slack.idp_groups("S123") # bound to a group_id
44
50
 
51
+ Binding a group_id performs no API call. `attributes` are loaded lazily via
52
+ `_require_attributes()` on first property access, so a single GET Groups/{id}
53
+ serves both the name and the members:
54
+
55
+ group = slack.idp_groups("S123")
56
+ name = group.display_name # triggers the fetch
57
+ members = group.members # served from the same cached response
58
+
45
59
  The SCIM session can be replaced for unit tests by passing scim_session argument.
46
60
  """
47
61
  group_id: Optional[str] = None
62
+ attributes: Dict[str, Any] = field(default_factory=dict)
48
63
  scim_session: requests.Session = field(default_factory=requests.Session, repr=False)
49
64
 
50
65
  # ---------- factory ----------
@@ -68,6 +83,41 @@ class IDP_groups(ScimMixin, SlackObjectBase):
68
83
  raise ValueError("group_id is required (passed or bound)")
69
84
  return gid
70
85
 
86
+ # ---------- lazily loaded attributes ----------
87
+
88
+ def refresh(self, group_id: Optional[str] = None) -> Dict[str, Any]:
89
+ """
90
+ Refresh cached attributes for group_id (or self.group_id) using GET Groups/{id}.
91
+
92
+ Layered like ``Users.refresh()``: it calls the public ``get_group()``, which
93
+ calls the endpoint wrapper.
94
+ """
95
+ if group_id:
96
+ self.group_id = group_id
97
+ if not self.group_id:
98
+ raise ValueError("refresh() requires group_id (passed or already set)")
99
+
100
+ self.attributes = self.get_group(self.group_id)
101
+ return self.attributes
102
+
103
+ def _require_attributes(self) -> Dict[str, Any]:
104
+ """Ensure attributes are loaded (via refresh) before using helpers that read group fields."""
105
+ if self.attributes:
106
+ return self.attributes
107
+ if self.group_id:
108
+ return self.refresh()
109
+ raise ValueError("Group attributes not loaded and no group_id set (call refresh() or bind a group_id).")
110
+
111
+ @property
112
+ def display_name(self) -> str:
113
+ """The group's SCIM ``displayName`` (loaded lazily)."""
114
+ return self._require_attributes().get("displayName", "")
115
+
116
+ @property
117
+ def members(self) -> List[Dict[str, str]]:
118
+ """The group's members as ``{'value': <user id>, 'display': <name>}`` dicts (loaded lazily)."""
119
+ return self._require_attributes().get("members", []) or []
120
+
71
121
  # ---------- endpoint wrappers (only these call _scim_request) ----------
72
122
 
73
123
  def _scim_groups_list(self, *, count: int = 1000, start_index: Optional[int] = None) -> ScimResponse:
@@ -80,10 +130,21 @@ class IDP_groups(ScimMixin, SlackObjectBase):
80
130
  params["startIndex"] = start_index
81
131
  return self._scim_request(path="Groups", method="GET", params=params) # https://docs.slack.dev/reference/scim-api/#get-groups
82
132
 
83
- def _scim_group_get(self, group_id: str) -> ScimResponse:
84
- """Wrapper for GET Groups/{id}"""
133
+ def _scim_group_get(self, group_id: str, *, count: Optional[int] = None, start_index: Optional[int] = None) -> ScimResponse:
134
+ """
135
+ Wrapper for GET Groups/{id}.
136
+
137
+ count/start_index page over the group's *members* sub-list, which matters for
138
+ groups larger than the server-side page size.
139
+ """
85
140
  validate_scim_id(group_id, "group_id")
86
- return self._scim_request(path=f"Groups/{group_id}", method="GET") # https://docs.slack.dev/reference/scim-api/#get-groups-id
141
+ params: Dict[str, Any] = {}
142
+ if count is not None:
143
+ params["count"] = count
144
+ if start_index is not None:
145
+ params["startIndex"] = start_index
146
+ # Pass params only when set, so the default call is byte-for-byte the legacy request.
147
+ return self._scim_request(path=f"Groups/{group_id}", method="GET", params=params or None) # https://docs.slack.dev/reference/scim-api/#get-groups-id
87
148
 
88
149
  # ---------- public helpers ----------
89
150
 
@@ -130,16 +191,60 @@ class IDP_groups(ScimMixin, SlackObjectBase):
130
191
 
131
192
  return groups_out
132
193
 
194
+ def get_group(self, group_id: Optional[str] = None, *, fetch_count: int = 1000) -> Dict[str, Any]:
195
+ """
196
+ Return the full SCIM record for a single group (GET Groups/{id}).
197
+
198
+ One call yields both ``displayName`` and ``members``, so callers that need
199
+ the name do not have to pay for a second request. The complete payload is
200
+ returned (``id``, ``displayName``, ``members``, ``meta``, ``schemas``, ...) so
201
+ new SCIM fields are reachable without adding a method here.
202
+
203
+ When the response reports a ``totalResults`` larger than the members returned,
204
+ the remaining member pages are fetched and merged, so large groups are not
205
+ silently truncated.
206
+
207
+ Raises:
208
+ requests.HTTPError on non-2xx responses.
209
+ """
210
+ gid = self._resolve_group_id(group_id)
211
+
212
+ # First request sends no pagination params, so it is identical to the legacy call.
213
+ # Copy so callers mutating the result cannot corrupt our cached attributes.
214
+ group = dict(self._scim_group_get(gid).data)
215
+ members: List[Dict[str, str]] = list(group.get("members") or [])
216
+
217
+ # totalResults is only present when the server actually paged the members;
218
+ # when it is absent the single response is complete and we skip the loop.
219
+ total_results = group.get("totalResults")
220
+ if total_results is not None:
221
+ while len(members) < int(total_results):
222
+ # SCIM startIndex is 1-based, and the server chooses the first page size,
223
+ # so derive the cursor from what we already have rather than from fetch_count.
224
+ start_index = len(members) + 1
225
+ page_members = self._scim_group_get(gid, count=fetch_count, start_index=start_index).data.get("members") or []
226
+ # An empty page means the server has nothing more; stop rather than loop forever.
227
+ if not page_members:
228
+ break
229
+ members.extend(page_members)
230
+
231
+ group["members"] = members
232
+ return group
233
+
133
234
  def get_members(self, group_id: Optional[str] = None) -> List[Dict[str, str]]:
134
235
  """
135
236
  Return group members via SCIM (GET Groups/{id}) as a list of dicts `{'value': <user_id>, 'display': <name>}`.
136
237
 
238
+ Composes on ``get_group()``; the cached attributes are reused when they already
239
+ describe the requested group, so property access and this method share one call.
240
+
137
241
  This module is SCIM-only. For Slack-native usergroups, see
138
242
  ``Usergroups.get_members()`` in ``usergroups.py``.
139
243
  """
140
244
  gid = self._resolve_group_id(group_id)
141
- scim_resp = self._scim_group_get(gid)
142
- return scim_resp.data.get("members", [])
245
+ if self.attributes and self.attributes.get("id") == gid:
246
+ return self.attributes.get("members", []) or []
247
+ return self.get_group(gid).get("members", []) or []
143
248
 
144
249
  def is_member(self, user_id: str, group_id: Optional[str] = None) -> bool:
145
250
  """
@@ -147,6 +252,9 @@ class IDP_groups(ScimMixin, SlackObjectBase):
147
252
 
148
253
  Higher-level convenience that composes on ``get_members()``.
149
254
  Preserves legacy semantics (scans the members list).
255
+
256
+ Each call fetches the group unless its attributes are already cached, so when
257
+ testing many users bind the group once (or hoist ``get_members()`` out of the loop).
150
258
  """
151
259
  members = self.get_members(group_id=group_id)
152
260
  for member in members:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: slack-objects
3
- Version: 0.2.1
3
+ Version: 0.3.0
4
4
  Summary: Opinionated, testable Python wrappers for Slack’s Web, Admin, and SCIM APIs, organized by object domain (users, conversations, messages, files, workspaces, and IdP groups). Designed for automation and administration workflows.
5
5
  Author-email: "Marcos E. Mercado" <marcos_elias@hotmail.com>
6
6
  Keywords: slack,objects,classes,slack objects,utilities,slack utilities,slack object types,slack types,types
@@ -0,0 +1,8 @@
1
+ {
2
+ "tag": "0.3.0",
3
+ "distance": 0,
4
+ "node": "g88ac46367071865a9bb07d9ae48e7cc6116f5b72",
5
+ "dirty": false,
6
+ "branch": "HEAD",
7
+ "node_date": "2026-08-06"
8
+ }
@@ -225,6 +225,134 @@ def test_get_members_and_is_member_with_bound_group():
225
225
  assert bound.is_member("U999") is False
226
226
 
227
227
 
228
+ def test_get_group_returns_full_record_in_one_call():
229
+ """
230
+ Verifies:
231
+ - get_group() returns the complete SCIM payload (not just members)
232
+ - a single GET Groups/{id} serves both displayName and members
233
+ """
234
+ from slack_objects.idp_groups import IDP_groups
235
+
236
+ cfg = DummyConfig()
237
+ base = _scim_base(cfg, "v1")
238
+
239
+ group_payload = {
240
+ "id": "S123",
241
+ "displayName": "Admins",
242
+ "meta": {"created": "2024-01-01T00:00:00Z"},
243
+ "members": [{"value": "U111", "display": "Alice"}],
244
+ }
245
+
246
+ sess = FakeScimSession({("GET", f"{base}Groups/S123"): (200, group_payload)})
247
+
248
+ idp = IDP_groups(cfg=cfg, client=DummySlackClient(), logger=logging.getLogger("test"), api=DummyApiCaller(), scim_session=sess)
249
+ idp.rate_policy = RateLimitPolicy(method_overrides={}, prefix_rules={}, default=0.0)
250
+
251
+ group = idp.get_group("S123")
252
+
253
+ assert group["displayName"] == "Admins"
254
+ assert group["members"] == group_payload["members"]
255
+ # Unmapped fields stay reachable, which is why the full payload is returned.
256
+ assert group["meta"]["created"] == "2024-01-01T00:00:00Z"
257
+ assert len(sess.calls) == 1
258
+
259
+
260
+ def test_bound_properties_load_lazily_and_share_one_call():
261
+ """
262
+ Verifies:
263
+ - binding a group_id performs no API call
264
+ - display_name triggers the fetch, and members reuses the cached response
265
+ """
266
+ from slack_objects.idp_groups import IDP_groups
267
+
268
+ cfg = DummyConfig()
269
+ base = _scim_base(cfg, "v1")
270
+
271
+ group_payload = {
272
+ "id": "S123",
273
+ "displayName": "Admins",
274
+ "members": [{"value": "U111", "display": "Alice"}],
275
+ }
276
+
277
+ sess = FakeScimSession({("GET", f"{base}Groups/S123"): (200, group_payload)})
278
+
279
+ group = IDP_groups(cfg=cfg, client=DummySlackClient(), logger=logging.getLogger("test"), api=DummyApiCaller(), scim_session=sess, group_id="S123")
280
+ group.rate_policy = RateLimitPolicy(method_overrides={}, prefix_rules={}, default=0.0)
281
+
282
+ # Construction must not perform network I/O.
283
+ assert sess.calls == []
284
+
285
+ assert group.display_name == "Admins"
286
+ assert group.members == group_payload["members"]
287
+ # get_members() must reuse the cache rather than issue a second request.
288
+ assert group.get_members() == group_payload["members"]
289
+ assert len(sess.calls) == 1
290
+
291
+
292
+ def test_properties_require_a_bound_group_id():
293
+ """An unbound instance cannot lazily load, and must say so clearly."""
294
+ from slack_objects.idp_groups import IDP_groups
295
+
296
+ cfg = DummyConfig()
297
+ sess = FakeScimSession({})
298
+
299
+ idp = IDP_groups(cfg=cfg, client=DummySlackClient(), logger=logging.getLogger("test"), api=DummyApiCaller(), scim_session=sess)
300
+ idp.rate_policy = RateLimitPolicy(method_overrides={}, prefix_rules={}, default=0.0)
301
+
302
+ try:
303
+ idp.display_name
304
+ except ValueError:
305
+ pass
306
+ else:
307
+ raise AssertionError("display_name should raise ValueError when no group_id is bound")
308
+
309
+ assert sess.calls == []
310
+
311
+
312
+ def test_get_group_merges_paginated_members():
313
+ """
314
+ Verifies:
315
+ - when totalResults exceeds the members returned, remaining pages are fetched
316
+ - members from all pages are merged, so large groups are not truncated
317
+ """
318
+ from slack_objects.idp_groups import IDP_groups
319
+
320
+ cfg = DummyConfig()
321
+
322
+ page1 = {
323
+ "id": "S123",
324
+ "displayName": "Admins",
325
+ "totalResults": 3,
326
+ "members": [{"value": "U111", "display": "Alice"}, {"value": "U222", "display": "Bob"}],
327
+ }
328
+ page2 = {
329
+ "id": "S123",
330
+ "displayName": "Admins",
331
+ "totalResults": 3,
332
+ "members": [{"value": "U333", "display": "Carol"}],
333
+ }
334
+
335
+ sess = FakeScimSession({})
336
+
337
+ calls = []
338
+
339
+ def request_side_effect(method: str, url: str, **kwargs):
340
+ calls.append(kwargs.get("params"))
341
+ return FakeResponse(200, page1 if len(calls) == 1 else page2)
342
+
343
+ sess.request = request_side_effect # type: ignore[method-assign]
344
+
345
+ idp = IDP_groups(cfg=cfg, client=DummySlackClient(), logger=logging.getLogger("test"), api=DummyApiCaller(), scim_session=sess)
346
+ idp.rate_policy = RateLimitPolicy(method_overrides={}, prefix_rules={}, default=0.0)
347
+
348
+ group = idp.get_group("S123", fetch_count=2)
349
+
350
+ assert [m["value"] for m in group["members"]] == ["U111", "U222", "U333"]
351
+ # First call is the plain legacy request; the second resumes after what we already have.
352
+ assert calls[0] is None
353
+ assert calls[1]["startIndex"] == 3
354
+
355
+
228
356
  # -----------------------------
229
357
  # Optional "factory-style" demo
230
358
  # -----------------------------
@@ -1,8 +0,0 @@
1
- {
2
- "tag": "0.2.1",
3
- "distance": 0,
4
- "node": "g19585aaad5c9198c6559655f4d5f4931a5246563",
5
- "dirty": false,
6
- "branch": "HEAD",
7
- "node_date": "2026-08-03"
8
- }
File without changes
File without changes
File without changes
File without changes
File without changes