Skip to content

PYTHON-2453 Add MongoDB Versioned API #536

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
Jan 12, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions .evergreen/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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":
Expand Down Expand Up @@ -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"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the REQUIRE_API_VERSION variable used for? I don't see it used anywhere.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need both variables. REQUIRE_API_VERSION tells drivers-evg-tools to start the cluster with the requireApiVersion parameter. MONGODB_API_VERSION tells the test suite which ServerApi.version to use. When we eventually test version 2, we'll need to use:

REQUIRE_API_VERSION: "1"
MONGODB_API_VERSION: "2"

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, I see! That's certainly going to be hard to figure out for a newcomer. Can we add a comment that REQUIRE_API_VERSION is used by cluster orchestration scripts in drivers-evergreen-tools?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

# MONGODB_API_VERSION is the apiVersion to use in the test suite.
MONGODB_API_VERSION: "1"

buildvariants:
- matrix_name: "tests-all"
matrix_spec:
Expand Down Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions .evergreen/run-tests.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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:-}
Expand All @@ -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
Expand Down
1 change: 1 addition & 0 deletions doc/api/pymongo/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ Sub-modules:
read_preferences
results
son_manipulator
server_api
uri_parser
write_concern
event_loggers
11 changes: 11 additions & 0 deletions doc/api/pymongo/server_api.rst
Original file line number Diff line number Diff line change
@@ -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:
5 changes: 5 additions & 0 deletions doc/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@ Breaking Changes in 4.0

- Removed :mod:`~pymongo.thread_util`.

Notable improvements
....................

- Support for MongoDB Versioned API, see :class:`~pymongo.server_api.ServerApi`.

Issues Resolved
...............

Expand Down
4 changes: 3 additions & 1 deletion pymongo/client_options.py
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand All @@ -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):
Expand Down
9 changes: 9 additions & 0 deletions pymongo/client_session.py
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,9 @@ def __init__(self, opts):
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
Expand Down Expand Up @@ -762,6 +765,12 @@ def in_transaction(self):
"""
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."""
Expand Down
11 changes: 11 additions & 0 deletions pymongo/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
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
Expand Down Expand Up @@ -528,6 +529,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:
Expand Down Expand Up @@ -643,6 +653,7 @@ def validate_tzinfo(dummy, value):
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,
Expand Down
6 changes: 6 additions & 0 deletions pymongo/database.py
Original file line number Diff line number Diff line change
Expand Up @@ -704,6 +704,12 @@ def command(self, command, value=1, check=True,
.. 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.

Expand Down
2 changes: 2 additions & 0 deletions pymongo/message.py
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,7 @@ def as_command(self, sock_info):
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.
Expand Down Expand Up @@ -892,6 +893,7 @@ def __init__(self, database_name, command, sock_info, operation_id,
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'
Expand Down
11 changes: 11 additions & 0 deletions pymongo/mongo_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -498,8 +498,19 @@ def __init__(
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:

Expand Down
22 changes: 19 additions & 3 deletions pymongo/pool.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@
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
Expand Down Expand Up @@ -311,7 +312,7 @@ class PoolOptions(object):
'__ssl_context', '__ssl_match_hostname', '__socket_keepalive',
'__event_listeners', '__appname', '__driver', '__metadata',
'__compression_settings', '__max_connecting',
'__pause_enabled')
'__pause_enabled', '__server_api')

def __init__(self, max_pool_size=MAX_POOL_SIZE,
min_pool_size=MIN_POOL_SIZE,
Expand All @@ -321,8 +322,7 @@ def __init__(self, max_pool_size=MAX_POOL_SIZE,
ssl_match_hostname=True, socket_keepalive=True,
event_listeners=None, appname=None, driver=None,
compression_settings=None, max_connecting=MAX_CONNECTING,
pause_enabled=True):

pause_enabled=True, 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
Expand All @@ -339,6 +339,7 @@ def __init__(self, max_pool_size=MAX_POOL_SIZE,
self.__compression_settings = compression_settings
self.__max_connecting = max_connecting
self.__pause_enabled = pause_enabled
self.__server_api = server_api
self.__metadata = copy.deepcopy(_METADATA)
if appname:
self.__metadata['application'] = {'name': appname}
Expand Down Expand Up @@ -495,6 +496,12 @@ def metadata(self):
"""
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.
Expand Down Expand Up @@ -705,6 +712,7 @@ def command(self, dbname, spec, slave_ok=False,
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)
Expand Down Expand Up @@ -894,6 +902,14 @@ def send_cluster_time(self, command, session, client):
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:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To make this more explicitly adhere to the spec, I think we should use if self.opts.server_api is not None. Though the enum doesn't have any such value, the above comparison could evaluate to false if the value is set to something which evaluates to a bool false.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

self.opts.server_api is either None or an instance of ServerApi so I'm not sure I follow.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My point here was that since the spec asks us to check if the user passed a value at all, that's exactly what we should do. Since None is the default for this kwarg, if <kwarg name> is not None is a (more) explicit check for whether it contains its default value.
It doesn't really affect behavior so if you prefer it the way it is, leave it.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If a user passes something that's not a ServerApi they will get an error when creating the client:

>>> MongoClient(server_api=False)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "pymongo/mongo_client.py", line 678, in __init__
    keyword_opts.cased_key(k), v) for k, v in keyword_opts.items()))
  File "pymongo/mongo_client.py", line 678, in <genexpr>
    keyword_opts.cased_key(k), v) for k, v in keyword_opts.items()))
  File "pymongo/common.py", line 764, in validate
    value = validator(option, value)
  File "pymongo/common.py", line 537, in validate_server_api_or_none
    raise TypeError("%s must be an instance of ServerApi" % (option,))
TypeError: server_api must be an instance of ServerApi

_add_to_command(command, self.opts.server_api)

def update_last_checkin_time(self):
self.last_checkin_time = _time()

Expand Down
Loading