Skip to content

Commit ac2f506

Browse files
authored
PYTHON-2453 Add MongoDB Versioned API (#536)
Add pymongo.server_api.ServerApi and the MongoClient server_api option. Support Unified Test Format version 1.1 (serverParameters in runOnRequirements) Skip dropRole tests due to SERVER-53499.
1 parent c96c5a9 commit ac2f506

25 files changed

+3047
-9
lines changed

.evergreen/config.yml

+26
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,7 @@ functions:
290290
STORAGE_ENGINE=${STORAGE_ENGINE} \
291291
DISABLE_TEST_COMMANDS=${DISABLE_TEST_COMMANDS} \
292292
ORCHESTRATION_FILE=${ORCHESTRATION_FILE} \
293+
REQUIRE_API_VERSION=${REQUIRE_API_VERSION} \
293294
sh ${DRIVERS_TOOLS}/.evergreen/run-orchestration.sh
294295
# run-orchestration generates expansion file with the MONGODB_URI for the cluster
295296
- command: expansions.update
@@ -425,6 +426,7 @@ functions:
425426
AUTH=${AUTH} \
426427
SSL=${SSL} \
427428
DATA_LAKE=${DATA_LAKE} \
429+
MONGODB_API_VERSION=${MONGODB_API_VERSION} \
428430
sh ${PROJECT_DIRECTORY}/.evergreen/run-tests.sh
429431
430432
"run enterprise auth tests":
@@ -2023,6 +2025,19 @@ axes:
20232025
variables:
20242026
SETDEFAULTENCODING: "cp1251"
20252027

2028+
- id: requireApiVersion
2029+
display_name: "requireApiVersion"
2030+
values:
2031+
- id: "requireApiVersion1"
2032+
display_name: "requireApiVersion1"
2033+
tags: [ "requireApiVersion_tag" ]
2034+
variables:
2035+
# REQUIRE_API_VERSION is set to make drivers-evergreen-tools
2036+
# start a cluster with the requireApiVersion parameter.
2037+
REQUIRE_API_VERSION: "1"
2038+
# MONGODB_API_VERSION is the apiVersion to use in the test suite.
2039+
MONGODB_API_VERSION: "1"
2040+
20262041
buildvariants:
20272042
- matrix_name: "tests-all"
20282043
matrix_spec:
@@ -2605,6 +2620,17 @@ buildvariants:
26052620
tasks:
26062621
- name: atlas-data-lake-tests
26072622

2623+
- matrix_name: "versioned-api-tests"
2624+
matrix_spec:
2625+
platform: ubuntu-16.04
2626+
python-version: ["2.7", "3.9"]
2627+
auth: "auth"
2628+
requireApiVersion: "*"
2629+
display_name: "requireApiVersion ${python-version}"
2630+
tasks:
2631+
# Versioned API was introduced in MongoDB 4.7
2632+
- "test-latest-standalone"
2633+
26082634
- matrix_name: "ocsp-test"
26092635
matrix_spec:
26102636
platform: ubuntu-16.04

.evergreen/run-tests.sh

+6
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ GREEN_FRAMEWORK=${GREEN_FRAMEWORK:-}
2626
C_EXTENSIONS=${C_EXTENSIONS:-}
2727
COVERAGE=${COVERAGE:-}
2828
COMPRESSORS=${COMPRESSORS:-}
29+
MONGODB_API_VERSION=${MONGODB_API_VERSION:-}
2930
TEST_ENCRYPTION=${TEST_ENCRYPTION:-}
3031
LIBMONGOCRYPT_URL=${LIBMONGOCRYPT_URL:-}
3132
SETDEFAULTENCODING=${SETDEFAULTENCODING:-}
@@ -35,6 +36,11 @@ if [ -n "$COMPRESSORS" ]; then
3536
export COMPRESSORS=$COMPRESSORS
3637
fi
3738

39+
if [ -n "$MONGODB_API_VERSION" ]; then
40+
export MONGODB_API_VERSION=$MONGODB_API_VERSION
41+
fi
42+
43+
3844
export JAVA_HOME=/opt/java/jdk8
3945

4046
if [ "$AUTH" != "noauth" ]; then

doc/api/pymongo/index.rst

+1
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ Sub-modules:
5454
read_preferences
5555
results
5656
son_manipulator
57+
server_api
5758
uri_parser
5859
write_concern
5960
event_loggers

doc/api/pymongo/server_api.rst

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
:mod:`server_api` -- Support for MongoDB Versioned API
2+
======================================================
3+
4+
.. automodule:: pymongo.server_api
5+
:synopsis: Support for MongoDB Versioned API
6+
7+
.. autoclass:: pymongo.server_api.ServerApi
8+
:members:
9+
10+
.. autoclass:: pymongo.server_api.ServerApiVersion
11+
:members:

doc/changelog.rst

+5
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,11 @@ Breaking Changes in 4.0
1717
- Removed support for Python 2.7, 3.4, and 3.5. Python 3.6+ is now required.
1818
- Removed :mod:`~pymongo.thread_util`.
1919

20+
Notable improvements
21+
....................
22+
23+
- Support for MongoDB Versioned API, see :class:`~pymongo.server_api.ServerApi`.
24+
2025
Issues Resolved
2126
...............
2227

pymongo/client_options.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@ def _parse_pool_options(options):
125125
event_listeners = options.get('event_listeners')
126126
appname = options.get('appname')
127127
driver = options.get('driver')
128+
server_api = options.get('server_api')
128129
compression_settings = CompressionSettings(
129130
options.get('compressors', []),
130131
options.get('zlibcompressionlevel', -1))
@@ -138,7 +139,8 @@ def _parse_pool_options(options):
138139
_EventListeners(event_listeners),
139140
appname,
140141
driver,
141-
compression_settings)
142+
compression_settings,
143+
server_api=server_api)
142144

143145

144146
class ClientOptions(object):

pymongo/client_session.py

+9
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,9 @@ def __init__(self, opts):
297297
def active(self):
298298
return self.state in (_TxnState.STARTING, _TxnState.IN_PROGRESS)
299299

300+
def starting(self):
301+
return self.state == _TxnState.STARTING
302+
300303
def reset(self):
301304
self.state = _TxnState.NONE
302305
self.sharded = False
@@ -762,6 +765,12 @@ def in_transaction(self):
762765
"""
763766
return self._transaction.active()
764767

768+
@property
769+
def _starting_transaction(self):
770+
"""True if this session is starting a multi-statement transaction.
771+
"""
772+
return self._transaction.starting()
773+
765774
@property
766775
def _pinned_address(self):
767776
"""The mongos address this transaction was created on."""

pymongo/common.py

+11
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
from pymongo.compression_support import (validate_compressors,
2828
validate_zlib_compression_level)
2929
from pymongo.driver_info import DriverInfo
30+
from pymongo.server_api import ServerApi
3031
from pymongo.encryption_options import validate_auto_encryption_opts_or_none
3132
from pymongo.errors import ConfigurationError
3233
from pymongo.monitoring import _validate_event_listeners
@@ -528,6 +529,15 @@ def validate_driver_or_none(option, value):
528529
return value
529530

530531

532+
def validate_server_api_or_none(option, value):
533+
"""Validate the server_api keyword arg."""
534+
if value is None:
535+
return value
536+
if not isinstance(value, ServerApi):
537+
raise TypeError("%s must be an instance of ServerApi" % (option,))
538+
return value
539+
540+
531541
def validate_is_callable_or_none(option, value):
532542
"""Validates that 'value' is a callable."""
533543
if value is None:
@@ -643,6 +653,7 @@ def validate_tzinfo(dummy, value):
643653
NONSPEC_OPTIONS_VALIDATOR_MAP = {
644654
'connect': validate_boolean_or_string,
645655
'driver': validate_driver_or_none,
656+
'server_api': validate_server_api_or_none,
646657
'fsync': validate_boolean_or_string,
647658
'minpoolsize': validate_non_negative_integer,
648659
'socketkeepalive': validate_boolean_or_string,

pymongo/database.py

+6
Original file line numberDiff line numberDiff line change
@@ -704,6 +704,12 @@ def command(self, command, value=1, check=True,
704704
.. note:: :meth:`command` does **not** apply any custom TypeDecoders
705705
when decoding the command response.
706706
707+
.. note:: If this client has been configured to use MongoDB Versioned
708+
API (see :ref:`versioned-api-ref`), then :meth:`command` will
709+
automactically add API versioning options to the given command.
710+
Explicitly adding API versioning options in the command and
711+
declaring an API version on the client is not supported.
712+
707713
.. versionchanged:: 3.6
708714
Added ``session`` parameter.
709715

pymongo/message.py

+2
Original file line numberDiff line numberDiff line change
@@ -307,6 +307,7 @@ def as_command(self, sock_info):
307307
self.name = 'explain'
308308
cmd = SON([('explain', cmd)])
309309
session = self.session
310+
sock_info.add_server_api(cmd, session)
310311
if session:
311312
session._apply_to(cmd, False, self.read_preference)
312313
# Explain does not support readConcern.
@@ -892,6 +893,7 @@ def __init__(self, database_name, command, sock_info, operation_id,
892893
self.compress = True if sock_info.compression_context else False
893894
self.op_type = op_type
894895
self.codec = codec
896+
sock_info.add_server_api(command, session)
895897

896898
def _batch_command(self, docs):
897899
namespace = self.db_name + '.$cmd'

pymongo/mongo_client.py

+11
Original file line numberDiff line numberDiff line change
@@ -498,8 +498,19 @@ def __init__(
498498
and automatically decrypt results. See
499499
:ref:`automatic-client-side-encryption` for an example.
500500
501+
| **Versioned API options:**
502+
| (If not set explicitly, Versioned API will not be enabled.)
503+
504+
- `server_api`: A
505+
:class:`~pymongo.server_api.ServerApi` which configures this
506+
client to use Versioned API. See :ref:`versioned-api-ref` for
507+
details.
508+
501509
.. mongodoc:: connections
502510
511+
.. versionchanged:: 3.12
512+
Added the ``server_api`` keyword argument.
513+
503514
.. versionchanged:: 3.11
504515
Added the following keyword arguments and URI options:
505516

pymongo/pool.py

+19-3
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@
6060
from pymongo.network import (command,
6161
receive_message)
6262
from pymongo.read_preferences import ReadPreference
63+
from pymongo.server_api import _add_to_command
6364
from pymongo.server_type import SERVER_TYPE
6465
from pymongo.socket_checker import SocketChecker
6566
# Always use our backport so we always have support for IP address matching
@@ -311,7 +312,7 @@ class PoolOptions(object):
311312
'__ssl_context', '__ssl_match_hostname', '__socket_keepalive',
312313
'__event_listeners', '__appname', '__driver', '__metadata',
313314
'__compression_settings', '__max_connecting',
314-
'__pause_enabled')
315+
'__pause_enabled', '__server_api')
315316

316317
def __init__(self, max_pool_size=MAX_POOL_SIZE,
317318
min_pool_size=MIN_POOL_SIZE,
@@ -321,8 +322,7 @@ def __init__(self, max_pool_size=MAX_POOL_SIZE,
321322
ssl_match_hostname=True, socket_keepalive=True,
322323
event_listeners=None, appname=None, driver=None,
323324
compression_settings=None, max_connecting=MAX_CONNECTING,
324-
pause_enabled=True):
325-
325+
pause_enabled=True, server_api=None):
326326
self.__max_pool_size = max_pool_size
327327
self.__min_pool_size = min_pool_size
328328
self.__max_idle_time_seconds = max_idle_time_seconds
@@ -339,6 +339,7 @@ def __init__(self, max_pool_size=MAX_POOL_SIZE,
339339
self.__compression_settings = compression_settings
340340
self.__max_connecting = max_connecting
341341
self.__pause_enabled = pause_enabled
342+
self.__server_api = server_api
342343
self.__metadata = copy.deepcopy(_METADATA)
343344
if appname:
344345
self.__metadata['application'] = {'name': appname}
@@ -495,6 +496,12 @@ def metadata(self):
495496
"""
496497
return self.__metadata.copy()
497498

499+
@property
500+
def server_api(self):
501+
"""A pymongo.server_api.ServerApi or None.
502+
"""
503+
return self.__server_api
504+
498505

499506
def _negotiate_creds(all_credentials):
500507
"""Return one credential that needs mechanism negotiation, if any.
@@ -705,6 +712,7 @@ def command(self, dbname, spec, slave_ok=False,
705712
raise ConfigurationError(
706713
'Must be connected to MongoDB 3.4+ to use a collation.')
707714

715+
self.add_server_api(spec, session)
708716
if session:
709717
session._apply_to(spec, retryable_write, read_preference)
710718
self.send_cluster_time(spec, session, client)
@@ -894,6 +902,14 @@ def send_cluster_time(self, command, session, client):
894902
if self.max_wire_version >= 6 and client:
895903
client._send_cluster_time(command, session)
896904

905+
def add_server_api(self, command, session):
906+
"""Add server_api parameters."""
907+
if (session and session.in_transaction and
908+
not session._starting_transaction):
909+
return
910+
if self.opts.server_api:
911+
_add_to_command(command, self.opts.server_api)
912+
897913
def update_last_checkin_time(self):
898914
self.last_checkin_time = _time()
899915

0 commit comments

Comments
 (0)