12
12
# implied. See the License for the specific language governing
13
13
# permissions and limitations under the License.
14
14
15
- """ChangeStream cursor to iterate over changes on a collection."""
15
+ """Watch changes on a collection, a database, or the entire cluster ."""
16
16
17
17
import copy
18
18
@@ -41,14 +41,12 @@ class ChangeStream(object):
41
41
"""The internal abstract base class for change stream cursors.
42
42
43
43
Should not be called directly by application developers. Use
44
- :meth:pymongo.collection.Collection.watch,
45
- :meth:pymongo.database.Database.watch, or
46
- :meth:pymongo.mongo_client.MongoClient.watch instead.
47
-
48
- Defines the interface for change streams. Should be subclassed to
49
- implement the `ChangeStream._create_cursor` abstract method, and
50
- the `ChangeStream._database`and ChangeStream._aggregation_target`
51
- abstract properties.
44
+ :meth:`pymongo.collection.Collection.watch`,
45
+ :meth:`pymongo.database.Database.watch`, or
46
+ :meth:`pymongo.mongo_client.MongoClient.watch` instead.
47
+
48
+ .. versionadded:: 3.6
49
+ .. mongodoc:: changeStreams
52
50
"""
53
51
def __init__ (self , target , pipeline , full_document , resume_after ,
54
52
max_await_time_ms , batch_size , collation ,
@@ -176,34 +174,97 @@ def next(self):
176
174
"""Advance the cursor.
177
175
178
176
This method blocks until the next change document is returned or an
179
- unrecoverable error is raised.
177
+ unrecoverable error is raised. This method is used when iterating over
178
+ all changes in the cursor. For example::
179
+
180
+ try:
181
+ with db.collection.watch(
182
+ [{'$match': {'operationType': 'insert'}}]) as stream:
183
+ for insert_change in stream:
184
+ print(insert_change)
185
+ except pymongo.errors.PyMongoError:
186
+ # The ChangeStream encountered an unrecoverable error or the
187
+ # resume attempt failed to recreate the cursor.
188
+ logging.error('...')
180
189
181
190
Raises :exc:`StopIteration` if this ChangeStream is closed.
182
191
"""
183
- while True :
184
- try :
185
- change = self ._cursor .next ()
186
- except ConnectionFailure :
187
- self ._resume ()
188
- continue
189
- except OperationFailure as exc :
190
- if exc .code in _NON_RESUMABLE_GETMORE_ERRORS :
191
- raise
192
- self ._resume ()
193
- continue
194
- try :
195
- resume_token = change ['_id' ]
196
- except KeyError :
197
- self .close ()
198
- raise InvalidOperation (
199
- "Cannot provide resume functionality when the resume "
200
- "token is missing." )
201
- self ._resume_token = copy .copy (resume_token )
202
- self ._start_at_operation_time = None
203
- return change
192
+ while self .alive :
193
+ doc = self .try_next ()
194
+ if doc is not None :
195
+ return doc
196
+
197
+ raise StopIteration
204
198
205
199
__next__ = next
206
200
201
+ @property
202
+ def alive (self ):
203
+ """Does this cursor have the potential to return more data?
204
+
205
+ .. note:: Even if :attr:`alive` is ``True``, :meth:`next` can raise
206
+ :exc:`StopIteration` and :meth:`try_next` can return ``None``.
207
+
208
+ .. versionadded:: 3.8
209
+ """
210
+ return self ._cursor .alive
211
+
212
+ def try_next (self ):
213
+ """Advance the cursor without blocking indefinitely.
214
+
215
+ This method returns the next change document without waiting
216
+ indefinitely for the next change. For example::
217
+
218
+ with db.collection.watch() as stream:
219
+ while stream.alive:
220
+ change = stream.try_next()
221
+ if change is not None:
222
+ print(change)
223
+ elif stream.alive:
224
+ # We end up here when there are no recent changes.
225
+ # Sleep for a while to avoid flooding the server with
226
+ # getMore requests when no changes are available.
227
+ time.sleep(10)
228
+
229
+ If no change document is cached locally then this method runs a single
230
+ getMore command. If the getMore yields any documents, the next
231
+ document is returned, otherwise, if the getMore returns no documents
232
+ (because there have been no changes) then ``None`` is returned.
233
+
234
+ :Returns:
235
+ The next change document or ``None`` when no document is available
236
+ after running a single getMore or when the cursor is closed.
237
+
238
+ .. versionadded:: 3.8
239
+ """
240
+ # Attempt to get the next change with at most one getMore and at most
241
+ # one resume attempt.
242
+ try :
243
+ change = self ._cursor ._try_next (True )
244
+ except ConnectionFailure :
245
+ self ._resume ()
246
+ change = self ._cursor ._try_next (False )
247
+ except OperationFailure as exc :
248
+ if exc .code in _NON_RESUMABLE_GETMORE_ERRORS :
249
+ raise
250
+ self ._resume ()
251
+ change = self ._cursor ._try_next (False )
252
+
253
+ # No changes are available.
254
+ if change is None :
255
+ return None
256
+
257
+ try :
258
+ resume_token = change ['_id' ]
259
+ except KeyError :
260
+ self .close ()
261
+ raise InvalidOperation (
262
+ "Cannot provide resume functionality when the resume "
263
+ "token is missing." )
264
+ self ._resume_token = copy .copy (resume_token )
265
+ self ._start_at_operation_time = None
266
+ return change
267
+
207
268
def __enter__ (self ):
208
269
return self
209
270
@@ -212,13 +273,12 @@ def __exit__(self, exc_type, exc_val, exc_tb):
212
273
213
274
214
275
class CollectionChangeStream (ChangeStream ):
215
- """Class for creating a change stream on a collection.
276
+ """A change stream that watches changes on a single collection.
216
277
217
278
Should not be called directly by application developers. Use
218
279
helper method :meth:`pymongo.collection.Collection.watch` instead.
219
280
220
- .. versionadded: 3.6
221
- .. mongodoc:: changeStreams
281
+ .. versionadded:: 3.7
222
282
"""
223
283
@property
224
284
def _aggregation_target (self ):
@@ -230,13 +290,12 @@ def _database(self):
230
290
231
291
232
292
class DatabaseChangeStream (ChangeStream ):
233
- """Class for creating a change stream on all collections in a database.
293
+ """A change stream that watches changes on all collections in a database.
234
294
235
295
Should not be called directly by application developers. Use
236
296
helper method :meth:`pymongo.database.Database.watch` instead.
237
297
238
- .. versionadded: 3.7
239
- .. mongodoc:: changeStreams
298
+ .. versionadded:: 3.7
240
299
"""
241
300
@property
242
301
def _aggregation_target (self ):
@@ -248,13 +307,12 @@ def _database(self):
248
307
249
308
250
309
class ClusterChangeStream (DatabaseChangeStream ):
251
- """Class for creating a change stream on all collections on a cluster.
310
+ """A change stream that watches changes on all collections in the cluster.
252
311
253
312
Should not be called directly by application developers. Use
254
313
helper method :meth:`pymongo.mongo_client.MongoClient.watch` instead.
255
314
256
- .. versionadded: 3.7
257
- .. mongodoc:: changeStreams
315
+ .. versionadded:: 3.7
258
316
"""
259
317
def _pipeline_options (self ):
260
318
options = super (ClusterChangeStream , self )._pipeline_options ()
0 commit comments