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
@@ -18,14 +18,16 @@ from collections.abc import Callable
18
18
  from datetime import datetime
19
19
  from typing import TYPE_CHECKING
20
20
 
21
- from office365.migration._util import coerce_int, emit_progress
22
21
  from office365.migration.assessment.containers import ScanContainer
23
22
  from office365.migration.assessment.report import AssessmentReport
24
23
  from office365.migration.assessment.runner import ScanRunner
25
24
  from office365.migration.assessment.scanners import AssessmentOptions
25
+ from office365.migration.sharepoint.options import SharePointAssessmentOptions
26
26
  from office365.migration.sharepoint.registry import sharepoint_scan_pairs
27
27
  from office365.migration.sharepoint.scanners.summary import SiteScanSummary
28
28
  from office365.runtime.client_result import ClientResult
29
+ from office365.runtime.converters.scalars import parse_int
30
+ from office365.runtime.operations import emit_progress
29
31
  from office365.sharepoint.entity import Entity
30
32
 
31
33
  if TYPE_CHECKING:
@@ -48,7 +50,7 @@ class MigrationTenantAssessor(Entity):
48
50
  def __init__(self, tenant: "Tenant", options: AssessmentOptions | None = None) -> None:
49
51
  super().__init__(tenant.context)
50
52
  self._tenant = tenant
51
- self._options = options or AssessmentOptions()
53
+ self._options = options or SharePointAssessmentOptions()
52
54
 
53
55
  def assess(
54
56
  self,
@@ -77,7 +79,7 @@ class MigrationTenantAssessor(Entity):
77
79
  site_url=site.url,
78
80
  owner=site.owner_login_name,
79
81
  storage_bytes=int(storage) if storage is not None else None,
80
- web_count=coerce_int(site.webs_count) or 0,
82
+ web_count=parse_int(site.webs_count) or 0,
81
83
  last_modified=_clean_modified(site.last_content_modified_date),
82
84
  lock_state=site.lock_state,
83
85
  report_impacted_only=True,
@@ -21,11 +21,12 @@ from collections.abc import Callable, Iterable
21
21
  from typing import TYPE_CHECKING, Tuple
22
22
 
23
23
  from office365.runtime.parallel import run_parallel
24
+ from office365.sharepoint.thresholds import Limits
24
25
 
25
26
  if TYPE_CHECKING:
26
27
  from office365.sharepoint.folders.folder import Folder
27
28
 
28
- _DEFAULT_CHUNK_SIZE = 4 * 1024 * 1024
29
+ _DEFAULT_CHUNK_SIZE = Limits.UPLOAD_SESSION_CHUNK.value
29
30
 
30
31
  # runtime-evaluated alias (typing form: the ``|``/``tuple[...]`` operators need 3.9+)
31
32
  Failure = Tuple[str, str] # (dest_path, error)
@@ -68,7 +69,8 @@ def _transfer_files_parallel(
68
69
 
69
70
  parents = {dest.rsplit("/", 1)[0] for dest, _ in files if "/" in dest}
70
71
  if parents:
71
- target_folder.ensure_folders(parents).execute_query()
72
+ target_folder.ensure_folders(parents)
73
+ target_folder.execute_query()
72
74
 
73
75
  failures: list[Failure] = []
74
76
  context_factory = context_factory or (lambda: target_folder.context.clone(target_folder.context.base_url))
@@ -0,0 +1,121 @@
1
+ """Migration tasks — declarative source → target descriptors (SPMT ``Add-SPMTTask``).
2
+
3
+ Mirrors ``Microsoft.SharePoint.MigrationTool.PowerShell``'s task types:
4
+ **FileShare** (a local folder), **SharePoint** (a source site — all lists, or
5
+ selected ones), and **Json**-defined tasks (the ``-JsonDefinition`` format).
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ from dataclasses import dataclass
12
+
13
+ __all__ = ["FILE_SHARE", "SHAREPOINT", "MigrationTask"]
14
+
15
+ FILE_SHARE = "FileShare"
16
+ SHAREPOINT = "SharePoint"
17
+
18
+
19
+ @dataclass
20
+ class MigrationTask:
21
+ """A declarative migration task (source → target mapping)."""
22
+
23
+ kind: str = FILE_SHARE
24
+ file_share_source: str | None = None
25
+ sharepoint_source_site_url: str | None = None
26
+ source_list: str | None = None
27
+ source_list_relative_path: str | None = None
28
+ target_site_url: str | None = None
29
+ target_list: str | None = None
30
+ target_list_relative_path: str | None = None
31
+ migrate_all: bool = False
32
+ migration_type: str = "Content"
33
+
34
+ @classmethod
35
+ def file_share(
36
+ cls,
37
+ source: str,
38
+ target_site_url: str,
39
+ target_list: str,
40
+ target_list_relative_path: str | None = None,
41
+ ) -> "MigrationTask":
42
+ """A file-share task (``Add-SPMTTask -FileShareSource …``)."""
43
+ return cls(
44
+ kind=FILE_SHARE,
45
+ file_share_source=source,
46
+ target_site_url=target_site_url,
47
+ target_list=target_list,
48
+ target_list_relative_path=target_list_relative_path,
49
+ )
50
+
51
+ @classmethod
52
+ def sharepoint(
53
+ cls,
54
+ source_site_url: str,
55
+ target_site_url: str,
56
+ *,
57
+ source_list: str | None = None,
58
+ target_list: str | None = None,
59
+ source_list_relative_path: str | None = None,
60
+ target_list_relative_path: str | None = None,
61
+ migrate_all: bool = False,
62
+ ) -> "MigrationTask":
63
+ """A SharePoint task — ``migrate_all``, or a single ``source_list`` → ``target_list``."""
64
+ return cls(
65
+ kind=SHAREPOINT,
66
+ sharepoint_source_site_url=source_site_url,
67
+ source_list=source_list,
68
+ source_list_relative_path=source_list_relative_path,
69
+ target_site_url=target_site_url,
70
+ target_list=target_list,
71
+ target_list_relative_path=target_list_relative_path,
72
+ migrate_all=migrate_all,
73
+ )
74
+
75
+ @classmethod
76
+ def from_json(cls, definition: str | dict) -> "MigrationTask":
77
+ """Parse an SPMT JSON task definition (``Add-SPMTTask -JsonDefinition``)."""
78
+ data = json.loads(definition) if isinstance(definition, str) else dict(definition)
79
+ if "Tasks" in data: # a {"Tasks": [...]} wrapper → the first task
80
+ tasks = data["Tasks"]
81
+ if not tasks:
82
+ raise ValueError("no tasks in the JSON definition")
83
+ data = tasks[0]
84
+ source = data.get("SourcePath")
85
+ target = data.get("TargetPath")
86
+ lists = (data.get("Items") or {}).get("Lists") or []
87
+ if lists:
88
+ first = lists[0]
89
+ return cls(
90
+ kind=SHAREPOINT,
91
+ sharepoint_source_site_url=source,
92
+ target_site_url=target,
93
+ source_list=first.get("SourceList"),
94
+ target_list=first.get("TargetList"),
95
+ )
96
+ return cls(
97
+ kind=FILE_SHARE,
98
+ file_share_source=source,
99
+ target_site_url=target,
100
+ target_list=data.get("TargetList"),
101
+ target_list_relative_path=data.get("TargetListRelativePath"),
102
+ )
103
+
104
+ def to_json(self) -> str:
105
+ """Serialize to the SPMT JSON task shape (round-trips through :meth:`from_json`)."""
106
+ if self.kind == SHAREPOINT:
107
+ data: dict = {"SourcePath": self.sharepoint_source_site_url, "TargetPath": self.target_site_url}
108
+ if not self.migrate_all:
109
+ data["Items"] = {"Lists": [{"SourceList": self.source_list, "TargetList": self.target_list}]}
110
+ else:
111
+ data = {
112
+ "SourcePath": self.file_share_source,
113
+ "TargetPath": self.target_site_url,
114
+ "TargetList": self.target_list,
115
+ "TargetListRelativePath": self.target_list_relative_path,
116
+ }
117
+ return json.dumps({name: value for name, value in data.items() if value is not None}, indent=2)
118
+
119
+ def label(self) -> str:
120
+ source = self.file_share_source or self.sharepoint_source_site_url or "?"
121
+ return f"{self.kind}: {source} -> {self.target_site_url or '?'}"
@@ -7,29 +7,12 @@ reports any missing, extra, or mismatched items.
7
7
  from __future__ import annotations
8
8
 
9
9
  import random
10
- from dataclasses import dataclass, field
11
10
 
12
11
  from office365.migration.adapters import DataSource, DataTarget
13
12
  from office365.migration.manifest import Manifest
13
+ from office365.runtime.verification import VerificationReport
14
14
 
15
-
16
- @dataclass
17
- class VerificationReport:
18
- source_count: int = 0
19
- target_count: int = 0
20
- checked: int = 0
21
- mismatches: list[str] = field(default_factory=list)
22
-
23
- @property
24
- def ok(self) -> bool:
25
- return self.source_count == self.target_count and not self.mismatches
26
-
27
- def summary(self) -> str:
28
- status = "OK" if self.ok else "MISMATCH"
29
- return (
30
- f"{status} | source: {self.source_count}, target: {self.target_count}, "
31
- f"checksums checked: {self.checked}, issues: {len(self.mismatches)}"
32
- )
15
+ __all__ = ["VerificationReport", "verify"]
33
16
 
34
17
 
35
18
  def verify(
@@ -249,17 +249,40 @@ class DriveItem(BaseItem):
249
249
  Returns:
250
250
  DriveItem: The target folder (existing or newly created).
251
251
  """
252
- from office365.runtime.client_request_exception import ObjectNotFoundException
253
-
254
- names = [name for name in url_path.replace("\\", "/").split("/") if name]
252
+ names = self._split_path(url_path)
255
253
  if not names:
256
254
  raise ValueError("Path is empty")
257
255
  return_type = DriveItem(self.context)
258
256
 
257
+ def _on_resolved(folder: DriveItem) -> None:
258
+ return_type._resource_path = folder._resource_path
259
+ return_type.copy_from(folder)
260
+
261
+ self._resolve_path(names, _on_resolved)
262
+ return return_type
263
+
264
+ @staticmethod
265
+ def _split_path(path: str) -> list[str]:
266
+ """Split a (server-relative) path into non-empty segments."""
267
+ return [name for name in path.replace("\\", "/").split("/") if name]
268
+
269
+ def _resolve_path(self, names: list[str], on_resolved: Callable[[DriveItem], None]) -> None:
270
+ """Walk and create the folders in ``names``, then call ``on_resolved`` with the resolved folder.
271
+
272
+ Reuses an existing folder per level or creates the missing one. The
273
+ callback receives a query-bound parent entity, so callers can queue
274
+ further work (e.g. a file upload) that depends on the parent id. With no
275
+ names the callback runs synchronously against ``self``.
276
+
277
+ Args:
278
+ names (list[str]): Folder names, relative to this item.
279
+ on_resolved (Callable[[DriveItem], None]): Invoked with the resolved folder.
280
+ """
281
+ from office365.runtime.exceptions import ObjectNotFoundException
282
+
259
283
  def _walk(parent: DriveItem, idx: int) -> None:
260
284
  if idx == len(names):
261
- return_type._resource_path = parent._resource_path
262
- return_type.copy_from(parent)
285
+ on_resolved(parent)
263
286
  return
264
287
 
265
288
  child = parent.get_by_path(names[idx])
@@ -282,6 +305,59 @@ class DriveItem(BaseItem):
282
305
  self.get_by_path(prefix).get().after_execute(lambda resolved: _walk(resolved, idx + 1))
283
306
 
284
307
  _walk(self, 0)
308
+
309
+ @require_permission(
310
+ delegated=["Files.ReadWrite", "Files.ReadWrite.All", "Sites.ReadWrite.All"],
311
+ application=["Files.ReadWrite.All", "Sites.ReadWrite.All"],
312
+ notes="Ensure a file exists in a drive, uploading its content when missing",
313
+ )
314
+ def ensure_file(self, relative_path: str, content: bytes | str = b"", *, on_conflict: str = "skip") -> DriveItem:
315
+ """Ensure a file exists at the given path, uploading content when needed.
316
+
317
+ The file counterpart of :meth:`ensure_folder`: missing parent folders are
318
+ created (via :meth:`ensure_folder`), then the leaf file is resolved —
319
+ reusing the existing file or uploading ``content`` when absent. Fully
320
+ deferred — run the chain with ``execute_query()`` and the returned item
321
+ addresses the target file:
322
+
323
+ >>> file = drive_item.ensure_file("2024/Q1/placeholder.txt").execute_query()
324
+
325
+ Args:
326
+ relative_path (str): File name or path relative to this item, e.g.
327
+ ``"report.txt"`` or ``"2024/Q1/report.txt"``.
328
+ content (bytes or str): File content; ``str`` is encoded as UTF-8.
329
+ on_conflict (str): Behaviour when the file already exists.
330
+ ``"skip"`` (default) leaves it untouched, so a re-run is a no-op
331
+ and ``content`` is never sent; ``"replace"`` overwrites it with
332
+ ``content`` in a single request.
333
+
334
+ Returns:
335
+ DriveItem: The target file (existing or newly uploaded).
336
+ """
337
+ from office365.runtime.queries.get_or_create import get_or_create
338
+
339
+ if on_conflict not in ("skip", "replace"):
340
+ raise ValueError(f"on_conflict must be 'skip' or 'replace', got {on_conflict!r}")
341
+ names = self._split_path(relative_path)
342
+ if not names:
343
+ raise ValueError("Path is empty")
344
+ name = names[-1]
345
+ data = content.encode("utf-8") if isinstance(content, str) else content
346
+ return_type = DriveItem(self.context)
347
+
348
+ def _on_resolved(parent_item: DriveItem) -> None:
349
+ return_type._resource_path = parent_item.get_by_path(name)._resource_path
350
+ if on_conflict == "replace":
351
+ parent_item.context.add_query(parent_item._build_upload_query(return_type, data))
352
+ return
353
+ get_or_create(
354
+ find=lambda: parent_item.get_by_path(name).get(),
355
+ create_query=lambda: parent_item._build_upload_query(return_type, data),
356
+ return_type=return_type,
357
+ on_conflict="skip",
358
+ )
359
+
360
+ self._resolve_path(names[:-1], _on_resolved)
285
361
  return return_type
286
362
 
287
363
  def create_powerpoint(self, name: str) -> DriveItem:
@@ -351,6 +427,22 @@ class DriveItem(BaseItem):
351
427
  self.context.add_query(qry)
352
428
  return self
353
429
 
430
+ def delete_object(self, bypass_shared_lock: bool = False) -> Self:
431
+ """Deletes the driveItem.
432
+
433
+ Args:
434
+ bypass_shared_lock: Send ``Prefer: bypass-shared-lock`` so the delete
435
+ succeeds even when the item is open for editing. Without it the
436
+ server rejects the request with
437
+ :class:`~office365.runtime.exceptions.FileLockedException` (HTTP 423).
438
+ """
439
+ super().delete_object()
440
+ if bypass_shared_lock:
441
+ from office365.runtime.http.prefer import prefer_bypass_shared_lock
442
+
443
+ self.context.before_execute(prefer_bypass_shared_lock, once=True)
444
+ return self
445
+
354
446
  def extract_sensitivity_labels(
355
447
  self,
356
448
  ) -> ClientResult[ExtractSensitivityLabelsResult]:
@@ -464,6 +556,24 @@ class DriveItem(BaseItem):
464
556
  self.context.add_query(qry)
465
557
  return qry.return_type
466
558
 
559
+ def _build_upload_query(self, item: DriveItem, content: bytes | None) -> ServiceOperationQuery[DriveItem]:
560
+ """Build (but do not queue) the ``PUT .../content`` query for ``item``.
561
+
562
+ Shared by :meth:`upload` and :meth:`ensure_file` — the latter needs a
563
+ query it can hand to ``get_or_create``'s deferred create slot.
564
+
565
+ Args:
566
+ item (DriveItem): The file that receives the content.
567
+ content (bytes or None): The binary content to upload.
568
+ """
569
+ qry = ServiceOperationQuery(item, "content", None, content, None, item)
570
+
571
+ def _modify_query(request: RequestOptions) -> None:
572
+ request.method = HttpMethod.Put
573
+
574
+ qry.before_execute(_modify_query)
575
+ return qry
576
+
467
577
  @require_permission(
468
578
  delegated=["Files.ReadWrite", "Files.ReadWrite.All", "Sites.ReadWrite.All"],
469
579
  application=["Files.ReadWrite.All", "Sites.ReadWrite.All"],
@@ -474,13 +584,7 @@ class DriveItem(BaseItem):
474
584
 
475
585
  return_type = DriveItem(self.context, UrlPath(name, self.resource_path))
476
586
  self.children.add_child(return_type)
477
-
478
- qry = ServiceOperationQuery(return_type, "content", None, content, None, return_type)
479
-
480
- def _modify_query(request: RequestOptions) -> None:
481
- request.method = HttpMethod.Put
482
-
483
- self.context.add_query(qry).before_execute(_modify_query)
587
+ self.context.add_query(self._build_upload_query(return_type, content))
484
588
  return return_type
485
589
 
486
590
  def upload_file(self, path_or_file: str | PathLike | IOBase) -> DriveItem:
@@ -1,7 +1,6 @@
1
1
  from office365.directory.permissions.require_permission import require_permission
2
2
  from office365.entity_collection import EntityCollection
3
3
  from office365.onedrive.termstore.groups.group import Group
4
- from office365.runtime.client_request_exception import ClientRequestException, DuplicatedObjectException
5
4
 
6
5
 
7
6
  class GroupCollection(EntityCollection[Group]):
@@ -33,12 +32,6 @@ class GroupCollection(EntityCollection[Group]):
33
32
 
34
33
  def ensure(self, name: str) -> Group:
35
34
  """Gets existing group by name or creates a new one (idempotent)."""
36
- return_type = self.add(name)
35
+ from office365.runtime.queries.get_or_create import create_or_get
37
36
 
38
- def _on_error(error: ClientRequestException):
39
- if not isinstance(error, DuplicatedObjectException):
40
- raise error
41
- self.get_by_name(name).after_execute(lambda existing: return_type.copy_from(existing), execute_first=True)
42
-
43
- return_type.on_error(_on_error)
44
- return return_type
37
+ return create_or_get(create=lambda: self.add(name), find=lambda: self.get_by_name(name))
@@ -6,7 +6,6 @@ from office365.directory.permissions.require_permission import require_permissio
6
6
  from office365.entity_collection import EntityCollection
7
7
  from office365.onedrive.termstore.sets.name import LocalizedName
8
8
  from office365.onedrive.termstore.sets.set import Set
9
- from office365.runtime.client_request_exception import ClientRequestException, DuplicatedObjectException
10
9
  from office365.runtime.client_value_collection import ClientValueCollection
11
10
  from office365.runtime.paths.v4.entity import EntityPath
12
11
  from office365.runtime.queries.create_entity import CreateEntityQuery
@@ -76,12 +75,6 @@ class SetCollection(EntityCollection[Set]):
76
75
 
77
76
  def ensure_set(self, name: str) -> Set:
78
77
  """Gets existing set by name or creates a new one (idempotent)."""
79
- return_type = self.add(name)
78
+ from office365.runtime.queries.get_or_create import create_or_get
80
79
 
81
- def _on_error(error: ClientRequestException):
82
- if not isinstance(error, DuplicatedObjectException):
83
- raise error
84
- self.get_by_name(name).after_execute(lambda existing: return_type.copy_from(existing), execute_first=True)
85
-
86
- return_type.on_error(_on_error)
87
- return return_type
80
+ return create_or_get(create=lambda: self.add(name), find=lambda: self.get_by_name(name))
@@ -4,7 +4,6 @@ from office365.directory.permissions.require_permission import require_permissio
4
4
  from office365.entity_collection import EntityCollection
5
5
  from office365.onedrive.termstore.terms.label import LocalizedLabel
6
6
  from office365.onedrive.termstore.terms.term import Term
7
- from office365.runtime.client_request_exception import ClientRequestException, DuplicatedObjectException
8
7
  from office365.runtime.client_value_collection import ClientValueCollection
9
8
  from office365.runtime.paths.v4.entity import EntityPath
10
9
  from office365.runtime.queries.create_entity import CreateEntityQuery
@@ -50,13 +49,7 @@ class TermCollection(EntityCollection[Term]):
50
49
  return return_type
51
50
 
52
51
  def ensure(self, label: str) -> Term:
53
- return_type = self.add(label)
52
+ """Gets an existing term by label or creates it (idempotent)."""
53
+ from office365.runtime.queries.get_or_create import create_or_get
54
54
 
55
- def _on_error(error: ClientRequestException):
56
- if not isinstance(error, DuplicatedObjectException):
57
- raise error
58
-
59
- self.get_by_label(label).after_execute(lambda existing: return_type.copy_from(existing), execute_first=True)
60
-
61
- return_type.on_error(_on_error)
62
- return return_type
55
+ return create_or_get(create=lambda: self.add(label), find=lambda: self.get_by_label(label))
@@ -17,11 +17,18 @@ from office365.onedrive.workbooks.worksheets.collection import (
17
17
  )
18
18
  from office365.runtime.client_result import ClientResult
19
19
  from office365.runtime.http.request_options import RequestOptions
20
+ from office365.runtime.limits import Limit, LimitKind, limit
20
21
  from office365.runtime.paths.resource_path import ResourcePath
21
22
  from office365.runtime.queries.function import FunctionQuery
22
23
  from office365.runtime.queries.service_operation import ServiceOperationQuery
23
24
 
25
+ _DOC = "https://learn.microsoft.com/en-us/graph/throttling-limits"
24
26
 
27
+
28
+ @limit(
29
+ Limit("excel", 5_000, LimitKind.SUPPORTED, "requests", "app", doc=_DOC, window_seconds=10),
30
+ Limit("excel", 1_500, LimitKind.SUPPORTED, "requests", "app+tenant", doc=_DOC, window_seconds=10),
31
+ )
25
32
  class Workbook(Entity):
26
33
  """The top-level object that contains related workbook objects such as worksheets, tables, and ranges."""
27
34
 
@@ -1,6 +1,5 @@
1
1
  from office365.delta_collection import DeltaCollection
2
2
  from office365.outlook.contacts.folders.folder import ContactFolder
3
- from office365.runtime.client_request_exception import ClientRequestException, DuplicatedObjectException
4
3
 
5
4
 
6
5
  class ContactFolderCollection(DeltaCollection[ContactFolder]):
@@ -18,18 +17,10 @@ class ContactFolderCollection(DeltaCollection[ContactFolder]):
18
17
  return super().add(**kwargs)
19
18
 
20
19
  def ensure(self, display_name: str) -> ContactFolder:
21
- """Gets existing group by name or creates a new one (idempotent)."""
22
- return_type = self.add(display_name)
23
-
24
- def _on_name_exists(error: ClientRequestException):
25
- if not isinstance(error, DuplicatedObjectException):
26
- raise error
27
- self.get_by_name(display_name).after_execute(
28
- lambda existing: return_type.copy_from(existing), execute_first=True
29
- )
30
-
31
- return_type.on_error(_on_name_exists)
32
- return return_type
20
+ """Gets an existing folder by name or creates it (idempotent)."""
21
+ from office365.runtime.queries.get_or_create import create_or_get
22
+
23
+ return create_or_get(create=lambda: self.add(display_name), find=lambda: self.get_by_name(display_name))
33
24
 
34
25
  def get_by_name(self, display_name: str) -> ContactFolder:
35
26
  """Returns the group with the specified name."""
@@ -381,6 +381,11 @@ class AuthenticationContext:
381
381
  self._authenticate = provider.authenticate_request
382
382
  return self
383
383
 
384
+ @property
385
+ def is_configured(self) -> bool:
386
+ """Whether an authentication provider has been set on this context."""
387
+ return self._authenticate is not None
388
+
384
389
  def authenticate_request(self, request: RequestOptions) -> None:
385
390
  """Authenticate the HTTP request
386
391
 
@@ -7,6 +7,7 @@ from typing import (
7
7
  Dict,
8
8
  List,
9
9
  Optional,
10
+ Tuple,
10
11
  TypeVar,
11
12
  )
12
13
 
@@ -16,6 +17,7 @@ from office365.runtime.client_request_exception import ClientRequestException
16
17
  from office365.runtime.client_runtime_context import ClientRuntimeContext
17
18
  from office365.runtime.converters.value import _add_type_metadata, declared_type, deserialize_value, serialize_value
18
19
  from office365.runtime.http.request_options import RequestOptions
20
+ from office365.runtime.limits import Limit, LimitDecl, collect_class_limits, collect_limit_meta
19
21
  from office365.runtime.odata.json_format import ODataJsonFormat
20
22
  from office365.runtime.odata.query_options import QueryOptions
21
23
  from office365.runtime.paths.resource_path import ResourcePath
@@ -32,6 +34,8 @@ class ClientObject:
32
34
  """Base client object which defines named properties and relationships of an entity."""
33
35
 
34
36
  _odata_meta: dict[str, ODataPropertyMeta] = {}
37
+ _limit_meta: dict[str, Tuple[LimitDecl, ...]] = {}
38
+ _class_limit_decls: Tuple[LimitDecl, ...] = ()
35
39
 
36
40
  def __init_subclass__(cls, **kwargs: object) -> None:
37
41
  super().__init_subclass__(**kwargs)
@@ -43,6 +47,8 @@ class ClientObject:
43
47
  m.attr = attr_name
44
48
  meta[m.name] = m
45
49
  cls._odata_meta = meta
50
+ cls._limit_meta = collect_limit_meta(cls)
51
+ cls._class_limit_decls = collect_class_limits(cls)
46
52
 
47
53
  def __init__(
48
54
  self,
@@ -114,6 +120,13 @@ class ClientObject:
114
120
  """Names of properties marked ``@odata(persist=True)``."""
115
121
  return {name for name, m in type(self)._odata_meta.items() if m.persist}
116
122
 
123
+ @classmethod
124
+ def declared_limits(cls) -> Tuple[Limit, ...]:
125
+ """The limits declared on this class and its methods/properties (``@limit``)."""
126
+ class_limits = tuple(decl.limit for decl in cls._class_limit_decls)
127
+ member_limits = tuple(decl.limit for decls in cls._limit_meta.values() for decl in decls)
128
+ return (*class_limits, *member_limits)
129
+
117
130
  def execute_query(self) -> Self:
118
131
  """
119
132
  Submits all pending requests to the server.
@@ -133,6 +146,7 @@ class ClientObject:
133
146
  success_callback=None,
134
147
  failure_callback=None,
135
148
  exceptions=(ClientRequestException,),
149
+ is_retriable=None,
136
150
  ) -> Self:
137
151
  """Executes the current set of data retrieval queries and method invocations and retries it if needed.
138
152
 
@@ -144,6 +158,9 @@ class ClientObject:
144
158
  success_callback (callable): A callback to call if the request executes successfully.
145
159
  failure_callback (callable): A callback to call if the request fails to execute.
146
160
  exceptions (tuple): Tuple of exceptions that we retry.
161
+ is_retriable (callable): Optional predicate deciding whether a caught
162
+ exception is retried. Defaults to transient errors; pass
163
+ ``retry_on(...)`` to also retry otherwise-permanent errors.
147
164
  """
148
165
  self.context.execute_query_retry(
149
166
  max_retry=max_retry,
@@ -153,6 +170,7 @@ class ClientObject:
153
170
  success_callback=success_callback,
154
171
  failure_callback=failure_callback,
155
172
  exceptions=exceptions,
173
+ is_retriable=is_retriable,
156
174
  )
157
175
  return self
158
176