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.
(cherry picked from commit ac2f506ba2)
This commit is contained in:
parent
f5be1bcbcc
commit
43d246f234
@ -290,6 +290,7 @@ functions:
|
||||
STORAGE_ENGINE=${STORAGE_ENGINE} \
|
||||
DISABLE_TEST_COMMANDS=${DISABLE_TEST_COMMANDS} \
|
||||
ORCHESTRATION_FILE=${ORCHESTRATION_FILE} \
|
||||
REQUIRE_API_VERSION=${REQUIRE_API_VERSION} \
|
||||
sh ${DRIVERS_TOOLS}/.evergreen/run-orchestration.sh
|
||||
# run-orchestration generates expansion file with the MONGODB_URI for the cluster
|
||||
- command: expansions.update
|
||||
@ -425,6 +426,7 @@ functions:
|
||||
AUTH=${AUTH} \
|
||||
SSL=${SSL} \
|
||||
DATA_LAKE=${DATA_LAKE} \
|
||||
MONGODB_API_VERSION=${MONGODB_API_VERSION} \
|
||||
sh ${PROJECT_DIRECTORY}/.evergreen/run-tests.sh
|
||||
|
||||
"run enterprise auth tests":
|
||||
@ -2023,6 +2025,19 @@ axes:
|
||||
variables:
|
||||
SETDEFAULTENCODING: "cp1251"
|
||||
|
||||
- id: requireApiVersion
|
||||
display_name: "requireApiVersion"
|
||||
values:
|
||||
- id: "requireApiVersion1"
|
||||
display_name: "requireApiVersion1"
|
||||
tags: [ "requireApiVersion_tag" ]
|
||||
variables:
|
||||
# REQUIRE_API_VERSION is set to make drivers-evergreen-tools
|
||||
# start a cluster with the requireApiVersion parameter.
|
||||
REQUIRE_API_VERSION: "1"
|
||||
# MONGODB_API_VERSION is the apiVersion to use in the test suite.
|
||||
MONGODB_API_VERSION: "1"
|
||||
|
||||
buildvariants:
|
||||
- matrix_name: "tests-all"
|
||||
matrix_spec:
|
||||
@ -2605,6 +2620,17 @@ buildvariants:
|
||||
tasks:
|
||||
- name: atlas-data-lake-tests
|
||||
|
||||
- matrix_name: "versioned-api-tests"
|
||||
matrix_spec:
|
||||
platform: ubuntu-16.04
|
||||
python-version: ["2.7", "3.9"]
|
||||
auth: "auth"
|
||||
requireApiVersion: "*"
|
||||
display_name: "requireApiVersion ${python-version}"
|
||||
tasks:
|
||||
# Versioned API was introduced in MongoDB 4.7
|
||||
- "test-latest-standalone"
|
||||
|
||||
- matrix_name: "ocsp-test"
|
||||
matrix_spec:
|
||||
platform: ubuntu-16.04
|
||||
|
||||
@ -26,6 +26,7 @@ GREEN_FRAMEWORK=${GREEN_FRAMEWORK:-}
|
||||
C_EXTENSIONS=${C_EXTENSIONS:-}
|
||||
COVERAGE=${COVERAGE:-}
|
||||
COMPRESSORS=${COMPRESSORS:-}
|
||||
MONGODB_API_VERSION=${MONGODB_API_VERSION:-}
|
||||
TEST_ENCRYPTION=${TEST_ENCRYPTION:-}
|
||||
LIBMONGOCRYPT_URL=${LIBMONGOCRYPT_URL:-}
|
||||
SETDEFAULTENCODING=${SETDEFAULTENCODING:-}
|
||||
@ -35,6 +36,11 @@ if [ -n "$COMPRESSORS" ]; then
|
||||
export COMPRESSORS=$COMPRESSORS
|
||||
fi
|
||||
|
||||
if [ -n "$MONGODB_API_VERSION" ]; then
|
||||
export MONGODB_API_VERSION=$MONGODB_API_VERSION
|
||||
fi
|
||||
|
||||
|
||||
export JAVA_HOME=/opt/java/jdk8
|
||||
|
||||
if [ "$AUTH" != "noauth" ]; then
|
||||
|
||||
@ -54,6 +54,7 @@ Sub-modules:
|
||||
read_preferences
|
||||
results
|
||||
son_manipulator
|
||||
server_api
|
||||
uri_parser
|
||||
write_concern
|
||||
event_loggers
|
||||
|
||||
11
doc/api/pymongo/server_api.rst
Normal file
11
doc/api/pymongo/server_api.rst
Normal file
@ -0,0 +1,11 @@
|
||||
:mod:`server_api` -- Support for MongoDB Versioned API
|
||||
======================================================
|
||||
|
||||
.. automodule:: pymongo.server_api
|
||||
:synopsis: Support for MongoDB Versioned API
|
||||
|
||||
.. autoclass:: pymongo.server_api.ServerApi
|
||||
:members:
|
||||
|
||||
.. autoclass:: pymongo.server_api.ServerApiVersion
|
||||
:members:
|
||||
@ -4,6 +4,19 @@ Changelog
|
||||
Changes in Version 3.12.0
|
||||
-------------------------
|
||||
|
||||
Notable improvements
|
||||
....................
|
||||
|
||||
- Support for MongoDB Versioned API, see :class:`~pymongo.server_api.ServerApi`.
|
||||
|
||||
Issues Resolved
|
||||
...............
|
||||
|
||||
See the `PyMongo 3.12 release notes in JIRA`_ for the list of resolved issues
|
||||
in this release.
|
||||
|
||||
.. _PyMongo 3.12 release notes in JIRA: https://jira.mongodb.org/secure/ReleaseNote.jspa?projectId=10004&version=29594
|
||||
|
||||
|
||||
Changes in Version 3.11.1
|
||||
-------------------------
|
||||
|
||||
@ -125,6 +125,7 @@ def _parse_pool_options(options):
|
||||
event_listeners = options.get('event_listeners')
|
||||
appname = options.get('appname')
|
||||
driver = options.get('driver')
|
||||
server_api = options.get('server_api')
|
||||
compression_settings = CompressionSettings(
|
||||
options.get('compressors', []),
|
||||
options.get('zlibcompressionlevel', -1))
|
||||
@ -138,7 +139,8 @@ def _parse_pool_options(options):
|
||||
_EventListeners(event_listeners),
|
||||
appname,
|
||||
driver,
|
||||
compression_settings)
|
||||
compression_settings,
|
||||
server_api=server_api)
|
||||
|
||||
|
||||
class ClientOptions(object):
|
||||
|
||||
@ -297,6 +297,9 @@ class _Transaction(object):
|
||||
def active(self):
|
||||
return self.state in (_TxnState.STARTING, _TxnState.IN_PROGRESS)
|
||||
|
||||
def starting(self):
|
||||
return self.state == _TxnState.STARTING
|
||||
|
||||
def reset(self):
|
||||
self.state = _TxnState.NONE
|
||||
self.sharded = False
|
||||
@ -762,6 +765,12 @@ class ClientSession(object):
|
||||
"""
|
||||
return self._transaction.active()
|
||||
|
||||
@property
|
||||
def _starting_transaction(self):
|
||||
"""True if this session is starting a multi-statement transaction.
|
||||
"""
|
||||
return self._transaction.starting()
|
||||
|
||||
@property
|
||||
def _pinned_address(self):
|
||||
"""The mongos address this transaction was created on."""
|
||||
|
||||
@ -27,6 +27,7 @@ from pymongo.auth import MECHANISMS
|
||||
from pymongo.compression_support import (validate_compressors,
|
||||
validate_zlib_compression_level)
|
||||
from pymongo.driver_info import DriverInfo
|
||||
from pymongo.server_api import ServerApi
|
||||
from pymongo.encryption_options import validate_auto_encryption_opts_or_none
|
||||
from pymongo.errors import ConfigurationError
|
||||
from pymongo.monitoring import _validate_event_listeners
|
||||
@ -525,6 +526,15 @@ def validate_driver_or_none(option, value):
|
||||
return value
|
||||
|
||||
|
||||
def validate_server_api_or_none(option, value):
|
||||
"""Validate the server_api keyword arg."""
|
||||
if value is None:
|
||||
return value
|
||||
if not isinstance(value, ServerApi):
|
||||
raise TypeError("%s must be an instance of ServerApi" % (option,))
|
||||
return value
|
||||
|
||||
|
||||
def validate_is_callable_or_none(option, value):
|
||||
"""Validates that 'value' is a callable."""
|
||||
if value is None:
|
||||
@ -640,6 +650,7 @@ URI_OPTIONS_VALIDATOR_MAP = {
|
||||
NONSPEC_OPTIONS_VALIDATOR_MAP = {
|
||||
'connect': validate_boolean_or_string,
|
||||
'driver': validate_driver_or_none,
|
||||
'server_api': validate_server_api_or_none,
|
||||
'fsync': validate_boolean_or_string,
|
||||
'minpoolsize': validate_non_negative_integer,
|
||||
'socketkeepalive': validate_boolean_or_string,
|
||||
|
||||
@ -704,6 +704,12 @@ class Database(common.BaseObject):
|
||||
.. note:: :meth:`command` does **not** apply any custom TypeDecoders
|
||||
when decoding the command response.
|
||||
|
||||
.. note:: If this client has been configured to use MongoDB Versioned
|
||||
API (see :ref:`versioned-api-ref`), then :meth:`command` will
|
||||
automactically add API versioning options to the given command.
|
||||
Explicitly adding API versioning options in the command and
|
||||
declaring an API version on the client is not supported.
|
||||
|
||||
.. versionchanged:: 3.6
|
||||
Added ``session`` parameter.
|
||||
|
||||
|
||||
@ -307,6 +307,7 @@ class _Query(object):
|
||||
self.name = 'explain'
|
||||
cmd = SON([('explain', cmd)])
|
||||
session = self.session
|
||||
sock_info.add_server_api(cmd, session)
|
||||
if session:
|
||||
session._apply_to(cmd, False, self.read_preference)
|
||||
# Explain does not support readConcern.
|
||||
@ -892,6 +893,7 @@ class _BulkWriteContext(object):
|
||||
self.compress = True if sock_info.compression_context else False
|
||||
self.op_type = op_type
|
||||
self.codec = codec
|
||||
sock_info.add_server_api(command, session)
|
||||
|
||||
def _batch_command(self, docs):
|
||||
namespace = self.db_name + '.$cmd'
|
||||
|
||||
@ -498,8 +498,19 @@ class MongoClient(common.BaseObject):
|
||||
and automatically decrypt results. See
|
||||
:ref:`automatic-client-side-encryption` for an example.
|
||||
|
||||
| **Versioned API options:**
|
||||
| (If not set explicitly, Versioned API will not be enabled.)
|
||||
|
||||
- `server_api`: A
|
||||
:class:`~pymongo.server_api.ServerApi` which configures this
|
||||
client to use Versioned API. See :ref:`versioned-api-ref` for
|
||||
details.
|
||||
|
||||
.. mongodoc:: connections
|
||||
|
||||
.. versionchanged:: 3.12
|
||||
Added the ``server_api`` keyword argument.
|
||||
|
||||
.. versionchanged:: 3.11
|
||||
Added the following keyword arguments and URI options:
|
||||
|
||||
|
||||
@ -58,6 +58,7 @@ from pymongo.monitoring import (ConnectionCheckOutFailedReason,
|
||||
from pymongo.network import (command,
|
||||
receive_message)
|
||||
from pymongo.read_preferences import ReadPreference
|
||||
from pymongo.server_api import _add_to_command
|
||||
from pymongo.server_type import SERVER_TYPE
|
||||
from pymongo.socket_checker import SocketChecker
|
||||
# Always use our backport so we always have support for IP address matching
|
||||
@ -294,7 +295,7 @@ class PoolOptions(object):
|
||||
'__wait_queue_timeout', '__wait_queue_multiple',
|
||||
'__ssl_context', '__ssl_match_hostname', '__socket_keepalive',
|
||||
'__event_listeners', '__appname', '__driver', '__metadata',
|
||||
'__compression_settings')
|
||||
'__compression_settings', '__server_api')
|
||||
|
||||
def __init__(self, max_pool_size=MAX_POOL_SIZE,
|
||||
min_pool_size=MIN_POOL_SIZE,
|
||||
@ -303,8 +304,7 @@ class PoolOptions(object):
|
||||
wait_queue_multiple=None, ssl_context=None,
|
||||
ssl_match_hostname=True, socket_keepalive=True,
|
||||
event_listeners=None, appname=None, driver=None,
|
||||
compression_settings=None):
|
||||
|
||||
compression_settings=None, server_api=None):
|
||||
self.__max_pool_size = max_pool_size
|
||||
self.__min_pool_size = min_pool_size
|
||||
self.__max_idle_time_seconds = max_idle_time_seconds
|
||||
@ -319,6 +319,7 @@ class PoolOptions(object):
|
||||
self.__appname = appname
|
||||
self.__driver = driver
|
||||
self.__compression_settings = compression_settings
|
||||
self.__server_api = server_api
|
||||
self.__metadata = copy.deepcopy(_METADATA)
|
||||
if appname:
|
||||
self.__metadata['application'] = {'name': appname}
|
||||
@ -462,6 +463,12 @@ class PoolOptions(object):
|
||||
"""
|
||||
return self.__metadata.copy()
|
||||
|
||||
@property
|
||||
def server_api(self):
|
||||
"""A pymongo.server_api.ServerApi or None.
|
||||
"""
|
||||
return self.__server_api
|
||||
|
||||
|
||||
def _negotiate_creds(all_credentials):
|
||||
"""Return one credential that needs mechanism negotiation, if any.
|
||||
@ -672,6 +679,7 @@ class SocketInfo(object):
|
||||
raise ConfigurationError(
|
||||
'Must be connected to MongoDB 3.4+ to use a collation.')
|
||||
|
||||
self.add_server_api(spec, session)
|
||||
if session:
|
||||
session._apply_to(spec, retryable_write, read_preference)
|
||||
self.send_cluster_time(spec, session, client)
|
||||
@ -861,6 +869,14 @@ class SocketInfo(object):
|
||||
if self.max_wire_version >= 6 and client:
|
||||
client._send_cluster_time(command, session)
|
||||
|
||||
def add_server_api(self, command, session):
|
||||
"""Add server_api parameters."""
|
||||
if (session and session.in_transaction and
|
||||
not session._starting_transaction):
|
||||
return
|
||||
if self.opts.server_api:
|
||||
_add_to_command(command, self.opts.server_api)
|
||||
|
||||
def update_last_checkin_time(self):
|
||||
self.last_checkin_time = _time()
|
||||
|
||||
|
||||
129
pymongo/server_api.py
Normal file
129
pymongo/server_api.py
Normal file
@ -0,0 +1,129 @@
|
||||
# Copyright 2020-present MongoDB, Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you
|
||||
# may not use this file except in compliance with the License. You
|
||||
# may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
# implied. See the License for the specific language governing
|
||||
# permissions and limitations under the License.
|
||||
|
||||
"""Support for MongoDB Versioned API.
|
||||
|
||||
.. _versioned-api-ref:
|
||||
|
||||
MongoDB Versioned API
|
||||
=====================
|
||||
|
||||
To configure MongoDB Versioned API, pass the ``server_api`` keyword option to
|
||||
:class:`~pymongo.mongo_client.MongoClient`::
|
||||
|
||||
from pymongo.mongo_client import MongoClient
|
||||
from pymongo.server_api import ServerApi
|
||||
|
||||
client = MongoClient(server_api=ServerApi('1'))
|
||||
|
||||
Note that Versioned API requires MongoDB >=5.0.
|
||||
|
||||
Strict Mode
|
||||
```````````
|
||||
|
||||
When ``strict`` mode is configured, commands that are not supported in the
|
||||
given :attr:`ServerApi.version` will fail. For example::
|
||||
|
||||
>>> client = MongoClient(server_api=ServerApi('1', strict=True))
|
||||
>>> client.test.command('count', 'test')
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
pymongo.errors.OperationFailure: Provided apiStrict:true, but the command count is not in API Version 1, full error: {'ok': 0.0, 'errmsg': 'Provided apiStrict:true, but the command count is not in API Version 1', 'code': 323, 'codeName': 'APIStrictError'
|
||||
|
||||
Classes
|
||||
=======
|
||||
"""
|
||||
|
||||
|
||||
class ServerApiVersion:
|
||||
"""An enum that defines values for :attr:`ServerApi.version`.
|
||||
|
||||
.. versionadded:: 3.12
|
||||
"""
|
||||
|
||||
V1 = "1"
|
||||
"""Server API version "1"."""
|
||||
|
||||
|
||||
class ServerApi(object):
|
||||
"""MongoDB Versioned API."""
|
||||
def __init__(self, version, strict=None, deprecation_errors=None):
|
||||
"""Options to configure MongoDB Versioned API.
|
||||
|
||||
:Parameters:
|
||||
- `version`: The API version string. Must be one of the values in
|
||||
:class:`ServerApiVersion`.
|
||||
- `strict` (optional): Set to ``True`` to enable API strict mode.
|
||||
Defaults to ``None`` which means "use the server's default".
|
||||
- `deprecation_errors` (optional): Set to ``True`` to enable
|
||||
deprecation errors. Defaults to ``None`` which means "use the
|
||||
server's default".
|
||||
|
||||
.. versionadded:: 3.12
|
||||
"""
|
||||
if version != ServerApiVersion.V1:
|
||||
raise ValueError("Unknown ServerApi version: %s" % (version,))
|
||||
if strict is not None and not isinstance(strict, bool):
|
||||
raise TypeError(
|
||||
"Wrong type for ServerApi strict, value must be an instance "
|
||||
"of bool, not %s" % (type(strict),))
|
||||
if (deprecation_errors is not None and
|
||||
not isinstance(deprecation_errors, bool)):
|
||||
raise TypeError(
|
||||
"Wrong type for ServerApi deprecation_errors, value must be "
|
||||
"an instance of bool, not %s" % (type(deprecation_errors),))
|
||||
self._version = version
|
||||
self._strict = strict
|
||||
self._deprecation_errors = deprecation_errors
|
||||
|
||||
@property
|
||||
def version(self):
|
||||
"""The API version setting.
|
||||
|
||||
This value is sent to the server in the "apiVersion" field.
|
||||
"""
|
||||
return self._version
|
||||
|
||||
@property
|
||||
def strict(self):
|
||||
"""The API strict mode setting.
|
||||
|
||||
When set, this value is sent to the server in the "apiStrict" field.
|
||||
"""
|
||||
return self._strict
|
||||
|
||||
@property
|
||||
def deprecation_errors(self):
|
||||
"""The API deprecation errors setting.
|
||||
|
||||
When set, this value is sent to the server in the
|
||||
"apiDeprecationErrors" field.
|
||||
"""
|
||||
return self._deprecation_errors
|
||||
|
||||
|
||||
def _add_to_command(cmd, server_api):
|
||||
"""Internal helper which adds API versioning options to a command.
|
||||
|
||||
:Parameters:
|
||||
- `cmd`: The command.
|
||||
- `server_api` (optional): A :class:`ServerApi` or ``None``.
|
||||
"""
|
||||
if not server_api:
|
||||
return
|
||||
cmd['apiVersion'] = server_api.version
|
||||
if server_api.strict is not None:
|
||||
cmd['apiStrict'] = server_api.strict
|
||||
if server_api.deprecation_errors is not None:
|
||||
cmd['apiDeprecationErrors'] = server_api.deprecation_errors
|
||||
@ -680,7 +680,9 @@ class Topology(object):
|
||||
ssl_match_hostname=options.ssl_match_hostname,
|
||||
event_listeners=options.event_listeners,
|
||||
appname=options.appname,
|
||||
driver=options.driver)
|
||||
driver=options.driver,
|
||||
server_api=options.server_api,
|
||||
)
|
||||
|
||||
return self._settings.pool_class(address, monitor_pool_options,
|
||||
handshake=False)
|
||||
|
||||
@ -48,6 +48,7 @@ import pymongo.errors
|
||||
from bson.son import SON
|
||||
from pymongo import common, message
|
||||
from pymongo.common import partition_node
|
||||
from pymongo.server_api import ServerApi
|
||||
from pymongo.ssl_support import HAVE_SSL, validate_cert_reqs
|
||||
from test.version import Version
|
||||
|
||||
@ -90,6 +91,8 @@ if CA_PEM:
|
||||
TLS_OPTIONS['tlsCAFile'] = CA_PEM
|
||||
|
||||
COMPRESSORS = os.environ.get("COMPRESSORS")
|
||||
MONGODB_API_VERSION = os.environ.get("MONGODB_API_VERSION")
|
||||
|
||||
|
||||
def is_server_resolvable():
|
||||
"""Returns True if 'server' is resolvable."""
|
||||
@ -200,6 +203,7 @@ class ClientContext(object):
|
||||
self.version = Version(-1) # Needs to be comparable with Version
|
||||
self.auth_enabled = False
|
||||
self.test_commands_enabled = False
|
||||
self.server_parameters = None
|
||||
self.is_mongos = False
|
||||
self.mongoses = []
|
||||
self.is_rs = False
|
||||
@ -212,9 +216,11 @@ class ClientContext(object):
|
||||
self.client = None
|
||||
self.conn_lock = threading.Lock()
|
||||
self.is_data_lake = False
|
||||
|
||||
if COMPRESSORS:
|
||||
self.default_client_options["compressors"] = COMPRESSORS
|
||||
if MONGODB_API_VERSION:
|
||||
server_api = ServerApi(MONGODB_API_VERSION)
|
||||
self.default_client_options["server_api"] = server_api
|
||||
|
||||
@property
|
||||
def ismaster(self):
|
||||
@ -226,8 +232,7 @@ class ClientContext(object):
|
||||
timeout_ms = 10000
|
||||
else:
|
||||
timeout_ms = 5000
|
||||
if COMPRESSORS:
|
||||
kwargs["compressors"] = COMPRESSORS
|
||||
kwargs.update(self.default_client_options)
|
||||
client = pymongo.MongoClient(
|
||||
host, port, serverSelectionTimeoutMS=timeout_ms, **kwargs)
|
||||
try:
|
||||
@ -341,6 +346,8 @@ class ClientContext(object):
|
||||
self.nodes = set([(host, port)])
|
||||
self.w = len(ismaster.get("hosts", [])) or 1
|
||||
self.version = Version.from_client(self.client)
|
||||
self.server_parameters = self.client.admin.command(
|
||||
'getParameter', '*')
|
||||
|
||||
if 'enableTestCommands=1' in self.cmd_line['argv']:
|
||||
self.test_commands_enabled = True
|
||||
@ -708,6 +715,12 @@ class ClientContext(object):
|
||||
"Transactions are not supported",
|
||||
func=func)
|
||||
|
||||
def require_no_api_version(self, func):
|
||||
"""Skip this test when testing with requireApiVersion."""
|
||||
return self._require(lambda: not MONGODB_API_VERSION,
|
||||
"This test does not work with requireApiVersion",
|
||||
func=func)
|
||||
|
||||
def mongos_seeds(self):
|
||||
return ','.join('%s:%s' % address for address in self.mongoses)
|
||||
|
||||
@ -751,6 +764,9 @@ def sanitize_cmd(cmd):
|
||||
cp.pop('$db', None)
|
||||
cp.pop('$readPreference', None)
|
||||
cp.pop('lsid', None)
|
||||
if MONGODB_API_VERSION:
|
||||
# Versioned api parameters
|
||||
cp.pop('apiVersion', None)
|
||||
# OP_MSG encoding may move the payload type one field to the
|
||||
# end of the command. Do the same here.
|
||||
name = next(iter(cp))
|
||||
|
||||
@ -364,6 +364,7 @@ class BulkAuthorizationTestBase(BulkTestBase):
|
||||
|
||||
@classmethod
|
||||
@client_context.require_auth
|
||||
@client_context.require_no_api_version
|
||||
def setUpClass(cls):
|
||||
super(BulkAuthorizationTestBase, cls).setUpClass()
|
||||
|
||||
|
||||
@ -176,6 +176,7 @@ class _TestPoolingBase(unittest.TestCase):
|
||||
pool_options = client_context.client._topology_settings.pool_options
|
||||
kwargs['ssl_context'] = pool_options.ssl_context
|
||||
kwargs['ssl_match_hostname'] = pool_options.ssl_match_hostname
|
||||
kwargs['server_api'] = pool_options.server_api
|
||||
return Pool(pair, PoolOptions(*args, **kwargs))
|
||||
|
||||
|
||||
|
||||
127
test/test_versioned_api.py
Normal file
127
test/test_versioned_api.py
Normal file
@ -0,0 +1,127 @@
|
||||
# Copyright 2020-present MongoDB, Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
sys.path[0:0] = [""]
|
||||
|
||||
from pymongo.mongo_client import MongoClient
|
||||
from pymongo.server_api import ServerApi, ServerApiVersion
|
||||
|
||||
from test import client_context, IntegrationTest, unittest
|
||||
from test.unified_format import generate_test_classes
|
||||
from test.utils import OvertCommandListener, rs_or_single_client
|
||||
|
||||
|
||||
TEST_PATH = os.path.join(
|
||||
os.path.dirname(os.path.realpath(__file__)), 'versioned-api')
|
||||
|
||||
# Generate unified tests.
|
||||
globals().update(generate_test_classes(TEST_PATH, module=__name__))
|
||||
|
||||
|
||||
class TestServerApi(IntegrationTest):
|
||||
def test_server_api_defaults(self):
|
||||
api = ServerApi(ServerApiVersion.V1)
|
||||
self.assertEqual(api.version, '1')
|
||||
self.assertIsNone(api.strict)
|
||||
self.assertIsNone(api.deprecation_errors)
|
||||
|
||||
def test_server_api_explicit_false(self):
|
||||
api = ServerApi('1', strict=False, deprecation_errors=False)
|
||||
self.assertEqual(api.version, '1')
|
||||
self.assertFalse(api.strict)
|
||||
self.assertFalse(api.deprecation_errors)
|
||||
|
||||
def test_server_api_strict(self):
|
||||
api = ServerApi('1', strict=True, deprecation_errors=True)
|
||||
self.assertEqual(api.version, '1')
|
||||
self.assertTrue(api.strict)
|
||||
self.assertTrue(api.deprecation_errors)
|
||||
|
||||
def test_server_api_validation(self):
|
||||
with self.assertRaises(ValueError):
|
||||
ServerApi('2')
|
||||
with self.assertRaises(TypeError):
|
||||
ServerApi('1', strict='not-a-bool')
|
||||
with self.assertRaises(TypeError):
|
||||
ServerApi('1', deprecation_errors='not-a-bool')
|
||||
with self.assertRaises(TypeError):
|
||||
MongoClient(server_api='not-a-ServerApi')
|
||||
|
||||
def assertServerApi(self, event):
|
||||
self.assertIn('apiVersion', event.command)
|
||||
self.assertEqual(event.command['apiVersion'], '1')
|
||||
|
||||
def assertNoServerApi(self, event):
|
||||
self.assertNotIn('apiVersion', event.command)
|
||||
|
||||
def assertServerApiOnlyInFirstCommand(self, events):
|
||||
self.assertServerApi(events[0])
|
||||
for event in events[1:]:
|
||||
self.assertNoServerApi(event)
|
||||
|
||||
@client_context.require_version_min(4, 7)
|
||||
def test_command_options(self):
|
||||
listener = OvertCommandListener()
|
||||
client = rs_or_single_client(server_api=ServerApi('1'),
|
||||
event_listeners=[listener])
|
||||
self.addCleanup(client.close)
|
||||
coll = client.test.test
|
||||
coll.insert_many([{} for _ in range(100)])
|
||||
self.addCleanup(coll.delete_many, {})
|
||||
list(coll.find(batch_size=25))
|
||||
client.admin.command('ping')
|
||||
for event in listener.results['started']:
|
||||
if event.command_name == 'getMore':
|
||||
self.assertNoServerApi(event)
|
||||
else:
|
||||
self.assertServerApi(event)
|
||||
|
||||
@client_context.require_version_min(4, 7)
|
||||
@client_context.require_transactions
|
||||
def test_command_options_txn(self):
|
||||
listener = OvertCommandListener()
|
||||
client = rs_or_single_client(server_api=ServerApi('1'),
|
||||
event_listeners=[listener])
|
||||
self.addCleanup(client.close)
|
||||
coll = client.test.test
|
||||
coll.insert_many([{} for _ in range(100)])
|
||||
self.addCleanup(coll.delete_many, {})
|
||||
|
||||
listener.reset()
|
||||
with client.start_session() as s, s.start_transaction():
|
||||
coll.insert_many([{} for _ in range(100)], session=s)
|
||||
list(coll.find(batch_size=25, session=s))
|
||||
client.test.command('find', 'test', session=s)
|
||||
self.assertServerApiOnlyInFirstCommand(listener.results['started'])
|
||||
|
||||
listener.reset()
|
||||
with client.start_session() as s, s.start_transaction():
|
||||
list(coll.find(batch_size=25, session=s))
|
||||
coll.insert_many([{} for _ in range(100)], session=s)
|
||||
client.test.command('find', 'test', session=s)
|
||||
self.assertServerApiOnlyInFirstCommand(listener.results['started'])
|
||||
|
||||
listener.reset()
|
||||
with client.start_session() as s, s.start_transaction():
|
||||
client.test.command('find', 'test', session=s)
|
||||
list(coll.find(batch_size=25, session=s))
|
||||
coll.insert_many([{} for _ in range(100)], session=s)
|
||||
self.assertServerApiOnlyInFirstCommand(listener.results['started'])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@ -45,6 +45,7 @@ from pymongo.monitoring import (
|
||||
from pymongo.read_concern import ReadConcern
|
||||
from pymongo.read_preferences import ReadPreference
|
||||
from pymongo.results import BulkWriteResult
|
||||
from pymongo.server_api import ServerApi
|
||||
from pymongo.write_concern import WriteConcern
|
||||
|
||||
from test import client_context, unittest, IntegrationTest
|
||||
@ -106,8 +107,17 @@ def is_run_on_requirement_satisfied(requirement):
|
||||
max_version_satisfied = Version.from_string(
|
||||
req_max_server_version) >= client_context.version
|
||||
|
||||
params_satisfied = True
|
||||
params = requirement.get('serverParameters')
|
||||
if params:
|
||||
for param, val in params.items():
|
||||
if param not in client_context.server_parameters:
|
||||
params_satisfied = False
|
||||
elif client_context.server_parameters[param] != val:
|
||||
params_satisfied = False
|
||||
|
||||
return (topology_satisfied and min_version_satisfied and
|
||||
max_version_satisfied)
|
||||
max_version_satisfied and params_satisfied)
|
||||
|
||||
|
||||
def parse_collection_or_database_options(options):
|
||||
@ -200,6 +210,11 @@ class EntityMapUtil(object):
|
||||
if client_context.is_mongos and spec.get('useMultipleMongoses'):
|
||||
kwargs['h'] = client_context.mongos_seeds()
|
||||
kwargs.update(spec.get('uriOptions', {}))
|
||||
server_api = spec.get('serverApi')
|
||||
if server_api:
|
||||
kwargs['server_api'] = ServerApi(
|
||||
server_api['version'], strict=server_api.get('strict'),
|
||||
deprecation_errors=server_api.get('deprecationErrors'))
|
||||
client = rs_or_single_client(**kwargs)
|
||||
self[spec['id']] = client
|
||||
self._test_class.addCleanup(client.close)
|
||||
@ -478,6 +493,12 @@ class MatchEvaluatorUtil(object):
|
||||
command = spec.get('command')
|
||||
database_name = spec.get('databaseName')
|
||||
if command:
|
||||
if actual.command_name == 'update':
|
||||
# TODO: remove this once PYTHON-1744 is done.
|
||||
# Add upsert and multi fields back into expectations.
|
||||
for update in command['updates']:
|
||||
update.setdefault('upsert', False)
|
||||
update.setdefault('multi', False)
|
||||
self.match_result(command, actual.command)
|
||||
if database_name:
|
||||
self._test_class.assertEqual(
|
||||
@ -503,7 +524,7 @@ class UnifiedSpecTestMixinV1(IntegrationTest):
|
||||
Specification of the test suite being currently run is available as
|
||||
a class attribute ``TEST_SPEC``.
|
||||
"""
|
||||
SCHEMA_VERSION = Version.from_string('1.0')
|
||||
SCHEMA_VERSION = Version.from_string('1.1')
|
||||
|
||||
@staticmethod
|
||||
def should_run_on(run_on_spec):
|
||||
|
||||
1073
test/versioned-api/crud-api-version-1-strict.json
Normal file
1073
test/versioned-api/crud-api-version-1-strict.json
Normal file
File diff suppressed because it is too large
Load Diff
1067
test/versioned-api/crud-api-version-1.json
Normal file
1067
test/versioned-api/crud-api-version-1.json
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,117 @@
|
||||
{
|
||||
"description": "RunCommand helper: No API version declared",
|
||||
"schemaVersion": "1.1",
|
||||
"runOnRequirements": [
|
||||
{
|
||||
"minServerVersion": "4.7",
|
||||
"serverParameters": {
|
||||
"requireApiVersion": false
|
||||
}
|
||||
}
|
||||
],
|
||||
"createEntities": [
|
||||
{
|
||||
"client": {
|
||||
"id": "client",
|
||||
"observeEvents": [
|
||||
"commandStartedEvent"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"database": {
|
||||
"id": "database",
|
||||
"client": "client",
|
||||
"databaseName": "versioned-api-tests"
|
||||
}
|
||||
}
|
||||
],
|
||||
"tests": [
|
||||
{
|
||||
"description": "runCommand does not inspect or change the command document",
|
||||
"operations": [
|
||||
{
|
||||
"name": "runCommand",
|
||||
"object": "database",
|
||||
"arguments": {
|
||||
"commandName": "ping",
|
||||
"command": {
|
||||
"ping": 1,
|
||||
"apiVersion": "server_will_never_support_this_api_version"
|
||||
}
|
||||
},
|
||||
"expectError": {
|
||||
"isError": true,
|
||||
"isClientError": false
|
||||
}
|
||||
}
|
||||
],
|
||||
"expectEvents": [
|
||||
{
|
||||
"client": "client",
|
||||
"events": [
|
||||
{
|
||||
"commandStartedEvent": {
|
||||
"command": {
|
||||
"ping": 1,
|
||||
"apiVersion": "server_will_never_support_this_api_version",
|
||||
"apiStrict": {
|
||||
"$$exists": false
|
||||
},
|
||||
"apiDeprecationErrors": {
|
||||
"$$exists": false
|
||||
}
|
||||
},
|
||||
"commandName": "ping",
|
||||
"databaseName": "versioned-api-tests"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "runCommand does not prevent sending invalid API version declarations",
|
||||
"operations": [
|
||||
{
|
||||
"name": "runCommand",
|
||||
"object": "database",
|
||||
"arguments": {
|
||||
"commandName": "ping",
|
||||
"command": {
|
||||
"ping": 1,
|
||||
"apiStrict": true
|
||||
}
|
||||
},
|
||||
"expectError": {
|
||||
"isError": true,
|
||||
"isClientError": false
|
||||
}
|
||||
}
|
||||
],
|
||||
"expectEvents": [
|
||||
{
|
||||
"client": "client",
|
||||
"events": [
|
||||
{
|
||||
"commandStartedEvent": {
|
||||
"command": {
|
||||
"ping": 1,
|
||||
"apiVersion": {
|
||||
"$$exists": false
|
||||
},
|
||||
"apiStrict": true,
|
||||
"apiDeprecationErrors": {
|
||||
"$$exists": false
|
||||
}
|
||||
},
|
||||
"commandName": "ping",
|
||||
"databaseName": "versioned-api-tests"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
73
test/versioned-api/test-commands-deprecation-errors.json
Normal file
73
test/versioned-api/test-commands-deprecation-errors.json
Normal file
@ -0,0 +1,73 @@
|
||||
{
|
||||
"description": "Test commands: deprecation errors",
|
||||
"schemaVersion": "1.1",
|
||||
"runOnRequirements": [
|
||||
{
|
||||
"minServerVersion": "4.7",
|
||||
"serverParameters": {
|
||||
"enableTestCommands": true,
|
||||
"acceptAPIVersion2": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"createEntities": [
|
||||
{
|
||||
"client": {
|
||||
"id": "client",
|
||||
"observeEvents": [
|
||||
"commandStartedEvent"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"database": {
|
||||
"id": "database",
|
||||
"client": "client",
|
||||
"databaseName": "versioned-api-tests"
|
||||
}
|
||||
}
|
||||
],
|
||||
"tests": [
|
||||
{
|
||||
"description": "Running a command that is deprecated raises a deprecation error",
|
||||
"operations": [
|
||||
{
|
||||
"name": "runCommand",
|
||||
"object": "database",
|
||||
"arguments": {
|
||||
"commandName": "testDeprecationInVersion2",
|
||||
"command": {
|
||||
"testDeprecationInVersion2": 1,
|
||||
"apiVersion": "2",
|
||||
"apiDeprecationErrors": true
|
||||
}
|
||||
},
|
||||
"expectError": {
|
||||
"isError": true,
|
||||
"errorContains": "command testDeprecationInVersion2 is deprecated in API Version 2",
|
||||
"errorCodeName": "APIDeprecationError"
|
||||
}
|
||||
}
|
||||
],
|
||||
"expectEvents": [
|
||||
{
|
||||
"client": "client",
|
||||
"events": [
|
||||
{
|
||||
"commandStartedEvent": {
|
||||
"command": {
|
||||
"testDeprecationInVersion2": 1,
|
||||
"apiVersion": "2",
|
||||
"apiStrict": {
|
||||
"$$exists": false
|
||||
},
|
||||
"apiDeprecationErrors": true
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
74
test/versioned-api/test-commands-strict-mode.json
Normal file
74
test/versioned-api/test-commands-strict-mode.json
Normal file
@ -0,0 +1,74 @@
|
||||
{
|
||||
"description": "Test commands: strict mode",
|
||||
"schemaVersion": "1.1",
|
||||
"runOnRequirements": [
|
||||
{
|
||||
"minServerVersion": "4.7",
|
||||
"serverParameters": {
|
||||
"enableTestCommands": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"createEntities": [
|
||||
{
|
||||
"client": {
|
||||
"id": "client",
|
||||
"observeEvents": [
|
||||
"commandStartedEvent"
|
||||
],
|
||||
"serverApi": {
|
||||
"version": "1",
|
||||
"strict": true
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"database": {
|
||||
"id": "database",
|
||||
"client": "client",
|
||||
"databaseName": "versioned-api-tests"
|
||||
}
|
||||
}
|
||||
],
|
||||
"tests": [
|
||||
{
|
||||
"description": "Running a command that is not part of the versioned API results in an error",
|
||||
"operations": [
|
||||
{
|
||||
"name": "runCommand",
|
||||
"object": "database",
|
||||
"arguments": {
|
||||
"commandName": "testVersion2",
|
||||
"command": {
|
||||
"testVersion2": 1
|
||||
}
|
||||
},
|
||||
"expectError": {
|
||||
"isError": true,
|
||||
"errorContains": "command testVersion2 is not in API Version 1",
|
||||
"errorCodeName": "APIStrictError"
|
||||
}
|
||||
}
|
||||
],
|
||||
"expectEvents": [
|
||||
{
|
||||
"client": "client",
|
||||
"events": [
|
||||
{
|
||||
"commandStartedEvent": {
|
||||
"command": {
|
||||
"testVersion2": 1,
|
||||
"apiVersion": "1",
|
||||
"apiStrict": true,
|
||||
"apiDeprecationErrors": {
|
||||
"$$unsetOrMatches": false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
232
test/versioned-api/transaction-handling.json
Normal file
232
test/versioned-api/transaction-handling.json
Normal file
@ -0,0 +1,232 @@
|
||||
{
|
||||
"description": "Transaction handling",
|
||||
"schemaVersion": "1.1",
|
||||
"runOnRequirements": [
|
||||
{
|
||||
"minServerVersion": "4.7",
|
||||
"topologies": [
|
||||
"replicaset",
|
||||
"sharded-replicaset"
|
||||
]
|
||||
}
|
||||
],
|
||||
"createEntities": [
|
||||
{
|
||||
"client": {
|
||||
"id": "client",
|
||||
"observeEvents": [
|
||||
"commandStartedEvent"
|
||||
],
|
||||
"serverApi": {
|
||||
"version": "1"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"database": {
|
||||
"id": "database",
|
||||
"client": "client",
|
||||
"databaseName": "versioned-api-tests"
|
||||
}
|
||||
},
|
||||
{
|
||||
"collection": {
|
||||
"id": "collection",
|
||||
"database": "database",
|
||||
"collectionName": "test"
|
||||
}
|
||||
},
|
||||
{
|
||||
"session": {
|
||||
"id": "session",
|
||||
"client": "client"
|
||||
}
|
||||
}
|
||||
],
|
||||
"_yamlAnchors": {
|
||||
"versions": [
|
||||
{
|
||||
"apiVersion": "1",
|
||||
"apiStrict": {
|
||||
"$$unsetOrMatches": false
|
||||
},
|
||||
"apiDeprecationErrors": {
|
||||
"$$unsetOrMatches": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"apiVersion": {
|
||||
"$$exists": false
|
||||
},
|
||||
"apiStrict": {
|
||||
"$$exists": false
|
||||
},
|
||||
"apiDeprecationErrors": {
|
||||
"$$exists": false
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"initialData": [
|
||||
{
|
||||
"collectionName": "test",
|
||||
"databaseName": "versioned-api-tests",
|
||||
"documents": [
|
||||
{
|
||||
"_id": 1,
|
||||
"x": 11
|
||||
},
|
||||
{
|
||||
"_id": 2,
|
||||
"x": 22
|
||||
},
|
||||
{
|
||||
"_id": 3,
|
||||
"x": 33
|
||||
},
|
||||
{
|
||||
"_id": 4,
|
||||
"x": 44
|
||||
},
|
||||
{
|
||||
"_id": 5,
|
||||
"x": 55
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"tests": [
|
||||
{
|
||||
"description": "Only the first command in a transaction declares an API version",
|
||||
"runOnRequirements": [
|
||||
{
|
||||
"topologies": [
|
||||
"replicaset",
|
||||
"sharded-replicaset"
|
||||
]
|
||||
}
|
||||
],
|
||||
"operations": [
|
||||
{
|
||||
"name": "startTransaction",
|
||||
"object": "session"
|
||||
},
|
||||
{
|
||||
"name": "insertOne",
|
||||
"object": "collection",
|
||||
"arguments": {
|
||||
"session": "session",
|
||||
"document": {
|
||||
"_id": 6,
|
||||
"x": 66
|
||||
}
|
||||
},
|
||||
"expectResult": {
|
||||
"$$unsetOrMatches": {
|
||||
"insertedId": {
|
||||
"$$unsetOrMatches": 6
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "insertOne",
|
||||
"object": "collection",
|
||||
"arguments": {
|
||||
"session": "session",
|
||||
"document": {
|
||||
"_id": 7,
|
||||
"x": 77
|
||||
}
|
||||
},
|
||||
"expectResult": {
|
||||
"$$unsetOrMatches": {
|
||||
"insertedId": {
|
||||
"$$unsetOrMatches": 7
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "commitTransaction",
|
||||
"object": "session"
|
||||
}
|
||||
],
|
||||
"expectEvents": [
|
||||
{
|
||||
"client": "client",
|
||||
"events": [
|
||||
{
|
||||
"commandStartedEvent": {
|
||||
"command": {
|
||||
"insert": "test",
|
||||
"documents": [
|
||||
{
|
||||
"_id": 6,
|
||||
"x": 66
|
||||
}
|
||||
],
|
||||
"lsid": {
|
||||
"$$sessionLsid": "session"
|
||||
},
|
||||
"startTransaction": true,
|
||||
"apiVersion": "1",
|
||||
"apiStrict": {
|
||||
"$$unsetOrMatches": false
|
||||
},
|
||||
"apiDeprecationErrors": {
|
||||
"$$unsetOrMatches": false
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"commandStartedEvent": {
|
||||
"command": {
|
||||
"insert": "test",
|
||||
"documents": [
|
||||
{
|
||||
"_id": 7,
|
||||
"x": 77
|
||||
}
|
||||
],
|
||||
"lsid": {
|
||||
"$$sessionLsid": "session"
|
||||
},
|
||||
"apiVersion": {
|
||||
"$$exists": false
|
||||
},
|
||||
"apiStrict": {
|
||||
"$$exists": false
|
||||
},
|
||||
"apiDeprecationErrors": {
|
||||
"$$exists": false
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"commandStartedEvent": {
|
||||
"command": {
|
||||
"commitTransaction": 1,
|
||||
"lsid": {
|
||||
"$$sessionLsid": "session"
|
||||
},
|
||||
"apiVersion": {
|
||||
"$$exists": false
|
||||
},
|
||||
"apiStrict": {
|
||||
"$$exists": false
|
||||
},
|
||||
"apiDeprecationErrors": {
|
||||
"$$exists": false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user