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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (245) hide show
  1. office365/communications/callrecords/call_record.py +18 -0
  2. office365/communications/calls/call.py +3 -0
  3. office365/communications/presences/presence.py +3 -0
  4. office365/communications/quotas.py +66 -0
  5. office365/communications/virtualevents/virtual_event.py +3 -0
  6. office365/directory/domains/domain.py +3 -0
  7. office365/directory/licenses/subscribed_sku.py +3 -0
  8. office365/directory/objects/object.py +3 -0
  9. office365/directory/quotas.py +34 -0
  10. office365/entity_collection.py +2 -2
  11. office365/graph_client.py +46 -2
  12. office365/limits.py +66 -0
  13. office365/migration/__init__.py +6 -1
  14. office365/migration/adapters/__init__.py +14 -4
  15. office365/migration/adapters/filesystem.py +4 -1
  16. office365/migration/assessment/export.py +5 -5
  17. office365/migration/assessment/issue.py +19 -0
  18. office365/migration/assessment/report.py +11 -0
  19. office365/migration/assessment/scanners/base.py +37 -49
  20. office365/migration/base.py +33 -8
  21. office365/migration/checkpoint.py +1 -1
  22. office365/migration/outlook/assessor.py +4 -4
  23. office365/migration/package/__init__.py +82 -0
  24. office365/migration/package/builder.py +603 -0
  25. office365/migration/package/crypto.py +54 -0
  26. office365/migration/package/manifest_xml.py +495 -0
  27. office365/migration/package/staging.py +180 -0
  28. office365/migration/report.py +7 -2
  29. office365/migration/report_io.py +79 -0
  30. office365/migration/runner.py +117 -10
  31. office365/migration/server_job.py +126 -14
  32. office365/migration/session.py +132 -49
  33. office365/migration/settings.py +67 -0
  34. office365/migration/sharepoint/__init__.py +2 -0
  35. office365/migration/sharepoint/adapters.py +207 -41
  36. office365/migration/sharepoint/assessor.py +45 -9
  37. office365/migration/sharepoint/options.py +57 -0
  38. office365/migration/sharepoint/package_target.py +209 -0
  39. office365/migration/sharepoint/registry.py +22 -0
  40. office365/migration/sharepoint/resolvers.py +96 -0
  41. office365/migration/sharepoint/scanners/__init__.py +29 -0
  42. office365/migration/sharepoint/scanners/browser_file_handling.py +55 -0
  43. office365/migration/sharepoint/scanners/checked_out_files.py +57 -0
  44. office365/migration/sharepoint/scanners/file_versions.py +56 -0
  45. office365/migration/sharepoint/scanners/files.py +5 -2
  46. office365/migration/sharepoint/scanners/large_excel_files.py +56 -0
  47. office365/migration/sharepoint/scanners/large_lists.py +58 -0
  48. office365/migration/sharepoint/scanners/large_sites.py +4 -3
  49. office365/migration/sharepoint/scanners/long_onedrive_urls.py +52 -0
  50. office365/migration/sharepoint/scanners/lookup.py +38 -0
  51. office365/migration/sharepoint/scanners/paths.py +9 -4
  52. office365/migration/sharepoint/scanners/permissions.py +20 -2
  53. office365/migration/sharepoint/scanners/smat.py +51 -0
  54. office365/migration/sharepoint/scanners/summary.py +1 -0
  55. office365/migration/sharepoint/scanners/thicket_folders.py +38 -0
  56. office365/migration/sharepoint/scanners/unsupported_site_templates.py +90 -0
  57. office365/migration/sharepoint/tenant_assessor.py +5 -3
  58. office365/migration/sharepoint/transfer.py +4 -2
  59. office365/migration/tasks.py +121 -0
  60. office365/migration/validators.py +2 -19
  61. office365/onedrive/driveitems/driveItem.py +116 -12
  62. office365/onedrive/termstore/groups/collection.py +2 -9
  63. office365/onedrive/termstore/sets/collection.py +2 -9
  64. office365/onedrive/termstore/terms/collection.py +3 -10
  65. office365/onedrive/workbooks/workbook.py +7 -0
  66. office365/outlook/contacts/folders/collection.py +4 -13
  67. office365/runtime/auth/authentication_context.py +5 -0
  68. office365/runtime/client_object.py +18 -0
  69. office365/runtime/client_object_collection.py +75 -245
  70. office365/runtime/client_request.py +77 -1
  71. office365/runtime/client_request_exception.py +127 -39
  72. office365/runtime/client_runtime_context.py +79 -8
  73. office365/runtime/converters/csv_writer.py +10 -4
  74. office365/runtime/converters/dataframe.py +84 -4
  75. office365/runtime/converters/json_file.py +12 -3
  76. office365/runtime/converters/parquet.py +67 -0
  77. office365/runtime/converters/records.py +31 -12
  78. office365/runtime/converters/registry.py +185 -0
  79. office365/runtime/converters/scalars.py +31 -0
  80. office365/runtime/converters/sql.py +69 -0
  81. office365/runtime/converters/streamers.py +105 -0
  82. office365/runtime/converters/streams.py +42 -0
  83. office365/runtime/converters/upsert.py +101 -0
  84. office365/runtime/exceptions.py +105 -0
  85. office365/runtime/http/prefer.py +26 -0
  86. office365/runtime/http/throttling.py +258 -56
  87. office365/runtime/imports.py +571 -0
  88. office365/runtime/limits.py +455 -0
  89. office365/runtime/odata/request.py +11 -2
  90. office365/runtime/odata/type.py +1 -0
  91. office365/runtime/odata/v3/batch_request.py +4 -2
  92. office365/runtime/odata/v4/batch_request.py +22 -7
  93. office365/runtime/operations.py +29 -0
  94. office365/runtime/parallel.py +12 -14
  95. office365/runtime/paths/v4/entity.py +2 -2
  96. office365/runtime/queries/batch.py +18 -1
  97. office365/runtime/queries/get_or_create.py +109 -0
  98. office365/runtime/record_collection.py +443 -0
  99. office365/runtime/retry.py +92 -26
  100. office365/runtime/transport/base.py +6 -1
  101. office365/runtime/transport/requests_transport.py +4 -0
  102. office365/runtime/transport/throttled_transport.py +67 -0
  103. office365/runtime/verification.py +40 -0
  104. office365/sharepoint/activities/logger.py +118 -8
  105. office365/sharepoint/administration/analytics/usage_service.py +15 -7
  106. office365/sharepoint/administration/sitemove/service.py +791 -8
  107. office365/sharepoint/alerts/alert.py +79 -0
  108. office365/sharepoint/apps/app_collection.py +25 -0
  109. office365/sharepoint/apps/launcher.py +16 -0
  110. office365/sharepoint/authpolicy/tenant/compliancereport.py +5 -0
  111. office365/sharepoint/businessdata/app_bdc_catalog.py +135 -0
  112. office365/sharepoint/businessdata/entityfieldvaluedictionary.py +65 -0
  113. office365/sharepoint/businessdata/entityidentifier.py +23 -0
  114. office365/sharepoint/businessdata/entityinstance.py +52 -0
  115. office365/sharepoint/businessdata/entityview.py +31 -0
  116. office365/sharepoint/businessdata/externalsubscriptionstore.py +9 -0
  117. office365/sharepoint/businessdata/typedescriptor.py +37 -0
  118. office365/sharepoint/client_context.py +40 -23
  119. office365/sharepoint/clientsidecomponent/hostedapps/app.py +21 -0
  120. office365/sharepoint/clientsidecomponent/hostedapps/manager.py +16 -0
  121. office365/sharepoint/comments/comment.py +84 -2
  122. office365/sharepoint/contentcenter/machinelearning/models/model.py +0 -23
  123. office365/sharepoint/contentcenter/machinelearning/publications/publication.py +0 -9
  124. office365/sharepoint/contentcenter/machinelearning/samples/sample.py +0 -14
  125. office365/sharepoint/contentcenter/machinelearning/workitems/item.py +0 -80
  126. office365/sharepoint/contentsolution/template_api.py +148 -0
  127. office365/sharepoint/contenttypes/collection.py +32 -10
  128. office365/sharepoint/convergence/addnoliveblobsinodcservicestateapi.py +9 -0
  129. office365/sharepoint/convergence/consumer_permissions.py +15 -0
  130. office365/sharepoint/convergence/migration_complete_state_api.py +25 -0
  131. office365/sharepoint/convergence/odcmetadatacleanedupapi.py +9 -0
  132. office365/sharepoint/documentmanagement/document_id.py +12 -0
  133. office365/sharepoint/entity.py +11 -2
  134. office365/sharepoint/entity_collection.py +2 -2
  135. office365/sharepoint/exceptions.py +194 -0
  136. office365/sharepoint/fields/builtin_field_id.py +45 -34
  137. office365/sharepoint/fields/builtin_field_name.py +123 -0
  138. office365/sharepoint/fields/coercion.py +131 -0
  139. office365/sharepoint/fields/collection.py +54 -37
  140. office365/sharepoint/fields/field.py +17 -0
  141. office365/sharepoint/fields/name.py +46 -2
  142. office365/sharepoint/files/collection.py +7 -1
  143. office365/sharepoint/files/file.py +54 -8
  144. office365/sharepoint/files/versions/version.py +38 -13
  145. office365/sharepoint/folders/collection.py +4 -0
  146. office365/sharepoint/folders/folder.py +91 -21
  147. office365/sharepoint/htmlpages/html_page.py +16 -0
  148. office365/sharepoint/htmlpages/html_page_service.py +19 -0
  149. office365/sharepoint/ir/migration.py +9 -0
  150. office365/sharepoint/listitems/caml/__init__.py +31 -0
  151. office365/sharepoint/listitems/caml/builder.py +129 -0
  152. office365/sharepoint/listitems/caml/expressions.py +424 -0
  153. office365/sharepoint/listitems/caml/fields.py +68 -0
  154. office365/sharepoint/listitems/caml/guard.py +63 -0
  155. office365/sharepoint/listitems/caml/query.py +71 -2
  156. office365/sharepoint/listitems/caml/values.py +113 -0
  157. office365/sharepoint/listitems/collection.py +21 -20
  158. office365/sharepoint/listitems/upsert.py +75 -0
  159. office365/sharepoint/lists/collection.py +29 -22
  160. office365/sharepoint/lists/exporter.py +4 -3
  161. office365/sharepoint/lists/list.py +397 -39
  162. office365/sharepoint/lists/smart_wiki_library_utility.py +58 -0
  163. office365/sharepoint/logger/export.py +12 -0
  164. office365/sharepoint/marketplace/app_metadata.py +97 -1
  165. office365/sharepoint/marketplace/appstatequeryjobdefinition.py +9 -0
  166. office365/sharepoint/marketplace/corporatecuratedgallery/carddesigns/card_designs.py +35 -0
  167. office365/sharepoint/marketplace/sitecollection/appcatalog/allowed_item.py +5 -0
  168. office365/sharepoint/marketplace/tenant/appcatalog/accessor.py +107 -30
  169. office365/sharepoint/microfeed/data.py +0 -6
  170. office365/sharepoint/microservice/manager.py +61 -0
  171. office365/sharepoint/migrationcenter/common/task.py +193 -4
  172. office365/sharepoint/migrationcenter/common/task_entity_data.py +1 -1
  173. office365/sharepoint/migrationcenter/deploy_status.py +13 -0
  174. office365/sharepoint/migrationcenter/service/properties.py +20 -0
  175. office365/sharepoint/migrationcenter/service/storage.py +34 -0
  176. office365/sharepoint/migrationcenter/spflight.py +13 -0
  177. office365/sharepoint/migrationcenter/urlparseresult.py +77 -0
  178. office365/sharepoint/multigeo/service/spmultigeoproxy.py +13 -0
  179. office365/sharepoint/navigation/servicerest.py +127 -0
  180. office365/sharepoint/oauth/authentication.py +13 -0
  181. office365/sharepoint/portal/theme_manager.py +120 -8
  182. office365/sharepoint/principal/users/user.py +10 -0
  183. office365/sharepoint/publishing/campaign/campaign.py +36 -1
  184. office365/sharepoint/publishing/feedvideopage.py +0 -7
  185. office365/sharepoint/publishing/pages/3d/page.py +0 -6
  186. office365/sharepoint/publishing/pages/page.py +0 -117
  187. office365/sharepoint/publishing/pages/reposts/repost.py +0 -7
  188. office365/sharepoint/publishing/pages/topics/topic.py +0 -7
  189. office365/sharepoint/publishing/subtitlefile.py +0 -40
  190. office365/sharepoint/quotamanagement/consumer/consumer_quota_info_api.py +14 -0
  191. office365/sharepoint/quotamanagement/consumer/non_quota_backfill_api.py +23 -0
  192. office365/sharepoint/quotamanagement/consumer/nonquotamigrationapi.py +9 -0
  193. office365/sharepoint/quotamanagement/consumer/quotamigrationapi.py +13 -0
  194. office365/sharepoint/request.py +57 -16
  195. office365/sharepoint/search/administration/document_crawl_log.py +24 -7
  196. office365/sharepoint/search/administration/providers/crawl_versions_info.py +29 -0
  197. office365/sharepoint/search/administration/providers/recycle_bin_info.py +23 -0
  198. office365/sharepoint/search/administration/providers/site_content_processing_info.py +67 -0
  199. office365/sharepoint/sites/site.py +976 -65
  200. office365/sharepoint/sites/siteprovisioninginfoprovider.py +13 -0
  201. office365/sharepoint/tenant/administration/datagovernance/insight_client.py +292 -23
  202. office365/sharepoint/tenant/administration/ddiadapter.py +97 -0
  203. office365/sharepoint/tenant/administration/internal/aad/collection.py +20 -0
  204. office365/sharepoint/tenant/administration/internal/aad/permission_grant.py +0 -19
  205. office365/sharepoint/tenant/administration/internal/appservice/permission_request.py +72 -1
  206. office365/sharepoint/tenant/administration/internal/appservice/principal.py +1 -3
  207. office365/sharepoint/tenant/administration/odb_license_enforcement_service.py +26 -0
  208. office365/sharepoint/tenant/administration/sites/properties.py +564 -6
  209. office365/sharepoint/tenant/administration/spdatagovernancesarrestapiclient.py +60 -1
  210. office365/sharepoint/tenant/insights/reports/manager.py +24 -12
  211. office365/sharepoint/tenant/management/office365_tenant.py +451 -2
  212. office365/sharepoint/tenantadministration/catalog_management_collection.py +69 -0
  213. office365/sharepoint/tenantadministration/catalog_management_custom_properties_collection.py +237 -0
  214. office365/sharepoint/tenantadministration/catalog_management_restricted_content_discovery_collection.py +56 -0
  215. office365/sharepoint/tenantadministration/sp_data_governance_client_exception.py +7 -0
  216. office365/sharepoint/thresholds.py +529 -0
  217. office365/sharepoint/translation/jobs/job.py +72 -2
  218. office365/sharepoint/translation/jobs/status.py +47 -9
  219. office365/sharepoint/translation/sync_translator.py +15 -2
  220. office365/sharepoint/utilities/move_copy_util.py +2 -1
  221. office365/sharepoint/viva/connections/page.py +27 -0
  222. office365/sharepoint/viva/home.py +38 -0
  223. office365/sharepoint/viva/learningmanager.py +9 -0
  224. office365/sharepoint/viva/vivaresources.py +60 -0
  225. office365/sharepoint/webhooks/subscription.py +0 -11
  226. office365/sharepoint/webparts/definitions/definition.py +13 -0
  227. office365/sharepoint/webparts/limited_manager.py +29 -7
  228. office365/sharepoint/webs/context_web_information.py +14 -9
  229. office365/sharepoint/webs/templates/__init__.py +11 -0
  230. office365/sharepoint/webs/templates/type.py +74 -15
  231. office365/sharepoint/workflow/definition.py +6 -0
  232. office365/sharepoint/workflow/deployment_service.py +155 -0
  233. office365/sharepoint/workflow/instances/service.py +33 -0
  234. office365/sharepoint/workflow/interopservice.py +68 -0
  235. office365/sharepoint/workflow/messagingservice.py +24 -0
  236. office365/sharepoint/workflow/servicesmanager.py +41 -2
  237. office365/sharepoint/workflow/subscriptions/service.py +67 -0
  238. office365/sharepoint/workflow/subscriptions/subscription.py +31 -0
  239. office365/sharepoint/workflow/task.py +425 -0
  240. office365_rest_python_client-3.2.0.dist-info/METADATA +605 -0
  241. {office365_rest_python_client-3.1.0.dist-info → office365_rest_python_client-3.2.0.dist-info}/RECORD +242 -185
  242. office365/migration/_util.py +0 -73
  243. office365/sharepoint/listitems/caml/types.py +0 -70
  244. office365_rest_python_client-3.1.0.dist-info/METADATA +0 -475
  245. {office365_rest_python_client-3.1.0.dist-info → office365_rest_python_client-3.2.0.dist-info}/WHEEL +0 -0
@@ -0,0 +1,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