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.
- office365/communications/callrecords/call_record.py +18 -0
- office365/communications/calls/call.py +3 -0
- office365/communications/presences/presence.py +3 -0
- office365/communications/quotas.py +66 -0
- office365/communications/virtualevents/virtual_event.py +3 -0
- office365/directory/domains/domain.py +3 -0
- office365/directory/licenses/subscribed_sku.py +3 -0
- office365/directory/objects/object.py +3 -0
- office365/directory/quotas.py +34 -0
- office365/entity_collection.py +2 -2
- office365/graph_client.py +46 -2
- office365/limits.py +66 -0
- office365/migration/__init__.py +6 -1
- office365/migration/adapters/__init__.py +14 -4
- office365/migration/adapters/filesystem.py +4 -1
- office365/migration/assessment/export.py +5 -5
- office365/migration/assessment/issue.py +19 -0
- office365/migration/assessment/report.py +11 -0
- office365/migration/assessment/scanners/base.py +37 -49
- office365/migration/base.py +33 -8
- office365/migration/checkpoint.py +1 -1
- office365/migration/outlook/assessor.py +4 -4
- office365/migration/package/__init__.py +82 -0
- office365/migration/package/builder.py +603 -0
- office365/migration/package/crypto.py +54 -0
- office365/migration/package/manifest_xml.py +495 -0
- office365/migration/package/staging.py +180 -0
- office365/migration/report.py +7 -2
- office365/migration/report_io.py +79 -0
- office365/migration/runner.py +117 -10
- office365/migration/server_job.py +126 -14
- office365/migration/session.py +132 -49
- office365/migration/settings.py +67 -0
- office365/migration/sharepoint/__init__.py +2 -0
- office365/migration/sharepoint/adapters.py +207 -41
- office365/migration/sharepoint/assessor.py +45 -9
- office365/migration/sharepoint/options.py +57 -0
- office365/migration/sharepoint/package_target.py +209 -0
- office365/migration/sharepoint/registry.py +22 -0
- office365/migration/sharepoint/resolvers.py +96 -0
- office365/migration/sharepoint/scanners/__init__.py +29 -0
- office365/migration/sharepoint/scanners/browser_file_handling.py +55 -0
- office365/migration/sharepoint/scanners/checked_out_files.py +57 -0
- office365/migration/sharepoint/scanners/file_versions.py +56 -0
- office365/migration/sharepoint/scanners/files.py +5 -2
- office365/migration/sharepoint/scanners/large_excel_files.py +56 -0
- office365/migration/sharepoint/scanners/large_lists.py +58 -0
- office365/migration/sharepoint/scanners/large_sites.py +4 -3
- office365/migration/sharepoint/scanners/long_onedrive_urls.py +52 -0
- office365/migration/sharepoint/scanners/lookup.py +38 -0
- office365/migration/sharepoint/scanners/paths.py +9 -4
- office365/migration/sharepoint/scanners/permissions.py +20 -2
- office365/migration/sharepoint/scanners/smat.py +51 -0
- office365/migration/sharepoint/scanners/summary.py +1 -0
- office365/migration/sharepoint/scanners/thicket_folders.py +38 -0
- office365/migration/sharepoint/scanners/unsupported_site_templates.py +90 -0
- office365/migration/sharepoint/tenant_assessor.py +5 -3
- office365/migration/sharepoint/transfer.py +4 -2
- office365/migration/tasks.py +121 -0
- office365/migration/validators.py +2 -19
- office365/onedrive/driveitems/driveItem.py +116 -12
- office365/onedrive/termstore/groups/collection.py +2 -9
- office365/onedrive/termstore/sets/collection.py +2 -9
- office365/onedrive/termstore/terms/collection.py +3 -10
- office365/onedrive/workbooks/workbook.py +7 -0
- office365/outlook/contacts/folders/collection.py +4 -13
- office365/runtime/auth/authentication_context.py +5 -0
- office365/runtime/client_object.py +18 -0
- office365/runtime/client_object_collection.py +75 -245
- office365/runtime/client_request.py +77 -1
- office365/runtime/client_request_exception.py +127 -39
- office365/runtime/client_runtime_context.py +79 -8
- office365/runtime/converters/csv_writer.py +10 -4
- office365/runtime/converters/dataframe.py +84 -4
- office365/runtime/converters/json_file.py +12 -3
- office365/runtime/converters/parquet.py +67 -0
- office365/runtime/converters/records.py +31 -12
- office365/runtime/converters/registry.py +185 -0
- office365/runtime/converters/scalars.py +31 -0
- office365/runtime/converters/sql.py +69 -0
- office365/runtime/converters/streamers.py +105 -0
- office365/runtime/converters/streams.py +42 -0
- office365/runtime/converters/upsert.py +101 -0
- office365/runtime/exceptions.py +105 -0
- office365/runtime/http/prefer.py +26 -0
- office365/runtime/http/throttling.py +258 -56
- office365/runtime/imports.py +571 -0
- office365/runtime/limits.py +455 -0
- office365/runtime/odata/request.py +11 -2
- office365/runtime/odata/type.py +1 -0
- office365/runtime/odata/v3/batch_request.py +4 -2
- office365/runtime/odata/v4/batch_request.py +22 -7
- office365/runtime/operations.py +29 -0
- office365/runtime/parallel.py +12 -14
- office365/runtime/paths/v4/entity.py +2 -2
- office365/runtime/queries/batch.py +18 -1
- office365/runtime/queries/get_or_create.py +109 -0
- office365/runtime/record_collection.py +443 -0
- office365/runtime/retry.py +92 -26
- office365/runtime/transport/base.py +6 -1
- office365/runtime/transport/requests_transport.py +4 -0
- office365/runtime/transport/throttled_transport.py +67 -0
- office365/runtime/verification.py +40 -0
- office365/sharepoint/activities/logger.py +118 -8
- office365/sharepoint/administration/analytics/usage_service.py +15 -7
- office365/sharepoint/administration/sitemove/service.py +791 -8
- office365/sharepoint/alerts/alert.py +79 -0
- office365/sharepoint/apps/app_collection.py +25 -0
- office365/sharepoint/apps/launcher.py +16 -0
- office365/sharepoint/authpolicy/tenant/compliancereport.py +5 -0
- office365/sharepoint/businessdata/app_bdc_catalog.py +135 -0
- office365/sharepoint/businessdata/entityfieldvaluedictionary.py +65 -0
- office365/sharepoint/businessdata/entityidentifier.py +23 -0
- office365/sharepoint/businessdata/entityinstance.py +52 -0
- office365/sharepoint/businessdata/entityview.py +31 -0
- office365/sharepoint/businessdata/externalsubscriptionstore.py +9 -0
- office365/sharepoint/businessdata/typedescriptor.py +37 -0
- office365/sharepoint/client_context.py +40 -23
- office365/sharepoint/clientsidecomponent/hostedapps/app.py +21 -0
- office365/sharepoint/clientsidecomponent/hostedapps/manager.py +16 -0
- office365/sharepoint/comments/comment.py +84 -2
- office365/sharepoint/contentcenter/machinelearning/models/model.py +0 -23
- office365/sharepoint/contentcenter/machinelearning/publications/publication.py +0 -9
- office365/sharepoint/contentcenter/machinelearning/samples/sample.py +0 -14
- office365/sharepoint/contentcenter/machinelearning/workitems/item.py +0 -80
- office365/sharepoint/contentsolution/template_api.py +148 -0
- office365/sharepoint/contenttypes/collection.py +32 -10
- office365/sharepoint/convergence/addnoliveblobsinodcservicestateapi.py +9 -0
- office365/sharepoint/convergence/consumer_permissions.py +15 -0
- office365/sharepoint/convergence/migration_complete_state_api.py +25 -0
- office365/sharepoint/convergence/odcmetadatacleanedupapi.py +9 -0
- office365/sharepoint/documentmanagement/document_id.py +12 -0
- office365/sharepoint/entity.py +11 -2
- office365/sharepoint/entity_collection.py +2 -2
- office365/sharepoint/exceptions.py +194 -0
- office365/sharepoint/fields/builtin_field_id.py +45 -34
- office365/sharepoint/fields/builtin_field_name.py +123 -0
- office365/sharepoint/fields/coercion.py +131 -0
- office365/sharepoint/fields/collection.py +54 -37
- office365/sharepoint/fields/field.py +17 -0
- office365/sharepoint/fields/name.py +46 -2
- office365/sharepoint/files/collection.py +7 -1
- office365/sharepoint/files/file.py +54 -8
- office365/sharepoint/files/versions/version.py +38 -13
- office365/sharepoint/folders/collection.py +4 -0
- office365/sharepoint/folders/folder.py +91 -21
- office365/sharepoint/htmlpages/html_page.py +16 -0
- office365/sharepoint/htmlpages/html_page_service.py +19 -0
- office365/sharepoint/ir/migration.py +9 -0
- office365/sharepoint/listitems/caml/__init__.py +31 -0
- office365/sharepoint/listitems/caml/builder.py +129 -0
- office365/sharepoint/listitems/caml/expressions.py +424 -0
- office365/sharepoint/listitems/caml/fields.py +68 -0
- office365/sharepoint/listitems/caml/guard.py +63 -0
- office365/sharepoint/listitems/caml/query.py +71 -2
- office365/sharepoint/listitems/caml/values.py +113 -0
- office365/sharepoint/listitems/collection.py +21 -20
- office365/sharepoint/listitems/upsert.py +75 -0
- office365/sharepoint/lists/collection.py +29 -22
- office365/sharepoint/lists/exporter.py +4 -3
- office365/sharepoint/lists/list.py +397 -39
- office365/sharepoint/lists/smart_wiki_library_utility.py +58 -0
- office365/sharepoint/logger/export.py +12 -0
- office365/sharepoint/marketplace/app_metadata.py +97 -1
- office365/sharepoint/marketplace/appstatequeryjobdefinition.py +9 -0
- office365/sharepoint/marketplace/corporatecuratedgallery/carddesigns/card_designs.py +35 -0
- office365/sharepoint/marketplace/sitecollection/appcatalog/allowed_item.py +5 -0
- office365/sharepoint/marketplace/tenant/appcatalog/accessor.py +107 -30
- office365/sharepoint/microfeed/data.py +0 -6
- office365/sharepoint/microservice/manager.py +61 -0
- office365/sharepoint/migrationcenter/common/task.py +193 -4
- office365/sharepoint/migrationcenter/common/task_entity_data.py +1 -1
- office365/sharepoint/migrationcenter/deploy_status.py +13 -0
- office365/sharepoint/migrationcenter/service/properties.py +20 -0
- office365/sharepoint/migrationcenter/service/storage.py +34 -0
- office365/sharepoint/migrationcenter/spflight.py +13 -0
- office365/sharepoint/migrationcenter/urlparseresult.py +77 -0
- office365/sharepoint/multigeo/service/spmultigeoproxy.py +13 -0
- office365/sharepoint/navigation/servicerest.py +127 -0
- office365/sharepoint/oauth/authentication.py +13 -0
- office365/sharepoint/portal/theme_manager.py +120 -8
- office365/sharepoint/principal/users/user.py +10 -0
- office365/sharepoint/publishing/campaign/campaign.py +36 -1
- office365/sharepoint/publishing/feedvideopage.py +0 -7
- office365/sharepoint/publishing/pages/3d/page.py +0 -6
- office365/sharepoint/publishing/pages/page.py +0 -117
- office365/sharepoint/publishing/pages/reposts/repost.py +0 -7
- office365/sharepoint/publishing/pages/topics/topic.py +0 -7
- office365/sharepoint/publishing/subtitlefile.py +0 -40
- office365/sharepoint/quotamanagement/consumer/consumer_quota_info_api.py +14 -0
- office365/sharepoint/quotamanagement/consumer/non_quota_backfill_api.py +23 -0
- office365/sharepoint/quotamanagement/consumer/nonquotamigrationapi.py +9 -0
- office365/sharepoint/quotamanagement/consumer/quotamigrationapi.py +13 -0
- office365/sharepoint/request.py +57 -16
- office365/sharepoint/search/administration/document_crawl_log.py +24 -7
- office365/sharepoint/search/administration/providers/crawl_versions_info.py +29 -0
- office365/sharepoint/search/administration/providers/recycle_bin_info.py +23 -0
- office365/sharepoint/search/administration/providers/site_content_processing_info.py +67 -0
- office365/sharepoint/sites/site.py +976 -65
- office365/sharepoint/sites/siteprovisioninginfoprovider.py +13 -0
- office365/sharepoint/tenant/administration/datagovernance/insight_client.py +292 -23
- office365/sharepoint/tenant/administration/ddiadapter.py +97 -0
- office365/sharepoint/tenant/administration/internal/aad/collection.py +20 -0
- office365/sharepoint/tenant/administration/internal/aad/permission_grant.py +0 -19
- office365/sharepoint/tenant/administration/internal/appservice/permission_request.py +72 -1
- office365/sharepoint/tenant/administration/internal/appservice/principal.py +1 -3
- office365/sharepoint/tenant/administration/odb_license_enforcement_service.py +26 -0
- office365/sharepoint/tenant/administration/sites/properties.py +564 -6
- office365/sharepoint/tenant/administration/spdatagovernancesarrestapiclient.py +60 -1
- office365/sharepoint/tenant/insights/reports/manager.py +24 -12
- office365/sharepoint/tenant/management/office365_tenant.py +451 -2
- office365/sharepoint/tenantadministration/catalog_management_collection.py +69 -0
- office365/sharepoint/tenantadministration/catalog_management_custom_properties_collection.py +237 -0
- office365/sharepoint/tenantadministration/catalog_management_restricted_content_discovery_collection.py +56 -0
- office365/sharepoint/tenantadministration/sp_data_governance_client_exception.py +7 -0
- office365/sharepoint/thresholds.py +529 -0
- office365/sharepoint/translation/jobs/job.py +72 -2
- office365/sharepoint/translation/jobs/status.py +47 -9
- office365/sharepoint/translation/sync_translator.py +15 -2
- office365/sharepoint/utilities/move_copy_util.py +2 -1
- office365/sharepoint/viva/connections/page.py +27 -0
- office365/sharepoint/viva/home.py +38 -0
- office365/sharepoint/viva/learningmanager.py +9 -0
- office365/sharepoint/viva/vivaresources.py +60 -0
- office365/sharepoint/webhooks/subscription.py +0 -11
- office365/sharepoint/webparts/definitions/definition.py +13 -0
- office365/sharepoint/webparts/limited_manager.py +29 -7
- office365/sharepoint/webs/context_web_information.py +14 -9
- office365/sharepoint/webs/templates/__init__.py +11 -0
- office365/sharepoint/webs/templates/type.py +74 -15
- office365/sharepoint/workflow/definition.py +6 -0
- office365/sharepoint/workflow/deployment_service.py +155 -0
- office365/sharepoint/workflow/instances/service.py +33 -0
- office365/sharepoint/workflow/interopservice.py +68 -0
- office365/sharepoint/workflow/messagingservice.py +24 -0
- office365/sharepoint/workflow/servicesmanager.py +41 -2
- office365/sharepoint/workflow/subscriptions/service.py +67 -0
- office365/sharepoint/workflow/subscriptions/subscription.py +31 -0
- office365/sharepoint/workflow/task.py +425 -0
- office365_rest_python_client-3.2.0.dist-info/METADATA +605 -0
- {office365_rest_python_client-3.1.0.dist-info → office365_rest_python_client-3.2.0.dist-info}/RECORD +242 -185
- office365/migration/_util.py +0 -73
- office365/sharepoint/listitems/caml/types.py +0 -70
- office365_rest_python_client-3.1.0.dist-info/METADATA +0 -475
- {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
|
-
|
|
4
|
-
from typing import
|
|
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
|
-
|
|
166
|
-
|
|
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)
|