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,455 @@
1
+ """Product-agnostic service limits, metadata and guardrails.
2
+
3
+ A :class:`Limit` describes one service limit — a static threshold *or* a rate
4
+ quota — with its value, kind, unit, scope and docs. Limits are declared on the
5
+ model with the :func:`limit` decorator (mirroring ``@odata`` / ``@require_permission``):
6
+ the declaration is stamped on the callable, appended to its docstring, and
7
+ collected into ``ClientObject._limit_meta``. ``arg=`` also *enforces* a numeric
8
+ argument (warn by default, ``raise`` or ``clamp``).
9
+
10
+ The helpers :func:`exceeds` / :func:`warn_if_exceeds` / :func:`ensure_within`
11
+ apply a limit imperatively at a call site; :func:`limits_of` / :func:`verify_limits`
12
+ inspect what a method/class declares.
13
+
14
+ Product packages (e.g. ``office365.sharepoint.thresholds``,
15
+ ``office365.graph_limits``) declare the concrete values; this module only owns
16
+ the mechanics so the runtime and every product share them.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import inspect
22
+ import warnings
23
+ from dataclasses import dataclass, field
24
+ from enum import Enum
25
+ from functools import wraps
26
+ from typing import Any, Callable, Iterable, Optional, Protocol, Tuple, TypeVar, overload, runtime_checkable
27
+
28
+ from typing_extensions import ParamSpec
29
+
30
+ P = ParamSpec("P")
31
+ R = TypeVar("R")
32
+ T = TypeVar("T")
33
+
34
+ _ON_EXCEED = ("warn", "raise")
35
+ _LIMIT_MARKER = "__limit_decls__"
36
+
37
+ #: A conservative default page/chunk size that stays below SharePoint's 5,000-item
38
+ #: list view threshold, so paged reads and imports are safe by default.
39
+ SAFE_PAGE_SIZE = 2000
40
+
41
+ #: Default number of items per OData batch (also SharePoint's bulk-operation limit).
42
+ DEFAULT_BATCH_SIZE = 100
43
+
44
+
45
+ class LimitKind(Enum):
46
+ """How strictly a limit is enforced (per Microsoft's definitions)."""
47
+
48
+ BOUNDARY = "boundary" # absolute, can't be exceeded by design
49
+ THRESHOLD = "threshold" # configurable default
50
+ SUPPORTED = "supported" # tested value
51
+
52
+
53
+ def _human_bytes(value: int) -> str:
54
+ """Format a byte count as ``250 GB`` when it divides evenly, else ``8,000 bytes``."""
55
+ for suffix, step in (("TB", 1024**4), ("GB", 1024**3), ("MB", 1024**2), ("KB", 1024)):
56
+ if value >= step and value % step == 0:
57
+ return f"{value // step} {suffix}"
58
+ return f"{value:,} bytes"
59
+
60
+
61
+ @dataclass(frozen=True)
62
+ class Limit:
63
+ """A single service limit — a static threshold or a rate quota.
64
+
65
+ Args:
66
+ name: Human-readable name (e.g. ``"list view threshold"``).
67
+ value: The numeric limit.
68
+ kind: :class:`LimitKind` — boundary / threshold / supported.
69
+ unit: What is counted (``items``, ``bytes``, ``requests``, ...).
70
+ scope: The object it applies to (``list``, ``file``, ``app+tenant``, ...).
71
+ note: Extra guidance surfaced in warnings.
72
+ doc: Link to the authoritative documentation.
73
+ window_seconds: The rate window in seconds (``None`` for a static
74
+ threshold; ``0`` for a concurrency limit).
75
+ request_type: ``""`` (n/a) | ``read`` | ``write`` | ``any``.
76
+ """
77
+
78
+ name: str
79
+ value: int
80
+ kind: LimitKind = LimitKind.THRESHOLD
81
+ unit: str = "items"
82
+ scope: str = ""
83
+ note: str = ""
84
+ doc: str = ""
85
+ window_seconds: Optional[int] = None
86
+ request_type: str = ""
87
+
88
+ def __str__(self) -> str:
89
+ if self.window_seconds is not None:
90
+ window = f" / {self.window_seconds:,}s" if self.window_seconds else ""
91
+ return f"{self.value:,} {self.unit}{window}".strip()
92
+ if self.unit == "bytes":
93
+ return _human_bytes(self.value)
94
+ return f"{self.value:,} {self.unit}".strip()
95
+
96
+ def exceeds(self, value: int) -> bool:
97
+ """Whether ``value`` is over this limit."""
98
+ return value > self.value
99
+
100
+ @property
101
+ def is_rate(self) -> bool:
102
+ """Whether this is a rate quota (has a window)."""
103
+ return self.window_seconds is not None
104
+
105
+
106
+ class LimitExceededError(ValueError):
107
+ """Raised when a value exceeds a service limit."""
108
+
109
+ def __init__(self, limit: Limit, value: int, *, context: str = "") -> None:
110
+ self.limit = limit
111
+ self.value = value
112
+ self.context = context
113
+ super().__init__(hint(limit, context=context, value=value))
114
+
115
+
116
+ def exceeds(limit: Limit, value: int) -> bool:
117
+ """Whether ``value`` is over ``limit``."""
118
+ return limit.exceeds(value)
119
+
120
+
121
+ def hint(limit: Limit, *, context: str = "", value: Optional[int] = None) -> str:
122
+ """An actionable message for a limit, optionally naming the offending value."""
123
+ where = f" for {context}" if context else ""
124
+ if value is None:
125
+ text = f"limit '{limit.name}'{where} is {limit}"
126
+ else:
127
+ text = f"{_format_value(limit, value)}{where} exceeds the limit '{limit.name}' ({limit})"
128
+ if limit.note:
129
+ text += f" — {limit.note}"
130
+ if limit.doc:
131
+ text += f" See {limit.doc}"
132
+ return text
133
+
134
+
135
+ def _format_value(limit: Limit, value: int) -> str:
136
+ if limit.unit == "bytes":
137
+ return _human_bytes(value)
138
+ return f"{value:,} {limit.unit}".strip()
139
+
140
+
141
+ def warn_if_exceeds(limit: Limit, value: int, *, context: str = "", stacklevel: int = 3) -> bool:
142
+ """Warn when ``value`` is over ``limit``. Returns whether it warned."""
143
+ if not limit.exceeds(value):
144
+ return False
145
+ warnings.warn(hint(limit, context=context, value=value), UserWarning, stacklevel=stacklevel)
146
+ return True
147
+
148
+
149
+ def ensure_within(
150
+ limit: Limit,
151
+ value: int,
152
+ *,
153
+ context: str = "",
154
+ on_exceed: str = "raise",
155
+ ) -> None:
156
+ """Enforce ``limit`` for ``value`` — raise :class:`LimitExceededError` or warn.
157
+
158
+ Args:
159
+ on_exceed: ``"raise"`` (default) or ``"warn"``.
160
+ """
161
+ if on_exceed not in _ON_EXCEED:
162
+ raise ValueError(f"on_exceed must be one of {_ON_EXCEED}, got {on_exceed!r}")
163
+ if not limit.exceeds(value):
164
+ return
165
+ if on_exceed == "warn":
166
+ warn_if_exceeds(limit, value, context=context, stacklevel=4)
167
+ else:
168
+ raise LimitExceededError(limit, value, context=context)
169
+
170
+
171
+ @dataclass(frozen=True)
172
+ class LimitDecl:
173
+ """One declared limit — a :class:`Limit`, optionally bound to an argument.
174
+
175
+ ``arg`` names the numeric argument the limit is enforced against (``None``
176
+ for a documentation-only declaration); ``on_exceed`` / ``clamp`` decide the
177
+ enforcement behaviour.
178
+ """
179
+
180
+ limit: Limit
181
+ arg: Optional[str] = None
182
+ on_exceed: str = "warn"
183
+ clamp: bool = False
184
+
185
+
186
+ def _append_doc(target: Callable[..., Any], decls: Tuple[LimitDecl, ...]) -> None:
187
+ lines: list[str] = []
188
+ for decl in decls:
189
+ bound = f" (<= {decl.limit})" if decl.arg else ""
190
+ lines.append(f" {decl.limit.name}: {decl.limit}{bound}")
191
+ if lines:
192
+ target.__doc__ = (target.__doc__ or "") + "\n Limits:\n" + "\n".join(lines)
193
+
194
+
195
+ @overload
196
+ def limit(
197
+ *limits: Limit,
198
+ arg: None = ...,
199
+ on_exceed: str = ...,
200
+ clamp: bool = ...,
201
+ ) -> Callable[[T], T]: ...
202
+
203
+
204
+ @overload
205
+ def limit(
206
+ *limits: Limit,
207
+ arg: str,
208
+ on_exceed: str = ...,
209
+ clamp: bool = ...,
210
+ ) -> Callable[[Callable[P, R]], Callable[P, R]]: ...
211
+
212
+
213
+ def limit(
214
+ *limits: Limit,
215
+ arg: Optional[str] = None,
216
+ on_exceed: str = "warn",
217
+ clamp: bool = False,
218
+ ) -> Any:
219
+ """Declare service limits on a class, method or property (and optionally enforce one).
220
+
221
+ Metadata-only by default (like ``@odata`` / ``@require_permission``): the
222
+ declared :class:`Limit` values are stamped on the target and appended to its
223
+ docstring, then collected into ``_limit_meta`` / ``_class_limit_decls``.
224
+ Pass ``arg`` to also enforce that argument against the (single) limit — warn
225
+ by default, ``on_exceed="raise"`` to raise, or ``clamp=True`` to lower it
226
+ (methods only).
227
+
228
+ Usage::
229
+
230
+ @limit(Limits.FILE_UPLOAD) # document
231
+ @limit(Limits.LIST_VIEW, arg="page_size") # document + warn
232
+ @limit(Limits.BATCH_ITEMS, arg="items_per_batch", on_exceed="raise")
233
+ @limit(*IDENTITY_QUOTAS) # on a class
234
+ class DirectoryObject(Entity): ...
235
+
236
+ Args:
237
+ *limits: The :class:`Limit` values to declare.
238
+ arg: Argument name to enforce (requires exactly one limit).
239
+ on_exceed: ``"warn"`` (default) or ``"raise"``.
240
+ clamp: Lower the argument to the limit instead of warning/raising.
241
+ """
242
+ if arg is not None and len(limits) != 1:
243
+ raise ValueError("arg= requires exactly one limit")
244
+ if on_exceed not in _ON_EXCEED:
245
+ raise ValueError(f"on_exceed must be one of {_ON_EXCEED}, got {on_exceed!r}")
246
+
247
+ decls = tuple(
248
+ LimitDecl(limit=item, arg=arg if index == 0 else None, on_exceed=on_exceed, clamp=clamp)
249
+ for index, item in enumerate(limits)
250
+ )
251
+
252
+ def decorator(func: Any) -> Any:
253
+ if isinstance(func, type):
254
+ if arg is not None:
255
+ raise ValueError("arg= cannot be used on a class")
256
+ _stamp(func, decls)
257
+ # ``__init_subclass__`` ran before this decorator (members already
258
+ # collected); merge the class-level declaration so subclasses inherit.
259
+ func._class_limit_decls = collect_class_limits(func)
260
+ register_catalog(func)
261
+ return func
262
+ if isinstance(func, property):
263
+ if arg is not None:
264
+ raise ValueError("arg= cannot be used on a property (properties take no arguments)")
265
+ _stamp(func.fget, decls)
266
+ return func
267
+
268
+ _stamp(func, decls)
269
+ if arg is None:
270
+ return func
271
+
272
+ arg_name: str = arg
273
+ signature = inspect.signature(func)
274
+ enforced = decls[0]
275
+
276
+ @wraps(func)
277
+ def wrapper(*args: Any, **kwargs: Any) -> Any:
278
+ bound = signature.bind_partial(*args, **kwargs)
279
+ value: Any = bound.arguments.get(arg_name)
280
+ if value is None or not enforced.limit.exceeds(value):
281
+ return func(*args, **kwargs)
282
+ if enforced.clamp:
283
+ bound.arguments[arg_name] = enforced.limit.value
284
+ return func(*bound.args, **bound.kwargs)
285
+ if enforced.on_exceed == "raise":
286
+ raise LimitExceededError(enforced.limit, value, context=func.__qualname__)
287
+ warn_if_exceeds(enforced.limit, value, context=func.__qualname__, stacklevel=2)
288
+ return func(*args, **kwargs)
289
+
290
+ return wrapper
291
+
292
+ return decorator
293
+
294
+
295
+ def _stamp(target: Any, decls: Tuple[LimitDecl, ...]) -> None:
296
+ setattr(target, _LIMIT_MARKER, decls)
297
+ _append_doc(target, decls)
298
+
299
+
300
+ _limit = limit # module alias (the ``bounded`` param name shadows ``limit`` below)
301
+
302
+
303
+ def bounded(
304
+ arg: str,
305
+ limit: Limit,
306
+ *,
307
+ on_exceed: str = "warn",
308
+ clamp: bool = False,
309
+ ) -> Callable[[Callable[P, R]], Callable[P, R]]:
310
+ """Back-compat: ``@bounded(arg, limit)`` is ``@limit(limit, arg=arg)``."""
311
+ return _limit(limit, arg=arg, on_exceed=on_exceed, clamp=clamp)
312
+
313
+
314
+ def collect_limit_meta(cls: type) -> dict[str, Tuple[LimitDecl, ...]]:
315
+ """Collect ``@limit`` declarations from a class's methods/properties.
316
+
317
+ Called from ``__init_subclass__`` (``ClientObject`` / ``ClientRuntimeContext``)
318
+ so a class carries its own limits — keyed by attribute/method name, inheriting
319
+ the base class's entries.
320
+ """
321
+ meta: dict[str, Tuple[LimitDecl, ...]] = dict(getattr(cls, "_limit_meta", {}))
322
+ for attr_name, attr in cls.__dict__.items():
323
+ target = attr.fget if isinstance(attr, property) else attr
324
+ decls = getattr(target, _LIMIT_MARKER, None)
325
+ if decls is not None:
326
+ meta[attr_name] = decls
327
+ return meta
328
+
329
+
330
+ @runtime_checkable
331
+ class LimitCatalog(Protocol):
332
+ """A source of :class:`Limit` values — anything with a ``catalog()`` method."""
333
+
334
+ def catalog(self) -> Iterable[Limit]: ...
335
+
336
+
337
+ #: Registered limit sources, keyed by product (``"model"`` for ``@limit`` classes).
338
+ _SOURCES: dict[str, list[Any]] = {}
339
+
340
+
341
+ def _limits_from(source: Any) -> list[Limit]:
342
+ catalog_fn = getattr(source, "catalog", None)
343
+ if catalog_fn is not None:
344
+ return list(catalog_fn())
345
+ declared = getattr(source, "declared_limits", None)
346
+ if declared is not None:
347
+ return list(declared())
348
+ raise TypeError(f"{source!r} is not a Limit catalog (needs catalog() or declared_limits())")
349
+
350
+
351
+ def register_catalog(*sources: Any, product: str = "model") -> None:
352
+ """Register limit sources — catalogs (``catalog()``) or classes (``declared_limits()``).
353
+
354
+ Products register their own catalogs at import time (e.g.
355
+ ``register_catalog(Limits, product="sharepoint")``); ``@limit`` on a class
356
+ registers that class under ``product="model"``.
357
+ """
358
+ bucket = _SOURCES.setdefault(product, [])
359
+ for source in sources:
360
+ if source not in bucket:
361
+ bucket.append(source)
362
+
363
+
364
+ def catalog(product: Optional[str] = None) -> list[Limit]:
365
+ """All registered limits (optionally for one product), de-duplicated by identity."""
366
+ products = [product] if product is not None else list(_SOURCES)
367
+ seen: set[int] = set()
368
+ result: list[Limit] = []
369
+ for name in products:
370
+ for source in _SOURCES.get(name, ()):
371
+ for item in _limits_from(source):
372
+ if id(item) not in seen:
373
+ seen.add(id(item))
374
+ result.append(item)
375
+ return result
376
+
377
+
378
+ def collect_class_limits(cls: type) -> Tuple[LimitDecl, ...]:
379
+ """The class-level ``@limit`` declarations of ``cls``.
380
+
381
+ Accumulates up the hierarchy: the base class's declarations plus the class's
382
+ own (a subclass is subject to both).
383
+ """
384
+ inherited = getattr(cls, "_class_limit_decls", ())
385
+ own = cls.__dict__.get(_LIMIT_MARKER)
386
+ if own is None:
387
+ return inherited
388
+ return (*inherited, *own)
389
+
390
+
391
+ def limits_of(target: Any) -> Tuple[LimitDecl, ...]:
392
+ """The limits declared on a method/property, or a class's collected limits.
393
+
394
+ Args:
395
+ target: A callable (reads ``__limit_decls__``), a ``property``, or a
396
+ class (reads ``_limit_meta``).
397
+ """
398
+ if isinstance(target, type):
399
+ meta = getattr(target, "_limit_meta", {})
400
+ return tuple(decl for decls in meta.values() for decl in decls)
401
+ if isinstance(target, property):
402
+ target = target.fget
403
+ return tuple(getattr(target, _LIMIT_MARKER, ()))
404
+
405
+
406
+ @dataclass
407
+ class LimitReport:
408
+ """Diagnostic result of inspecting the limits declared on a target.
409
+
410
+ ``violations`` holds the ``(decl, value)`` pairs whose supplied value is over
411
+ the limit. This never raises and never touches the network.
412
+ """
413
+
414
+ target: str
415
+ decls: Tuple[LimitDecl, ...] = ()
416
+ violations: Tuple[Tuple[LimitDecl, int], ...] = field(default_factory=tuple)
417
+
418
+ @property
419
+ def limits(self) -> Tuple[Limit, ...]:
420
+ return tuple(decl.limit for decl in self.decls)
421
+
422
+ @property
423
+ def ok(self) -> bool:
424
+ """Whether all supplied values are within their limits."""
425
+ return not self.violations
426
+
427
+ def __str__(self) -> str:
428
+ head = f"{self.target}: {len(self.decls)} limit(s)"
429
+ if self.violations:
430
+ over = ", ".join(f"{decl.limit.name}={value:,}" for decl, value in self.violations)
431
+ return f"{head}; over: {over}"
432
+ return head
433
+
434
+
435
+ def verify_limits(target: Any, **values: int) -> LimitReport:
436
+ """Inspect the limits declared on a method/property/class (diagnostic-only).
437
+
438
+ Keyword ``values`` are checked against the arg-bound declarations
439
+ (``@limit(..., arg=...)``); other limits are reported for reference.
440
+
441
+ Args:
442
+ target: A callable, ``property``, or class.
443
+ **values: Argument name -> value to check.
444
+
445
+ Returns:
446
+ A :class:`LimitReport`.
447
+ """
448
+ name = getattr(target, "__qualname__", None) or getattr(target, "__name__", str(target))
449
+ decls = limits_of(target)
450
+ violations = tuple(
451
+ (decl, values[decl.arg])
452
+ for decl in decls
453
+ if decl.arg is not None and decl.arg in values and decl.limit.exceeds(values[decl.arg])
454
+ )
455
+ return LimitReport(target=name, decls=decls, violations=violations)
@@ -20,19 +20,28 @@ from office365.runtime.queries.delete_entity import DeleteEntityQuery
20
20
  from office365.runtime.queries.function import FunctionQuery
21
21
  from office365.runtime.queries.service_operation import ServiceOperationQuery
22
22
  from office365.runtime.queries.update_entity import UpdateEntityQuery
23
+ from office365.runtime.transport.base import BaseTransport
23
24
 
24
25
 
25
26
  class ODataRequest(ClientRequest):
26
27
  """Handles OData protocol specific request/response processing for API calls."""
27
28
 
28
- def __init__(self, base_url: str, json_format: ODataJsonFormat) -> None:
29
+ def __init__(
30
+ self,
31
+ base_url: str,
32
+ json_format: ODataJsonFormat,
33
+ transport: "BaseTransport | None" = None,
34
+ ) -> None:
29
35
  """
30
36
  Initialize a new OData request processor.
31
37
 
32
38
  Args:
39
+ base_url: The service base URL.
33
40
  json_format: The JSON format handler for OData serialization/deserialization
41
+ transport: Optional shared transport (e.g. one pooled ``Session`` for
42
+ parallel batch requests); defaults to a new ``RequestsTransport``.
34
43
  """
35
- super().__init__()
44
+ super().__init__(transport=transport)
36
45
  self._base_url = base_url
37
46
  self._default_json_format = json_format
38
47
  self.beforeExecute += self._ensure_http_headers
@@ -22,6 +22,7 @@ _PRIMITIVE_TYPES = {
22
22
  "Edm.Duration": datetime.timedelta,
23
23
  "Edm.Date": datetime.date,
24
24
  "Edm.TimeOfDay": datetime.time,
25
+ "Edm.Time": datetime.time, # OData v3 name for a time-of-day (kept after TimeOfDay for reverse lookup)
25
26
  "Edm.Json": dict,
26
27
  "Collection(SP.KeyValue)": dict,
27
28
  "Collection(Edm.Guid)": GuidCollection,
@@ -53,6 +53,7 @@ class ODataBatchV3Request(ODataRequest):
53
53
  query: The original batch query
54
54
  """
55
55
  for sub_qry, sub_resp in self._extract_response(response, query):
56
+ self._observe_throttle(sub_resp)
56
57
  sub_resp.raise_for_status()
57
58
  super().process_response(sub_resp, sub_qry)
58
59
 
@@ -77,7 +78,7 @@ class ODataBatchV3Request(ODataRequest):
77
78
  base_delay: Base delay for exponential backoff (seconds)
78
79
  jitter: Whether to randomize the delay (default True)
79
80
  """
80
- from office365.runtime.retry import TRANSIENT_STATUS_CODES, response_retry_after, retry
81
+ from office365.runtime.retry import TRANSIENT_STATUS_CODES, response_retry_after, retry, retry_after_delay
81
82
 
82
83
  state: dict = {"pending": query, "retry_after": None}
83
84
 
@@ -94,6 +95,7 @@ class ODataBatchV3Request(ODataRequest):
94
95
  failures: list[tuple[ClientQuery, Response]] = []
95
96
  retry_after: Optional[int] = None
96
97
  for sub_qry, sub_resp in self._extract_response(response, state["pending"]):
98
+ self._observe_throttle(sub_resp)
97
99
  if sub_resp.status_code in TRANSIENT_STATUS_CODES:
98
100
  failures.append((sub_qry, sub_resp))
99
101
  retry_after = max(retry_after or 0, response_retry_after(sub_resp) or 0)
@@ -113,7 +115,7 @@ class ODataBatchV3Request(ODataRequest):
113
115
  max_retry=max_retry,
114
116
  timeout_secs=base_delay,
115
117
  jitter=jitter,
116
- on_failure=lambda _attempt_num, _ex: state["retry_after"],
118
+ on_failure=lambda _attempt_num, ex: state["retry_after"] or retry_after_delay(ex),
117
119
  )
118
120
  except WholeBatchRejected as reject:
119
121
  self._split_and_retry(query, reject, max_retry, base_delay, jitter)
@@ -51,6 +51,7 @@ class ODataV4BatchRequest(ODataRequest):
51
51
  HTTPError: If any sub-request in the batch fails
52
52
  """
53
53
  for sub_qry, sub_resp in self._extract_response(response, query):
54
+ self._observe_throttle(sub_resp)
54
55
  sub_resp.raise_for_status()
55
56
  super().process_response(sub_resp, sub_qry)
56
57
 
@@ -76,7 +77,7 @@ class ODataV4BatchRequest(ODataRequest):
76
77
  base_delay: Base delay for exponential backoff (seconds)
77
78
  jitter: Whether to randomize the delay (default True)
78
79
  """
79
- from office365.runtime.retry import TRANSIENT_STATUS_CODES, response_retry_after, retry
80
+ from office365.runtime.retry import TRANSIENT_STATUS_CODES, response_retry_after, retry, retry_after_delay
80
81
 
81
82
  state: dict = {"pending": query, "retry_after": None}
82
83
 
@@ -93,6 +94,7 @@ class ODataV4BatchRequest(ODataRequest):
93
94
  failures: list[tuple[ClientQuery, Response]] = []
94
95
  retry_after: Optional[int] = None
95
96
  for sub_qry, sub_resp in self._extract_response(response, state["pending"]):
97
+ self._observe_throttle(sub_resp)
96
98
  if sub_resp.status_code in TRANSIENT_STATUS_CODES:
97
99
  failures.append((sub_qry, sub_resp))
98
100
  retry_after = max(retry_after or 0, response_retry_after(sub_resp) or 0)
@@ -103,7 +105,7 @@ class ODataV4BatchRequest(ODataRequest):
103
105
  self.afterExecute(response)
104
106
  return
105
107
  state["retry_after"] = retry_after or None
106
- state["pending"] = BatchQuery(query.context, [qry for qry, _ in failures])
108
+ state["pending"] = BatchQuery(query.context, [qry for qry, _ in failures], sequential=query.sequential)
107
109
  raise ClientRequestException.from_response(failures[0][1])
108
110
 
109
111
  try:
@@ -112,7 +114,7 @@ class ODataV4BatchRequest(ODataRequest):
112
114
  max_retry=max_retry,
113
115
  timeout_secs=base_delay,
114
116
  jitter=jitter,
115
- on_failure=lambda _attempt_num, _ex: state["retry_after"],
117
+ on_failure=lambda _attempt_num, ex: state["retry_after"] or retry_after_delay(ex),
116
118
  )
117
119
  except WholeBatchRejected as reject:
118
120
  self._split_and_retry(query, reject, max_retry, base_delay, jitter)
@@ -134,7 +136,9 @@ class ODataV4BatchRequest(ODataRequest):
134
136
  raise ClientRequestException(message, response=reject.response) from reject
135
137
  mid = len(queries) // 2 # noqa: PLR2004
136
138
  for half in (queries[:mid], queries[mid:]):
137
- self.execute_query_with_retry(BatchQuery(query.context, half), max_retry, base_delay, jitter)
139
+ self.execute_query_with_retry(
140
+ BatchQuery(query.context, half, sequential=query.sequential), max_retry, base_delay, jitter
141
+ )
138
142
 
139
143
  @staticmethod
140
144
  def _extract_response(response: Response, query: BatchQuery) -> Iterator[Tuple[ClientQuery, Response]]:
@@ -154,22 +158,33 @@ class ODataV4BatchRequest(ODataRequest):
154
158
  resp.headers = CaseInsensitiveDict(json_resp["headers"])
155
159
  resp._content = json.dumps(json_resp["body"]).encode("utf-8")
156
160
  qry_id = int(json_resp["id"])
157
- qry = query.ordered_queries[qry_id]
161
+ # Ids are assigned in submission order (see ``_prepare_payload``), so
162
+ # map back with the same list — ``ordered_queries`` re-groups (non-GET
163
+ # first) and would attribute mixed batches to the wrong queries.
164
+ qry = query.queries[qry_id]
158
165
  yield qry, resp
159
166
 
160
167
  def _prepare_payload(self, query: BatchQuery) -> Dict[str, Any]:
161
168
  """Prepares the batch request payload.
162
169
 
170
+ With ``query.sequential`` the sub-requests are chained with ``dependsOn``
171
+ so Graph runs them in order (a failed dependency yields ``424``). Ids are
172
+ assigned in submission order — the same order :meth:`_extract_response`
173
+ maps responses back with.
174
+
163
175
  Args:
164
176
  query: The BatchQuery containing individual queries
165
177
 
166
178
  Returns:
167
179
  Dictionary containing the JSON batch request structure
168
180
  """
169
- requests_json = []
181
+ requests_json: list[dict] = []
182
+ previous_id: Optional[str] = None
170
183
  for qry in query.queries:
171
184
  qry_id = str(len(requests_json))
172
- requests_json.append(self._normalize_request(qry, qry_id))
185
+ depends_on = [previous_id] if (query.sequential and previous_id is not None) else None
186
+ requests_json.append(self._normalize_request(qry, qry_id, depends_on))
187
+ previous_id = qry_id
173
188
 
174
189
  return {"requests": requests_json}
175
190
 
@@ -49,6 +49,35 @@ class Progress(Generic[T_co]):
49
49
  ProgressCallback = Callable[[Progress[Any]], None]
50
50
 
51
51
 
52
+ def emit_progress(
53
+ progress: Optional[ProgressCallback],
54
+ *,
55
+ done: int,
56
+ total: Optional[int] = None,
57
+ stage: str = "",
58
+ items: Optional[Sequence[Any]] = None,
59
+ ) -> None:
60
+ """Invoke a progress hook (if any) with a :class:`Progress` snapshot."""
61
+ if callable(progress):
62
+ progress(Progress(done=done, total=total, stage=stage, items=items))
63
+
64
+
65
+ @dataclass
66
+ class OperationStats:
67
+ """Common counters shared by bulk operations (imports, migrations).
68
+
69
+ The base carries only the fields every operation can report, so a consumer
70
+ that needs just totals accepts any specialization (``ImportStats``,
71
+ ``MigrationStats``) without a lossy conversion. Specializations add their own
72
+ extras (``chunks``/``duration``, ``bytes_transferred``).
73
+ """
74
+
75
+ total: int = 0
76
+ success: int = 0
77
+ skipped: int = 0
78
+ errors: int = 0
79
+
80
+
52
81
  class ProgressTracker:
53
82
  """Shared emitter that turns operation sub-steps into ``Progress`` snapshots.
54
83