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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (245) hide show
  1. office365/communications/callrecords/call_record.py +18 -0
  2. office365/communications/calls/call.py +3 -0
  3. office365/communications/presences/presence.py +3 -0
  4. office365/communications/quotas.py +66 -0
  5. office365/communications/virtualevents/virtual_event.py +3 -0
  6. office365/directory/domains/domain.py +3 -0
  7. office365/directory/licenses/subscribed_sku.py +3 -0
  8. office365/directory/objects/object.py +3 -0
  9. office365/directory/quotas.py +34 -0
  10. office365/entity_collection.py +2 -2
  11. office365/graph_client.py +46 -2
  12. office365/limits.py +66 -0
  13. office365/migration/__init__.py +6 -1
  14. office365/migration/adapters/__init__.py +14 -4
  15. office365/migration/adapters/filesystem.py +4 -1
  16. office365/migration/assessment/export.py +5 -5
  17. office365/migration/assessment/issue.py +19 -0
  18. office365/migration/assessment/report.py +11 -0
  19. office365/migration/assessment/scanners/base.py +37 -49
  20. office365/migration/base.py +33 -8
  21. office365/migration/checkpoint.py +1 -1
  22. office365/migration/outlook/assessor.py +4 -4
  23. office365/migration/package/__init__.py +82 -0
  24. office365/migration/package/builder.py +603 -0
  25. office365/migration/package/crypto.py +54 -0
  26. office365/migration/package/manifest_xml.py +495 -0
  27. office365/migration/package/staging.py +180 -0
  28. office365/migration/report.py +7 -2
  29. office365/migration/report_io.py +79 -0
  30. office365/migration/runner.py +117 -10
  31. office365/migration/server_job.py +126 -14
  32. office365/migration/session.py +132 -49
  33. office365/migration/settings.py +67 -0
  34. office365/migration/sharepoint/__init__.py +2 -0
  35. office365/migration/sharepoint/adapters.py +207 -41
  36. office365/migration/sharepoint/assessor.py +45 -9
  37. office365/migration/sharepoint/options.py +57 -0
  38. office365/migration/sharepoint/package_target.py +209 -0
  39. office365/migration/sharepoint/registry.py +22 -0
  40. office365/migration/sharepoint/resolvers.py +96 -0
  41. office365/migration/sharepoint/scanners/__init__.py +29 -0
  42. office365/migration/sharepoint/scanners/browser_file_handling.py +55 -0
  43. office365/migration/sharepoint/scanners/checked_out_files.py +57 -0
  44. office365/migration/sharepoint/scanners/file_versions.py +56 -0
  45. office365/migration/sharepoint/scanners/files.py +5 -2
  46. office365/migration/sharepoint/scanners/large_excel_files.py +56 -0
  47. office365/migration/sharepoint/scanners/large_lists.py +58 -0
  48. office365/migration/sharepoint/scanners/large_sites.py +4 -3
  49. office365/migration/sharepoint/scanners/long_onedrive_urls.py +52 -0
  50. office365/migration/sharepoint/scanners/lookup.py +38 -0
  51. office365/migration/sharepoint/scanners/paths.py +9 -4
  52. office365/migration/sharepoint/scanners/permissions.py +20 -2
  53. office365/migration/sharepoint/scanners/smat.py +51 -0
  54. office365/migration/sharepoint/scanners/summary.py +1 -0
  55. office365/migration/sharepoint/scanners/thicket_folders.py +38 -0
  56. office365/migration/sharepoint/scanners/unsupported_site_templates.py +90 -0
  57. office365/migration/sharepoint/tenant_assessor.py +5 -3
  58. office365/migration/sharepoint/transfer.py +4 -2
  59. office365/migration/tasks.py +121 -0
  60. office365/migration/validators.py +2 -19
  61. office365/onedrive/driveitems/driveItem.py +116 -12
  62. office365/onedrive/termstore/groups/collection.py +2 -9
  63. office365/onedrive/termstore/sets/collection.py +2 -9
  64. office365/onedrive/termstore/terms/collection.py +3 -10
  65. office365/onedrive/workbooks/workbook.py +7 -0
  66. office365/outlook/contacts/folders/collection.py +4 -13
  67. office365/runtime/auth/authentication_context.py +5 -0
  68. office365/runtime/client_object.py +18 -0
  69. office365/runtime/client_object_collection.py +75 -245
  70. office365/runtime/client_request.py +77 -1
  71. office365/runtime/client_request_exception.py +127 -39
  72. office365/runtime/client_runtime_context.py +79 -8
  73. office365/runtime/converters/csv_writer.py +10 -4
  74. office365/runtime/converters/dataframe.py +84 -4
  75. office365/runtime/converters/json_file.py +12 -3
  76. office365/runtime/converters/parquet.py +67 -0
  77. office365/runtime/converters/records.py +31 -12
  78. office365/runtime/converters/registry.py +185 -0
  79. office365/runtime/converters/scalars.py +31 -0
  80. office365/runtime/converters/sql.py +69 -0
  81. office365/runtime/converters/streamers.py +105 -0
  82. office365/runtime/converters/streams.py +42 -0
  83. office365/runtime/converters/upsert.py +101 -0
  84. office365/runtime/exceptions.py +105 -0
  85. office365/runtime/http/prefer.py +26 -0
  86. office365/runtime/http/throttling.py +258 -56
  87. office365/runtime/imports.py +571 -0
  88. office365/runtime/limits.py +455 -0
  89. office365/runtime/odata/request.py +11 -2
  90. office365/runtime/odata/type.py +1 -0
  91. office365/runtime/odata/v3/batch_request.py +4 -2
  92. office365/runtime/odata/v4/batch_request.py +22 -7
  93. office365/runtime/operations.py +29 -0
  94. office365/runtime/parallel.py +12 -14
  95. office365/runtime/paths/v4/entity.py +2 -2
  96. office365/runtime/queries/batch.py +18 -1
  97. office365/runtime/queries/get_or_create.py +109 -0
  98. office365/runtime/record_collection.py +443 -0
  99. office365/runtime/retry.py +92 -26
  100. office365/runtime/transport/base.py +6 -1
  101. office365/runtime/transport/requests_transport.py +4 -0
  102. office365/runtime/transport/throttled_transport.py +67 -0
  103. office365/runtime/verification.py +40 -0
  104. office365/sharepoint/activities/logger.py +118 -8
  105. office365/sharepoint/administration/analytics/usage_service.py +15 -7
  106. office365/sharepoint/administration/sitemove/service.py +791 -8
  107. office365/sharepoint/alerts/alert.py +79 -0
  108. office365/sharepoint/apps/app_collection.py +25 -0
  109. office365/sharepoint/apps/launcher.py +16 -0
  110. office365/sharepoint/authpolicy/tenant/compliancereport.py +5 -0
  111. office365/sharepoint/businessdata/app_bdc_catalog.py +135 -0
  112. office365/sharepoint/businessdata/entityfieldvaluedictionary.py +65 -0
  113. office365/sharepoint/businessdata/entityidentifier.py +23 -0
  114. office365/sharepoint/businessdata/entityinstance.py +52 -0
  115. office365/sharepoint/businessdata/entityview.py +31 -0
  116. office365/sharepoint/businessdata/externalsubscriptionstore.py +9 -0
  117. office365/sharepoint/businessdata/typedescriptor.py +37 -0
  118. office365/sharepoint/client_context.py +40 -23
  119. office365/sharepoint/clientsidecomponent/hostedapps/app.py +21 -0
  120. office365/sharepoint/clientsidecomponent/hostedapps/manager.py +16 -0
  121. office365/sharepoint/comments/comment.py +84 -2
  122. office365/sharepoint/contentcenter/machinelearning/models/model.py +0 -23
  123. office365/sharepoint/contentcenter/machinelearning/publications/publication.py +0 -9
  124. office365/sharepoint/contentcenter/machinelearning/samples/sample.py +0 -14
  125. office365/sharepoint/contentcenter/machinelearning/workitems/item.py +0 -80
  126. office365/sharepoint/contentsolution/template_api.py +148 -0
  127. office365/sharepoint/contenttypes/collection.py +32 -10
  128. office365/sharepoint/convergence/addnoliveblobsinodcservicestateapi.py +9 -0
  129. office365/sharepoint/convergence/consumer_permissions.py +15 -0
  130. office365/sharepoint/convergence/migration_complete_state_api.py +25 -0
  131. office365/sharepoint/convergence/odcmetadatacleanedupapi.py +9 -0
  132. office365/sharepoint/documentmanagement/document_id.py +12 -0
  133. office365/sharepoint/entity.py +11 -2
  134. office365/sharepoint/entity_collection.py +2 -2
  135. office365/sharepoint/exceptions.py +194 -0
  136. office365/sharepoint/fields/builtin_field_id.py +45 -34
  137. office365/sharepoint/fields/builtin_field_name.py +123 -0
  138. office365/sharepoint/fields/coercion.py +131 -0
  139. office365/sharepoint/fields/collection.py +54 -37
  140. office365/sharepoint/fields/field.py +17 -0
  141. office365/sharepoint/fields/name.py +46 -2
  142. office365/sharepoint/files/collection.py +7 -1
  143. office365/sharepoint/files/file.py +54 -8
  144. office365/sharepoint/files/versions/version.py +38 -13
  145. office365/sharepoint/folders/collection.py +4 -0
  146. office365/sharepoint/folders/folder.py +91 -21
  147. office365/sharepoint/htmlpages/html_page.py +16 -0
  148. office365/sharepoint/htmlpages/html_page_service.py +19 -0
  149. office365/sharepoint/ir/migration.py +9 -0
  150. office365/sharepoint/listitems/caml/__init__.py +31 -0
  151. office365/sharepoint/listitems/caml/builder.py +129 -0
  152. office365/sharepoint/listitems/caml/expressions.py +424 -0
  153. office365/sharepoint/listitems/caml/fields.py +68 -0
  154. office365/sharepoint/listitems/caml/guard.py +63 -0
  155. office365/sharepoint/listitems/caml/query.py +71 -2
  156. office365/sharepoint/listitems/caml/values.py +113 -0
  157. office365/sharepoint/listitems/collection.py +21 -20
  158. office365/sharepoint/listitems/upsert.py +75 -0
  159. office365/sharepoint/lists/collection.py +29 -22
  160. office365/sharepoint/lists/exporter.py +4 -3
  161. office365/sharepoint/lists/list.py +397 -39
  162. office365/sharepoint/lists/smart_wiki_library_utility.py +58 -0
  163. office365/sharepoint/logger/export.py +12 -0
  164. office365/sharepoint/marketplace/app_metadata.py +97 -1
  165. office365/sharepoint/marketplace/appstatequeryjobdefinition.py +9 -0
  166. office365/sharepoint/marketplace/corporatecuratedgallery/carddesigns/card_designs.py +35 -0
  167. office365/sharepoint/marketplace/sitecollection/appcatalog/allowed_item.py +5 -0
  168. office365/sharepoint/marketplace/tenant/appcatalog/accessor.py +107 -30
  169. office365/sharepoint/microfeed/data.py +0 -6
  170. office365/sharepoint/microservice/manager.py +61 -0
  171. office365/sharepoint/migrationcenter/common/task.py +193 -4
  172. office365/sharepoint/migrationcenter/common/task_entity_data.py +1 -1
  173. office365/sharepoint/migrationcenter/deploy_status.py +13 -0
  174. office365/sharepoint/migrationcenter/service/properties.py +20 -0
  175. office365/sharepoint/migrationcenter/service/storage.py +34 -0
  176. office365/sharepoint/migrationcenter/spflight.py +13 -0
  177. office365/sharepoint/migrationcenter/urlparseresult.py +77 -0
  178. office365/sharepoint/multigeo/service/spmultigeoproxy.py +13 -0
  179. office365/sharepoint/navigation/servicerest.py +127 -0
  180. office365/sharepoint/oauth/authentication.py +13 -0
  181. office365/sharepoint/portal/theme_manager.py +120 -8
  182. office365/sharepoint/principal/users/user.py +10 -0
  183. office365/sharepoint/publishing/campaign/campaign.py +36 -1
  184. office365/sharepoint/publishing/feedvideopage.py +0 -7
  185. office365/sharepoint/publishing/pages/3d/page.py +0 -6
  186. office365/sharepoint/publishing/pages/page.py +0 -117
  187. office365/sharepoint/publishing/pages/reposts/repost.py +0 -7
  188. office365/sharepoint/publishing/pages/topics/topic.py +0 -7
  189. office365/sharepoint/publishing/subtitlefile.py +0 -40
  190. office365/sharepoint/quotamanagement/consumer/consumer_quota_info_api.py +14 -0
  191. office365/sharepoint/quotamanagement/consumer/non_quota_backfill_api.py +23 -0
  192. office365/sharepoint/quotamanagement/consumer/nonquotamigrationapi.py +9 -0
  193. office365/sharepoint/quotamanagement/consumer/quotamigrationapi.py +13 -0
  194. office365/sharepoint/request.py +57 -16
  195. office365/sharepoint/search/administration/document_crawl_log.py +24 -7
  196. office365/sharepoint/search/administration/providers/crawl_versions_info.py +29 -0
  197. office365/sharepoint/search/administration/providers/recycle_bin_info.py +23 -0
  198. office365/sharepoint/search/administration/providers/site_content_processing_info.py +67 -0
  199. office365/sharepoint/sites/site.py +976 -65
  200. office365/sharepoint/sites/siteprovisioninginfoprovider.py +13 -0
  201. office365/sharepoint/tenant/administration/datagovernance/insight_client.py +292 -23
  202. office365/sharepoint/tenant/administration/ddiadapter.py +97 -0
  203. office365/sharepoint/tenant/administration/internal/aad/collection.py +20 -0
  204. office365/sharepoint/tenant/administration/internal/aad/permission_grant.py +0 -19
  205. office365/sharepoint/tenant/administration/internal/appservice/permission_request.py +72 -1
  206. office365/sharepoint/tenant/administration/internal/appservice/principal.py +1 -3
  207. office365/sharepoint/tenant/administration/odb_license_enforcement_service.py +26 -0
  208. office365/sharepoint/tenant/administration/sites/properties.py +564 -6
  209. office365/sharepoint/tenant/administration/spdatagovernancesarrestapiclient.py +60 -1
  210. office365/sharepoint/tenant/insights/reports/manager.py +24 -12
  211. office365/sharepoint/tenant/management/office365_tenant.py +451 -2
  212. office365/sharepoint/tenantadministration/catalog_management_collection.py +69 -0
  213. office365/sharepoint/tenantadministration/catalog_management_custom_properties_collection.py +237 -0
  214. office365/sharepoint/tenantadministration/catalog_management_restricted_content_discovery_collection.py +56 -0
  215. office365/sharepoint/tenantadministration/sp_data_governance_client_exception.py +7 -0
  216. office365/sharepoint/thresholds.py +529 -0
  217. office365/sharepoint/translation/jobs/job.py +72 -2
  218. office365/sharepoint/translation/jobs/status.py +47 -9
  219. office365/sharepoint/translation/sync_translator.py +15 -2
  220. office365/sharepoint/utilities/move_copy_util.py +2 -1
  221. office365/sharepoint/viva/connections/page.py +27 -0
  222. office365/sharepoint/viva/home.py +38 -0
  223. office365/sharepoint/viva/learningmanager.py +9 -0
  224. office365/sharepoint/viva/vivaresources.py +60 -0
  225. office365/sharepoint/webhooks/subscription.py +0 -11
  226. office365/sharepoint/webparts/definitions/definition.py +13 -0
  227. office365/sharepoint/webparts/limited_manager.py +29 -7
  228. office365/sharepoint/webs/context_web_information.py +14 -9
  229. office365/sharepoint/webs/templates/__init__.py +11 -0
  230. office365/sharepoint/webs/templates/type.py +74 -15
  231. office365/sharepoint/workflow/definition.py +6 -0
  232. office365/sharepoint/workflow/deployment_service.py +155 -0
  233. office365/sharepoint/workflow/instances/service.py +33 -0
  234. office365/sharepoint/workflow/interopservice.py +68 -0
  235. office365/sharepoint/workflow/messagingservice.py +24 -0
  236. office365/sharepoint/workflow/servicesmanager.py +41 -2
  237. office365/sharepoint/workflow/subscriptions/service.py +67 -0
  238. office365/sharepoint/workflow/subscriptions/subscription.py +31 -0
  239. office365/sharepoint/workflow/task.py +425 -0
  240. office365_rest_python_client-3.2.0.dist-info/METADATA +605 -0
  241. {office365_rest_python_client-3.1.0.dist-info → office365_rest_python_client-3.2.0.dist-info}/RECORD +242 -185
  242. office365/migration/_util.py +0 -73
  243. office365/sharepoint/listitems/caml/types.py +0 -70
  244. office365_rest_python_client-3.1.0.dist-info/METADATA +0 -475
  245. {office365_rest_python_client-3.1.0.dist-info → office365_rest_python_client-3.2.0.dist-info}/WHEEL +0 -0
@@ -1,24 +1,41 @@
1
- """Migration session — coordinate a batch of migrations.
2
-
3
- A session runs one or more :class:`MigrationJob` together. Each job is one
4
- source → target migration; add it explicitly and start the whole batch::
5
-
6
- session = MigrationSession()
7
- job = session.add_task(FileSystemSource("src"), SharePointLibraryTarget(folder))
8
- session.start()
9
- print(session.status())
10
- session.stop()
11
- session.unregister()
1
+ """Migration session — the SPMT-shaped facade over a batch of migrations.
2
+
3
+ Mirrors the ``Microsoft.SharePoint.MigrationTool.PowerShell`` cmdlets:
4
+
5
+ | cmdlet | method |
6
+ |---|---|
7
+ | ``Register-SPMTMigration`` | :meth:`MigrationSession.register` |
8
+ | ``Get-SPMTMigration`` | :meth:`MigrationSession.get` |
9
+ | ``Add-SPMTTask`` | :meth:`MigrationSession.add_task` |
10
+ | ``Remove-SPMTTask`` | :meth:`MigrationSession.remove_task` |
11
+ | ``Show-SPMTMigration`` | :meth:`MigrationSession.show` |
12
+ | ``Start-SPMTMigration`` | :meth:`MigrationSession.start` |
13
+ | ``Stop-SPMTMigration`` | :meth:`MigrationSession.stop` |
14
+ | ``Unregister-SPMTMigration`` | :meth:`MigrationSession.unregister` |
15
+
16
+ A task pairs a source and a target adapter — either supplied directly, or
17
+ declared as a :class:`~office365.migration.tasks.MigrationTask` (resolved against
18
+ the registered ``ClientContext``).
12
19
  """
13
20
 
14
21
  from __future__ import annotations
15
22
 
23
+ import itertools
24
+ from dataclasses import dataclass
25
+ from typing import TYPE_CHECKING
26
+
16
27
  from office365.migration.base import MigrationOptions, MigrationPhase
17
28
  from office365.migration.job import MigrationJob
18
29
 
30
+ if TYPE_CHECKING:
31
+ from office365.migration.settings import MigrationSettings
32
+ from office365.migration.tasks import MigrationTask
33
+
34
+ __all__ = ["MigrationSession", "SessionTask"]
35
+
19
36
 
20
37
  def _job_status(job: MigrationJob) -> dict:
21
- """Project a job's state — the status form of a session."""
38
+ """Project a job's state — the status form of a session task."""
22
39
  return {
23
40
  "source": job.source_label,
24
41
  "target": job.target_label,
@@ -33,34 +50,72 @@ def _job_status(job: MigrationJob) -> dict:
33
50
  }
34
51
 
35
52
 
36
- class MigrationSession:
37
- """A batch of :class:`MigrationJob` tasks, run together.
53
+ @dataclass
54
+ class SessionTask:
55
+ """A registered task: an id, its descriptor (when declared), and its job."""
56
+
57
+ id: str
58
+ job: MigrationJob
59
+ descriptor: "MigrationTask | None" = None
38
60
 
39
- Tasks are added explicitly with their own source/target (``options`` falls
40
- back to the session's defaults when omitted)::
61
+ def status(self) -> dict:
62
+ data = _job_status(self.job)
63
+ data["id"] = self.id
64
+ if self.descriptor is not None:
65
+ data["descriptor"] = self.descriptor.label()
66
+ return data
41
67
 
42
- session = MigrationSession()
43
- job = session.add_task(FileSystemSource("src"), SharePointLibraryTarget(folder))
44
- session.start()
45
- """
46
68
 
47
- def __init__(self, options: MigrationOptions | None = None) -> None:
69
+ class MigrationSession:
70
+ """A batch of migration tasks, driven like the SPMT PowerShell cmdlets."""
71
+
72
+ def __init__(self, options: MigrationOptions | None = None, context=None, settings=None) -> None:
48
73
  self._options = options
49
- self._jobs: list[MigrationJob] = []
74
+ self._context = context
75
+ self._settings = settings
76
+ self._tasks: list[SessionTask] = []
77
+ self._ids = itertools.count(1)
50
78
 
51
- @property
52
- def jobs(self) -> list[MigrationJob]:
53
- return self._jobs
79
+ # ── Cmdlets ──────────────────────────────────────────────────
80
+
81
+ def register(self, settings: "MigrationSettings | None" = None, context=None) -> "MigrationSession":
82
+ """Register the session — its settings and the target ``ClientContext``."""
83
+ if settings is not None:
84
+ self._settings = settings
85
+ if context is not None:
86
+ self._context = context
87
+ return self
88
+
89
+ def get(self) -> "MigrationSession":
90
+ """The registered session (``Get-SPMTMigration``)."""
91
+ return self
54
92
 
55
93
  def add_task(
56
94
  self,
57
- source,
58
- target,
95
+ source=None,
96
+ target=None,
97
+ *,
98
+ task: "MigrationTask | None" = None,
59
99
  options: MigrationOptions | None = None,
60
100
  manifest_path=None,
61
101
  checkpoint_path=None,
102
+ **descriptor,
62
103
  ) -> MigrationJob:
63
- """Add a migration task (a source → target :class:`MigrationJob`) to the session."""
104
+ """Add a task — adapters directly, a ``MigrationTask``, or task kwargs.
105
+
106
+ Descriptor kwargs (``file_share_source`` / ``target_site_url`` /
107
+ ``target_list`` …) are resolved against the registered context.
108
+ """
109
+ if task is None and descriptor:
110
+ from office365.migration.tasks import MigrationTask
111
+
112
+ task = MigrationTask(**descriptor)
113
+ if task is not None:
114
+ if self._context is None:
115
+ raise ValueError("register a ClientContext first (session.register(context=ctx))")
116
+ from office365.migration.sharepoint.resolvers import resolve_task
117
+
118
+ source, target = resolve_task(task, self._context, self._settings)
64
119
  job = MigrationJob(
65
120
  source,
66
121
  target,
@@ -68,38 +123,66 @@ class MigrationSession:
68
123
  manifest_path=manifest_path,
69
124
  checkpoint_path=checkpoint_path,
70
125
  )
71
- self._jobs.append(job)
126
+ self._tasks.append(SessionTask(str(next(self._ids)), job, task))
72
127
  return job
73
128
 
129
+ def remove_task(self, task) -> None:
130
+ """Remove a task by its ``MigrationJob``, ``SessionTask`` or id."""
131
+ for entry in list(self._tasks):
132
+ if task in (entry, entry.job, entry.id):
133
+ self._tasks.remove(entry)
134
+ return
135
+ raise KeyError(task)
136
+
137
+ def show(self) -> list[dict]:
138
+ """The per-task status (``Show-SPMTMigration``)."""
139
+ return [entry.status() for entry in self._tasks]
140
+
74
141
  def start(self, progress=None) -> list[dict]:
75
142
  """Start the migration: plan + run every registered task."""
76
143
  statuses: list[dict] = []
77
- for job in self._jobs:
78
- if job.phase in (MigrationPhase.COMPLETED, MigrationPhase.COMPLETED_WITH_ERRORS, MigrationPhase.CANCELLED):
79
- statuses.append(_job_status(job))
144
+ for entry in self._tasks:
145
+ job = entry.job
146
+ if job.phase in (
147
+ MigrationPhase.COMPLETED,
148
+ MigrationPhase.COMPLETED_WITH_ERRORS,
149
+ MigrationPhase.CANCELLED,
150
+ ):
151
+ statuses.append(entry.status())
80
152
  continue
81
153
  job.plan(progress)
82
154
  job.run(progress)
83
- statuses.append(_job_status(job))
155
+ statuses.append(entry.status())
84
156
  return statuses
85
157
 
86
- def verify(self, spot_checks: int = 20):
87
- """Reconcile source vs target for every task after migration."""
88
- return [job.verify(spot_checks) for job in self._jobs]
89
-
90
- def status(self) -> list[dict]:
91
- """Get the current per-task status."""
92
- return [_job_status(job) for job in self._jobs]
93
-
94
158
  def stop(self) -> None:
95
- """Request a clean stop at the next batch boundary."""
96
- for job in self._jobs:
97
- job.pause()
159
+ """Cancel the session (``Stop-SPMTMigration``); tasks stay resumable."""
160
+ for entry in self._tasks:
161
+ entry.job.cancel()
98
162
 
99
- def remove_task(self, job: MigrationJob) -> None:
100
- """Remove a task (job) from the session."""
101
- self._jobs.remove(job)
163
+ def pause(self) -> None:
164
+ """Request a clean stop at the next batch boundary (resumable)."""
165
+ for entry in self._tasks:
166
+ entry.job.pause()
102
167
 
103
168
  def unregister(self) -> None:
104
- """Drop all tasks and release the session."""
105
- self._jobs.clear()
169
+ """Drop all tasks and release the session (``Unregister-SPMTMigration``)."""
170
+ self._tasks.clear()
171
+
172
+ # ── State ────────────────────────────────────────────────────
173
+
174
+ @property
175
+ def tasks(self) -> list[SessionTask]:
176
+ return self._tasks
177
+
178
+ @property
179
+ def jobs(self) -> list[MigrationJob]:
180
+ return [entry.job for entry in self._tasks]
181
+
182
+ def status(self) -> list[dict]:
183
+ """The per-task status (alias of :meth:`show`)."""
184
+ return self.show()
185
+
186
+ def verify(self, spot_checks: int = 20):
187
+ """Reconcile source vs target for every task after migration."""
188
+ return [entry.job.verify(spot_checks) for entry in self._tasks]
@@ -0,0 +1,67 @@
1
+ """Migration settings — the session-level surface (SPMT ``Register-SPMTMigration``).
2
+
3
+ Mirrors the parameters of ``Microsoft.SharePoint.MigrationTool.PowerShell``'s
4
+ ``Register-SPMTMigration`` cmdlet (snake-cased) and maps the subset the core
5
+ enforces onto :class:`~office365.migration.base.MigrationOptions`. Settings that
6
+ only apply to the SPMT Windows tool (``working_folder``, ``enable_multi_round``,
7
+ ...) are kept for parity/documentation and ignored by the core.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from dataclasses import dataclass, field
13
+
14
+ from office365.migration.base import MigrationOptions
15
+
16
+ __all__ = ["MigrationSettings"]
17
+
18
+
19
+ @dataclass
20
+ class MigrationSettings:
21
+ """Session-level migration settings (the ``Register-SPMTMigration`` surface)."""
22
+
23
+ migration_type: str = "Content" # Content | Workflow
24
+ preserve_permissions: bool = False
25
+ preserve_inheritance: bool = True
26
+ preserve_versions: bool = False
27
+ keep_all_versions: bool = True
28
+ number_of_versions: int = 10
29
+ include_hidden_files: bool = True
30
+ migrate_files_created_after: str | None = None
31
+ migrate_files_modified_after: str | None = None
32
+ skip_files_with_extension: list[str] = field(default_factory=list)
33
+ skip_sites_with_name: str | None = None
34
+ skip_lists_with_name: str | None = None
35
+ skip_content_types_with_name: str | None = None
36
+ replacement_of_invalid_char: str | None = None
37
+ duplicate_page_behavior: str = "RENAME"
38
+ automatic_user_mapping: bool = True
39
+ user_mapping_file: str | None = None
40
+ # use the server-side Migration API (full fidelity: versions/ACLs) instead of
41
+ # client-side REST uploads — the SPMT path
42
+ use_migration_api: bool = False
43
+ use_custom_azure_storage: bool = False
44
+ custom_azure_storage_account: str | None = None
45
+ custom_azure_access_key: str | None = None
46
+ enable_encryption: bool = True
47
+ delete_temp_files_when_migration_done: bool = False
48
+ working_folder: str | None = None
49
+ enable_multi_round: bool = False
50
+ scan_only: bool = False
51
+ migrate_all_site_fields_and_content_types: bool = False
52
+ migrate_site_settings: str = "PRESERVE_ALL_SETTINGS"
53
+ migrate_navigation: bool = True
54
+ migrate_term_groups: bool = True
55
+ lookup_reference_policy: int = 0
56
+
57
+ def to_options(self, **overrides) -> MigrationOptions:
58
+ """Build :class:`MigrationOptions` from the settings (``overrides`` win)."""
59
+ values = {
60
+ "preserve_permissions": self.preserve_permissions,
61
+ "preserve_versions": self.preserve_versions,
62
+ "exclude_patterns": [f"*.{extension.lstrip('.')}" for extension in self.skip_files_with_extension],
63
+ "created_after": self.migrate_files_created_after,
64
+ "modified_after": self.migrate_files_modified_after,
65
+ }
66
+ values.update(overrides)
67
+ return MigrationOptions(**values)
@@ -11,6 +11,7 @@ from office365.migration.sharepoint.adapters import (
11
11
  SharePointListTarget,
12
12
  )
13
13
  from office365.migration.sharepoint.assessor import MigrationAssessor
14
+ from office365.migration.sharepoint.options import SharePointAssessmentOptions
14
15
  from office365.migration.sharepoint.registry import (
15
16
  SHAREPOINT_SCANS,
16
17
  get_scan,
@@ -42,6 +43,7 @@ __all__ = [
42
43
  "PermissionScanner",
43
44
  "SHAREPOINT_SCANS",
44
45
  "ScanTarget",
46
+ "SharePointAssessmentOptions",
45
47
  "SharePointLibrarySource",
46
48
  "SharePointLibraryTarget",
47
49
  "SharePointListSource",
@@ -13,47 +13,44 @@ from __future__ import annotations
13
13
  import hashlib
14
14
  import io
15
15
  from collections.abc import Callable
16
+ from datetime import timezone
16
17
  from typing import TYPE_CHECKING, cast
17
18
 
18
- from office365.migration._util import emit_progress, record_to_json
19
19
  from office365.migration.adapters import MigrationProgress
20
- from office365.migration.base import MigrationItem
20
+ from office365.migration.base import MigrationItem, PermissionEntry
21
21
  from office365.migration.sharepoint.transfer import Failure
22
+ from office365.runtime.converters.json_file import record_to_json
23
+ from office365.runtime.converters.scalars import iso_or_none, parse_datetime, parse_int
24
+ from office365.runtime.limits import DEFAULT_BATCH_SIZE
25
+ from office365.runtime.operations import emit_progress
26
+ from office365.sharepoint.fields.builtin_field_name import SYSTEM_FIELD_NAMES
22
27
 
23
28
  if TYPE_CHECKING:
24
29
  from office365.sharepoint.files.file import File
25
30
  from office365.sharepoint.folders.folder import Folder
26
31
  from office365.sharepoint.lists.list import List as SPList
32
+ from office365.sharepoint.permissions.roles.definitions.definition import RoleDefinition
27
33
 
28
34
  _TAXONOMY_FIELD_TYPES = {"TaxonomyFieldType", "TaxonomyFieldTypeMulti"}
29
35
 
30
- # Read-only bookkeeping columns that add no content and can require extra
31
- # permissions to project; never part of the fallback select.
32
- _SYSTEM_INTERNAL_NAMES = {
33
- "ContentTypeId",
34
- "ContentType",
35
- "Created",
36
- "Modified",
37
- "Author",
38
- "Editor",
39
- "FileRef",
40
- "FileDirRef",
41
- "FileLeafRef",
42
- "File_x0020_Type",
43
- "FSObjType",
44
- "ID",
45
- "GUID",
46
- "UniqueId",
47
- "PermMask",
48
- "MetaInfo",
49
- "owshiddenversion",
50
- "AppAuthor",
51
- "AppEditor",
52
- "VirusStatus",
53
- "ScopeId",
54
- "InstanceID",
55
- "Order",
56
- }
36
+ # Always-projected system metadata (reliable author/editor identity).
37
+ _METADATA_SELECT = ["AuthorId", "EditorId"]
38
+
39
+
40
+ def _sp_timestamp(value: str | None) -> str | None:
41
+ """Normalize an ISO-8601 timestamp to the ``...Z`` UTC form SharePoint expects.
42
+
43
+ ``ValidateUpdateListItem`` (``datesInUTC=True``) rejects the ``+00:00`` offset
44
+ form the migration items carry, so convert to a naive UTC ``Z`` string.
45
+ """
46
+ if not value:
47
+ return None
48
+ parsed = parse_datetime(value)
49
+ if parsed is None:
50
+ return value
51
+ if parsed.tzinfo is not None:
52
+ parsed = parsed.astimezone(timezone.utc).replace(tzinfo=None)
53
+ return parsed.strftime("%Y-%m-%dT%H:%M:%SZ")
57
54
 
58
55
 
59
56
  def is_taxonomy_validation(exc: Exception) -> bool:
@@ -117,9 +114,10 @@ class SharePointListSource:
117
114
 
118
115
  def _load_items(self, select: list[str] | None):
119
116
  items = self._list.items
120
- if select:
121
- items = items.select(select)
122
- return items.get_all().execute_query()
117
+ # A full read projects ``*`` plus the system metadata so author/editor ids
118
+ # are captured reliably; an explicit/safe projection is honored as-is.
119
+ columns = ["*", *_METADATA_SELECT] if select is None else select
120
+ return items.select(columns).get_all().execute_query()
123
121
 
124
122
  def _safe_select(self) -> list[str]:
125
123
  """Fallback projection: visible, non-system columns minus Managed Metadata.
@@ -138,21 +136,29 @@ class SharePointListSource:
138
136
  continue
139
137
  if internal_name.startswith("_") or getattr(field, "hidden", False):
140
138
  continue # hidden/system metadata — not safe to project
141
- if internal_name in _SYSTEM_INTERNAL_NAMES:
139
+ if internal_name in SYSTEM_FIELD_NAMES:
142
140
  continue
143
141
  columns.append(internal_name)
144
142
  return columns
145
143
 
146
144
  def _project(self, loaded, progress: MigrationProgress) -> list[MigrationItem]:
145
+ from office365.runtime.converters.records import iter_records
146
+
147
+ # Reuse the shared record projection (raw values, no JSON coercion) so the
148
+ # migration payload matches the data pipeline's.
149
+ records = iter_records(loaded, raw=True)
147
150
  result: list[MigrationItem] = []
148
- for item in loaded:
149
- record = {k: v for k, v in item.properties.items() if not str(k).startswith("__")}
151
+ for item, record in zip(loaded, records):
150
152
  self._records[str(item.id)] = record
151
153
  result.append(
152
154
  MigrationItem(
153
155
  source_path=f"{self._list.title}/{item.id}",
154
156
  dest_path=str(item.id),
155
157
  item_type="record",
158
+ modified=iso_or_none(record.get("Modified")),
159
+ created=iso_or_none(record.get("Created")),
160
+ author_id=parse_int(record.get("AuthorId")),
161
+ editor_id=parse_int(record.get("EditorId")),
156
162
  )
157
163
  )
158
164
  emit_progress(progress, done=len(result), stage="planning", items=[item])
@@ -170,19 +176,81 @@ class SharePointListSource:
170
176
 
171
177
 
172
178
  class SharePointListTarget:
173
- """Imports record payloads into a SharePoint list via ``from_records``."""
179
+ """Imports record payloads into a SharePoint list.
174
180
 
175
- def __init__(self, target_list: "SPList") -> None:
181
+ Pass ``key`` (one or more natural-key columns) to make the target idempotent:
182
+ a hash of those columns is stored in ``key_field`` and each payload is created
183
+ or (``on_conflict="upsert"``) updated, so re-runs never duplicate. Without a
184
+ key, records are appended and idempotency relies on the checkpoint/manifest.
185
+ """
186
+
187
+ def __init__(
188
+ self,
189
+ target_list: "SPList",
190
+ key: str | list[str] | None = None,
191
+ key_field: str = "MigrationKey",
192
+ on_conflict: str = "skip",
193
+ ) -> None:
176
194
  self._list = target_list
195
+ self._key_columns = [key] if isinstance(key, str) else (list(key) if key else [])
196
+ self._key_field = key_field
197
+ self._on_conflict = on_conflict
198
+ self._target = None
199
+ self._existing: dict = {}
177
200
 
178
201
  def label(self) -> str:
179
202
  return f"list:{self._list.title}"
180
203
 
181
204
  def exists(self, item: MigrationItem) -> bool:
182
- return False # records are appended; idempotency via manifest/checkpoint
205
+ # Keyed dedup happens in _queue (the key is derived from the payload, which
206
+ # exists() doesn't receive); without a key, records are appended.
207
+ return False
183
208
 
184
209
  def write(self, item: MigrationItem, payload: object) -> None:
185
- self._list.items.from_records([cast(dict, payload)])
210
+ self._queue([cast(dict, payload)])
211
+
212
+ def write_many(
213
+ self,
214
+ items: list[MigrationItem],
215
+ payloads: list[object],
216
+ concurrency: int = 1,
217
+ ) -> list[Failure]:
218
+ """Queue a chunk and flush it immediately, then discard the entities.
219
+
220
+ Flushing per chunk (instead of only at :meth:`commit`) keeps a large list
221
+ migration memory-bounded — the queued creates never accumulate for the
222
+ whole run.
223
+
224
+ Returns:
225
+ No failures (a batch failure raises; there are no partial successes).
226
+ """
227
+ self._queue([cast(dict, payload) for payload in payloads])
228
+ self._flush(len(payloads), concurrency)
229
+ return []
230
+
231
+ def _queue(self, records: list[dict]) -> None:
232
+ if not self._key_columns:
233
+ for record in records:
234
+ self._list.items.queue_records([record])
235
+ return
236
+ from office365.runtime.converters.upsert import keyed_queue
237
+ from office365.sharepoint.fields.name import internal_field_name
238
+
239
+ target = self._target
240
+ if target is None:
241
+ target = self._list.items.upsert_target(key_field=self._key_field)
242
+ target.ensure_key_field()
243
+ self._list.context.execute_query()
244
+ self._existing.update(target.load_keys())
245
+ self._target = target
246
+ key_columns = [internal_field_name(c) for c in self._key_columns]
247
+ keyed_queue(
248
+ target,
249
+ records,
250
+ key_columns=key_columns,
251
+ existing=self._existing,
252
+ on_conflict=self._on_conflict,
253
+ )
186
254
 
187
255
  def list_paths(self) -> list[str]:
188
256
  return [str(i.id) for i in self._list.items.get().execute_query()]
@@ -191,10 +259,16 @@ class SharePointListTarget:
191
259
  return ""
192
260
 
193
261
  def commit(self, options=None) -> None:
194
- """Flush the queued record writes through an OData batch (JSON-only parallel mode)."""
195
- batch_size = getattr(options, "batch_size", None) or 100
262
+ """Flush any remaining queued record writes through an OData batch."""
263
+ batch_size = getattr(options, "batch_size", None) or DEFAULT_BATCH_SIZE
196
264
  concurrency = getattr(options, "concurrency", None) or 1
265
+ self._flush(batch_size, concurrency)
266
+
267
+ def _flush(self, batch_size: int, concurrency: int) -> None:
268
+ if not self._list.context.has_pending_request:
269
+ return
197
270
  self._list.context.execute_batch(items_per_batch=batch_size, concurrency=concurrency)
271
+ self._list.items.clear()
198
272
 
199
273
  def close(self) -> None:
200
274
  pass
@@ -255,6 +329,10 @@ class SharePointLibrarySource:
255
329
  dest_path=rel,
256
330
  size_bytes=file.length or 0,
257
331
  item_type="file",
332
+ modified=iso_or_none(file.time_last_modified),
333
+ created=iso_or_none(file.time_created),
334
+ author_id=parse_int(file.author.id),
335
+ editor_id=parse_int(file.modified_by.id),
258
336
  )
259
337
  )
260
338
  emit_progress(progress, done=len(result), stage="planning", items=[file])
@@ -281,6 +359,32 @@ class SharePointLibrarySource:
281
359
  return hashlib.md5(b"").hexdigest()
282
360
  return hashlib.md5(self.read(item)).hexdigest()
283
361
 
362
+ def read_permissions(self, item: MigrationItem) -> list[PermissionEntry]:
363
+ """Read the item's role assignments (for best-effort ACL preservation).
364
+
365
+ Returns one :class:`PermissionEntry` per principal (user or group), keyed
366
+ by login name; empty for items with no unique assignments.
367
+ """
368
+ securable = self._securable(item)
369
+ if securable is None:
370
+ return []
371
+ assignments = securable.role_assignments.expand(["Member", "RoleDefinitionBindings"]).get().execute_query()
372
+ result: list[PermissionEntry] = []
373
+ for assignment in assignments:
374
+ member = assignment.member
375
+ name = member.login_name or member.user_principal_name
376
+ if not name:
377
+ continue
378
+ roles = [r.name for r in assignment.role_definition_bindings if r.name]
379
+ result.append(PermissionEntry(principal_name=name, roles=roles))
380
+ return result
381
+
382
+ def _securable(self, item: MigrationItem):
383
+ if item.item_type == "folder":
384
+ return self._folder.context.web.get_folder_by_server_relative_path(item.source_path).list_item_all_fields
385
+ file = self._files.get(item.dest_path)
386
+ return file.listItemAllFields if file is not None else None
387
+
284
388
  def close(self) -> None:
285
389
  pass
286
390
 
@@ -295,6 +399,7 @@ class SharePointLibraryTarget:
295
399
  def __init__(self, library_folder: "Folder", concurrency: int = 1) -> None:
296
400
  self._folder = library_folder
297
401
  self._concurrency = concurrency
402
+ self._role_definitions: dict[str, RoleDefinition] | None = None
298
403
 
299
404
  def label(self) -> str:
300
405
  return f"library:{self._folder.server_relative_url}"
@@ -347,6 +452,67 @@ class SharePointLibraryTarget:
347
452
  file.download(buffer).execute_query()
348
453
  return hashlib.md5(buffer.getvalue()).hexdigest()
349
454
 
455
+ def modified(self, item: MigrationItem) -> str:
456
+ """Last-modified of the target file (for incremental migration)."""
457
+ file = self._folder.context.web.get_file_by_server_relative_url(self._url(item)).get().execute_query()
458
+ return iso_or_none(file.time_last_modified) or ""
459
+
460
+ def apply_timestamps(self, item: MigrationItem) -> None:
461
+ """Best-effort: restore ``Created``/``Modified`` on the written item.
462
+
463
+ Uses ``ValidateUpdateListItem`` (the same mechanism as
464
+ ``ListItem.system_update``), which is the only REST path that can set these
465
+ system fields. ``preserve_versions`` still needs the server-side API.
466
+ """
467
+ form_values: dict[str, str] = {}
468
+ created = _sp_timestamp(item.created)
469
+ modified = _sp_timestamp(item.modified)
470
+ if created:
471
+ form_values["Created"] = created
472
+ if modified:
473
+ form_values["Modified"] = modified
474
+ if not form_values:
475
+ return
476
+ securable = self._securable(item)
477
+ if securable is None:
478
+ return
479
+ securable.validate_update_list_item(
480
+ form_values,
481
+ dates_in_utc=True,
482
+ new_document_update=item.item_type != "folder",
483
+ ).execute_query()
484
+
485
+ def apply_permissions(self, item: MigrationItem, permissions: list[PermissionEntry]) -> None:
486
+ """Best-effort: break inheritance and recreate the source role assignments.
487
+
488
+ Principals are resolved by login name on the target; roles that don't exist
489
+ there are skipped. This is a same-tenant copy — cross-tenant identity
490
+ mapping is out of scope.
491
+ """
492
+ securable = self._securable(item)
493
+ if securable is None:
494
+ return
495
+ securable.break_role_inheritance(copy_role_assignments=False, clear_sub_scopes=False)
496
+ for entry in permissions:
497
+ for role_name in entry.roles:
498
+ role = self._role_definition(role_name)
499
+ if role is not None:
500
+ securable.add_role_assignment(entry.principal_name, role)
501
+ securable.context.execute_query()
502
+
503
+ def _securable(self, item: MigrationItem):
504
+ url = self._url(item).rstrip("/")
505
+ if item.item_type == "folder":
506
+ return self._folder.context.web.get_folder_by_server_relative_path(url).list_item_all_fields
507
+ return self._folder.context.web.get_file_by_server_relative_url(url).listItemAllFields
508
+
509
+ def _role_definition(self, name: str) -> "RoleDefinition | None":
510
+ """Resolve (and cache) a target role definition by name."""
511
+ if self._role_definitions is None:
512
+ loaded = self._folder.context.web.role_definitions.get().execute_query()
513
+ self._role_definitions = {rd.name: rd for rd in loaded if rd.name}
514
+ return self._role_definitions.get(name)
515
+
350
516
  def commit(self, options=None) -> None:
351
517
  pass
352
518