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,424 @@
1
+ """Typed CAML query expressions.
2
+
3
+ Build a ``<Where>`` clause from composable Python objects instead of raw CAML
4
+ strings. Every ``And``/``Or`` node holds **exactly two** children, so any
5
+ combination renders valid, binary-nested CAML (CAML forbids 3+ children):
6
+
7
+ from office365.sharepoint.listitems.caml import Caml
8
+
9
+ where = (
10
+ Caml.text("Email").eq("support@google.com")
11
+ .or_(Caml.text("Email").eq("plus@google.com"))
12
+ .or_(Caml.text("Title").begins_with("[Google]"))
13
+ )
14
+ # identical alternatives:
15
+ where = Caml.or_(a, b, c) # variadic -> left-folded binary nesting
16
+ where = a | b | c # operators
17
+
18
+ Logical operators are ``&`` (And), ``|`` (Or), ``~`` (Not); the method forms
19
+ ``.and_()/.or_()/.not_()`` read closer to CAML. Python's ``and``/``or``/``not``
20
+ cannot be used (they are keywords) and are rejected at runtime via ``__bool__``.
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ from abc import ABC, abstractmethod
26
+ from typing import Any, Callable, Iterable, Optional, TypeVar
27
+
28
+ from office365.sharepoint.listitems.caml.fields import FieldRef
29
+ from office365.sharepoint.listitems.caml.values import Value, ValueType, list_property
30
+ from office365.sharepoint.listitems.caml.values import month as _month
31
+ from office365.sharepoint.listitems.caml.values import now as _now
32
+ from office365.sharepoint.listitems.caml.values import today as _today
33
+ from office365.sharepoint.listitems.caml.values import user_id as _user_id
34
+
35
+ _T = TypeVar("_T")
36
+
37
+
38
+ class CamlExpr(ABC):
39
+ """Base class for CAML query expressions."""
40
+
41
+ @abstractmethod
42
+ def to_xml(self) -> str:
43
+ """Render this expression as CAML XML."""
44
+
45
+ @property
46
+ def field_refs(self) -> set[str]:
47
+ """The field names referenced by this expression."""
48
+ return set()
49
+
50
+ def __and__(self, other: "CamlExpr") -> "And":
51
+ return And(self, other)
52
+
53
+ def __or__(self, other: "CamlExpr") -> "Or":
54
+ return Or(self, other)
55
+
56
+ def __invert__(self) -> "Not":
57
+ return Not(self)
58
+
59
+ def and_(self, other: "CamlExpr") -> "And":
60
+ """Combine with ``other`` using ``<And>`` (same as ``&``)."""
61
+ return And(self, other)
62
+
63
+ def or_(self, other: "CamlExpr") -> "Or":
64
+ """Combine with ``other`` using ``<Or>`` (same as ``|``)."""
65
+ return Or(self, other)
66
+
67
+ def not_(self) -> "Not":
68
+ """Negate this expression (same as ``~``)."""
69
+ return Not(self)
70
+
71
+ def __bool__(self) -> bool:
72
+ raise TypeError(
73
+ "CAML expressions cannot be used with Python 'and'/'or'/'not'; "
74
+ "use '&', '|', '~' or the .and_()/.or_()/.not_() methods"
75
+ )
76
+
77
+
78
+ class Comparison(CamlExpr):
79
+ """A comparison operator element, e.g. ``<Eq><FieldRef/><Value/></Eq>``."""
80
+
81
+ def __init__(self, op: str, field: FieldRef, value: Value) -> None:
82
+ self.op = op
83
+ self.field = field
84
+ self.value = value
85
+
86
+ @property
87
+ def field_refs(self) -> set[str]:
88
+ return {self.field.name}
89
+
90
+ def to_xml(self) -> str:
91
+ return f"<{self.op}>{self.field.to_xml()}{self.value.to_xml()}</{self.op}>"
92
+
93
+
94
+ class IsNull(CamlExpr):
95
+ """``<IsNull>`` (or ``<IsNotNull>``) element."""
96
+
97
+ def __init__(self, field: FieldRef, negate: bool = False) -> None:
98
+ self.field = field
99
+ self.negate = negate
100
+
101
+ @property
102
+ def field_refs(self) -> set[str]:
103
+ return {self.field.name}
104
+
105
+ def to_xml(self) -> str:
106
+ tag = "IsNotNull" if self.negate else "IsNull"
107
+ return f"<{tag}>{self.field.to_xml()}</{tag}>"
108
+
109
+
110
+ class In(CamlExpr):
111
+ """``<In>`` element over a list of values."""
112
+
113
+ def __init__(self, field: FieldRef, values: Iterable[Value]) -> None:
114
+ self.field = field
115
+ self.values = list(values)
116
+
117
+ @property
118
+ def field_refs(self) -> set[str]:
119
+ return {self.field.name}
120
+
121
+ def to_xml(self) -> str:
122
+ values = "".join(value.to_xml() for value in self.values)
123
+ return f"<In>{self.field.to_xml()}<Values>{values}</Values></In>"
124
+
125
+
126
+ class Membership(CamlExpr):
127
+ """``<Membership>`` element (e.g. ``Type="CurrentUserGroups"``)."""
128
+
129
+ def __init__(self, field: FieldRef, type: str = "CurrentUserGroups") -> None: # noqa: A002
130
+ self.field = field
131
+ self.type = type
132
+
133
+ @property
134
+ def field_refs(self) -> set[str]:
135
+ return {self.field.name}
136
+
137
+ def to_xml(self) -> str:
138
+ from xml.sax.saxutils import quoteattr
139
+
140
+ return f"<Membership Type={quoteattr(self.type)}>{self.field.to_xml()}</Membership>"
141
+
142
+
143
+ class DateRangesOverlap(CamlExpr):
144
+ """``<DateRangesOverlap>`` element."""
145
+
146
+ def __init__(self, field: FieldRef, value: Value) -> None:
147
+ self.field = field
148
+ self.value = value
149
+
150
+ @property
151
+ def field_refs(self) -> set[str]:
152
+ return {self.field.name}
153
+
154
+ def to_xml(self) -> str:
155
+ return f"<DateRangesOverlap>{self.field.to_xml()}{self.value.to_xml()}</DateRangesOverlap>"
156
+
157
+
158
+ class _Binary(CamlExpr):
159
+ """Base for the binary logical joins (``<And>``/``<Or>``)."""
160
+
161
+ tag = ""
162
+
163
+ def __init__(self, left: CamlExpr, right: CamlExpr) -> None:
164
+ self.left = left
165
+ self.right = right
166
+
167
+ @property
168
+ def field_refs(self) -> set[str]:
169
+ return self.left.field_refs | self.right.field_refs
170
+
171
+ def to_xml(self) -> str:
172
+ return f"<{self.tag}>{self.left.to_xml()}{self.right.to_xml()}</{self.tag}>"
173
+
174
+
175
+ class And(_Binary):
176
+ """``<And>`` — exactly two children (nest for 3+)."""
177
+
178
+ tag = "And"
179
+
180
+
181
+ class Or(_Binary):
182
+ """``<Or>`` — exactly two children (nest for 3+)."""
183
+
184
+ tag = "Or"
185
+
186
+
187
+ class Not(CamlExpr):
188
+ """``<Not>`` — exactly one child."""
189
+
190
+ def __init__(self, child: CamlExpr) -> None:
191
+ self.child = child
192
+
193
+ @property
194
+ def field_refs(self) -> set[str]:
195
+ return self.child.field_refs
196
+
197
+ def to_xml(self) -> str:
198
+ return f"<Not>{self.child.to_xml()}</Not>"
199
+
200
+
201
+ def _fold(op: Callable[[CamlExpr, CamlExpr], _T], exprs: Iterable[CamlExpr]) -> _T:
202
+ """Left-fold conditions into binary-nested nodes (``And(And(A,B),C)``)."""
203
+ items = list(exprs)
204
+ if not items:
205
+ raise ValueError("at least one condition is required")
206
+ result: Any = items[0]
207
+ for item in items[1:]:
208
+ result = op(result, item)
209
+ return result # type: ignore[no-any-return]
210
+
211
+
212
+ def _as_field_ref(field: Any) -> FieldRef:
213
+ if isinstance(field, FieldRef):
214
+ return field
215
+ if isinstance(field, Field):
216
+ return field.field_ref
217
+ return FieldRef(str(field))
218
+
219
+
220
+ def _as_value(value: Any, value_type: Optional[ValueType] = None, **value_opts: Any) -> Value:
221
+ if isinstance(value, Value):
222
+ return value
223
+ return Value(value, type=value_type, **value_opts)
224
+
225
+
226
+ class Field:
227
+ """A typed field with fluent comparison methods.
228
+
229
+ ``Caml.text("Status").eq("Active")`` renders ``<Eq>`` with ``Type='Text'``;
230
+ ``Caml.lookup("Category").id().in_([2, 3])`` renders an ``<In>`` over
231
+ ``Integer`` values with ``LookupId="TRUE"``.
232
+ """
233
+
234
+ def __init__(self, name: str, value_type: ValueType = ValueType.Text, *, lookup_id: bool = False) -> None:
235
+ self.name = name
236
+ self.value_type = value_type
237
+ self.lookup_id = lookup_id
238
+
239
+ @property
240
+ def field_ref(self) -> FieldRef:
241
+ return FieldRef(self.name, lookup_id=self.lookup_id)
242
+
243
+ def id(self) -> "Field":
244
+ """Treat this field as a lookup id (``LookupId="TRUE"``, ``Integer``)."""
245
+ return Field(self.name, ValueType.Integer, lookup_id=True)
246
+
247
+ def _compare(self, op: str, value: Any, **value_opts: Any) -> Comparison:
248
+ return Comparison(op, self.field_ref, _as_value(value, self.value_type, **value_opts))
249
+
250
+ def eq(self, value: Any, **value_opts: Any) -> Comparison:
251
+ return self._compare("Eq", value, **value_opts)
252
+
253
+ def neq(self, value: Any, **value_opts: Any) -> Comparison:
254
+ return self._compare("Neq", value, **value_opts)
255
+
256
+ def gt(self, value: Any, **value_opts: Any) -> Comparison:
257
+ return self._compare("Gt", value, **value_opts)
258
+
259
+ def geq(self, value: Any, **value_opts: Any) -> Comparison:
260
+ return self._compare("Geq", value, **value_opts)
261
+
262
+ def lt(self, value: Any, **value_opts: Any) -> Comparison:
263
+ return self._compare("Lt", value, **value_opts)
264
+
265
+ def leq(self, value: Any, **value_opts: Any) -> Comparison:
266
+ return self._compare("Leq", value, **value_opts)
267
+
268
+ def begins_with(self, value: Any, **value_opts: Any) -> Comparison:
269
+ return self._compare("BeginsWith", value, **value_opts)
270
+
271
+ def contains(self, value: Any, **value_opts: Any) -> Comparison:
272
+ return self._compare("Contains", value, **value_opts)
273
+
274
+ def not_contains(self, value: Any, **value_opts: Any) -> Comparison:
275
+ return self._compare("NotContains", value, **value_opts)
276
+
277
+ def includes(self, value: Any, **value_opts: Any) -> Comparison:
278
+ return self._compare("Includes", value, **value_opts)
279
+
280
+ def not_includes(self, value: Any, **value_opts: Any) -> Comparison:
281
+ return self._compare("NotIncludes", value, **value_opts)
282
+
283
+ def is_null(self) -> IsNull:
284
+ return IsNull(self.field_ref)
285
+
286
+ def is_not_null(self) -> IsNull:
287
+ return IsNull(self.field_ref, negate=True)
288
+
289
+ def in_(self, values: Iterable[Any]) -> In:
290
+ return In(self.field_ref, [_as_value(value, self.value_type) for value in values])
291
+
292
+ def eq_current_user(self) -> Comparison:
293
+ """``<Eq><FieldRef LookupId='TRUE'/><Value><UserID/></Value></Eq>``."""
294
+ return Comparison("Eq", self.field_ref, _user_id())
295
+
296
+ def in_current_user_groups(self) -> Membership:
297
+ """``<Membership Type='CurrentUserGroups'>``."""
298
+ return Membership(self.field_ref, "CurrentUserGroups")
299
+
300
+
301
+ class Caml:
302
+ """Factory namespace for CAML expressions and values."""
303
+
304
+ # ── Special values ───────────────────────────────────────────
305
+ now: Value = _now()
306
+ today: Value = _today()
307
+ month: Value = _month()
308
+ user_id: Value = _user_id()
309
+ list_property = staticmethod(list_property)
310
+
311
+ # ── Typed fields ─────────────────────────────────────────────
312
+ @staticmethod
313
+ def field(name: str, value_type: ValueType = ValueType.Text, *, lookup_id: bool = False) -> Field:
314
+ return Field(name, value_type, lookup_id=lookup_id)
315
+
316
+ @staticmethod
317
+ def text(name: str) -> Field:
318
+ return Field(name, ValueType.Text)
319
+
320
+ @staticmethod
321
+ def number(name: str) -> Field:
322
+ return Field(name, ValueType.Number)
323
+
324
+ @staticmethod
325
+ def integer(name: str) -> Field:
326
+ return Field(name, ValueType.Integer)
327
+
328
+ @staticmethod
329
+ def date(name: str) -> Field:
330
+ return Field(name, ValueType.DateTime)
331
+
332
+ @staticmethod
333
+ def boolean(name: str) -> Field:
334
+ return Field(name, ValueType.Boolean)
335
+
336
+ @staticmethod
337
+ def lookup(name: str, *, id: bool = False) -> Field: # noqa: A002
338
+ return Field(name, ValueType.Integer, lookup_id=id)
339
+
340
+ @staticmethod
341
+ def user(name: str) -> Field:
342
+ return Field(name, ValueType.Integer, lookup_id=True)
343
+
344
+ # ── Comparisons (by field name, Field or FieldRef) ───────────
345
+ @staticmethod
346
+ def _compare(op: str, field: Any, value: Any, value_type: Optional[ValueType] = None) -> Comparison:
347
+ return Comparison(op, _as_field_ref(field), _as_value(value, value_type))
348
+
349
+ @staticmethod
350
+ def eq(field: Any, value: Any) -> Comparison:
351
+ return Caml._compare("Eq", field, value)
352
+
353
+ @staticmethod
354
+ def neq(field: Any, value: Any) -> Comparison:
355
+ return Caml._compare("Neq", field, value)
356
+
357
+ @staticmethod
358
+ def gt(field: Any, value: Any) -> Comparison:
359
+ return Caml._compare("Gt", field, value)
360
+
361
+ @staticmethod
362
+ def geq(field: Any, value: Any) -> Comparison:
363
+ return Caml._compare("Geq", field, value)
364
+
365
+ @staticmethod
366
+ def lt(field: Any, value: Any) -> Comparison:
367
+ return Caml._compare("Lt", field, value)
368
+
369
+ @staticmethod
370
+ def leq(field: Any, value: Any) -> Comparison:
371
+ return Caml._compare("Leq", field, value)
372
+
373
+ @staticmethod
374
+ def begins_with(field: Any, value: Any) -> Comparison:
375
+ return Caml._compare("BeginsWith", field, value)
376
+
377
+ @staticmethod
378
+ def contains(field: Any, value: Any) -> Comparison:
379
+ return Caml._compare("Contains", field, value)
380
+
381
+ @staticmethod
382
+ def not_contains(field: Any, value: Any) -> Comparison:
383
+ return Caml._compare("NotContains", field, value)
384
+
385
+ @staticmethod
386
+ def includes(field: Any, value: Any) -> Comparison:
387
+ return Caml._compare("Includes", field, value)
388
+
389
+ @staticmethod
390
+ def not_includes(field: Any, value: Any) -> Comparison:
391
+ return Caml._compare("NotIncludes", field, value)
392
+
393
+ @staticmethod
394
+ def is_null(field: Any) -> IsNull:
395
+ return IsNull(_as_field_ref(field))
396
+
397
+ @staticmethod
398
+ def is_not_null(field: Any) -> IsNull:
399
+ return IsNull(_as_field_ref(field), negate=True)
400
+
401
+ @staticmethod
402
+ def in_(field: Any, values: Iterable[Any], value_type: Optional[ValueType] = None) -> In:
403
+ return In(_as_field_ref(field), [_as_value(value, value_type) for value in values])
404
+
405
+ @staticmethod
406
+ def membership(field: Any, type: str = "CurrentUserGroups") -> Membership: # noqa: A002
407
+ return Membership(_as_field_ref(field), type)
408
+
409
+ @staticmethod
410
+ def date_ranges_overlap(field: Any, value: Any) -> DateRangesOverlap:
411
+ return DateRangesOverlap(_as_field_ref(field), _as_value(value))
412
+
413
+ # ── Logical joins ────────────────────────────────────────────
414
+ @staticmethod
415
+ def and_(*exprs: CamlExpr) -> CamlExpr:
416
+ return _fold(And, exprs)
417
+
418
+ @staticmethod
419
+ def or_(*exprs: CamlExpr) -> CamlExpr:
420
+ return _fold(Or, exprs)
421
+
422
+ @staticmethod
423
+ def not_(expr: CamlExpr) -> Not:
424
+ return Not(expr)
@@ -0,0 +1,68 @@
1
+ """Typed CAML ``<FieldRef>`` nodes."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Optional
6
+ from xml.sax.saxutils import quoteattr
7
+
8
+
9
+ class FieldRef:
10
+ """A CAML ``<FieldRef>`` element.
11
+
12
+ ``Ascending``/``Collapse`` apply inside ``<OrderBy>``/``<GroupBy>``;
13
+ ``LookupId``/``RefType``/``List`` support lookup and join clauses.
14
+ """
15
+
16
+ def __init__(
17
+ self,
18
+ name: str,
19
+ *,
20
+ ascending: Optional[bool] = None,
21
+ collapse: Optional[bool] = None,
22
+ nullable: Optional[bool] = None,
23
+ lookup_id: bool = False,
24
+ ref_type: Optional[str] = None,
25
+ list_alias: Optional[str] = None,
26
+ ) -> None:
27
+ self.name = name
28
+ self.ascending = ascending
29
+ self.collapse = collapse
30
+ self.nullable = nullable
31
+ self.lookup_id = lookup_id
32
+ self.ref_type = ref_type
33
+ self.list_alias = list_alias
34
+
35
+ def asc(self) -> "FieldRef":
36
+ """A copy sorted ascending (for ``<OrderBy>``)."""
37
+ return self._copy(ascending=True)
38
+
39
+ def desc(self) -> "FieldRef":
40
+ """A copy sorted descending (for ``<OrderBy>``)."""
41
+ return self._copy(ascending=False)
42
+
43
+ def _copy(self, **overrides) -> "FieldRef":
44
+ return FieldRef(
45
+ self.name,
46
+ ascending=overrides.get("ascending", self.ascending),
47
+ collapse=overrides.get("collapse", self.collapse),
48
+ nullable=overrides.get("nullable", self.nullable),
49
+ lookup_id=self.lookup_id,
50
+ ref_type=self.ref_type,
51
+ list_alias=self.list_alias,
52
+ )
53
+
54
+ def to_xml(self) -> str:
55
+ attrs = [f"Name={quoteattr(self.name)}"]
56
+ if self.ascending is not None:
57
+ attrs.append(f'Ascending="{str(self.ascending).upper()}"')
58
+ if self.collapse is not None:
59
+ attrs.append(f'Collapse="{str(self.collapse).upper()}"')
60
+ if self.nullable is not None:
61
+ attrs.append(f'Nullable="{str(self.nullable).upper()}"')
62
+ if self.lookup_id:
63
+ attrs.append('LookupId="TRUE"')
64
+ if self.ref_type is not None:
65
+ attrs.append(f"RefType={quoteattr(self.ref_type)}")
66
+ if self.list_alias is not None:
67
+ attrs.append(f"List={quoteattr(self.list_alias)}")
68
+ return f"<FieldRef {' '.join(attrs)}/>"
@@ -0,0 +1,63 @@
1
+ """CAML guards for the SharePoint **list view threshold**.
2
+
3
+ CAML parsing lives on :class:`~office365.sharepoint.listitems.caml.query.CamlQuery`
4
+ (``is_paged``/``field_refs``); this module owns the policy that turns it into
5
+ warnings and the columns worth indexing. It is a plain function API (not a
6
+ decorator), so it stays explicit and easy to test in isolation.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import warnings
12
+ from typing import Optional
13
+
14
+ from office365.sharepoint.listitems.caml.query import CamlQuery
15
+ from office365.sharepoint.thresholds import LIST_VIEW_THRESHOLD, SAFE_PAGE_SIZE
16
+
17
+
18
+ def indexing_candidates(query: CamlQuery) -> set[str]:
19
+ """Field names referenced by the query that are worth indexing (``ID`` excluded)."""
20
+ return set(query.index_candidates)
21
+
22
+
23
+ def threshold_warnings(
24
+ query: CamlQuery,
25
+ *,
26
+ item_count: Optional[int] = None,
27
+ threshold: int = LIST_VIEW_THRESHOLD,
28
+ ) -> list[str]:
29
+ """Warnings for a CAML query that may exceed the list view threshold.
30
+
31
+ Empty when the query is paged, when the list is known to be at/below the
32
+ threshold, or when an unpaged query references no columns (a plain
33
+ ``create_all_items_query`` on a list whose size is unknown).
34
+ """
35
+ if query.is_paged:
36
+ return []
37
+ if item_count is not None and item_count <= threshold:
38
+ return []
39
+ names = sorted(indexing_candidates(query))
40
+ if not names and item_count is None:
41
+ return []
42
+ fields = f" on column(s) {', '.join(repr(name) for name in names)}" if names else ""
43
+ hint = (
44
+ f"index them with ensure_indexed(...) (e.g. lst.ensure_indexed({names[0]!r}).execute_query())"
45
+ if names
46
+ else "index the filtered/sorted columns with ensure_indexed(...)"
47
+ )
48
+ return [
49
+ f"This CAML query is not paged and may exceed the SharePoint list view threshold "
50
+ f"({threshold:,} items){fields}. Pass page_size={SAFE_PAGE_SIZE} to get_items(), add "
51
+ f"RowLimit Paged='TRUE' and iterate, or {hint} to avoid throttling."
52
+ ]
53
+
54
+
55
+ def warn_if_unpaged(
56
+ query: CamlQuery,
57
+ *,
58
+ item_count: Optional[int] = None,
59
+ threshold: int = LIST_VIEW_THRESHOLD,
60
+ ) -> None:
61
+ """Emit a :class:`UserWarning` per issue from :func:`threshold_warnings`."""
62
+ for message in threshold_warnings(query, item_count=item_count, threshold=threshold):
63
+ warnings.warn(message, stacklevel=3)
@@ -1,14 +1,22 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import re
3
4
  from dataclasses import dataclass
5
+ from typing import TYPE_CHECKING, Any, Iterator, Tuple
4
6
 
5
7
  from office365.runtime.client_value import ClientValue
8
+ from office365.sharepoint.fields.builtin_field_name import SPBuiltInFieldName
6
9
  from office365.sharepoint.listitems.collection_position import (
7
10
  ListItemCollectionPosition,
8
11
  )
9
12
  from office365.sharepoint.types.resource_path import ResourcePath
10
13
  from office365.sharepoint.views.scope import ViewScope
11
14
 
15
+ if TYPE_CHECKING:
16
+ from office365.sharepoint.listitems.caml.builder import QueryBuilder
17
+
18
+ _FIELD_REF_RE = re.compile(r"<FieldRef\s+Name=['\"]([^'\"]+)['\"]", re.IGNORECASE)
19
+
12
20
 
13
21
  @dataclass
14
22
  class CamlQuery(ClientValue):
@@ -31,6 +39,23 @@ class CamlQuery(ClientValue):
31
39
  AllowIncrementalResults: bool = True
32
40
  FolderServerRelativePath: ResourcePath | None = None
33
41
 
42
+ def __post_init__(self) -> None:
43
+ # The expression AST (set by QueryBuilder.build()); not serialized.
44
+ self._expr: "QueryBuilder | None" = None
45
+
46
+ @staticmethod
47
+ def builder() -> "QueryBuilder":
48
+ """A fluent builder for this query (``ViewFields``/``Where``/``OrderBy``/...)."""
49
+ from office365.sharepoint.listitems.caml.builder import QueryBuilder
50
+
51
+ return QueryBuilder()
52
+
53
+ def __iter__(self) -> Iterator[Tuple[str, Any]]:
54
+ """Yield serializable properties (skips private attrs such as the AST)."""
55
+ for name, value in vars(self).items():
56
+ if not name.startswith("_"):
57
+ yield name, value
58
+
34
59
  @staticmethod
35
60
  def parse(query_expr: str, scope: ViewScope = ViewScope.DefaultValue) -> CamlQuery:
36
61
  """Creates a CamlQuery object from a query expression
@@ -51,15 +76,59 @@ class CamlQuery(ClientValue):
51
76
  @staticmethod
52
77
  def create_all_folders_query() -> CamlQuery:
53
78
  """Constructs a query to return folder objects"""
54
- qry_text = '<Where><Eq><FieldRef Name="FSObjType" /><Value Type="Integer">1</Value></Eq></Where>'
79
+ qry_text = (
80
+ f'<Where><Eq><FieldRef Name="{SPBuiltInFieldName.FSObjType}" /><Value Type="Integer">1</Value></Eq></Where>'
81
+ )
55
82
  return CamlQuery.parse(qry_text, ViewScope.DefaultValue)
56
83
 
57
84
  @staticmethod
58
85
  def create_all_files_query() -> CamlQuery:
59
86
  """Constructs a query to return file objects"""
60
- qry_text = '<Where><Eq><FieldRef Name="FSObjType" /><Value Type="Integer">0</Value></Eq></Where>'
87
+ qry_text = (
88
+ f'<Where><Eq><FieldRef Name="{SPBuiltInFieldName.FSObjType}" /><Value Type="Integer">0</Value></Eq></Where>'
89
+ )
61
90
  return CamlQuery.parse(qry_text, ViewScope.DefaultValue)
62
91
 
92
+ @property
93
+ def is_paged(self) -> bool:
94
+ """Whether the query uses server-driven paging (``RowLimit Paged='TRUE'``)."""
95
+ if self._expr is not None:
96
+ return self._expr.is_paged
97
+ xml = self.ViewXml or ""
98
+ return "Paged='TRUE'" in xml or 'Paged="TRUE"' in xml
99
+
100
+ @property
101
+ def order_by_fields(self) -> list[str]:
102
+ """The field names in ``<OrderBy>``, in order (empty when unsorted).
103
+
104
+ Used to build the ``ListItemCollectionPosition`` paging token so a
105
+ multi-page CAML query continues where the previous page stopped.
106
+ """
107
+ if self._expr is not None:
108
+ return [ref.name for ref in self._expr._order_by]
109
+ match = re.search(r"<OrderBy>(.*?)</OrderBy>", self.ViewXml or "", re.DOTALL | re.IGNORECASE)
110
+ return _FIELD_REF_RE.findall(match.group(1)) if match else []
111
+
112
+ @property
113
+ def field_refs(self) -> set[str]:
114
+ """The field names referenced by the query.
115
+
116
+ Uses the expression AST when the query was produced by the builder, else
117
+ falls back to scanning the raw ``ViewXml``.
118
+ """
119
+ if self._expr is not None:
120
+ return self._expr.field_refs
121
+ return {match.group(1) for match in _FIELD_REF_RE.finditer(self.ViewXml or "")}
122
+
123
+ @property
124
+ def index_candidates(self) -> list[str]:
125
+ """Fields the query filters/sorts on that are worth indexing (``ID`` excluded).
126
+
127
+ Feed these to :meth:`~office365.sharepoint.lists.list.List.ensure_indexed`
128
+ so a large-list query on them is not throttled by the list view threshold.
129
+ """
130
+ return sorted(self.field_refs - {"ID"})
131
+
63
132
  def __repr__(self):
64
133
  return self.ViewXml or ""
65
134