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
@@ -22,16 +22,17 @@ from __future__ import annotations
22
22
  from collections.abc import Callable
23
23
  from typing import TYPE_CHECKING
24
24
 
25
- from office365.migration._util import emit_progress
26
25
  from office365.migration.assessment.containers import ScanContainer
27
26
  from office365.migration.assessment.issue import AssessmentIssue
28
27
  from office365.migration.assessment.report import AssessmentReport
29
28
  from office365.migration.assessment.runner import ScanRunner
30
29
  from office365.migration.assessment.scanners import AssessmentOptions
31
30
  from office365.migration.sharepoint.adapters import is_taxonomy_validation
31
+ from office365.migration.sharepoint.options import SharePointAssessmentOptions
32
32
  from office365.migration.sharepoint.registry import sharepoint_scan_pairs
33
33
  from office365.migration.sharepoint.scanners.summary import SiteScanSummary
34
34
  from office365.runtime.client_result import ClientResult
35
+ from office365.runtime.operations import emit_progress
35
36
  from office365.sharepoint.entity import Entity
36
37
 
37
38
  if TYPE_CHECKING:
@@ -56,7 +57,7 @@ class MigrationAssessor(Entity):
56
57
  def __init__(self, web: "Web", options: AssessmentOptions | None = None) -> None:
57
58
  super().__init__(web.context)
58
59
  self._web = web
59
- self._options = options or AssessmentOptions()
60
+ self._options = options or SharePointAssessmentOptions()
60
61
 
61
62
  # ── Configuration ────────────────────────────────────────────
62
63
 
@@ -104,7 +105,11 @@ class MigrationAssessor(Entity):
104
105
  report = runner.report
105
106
 
106
107
  needs_site = bool(runner.scanners(ScanContainer.SITE))
107
- needs_list_metadata = needs_site # item counts / last-modified for the summary
108
+ needs_site_template = any(
109
+ scanner.scan_name == "UnsupportedSiteTemplates" for scanner in runner.scanners(ScanContainer.SITE)
110
+ )
111
+ # item counts / last-modified feed the SITE summary and the LIST-container scans
112
+ needs_list_metadata = needs_site or bool(runner.scanners(ScanContainer.LIST))
108
113
  summary = SiteScanSummary()
109
114
 
110
115
  def _flag_failure(location: str, error: Exception) -> None:
@@ -138,9 +143,14 @@ class MigrationAssessor(Entity):
138
143
  # site collection metadata (usage/storage, owner) for SITE-container scans
139
144
  if needs_site:
140
145
  site = self._web.context.site
146
+ columns = ["Id", "Url", "UsageInfo", "Owner/Title", "Owner/Email"]
147
+ expands = ["Owner"]
148
+ if needs_site_template: # the root web template feeds UnsupportedSiteTemplates
149
+ columns += ["RootWeb/WebTemplate", "RootWeb/Configuration"]
150
+ expands.append("RootWeb")
141
151
  (
142
- site.select(["Id", "Url", "UsageInfo", "Owner/Title", "Owner/Email"])
143
- .expand(["Owner"])
152
+ site.select(columns)
153
+ .expand(expands)
144
154
  .get()
145
155
  .on_error(lambda e: _flag_failure("web", e))
146
156
  .after_execute(lambda site: self._on_site_loaded(site, summary, needs_site))
@@ -182,6 +192,20 @@ class MigrationAssessor(Entity):
182
192
  site.root_web.associated_owner_group.users.get().on_error(lambda e: None).after_execute(
183
193
  lambda users: self._set_site_admins(summary, users)
184
194
  )
195
+ root_web = site.properties.get("RootWeb")
196
+ if root_web is not None:
197
+ summary.web_template = self._web_template(root_web)
198
+
199
+ @staticmethod
200
+ def _web_template(web: "Web") -> str | None:
201
+ """The root web's ``WebTemplate`` plus its configuration id (e.g. ``STS#0``)."""
202
+ template = web.properties.get("WebTemplate")
203
+ if not template:
204
+ return None
205
+ if "#" in template:
206
+ return template
207
+ configuration = web.properties.get("Configuration")
208
+ return f"{template}#{configuration}" if configuration is not None else template
185
209
 
186
210
  @staticmethod
187
211
  def _set_site_admins(summary: SiteScanSummary, users) -> None:
@@ -247,16 +271,19 @@ class MigrationAssessor(Entity):
247
271
  has_fields = bool(runner.scanners(ScanContainer.FIELDS))
248
272
  has_items = bool(runner.scanners(ScanContainer.ITEMS, "default"))
249
273
  has_unique_items = bool(runner.scanners(ScanContainer.ITEMS, "unique"))
274
+ has_list = bool(runner.scanners(ScanContainer.LIST))
250
275
 
251
276
  def _progress(lst) -> None:
252
277
  completed["count"] += 1
253
278
  emit_progress(progress, done=completed["count"], total=total, stage="assessing", items=[lst])
254
279
 
255
280
  for lst in lists:
281
+ location = f"{prefix}/lists/{lst.title}"
282
+ if has_list:
283
+ runner.dispatch(ScanContainer.LIST, lst, location)
256
284
  if lst.hidden:
257
285
  _progress(lst)
258
286
  continue
259
- location = f"{prefix}/lists/{lst.title}"
260
287
  pending = {"count": 0}
261
288
 
262
289
  def _scan_done(lst=lst, pending=pending) -> None:
@@ -278,9 +305,18 @@ class MigrationAssessor(Entity):
278
305
  )
279
306
  if has_items:
280
307
  pending["count"] += 1
281
- lst.items.select(["FileRef", "FileLeafRef", "File/Length"]).expand(["File"]).get().on_error(
282
- _fail
283
- ).after_execute(
308
+ lst.items.select(
309
+ [
310
+ "FileRef",
311
+ "FileLeafRef",
312
+ "File/Length",
313
+ "File/MajorVersion",
314
+ "File/MinorVersion",
315
+ "File/CheckOutType",
316
+ "File/TimeCreated",
317
+ "File/TimeLastModified",
318
+ ]
319
+ ).expand(["File", "File/ModifiedBy", "File/CheckedOutByUser"]).get().on_error(_fail).after_execute(
284
320
  lambda col, lst=lst, loc=location, done=_scan_done: (
285
321
  self._scan_items(runner, col, loc),
286
322
  done(lst),
@@ -0,0 +1,57 @@
1
+ """SharePoint assessment options — the SharePoint defaults for the scanners.
2
+
3
+ Seeds the product-agnostic :class:`~office365.migration.assessment.scanners.AssessmentOptions`
4
+ with the SharePoint thresholds (from ``office365.sharepoint.thresholds.Limits``)
5
+ and the ``limits`` mapping, so findings reference the authoritative limit.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from dataclasses import dataclass, field
11
+ from typing import Optional
12
+
13
+ from office365.migration.assessment.scanners import AssessmentOptions
14
+ from office365.runtime.limits import Limit
15
+ from office365.sharepoint.fields.builtin_field_name import SYSTEM_FIELD_NAMES
16
+ from office365.sharepoint.thresholds import Limits
17
+
18
+
19
+ def _limits() -> dict[str, Limit]:
20
+ return {
21
+ "max_path_length": Limits.FILE_PATH_LENGTH,
22
+ "max_name_length": Limits.FILE_NAME_LENGTH,
23
+ "large_file_bytes": Limits.MIGRATION_FILE_SIZE,
24
+ "large_excel_bytes": Limits.LARGE_EXCEL_FILE,
25
+ "long_onedrive_url": Limits.LONG_ONEDRIVE_URL,
26
+ "list_view_threshold": Limits.LIST_VIEW,
27
+ "index_threshold": Limits.INDEX_ADD_REMOVE,
28
+ "max_list_items": Limits.MAX_LIST_ITEMS,
29
+ "lookup_joins": Limits.LOOKUP_JOINS,
30
+ "unique_scopes": Limits.UNIQUE_SCOPES,
31
+ "recommended_unique_scopes": Limits.UNIQUE_SCOPES_RECOMMENDED,
32
+ }
33
+
34
+
35
+ @dataclass
36
+ class SharePointAssessmentOptions(AssessmentOptions):
37
+ """The SharePoint defaults — thresholds + the ``limits`` mapping."""
38
+
39
+ max_path_length: Optional[int] = Limits.FILE_PATH_LENGTH.value
40
+ max_name_length: Optional[int] = Limits.FILE_NAME_LENGTH.value
41
+ invalid_chars: set[str] = field(default_factory=lambda: set(r'~"#%&*:<>?/\{|}'))
42
+ large_file_bytes: Optional[int] = Limits.MIGRATION_FILE_SIZE.value
43
+ large_excel_bytes: Optional[int] = Limits.LARGE_EXCEL_FILE.value
44
+ long_onedrive_url: Optional[int] = Limits.LONG_ONEDRIVE_URL.value
45
+ list_view_threshold: Optional[int] = Limits.LIST_VIEW.value
46
+ index_threshold: Optional[int] = Limits.INDEX_ADD_REMOVE.value
47
+ max_list_items: Optional[int] = Limits.MAX_LIST_ITEMS.value
48
+ lookup_joins: Optional[int] = Limits.LOOKUP_JOINS.value
49
+ unique_scopes: Optional[int] = Limits.UNIQUE_SCOPES.value
50
+ recommended_unique_scopes: Optional[int] = Limits.UNIQUE_SCOPES_RECOMMENDED.value
51
+ large_site_threshold_gb: Optional[float] = 500.0
52
+ strip_field_attrs: set[str] = field(default_factory=lambda: {"ColName", "RowOrdinal", "SourceID", "Version"})
53
+ approval_workflow_fields: set[str] = field(
54
+ default_factory=lambda: {"_ApprovalStatus", "_ApprovalRespondedBy", "_ApprovalAssignedTo"}
55
+ )
56
+ system_field_names: set[str] = field(default_factory=lambda: set(SYSTEM_FIELD_NAMES))
57
+ limits: dict[str, Limit] = field(default_factory=_limits)
@@ -0,0 +1,209 @@
1
+ """Server-side migration target — build a Migration API package and ingest it.
2
+
3
+ Unlike the client-side :class:`SharePointLibraryTarget` (which uploads each file
4
+ over REST), this target accumulates files/folders into a Migration API package,
5
+ stages the blobs in Azure Storage, and submits an ingestion job — the only path
6
+ that preserves version history and full ACL fidelity.
7
+
8
+ Content and manifest go to **different** containers; the caller supplies their SAS
9
+ URIs (e.g. from ``Site.provision_migration_containers``).
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from typing import TYPE_CHECKING
15
+
16
+ from office365.migration.base import MigrationItem
17
+ from office365.migration.package.builder import PackageBuilder
18
+ from office365.migration.package.staging import create_staging
19
+ from office365.migration.server_job import MigrationServerJob
20
+
21
+ if TYPE_CHECKING:
22
+ from office365.migration.package.builder import Package
23
+
24
+ __all__ = ["SharePointPackageTarget"]
25
+
26
+
27
+ def _resolve_site_url(site, site_url: str | None) -> str:
28
+ """The target site URL for ``ExportSettings.xml`` (the API rejects an empty URI)."""
29
+ resolved = site_url or getattr(site, "url", None)
30
+ if not resolved and hasattr(site, "ensure_property"):
31
+ site.ensure_property("Url").execute_query()
32
+ resolved = site.url
33
+ if not resolved:
34
+ raise ValueError("site_url is required — ExportSettings.xml needs the target site URL")
35
+ return resolved
36
+
37
+
38
+ class SharePointPackageTarget:
39
+ """A ``DataTarget`` that builds a package and submits a server-side ingestion job."""
40
+
41
+ def __init__(
42
+ self,
43
+ site,
44
+ web_id,
45
+ *,
46
+ content_uri: str | None = None,
47
+ manifest_uri: str | None = None,
48
+ site_url: str | None = None,
49
+ list_title: str = "Documents",
50
+ list_url: str | None = None,
51
+ web_url: str = "/",
52
+ root_folder_id: str | None = None,
53
+ root_folder_parent_id: str | None = None,
54
+ source_type: str = "SharePointOnline",
55
+ azure_queue_report_uri: str | None = None,
56
+ encryption_key: bytes | None = None,
57
+ staging=None,
58
+ ) -> None:
59
+ self._site = site
60
+ self._web_id = web_id
61
+ self._list_title = list_title
62
+ self._content_uri = content_uri
63
+ self._manifest_uri = manifest_uri
64
+ self._azure_queue_report_uri = azure_queue_report_uri
65
+ self._encryption_key = encryption_key
66
+ if staging is None:
67
+ if not (content_uri and manifest_uri):
68
+ raise ValueError("content_uri and manifest_uri are required unless staging is provided")
69
+ staging = create_staging(content_uri, manifest_uri, encryption_key=encryption_key)
70
+ self._staging = staging
71
+ self._builder = PackageBuilder(
72
+ _resolve_site_url(site, site_url),
73
+ web_id=web_id,
74
+ web_url=web_url,
75
+ list_title=list_title,
76
+ list_url=list_url,
77
+ root_folder_id=root_folder_id,
78
+ root_folder_parent_id=root_folder_parent_id,
79
+ source_type=source_type,
80
+ )
81
+ self.job_id: str | None = None
82
+ self._package: "Package | None" = None
83
+
84
+ def label(self) -> str:
85
+ return f"migration-package:{self._list_title}"
86
+
87
+ # ── DataTarget ───────────────────────────────────────────────
88
+
89
+ def exists(self, item: MigrationItem) -> bool:
90
+ # The server-side API handles overwrite/merge; nothing is checked client-side.
91
+ return False
92
+
93
+ def write(self, item: MigrationItem, payload: object) -> None:
94
+ if item.item_type == "folder":
95
+ self._builder.add_folder(
96
+ item.dest_path,
97
+ time_created=item.created,
98
+ time_last_modified=item.modified,
99
+ )
100
+ return
101
+ content = payload if isinstance(payload, bytes) else str(payload).encode("utf-8")
102
+ self._builder.add_file(
103
+ item.dest_path,
104
+ content,
105
+ time_created=item.created,
106
+ time_last_modified=item.modified,
107
+ )
108
+
109
+ def list_paths(self) -> list[str]:
110
+ return []
111
+
112
+ def checksum(self, item: MigrationItem) -> str:
113
+ return ""
114
+
115
+ # ── Package + submission ─────────────────────────────────────
116
+
117
+ def build(self) -> "Package":
118
+ """Render the package from the items written so far."""
119
+ self._package = self._builder.build()
120
+ return self._package
121
+
122
+ def stage(self) -> "Package":
123
+ """Build the package and stage its blobs for ingestion."""
124
+ package = self.build()
125
+ self._staging.stage(package)
126
+ return package
127
+
128
+ def commit(self, options=None) -> None:
129
+ """Stage the package and submit the ingestion job (called by the runner)."""
130
+ if not (self._content_uri and self._manifest_uri):
131
+ raise ValueError("content_uri and manifest_uri are required to submit an ingestion job")
132
+ self.stage()
133
+ server = MigrationServerJob(self._site)
134
+ if self._encryption_key is not None:
135
+ self.job_id = server.submit_encrypted(
136
+ self._web_id,
137
+ self._content_uri,
138
+ self._manifest_uri,
139
+ self._encryption_key,
140
+ self._azure_queue_report_uri,
141
+ )
142
+ else:
143
+ self.job_id = server.submit(
144
+ self._web_id,
145
+ self._content_uri,
146
+ self._manifest_uri,
147
+ self._azure_queue_report_uri,
148
+ )
149
+
150
+ def monitor(self, interval: float = 5, timeout: float = 1800, progress=None) -> str:
151
+ """Poll the submitted job to completion (``GetMigrationJobProgress``)."""
152
+ if self.job_id is None:
153
+ raise ValueError("no job submitted yet — call commit() first")
154
+ return MigrationServerJob(self._site).monitor(self.job_id, interval=interval, timeout=timeout, progress=progress)
155
+
156
+ @property
157
+ def staging(self):
158
+ """The staging strategy (``FileSystemStaging`` / ``BlobStaging``)."""
159
+ return self._staging
160
+
161
+ def events(self) -> list[dict]:
162
+ """All progress events for the submitted job (call after :meth:`monitor`)."""
163
+ if self.job_id is None:
164
+ return []
165
+ return MigrationServerJob(self._site).all_events(self.job_id)
166
+
167
+ def errors(self) -> list[dict]:
168
+ """The ``JobError`` events for the submitted job (call after :meth:`monitor`)."""
169
+ if self.job_id is None:
170
+ return []
171
+ return MigrationServerJob(self._site).errors(self.job_id)
172
+
173
+ def diagnose(self) -> list[str]:
174
+ """Human-readable reasons the submitted job failed (errors, events, API log).
175
+
176
+ The progress events omit the detail; the API writes it to a log in the
177
+ manifest container (named by the ``JobLogFileCreate`` event), which this
178
+ fetches — the container grants ``Read``.
179
+ """
180
+ if self.job_id is None:
181
+ return []
182
+ events = self.events()
183
+ lines = [f"! {e.get('ErrorType', 'error')}: {e.get('Message', '')}".rstrip() for e in self.errors()]
184
+ if not lines:
185
+ lines = [f"· {e.get('Event')}: {e.get('Message', '')}".rstrip() for e in events]
186
+ lines.extend(self._read_logs(events))
187
+ return lines
188
+
189
+ def _read_logs(self, events: list[dict]) -> list[str]:
190
+ reader = getattr(self._staging, "read_manifest_blob", None)
191
+ if not callable(reader):
192
+ return []
193
+ names = [event.get("FileName") for event in events if event.get("FileName")]
194
+ if not names and self.job_id:
195
+ names = [f"Import-{self.job_id}-1.log"]
196
+ lines: list[str] = []
197
+ for name in names:
198
+ try:
199
+ data = reader(name)
200
+ text = data.decode("utf-8", "replace") if isinstance(data, bytes) else str(data)
201
+ except Exception as exc: # noqa: BLE001 — diagnostics are best-effort
202
+ lines.append(f"(log {name} unavailable: {exc})")
203
+ else:
204
+ lines.append(f"--- {name} ---")
205
+ lines.append(text[:4000])
206
+ return lines
207
+
208
+ def close(self) -> None:
209
+ pass
@@ -10,19 +10,41 @@ from __future__ import annotations
10
10
  from office365.migration.assessment.containers import ScanContainer
11
11
  from office365.migration.assessment.registry import ScanDefinition, scan_pairs
12
12
  from office365.migration.assessment.scanners.base import AssessmentOptions, BaseScanner
13
+ from office365.migration.sharepoint.scanners.browser_file_handling import BrowserFileHandlingScanner
14
+ from office365.migration.sharepoint.scanners.checked_out_files import CheckedOutFilesScanner
13
15
  from office365.migration.sharepoint.scanners.fields import FieldScanner
16
+ from office365.migration.sharepoint.scanners.file_versions import FileVersionsScanner
14
17
  from office365.migration.sharepoint.scanners.files import FileScanner
18
+ from office365.migration.sharepoint.scanners.large_excel_files import LargeExcelFilesScanner
19
+ from office365.migration.sharepoint.scanners.large_lists import LargeListScanner
15
20
  from office365.migration.sharepoint.scanners.large_sites import LargeSitesScanner
16
21
  from office365.migration.sharepoint.scanners.locked_sites import SiteLockedScanner
22
+ from office365.migration.sharepoint.scanners.long_onedrive_urls import LongOneDriveUrlsScanner
23
+ from office365.migration.sharepoint.scanners.lookup import LookupColumnScanner
17
24
  from office365.migration.sharepoint.scanners.paths import PathScanner
18
25
  from office365.migration.sharepoint.scanners.permissions import PermissionScanner
26
+ from office365.migration.sharepoint.scanners.thicket_folders import ThicketFolderScanner
27
+ from office365.migration.sharepoint.scanners.unsupported_site_templates import UnsupportedSiteTemplatesScanner
19
28
 
20
29
  SHAREPOINT_SCANS: list[ScanDefinition] = [
21
30
  ScanDefinition(name="fields", scanner=FieldScanner, container=ScanContainer.FIELDS),
31
+ ScanDefinition(name="lookups", scanner=LookupColumnScanner, container=ScanContainer.FIELDS),
32
+ ScanDefinition(name="largeLists", scanner=LargeListScanner, container=ScanContainer.LIST),
22
33
  ScanDefinition(name="paths", scanner=PathScanner, container=ScanContainer.ITEMS),
23
34
  ScanDefinition(name="files", scanner=FileScanner, container=ScanContainer.ITEMS),
35
+ ScanDefinition(name="FileVersions", scanner=FileVersionsScanner, container=ScanContainer.ITEMS),
36
+ ScanDefinition(name="CheckedOutFiles", scanner=CheckedOutFilesScanner, container=ScanContainer.ITEMS),
37
+ ScanDefinition(name="LargeExcelFiles", scanner=LargeExcelFilesScanner, container=ScanContainer.ITEMS),
38
+ ScanDefinition(name="LongOneDriveUrls", scanner=LongOneDriveUrlsScanner, container=ScanContainer.ITEMS),
39
+ ScanDefinition(name="BrowserFileHandling", scanner=BrowserFileHandlingScanner, container=ScanContainer.ITEMS),
40
+ ScanDefinition(name="ThicketFolder", scanner=ThicketFolderScanner, container=ScanContainer.ITEMS),
24
41
  ScanDefinition(name="permissions", scanner=PermissionScanner, container=ScanContainer.ITEMS),
25
42
  ScanDefinition(name="LargeSites", scanner=LargeSitesScanner, container=ScanContainer.SITE),
43
+ ScanDefinition(
44
+ name="UnsupportedSiteTemplates",
45
+ scanner=UnsupportedSiteTemplatesScanner,
46
+ container=ScanContainer.SITE,
47
+ ),
26
48
  ScanDefinition(
27
49
  name="LockedSites",
28
50
  scanner=SiteLockedScanner,
@@ -0,0 +1,96 @@
1
+ """Resolve a declarative :class:`MigrationTask` into source/target adapters.
2
+
3
+ SPMT-shaped tasks (``Add-SPMTTask``) name a source and a target site/list; this
4
+ turns them into the toolkit's ``DataSource``/``DataTarget`` pair against a
5
+ SharePoint ``ClientContext``. Client-side REST uploads are the default; set
6
+ ``MigrationSettings.use_migration_api`` for the server-side Migration API path
7
+ (full fidelity: versions/ACLs).
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from typing import TYPE_CHECKING
13
+
14
+ from office365.migration.adapters.filesystem import FileSystemSource
15
+ from office365.migration.tasks import FILE_SHARE, SHAREPOINT, MigrationTask
16
+
17
+ if TYPE_CHECKING:
18
+ from office365.migration.settings import MigrationSettings
19
+
20
+ __all__ = ["resolve_task"]
21
+
22
+
23
+ def resolve_task(task: MigrationTask, context, settings: "MigrationSettings | None" = None):
24
+ """Return a ``(source, target)`` adapter pair for a task descriptor."""
25
+ if task.kind == FILE_SHARE:
26
+ return _file_share(task, context, settings)
27
+ if task.kind == SHAREPOINT:
28
+ return _sharepoint(task, context, settings)
29
+ raise ValueError(f"unsupported task kind {task.kind!r}")
30
+
31
+
32
+ def _file_share(task: MigrationTask, context, settings):
33
+ if not task.file_share_source:
34
+ raise ValueError("a file-share task requires file_share_source")
35
+ source = FileSystemSource(task.file_share_source)
36
+ library, folder = _target_folder(context, task)
37
+ return source, _build_target(task, context, settings, library, folder)
38
+
39
+
40
+ def _sharepoint(task: MigrationTask, context, settings):
41
+ from office365.migration.sharepoint.adapters import SharePointLibrarySource
42
+
43
+ if task.migrate_all or not task.source_list:
44
+ raise NotImplementedError(
45
+ "SharePoint-source tasks need a source list; migrate-all / whole-site migrations are not supported yet"
46
+ )
47
+ source_list = context.web.get_list_by_title(task.source_list)
48
+ source = SharePointLibrarySource(source_list.root_folder)
49
+ library, folder = _target_folder(context, task)
50
+ return source, _build_target(task, context, settings, library, folder)
51
+
52
+
53
+ def _target_folder(context, task: MigrationTask):
54
+ """The target library (and, when set, its relative subfolder)."""
55
+ library = context.web.get_list_by_title(task.target_list).get().execute_query()
56
+ folder = library.root_folder.get().execute_query()
57
+ if task.target_list_relative_path:
58
+ url = f"{(folder.server_relative_url or '').rstrip('/')}/{task.target_list_relative_path.strip('/')}"
59
+ folder = context.web.get_folder_by_server_relative_url(url)
60
+ return library, folder
61
+
62
+
63
+ def _build_target(task: MigrationTask, context, settings, library, folder):
64
+ if settings is not None and settings.use_migration_api:
65
+ return _package_target(task, context, library)
66
+ from office365.migration.sharepoint.adapters import SharePointLibraryTarget
67
+
68
+ return SharePointLibraryTarget(folder)
69
+
70
+
71
+ def _package_target(task: MigrationTask, context, library):
72
+ """The server-side Migration API target (provisions containers)."""
73
+ from urllib.parse import urlparse
74
+
75
+ from office365.migration.sharepoint.package_target import SharePointPackageTarget
76
+
77
+ web = context.web.get().execute_query()
78
+ root = library.root_folder
79
+ root.ensure_properties(["UniqueId", "ServerRelativeUrl"]).execute_query()
80
+ parent = root.parent_folder.ensure_property("UniqueId")
81
+ parent.execute_query()
82
+ containers = context.site.provision_migration_containers().execute_query().value
83
+ return SharePointPackageTarget(
84
+ context.site,
85
+ web.id,
86
+ content_uri=containers.DataContainerUri,
87
+ manifest_uri=containers.MetadataContainerUri,
88
+ encryption_key=containers.EncryptionKey,
89
+ site_url=context.base_url,
90
+ list_title=library.title,
91
+ list_url=root.server_relative_url,
92
+ web_url=urlparse(context.base_url).path or "/",
93
+ root_folder_id=root.unique_id,
94
+ root_folder_parent_id=parent.unique_id,
95
+ source_type="FileShare" if task.kind == FILE_SHARE else "SharePointOnline",
96
+ )
@@ -5,23 +5,52 @@ from office365.migration.assessment.scanners.base import (
5
5
  BaseScanner,
6
6
  ScanTarget,
7
7
  )
8
+ from office365.migration.sharepoint.scanners.browser_file_handling import (
9
+ BrowserFileHandlingRecord,
10
+ BrowserFileHandlingScanner,
11
+ )
12
+ from office365.migration.sharepoint.scanners.checked_out_files import (
13
+ CheckedOutFilesRecord,
14
+ CheckedOutFilesScanner,
15
+ )
8
16
  from office365.migration.sharepoint.scanners.fields import FieldScanner
17
+ from office365.migration.sharepoint.scanners.file_versions import FileVersionsRecord, FileVersionsScanner
9
18
  from office365.migration.sharepoint.scanners.files import FileScanner
19
+ from office365.migration.sharepoint.scanners.large_excel_files import LargeExcelFilesRecord, LargeExcelFilesScanner
10
20
  from office365.migration.sharepoint.scanners.large_sites import LargeSitesScanner
11
21
  from office365.migration.sharepoint.scanners.locked_sites import SiteLockedScanner
22
+ from office365.migration.sharepoint.scanners.long_onedrive_urls import LongOneDriveUrlsRecord, LongOneDriveUrlsScanner
12
23
  from office365.migration.sharepoint.scanners.paths import PathScanner
13
24
  from office365.migration.sharepoint.scanners.permissions import PermissionScanner
14
25
  from office365.migration.sharepoint.scanners.summary import SiteScanSummary
26
+ from office365.migration.sharepoint.scanners.thicket_folders import ThicketFolderScanner
27
+ from office365.migration.sharepoint.scanners.unsupported_site_templates import (
28
+ UnsupportedSiteTemplatesRecord,
29
+ UnsupportedSiteTemplatesScanner,
30
+ )
15
31
 
16
32
  __all__ = [
17
33
  "AssessmentOptions",
18
34
  "BaseScanner",
35
+ "BrowserFileHandlingRecord",
36
+ "BrowserFileHandlingScanner",
37
+ "CheckedOutFilesRecord",
38
+ "CheckedOutFilesScanner",
19
39
  "FieldScanner",
20
40
  "FileScanner",
41
+ "FileVersionsRecord",
42
+ "FileVersionsScanner",
43
+ "LargeExcelFilesRecord",
44
+ "LargeExcelFilesScanner",
21
45
  "LargeSitesScanner",
46
+ "LongOneDriveUrlsRecord",
47
+ "LongOneDriveUrlsScanner",
22
48
  "PathScanner",
23
49
  "PermissionScanner",
24
50
  "ScanTarget",
25
51
  "SiteLockedScanner",
26
52
  "SiteScanSummary",
53
+ "ThicketFolderScanner",
54
+ "UnsupportedSiteTemplatesRecord",
55
+ "UnsupportedSiteTemplatesScanner",
27
56
  ]
@@ -0,0 +1,55 @@
1
+ """Browser file handling scan — the SMAT ``BrowserFileHandling`` report.
2
+
3
+ The target enforces the "Strict" browser file handling setting, so ``.htm`` /
4
+ ``.html`` files no longer open in the browser — users are prompted to download
5
+ them. This ``ITEMS``-container report lists the affected files.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from dataclasses import dataclass
11
+ from datetime import datetime
12
+
13
+ from office365.migration.assessment.report import AssessmentReport
14
+ from office365.migration.assessment.scanners.base import BaseScanner, ScanTarget
15
+ from office365.migration.sharepoint.scanners.smat import SiteScanRecord, file_extension, site_url
16
+
17
+ #: Extensions affected by the Strict browser file handling change.
18
+ HTML_EXTENSIONS = frozenset({".htm", ".html"})
19
+
20
+
21
+ @dataclass
22
+ class BrowserFileHandlingRecord(SiteScanRecord):
23
+ """One row of the SMAT ``BrowserFileHandling-detail`` report."""
24
+
25
+ File: str | None = None
26
+ TimeCreated: datetime | None = None
27
+ TimeModified: datetime | None = None
28
+ ModifiedBy: str | None = None
29
+ ScanID: str | None = None
30
+
31
+
32
+ class BrowserFileHandlingScanner(BaseScanner[BrowserFileHandlingRecord]):
33
+ """Reports ``.htm``/``.html`` files affected by Strict browser handling."""
34
+
35
+ category = "file"
36
+ scan_name = "BrowserFileHandling"
37
+ record_type = BrowserFileHandlingRecord
38
+
39
+ def run(self, target: ScanTarget, report: AssessmentReport) -> None:
40
+ location = site_url(target.location)
41
+ for item in target.entity:
42
+ if file_extension(item) not in HTML_EXTENSIONS:
43
+ continue
44
+ file = getattr(item, "file", None)
45
+ modified_by = file.modified_by if file is not None else None
46
+ self.records.append(
47
+ BrowserFileHandlingRecord(
48
+ SiteURL=location,
49
+ File=item.properties.get("FileRef", ""),
50
+ TimeCreated=file.time_created if file is not None else None,
51
+ TimeModified=file.time_last_modified if file is not None else None,
52
+ ModifiedBy=(modified_by.login_name or modified_by.title) if modified_by is not None else None,
53
+ ScanID=report.scan_id or None,
54
+ )
55
+ )