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,603 @@
|
|
|
1
|
+
"""Package builder — assemble a SharePoint Migration API package.
|
|
2
|
+
|
|
3
|
+
Walks a document library's files and folders into the manifest XML documents
|
|
4
|
+
(``Manifest.xml`` / ``ExportSettings.xml`` / ``SystemData.xml`` / ``UserGroup.xml``)
|
|
5
|
+
plus the content blobs, ready to stage in Azure Storage and submit via
|
|
6
|
+
``Site.create_migration_job`` / ``create_migration_job_encrypted``.
|
|
7
|
+
|
|
8
|
+
The ``Manifest.xml`` shape follows the working ``MigrationApiDemo`` sample: a
|
|
9
|
+
library root ``SPFolder``, the ``SPDocumentLibrary``, then an ``SPFolder`` per
|
|
10
|
+
subfolder and an ``SPFile`` per file (no ``SPWeb``/``SPList`` objects — those are
|
|
11
|
+
the target system objects). Content blobs are named after the file's GUID and
|
|
12
|
+
referenced from ``<File FileValue="…">``; older versions get a ``<guid>.<version>``
|
|
13
|
+
blob. IDs are **deterministic** (``uuid5`` of the item path), so rebuilding the
|
|
14
|
+
same content yields the same package.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import uuid
|
|
20
|
+
from dataclasses import dataclass, field
|
|
21
|
+
from pathlib import Path, PurePosixPath
|
|
22
|
+
|
|
23
|
+
from office365.migration.package.manifest_xml import (
|
|
24
|
+
LOOKUP_LIST_MAP_NS,
|
|
25
|
+
REQUIREMENTS_NS,
|
|
26
|
+
VIEW_FORMS_LIST_NS,
|
|
27
|
+
Assignment,
|
|
28
|
+
DeploymentObject,
|
|
29
|
+
ExportSettings,
|
|
30
|
+
Group,
|
|
31
|
+
Manifest,
|
|
32
|
+
ManifestElement,
|
|
33
|
+
ManifestObject,
|
|
34
|
+
Role,
|
|
35
|
+
RoleAssignment,
|
|
36
|
+
RootObject,
|
|
37
|
+
RootObjectMap,
|
|
38
|
+
SharedWithEvent,
|
|
39
|
+
SystemData,
|
|
40
|
+
SystemObject,
|
|
41
|
+
User,
|
|
42
|
+
UserGroupMap,
|
|
43
|
+
empty_document,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
# Stable namespace so item GUIDs are reproducible across runs.
|
|
47
|
+
DEFAULT_NAMESPACE = uuid.uuid5(uuid.NAMESPACE_URL, "office365-rest-python-client/migration")
|
|
48
|
+
|
|
49
|
+
# ``SPListTemplateType`` for a document library (a free string in the schema).
|
|
50
|
+
_DOCUMENT_LIBRARY_TEMPLATE = "DocumentLibrary"
|
|
51
|
+
|
|
52
|
+
__all__ = ["DEFAULT_NAMESPACE", "Package", "PackageBuilder"]
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _normalize(path: str) -> str:
|
|
56
|
+
"""Normalize a library-relative path (no leading/trailing slash, ``/`` separators)."""
|
|
57
|
+
return path.replace("\\", "/").strip("/")
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _append_shared_with(item: ManifestObject, events: list[SharedWithEvent]) -> None:
|
|
61
|
+
"""Append "Shared with Me" events to a list item (reusing its existing block)."""
|
|
62
|
+
for child in item.children:
|
|
63
|
+
if child.name == "SharedWithEvents":
|
|
64
|
+
child.children.extend(event.to_element() for event in events)
|
|
65
|
+
return
|
|
66
|
+
item.children.append(ManifestElement("SharedWithEvents", children=[event.to_element() for event in events]))
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@dataclass
|
|
70
|
+
class Package:
|
|
71
|
+
"""An assembled migration package: manifest documents + content blobs.
|
|
72
|
+
|
|
73
|
+
``Requirements.xml`` / ``RootObjectMap.xml`` / ``LookupListMap.xml`` /
|
|
74
|
+
``ViewFormsList.xml`` are documented as optional, but the ingestion service
|
|
75
|
+
fetches them by name — a missing one fails the job with a 404.
|
|
76
|
+
"""
|
|
77
|
+
|
|
78
|
+
manifest: bytes
|
|
79
|
+
export_settings: bytes
|
|
80
|
+
system_data: bytes
|
|
81
|
+
user_group_map: bytes
|
|
82
|
+
requirements: bytes = b""
|
|
83
|
+
root_object_map: bytes = b""
|
|
84
|
+
lookup_list_map: bytes = b""
|
|
85
|
+
view_forms_list: bytes = b""
|
|
86
|
+
content: dict[str, bytes] = field(default_factory=dict)
|
|
87
|
+
|
|
88
|
+
def blobs(self) -> dict[str, bytes]:
|
|
89
|
+
"""The manifest-package blobs by file name (go to the manifest container)."""
|
|
90
|
+
return {
|
|
91
|
+
"Manifest.xml": self.manifest,
|
|
92
|
+
"ExportSettings.xml": self.export_settings,
|
|
93
|
+
"SystemData.xml": self.system_data,
|
|
94
|
+
"UserGroup.xml": self.user_group_map,
|
|
95
|
+
"Requirements.xml": self.requirements,
|
|
96
|
+
"RootObjectMap.xml": self.root_object_map,
|
|
97
|
+
"LookupListMap.xml": self.lookup_list_map,
|
|
98
|
+
"ViewFormsList.xml": self.view_forms_list,
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
def save(self, directory: str | Path) -> list[Path]:
|
|
102
|
+
"""Write the manifest files and content blobs under ``directory``.
|
|
103
|
+
|
|
104
|
+
Layout: ``<directory>/manifest/<Name>.xml`` and
|
|
105
|
+
``<directory>/content/<blob>`` — matching the two Azure containers.
|
|
106
|
+
"""
|
|
107
|
+
root = Path(directory)
|
|
108
|
+
(root / "manifest").mkdir(parents=True, exist_ok=True)
|
|
109
|
+
(root / "content").mkdir(parents=True, exist_ok=True)
|
|
110
|
+
written: list[Path] = []
|
|
111
|
+
for name, data in self.blobs().items():
|
|
112
|
+
path = root / "manifest" / name
|
|
113
|
+
path.write_bytes(data)
|
|
114
|
+
written.append(path)
|
|
115
|
+
for name, data in self.content.items():
|
|
116
|
+
path = root / "content" / name
|
|
117
|
+
path.write_bytes(data)
|
|
118
|
+
written.append(path)
|
|
119
|
+
return written
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
class PackageBuilder:
|
|
123
|
+
"""Assembles a document-library migration package."""
|
|
124
|
+
|
|
125
|
+
def __init__(
|
|
126
|
+
self,
|
|
127
|
+
site_url: str,
|
|
128
|
+
*,
|
|
129
|
+
web_id: str | None = None,
|
|
130
|
+
web_url: str = "/",
|
|
131
|
+
list_id: str | None = None,
|
|
132
|
+
list_title: str = "Documents",
|
|
133
|
+
list_url: str | None = None,
|
|
134
|
+
root_folder_id: str | None = None,
|
|
135
|
+
root_folder_parent_id: str | None = None,
|
|
136
|
+
source_type: str = "SharePointOnline",
|
|
137
|
+
include_security: str = "None",
|
|
138
|
+
namespace: uuid.UUID | None = None,
|
|
139
|
+
) -> None:
|
|
140
|
+
self._namespace = namespace or DEFAULT_NAMESPACE
|
|
141
|
+
self._web_id = web_id or str(self._guid("web"))
|
|
142
|
+
self._web_url = web_url
|
|
143
|
+
self._list_id = list_id or str(self._guid("list"))
|
|
144
|
+
self._list_title = list_title
|
|
145
|
+
self._list_url = list_url or f"{web_url.rstrip('/')}/{list_title}"
|
|
146
|
+
self._root_folder_id = root_folder_id or self._list_id
|
|
147
|
+
self._root_folder_parent_id = root_folder_parent_id or self._web_id
|
|
148
|
+
self._export = ExportSettings(
|
|
149
|
+
site_url=site_url,
|
|
150
|
+
source_type=source_type,
|
|
151
|
+
include_security=include_security,
|
|
152
|
+
export_objects=[DeploymentObject(id=self._list_id, type="List", parent_id=self._web_id)],
|
|
153
|
+
)
|
|
154
|
+
self._manifest = Manifest()
|
|
155
|
+
self._user_group_map = UserGroupMap()
|
|
156
|
+
self._content: dict[str, bytes] = {}
|
|
157
|
+
self._folders: dict[str, str] = {}
|
|
158
|
+
self._files: dict[str, str] = {}
|
|
159
|
+
self._list_item_int_id = 0
|
|
160
|
+
self._roles: list[Role] = []
|
|
161
|
+
self._role_assignments: list[RoleAssignment] = []
|
|
162
|
+
self._roles_id = str(self._guid("roles"))
|
|
163
|
+
self._role_assignments_id = str(self._guid("roleassignments"))
|
|
164
|
+
self._principal_seq = 0
|
|
165
|
+
self._security_added = False
|
|
166
|
+
self._list_items: dict[str, ManifestObject] = {}
|
|
167
|
+
self._add_root()
|
|
168
|
+
self._add_document_library()
|
|
169
|
+
|
|
170
|
+
# ── Content ──────────────────────────────────────────────────
|
|
171
|
+
|
|
172
|
+
def add_folder(
|
|
173
|
+
self,
|
|
174
|
+
path: str,
|
|
175
|
+
*,
|
|
176
|
+
time_created: str | None = None,
|
|
177
|
+
time_last_modified: str | None = None,
|
|
178
|
+
author: str | None = None,
|
|
179
|
+
editor: str | None = None,
|
|
180
|
+
) -> str:
|
|
181
|
+
"""Add a folder (and any missing parents); returns its GUID."""
|
|
182
|
+
path = _normalize(path)
|
|
183
|
+
if path in self._folders:
|
|
184
|
+
return self._folders[path]
|
|
185
|
+
parent_id = self._parent_id(path)
|
|
186
|
+
folder_id = str(self._guid(f"folder:{path}"))
|
|
187
|
+
self._manifest.add(
|
|
188
|
+
ManifestObject(
|
|
189
|
+
id=folder_id,
|
|
190
|
+
object_type="SPFolder",
|
|
191
|
+
element="Folder",
|
|
192
|
+
attributes={
|
|
193
|
+
"Id": folder_id,
|
|
194
|
+
"Name": path,
|
|
195
|
+
"Url": self._url(path),
|
|
196
|
+
"ParentFolderId": parent_id,
|
|
197
|
+
"ParentWebId": self._web_id,
|
|
198
|
+
"ParentWebUrl": self._web_url,
|
|
199
|
+
"ContainingDocumentLibrary": self._list_id,
|
|
200
|
+
"TimeCreated": time_created,
|
|
201
|
+
"TimeLastModified": time_last_modified,
|
|
202
|
+
"Author": author,
|
|
203
|
+
"ModifiedBy": editor,
|
|
204
|
+
},
|
|
205
|
+
parent_id=parent_id,
|
|
206
|
+
parent_web_id=self._web_id,
|
|
207
|
+
parent_web_url=self._web_url,
|
|
208
|
+
url=self._url(path),
|
|
209
|
+
)
|
|
210
|
+
)
|
|
211
|
+
self._folders[path] = folder_id
|
|
212
|
+
return folder_id
|
|
213
|
+
|
|
214
|
+
def add_file(
|
|
215
|
+
self,
|
|
216
|
+
path: str,
|
|
217
|
+
content: bytes,
|
|
218
|
+
*,
|
|
219
|
+
time_created: str | None = None,
|
|
220
|
+
time_last_modified: str | None = None,
|
|
221
|
+
version: str = "1.0",
|
|
222
|
+
versions: list[tuple[str, bytes]] | None = None,
|
|
223
|
+
list_item_int_id: int | None = None,
|
|
224
|
+
author: str | None = None,
|
|
225
|
+
editor: str | None = None,
|
|
226
|
+
with_list_item: bool = True,
|
|
227
|
+
fields: dict[str, str] | None = None,
|
|
228
|
+
shared_with: list[SharedWithEvent] | None = None,
|
|
229
|
+
) -> str:
|
|
230
|
+
"""Add a file (and any missing parent folders); returns its GUID.
|
|
231
|
+
|
|
232
|
+
Args:
|
|
233
|
+
path: Library-relative path.
|
|
234
|
+
content: The current version's bytes.
|
|
235
|
+
version: The current version label (e.g. ``"1.0"``).
|
|
236
|
+
versions: Older versions as ``(version, content)`` pairs.
|
|
237
|
+
list_item_int_id: Optional list-item integer id to preserve.
|
|
238
|
+
author: Original author (a login name in ``UserGroup.xml``).
|
|
239
|
+
editor: Last modified by (a login name in ``UserGroup.xml``).
|
|
240
|
+
"""
|
|
241
|
+
path = _normalize(path)
|
|
242
|
+
parent_id = self._parent_id(path)
|
|
243
|
+
file_id = str(self._guid(f"file:{path}"))
|
|
244
|
+
url = self._url(path)
|
|
245
|
+
self._content[file_id] = content
|
|
246
|
+
self._list_item_int_id += 1
|
|
247
|
+
version_rows: list[ManifestElement] = []
|
|
248
|
+
for older_version, older_content in versions or []:
|
|
249
|
+
blob = f"{file_id}.{older_version}"
|
|
250
|
+
self._content[blob] = older_content
|
|
251
|
+
version_rows.append(
|
|
252
|
+
ManifestElement(
|
|
253
|
+
"File",
|
|
254
|
+
{
|
|
255
|
+
"Id": file_id,
|
|
256
|
+
"Name": path,
|
|
257
|
+
"Url": url,
|
|
258
|
+
"ParentWebId": self._web_id,
|
|
259
|
+
"ParentWebUrl": self._web_url,
|
|
260
|
+
"ParentId": parent_id,
|
|
261
|
+
"ListId": self._list_id,
|
|
262
|
+
"FileValue": blob,
|
|
263
|
+
"Version": older_version,
|
|
264
|
+
},
|
|
265
|
+
)
|
|
266
|
+
)
|
|
267
|
+
children = [ManifestElement("Versions", children=version_rows)] if version_rows else []
|
|
268
|
+
self._manifest.add(
|
|
269
|
+
ManifestObject(
|
|
270
|
+
id=file_id,
|
|
271
|
+
object_type="SPFile",
|
|
272
|
+
element="File",
|
|
273
|
+
attributes={
|
|
274
|
+
"Id": file_id,
|
|
275
|
+
"Name": path,
|
|
276
|
+
"Url": url,
|
|
277
|
+
"ParentId": parent_id,
|
|
278
|
+
"ParentWebId": self._web_id,
|
|
279
|
+
"ParentWebUrl": self._web_url,
|
|
280
|
+
"ListId": self._list_id,
|
|
281
|
+
"FileValue": file_id,
|
|
282
|
+
"ListItemIntId": list_item_int_id or self._list_item_int_id,
|
|
283
|
+
"Version": version,
|
|
284
|
+
"TimeCreated": time_created,
|
|
285
|
+
"TimeLastModified": time_last_modified,
|
|
286
|
+
"Author": author,
|
|
287
|
+
"ModifiedBy": editor,
|
|
288
|
+
},
|
|
289
|
+
children=children,
|
|
290
|
+
parent_id=parent_id,
|
|
291
|
+
parent_web_id=self._web_id,
|
|
292
|
+
parent_web_url=self._web_url,
|
|
293
|
+
url=url,
|
|
294
|
+
)
|
|
295
|
+
)
|
|
296
|
+
self._files[path] = file_id
|
|
297
|
+
if with_list_item:
|
|
298
|
+
# the service silently skips an SPFile that has no matching SPListItem
|
|
299
|
+
self.add_list_item(
|
|
300
|
+
url,
|
|
301
|
+
doc_id=file_id,
|
|
302
|
+
parent_folder_id=parent_id,
|
|
303
|
+
name=path,
|
|
304
|
+
int_id=list_item_int_id or self._list_item_int_id,
|
|
305
|
+
version=version,
|
|
306
|
+
time_created=time_created,
|
|
307
|
+
time_last_modified=time_last_modified,
|
|
308
|
+
fields=fields if fields is not None else {"Title": PurePosixPath(path).name},
|
|
309
|
+
shared_with=shared_with,
|
|
310
|
+
)
|
|
311
|
+
return file_id
|
|
312
|
+
|
|
313
|
+
def add_user(
|
|
314
|
+
self,
|
|
315
|
+
login: str,
|
|
316
|
+
*,
|
|
317
|
+
name: str | None = None,
|
|
318
|
+
email: str | None = None,
|
|
319
|
+
system_id: str | None = None,
|
|
320
|
+
is_site_admin: bool = False,
|
|
321
|
+
) -> int:
|
|
322
|
+
"""Register a user in ``UserGroup.xml``; returns its integer id."""
|
|
323
|
+
user_id = self._next_principal_id()
|
|
324
|
+
self._user_group_map.add_user(
|
|
325
|
+
User(
|
|
326
|
+
id=user_id,
|
|
327
|
+
name=name or login,
|
|
328
|
+
login=login,
|
|
329
|
+
email=email,
|
|
330
|
+
system_id=system_id,
|
|
331
|
+
is_site_admin=is_site_admin,
|
|
332
|
+
)
|
|
333
|
+
)
|
|
334
|
+
return user_id
|
|
335
|
+
|
|
336
|
+
def add_group(
|
|
337
|
+
self,
|
|
338
|
+
name: str,
|
|
339
|
+
*,
|
|
340
|
+
description: str | None = None,
|
|
341
|
+
owner: int | None = None,
|
|
342
|
+
members: list[int] | None = None,
|
|
343
|
+
) -> int:
|
|
344
|
+
"""Register a SharePoint group in ``UserGroup.xml``; returns its integer id."""
|
|
345
|
+
group_id = self._next_principal_id()
|
|
346
|
+
self._user_group_map.groups.append(
|
|
347
|
+
Group(id=group_id, name=name, description=description, owner=owner, members=list(members or []))
|
|
348
|
+
)
|
|
349
|
+
return group_id
|
|
350
|
+
|
|
351
|
+
def add_role(
|
|
352
|
+
self,
|
|
353
|
+
role_id: str,
|
|
354
|
+
title: str,
|
|
355
|
+
perm_mask: str,
|
|
356
|
+
*,
|
|
357
|
+
hidden: bool = False,
|
|
358
|
+
description: str | None = None,
|
|
359
|
+
role_order: str | None = None,
|
|
360
|
+
role_type: str | None = None,
|
|
361
|
+
) -> None:
|
|
362
|
+
"""Register a permission level (a ``DeploymentRoles`` ``<Role>``)."""
|
|
363
|
+
if any(role.id == role_id for role in self._roles):
|
|
364
|
+
return
|
|
365
|
+
self._roles.append(Role(role_id, title, perm_mask, hidden, description, role_order, role_type))
|
|
366
|
+
|
|
367
|
+
def add_role_assignment(
|
|
368
|
+
self,
|
|
369
|
+
*,
|
|
370
|
+
object_id: str,
|
|
371
|
+
object_url: str,
|
|
372
|
+
object_type: str = "2",
|
|
373
|
+
role_def_web_id: str,
|
|
374
|
+
role_def_web_url: str,
|
|
375
|
+
assignments: list[tuple[str, int]],
|
|
376
|
+
scope_id: str | None = None,
|
|
377
|
+
anonymous_perm_mask: str = "0",
|
|
378
|
+
) -> None:
|
|
379
|
+
"""Register an ACL scope — ``(role_id, principal_id)`` grants on an object.
|
|
380
|
+
|
|
381
|
+
Args:
|
|
382
|
+
object_id/object_url: the secured object (a file/folder GUID + URL).
|
|
383
|
+
object_type: the secured-object kind — a numeric enum the service parses
|
|
384
|
+
(``0`` web, ``1`` list, ``2`` item/file; live-validated: ``2`` breaks
|
|
385
|
+
inheritance for a file).
|
|
386
|
+
role_def_web_id/role_def_web_url: the web whose role definitions apply.
|
|
387
|
+
assignments: ``(RoleId, PrincipalId)`` pairs (principal ids from ``UserGroup.xml``).
|
|
388
|
+
scope_id: the scope the assignment applies to (defaults to ``object_id``).
|
|
389
|
+
"""
|
|
390
|
+
self._role_assignments.append(
|
|
391
|
+
RoleAssignment(
|
|
392
|
+
object_id=object_id,
|
|
393
|
+
object_url=object_url,
|
|
394
|
+
object_type=object_type,
|
|
395
|
+
role_def_web_id=role_def_web_id,
|
|
396
|
+
role_def_web_url=role_def_web_url,
|
|
397
|
+
scope_id=scope_id or object_id,
|
|
398
|
+
assignments=[Assignment(role_id=role, principal_id=principal) for role, principal in assignments],
|
|
399
|
+
anonymous_perm_mask=anonymous_perm_mask,
|
|
400
|
+
)
|
|
401
|
+
)
|
|
402
|
+
|
|
403
|
+
def add_list_item(
|
|
404
|
+
self,
|
|
405
|
+
file_url: str,
|
|
406
|
+
*,
|
|
407
|
+
item_id: str | None = None,
|
|
408
|
+
doc_id: str | None = None,
|
|
409
|
+
parent_folder_id: str | None = None,
|
|
410
|
+
name: str | None = None,
|
|
411
|
+
int_id: int | None = None,
|
|
412
|
+
version: str = "1.0",
|
|
413
|
+
time_created: str | None = None,
|
|
414
|
+
time_last_modified: str | None = None,
|
|
415
|
+
fields: dict[str, str] | None = None,
|
|
416
|
+
shared_with: list[SharedWithEvent] | None = None,
|
|
417
|
+
) -> str:
|
|
418
|
+
"""Add an ``SPListItem`` for a file (metadata + "Shared with Me" events); returns its GUID.
|
|
419
|
+
|
|
420
|
+
``doc_id`` links the item to its ``SPFile`` (``DocType=File``); ``fields``
|
|
421
|
+
are the item's field values (the serializer requires a ``<Fields>`` member).
|
|
422
|
+
"""
|
|
423
|
+
existing = self._list_items.get(file_url)
|
|
424
|
+
if existing is not None:
|
|
425
|
+
if shared_with:
|
|
426
|
+
_append_shared_with(existing, shared_with)
|
|
427
|
+
return existing.id
|
|
428
|
+
item_id = item_id or str(self._guid(f"listitem:{file_url}"))
|
|
429
|
+
children = [
|
|
430
|
+
ManifestElement(
|
|
431
|
+
"Fields",
|
|
432
|
+
children=[
|
|
433
|
+
ManifestElement("Field", {"Name": field, "Value": value, "Type": "Text"})
|
|
434
|
+
for field, value in (fields or {}).items()
|
|
435
|
+
],
|
|
436
|
+
)
|
|
437
|
+
]
|
|
438
|
+
if shared_with:
|
|
439
|
+
children.append(ManifestElement("SharedWithEvents", children=[event.to_element() for event in shared_with]))
|
|
440
|
+
item = ManifestObject(
|
|
441
|
+
id=item_id,
|
|
442
|
+
object_type="SPListItem",
|
|
443
|
+
element="ListItem",
|
|
444
|
+
attributes={
|
|
445
|
+
"Id": item_id,
|
|
446
|
+
"ParentWebId": self._web_id,
|
|
447
|
+
"ParentWebUrl": self._web_url,
|
|
448
|
+
"TimeCreated": time_created,
|
|
449
|
+
"TimeLastModified": time_last_modified,
|
|
450
|
+
"FileUrl": file_url,
|
|
451
|
+
"DocType": "File",
|
|
452
|
+
"ParentFolderId": parent_folder_id or self._root_folder_id,
|
|
453
|
+
"ParentListId": self._list_id,
|
|
454
|
+
"Name": name or file_url.rsplit("/", 1)[-1],
|
|
455
|
+
"DirName": self._list_url,
|
|
456
|
+
"IntId": int_id,
|
|
457
|
+
"DocId": doc_id,
|
|
458
|
+
"Version": version,
|
|
459
|
+
"ModerationStatus": "Approved",
|
|
460
|
+
},
|
|
461
|
+
children=children,
|
|
462
|
+
parent_id=self._list_id,
|
|
463
|
+
parent_web_id=self._web_id,
|
|
464
|
+
parent_web_url=self._web_url,
|
|
465
|
+
url=file_url,
|
|
466
|
+
)
|
|
467
|
+
self._manifest.add(item)
|
|
468
|
+
self._list_items[file_url] = item
|
|
469
|
+
return item_id
|
|
470
|
+
|
|
471
|
+
def build(self) -> Package:
|
|
472
|
+
"""Render the manifest documents and freeze the content blobs."""
|
|
473
|
+
self._add_security_objects()
|
|
474
|
+
system_data = SystemData(
|
|
475
|
+
system_objects=[
|
|
476
|
+
SystemObject(id=self._web_id, type="Web", url=self._web_url),
|
|
477
|
+
SystemObject(id=self._list_id, type="List", url=self._list_url),
|
|
478
|
+
]
|
|
479
|
+
)
|
|
480
|
+
root_object_map = RootObjectMap(
|
|
481
|
+
objects=[
|
|
482
|
+
RootObject(
|
|
483
|
+
id=self._list_id,
|
|
484
|
+
type="List",
|
|
485
|
+
parent_id=self._web_id,
|
|
486
|
+
web_url=self._web_url,
|
|
487
|
+
url=self._list_url,
|
|
488
|
+
)
|
|
489
|
+
]
|
|
490
|
+
)
|
|
491
|
+
return Package(
|
|
492
|
+
manifest=self._manifest.to_xml(),
|
|
493
|
+
export_settings=self._export.to_xml(),
|
|
494
|
+
system_data=system_data.to_xml(),
|
|
495
|
+
user_group_map=self._user_group_map.to_xml(),
|
|
496
|
+
requirements=empty_document(REQUIREMENTS_NS, "Requirements"),
|
|
497
|
+
root_object_map=root_object_map.to_xml(),
|
|
498
|
+
lookup_list_map=empty_document(LOOKUP_LIST_MAP_NS, "LookupLists"),
|
|
499
|
+
view_forms_list=empty_document(VIEW_FORMS_LIST_NS, "ViewFormsList"),
|
|
500
|
+
content=dict(self._content),
|
|
501
|
+
)
|
|
502
|
+
|
|
503
|
+
# ── Manifest objects ─────────────────────────────────────────
|
|
504
|
+
|
|
505
|
+
def _add_root(self) -> None:
|
|
506
|
+
"""The library root folder (``SPFolder``) — the package's entry point."""
|
|
507
|
+
self._manifest.add(
|
|
508
|
+
ManifestObject(
|
|
509
|
+
id=self._root_folder_id,
|
|
510
|
+
object_type="SPFolder",
|
|
511
|
+
element="Folder",
|
|
512
|
+
attributes={
|
|
513
|
+
"Id": self._root_folder_id,
|
|
514
|
+
"Name": self._list_name,
|
|
515
|
+
"Url": self._list_name,
|
|
516
|
+
"ParentFolderId": self._root_folder_parent_id,
|
|
517
|
+
"ParentWebId": self._web_id,
|
|
518
|
+
"ParentWebUrl": self._web_url,
|
|
519
|
+
"ContainingDocumentLibrary": self._list_id,
|
|
520
|
+
},
|
|
521
|
+
parent_id=self._root_folder_parent_id,
|
|
522
|
+
parent_web_id=self._web_id,
|
|
523
|
+
parent_web_url=self._web_url,
|
|
524
|
+
url=self._list_url,
|
|
525
|
+
)
|
|
526
|
+
)
|
|
527
|
+
|
|
528
|
+
def _add_document_library(self) -> None:
|
|
529
|
+
"""The target document library (``SPDocumentLibrary``)."""
|
|
530
|
+
self._manifest.add(
|
|
531
|
+
ManifestObject(
|
|
532
|
+
id=self._list_id,
|
|
533
|
+
object_type="SPDocumentLibrary",
|
|
534
|
+
element="DocumentLibrary",
|
|
535
|
+
attributes={
|
|
536
|
+
"Id": self._list_id,
|
|
537
|
+
"BaseTemplate": _DOCUMENT_LIBRARY_TEMPLATE,
|
|
538
|
+
"ParentWebId": self._web_id,
|
|
539
|
+
"ParentWebUrl": self._web_url,
|
|
540
|
+
"RootFolderId": self._root_folder_id,
|
|
541
|
+
"RootFolderUrl": self._list_url,
|
|
542
|
+
"Title": self._list_title,
|
|
543
|
+
},
|
|
544
|
+
parent_id=self._web_id,
|
|
545
|
+
parent_web_id=self._web_id,
|
|
546
|
+
parent_web_url=self._web_url,
|
|
547
|
+
url=self._list_url,
|
|
548
|
+
)
|
|
549
|
+
)
|
|
550
|
+
|
|
551
|
+
# ── Helpers ──────────────────────────────────────────────────
|
|
552
|
+
|
|
553
|
+
@property
|
|
554
|
+
def _list_name(self) -> str:
|
|
555
|
+
return self._list_url.rstrip("/").rsplit("/", 1)[-1]
|
|
556
|
+
|
|
557
|
+
def _add_security_objects(self) -> None:
|
|
558
|
+
"""Emit the ``DeploymentRoles`` / ``DeploymentRoleAssignments`` objects (once)."""
|
|
559
|
+
if self._security_added:
|
|
560
|
+
return
|
|
561
|
+
self._security_added = True
|
|
562
|
+
if self._roles:
|
|
563
|
+
self._manifest.add(
|
|
564
|
+
ManifestObject(
|
|
565
|
+
id=self._roles_id,
|
|
566
|
+
object_type="DeploymentRoles",
|
|
567
|
+
element="Roles",
|
|
568
|
+
children=[role.to_element() for role in self._roles],
|
|
569
|
+
parent_id=self._web_id,
|
|
570
|
+
parent_web_id=self._web_id,
|
|
571
|
+
parent_web_url=self._web_url,
|
|
572
|
+
)
|
|
573
|
+
)
|
|
574
|
+
if self._role_assignments:
|
|
575
|
+
self._manifest.add(
|
|
576
|
+
ManifestObject(
|
|
577
|
+
id=self._role_assignments_id,
|
|
578
|
+
object_type="DeploymentRoleAssignments",
|
|
579
|
+
element="RoleAssignments",
|
|
580
|
+
children=[assignment.to_element() for assignment in self._role_assignments],
|
|
581
|
+
parent_id=self._web_id,
|
|
582
|
+
parent_web_id=self._web_id,
|
|
583
|
+
parent_web_url=self._web_url,
|
|
584
|
+
)
|
|
585
|
+
)
|
|
586
|
+
|
|
587
|
+
def _next_principal_id(self) -> int:
|
|
588
|
+
self._principal_seq += 1
|
|
589
|
+
return self._principal_seq
|
|
590
|
+
|
|
591
|
+
def _guid(self, key: str) -> uuid.UUID:
|
|
592
|
+
return uuid.uuid5(self._namespace, key)
|
|
593
|
+
|
|
594
|
+
def _url(self, path: str) -> str:
|
|
595
|
+
"""The website-relative URL of an item (no leading slash, per the schema)."""
|
|
596
|
+
return f"{self._list_url.strip('/')}/{path}"
|
|
597
|
+
|
|
598
|
+
def _parent_id(self, path: str) -> str:
|
|
599
|
+
"""The parent folder's GUID (the library root when top-level)."""
|
|
600
|
+
parent = PurePosixPath(path).parent
|
|
601
|
+
if str(parent) in (".", ""):
|
|
602
|
+
return self._root_folder_id
|
|
603
|
+
return self.add_folder(str(parent))
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""AES-256-CBC helpers for SharePoint Migration API packages.
|
|
2
|
+
|
|
3
|
+
SharePoint-provided migration containers require the content **and** manifest
|
|
4
|
+
blobs to be encrypted client-side with the AES256CBC key returned by
|
|
5
|
+
``Site.provision_migration_containers``. Each blob gets a unique random IV, stored
|
|
6
|
+
as its base64 ``IV`` blob property.
|
|
7
|
+
|
|
8
|
+
Requires the ``[azure]`` extra (``cryptography``).
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import base64
|
|
14
|
+
import os
|
|
15
|
+
|
|
16
|
+
__all__ = ["decrypt", "encrypt", "normalize_key"]
|
|
17
|
+
|
|
18
|
+
_HINT = "AES encryption requires the 'azure' extra: pip install office365-rest-python-client[azure]"
|
|
19
|
+
|
|
20
|
+
_BLOCK_SIZE = 128
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def normalize_key(value: str | bytes) -> bytes:
|
|
24
|
+
"""The raw AES key — a base64 string is decoded, bytes are used as-is."""
|
|
25
|
+
return base64.b64decode(value) if isinstance(value, str) else value
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def encrypt(data: bytes, key: bytes) -> tuple[bytes, str]:
|
|
29
|
+
"""AES-256-CBC encrypt ``data``; returns ``(ciphertext, base64 IV)``."""
|
|
30
|
+
Cipher, algorithms, modes, padding = _crypto()
|
|
31
|
+
iv = os.urandom(algorithms.AES.block_size // 8)
|
|
32
|
+
padder = padding.PKCS7(_BLOCK_SIZE).padder()
|
|
33
|
+
padded = padder.update(data) + padder.finalize()
|
|
34
|
+
encryptor = Cipher(algorithms.AES(key), modes.CBC(iv)).encryptor()
|
|
35
|
+
return encryptor.update(padded) + encryptor.finalize(), base64.b64encode(iv).decode("ascii")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def decrypt(data: bytes, key: bytes, iv: str) -> bytes:
|
|
39
|
+
"""AES-256-CBC decrypt ``data`` given the base64 ``iv`` (used by tests)."""
|
|
40
|
+
Cipher, algorithms, modes, padding = _crypto()
|
|
41
|
+
decryptor = Cipher(algorithms.AES(key), modes.CBC(base64.b64decode(iv))).decryptor()
|
|
42
|
+
padded = decryptor.update(data) + decryptor.finalize()
|
|
43
|
+
unpadder = padding.PKCS7(_BLOCK_SIZE).unpadder()
|
|
44
|
+
return unpadder.update(padded) + unpadder.finalize()
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _crypto():
|
|
48
|
+
"""Lazily import the ``cryptography`` primitives (optional extra)."""
|
|
49
|
+
try:
|
|
50
|
+
from cryptography.hazmat.primitives import padding
|
|
51
|
+
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
|
|
52
|
+
except ImportError as exc: # pragma: no cover - exercised only without the extra
|
|
53
|
+
raise ImportError(_HINT) from exc
|
|
54
|
+
return Cipher, algorithms, modes, padding
|