77from contextlib import contextmanager
88from typing import Any , Dict , Iterable , Iterator , List , Optional , Union
99
10+ import requests
11+
1012from azure .core .credentials import TokenCredential
1113
1214from .core ._auth import _AuthManager
@@ -59,31 +61,29 @@ class DataverseClient:
5961 - ``client.tables`` -- table and column metadata management
6062 - ``client.files`` -- file upload operations
6163
64+ The client supports Python's context manager protocol for automatic resource
65+ cleanup and HTTP connection pooling:
66+
6267 Example:
63- Create a client and perform basic operations ::
68+ **Recommended -- context manager** (enables HTTP connection pooling) ::
6469
6570 from azure.identity import InteractiveBrowserCredential
6671 from PowerPlatform.Dataverse.client import DataverseClient
6772
6873 credential = InteractiveBrowserCredential()
69- client = DataverseClient(
70- "https://org.crm.dynamics.com",
71- credential
72- )
7374
74- # Create a record
75- record_id = client.records.create("account", {"name": "Contoso Ltd"})
75+ with DataverseClient("https://org.crm.dynamics.com", credential) as client:
76+ record_id = client.records.create("account", {"name": "Contoso Ltd"})
77+ client.records.update("account", record_id, {"telephone1": "555-0100"})
78+ # Session closed, caches cleared automatically
7679
77- # Update a record
78- client.records.update("account", record_id, {"telephone1": "555-0100"})
80+ **Manual lifecycle**::
7981
80- # Query records
81- for page in client.records.get("account", filter="name eq 'Contoso Ltd'"):
82- for account in page:
83- print(account["name"])
84-
85- # Delete a record
86- client.records.delete("account", record_id)
82+ client = DataverseClient("https://org.crm.dynamics.com", credential)
83+ try:
84+ record_id = client.records.create("account", {"name": "Contoso Ltd"})
85+ finally:
86+ client.close()
8787 """
8888
8989 def __init__ (
@@ -98,6 +98,8 @@ def __init__(
9898 raise ValueError ("base_url is required." )
9999 self ._config = config or DataverseConfig .from_env ()
100100 self ._odata : Optional [_ODataClient ] = None
101+ self ._session : Optional [requests .Session ] = None
102+ self ._closed : bool = False
101103
102104 # Operation namespaces
103105 self .records = RecordOperations (self )
@@ -120,16 +122,74 @@ def _get_odata(self) -> _ODataClient:
120122 self .auth ,
121123 self ._base_url ,
122124 self ._config ,
125+ session = self ._session ,
123126 )
124127 return self ._odata
125128
126129 @contextmanager
127130 def _scoped_odata (self ) -> Iterator [_ODataClient ]:
128131 """Yield the low-level client while ensuring a correlation scope is active."""
132+ self ._check_closed ()
129133 od = self ._get_odata ()
130134 with od ._call_scope ():
131135 yield od
132136
137+ # ---------------- Context manager / lifecycle ----------------
138+
139+ def __enter__ (self ) -> DataverseClient :
140+ """Enter the context manager.
141+
142+ Creates a :class:`requests.Session` for HTTP connection pooling.
143+ All operations within the ``with`` block reuse this session for
144+ better performance (TCP and TLS reuse).
145+
146+ :return: The client instance.
147+ :rtype: DataverseClient
148+ """
149+ if self ._session is None :
150+ self ._session = requests .Session ()
151+ return self
152+
153+ def __exit__ (self , exc_type , exc_val , exc_tb ) -> None :
154+ """Exit the context manager with cleanup.
155+
156+ Calls :meth:`close` to release resources. Exceptions are not
157+ suppressed.
158+ """
159+ self .close ()
160+
161+ def close (self ) -> None :
162+ """Close the client and release resources.
163+
164+ Closes the HTTP session (if any), clears internal caches, and
165+ marks the client as closed. Safe to call multiple times. After
166+ closing, any operation will raise :class:`RuntimeError`.
167+
168+ Called automatically when using the client as a context manager.
169+
170+ Example::
171+
172+ client = DataverseClient(base_url, credential)
173+ try:
174+ client.records.create("account", {"name": "Contoso"})
175+ finally:
176+ client.close()
177+ """
178+ if self ._closed :
179+ return
180+ if self ._odata is not None :
181+ self ._odata .close ()
182+ self ._odata = None
183+ if self ._session is not None :
184+ self ._session .close ()
185+ self ._session = None
186+ self ._closed = True
187+
188+ def _check_closed (self ) -> None :
189+ """Raise :class:`RuntimeError` if the client has been closed."""
190+ if self ._closed :
191+ raise RuntimeError ("DataverseClient is closed" )
192+
133193 # ---------------- Unified CRUD: create/update/delete ----------------
134194 def create (self , table_schema_name : str , records : Union [Dict [str , Any ], List [Dict [str , Any ]]]) -> List [str ]:
135195 """
0 commit comments