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
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""Report record IO — write record datasets in a chosen format.
|
|
2
|
+
|
|
3
|
+
Per-format, not CSV+JSON-coupled: :func:`write_dataset` picks a writer by name so
|
|
4
|
+
callers write whichever formats they need (``csv``/``json``/``ndjson``) by
|
|
5
|
+
looping, and adding a format is one registration.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from os import PathLike
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Callable, List, Optional, Sequence
|
|
13
|
+
|
|
14
|
+
RecordWriter = Callable[[List[dict], Path, Optional[Sequence[str]]], None]
|
|
15
|
+
|
|
16
|
+
_EXTENSIONS = {"csv": "csv", "json": "json", "ndjson": "ndjson"}
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _write_csv(records: list[dict], path: Path, columns: Optional[Sequence[str]] = None) -> None:
|
|
20
|
+
from office365.runtime.converters.csv_writer import write_records
|
|
21
|
+
|
|
22
|
+
with open(path, "w", newline="", encoding="utf-8") as f:
|
|
23
|
+
write_records(records, f, list(columns) if columns else None)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _write_json(records: list[dict], path: Path, columns: Optional[Sequence[str]] = None) -> None:
|
|
27
|
+
from office365.runtime.converters.json_file import write_json
|
|
28
|
+
|
|
29
|
+
with open(path, "w", encoding="utf-8") as f:
|
|
30
|
+
write_json(records, f, indent=2)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _write_ndjson(records: list[dict], path: Path, columns: Optional[Sequence[str]] = None) -> None:
|
|
34
|
+
from office365.runtime.converters.ndjson import write_ndjson
|
|
35
|
+
|
|
36
|
+
with open(path, "w", encoding="utf-8") as f:
|
|
37
|
+
write_ndjson(records, f)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
_WRITERS: dict[str, RecordWriter] = {"csv": _write_csv, "json": _write_json, "ndjson": _write_ndjson}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def write_dataset(
|
|
44
|
+
records: list[dict],
|
|
45
|
+
dir_: str | PathLike,
|
|
46
|
+
stem: str,
|
|
47
|
+
*,
|
|
48
|
+
fmt: str = "csv",
|
|
49
|
+
columns: Optional[Sequence[str]] = None,
|
|
50
|
+
) -> str:
|
|
51
|
+
"""Write ``records`` under ``dir_`` as ``stem.<ext>`` in ``fmt``; returns the path.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
records: The record rows.
|
|
55
|
+
dir_: Destination directory (created when missing).
|
|
56
|
+
stem: File stem (without extension).
|
|
57
|
+
fmt: One of ``csv``/``json``/``ndjson``.
|
|
58
|
+
columns: Optional column order for CSV.
|
|
59
|
+
"""
|
|
60
|
+
try:
|
|
61
|
+
writer = _WRITERS[fmt]
|
|
62
|
+
except KeyError:
|
|
63
|
+
raise KeyError(f"Unknown report format {fmt!r}; known: {sorted(_WRITERS)}") from None
|
|
64
|
+
path = Path(dir_) / f"{stem}.{_EXTENSIONS[fmt]}"
|
|
65
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
66
|
+
writer(records, path, columns)
|
|
67
|
+
return str(path)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def write_formats(
|
|
71
|
+
records: list[dict],
|
|
72
|
+
dir_: str | PathLike,
|
|
73
|
+
stem: str,
|
|
74
|
+
*,
|
|
75
|
+
formats: Sequence[str] = ("csv", "json"),
|
|
76
|
+
columns: Optional[Sequence[str]] = None,
|
|
77
|
+
) -> list[str]:
|
|
78
|
+
"""Write the same records in each of ``formats``; returns the written paths."""
|
|
79
|
+
return [write_dataset(records, dir_, stem, fmt=fmt, columns=columns) for fmt in formats]
|
office365/migration/runner.py
CHANGED
|
@@ -17,7 +17,6 @@ from __future__ import annotations
|
|
|
17
17
|
from pathlib import Path
|
|
18
18
|
from typing import TYPE_CHECKING, Callable, Iterable
|
|
19
19
|
|
|
20
|
-
from office365.migration._util import emit_progress
|
|
21
20
|
from office365.migration.adapters import DataTarget, resolve_dest
|
|
22
21
|
from office365.migration.base import (
|
|
23
22
|
ConflictResolution,
|
|
@@ -28,11 +27,80 @@ from office365.migration.base import (
|
|
|
28
27
|
MigrationStats,
|
|
29
28
|
)
|
|
30
29
|
from office365.migration.checkpoint import Checkpoint
|
|
30
|
+
from office365.runtime.operations import emit_progress
|
|
31
31
|
|
|
32
32
|
if TYPE_CHECKING:
|
|
33
33
|
from office365.runtime.operations import Progress
|
|
34
34
|
|
|
35
35
|
|
|
36
|
+
def _assert_fidelity_supported(options: MigrationOptions, source, target: DataTarget) -> None:
|
|
37
|
+
"""Reject fidelity flags the adapter pair cannot honor.
|
|
38
|
+
|
|
39
|
+
``preserve_versions`` always needs the server-side Migration API (REST can't
|
|
40
|
+
restore version history). ``preserve_timestamps`` / ``preserve_permissions``
|
|
41
|
+
are applied client-side on a best-effort basis, but only when the adapters
|
|
42
|
+
expose the matching optional hooks — failing fast beats silently migrating
|
|
43
|
+
without the requested fidelity.
|
|
44
|
+
"""
|
|
45
|
+
if options.preserve_versions:
|
|
46
|
+
raise NotImplementedError(
|
|
47
|
+
"preserve_versions is not supported by the client-side runner: REST cannot restore "
|
|
48
|
+
"version history. Use the server-side Migration API (MigrationServerJob) or set "
|
|
49
|
+
"preserve_versions to False."
|
|
50
|
+
)
|
|
51
|
+
if options.preserve_timestamps and not callable(getattr(target, "apply_timestamps", None)):
|
|
52
|
+
raise NotImplementedError(
|
|
53
|
+
"preserve_timestamps is not supported by this target: it does not implement apply_timestamps(item)."
|
|
54
|
+
)
|
|
55
|
+
if options.preserve_permissions and not (
|
|
56
|
+
callable(getattr(source, "read_permissions", None)) and callable(getattr(target, "apply_permissions", None))
|
|
57
|
+
):
|
|
58
|
+
raise NotImplementedError(
|
|
59
|
+
"preserve_permissions is not supported by this adapter pair: it needs "
|
|
60
|
+
"source.read_permissions(item) and target.apply_permissions(item, permissions)."
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _apply_fidelity(source, target: DataTarget, item: MigrationItem, options: MigrationOptions) -> None:
|
|
65
|
+
"""Best-effort fidelity after a successful write (timestamps / ACLs).
|
|
66
|
+
|
|
67
|
+
Failures propagate (and are captured per-item by the caller) so a requested
|
|
68
|
+
fidelity that couldn't be applied is visible and the item stays resumable.
|
|
69
|
+
"""
|
|
70
|
+
if options.preserve_timestamps:
|
|
71
|
+
_call_optional(target, "apply_timestamps", item)
|
|
72
|
+
if options.preserve_permissions:
|
|
73
|
+
permissions = source.read_permissions(item)
|
|
74
|
+
if permissions:
|
|
75
|
+
_call_optional(target, "apply_permissions", item, permissions)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class _Watermark:
|
|
79
|
+
"""Incremental watermark — the highest source ``modified`` migrated so far.
|
|
80
|
+
|
|
81
|
+
Persisted in ``Checkpoint.source_watermark``: a resumed incremental run skips
|
|
82
|
+
every item at or below it, so only new/changed items are re-scanned.
|
|
83
|
+
"""
|
|
84
|
+
|
|
85
|
+
def __init__(self, checkpoint: Checkpoint) -> None:
|
|
86
|
+
self._checkpoint = checkpoint
|
|
87
|
+
self._value = checkpoint.source_watermark
|
|
88
|
+
|
|
89
|
+
@property
|
|
90
|
+
def value(self) -> str | None:
|
|
91
|
+
return self._value
|
|
92
|
+
|
|
93
|
+
def is_stale(self, item: MigrationItem) -> bool:
|
|
94
|
+
"""Whether the item is at or below the watermark (already migrated)."""
|
|
95
|
+
return self._value is not None and item.modified is not None and item.modified <= self._value
|
|
96
|
+
|
|
97
|
+
def advance(self, item: MigrationItem) -> None:
|
|
98
|
+
"""Raise the watermark to the item's ``modified`` when it is newer."""
|
|
99
|
+
if item.modified is not None and (self._value is None or item.modified > self._value):
|
|
100
|
+
self._value = item.modified
|
|
101
|
+
self._checkpoint.source_watermark = item.modified
|
|
102
|
+
|
|
103
|
+
|
|
36
104
|
class MigrationRunner:
|
|
37
105
|
"""Executes migration items between a source and a target adapter."""
|
|
38
106
|
|
|
@@ -47,16 +115,20 @@ class MigrationRunner:
|
|
|
47
115
|
progress: Callable[["Progress"], None] | None = None,
|
|
48
116
|
stop_event: Callable[[], bool] | None = None,
|
|
49
117
|
) -> MigrationStats:
|
|
118
|
+
_assert_fidelity_supported(options, source, target)
|
|
119
|
+
watermark = _Watermark(checkpoint) if options.incremental else None
|
|
50
120
|
parallel = (
|
|
51
121
|
options.concurrency > 1
|
|
52
122
|
and hasattr(target, "write_many")
|
|
53
123
|
and options.conflict_resolution != ConflictResolution.RENAME
|
|
54
124
|
)
|
|
55
125
|
if parallel:
|
|
56
|
-
stats = self._run_parallel(
|
|
126
|
+
stats = self._run_parallel(
|
|
127
|
+
source, target, items, options, checkpoint, checkpoint_path, progress, stop_event, watermark
|
|
128
|
+
)
|
|
57
129
|
else:
|
|
58
130
|
stats = self._run_sequential(
|
|
59
|
-
source, target, items, options, checkpoint, checkpoint_path, progress, stop_event
|
|
131
|
+
source, target, items, options, checkpoint, checkpoint_path, progress, stop_event, watermark
|
|
60
132
|
)
|
|
61
133
|
_call_optional(target, "commit", options)
|
|
62
134
|
|
|
@@ -78,6 +150,7 @@ class MigrationRunner:
|
|
|
78
150
|
checkpoint_path: str | Path | None,
|
|
79
151
|
progress: Callable[["Progress"], None] | None,
|
|
80
152
|
stop_event: Callable[[], bool] | None,
|
|
153
|
+
watermark: _Watermark | None = None,
|
|
81
154
|
) -> MigrationStats:
|
|
82
155
|
items = list(items)
|
|
83
156
|
stats = MigrationStats(total=len(items))
|
|
@@ -90,12 +163,19 @@ class MigrationRunner:
|
|
|
90
163
|
if callable(stop_event) and stop_event():
|
|
91
164
|
checkpoint.phase = MigrationPhase.PAUSED
|
|
92
165
|
break
|
|
166
|
+
if watermark is not None and watermark.is_stale(item):
|
|
167
|
+
checkpoint.record(item, ItemStatus.SKIPPED)
|
|
168
|
+
stats.skipped += 1
|
|
169
|
+
self._report_progress(progress, stats, item)
|
|
170
|
+
continue
|
|
93
171
|
checkpoint.record(item, ItemStatus.IN_PROGRESS)
|
|
94
172
|
try:
|
|
95
173
|
if self._migrate(source, target, item, options):
|
|
96
174
|
checkpoint.record(item, ItemStatus.DONE)
|
|
97
175
|
stats.success += 1
|
|
98
176
|
stats.bytes_transferred += item.size_bytes
|
|
177
|
+
if watermark is not None:
|
|
178
|
+
watermark.advance(item)
|
|
99
179
|
else:
|
|
100
180
|
checkpoint.record(item, ItemStatus.SKIPPED)
|
|
101
181
|
stats.skipped += 1
|
|
@@ -119,11 +199,17 @@ class MigrationRunner:
|
|
|
119
199
|
checkpoint_path: str | Path | None,
|
|
120
200
|
progress: Callable[["Progress"], None] | None,
|
|
121
201
|
stop_event: Callable[[], bool] | None,
|
|
202
|
+
watermark: _Watermark | None = None,
|
|
122
203
|
) -> MigrationStats:
|
|
123
204
|
items = list(items)
|
|
124
205
|
stats = MigrationStats(total=len(items))
|
|
125
206
|
chunk: list[MigrationItem] = []
|
|
126
207
|
|
|
208
|
+
def _skip(item: MigrationItem) -> None:
|
|
209
|
+
checkpoint.record(item, ItemStatus.SKIPPED)
|
|
210
|
+
stats.skipped += 1
|
|
211
|
+
self._report_progress(progress, stats, item)
|
|
212
|
+
|
|
127
213
|
def _flush() -> None:
|
|
128
214
|
if not chunk:
|
|
129
215
|
return
|
|
@@ -137,9 +223,19 @@ class MigrationRunner:
|
|
|
137
223
|
checkpoint.record(item, ItemStatus.FAILED)
|
|
138
224
|
stats.errors += 1
|
|
139
225
|
else:
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
226
|
+
try:
|
|
227
|
+
_apply_fidelity(source, target, item, options)
|
|
228
|
+
except Exception as e: # noqa: BLE001 — per-item errors are captured, not fatal
|
|
229
|
+
item.error = str(e)
|
|
230
|
+
item.error_code = type(e).__name__
|
|
231
|
+
checkpoint.record(item, ItemStatus.FAILED)
|
|
232
|
+
stats.errors += 1
|
|
233
|
+
else:
|
|
234
|
+
checkpoint.record(item, ItemStatus.DONE)
|
|
235
|
+
stats.success += 1
|
|
236
|
+
stats.bytes_transferred += item.size_bytes
|
|
237
|
+
if watermark is not None:
|
|
238
|
+
watermark.advance(item)
|
|
143
239
|
self._report_progress(progress, stats, item)
|
|
144
240
|
chunk.clear()
|
|
145
241
|
if checkpoint_path is not None:
|
|
@@ -154,11 +250,12 @@ class MigrationRunner:
|
|
|
154
250
|
if callable(stop_event) and stop_event():
|
|
155
251
|
checkpoint.phase = MigrationPhase.PAUSED
|
|
156
252
|
break
|
|
253
|
+
if (watermark is not None and watermark.is_stale(item)) or _outside_window(item, options):
|
|
254
|
+
_skip(item)
|
|
255
|
+
continue
|
|
157
256
|
checkpoint.record(item, ItemStatus.IN_PROGRESS)
|
|
158
257
|
if options.incremental and _target_up_to_date(source, target, item):
|
|
159
|
-
|
|
160
|
-
stats.skipped += 1
|
|
161
|
-
self._report_progress(progress, stats, item)
|
|
258
|
+
_skip(item)
|
|
162
259
|
continue
|
|
163
260
|
if options.conflict_resolution == ConflictResolution.SKIP and target.exists(item):
|
|
164
261
|
checkpoint.record(item, ItemStatus.SKIPPED)
|
|
@@ -178,7 +275,9 @@ class MigrationRunner:
|
|
|
178
275
|
|
|
179
276
|
@staticmethod
|
|
180
277
|
def _migrate(source, target: DataTarget, item: MigrationItem, options: MigrationOptions) -> bool:
|
|
181
|
-
"""Move one item; returns ``False`` when skipped (conflict/incremental)."""
|
|
278
|
+
"""Move one item; returns ``False`` when skipped (conflict/incremental/window)."""
|
|
279
|
+
if _outside_window(item, options):
|
|
280
|
+
return False
|
|
182
281
|
if options.incremental and _target_up_to_date(source, target, item):
|
|
183
282
|
return False
|
|
184
283
|
if options.conflict_resolution == ConflictResolution.SKIP and target.exists(item):
|
|
@@ -187,7 +286,15 @@ class MigrationRunner:
|
|
|
187
286
|
item.dest_path = dest
|
|
188
287
|
payload = source.read(item)
|
|
189
288
|
target.write(item, payload)
|
|
289
|
+
_apply_fidelity(source, target, item, options)
|
|
290
|
+
return True
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def _outside_window(item: MigrationItem, options: MigrationOptions) -> bool:
|
|
294
|
+
"""Whether the item is outside the ``created_after``/``modified_after`` window (skip it)."""
|
|
295
|
+
if options.modified_after and item.modified is not None and item.modified <= options.modified_after:
|
|
190
296
|
return True
|
|
297
|
+
return bool(options.created_after and item.created is not None and item.created <= options.created_after)
|
|
191
298
|
|
|
192
299
|
|
|
193
300
|
def _target_up_to_date(source, target: DataTarget, item: MigrationItem) -> bool:
|
|
@@ -2,26 +2,68 @@
|
|
|
2
2
|
|
|
3
3
|
The SharePoint Migration API runs large migrations server-side: content is
|
|
4
4
|
packaged and uploaded to Azure Storage, then an *ingestion job* ingests it. This
|
|
5
|
-
wrapper submits such a job
|
|
6
|
-
its status with a ``Progress`` hook.
|
|
5
|
+
wrapper submits such a job and polls its status with a ``Progress`` hook.
|
|
7
6
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
7
|
+
Status can be read two ways:
|
|
8
|
+
|
|
9
|
+
- ``GetMigrationJobProgress`` — the recommended API; use :meth:`status_fn` as the
|
|
10
|
+
``monitor`` status source (or :meth:`progress` directly);
|
|
11
|
+
- a caller-supplied ``status_fn`` — e.g. an Azure queue or the Graph
|
|
12
|
+
``SharePointMigrationJobProgressEvent`` stream.
|
|
11
13
|
"""
|
|
12
14
|
|
|
13
15
|
from __future__ import annotations
|
|
14
16
|
|
|
17
|
+
import json
|
|
15
18
|
import time
|
|
16
19
|
import uuid
|
|
17
20
|
from collections.abc import Callable
|
|
18
21
|
from typing import TYPE_CHECKING
|
|
19
22
|
|
|
20
|
-
from office365.
|
|
23
|
+
from office365.runtime.operations import emit_progress
|
|
21
24
|
|
|
22
25
|
if TYPE_CHECKING:
|
|
23
26
|
from office365.runtime.operations import Progress
|
|
24
27
|
|
|
28
|
+
__all__ = ["MigrationServerJob", "job_errors", "parse_progress_events"]
|
|
29
|
+
|
|
30
|
+
_TERMINAL = ("succeeded", "completed", "failed", "cancelled")
|
|
31
|
+
|
|
32
|
+
# Migration API progress events -> monitor statuses (JobEnd resolved separately).
|
|
33
|
+
_EVENT_STATUS = {
|
|
34
|
+
"JobQueued": "queued",
|
|
35
|
+
"JobStart": "processing",
|
|
36
|
+
"JobProgress": "processing",
|
|
37
|
+
"JobError": "processing",
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def parse_progress_events(events: list[dict]) -> tuple[str, int, int | None]:
|
|
42
|
+
"""Reduce accumulated Migration API progress events to ``(status, done, total)``.
|
|
43
|
+
|
|
44
|
+
Pass the events accumulated across polls (they are append-only), so the status
|
|
45
|
+
stays monotonic even when a poll returns no new events.
|
|
46
|
+
"""
|
|
47
|
+
status = "queued"
|
|
48
|
+
done = 0
|
|
49
|
+
total: int | None = None
|
|
50
|
+
for event in events:
|
|
51
|
+
kind = event.get("Event")
|
|
52
|
+
if kind == "JobEnd":
|
|
53
|
+
status = "failed" if int(event.get("TotalErrors") or 0) else "succeeded"
|
|
54
|
+
elif kind in _EVENT_STATUS:
|
|
55
|
+
status = _EVENT_STATUS[kind]
|
|
56
|
+
if event.get("ObjectsProcessed") is not None:
|
|
57
|
+
done = int(event["ObjectsProcessed"])
|
|
58
|
+
if event.get("TotalExpectedSPObjects") is not None:
|
|
59
|
+
total = int(event["TotalExpectedSPObjects"])
|
|
60
|
+
return status, done, total
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def job_errors(events: list[dict]) -> list[dict]:
|
|
64
|
+
"""The ``JobError`` events from a progress log (each carries ``Message``/``ErrorType``/``Url``)."""
|
|
65
|
+
return [event for event in events if event.get("Event") == "JobError"]
|
|
66
|
+
|
|
25
67
|
|
|
26
68
|
class MigrationServerJob:
|
|
27
69
|
"""A server-side migration job: submit an ingestion job, then monitor it."""
|
|
@@ -34,16 +76,16 @@ class MigrationServerJob:
|
|
|
34
76
|
g_web_id,
|
|
35
77
|
azure_container_source_uri: str,
|
|
36
78
|
azure_container_manifest_uri: str,
|
|
37
|
-
azure_queue_report_uri: str,
|
|
79
|
+
azure_queue_report_uri: str | None = None,
|
|
38
80
|
ingestion_task_key: str | None = None,
|
|
39
81
|
) -> str:
|
|
40
|
-
"""Submit an ingestion job and return its job id.
|
|
82
|
+
"""Submit an ingestion job for the staged package and return its job id.
|
|
41
83
|
|
|
42
84
|
Args:
|
|
43
85
|
g_web_id: Identifier of the destination web.
|
|
44
|
-
azure_container_source_uri:
|
|
45
|
-
azure_container_manifest_uri:
|
|
46
|
-
azure_queue_report_uri: Azure queue URI receiving progress reports.
|
|
86
|
+
azure_container_source_uri: Content container URI (with SAS token).
|
|
87
|
+
azure_container_manifest_uri: Manifest container URI (with SAS token).
|
|
88
|
+
azure_queue_report_uri: Optional Azure queue URI receiving progress reports.
|
|
47
89
|
ingestion_task_key: Optional task key (a UUID is generated when omitted).
|
|
48
90
|
"""
|
|
49
91
|
result = self._site.create_migration_ingestion_job(
|
|
@@ -55,10 +97,79 @@ class MigrationServerJob:
|
|
|
55
97
|
)
|
|
56
98
|
return result.execute_query().value
|
|
57
99
|
|
|
100
|
+
def submit_encrypted(
|
|
101
|
+
self,
|
|
102
|
+
g_web_id,
|
|
103
|
+
azure_container_source_uri: str,
|
|
104
|
+
azure_container_manifest_uri: str,
|
|
105
|
+
aes256_cbc_key: str | bytes,
|
|
106
|
+
azure_queue_report_uri: str | None = None,
|
|
107
|
+
) -> str:
|
|
108
|
+
"""Submit an ingestion job for an AES-256-CBC encrypted package.
|
|
109
|
+
|
|
110
|
+
Required for SharePoint-provided containers; use the ``EncryptionKey`` from
|
|
111
|
+
``Site.provision_migration_containers`` (a base64 string, passed through).
|
|
112
|
+
"""
|
|
113
|
+
result = self._site.create_migration_job_encrypted(
|
|
114
|
+
g_web_id=g_web_id,
|
|
115
|
+
azure_container_source_uri=azure_container_source_uri,
|
|
116
|
+
azure_container_manifest_uri=azure_container_manifest_uri,
|
|
117
|
+
aes256_cbc_key=aes256_cbc_key,
|
|
118
|
+
azure_queue_report_uri=azure_queue_report_uri,
|
|
119
|
+
)
|
|
120
|
+
return result.execute_query().value
|
|
121
|
+
|
|
122
|
+
def progress(self, job_id: str, next_token: str = "0") -> tuple[list[dict], str]:
|
|
123
|
+
"""Fetch a page of progress events and the next paging token.
|
|
124
|
+
|
|
125
|
+
Args:
|
|
126
|
+
job_id: The migration job id.
|
|
127
|
+
next_token: Paging token; use ``"0"`` for the initial request.
|
|
128
|
+
|
|
129
|
+
Returns:
|
|
130
|
+
``(events, next_token)`` — events are decoded from the JSON log strings.
|
|
131
|
+
"""
|
|
132
|
+
result = self._site.get_migration_job_progress(job_id, next_token).execute_query()
|
|
133
|
+
value = result.value
|
|
134
|
+
events = [json.loads(line) for line in (value.Logs or [])]
|
|
135
|
+
return events, value.NextToken or next_token
|
|
136
|
+
|
|
137
|
+
def all_events(self, job_id: str) -> list[dict]:
|
|
138
|
+
"""All progress events for a job (paged until the token stops advancing).
|
|
139
|
+
|
|
140
|
+
For a completed job the API is idempotent per token, so this collects the
|
|
141
|
+
full lifecycle — including any ``JobError`` entries.
|
|
142
|
+
"""
|
|
143
|
+
events: list[dict] = []
|
|
144
|
+
token = "0"
|
|
145
|
+
for _ in range(100): # bound: a page with no new events ends the loop
|
|
146
|
+
page, next_token = self.progress(job_id, token)
|
|
147
|
+
events.extend(page)
|
|
148
|
+
if not page or next_token == token:
|
|
149
|
+
break
|
|
150
|
+
token = next_token
|
|
151
|
+
return events
|
|
152
|
+
|
|
153
|
+
def errors(self, job_id: str) -> list[dict]:
|
|
154
|
+
"""The ``JobError`` events for a job (message, type, url)."""
|
|
155
|
+
return job_errors(self.all_events(job_id))
|
|
156
|
+
|
|
157
|
+
def status_fn(self) -> Callable[[str], tuple[str, int, int | None]]:
|
|
158
|
+
"""A ``monitor`` status function backed by ``GetMigrationJobProgress``."""
|
|
159
|
+
state: dict = {"token": "0", "events": []}
|
|
160
|
+
|
|
161
|
+
def _status(job_id: str) -> tuple[str, int, int | None]:
|
|
162
|
+
events, token = self.progress(job_id, state["token"])
|
|
163
|
+
state["token"] = token
|
|
164
|
+
state["events"].extend(events)
|
|
165
|
+
return parse_progress_events(state["events"])
|
|
166
|
+
|
|
167
|
+
return _status
|
|
168
|
+
|
|
58
169
|
def monitor(
|
|
59
170
|
self,
|
|
60
171
|
job_id: str,
|
|
61
|
-
status_fn: Callable[[str], tuple[str, int, int | None]],
|
|
172
|
+
status_fn: Callable[[str], tuple[str, int, int | None]] | None = None,
|
|
62
173
|
interval: float = 5,
|
|
63
174
|
timeout: float = 1800,
|
|
64
175
|
progress: Callable[["Progress"], None] | None = None,
|
|
@@ -68,7 +179,7 @@ class MigrationServerJob:
|
|
|
68
179
|
Args:
|
|
69
180
|
job_id: The migration job id.
|
|
70
181
|
status_fn: Callable returning ``(status, done, total)`` for a job id.
|
|
71
|
-
|
|
182
|
+
Defaults to a ``GetMigrationJobProgress``-backed reader.
|
|
72
183
|
interval: Seconds between polls.
|
|
73
184
|
timeout: Maximum seconds to wait before raising ``TimeoutError``.
|
|
74
185
|
progress: Optional hook fired per poll with a ``Progress`` snapshot.
|
|
@@ -79,11 +190,12 @@ class MigrationServerJob:
|
|
|
79
190
|
Raises:
|
|
80
191
|
TimeoutError: When the job doesn't finish within ``timeout`` seconds.
|
|
81
192
|
"""
|
|
193
|
+
status_fn = status_fn or self.status_fn()
|
|
82
194
|
elapsed = 0.0
|
|
83
195
|
while elapsed < timeout:
|
|
84
196
|
status, done, total = status_fn(job_id)
|
|
85
197
|
emit_progress(progress, done=done, total=total, stage="migrating")
|
|
86
|
-
if status.lower() in
|
|
198
|
+
if status.lower() in _TERMINAL:
|
|
87
199
|
return status
|
|
88
200
|
time.sleep(interval)
|
|
89
201
|
elapsed += interval
|