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