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,495 @@
1
+ """SPO Migration API manifest package — XML models and serialization.
2
+
3
+ The SharePoint Migration API (``CreateMigrationJob`` /
4
+ ``CreateMigrationJobEncrypted``) ingests content described by a set of XML
5
+ manifest files. This module models the **document-library subset** — webs, lists,
6
+ folders, files, and file versions — plus the small, fully-specified
7
+ ``ExportSettings.xml`` / ``SystemData.xml`` / ``UserGroup.xml``.
8
+
9
+ The full ``DeploymentManifest`` schema has 100+ elements (content types, views,
10
+ web parts, list items, taxonomy, ...); only what a document-library migration
11
+ needs is modeled here. The generated XML follows the documented format but is
12
+ **not yet verified against a live tenant**.
13
+
14
+ References:
15
+ - https://learn.microsoft.com/en-us/sharepoint/dev/apis/migration-manifest
16
+ - https://learn.microsoft.com/en-us/sharepoint/dev/schema/deploymentmanifest-schema
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import xml.etree.ElementTree as ET
22
+ from dataclasses import dataclass, field
23
+
24
+ MANIFEST_NS = "urn:deployment-manifest-schema"
25
+ SYSTEM_DATA_NS = "urn:deployment-systemdata-schema"
26
+ EXPORT_SETTINGS_NS = "urn:deployment-exportsettings-schema"
27
+ USER_GROUP_MAP_NS = "urn:deployment-usergroupmap-schema"
28
+ REQUIREMENTS_NS = "urn:deployment-requirements-schema"
29
+ ROOT_OBJECT_MAP_NS = "urn:deployment-rootobjectmap-schema"
30
+ LOOKUP_LIST_MAP_NS = "urn:deployment-lookuplistmap-schema"
31
+ VIEW_FORMS_LIST_NS = "urn:deployment-viewformlist-schema"
32
+
33
+ # ``ExportSettings/@SourceType`` accepted values.
34
+ SOURCE_TYPES = (
35
+ "AmazonS3",
36
+ "AzureStorage",
37
+ "Box",
38
+ "Dropbox",
39
+ "Egnyte",
40
+ "FileShare",
41
+ "GoogleCloudStorage",
42
+ "GoogleDrive",
43
+ "MicrosoftStream",
44
+ "OneDrive",
45
+ "SharePointOnline",
46
+ "SharePointOnPremServer",
47
+ "Other",
48
+ )
49
+
50
+ __all__ = [
51
+ "EXPORT_SETTINGS_NS",
52
+ "LOOKUP_LIST_MAP_NS",
53
+ "MANIFEST_NS",
54
+ "REQUIREMENTS_NS",
55
+ "ROOT_OBJECT_MAP_NS",
56
+ "SOURCE_TYPES",
57
+ "SYSTEM_DATA_NS",
58
+ "USER_GROUP_MAP_NS",
59
+ "VIEW_FORMS_LIST_NS",
60
+ "Assignment",
61
+ "DeploymentObject",
62
+ "ExportSettings",
63
+ "Group",
64
+ "Manifest",
65
+ "ManifestElement",
66
+ "ManifestObject",
67
+ "Role",
68
+ "RoleAssignment",
69
+ "RootObject",
70
+ "RootObjectMap",
71
+ "SharedWithEvent",
72
+ "SystemData",
73
+ "SystemObject",
74
+ "User",
75
+ "UserGroupMap",
76
+ "empty_document",
77
+ ]
78
+
79
+
80
+ def _q(ns: str, tag: str) -> str:
81
+ """Qualify a tag with its XML namespace."""
82
+ return f"{{{ns}}}{tag}"
83
+
84
+
85
+ def _str(value: object) -> str:
86
+ """Render an attribute value (booleans as lowercase ``true``/``false``)."""
87
+ if isinstance(value, bool):
88
+ return "true" if value else "false"
89
+ return str(value)
90
+
91
+
92
+ def _set(element: ET.Element, attributes: dict) -> None:
93
+ """Set non-``None`` attributes on an element, in insertion order."""
94
+ for name, value in attributes.items():
95
+ if value is not None:
96
+ element.set(name, _str(value))
97
+
98
+
99
+ def _indent(element: ET.Element, level: int = 0) -> None:
100
+ """Indent element content (``ET.indent`` is Python 3.9+; this library supports 3.8)."""
101
+ prefix = "\n" + " " * level
102
+ children = list(element)
103
+ if children:
104
+ if not (element.text or "").strip():
105
+ element.text = prefix + " "
106
+ for child in children:
107
+ _indent(child, level + 1)
108
+ if not (children[-1].tail or "").strip():
109
+ children[-1].tail = prefix
110
+ if level and not (element.tail or "").strip():
111
+ element.tail = prefix
112
+
113
+
114
+ def _serialize(root: ET.Element, ns: str) -> bytes:
115
+ """Serialize a document with ``ns`` as the default namespace + XML declaration."""
116
+ ET.register_namespace("", ns)
117
+ _indent(root)
118
+ return ET.tostring(root, encoding="utf-8", xml_declaration=True)
119
+
120
+
121
+ def empty_document(ns: str, root: str) -> bytes:
122
+ """A document with a childless root element (the API fetches these files by name)."""
123
+ return _serialize(ET.Element(_q(ns, root)), ns)
124
+
125
+
126
+ @dataclass
127
+ class ManifestElement:
128
+ """A nested element inside an ``<SPObject>`` payload element.
129
+
130
+ Generic recursion (name + attributes + children) covers ``<Versions>``,
131
+ ``<Roles>``, ``<RoleAssignments>``, ``<SharedWithEvents>``, etc.
132
+ """
133
+
134
+ name: str
135
+ attributes: dict = field(default_factory=dict)
136
+ children: list["ManifestElement"] = field(default_factory=list)
137
+
138
+ def to_etree(self, ns: str = MANIFEST_NS) -> ET.Element:
139
+ element = ET.Element(_q(ns, self.name))
140
+ _set(element, self.attributes)
141
+ for child in self.children:
142
+ element.append(child.to_etree(ns))
143
+ return element
144
+
145
+
146
+ @dataclass
147
+ class ManifestObject:
148
+ """A single ``<SPObject>`` entry in ``Manifest.xml``.
149
+
150
+ ``attributes`` are rendered on the child element (``element``); ``children``
151
+ render nested blocks (file versions, roles, role assignments, sharing).
152
+ """
153
+
154
+ id: str
155
+ object_type: str # SPFolder | SPDocumentLibrary | SPFile | ListItem | ...
156
+ element: str # child element name (Folder/DocumentLibrary/File/ListItem/...)
157
+ attributes: dict = field(default_factory=dict)
158
+ children: list[ManifestElement] = field(default_factory=list)
159
+ parent_id: str | None = None
160
+ parent_web_id: str | None = None
161
+ parent_web_url: str | None = None
162
+ url: str | None = None
163
+
164
+
165
+ @dataclass
166
+ class Manifest:
167
+ """The ``Manifest.xml`` document — the content/structure catalog."""
168
+
169
+ objects: list[ManifestObject] = field(default_factory=list)
170
+
171
+ def add(self, obj: ManifestObject) -> "Manifest":
172
+ self.objects.append(obj)
173
+ return self
174
+
175
+ def to_xml(self) -> bytes:
176
+ root = ET.Element(_q(MANIFEST_NS, "SPObjects"))
177
+ for obj in self.objects:
178
+ sp = ET.SubElement(root, _q(MANIFEST_NS, "SPObject"))
179
+ _set(
180
+ sp,
181
+ {
182
+ "Id": obj.id,
183
+ "ObjectType": obj.object_type,
184
+ "ParentId": obj.parent_id,
185
+ "ParentWebId": obj.parent_web_id,
186
+ "ParentWebUrl": obj.parent_web_url,
187
+ "Url": obj.url,
188
+ },
189
+ )
190
+ element = ET.SubElement(sp, _q(MANIFEST_NS, obj.element))
191
+ _set(element, obj.attributes)
192
+ for child in obj.children:
193
+ element.append(child.to_etree(MANIFEST_NS))
194
+ return _serialize(root, MANIFEST_NS)
195
+
196
+
197
+ @dataclass
198
+ class Role:
199
+ """A ``<Role>`` (permission level) inside a ``DeploymentRoles`` object."""
200
+
201
+ id: str # RoleId
202
+ title: str
203
+ perm_mask: str
204
+ hidden: bool = False
205
+ description: str | None = None
206
+ role_order: str | None = None
207
+ type: str | None = None
208
+
209
+ def to_element(self) -> ManifestElement:
210
+ return ManifestElement(
211
+ "Role",
212
+ {
213
+ "RoleId": self.id,
214
+ "Title": self.title,
215
+ "Description": self.description,
216
+ "PermMask": self.perm_mask,
217
+ "Hidden": self.hidden,
218
+ "RoleOrder": self.role_order,
219
+ "Type": self.type,
220
+ },
221
+ )
222
+
223
+
224
+ @dataclass
225
+ class Assignment:
226
+ """An ``<Assignment>`` — a role granted to a principal (a ``UserGroup`` id)."""
227
+
228
+ role_id: str
229
+ principal_id: int
230
+
231
+ def to_element(self) -> ManifestElement:
232
+ return ManifestElement("Assignment", {"RoleId": self.role_id, "PrincipalId": self.principal_id})
233
+
234
+
235
+ @dataclass
236
+ class RoleAssignment:
237
+ """A ``<RoleAssignment>`` (an ACL scope) inside a ``DeploymentRoleAssignments`` object.
238
+
239
+ ``object_id``/``object_url``/``object_type`` identify the secured object (a
240
+ file/folder GUID), ``scope_id`` the scope it applies to; ``assignments`` map
241
+ roles to principals from ``UserGroup.xml``.
242
+ """
243
+
244
+ object_id: str
245
+ object_url: str
246
+ object_type: str
247
+ role_def_web_id: str
248
+ role_def_web_url: str
249
+ scope_id: str
250
+ assignments: list[Assignment] = field(default_factory=list)
251
+ anonymous_perm_mask: str = "0"
252
+
253
+ def to_element(self) -> ManifestElement:
254
+ return ManifestElement(
255
+ "RoleAssignment",
256
+ {
257
+ "ScopeId": self.scope_id,
258
+ "RoleDefWebId": self.role_def_web_id,
259
+ "RoleDefWebUrl": self.role_def_web_url,
260
+ "ObjectId": self.object_id,
261
+ "ObjectType": self.object_type,
262
+ "ObjectUrl": self.object_url,
263
+ "AnonymousPermMask": self.anonymous_perm_mask,
264
+ },
265
+ children=[assignment.to_element() for assignment in self.assignments],
266
+ )
267
+
268
+
269
+ @dataclass
270
+ class SharedWithEvent:
271
+ """A ``<SharedWithEvent>`` — a "Shared with Me" occurrence on a list item."""
272
+
273
+ shared_time: str
274
+ sharing_initiator_id: int
275
+ shared_by_id: int
276
+ members: list[int] = field(default_factory=list)
277
+
278
+ def to_element(self) -> ManifestElement:
279
+ members = ManifestElement(
280
+ "SharedWithMembers",
281
+ children=[ManifestElement("SharedWithMember", {"SharedWithId": member}) for member in self.members],
282
+ )
283
+ return ManifestElement(
284
+ "SharedWithEvent",
285
+ {
286
+ "SharedTime": self.shared_time,
287
+ "SharingInitiatorId": self.sharing_initiator_id,
288
+ "SharedById": self.shared_by_id,
289
+ },
290
+ children=[members],
291
+ )
292
+
293
+
294
+ @dataclass
295
+ class RootObject:
296
+ """A ``<RootObject>`` mapping in ``RootObjectMap.xml``."""
297
+
298
+ id: str
299
+ type: str # Site | Web | Folder | List | ListItem | File
300
+ parent_id: str | None = None
301
+ web_url: str | None = None
302
+ url: str | None = None
303
+ is_dependency: bool = False
304
+
305
+
306
+ @dataclass
307
+ class RootObjectMap:
308
+ """The ``RootObjectMap.xml`` document — dependent-object placement."""
309
+
310
+ objects: list[RootObject] = field(default_factory=list)
311
+
312
+ def to_xml(self) -> bytes:
313
+ root = ET.Element(_q(ROOT_OBJECT_MAP_NS, "RootObjects"))
314
+ for obj in self.objects:
315
+ _set(
316
+ ET.SubElement(root, _q(ROOT_OBJECT_MAP_NS, "RootObject")),
317
+ {
318
+ "Id": obj.id,
319
+ "Type": obj.type,
320
+ "ParentId": obj.parent_id,
321
+ "WebUrl": obj.web_url,
322
+ "Url": obj.url,
323
+ "IsDependency": obj.is_dependency,
324
+ },
325
+ )
326
+ return _serialize(root, ROOT_OBJECT_MAP_NS)
327
+
328
+
329
+ @dataclass
330
+ class DeploymentObject:
331
+ """A ``<DeploymentObject>`` entry in ``ExportSettings.xml``."""
332
+
333
+ id: str
334
+ type: str # List | Web | ...
335
+ parent_id: str | None = None
336
+
337
+
338
+ @dataclass
339
+ class ExportSettings:
340
+ """The required ``ExportSettings.xml`` document."""
341
+
342
+ site_url: str
343
+ source_type: str = "SharePointOnline"
344
+ include_security: str = "None"
345
+ file_location: str | None = None
346
+ detailed_source: str | None = None
347
+ export_objects: list[DeploymentObject] = field(default_factory=list)
348
+
349
+ def to_xml(self) -> bytes:
350
+ root = ET.Element(_q(EXPORT_SETTINGS_NS, "ExportSettings"))
351
+ _set(
352
+ root,
353
+ {
354
+ "SiteUrl": self.site_url,
355
+ "FileLocation": self.file_location,
356
+ "IncludeSecurity": self.include_security,
357
+ "SourceType": self.source_type,
358
+ "DetailedSource": self.detailed_source,
359
+ },
360
+ )
361
+ if self.export_objects:
362
+ objects = ET.SubElement(root, _q(EXPORT_SETTINGS_NS, "ExportObjects"))
363
+ for obj in self.export_objects:
364
+ _set(
365
+ ET.SubElement(objects, _q(EXPORT_SETTINGS_NS, "DeploymentObject")),
366
+ {"Id": obj.id, "Type": obj.type, "ParentId": obj.parent_id},
367
+ )
368
+ return _serialize(root, EXPORT_SETTINGS_NS)
369
+
370
+
371
+ @dataclass
372
+ class SystemObject:
373
+ """A ``<SystemObject>`` entry (an immutable dependent object)."""
374
+
375
+ id: str
376
+ type: str # Web | List | Folder
377
+ url: str
378
+
379
+
380
+ @dataclass
381
+ class SystemData:
382
+ """The required ``SystemData.xml`` document."""
383
+
384
+ manifest_files: list[str] = field(default_factory=lambda: ["Manifest.xml"])
385
+ system_objects: list[SystemObject] = field(default_factory=list)
386
+ root_web_only_lists: list[str] = field(default_factory=list)
387
+ version: str = "15.0.0.0"
388
+ build: str = "16.0.3111.1200"
389
+ database_version: int = 11552
390
+ site_version: str = "15"
391
+
392
+ def to_xml(self) -> bytes:
393
+ root = ET.Element(_q(SYSTEM_DATA_NS, "SystemData"))
394
+ _set(
395
+ ET.SubElement(root, _q(SYSTEM_DATA_NS, "SchemaVersion")),
396
+ {
397
+ "Version": self.version,
398
+ "Build": self.build,
399
+ "DatabaseVersion": self.database_version,
400
+ "SiteVersion": self.site_version,
401
+ },
402
+ )
403
+ manifests = ET.SubElement(root, _q(SYSTEM_DATA_NS, "ManifestFiles"))
404
+ for name in self.manifest_files:
405
+ _set(ET.SubElement(manifests, _q(SYSTEM_DATA_NS, "ManifestFile")), {"Name": name})
406
+ objects = ET.SubElement(root, _q(SYSTEM_DATA_NS, "SystemObjects"))
407
+ for obj in self.system_objects:
408
+ _set(
409
+ ET.SubElement(objects, _q(SYSTEM_DATA_NS, "SystemObject")),
410
+ {"Id": obj.id, "Type": obj.type, "Url": obj.url},
411
+ )
412
+ ET.SubElement(root, _q(SYSTEM_DATA_NS, "RootWebOnlyLists"))
413
+ for url in self.root_web_only_lists:
414
+ _set(ET.SubElement(root, _q(SYSTEM_DATA_NS, "RootWebOnlyLists")), {"Url": url})
415
+ return _serialize(root, SYSTEM_DATA_NS)
416
+
417
+
418
+ @dataclass
419
+ class User:
420
+ """A ``<User>`` entry in ``UserGroup.xml``.
421
+
422
+ ``login`` must be a UPN-based login name (``i:0#.f|membership|user@contoso.com``)
423
+ — non-UPN emails cause unexpected behavior in SharePoint Online.
424
+ """
425
+
426
+ id: int
427
+ name: str
428
+ login: str
429
+ email: str | None = None
430
+ is_domain_group: bool = False
431
+ is_site_admin: bool = False
432
+ system_id: str | None = None
433
+ is_deleted: bool = False
434
+ flags: int = 0
435
+
436
+
437
+ @dataclass
438
+ class Group:
439
+ """A ``<Group>`` entry in ``UserGroup.xml``."""
440
+
441
+ id: int
442
+ name: str
443
+ description: str | None = None
444
+ owner: int | None = None
445
+ owner_is_user: bool = True
446
+ only_allow_members_view_membership: bool = True
447
+ members: list[int] = field(default_factory=list)
448
+
449
+
450
+ @dataclass
451
+ class UserGroupMap:
452
+ """The required ``UserGroup.xml`` document (entries are optional)."""
453
+
454
+ users: list[User] = field(default_factory=list)
455
+ groups: list[Group] = field(default_factory=list)
456
+
457
+ def add_user(self, user: User) -> "UserGroupMap":
458
+ self.users.append(user)
459
+ return self
460
+
461
+ def to_xml(self) -> bytes:
462
+ root = ET.Element(_q(USER_GROUP_MAP_NS, "UserGroupMap"))
463
+ users = ET.SubElement(root, _q(USER_GROUP_MAP_NS, "Users"))
464
+ for user in self.users:
465
+ _set(
466
+ ET.SubElement(users, _q(USER_GROUP_MAP_NS, "User")),
467
+ {
468
+ "Id": user.id,
469
+ "Name": user.name,
470
+ "Login": user.login,
471
+ "Email": user.email,
472
+ "IsDomainGroup": user.is_domain_group,
473
+ "IsSiteAdmin": user.is_site_admin,
474
+ # the service schema requires the attribute even when unknown
475
+ "SystemId": user.system_id or "",
476
+ "IsDeleted": user.is_deleted,
477
+ },
478
+ )
479
+ groups = ET.SubElement(root, _q(USER_GROUP_MAP_NS, "Groups"))
480
+ for group in self.groups:
481
+ element = ET.SubElement(groups, _q(USER_GROUP_MAP_NS, "Group"))
482
+ _set(
483
+ element,
484
+ {
485
+ "Id": group.id,
486
+ "Name": group.name,
487
+ "Description": group.description,
488
+ "Owner": group.owner,
489
+ "OwnerIsUser": group.owner_is_user,
490
+ "OnlyAllowMembersViewMembership": group.only_allow_members_view_membership,
491
+ },
492
+ )
493
+ for member_id in group.members:
494
+ _set(ET.SubElement(element, _q(USER_GROUP_MAP_NS, "Member")), {"UserId": member_id})
495
+ return _serialize(root, USER_GROUP_MAP_NS)
@@ -0,0 +1,180 @@
1
+ """Content staging — put a package's blobs where the Migration API can read them.
2
+
3
+ Staging is the **swappable seam** between the vendor-neutral package (documented
4
+ XML + content blobs) and the transport SharePoint's ingestion service reads from.
5
+ Two strategies are provided:
6
+
7
+ - :class:`FileSystemStaging` — writes blobs under a directory (offline/testing, or
8
+ a staging area for a later upload) — **no Azure**;
9
+ - :class:`BlobStaging` — uploads to the content/manifest containers (requires the
10
+ ``[azure]`` extra: ``pip install office365-rest-python-client[azure]``); with an
11
+ ``encryption_key`` every blob is AES-256-CBC encrypted (SharePoint-provided
12
+ containers require this).
13
+
14
+ :func:`create_staging` picks a strategy from the container URLs; it is the
15
+ extension point for a future S3 / Azure Files staging.
16
+
17
+ Content and manifest blobs go to **different** containers — the API rejects a job
18
+ whose content and manifest containers match.
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ import base64
24
+ import importlib
25
+ from pathlib import Path
26
+ from typing import TYPE_CHECKING
27
+ from urllib.parse import urlparse
28
+
29
+ from office365.migration.package.crypto import decrypt, encrypt, normalize_key
30
+
31
+ if TYPE_CHECKING:
32
+ from office365.migration.package.builder import Package
33
+
34
+ __all__ = ["AzureBlobStaging", "BlobStaging", "FileSystemStaging", "Staging", "create_staging"]
35
+
36
+ _AZURE_EXTRA_HINT = "Azure staging requires the 'azure' extra: pip install office365-rest-python-client[azure]"
37
+
38
+ # Azure Blob hosts (public cloud + US Government).
39
+ _AZURE_BLOB_HOSTS = ("blob.core.windows.net", "blob.core.usgovcloudapi.net")
40
+
41
+
42
+ class Staging:
43
+ """Strategy that places a package's content + manifest blobs for ingestion."""
44
+
45
+ def stage(self, package: "Package") -> None:
46
+ raise NotImplementedError
47
+
48
+
49
+ class FileSystemStaging(Staging):
50
+ """Writes the package's blobs under ``content_dir`` / ``manifest_dir``."""
51
+
52
+ def __init__(self, content_dir: str | Path, manifest_dir: str | Path | None = None) -> None:
53
+ self._content_dir = Path(content_dir)
54
+ self._manifest_dir = Path(manifest_dir) if manifest_dir is not None else self._content_dir / "manifest"
55
+
56
+ @property
57
+ def content_dir(self) -> Path:
58
+ return self._content_dir
59
+
60
+ @property
61
+ def manifest_dir(self) -> Path:
62
+ return self._manifest_dir
63
+
64
+ def stage(self, package: "Package") -> None:
65
+ self._content_dir.mkdir(parents=True, exist_ok=True)
66
+ self._manifest_dir.mkdir(parents=True, exist_ok=True)
67
+ for name, data in package.content.items():
68
+ (self._content_dir / name).write_bytes(data)
69
+ for name, data in package.blobs().items():
70
+ (self._manifest_dir / name).write_bytes(data)
71
+
72
+
73
+ class BlobStaging(Staging):
74
+ """Uploads the package's blobs to the content/manifest Azure Blob containers.
75
+
76
+ The container URIs must embed a SAS token (e.g. the ``DataContainerUri`` /
77
+ ``MetadataContainerUri`` returned by ``Site.provision_migration_containers``).
78
+ A snapshot is created for every content blob, as the API requires.
79
+
80
+ Pass ``encryption_key`` (the base64 ``EncryptionKey`` from
81
+ ``provision_migration_containers``) for SharePoint-provided containers: every
82
+ content and manifest blob is then AES-256-CBC encrypted, with its base64 IV
83
+ stored as the ``IV`` blob property.
84
+ """
85
+
86
+ def __init__(
87
+ self,
88
+ content_uri: str,
89
+ manifest_uri: str,
90
+ encryption_key: str | bytes | None = None,
91
+ ) -> None:
92
+ if content_uri.rstrip("?") == manifest_uri.rstrip("?"):
93
+ raise ValueError("content and manifest containers must differ")
94
+ self._content_uri = content_uri
95
+ self._manifest_uri = manifest_uri
96
+ self._key = normalize_key(encryption_key) if encryption_key else None
97
+
98
+ def stage(self, package: "Package") -> None:
99
+ container_client = _container_client()
100
+ content = container_client.from_container_url(self._content_uri)
101
+ manifest = container_client.from_container_url(self._manifest_uri)
102
+ for name, data in package.content.items():
103
+ blob = content.get_blob_client(name)
104
+ self._upload(blob, data)
105
+ blob.create_snapshot() # the API reads the latest snapshot
106
+ for name, data in package.blobs().items():
107
+ self._upload(manifest.get_blob_client(name), data)
108
+
109
+ def _upload(self, blob, data: bytes) -> None:
110
+ if self._key is None:
111
+ blob.upload_blob(data, overwrite=True)
112
+ return
113
+ ciphertext, iv = encrypt(data, self._key)
114
+ blob.upload_blob(ciphertext, overwrite=True, metadata={"IV": iv})
115
+
116
+ def read_manifest_blob(self, name: str) -> bytes:
117
+ """Download a blob (e.g. the API's import log) from the manifest container.
118
+
119
+ Provisioned containers grant ``Read`` (not ``List``), so the caller must
120
+ know the blob name — ``JobLogFileCreate`` events carry it. The containers
121
+ are AES-256-CBC encrypted end-to-end, so the API's own logs come back as
122
+ ciphertext; with a key set they are decrypted using the blob's ``IV``
123
+ metadata (falling back to a 16-byte IV prefixed to the payload).
124
+ """
125
+ blob = _container_client().from_container_url(self._manifest_uri).get_blob_client(name)
126
+ data = blob.download_blob().readall()
127
+ if self._key is None:
128
+ return data
129
+ iv = _blob_iv(blob)
130
+ if iv is None and len(data) > 16: # noqa: PLR2004
131
+ iv = base64.b64encode(data[:16]).decode("ascii")
132
+ data = data[16:]
133
+ if iv is None:
134
+ return data
135
+ try:
136
+ return decrypt(data, self._key, iv)
137
+ except Exception: # noqa: BLE001 — not encrypted with our key; return raw
138
+ return data
139
+
140
+
141
+ # Back-compat alias (the class was introduced as ``AzureBlobStaging``).
142
+ AzureBlobStaging = BlobStaging
143
+
144
+
145
+ def create_staging(
146
+ content_url: str,
147
+ manifest_url: str,
148
+ encryption_key: str | bytes | None = None,
149
+ ) -> Staging:
150
+ """Return a :class:`Staging` for the given container URLs.
151
+
152
+ Azure Blob is the only backend today (the SharePoint Migration API requires it
153
+ for the server-side ingest leg). This factory is the extension point: a future
154
+ S3 / Azure Files staging can be selected here by URL host without changing
155
+ callers.
156
+ """
157
+ host = urlparse(content_url).hostname or ""
158
+ if host.endswith(_AZURE_BLOB_HOSTS):
159
+ return BlobStaging(content_url, manifest_url, encryption_key=encryption_key)
160
+ raise ValueError(
161
+ f"unsupported staging container host {host!r}: only Azure Blob ({', '.join(_AZURE_BLOB_HOSTS)}) is supported"
162
+ )
163
+
164
+
165
+ def _container_client():
166
+ """Lazily import ``azure.storage.blob.ContainerClient`` (optional extra)."""
167
+ try:
168
+ module = importlib.import_module("azure.storage.blob")
169
+ except ImportError as exc: # pragma: no cover - exercised only without the extra
170
+ raise ImportError(_AZURE_EXTRA_HINT) from exc
171
+ return module.ContainerClient
172
+
173
+
174
+ def _blob_iv(blob) -> str | None:
175
+ """The base64 ``IV`` from a blob's metadata, when present."""
176
+ try:
177
+ metadata = blob.get_blob_properties().metadata or {}
178
+ except Exception: # noqa: BLE001 — metadata is best-effort
179
+ return None
180
+ return next((value for key, value in metadata.items() if key.lower() == "iv"), None)
@@ -12,7 +12,8 @@ from dataclasses import dataclass, field
12
12
  from pathlib import Path
13
13
  from typing import TYPE_CHECKING
14
14
 
15
- from office365.migration._util import iso, write_csv_json
15
+ from office365.migration.report_io import write_formats
16
+ from office365.runtime.converters.scalars import iso
16
17
 
17
18
  if TYPE_CHECKING:
18
19
  from office365.migration.job import MigrationJob
@@ -79,6 +80,10 @@ def build_report(job: "MigrationJob") -> MigrationReport:
79
80
  "status": item.status.value,
80
81
  "error": item.error or "",
81
82
  "error_code": item.error_code or "",
83
+ "modified": item.modified or "",
84
+ "created": item.created or "",
85
+ "author_id": item.author_id,
86
+ "editor_id": item.editor_id,
82
87
  }
83
88
  for item in job.manifest.items
84
89
  ]
@@ -107,5 +112,5 @@ def export_reports(job: "MigrationJob", output_dir: str | Path) -> list[str]:
107
112
  ):
108
113
  if not records:
109
114
  continue
110
- written += write_csv_json(output_dir, name, records)
115
+ written += write_formats(records, output_dir, name)
111
116
  return written