Office365-REST-Python-Client 3.1.0__py3-none-any.whl → 3.2.0__py3-none-any.whl

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 (245) hide show
  1. office365/communications/callrecords/call_record.py +18 -0
  2. office365/communications/calls/call.py +3 -0
  3. office365/communications/presences/presence.py +3 -0
  4. office365/communications/quotas.py +66 -0
  5. office365/communications/virtualevents/virtual_event.py +3 -0
  6. office365/directory/domains/domain.py +3 -0
  7. office365/directory/licenses/subscribed_sku.py +3 -0
  8. office365/directory/objects/object.py +3 -0
  9. office365/directory/quotas.py +34 -0
  10. office365/entity_collection.py +2 -2
  11. office365/graph_client.py +46 -2
  12. office365/limits.py +66 -0
  13. office365/migration/__init__.py +6 -1
  14. office365/migration/adapters/__init__.py +14 -4
  15. office365/migration/adapters/filesystem.py +4 -1
  16. office365/migration/assessment/export.py +5 -5
  17. office365/migration/assessment/issue.py +19 -0
  18. office365/migration/assessment/report.py +11 -0
  19. office365/migration/assessment/scanners/base.py +37 -49
  20. office365/migration/base.py +33 -8
  21. office365/migration/checkpoint.py +1 -1
  22. office365/migration/outlook/assessor.py +4 -4
  23. office365/migration/package/__init__.py +82 -0
  24. office365/migration/package/builder.py +603 -0
  25. office365/migration/package/crypto.py +54 -0
  26. office365/migration/package/manifest_xml.py +495 -0
  27. office365/migration/package/staging.py +180 -0
  28. office365/migration/report.py +7 -2
  29. office365/migration/report_io.py +79 -0
  30. office365/migration/runner.py +117 -10
  31. office365/migration/server_job.py +126 -14
  32. office365/migration/session.py +132 -49
  33. office365/migration/settings.py +67 -0
  34. office365/migration/sharepoint/__init__.py +2 -0
  35. office365/migration/sharepoint/adapters.py +207 -41
  36. office365/migration/sharepoint/assessor.py +45 -9
  37. office365/migration/sharepoint/options.py +57 -0
  38. office365/migration/sharepoint/package_target.py +209 -0
  39. office365/migration/sharepoint/registry.py +22 -0
  40. office365/migration/sharepoint/resolvers.py +96 -0
  41. office365/migration/sharepoint/scanners/__init__.py +29 -0
  42. office365/migration/sharepoint/scanners/browser_file_handling.py +55 -0
  43. office365/migration/sharepoint/scanners/checked_out_files.py +57 -0
  44. office365/migration/sharepoint/scanners/file_versions.py +56 -0
  45. office365/migration/sharepoint/scanners/files.py +5 -2
  46. office365/migration/sharepoint/scanners/large_excel_files.py +56 -0
  47. office365/migration/sharepoint/scanners/large_lists.py +58 -0
  48. office365/migration/sharepoint/scanners/large_sites.py +4 -3
  49. office365/migration/sharepoint/scanners/long_onedrive_urls.py +52 -0
  50. office365/migration/sharepoint/scanners/lookup.py +38 -0
  51. office365/migration/sharepoint/scanners/paths.py +9 -4
  52. office365/migration/sharepoint/scanners/permissions.py +20 -2
  53. office365/migration/sharepoint/scanners/smat.py +51 -0
  54. office365/migration/sharepoint/scanners/summary.py +1 -0
  55. office365/migration/sharepoint/scanners/thicket_folders.py +38 -0
  56. office365/migration/sharepoint/scanners/unsupported_site_templates.py +90 -0
  57. office365/migration/sharepoint/tenant_assessor.py +5 -3
  58. office365/migration/sharepoint/transfer.py +4 -2
  59. office365/migration/tasks.py +121 -0
  60. office365/migration/validators.py +2 -19
  61. office365/onedrive/driveitems/driveItem.py +116 -12
  62. office365/onedrive/termstore/groups/collection.py +2 -9
  63. office365/onedrive/termstore/sets/collection.py +2 -9
  64. office365/onedrive/termstore/terms/collection.py +3 -10
  65. office365/onedrive/workbooks/workbook.py +7 -0
  66. office365/outlook/contacts/folders/collection.py +4 -13
  67. office365/runtime/auth/authentication_context.py +5 -0
  68. office365/runtime/client_object.py +18 -0
  69. office365/runtime/client_object_collection.py +75 -245
  70. office365/runtime/client_request.py +77 -1
  71. office365/runtime/client_request_exception.py +127 -39
  72. office365/runtime/client_runtime_context.py +79 -8
  73. office365/runtime/converters/csv_writer.py +10 -4
  74. office365/runtime/converters/dataframe.py +84 -4
  75. office365/runtime/converters/json_file.py +12 -3
  76. office365/runtime/converters/parquet.py +67 -0
  77. office365/runtime/converters/records.py +31 -12
  78. office365/runtime/converters/registry.py +185 -0
  79. office365/runtime/converters/scalars.py +31 -0
  80. office365/runtime/converters/sql.py +69 -0
  81. office365/runtime/converters/streamers.py +105 -0
  82. office365/runtime/converters/streams.py +42 -0
  83. office365/runtime/converters/upsert.py +101 -0
  84. office365/runtime/exceptions.py +105 -0
  85. office365/runtime/http/prefer.py +26 -0
  86. office365/runtime/http/throttling.py +258 -56
  87. office365/runtime/imports.py +571 -0
  88. office365/runtime/limits.py +455 -0
  89. office365/runtime/odata/request.py +11 -2
  90. office365/runtime/odata/type.py +1 -0
  91. office365/runtime/odata/v3/batch_request.py +4 -2
  92. office365/runtime/odata/v4/batch_request.py +22 -7
  93. office365/runtime/operations.py +29 -0
  94. office365/runtime/parallel.py +12 -14
  95. office365/runtime/paths/v4/entity.py +2 -2
  96. office365/runtime/queries/batch.py +18 -1
  97. office365/runtime/queries/get_or_create.py +109 -0
  98. office365/runtime/record_collection.py +443 -0
  99. office365/runtime/retry.py +92 -26
  100. office365/runtime/transport/base.py +6 -1
  101. office365/runtime/transport/requests_transport.py +4 -0
  102. office365/runtime/transport/throttled_transport.py +67 -0
  103. office365/runtime/verification.py +40 -0
  104. office365/sharepoint/activities/logger.py +118 -8
  105. office365/sharepoint/administration/analytics/usage_service.py +15 -7
  106. office365/sharepoint/administration/sitemove/service.py +791 -8
  107. office365/sharepoint/alerts/alert.py +79 -0
  108. office365/sharepoint/apps/app_collection.py +25 -0
  109. office365/sharepoint/apps/launcher.py +16 -0
  110. office365/sharepoint/authpolicy/tenant/compliancereport.py +5 -0
  111. office365/sharepoint/businessdata/app_bdc_catalog.py +135 -0
  112. office365/sharepoint/businessdata/entityfieldvaluedictionary.py +65 -0
  113. office365/sharepoint/businessdata/entityidentifier.py +23 -0
  114. office365/sharepoint/businessdata/entityinstance.py +52 -0
  115. office365/sharepoint/businessdata/entityview.py +31 -0
  116. office365/sharepoint/businessdata/externalsubscriptionstore.py +9 -0
  117. office365/sharepoint/businessdata/typedescriptor.py +37 -0
  118. office365/sharepoint/client_context.py +40 -23
  119. office365/sharepoint/clientsidecomponent/hostedapps/app.py +21 -0
  120. office365/sharepoint/clientsidecomponent/hostedapps/manager.py +16 -0
  121. office365/sharepoint/comments/comment.py +84 -2
  122. office365/sharepoint/contentcenter/machinelearning/models/model.py +0 -23
  123. office365/sharepoint/contentcenter/machinelearning/publications/publication.py +0 -9
  124. office365/sharepoint/contentcenter/machinelearning/samples/sample.py +0 -14
  125. office365/sharepoint/contentcenter/machinelearning/workitems/item.py +0 -80
  126. office365/sharepoint/contentsolution/template_api.py +148 -0
  127. office365/sharepoint/contenttypes/collection.py +32 -10
  128. office365/sharepoint/convergence/addnoliveblobsinodcservicestateapi.py +9 -0
  129. office365/sharepoint/convergence/consumer_permissions.py +15 -0
  130. office365/sharepoint/convergence/migration_complete_state_api.py +25 -0
  131. office365/sharepoint/convergence/odcmetadatacleanedupapi.py +9 -0
  132. office365/sharepoint/documentmanagement/document_id.py +12 -0
  133. office365/sharepoint/entity.py +11 -2
  134. office365/sharepoint/entity_collection.py +2 -2
  135. office365/sharepoint/exceptions.py +194 -0
  136. office365/sharepoint/fields/builtin_field_id.py +45 -34
  137. office365/sharepoint/fields/builtin_field_name.py +123 -0
  138. office365/sharepoint/fields/coercion.py +131 -0
  139. office365/sharepoint/fields/collection.py +54 -37
  140. office365/sharepoint/fields/field.py +17 -0
  141. office365/sharepoint/fields/name.py +46 -2
  142. office365/sharepoint/files/collection.py +7 -1
  143. office365/sharepoint/files/file.py +54 -8
  144. office365/sharepoint/files/versions/version.py +38 -13
  145. office365/sharepoint/folders/collection.py +4 -0
  146. office365/sharepoint/folders/folder.py +91 -21
  147. office365/sharepoint/htmlpages/html_page.py +16 -0
  148. office365/sharepoint/htmlpages/html_page_service.py +19 -0
  149. office365/sharepoint/ir/migration.py +9 -0
  150. office365/sharepoint/listitems/caml/__init__.py +31 -0
  151. office365/sharepoint/listitems/caml/builder.py +129 -0
  152. office365/sharepoint/listitems/caml/expressions.py +424 -0
  153. office365/sharepoint/listitems/caml/fields.py +68 -0
  154. office365/sharepoint/listitems/caml/guard.py +63 -0
  155. office365/sharepoint/listitems/caml/query.py +71 -2
  156. office365/sharepoint/listitems/caml/values.py +113 -0
  157. office365/sharepoint/listitems/collection.py +21 -20
  158. office365/sharepoint/listitems/upsert.py +75 -0
  159. office365/sharepoint/lists/collection.py +29 -22
  160. office365/sharepoint/lists/exporter.py +4 -3
  161. office365/sharepoint/lists/list.py +397 -39
  162. office365/sharepoint/lists/smart_wiki_library_utility.py +58 -0
  163. office365/sharepoint/logger/export.py +12 -0
  164. office365/sharepoint/marketplace/app_metadata.py +97 -1
  165. office365/sharepoint/marketplace/appstatequeryjobdefinition.py +9 -0
  166. office365/sharepoint/marketplace/corporatecuratedgallery/carddesigns/card_designs.py +35 -0
  167. office365/sharepoint/marketplace/sitecollection/appcatalog/allowed_item.py +5 -0
  168. office365/sharepoint/marketplace/tenant/appcatalog/accessor.py +107 -30
  169. office365/sharepoint/microfeed/data.py +0 -6
  170. office365/sharepoint/microservice/manager.py +61 -0
  171. office365/sharepoint/migrationcenter/common/task.py +193 -4
  172. office365/sharepoint/migrationcenter/common/task_entity_data.py +1 -1
  173. office365/sharepoint/migrationcenter/deploy_status.py +13 -0
  174. office365/sharepoint/migrationcenter/service/properties.py +20 -0
  175. office365/sharepoint/migrationcenter/service/storage.py +34 -0
  176. office365/sharepoint/migrationcenter/spflight.py +13 -0
  177. office365/sharepoint/migrationcenter/urlparseresult.py +77 -0
  178. office365/sharepoint/multigeo/service/spmultigeoproxy.py +13 -0
  179. office365/sharepoint/navigation/servicerest.py +127 -0
  180. office365/sharepoint/oauth/authentication.py +13 -0
  181. office365/sharepoint/portal/theme_manager.py +120 -8
  182. office365/sharepoint/principal/users/user.py +10 -0
  183. office365/sharepoint/publishing/campaign/campaign.py +36 -1
  184. office365/sharepoint/publishing/feedvideopage.py +0 -7
  185. office365/sharepoint/publishing/pages/3d/page.py +0 -6
  186. office365/sharepoint/publishing/pages/page.py +0 -117
  187. office365/sharepoint/publishing/pages/reposts/repost.py +0 -7
  188. office365/sharepoint/publishing/pages/topics/topic.py +0 -7
  189. office365/sharepoint/publishing/subtitlefile.py +0 -40
  190. office365/sharepoint/quotamanagement/consumer/consumer_quota_info_api.py +14 -0
  191. office365/sharepoint/quotamanagement/consumer/non_quota_backfill_api.py +23 -0
  192. office365/sharepoint/quotamanagement/consumer/nonquotamigrationapi.py +9 -0
  193. office365/sharepoint/quotamanagement/consumer/quotamigrationapi.py +13 -0
  194. office365/sharepoint/request.py +57 -16
  195. office365/sharepoint/search/administration/document_crawl_log.py +24 -7
  196. office365/sharepoint/search/administration/providers/crawl_versions_info.py +29 -0
  197. office365/sharepoint/search/administration/providers/recycle_bin_info.py +23 -0
  198. office365/sharepoint/search/administration/providers/site_content_processing_info.py +67 -0
  199. office365/sharepoint/sites/site.py +976 -65
  200. office365/sharepoint/sites/siteprovisioninginfoprovider.py +13 -0
  201. office365/sharepoint/tenant/administration/datagovernance/insight_client.py +292 -23
  202. office365/sharepoint/tenant/administration/ddiadapter.py +97 -0
  203. office365/sharepoint/tenant/administration/internal/aad/collection.py +20 -0
  204. office365/sharepoint/tenant/administration/internal/aad/permission_grant.py +0 -19
  205. office365/sharepoint/tenant/administration/internal/appservice/permission_request.py +72 -1
  206. office365/sharepoint/tenant/administration/internal/appservice/principal.py +1 -3
  207. office365/sharepoint/tenant/administration/odb_license_enforcement_service.py +26 -0
  208. office365/sharepoint/tenant/administration/sites/properties.py +564 -6
  209. office365/sharepoint/tenant/administration/spdatagovernancesarrestapiclient.py +60 -1
  210. office365/sharepoint/tenant/insights/reports/manager.py +24 -12
  211. office365/sharepoint/tenant/management/office365_tenant.py +451 -2
  212. office365/sharepoint/tenantadministration/catalog_management_collection.py +69 -0
  213. office365/sharepoint/tenantadministration/catalog_management_custom_properties_collection.py +237 -0
  214. office365/sharepoint/tenantadministration/catalog_management_restricted_content_discovery_collection.py +56 -0
  215. office365/sharepoint/tenantadministration/sp_data_governance_client_exception.py +7 -0
  216. office365/sharepoint/thresholds.py +529 -0
  217. office365/sharepoint/translation/jobs/job.py +72 -2
  218. office365/sharepoint/translation/jobs/status.py +47 -9
  219. office365/sharepoint/translation/sync_translator.py +15 -2
  220. office365/sharepoint/utilities/move_copy_util.py +2 -1
  221. office365/sharepoint/viva/connections/page.py +27 -0
  222. office365/sharepoint/viva/home.py +38 -0
  223. office365/sharepoint/viva/learningmanager.py +9 -0
  224. office365/sharepoint/viva/vivaresources.py +60 -0
  225. office365/sharepoint/webhooks/subscription.py +0 -11
  226. office365/sharepoint/webparts/definitions/definition.py +13 -0
  227. office365/sharepoint/webparts/limited_manager.py +29 -7
  228. office365/sharepoint/webs/context_web_information.py +14 -9
  229. office365/sharepoint/webs/templates/__init__.py +11 -0
  230. office365/sharepoint/webs/templates/type.py +74 -15
  231. office365/sharepoint/workflow/definition.py +6 -0
  232. office365/sharepoint/workflow/deployment_service.py +155 -0
  233. office365/sharepoint/workflow/instances/service.py +33 -0
  234. office365/sharepoint/workflow/interopservice.py +68 -0
  235. office365/sharepoint/workflow/messagingservice.py +24 -0
  236. office365/sharepoint/workflow/servicesmanager.py +41 -2
  237. office365/sharepoint/workflow/subscriptions/service.py +67 -0
  238. office365/sharepoint/workflow/subscriptions/subscription.py +31 -0
  239. office365/sharepoint/workflow/task.py +425 -0
  240. office365_rest_python_client-3.2.0.dist-info/METADATA +605 -0
  241. {office365_rest_python_client-3.1.0.dist-info → office365_rest_python_client-3.2.0.dist-info}/RECORD +242 -185
  242. office365/migration/_util.py +0 -73
  243. office365/sharepoint/listitems/caml/types.py +0 -70
  244. office365_rest_python_client-3.1.0.dist-info/METADATA +0 -475
  245. {office365_rest_python_client-3.1.0.dist-info → office365_rest_python_client-3.2.0.dist-info}/WHEEL +0 -0
@@ -1,7 +1,18 @@
1
1
  from __future__ import annotations
2
2
 
3
- from os import PathLike
4
- from typing import IO, TYPE_CHECKING, Any, Callable, Dict, Generic, Iterator, List, Optional, Type, Union, cast
3
+ import warnings
4
+ from typing import (
5
+ TYPE_CHECKING,
6
+ Any,
7
+ Callable,
8
+ Dict,
9
+ Generic,
10
+ Iterator,
11
+ List,
12
+ Optional,
13
+ Type,
14
+ cast,
15
+ )
5
16
 
6
17
  from typing_extensions import Self
7
18
 
@@ -15,7 +26,6 @@ from office365.runtime.types.event_handler import EventHandler
15
26
  from office365.runtime.types.exceptions import NotFoundException
16
27
 
17
28
  if TYPE_CHECKING:
18
- from office365.runtime.converters.dataframe import DataFrameResult
19
29
  from office365.runtime.operations import ProgressCallback
20
30
 
21
31
 
@@ -28,8 +38,15 @@ class ClientObjectCollection(ClientObject, Generic[ClientObjectT]):
28
38
  - LINQ-style query operations (filter, order, skip, top)
29
39
  - Type-safe object creation and manipulation
30
40
  - Event-based loading notifications
41
+
42
+ Subclasses backed by a SharePoint list view (list items, files, folders) set
43
+ ``_list_view_threshold`` so a single-shot load that reaches the threshold warns
44
+ instead of silently returning a truncated page.
31
45
  """
32
46
 
47
+ _list_view_threshold: int | None = None
48
+ _truncation_hint: str = "page it with get_all(page_size=2000)"
49
+
33
50
  def __init__(
34
51
  self,
35
52
  context: ClientRuntimeContext,
@@ -55,6 +72,9 @@ class ClientObjectCollection(ClientObject, Generic[ClientObjectT]):
55
72
  self._current_pos: int | None = None
56
73
  self._next_request_url: str | None = None
57
74
  self._page_headers: dict[str, str] | None = None
75
+ self._server_paged: bool = False
76
+ self._next_page: Callable[[], Any] | None = None
77
+ self._truncation_warned: bool = False
58
78
  self._parent = parent
59
79
 
60
80
  def clear_state(self) -> Self:
@@ -69,6 +89,7 @@ class ClientObjectCollection(ClientObject, Generic[ClientObjectT]):
69
89
  """
70
90
  if not self._paged_mode:
71
91
  self._data = []
92
+ self._server_paged = False
72
93
  self._next_request_url = None
73
94
  self._page_headers = None
74
95
  self._current_pos = len(self._data)
@@ -119,6 +140,7 @@ class ClientObjectCollection(ClientObject, Generic[ClientObjectT]):
119
140
  if name == "__nextLinkUrl":
120
141
  if isinstance(value, str):
121
142
  self._next_request_url = value
143
+ self._server_paged = True
122
144
  else:
123
145
  raise ValueError(f"Invalid value for __nextLinkUrl: expected a string {value}")
124
146
  else:
@@ -139,14 +161,33 @@ class ClientObjectCollection(ClientObject, Generic[ClientObjectT]):
139
161
  """
140
162
  client_object._parent_collection = self
141
163
  self._data.append(client_object)
164
+ self._maybe_warn_truncated()
142
165
  return self
143
166
 
167
+ def _maybe_warn_truncated(self) -> None:
168
+ """Warn once when a single-shot load fills the SharePoint list view threshold.
169
+
170
+ A non-paged collection that returns the threshold number of rows may have
171
+ been silently trimmed by SharePoint, so point the caller at the paged API.
172
+ """
173
+ threshold = self._list_view_threshold
174
+ if threshold is None or self._truncation_warned or self._paged_mode:
175
+ return
176
+ if len(self._data) >= threshold:
177
+ self._truncation_warned = True
178
+ warnings.warn(
179
+ f"This collection returned {len(self._data):,} items in a single request and may be "
180
+ f"truncated at the SharePoint list view threshold ({threshold:,}); {self._truncation_hint}.",
181
+ UserWarning,
182
+ stacklevel=3,
183
+ )
184
+
144
185
  def remove_child(self, client_object: ClientObjectT) -> Self:
145
186
  """
146
187
  Remove an item from the collection.
147
188
 
148
189
  Args:
149
- client_object: The item to remove
190
+ client_object: The item to remove from the collection
150
191
 
151
192
  Returns:
152
193
  self: Supports fluent method chaining
@@ -154,16 +195,31 @@ class ClientObjectCollection(ClientObject, Generic[ClientObjectT]):
154
195
  self._data = [item for item in self._data if item != client_object]
155
196
  return self
156
197
 
198
+ def clear(self) -> Self:
199
+ """Discard the collection's items, keeping its configuration.
200
+
201
+ Unlike :meth:`clear_state` this only drops the loaded/queued entities
202
+ (``_data``) — used by streaming imports to keep memory bounded between
203
+ chunks.
204
+
205
+ Returns:
206
+ self: Supports fluent method chaining
207
+ """
208
+ self._data = []
209
+ return self
210
+
157
211
  def __iter__(self) -> Iterator[ClientObjectT]:
158
212
  """Iterate through all items, automatically handling paged results."""
159
213
  yield from self._data
160
214
 
161
- # Handle server-side paging
215
+ # Handle server-side paging: follow __next, yielding each new page only
216
+ # (a local position, so it stays correct regardless of _current_pos).
162
217
  if self._paged_mode:
218
+ position = len(self._data)
163
219
  while self.has_next:
164
220
  self._get_next().execute_query()
165
- next_items = self._data[self._current_pos :]
166
- yield from next_items
221
+ yield from self._data[position:]
222
+ position = len(self._data)
167
223
 
168
224
  def __len__(self) -> int:
169
225
  """Get the current number of loaded items in the collection."""
@@ -318,244 +374,6 @@ class ClientObjectCollection(ClientObject, Generic[ClientObjectT]):
318
374
  self.paged(page_size, page_loaded).get().after_execute(_page_loaded)
319
375
  return self
320
376
 
321
- def to_csv(self, file: IO[str]) -> Self:
322
- """Export collection items to CSV using ``.select()`` and ``.expand()``.
323
-
324
- Plain select fields (e.g. ``"displayName"``) produce one column.
325
- Dotted select fields (e.g. ``"members/displayName"``) walk into an
326
- expanded navigation property — one CSV row is emitted per child item.
327
-
328
- Usage:
329
- >>> client.teams.get_all() \\
330
- ... .select(["displayName", "members/displayName"]) \\
331
- ... .expand(["members"]) \\
332
- ... .to_csv(f) \\
333
- ... .execute_query()
334
- """
335
- from office365.runtime.converters.csv_writer import write_csv
336
-
337
- return self.after_execute(lambda _: write_csv(self, file))
338
-
339
- def from_csv(
340
- self,
341
- file: IO[str],
342
- delimiter: str = ",",
343
- progress: "ProgressCallback | None" = None,
344
- ) -> Self:
345
- """Import CSV rows by queueing a create per row (deferred).
346
-
347
- The symmetric counterpart of ``to_csv``: parsing happens immediately and
348
- each record becomes an entity of this collection's item type (via
349
- ``create_typed_object`` + the shared ``set_property`` coercion), queued
350
- for creation. The creates run on ``execute_query()``.
351
-
352
- Args:
353
- file: A readable CSV stream.
354
- delimiter: CSV field delimiter.
355
- progress: Optional hook invoked per imported row as its create
356
- completes during ``execute_query()``.
357
-
358
- Usage:
359
- >>> client.users.from_csv(f).execute_query()
360
- >>> client.users.from_csv(f, progress=my_callback).execute_query()
361
- """
362
- from office365.runtime.converters.csv_reader import read_csv_records
363
-
364
- return self.from_records(read_csv_records(file, delimiter), progress=progress)
365
-
366
- def from_json(self, records: List[dict], progress: "ProgressCallback | None" = None) -> Self:
367
- """Import JSON records (``to_json`` output) by queueing a create per record.
368
-
369
- Like ``from_csv`` but takes a list of dicts instead of a file. Deferred
370
- until ``execute_query()``.
371
-
372
- Args:
373
- records: Plain dict records to import.
374
- progress: Optional hook invoked per record as its create completes
375
- during ``execute_query()``.
376
-
377
- Usage:
378
- >>> client.users.from_json(client.users.to_json()).execute_query()
379
- """
380
- return self.from_records(records, progress=progress)
381
-
382
- def to_records(self) -> List[Dict[str, Any]]:
383
- """Project loaded items into plain dict records — the neutral export form.
384
-
385
- Same projection as ``to_csv``/``to_dataframe``: ``.select()``/``.expand()``
386
- columns, one record per expanded child item, native JSON-safe values.
387
- Every format adapter (``to_csv``, ``to_ndjson``, ``to_excel``,
388
- ``to_dataframe``) builds on this projection.
389
-
390
- Unlike ``to_csv`` this returns a value directly, so load first:
391
-
392
- >>> records = client.users.get_all() \\
393
- ... .select(["displayName", "mail"]) \\
394
- ... .execute_query() \\
395
- ... .to_records()
396
-
397
- Note: this is **data export** — for OData *payload* serialization (request
398
- bodies with type metadata) use :meth:`to_json`.
399
- """
400
- from office365.runtime.converters.records import iter_records
401
-
402
- return iter_records(self)
403
-
404
- def from_records(self, records: List[dict], progress: "ProgressCallback | None" = None) -> Self:
405
- """Import plain dict records by queueing a create per record (deferred).
406
-
407
- The neutral import counterpart of :meth:`to_records`. Every import
408
- adapter (``from_csv``, ``from_ndjson``, ``from_excel``, ``from_json``,
409
- ``from_dataframe``) routes through here: records are normalized to the
410
- item type (dotted keys re-nested, ``"; "`` collections split, unknown
411
- columns skipped) and each becomes an entity queued for creation, which
412
- runs on ``execute_query()``.
413
-
414
- Args:
415
- records: Plain dict records to import.
416
- progress: Optional hook invoked per queued create as it completes
417
- during ``execute_query()`` (a ``Progress`` snapshot per record).
418
-
419
- Usage:
420
- >>> client.users.from_records(records).execute_query()
421
- >>> client.users.from_records(records, progress=my_callback).execute_query()
422
- """
423
- from office365.runtime.converters.csv_reader import coerce_records
424
-
425
- return self._import_records(coerce_records(self._item_type, records), progress=progress)
426
-
427
- def to_dataframe(self) -> "DataFrameResult":
428
- """Build a pandas DataFrame from the loaded items.
429
-
430
- Returns a ``DataFrameResult`` whose ``.value`` holds the DataFrame after
431
- ``execute_query()`` — fully deferred, so chain it like ``to_csv``:
432
-
433
- >>> df = client.users.get_all() \\
434
- ... .select(["displayName", "mail"]) \\
435
- ... .to_dataframe() \\
436
- ... .execute_query() \\
437
- ... .value
438
-
439
- Requires the optional dependency (``pip install
440
- office365-rest-python-client[pandas]``). The projection is the same as
441
- ``to_csv``: ``.select()``/``.expand()`` columns, one row per expanded
442
- child item.
443
- """
444
- from office365.runtime.converters.dataframe import DataFrameResult, require_pandas, write_dataframe
445
-
446
- require_pandas() # fail fast on a missing optional dependency
447
- result = DataFrameResult(self.context)
448
- self.after_execute(lambda _: write_dataframe(self, result))
449
- return result
450
-
451
- def from_dataframe(self, df, progress: "ProgressCallback | None" = None) -> Self:
452
- """Import a pandas DataFrame by queueing a create per row (deferred).
453
-
454
- The symmetric counterpart of ``to_dataframe``: each row is parsed
455
- immediately into an entity and queued for creation, which runs on
456
- ``execute_query()``.
457
-
458
- Args:
459
- df: A pandas DataFrame.
460
- progress: Optional hook invoked per row as its create completes
461
- during ``execute_query()``.
462
-
463
- Usage:
464
- >>> client.users.from_dataframe(df).execute_query()
465
- >>> client.users.from_dataframe(df, progress=my_callback).execute_query()
466
- """
467
- from office365.runtime.converters.dataframe import read_dataframe
468
-
469
- return self.from_records(read_dataframe(df), progress=progress)
470
-
471
- def to_ndjson(self, file: IO[str]) -> Self:
472
- """Export loaded items as NDJSON (JSON Lines) — one record per line.
473
-
474
- Deferred like ``to_csv`` — the records are written on ``execute_query()``:
475
-
476
- >>> client.users.get_all().select(["displayName", "mail"]).to_ndjson(f).execute_query()
477
- """
478
- from office365.runtime.converters.ndjson import write_ndjson
479
- from office365.runtime.converters.records import iter_records
480
-
481
- return self.after_execute(lambda _: write_ndjson(iter_records(self), file))
482
-
483
- def from_ndjson(self, file: IO[str], progress: "ProgressCallback | None" = None) -> Self:
484
- """Import NDJSON (JSON Lines) by queueing a create per line (deferred).
485
-
486
- The symmetric counterpart of ``to_ndjson``:
487
-
488
- >>> client.users.from_ndjson(f).execute_query()
489
- """
490
- from office365.runtime.converters.ndjson import read_ndjson
491
-
492
- return self.from_records(read_ndjson(file), progress=progress)
493
-
494
- def to_json_file(self, file: IO[str]) -> Self:
495
- """Export loaded items as a JSON array file.
496
-
497
- Deferred like ``to_csv`` — the records are written on ``execute_query()``.
498
- Note this is the file format; :meth:`to_json` is OData *payload*
499
- serialization for request bodies:
500
-
501
- >>> client.users.get_all().select(["displayName", "mail"]).to_json_file(f).execute_query()
502
- """
503
- from office365.runtime.converters.json_file import write_json
504
- from office365.runtime.converters.records import iter_records
505
-
506
- return self.after_execute(lambda _: write_json(iter_records(self), file))
507
-
508
- def from_json_file(self, file: IO[str], progress: "ProgressCallback | None" = None) -> Self:
509
- """Import a JSON array file by queueing a create per record (deferred).
510
-
511
- The symmetric counterpart of ``to_json_file``:
512
-
513
- >>> client.users.from_json_file(f).execute_query()
514
- """
515
- from office365.runtime.converters.json_file import read_json
516
-
517
- return self.from_records(read_json(file), progress=progress)
518
-
519
- def to_excel(self, path: Union[str, PathLike]) -> Self:
520
- """Export loaded items to an Excel (.xlsx) worksheet.
521
-
522
- Deferred like ``to_csv`` — the workbook is written on ``execute_query()``.
523
- Requires the optional dependency (``pip install
524
- office365-rest-python-client[excel]``):
525
-
526
- >>> client.users.get_all().select(["displayName", "mail"]).to_excel("users.xlsx").execute_query()
527
- """
528
- from office365.runtime.converters.excel import write_excel
529
- from office365.runtime.converters.records import iter_records
530
-
531
- return self.after_execute(lambda _: write_excel(iter_records(self), path))
532
-
533
- def from_excel(self, path: Union[str, PathLike], progress: "ProgressCallback | None" = None) -> Self:
534
- """Import an Excel (.xlsx) worksheet by queueing a create per row (deferred).
535
-
536
- The symmetric counterpart of ``to_excel`` (reads the first worksheet):
537
-
538
- >>> client.users.from_excel("users.xlsx").execute_query()
539
- """
540
- from office365.runtime.converters.excel import read_excel
541
-
542
- return self.from_records(read_excel(path), progress=progress)
543
-
544
- def _import_records(self, records: List[dict], progress: "ProgressCallback | None" = None) -> Self:
545
- """Queue a create per record, appending the pending entities to this collection."""
546
- from office365.runtime.operations import query_progress_hook
547
- from office365.runtime.queries.create_entity import CreateEntityQuery
548
-
549
- hook = query_progress_hook(len(records), progress) if callable(progress) else None
550
- for record in records:
551
- entity = self.create_typed_object(record)
552
- self.add_child(entity)
553
- qry = CreateEntityQuery(self, entity, entity)
554
- self.context.add_query(qry)
555
- if hook is not None:
556
- self.context.after_execute(hook)
557
- return self
558
-
559
377
  def _can_offset_next(self) -> bool:
560
378
  """Whether the next page can be fetched with a client-driven offset request.
561
379
 
@@ -567,6 +385,10 @@ class ClientObjectCollection(ClientObject, Generic[ClientObjectT]):
567
385
  """
568
386
  if not self._paged_mode or not self._page_size or self._next_request_url is not None:
569
387
  return False
388
+ if self._server_paged:
389
+ # The server drives paging (``__next``/``$skiptoken``); a ``$skip``
390
+ # fallback would conflict with the token on the final page.
391
+ return False
570
392
  from office365.runtime.odata.v3.json_light_format import JsonLightFormat
571
393
 
572
394
  json_format = getattr(self.context.pending_request(), "json_format", None)
@@ -577,6 +399,11 @@ class ClientObjectCollection(ClientObject, Generic[ClientObjectT]):
577
399
  def _get_next(self) -> Self:
578
400
  """Submit a request to retrieve next collection of items"""
579
401
 
402
+ if self._next_page is not None:
403
+ # Custom loader (e.g. CAML paging via ListItemCollectionPosition).
404
+ self._next_page()
405
+ return self
406
+
580
407
  _PAGE_EXCLUDED_HEADERS = ("authorization", "content-length")
581
408
 
582
409
  def _construct_request(request: RequestOptions) -> None:
@@ -657,6 +484,9 @@ class ClientObjectCollection(ClientObject, Generic[ClientObjectT]):
657
484
  or when a SharePoint collection loaded a full explicit page and the next
658
485
  one can still be fetched via ``$skip``.
659
486
  """
487
+ if self._next_page is not None:
488
+ # Custom loader (e.g. CAML): more pages while the last one was full.
489
+ return self._paged_mode and self._page_size is not None and len(self.current_page) == self._page_size
660
490
  return self._next_request_url is not None or self._can_offset_next()
661
491
 
662
492
  @property
@@ -9,15 +9,18 @@ from typing_extensions import Self
9
9
 
10
10
  from office365.runtime.client_request_exception import ClientRequestException
11
11
  from office365.runtime.http.request_options import RequestOptions
12
+ from office365.runtime.http.throttling import RateLimiter
12
13
  from office365.runtime.queries.client_query import ClientQuery
13
14
  from office365.runtime.transport.base import BaseTransport
14
15
  from office365.runtime.transport.requests_transport import RequestsTransport
16
+ from office365.runtime.transport.throttled_transport import ThrottledTransport
15
17
  from office365.runtime.types.event_handler import EventHandler
16
18
 
17
19
 
18
20
  class ClientRequest(ABC):
19
21
  def __init__(self, transport: BaseTransport | None = None):
20
22
  self._transport = transport or RequestsTransport()
23
+ self._rate_limiter: RateLimiter | None = None
21
24
  self.beforeExecute: EventHandler[[RequestOptions]] = EventHandler()
22
25
  self.afterExecute: EventHandler[[Response]] = EventHandler()
23
26
  self.onError: EventHandler[[ClientRequestException]] = EventHandler()
@@ -31,6 +34,11 @@ class ClientRequest(ABC):
31
34
  def transport(self, value: BaseTransport) -> None:
32
35
  self._transport = value
33
36
 
37
+ @property
38
+ def rate_limiter(self) -> RateLimiter | None:
39
+ """The shared rate limiter pacing this request's transport, if any."""
40
+ return self._rate_limiter
41
+
34
42
  def with_transport(
35
43
  self,
36
44
  proxies: dict[str, str] | None = None,
@@ -41,7 +49,8 @@ class ClientRequest(ABC):
41
49
  """Configure the HTTP transport (proxy, SSL, timeout, custom session).
42
50
 
43
51
  All parameters are optional. Per-request values (``request.proxies``,
44
- ``request.verify``) take precedence over transport-level values.
52
+ ``request.verify``) take precedence over transport-level values. A rate
53
+ limiter configured with :meth:`with_rate_limit` is preserved.
45
54
 
46
55
  Args:
47
56
  proxies: Proxy URLs (e.g. ``{"https": "http://proxy:8080"}``)
@@ -58,6 +67,46 @@ class ClientRequest(ABC):
58
67
  verify=True if verify is None else verify,
59
68
  timeout=timeout,
60
69
  )
70
+ if self._rate_limiter is not None:
71
+ self._transport = ThrottledTransport(self._transport, self._rate_limiter)
72
+ return self
73
+
74
+ def with_rate_limit(self, health_threshold: int = 80, min_interval: float = 0.0) -> Self:
75
+ """Pace every request of this request through a new shared rate limiter.
76
+
77
+ Control plane: wraps the transport so the whole fleet — including the
78
+ parallel batches dispatched by a context's ``execute_batch`` — is gated
79
+ by ``Retry-After`` / ``X-SharePointHealthScore`` as a group. Unlike the
80
+ ``after_execute`` observation hooks (``rate_limit_hook`` /
81
+ ``throttle_guard``), this acts on the signals without affecting error
82
+ propagation. Re-applying replaces the limiter (the underlying transport
83
+ is not stacked).
84
+
85
+ Args:
86
+ health_threshold: Health score at/above which the group paces.
87
+ min_interval: Minimum pause applied on a high health score (seconds).
88
+
89
+ Returns:
90
+ Self: Supports method chaining
91
+ """
92
+ return self.with_rate_limiter(RateLimiter(health_threshold=health_threshold, min_interval=min_interval))
93
+
94
+ def with_rate_limiter(self, limiter: RateLimiter) -> Self:
95
+ """Pace every request through a caller-provided shared rate limiter.
96
+
97
+ Used to share one limiter across a fleet of contexts/clones (see
98
+ :meth:`RateLimiter.bind`). Re-applying replaces the limiter without
99
+ stacking wrappers.
100
+
101
+ Args:
102
+ limiter: The shared :class:`RateLimiter` gating the fleet.
103
+
104
+ Returns:
105
+ Self: Supports method chaining
106
+ """
107
+ inner = self._transport.inner if isinstance(self._transport, ThrottledTransport) else self._transport
108
+ self._rate_limiter = limiter
109
+ self._transport = ThrottledTransport(inner, limiter)
61
110
  return self
62
111
 
63
112
  @property
@@ -104,6 +153,22 @@ class ClientRequest(ABC):
104
153
  once: bool = True,
105
154
  condition: Optional[Callable[[], bool]] = None,
106
155
  ) -> Self:
156
+ """Register a handler that **handles** (and therefore swallows) an error.
157
+
158
+ Once any handler is registered, :meth:`execute_query` dispatches the
159
+ error to it and does not re-raise — this powers the library's
160
+ create-if-not-exists flows. It is a control hook, not an observation
161
+ hook: do not attach a limiter or a pure reporter here, or request
162
+ failures will be silently suppressed (breaking retries). Use
163
+ ``after_execute`` / ``throttle_guard`` for observation.
164
+
165
+ Args:
166
+ action: Callback receiving the error.
167
+ once: Remove the handler after it fires (default True).
168
+ condition: Optional predicate; when it returns False the handler is
169
+ skipped (but still counts as a registered error handler).
170
+ """
171
+
107
172
  def _process_error(e: ClientRequestException):
108
173
  matches = condition is None or condition()
109
174
  if matches:
@@ -174,6 +239,17 @@ class ClientRequest(ABC):
174
239
  except HTTPError:
175
240
  raise ClientRequestException.from_response(response) from None
176
241
 
242
+ def _observe_throttle(self, response: Optional[Response]) -> None:
243
+ """Feed a sub-response into the transport's shared rate limiter, if any.
244
+
245
+ Batch responses carry per-sub-request throttling signals that the
246
+ transport (which only sees the outer ``200``) cannot observe, so the
247
+ batch loop surfaces each sub-response here.
248
+ """
249
+ limiter = getattr(self._transport, "limiter", None)
250
+ if limiter is not None:
251
+ limiter.observe(response)
252
+
177
253
  def execute_request_direct(self, request: RequestOptions) -> Response:
178
254
  """Execute the client request"""
179
255
  self.beforeExecute(request)