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
@@ -0,0 +1,57 @@
1
+ """Checked-out files scan — the SMAT ``CheckedOutFiles`` report.
2
+
3
+ Only checked-in content is migrated: a file a user has checked out is read as its
4
+ last checked-in version, so uncommitted edits are lost. This ``ITEMS``-container
5
+ report lists the files that are checked out and by whom, and raises a per-list
6
+ warning (data-loss risk).
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from dataclasses import dataclass
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, site_url
16
+
17
+
18
+ @dataclass
19
+ class CheckedOutFilesRecord(SiteScanRecord):
20
+ """One row of the SMAT ``CheckedOutFiles-detail`` report."""
21
+
22
+ File: str | None = None
23
+ CheckedOutUser: str | None = None
24
+ ScanID: str | None = None
25
+
26
+
27
+ class CheckedOutFilesScanner(BaseScanner[CheckedOutFilesRecord]):
28
+ """Reports checked-out files (SMAT ``CheckedOutFiles``) and warns per list."""
29
+
30
+ category = "file"
31
+ scan_name = "CheckedOutFiles"
32
+ record_type = CheckedOutFilesRecord
33
+
34
+ def run(self, target: ScanTarget, report: AssessmentReport) -> None:
35
+ page: list[CheckedOutFilesRecord] = []
36
+ for item in target.entity:
37
+ file = getattr(item, "file", None)
38
+ if file is None or not file.check_out_type:
39
+ continue # 0/None = not checked out
40
+ user = file.checked_out_by_user
41
+ page.append(
42
+ CheckedOutFilesRecord(
43
+ SiteURL=site_url(target.location),
44
+ File=item.properties.get("FileRef", ""),
45
+ CheckedOutUser=(user.login_name or user.title) if user is not None else None,
46
+ ScanID=report.scan_id or None,
47
+ )
48
+ )
49
+ if page:
50
+ self.records.extend(page)
51
+ self.flag(
52
+ report,
53
+ "warning",
54
+ target.location or "list",
55
+ f"{len(page)} checked-out file(s) — only the last checked-in version migrates",
56
+ "Ask users to check in their files before migrating",
57
+ )
@@ -0,0 +1,56 @@
1
+ """File versions scan — the SMAT ``FileVersions`` report (files with version history).
2
+
3
+ Version history grows a migration's runtime roughly linearly, so before migrating
4
+ it is worth knowing which files carry versions. This ``ITEMS``-container report
5
+ scan lists each file that has more than its current version, using the file's
6
+ ``MajorVersion``/``MinorVersion`` (loaded with the item's ``File`` expand).
7
+
8
+ ``VersionCount`` is ``major + minor`` — the current version level, a close
9
+ approximation of version-history depth for major-only versioning. Site-level SMAT
10
+ columns that only exist on-premises (``ContentDB*``, usage-logging) are ``None``
11
+ and exported as ``n/a``.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ from dataclasses import dataclass
17
+
18
+ from office365.migration.assessment.report import AssessmentReport
19
+ from office365.migration.assessment.scanners.base import BaseScanner, ScanTarget
20
+ from office365.migration.sharepoint.scanners.smat import SiteScanRecord, site_url
21
+
22
+
23
+ @dataclass
24
+ class FileVersionsRecord(SiteScanRecord):
25
+ """One row of the SMAT ``FileVersions-detail`` report."""
26
+
27
+ VersionCount: int | None = None
28
+ File: str | None = None
29
+ ScanID: str | None = None
30
+
31
+
32
+ class FileVersionsScanner(BaseScanner[FileVersionsRecord]):
33
+ """Reports files that have version history (SMAT ``FileVersions``)."""
34
+
35
+ category = "file"
36
+ scan_name = "FileVersions"
37
+ record_type = FileVersionsRecord
38
+
39
+ def run(self, target: ScanTarget, report: AssessmentReport) -> None:
40
+ location = site_url(target.location)
41
+ for item in target.entity:
42
+ file = getattr(item, "file", None)
43
+ if file is None:
44
+ continue
45
+ major = file.major_version or 0
46
+ minor = file.minor_version or 0
47
+ if major <= 1 and minor == 0:
48
+ continue # only the current version — no history
49
+ self.records.append(
50
+ FileVersionsRecord(
51
+ SiteURL=location,
52
+ VersionCount=major + minor,
53
+ File=item.properties.get("FileRef", ""),
54
+ ScanID=report.scan_id or None,
55
+ )
56
+ )
@@ -4,6 +4,7 @@ from __future__ import annotations
4
4
 
5
5
  from office365.migration.assessment.report import AssessmentReport
6
6
  from office365.migration.assessment.scanners.base import BaseScanner, ScanTarget
7
+ from office365.runtime.limits import hint
7
8
 
8
9
 
9
10
  class FileScanner(BaseScanner):
@@ -12,14 +13,16 @@ class FileScanner(BaseScanner):
12
13
  category = "file"
13
14
 
14
15
  def run(self, target: ScanTarget, report: AssessmentReport) -> None:
16
+ limit = self.options.limit("large_file_bytes")
15
17
  for item in target.entity:
16
18
  path = item.properties.get("FileRef", "")
17
19
  size = (item.file.length if getattr(item, "file", None) is not None else None) or 0
18
- if size > self.options.large_file_bytes:
20
+ if size > limit.value:
19
21
  self.flag(
20
22
  report,
21
23
  "warning",
22
24
  path,
23
- f"File size {size / 1024 / 1024 / 1024:.1f}GB exceeds the size limit",
25
+ hint(limit, value=size, context="file"),
24
26
  "Use chunked upload or split the file",
27
+ limit=limit,
25
28
  )
@@ -0,0 +1,56 @@
1
+ """Large Excel files scan — the SMAT ``LargeExcelFiles`` report.
2
+
3
+ Excel workbooks larger than 10 MB can't open in the browser on the target — users
4
+ are prompted to open them in the Excel client. This ``ITEMS``-container report
5
+ lists the affected ``.xlsx``/``.xls``/``.xlsm``/``.xlsb`` files (SMAT reports the
6
+ ``.xlsx`` set; the common Excel extensions are included here).
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from dataclasses import dataclass
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
+ #: Excel workbook extensions included in the report.
18
+ EXCEL_EXTENSIONS = frozenset({".xlsx", ".xls", ".xlsm", ".xlsb"})
19
+
20
+ _MB = 1024 * 1024
21
+
22
+
23
+ @dataclass
24
+ class LargeExcelFilesRecord(SiteScanRecord):
25
+ """One row of the SMAT ``LargeExcelFiles-detail`` report."""
26
+
27
+ File: str | None = None
28
+ FileSizeinMB: float | None = None
29
+ ScanID: str | None = None
30
+
31
+
32
+ class LargeExcelFilesScanner(BaseScanner[LargeExcelFilesRecord]):
33
+ """Reports Excel workbooks over the browser-open limit (SMAT ``LargeExcelFiles``)."""
34
+
35
+ category = "file"
36
+ scan_name = "LargeExcelFiles"
37
+ record_type = LargeExcelFilesRecord
38
+
39
+ def run(self, target: ScanTarget, report: AssessmentReport) -> None:
40
+ limit = self.options.limit("large_excel_bytes")
41
+ location = site_url(target.location)
42
+ for item in target.entity:
43
+ if file_extension(item) not in EXCEL_EXTENSIONS:
44
+ continue
45
+ file = getattr(item, "file", None)
46
+ size = (file.length if file is not None else None) or 0
47
+ if size <= limit.value:
48
+ continue
49
+ self.records.append(
50
+ LargeExcelFilesRecord(
51
+ SiteURL=location,
52
+ File=item.properties.get("FileRef", ""),
53
+ FileSizeinMB=round(size / _MB, 2),
54
+ ScanID=report.scan_id or None,
55
+ )
56
+ )
@@ -0,0 +1,58 @@
1
+ """Large list scan — the SPMT ``LIST_VIEW_EXCEED_LIMIT`` /
2
+ ``ITEM_COUNT_EXCEED_INDEX_LIMIT`` / ``ITEM_COUNT_EXCEED_LIMIT`` readiness checks.
3
+
4
+ A ``LIST``-container scan over a list's item count, graded by the SharePoint
5
+ limits (``office365.sharepoint.thresholds.Limits``):
6
+
7
+ - over the list view threshold (5,000) → info (classic views throttle; migration is fine)
8
+ - over the index-add threshold (20,000) → warning (a column index can't be added)
9
+ - over the per-list maximum (30M) → blocker (migration fails)
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from office365.migration.assessment.report import AssessmentReport
15
+ from office365.migration.assessment.scanners.base import BaseScanner, ScanTarget
16
+ from office365.runtime.limits import hint
17
+
18
+ #: (threshold field, severity, SPMT risk code, remediation) — checked in order.
19
+ _CHECKS = (
20
+ ("max_list_items", "blocker", "ITEM_COUNT_EXCEED_LIMIT", "Split or archive the list before migrating"),
21
+ (
22
+ "index_threshold",
23
+ "warning",
24
+ "ITEM_COUNT_EXCEED_INDEX_LIMIT",
25
+ "Migrate without adding indexes, or split the list",
26
+ ),
27
+ (
28
+ "list_view_threshold",
29
+ "info",
30
+ "LIST_VIEW_EXCEED_LIMIT",
31
+ "Index the filtered/sorted columns or use the modern experience",
32
+ ),
33
+ )
34
+
35
+
36
+ class LargeListScanner(BaseScanner):
37
+ """Flags lists whose item count crosses the list view / index / max thresholds."""
38
+
39
+ category = "list"
40
+
41
+ def run(self, target: ScanTarget, report: AssessmentReport) -> None:
42
+ lst = target.entity
43
+ count = lst.item_count or 0
44
+ location = target.location or (lst.title or "list")
45
+
46
+ for field, severity, risk_code, suggestion in _CHECKS:
47
+ limit = self.options.limit(field)
48
+ if count > limit.value:
49
+ self.flag(
50
+ report,
51
+ severity,
52
+ location,
53
+ hint(limit, value=count, context="list"),
54
+ suggestion,
55
+ risk_code=risk_code,
56
+ limit=limit,
57
+ )
58
+ break
@@ -90,7 +90,8 @@ class LargeSitesScanner(BaseScanner[LargeSitesRecord]):
90
90
  size_gb = (summary.storage_bytes or 0) / (1024**3) if summary.storage_bytes else None
91
91
  size_mb = round((summary.storage_bytes or 0) / (1024**2), 1) if summary.storage_bytes else None
92
92
  locked = summary.lock_state in {"NoAccess", "Locked"}
93
- over = size_gb is not None and size_gb > self.options.large_site_threshold_gb
93
+ threshold = self.options.large_site_threshold_gb
94
+ over = size_gb is not None and threshold is not None and size_gb > threshold
94
95
 
95
96
  if summary.report_impacted_only and (locked or not over):
96
97
  return
@@ -109,12 +110,12 @@ class LargeSitesScanner(BaseScanner[LargeSitesRecord]):
109
110
  row.TotalItemCount = summary.item_count
110
111
  self.records.append(row)
111
112
 
112
- if over and not summary.report_impacted_only:
113
+ if over and not summary.report_impacted_only and threshold is not None and size_gb is not None:
113
114
  self.flag(
114
115
  report,
115
116
  "warning",
116
117
  summary.site_url or "site",
117
- f"Site size {size_gb:.1f}GB exceeds the {self.options.large_site_threshold_gb:g}GB guidance — "
118
+ f"Site size {size_gb:.1f}GB exceeds the {threshold:g}GB guidance — "
118
119
  "migration takes longer to schedule and run",
119
120
  "Split the site collection, archive old content, or store large binaries externally",
120
121
  )
@@ -0,0 +1,52 @@
1
+ """Long OneDrive URLs scan — the SMAT ``LongOneDriveUrls`` report.
2
+
3
+ OneDrive and SharePoint sync fail on file paths whose decoded URL exceeds the
4
+ sync limit, so before a migration it is worth listing them. This ``ITEMS``
5
+ container report measures each file's full URL (the tenant origin plus its
6
+ server-relative path) against ``Limits.LONG_ONEDRIVE_URL``.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from dataclasses import dataclass
12
+ from urllib.parse import urlsplit
13
+
14
+ from office365.migration.assessment.report import AssessmentReport
15
+ from office365.migration.assessment.scanners.base import BaseScanner, ScanTarget
16
+ from office365.migration.sharepoint.scanners.smat import SiteScanRecord, site_url
17
+
18
+
19
+ @dataclass
20
+ class LongOneDriveUrlsRecord(SiteScanRecord):
21
+ """One row of the SMAT ``LongOneDriveUrls-detail`` report."""
22
+
23
+ File: str | None = None
24
+ UrlLength: int | None = None
25
+ ScanID: str | None = None
26
+
27
+
28
+ class LongOneDriveUrlsScanner(BaseScanner[LongOneDriveUrlsRecord]):
29
+ """Reports files whose full URL is over the OneDrive sync limit (SMAT ``LongOneDriveUrls``)."""
30
+
31
+ category = "file"
32
+ scan_name = "LongOneDriveUrls"
33
+ record_type = LongOneDriveUrlsRecord
34
+
35
+ def run(self, target: ScanTarget, report: AssessmentReport) -> None:
36
+ limit = self.options.limit("long_onedrive_url")
37
+ location = site_url(target.location)
38
+ parts = urlsplit(target.location or "")
39
+ origin = f"{parts.scheme}://{parts.netloc}" if parts.scheme and parts.netloc else ""
40
+ for item in target.entity:
41
+ path = item.properties.get("FileRef", "")
42
+ url = f"{origin}{path}"
43
+ if len(url) <= limit.value:
44
+ continue
45
+ self.records.append(
46
+ LongOneDriveUrlsRecord(
47
+ SiteURL=location,
48
+ File=path,
49
+ UrlLength=len(url),
50
+ ScanID=report.scan_id or None,
51
+ )
52
+ )
@@ -0,0 +1,38 @@
1
+ """Lookup column scan — the SPMT ``LIST_VIEW_LOOKUP_EXCEED_LIMIT`` check.
2
+
3
+ A ``FIELDS``-container scan: displaying/querying too many lookup, person/group,
4
+ or managed-metadata columns trips the list view lookup threshold (8 joins
5
+ blocked, 12 returned), so the list view (and some tooling) can fail.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from office365.migration.assessment.report import AssessmentReport
11
+ from office365.migration.assessment.scanners.base import BaseScanner, ScanTarget
12
+ from office365.runtime.limits import hint
13
+
14
+ _LOOKUP_TYPES = frozenset({"Lookup", "LookupMulti", "User", "UserMulti", "TaxonomyFieldType", "TaxonomyFieldTypeMulti"})
15
+
16
+
17
+ class LookupColumnScanner(BaseScanner):
18
+ """Flags lists whose lookup/people/managed-metadata columns exceed the lookup threshold."""
19
+
20
+ category = "field"
21
+
22
+ def run(self, target: ScanTarget, report: AssessmentReport) -> None:
23
+ lookups = [
24
+ field.properties.get("InternalName", "")
25
+ for field in target.entity
26
+ if field.properties.get("TypeAsString") in _LOOKUP_TYPES
27
+ ]
28
+ limit = self.options.limit("lookup_joins")
29
+ if len(lookups) > limit.value:
30
+ self.flag(
31
+ report,
32
+ "warning",
33
+ target.location,
34
+ hint(limit, value=len(lookups), context="list"),
35
+ "Reduce the lookup columns or raise the resource-throttling limit",
36
+ risk_code="LIST_VIEW_LOOKUP_EXCEED_LIMIT",
37
+ limit=limit,
38
+ )
@@ -4,6 +4,7 @@ from __future__ import annotations
4
4
 
5
5
  from office365.migration.assessment.report import AssessmentReport
6
6
  from office365.migration.assessment.scanners.base import BaseScanner, ScanTarget
7
+ from office365.runtime.limits import hint
7
8
 
8
9
 
9
10
  class PathScanner(BaseScanner):
@@ -12,26 +13,30 @@ class PathScanner(BaseScanner):
12
13
  category = "path"
13
14
 
14
15
  def run(self, target: ScanTarget, report: AssessmentReport) -> None:
16
+ path_limit = self.options.limit("max_path_length")
17
+ name_limit = self.options.limit("max_name_length")
15
18
  for item in target.entity:
16
19
  path = item.properties.get("FileRef", "")
17
20
  name = item.properties.get("FileLeafRef", "")
18
21
 
19
- if len(path) > self.options.max_path_length:
22
+ if len(path) > path_limit.value:
20
23
  self.flag(
21
24
  report,
22
25
  "blocker",
23
26
  path,
24
- f"Path length {len(path)} exceeds SharePoint limit of {self.options.max_path_length}",
27
+ hint(path_limit, value=len(path), context="path"),
25
28
  "Shorten folder names or restructure hierarchy",
29
+ limit=path_limit,
26
30
  )
27
31
 
28
- if len(name) > self.options.max_name_length:
32
+ if len(name) > name_limit.value:
29
33
  self.flag(
30
34
  report,
31
35
  "blocker",
32
36
  path,
33
- f"File name length {len(name)} exceeds limit of {self.options.max_name_length}",
37
+ hint(name_limit, value=len(name), context="file name"),
34
38
  "Rename file before migration",
39
+ limit=name_limit,
35
40
  )
36
41
 
37
42
  bad = [c for c in name if c in self.options.invalid_chars]
@@ -4,6 +4,7 @@ from __future__ import annotations
4
4
 
5
5
  from office365.migration.assessment.report import AssessmentReport
6
6
  from office365.migration.assessment.scanners.base import BaseScanner, ScanTarget
7
+ from office365.runtime.limits import hint
7
8
 
8
9
 
9
10
  class PermissionScanner(BaseScanner):
@@ -14,11 +15,28 @@ class PermissionScanner(BaseScanner):
14
15
 
15
16
  def run(self, target: ScanTarget, report: AssessmentReport) -> None:
16
17
  unique = sum(1 for i in target.entity if i.properties.get("HasUniqueRoleAssignments", False))
17
- if unique > 0:
18
+ supported = self.options.limit("unique_scopes")
19
+ recommended = self.options.limit("recommended_unique_scopes")
20
+ if unique > supported.value:
21
+ self.flag(
22
+ report,
23
+ "blocker",
24
+ target.location,
25
+ hint(supported, value=unique, context="list"),
26
+ "Reduce the number of uniquely-permitted items before migrating",
27
+ risk_code="UNIQUE_PERMISSION_EXCEED_LIMIT",
28
+ limit=supported,
29
+ )
30
+ elif unique > 0:
31
+ over_recommended = unique > recommended.value
18
32
  self.flag(
19
33
  report,
20
34
  "warning",
21
35
  target.location,
22
- f"{unique} items have unique permissions",
36
+ hint(recommended, value=unique, context="list")
37
+ if over_recommended
38
+ else f"{unique} items have unique permissions",
23
39
  "Set preserve_permissions=True in MigrationOptions (slower migration)",
40
+ risk_code="UNIQUE_PERMISSION_EXCEED_LIMIT",
41
+ limit=recommended if over_recommended else None,
24
42
  )
@@ -0,0 +1,51 @@
1
+ """Shared SMAT detail-report scaffolding.
2
+
3
+ Every SMAT ``<Scan>-detail.csv`` starts with the same site-collection context
4
+ columns (``SiteId … DaysOfUsageData``). :class:`SiteScanRecord` declares that
5
+ prefix once; each report scan subclasses it and appends its own columns (the
6
+ dataclass fields ARE the report columns, base-first).
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from dataclasses import dataclass
12
+ from datetime import datetime
13
+ from typing import Any
14
+
15
+ __all__ = ["SiteScanRecord", "file_extension", "site_url"]
16
+
17
+
18
+ @dataclass
19
+ class SiteScanRecord:
20
+ """The SMAT detail-report site-collection column prefix.
21
+
22
+ ``None`` renders as ``n/a`` (the report convention for unavailable data);
23
+ the ``ContentDB*`` and usage-logging columns only exist on-premises.
24
+ """
25
+
26
+ SiteId: str | None = None
27
+ SiteURL: str | None = None
28
+ SiteOwner: str | None = None
29
+ SiteAdmins: str | None = None
30
+ SiteSizeInMB: float | None = None
31
+ NumOfWebs: int | None = None
32
+ ContentDBName: str | None = None
33
+ ContentDBServerName: str | None = None
34
+ ContentDBSizeInMB: str | None = None
35
+ LastContentModifiedDate: datetime | None = None
36
+ TotalItemCount: int | None = None
37
+ Hits: int | None = None
38
+ DistinctUsers: str | None = None
39
+ DaysOfUsageData: str | None = None
40
+
41
+
42
+ def file_extension(item: Any) -> str:
43
+ """The item's lower-cased extension including the dot (``""`` when none)."""
44
+ name = item.properties.get("FileLeafRef") or ""
45
+ _, dot, extension = name.rpartition(".")
46
+ return f".{extension.lower()}" if dot and extension else ""
47
+
48
+
49
+ def site_url(location: str | None) -> str | None:
50
+ """Best-effort site/web URL from a scan location (``<web>/lists/<title>``)."""
51
+ return location.rsplit("/lists/", 1)[0] if location and "/lists/" in location else None
@@ -34,4 +34,5 @@ class SiteScanSummary:
34
34
  item_count: int = 0
35
35
  last_modified: Any | None = None
36
36
  lock_state: str | None = None
37
+ web_template: str | None = None
37
38
  report_impacted_only: bool = False
@@ -0,0 +1,38 @@
1
+ """Thicket folders scan — the SPMT ``THICKET_FOLDER_UNSUPPORTED`` risk.
2
+
3
+ Folders whose name ends in ``_file`` or ``_files`` are treated as *thicket*
4
+ folders — the hidden linked folder SharePoint creates to store the supporting
5
+ content of an HTM file. SharePoint in Microsoft 365 can't create them, so a
6
+ migration carrying such a folder fails. This ``ITEMS``-container scan flags each
7
+ one so it can be renamed first.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from office365.migration.assessment.report import AssessmentReport
13
+ from office365.migration.assessment.scanners.base import BaseScanner, ScanTarget
14
+
15
+ #: Folder-name suffixes SharePoint treats as thicket folders.
16
+ _THICKET_SUFFIXES = ("_file", "_files")
17
+
18
+
19
+ class ThicketFolderScanner(BaseScanner):
20
+ """Flags folders whose name ends in ``_file``/``_files``."""
21
+
22
+ category = "file"
23
+
24
+ def run(self, target: ScanTarget, report: AssessmentReport) -> None:
25
+ for item in target.entity:
26
+ if getattr(item, "file", None) is not None:
27
+ continue # a file, not a folder
28
+ name = item.properties.get("FileLeafRef", "")
29
+ if not name.lower().endswith(_THICKET_SUFFIXES):
30
+ continue
31
+ self.flag(
32
+ report,
33
+ "blocker",
34
+ item.properties.get("FileRef", "") or name,
35
+ f"Thicket folder '{name}' isn't supported in SharePoint in Microsoft 365",
36
+ "Rename the folder so it no longer ends in '_file'/'_files'",
37
+ risk_code="THICKET_FOLDER_UNSUPPORTED",
38
+ )
@@ -0,0 +1,90 @@
1
+ """Unsupported site templates scan — the SMAT ``UnsupportedSiteTemplates`` report.
2
+
3
+ A site collection created from an on-premises-only site definition (a Central
4
+ Administration site, a My Site host, a meeting workspace, an Access services
5
+ database, ...) has no Microsoft 365 equivalent, so it can't be migrated as-is.
6
+ This ``SITE``-container report lists the collections whose root web template is
7
+ in that legacy set.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from dataclasses import dataclass
13
+
14
+ from office365.migration.assessment.report import AssessmentReport
15
+ from office365.migration.assessment.scanners.base import BaseScanner, ScanTarget
16
+ from office365.migration.sharepoint.scanners.summary import SiteScanSummary
17
+ from office365.sharepoint.webs.templates import WebTemplateType
18
+
19
+ #: Templates with no Microsoft 365 equivalent — a collection built from one can't
20
+ #: be migrated as-is. Held as template names (the part before ``#``) so the match
21
+ #: is independent of the site-definition configuration id.
22
+ _UNSUPPORTED_TEMPLATES = frozenset(
23
+ template.name_part.upper()
24
+ for template in (
25
+ WebTemplateType.CENTRAL_ADMIN,
26
+ WebTemplateType.TENANT_ADMIN,
27
+ WebTemplateType.SHAREPOINT_ONLINE_TENANT_ADMIN,
28
+ WebTemplateType.SHARED_SERVICES_ADMINISTRATION,
29
+ WebTemplateType.SHAREPOINT_PORTAL_SERVER_SITE,
30
+ WebTemplateType.SHAREPOINT_PORTAL_PERSONAL_SPACE,
31
+ WebTemplateType.PERSONALIZATION_SITE,
32
+ WebTemplateType.MY_SITE_HOST,
33
+ WebTemplateType.CONTENTS_AREA,
34
+ WebTemplateType.TOPIC_AREA,
35
+ WebTemplateType.NEWS_SITE,
36
+ WebTemplateType.NEWS_HOME,
37
+ WebTemplateType.SITE_DIRECTORY,
38
+ WebTemplateType.COMMUNITY_AREA,
39
+ WebTemplateType.COLLABORATION_PORTAL,
40
+ WebTemplateType.REPORT_CENTER,
41
+ WebTemplateType.PROFILES,
42
+ WebTemplateType.ACCESS_SERVICES,
43
+ WebTemplateType.ASSETS_WEB_DATABASE,
44
+ WebTemplateType.CHARITABLE_CONTRIBUTIONS_WEB_DATABASE,
45
+ WebTemplateType.CONTACTS_WEB_DATABASE,
46
+ WebTemplateType.ISSUES_WEB_DATABASE,
47
+ WebTemplateType.PROJECTS_WEB_DATABASE,
48
+ WebTemplateType.DOCUMENT_CENTER,
49
+ WebTemplateType.EXPRESS_TEAM_SITE,
50
+ WebTemplateType.EXPRESS_HOSTED_SITE,
51
+ WebTemplateType.MEETING_BASIC,
52
+ WebTemplateType.MEETING_BLANK,
53
+ WebTemplateType.MEETING_DECISION,
54
+ WebTemplateType.MEETING_SOCIAL,
55
+ WebTemplateType.MEETING_MULTIPAGE,
56
+ WebTemplateType.POWERPOINT_BROADCAST,
57
+ WebTemplateType.BUSINESS_INTELLIGENCE_CENTER,
58
+ WebTemplateType.VISIO_PROCESS_REPOSITORY,
59
+ )
60
+ )
61
+
62
+
63
+ @dataclass
64
+ class UnsupportedSiteTemplatesRecord:
65
+ """One row of the SMAT ``UnsupportedSiteTemplates-detail`` report."""
66
+
67
+ URL: str | None = None
68
+ Template: str | None = None
69
+ ScanID: str | None = None
70
+
71
+
72
+ class UnsupportedSiteTemplatesScanner(BaseScanner[UnsupportedSiteTemplatesRecord]):
73
+ """SITE-container scan: reports legacy site templates with no Microsoft 365 equivalent."""
74
+
75
+ category = "site"
76
+ scan_name = "UnsupportedSiteTemplates"
77
+ record_type = UnsupportedSiteTemplatesRecord
78
+
79
+ def run(self, target: ScanTarget[SiteScanSummary], report: AssessmentReport) -> None:
80
+ summary = target.entity
81
+ template = summary.web_template
82
+ if not template or template.split("#", 1)[0].upper() not in _UNSUPPORTED_TEMPLATES:
83
+ return
84
+ self.records.append(
85
+ UnsupportedSiteTemplatesRecord(
86
+ URL=summary.site_url,
87
+ Template=template,
88
+ ScanID=report.scan_id or None,
89
+ )
90
+ )