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,131 @@
1
+ """Typed field value coercion for record imports.
2
+
3
+ Maps plain source values (CSV/DataFrame cells) to the payload shapes SharePoint
4
+ expects for typed fields, driven by an explicit ``schema={column: FieldType}``.
5
+ ``List.from_records(..., schema=...)`` (and the streaming conveniences) applies
6
+ this, so typed columns (choice, lookup, user, URL, geolocation, ...) can be
7
+ populated without hand-building payloads:
8
+
9
+ lst.from_dataframe(df, schema={"Status": FieldType.Choice, "Tags": FieldType.MultiChoice})
10
+
11
+ Values already in the expected shape (a ``ClientValue``) pass through unchanged.
12
+ ``None`` values and unknown/``None`` field types are left as-is, so this is safe
13
+ to apply to every cell of a record.
14
+
15
+ SharePoint has no distinct field-type kind for multi-valued lookups/users (they
16
+ are ``Lookup``/``User`` fields with ``AllowMultipleValues``), so the value's
17
+ shape decides: a list, tuple or ``"; "``-separated string becomes the multi form.
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ from collections.abc import Iterable, Mapping
23
+ from typing import Any, Callable, Dict, Optional
24
+
25
+ from office365.runtime.client_value import ClientValue
26
+ from office365.runtime.converters.scalars import parse_bool, parse_datetime, try_float, try_int
27
+ from office365.sharepoint.fields.geolocation_value import FieldGeolocationValue
28
+ from office365.sharepoint.fields.lookup_value import FieldLookupValue
29
+ from office365.sharepoint.fields.multi_choice_value import FieldMultiChoiceValue
30
+ from office365.sharepoint.fields.multi_lookup_value import FieldMultiLookupValue
31
+ from office365.sharepoint.fields.multi_user_value import FieldMultiUserValue
32
+ from office365.sharepoint.fields.type import FieldType
33
+ from office365.sharepoint.fields.url_value import FieldUrlValue
34
+ from office365.sharepoint.fields.user_value import FieldUserValue
35
+
36
+ _LIST_SEPARATOR = ";"
37
+
38
+
39
+ def _as_list(value: Any) -> list[Any]:
40
+ """Normalize a value into a list (a ``"; "`` string is split)."""
41
+ if isinstance(value, str):
42
+ return [part.strip() for part in value.split(_LIST_SEPARATOR) if part.strip()]
43
+ if isinstance(value, Iterable):
44
+ return list(value)
45
+ return [value]
46
+
47
+
48
+ def _is_multi(value: Any) -> bool:
49
+ """Whether a value should map to a multi-valued field."""
50
+ return isinstance(value, (list, tuple)) or (isinstance(value, str) and _LIST_SEPARATOR in value)
51
+
52
+
53
+ def _lookup_id(value: Any) -> int:
54
+ """Read a lookup id from a mapping (``LookupId``/``Id``) or a scalar."""
55
+ if isinstance(value, Mapping):
56
+ value = value.get("LookupId", value.get("Id", value.get("id")))
57
+ return int(value)
58
+
59
+
60
+ def _lookup_value(value: Any) -> Any:
61
+ if _is_multi(value):
62
+ return FieldMultiLookupValue([FieldLookupValue(LookupId=_lookup_id(item)) for item in _as_list(value)])
63
+ return FieldLookupValue(LookupId=_lookup_id(value))
64
+
65
+
66
+ def _user_value(value: Any) -> FieldUserValue:
67
+ if isinstance(value, Mapping):
68
+ has_id = value.get("LookupId") is not None or value.get("Id") is not None
69
+ return FieldUserValue(
70
+ LookupId=_lookup_id(value) if has_id else None,
71
+ LookupValue=value.get("LookupValue"),
72
+ Email=value.get("Email", value.get("email")),
73
+ )
74
+ if isinstance(value, str) and "@" in value:
75
+ return FieldUserValue(Email=value)
76
+ return FieldUserValue(LookupId=_lookup_id(value))
77
+
78
+
79
+ def _user(value: Any) -> Any:
80
+ if _is_multi(value):
81
+ return FieldMultiUserValue([_user_value(item) for item in _as_list(value)])
82
+ return _user_value(value)
83
+
84
+
85
+ def _url(value: Any) -> FieldUrlValue:
86
+ if isinstance(value, Mapping):
87
+ return FieldUrlValue(Url=value.get("Url"), Description=value.get("Description"))
88
+ if isinstance(value, (tuple, list)):
89
+ return FieldUrlValue(Url=value[0], Description=value[1] if len(value) > 1 else None)
90
+ return FieldUrlValue(Url=str(value))
91
+
92
+
93
+ def _geolocation(value: Any) -> FieldGeolocationValue:
94
+ if isinstance(value, Mapping):
95
+ return FieldGeolocationValue(Latitude=float(value["Latitude"]), Longitude=float(value["Longitude"]))
96
+ if isinstance(value, (tuple, list)) and len(value) > 1:
97
+ return FieldGeolocationValue(Latitude=float(value[0]), Longitude=float(value[1]))
98
+ latitude, _, longitude = str(value).partition(",")
99
+ return FieldGeolocationValue(Latitude=float(latitude), Longitude=float(longitude))
100
+
101
+
102
+ def _datetime(value: Any) -> Any:
103
+ return parse_datetime(value) or value
104
+
105
+
106
+ _CONVERTERS: Dict[FieldType, Callable[[Any], Any]] = {
107
+ FieldType.Boolean: parse_bool,
108
+ FieldType.Integer: try_int,
109
+ FieldType.Counter: try_int,
110
+ FieldType.Number: try_float,
111
+ FieldType.Currency: try_float,
112
+ FieldType.DateTime: _datetime,
113
+ FieldType.MultiChoice: lambda value: FieldMultiChoiceValue([str(item) for item in _as_list(value)]),
114
+ FieldType.Lookup: _lookup_value,
115
+ FieldType.User: _user,
116
+ FieldType.URL: _url,
117
+ FieldType.Geolocation: _geolocation,
118
+ }
119
+
120
+
121
+ def coerce_field_value(field_type: Optional[FieldType], value: Any) -> Any:
122
+ """Coerce a plain value into the payload shape for ``field_type``.
123
+
124
+ Supported: Boolean, Integer, Number/Currency, DateTime, MultiChoice, Lookup
125
+ (single/multi), User (single/multi), URL and Geolocation. Anything else (and
126
+ already-typed ``ClientValue`` inputs) passes through unchanged.
127
+ """
128
+ if field_type is None or value is None or isinstance(value, ClientValue):
129
+ return value
130
+ converter = _CONVERTERS.get(field_type)
131
+ return converter(value) if converter is not None else value
@@ -2,12 +2,6 @@ from __future__ import annotations
2
2
 
3
3
  from typing import TYPE_CHECKING, Any, Optional, TypeVar, Union, cast
4
4
 
5
- from typing_extensions import Self
6
-
7
- from office365.runtime.client_request_exception import (
8
- ClientRequestException,
9
- ObjectNotFoundException,
10
- )
11
5
  from office365.runtime.paths.service_operation import ServiceOperationPath
12
6
  from office365.runtime.queries.create_entity import CreateEntityQuery
13
7
  from office365.runtime.queries.service_operation import ServiceOperationQuery
@@ -42,8 +36,14 @@ _FIELD_TYPE_BY_KIND = {
42
36
 
43
37
 
44
38
  def field_type_from_kind(kind: str) -> FieldType:
45
- """Map a generic pandas dtype kind (see ``series_kind``) to a SharePoint ``FieldType``."""
46
- return _FIELD_TYPE_BY_KIND[kind]
39
+ """Map a generic pandas dtype kind (see ``series_kind``) to a SharePoint ``FieldType``.
40
+
41
+ Unknown kinds fall back to ``FieldType.Text``. Only the four basic column
42
+ types can be inferred from a pandas dtype; richer SharePoint types (Choice,
43
+ Lookup, User, URL, Currency, MultiLineText, ...) require an explicit
44
+ ``schema`` passed to ``List.from_dataframe``.
45
+ """
46
+ return _FIELD_TYPE_BY_KIND.get(kind, FieldType.Text)
47
47
 
48
48
 
49
49
  if TYPE_CHECKING:
@@ -57,59 +57,76 @@ class FieldCollection(EntityCollection[Field]):
57
57
  def __init__(self, context, resource_path=None, parent=None):
58
58
  super().__init__(context, Field, resource_path, parent)
59
59
 
60
- def from_dataframe(self, df, progress=None) -> Self:
60
+ def ensure_from_dataframe(self, df, progress=None, *, on_conflict: str = "skip") -> list[Field]:
61
61
  """Define a field per DataFrame column, inferring the field type.
62
62
 
63
- The schema counterpart of ``ClientObjectCollection.from_dataframe``
64
- (which imports rows): here each column becomes a field definition.
65
- Column names are sanitized into field internal names and the
66
- ``FieldType`` is inferred from the pandas dtype (Boolean/DateTime/
67
- Number/Text). Each field is ensured idempotently (looked up first,
68
- created when missing). Deferred — run the lookups/creates with
69
- ``execute_query()``:
63
+ The schema counterpart of ``RecordCollection.from_dataframe`` (which
64
+ imports rows): here each column becomes a field definition. Column names
65
+ are sanitized into field internal names and the ``FieldType`` is inferred
66
+ from the pandas dtype (Boolean/DateTime/Number/Text). Each field is
67
+ ensured idempotently (looked up first, created when missing). Deferred —
68
+ run the lookups/creates with ``execute_query()``.
70
69
 
71
- >>> lst.fields.from_dataframe(df).execute_query()
70
+ A column whose title collides with a built-in field (e.g. ``Name`` ->
71
+ ``FileLeafRef``) is suffixed with ``_`` and a warning is emitted, since
72
+ SharePoint cannot create a second column with that display name.
72
73
 
73
74
  Requires ``pip install office365-rest-python-client[pandas]``.
74
75
 
75
76
  Args:
76
77
  df: A pandas DataFrame whose columns become the field definitions.
78
+ progress: Reserved (kept for signature parity).
79
+ on_conflict: ``"skip"`` (default) or ``"update"``.
77
80
 
78
81
  Returns:
79
- Self: The field collection, for method chaining.
82
+ list[Field]: The ensured fields.
80
83
  """
84
+ import warnings
85
+
81
86
  from office365.runtime.converters.dataframe import require_pandas, series_kind
82
- from office365.sharepoint.fields.name import internal_field_name
87
+ from office365.sharepoint.fields.name import internal_field_name, is_reserved_field_title
83
88
 
84
89
  pd = require_pandas()
90
+ fields: list[Field] = []
85
91
  for column in df.columns:
92
+ if is_reserved_field_title(str(column)):
93
+ warnings.warn(
94
+ f"Column '{column}' collides with a built-in SharePoint field; "
95
+ f"importing it as '{internal_field_name(str(column))}'.",
96
+ stacklevel=2,
97
+ )
86
98
  field_type = field_type_from_kind(series_kind(pd, df[column]))
87
99
  info = FieldCreationInformation(
88
100
  Title=internal_field_name(str(column)),
89
101
  FieldTypeKind=field_type,
90
102
  )
91
- self.ensure(info)
92
- return self
103
+ fields.append(self.ensure(info, on_conflict=on_conflict))
104
+ return fields
93
105
 
94
- def ensure(self, parameters: FieldCreationInformation) -> Field:
95
- from office365.runtime.queries.deferred import DeferredOperationQuery
106
+ def ensure(self, parameters: FieldCreationInformation, *, on_conflict: str = "skip") -> Field:
107
+ """Ensure the field exists (get-or-create); ``on_conflict="update"`` reconciles it."""
108
+ from office365.runtime.queries.get_or_create import get_or_create
96
109
 
97
110
  return_type = Field(self.context)
98
- barrier = DeferredOperationQuery(self.context, return_type=return_type)
99
-
100
- def _on_success(existing):
101
- return_type.copy_from(existing)
102
- barrier.resolve()
103
111
 
104
- def _on_error(error: ClientRequestException):
105
- if not isinstance(error, ObjectNotFoundException):
106
- raise error
107
-
108
- barrier.defer(self._build_add_field_query(parameters, return_type))
109
-
110
- self.get_by_title(parameters.Title).get().after_execute(_on_success).on_error(_on_error)
111
- self.context.add_query(barrier)
112
- return return_type
112
+ def _reconcile(field: Field) -> None:
113
+ changed = False
114
+ if parameters.FieldTypeKind is not None and field.field_type_kind != parameters.FieldTypeKind:
115
+ field.set_property("FieldTypeKind", parameters.FieldTypeKind)
116
+ changed = True
117
+ if parameters.Description is not None and field.properties.get("Description") != parameters.Description:
118
+ field.set_property("Description", parameters.Description)
119
+ changed = True
120
+ if changed:
121
+ field.update()
122
+
123
+ return get_or_create(
124
+ find=lambda: self.get_by_title(parameters.Title).get(),
125
+ create_query=lambda: self._build_add_field_query(parameters, return_type),
126
+ return_type=return_type,
127
+ on_conflict=on_conflict,
128
+ reconcile=_reconcile,
129
+ )
113
130
 
114
131
  def add_calculated(self, title: str, formula: str, description: Optional[str] = None) -> FieldCalculated:
115
132
  """Creates a Calculated field
@@ -279,6 +279,23 @@ class Field(Entity):
279
279
  def indexed(self):
280
280
  return self.properties.get("Indexed", None)
281
281
 
282
+ def ensure_indexed(self) -> Self:
283
+ """Ensure this field is indexed (idempotent, deferred).
284
+
285
+ Indexing a column lets queries filter/sort on it past the 5,000-item list
286
+ view threshold. Run with ``execute_query()``.
287
+ """
288
+
289
+ def _enable(_=None) -> None:
290
+ if self.indexed is not True:
291
+ self.enable_index()
292
+
293
+ if self.indexed is None:
294
+ self.ensure_property("Indexed").after_execute(_enable)
295
+ else:
296
+ _enable()
297
+ return self
298
+
282
299
  @property
283
300
  def type_display_name(self) -> Optional[str]:
284
301
  """
@@ -2,10 +2,54 @@ from __future__ import annotations
2
2
 
3
3
  import re
4
4
 
5
+ # Built-in SharePoint field titles that collide with a user column of the same
6
+ # name. ``FieldCollection.ensure`` looks fields up by *display name*
7
+ # (``getByTitle``), so a column titled e.g. "Name" resolves to the built-in
8
+ # ``FileLeafRef`` field and is never created as a custom column. Such titles are
9
+ # suffixed with ``_`` so the column can be imported (and the item keys stay
10
+ # consistent, since both field creation and record keys use this mapping).
11
+ _RESERVED_FIELD_TITLES = frozenset(
12
+ {
13
+ "name", # FileLeafRef
14
+ "title",
15
+ "id",
16
+ "created",
17
+ "modified",
18
+ "author",
19
+ "editor",
20
+ "attachments",
21
+ "contenttype",
22
+ "contenttypeid",
23
+ "version",
24
+ "uniqueid",
25
+ "fileleafref",
26
+ "fileref",
27
+ "filedirref",
28
+ "fsobjtype",
29
+ "owshiddenversion",
30
+ "workflowversion",
31
+ "metainfo",
32
+ "scopeid",
33
+ "instanceid",
34
+ "permask",
35
+ }
36
+ )
37
+
5
38
 
6
39
  def internal_field_name(name: str) -> str:
7
40
  """Sanitize a display/column name into a SharePoint field internal name.
8
41
 
9
- SharePoint field internal names cannot contain spaces or punctuation.
42
+ SharePoint field internal names cannot contain spaces or punctuation, and a
43
+ title that collides with a built-in field's display name (e.g. ``Name`` ->
44
+ ``FileLeafRef``) cannot be created as a custom column — those are suffixed
45
+ with ``_`` so the import still works.
10
46
  """
11
- return re.sub(r"\W", "_", name)
47
+ sanitized = re.sub(r"\W", "_", name)
48
+ if sanitized.lower() in _RESERVED_FIELD_TITLES:
49
+ return f"{sanitized}_"
50
+ return sanitized
51
+
52
+
53
+ def is_reserved_field_title(name: str) -> bool:
54
+ """Whether a column title collides with a built-in SharePoint field title."""
55
+ return re.sub(r"\W", "_", name).lower() in _RESERVED_FIELD_TITLES
@@ -17,12 +17,13 @@ from office365.sharepoint.files.creation_information import FileCreationInformat
17
17
  from office365.sharepoint.files.file import File
18
18
  from office365.sharepoint.files.publish.status import FileStatus
19
19
  from office365.sharepoint.pages.template_file_type import TemplateFileType
20
+ from office365.sharepoint.thresholds import LIST_VIEW_THRESHOLD, Limits, ensure_within, limit
20
21
  from office365.sharepoint.types.resource_path import ResourcePath as SPResPath
21
22
 
22
23
  if TYPE_CHECKING:
23
24
  from office365.sharepoint.folders.folder import Folder
24
25
 
25
- _DEFAULT_CHUNK_SIZE = 4 * 1024 * 1024 # simple-upload threshold / upload-session chunk
26
+ _DEFAULT_CHUNK_SIZE = Limits.UPLOAD_SESSION_CHUNK.value # simple-upload threshold / upload-session chunk
26
27
 
27
28
 
28
29
  def _stream_size(stream: IO) -> int:
@@ -47,6 +48,9 @@ def _stream_size(stream: IO) -> int:
47
48
  class FileCollection(EntityCollection[File]):
48
49
  """Represents a collection of File resources."""
49
50
 
51
+ _list_view_threshold = LIST_VIEW_THRESHOLD
52
+ _truncation_hint = "page it with Folder.get_files(page_size=2000) or files.get_all(page_size=2000)"
53
+
50
54
  def __init__(
51
55
  self,
52
56
  context: ClientContext,
@@ -82,6 +86,7 @@ class FileCollection(EntityCollection[File]):
82
86
  content = path_or_file.read()
83
87
  return self.add(name, content, True)
84
88
 
89
+ @limit(Limits.FILE_UPLOAD)
85
90
  def upload_content(
86
91
  self,
87
92
  content: bytes,
@@ -103,6 +108,7 @@ class FileCollection(EntityCollection[File]):
103
108
  (``done``/``total`` in bytes; per chunk for session uploads, once
104
109
  after the upload completes for the simple path).
105
110
  """
111
+ ensure_within(Limits.FILE_UPLOAD, len(content), context=f"file '{file_name}'")
106
112
  if len(content) <= chunk_size:
107
113
  file = self.upload(io.BytesIO(content), file_name)
108
114
  if callable(progress):
@@ -49,6 +49,7 @@ from office365.sharepoint.webparts.limited_manager import LimitedWebPartManager
49
49
  from office365.sharepoint.webparts.personalization_scope import PersonalizationScope
50
50
 
51
51
  if TYPE_CHECKING:
52
+ from office365.runtime.converters.dataframe import DataFrameResult
52
53
  from office365.sharepoint.client_context import ClientContext
53
54
 
54
55
 
@@ -145,6 +146,37 @@ class File(AbstractFile):
145
146
  self.context.add_query(qry)
146
147
  return return_type
147
148
 
149
+ def read_dataframe(self, *, format: str = "csv", **opts) -> "DataFrameResult": # noqa: A002
150
+ """Read this file's **content** into a pandas DataFrame (deferred result).
151
+
152
+ Parses the file content (CSV/XLSX/JSON/Parquet) — it does **not** export
153
+ the file's metadata; those are plain properties (``file.name``,
154
+ ``file.length``, ``file.time_last_modified``, ...). Run with
155
+ ``execute_query()`` and read ``.value``:
156
+
157
+ >>> df = file.read_dataframe().execute_query().value
158
+ """
159
+ from office365.runtime.converters.dataframe import DataFrameResult, dataframe_from_bytes
160
+
161
+ result = DataFrameResult(self.context)
162
+
163
+ def _convert(return_type: ClientResult[bytes]) -> None:
164
+ result.set_property("__value", dataframe_from_bytes(return_type.value, format, **opts))
165
+
166
+ self.get_content().after_execute(_convert)
167
+ return result
168
+
169
+ def write_dataframe(self, df, *, format: str = "csv", index: bool = False, **opts) -> Self: # noqa: A002
170
+ """Write a pandas DataFrame to this file's **content** (deferred).
171
+
172
+ Serializes the frame (CSV is UTF-8 with a BOM) and queues a
173
+ ``SaveBinaryStream``; run with ``execute_query()``. This replaces the
174
+ file content, not its metadata.
175
+ """
176
+ from office365.runtime.converters.dataframe import dataframe_to_bytes
177
+
178
+ return self.save_binary_stream(dataframe_to_bytes(df, format, index, **opts))
179
+
148
180
  def get_exists(self) -> ClientResult[bool]:
149
181
  result = ClientResult(self.context, bool())
150
182
  result.set_property("__value", False)
@@ -272,6 +304,22 @@ class File(AbstractFile):
272
304
  self.context.add_query(qry)
273
305
  return return_type
274
306
 
307
+ def delete_object(self, bypass_shared_lock: bool = False) -> Self:
308
+ """Deletes the file.
309
+
310
+ Args:
311
+ bypass_shared_lock: Send ``Prefer: bypass-shared-lock`` so the delete
312
+ succeeds even when the file is open for editing. Without it the
313
+ server rejects the request with
314
+ :class:`~office365.runtime.exceptions.FileLockedException` (HTTP 423).
315
+ """
316
+ super().delete_object()
317
+ if bypass_shared_lock:
318
+ from office365.runtime.http.prefer import prefer_bypass_shared_lock
319
+
320
+ self.context.before_execute(prefer_bypass_shared_lock, once=True)
321
+ return self
322
+
275
323
  def recycle(self) -> ClientResult[str]:
276
324
  """Moves the file to the Recycle Bin and returns the identifier of the new Recycle Bin item."""
277
325
  return_type = ClientResult(self.context, str())
@@ -686,10 +734,9 @@ class File(AbstractFile):
686
734
  # OData literals escape embedded single quotes by doubling them (' -> '')
687
735
  decoded_server_relative_url = decoded.replace("'", "''")
688
736
 
689
- url = quote(
690
- rf"{context.service_root_url}/web/getFileByServerRelativePath"
691
- rf"(DecodedUrl='{decoded_server_relative_url}')/\$value",
692
- safe=":/",
737
+ url = (
738
+ f"{context.service_root_url}/web/getFileByServerRelativePath"
739
+ f"(DecodedUrl='{quote(decoded_server_relative_url)}')/$value"
693
740
  )
694
741
 
695
742
  request = RequestOptions(url)
@@ -711,10 +758,9 @@ class File(AbstractFile):
711
758
  # OData literals escape embedded single quotes by doubling them (' -> '')
712
759
  decoded_server_relative_url = decoded.replace("'", "''")
713
760
 
714
- url = quote(
715
- rf"{context.service_root_url}/web/getFileByServerRelativePath("
716
- rf"DecodedUrl='{decoded_server_relative_url}')/\$value",
717
- safe=":/",
761
+ url = (
762
+ f"{context.service_root_url}/web/getFileByServerRelativePath"
763
+ f"(DecodedUrl='{quote(decoded_server_relative_url)}')/$value"
718
764
  )
719
765
 
720
766
  request = RequestOptions(url)
@@ -5,7 +5,6 @@ from typing_extensions import Self
5
5
 
6
6
  from office365.runtime.client_result import ClientResult
7
7
  from office365.runtime.paths.resource_path import ResourcePath
8
- from office365.runtime.paths.v3.entity import EntityPath
9
8
  from office365.runtime.queries.service_operation import ServiceOperationQuery
10
9
  from office365.runtime.types.odata_property import odata
11
10
  from office365.sharepoint.entity import Entity
@@ -57,10 +56,7 @@ class FileVersion(Entity):
57
56
  """Gets the user that created the file version."""
58
57
  from office365.sharepoint.principal.users.user import User
59
58
 
60
- return self.properties.get(
61
- "CreatedBy",
62
- User(self.context, ResourcePath("CreatedBy", self.resource_path)),
63
- )
59
+ return self.properties.get("CreatedBy", User(self.context, ResourcePath("CreatedBy", self.resource_path)))
64
60
 
65
61
  @property
66
62
  def id(self) -> Optional[int]:
@@ -87,12 +83,41 @@ class FileVersion(Entity):
87
83
  """Gets a value that specifies the check-in comment."""
88
84
  return self.properties.get("CheckInComment", None)
89
85
 
90
- def set_property(self, key, value, persist_changes=True): # type: ignore[override]
91
- super().set_property(key, value, persist_changes)
92
- if key.lower() == self.property_ref_name.lower():
93
- assert self.parent_collection is not None
94
- if self._resource_path is None:
95
- self._resource_path = EntityPath(value, self.parent_collection.resource_path)
96
- else:
97
- self._resource_path.set_segment(value)
86
+ @property
87
+ def check_in_comment(self) -> Optional[str]:
88
+ """Gets the CheckInComment property"""
89
+ return self.properties.get("CheckInComment", None)
90
+
91
+ @property
92
+ def expiration_date(self) -> Optional[str]:
93
+ """Gets the ExpirationDate property"""
94
+ return self.properties.get("ExpirationDate", None)
95
+
96
+ @property
97
+ def length(self) -> Optional[int]:
98
+ """Gets the Length property"""
99
+ return self.properties.get("Length", None)
100
+
101
+ @property
102
+ def size(self) -> Optional[int]:
103
+ """Gets the Size property"""
104
+ return self.properties.get("Size", None)
105
+
106
+ @property
107
+ def snapshot_date(self) -> Optional[str]:
108
+ """Gets the SnapshotDate property"""
109
+ return self.properties.get("SnapshotDate", None)
110
+
111
+ @property
112
+ def property_ref_name(self) -> str:
113
+ return "ID"
114
+
115
+ def set_expiration_date(self, expiration_date: datetime) -> Self:
116
+ """SetExpirationDate operation.
117
+
118
+ Args:
119
+ expiration_date (datetime): expirationDate parameter
120
+ """
121
+ qry = ServiceOperationQuery(self, "SetExpirationDate", None, {"expirationDate": expiration_date}, None, None)
122
+ self.context.add_query(qry)
98
123
  return self
@@ -10,12 +10,16 @@ from office365.sharepoint.folders.coloring_information import (
10
10
  )
11
11
  from office365.sharepoint.folders.colors import FolderColors
12
12
  from office365.sharepoint.folders.folder import Folder
13
+ from office365.sharepoint.thresholds import LIST_VIEW_THRESHOLD
13
14
  from office365.sharepoint.types.resource_path import ResourcePath as SPResPath
14
15
 
15
16
 
16
17
  class FolderCollection(EntityCollection[Folder]):
17
18
  """Represents a collection of Folder resources."""
18
19
 
20
+ _list_view_threshold = LIST_VIEW_THRESHOLD
21
+ _truncation_hint = "page it with folders.get_all(page_size=2000)"
22
+
19
23
  def __init__(self, context, resource_path=None, parent=None):
20
24
  super().__init__(context, Folder, resource_path, parent)
21
25